pyPhotoAlbum/tests/test_page_setup_dialog.py
Duncan Tourolle 6755549dfd
All checks were successful
Python CI / test (push) Successful in 1m37s
Lint / lint (push) Successful in 1m32s
Tests / test (3.10) (push) Successful in 1m11s
Tests / test (3.11) (push) Successful in 1m11s
Tests / test (3.9) (push) Successful in 1m8s
Refactor to allow indepth testing
2025-11-23 11:05:46 +01:00

732 lines
27 KiB
Python

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