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