pyPhotoAlbum/tests/test_page_ops_mixin.py
Duncan Tourolle 0d698a83b4
Some checks failed
Python CI / test (push) Successful in 55s
Lint / lint (push) Successful in 1m31s
Tests / test (3.10) (push) Failing after 44s
Tests / test (3.11) (push) Failing after 42s
Tests / test (3.9) (push) Failing after 42s
large change to allow project merging
2025-11-23 00:33:42 +01:00

472 lines
17 KiB
Python
Executable File

"""
Tests for PageOperationsMixin
"""
import pytest
from unittest.mock import Mock, MagicMock, patch
from PyQt6.QtWidgets import QMainWindow
from pyPhotoAlbum.mixins.base import ApplicationStateMixin
from pyPhotoAlbum.mixins.operations.page_ops import PageOperationsMixin
from pyPhotoAlbum.project import Project, Page
from pyPhotoAlbum.page_layout import PageLayout
class TestPageOpsWindow(PageOperationsMixin, ApplicationStateMixin, QMainWindow):
"""Test window with page operations mixin"""
def __init__(self):
super().__init__()
self._gl_widget = Mock()
self._gl_widget.current_page_index = 0
self._gl_widget.zoom_level = 1.0
self._gl_widget.pan_offset = [0, 0]
self._gl_widget._page_renderers = []
self._gl_widget.width = Mock(return_value=800)
self._gl_widget.height = Mock(return_value=600)
self._project = Project(name="Test")
self._project.working_dpi = 96
self._project.page_size_mm = (210, 297)
self._update_view_called = False
self._status_message = None
self._status_bar = Mock()
def update_view(self):
self._update_view_called = True
def show_status(self, message, timeout=0):
self._status_message = message
def show_warning(self, title, message):
pass
class TestGetMostVisiblePageIndex:
"""Test _get_most_visible_page_index method"""
def test_no_renderers_returns_current_index(self, qtbot):
"""Test returns current_page_index when no renderers"""
window = TestPageOpsWindow()
qtbot.addWidget(window)
window.gl_widget.current_page_index = 3
window.gl_widget._page_renderers = []
result = window._get_most_visible_page_index()
assert result == 3
def test_single_page_returns_zero(self, qtbot):
"""Test with single page returns index 0"""
window = TestPageOpsWindow()
qtbot.addWidget(window)
# Create a single page
page = Page(layout=PageLayout(width=210, height=297), page_number=1)
window.project.pages = [page]
# Create mock renderer
mock_renderer = Mock()
mock_renderer.screen_y = 100
window.gl_widget._page_renderers = [(mock_renderer, page)]
result = window._get_most_visible_page_index()
assert result == 0
def test_multiple_pages_finds_closest_to_center(self, qtbot):
"""Test finds page closest to viewport center"""
window = TestPageOpsWindow()
qtbot.addWidget(window)
window.gl_widget.height = Mock(return_value=600) # Viewport center at y=300
# Create three pages
page1 = Page(layout=PageLayout(width=210, height=297), page_number=1)
page2 = Page(layout=PageLayout(width=210, height=297), page_number=2)
page3 = Page(layout=PageLayout(width=210, height=297), page_number=3)
window.project.pages = [page1, page2, page3]
# Calculate page height in pixels: 297mm * 96dpi / 25.4 = ~1122px
# At zoom 1.0, half page height = ~561px
# Viewport center is at y=300
# Create renderers with different screen_y positions
# Page 1: screen_y = 50, center at 50 + 561 = 611, distance = |611 - 300| = 311
# Page 2: screen_y = -300, center at -300 + 561 = 261, distance = |261 - 300| = 39 <- closest!
# Page 3: screen_y = 800, center at 800 + 561 = 1361, distance = |1361 - 300| = 1061
renderer1 = Mock()
renderer1.screen_y = 50
renderer2 = Mock()
renderer2.screen_y = -300 # This will put page center near viewport center
renderer3 = Mock()
renderer3.screen_y = 800
window.gl_widget._page_renderers = [
(renderer1, page1),
(renderer2, page2),
(renderer3, page3)
]
result = window._get_most_visible_page_index()
# Page 2 (index 1) should be closest to viewport center
assert result == 1
def test_handles_page_not_in_project_list(self, qtbot):
"""Test handles case where page is not in project.pages"""
window = TestPageOpsWindow()
qtbot.addWidget(window)
page1 = Page(layout=PageLayout(width=210, height=297), page_number=1)
orphan_page = Page(layout=PageLayout(width=210, height=297), page_number=99)
window.project.pages = [page1]
renderer1 = Mock()
renderer1.screen_y = 100
renderer_orphan = Mock()
renderer_orphan.screen_y = 50 # Closer to center
window.gl_widget._page_renderers = [
(renderer1, page1),
(renderer_orphan, orphan_page) # Not in project.pages
]
window.gl_widget.current_page_index = 0
result = window._get_most_visible_page_index()
# Should fallback to valid page (page1) or current_page_index
assert result == 0
class TestToggleDoubleSpread:
"""Test toggle_double_spread method"""
def test_toggle_spread_no_pages(self, qtbot):
"""Test returns early when no pages"""
window = TestPageOpsWindow()
qtbot.addWidget(window)
window.project.pages = []
window.toggle_double_spread()
# Should return early without error
assert not window._update_view_called
def test_toggle_spread_enables_double_spread(self, qtbot):
"""Test enables double spread on single page"""
window = TestPageOpsWindow()
qtbot.addWidget(window)
# Create single page
page = Page(layout=PageLayout(width=210, height=297), page_number=1)
page.is_double_spread = False
window.project.pages = [page]
# Mock renderer
mock_renderer = Mock()
mock_renderer.screen_y = 100
window.gl_widget._page_renderers = [(mock_renderer, page)]
window.toggle_double_spread()
assert page.is_double_spread is True
assert page.manually_sized is True
assert page.layout.is_facing_page is True
assert page.layout.size[0] == 420 # 210 * 2
assert page.layout.size[1] == 297
assert window._update_view_called
assert "enabled" in window._status_message
def test_toggle_spread_disables_double_spread(self, qtbot):
"""Test disables double spread on double page"""
window = TestPageOpsWindow()
qtbot.addWidget(window)
# 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
window.project.pages = [page]
mock_renderer = Mock()
mock_renderer.screen_y = 100
window.gl_widget._page_renderers = [(mock_renderer, page)]
window.toggle_double_spread()
assert page.is_double_spread is False
assert page.layout.is_facing_page is False
assert page.layout.size[0] == 210 # Back to single width
assert page.layout.size[1] == 297
assert window._update_view_called
assert "disabled" in window._status_message
def test_toggle_spread_uses_most_visible_page(self, qtbot):
"""Test toggles the most visible page, not always first page"""
window = TestPageOpsWindow()
qtbot.addWidget(window)
window.gl_widget.height = Mock(return_value=600) # Viewport center at y=300
# Create three pages
page1 = Page(layout=PageLayout(width=210, height=297), page_number=1)
page1.is_double_spread = False
page2 = Page(layout=PageLayout(width=210, height=297), page_number=2)
page2.is_double_spread = False
page3 = Page(layout=PageLayout(width=210, height=297), page_number=3)
page3.is_double_spread = False
window.project.pages = [page1, page2, page3]
# Set up renderers so page 2 is most visible (see calculation above)
# Page 2 center should be closest to viewport center at y=300
renderer1 = Mock()
renderer1.screen_y = 50
renderer2 = Mock()
renderer2.screen_y = -300 # This will put page 2 center near viewport center
renderer3 = Mock()
renderer3.screen_y = 800
window.gl_widget._page_renderers = [
(renderer1, page1),
(renderer2, page2),
(renderer3, page3)
]
window.toggle_double_spread()
# Only page 2 should be toggled
assert page1.is_double_spread is False
assert page2.is_double_spread is True # Toggled
assert page3.is_double_spread is False
assert window._update_view_called
def test_toggle_spread_invalid_index_uses_zero(self, qtbot):
"""Test uses index 0 when calculated index is invalid"""
window = TestPageOpsWindow()
qtbot.addWidget(window)
page = Page(layout=PageLayout(width=210, height=297), page_number=1)
page.is_double_spread = False
window.project.pages = [page]
# Mock _get_most_visible_page_index to return invalid index
window._get_most_visible_page_index = Mock(return_value=999)
window.toggle_double_spread()
# Should fallback to first page (index 0)
assert page.is_double_spread is True
assert window._update_view_called
def test_toggle_spread_calculates_base_width(self, qtbot):
"""Test correctly calculates base_width from facing page"""
window = TestPageOpsWindow()
qtbot.addWidget(window)
# Create page with is_facing_page=True (which doubles the width automatically)
# PageLayout(width=210, is_facing_page=True) creates size=(420, 297) and base_width=210
page = Page(layout=PageLayout(width=210, height=297, is_facing_page=True), page_number=1)
page.is_double_spread = False # Not marked as double spread yet
window.project.pages = [page]
mock_renderer = Mock()
mock_renderer.screen_y = 100
window.gl_widget._page_renderers = [(mock_renderer, page)]
# Now toggle it on
window.toggle_double_spread()
# Should enable double spread
assert page.is_double_spread is True
# base_width should remain 210 (was already set correctly)
assert page.layout.base_width == 210
# Width should still be doubled
assert page.layout.size[0] == 420 # base_width * 2
assert page.layout.is_facing_page is True
class TestAddPage:
"""Test add_page method"""
def test_add_page_to_empty_project(self, qtbot):
"""Test adds first page to empty project"""
window = TestPageOpsWindow()
qtbot.addWidget(window)
window.project.pages = []
window.add_page()
assert len(window.project.pages) == 1
assert window.project.pages[0].page_number == 1
assert window.project.pages[0].layout.size == (210, 297)
assert window.project.pages[0].manually_sized is False
assert window._update_view_called
def test_add_page_to_existing_pages(self, qtbot):
"""Test adds page after the current page"""
window = TestPageOpsWindow()
qtbot.addWidget(window)
page1 = Page(layout=PageLayout(width=210, height=297), page_number=1)
window.project.pages = [page1]
# Mock _get_most_visible_page_index to return page 1 (index 0)
mock_renderer = Mock()
mock_renderer.screen_y = 100
window.gl_widget._page_renderers = [(mock_renderer, page1)]
window.add_page()
assert len(window.project.pages) == 2
# New page should be inserted after page 1
assert window.project.pages[0].page_number == 1
assert window.project.pages[1].page_number == 2
assert window._update_view_called
def test_add_page_inserts_after_current_page(self, qtbot):
"""Test adds page after the currently visible page, not at the end"""
window = TestPageOpsWindow()
qtbot.addWidget(window)
# Create three pages
page1 = Page(layout=PageLayout(width=210, height=297), page_number=1)
page2 = Page(layout=PageLayout(width=210, height=297), page_number=2)
page3 = Page(layout=PageLayout(width=210, height=297), page_number=3)
window.project.pages = [page1, page2, page3]
# Mock _get_most_visible_page_index to return page 2 (index 1)
window.gl_widget.height = Mock(return_value=600)
renderer1 = Mock()
renderer1.screen_y = 50
renderer2 = Mock()
renderer2.screen_y = -300 # Page 2 is most visible
renderer3 = Mock()
renderer3.screen_y = 800
window.gl_widget._page_renderers = [
(renderer1, page1),
(renderer2, page2),
(renderer3, page3)
]
window.add_page()
assert len(window.project.pages) == 4
# Verify pages are in correct order (physical order in list)
# After inserting after page2 (index 1), the new page is at index 2
assert window.project.pages[0] == page1
assert window.project.pages[1] == page2
# window.project.pages[2] is the new page
assert window.project.pages[3] == page3
# Page numbers should be renumbered sequentially
assert window.project.pages[0].page_number == 1
assert window.project.pages[1].page_number == 2
assert window.project.pages[2].page_number == 3 # New page
assert window.project.pages[3].page_number == 4 # Old page 3, renumbered
assert window._update_view_called
def test_add_page_with_double_spreads(self, qtbot):
"""Test page numbering with double spreads"""
window = TestPageOpsWindow()
qtbot.addWidget(window)
# Create pages: single, double spread, single
page1 = Page(layout=PageLayout(width=210, height=297), page_number=1)
page1.is_double_spread = False
page2 = Page(layout=PageLayout(width=420, height=297), page_number=2)
page2.is_double_spread = True
page2.layout.is_facing_page = True
page3 = Page(layout=PageLayout(width=210, height=297), page_number=4)
page3.is_double_spread = False
window.project.pages = [page1, page2, page3]
# Mock renderers - page 2 is most visible
window.gl_widget.height = Mock(return_value=600)
renderer1 = Mock()
renderer1.screen_y = 800
renderer2 = Mock()
renderer2.screen_y = -300 # Page 2 (double spread) is most visible
renderer3 = Mock()
renderer3.screen_y = 1500
window.gl_widget._page_renderers = [
(renderer1, page1),
(renderer2, page2),
(renderer3, page3)
]
window.add_page()
assert len(window.project.pages) == 4
# Page numbers should account for double spread:
# page1: 1 (single)
# page2: 2-3 (double spread, counts as 2 pages)
# new_page: 4 (single)
# page3: 5 (was 4, renumbered)
assert window.project.pages[0].page_number == 1
assert window.project.pages[1].page_number == 2 # Double spread starts at 2
assert window.project.pages[2].page_number == 4 # New page after double spread
assert window.project.pages[3].page_number == 5 # Old page3 renumbered
class TestRemovePage:
"""Test remove_page method"""
def test_remove_last_page(self, qtbot):
"""Test removes last page"""
window = TestPageOpsWindow()
qtbot.addWidget(window)
page1 = Page(layout=PageLayout(width=210, height=297), page_number=1)
page2 = Page(layout=PageLayout(width=210, height=297), page_number=2)
window.project.pages = [page1, page2]
window.remove_page()
assert len(window.project.pages) == 1
assert window.project.pages[0].page_number == 1
assert window._update_view_called
def test_cannot_remove_only_page(self, qtbot):
"""Test cannot remove when only one page exists"""
window = TestPageOpsWindow()
qtbot.addWidget(window)
page1 = Page(layout=PageLayout(width=210, height=297), page_number=1)
window.project.pages = [page1]
window.remove_page()
# Should still have one page
assert len(window.project.pages) == 1
assert not window._update_view_called
def test_remove_page_renumbers_remaining(self, qtbot):
"""Test remaining pages are renumbered after removal"""
window = TestPageOpsWindow()
qtbot.addWidget(window)
page1 = Page(layout=PageLayout(width=210, height=297), page_number=1)
page2 = Page(layout=PageLayout(width=210, height=297), page_number=2)
page3 = Page(layout=PageLayout(width=210, height=297), page_number=3)
window.project.pages = [page1, page2, page3]
# Mock renderers to make page3 the most visible (so it gets removed)
window.gl_widget.height = Mock(return_value=600)
renderer1 = Mock()
renderer1.screen_y = 800
renderer2 = Mock()
renderer2.screen_y = 600
renderer3 = Mock()
renderer3.screen_y = -300 # Page 3 is most visible
window.gl_widget._page_renderers = [
(renderer1, page1),
(renderer2, page2),
(renderer3, page3)
]
window.remove_page()
assert len(window.project.pages) == 2
assert window.project.pages[0].page_number == 1
assert window.project.pages[1].page_number == 2