""" Integration tests for GLWidget - verifying mixin composition """ import pytest from unittest.mock import Mock, patch from PyQt6.QtCore import Qt, QPointF from PyQt6.QtGui import QMouseEvent from pyPhotoAlbum.gl_widget import GLWidget from pyPhotoAlbum.project import Project, Page from pyPhotoAlbum.page_layout import PageLayout from pyPhotoAlbum.models import ImageData, TextBoxData class TestGLWidgetInitialization: """Test GLWidget initialization and mixin integration""" def test_gl_widget_initializes(self, qtbot): """Test GLWidget can be instantiated with all mixins""" widget = GLWidget() qtbot.addWidget(widget) # Verify mixin state is initialized assert hasattr(widget, 'zoom_level') assert hasattr(widget, 'pan_offset') assert hasattr(widget, 'selected_elements') assert hasattr(widget, 'drag_start_pos') assert hasattr(widget, 'is_dragging') assert hasattr(widget, 'is_panning') assert hasattr(widget, 'rotation_mode') def test_gl_widget_accepts_drops(self, qtbot): """Test GLWidget is configured to accept drops""" widget = GLWidget() qtbot.addWidget(widget) assert widget.acceptDrops() is True def test_gl_widget_tracks_mouse(self, qtbot): """Test GLWidget has mouse tracking enabled""" widget = GLWidget() qtbot.addWidget(widget) assert widget.hasMouseTracking() is True class TestGLWidgetMixinIntegration: """Test that mixins work together correctly""" def test_viewport_and_rendering_integration(self, qtbot): """Test viewport state affects rendering""" widget = GLWidget() qtbot.addWidget(widget) # Set zoom level initial_zoom = widget.zoom_level widget.zoom_level = 2.0 assert widget.zoom_level == 2.0 assert widget.zoom_level != initial_zoom # Pan offset initial_pan = widget.pan_offset.copy() widget.pan_offset[0] += 100 widget.pan_offset[1] += 50 assert widget.pan_offset != initial_pan def test_selection_and_manipulation_integration(self, qtbot): """Test element selection works with manipulation""" widget = GLWidget() qtbot.addWidget(widget) # Create an element element = ImageData(image_path="/test.jpg", x=100, y=100, width=200, height=150) # Select it widget.selected_elements.add(element) # Verify selection assert element in widget.selected_elements assert widget.selected_element == element # Clear selection widget.selected_elements.clear() assert len(widget.selected_elements) == 0 assert widget.selected_element is None def test_mouse_interaction_with_selection(self, qtbot): """Test mouse events trigger selection changes""" widget = GLWidget() qtbot.addWidget(widget) widget.update = Mock() # Mock element at position test_element = ImageData(image_path="/test.jpg", x=50, y=50, width=100, height=100) widget._get_element_at = Mock(return_value=test_element) widget._get_page_at = Mock(return_value=(None, -1, None)) widget._check_ghost_page_click = Mock(return_value=False) # Create mouse press event event = QMouseEvent( QMouseEvent.Type.MouseButtonPress, QPointF(75, 75), Qt.MouseButton.LeftButton, Qt.MouseButton.LeftButton, Qt.KeyboardModifier.NoModifier ) widget.mousePressEvent(event) # Should select the element assert test_element in widget.selected_elements def test_undo_integration_with_operations(self, qtbot): """Test undo/redo integration with element operations""" widget = GLWidget() qtbot.addWidget(widget) # Create element element = ImageData(image_path="/test.jpg", x=100, y=100, width=200, height=150) widget.selected_elements.add(element) # Begin operation (should be tracked for undo) widget._begin_move(element) assert widget._interaction_element is not None assert widget._interaction_type == 'move' assert widget._interaction_start_pos == (100, 100) # End operation widget._end_interaction() # Interaction state should be cleared after operation assert widget._interaction_element is None assert widget._interaction_type is None class TestGLWidgetKeyEvents: """Test keyboard event handling""" def test_escape_clears_selection(self, qtbot): """Test Escape key clears selection and rotation mode""" widget = GLWidget() qtbot.addWidget(widget) widget.update = Mock() # Set up selection and rotation mode element = ImageData(image_path="/test.jpg", x=100, y=100, width=200, height=150) widget.selected_elements.add(element) widget.rotation_mode = True # Create key press event for Escape from PyQt6.QtGui import QKeyEvent event = QKeyEvent( QKeyEvent.Type.KeyPress, Qt.Key.Key_Escape, Qt.KeyboardModifier.NoModifier ) widget.keyPressEvent(event) # Should clear selection and rotation mode assert widget.selected_element is None assert widget.rotation_mode is False assert widget.update.called def test_tab_toggles_rotation_mode(self, qtbot): """Test Tab key toggles rotation mode when element is selected""" widget = GLWidget() qtbot.addWidget(widget) widget.update = Mock() # Set up mock window for status message mock_window = Mock() mock_window.show_status = Mock() widget.window = Mock(return_value=mock_window) # Select an element element = ImageData(image_path="/test.jpg", x=100, y=100, width=200, height=150) widget.selected_elements.add(element) # Initially not in rotation mode assert widget.rotation_mode is False # Create key press event for Tab from PyQt6.QtGui import QKeyEvent event = QKeyEvent( QKeyEvent.Type.KeyPress, Qt.Key.Key_Tab, Qt.KeyboardModifier.NoModifier ) widget.keyPressEvent(event) # Should toggle rotation mode assert widget.rotation_mode is True assert widget.update.called # Press Tab again widget.keyPressEvent(event) # Should toggle back assert widget.rotation_mode is False def test_delete_key_requires_main_window(self, qtbot): """Test Delete key calls main window's delete method""" widget = GLWidget() qtbot.addWidget(widget) # Set up mock window mock_window = Mock() mock_window.delete_selected_element = Mock() widget.window = Mock(return_value=mock_window) # Select an element element = ImageData(image_path="/test.jpg", x=100, y=100, width=200, height=150) widget.selected_elements.add(element) # Create key press event for Delete from PyQt6.QtGui import QKeyEvent event = QKeyEvent( QKeyEvent.Type.KeyPress, Qt.Key.Key_Delete, Qt.KeyboardModifier.NoModifier ) widget.keyPressEvent(event) # Should call main window's delete method assert mock_window.delete_selected_element.called class TestGLWidgetWithProject: """Test GLWidget with a full project setup""" def test_gl_widget_with_project(self, qtbot): """Test GLWidget can work with a project and pages""" widget = GLWidget() qtbot.addWidget(widget) # Create a mock main window with project mock_window = Mock() mock_window.project = Project(name="Test Project") mock_window.project.working_dpi = 96 # Add a page page = Page(layout=PageLayout(width=210, height=297), page_number=1) mock_window.project.pages.append(page) # Add an element to the page element = ImageData(image_path="/test.jpg", x=100, y=100, width=200, height=150) page.layout.add_element(element) widget.window = Mock(return_value=mock_window) # Verify we can access project through widget main_window = widget.window() assert hasattr(main_window, 'project') assert main_window.project.name == "Test Project" assert len(main_window.project.pages) == 1 assert len(main_window.project.pages[0].layout.elements) == 1 def test_fit_to_screen_zoom_calculation(self, qtbot): """Test fit-to-screen zoom calculation with project""" widget = GLWidget() qtbot.addWidget(widget) # Create 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.append(page) widget.window = Mock(return_value=mock_window) # Mock widget dimensions widget.width = Mock(return_value=800) widget.height = Mock(return_value=600) # Calculate fit-to-screen zoom zoom = widget._calculate_fit_to_screen_zoom() # Should return a valid zoom level assert isinstance(zoom, float) assert zoom > 0 def test_gl_widget_without_project(self, qtbot): """Test GLWidget handles missing project gracefully""" widget = GLWidget() qtbot.addWidget(widget) # Create mock window without project mock_window = Mock() del mock_window.project widget.window = Mock(return_value=mock_window) # Should not crash when calculating zoom zoom = widget._calculate_fit_to_screen_zoom() assert zoom == 1.0 class TestGLWidgetOpenGL: """Test OpenGL-specific functionality""" def test_gl_widget_has_opengl_format(self, qtbot): """Test GLWidget has OpenGL format configured""" widget = GLWidget() qtbot.addWidget(widget) # Should have a format format = widget.format() assert format is not None def test_gl_widget_update_behavior(self, qtbot): """Test GLWidget update behavior is configured""" widget = GLWidget() qtbot.addWidget(widget) # Should have NoPartialUpdate set from PyQt6.QtOpenGLWidgets import QOpenGLWidget assert widget.updateBehavior() == QOpenGLWidget.UpdateBehavior.NoPartialUpdate class TestGLWidgetStateManagement: """Test state management across mixins""" def test_rotation_mode_state(self, qtbot): """Test rotation mode state is properly managed""" widget = GLWidget() qtbot.addWidget(widget) # Initial state assert widget.rotation_mode is False # Toggle rotation mode widget.rotation_mode = True assert widget.rotation_mode is True # Toggle back widget.rotation_mode = False assert widget.rotation_mode is False def test_drag_state_management(self, qtbot): """Test drag state is properly managed""" widget = GLWidget() qtbot.addWidget(widget) # Initial state assert widget.is_dragging is False assert widget.drag_start_pos is None # Start drag widget.is_dragging = True widget.drag_start_pos = (100, 100) assert widget.is_dragging is True assert widget.drag_start_pos == (100, 100) # End drag widget.is_dragging = False widget.drag_start_pos = None assert widget.is_dragging is False assert widget.drag_start_pos is None def test_pan_state_management(self, qtbot): """Test pan state is properly managed""" widget = GLWidget() qtbot.addWidget(widget) # Initial state assert widget.is_panning is False # Start panning widget.is_panning = True widget.drag_start_pos = (200, 200) assert widget.is_panning is True # End panning widget.is_panning = False widget.drag_start_pos = None assert widget.is_panning is False