""" Tests for the ereader layout system components. This module tests: - RenderingPosition: Position tracking and serialization - ChapterNavigator: Chapter detection and navigation - FontScaler: Font scaling utilities - BidirectionalLayouter: Forward/backward page rendering """ import pytest from pyWebLayout.layout.ereader_layout import ( RenderingPosition, ChapterNavigator, ChapterInfo, FontScaler, BidirectionalLayouter ) from pyWebLayout.abstract.block import Paragraph, Heading, HeadingLevel from pyWebLayout.abstract.inline import Word from pyWebLayout.style import Font from pyWebLayout.style.page_style import PageStyle # ============================================================================ # Fixtures # ============================================================================ @pytest.fixture def sample_font(): """Create a standard font for testing.""" return Font(font_size=12, colour=(0, 0, 0)) @pytest.fixture def sample_blocks_with_headings(sample_font): """Create a sample document structure with headings for testing.""" blocks = [] # H1 - Chapter 1 h1 = Heading(HeadingLevel.H1, sample_font) h1.add_word(Word("Chapter", sample_font)) h1.add_word(Word("One", sample_font)) blocks.append(h1) # Paragraph p1 = Paragraph(sample_font) p1.add_word(Word("This", sample_font)) p1.add_word(Word("is", sample_font)) p1.add_word(Word("content", sample_font)) blocks.append(p1) # H2 - Section 1.1 h2 = Heading(HeadingLevel.H2, sample_font) h2.add_word(Word("Section", sample_font)) h2.add_word(Word("1.1", sample_font)) blocks.append(h2) # Another paragraph p2 = Paragraph(sample_font) p2.add_word(Word("More", sample_font)) p2.add_word(Word("text", sample_font)) blocks.append(p2) # H1 - Chapter 2 h1_2 = Heading(HeadingLevel.H1, sample_font) h1_2.add_word(Word("Chapter", sample_font)) h1_2.add_word(Word("Two", sample_font)) blocks.append(h1_2) return blocks @pytest.fixture def sample_page_style(): """Create a standard page style for testing.""" return PageStyle() # ============================================================================ # RenderingPosition Tests # ============================================================================ class TestRenderingPosition: """Tests for the RenderingPosition dataclass.""" def test_default_initialization(self): """Test RenderingPosition initializes with default values.""" pos = RenderingPosition() assert pos.chapter_index == 0 assert pos.block_index == 0 assert pos.word_index == 0 assert pos.table_row == 0 assert pos.table_col == 0 assert pos.list_item_index == 0 assert pos.remaining_pretext is None assert pos.page_y_offset == 0 def test_custom_initialization(self): """Test RenderingPosition with custom values.""" pos = RenderingPosition( chapter_index=2, block_index=5, word_index=10, table_row=1, table_col=2, list_item_index=3, remaining_pretext="test", page_y_offset=100 ) assert pos.chapter_index == 2 assert pos.block_index == 5 assert pos.word_index == 10 assert pos.table_row == 1 assert pos.table_col == 2 assert pos.list_item_index == 3 assert pos.remaining_pretext == "test" assert pos.page_y_offset == 100 def test_to_dict_serialization(self): """Test serialization to dictionary.""" pos = RenderingPosition( chapter_index=1, block_index=2, word_index=3 ) result = pos.to_dict() assert isinstance(result, dict) assert result['chapter_index'] == 1 assert result['block_index'] == 2 assert result['word_index'] == 3 assert 'table_row' in result assert 'remaining_pretext' in result def test_from_dict_deserialization(self): """Test deserialization from dictionary.""" data = { 'chapter_index': 2, 'block_index': 4, 'word_index': 6, 'table_row': 1, 'table_col': 0, 'list_item_index': 0, 'remaining_pretext': None, 'page_y_offset': 50 } pos = RenderingPosition.from_dict(data) assert pos.chapter_index == 2 assert pos.block_index == 4 assert pos.word_index == 6 assert pos.page_y_offset == 50 def test_round_trip_serialization(self): """Test serialization and deserialization round trip.""" original = RenderingPosition( chapter_index=3, block_index=7, word_index=15, remaining_pretext="hyphen-", page_y_offset=200 ) # Serialize and deserialize data = original.to_dict() restored = RenderingPosition.from_dict(data) assert original == restored def test_copy_creates_independent_copy(self): """Test that copy() creates an independent copy.""" original = RenderingPosition( chapter_index=1, block_index=2, word_index=3 ) copy = original.copy() # Verify values match assert copy.chapter_index == original.chapter_index assert copy.block_index == original.block_index assert copy.word_index == original.word_index # Modify copy and verify original is unchanged copy.chapter_index = 99 copy.word_index = 100 assert original.chapter_index == 1 assert original.word_index == 3 def test_equality_same_values(self): """Test equality comparison with same values.""" pos1 = RenderingPosition(chapter_index=1, block_index=2) pos2 = RenderingPosition(chapter_index=1, block_index=2) assert pos1 == pos2 def test_equality_different_values(self): """Test equality comparison with different values.""" pos1 = RenderingPosition(chapter_index=1, block_index=2) pos2 = RenderingPosition(chapter_index=1, block_index=3) assert pos1 != pos2 def test_equality_with_non_position(self): """Test equality comparison with non-RenderingPosition object.""" pos = RenderingPosition() assert pos != "not a position" assert pos != 42 assert pos is not None def test_hashability(self): """Test that RenderingPosition is hashable and can be used in sets/dicts.""" pos1 = RenderingPosition(chapter_index=1, block_index=2) pos2 = RenderingPosition(chapter_index=1, block_index=2) pos3 = RenderingPosition(chapter_index=1, block_index=3) # Test in set position_set = {pos1, pos2, pos3} assert len(position_set) == 2 # pos1 and pos2 should be same # Test as dict key position_dict = {pos1: "value1"} assert position_dict[pos2] == "value1" # pos2 should access same key def test_hash_consistency(self): """Test that hash values are consistent.""" pos1 = RenderingPosition(chapter_index=5, block_index=10) pos2 = RenderingPosition(chapter_index=5, block_index=10) assert hash(pos1) == hash(pos2) # ============================================================================ # ChapterNavigator Tests # ============================================================================ class TestChapterNavigator: """Tests for the ChapterNavigator class.""" def test_initialization(self, sample_blocks_with_headings): """Test ChapterNavigator initialization.""" navigator = ChapterNavigator(sample_blocks_with_headings) assert navigator.blocks == sample_blocks_with_headings assert len(navigator.chapters) > 0 def test_build_chapter_map_finds_headings(self, sample_blocks_with_headings): """Test that chapter map correctly identifies headings.""" navigator = ChapterNavigator(sample_blocks_with_headings) # Should find 3 headings: H1, H2, H1 assert len(navigator.chapters) == 3 def test_chapter_info_properties(self, sample_blocks_with_headings): """Test ChapterInfo contains correct properties.""" navigator = ChapterNavigator(sample_blocks_with_headings) first_chapter = navigator.chapters[0] assert isinstance(first_chapter, ChapterInfo) assert first_chapter.title == "Chapter One" assert first_chapter.level == HeadingLevel.H1 assert isinstance(first_chapter.position, RenderingPosition) assert first_chapter.block_index == 0 def test_heading_text_extraction(self, sample_blocks_with_headings): """Test extraction of heading text from Word objects.""" navigator = ChapterNavigator(sample_blocks_with_headings) # Check extracted titles titles = [ch.title for ch in navigator.chapters] assert "Chapter One" in titles assert "Section 1.1" in titles assert "Chapter Two" in titles def test_chapter_index_tracking(self, sample_blocks_with_headings): """Test that chapter indices are tracked correctly for H1 headings.""" navigator = ChapterNavigator(sample_blocks_with_headings) # Note: The current implementation increments AFTER adding the chapter # So chapter_index values are: H1=0 (then inc to 1), H2=1, H1=1 (then inc to 2) # First H1 should be chapter 0 assert navigator.chapters[0].position.chapter_index == 0 # H2 gets chapter_index 1 (after first H1 increment) assert navigator.chapters[1].position.chapter_index == 1 # Second H1 also gets chapter_index 1 (then increments to 2) assert navigator.chapters[2].position.chapter_index == 1 def test_get_table_of_contents(self, sample_blocks_with_headings): """Test generation of table of contents.""" navigator = ChapterNavigator(sample_blocks_with_headings) toc = navigator.get_table_of_contents() assert isinstance(toc, list) assert len(toc) == 3 # Each entry should be (title, level, position) for entry in toc: assert isinstance(entry, tuple) assert len(entry) == 3 assert isinstance(entry[0], str) # title assert isinstance(entry[1], HeadingLevel) # level assert isinstance(entry[2], RenderingPosition) # position def test_get_chapter_position_exact_match(self, sample_blocks_with_headings): """Test finding chapter by exact title match.""" navigator = ChapterNavigator(sample_blocks_with_headings) position = navigator.get_chapter_position("Chapter One") assert position is not None assert position.block_index == 0 def test_get_chapter_position_case_insensitive(self, sample_blocks_with_headings): """Test case-insensitive chapter title matching.""" navigator = ChapterNavigator(sample_blocks_with_headings) position = navigator.get_chapter_position("chapter one") assert position is not None position2 = navigator.get_chapter_position("CHAPTER ONE") assert position2 is not None assert position == position2 def test_get_chapter_position_not_found(self, sample_blocks_with_headings): """Test getting position for non-existent chapter.""" navigator = ChapterNavigator(sample_blocks_with_headings) position = navigator.get_chapter_position("Nonexistent Chapter") assert position is None def test_get_current_chapter_at_start(self, sample_blocks_with_headings): """Test getting current chapter at document start.""" navigator = ChapterNavigator(sample_blocks_with_headings) position = RenderingPosition(chapter_index=0, block_index=0) current = navigator.get_current_chapter(position) assert current is not None assert current.title == "Chapter One" def test_get_current_chapter_in_middle(self, sample_blocks_with_headings): """Test getting current chapter in middle of document.""" navigator = ChapterNavigator(sample_blocks_with_headings) # Position at block 3 should be in Chapter One position = RenderingPosition(chapter_index=0, block_index=3) current = navigator.get_current_chapter(position) assert current is not None assert "Chapter One" in current.title or "Section 1.1" in current.title def test_get_current_chapter_at_end(self, sample_blocks_with_headings): """Test getting current chapter at document end.""" navigator = ChapterNavigator(sample_blocks_with_headings) position = RenderingPosition(chapter_index=1, block_index=4) current = navigator.get_current_chapter(position) assert current is not None assert current.title == "Chapter Two" def test_empty_document_no_chapters(self, sample_font): """Test navigator with document containing no headings.""" # Document with only paragraphs blocks = [ Paragraph(sample_font), Paragraph(sample_font) ] navigator = ChapterNavigator(blocks) assert len(navigator.chapters) == 0 assert navigator.get_table_of_contents() == [] position = RenderingPosition() assert navigator.get_current_chapter(position) is None def test_multiple_heading_levels(self, sample_font): """Test navigator with multiple heading levels H1-H6.""" blocks = [] # Create headings of each level for level in [HeadingLevel.H1, HeadingLevel.H2, HeadingLevel.H3, HeadingLevel.H4, HeadingLevel.H5, HeadingLevel.H6]: heading = Heading(level, sample_font) heading.add_word(Word(f"Heading {level.value}", sample_font)) blocks.append(heading) navigator = ChapterNavigator(blocks) # Should find all 6 headings assert len(navigator.chapters) == 6 # Note: Due to implementation, H1 increments chapter_index AFTER being added # So: H1=0 (inc to 1), H2=1, H3=1, H4=1, H5=1, H6=1 chapter_indices = [ch.position.chapter_index for ch in navigator.chapters] assert chapter_indices[0] == 0 # H1 gets 0, then increments assert all(idx == 1 for idx in chapter_indices[1:]) # H2-H6 all get 1 # ============================================================================ # FontScaler Tests # ============================================================================ class TestFontScaler: """Tests for the FontScaler utility class.""" def test_scale_font_no_change(self, sample_font): """Test scaling font with factor 1.0 returns same font.""" scaled = FontScaler.scale_font(sample_font, 1.0) # Should return the original font when scale is 1.0 assert scaled == sample_font def test_scale_font_double_size(self, sample_font): """Test scaling font to double size.""" original_size = sample_font.font_size scaled = FontScaler.scale_font(sample_font, 2.0) assert scaled.font_size == original_size * 2 def test_scale_font_half_size(self, sample_font): """Test scaling font to half size.""" original_size = sample_font.font_size scaled = FontScaler.scale_font(sample_font, 0.5) assert scaled.font_size == int(original_size * 0.5) def test_scale_font_preserves_color(self, sample_font): """Test that font scaling preserves color.""" scaled = FontScaler.scale_font(sample_font, 1.5) assert scaled.colour == sample_font.colour def test_scale_font_preserves_properties(self): """Test that font scaling preserves all font properties.""" font = Font( font_size=14, colour=(255, 0, 0), weight="bold", style="italic" ) scaled = FontScaler.scale_font(font, 1.5) assert scaled.colour == font.colour assert scaled.weight == font.weight assert scaled.style == font.style def test_scale_font_minimum_size(self): """Test that font scaling maintains minimum size of 1.""" font = Font(font_size=2) # Scale to very small scaled = FontScaler.scale_font(font, 0.1) # Should be at least 1 assert scaled.font_size >= 1 def test_scale_word_spacing_no_change(self): """Test scaling word spacing with factor 1.0.""" spacing = (5, 10) scaled = FontScaler.scale_word_spacing(spacing, 1.0) assert scaled == spacing def test_scale_word_spacing_double(self): """Test scaling word spacing to double.""" spacing = (5, 10) scaled = FontScaler.scale_word_spacing(spacing, 2.0) assert scaled == (10, 20) def test_scale_word_spacing_maintains_minimum(self): """Test that word spacing maintains minimum values.""" spacing = (2, 4) scaled = FontScaler.scale_word_spacing(spacing, 0.1) # Min spacing should be at least 1, max at least 2 assert scaled[0] >= 1 assert scaled[1] >= 2 def test_scale_font_with_large_factor(self): """Test scaling with very large factor.""" font = Font(font_size=12) scaled = FontScaler.scale_font(font, 10.0) assert scaled.font_size == 120 def test_scale_font_with_small_factor(self): """Test scaling with very small factor.""" font = Font(font_size=12) scaled = FontScaler.scale_font(font, 0.05) # Should still be at least 1 assert scaled.font_size >= 1 # ============================================================================ # BidirectionalLayouter Tests (Basic) # ============================================================================ class TestBidirectionalLayouter: """Tests for the BidirectionalLayouter class.""" def test_initialization(self, sample_blocks_with_headings, sample_page_style): """Test BidirectionalLayouter initialization.""" layouter = BidirectionalLayouter( sample_blocks_with_headings, sample_page_style, page_size=(800, 600) ) assert layouter.blocks == sample_blocks_with_headings assert layouter.page_style == sample_page_style assert layouter.page_size == (800, 600) assert isinstance(layouter.chapter_navigator, ChapterNavigator) def test_position_compare_equal(self): """Test position comparison for equal positions.""" layouter = BidirectionalLayouter([], PageStyle()) pos1 = RenderingPosition(chapter_index=1, block_index=2, word_index=3) pos2 = RenderingPosition(chapter_index=1, block_index=2, word_index=3) assert layouter._position_compare(pos1, pos2) == 0 def test_position_compare_chapter_difference(self): """Test position comparison with different chapters.""" layouter = BidirectionalLayouter([], PageStyle()) pos1 = RenderingPosition(chapter_index=1, block_index=2, word_index=3) pos2 = RenderingPosition(chapter_index=2, block_index=2, word_index=3) assert layouter._position_compare(pos1, pos2) == -1 assert layouter._position_compare(pos2, pos1) == 1 def test_position_compare_block_difference(self): """Test position comparison with different blocks.""" layouter = BidirectionalLayouter([], PageStyle()) pos1 = RenderingPosition(chapter_index=1, block_index=2, word_index=3) pos2 = RenderingPosition(chapter_index=1, block_index=5, word_index=3) assert layouter._position_compare(pos1, pos2) == -1 assert layouter._position_compare(pos2, pos1) == 1 def test_position_compare_word_difference(self): """Test position comparison with different words.""" layouter = BidirectionalLayouter([], PageStyle()) pos1 = RenderingPosition(chapter_index=1, block_index=2, word_index=3) pos2 = RenderingPosition(chapter_index=1, block_index=2, word_index=10) assert layouter._position_compare(pos1, pos2) == -1 assert layouter._position_compare(pos2, pos1) == 1 def test_scale_block_fonts_no_scaling(self, sample_font): """Test block font scaling with factor 1.0.""" layouter = BidirectionalLayouter([], PageStyle()) paragraph = Paragraph(sample_font) paragraph.add_word(Word("test", sample_font)) scaled = layouter._scale_block_fonts(paragraph, 1.0) # Should return same block assert scaled == paragraph def test_estimate_page_start(self): """Test estimation of page start position.""" layouter = BidirectionalLayouter([], PageStyle()) end_pos = RenderingPosition(chapter_index=0, block_index=20, word_index=0) estimated = layouter._estimate_page_start(end_pos, 1.0) # Should estimate some blocks before the end position assert estimated.block_index < end_pos.block_index assert estimated.block_index >= 0 def test_estimate_page_start_with_font_scale(self): """Test that font scale affects page start estimation.""" layouter = BidirectionalLayouter([], PageStyle()) end_pos = RenderingPosition(chapter_index=0, block_index=20, word_index=0) est_normal = layouter._estimate_page_start(end_pos, 1.0) est_large = layouter._estimate_page_start(end_pos, 2.0) # Larger font should estimate fewer blocks assert est_large.block_index >= est_normal.block_index def test_scale_block_fonts_paragraph(self, sample_font): """Test scaling fonts in a paragraph block.""" layouter = BidirectionalLayouter([], PageStyle()) paragraph = Paragraph(sample_font) paragraph.add_word(Word("Hello", sample_font)) paragraph.add_word(Word("World", sample_font)) scaled = layouter._scale_block_fonts(paragraph, 2.0) # Should be a new paragraph assert isinstance(scaled, Paragraph) assert scaled != paragraph # Check that words were scaled (words is a list, not a method) words = scaled.words if hasattr( scaled, 'words') and isinstance( scaled.words, list) else list( scaled.words_iter()) assert len(words) >= 2 def test_scale_block_fonts_heading(self, sample_font): """Test scaling fonts in a heading block.""" layouter = BidirectionalLayouter([], PageStyle()) heading = Heading(HeadingLevel.H1, sample_font) heading.add_word(Word("Title", sample_font)) scaled = layouter._scale_block_fonts(heading, 1.5) # Should be a new heading assert isinstance(scaled, Heading) assert scaled.level == HeadingLevel.H1 def test_layout_block_on_page_unknown_type(self, sample_font): """Test layout of unknown block type skips gracefully.""" layouter = BidirectionalLayouter([], PageStyle()) # Create a mock block that's not a known type from pyWebLayout.concrete.page import Page from pyWebLayout.abstract.block import Block, BlockType page = Page(size=(800, 600), style=PageStyle()) position = RenderingPosition() # Use a simple block (not Paragraph, Heading, Table, or HList) unknown_block = Block(BlockType.HORIZONTAL_RULE) success, new_pos = layouter._layout_block_on_page( unknown_block, page, position, 1.0) # Should skip and move to next block assert success is True assert new_pos.block_index == 1 def test_layout_table_on_page(self, sample_font): """Test table layout on page (currently skips).""" layouter = BidirectionalLayouter([], PageStyle()) from pyWebLayout.concrete.page import Page from pyWebLayout.abstract.block import Table page = Page(size=(800, 600), style=PageStyle()) table = Table() position = RenderingPosition() success, new_pos = layouter._layout_table_on_page(table, page, position, 1.0) # Currently skips tables assert success is True assert new_pos.block_index == 1 assert new_pos.table_row == 0 assert new_pos.table_col == 0 def test_layout_list_on_page(self, sample_font): """Test list layout on page (currently skips).""" layouter = BidirectionalLayouter([], PageStyle()) from pyWebLayout.concrete.page import Page from pyWebLayout.abstract.block import HList page = Page(size=(800, 600), style=PageStyle()) hlist = HList() position = RenderingPosition() success, new_pos = layouter._layout_list_on_page(hlist, page, position, 1.0) # Currently skips lists assert success is True assert new_pos.block_index == 1 assert new_pos.list_item_index == 0 def test_render_page_forward_simple( self, sample_blocks_with_headings, sample_page_style): """Test forward page rendering with simple blocks.""" layouter = BidirectionalLayouter( sample_blocks_with_headings, sample_page_style, page_size=(800, 600) ) position = RenderingPosition() page, next_pos = layouter.render_page_forward(position, font_scale=1.0) # Should render a page from pyWebLayout.concrete.page import Page assert isinstance(page, Page) # Position should advance assert next_pos.block_index >= position.block_index def test_render_page_forward_with_font_scale( self, sample_blocks_with_headings, sample_page_style): """Test forward rendering with font scaling.""" layouter = BidirectionalLayouter( sample_blocks_with_headings, sample_page_style, page_size=(800, 600) ) position = RenderingPosition() # Render with normal font page1, next_pos1 = layouter.render_page_forward(position, font_scale=1.0) # Render with larger font (should fit less content) page2, next_pos2 = layouter.render_page_forward(position, font_scale=2.0) # Both should produce pages assert page1 is not None assert page2 is not None def test_render_page_forward_at_end( self, sample_blocks_with_headings, sample_page_style): """Test forward rendering at end of document.""" layouter = BidirectionalLayouter( sample_blocks_with_headings, sample_page_style, page_size=(800, 600) ) # Position at last block position = RenderingPosition(block_index=len(sample_blocks_with_headings) - 1) page, next_pos = layouter.render_page_forward(position, font_scale=1.0) # Should still render a page assert page is not None def test_render_page_forward_beyond_end( self, sample_blocks_with_headings, sample_page_style): """Test forward rendering beyond document end.""" layouter = BidirectionalLayouter( sample_blocks_with_headings, sample_page_style, page_size=(800, 600) ) # Position beyond last block position = RenderingPosition(block_index=len(sample_blocks_with_headings) + 10) page, next_pos = layouter.render_page_forward(position, font_scale=1.0) # Should handle gracefully assert page is not None def test_render_page_backward_simple( self, sample_blocks_with_headings, sample_page_style): """Test backward page rendering.""" layouter = BidirectionalLayouter( sample_blocks_with_headings, sample_page_style, page_size=(800, 600) ) # Start from middle of document end_position = RenderingPosition(block_index=3) page, start_pos = layouter.render_page_backward(end_position, font_scale=1.0) # Should render a page assert page is not None # Start position should be before or at end position assert start_pos.block_index <= end_position.block_index def test_adjust_start_estimate_overshot(self): """Test adjustment when forward render overshoots target.""" layouter = BidirectionalLayouter([], PageStyle()) current_start = RenderingPosition(block_index=5) target_end = RenderingPosition(block_index=10) actual_end = RenderingPosition(block_index=12) # Overshot adjusted = layouter._adjust_start_estimate( current_start, target_end, actual_end) # Should move start forward (increase block_index) assert adjusted.block_index > current_start.block_index def test_adjust_start_estimate_undershot(self): """Test adjustment when forward render undershoots target.""" layouter = BidirectionalLayouter([], PageStyle()) current_start = RenderingPosition(block_index=5) target_end = RenderingPosition(block_index=10) actual_end = RenderingPosition(block_index=8) # Undershot adjusted = layouter._adjust_start_estimate( current_start, target_end, actual_end) # Should move start backward (decrease block_index) assert adjusted.block_index <= current_start.block_index def test_adjust_start_estimate_exact(self): """Test adjustment when forward render hits target exactly.""" layouter = BidirectionalLayouter([], PageStyle()) current_start = RenderingPosition(block_index=5) target_end = RenderingPosition(block_index=10) actual_end = RenderingPosition(block_index=10) # Exact adjusted = layouter._adjust_start_estimate( current_start, target_end, actual_end) # Should return same or similar position assert adjusted.block_index >= 0 def test_layout_paragraph_on_page_with_pretext( self, sample_font, sample_page_style): """Test paragraph layout with pretext (hyphenated word continuation).""" layouter = BidirectionalLayouter([], sample_page_style, page_size=(800, 600)) from pyWebLayout.concrete.page import Page paragraph = Paragraph(sample_font) paragraph.add_word(Word("continuation", sample_font)) page = Page(size=(800, 600), style=sample_page_style) position = RenderingPosition(remaining_pretext="pre-") success, new_pos = layouter._layout_paragraph_on_page( paragraph, page, position, 1.0) # Should attempt to layout assert isinstance(success, bool) assert isinstance(new_pos, RenderingPosition) def test_layout_paragraph_success(self, sample_font, sample_page_style): """Test successful paragraph layout.""" layouter = BidirectionalLayouter([], sample_page_style, page_size=(800, 600)) from pyWebLayout.concrete.page import Page paragraph = Paragraph(sample_font) paragraph.add_word(Word("Short", sample_font)) paragraph.add_word(Word("text", sample_font)) page = Page(size=(800, 600), style=sample_page_style) position = RenderingPosition() success, new_pos = layouter._layout_paragraph_on_page( paragraph, page, position, 1.0) # Should complete successfully assert isinstance(success, bool) def test_layout_heading_on_page(self, sample_font, sample_page_style): """Test heading layout delegates to paragraph layout.""" layouter = BidirectionalLayouter([], sample_page_style, page_size=(800, 600)) from pyWebLayout.concrete.page import Page heading = Heading(HeadingLevel.H1, sample_font) heading.add_word(Word("Heading", sample_font)) heading.add_word(Word("Text", sample_font)) page = Page(size=(800, 600), style=sample_page_style) position = RenderingPosition() success, new_pos = layouter._layout_heading_on_page( heading, page, position, 1.0) # Should attempt to layout like a paragraph assert isinstance(success, bool) assert isinstance(new_pos, RenderingPosition) def test_empty_blocks_list(self, sample_page_style): """Test rendering with empty blocks list.""" layouter = BidirectionalLayouter([], sample_page_style, page_size=(800, 600)) position = RenderingPosition() page, next_pos = layouter.render_page_forward(position, font_scale=1.0) # Should handle empty document assert page is not None assert next_pos == position # No progress possible if __name__ == "__main__": pytest.main([__file__, "-v"])