pyWebLayout/tests/test_concrete_text.py
Duncan Tourolle 8c35cbf5ce
Some checks failed
Python CI / test (push) Failing after 4m8s
Improved handling of pagnination.
2025-06-08 13:29:44 +02:00

366 lines
13 KiB
Python

"""
Unit tests for pyWebLayout.concrete.text module.
Tests the Text 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, 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 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._text_objects), 0) # Updated to _text_objects
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 (compatibility)"""
line = Line(self.spacing, self.origin, self.size, self.font)
self.assertIsInstance(line.renderable_words, list)
self.assertEqual(len(line.renderable_words), 0)
# Test that it returns the same as text_objects
self.assertEqual(line.renderable_words, line.text_objects)
def test_text_objects_property(self):
"""Test text_objects property"""
line = Line(self.spacing, self.origin, self.size, self.font)
self.assertIsInstance(line.text_objects, list)
self.assertEqual(len(line.text_objects), 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._text_objects), 1) # Updated to _text_objects
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._text_objects), 3) # Updated to _text_objects
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))
def test_text_objects_contain_text_instances(self):
"""Test that text_objects contain Text instances"""
line = Line(self.spacing, self.origin, self.size, self.font)
line.add_word("test")
self.assertEqual(len(line.text_objects), 1)
self.assertIsInstance(line.text_objects[0], Text)
self.assertEqual(line.text_objects[0].text, "test")
def test_text_objects_linked_to_line(self):
"""Test that Text objects are properly linked to the line"""
line = Line(self.spacing, self.origin, self.size, self.font)
line.add_word("test")
text_obj = line.text_objects[0]
self.assertEqual(text_obj.line, line)
if __name__ == '__main__':
unittest.main()