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