All checks were successful
Python CI / test (push) Successful in 1m20s
Lint / lint (push) Successful in 1m4s
Tests / test (3.11) (push) Successful in 1m27s
Tests / test (3.12) (push) Successful in 2m25s
Tests / test (3.13) (push) Successful in 2m52s
Tests / test (3.14) (push) Successful in 1m9s
190 lines
7.5 KiB
Python
Executable File
190 lines
7.5 KiB
Python
Executable File
"""
|
|
Unit tests for photo rotation serialization/deserialization
|
|
Tests that rotated photos render correctly after reload
|
|
"""
|
|
|
|
import pytest
|
|
import tempfile
|
|
import json
|
|
from pathlib import Path
|
|
from unittest.mock import MagicMock, patch
|
|
from PIL import Image
|
|
from pyPhotoAlbum.models import ImageData
|
|
|
|
|
|
class TestRotationSerialization:
|
|
"""Tests for rotation serialization and deserialization"""
|
|
|
|
@pytest.fixture
|
|
def sample_image(self):
|
|
"""Create a sample test image"""
|
|
# Create a 400x200 test image (wider than tall)
|
|
img = Image.new("RGBA", (400, 200), color=(255, 0, 0, 255))
|
|
return img
|
|
|
|
def test_serialize_rotation_metadata(self):
|
|
"""Test that rotation metadata is serialized correctly"""
|
|
img_data = ImageData(image_path="test.jpg", x=10, y=20, width=100, height=50)
|
|
img_data.pil_rotation_90 = 1 # 90 degree rotation
|
|
img_data.image_dimensions = (400, 200) # Original dimensions
|
|
|
|
# Serialize
|
|
data = img_data.serialize()
|
|
|
|
# Verify rotation is saved
|
|
assert data["pil_rotation_90"] == 1
|
|
assert data["image_dimensions"] == (400, 200)
|
|
assert data["rotation"] == 0 # Visual rotation should be 0
|
|
|
|
def test_deserialize_rotation_metadata(self):
|
|
"""Test that rotation metadata is deserialized correctly"""
|
|
data = {
|
|
"type": "image",
|
|
"position": (10, 20),
|
|
"size": (100, 50),
|
|
"rotation": 0,
|
|
"z_index": 0,
|
|
"image_path": "test.jpg",
|
|
"crop_info": (0, 0, 1, 1),
|
|
"pil_rotation_90": 1,
|
|
"image_dimensions": (400, 200),
|
|
}
|
|
|
|
img_data = ImageData()
|
|
img_data.deserialize(data)
|
|
|
|
# Verify rotation is loaded
|
|
assert img_data.pil_rotation_90 == 1
|
|
assert img_data.image_dimensions == (400, 200)
|
|
assert img_data.rotation == 0
|
|
|
|
def test_image_dimensions_updated_after_rotation(self, sample_image):
|
|
"""Test that image_dimensions are updated after rotation is applied"""
|
|
# Create ImageData with original dimensions
|
|
img_data = ImageData(image_path="test.jpg", x=10, y=20, width=100, height=50)
|
|
img_data.pil_rotation_90 = 1 # 90 degree rotation
|
|
img_data.image_dimensions = (400, 200) # Original dimensions (width=400, height=200)
|
|
|
|
# Simulate async image loading with rotation
|
|
# Pass the UNROTATED image - _on_async_image_loaded will apply rotation
|
|
# After 90° rotation, a 400x200 image becomes 200x400
|
|
img_data._on_async_image_loaded(sample_image)
|
|
|
|
# Verify dimensions are updated to rotated dimensions
|
|
assert img_data.image_dimensions == (
|
|
200,
|
|
400,
|
|
), f"Expected rotated dimensions (200, 400), got {img_data.image_dimensions}"
|
|
assert img_data._img_width == 200
|
|
assert img_data._img_height == 400
|
|
|
|
def test_image_dimensions_updated_after_180_rotation(self, sample_image):
|
|
"""Test that image_dimensions are updated after 180° rotation"""
|
|
img_data = ImageData(image_path="test.jpg", x=10, y=20, width=100, height=50)
|
|
img_data.pil_rotation_90 = 2 # 180 degree rotation
|
|
img_data.image_dimensions = (400, 200) # Original dimensions
|
|
|
|
# Pass the UNROTATED image - _on_async_image_loaded will apply rotation
|
|
# After 180° rotation, dimensions should stay the same
|
|
img_data._on_async_image_loaded(sample_image)
|
|
|
|
# Verify dimensions are updated (same as original for 180°)
|
|
assert img_data.image_dimensions == (400, 200)
|
|
assert img_data._img_width == 400
|
|
assert img_data._img_height == 200
|
|
|
|
def test_image_dimensions_updated_after_270_rotation(self, sample_image):
|
|
"""Test that image_dimensions are updated after 270° rotation"""
|
|
img_data = ImageData(image_path="test.jpg", x=10, y=20, width=100, height=50)
|
|
img_data.pil_rotation_90 = 3 # 270 degree rotation
|
|
img_data.image_dimensions = (400, 200) # Original dimensions
|
|
|
|
# Pass the UNROTATED image - _on_async_image_loaded will apply rotation
|
|
# After 270° rotation, a 400x200 image becomes 200x400
|
|
img_data._on_async_image_loaded(sample_image)
|
|
|
|
# Verify dimensions are updated to rotated dimensions
|
|
assert img_data.image_dimensions == (200, 400)
|
|
assert img_data._img_width == 200
|
|
assert img_data._img_height == 400
|
|
|
|
def test_serialize_deserialize_roundtrip_with_rotation(self):
|
|
"""Test that rotation survives serialize/deserialize roundtrip"""
|
|
# Create ImageData with rotation
|
|
original = ImageData(image_path="test.jpg", x=10, y=20, width=100, height=50)
|
|
original.pil_rotation_90 = 1
|
|
original.image_dimensions = (400, 200)
|
|
original.rotation = 0
|
|
original.z_index = 5
|
|
|
|
# Serialize
|
|
data = original.serialize()
|
|
|
|
# Deserialize into new object
|
|
restored = ImageData()
|
|
restored.deserialize(data)
|
|
|
|
# Verify all fields match
|
|
assert restored.pil_rotation_90 == original.pil_rotation_90
|
|
assert restored.image_dimensions == original.image_dimensions
|
|
assert restored.rotation == original.rotation
|
|
assert restored.position == original.position
|
|
assert restored.size == original.size
|
|
assert restored.z_index == original.z_index
|
|
assert restored.image_path == original.image_path
|
|
|
|
def test_backward_compatibility_visual_rotation_conversion(self):
|
|
"""Test that old visual rotations are converted to pil_rotation_90"""
|
|
# Old format data with visual rotation
|
|
data = {
|
|
"type": "image",
|
|
"position": (10, 20),
|
|
"size": (100, 50),
|
|
"rotation": 90, # Old visual rotation
|
|
"z_index": 0,
|
|
"image_path": "test.jpg",
|
|
"crop_info": (0, 0, 1, 1),
|
|
"pil_rotation_90": 0, # Not set in old format
|
|
"image_dimensions": (400, 200),
|
|
}
|
|
|
|
img_data = ImageData()
|
|
img_data.deserialize(data)
|
|
|
|
# Should convert to pil_rotation_90
|
|
assert img_data.pil_rotation_90 == 1
|
|
assert img_data.rotation == 0 # Visual rotation reset
|
|
|
|
def test_dimensions_not_lost_on_reload(self, sample_image):
|
|
"""Integration test: dimensions are preserved through save/load cycle"""
|
|
# Step 1: Create and "rotate" an image
|
|
img1 = ImageData(image_path="test.jpg", x=10, y=20, width=100, height=50)
|
|
img1.pil_rotation_90 = 1
|
|
img1.image_dimensions = (400, 200)
|
|
|
|
# Simulate first load and rotation - pass UNROTATED image
|
|
img1._on_async_image_loaded(sample_image)
|
|
|
|
# Verify dimensions after rotation
|
|
assert img1.image_dimensions == (200, 400)
|
|
|
|
# Step 2: Serialize (like saving the project)
|
|
saved_data = img1.serialize()
|
|
|
|
# Step 3: Deserialize (like loading the project)
|
|
img2 = ImageData()
|
|
img2.deserialize(saved_data)
|
|
|
|
# Verify rotation metadata is preserved
|
|
assert img2.pil_rotation_90 == 1
|
|
# Note: image_dimensions from save will be the rotated dimensions
|
|
assert img2.image_dimensions == (200, 400)
|
|
|
|
# Step 4: Simulate reload (async loading happens again) - pass UNROTATED image
|
|
img2._on_async_image_loaded(sample_image)
|
|
|
|
# Verify dimensions are STILL correct after reload
|
|
assert img2.image_dimensions == (200, 400), "Dimensions should remain correct after reload"
|
|
assert img2._img_width == 200
|
|
assert img2._img_height == 400
|