278 lines
10 KiB
Python
278 lines
10 KiB
Python
"""
|
|
Tests for ImagePanMixin
|
|
"""
|
|
|
|
import pytest
|
|
from unittest.mock import Mock
|
|
from PyQt6.QtOpenGLWidgets import QOpenGLWidget
|
|
from pyPhotoAlbum.mixins.image_pan import ImagePanMixin
|
|
from pyPhotoAlbum.mixins.element_selection import ElementSelectionMixin
|
|
from pyPhotoAlbum.mixins.viewport import ViewportMixin
|
|
from pyPhotoAlbum.models import ImageData, PlaceholderData
|
|
|
|
|
|
# Create test widget combining necessary mixins
|
|
class TestImagePanWidget(ImagePanMixin, ElementSelectionMixin, ViewportMixin, QOpenGLWidget):
|
|
"""Test widget combining image pan, selection, and viewport mixins"""
|
|
def __init__(self):
|
|
super().__init__()
|
|
self.drag_start_pos = None
|
|
|
|
|
|
class TestImagePanInitialization:
|
|
"""Test ImagePanMixin initialization"""
|
|
|
|
def test_initialization_sets_defaults(self, qtbot):
|
|
"""Test that mixin initializes with correct defaults"""
|
|
widget = TestImagePanWidget()
|
|
qtbot.addWidget(widget)
|
|
|
|
assert widget.image_pan_mode is False
|
|
assert widget.image_pan_start_crop is None
|
|
|
|
def test_image_pan_mode_is_mutable(self, qtbot):
|
|
"""Test that image pan mode can be toggled"""
|
|
widget = TestImagePanWidget()
|
|
qtbot.addWidget(widget)
|
|
|
|
widget.image_pan_mode = True
|
|
assert widget.image_pan_mode is True
|
|
|
|
widget.image_pan_mode = False
|
|
assert widget.image_pan_mode is False
|
|
|
|
|
|
class TestHandleImagePanMove:
|
|
"""Test _handle_image_pan_move method"""
|
|
|
|
def test_pan_right_shifts_crop_left(self, qtbot):
|
|
"""Test panning mouse right shifts crop window left (shows more of right side)"""
|
|
widget = TestImagePanWidget()
|
|
qtbot.addWidget(widget)
|
|
|
|
elem = ImageData(image_path="test.jpg", x=100, y=100, width=200, height=150)
|
|
elem.crop_info = (0.2, 0.2, 0.8, 0.8) # 60% view in center
|
|
|
|
widget.selected_element = elem
|
|
widget.image_pan_mode = True
|
|
widget.image_pan_start_crop = (0.2, 0.2, 0.8, 0.8)
|
|
widget.drag_start_pos = (100, 100)
|
|
widget.zoom_level = 1.0
|
|
|
|
# Pan mouse 50 pixels right
|
|
widget._handle_image_pan_move(150, 100, elem)
|
|
|
|
# Crop should shift left (x_min increases)
|
|
# crop_dx = -50 / (200 * 1.0) = -0.25
|
|
# new_x_min = 0.2 + (-0.25) = -0.05 -> clamped to 0.0
|
|
# new_x_max = 0.0 + 0.6 = 0.6
|
|
assert elem.crop_info[0] == 0.0 # Left edge
|
|
assert abs(elem.crop_info[2] - 0.6) < 0.001 # Right edge (floating point tolerance)
|
|
|
|
def test_pan_down_shifts_crop_up(self, qtbot):
|
|
"""Test panning mouse down shifts crop window up"""
|
|
widget = TestImagePanWidget()
|
|
qtbot.addWidget(widget)
|
|
|
|
elem = ImageData(image_path="test.jpg", x=100, y=100, width=200, height=150)
|
|
elem.crop_info = (0.2, 0.2, 0.8, 0.8)
|
|
|
|
widget.selected_element = elem
|
|
widget.image_pan_mode = True
|
|
widget.image_pan_start_crop = (0.2, 0.2, 0.8, 0.8)
|
|
widget.drag_start_pos = (100, 100)
|
|
widget.zoom_level = 1.0
|
|
|
|
# Pan mouse 30 pixels down
|
|
widget._handle_image_pan_move(100, 130, elem)
|
|
|
|
# crop_dy = -30 / (150 * 1.0) = -0.2
|
|
# new_y_min = 0.2 + (-0.2) = 0.0
|
|
# new_y_max = 0.0 + 0.6 = 0.6
|
|
assert elem.crop_info[1] == 0.0 # Top edge
|
|
assert abs(elem.crop_info[3] - 0.6) < 0.001 # Bottom edge (floating point tolerance)
|
|
|
|
def test_pan_clamps_to_image_boundaries(self, qtbot):
|
|
"""Test panning is clamped to 0-1 range"""
|
|
widget = TestImagePanWidget()
|
|
qtbot.addWidget(widget)
|
|
|
|
elem = ImageData(image_path="test.jpg", x=100, y=100, width=200, height=150)
|
|
elem.crop_info = (0.1, 0.1, 0.6, 0.6)
|
|
|
|
widget.selected_element = elem
|
|
widget.image_pan_mode = True
|
|
widget.image_pan_start_crop = (0.1, 0.1, 0.6, 0.6)
|
|
widget.drag_start_pos = (100, 100)
|
|
widget.zoom_level = 1.0
|
|
|
|
# Try to pan way past boundaries
|
|
widget._handle_image_pan_move(500, 500, elem)
|
|
|
|
# Crop should be clamped to valid 0-1 range
|
|
assert 0.0 <= elem.crop_info[0] <= 1.0
|
|
assert 0.0 <= elem.crop_info[1] <= 1.0
|
|
assert 0.0 <= elem.crop_info[2] <= 1.0
|
|
assert 0.0 <= elem.crop_info[3] <= 1.0
|
|
|
|
# And crop window dimensions should be preserved
|
|
crop_width = elem.crop_info[2] - elem.crop_info[0]
|
|
crop_height = elem.crop_info[3] - elem.crop_info[1]
|
|
assert abs(crop_width - 0.5) < 0.001
|
|
assert abs(crop_height - 0.5) < 0.001
|
|
|
|
def test_pan_respects_zoom_level(self, qtbot):
|
|
"""Test panning calculation respects zoom level"""
|
|
widget = TestImagePanWidget()
|
|
qtbot.addWidget(widget)
|
|
|
|
elem = ImageData(image_path="test.jpg", x=100, y=100, width=200, height=150)
|
|
elem.crop_info = (0.2, 0.2, 0.8, 0.8)
|
|
|
|
widget.selected_element = elem
|
|
widget.image_pan_mode = True
|
|
widget.image_pan_start_crop = (0.2, 0.2, 0.8, 0.8)
|
|
widget.drag_start_pos = (100, 100)
|
|
widget.zoom_level = 2.0 # Zoomed in 2x
|
|
|
|
# Pan 100 pixels right at 2x zoom
|
|
widget._handle_image_pan_move(200, 100, elem)
|
|
|
|
# crop_dx = -100 / (200 * 2.0) = -0.25
|
|
# new_x_min = 0.2 + (-0.25) = -0.05 -> clamped to 0.0
|
|
assert elem.crop_info[0] == 0.0
|
|
|
|
def test_pan_no_op_when_not_in_pan_mode(self, qtbot):
|
|
"""Test panning does nothing when not in pan mode"""
|
|
widget = TestImagePanWidget()
|
|
qtbot.addWidget(widget)
|
|
|
|
elem = ImageData(image_path="test.jpg", x=100, y=100, width=200, height=150)
|
|
original_crop = (0.2, 0.2, 0.8, 0.8)
|
|
elem.crop_info = original_crop
|
|
|
|
widget.selected_element = elem
|
|
widget.image_pan_mode = False # Not in pan mode
|
|
widget.drag_start_pos = (100, 100)
|
|
widget.zoom_level = 1.0
|
|
|
|
widget._handle_image_pan_move(200, 200, elem)
|
|
|
|
# Crop should be unchanged
|
|
assert elem.crop_info == original_crop
|
|
|
|
def test_pan_no_op_on_non_image_element(self, qtbot):
|
|
"""Test panning does nothing on non-ImageData elements"""
|
|
widget = TestImagePanWidget()
|
|
qtbot.addWidget(widget)
|
|
|
|
elem = PlaceholderData(x=100, y=100, width=200, height=150)
|
|
|
|
widget.image_pan_mode = True
|
|
widget.drag_start_pos = (100, 100)
|
|
widget.zoom_level = 1.0
|
|
|
|
# Should not crash, just do nothing
|
|
widget._handle_image_pan_move(200, 200, elem)
|
|
|
|
def test_pan_no_op_without_drag_start(self, qtbot):
|
|
"""Test panning does nothing without drag start position"""
|
|
widget = TestImagePanWidget()
|
|
qtbot.addWidget(widget)
|
|
|
|
elem = ImageData(image_path="test.jpg", x=100, y=100, width=200, height=150)
|
|
original_crop = (0.2, 0.2, 0.8, 0.8)
|
|
elem.crop_info = original_crop
|
|
|
|
widget.selected_element = elem
|
|
widget.image_pan_mode = True
|
|
widget.drag_start_pos = None # No drag start
|
|
widget.zoom_level = 1.0
|
|
|
|
widget._handle_image_pan_move(200, 200, elem)
|
|
|
|
# Crop should be unchanged
|
|
assert elem.crop_info == original_crop
|
|
|
|
def test_pan_uses_default_crop_when_none(self, qtbot):
|
|
"""Test panning uses (0,0,1,1) when start crop is None"""
|
|
widget = TestImagePanWidget()
|
|
qtbot.addWidget(widget)
|
|
|
|
elem = ImageData(image_path="test.jpg", x=100, y=100, width=200, height=150)
|
|
elem.crop_info = (0, 0, 1, 1)
|
|
|
|
widget.selected_element = elem
|
|
widget.image_pan_mode = True
|
|
widget.image_pan_start_crop = None # No start crop
|
|
widget.drag_start_pos = (100, 100)
|
|
widget.zoom_level = 1.0
|
|
|
|
# Pan 100 pixels right
|
|
widget._handle_image_pan_move(200, 100, elem)
|
|
|
|
# Should use full image as start (crop_width = 1.0)
|
|
# crop_dx = -100 / 200 = -0.5
|
|
# new_x_min = 0 + (-0.5) = -0.5 -> clamped to 0
|
|
# new_x_max = 0 + 1.0 = 1.0
|
|
assert elem.crop_info[0] == 0.0
|
|
assert elem.crop_info[2] == 1.0
|
|
|
|
def test_pan_maintains_crop_dimensions(self, qtbot):
|
|
"""Test panning maintains the crop window dimensions"""
|
|
widget = TestImagePanWidget()
|
|
qtbot.addWidget(widget)
|
|
|
|
elem = ImageData(image_path="test.jpg", x=100, y=100, width=200, height=150)
|
|
original_crop = (0.2, 0.3, 0.7, 0.8) # width=0.5, height=0.5
|
|
elem.crop_info = original_crop
|
|
|
|
widget.selected_element = elem
|
|
widget.image_pan_mode = True
|
|
widget.image_pan_start_crop = original_crop
|
|
widget.drag_start_pos = (100, 100)
|
|
widget.zoom_level = 1.0
|
|
|
|
# Pan 20 pixels right and 15 pixels down
|
|
widget._handle_image_pan_move(120, 115, elem)
|
|
|
|
# Crop dimensions should remain the same
|
|
new_crop = elem.crop_info
|
|
new_width = new_crop[2] - new_crop[0]
|
|
new_height = new_crop[3] - new_crop[1]
|
|
|
|
original_width = original_crop[2] - original_crop[0]
|
|
original_height = original_crop[3] - original_crop[1]
|
|
|
|
assert abs(new_width - original_width) < 0.001
|
|
assert abs(new_height - original_height) < 0.001
|
|
|
|
def test_pan_left_boundary_clamping(self, qtbot):
|
|
"""Test panning respects left boundary"""
|
|
widget = TestImagePanWidget()
|
|
qtbot.addWidget(widget)
|
|
|
|
elem = ImageData(image_path="test.jpg", x=100, y=100, width=200, height=150)
|
|
elem.crop_info = (0.5, 0.2, 1.0, 0.8) # Right half
|
|
|
|
widget.selected_element = elem
|
|
widget.image_pan_mode = True
|
|
widget.image_pan_start_crop = (0.5, 0.2, 1.0, 0.8)
|
|
widget.drag_start_pos = (100, 100)
|
|
widget.zoom_level = 1.0
|
|
|
|
# Try to pan left beyond boundary (pan mouse left = positive crop delta)
|
|
widget._handle_image_pan_move(50, 100, elem)
|
|
|
|
# crop_dx = -(-50) / 200 = 0.25
|
|
# new_x_min = 0.5 + 0.25 = 0.75
|
|
# But if we go further...
|
|
widget.drag_start_pos = (100, 100)
|
|
widget._handle_image_pan_move(0, 100, elem)
|
|
|
|
# crop_dx = -(-100) / 200 = 0.5
|
|
# new_x_min = 0.5 + 0.5 = 1.0
|
|
# new_x_max = 1.0 + 0.5 = 1.5 -> should clamp
|
|
assert elem.crop_info[2] == 1.0 # Right boundary
|
|
assert elem.crop_info[0] == 0.5 # 1.0 - crop_width
|