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
435 lines
15 KiB
Python
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"])
|