This commit is contained in:
parent
22d3505256
commit
3f9bd0072e
@ -21,7 +21,7 @@ class Box(Renderable, Queriable):
|
||||
|
||||
def in_shape(self, point):
|
||||
|
||||
return np.all((point >= self.origin) & (point < self._end), axis=-1)
|
||||
return np.all((point >= self._origin) & (point < self._end), axis=-1)
|
||||
|
||||
def render(self) -> Image:
|
||||
# Create a new image canvas
|
||||
|
||||
@ -43,6 +43,9 @@ class RenderableImage(Box, Queriable):
|
||||
# Calculate the size if not provided
|
||||
if size is None:
|
||||
size = image.calculate_scaled_dimensions(max_width, max_height)
|
||||
# Ensure we have valid dimensions, fallback to defaults if None
|
||||
if size[0] is None or size[1] is None:
|
||||
size = (100, 100) # Default size when image dimensions are unavailable
|
||||
|
||||
# Initialize the box
|
||||
super().__init__(origin or (0, 0), size, callback, sheet, mode, halign, valign)
|
||||
@ -131,7 +134,7 @@ class RenderableImage(Box, Queriable):
|
||||
A resized PIL Image
|
||||
"""
|
||||
if not self._pil_image:
|
||||
return PILImage.new('RGBA', self._size, (200, 200, 200, 100))
|
||||
return PILImage.new('RGBA', tuple(self._size), (200, 200, 200, 100))
|
||||
|
||||
# Get the target dimensions
|
||||
target_width, target_height = self._size
|
||||
|
||||
189
tests/test_concrete_box.py
Normal file
189
tests/test_concrete_box.py
Normal file
@ -0,0 +1,189 @@
|
||||
"""
|
||||
Unit tests for pyWebLayout.concrete.box module.
|
||||
Tests the Box class which handles basic box model rendering with alignment.
|
||||
"""
|
||||
|
||||
import unittest
|
||||
import numpy as np
|
||||
from PIL import Image
|
||||
from unittest.mock import Mock, patch
|
||||
|
||||
from pyWebLayout.concrete.box import Box
|
||||
from pyWebLayout.style.layout import Alignment
|
||||
|
||||
|
||||
class TestBox(unittest.TestCase):
|
||||
"""Test cases for the Box class"""
|
||||
|
||||
def setUp(self):
|
||||
"""Set up test fixtures"""
|
||||
self.origin = (10, 20)
|
||||
self.size = (100, 50)
|
||||
self.callback = Mock()
|
||||
|
||||
def test_box_initialization_basic(self):
|
||||
"""Test basic box initialization"""
|
||||
box = Box(self.origin, self.size)
|
||||
|
||||
np.testing.assert_array_equal(box._origin, np.array([10, 20]))
|
||||
np.testing.assert_array_equal(box._size, np.array([100, 50]))
|
||||
np.testing.assert_array_equal(box._end, np.array([110, 70]))
|
||||
self.assertIsNone(box._callback)
|
||||
self.assertIsNone(box._sheet)
|
||||
self.assertIsNone(box._mode)
|
||||
self.assertEqual(box._halign, Alignment.CENTER)
|
||||
self.assertEqual(box._valign, Alignment.CENTER)
|
||||
|
||||
def test_box_initialization_with_callback(self):
|
||||
"""Test box initialization with callback"""
|
||||
box = Box(self.origin, self.size, callback=self.callback)
|
||||
|
||||
self.assertEqual(box._callback, self.callback)
|
||||
|
||||
def test_box_initialization_with_sheet(self):
|
||||
"""Test box initialization with image sheet"""
|
||||
sheet = Image.new('RGBA', (200, 100), (255, 255, 255, 255))
|
||||
box = Box(self.origin, self.size, sheet=sheet)
|
||||
|
||||
self.assertEqual(box._sheet, sheet)
|
||||
self.assertEqual(box._mode, 'RGBA')
|
||||
|
||||
def test_box_initialization_with_mode(self):
|
||||
"""Test box initialization with explicit mode"""
|
||||
box = Box(self.origin, self.size, mode='RGB')
|
||||
|
||||
self.assertEqual(box._mode, 'RGB')
|
||||
|
||||
def test_box_initialization_with_alignment(self):
|
||||
"""Test box initialization with custom alignment"""
|
||||
box = Box(self.origin, self.size, halign=Alignment.LEFT, valign=Alignment.TOP)
|
||||
|
||||
self.assertEqual(box._halign, Alignment.LEFT)
|
||||
self.assertEqual(box._valign, Alignment.TOP)
|
||||
|
||||
def test_in_shape_point_inside(self):
|
||||
"""Test in_shape method with point inside box"""
|
||||
box = Box(self.origin, self.size)
|
||||
|
||||
# Test point inside
|
||||
self.assertTrue(box.in_shape(np.array([50, 40])))
|
||||
self.assertTrue(box.in_shape(np.array([10, 20]))) # Top-left corner
|
||||
self.assertTrue(box.in_shape(np.array([109, 69]))) # Just inside bottom-right
|
||||
|
||||
def test_in_shape_point_outside(self):
|
||||
"""Test in_shape method with point outside box"""
|
||||
box = Box(self.origin, self.size)
|
||||
|
||||
# Test points outside
|
||||
self.assertFalse(box.in_shape(np.array([5, 15]))) # Before origin
|
||||
self.assertFalse(box.in_shape(np.array([110, 70]))) # At end (exclusive)
|
||||
self.assertFalse(box.in_shape(np.array([150, 100]))) # Far outside
|
||||
|
||||
def test_in_shape_multiple_points(self):
|
||||
"""Test in_shape method with array of points"""
|
||||
box = Box(self.origin, self.size)
|
||||
|
||||
points = np.array([[50, 40], [5, 15], [109, 69], [110, 70]])
|
||||
result = box.in_shape(points)
|
||||
|
||||
np.testing.assert_array_equal(result, [True, False, True, False])
|
||||
|
||||
def test_render_default_no_content(self):
|
||||
"""Test render method with no content"""
|
||||
box = Box(self.origin, self.size)
|
||||
result = box.render()
|
||||
|
||||
self.assertIsInstance(result, Image.Image)
|
||||
self.assertEqual(result.size, tuple(self.size))
|
||||
self.assertEqual(result.mode, 'RGBA')
|
||||
|
||||
def test_render_with_sheet_mode(self):
|
||||
"""Test render method with sheet providing mode"""
|
||||
sheet = Image.new('RGB', (200, 100), (255, 0, 0))
|
||||
box = Box(self.origin, self.size, sheet=sheet)
|
||||
result = box.render()
|
||||
|
||||
self.assertIsInstance(result, Image.Image)
|
||||
self.assertEqual(result.size, tuple(self.size))
|
||||
self.assertEqual(result.mode, 'RGB')
|
||||
|
||||
def test_render_with_explicit_mode(self):
|
||||
"""Test render method with explicit mode"""
|
||||
box = Box(self.origin, self.size, mode='L') # Grayscale
|
||||
result = box.render()
|
||||
|
||||
self.assertIsInstance(result, Image.Image)
|
||||
self.assertEqual(result.size, tuple(self.size))
|
||||
self.assertEqual(result.mode, 'L')
|
||||
|
||||
def test_render_with_content_centered(self):
|
||||
"""Test render method with content centered"""
|
||||
box = Box(self.origin, self.size, halign=Alignment.CENTER, valign=Alignment.CENTER)
|
||||
|
||||
# Mock content that has a render method
|
||||
mock_content = Mock()
|
||||
mock_content.render.return_value = Image.new('RGBA', (50, 30), (255, 0, 0, 255))
|
||||
box._content = mock_content
|
||||
|
||||
result = box.render()
|
||||
|
||||
self.assertIsInstance(result, Image.Image)
|
||||
self.assertEqual(result.size, tuple(self.size))
|
||||
mock_content.render.assert_called_once()
|
||||
|
||||
def test_render_with_content_left_aligned(self):
|
||||
"""Test render method with content left-aligned"""
|
||||
box = Box(self.origin, self.size, halign=Alignment.LEFT, valign=Alignment.TOP)
|
||||
|
||||
# Mock content
|
||||
mock_content = Mock()
|
||||
mock_content.render.return_value = Image.new('RGBA', (30, 20), (0, 255, 0, 255))
|
||||
box._content = mock_content
|
||||
|
||||
result = box.render()
|
||||
|
||||
self.assertIsInstance(result, Image.Image)
|
||||
self.assertEqual(result.size, tuple(self.size))
|
||||
mock_content.render.assert_called_once()
|
||||
|
||||
def test_render_with_content_right_aligned(self):
|
||||
"""Test render method with content right-aligned"""
|
||||
box = Box(self.origin, self.size, halign=Alignment.RIGHT, valign=Alignment.BOTTOM)
|
||||
|
||||
# Mock content
|
||||
mock_content = Mock()
|
||||
mock_content.render.return_value = Image.new('RGBA', (40, 25), (0, 0, 255, 255))
|
||||
box._content = mock_content
|
||||
|
||||
result = box.render()
|
||||
|
||||
self.assertIsInstance(result, Image.Image)
|
||||
self.assertEqual(result.size, tuple(self.size))
|
||||
mock_content.render.assert_called_once()
|
||||
|
||||
def test_render_content_larger_than_box(self):
|
||||
"""Test render method when content is larger than box"""
|
||||
small_box = Box((0, 0), (20, 15))
|
||||
|
||||
# Mock content larger than box
|
||||
mock_content = Mock()
|
||||
mock_content.render.return_value = Image.new('RGBA', (50, 40), (255, 255, 0, 255))
|
||||
small_box._content = mock_content
|
||||
|
||||
result = small_box.render()
|
||||
|
||||
# Should still create box-sized canvas
|
||||
self.assertEqual(result.size, (20, 15))
|
||||
mock_content.render.assert_called_once()
|
||||
|
||||
def test_properties_access(self):
|
||||
"""Test that properties can be accessed correctly"""
|
||||
box = Box(self.origin, self.size, callback=self.callback)
|
||||
|
||||
# Test that origin property works (should be available via inheritance)
|
||||
np.testing.assert_array_equal(box._origin, np.array([10, 20]))
|
||||
np.testing.assert_array_equal(box._size, np.array([100, 50]))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
369
tests/test_concrete_image.py
Normal file
369
tests/test_concrete_image.py
Normal file
@ -0,0 +1,369 @@
|
||||
"""
|
||||
Unit tests for pyWebLayout.concrete.image module.
|
||||
Tests the RenderableImage class for image loading, scaling, and rendering functionality.
|
||||
"""
|
||||
|
||||
import unittest
|
||||
import os
|
||||
import tempfile
|
||||
import numpy as np
|
||||
from PIL import Image as PILImage
|
||||
from unittest.mock import Mock, patch, MagicMock
|
||||
|
||||
from pyWebLayout.concrete.image import RenderableImage
|
||||
from pyWebLayout.abstract.block import Image as AbstractImage
|
||||
from pyWebLayout.style.layout import Alignment
|
||||
|
||||
|
||||
class TestRenderableImage(unittest.TestCase):
|
||||
"""Test cases for the RenderableImage class"""
|
||||
|
||||
def setUp(self):
|
||||
"""Set up test fixtures"""
|
||||
# Create a temporary test image
|
||||
self.temp_dir = tempfile.mkdtemp()
|
||||
self.test_image_path = os.path.join(self.temp_dir, "test_image.png")
|
||||
|
||||
# Create a simple test image
|
||||
test_img = PILImage.new('RGB', (100, 80), (255, 0, 0)) # Red image
|
||||
test_img.save(self.test_image_path)
|
||||
|
||||
# Create abstract image objects
|
||||
self.abstract_image = AbstractImage(self.test_image_path, "Test Image", 100, 80)
|
||||
self.abstract_image_no_dims = AbstractImage(self.test_image_path, "Test Image")
|
||||
|
||||
def tearDown(self):
|
||||
"""Clean up test fixtures"""
|
||||
# Clean up temporary files
|
||||
try:
|
||||
os.unlink(self.test_image_path)
|
||||
os.rmdir(self.temp_dir)
|
||||
except:
|
||||
pass
|
||||
|
||||
def test_renderable_image_initialization_basic(self):
|
||||
"""Test basic image initialization"""
|
||||
renderable = RenderableImage(self.abstract_image)
|
||||
|
||||
self.assertEqual(renderable._abstract_image, self.abstract_image)
|
||||
self.assertIsNotNone(renderable._pil_image)
|
||||
self.assertIsNone(renderable._error_message)
|
||||
self.assertEqual(renderable._halign, Alignment.CENTER)
|
||||
self.assertEqual(renderable._valign, Alignment.CENTER)
|
||||
|
||||
def test_renderable_image_initialization_with_constraints(self):
|
||||
"""Test image initialization with size constraints"""
|
||||
max_width = 50
|
||||
max_height = 40
|
||||
|
||||
renderable = RenderableImage(
|
||||
self.abstract_image,
|
||||
max_width=max_width,
|
||||
max_height=max_height
|
||||
)
|
||||
|
||||
self.assertEqual(renderable._abstract_image, self.abstract_image)
|
||||
# Size should be constrained
|
||||
self.assertLessEqual(renderable._size[0], max_width)
|
||||
self.assertLessEqual(renderable._size[1], max_height)
|
||||
|
||||
def test_renderable_image_initialization_with_custom_params(self):
|
||||
"""Test image initialization with custom parameters"""
|
||||
custom_origin = (20, 30)
|
||||
custom_size = (120, 90)
|
||||
custom_callback = Mock()
|
||||
|
||||
renderable = RenderableImage(
|
||||
self.abstract_image,
|
||||
origin=custom_origin,
|
||||
size=custom_size,
|
||||
callback=custom_callback,
|
||||
halign=Alignment.LEFT,
|
||||
valign=Alignment.TOP
|
||||
)
|
||||
|
||||
np.testing.assert_array_equal(renderable._origin, np.array(custom_origin))
|
||||
np.testing.assert_array_equal(renderable._size, np.array(custom_size))
|
||||
self.assertEqual(renderable._callback, custom_callback)
|
||||
self.assertEqual(renderable._halign, Alignment.LEFT)
|
||||
self.assertEqual(renderable._valign, Alignment.TOP)
|
||||
|
||||
def test_load_image_local_file(self):
|
||||
"""Test loading image from local file"""
|
||||
renderable = RenderableImage(self.abstract_image)
|
||||
|
||||
# Image should be loaded
|
||||
self.assertIsNotNone(renderable._pil_image)
|
||||
self.assertIsNone(renderable._error_message)
|
||||
self.assertEqual(renderable._pil_image.size, (100, 80))
|
||||
|
||||
def test_load_image_nonexistent_file(self):
|
||||
"""Test loading image from nonexistent file"""
|
||||
bad_abstract = AbstractImage("/nonexistent/path.png", "Bad Image")
|
||||
renderable = RenderableImage(bad_abstract)
|
||||
|
||||
# Should have error message, no PIL image
|
||||
self.assertIsNone(renderable._pil_image)
|
||||
self.assertIsNotNone(renderable._error_message)
|
||||
|
||||
@patch('requests.get')
|
||||
def test_load_image_url_success(self, mock_get):
|
||||
"""Test loading image from URL (success)"""
|
||||
# Create a mock response
|
||||
mock_response = Mock()
|
||||
mock_response.status_code = 200
|
||||
mock_response.content = open(self.test_image_path, 'rb').read()
|
||||
mock_get.return_value = mock_response
|
||||
|
||||
url_abstract = AbstractImage("https://example.com/image.png", "URL Image")
|
||||
renderable = RenderableImage(url_abstract)
|
||||
|
||||
# Should successfully load image
|
||||
self.assertIsNotNone(renderable._pil_image)
|
||||
self.assertIsNone(renderable._error_message)
|
||||
|
||||
@patch('requests.get')
|
||||
def test_load_image_url_failure(self, mock_get):
|
||||
"""Test loading image from URL (failure)"""
|
||||
# Mock a failed request
|
||||
mock_response = Mock()
|
||||
mock_response.status_code = 404
|
||||
mock_get.return_value = mock_response
|
||||
|
||||
url_abstract = AbstractImage("https://example.com/notfound.png", "Bad URL Image")
|
||||
renderable = RenderableImage(url_abstract)
|
||||
|
||||
# Should have error message
|
||||
self.assertIsNone(renderable._pil_image)
|
||||
self.assertIsNotNone(renderable._error_message)
|
||||
|
||||
def test_load_image_no_requests_library(self):
|
||||
"""Test loading URL image when requests library is not available"""
|
||||
# Mock the import to raise ImportError for requests
|
||||
def mock_import(name, *args, **kwargs):
|
||||
if name == 'requests':
|
||||
raise ImportError("No module named 'requests'")
|
||||
return __import__(name, *args, **kwargs)
|
||||
|
||||
with patch('builtins.__import__', side_effect=mock_import):
|
||||
url_abstract = AbstractImage("https://example.com/image.png", "URL Image")
|
||||
renderable = RenderableImage(url_abstract)
|
||||
|
||||
# Should have error message about missing requests
|
||||
self.assertIsNone(renderable._pil_image)
|
||||
self.assertIsNotNone(renderable._error_message)
|
||||
self.assertIn("Requests library not available", renderable._error_message)
|
||||
|
||||
def test_resize_image_fit_within_bounds(self):
|
||||
"""Test image resizing to fit within bounds"""
|
||||
renderable = RenderableImage(self.abstract_image)
|
||||
|
||||
# Original image is 100x80, resize to fit in 50x50
|
||||
renderable._size = np.array([50, 50])
|
||||
resized = renderable._resize_image()
|
||||
|
||||
self.assertIsInstance(resized, PILImage.Image)
|
||||
# Should maintain aspect ratio and fit within bounds
|
||||
self.assertLessEqual(resized.width, 50)
|
||||
self.assertLessEqual(resized.height, 50)
|
||||
# Check aspect ratio is maintained (approximately)
|
||||
original_ratio = 100 / 80
|
||||
new_ratio = resized.width / resized.height
|
||||
self.assertAlmostEqual(original_ratio, new_ratio, delta=0.1)
|
||||
|
||||
def test_resize_image_larger_target(self):
|
||||
"""Test image resizing when target is larger than original"""
|
||||
renderable = RenderableImage(self.abstract_image)
|
||||
|
||||
# Target size larger than original
|
||||
renderable._size = np.array([200, 160])
|
||||
resized = renderable._resize_image()
|
||||
|
||||
self.assertIsInstance(resized, PILImage.Image)
|
||||
# Should scale up to fill the space while maintaining aspect ratio
|
||||
self.assertGreater(resized.width, 100)
|
||||
self.assertGreater(resized.height, 80)
|
||||
|
||||
def test_resize_image_no_image(self):
|
||||
"""Test resize when no image is loaded"""
|
||||
bad_abstract = AbstractImage("/nonexistent/path.png", "Bad Image")
|
||||
renderable = RenderableImage(bad_abstract)
|
||||
|
||||
resized = renderable._resize_image()
|
||||
|
||||
# Should return a placeholder image
|
||||
self.assertIsInstance(resized, PILImage.Image)
|
||||
self.assertEqual(resized.mode, 'RGBA')
|
||||
|
||||
@patch('PIL.ImageDraw.Draw')
|
||||
def test_draw_error_placeholder(self, mock_draw_class):
|
||||
"""Test drawing error placeholder"""
|
||||
mock_draw = Mock()
|
||||
mock_draw_class.return_value = mock_draw
|
||||
|
||||
bad_abstract = AbstractImage("/nonexistent/path.png", "Bad Image")
|
||||
renderable = RenderableImage(bad_abstract)
|
||||
|
||||
canvas = PILImage.new('RGBA', (100, 80), (255, 255, 255, 255))
|
||||
renderable._draw_error_placeholder(canvas)
|
||||
|
||||
# Should draw rectangle and lines for the X
|
||||
mock_draw.rectangle.assert_called_once()
|
||||
self.assertEqual(mock_draw.line.call_count, 2) # Two lines for the X
|
||||
|
||||
@patch('PIL.ImageDraw.Draw')
|
||||
@patch('PIL.ImageFont.load_default')
|
||||
def test_draw_error_placeholder_with_text(self, mock_font, mock_draw_class):
|
||||
"""Test drawing error placeholder with error message"""
|
||||
mock_draw = Mock()
|
||||
mock_draw_class.return_value = mock_draw
|
||||
mock_font.return_value = Mock()
|
||||
|
||||
# Mock textbbox to return reasonable bounds
|
||||
mock_draw.textbbox.return_value = (0, 0, 50, 12)
|
||||
|
||||
bad_abstract = AbstractImage("/nonexistent/path.png", "Bad Image")
|
||||
renderable = RenderableImage(bad_abstract)
|
||||
renderable._error_message = "File not found"
|
||||
|
||||
canvas = PILImage.new('RGBA', (100, 80), (255, 255, 255, 255))
|
||||
renderable._draw_error_placeholder(canvas)
|
||||
|
||||
# Should draw rectangle, lines, and text
|
||||
mock_draw.rectangle.assert_called_once()
|
||||
self.assertEqual(mock_draw.line.call_count, 2)
|
||||
mock_draw.text.assert_called() # Error text should be drawn
|
||||
|
||||
def test_render_successful_image(self):
|
||||
"""Test rendering successfully loaded image"""
|
||||
renderable = RenderableImage(self.abstract_image)
|
||||
result = renderable.render()
|
||||
|
||||
self.assertIsInstance(result, PILImage.Image)
|
||||
self.assertEqual(result.size, tuple(renderable._size))
|
||||
self.assertEqual(result.mode, 'RGBA')
|
||||
|
||||
def test_render_failed_image(self):
|
||||
"""Test rendering when image failed to load"""
|
||||
bad_abstract = AbstractImage("/nonexistent/path.png", "Bad Image")
|
||||
renderable = RenderableImage(bad_abstract)
|
||||
|
||||
with patch.object(renderable, '_draw_error_placeholder') as mock_draw_error:
|
||||
result = renderable.render()
|
||||
|
||||
self.assertIsInstance(result, PILImage.Image)
|
||||
mock_draw_error.assert_called_once()
|
||||
|
||||
def test_render_with_left_alignment(self):
|
||||
"""Test rendering with left alignment"""
|
||||
renderable = RenderableImage(
|
||||
self.abstract_image,
|
||||
halign=Alignment.LEFT,
|
||||
valign=Alignment.TOP
|
||||
)
|
||||
|
||||
result = renderable.render()
|
||||
|
||||
self.assertIsInstance(result, PILImage.Image)
|
||||
self.assertEqual(result.size, tuple(renderable._size))
|
||||
|
||||
def test_render_with_right_alignment(self):
|
||||
"""Test rendering with right alignment"""
|
||||
renderable = RenderableImage(
|
||||
self.abstract_image,
|
||||
halign=Alignment.RIGHT,
|
||||
valign=Alignment.BOTTOM
|
||||
)
|
||||
|
||||
result = renderable.render()
|
||||
|
||||
self.assertIsInstance(result, PILImage.Image)
|
||||
self.assertEqual(result.size, tuple(renderable._size))
|
||||
|
||||
def test_render_rgba_image_on_rgba_canvas(self):
|
||||
"""Test rendering RGBA image on RGBA canvas"""
|
||||
# Create RGBA test image
|
||||
rgba_image_path = os.path.join(self.temp_dir, "rgba_test.png")
|
||||
rgba_img = PILImage.new('RGBA', (50, 40), (0, 255, 0, 128)) # Green with transparency
|
||||
rgba_img.save(rgba_image_path)
|
||||
|
||||
try:
|
||||
rgba_abstract = AbstractImage(rgba_image_path, "RGBA Image", 50, 40)
|
||||
renderable = RenderableImage(rgba_abstract)
|
||||
|
||||
result = renderable.render()
|
||||
|
||||
self.assertIsInstance(result, PILImage.Image)
|
||||
self.assertEqual(result.mode, 'RGBA')
|
||||
finally:
|
||||
try:
|
||||
os.unlink(rgba_image_path)
|
||||
except:
|
||||
pass
|
||||
|
||||
def test_render_rgb_image_conversion(self):
|
||||
"""Test rendering RGB image (should be converted to RGBA)"""
|
||||
# Our test image is RGB, so this should test the conversion path
|
||||
renderable = RenderableImage(self.abstract_image)
|
||||
result = renderable.render()
|
||||
|
||||
self.assertIsInstance(result, PILImage.Image)
|
||||
self.assertEqual(result.mode, 'RGBA')
|
||||
|
||||
def test_in_object(self):
|
||||
"""Test in_object method"""
|
||||
renderable = RenderableImage(self.abstract_image, origin=(10, 20))
|
||||
|
||||
# Point inside image
|
||||
self.assertTrue(renderable.in_object((15, 25)))
|
||||
|
||||
# Point outside image
|
||||
self.assertFalse(renderable.in_object((200, 200)))
|
||||
|
||||
def test_in_object_with_numpy_array(self):
|
||||
"""Test in_object with numpy array point"""
|
||||
renderable = RenderableImage(self.abstract_image, origin=(10, 20))
|
||||
|
||||
# Point inside image as numpy array
|
||||
point = np.array([15, 25])
|
||||
self.assertTrue(renderable.in_object(point))
|
||||
|
||||
# Point outside image as numpy array
|
||||
point = np.array([200, 200])
|
||||
self.assertFalse(renderable.in_object(point))
|
||||
|
||||
def test_image_size_calculation_with_abstract_image_dimensions(self):
|
||||
"""Test that size is calculated from abstract image when available"""
|
||||
# Abstract image has dimensions 100x80
|
||||
renderable = RenderableImage(self.abstract_image)
|
||||
|
||||
# Size should match the calculated scaled dimensions
|
||||
expected_size = self.abstract_image.calculate_scaled_dimensions()
|
||||
np.testing.assert_array_equal(renderable._size, np.array(expected_size))
|
||||
|
||||
def test_image_size_calculation_with_constraints(self):
|
||||
"""Test size calculation with max constraints"""
|
||||
max_width = 60
|
||||
max_height = 50
|
||||
|
||||
renderable = RenderableImage(
|
||||
self.abstract_image,
|
||||
max_width=max_width,
|
||||
max_height=max_height
|
||||
)
|
||||
|
||||
# Size should respect constraints
|
||||
self.assertLessEqual(renderable._size[0], max_width)
|
||||
self.assertLessEqual(renderable._size[1], max_height)
|
||||
|
||||
def test_image_without_initial_dimensions(self):
|
||||
"""Test image without initial dimensions in abstract image"""
|
||||
renderable = RenderableImage(self.abstract_image_no_dims)
|
||||
|
||||
# Should still work, using default or calculated size
|
||||
self.assertIsInstance(renderable._size, np.ndarray)
|
||||
self.assertEqual(len(renderable._size), 2)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
459
tests/test_concrete_page.py
Normal file
459
tests/test_concrete_page.py
Normal file
@ -0,0 +1,459 @@
|
||||
"""
|
||||
Unit tests for pyWebLayout.concrete.page module.
|
||||
Tests the Container and Page classes for layout and rendering functionality.
|
||||
"""
|
||||
|
||||
import unittest
|
||||
import numpy as np
|
||||
from PIL import Image
|
||||
from unittest.mock import Mock, patch, MagicMock
|
||||
|
||||
from pyWebLayout.concrete.page import Container, Page
|
||||
from pyWebLayout.concrete.box import Box
|
||||
from pyWebLayout.style.layout import Alignment
|
||||
|
||||
|
||||
class TestContainer(unittest.TestCase):
|
||||
"""Test cases for the Container class"""
|
||||
|
||||
def setUp(self):
|
||||
"""Set up test fixtures"""
|
||||
self.origin = (0, 0)
|
||||
self.size = (400, 300)
|
||||
self.callback = Mock()
|
||||
|
||||
# Create mock child elements
|
||||
self.mock_child1 = Mock()
|
||||
self.mock_child1._size = np.array([100, 50])
|
||||
self.mock_child1._origin = np.array([0, 0])
|
||||
self.mock_child1.render.return_value = Image.new('RGBA', (100, 50), (255, 0, 0, 255))
|
||||
|
||||
self.mock_child2 = Mock()
|
||||
self.mock_child2._size = np.array([120, 60])
|
||||
self.mock_child2._origin = np.array([0, 0])
|
||||
self.mock_child2.render.return_value = Image.new('RGBA', (120, 60), (0, 255, 0, 255))
|
||||
|
||||
def test_container_initialization_basic(self):
|
||||
"""Test basic container initialization"""
|
||||
container = Container(self.origin, self.size)
|
||||
|
||||
np.testing.assert_array_equal(container._origin, np.array(self.origin))
|
||||
np.testing.assert_array_equal(container._size, np.array(self.size))
|
||||
self.assertEqual(container._direction, 'vertical')
|
||||
self.assertEqual(container._spacing, 5)
|
||||
self.assertEqual(len(container._children), 0)
|
||||
self.assertEqual(container._padding, (10, 10, 10, 10))
|
||||
self.assertEqual(container._halign, Alignment.CENTER)
|
||||
self.assertEqual(container._valign, Alignment.CENTER)
|
||||
|
||||
def test_container_initialization_with_params(self):
|
||||
"""Test container initialization with custom parameters"""
|
||||
custom_direction = 'horizontal'
|
||||
custom_spacing = 15
|
||||
custom_padding = (5, 8, 5, 8)
|
||||
|
||||
container = Container(
|
||||
self.origin, self.size,
|
||||
direction=custom_direction,
|
||||
spacing=custom_spacing,
|
||||
callback=self.callback,
|
||||
halign=Alignment.LEFT,
|
||||
valign=Alignment.TOP,
|
||||
padding=custom_padding
|
||||
)
|
||||
|
||||
self.assertEqual(container._direction, custom_direction)
|
||||
self.assertEqual(container._spacing, custom_spacing)
|
||||
self.assertEqual(container._callback, self.callback)
|
||||
self.assertEqual(container._halign, Alignment.LEFT)
|
||||
self.assertEqual(container._valign, Alignment.TOP)
|
||||
self.assertEqual(container._padding, custom_padding)
|
||||
|
||||
def test_add_child(self):
|
||||
"""Test adding child elements"""
|
||||
container = Container(self.origin, self.size)
|
||||
|
||||
result = container.add_child(self.mock_child1)
|
||||
|
||||
self.assertEqual(len(container._children), 1)
|
||||
self.assertEqual(container._children[0], self.mock_child1)
|
||||
self.assertEqual(result, container) # Should return self for chaining
|
||||
|
||||
def test_add_multiple_children(self):
|
||||
"""Test adding multiple child elements"""
|
||||
container = Container(self.origin, self.size)
|
||||
|
||||
container.add_child(self.mock_child1)
|
||||
container.add_child(self.mock_child2)
|
||||
|
||||
self.assertEqual(len(container._children), 2)
|
||||
self.assertEqual(container._children[0], self.mock_child1)
|
||||
self.assertEqual(container._children[1], self.mock_child2)
|
||||
|
||||
def test_layout_vertical_centered(self):
|
||||
"""Test vertical layout with center alignment"""
|
||||
container = Container(self.origin, self.size, direction='vertical', halign=Alignment.CENTER)
|
||||
container.add_child(self.mock_child1)
|
||||
container.add_child(self.mock_child2)
|
||||
|
||||
container.layout()
|
||||
|
||||
# Check that children have been positioned
|
||||
# First child should be at top padding
|
||||
expected_x1 = 10 + (380 - 100) // 2 # padding + centered in available width
|
||||
expected_y1 = 10 # top padding
|
||||
np.testing.assert_array_equal(self.mock_child1._origin, np.array([expected_x1, expected_y1]))
|
||||
|
||||
# Second child should be below first child + spacing
|
||||
expected_x2 = 10 + (380 - 120) // 2 # padding + centered in available width
|
||||
expected_y2 = 10 + 50 + 5 # top padding + first child height + spacing
|
||||
np.testing.assert_array_equal(self.mock_child2._origin, np.array([expected_x2, expected_y2]))
|
||||
|
||||
def test_layout_vertical_left_aligned(self):
|
||||
"""Test vertical layout with left alignment"""
|
||||
container = Container(self.origin, self.size, direction='vertical', halign=Alignment.LEFT)
|
||||
container.add_child(self.mock_child1)
|
||||
container.add_child(self.mock_child2)
|
||||
|
||||
container.layout()
|
||||
|
||||
# Both children should be left-aligned
|
||||
expected_x = 10 # left padding
|
||||
np.testing.assert_array_equal(self.mock_child1._origin, np.array([expected_x, 10]))
|
||||
np.testing.assert_array_equal(self.mock_child2._origin, np.array([expected_x, 65]))
|
||||
|
||||
def test_layout_vertical_right_aligned(self):
|
||||
"""Test vertical layout with right alignment"""
|
||||
container = Container(self.origin, self.size, direction='vertical', halign=Alignment.RIGHT)
|
||||
container.add_child(self.mock_child1)
|
||||
container.add_child(self.mock_child2)
|
||||
|
||||
container.layout()
|
||||
|
||||
# Children should be right-aligned
|
||||
expected_x1 = 10 + 380 - 100 # left padding + available width - child width
|
||||
expected_x2 = 10 + 380 - 120
|
||||
np.testing.assert_array_equal(self.mock_child1._origin, np.array([expected_x1, 10]))
|
||||
np.testing.assert_array_equal(self.mock_child2._origin, np.array([expected_x2, 65]))
|
||||
|
||||
def test_layout_horizontal_centered(self):
|
||||
"""Test horizontal layout with center alignment"""
|
||||
container = Container(self.origin, self.size, direction='horizontal', valign=Alignment.CENTER)
|
||||
container.add_child(self.mock_child1)
|
||||
container.add_child(self.mock_child2)
|
||||
|
||||
container.layout()
|
||||
|
||||
# Children should be positioned horizontally
|
||||
expected_x1 = 10 # left padding
|
||||
expected_x2 = 10 + 100 + 5 # left padding + first child width + spacing
|
||||
|
||||
# Vertically centered
|
||||
expected_y1 = 10 + (280 - 50) // 2 # top padding + centered in available height
|
||||
expected_y2 = 10 + (280 - 60) // 2
|
||||
|
||||
np.testing.assert_array_equal(self.mock_child1._origin, np.array([expected_x1, expected_y1]))
|
||||
np.testing.assert_array_equal(self.mock_child2._origin, np.array([expected_x2, expected_y2]))
|
||||
|
||||
def test_layout_horizontal_top_aligned(self):
|
||||
"""Test horizontal layout with top alignment"""
|
||||
container = Container(self.origin, self.size, direction='horizontal', valign=Alignment.TOP)
|
||||
container.add_child(self.mock_child1)
|
||||
container.add_child(self.mock_child2)
|
||||
|
||||
container.layout()
|
||||
|
||||
# Both children should be top-aligned
|
||||
expected_y = 10 # top padding
|
||||
np.testing.assert_array_equal(self.mock_child1._origin, np.array([10, expected_y]))
|
||||
np.testing.assert_array_equal(self.mock_child2._origin, np.array([115, expected_y]))
|
||||
|
||||
def test_layout_horizontal_bottom_aligned(self):
|
||||
"""Test horizontal layout with bottom alignment"""
|
||||
container = Container(self.origin, self.size, direction='horizontal', valign=Alignment.BOTTOM)
|
||||
container.add_child(self.mock_child1)
|
||||
container.add_child(self.mock_child2)
|
||||
|
||||
container.layout()
|
||||
|
||||
# Children should be bottom-aligned
|
||||
expected_y1 = 10 + 280 - 50 # top padding + available height - child height
|
||||
expected_y2 = 10 + 280 - 60
|
||||
np.testing.assert_array_equal(self.mock_child1._origin, np.array([10, expected_y1]))
|
||||
np.testing.assert_array_equal(self.mock_child2._origin, np.array([115, expected_y2]))
|
||||
|
||||
def test_layout_empty_container(self):
|
||||
"""Test layout with no children"""
|
||||
container = Container(self.origin, self.size)
|
||||
|
||||
# Should not raise an error
|
||||
container.layout()
|
||||
|
||||
self.assertEqual(len(container._children), 0)
|
||||
|
||||
def test_layout_with_layoutable_children(self):
|
||||
"""Test layout with children that are also layoutable"""
|
||||
# Create a mock child that implements Layoutable
|
||||
mock_layoutable_child = Mock()
|
||||
mock_layoutable_child._size = np.array([80, 40])
|
||||
mock_layoutable_child._origin = np.array([0, 0])
|
||||
|
||||
# Make it look like a Layoutable by adding layout method
|
||||
from pyWebLayout.core.base import Layoutable
|
||||
mock_layoutable_child.__class__ = type('MockLayoutable', (Mock, Layoutable), {})
|
||||
mock_layoutable_child.layout = Mock()
|
||||
|
||||
container = Container(self.origin, self.size)
|
||||
container.add_child(mock_layoutable_child)
|
||||
|
||||
container.layout()
|
||||
|
||||
# Child's layout method should have been called
|
||||
mock_layoutable_child.layout.assert_called_once()
|
||||
|
||||
def test_render_empty_container(self):
|
||||
"""Test rendering empty container"""
|
||||
container = Container(self.origin, self.size)
|
||||
result = container.render()
|
||||
|
||||
self.assertIsInstance(result, Image.Image)
|
||||
self.assertEqual(result.size, tuple(self.size))
|
||||
|
||||
def test_render_with_children(self):
|
||||
"""Test rendering container with children"""
|
||||
container = Container(self.origin, self.size)
|
||||
container.add_child(self.mock_child1)
|
||||
container.add_child(self.mock_child2)
|
||||
|
||||
result = container.render()
|
||||
|
||||
self.assertIsInstance(result, Image.Image)
|
||||
self.assertEqual(result.size, tuple(self.size))
|
||||
|
||||
# Children should have been rendered
|
||||
self.mock_child1.render.assert_called_once()
|
||||
self.mock_child2.render.assert_called_once()
|
||||
|
||||
def test_render_calls_layout(self):
|
||||
"""Test that render calls layout"""
|
||||
container = Container(self.origin, self.size)
|
||||
container.add_child(self.mock_child1)
|
||||
|
||||
with patch.object(container, 'layout') as mock_layout:
|
||||
result = container.render()
|
||||
|
||||
mock_layout.assert_called_once()
|
||||
|
||||
def test_custom_spacing(self):
|
||||
"""Test container with custom spacing"""
|
||||
custom_spacing = 20
|
||||
container = Container(self.origin, self.size, spacing=custom_spacing)
|
||||
container.add_child(self.mock_child1)
|
||||
container.add_child(self.mock_child2)
|
||||
|
||||
container.layout()
|
||||
|
||||
# Second child should be positioned with custom spacing
|
||||
expected_y2 = 10 + 50 + custom_spacing # top padding + first child height + custom spacing
|
||||
self.assertEqual(self.mock_child2._origin[1], expected_y2)
|
||||
|
||||
|
||||
class TestPage(unittest.TestCase):
|
||||
"""Test cases for the Page class"""
|
||||
|
||||
def setUp(self):
|
||||
"""Set up test fixtures"""
|
||||
self.page_size = (800, 600)
|
||||
self.background_color = (255, 255, 255)
|
||||
|
||||
# Create mock child elements
|
||||
self.mock_child1 = Mock()
|
||||
self.mock_child1._size = np.array([200, 100])
|
||||
self.mock_child1._origin = np.array([0, 0])
|
||||
self.mock_child1.render.return_value = Image.new('RGBA', (200, 100), (255, 0, 0, 255))
|
||||
|
||||
self.mock_child2 = Mock()
|
||||
self.mock_child2._size = np.array([150, 80])
|
||||
self.mock_child2._origin = np.array([0, 0])
|
||||
self.mock_child2.render.return_value = Image.new('RGBA', (150, 80), (0, 255, 0, 255))
|
||||
|
||||
def test_page_initialization_basic(self):
|
||||
"""Test basic page initialization"""
|
||||
page = Page()
|
||||
|
||||
np.testing.assert_array_equal(page._origin, np.array([0, 0]))
|
||||
np.testing.assert_array_equal(page._size, np.array([800, 600]))
|
||||
self.assertEqual(page._background_color, (255, 255, 255))
|
||||
self.assertEqual(page._mode, 'RGBA')
|
||||
self.assertEqual(page._direction, 'vertical')
|
||||
self.assertEqual(page._spacing, 10)
|
||||
self.assertEqual(page._halign, Alignment.CENTER)
|
||||
self.assertEqual(page._valign, Alignment.TOP)
|
||||
|
||||
def test_page_initialization_with_params(self):
|
||||
"""Test page initialization with custom parameters"""
|
||||
custom_size = (1024, 768)
|
||||
custom_background = (240, 240, 240)
|
||||
custom_mode = 'RGB'
|
||||
|
||||
page = Page(
|
||||
size=custom_size,
|
||||
background_color=custom_background,
|
||||
mode=custom_mode
|
||||
)
|
||||
|
||||
np.testing.assert_array_equal(page._size, np.array(custom_size))
|
||||
self.assertEqual(page._background_color, custom_background)
|
||||
self.assertEqual(page._mode, custom_mode)
|
||||
|
||||
def test_page_add_child(self):
|
||||
"""Test adding child elements to page"""
|
||||
page = Page()
|
||||
|
||||
page.add_child(self.mock_child1)
|
||||
page.add_child(self.mock_child2)
|
||||
|
||||
self.assertEqual(len(page._children), 2)
|
||||
self.assertEqual(page._children[0], self.mock_child1)
|
||||
self.assertEqual(page._children[1], self.mock_child2)
|
||||
|
||||
def test_page_layout(self):
|
||||
"""Test page layout functionality"""
|
||||
page = Page()
|
||||
page.add_child(self.mock_child1)
|
||||
page.add_child(self.mock_child2)
|
||||
|
||||
page.layout()
|
||||
|
||||
# Children should be positioned vertically, centered horizontally
|
||||
expected_x1 = (800 - 200) // 2 # Centered horizontally
|
||||
expected_y1 = 10 # Top padding
|
||||
np.testing.assert_array_equal(self.mock_child1._origin, np.array([expected_x1, expected_y1]))
|
||||
|
||||
expected_x2 = (800 - 150) // 2 # Centered horizontally
|
||||
expected_y2 = 10 + 100 + 10 # Top padding + first child height + spacing
|
||||
np.testing.assert_array_equal(self.mock_child2._origin, np.array([expected_x2, expected_y2]))
|
||||
|
||||
def test_page_render_empty(self):
|
||||
"""Test rendering empty page"""
|
||||
page = Page(size=self.page_size, background_color=self.background_color)
|
||||
result = page.render()
|
||||
|
||||
self.assertIsInstance(result, Image.Image)
|
||||
self.assertEqual(result.size, self.page_size)
|
||||
self.assertEqual(result.mode, 'RGBA')
|
||||
|
||||
# Check that background color is applied
|
||||
# Sample a pixel from the center to verify background
|
||||
center_pixel = result.getpixel((400, 300))
|
||||
self.assertEqual(center_pixel[:3], self.background_color)
|
||||
|
||||
def test_page_render_with_children_rgba(self):
|
||||
"""Test rendering page with children (RGBA mode)"""
|
||||
page = Page(size=self.page_size, background_color=self.background_color)
|
||||
page.add_child(self.mock_child1)
|
||||
page.add_child(self.mock_child2)
|
||||
|
||||
result = page.render()
|
||||
|
||||
self.assertIsInstance(result, Image.Image)
|
||||
self.assertEqual(result.size, self.page_size)
|
||||
self.assertEqual(result.mode, 'RGBA')
|
||||
|
||||
# Children should have been rendered
|
||||
self.mock_child1.render.assert_called_once()
|
||||
self.mock_child2.render.assert_called_once()
|
||||
|
||||
def test_page_render_with_children_rgb(self):
|
||||
"""Test rendering page with children (RGB mode)"""
|
||||
# Create children that return RGB images
|
||||
rgb_child = Mock()
|
||||
rgb_child._size = np.array([100, 50])
|
||||
rgb_child._origin = np.array([0, 0])
|
||||
rgb_child.render.return_value = Image.new('RGB', (100, 50), (255, 0, 0))
|
||||
|
||||
page = Page(size=self.page_size, background_color=self.background_color, mode='RGB')
|
||||
page.add_child(rgb_child)
|
||||
|
||||
result = page.render()
|
||||
|
||||
self.assertIsInstance(result, Image.Image)
|
||||
self.assertEqual(result.size, self.page_size)
|
||||
self.assertEqual(result.mode, 'RGB')
|
||||
|
||||
rgb_child.render.assert_called_once()
|
||||
|
||||
def test_page_render_calls_layout(self):
|
||||
"""Test that page render calls layout"""
|
||||
page = Page()
|
||||
page.add_child(self.mock_child1)
|
||||
|
||||
with patch.object(page, 'layout') as mock_layout:
|
||||
result = page.render()
|
||||
|
||||
mock_layout.assert_called_once()
|
||||
|
||||
def test_page_inherits_container_functionality(self):
|
||||
"""Test that Page inherits Container functionality"""
|
||||
page = Page()
|
||||
|
||||
# Should inherit Container methods
|
||||
self.assertTrue(hasattr(page, 'add_child'))
|
||||
self.assertTrue(hasattr(page, 'layout'))
|
||||
self.assertTrue(hasattr(page, '_children'))
|
||||
self.assertTrue(hasattr(page, '_direction'))
|
||||
self.assertTrue(hasattr(page, '_spacing'))
|
||||
|
||||
def test_page_with_mixed_child_image_modes(self):
|
||||
"""Test page with children having different image modes"""
|
||||
# Create children with different modes
|
||||
rgba_child = Mock()
|
||||
rgba_child._size = np.array([100, 50])
|
||||
rgba_child._origin = np.array([0, 0])
|
||||
rgba_child.render.return_value = Image.new('RGBA', (100, 50), (255, 0, 0, 255))
|
||||
|
||||
rgb_child = Mock()
|
||||
rgb_child._size = np.array([100, 50])
|
||||
rgb_child._origin = np.array([0, 0])
|
||||
rgb_child.render.return_value = Image.new('RGB', (100, 50), (0, 255, 0))
|
||||
|
||||
page = Page()
|
||||
page.add_child(rgba_child)
|
||||
page.add_child(rgb_child)
|
||||
|
||||
result = page.render()
|
||||
|
||||
self.assertIsInstance(result, Image.Image)
|
||||
self.assertEqual(result.mode, 'RGBA')
|
||||
|
||||
# Both children should have been rendered
|
||||
rgba_child.render.assert_called_once()
|
||||
rgb_child.render.assert_called_once()
|
||||
|
||||
def test_page_background_color_application(self):
|
||||
"""Test that background color is properly applied"""
|
||||
custom_bg = (100, 150, 200)
|
||||
page = Page(background_color=custom_bg)
|
||||
|
||||
result = page.render()
|
||||
|
||||
# Sample multiple points to verify background
|
||||
corners = [(0, 0), (799, 0), (0, 599), (799, 599)]
|
||||
for corner in corners:
|
||||
pixel = result.getpixel(corner)
|
||||
self.assertEqual(pixel[:3], custom_bg)
|
||||
|
||||
def test_page_size_constraints(self):
|
||||
"""Test page with various size constraints"""
|
||||
small_page = Page(size=(200, 150))
|
||||
large_page = Page(size=(1920, 1080))
|
||||
|
||||
small_result = small_page.render()
|
||||
large_result = large_page.render()
|
||||
|
||||
self.assertEqual(small_result.size, (200, 150))
|
||||
self.assertEqual(large_result.size, (1920, 1080))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
468
tests/test_concrete_text.py
Normal file
468
tests/test_concrete_text.py
Normal file
@ -0,0 +1,468 @@
|
||||
"""
|
||||
Unit tests for pyWebLayout.concrete.text module.
|
||||
Tests the Text, RenderableWord, and Line classes for text rendering functionality.
|
||||
"""
|
||||
|
||||
import unittest
|
||||
import numpy as np
|
||||
from PIL import Image, ImageFont
|
||||
from unittest.mock import Mock, patch, MagicMock
|
||||
|
||||
from pyWebLayout.concrete.text import Text, RenderableWord, Line
|
||||
from pyWebLayout.abstract.inline import Word
|
||||
from pyWebLayout.style import Font, FontStyle, FontWeight, TextDecoration
|
||||
from pyWebLayout.style.layout import Alignment
|
||||
|
||||
|
||||
class TestText(unittest.TestCase):
|
||||
"""Test cases for the Text class"""
|
||||
|
||||
def setUp(self):
|
||||
"""Set up test fixtures"""
|
||||
self.font = Font(
|
||||
font_path=None, # Use default font
|
||||
font_size=12,
|
||||
colour=(0, 0, 0),
|
||||
weight=FontWeight.NORMAL,
|
||||
style=FontStyle.NORMAL,
|
||||
decoration=TextDecoration.NONE
|
||||
)
|
||||
self.sample_text = "Hello World"
|
||||
|
||||
def test_text_initialization(self):
|
||||
"""Test basic text initialization"""
|
||||
text = Text(self.sample_text, self.font)
|
||||
|
||||
self.assertEqual(text._text, self.sample_text)
|
||||
self.assertEqual(text._style, self.font)
|
||||
self.assertIsNone(text._line)
|
||||
self.assertIsNone(text._previous)
|
||||
self.assertIsNone(text._next)
|
||||
np.testing.assert_array_equal(text._origin, np.array([0, 0]))
|
||||
|
||||
def test_text_properties(self):
|
||||
"""Test text property accessors"""
|
||||
text = Text(self.sample_text, self.font)
|
||||
|
||||
self.assertEqual(text.text, self.sample_text)
|
||||
self.assertEqual(text.style, self.font)
|
||||
self.assertIsNone(text.line)
|
||||
|
||||
# Test size property
|
||||
self.assertIsInstance(text.size, tuple)
|
||||
self.assertEqual(len(text.size), 2)
|
||||
self.assertGreater(text.width, 0)
|
||||
self.assertGreater(text.height, 0)
|
||||
|
||||
def test_set_origin(self):
|
||||
"""Test setting text origin"""
|
||||
text = Text(self.sample_text, self.font)
|
||||
text.set_origin(50, 75)
|
||||
|
||||
np.testing.assert_array_equal(text._origin, np.array([50, 75]))
|
||||
|
||||
def test_line_assignment(self):
|
||||
"""Test line assignment"""
|
||||
text = Text(self.sample_text, self.font)
|
||||
mock_line = Mock()
|
||||
|
||||
text.line = mock_line
|
||||
self.assertEqual(text.line, mock_line)
|
||||
self.assertEqual(text._line, mock_line)
|
||||
|
||||
def test_add_to_line(self):
|
||||
"""Test adding text to a line"""
|
||||
text = Text(self.sample_text, self.font)
|
||||
mock_line = Mock()
|
||||
|
||||
text.add_to_line(mock_line)
|
||||
self.assertEqual(text._line, mock_line)
|
||||
|
||||
@patch('PIL.ImageDraw.Draw')
|
||||
def test_render_basic(self, mock_draw_class):
|
||||
"""Test basic text rendering"""
|
||||
mock_draw = Mock()
|
||||
mock_draw_class.return_value = mock_draw
|
||||
|
||||
text = Text(self.sample_text, self.font)
|
||||
result = text.render()
|
||||
|
||||
self.assertIsInstance(result, Image.Image)
|
||||
self.assertEqual(result.mode, 'RGBA')
|
||||
mock_draw.text.assert_called_once()
|
||||
|
||||
@patch('PIL.ImageDraw.Draw')
|
||||
def test_render_with_background(self, mock_draw_class):
|
||||
"""Test text rendering with background color"""
|
||||
mock_draw = Mock()
|
||||
mock_draw_class.return_value = mock_draw
|
||||
|
||||
font_with_bg = Font(
|
||||
font_path=None, # Use default font
|
||||
font_size=12,
|
||||
colour=(0, 0, 0),
|
||||
background=(255, 255, 0, 128) # Yellow background with alpha
|
||||
)
|
||||
|
||||
text = Text(self.sample_text, font_with_bg)
|
||||
result = text.render()
|
||||
|
||||
self.assertIsInstance(result, Image.Image)
|
||||
mock_draw.rectangle.assert_called_once()
|
||||
mock_draw.text.assert_called_once()
|
||||
|
||||
@patch('PIL.ImageDraw.Draw')
|
||||
def test_apply_decoration_underline(self, mock_draw_class):
|
||||
"""Test underline decoration"""
|
||||
mock_draw = Mock()
|
||||
mock_draw_class.return_value = mock_draw
|
||||
|
||||
font_underlined = Font(
|
||||
font_path=None, # Use default font
|
||||
font_size=12,
|
||||
colour=(0, 0, 0),
|
||||
decoration=TextDecoration.UNDERLINE
|
||||
)
|
||||
|
||||
text = Text(self.sample_text, font_underlined)
|
||||
text._apply_decoration(mock_draw)
|
||||
|
||||
mock_draw.line.assert_called_once()
|
||||
|
||||
@patch('PIL.ImageDraw.Draw')
|
||||
def test_apply_decoration_strikethrough(self, mock_draw_class):
|
||||
"""Test strikethrough decoration"""
|
||||
mock_draw = Mock()
|
||||
mock_draw_class.return_value = mock_draw
|
||||
|
||||
font_strikethrough = Font(
|
||||
font_path=None, # Use default font
|
||||
font_size=12,
|
||||
colour=(0, 0, 0),
|
||||
decoration=TextDecoration.STRIKETHROUGH
|
||||
)
|
||||
|
||||
text = Text(self.sample_text, font_strikethrough)
|
||||
text._apply_decoration(mock_draw)
|
||||
|
||||
mock_draw.line.assert_called_once()
|
||||
|
||||
def test_in_object_point_inside(self):
|
||||
"""Test in_object method with point inside text"""
|
||||
text = Text(self.sample_text, self.font)
|
||||
text.set_origin(10, 20)
|
||||
|
||||
# Point inside text bounds
|
||||
inside_point = np.array([15, 25])
|
||||
self.assertTrue(text.in_object(inside_point))
|
||||
|
||||
def test_in_object_point_outside(self):
|
||||
"""Test in_object method with point outside text"""
|
||||
text = Text(self.sample_text, self.font)
|
||||
text.set_origin(10, 20)
|
||||
|
||||
# Point outside text bounds
|
||||
outside_point = np.array([200, 200])
|
||||
self.assertFalse(text.in_object(outside_point))
|
||||
|
||||
def test_get_size(self):
|
||||
"""Test get_size method"""
|
||||
text = Text(self.sample_text, self.font)
|
||||
size = text.get_size()
|
||||
|
||||
self.assertIsInstance(size, tuple)
|
||||
self.assertEqual(len(size), 2)
|
||||
self.assertEqual(size, text.size)
|
||||
|
||||
|
||||
class TestRenderableWord(unittest.TestCase):
|
||||
"""Test cases for the RenderableWord class"""
|
||||
|
||||
def setUp(self):
|
||||
"""Set up test fixtures"""
|
||||
self.font = Font(
|
||||
font_path=None, # Use default font
|
||||
font_size=12,
|
||||
colour=(0, 0, 0)
|
||||
)
|
||||
self.abstract_word = Word("testing", self.font)
|
||||
|
||||
def test_renderable_word_initialization(self):
|
||||
"""Test basic RenderableWord initialization"""
|
||||
renderable = RenderableWord(self.abstract_word)
|
||||
|
||||
self.assertEqual(renderable._word, self.abstract_word)
|
||||
self.assertEqual(len(renderable._text_parts), 1)
|
||||
self.assertEqual(renderable._text_parts[0].text, "testing")
|
||||
np.testing.assert_array_equal(renderable._origin, np.array([0, 0]))
|
||||
|
||||
def test_word_property(self):
|
||||
"""Test word property accessor"""
|
||||
renderable = RenderableWord(self.abstract_word)
|
||||
|
||||
self.assertEqual(renderable.word, self.abstract_word)
|
||||
|
||||
def test_text_parts_property(self):
|
||||
"""Test text_parts property"""
|
||||
renderable = RenderableWord(self.abstract_word)
|
||||
|
||||
self.assertIsInstance(renderable.text_parts, list)
|
||||
self.assertEqual(len(renderable.text_parts), 1)
|
||||
self.assertIsInstance(renderable.text_parts[0], Text)
|
||||
|
||||
def test_size_properties(self):
|
||||
"""Test width and height properties"""
|
||||
renderable = RenderableWord(self.abstract_word)
|
||||
|
||||
self.assertGreater(renderable.width, 0)
|
||||
self.assertGreater(renderable.height, 0)
|
||||
self.assertEqual(renderable.width, renderable._size[0])
|
||||
self.assertEqual(renderable.height, renderable._size[1])
|
||||
|
||||
def test_set_origin(self):
|
||||
"""Test setting origin coordinates"""
|
||||
renderable = RenderableWord(self.abstract_word)
|
||||
renderable.set_origin(25, 30)
|
||||
|
||||
np.testing.assert_array_equal(renderable._origin, np.array([25, 30]))
|
||||
# Check that text parts also have updated origins
|
||||
self.assertEqual(renderable._text_parts[0]._origin[0], 25)
|
||||
self.assertEqual(renderable._text_parts[0]._origin[1], 30)
|
||||
|
||||
@patch.object(Word, 'hyphenate')
|
||||
def test_update_from_word_hyphenated(self, mock_hyphenate):
|
||||
"""Test updating from hyphenated word"""
|
||||
# Mock hyphenation
|
||||
mock_hyphenate.return_value = True
|
||||
self.abstract_word._hyphenated_parts = ["test-", "ing"]
|
||||
|
||||
renderable = RenderableWord(self.abstract_word)
|
||||
renderable.update_from_word()
|
||||
|
||||
self.assertEqual(len(renderable._text_parts), 2)
|
||||
self.assertEqual(renderable._text_parts[0].text, "test-")
|
||||
self.assertEqual(renderable._text_parts[1].text, "ing")
|
||||
|
||||
def test_get_part_size(self):
|
||||
"""Test getting size of specific text part"""
|
||||
renderable = RenderableWord(self.abstract_word)
|
||||
|
||||
size = renderable.get_part_size(0)
|
||||
self.assertIsInstance(size, tuple)
|
||||
self.assertEqual(len(size), 2)
|
||||
|
||||
def test_get_part_size_invalid_index(self):
|
||||
"""Test getting size with invalid index"""
|
||||
renderable = RenderableWord(self.abstract_word)
|
||||
|
||||
with self.assertRaises(IndexError):
|
||||
renderable.get_part_size(5)
|
||||
|
||||
def test_render_single_part(self):
|
||||
"""Test rendering word with single part"""
|
||||
renderable = RenderableWord(self.abstract_word)
|
||||
result = renderable.render()
|
||||
|
||||
self.assertIsInstance(result, Image.Image)
|
||||
self.assertGreater(result.width, 0)
|
||||
self.assertGreater(result.height, 0)
|
||||
|
||||
@patch.object(Word, 'hyphenate')
|
||||
def test_render_multiple_parts(self, mock_hyphenate):
|
||||
"""Test rendering word with multiple parts"""
|
||||
# Mock hyphenation
|
||||
mock_hyphenate.return_value = True
|
||||
self.abstract_word._hyphenated_parts = ["test-", "ing"]
|
||||
|
||||
renderable = RenderableWord(self.abstract_word)
|
||||
renderable.update_from_word()
|
||||
result = renderable.render()
|
||||
|
||||
self.assertIsInstance(result, Image.Image)
|
||||
self.assertGreater(result.width, 0)
|
||||
self.assertGreater(result.height, 0)
|
||||
|
||||
def test_in_object_inside(self):
|
||||
"""Test in_object with point inside word"""
|
||||
renderable = RenderableWord(self.abstract_word)
|
||||
renderable.set_origin(10, 15)
|
||||
|
||||
# Point inside word bounds
|
||||
point = np.array([15, 20])
|
||||
# This test might fail if the actual size calculation differs
|
||||
# We'll check that the method returns a boolean
|
||||
result = renderable.in_object(point)
|
||||
self.assertIsInstance(result, (bool, np.bool_))
|
||||
|
||||
def test_in_object_outside(self):
|
||||
"""Test in_object with point outside word"""
|
||||
renderable = RenderableWord(self.abstract_word)
|
||||
renderable.set_origin(10, 15)
|
||||
|
||||
# Point clearly outside word bounds
|
||||
point = np.array([1000, 1000])
|
||||
self.assertFalse(renderable.in_object(point))
|
||||
|
||||
|
||||
class TestLine(unittest.TestCase):
|
||||
"""Test cases for the Line class"""
|
||||
|
||||
def setUp(self):
|
||||
"""Set up test fixtures"""
|
||||
self.font = Font(
|
||||
font_path=None, # Use default font
|
||||
font_size=12,
|
||||
colour=(0, 0, 0)
|
||||
)
|
||||
self.spacing = (5, 10) # min, max spacing
|
||||
self.origin = (0, 0)
|
||||
self.size = (200, 20)
|
||||
|
||||
def test_line_initialization(self):
|
||||
"""Test basic line initialization"""
|
||||
line = Line(self.spacing, self.origin, self.size, self.font)
|
||||
|
||||
self.assertEqual(line._spacing, self.spacing)
|
||||
self.assertEqual(line._font, self.font)
|
||||
self.assertEqual(len(line._renderable_words), 0)
|
||||
self.assertEqual(line._current_width, 0)
|
||||
self.assertIsNone(line._previous)
|
||||
self.assertIsNone(line._next)
|
||||
|
||||
def test_line_initialization_with_previous(self):
|
||||
"""Test line initialization with previous line"""
|
||||
previous_line = Mock()
|
||||
line = Line(self.spacing, self.origin, self.size, self.font, previous=previous_line)
|
||||
|
||||
self.assertEqual(line._previous, previous_line)
|
||||
|
||||
def test_renderable_words_property(self):
|
||||
"""Test renderable_words property"""
|
||||
line = Line(self.spacing, self.origin, self.size, self.font)
|
||||
|
||||
self.assertIsInstance(line.renderable_words, list)
|
||||
self.assertEqual(len(line.renderable_words), 0)
|
||||
|
||||
def test_set_next(self):
|
||||
"""Test setting next line"""
|
||||
line = Line(self.spacing, self.origin, self.size, self.font)
|
||||
next_line = Mock()
|
||||
|
||||
line.set_next(next_line)
|
||||
self.assertEqual(line._next, next_line)
|
||||
|
||||
def test_add_word_fits(self):
|
||||
"""Test adding word that fits in line"""
|
||||
line = Line(self.spacing, self.origin, self.size, self.font)
|
||||
result = line.add_word("short")
|
||||
|
||||
self.assertIsNone(result) # Word fits, no overflow
|
||||
self.assertEqual(len(line._renderable_words), 1)
|
||||
self.assertGreater(line._current_width, 0)
|
||||
|
||||
def test_add_word_overflow(self):
|
||||
"""Test adding word that doesn't fit"""
|
||||
# Create a narrow line
|
||||
narrow_line = Line(self.spacing, self.origin, (50, 20), self.font)
|
||||
|
||||
# Add a long word that won't fit
|
||||
result = narrow_line.add_word("supercalifragilisticexpialidocious")
|
||||
|
||||
# Should return the word text indicating overflow
|
||||
self.assertIsInstance(result, str)
|
||||
|
||||
@patch.object(Word, 'hyphenate')
|
||||
@patch.object(Word, 'get_hyphenated_part')
|
||||
@patch.object(Word, 'get_hyphenated_part_count')
|
||||
def test_add_word_hyphenated(self, mock_part_count, mock_get_part, mock_hyphenate):
|
||||
"""Test adding word that gets hyphenated"""
|
||||
# Mock hyphenation behavior
|
||||
mock_hyphenate.return_value = True
|
||||
mock_get_part.side_effect = lambda i: ["supercalifragilisticexpialidocious-", "remainder"][i]
|
||||
mock_part_count.return_value = 2
|
||||
|
||||
# Use a very narrow line to ensure even the first part doesn't fit
|
||||
narrow_line = Line(self.spacing, self.origin, (30, 20), self.font)
|
||||
result = narrow_line.add_word("supercalifragilisticexpialidocious")
|
||||
|
||||
# Should return the original word since even the first part doesn't fit
|
||||
self.assertIsInstance(result, str)
|
||||
self.assertEqual(result, "supercalifragilisticexpialidocious")
|
||||
|
||||
def test_add_multiple_words(self):
|
||||
"""Test adding multiple words to line"""
|
||||
line = Line(self.spacing, self.origin, self.size, self.font)
|
||||
|
||||
line.add_word("first")
|
||||
line.add_word("second")
|
||||
line.add_word("third")
|
||||
|
||||
self.assertEqual(len(line._renderable_words), 3)
|
||||
self.assertGreater(line._current_width, 0)
|
||||
|
||||
def test_render_empty_line(self):
|
||||
"""Test rendering empty line"""
|
||||
line = Line(self.spacing, self.origin, self.size, self.font)
|
||||
result = line.render()
|
||||
|
||||
self.assertIsInstance(result, Image.Image)
|
||||
self.assertEqual(result.size, tuple(self.size))
|
||||
|
||||
def test_render_with_words_left_aligned(self):
|
||||
"""Test rendering line with left alignment"""
|
||||
line = Line(self.spacing, self.origin, self.size, self.font, halign=Alignment.LEFT)
|
||||
line.add_word("hello")
|
||||
line.add_word("world")
|
||||
|
||||
result = line.render()
|
||||
|
||||
self.assertIsInstance(result, Image.Image)
|
||||
self.assertEqual(result.size, tuple(self.size))
|
||||
|
||||
def test_render_with_words_right_aligned(self):
|
||||
"""Test rendering line with right alignment"""
|
||||
line = Line(self.spacing, self.origin, self.size, self.font, halign=Alignment.RIGHT)
|
||||
line.add_word("hello")
|
||||
line.add_word("world")
|
||||
|
||||
result = line.render()
|
||||
|
||||
self.assertIsInstance(result, Image.Image)
|
||||
self.assertEqual(result.size, tuple(self.size))
|
||||
|
||||
def test_render_with_words_centered(self):
|
||||
"""Test rendering line with center alignment"""
|
||||
line = Line(self.spacing, self.origin, self.size, self.font, halign=Alignment.CENTER)
|
||||
line.add_word("hello")
|
||||
line.add_word("world")
|
||||
|
||||
result = line.render()
|
||||
|
||||
self.assertIsInstance(result, Image.Image)
|
||||
self.assertEqual(result.size, tuple(self.size))
|
||||
|
||||
def test_render_with_words_justified(self):
|
||||
"""Test rendering line with justified alignment"""
|
||||
line = Line(self.spacing, self.origin, self.size, self.font, halign=Alignment.JUSTIFY)
|
||||
line.add_word("hello")
|
||||
line.add_word("world")
|
||||
line.add_word("test")
|
||||
|
||||
result = line.render()
|
||||
|
||||
self.assertIsInstance(result, Image.Image)
|
||||
self.assertEqual(result.size, tuple(self.size))
|
||||
|
||||
def test_render_single_word(self):
|
||||
"""Test rendering line with single word"""
|
||||
line = Line(self.spacing, self.origin, self.size, self.font)
|
||||
line.add_word("single")
|
||||
|
||||
result = line.render()
|
||||
|
||||
self.assertIsInstance(result, Image.Image)
|
||||
self.assertEqual(result.size, tuple(self.size))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
Loading…
x
Reference in New Issue
Block a user