diff --git a/tests/test_line_overflow.py b/tests/test_line_overflow.py new file mode 100644 index 0000000..346bed1 --- /dev/null +++ b/tests/test_line_overflow.py @@ -0,0 +1,174 @@ +""" +Test for line overflow behavior in pyWebLayout/concrete/text.py + +This test demonstrates how line overflow is triggered when words cannot fit +in progressively shorter lines. Uses a simple sentence with non-hyphenatable words. +""" + +import unittest +import numpy as np +from PIL import ImageFont +from pyWebLayout.concrete.text import Line, Text +from pyWebLayout.style import Font, FontStyle, FontWeight +from pyWebLayout.style.layout import Alignment + + +class TestLineOverflow(unittest.TestCase): + """Test line overflow behavior with progressively shorter lines.""" + + def setUp(self): + """Set up test fixtures with a basic font and simple sentence.""" + # Create a simple font for testing + self.font = Font() + + # Simple sentence with short, non-hyphenatable words + self.simple_words = ["cat", "dog", "pig", "rat", "ox", "bee", "fly", "ant"] + + def test_line_overflow_progression(self): + """Test that line overflow is triggered as line width decreases.""" + # Start with a reasonably wide line that can fit all words + initial_width = 800 + line_height = 50 + spacing = (5, 20) # min_spacing, max_spacing + + # Test results storage + results = [] + + # Test with progressively shorter lines + for width in range(initial_width, 50, -50): # Decrease by 50px each time + # Create a new line with current width + origin = np.array([0, 0]) + size = (width, line_height) + line = Line(spacing, origin, size, self.font, halign=Alignment.LEFT) + + # Try to add words until overflow occurs + words_added = [] + overflow_word = None + + for word in self.simple_words: + result = line.add_word(word, self.font) + if result is None: + # Word fit in line + words_added.append(word) + else: + # Overflow occurred + overflow_word = word + break + + # Record the test result + test_result = { + 'width': width, + 'words_added': words_added.copy(), + 'overflow_word': overflow_word, + 'total_words_fit': len(words_added) + } + results.append(test_result) + + # Print progress for visibility + print(f"Width {width}px: Fit {len(words_added)} words: {' '.join(words_added)}") + if overflow_word: + print(f" -> Overflow on word: '{overflow_word}'") + + # Assertions to verify expected behavior + self.assertTrue(len(results) > 0, "Should have test results") + + # First (widest) line should fit more words than later lines + first_result = results[0] + last_result = results[-1] + + self.assertGreaterEqual( + first_result['total_words_fit'], + last_result['total_words_fit'], + "Wider lines should fit at least as many words as narrower lines" + ) + + # At some point, overflow should occur (not all words fit) + overflow_occurred = any(r['overflow_word'] is not None for r in results) + self.assertTrue(overflow_occurred, "Line overflow should occur with narrow lines") + + # Very narrow lines should fit fewer words + narrow_results = [r for r in results if r['width'] < 200] + if narrow_results: + # At least one narrow line should have triggered overflow + narrow_overflow = any(r['overflow_word'] is not None for r in narrow_results) + self.assertTrue(narrow_overflow, "Narrow lines should trigger overflow") + + def test_single_word_overflow(self): + """Test overflow behavior when even a single word doesn't fit.""" + # Create a very narrow line + narrow_width = 30 + line_height = 50 + spacing = (5, 20) + + origin = np.array([0, 0]) + size = (narrow_width, line_height) + line = Line(spacing, origin, size, self.font, halign=Alignment.LEFT) + + # Try to add a word that likely won't fit + test_word = "elephant" # Longer word that should overflow + result = line.add_word(test_word, self.font) + + # The implementation may partially fit the word and return the remaining part + + # Some overflow occurred - either full word or remaining part + self.assertIsInstance(result, str, "Should return remaining text as string") + self.assertGreater(len(result), 0, "Remaining text should not be empty") + + # Check that some part was fitted + self.assertGreater(len(line.text_objects), 0, "Should have fitted at least some characters") + + # The fitted part + remaining part should equal the original word + fitted_text = line.text_objects[0].text if line.text_objects else "" + self.assertEqual(fitted_text + result, test_word, + f"Fitted part '{fitted_text}' + remaining '{result}' should equal '{test_word}'") + + print(f"Word '{test_word}' partially fit: '{fitted_text}' fitted, '{result}' remaining") + + + def test_empty_line_behavior(self): + """Test behavior when adding words to an empty line.""" + width = 300 + line_height = 50 + spacing = (5, 20) + + origin = np.array([0, 0]) + size = (width, line_height) + line = Line(spacing, origin, size, self.font, halign=Alignment.LEFT) + + # Initially empty + self.assertEqual(len(line.text_objects), 0, "Line should start empty") + + # Add first word + result = line.add_word("cat", self.font) + self.assertIsNone(result, "First word should fit in reasonable width") + self.assertEqual(len(line.text_objects), 1, "Should have one text object") + self.assertEqual(line.text_objects[0].text, "cat", "Text should match added word") + + def test_progressive_overflow_demonstration(self): + """Demonstrate the exact point where overflow begins.""" + # Use a specific set of short words + words = ["a", "bb", "ccc", "dd", "e"] + + # Start wide and narrow down until we find the overflow point + for width in range(200, 10, -10): + origin = np.array([0, 0]) + size = (width, 50) + line = Line((3, 15), origin, size, self.font, halign=Alignment.LEFT) + + words_that_fit = [] + for word in words: + result = line.add_word(word, self.font) + if result is None: + words_that_fit.append(word) + else: + # This is where overflow started + print(f"Overflow at width {width}px: '{' '.join(words_that_fit)}' + '{word}' (overflow)") + self.assertGreater(len(words_that_fit), 0, "Should fit at least some words before overflow") + return # Test successful + + # If we get here, no overflow occurred even at minimum width + print("No overflow occurred - all words fit even in narrowest line") + + +if __name__ == '__main__': + unittest.main(verbosity=2)