""" Tests for PageSetupDialog """ import pytest from unittest.mock import Mock, MagicMock, patch from PyQt6.QtWidgets import QDialog from pyPhotoAlbum.dialogs.page_setup_dialog import PageSetupDialog from pyPhotoAlbum.project import Project, Page from pyPhotoAlbum.page_layout import PageLayout class TestPageSetupDialog: """Test PageSetupDialog UI component""" def test_dialog_initialization(self, qtbot): """Test dialog initializes with project data""" project = Project(name="Test") project.paper_thickness_mm = 0.1 project.cover_bleed_mm = 3.0 project.working_dpi = 96 project.export_dpi = 300 project.page_size_mm = (210, 297) page = Page(layout=PageLayout(width=210, height=297), page_number=1) project.pages = [page] dialog = PageSetupDialog(None, project, initial_page_index=0) qtbot.addWidget(dialog) # Check dialog is created assert dialog.windowTitle() == "Page Setup" assert dialog.minimumWidth() == 450 # Check DPI values initialized correctly assert dialog.working_dpi_spinbox.value() == 96 assert dialog.export_dpi_spinbox.value() == 300 # Check cover settings initialized correctly assert dialog.thickness_spinbox.value() == 0.1 assert dialog.bleed_spinbox.value() == 3.0 def test_dialog_page_selection(self, qtbot): """Test page selection combo box populated correctly""" project = Project(name="Test") project.page_size_mm = (210, 297) page1 = Page(layout=PageLayout(width=210, height=297), page_number=1) page2 = Page(layout=PageLayout(width=210, height=297), page_number=2) page2.manually_sized = True page3 = Page(layout=PageLayout(width=420, height=297), page_number=3) page3.is_double_spread = True project.pages = [page1, page2, page3] dialog = PageSetupDialog(None, project, initial_page_index=0) qtbot.addWidget(dialog) # Check combo box has all pages assert dialog.page_combo.count() == 3 # Check page labels assert "Page 1" in dialog.page_combo.itemText(0) assert "Page 2" in dialog.page_combo.itemText(1) assert "*" in dialog.page_combo.itemText(1) # Manually sized marker # Page 3 is a double spread, so it shows as "Pages 3-4" assert "Pages 3-4" in dialog.page_combo.itemText(2) or "Page 3" in dialog.page_combo.itemText(2) assert "Double Spread" in dialog.page_combo.itemText(2) def test_dialog_cover_settings_visibility(self, qtbot): """Test cover settings visibility toggled based on page selection""" project = Project(name="Test") project.page_size_mm = (210, 297) page1 = Page(layout=PageLayout(width=210, height=297), page_number=1) page2 = Page(layout=PageLayout(width=210, height=297), page_number=2) project.pages = [page1, page2] dialog = PageSetupDialog(None, project, initial_page_index=0) qtbot.addWidget(dialog) # Test the _on_page_changed method directly (testing business logic) # When showing first page (index 0), cover group should be made visible dialog._on_page_changed(0) # We can't reliably test isVisible() in headless Qt, but we can verify # the method was called and completed without error # When showing second page (index 1), cover group should be hidden dialog._on_page_changed(1) # Test that invalid indices are handled gracefully dialog._on_page_changed(-1) # Should return early dialog._on_page_changed(999) # Should return early # Verify page combo was populated correctly assert dialog.page_combo.count() == 2 def test_dialog_cover_disables_size_editing(self, qtbot): """Test cover pages disable size editing""" project = Project(name="Test") project.page_size_mm = (210, 297) page1 = Page(layout=PageLayout(width=500, height=297), page_number=1) page1.is_cover = True project.pages = [page1] dialog = PageSetupDialog(None, project, initial_page_index=0) qtbot.addWidget(dialog) # Size editing should be disabled for covers assert not dialog.width_spinbox.isEnabled() assert not dialog.height_spinbox.isEnabled() assert not dialog.set_default_checkbox.isEnabled() def test_dialog_double_spread_width_calculation(self, qtbot): """Test double spread shows per-page width, not total width""" project = Project(name="Test") project.page_size_mm = (210, 297) page = Page(layout=PageLayout(width=420, height=297), page_number=1) page.is_double_spread = True page.layout.base_width = 210 project.pages = [page] dialog = PageSetupDialog(None, project, initial_page_index=0) qtbot.addWidget(dialog) # Should show base width (per-page width), not total width assert dialog.width_spinbox.value() == 210 assert dialog.height_spinbox.value() == 297 def test_dialog_spine_info_calculation(self, qtbot): """Test spine info is calculated correctly""" project = Project(name="Test") project.page_size_mm = (210, 297) project.paper_thickness_mm = 0.1 project.cover_bleed_mm = 3.0 page1 = Page(layout=PageLayout(width=210, height=297), page_number=1) page1.is_cover = False page2 = Page(layout=PageLayout(width=210, height=297), page_number=2) page3 = Page(layout=PageLayout(width=210, height=297), page_number=3) project.pages = [page1, page2, page3] dialog = PageSetupDialog(None, project, initial_page_index=0) qtbot.addWidget(dialog) # Enable cover checkbox dialog.cover_checkbox.setChecked(True) # Check spine info label has content spine_text = dialog.spine_info_label.text() assert "Cover Layout" in spine_text assert "Front" in spine_text assert "Spine" in spine_text assert "Back" in spine_text assert "Bleed" in spine_text # Disable cover checkbox dialog.cover_checkbox.setChecked(False) # Spine info should be empty assert dialog.spine_info_label.text() == "" def test_get_values_returns_correct_data(self, qtbot): """Test get_values returns all dialog values""" project = Project(name="Test") project.page_size_mm = (210, 297) project.paper_thickness_mm = 0.1 project.cover_bleed_mm = 3.0 project.working_dpi = 96 project.export_dpi = 300 page = Page(layout=PageLayout(width=210, height=297), page_number=1) project.pages = [page] dialog = PageSetupDialog(None, project, initial_page_index=0) qtbot.addWidget(dialog) # Modify some values dialog.width_spinbox.setValue(200) dialog.height_spinbox.setValue(280) dialog.working_dpi_spinbox.setValue(150) dialog.export_dpi_spinbox.setValue(600) dialog.set_default_checkbox.setChecked(True) dialog.cover_checkbox.setChecked(True) dialog.thickness_spinbox.setValue(0.15) dialog.bleed_spinbox.setValue(5.0) values = dialog.get_values() # Check all values returned assert values["selected_index"] == 0 assert values["selected_page"] == page assert values["is_cover"] is True assert values["paper_thickness_mm"] == 0.15 assert values["cover_bleed_mm"] == 5.0 assert values["width_mm"] == 200 assert values["height_mm"] == 280 assert values["working_dpi"] == 150 assert values["export_dpi"] == 600 assert values["set_as_default"] is True def test_dialog_page_change_updates_values(self, qtbot): """Test changing selected page updates displayed values""" project = Project(name="Test") project.page_size_mm = (210, 297) page1 = Page(layout=PageLayout(width=210, height=297), page_number=1) page2 = Page(layout=PageLayout(width=180, height=250), page_number=2) project.pages = [page1, page2] dialog = PageSetupDialog(None, project, initial_page_index=0) qtbot.addWidget(dialog) # Initially showing page 1 values assert dialog.width_spinbox.value() == 210 assert dialog.height_spinbox.value() == 297 # Change to page 2 dialog.page_combo.setCurrentIndex(1) # Should now show page 2 values assert dialog.width_spinbox.value() == 180 assert dialog.height_spinbox.value() == 250 class TestDialogMixin: """Test DialogMixin functionality""" def test_dialog_mixin_create_dialog_accepted(self, qtbot): """Test create_dialog returns values when accepted""" from pyPhotoAlbum.mixins.dialog_mixin import DialogMixin class TestWindow(DialogMixin): pass window = TestWindow() # Create mock dialog with get_values as a proper method mock_dialog = MagicMock(spec=QDialog) mock_dialog.exec = Mock(return_value=QDialog.DialogCode.Accepted) mock_dialog.get_values = Mock(return_value={"test": "value"}) # Mock dialog class mock_dialog_class = Mock(return_value=mock_dialog) result = window.create_dialog(mock_dialog_class) assert result == {"test": "value"} mock_dialog.exec.assert_called_once() def test_dialog_mixin_create_dialog_rejected(self, qtbot): """Test create_dialog returns None when rejected""" from pyPhotoAlbum.mixins.dialog_mixin import DialogMixin class TestWindow(DialogMixin): pass window = TestWindow() # Create mock dialog mock_dialog = Mock(spec=QDialog) mock_dialog.exec.return_value = QDialog.DialogCode.Rejected # Mock dialog class mock_dialog_class = Mock(return_value=mock_dialog) result = window.create_dialog(mock_dialog_class) assert result is None mock_dialog.exec.assert_called_once() def test_dialog_mixin_show_dialog_with_callback(self, qtbot): """Test show_dialog executes callback on acceptance""" from pyPhotoAlbum.mixins.dialog_mixin import DialogMixin class TestWindow(DialogMixin): pass window = TestWindow() # Create mock dialog with get_values as a proper method mock_dialog = MagicMock(spec=QDialog) mock_dialog.exec = Mock(return_value=QDialog.DialogCode.Accepted) mock_dialog.get_values = Mock(return_value={"test": "value"}) # Mock dialog class mock_dialog_class = Mock(return_value=mock_dialog) # Mock callback callback = Mock() result = window.show_dialog(mock_dialog_class, on_accept=callback) assert result is True callback.assert_called_once_with({"test": "value"}) class TestDialogActionDecorator: """Test the @dialog_action decorator functionality""" def test_decorator_with_title_override(self, qtbot): """Test decorator can set custom dialog title""" from pyPhotoAlbum.decorators import dialog_action # We'll test that the decorator can pass through kwargs # This is more of a structural test decorator = dialog_action(dialog_class=PageSetupDialog, requires_pages=True) assert decorator.dialog_class == PageSetupDialog assert decorator.requires_pages is True def test_decorator_without_pages_requirement(self, qtbot): """Test decorator can disable page requirement""" from pyPhotoAlbum.decorators import dialog_action decorator = dialog_action(dialog_class=PageSetupDialog, requires_pages=False) assert decorator.requires_pages is False def test_dialog_action_class_decorator(self, qtbot): """Test DialogAction class directly""" from pyPhotoAlbum.decorators import DialogAction decorator = DialogAction(dialog_class=PageSetupDialog, requires_pages=True) assert decorator.dialog_class == PageSetupDialog assert decorator.requires_pages is True class TestDialogMixinEdgeCases: """Test edge cases for DialogMixin""" def test_create_dialog_without_get_values(self, qtbot): """Test create_dialog when dialog has no get_values method""" from pyPhotoAlbum.mixins.dialog_mixin import DialogMixin class TestWindow(DialogMixin): pass window = TestWindow() # Create mock dialog WITHOUT get_values mock_dialog = MagicMock(spec=QDialog) mock_dialog.exec = Mock(return_value=QDialog.DialogCode.Accepted) # Explicitly make get_values unavailable del mock_dialog.get_values mock_dialog_class = Mock(return_value=mock_dialog) result = window.create_dialog(mock_dialog_class) # Should return True when accepted even without get_values assert result is True def test_create_dialog_with_title(self, qtbot): """Test create_dialog with custom title""" from pyPhotoAlbum.mixins.dialog_mixin import DialogMixin class TestWindow(DialogMixin): pass window = TestWindow() mock_dialog = MagicMock(spec=QDialog) mock_dialog.exec = Mock(return_value=QDialog.DialogCode.Accepted) mock_dialog.get_values = Mock(return_value={"data": "test"}) mock_dialog_class = Mock(return_value=mock_dialog) result = window.create_dialog(mock_dialog_class, title="Custom Title") # Verify setWindowTitle was called mock_dialog.setWindowTitle.assert_called_once_with("Custom Title") assert result == {"data": "test"} def test_show_dialog_rejected(self, qtbot): """Test show_dialog when user rejects dialog""" from pyPhotoAlbum.mixins.dialog_mixin import DialogMixin class TestWindow(DialogMixin): pass window = TestWindow() mock_dialog = MagicMock(spec=QDialog) mock_dialog.exec = Mock(return_value=QDialog.DialogCode.Rejected) mock_dialog_class = Mock(return_value=mock_dialog) callback = Mock() result = window.show_dialog(mock_dialog_class, on_accept=callback) # Callback should not be called callback.assert_not_called() assert result is False class TestPageSetupDialogEdgeCases: """Test edge cases in PageSetupDialog""" def test_dialog_with_cover_page(self, qtbot): """Test dialog correctly handles cover pages""" project = Project(name="Test") project.page_size_mm = (210, 297) project.paper_thickness_mm = 0.1 project.cover_bleed_mm = 3.0 page1 = Page(layout=PageLayout(width=500, height=297), page_number=1) page1.is_cover = True project.pages = [page1] dialog = PageSetupDialog(None, project, initial_page_index=0) qtbot.addWidget(dialog) # Cover checkbox should be checked assert dialog.cover_checkbox.isChecked() # Width spinbox should show full cover width assert dialog.width_spinbox.value() == 500 def test_dialog_invalid_initial_page_index(self, qtbot): """Test dialog handles invalid initial page index gracefully""" project = Project(name="Test") project.page_size_mm = (210, 297) page = Page(layout=PageLayout(width=210, height=297), page_number=1) project.pages = [page] # Invalid initial index (out of bounds) dialog = PageSetupDialog(None, project, initial_page_index=999) qtbot.addWidget(dialog) # Should still work, defaulting to first available page or handling gracefully assert dialog.page_combo.count() == 1 def test_on_page_changed_invalid_index(self, qtbot): """Test _on_page_changed handles invalid indices""" project = Project(name="Test") project.page_size_mm = (210, 297) page = Page(layout=PageLayout(width=210, height=297), page_number=1) project.pages = [page] dialog = PageSetupDialog(None, project, initial_page_index=0) qtbot.addWidget(dialog) # Call with negative index - should return early dialog._on_page_changed(-1) # Call with out of bounds index - should return early dialog._on_page_changed(999) # Dialog should still be functional assert dialog.page_combo.count() == 1 def test_update_spine_info_when_not_cover(self, qtbot): """Test spine info is empty when cover checkbox is unchecked""" project = Project(name="Test") project.page_size_mm = (210, 297) page = Page(layout=PageLayout(width=210, height=297), page_number=1) project.pages = [page] dialog = PageSetupDialog(None, project, initial_page_index=0) qtbot.addWidget(dialog) # Uncheck cover dialog.cover_checkbox.setChecked(False) # Spine info should be empty assert dialog.spine_info_label.text() == "" class TestPageSetupIntegration: """Integration tests for page_setup with decorator""" def test_page_setup_decorator_requires_pages(self, qtbot): """Test page_setup decorator returns early when no pages""" from PyQt6.QtWidgets import QMainWindow from pyPhotoAlbum.mixins.base import ApplicationStateMixin from pyPhotoAlbum.mixins.operations.page_ops import PageOperationsMixin class TestWindow(PageOperationsMixin, ApplicationStateMixin, QMainWindow): def __init__(self): super().__init__() self._project = Project(name="Test") self._project.pages = [] # No pages self._gl_widget = Mock() self._status_bar = Mock() self._update_view_called = False def update_view(self): self._update_view_called = True def show_status(self, message, timeout=0): pass window = TestWindow() qtbot.addWidget(window) # Should return early without showing dialog result = window.page_setup() # No update should occur assert not window._update_view_called assert result is None def test_page_setup_applies_values(self, qtbot): """Test page_setup applies dialog values to project""" from PyQt6.QtWidgets import QMainWindow from pyPhotoAlbum.mixins.base import ApplicationStateMixin from pyPhotoAlbum.mixins.operations.page_ops import PageOperationsMixin class TestWindow(PageOperationsMixin, ApplicationStateMixin, QMainWindow): def __init__(self): super().__init__() self._project = Project(name="Test") self._project.page_size_mm = (210, 297) self._project.working_dpi = 96 self._project.export_dpi = 300 self._project.paper_thickness_mm = 0.1 self._project.cover_bleed_mm = 3.0 page = Page(layout=PageLayout(width=210, height=297), page_number=1) self._project.pages = [page] self._gl_widget = Mock() self._gl_widget._page_renderers = [] self._status_bar = Mock() self._update_view_called = False self._status_message = None def _get_most_visible_page_index(self): return 0 def update_view(self): self._update_view_called = True def show_status(self, message, timeout=0): self._status_message = message window = TestWindow() qtbot.addWidget(window) # Create mock values that would come from dialog values = { "selected_index": 0, "selected_page": window.project.pages[0], "is_cover": False, "paper_thickness_mm": 0.15, "cover_bleed_mm": 5.0, "width_mm": 200, "height_mm": 280, "working_dpi": 150, "export_dpi": 600, "set_as_default": True, } # Access the unwrapped function to test business logic directly # The decorator wraps the function, so we need to get the original # or call it through the wrapper with the right setup import inspect # Get the original function before decorators original_func = window.page_setup # Decorators return wrappers, but we can call them with values directly # by accessing the innermost wrapped function while hasattr(original_func, "__wrapped__"): original_func = original_func.__wrapped__ # If no __wrapped__, the decorator system is different # Let's just call the business logic method manually # First, let's extract and call just the business logic from pyPhotoAlbum.mixins.operations import page_ops # Get the undecorated method from the class undecorated_page_setup = page_ops.PageOperationsMixin.page_setup # Find the innermost function while hasattr(undecorated_page_setup, "__wrapped__"): undecorated_page_setup = undecorated_page_setup.__wrapped__ # Call the business logic directly undecorated_page_setup(window, values) # Check values applied to project assert window.project.paper_thickness_mm == 0.15 assert window.project.cover_bleed_mm == 5.0 assert window.project.working_dpi == 150 assert window.project.export_dpi == 600 assert window.project.page_size_mm == (200, 280) # set_as_default=True # Check page size updated assert window.project.pages[0].layout.size == (200, 280) assert window.project.pages[0].manually_sized is True # Check view updated assert window._update_view_called assert window._status_message is not None def test_page_setup_cover_designation(self, qtbot): """Test page_setup correctly designates and un-designates covers""" from PyQt6.QtWidgets import QMainWindow from pyPhotoAlbum.mixins.base import ApplicationStateMixin from pyPhotoAlbum.mixins.operations.page_ops import PageOperationsMixin class TestWindow(PageOperationsMixin, ApplicationStateMixin, QMainWindow): def __init__(self): super().__init__() self._project = Project(name="Test") self._project.page_size_mm = (210, 297) self._project.working_dpi = 96 self._project.export_dpi = 300 self._project.paper_thickness_mm = 0.1 self._project.cover_bleed_mm = 3.0 page = Page(layout=PageLayout(width=210, height=297), page_number=1) page.is_cover = False self._project.pages = [page] self._gl_widget = Mock() self._gl_widget._page_renderers = [] self._status_bar = Mock() self._update_view_called = False def _get_most_visible_page_index(self): return 0 def update_view(self): self._update_view_called = True def show_status(self, message, timeout=0): pass window = TestWindow() qtbot.addWidget(window) # Test designating first page as cover values = { "selected_index": 0, "selected_page": window.project.pages[0], "is_cover": True, # Designate as cover "paper_thickness_mm": 0.1, "cover_bleed_mm": 3.0, "width_mm": 210, "height_mm": 297, "working_dpi": 96, "export_dpi": 300, "set_as_default": False, } # Get the undecorated method from pyPhotoAlbum.mixins.operations import page_ops undecorated_page_setup = page_ops.PageOperationsMixin.page_setup while hasattr(undecorated_page_setup, "__wrapped__"): undecorated_page_setup = undecorated_page_setup.__wrapped__ # Mock update_cover_dimensions window.project.update_cover_dimensions = Mock() # Call with cover designation undecorated_page_setup(window, values) # Check cover was designated assert window.project.pages[0].is_cover is True assert window.project.has_cover is True window.project.update_cover_dimensions.assert_called_once() def test_page_setup_double_spread_sizing(self, qtbot): """Test page_setup correctly handles double spread page sizing""" from PyQt6.QtWidgets import QMainWindow from pyPhotoAlbum.mixins.base import ApplicationStateMixin from pyPhotoAlbum.mixins.operations.page_ops import PageOperationsMixin class TestWindow(PageOperationsMixin, ApplicationStateMixin, QMainWindow): def __init__(self): super().__init__() self._project = Project(name="Test") self._project.page_size_mm = (210, 297) self._project.working_dpi = 96 self._project.export_dpi = 300 self._project.paper_thickness_mm = 0.1 self._project.cover_bleed_mm = 3.0 # Create double spread page page = Page(layout=PageLayout(width=420, height=297), page_number=1) page.is_double_spread = True page.layout.base_width = 210 page.layout.is_facing_page = True self._project.pages = [page] self._gl_widget = Mock() self._gl_widget._page_renderers = [] self._status_bar = Mock() self._update_view_called = False def _get_most_visible_page_index(self): return 0 def update_view(self): self._update_view_called = True def show_status(self, message, timeout=0): pass window = TestWindow() qtbot.addWidget(window) # Test changing double spread page size values = { "selected_index": 0, "selected_page": window.project.pages[0], "is_cover": False, "paper_thickness_mm": 0.1, "cover_bleed_mm": 3.0, "width_mm": 200, # New base width "height_mm": 280, # New height "working_dpi": 96, "export_dpi": 300, "set_as_default": False, } from pyPhotoAlbum.mixins.operations import page_ops undecorated_page_setup = page_ops.PageOperationsMixin.page_setup while hasattr(undecorated_page_setup, "__wrapped__"): undecorated_page_setup = undecorated_page_setup.__wrapped__ undecorated_page_setup(window, values) # Check double spread sizing assert window.project.pages[0].layout.base_width == 200 assert window.project.pages[0].layout.size == (400, 280) # Double width assert window.project.pages[0].manually_sized is True