pyWebLayout/tests/test_concrete_text.py
Duncan Tourolle 3f9bd0072e
All checks were successful
Python CI / test (push) Successful in 48s
test for concrete
2025-06-07 19:43:59 +02:00

469 lines
17 KiB
Python

"""
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()