472 lines
17 KiB
Python
Executable File
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
|