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