paragraph_layouter placed lines at page.border_size while sizing them to available_width, which subtracts both paddings. Text therefore started flush against the left border and the entire padding budget accumulated on the right, so lines broke well short of the right border. Page now describes its content box directly - content_origin, content_rect and remaining_height - and the layouters use it instead of each recomputing the geometry from border_size. The four block layouters had all been computing remaining space as size[1] - y_offset - border_size, subtracting the border but not the bottom padding, so every block type could be placed into the bottom padding; remaining_height fixes that too. Page also gains an origin, defaulting to (0, 0). That is inert for a top-level page but lets a page be positioned inside another surface, which table cells need in order to be laid out by the normal engine. Golden images regenerated: content now sits inside the padding on all sides.
803 lines
32 KiB
Python
803 lines
32 KiB
Python
"""
|
|
Test file for document layouter functionality.
|
|
|
|
This test focuses on verifying that the document layouter properly
|
|
integrates word spacing constraints from the style system.
|
|
"""
|
|
|
|
from unittest.mock import Mock, patch
|
|
|
|
from pyWebLayout.layout.document_layouter import paragraph_layouter, table_layouter, DocumentLayouter
|
|
from pyWebLayout.style.abstract_style import AbstractStyle
|
|
from pyWebLayout.style.concrete_style import StyleResolver, RenderingContext
|
|
from pyWebLayout.abstract.block import Table
|
|
from pyWebLayout.concrete.table import TableStyle
|
|
|
|
|
|
class TestDocumentLayouter:
|
|
"""Test cases for document layouter functionality."""
|
|
|
|
def setup_method(self):
|
|
"""Set up test fixtures before each test method."""
|
|
# Create mock objects
|
|
self.mock_page = Mock()
|
|
self.mock_page.border_size = 20
|
|
self.mock_page._current_y_offset = 50
|
|
self.mock_page.available_width = 400
|
|
# Content geometry: a 440x600 page with a 20px border and no padding, so
|
|
# the content box starts at (20, 20) and is 400 wide.
|
|
self.mock_page.size = (440, 600)
|
|
self.mock_page.content_origin = (20, 20)
|
|
self.mock_page.content_rect = (20, 20, 400, 560)
|
|
self.mock_page.remaining_height = 530 # 20 + 560 - 50
|
|
self.mock_page.draw = Mock()
|
|
self.mock_page.can_fit_line = Mock(return_value=True)
|
|
self.mock_page.add_child = Mock()
|
|
|
|
# Create mock page style with all required numeric properties
|
|
self.mock_page.style = Mock()
|
|
self.mock_page.style.max_font_size = 72 # Reasonable maximum font size
|
|
self.mock_page.style.line_spacing_multiplier = 1.2 # Standard line spacing
|
|
|
|
# Create mock style resolver
|
|
self.mock_style_resolver = Mock()
|
|
self.mock_page.style_resolver = self.mock_style_resolver
|
|
|
|
# Create mock paragraph
|
|
self.mock_paragraph = Mock()
|
|
self.mock_paragraph.line_height = 20
|
|
self.mock_paragraph.style = AbstractStyle()
|
|
|
|
# Create mock words
|
|
self.mock_words = []
|
|
for i in range(5):
|
|
word = Mock()
|
|
word.text = f"word{i}"
|
|
self.mock_words.append(word)
|
|
self.mock_paragraph.words = self.mock_words
|
|
|
|
# Create mock concrete style with word spacing constraints
|
|
self.mock_concrete_style = Mock()
|
|
self.mock_concrete_style.word_spacing_min = 2.0
|
|
self.mock_concrete_style.word_spacing_max = 8.0
|
|
self.mock_concrete_style.text_align = "left"
|
|
|
|
# Create mock font that returns proper numeric metrics (not Mock objects)
|
|
mock_font = Mock()
|
|
# CRITICAL: getmetrics() must return actual numeric values, not Mock objects
|
|
# This prevents "TypeError: '>' not supported between instances of 'Mock'
|
|
# and 'Mock'"
|
|
# (ascent, descent) as actual integers
|
|
mock_font.getmetrics.return_value = (12, 4)
|
|
mock_font.font = mock_font # For accessing .font property
|
|
|
|
# Create mock font object that can be used by create_font
|
|
mock_font_instance = Mock()
|
|
mock_font_instance.font = mock_font
|
|
mock_font_instance.font_size = 16
|
|
mock_font_instance.colour = (0, 0, 0)
|
|
mock_font_instance.background = (255, 255, 255, 0)
|
|
self.mock_concrete_style.create_font = Mock(return_value=mock_font_instance)
|
|
|
|
# Update mock words to have proper style with font
|
|
for word in self.mock_words:
|
|
word.style = Mock()
|
|
word.style.font = mock_font
|
|
word.style.font_size = 16
|
|
word.style.colour = (0, 0, 0)
|
|
word.style.background = None
|
|
|
|
@patch('pyWebLayout.layout.document_layouter.StyleResolver')
|
|
@patch('pyWebLayout.layout.document_layouter.ConcreteStyleRegistry')
|
|
@patch('pyWebLayout.layout.document_layouter.Line')
|
|
def test_paragraph_layouter_basic_flow(
|
|
self,
|
|
mock_line_class,
|
|
mock_style_registry_class,
|
|
mock_style_resolver_class):
|
|
"""Test basic paragraph layouter functionality."""
|
|
# Setup mocks for StyleResolver and ConcreteStyleRegistry
|
|
mock_style_resolver = Mock()
|
|
mock_style_resolver_class.return_value = mock_style_resolver
|
|
|
|
mock_style_registry = Mock()
|
|
mock_style_registry_class.return_value = mock_style_registry
|
|
mock_style_registry.get_concrete_style.return_value = self.mock_concrete_style
|
|
|
|
mock_line = Mock()
|
|
mock_line_class.return_value = mock_line
|
|
mock_line.add_word.return_value = (True, None) # All words fit successfully
|
|
|
|
# Call function
|
|
result = paragraph_layouter(self.mock_paragraph, self.mock_page)
|
|
|
|
# Verify results
|
|
success, failed_word_index, remaining_pretext = result
|
|
assert success is True
|
|
assert failed_word_index is None
|
|
assert remaining_pretext is None
|
|
|
|
# Verify StyleResolver and ConcreteStyleRegistry were created correctly
|
|
mock_style_resolver_class.assert_called_once()
|
|
mock_style_registry_class.assert_called_once_with(mock_style_resolver)
|
|
mock_style_registry.get_concrete_style.assert_called_once_with(
|
|
self.mock_paragraph.style)
|
|
|
|
# Verify Line was created with correct spacing constraints
|
|
expected_spacing = (2, 8) # From mock_concrete_style
|
|
mock_line_class.assert_called_once()
|
|
call_args = mock_line_class.call_args
|
|
assert call_args[1]['spacing'] == expected_spacing
|
|
|
|
@patch('pyWebLayout.layout.document_layouter.StyleResolver')
|
|
@patch('pyWebLayout.layout.document_layouter.ConcreteStyleRegistry')
|
|
@patch('pyWebLayout.layout.document_layouter.Line')
|
|
def test_paragraph_layouter_word_spacing_constraints_extraction(
|
|
self, mock_line_class, mock_style_registry_class, mock_style_resolver_class):
|
|
"""Test that word spacing constraints are correctly extracted from style."""
|
|
# Create concrete style with specific constraints
|
|
concrete_style = Mock()
|
|
concrete_style.word_spacing_min = 5.5
|
|
concrete_style.word_spacing_max = 15.2
|
|
concrete_style.text_align = "justify"
|
|
|
|
# Create a mock font that concrete_style.create_font returns
|
|
mock_font = Mock()
|
|
mock_font.font = Mock()
|
|
mock_font.font.getmetrics.return_value = (12, 4)
|
|
mock_font.font_size = 16
|
|
concrete_style.create_font = Mock(return_value=mock_font)
|
|
|
|
# Setup StyleResolver and ConcreteStyleRegistry mocks
|
|
mock_style_resolver = Mock()
|
|
mock_style_resolver_class.return_value = mock_style_resolver
|
|
|
|
mock_style_registry = Mock()
|
|
mock_style_registry_class.return_value = mock_style_registry
|
|
mock_style_registry.get_concrete_style.return_value = concrete_style
|
|
|
|
mock_line = Mock()
|
|
mock_line_class.return_value = mock_line
|
|
mock_line.add_word.return_value = (True, None)
|
|
|
|
# Call function
|
|
paragraph_layouter(self.mock_paragraph, self.mock_page)
|
|
|
|
# Verify spacing constraints were extracted correctly (converted to int)
|
|
expected_spacing = (5, 15) # int() conversion of 5.5 and 15.2
|
|
call_args = mock_line_class.call_args
|
|
assert call_args[1]['spacing'] == expected_spacing
|
|
|
|
@patch('pyWebLayout.layout.document_layouter.ConcreteStyleRegistry')
|
|
@patch('pyWebLayout.layout.document_layouter.Line')
|
|
@patch('pyWebLayout.layout.document_layouter.Text')
|
|
def test_paragraph_layouter_line_overflow(
|
|
self,
|
|
mock_text_class,
|
|
mock_line_class,
|
|
mock_style_registry_class):
|
|
"""Test handling of line overflow when words don't fit."""
|
|
# Setup mocks
|
|
mock_style_registry = Mock()
|
|
mock_style_registry_class.return_value = mock_style_registry
|
|
mock_style_registry.get_concrete_style.return_value = self.mock_concrete_style
|
|
|
|
# Create two mock lines with proper size attribute
|
|
mock_line1 = Mock()
|
|
mock_line1.size = (400, 20) # (width, height)
|
|
mock_line2 = Mock()
|
|
mock_line2.size = (400, 20) # (width, height)
|
|
mock_line_class.side_effect = [mock_line1, mock_line2]
|
|
|
|
# Mock Text.from_word to return mock text objects with numeric width
|
|
mock_text = Mock()
|
|
mock_text.width = 50 # Reasonable word width
|
|
mock_text_class.from_word.return_value = mock_text
|
|
|
|
# First line: first 2 words fit, third doesn't
|
|
# Second line: remaining words fit
|
|
mock_line1.add_word.side_effect = [
|
|
(True, None), # word0 fits
|
|
(True, None), # word1 fits
|
|
(False, None), # word2 doesn't fit
|
|
]
|
|
mock_line2.add_word.side_effect = [
|
|
(True, None), # word2 fits on new line
|
|
(True, None), # word3 fits
|
|
(True, None), # word4 fits
|
|
]
|
|
|
|
# Call function
|
|
result = paragraph_layouter(self.mock_paragraph, self.mock_page)
|
|
|
|
# Verify results
|
|
success, failed_word_index, remaining_pretext = result
|
|
assert success is True
|
|
assert failed_word_index is None
|
|
assert remaining_pretext is None
|
|
|
|
# Verify two lines were created
|
|
assert mock_line_class.call_count == 2
|
|
assert self.mock_page.add_child.call_count == 2
|
|
|
|
@patch('pyWebLayout.layout.document_layouter.ConcreteStyleRegistry')
|
|
@patch('pyWebLayout.layout.document_layouter.Line')
|
|
def test_paragraph_layouter_page_full(
|
|
self, mock_line_class, mock_style_registry_class):
|
|
"""Test handling when page runs out of space."""
|
|
# Setup mocks
|
|
mock_style_registry = Mock()
|
|
mock_style_registry_class.return_value = mock_style_registry
|
|
mock_style_registry.get_concrete_style.return_value = self.mock_concrete_style
|
|
|
|
# Page can fit first line but not second
|
|
self.mock_page.can_fit_line.side_effect = [True, False]
|
|
|
|
mock_line = Mock()
|
|
mock_line_class.return_value = mock_line
|
|
mock_line.add_word.side_effect = [
|
|
(True, None), # word0 fits
|
|
(False, None), # word1 doesn't fit, need new line
|
|
]
|
|
|
|
# Call function
|
|
result = paragraph_layouter(self.mock_paragraph, self.mock_page)
|
|
|
|
# Verify results indicate page is full
|
|
success, failed_word_index, remaining_pretext = result
|
|
assert success is False
|
|
assert failed_word_index == 1 # word1 didn't fit
|
|
assert remaining_pretext is None
|
|
|
|
def test_paragraph_layouter_empty_paragraph(self):
|
|
"""Test handling of empty paragraph."""
|
|
empty_paragraph = Mock()
|
|
empty_paragraph.words = []
|
|
|
|
result = paragraph_layouter(empty_paragraph, self.mock_page)
|
|
|
|
success, failed_word_index, remaining_pretext = result
|
|
assert success is True
|
|
assert failed_word_index is None
|
|
assert remaining_pretext is None
|
|
|
|
def test_paragraph_layouter_invalid_start_word(self):
|
|
"""Test handling of invalid start_word index."""
|
|
result = paragraph_layouter(self.mock_paragraph, self.mock_page, start_word=10)
|
|
|
|
success, failed_word_index, remaining_pretext = result
|
|
assert success is True
|
|
assert failed_word_index is None
|
|
assert remaining_pretext is None
|
|
|
|
@patch('pyWebLayout.layout.document_layouter.ConcreteStyleRegistry')
|
|
def test_document_layouter_class(self, mock_style_registry_class):
|
|
"""Test DocumentLayouter class functionality."""
|
|
# Setup mock
|
|
mock_style_registry = Mock()
|
|
mock_style_registry_class.return_value = mock_style_registry
|
|
|
|
# Create layouter
|
|
layouter = DocumentLayouter(self.mock_page)
|
|
|
|
# Verify initialization
|
|
assert layouter.page == self.mock_page
|
|
mock_style_registry_class.assert_called_once_with(self.mock_page.style_resolver)
|
|
|
|
@patch('pyWebLayout.layout.document_layouter.paragraph_layouter')
|
|
def test_document_layouter_layout_paragraph(self, mock_paragraph_layouter):
|
|
"""Test DocumentLayouter.layout_paragraph method."""
|
|
mock_paragraph_layouter.return_value = (True, None, None)
|
|
|
|
with patch('pyWebLayout.layout.document_layouter.ConcreteStyleRegistry'):
|
|
layouter = DocumentLayouter(self.mock_page)
|
|
|
|
result = layouter.layout_paragraph(
|
|
self.mock_paragraph, start_word=2, pretext="test")
|
|
|
|
# Verify the function was called correctly
|
|
mock_paragraph_layouter.assert_called_once_with(
|
|
self.mock_paragraph, self.mock_page, 2, "test"
|
|
)
|
|
assert result == (True, None, None)
|
|
|
|
def test_document_layouter_layout_document_success(self):
|
|
"""Test DocumentLayouter.layout_document with successful layout."""
|
|
from pyWebLayout.abstract import Paragraph
|
|
|
|
# Create Mock paragraphs that pass isinstance checks
|
|
paragraphs = [
|
|
Mock(spec=Paragraph),
|
|
Mock(spec=Paragraph),
|
|
Mock(spec=Paragraph)
|
|
]
|
|
|
|
with patch('pyWebLayout.layout.document_layouter.ConcreteStyleRegistry'):
|
|
layouter = DocumentLayouter(self.mock_page)
|
|
|
|
# Mock the layout_paragraph method to return success
|
|
layouter.layout_paragraph = Mock(return_value=(True, None, None))
|
|
|
|
result = layouter.layout_document(paragraphs)
|
|
|
|
assert result is True
|
|
assert layouter.layout_paragraph.call_count == 3
|
|
|
|
def test_document_layouter_layout_document_failure(self):
|
|
"""Test DocumentLayouter.layout_document with layout failure."""
|
|
from pyWebLayout.abstract import Paragraph
|
|
|
|
# Create Mock paragraphs that pass isinstance checks
|
|
paragraphs = [
|
|
Mock(spec=Paragraph),
|
|
Mock(spec=Paragraph)
|
|
]
|
|
|
|
with patch('pyWebLayout.layout.document_layouter.ConcreteStyleRegistry'):
|
|
layouter = DocumentLayouter(self.mock_page)
|
|
|
|
# Mock the layout_paragraph method: first succeeds, second fails
|
|
layouter.layout_paragraph = Mock(side_effect=[
|
|
(True, None, None), # First paragraph succeeds
|
|
(False, 3, None), # Second paragraph fails
|
|
])
|
|
|
|
result = layouter.layout_document(paragraphs)
|
|
|
|
assert result is False
|
|
assert layouter.layout_paragraph.call_count == 2
|
|
|
|
def test_real_style_integration(self):
|
|
"""Test integration with real style system."""
|
|
# Create real style objects
|
|
context = RenderingContext(base_font_size=16)
|
|
resolver = StyleResolver(context)
|
|
|
|
abstract_style = AbstractStyle(
|
|
word_spacing=4.0,
|
|
word_spacing_min=2.0,
|
|
word_spacing_max=10.0
|
|
)
|
|
|
|
concrete_style = resolver.resolve_style(abstract_style)
|
|
|
|
# Verify constraints are resolved correctly
|
|
assert concrete_style.word_spacing_min == 2.0
|
|
assert concrete_style.word_spacing_max == 10.0
|
|
|
|
# This demonstrates the integration works end-to-end
|
|
|
|
|
|
class TestWordSpacingConstraintsInLayout:
|
|
"""Specific tests for word spacing constraints in layout context."""
|
|
|
|
def test_different_spacing_scenarios(self):
|
|
"""Test various word spacing constraint scenarios."""
|
|
context = RenderingContext(base_font_size=16)
|
|
resolver = StyleResolver(context)
|
|
|
|
test_cases = [
|
|
# (word_spacing, word_spacing_min, word_spacing_max, expected_min, expected_max)
|
|
(None, None, None, 2.0, 8.0), # Default case
|
|
(5.0, None, None, 5.0, 10.0), # Only base specified
|
|
(4.0, 2.0, 8.0, 2.0, 8.0), # All specified
|
|
# Min specified, max = max(word_spacing, min*2) = max(3.0, 2.0) = 3.0
|
|
(3.0, 1.0, None, 1.0, 3.0),
|
|
(6.0, None, 12.0, 6.0, 12.0), # Max specified, min from base
|
|
]
|
|
|
|
for word_spacing, min_spacing, max_spacing, expected_min, expected_max in test_cases:
|
|
style_kwargs = {}
|
|
if word_spacing is not None:
|
|
style_kwargs['word_spacing'] = word_spacing
|
|
if min_spacing is not None:
|
|
style_kwargs['word_spacing_min'] = min_spacing
|
|
if max_spacing is not None:
|
|
style_kwargs['word_spacing_max'] = max_spacing
|
|
|
|
abstract_style = AbstractStyle(**style_kwargs)
|
|
concrete_style = resolver.resolve_style(abstract_style)
|
|
|
|
assert concrete_style.word_spacing_min == expected_min, f"Failed for case: {style_kwargs}"
|
|
assert concrete_style.word_spacing_max == expected_max, f"Failed for case: {style_kwargs}"
|
|
|
|
|
|
class TestMultiPageLayout:
|
|
"""Test cases for multi-page document layout scenarios."""
|
|
|
|
def setup_method(self):
|
|
"""Set up test fixtures for multi-page tests."""
|
|
# Create multiple mock pages
|
|
self.mock_pages = []
|
|
for i in range(3):
|
|
page = Mock()
|
|
page.page_number = i + 1
|
|
page.border_size = 20
|
|
page._current_y_offset = 50
|
|
page.available_width = 400
|
|
page.available_height = 600
|
|
page.draw = Mock()
|
|
page.add_child = Mock()
|
|
page.style_resolver = Mock()
|
|
self.mock_pages.append(page)
|
|
|
|
# Create a long paragraph that will span multiple pages
|
|
self.long_paragraph = Mock()
|
|
self.long_paragraph.line_height = 25
|
|
self.long_paragraph.style = AbstractStyle()
|
|
|
|
# Create many words to ensure page overflow
|
|
self.long_paragraph.words = []
|
|
for i in range(50): # 50 words should definitely overflow a page
|
|
word = Mock()
|
|
word.text = f"word_{i:02d}"
|
|
self.long_paragraph.words.append(word)
|
|
|
|
# Create mock concrete style
|
|
self.mock_concrete_style = Mock()
|
|
self.mock_concrete_style.word_spacing_min = 3.0
|
|
self.mock_concrete_style.word_spacing_max = 12.0
|
|
self.mock_concrete_style.text_align = "justify"
|
|
self.mock_concrete_style.create_font = Mock()
|
|
|
|
@patch('pyWebLayout.layout.document_layouter.ConcreteStyleRegistry')
|
|
def test_document_layouter_multi_page_scenario(self, mock_style_registry_class):
|
|
"""Test DocumentLayouter handling multiple pages with continuation."""
|
|
# Setup style registry
|
|
mock_style_registry = Mock()
|
|
mock_style_registry_class.return_value = mock_style_registry
|
|
mock_style_registry.get_concrete_style.return_value = self.mock_concrete_style
|
|
|
|
# Create a multi-page document layouter
|
|
class MultiPageDocumentLayouter(DocumentLayouter):
|
|
def __init__(self, pages):
|
|
self.pages = pages
|
|
self.current_page_index = 0
|
|
self.page = pages[0]
|
|
self.style_registry = Mock()
|
|
|
|
def get_next_page(self):
|
|
"""Get the next available page."""
|
|
if self.current_page_index + 1 < len(self.pages):
|
|
self.current_page_index += 1
|
|
self.page = self.pages[self.current_page_index]
|
|
return self.page
|
|
return None
|
|
|
|
def layout_document_with_pagination(self, paragraphs):
|
|
"""Layout document with automatic pagination."""
|
|
for paragraph in paragraphs:
|
|
start_word = 0
|
|
pretext = None
|
|
|
|
while start_word < len(paragraph.words):
|
|
complete, next_word, remaining_pretext = self.layout_paragraph(
|
|
paragraph, start_word, pretext
|
|
)
|
|
|
|
if complete:
|
|
# Paragraph finished
|
|
break
|
|
|
|
if next_word is None:
|
|
# Error condition
|
|
return False, f"Failed to layout paragraph at word {start_word}"
|
|
|
|
# Try to get next page
|
|
next_page = self.get_next_page()
|
|
if not next_page:
|
|
return False, f"Ran out of pages at word {next_word}"
|
|
|
|
# Continue with remaining words on next page
|
|
start_word = next_word
|
|
pretext = remaining_pretext
|
|
|
|
return True, "All paragraphs laid out successfully"
|
|
|
|
# Create layouter with multiple pages
|
|
layouter = MultiPageDocumentLayouter(self.mock_pages)
|
|
|
|
# Mock the layout_paragraph method to simulate page filling
|
|
layouter.layout_paragraph
|
|
call_count = [0]
|
|
|
|
def mock_layout_paragraph(paragraph, start_word=0, pretext=None):
|
|
call_count[0] += 1
|
|
|
|
# Simulate different scenarios based on call count
|
|
if call_count[0] == 1:
|
|
# First page: can fit words 0-19, fails at word 20
|
|
return (False, 20, None)
|
|
elif call_count[0] == 2:
|
|
# Second page: can fit words 20-39, fails at word 40
|
|
return (False, 40, None)
|
|
elif call_count[0] == 3:
|
|
# Third page: can fit remaining words 40-49
|
|
return (True, None, None)
|
|
else:
|
|
return (False, start_word, None)
|
|
|
|
layouter.layout_paragraph = mock_layout_paragraph
|
|
|
|
# Test multi-page layout
|
|
success, message = layouter.layout_document_with_pagination(
|
|
[self.long_paragraph])
|
|
|
|
# Verify results
|
|
assert success is True
|
|
assert "successfully" in message
|
|
assert call_count[0] == 3 # Should have made 3 layout attempts
|
|
assert layouter.current_page_index == 2 # Should end on page 3 (index 2)
|
|
|
|
def test_realistic_multi_page_scenario(self):
|
|
"""Test a realistic scenario with actual content and page constraints."""
|
|
# Create realistic paragraph with varied content
|
|
realistic_paragraph = Mock()
|
|
realistic_paragraph.line_height = 20
|
|
realistic_paragraph.style = AbstractStyle(
|
|
word_spacing=4.0,
|
|
word_spacing_min=2.0,
|
|
word_spacing_max=8.0,
|
|
text_align="justify"
|
|
)
|
|
|
|
# Create words of varying lengths (realistic text)
|
|
words = [
|
|
"The", "quick", "brown", "fox", "jumps", "over", "the", "lazy", "dog.",
|
|
"This", "sentence", "contains", "words", "of", "varying", "lengths",
|
|
"to", "simulate", "realistic", "text", "content", "that", "would",
|
|
"require", "proper", "word", "spacing", "calculations", "and",
|
|
"potentially", "multiple", "pages", "for", "layout.", "Each",
|
|
"word", "represents", "a", "challenge", "for", "the", "layouter",
|
|
"system", "to", "handle", "appropriately", "with", "the", "given",
|
|
"constraints", "and", "spacing", "requirements."
|
|
]
|
|
|
|
realistic_paragraph.words = []
|
|
for word_text in words:
|
|
word = Mock()
|
|
word.text = word_text
|
|
realistic_paragraph.words.append(word)
|
|
|
|
# Create page with realistic constraints
|
|
realistic_page = Mock()
|
|
realistic_page.border_size = 30
|
|
realistic_page._current_y_offset = 100
|
|
realistic_page.available_width = 350 # Narrower page
|
|
realistic_page.available_height = 500
|
|
realistic_page.draw = Mock()
|
|
realistic_page.add_child = Mock()
|
|
realistic_page.style_resolver = Mock()
|
|
|
|
# Simulate page that can fit approximately 20 lines
|
|
lines_fitted = [0]
|
|
max_lines = 20
|
|
|
|
def realistic_can_fit_line(line_height):
|
|
lines_fitted[0] += 1
|
|
return lines_fitted[0] <= max_lines
|
|
|
|
realistic_page.can_fit_line = realistic_can_fit_line
|
|
|
|
# Test with real style system
|
|
context = RenderingContext(base_font_size=14)
|
|
resolver = StyleResolver(context)
|
|
concrete_style = resolver.resolve_style(realistic_paragraph.style)
|
|
|
|
# Verify realistic constraints were calculated
|
|
assert concrete_style.word_spacing == 4.0
|
|
assert concrete_style.word_spacing_min == 2.0
|
|
assert concrete_style.word_spacing_max == 8.0
|
|
|
|
# This test demonstrates the integration without mocking everything
|
|
# In a real scenario, this would interface with actual Line and Text objects
|
|
print("✓ Realistic scenario test completed")
|
|
print(f" - Words to layout: {len(realistic_paragraph.words)}")
|
|
print(f" - Page width: {realistic_page.available_width}px")
|
|
print(
|
|
f" - Word spacing constraints: {concrete_style.word_spacing_min}-{concrete_style.word_spacing_max}px")
|
|
|
|
|
|
class TestTableLayouter:
|
|
"""Test cases for table layouter functionality."""
|
|
|
|
def setup_method(self):
|
|
"""Set up test fixtures before each test method."""
|
|
# Create mock page
|
|
self.mock_page = Mock()
|
|
self.mock_page.border_size = 20
|
|
self.mock_page._current_y_offset = 50
|
|
self.mock_page.available_width = 600
|
|
self.mock_page.size = (800, 1000)
|
|
# Content geometry: 800x1000 page, 20px border, no padding.
|
|
self.mock_page.content_origin = (20, 20)
|
|
self.mock_page.content_rect = (20, 20, 600, 960)
|
|
self.mock_page.remaining_height = 930 # 20 + 960 - 50
|
|
|
|
# Create mock draw and canvas
|
|
self.mock_draw = Mock()
|
|
self.mock_canvas = Mock()
|
|
self.mock_page.draw = self.mock_draw
|
|
self.mock_page._canvas = self.mock_canvas
|
|
|
|
# Create mock table
|
|
self.mock_table = Mock(spec=Table)
|
|
|
|
# Create mock style resolver
|
|
self.mock_style_resolver = Mock()
|
|
self.mock_page.style_resolver = self.mock_style_resolver
|
|
|
|
@patch('pyWebLayout.layout.document_layouter.TableRenderer')
|
|
def test_table_layouter_success(self, mock_table_renderer_class):
|
|
"""Test table_layouter with successful table rendering."""
|
|
# Setup mock renderer
|
|
mock_renderer = Mock()
|
|
mock_table_renderer_class.return_value = mock_renderer
|
|
mock_renderer.size = (500, 200) # Table fits on page
|
|
|
|
# Call function
|
|
result = table_layouter(self.mock_table, self.mock_page)
|
|
|
|
# Verify results
|
|
assert result is True
|
|
|
|
# Verify TableRenderer was created with correct parameters
|
|
mock_table_renderer_class.assert_called_once_with(
|
|
table=self.mock_table,
|
|
origin=(20, 50), # (border_size, current_y_offset)
|
|
available_width=600,
|
|
draw=self.mock_draw,
|
|
style=None,
|
|
canvas=self.mock_canvas
|
|
)
|
|
|
|
# Verify render was called
|
|
mock_renderer.render.assert_called_once()
|
|
|
|
# Verify y_offset was updated
|
|
assert self.mock_page._current_y_offset == 250 # 50 + 200
|
|
|
|
@patch('pyWebLayout.layout.document_layouter.TableRenderer')
|
|
def test_table_layouter_with_custom_style(self, mock_table_renderer_class):
|
|
"""Test table_layouter with custom TableStyle."""
|
|
# Create custom style
|
|
custom_style = TableStyle(
|
|
border_width=2,
|
|
border_color=(100, 100, 100),
|
|
cell_padding=(10, 10, 10, 10)
|
|
)
|
|
|
|
# Setup mock renderer
|
|
mock_renderer = Mock()
|
|
mock_table_renderer_class.return_value = mock_renderer
|
|
mock_renderer.size = (500, 150)
|
|
|
|
# Call function with style
|
|
result = table_layouter(self.mock_table, self.mock_page, style=custom_style)
|
|
|
|
# Verify TableRenderer was created with custom style
|
|
assert result is True
|
|
call_args = mock_table_renderer_class.call_args
|
|
assert call_args[1]['style'] == custom_style
|
|
|
|
@patch('pyWebLayout.layout.document_layouter.TableRenderer')
|
|
def test_table_layouter_table_too_large(self, mock_table_renderer_class):
|
|
"""Test table_layouter when table doesn't fit on page."""
|
|
# Setup mock renderer with table larger than available space
|
|
mock_renderer = Mock()
|
|
mock_table_renderer_class.return_value = mock_renderer
|
|
mock_renderer.size = (500, 1000) # Table height exceeds available space
|
|
|
|
# Available height = page_size[1] - y_offset - border_size
|
|
# = 1000 - 50 - 20 = 930, but table is 1000 pixels tall
|
|
|
|
# Call function
|
|
result = table_layouter(self.mock_table, self.mock_page)
|
|
|
|
# Verify failure
|
|
assert result is False
|
|
|
|
# Verify render was NOT called
|
|
mock_renderer.render.assert_not_called()
|
|
|
|
# Verify y_offset was NOT updated
|
|
assert self.mock_page._current_y_offset == 50
|
|
|
|
@patch('pyWebLayout.layout.document_layouter.ConcreteStyleRegistry')
|
|
def test_document_layouter_layout_table(self, mock_style_registry_class):
|
|
"""Test DocumentLayouter.layout_table method."""
|
|
# Setup mocks
|
|
mock_style_registry = Mock()
|
|
mock_style_registry_class.return_value = mock_style_registry
|
|
|
|
layouter = DocumentLayouter(self.mock_page)
|
|
|
|
# Mock the table_layouter function
|
|
with patch('pyWebLayout.layout.document_layouter.table_layouter') as mock_table_layouter:
|
|
mock_table_layouter.return_value = True
|
|
|
|
custom_style = TableStyle(border_width=1)
|
|
result = layouter.layout_table(self.mock_table, style=custom_style)
|
|
|
|
# Verify the function was called correctly
|
|
mock_table_layouter.assert_called_once_with(
|
|
self.mock_table, self.mock_page, custom_style
|
|
)
|
|
assert result is True
|
|
|
|
def test_document_layouter_layout_document_with_table(self):
|
|
"""Test DocumentLayouter.layout_document with Table elements."""
|
|
from pyWebLayout.abstract import Paragraph
|
|
from pyWebLayout.abstract.block import Image as AbstractImage
|
|
|
|
# Create mixed elements
|
|
elements = [
|
|
Mock(spec=Paragraph),
|
|
Mock(spec=Table),
|
|
Mock(spec=AbstractImage),
|
|
Mock(spec=Table)
|
|
]
|
|
|
|
with patch('pyWebLayout.layout.document_layouter.ConcreteStyleRegistry'):
|
|
layouter = DocumentLayouter(self.mock_page)
|
|
|
|
# Mock the layout methods
|
|
layouter.layout_paragraph = Mock(return_value=(True, None, None))
|
|
layouter.layout_table = Mock(return_value=True)
|
|
layouter.layout_image = Mock(return_value=True)
|
|
|
|
result = layouter.layout_document(elements)
|
|
|
|
# Verify all elements were laid out
|
|
assert result is True
|
|
assert layouter.layout_paragraph.call_count == 1
|
|
assert layouter.layout_table.call_count == 2
|
|
assert layouter.layout_image.call_count == 1
|
|
|
|
def test_document_layouter_layout_document_table_failure(self):
|
|
"""Test DocumentLayouter.layout_document when table layout fails."""
|
|
# Create elements with table that will fail
|
|
elements = [
|
|
Mock(spec=Table),
|
|
Mock(spec=Table)
|
|
]
|
|
|
|
with patch('pyWebLayout.layout.document_layouter.ConcreteStyleRegistry'):
|
|
layouter = DocumentLayouter(self.mock_page)
|
|
|
|
# Mock layout_table: first succeeds, second fails
|
|
layouter.layout_table = Mock(side_effect=[True, False])
|
|
|
|
result = layouter.layout_document(elements)
|
|
|
|
# Verify it stopped after failure
|
|
assert result is False
|
|
assert layouter.layout_table.call_count == 2
|
|
|
|
|
|
if __name__ == "__main__":
|
|
# Run specific tests for debugging
|
|
test = TestDocumentLayouter()
|
|
test.setup_method()
|
|
|
|
# Run a simple test
|
|
with patch('pyWebLayout.layout.document_layouter.ConcreteStyleRegistry') as mock_registry:
|
|
with patch('pyWebLayout.layout.document_layouter.Line') as mock_line:
|
|
mock_style_registry = Mock()
|
|
mock_registry.return_value = mock_style_registry
|
|
mock_style_registry.get_concrete_style.return_value = test.mock_concrete_style
|
|
|
|
mock_line_instance = Mock()
|
|
mock_line.return_value = mock_line_instance
|
|
mock_line_instance.add_word.return_value = (True, None)
|
|
|
|
result = paragraph_layouter(test.mock_paragraph, test.mock_page)
|
|
print(f"Test result: {result}")
|
|
|
|
# Run multi-page tests
|
|
multi_test = TestMultiPageLayout()
|
|
multi_test.setup_method()
|
|
multi_test.test_realistic_multi_page_scenario()
|
|
|
|
print("Document layouter tests completed!")
|