388 lines
12 KiB
Python
388 lines
12 KiB
Python
"""
|
|
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
|