pyWebLayout/tests/test_line_splitting_bug.py
Duncan Tourolle df775ee462
Some checks failed
Python CI / test (push) Has been cancelled
view port based browser and test
2025-06-08 17:00:41 +02:00

222 lines
9.2 KiB
Python

#!/usr/bin/env python3
"""
Test to demonstrate and verify fix for the line splitting bug where
text is lost at line breaks due to improper hyphenation handling.
"""
import unittest
from unittest.mock import patch, Mock
from pyWebLayout.concrete.text import Line
from pyWebLayout.abstract.inline import Word
from pyWebLayout.style import Font
from pyWebLayout.style.layout import Alignment
class TestLineSplittingBug(unittest.TestCase):
"""Test cases for the line splitting bug"""
def setUp(self):
"""Set up test fixtures"""
self.font = Font(
font_path=None,
font_size=12,
colour=(0, 0, 0),
min_hyphenation_width=20 # Allow hyphenation in narrow spaces for testing
)
self.spacing = (5, 10)
self.origin = (0, 0)
self.size = (100, 20) # Narrow line to force hyphenation
@patch('pyWebLayout.abstract.inline.pyphen')
def test_hyphenation_preserves_word_boundaries(self, mock_pyphen_module):
"""Test that hyphenation properly preserves word boundaries"""
# Mock pyphen to return a multi-part hyphenated word
mock_dic = Mock()
mock_pyphen_module.Pyphen.return_value = mock_dic
# Simulate hyphenating "supercalifragilisticexpialidocious"
# into multiple parts: "super-", "cali-", "fragi-", "listic-", "expiali-", "docious"
mock_dic.inserted.return_value = "super-cali-fragi-listic-expiali-docious"
line = Line(self.spacing, self.origin, self.size, self.font)
# Add the word that will be hyphenated
overflow = line.add_word("supercalifragilisticexpialidocious")
# The overflow should be the next part only, not all remaining parts joined
# In the current buggy implementation, this would return "cali-fragi-listic-expiali-docious"
# But it should return "cali-" (the next single part)
print(f"Overflow returned: '{overflow}'")
# Check that the first part was added to the line
self.assertEqual(len(line.text_objects), 1)
first_word_text = line.text_objects[0].text
self.assertEqual(first_word_text, "super-")
# The overflow should be just the next part, not all parts joined
# This assertion will fail with the current bug, showing the issue
self.assertEqual(overflow, "cali-") # Should be next part only
# NOT this (which is what the bug produces):
# self.assertEqual(overflow, "cali-fragi-listic-expiali-docious")
@patch('pyWebLayout.abstract.inline.pyphen')
def test_single_word_overflow_behavior(self, mock_pyphen_module):
"""Test that overflow returns only the next part, not all remaining parts joined"""
# Mock pyphen to return a simple two-part hyphenated word
mock_dic = Mock()
mock_pyphen_module.Pyphen.return_value = mock_dic
mock_dic.inserted.return_value = "very-long"
# Create a narrow line that will force hyphenation
line = Line(self.spacing, (0, 0), (40, 20), self.font)
# Add the word that will be hyphenated
overflow = line.add_word("verylong")
# Check that the first part was added to the line
self.assertEqual(len(line.text_objects), 1)
first_word_text = line.text_objects[0].text
self.assertEqual(first_word_text, "very-")
# The overflow should be just the next part ("long"), not multiple parts joined
# This tests the core fix for the line splitting bug
self.assertEqual(overflow, "long")
print(f"First part in line: '{first_word_text}'")
print(f"Overflow returned: '{overflow}'")
def test_simple_overflow_case(self):
"""Test a simple word overflow without hyphenation to verify baseline behavior"""
line = Line(self.spacing, self.origin, (50, 20), self.font)
# Add a word that fits
result1 = line.add_word("short")
self.assertIsNone(result1)
# Add a word that doesn't fit (should overflow)
result2 = line.add_word("verylongword")
self.assertEqual(result2, "verylongword")
# Only the first word should be in the line
self.assertEqual(len(line.text_objects), 1)
self.assertEqual(line.text_objects[0].text, "short")
def test_conservative_justified_hyphenation(self):
"""Test that justified alignment is more conservative about mid-sentence hyphenation"""
font = Font(font_path=None, font_size=12, colour=(0, 0, 0))
line = Line((5, 15), (0, 0), (200, 20), font, halign=Alignment.JUSTIFY)
with patch('pyWebLayout.abstract.inline.pyphen') as mock_pyphen_module:
mock_dic = Mock()
mock_pyphen_module.Pyphen.return_value = mock_dic
mock_dic.inserted.return_value = "test-word"
# Add words that should fit without hyphenation
result1 = line.add_word("This")
result2 = line.add_word("should")
result3 = line.add_word("testword") # Should NOT be hyphenated with conservative settings
self.assertIsNone(result1)
self.assertIsNone(result2)
self.assertIsNone(result3) # Should fit without hyphenation
self.assertEqual(len(line.text_objects), 3)
self.assertEqual([obj.text for obj in line.text_objects], ["This", "should", "testword"])
def test_helper_methods_exist(self):
"""Test that refactored helper methods exist and work"""
font = Font(font_path=None, font_size=12, colour=(0, 0, 0))
line = Line((5, 10), (0, 0), (200, 20), font)
# Test helper methods exist and return reasonable values
available_width = line._calculate_available_width(font)
self.assertIsInstance(available_width, int)
self.assertGreater(available_width, 0)
safety_margin = line._get_safety_margin(font)
self.assertIsInstance(safety_margin, int)
self.assertGreaterEqual(safety_margin, 1)
fits = line._fits_with_normal_spacing(50, 100, font)
self.assertIsInstance(fits, bool)
def test_no_cropping_with_safety_margin(self):
"""Test that safety margin prevents text cropping"""
font = Font(font_path=None, font_size=12, colour=(0, 0, 0))
# Create a line that's just barely wide enough
line = Line((2, 5), (0, 0), (80, 20), font)
# Add words that should fit with safety margin
result1 = line.add_word("test")
result2 = line.add_word("word")
self.assertIsNone(result1)
self.assertIsNone(result2)
# Verify both words were added
self.assertEqual(len(line.text_objects), 2)
self.assertEqual([obj.text for obj in line.text_objects], ["test", "word"])
def test_modular_word_fitting_strategies(self):
"""Test that word fitting strategies work in proper order"""
font = Font(font_path=None, font_size=12, colour=(0, 0, 0))
line = Line((5, 10), (0, 0), (80, 20), font) # Narrower line to force overflow
# Test normal spacing strategy
result1 = line.add_word("short")
self.assertIsNone(result1)
# Test that we can add multiple words
result2 = line.add_word("words")
self.assertIsNone(result2)
# Test overflow handling with a definitely too-long word
result3 = line.add_word("verylongwordthatdefinitelywontfitinnarrowline")
self.assertIsNotNone(result3) # Should return overflow
# Line should have the first two words only
self.assertEqual(len(line.text_objects), 2)
self.assertEqual([obj.text for obj in line.text_objects], ["short", "words"])
def demonstrate_bug():
"""Demonstrate the bug with a practical example"""
print("=" * 60)
print("DEMONSTRATING LINE SPLITTING BUG")
print("=" * 60)
font = Font(font_path=None, font_size=12, colour=(0, 0, 0))
# Create a very narrow line that will force hyphenation
line = Line((3, 6), (0, 0), (80, 20), font)
# Try to add a long word that should be hyphenated
with patch('pyWebLayout.abstract.inline.pyphen') as mock_pyphen_module:
mock_dic = Mock()
mock_pyphen_module.Pyphen.return_value = mock_dic
mock_dic.inserted.return_value = "hyper-long-example-word"
overflow = line.add_word("hyperlongexampleword")
print(f"Original word: 'hyperlongexampleword'")
print(f"Hyphenated to: 'hyper-long-example-word'")
print(f"First part added to line: '{line.text_objects[0].text if line.text_objects else 'None'}'")
print(f"Overflow returned: '{overflow}'")
print()
print("PROBLEM: The overflow should be 'long-' (next part only)")
print("but instead it returns 'long-example-word' (all remaining parts joined)")
print("This causes word boundary information to be lost!")
if __name__ == "__main__":
# First demonstrate the bug
demonstrate_bug()
print("\n" + "=" * 60)
print("RUNNING UNIT TESTS")
print("=" * 60)
# Run unit tests
unittest.main()