""" Unit tests for PageSetupDialog with mocked Qt widgets These tests mock Qt widgets to avoid dependencies on the display system and test the dialog logic in isolation. """ import pytest from unittest.mock import Mock, MagicMock, patch, call from pyPhotoAlbum.project import Project, Page from pyPhotoAlbum.page_layout import PageLayout class TestPageSetupDialogWithMocks: """Test PageSetupDialog with fully mocked Qt widgets""" def test_dialog_stores_initialization_params(self): """Test dialog stores project and initial page index""" # We test that the dialog class properly stores init parameters # without actually creating Qt widgets from pyPhotoAlbum.dialogs.page_setup_dialog import PageSetupDialog project = Project(name="Test") page = Page(layout=PageLayout(width=210, height=297), page_number=1) project.pages = [page] # We can verify the class signature and that it would accept these params # This is a structural test rather than a full initialization test assert hasattr(PageSetupDialog, "__init__") # The actual widget creation tests are in test_page_setup_dialog.py # using qtbot which handles Qt properly def test_on_page_changed_logic_isolated(self): """Test _on_page_changed logic without Qt dependencies""" from pyPhotoAlbum.dialogs.page_setup_dialog import PageSetupDialog # Setup project 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] # Mock the dialog instance with patch.object(PageSetupDialog, "__init__", lambda self, *args, **kwargs: None): dialog = PageSetupDialog(None, None, 0) # Manually set required attributes dialog.project = project dialog._cover_group = Mock() dialog.cover_checkbox = Mock() dialog.width_spinbox = Mock() dialog.height_spinbox = Mock() dialog.set_default_checkbox = Mock() # Mock the update spine info method dialog._update_spine_info = Mock() # Test with first page (index 0) dialog._on_page_changed(0) # Verify cover group was made visible (first page) dialog._cover_group.setVisible.assert_called_with(True) # Verify cover checkbox was updated dialog.cover_checkbox.setChecked.assert_called_once() # Verify spine info was updated dialog._update_spine_info.assert_called_once() # Reset mocks dialog._cover_group.reset_mock() dialog._update_spine_info.reset_mock() # Test with second page (index 1) dialog._on_page_changed(1) # Verify cover group was hidden (not first page) dialog._cover_group.setVisible.assert_called_with(False) # Verify spine info was NOT updated (not first page) dialog._update_spine_info.assert_not_called() def test_on_page_changed_invalid_indices(self): """Test _on_page_changed handles invalid indices""" from pyPhotoAlbum.dialogs.page_setup_dialog import PageSetupDialog project = Project(name="Test") page = Page(layout=PageLayout(width=210, height=297), page_number=1) project.pages = [page] with patch.object(PageSetupDialog, "__init__", lambda self, *args, **kwargs: None): dialog = PageSetupDialog(None, None, 0) dialog.project = project dialog._cover_group = Mock() # Test negative index - should return early dialog._on_page_changed(-1) dialog._cover_group.setVisible.assert_not_called() # Test out of bounds index - should return early dialog._on_page_changed(999) dialog._cover_group.setVisible.assert_not_called() def test_update_spine_info_calculation(self): """Test spine info calculation logic""" from pyPhotoAlbum.dialogs.page_setup_dialog import PageSetupDialog project = Project(name="Test") project.page_size_mm = (210, 297) project.paper_thickness_mm = 0.1 project.cover_bleed_mm = 3.0 # Create 3 content pages (not covers) for i in range(3): page = Page(layout=PageLayout(width=210, height=297), page_number=i + 1) page.is_cover = False project.pages.append(page) with patch.object(PageSetupDialog, "__init__", lambda self, *args, **kwargs: None): dialog = PageSetupDialog(None, None, 0) dialog.project = project dialog.cover_checkbox = Mock() dialog.thickness_spinbox = Mock() dialog.bleed_spinbox = Mock() dialog.spine_info_label = Mock() # Test when cover is enabled dialog.cover_checkbox.isChecked.return_value = True dialog.thickness_spinbox.value.return_value = 0.1 dialog.bleed_spinbox.value.return_value = 3.0 dialog._update_spine_info() # Verify spine info was set (not empty) assert dialog.spine_info_label.setText.called call_args = dialog.spine_info_label.setText.call_args[0][0] assert "Cover Layout" in call_args assert "Spine" in call_args assert "Front" in call_args # Reset dialog.spine_info_label.reset_mock() # Test when cover is disabled dialog.cover_checkbox.isChecked.return_value = False dialog._update_spine_info() # Verify spine info was cleared dialog.spine_info_label.setText.assert_called_once_with("") def test_get_values_data_extraction(self): """Test get_values extracts all data correctly""" from pyPhotoAlbum.dialogs.page_setup_dialog import PageSetupDialog project = Project(name="Test") project.page_size_mm = (210, 297) page = Page(layout=PageLayout(width=210, height=297), page_number=1) project.pages = [page] with patch.object(PageSetupDialog, "__init__", lambda self, *args, **kwargs: None): dialog = PageSetupDialog(None, None, 0) dialog.project = project # Mock all input widgets dialog.page_combo = Mock() dialog.page_combo.currentData.return_value = 0 dialog.cover_checkbox = Mock() dialog.cover_checkbox.isChecked.return_value = True dialog.thickness_spinbox = Mock() dialog.thickness_spinbox.value.return_value = 0.15 dialog.bleed_spinbox = Mock() dialog.bleed_spinbox.value.return_value = 5.0 dialog.width_spinbox = Mock() dialog.width_spinbox.value.return_value = 200.0 dialog.height_spinbox = Mock() dialog.height_spinbox.value.return_value = 280.0 dialog.working_dpi_spinbox = Mock() dialog.working_dpi_spinbox.value.return_value = 150 dialog.export_dpi_spinbox = Mock() dialog.export_dpi_spinbox.value.return_value = 600 dialog.set_default_checkbox = Mock() dialog.set_default_checkbox.isChecked.return_value = True # Get values values = dialog.get_values() # Verify all values were extracted 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.0 assert values["height_mm"] == 280.0 assert values["working_dpi"] == 150 assert values["export_dpi"] == 600 assert values["set_as_default"] is True def test_cover_page_width_display(self): """Test cover page shows full width, not base width""" from pyPhotoAlbum.dialogs.page_setup_dialog import PageSetupDialog project = Project(name="Test") project.page_size_mm = (210, 297) # Create cover page with special width page = Page(layout=PageLayout(width=500, height=297), page_number=1) page.is_cover = True project.pages = [page] with patch.object(PageSetupDialog, "__init__", lambda self, *args, **kwargs: None): dialog = PageSetupDialog(None, None, 0) dialog.project = project dialog._cover_group = Mock() dialog.cover_checkbox = Mock() dialog.width_spinbox = Mock() dialog.height_spinbox = Mock() dialog.set_default_checkbox = Mock() dialog._update_spine_info = Mock() # Call _on_page_changed for cover page dialog._on_page_changed(0) # Verify width was set to full cover width (500), not base width dialog.width_spinbox.setValue.assert_called() width_call = dialog.width_spinbox.setValue.call_args[0][0] assert width_call == 500 # Verify widgets were disabled for cover dialog.width_spinbox.setEnabled.assert_called_with(False) dialog.height_spinbox.setEnabled.assert_called_with(False) dialog.set_default_checkbox.setEnabled.assert_called_with(False) # Note: Additional widget state tests are covered in test_page_setup_dialog.py # using qtbot which properly handles Qt widget initialization class TestDialogMixinMocked: """Test DialogMixin with mocked dialogs""" def test_create_dialog_flow(self): """Test create_dialog method flow""" from pyPhotoAlbum.mixins.dialog_mixin import DialogMixin class TestWindow(DialogMixin): pass window = TestWindow() # Mock dialog class mock_dialog_instance = Mock() mock_dialog_instance.exec.return_value = 1 # Accepted mock_dialog_instance.get_values.return_value = {"key": "value"} mock_dialog_class = Mock(return_value=mock_dialog_instance) # Call create_dialog result = window.create_dialog(mock_dialog_class, title="Test Title", extra_param="test") # Verify dialog was created with correct params mock_dialog_class.assert_called_once_with(parent=window, extra_param="test") # Verify title was set mock_dialog_instance.setWindowTitle.assert_called_once_with("Test Title") # Verify dialog was executed mock_dialog_instance.exec.assert_called_once() # Verify get_values was called mock_dialog_instance.get_values.assert_called_once() # Verify result assert result == {"key": "value"} def test_show_dialog_with_callback_flow(self): """Test show_dialog method with callback""" from pyPhotoAlbum.mixins.dialog_mixin import DialogMixin class TestWindow(DialogMixin): pass window = TestWindow() # Mock dialog mock_dialog_instance = Mock() mock_dialog_instance.exec.return_value = 1 # Accepted mock_dialog_instance.get_values.return_value = {"data": "test"} mock_dialog_class = Mock(return_value=mock_dialog_instance) # Mock callback callback = Mock() # Call show_dialog result = window.show_dialog(mock_dialog_class, on_accept=callback, param="value") # Verify callback was called with dialog values callback.assert_called_once_with({"data": "test"}) # Verify result assert result is True def test_show_dialog_rejected_no_callback(self): """Test show_dialog when dialog is rejected""" from pyPhotoAlbum.mixins.dialog_mixin import DialogMixin class TestWindow(DialogMixin): pass window = TestWindow() # Mock rejected dialog mock_dialog_instance = Mock() mock_dialog_instance.exec.return_value = 0 # Rejected mock_dialog_class = Mock(return_value=mock_dialog_instance) callback = Mock() # Call show_dialog result = window.show_dialog(mock_dialog_class, on_accept=callback) # Verify callback was NOT called callback.assert_not_called() # Verify result assert result is False class TestDialogActionDecoratorMocked: """Test @dialog_action decorator with mocks""" def test_decorator_creates_and_shows_dialog(self): """Test decorator creates dialog and passes values to function""" from pyPhotoAlbum.decorators import dialog_action from PyQt6.QtWidgets import QDialog # Mock dialog instance mock_dialog = Mock() mock_dialog.exec.return_value = QDialog.DialogCode.Accepted # Accepted mock_dialog.get_values.return_value = {"test": "data"} # Mock dialog class mock_dialog_cls = Mock(return_value=mock_dialog) # Create decorated function @dialog_action(dialog_class=mock_dialog_cls, requires_pages=True) def test_function(self, values): return values["test"] # Mock instance with required attributes instance = Mock() instance.project = Mock() instance.project.pages = [Mock()] # Has pages instance._get_most_visible_page_index = Mock(return_value=0) # Call decorated function result = test_function(instance) # Verify dialog was created mock_dialog_cls.assert_called_once() # Verify dialog was shown mock_dialog.exec.assert_called_once() # Verify values were extracted mock_dialog.get_values.assert_called_once() # Verify original function received values assert result == "data" def test_decorator_returns_early_when_no_pages(self): """Test decorator returns early when pages required but not present""" from pyPhotoAlbum.decorators import dialog_action mock_dialog_cls = Mock() @dialog_action(dialog_class=mock_dialog_cls, requires_pages=True) def test_function(self, values): return "should not reach" # Mock instance with no pages instance = Mock() instance.project = Mock() instance.project.pages = [] # No pages # Call decorated function result = test_function(instance) # Verify dialog was NOT created mock_dialog_cls.assert_not_called() # Verify result is None assert result is None def test_decorator_works_without_pages_requirement(self): """Test decorator works when pages not required""" from pyPhotoAlbum.decorators import dialog_action mock_dialog = Mock() mock_dialog.exec.return_value = 1 mock_dialog.get_values.return_value = {"key": "val"} mock_dialog_cls = Mock(return_value=mock_dialog) @dialog_action(dialog_class=mock_dialog_cls, requires_pages=False) def test_function(self, values): return values # Mock instance with no pages instance = Mock() instance.project = Mock() instance.project.pages = [] # No pages, but that's OK # Call decorated function result = test_function(instance) # Verify dialog WAS created (pages not required) mock_dialog_cls.assert_called_once() # Verify result assert result == {"key": "val"} if __name__ == "__main__": pytest.main([__file__, "-v"])