pyPhotoAlbum/tests/test_page_setup_dialog_mocked.py
Duncan Tourolle f6ed11b0bc
All checks were successful
Python CI / test (push) Successful in 1m20s
Lint / lint (push) Successful in 1m4s
Tests / test (3.11) (push) Successful in 1m27s
Tests / test (3.12) (push) Successful in 2m25s
Tests / test (3.13) (push) Successful in 2m52s
Tests / test (3.14) (push) Successful in 1m9s
black formatting
2025-11-27 23:07:16 +01:00

435 lines
15 KiB
Python

"""
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"])