""" Tests for async_loading mixin module """ import pytest from pathlib import Path from unittest.mock import Mock, MagicMock, patch, PropertyMock class TestAsyncLoadingMixinInit: """Tests for AsyncLoadingMixin initialization""" def test_init_async_loading_creates_cache(self): """Test that _init_async_loading creates image cache""" from pyPhotoAlbum.mixins.async_loading import AsyncLoadingMixin with ( patch("pyPhotoAlbum.mixins.async_loading.ImageCache") as mock_cache, patch("pyPhotoAlbum.mixins.async_loading.AsyncImageLoader") as mock_loader, patch("pyPhotoAlbum.mixins.async_loading.AsyncPDFGenerator") as mock_pdf, ): mock_loader_instance = Mock() mock_loader.return_value = mock_loader_instance mock_pdf_instance = Mock() mock_pdf.return_value = mock_pdf_instance class TestClass(AsyncLoadingMixin): pass obj = TestClass() obj._init_async_loading() mock_cache.assert_called_once_with(max_memory_mb=512) assert hasattr(obj, "image_cache") def test_init_async_loading_creates_image_loader(self): """Test that _init_async_loading creates async image loader""" from pyPhotoAlbum.mixins.async_loading import AsyncLoadingMixin with ( patch("pyPhotoAlbum.mixins.async_loading.ImageCache") as mock_cache, patch("pyPhotoAlbum.mixins.async_loading.AsyncImageLoader") as mock_loader, patch("pyPhotoAlbum.mixins.async_loading.AsyncPDFGenerator") as mock_pdf, ): mock_loader_instance = Mock() mock_loader.return_value = mock_loader_instance mock_pdf_instance = Mock() mock_pdf.return_value = mock_pdf_instance class TestClass(AsyncLoadingMixin): pass obj = TestClass() obj._init_async_loading() mock_loader.assert_called_once() assert hasattr(obj, "async_image_loader") mock_loader_instance.start.assert_called_once() def test_init_async_loading_creates_pdf_generator(self): """Test that _init_async_loading creates async PDF generator""" from pyPhotoAlbum.mixins.async_loading import AsyncLoadingMixin with ( patch("pyPhotoAlbum.mixins.async_loading.ImageCache") as mock_cache, patch("pyPhotoAlbum.mixins.async_loading.AsyncImageLoader") as mock_loader, patch("pyPhotoAlbum.mixins.async_loading.AsyncPDFGenerator") as mock_pdf, ): mock_loader_instance = Mock() mock_loader.return_value = mock_loader_instance mock_pdf_instance = Mock() mock_pdf.return_value = mock_pdf_instance class TestClass(AsyncLoadingMixin): pass obj = TestClass() obj._init_async_loading() mock_pdf.assert_called_once() assert hasattr(obj, "async_pdf_generator") mock_pdf_instance.start.assert_called_once() class TestAsyncLoadingMixinCleanup: """Tests for AsyncLoadingMixin cleanup""" def test_cleanup_stops_image_loader(self): """Test that _cleanup_async_loading stops image loader""" from pyPhotoAlbum.mixins.async_loading import AsyncLoadingMixin class TestClass(AsyncLoadingMixin): pass obj = TestClass() obj.async_image_loader = Mock() obj.async_pdf_generator = Mock() obj.image_cache = Mock() obj._cleanup_async_loading() obj.async_image_loader.stop.assert_called_once() def test_cleanup_stops_pdf_generator(self): """Test that _cleanup_async_loading stops PDF generator""" from pyPhotoAlbum.mixins.async_loading import AsyncLoadingMixin class TestClass(AsyncLoadingMixin): pass obj = TestClass() obj.async_image_loader = Mock() obj.async_pdf_generator = Mock() obj.image_cache = Mock() obj._cleanup_async_loading() obj.async_pdf_generator.stop.assert_called_once() def test_cleanup_clears_cache(self): """Test that _cleanup_async_loading clears image cache""" from pyPhotoAlbum.mixins.async_loading import AsyncLoadingMixin class TestClass(AsyncLoadingMixin): pass obj = TestClass() obj.async_image_loader = Mock() obj.async_pdf_generator = Mock() obj.image_cache = Mock() obj._cleanup_async_loading() obj.image_cache.clear.assert_called_once() def test_cleanup_handles_missing_components(self): """Test that _cleanup_async_loading handles missing components gracefully""" from pyPhotoAlbum.mixins.async_loading import AsyncLoadingMixin class TestClass(AsyncLoadingMixin): pass obj = TestClass() # Don't set any async components # Should not raise obj._cleanup_async_loading() class TestOnImageLoaded: """Tests for _on_image_loaded callback""" def test_on_image_loaded_calls_element_callback(self): """Test that _on_image_loaded calls element's callback""" from pyPhotoAlbum.mixins.async_loading import AsyncLoadingMixin class TestClass(AsyncLoadingMixin): def update(self): pass obj = TestClass() mock_image = Mock() mock_user_data = Mock() mock_user_data._on_async_image_loaded = Mock() obj._on_image_loaded(Path("/test/image.jpg"), mock_image, mock_user_data) mock_user_data._on_async_image_loaded.assert_called_once_with(mock_image) def test_on_image_loaded_triggers_update(self): """Test that _on_image_loaded triggers widget update""" from pyPhotoAlbum.mixins.async_loading import AsyncLoadingMixin class TestClass(AsyncLoadingMixin): def __init__(self): self.update_called = False def update(self): self.update_called = True obj = TestClass() obj._on_image_loaded(Path("/test/image.jpg"), Mock(), None) assert obj.update_called def test_on_image_loaded_handles_none_user_data(self): """Test that _on_image_loaded handles None user_data""" from pyPhotoAlbum.mixins.async_loading import AsyncLoadingMixin class TestClass(AsyncLoadingMixin): def update(self): pass obj = TestClass() # Should not raise obj._on_image_loaded(Path("/test/image.jpg"), Mock(), None) class TestOnImageLoadFailed: """Tests for _on_image_load_failed callback""" def test_on_image_load_failed_calls_element_callback(self): """Test that _on_image_load_failed calls element's callback""" from pyPhotoAlbum.mixins.async_loading import AsyncLoadingMixin class TestClass(AsyncLoadingMixin): pass obj = TestClass() mock_user_data = Mock() mock_user_data._on_async_image_load_failed = Mock() obj._on_image_load_failed(Path("/test/image.jpg"), "Error message", mock_user_data) mock_user_data._on_async_image_load_failed.assert_called_once_with("Error message") def test_on_image_load_failed_handles_none_user_data(self): """Test that _on_image_load_failed handles None user_data""" from pyPhotoAlbum.mixins.async_loading import AsyncLoadingMixin class TestClass(AsyncLoadingMixin): pass obj = TestClass() # Should not raise obj._on_image_load_failed(Path("/test/image.jpg"), "Error", None) class TestOnPdfProgress: """Tests for _on_pdf_progress callback""" def test_on_pdf_progress_updates_dialog(self): """Test that _on_pdf_progress updates progress dialog""" from pyPhotoAlbum.mixins.async_loading import AsyncLoadingMixin class TestClass(AsyncLoadingMixin): pass obj = TestClass() obj._pdf_progress_dialog = Mock() obj._on_pdf_progress(5, 10, "Processing page 5") obj._pdf_progress_dialog.setValue.assert_called_once_with(5) obj._pdf_progress_dialog.setLabelText.assert_called_once_with("Processing page 5") def test_on_pdf_progress_handles_no_dialog(self): """Test that _on_pdf_progress handles missing dialog""" from pyPhotoAlbum.mixins.async_loading import AsyncLoadingMixin class TestClass(AsyncLoadingMixin): pass obj = TestClass() # No _pdf_progress_dialog attribute # Should not raise obj._on_pdf_progress(5, 10, "Processing") class TestOnPdfComplete: """Tests for _on_pdf_complete callback""" def test_on_pdf_complete_closes_dialog(self): """Test that _on_pdf_complete closes progress dialog""" from pyPhotoAlbum.mixins.async_loading import AsyncLoadingMixin class TestClass(AsyncLoadingMixin): def window(self): return Mock(spec=[]) obj = TestClass() mock_dialog = Mock() obj._pdf_progress_dialog = mock_dialog obj._on_pdf_complete(True, []) mock_dialog.close.assert_called_once() assert obj._pdf_progress_dialog is None def test_on_pdf_complete_shows_success_status(self): """Test that _on_pdf_complete shows success status""" from pyPhotoAlbum.mixins.async_loading import AsyncLoadingMixin mock_main_window = Mock() class TestClass(AsyncLoadingMixin): def window(self): return mock_main_window obj = TestClass() obj._on_pdf_complete(True, []) mock_main_window.show_status.assert_called_once() call_args = mock_main_window.show_status.call_args[0] assert "successfully" in call_args[0] def test_on_pdf_complete_shows_warnings(self): """Test that _on_pdf_complete shows warning count""" from pyPhotoAlbum.mixins.async_loading import AsyncLoadingMixin mock_main_window = Mock() class TestClass(AsyncLoadingMixin): def window(self): return mock_main_window obj = TestClass() obj._on_pdf_complete(True, ["warning1", "warning2"]) mock_main_window.show_status.assert_called_once() call_args = mock_main_window.show_status.call_args[0] assert "2 warnings" in call_args[0] def test_on_pdf_complete_shows_failure_status(self): """Test that _on_pdf_complete shows failure status""" from pyPhotoAlbum.mixins.async_loading import AsyncLoadingMixin mock_main_window = Mock() class TestClass(AsyncLoadingMixin): def window(self): return mock_main_window obj = TestClass() obj._on_pdf_complete(False, []) mock_main_window.show_status.assert_called_once() call_args = mock_main_window.show_status.call_args[0] assert "failed" in call_args[0] class TestOnPdfFailed: """Tests for _on_pdf_failed callback""" def test_on_pdf_failed_closes_dialog(self): """Test that _on_pdf_failed closes progress dialog""" from pyPhotoAlbum.mixins.async_loading import AsyncLoadingMixin class TestClass(AsyncLoadingMixin): def window(self): return Mock(spec=[]) obj = TestClass() mock_dialog = Mock() obj._pdf_progress_dialog = mock_dialog obj._on_pdf_failed("Error occurred") mock_dialog.close.assert_called_once() assert obj._pdf_progress_dialog is None def test_on_pdf_failed_shows_error_status(self): """Test that _on_pdf_failed shows error status""" from pyPhotoAlbum.mixins.async_loading import AsyncLoadingMixin mock_main_window = Mock() class TestClass(AsyncLoadingMixin): def window(self): return mock_main_window obj = TestClass() obj._on_pdf_failed("Something went wrong") mock_main_window.show_status.assert_called_once() call_args = mock_main_window.show_status.call_args[0] assert "failed" in call_args[0] assert "Something went wrong" in call_args[0] class TestRequestImageLoad: """Tests for request_image_load method""" def test_request_image_load_no_loader(self): """Test request_image_load when loader not initialized""" from pyPhotoAlbum.mixins.async_loading import AsyncLoadingMixin class TestClass(AsyncLoadingMixin): pass obj = TestClass() mock_image_data = Mock() # Should not raise obj.request_image_load(mock_image_data) def test_request_image_load_empty_path(self): """Test request_image_load with empty image path""" from pyPhotoAlbum.mixins.async_loading import AsyncLoadingMixin class TestClass(AsyncLoadingMixin): pass obj = TestClass() obj.async_image_loader = Mock() mock_image_data = Mock() mock_image_data.image_path = "" obj.request_image_load(mock_image_data) obj.async_image_loader.request_load.assert_not_called() def test_request_image_load_non_assets_path_skipped(self): """Test request_image_load skips paths outside assets folder""" from pyPhotoAlbum.mixins.async_loading import AsyncLoadingMixin class TestClass(AsyncLoadingMixin): pass obj = TestClass() obj.async_image_loader = Mock() mock_image_data = Mock() mock_image_data.image_path = "/absolute/path/image.jpg" obj.request_image_load(mock_image_data) obj.async_image_loader.request_load.assert_not_called() def test_request_image_load_path_not_resolved(self): """Test request_image_load when path resolution fails""" from pyPhotoAlbum.mixins.async_loading import AsyncLoadingMixin class TestClass(AsyncLoadingMixin): pass obj = TestClass() obj.async_image_loader = Mock() mock_image_data = Mock() mock_image_data.image_path = "assets/missing.jpg" mock_image_data.resolve_image_path.return_value = None obj.request_image_load(mock_image_data) obj.async_image_loader.request_load.assert_not_called() def test_request_image_load_success(self, tmp_path): """Test successful request_image_load""" from pyPhotoAlbum.mixins.async_loading import AsyncLoadingMixin, LoadPriority class TestClass(AsyncLoadingMixin): pass obj = TestClass() obj.async_image_loader = Mock() # Create actual file asset_path = tmp_path / "assets" / "photo.jpg" asset_path.parent.mkdir(parents=True) asset_path.write_text("test") mock_image_data = Mock() mock_image_data.image_path = "assets/photo.jpg" mock_image_data.resolve_image_path.return_value = str(asset_path) obj.request_image_load(mock_image_data, priority=LoadPriority.HIGH) obj.async_image_loader.request_load.assert_called_once() call_kwargs = obj.async_image_loader.request_load.call_args[1] assert call_kwargs["priority"] == LoadPriority.HIGH assert call_kwargs["user_data"] == mock_image_data class TestExportPdfAsync: """Tests for export_pdf_async method""" def test_export_pdf_async_no_generator(self): """Test export_pdf_async when generator not initialized""" from pyPhotoAlbum.mixins.async_loading import AsyncLoadingMixin class TestClass(AsyncLoadingMixin): pass obj = TestClass() mock_project = Mock() result = obj.export_pdf_async(mock_project, "/output.pdf") assert result is False def test_export_pdf_async_creates_progress_dialog(self, qtbot): """Test export_pdf_async creates progress dialog""" from pyPhotoAlbum.mixins.async_loading import AsyncLoadingMixin from PyQt6.QtWidgets import QWidget class TestWidget(QWidget, AsyncLoadingMixin): pass widget = TestWidget() qtbot.addWidget(widget) widget.async_pdf_generator = Mock() widget.async_pdf_generator.export_pdf.return_value = True mock_project = Mock() mock_project.pages = [Mock(is_cover=False, is_double_spread=False)] widget.export_pdf_async(mock_project, "/output.pdf") assert hasattr(widget, "_pdf_progress_dialog") assert widget._pdf_progress_dialog is not None def test_export_pdf_async_calls_generator(self, qtbot): """Test export_pdf_async calls the PDF generator""" from pyPhotoAlbum.mixins.async_loading import AsyncLoadingMixin from PyQt6.QtWidgets import QWidget class TestWidget(QWidget, AsyncLoadingMixin): pass widget = TestWidget() qtbot.addWidget(widget) widget.async_pdf_generator = Mock() widget.async_pdf_generator.export_pdf.return_value = True mock_project = Mock() mock_project.pages = [] result = widget.export_pdf_async(mock_project, "/output.pdf", export_dpi=150) widget.async_pdf_generator.export_pdf.assert_called_once_with(mock_project, "/output.pdf", 150) assert result is True class TestOnPdfCancel: """Tests for _on_pdf_cancel callback""" def test_on_pdf_cancel_cancels_export(self): """Test that _on_pdf_cancel cancels the export""" from pyPhotoAlbum.mixins.async_loading import AsyncLoadingMixin class TestClass(AsyncLoadingMixin): pass obj = TestClass() obj.async_pdf_generator = Mock() obj._on_pdf_cancel() obj.async_pdf_generator.cancel_export.assert_called_once() def test_on_pdf_cancel_handles_no_generator(self): """Test that _on_pdf_cancel handles missing generator""" from pyPhotoAlbum.mixins.async_loading import AsyncLoadingMixin class TestClass(AsyncLoadingMixin): pass obj = TestClass() # No async_pdf_generator # Should not raise obj._on_pdf_cancel() class TestGetAsyncStats: """Tests for get_async_stats method""" def test_get_async_stats_empty(self): """Test get_async_stats with no components initialized""" from pyPhotoAlbum.mixins.async_loading import AsyncLoadingMixin class TestClass(AsyncLoadingMixin): pass obj = TestClass() stats = obj.get_async_stats() assert stats == {} def test_get_async_stats_with_loader(self): """Test get_async_stats includes loader stats""" from pyPhotoAlbum.mixins.async_loading import AsyncLoadingMixin class TestClass(AsyncLoadingMixin): pass obj = TestClass() obj.async_image_loader = Mock() obj.async_image_loader.get_stats.return_value = {"loaded": 10} stats = obj.get_async_stats() assert "image_loader" in stats assert stats["image_loader"]["loaded"] == 10 def test_get_async_stats_with_pdf_generator(self): """Test get_async_stats includes PDF generator stats""" from pyPhotoAlbum.mixins.async_loading import AsyncLoadingMixin class TestClass(AsyncLoadingMixin): pass obj = TestClass() obj.async_pdf_generator = Mock() obj.async_pdf_generator.get_stats.return_value = {"exports": 5} stats = obj.get_async_stats() assert "pdf_generator" in stats assert stats["pdf_generator"]["exports"] == 5 def test_get_async_stats_with_all_components(self): """Test get_async_stats includes all component stats""" from pyPhotoAlbum.mixins.async_loading import AsyncLoadingMixin class TestClass(AsyncLoadingMixin): pass obj = TestClass() obj.async_image_loader = Mock() obj.async_image_loader.get_stats.return_value = {"loaded": 10} obj.async_pdf_generator = Mock() obj.async_pdf_generator.get_stats.return_value = {"exports": 5} stats = obj.get_async_stats() assert "image_loader" in stats assert "pdf_generator" in stats