""" Regression tests for blocks that span more than one page. A block larger than a single page is laid out partially, and the layouter reports where it stopped. If that resume point is discarded, the reader is told it made no progress and navigation dead-ends on that block (spec S11). """ import pytest from pyWebLayout.abstract.block import Paragraph from pyWebLayout.abstract.inline import Word from pyWebLayout.concrete.page import Page from pyWebLayout.layout.ereader_layout import BidirectionalLayouter, RenderingPosition from pyWebLayout.style import Font from pyWebLayout.style.page_style import PageStyle PAGE_SIZE = (800, 600) def make_paragraph(word_count, font): """A paragraph of distinct words, so we can verify none are lost or repeated.""" paragraph = Paragraph(font) for i in range(word_count): paragraph.add_word(Word(f"word{i}", font)) return paragraph @pytest.fixture def font(): return Font(font_size=16) @pytest.fixture def huge_paragraph(font): """A paragraph far larger than one page - the shape that dead-ended.""" return make_paragraph(2877, font) class TestPageSpanningParagraph: """A single paragraph larger than one page must paginate, not dead-end.""" def test_layouter_reports_where_it_stopped(self, huge_paragraph): layouter = BidirectionalLayouter([huge_paragraph], PageStyle(), PAGE_SIZE) page = Page(size=PAGE_SIZE, style=PageStyle()) success, new_pos = layouter._layout_block_on_page( huge_paragraph, page, RenderingPosition(), 1.0) assert not success, "a 2877-word paragraph cannot fit on one page" assert new_pos.word_index > 0, "the resume point must be reported" def test_first_page_advances(self, huge_paragraph): """The regression: next position equalled the start position.""" layouter = BidirectionalLayouter([huge_paragraph], PageStyle(), PAGE_SIZE) start = RenderingPosition() page, next_pos = layouter.render_page_forward(start, 1.0) assert len(page.children) > 0, "content was placed on the page" assert (next_pos.block_index, next_pos.word_index) > \ (start.block_index, start.word_index), \ "a page with content on it must advance the position" def test_paginates_to_completion(self, huge_paragraph): """Every page advances, and the document terminates.""" layouter = BidirectionalLayouter([huge_paragraph], PageStyle(), PAGE_SIZE) pos = RenderingPosition() positions = [(pos.block_index, pos.word_index)] for _ in range(100): page, next_pos = layouter.render_page_forward(pos, 1.0) key = (next_pos.block_index, next_pos.word_index) if next_pos.block_index >= 1: break # ran off the end of the (single-block) document assert key > positions[-1], f"no progress at page {len(positions)}" positions.append(key) pos = next_pos else: pytest.fail("pagination did not terminate") assert len(positions) > 5, "a 2877-word paragraph spans several pages" def test_no_words_lost_or_repeated(self, huge_paragraph): """Word coverage across pages is exactly the paragraph, in order.""" layouter = BidirectionalLayouter([huge_paragraph], PageStyle(), PAGE_SIZE) pos = RenderingPosition() boundaries = [0] for _ in range(100): _, next_pos = layouter.render_page_forward(pos, 1.0) if next_pos.block_index >= 1: break boundaries.append(next_pos.word_index) pos = next_pos assert boundaries == sorted(boundaries), "word indices must not go backward" assert len(boundaries) == len(set(boundaries)), "a page must not be re-rendered" class TestNonSpanningBlocksUnaffected: """The fix must not change behaviour for blocks that fit.""" def test_small_paragraphs_still_advance_by_block(self, font): blocks = [make_paragraph(20, font) for _ in range(3)] layouter = BidirectionalLayouter(blocks, PageStyle(), PAGE_SIZE) _, next_pos = layouter.render_page_forward(RenderingPosition(), 1.0) assert next_pos.block_index == 3, "all three short paragraphs fit on one page" assert next_pos.word_index == 0 def test_empty_document_terminates(self): layouter = BidirectionalLayouter([], PageStyle(), PAGE_SIZE) start = RenderingPosition() page, next_pos = layouter.render_page_forward(start, 1.0) assert next_pos.block_index == start.block_index assert len(page.children) == 0