""" 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 from unittest.mock import Mock, patch from PIL import Image, ImageDraw import numpy as np from typing import List, Optional from pyWebLayout.layout.document_layouter import paragraph_layouter, DocumentLayouter from pyWebLayout.style.abstract_style import AbstractStyle from pyWebLayout.style.concrete_style import ConcreteStyle, StyleResolver, RenderingContext from pyWebLayout.style.fonts import Font from pyWebLayout.concrete.text import Line, Text from pyWebLayout.abstract.inline import Word class MockPage: """A realistic mock page that behaves like a real page.""" def __init__(self, width=400, height=600, max_lines=20): self.border_size = 20 self._current_y_offset = 50 self.available_width = width self.available_height = height self.max_lines = max_lines self.lines_added = 0 self.children = [] # Create a real drawing context self.image = Image.new('RGB', (width + 40, height + 100), 'white') self.draw = ImageDraw.Draw(self.image) # Create a real style resolver context = RenderingContext(base_font_size=16) self.style_resolver = StyleResolver(context) def can_fit_line(self, line_height): """Check if another line can fit on the page.""" remaining_height = self.available_height - self._current_y_offset can_fit = remaining_height >= line_height and self.lines_added < self.max_lines return can_fit def add_child(self, child): """Add a child element (like a Line) to the page.""" self.children.append(child) self.lines_added += 1 return True class MockWord(Word): """A simple mock word that extends the real Word class.""" def __init__(self, text, style=None): if style is None: style = Font(font_size=16) # 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.""" def test_single_page_layout_with_real_components(self): """Test layout on a single page using real Line and Text objects.""" # Create a page that can fit content page = MockPage(width=500, height=400, max_lines=10) # 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 assert page.lines_added > 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 page that will definitely overflow small_page = MockPage(width=150, height=80, max_lines=1) # Extremely small page # 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 assert len(small_page.children) <= small_page.max_lines 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 page page = MockPage(width=400, height=300) # 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 = MockPage(width=350, height=200) 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 pages pages = [MockPage(width=400, height=300, max_lines=5) 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 = MockPage() # 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 page for each test test_page = MockPage() 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 page to force hyphenation narrow_page = MockPage(width=200, height=300) # 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!")