222 lines
9.2 KiB
Python
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()
|