""" Tests for ViewportMixin """ import pytest from unittest.mock import Mock, MagicMock, patch from PyQt6.QtWidgets import QApplication from PyQt6.QtOpenGLWidgets import QOpenGLWidget from pyPhotoAlbum.mixins.viewport import ViewportMixin from pyPhotoAlbum.project import Project, Page from pyPhotoAlbum.page_layout import PageLayout # Create a minimal test widget class class TestViewportWidget(ViewportMixin, QOpenGLWidget): """Test widget combining ViewportMixin with QOpenGLWidget""" pass class TestViewportMixinInitialization: """Test ViewportMixin initialization""" def test_initialization_sets_defaults(self, qtbot): """Test that mixin initializes with correct defaults""" widget = TestViewportWidget() qtbot.addWidget(widget) assert widget.zoom_level == 1.0 assert widget.pan_offset == [0, 0] assert widget.initial_zoom_set is False def test_zoom_level_is_mutable(self, qtbot): """Test that zoom level can be changed""" widget = TestViewportWidget() qtbot.addWidget(widget) widget.zoom_level = 1.5 assert widget.zoom_level == 1.5 def test_pan_offset_is_mutable(self, qtbot): """Test that pan offset can be changed""" widget = TestViewportWidget() qtbot.addWidget(widget) widget.pan_offset = [100, 50] assert widget.pan_offset == [100, 50] class TestViewportCalculations: """Test viewport zoom calculations""" def test_calculate_fit_to_screen_no_project(self, qtbot): """Test fit-to-screen with no project returns 1.0""" widget = TestViewportWidget() qtbot.addWidget(widget) widget.resize(800, 600) # Mock window() to return a window without project mock_window = Mock() mock_window.project = None widget.window = Mock(return_value=mock_window) zoom = widget._calculate_fit_to_screen_zoom() assert zoom == 1.0 def test_calculate_fit_to_screen_empty_project(self, qtbot): """Test fit-to-screen with empty project returns 1.0""" widget = TestViewportWidget() qtbot.addWidget(widget) widget.resize(800, 600) # Mock window() to return a window with empty project mock_window = Mock() mock_window.project = Project(name="Empty") mock_window.project.pages = [] widget.window = Mock(return_value=mock_window) zoom = widget._calculate_fit_to_screen_zoom() assert zoom == 1.0 def test_calculate_fit_to_screen_with_page(self, qtbot): """Test fit-to-screen calculates correct zoom for A4 page""" widget = TestViewportWidget() qtbot.addWidget(widget) widget.resize(1000, 800) # Mock window with project and A4 page mock_window = Mock() mock_window.project = Project(name="Test") mock_window.project.working_dpi = 96 # A4 page: 210mm x 297mm page = Page( layout=PageLayout(width=210, height=297), page_number=1 ) mock_window.project.pages = [page] widget.window = Mock(return_value=mock_window) zoom = widget._calculate_fit_to_screen_zoom() # Calculate expected zoom # A4 at 96 DPI: width=794px, height=1123px # Window: 1000x800, margins: 100px each side # Available: 800x600 # zoom_w = 800/794 ≈ 1.007, zoom_h = 600/1123 ≈ 0.534 # Should use min(zoom_w, zoom_h, 1.0) = 0.534 assert 0.5 < zoom < 0.6 # Approximately 0.534 assert zoom <= 1.0 # Never zoom beyond 100% def test_calculate_fit_to_screen_small_window(self, qtbot): """Test fit-to-screen with small window returns small zoom""" widget = TestViewportWidget() qtbot.addWidget(widget) widget.resize(400, 300) # Small window mock_window = Mock() mock_window.project = Project(name="Test") mock_window.project.working_dpi = 96 page = Page( layout=PageLayout(width=210, height=297), page_number=1 ) mock_window.project.pages = [page] widget.window = Mock(return_value=mock_window) zoom = widget._calculate_fit_to_screen_zoom() # With 400x300 window and 200px margins, available space is 200x100 # This should produce a very small zoom assert zoom < 0.3 def test_calculate_fit_to_screen_large_window(self, qtbot): """Test fit-to-screen with large window caps at 1.0""" widget = TestViewportWidget() qtbot.addWidget(widget) widget.resize(3000, 2000) # Very large window mock_window = Mock() mock_window.project = Project(name="Test") mock_window.project.working_dpi = 96 page = Page( layout=PageLayout(width=210, height=297), page_number=1 ) mock_window.project.pages = [page] widget.window = Mock(return_value=mock_window) zoom = widget._calculate_fit_to_screen_zoom() # Even with huge window, zoom should not exceed 1.0 assert zoom == 1.0 def test_calculate_fit_to_screen_different_dpi(self, qtbot): """Test fit-to-screen respects different DPI values""" widget = TestViewportWidget() qtbot.addWidget(widget) widget.resize(1000, 800) mock_window = Mock() mock_window.project = Project(name="Test") mock_window.project.working_dpi = 300 # High DPI page = Page( layout=PageLayout(width=210, height=297), page_number=1 ) mock_window.project.pages = [page] widget.window = Mock(return_value=mock_window) zoom = widget._calculate_fit_to_screen_zoom() # At 300 DPI, page is much larger in pixels # So zoom should be smaller assert zoom < 0.3 class TestViewportCentering: """Test viewport centering calculations""" def test_calculate_center_pan_offset_no_project(self, qtbot): """Test center calculation with no project returns [0, 0]""" widget = TestViewportWidget() qtbot.addWidget(widget) widget.resize(800, 600) # Mock window() to return a window without project mock_window = Mock() mock_window.project = None widget.window = Mock(return_value=mock_window) offset = widget._calculate_center_pan_offset(1.0) assert offset == [0, 0] def test_calculate_center_pan_offset_empty_project(self, qtbot): """Test center calculation with empty project returns [0, 0]""" widget = TestViewportWidget() qtbot.addWidget(widget) widget.resize(800, 600) # Mock window() to return a window with empty project mock_window = Mock() mock_window.project = Project(name="Empty") mock_window.project.pages = [] widget.window = Mock(return_value=mock_window) offset = widget._calculate_center_pan_offset(1.0) assert offset == [0, 0] def test_calculate_center_pan_offset_with_page_at_100_percent(self, qtbot): """Test center calculation for A4 page at 100% zoom""" widget = TestViewportWidget() qtbot.addWidget(widget) widget.resize(1000, 800) # Mock window with project and A4 page mock_window = Mock() mock_window.project = Project(name="Test") mock_window.project.working_dpi = 96 # A4 page: 210mm x 297mm page = Page( layout=PageLayout(width=210, height=297), page_number=1 ) mock_window.project.pages = [page] widget.window = Mock(return_value=mock_window) zoom_level = 1.0 offset = widget._calculate_center_pan_offset(zoom_level) # Calculate expected offset # A4 at 96 DPI: width=794px, height=1123px # Window: 1000x800 # PAGE_MARGIN = 50 # x_offset = (1000 - 794) / 2 - 50 = 103 - 50 = 53 # y_offset = (800 - 1123) / 2 = -161.5 assert isinstance(offset, list) assert len(offset) == 2 # X offset should center horizontally (accounting for PAGE_MARGIN) assert 50 < offset[0] < 60 # Approximately 53 # Y offset should be negative (page taller than window) assert offset[1] < 0 def test_calculate_center_pan_offset_with_page_at_50_percent(self, qtbot): """Test center calculation for A4 page at 50% zoom""" widget = TestViewportWidget() qtbot.addWidget(widget) widget.resize(1000, 800) mock_window = Mock() mock_window.project = Project(name="Test") mock_window.project.working_dpi = 96 # A4 page: 210mm x 297mm page = Page( layout=PageLayout(width=210, height=297), page_number=1 ) mock_window.project.pages = [page] widget.window = Mock(return_value=mock_window) zoom_level = 0.5 offset = widget._calculate_center_pan_offset(zoom_level) # At 50% zoom, page dimensions are halved # A4 at 96 DPI and 50%: width=397px, height=561.5px # Window: 1000x800 # PAGE_MARGIN = 50 # x_offset = (1000 - 397) / 2 - 50 = 301.5 - 50 = 251.5 # y_offset = (800 - 561.5) / 2 = 119.25 assert isinstance(offset, list) assert len(offset) == 2 # X offset should be larger (more centering space) assert 240 < offset[0] < 260 # Approximately 251.5 # Y offset should be positive (page fits vertically) assert offset[1] > 100 def test_calculate_center_pan_offset_small_page(self, qtbot): """Test center calculation for small page (6x4 photo)""" widget = TestViewportWidget() qtbot.addWidget(widget) widget.resize(1000, 800) mock_window = Mock() mock_window.project = Project(name="Test") mock_window.project.working_dpi = 96 # 6x4 inch photo: 152.4mm x 101.6mm page = Page( layout=PageLayout(width=152.4, height=101.6), page_number=1 ) mock_window.project.pages = [page] widget.window = Mock(return_value=mock_window) zoom_level = 1.0 offset = widget._calculate_center_pan_offset(zoom_level) # Small page should have large positive offsets (lots of centering space) assert offset[0] > 150 # Horizontally centered with room to spare assert offset[1] > 200 # Vertically centered with room to spare def test_calculate_center_pan_offset_large_window(self, qtbot): """Test center calculation with large window""" widget = TestViewportWidget() qtbot.addWidget(widget) widget.resize(3000, 2000) mock_window = Mock() mock_window.project = Project(name="Test") mock_window.project.working_dpi = 96 # A4 page: 210mm x 297mm page = Page( layout=PageLayout(width=210, height=297), page_number=1 ) mock_window.project.pages = [page] widget.window = Mock(return_value=mock_window) zoom_level = 1.0 offset = widget._calculate_center_pan_offset(zoom_level) # Large window should have large positive offsets assert offset[0] > 1000 # Lots of horizontal space assert offset[1] > 400 # Lots of vertical space def test_calculate_center_pan_offset_different_zoom_levels(self, qtbot): """Test that different zoom levels produce different offsets""" widget = TestViewportWidget() qtbot.addWidget(widget) widget.resize(1000, 800) mock_window = Mock() mock_window.project = Project(name="Test") mock_window.project.working_dpi = 96 page = Page( layout=PageLayout(width=210, height=297), page_number=1 ) mock_window.project.pages = [page] widget.window = Mock(return_value=mock_window) # Get offsets at different zoom levels offset_100 = widget._calculate_center_pan_offset(1.0) offset_50 = widget._calculate_center_pan_offset(0.5) offset_25 = widget._calculate_center_pan_offset(0.25) # As zoom decreases, both offsets should increase (more centering space) assert offset_50[0] > offset_100[0] assert offset_25[0] > offset_50[0] # Y offset behavior depends on if page fits vertically # At lower zoom, page is smaller, so more vertical centering space assert offset_25[1] > offset_50[1] def test_calculate_center_pan_offset_different_dpi(self, qtbot): """Test center calculation with different DPI values""" widget = TestViewportWidget() qtbot.addWidget(widget) widget.resize(1000, 800) mock_window = Mock() mock_window.project = Project(name="Test") page = Page( layout=PageLayout(width=210, height=297), page_number=1 ) mock_window.project.pages = [page] # Test at 96 DPI mock_window.project.working_dpi = 96 widget.window = Mock(return_value=mock_window) offset_96 = widget._calculate_center_pan_offset(1.0) # Test at 300 DPI (page will be much larger in pixels) mock_window.project.working_dpi = 300 offset_300 = widget._calculate_center_pan_offset(1.0) # At higher DPI, page is larger, so centering offsets should be smaller # (or negative if page doesn't fit) assert offset_300[0] < offset_96[0] assert offset_300[1] < offset_96[1] class TestViewportResizing: """Test viewport behavior during window resizing""" def test_resizeGL_recenters_when_project_loaded(self, qtbot): """Test that resizing window recenters the page""" widget = TestViewportWidget() qtbot.addWidget(widget) # Mock window with project mock_window = Mock() mock_window.project = Project(name="Test") mock_window.project.working_dpi = 96 page = Page( layout=PageLayout(width=210, height=297), page_number=1 ) mock_window.project.pages = [page] widget.window = Mock(return_value=mock_window) # Simulate initial load (sets initial_zoom_set to True) widget.initial_zoom_set = True widget.zoom_level = 0.5 # Get initial centered offset for 1000x800 with patch.object(widget, 'width', return_value=1000): with patch.object(widget, 'height', return_value=800): initial_offset = list(widget._calculate_center_pan_offset(0.5)) # Trigger a resize to larger window (1200x900) # Mock the widget's dimensions during resizeGL with patch.object(widget, 'width', return_value=1200): with patch.object(widget, 'height', return_value=900): widget.resizeGL(1200, 900) new_offset = widget.pan_offset # Offsets should be larger due to increased window size assert new_offset[0] > initial_offset[0] # More horizontal space assert new_offset[1] > initial_offset[1] # More vertical space def test_resizeGL_does_not_recenter_before_project_load(self, qtbot): """Test that resizing before project load doesn't change pan offset""" widget = TestViewportWidget() qtbot.addWidget(widget) widget.resize(800, 600) # Don't set initial_zoom_set (simulates no project loaded yet) widget.initial_zoom_set = False widget.pan_offset = [100, 50] # Trigger a resize widget.resizeGL(1000, 800) # Pan offset should remain unchanged (no project to center) assert widget.pan_offset == [100, 50] def test_resizeGL_maintains_zoom_level(self, qtbot): """Test that resizing maintains the current zoom level""" widget = TestViewportWidget() qtbot.addWidget(widget) widget.resize(1000, 800) # Mock window with project mock_window = Mock() mock_window.project = Project(name="Test") mock_window.project.working_dpi = 96 page = Page( layout=PageLayout(width=210, height=297), page_number=1 ) mock_window.project.pages = [page] widget.window = Mock(return_value=mock_window) # Set initial state widget.initial_zoom_set = True widget.zoom_level = 0.75 # Trigger a resize widget.resizeGL(1200, 900) # Zoom level should remain the same assert widget.zoom_level == 0.75 def test_resizeGL_with_different_sizes(self, qtbot): """Test that resizing to different sizes produces appropriate centering""" widget = TestViewportWidget() qtbot.addWidget(widget) # Mock window with project mock_window = Mock() mock_window.project = Project(name="Test") mock_window.project.working_dpi = 96 page = Page( layout=PageLayout(width=210, height=297), page_number=1 ) mock_window.project.pages = [page] widget.window = Mock(return_value=mock_window) widget.initial_zoom_set = True widget.zoom_level = 0.5 # Resize to small window widget.resize(600, 400) widget.resizeGL(600, 400) small_offset = widget.pan_offset.copy() # Resize to large window widget.resize(2000, 1500) widget.resizeGL(2000, 1500) large_offset = widget.pan_offset.copy() # Larger window should have larger centering offsets assert large_offset[0] > small_offset[0] assert large_offset[1] > small_offset[1] class TestViewportOpenGL: """Test OpenGL-related viewport methods""" def test_initializeGL_sets_clear_color(self, qtbot): """Test that initializeGL is callable (actual GL testing is integration)""" widget = TestViewportWidget() qtbot.addWidget(widget) # Just verify the method exists and is callable assert hasattr(widget, 'initializeGL') assert callable(widget.initializeGL) def test_resizeGL_is_callable(self, qtbot): """Test that resizeGL is callable""" widget = TestViewportWidget() qtbot.addWidget(widget) assert hasattr(widget, 'resizeGL') assert callable(widget.resizeGL)