385 lines
16 KiB
Python
385 lines
16 KiB
Python
"""
|
|
Integration tests for document layouter functionality.
|
|
|
|
This test file focuses on realistic scenarios using actual Line, Text, and other
|
|
concrete objects to test the complete integration of word spacing constraints
|
|
in multi-page layout scenarios.
|
|
"""
|
|
|
|
import pytest
|
|
import os
|
|
import logging
|
|
|
|
from pyWebLayout.layout.document_layouter import paragraph_layouter
|
|
from pyWebLayout.style.abstract_style import AbstractStyle
|
|
from pyWebLayout.style.fonts import Font
|
|
from pyWebLayout.style.page_style import PageStyle
|
|
from pyWebLayout.concrete.page import Page
|
|
from pyWebLayout.concrete.text import Line
|
|
from pyWebLayout.abstract.inline import Word
|
|
|
|
# Enable logging to see font loading messages
|
|
logging.basicConfig(level=logging.INFO)
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
def verify_bundled_font_available():
|
|
"""Verify that the bundled font is available for integration tests."""
|
|
# Get the bundled font path
|
|
current_dir = os.path.dirname(os.path.abspath(__file__))
|
|
# Navigate up to pyWebLayout root, then to assets/fonts
|
|
project_root = os.path.dirname(os.path.dirname(current_dir))
|
|
bundled_font_path = os.path.join(
|
|
project_root,
|
|
'pyWebLayout',
|
|
'assets',
|
|
'fonts',
|
|
'DejaVuSans.ttf')
|
|
|
|
logger.info(f"Integration tests checking for bundled font at: {bundled_font_path}")
|
|
|
|
if not os.path.exists(bundled_font_path):
|
|
pytest.fail(
|
|
f"INTEGRATION TEST FAILURE: Bundled font not found at {bundled_font_path}\n"
|
|
f"Integration tests require the bundled font to ensure consistent behavior.\n"
|
|
f"This likely means the font was not included in the package build."
|
|
)
|
|
|
|
logger.info(f"Bundled font found at: {bundled_font_path}")
|
|
return bundled_font_path
|
|
|
|
|
|
class MockWord(Word):
|
|
"""A simple mock word that extends the real Word class."""
|
|
|
|
def __init__(self, text, style=None):
|
|
if style is None:
|
|
# Integration tests MUST use the bundled font for consistency
|
|
style = Font(font_size=16)
|
|
# Verify the font loaded properly
|
|
if style.font.path is None:
|
|
logger.warning(
|
|
"Font loaded without explicit path - may be using PIL default")
|
|
# Initialize the base Word with required parameters
|
|
super().__init__(text, style)
|
|
self._concrete_texts = []
|
|
|
|
def add_concete(self, texts):
|
|
"""Add concrete text representations."""
|
|
if isinstance(texts, list):
|
|
self._concrete_texts.extend(texts)
|
|
else:
|
|
self._concrete_texts.append(texts)
|
|
|
|
def possible_hyphenation(self):
|
|
"""Return possible hyphenation points."""
|
|
if len(self.text) <= 6:
|
|
return []
|
|
|
|
# Simple hyphenation: split roughly in the middle
|
|
mid = len(self.text) // 2
|
|
return [(self.text[:mid] + "-", self.text[mid:])]
|
|
|
|
|
|
class MockParagraph:
|
|
"""A simple paragraph with words and styling."""
|
|
|
|
def __init__(self, text_content, word_spacing_style=None):
|
|
if word_spacing_style is None:
|
|
word_spacing_style = AbstractStyle(
|
|
word_spacing=4.0,
|
|
word_spacing_min=2.0,
|
|
word_spacing_max=8.0
|
|
)
|
|
|
|
self.style = word_spacing_style
|
|
self.line_height = 25
|
|
|
|
# Create words from text content
|
|
self.words = []
|
|
for word_text in text_content.split():
|
|
word = MockWord(word_text)
|
|
self.words.append(word)
|
|
|
|
|
|
class TestDocumentLayouterIntegration:
|
|
"""Integration tests using real components."""
|
|
|
|
@classmethod
|
|
def setup_class(cls):
|
|
"""Verify bundled font is available before running any tests."""
|
|
verify_bundled_font_available()
|
|
|
|
def test_single_page_layout_with_real_components(self):
|
|
"""Test layout on a single page using real Line and Text objects."""
|
|
# Create a real page that can fit content
|
|
page_style = PageStyle(border_width=20, padding=(10, 10, 10, 10))
|
|
page = Page(size=(500, 400), style=page_style)
|
|
|
|
# Create a paragraph with realistic content
|
|
paragraph = MockParagraph(
|
|
"The quick brown fox jumps over the lazy dog and runs through the forest.",
|
|
AbstractStyle(word_spacing=3.0, word_spacing_min=2.0, word_spacing_max=6.0)
|
|
)
|
|
|
|
# Layout the paragraph
|
|
success, failed_word_index, remaining_pretext = paragraph_layouter(
|
|
paragraph, page)
|
|
|
|
# Verify successful layout
|
|
assert success is True
|
|
assert failed_word_index is None
|
|
assert remaining_pretext is None
|
|
|
|
# Verify lines were added to page
|
|
assert len(page.children) > 0
|
|
|
|
# Verify actual Line objects were created
|
|
for child in page.children:
|
|
assert isinstance(child, Line)
|
|
|
|
print(f"✓ Single page test: {len(page.children)} lines created")
|
|
|
|
def test_multi_page_scenario_with_page_overflow(self):
|
|
"""Test realistic multi-page scenario with actual page overflow."""
|
|
# Create a very small real page that will definitely overflow
|
|
small_page_style = PageStyle(border_width=5, padding=(5, 5, 5, 5))
|
|
small_page = Page(size=(150, 80), style=small_page_style)
|
|
|
|
# Create a long paragraph that will definitely overflow
|
|
long_text = " ".join(
|
|
[f"verylongword{i:02d}" for i in range(20)]) # 20 long words
|
|
paragraph = MockParagraph(
|
|
long_text,
|
|
AbstractStyle(word_spacing=4.0, word_spacing_min=2.0, word_spacing_max=8.0)
|
|
)
|
|
|
|
# Layout the paragraph - should fail due to page overflow
|
|
success, failed_word_index, remaining_pretext = paragraph_layouter(
|
|
paragraph, small_page)
|
|
|
|
# Either should fail due to overflow OR succeed with limited content
|
|
if success:
|
|
# If it succeeded, verify it fit some content
|
|
assert len(small_page.children) > 0
|
|
print(
|
|
f"✓ Multi-page test: Content fit on small page, {len(small_page.children)} lines created")
|
|
else:
|
|
# If it failed, verify overflow handling
|
|
assert failed_word_index is not None # Should indicate where it failed
|
|
assert failed_word_index < len(
|
|
paragraph.words) # Should be within word range
|
|
print(
|
|
f"✓ Multi-page test: Page overflow at word {failed_word_index}, {len(small_page.children)} lines fit")
|
|
|
|
def test_word_spacing_constraints_in_real_lines(self):
|
|
"""Test that word spacing constraints are properly used in real Line objects."""
|
|
# Create real page
|
|
page_style = PageStyle(border_width=20, padding=(10, 10, 10, 10))
|
|
page = Page(size=(400, 300), style=page_style)
|
|
|
|
# Create paragraph with specific spacing constraints
|
|
paragraph = MockParagraph(
|
|
"Testing word spacing constraints with realistic content.",
|
|
AbstractStyle(word_spacing=5.0, word_spacing_min=3.0, word_spacing_max=10.0)
|
|
)
|
|
|
|
# Layout paragraph
|
|
success, _, _ = paragraph_layouter(paragraph, page)
|
|
assert success is True
|
|
|
|
# Verify that Line objects were created with correct spacing
|
|
assert len(page.children) > 0
|
|
|
|
for line in page.children:
|
|
assert isinstance(line, Line)
|
|
# Verify spacing constraints were applied
|
|
assert hasattr(line, '_spacing')
|
|
min_spacing, max_spacing = line._spacing
|
|
assert min_spacing == 3 # From our constraint
|
|
assert max_spacing == 10 # From our constraint
|
|
|
|
print(
|
|
f"✓ Word spacing test: {len(page.children)} lines with constraints (3, 10)")
|
|
|
|
def test_different_alignment_strategies_with_constraints(self):
|
|
"""Test different text alignment strategies with word spacing constraints."""
|
|
alignments_to_test = [
|
|
("left", AbstractStyle(
|
|
text_align="left", word_spacing_min=2.0, word_spacing_max=6.0)), ("justify", AbstractStyle(
|
|
text_align="justify", word_spacing_min=3.0, word_spacing_max=12.0)), ("center", AbstractStyle(
|
|
text_align="center", word_spacing_min=1.0, word_spacing_max=5.0))]
|
|
|
|
for alignment_name, style in alignments_to_test:
|
|
page_style = PageStyle(border_width=20, padding=(10, 10, 10, 10))
|
|
page = Page(size=(350, 200), style=page_style)
|
|
paragraph = MockParagraph(
|
|
"This sentence will test different alignment strategies with word spacing.", style)
|
|
|
|
success, _, _ = paragraph_layouter(paragraph, page)
|
|
assert success is True
|
|
assert len(page.children) > 0
|
|
|
|
# Verify alignment was applied to lines
|
|
for line in page.children:
|
|
assert isinstance(line, Line)
|
|
# Check that the alignment handler was set correctly
|
|
assert line._alignment_handler is not None
|
|
|
|
print(f"✓ {alignment_name} alignment: {len(page.children)} lines created")
|
|
|
|
def test_realistic_document_with_multiple_pages(self):
|
|
"""Test a realistic document that spans multiple pages."""
|
|
# Create multiple real pages
|
|
page_style = PageStyle(border_width=20, padding=(10, 10, 10, 10))
|
|
pages = [Page(size=(400, 300), style=page_style) for _ in range(3)]
|
|
|
|
# Create a document with multiple paragraphs
|
|
paragraphs = [
|
|
MockParagraph(
|
|
"This is the first paragraph of our document. It contains enough text to "
|
|
"potentially span multiple lines and test the word spacing constraints properly.",
|
|
AbstractStyle(word_spacing=3.0, word_spacing_min=2.0, word_spacing_max=8.0)
|
|
),
|
|
MockParagraph(
|
|
"Here is a second paragraph with different styling. This paragraph uses "
|
|
"different word spacing constraints to test the flexibility of the system.",
|
|
AbstractStyle(word_spacing=5.0, word_spacing_min=3.0, word_spacing_max=12.0)
|
|
),
|
|
MockParagraph(
|
|
"The third and final paragraph completes our test document. It should "
|
|
"demonstrate that the layouter can handle multiple paragraphs with varying "
|
|
"content lengths and styling requirements.",
|
|
AbstractStyle(word_spacing=4.0, word_spacing_min=2.5, word_spacing_max=10.0)
|
|
)
|
|
]
|
|
|
|
# Layout paragraphs across pages
|
|
current_page_index = 0
|
|
|
|
for para_index, paragraph in enumerate(paragraphs):
|
|
start_word = 0
|
|
|
|
while start_word < len(paragraph.words):
|
|
if current_page_index >= len(pages):
|
|
break # Out of pages
|
|
|
|
current_page = pages[current_page_index]
|
|
success, failed_word_index, _ = paragraph_layouter(
|
|
paragraph, current_page, start_word
|
|
)
|
|
|
|
if success:
|
|
# Paragraph completed on this page
|
|
break
|
|
else:
|
|
# Page full, move to next page
|
|
if failed_word_index is not None:
|
|
start_word = failed_word_index
|
|
current_page_index += 1
|
|
|
|
# If we're out of pages, stop
|
|
if current_page_index >= len(pages):
|
|
break
|
|
|
|
# Verify pages have content
|
|
total_lines = sum(len(page.children) for page in pages)
|
|
pages_used = sum(1 for page in pages if len(page.children) > 0)
|
|
|
|
assert total_lines > 0
|
|
assert pages_used > 1 # Should use multiple pages
|
|
|
|
print(f"✓ Multi-document test: {total_lines} lines across {pages_used} pages")
|
|
|
|
def test_word_spacing_constraint_resolution_integration(self):
|
|
"""Test the complete integration from AbstractStyle to Line spacing."""
|
|
page_style = PageStyle(border_width=20, padding=(10, 10, 10, 10))
|
|
_page = Page(size=(400, 600), style=page_style)
|
|
|
|
# Test different constraint scenarios
|
|
test_cases = [
|
|
{
|
|
"name": "explicit_constraints",
|
|
"style": AbstractStyle(word_spacing=5.0, word_spacing_min=3.0, word_spacing_max=12.0),
|
|
"expected_min": 3,
|
|
"expected_max": 12
|
|
},
|
|
{
|
|
"name": "default_constraints",
|
|
"style": AbstractStyle(word_spacing=6.0),
|
|
"expected_min": 6, # Should use word_spacing as min
|
|
"expected_max": 12 # Should use word_spacing * 2 as max
|
|
},
|
|
{
|
|
"name": "no_word_spacing",
|
|
"style": AbstractStyle(),
|
|
"expected_min": 2, # Default minimum
|
|
"expected_max": 8 # Default based on font size (16 * 0.5)
|
|
}
|
|
]
|
|
|
|
for case in test_cases:
|
|
# Create fresh real page for each test
|
|
test_page_style = PageStyle(border_width=20, padding=(10, 10, 10, 10))
|
|
test_page = Page(size=(400, 600), style=test_page_style)
|
|
paragraph = MockParagraph(
|
|
"Testing constraint resolution with different scenarios.",
|
|
case["style"]
|
|
)
|
|
|
|
success, _, _ = paragraph_layouter(paragraph, test_page)
|
|
assert success is True
|
|
assert len(test_page.children) > 0
|
|
|
|
# Verify constraints were resolved correctly
|
|
line = test_page.children[0]
|
|
min_spacing, max_spacing = line._spacing
|
|
|
|
assert min_spacing == case["expected_min"], f"Min constraint failed for {case['name']}"
|
|
assert max_spacing == case["expected_max"], f"Max constraint failed for {case['name']}"
|
|
|
|
print(f"✓ {case['name']}: constraints ({min_spacing}, {max_spacing})")
|
|
|
|
def test_hyphenation_with_word_spacing_constraints(self):
|
|
"""Test that hyphenation works correctly with word spacing constraints."""
|
|
# Create a narrow real page to force hyphenation
|
|
narrow_page_style = PageStyle(border_width=20, padding=(10, 10, 10, 10))
|
|
narrow_page = Page(size=(200, 300), style=narrow_page_style)
|
|
|
|
# Create paragraph with long words that will need hyphenation
|
|
paragraph = MockParagraph(
|
|
"supercalifragilisticexpialidocious antidisestablishmentarianism",
|
|
AbstractStyle(word_spacing=3.0, word_spacing_min=2.0, word_spacing_max=8.0)
|
|
)
|
|
|
|
success, failed_word_index, remaining_pretext = paragraph_layouter(
|
|
paragraph, narrow_page)
|
|
|
|
# Should succeed with hyphenation or handle overflow gracefully
|
|
if success:
|
|
assert len(narrow_page.children) > 0
|
|
print(f"✓ Hyphenation test: {len(narrow_page.children)} lines created")
|
|
else:
|
|
# If it failed, it should be due to layout constraints, not errors
|
|
assert failed_word_index is not None
|
|
print(f"✓ Hyphenation test: Handled overflow at word {failed_word_index}")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
# Run integration tests
|
|
test = TestDocumentLayouterIntegration()
|
|
|
|
print("Running document layouter integration tests...")
|
|
print("=" * 50)
|
|
|
|
test.test_single_page_layout_with_real_components()
|
|
test.test_multi_page_scenario_with_page_overflow()
|
|
test.test_word_spacing_constraints_in_real_lines()
|
|
test.test_different_alignment_strategies_with_constraints()
|
|
test.test_realistic_document_with_multiple_pages()
|
|
test.test_word_spacing_constraint_resolution_integration()
|
|
test.test_hyphenation_with_word_spacing_constraints()
|
|
|
|
print("=" * 50)
|
|
print("✅ All integration tests completed successfully!")
|