130 lines
4.3 KiB
Python
130 lines
4.3 KiB
Python
"""
|
|
Regression tests for the page draw/canvas lifecycle (spec S3).
|
|
|
|
add_child invalidates the canvas but left _draw pointing at it, and the draw
|
|
property only rebuilt when _draw was None. Callers therefore received a context
|
|
bound to a discarded image while page._canvas stayed None - which is how images
|
|
inside table cells ended up as grey placeholders: table_layouter passed
|
|
canvas=None through to the cell renderer.
|
|
|
|
Fixing that alone would make layout allocate a full-page canvas per line, since
|
|
layout measures text through the page. Measurement now goes through a dedicated
|
|
scratch context.
|
|
"""
|
|
|
|
import pytest
|
|
from PIL import Image
|
|
|
|
from pyWebLayout.abstract.block import Image as AbstractImage, Paragraph
|
|
from pyWebLayout.abstract.inline import Word
|
|
from pyWebLayout.concrete.page import Page
|
|
from pyWebLayout.layout.document_layouter import DocumentLayouter
|
|
from pyWebLayout.style import Font
|
|
from pyWebLayout.style.page_style import PageStyle
|
|
|
|
|
|
@pytest.fixture
|
|
def font():
|
|
return Font(font_size=12)
|
|
|
|
|
|
@pytest.fixture
|
|
def page():
|
|
return Page(size=(400, 600), style=PageStyle())
|
|
|
|
|
|
def paragraph_of(font, count=40):
|
|
paragraph = Paragraph(font)
|
|
for i in range(count):
|
|
paragraph.add_word(Word(f"word{i}", font))
|
|
return paragraph
|
|
|
|
|
|
class TestDrawIsNeverStale:
|
|
|
|
def test_draw_matches_canvas_after_add_child(self, page, font):
|
|
page.draw # force canvas creation
|
|
DocumentLayouter(page).layout_paragraph(paragraph_of(font))
|
|
|
|
assert page.draw.im is page._canvas.im, \
|
|
"draw must be bound to the page's current canvas"
|
|
|
|
def test_canvas_is_present_after_layout(self, page, font):
|
|
DocumentLayouter(page).layout_paragraph(paragraph_of(font))
|
|
page.draw
|
|
|
|
assert page._canvas is not None
|
|
|
|
def test_repeated_draw_access_is_stable(self, page):
|
|
first = page.draw
|
|
assert page.draw is first, "draw must not be rebuilt while the canvas stands"
|
|
|
|
|
|
class TestMeasurementDoesNotAllocateCanvases:
|
|
|
|
def test_layout_allocates_no_page_canvas(self, page, font, monkeypatch):
|
|
calls = []
|
|
original = Page._create_canvas
|
|
|
|
def counting(self):
|
|
calls.append(1)
|
|
return original(self)
|
|
|
|
monkeypatch.setattr(Page, "_create_canvas", counting)
|
|
DocumentLayouter(page).layout_paragraph(paragraph_of(font, 400))
|
|
|
|
assert calls == [], \
|
|
f"layout allocated {len(calls)} full-page canvases; it should allocate none"
|
|
|
|
def test_measurement_context_is_tiny_and_matches_canvas_mode(self, page):
|
|
scratch = page.measurement_draw
|
|
assert scratch.im.size == (1, 1)
|
|
assert scratch.mode == Page._CANVAS_MODE
|
|
|
|
def test_measurement_context_is_stable(self, page, font):
|
|
first = page.measurement_draw
|
|
DocumentLayouter(page).layout_paragraph(paragraph_of(font))
|
|
assert page.measurement_draw is first, \
|
|
"the scratch context must survive canvas invalidation"
|
|
|
|
|
|
class TestRenderIsRepeatable:
|
|
|
|
def test_two_renders_are_identical(self, page, font):
|
|
DocumentLayouter(page).layout_paragraph(paragraph_of(font))
|
|
|
|
first = page.render().copy()
|
|
second = page.render().copy()
|
|
|
|
assert first.tobytes() == second.tobytes()
|
|
|
|
|
|
class TestImageInCellGetsARealCanvas:
|
|
"""The concrete symptom: table images degraded to placeholders."""
|
|
|
|
@pytest.fixture
|
|
def image_path(self, tmp_path):
|
|
path = tmp_path / "swatch.png"
|
|
Image.new("RGB", (40, 30), (10, 200, 10)).save(path)
|
|
return str(path)
|
|
|
|
def test_table_after_paragraph_receives_a_canvas(self, page, font, image_path):
|
|
from pyWebLayout.abstract.block import Table, TableCell, TableRow
|
|
from pyWebLayout.layout.document_layouter import table_layouter
|
|
|
|
layouter = DocumentLayouter(page)
|
|
layouter.layout_paragraph(paragraph_of(font, 10))
|
|
|
|
table = Table()
|
|
row = TableRow()
|
|
cell = TableCell()
|
|
cell.add_block(AbstractImage(image_path))
|
|
row.add_cell(cell)
|
|
table.add_row(row)
|
|
|
|
# The canvas is invalidated by the preceding add_child; the table must
|
|
# still be handed a real one.
|
|
assert table_layouter(table, page) or True # placement may fail on space
|
|
assert page._canvas is not None, \
|
|
"table layout must not run against a None canvas"
|