Files
pyWebLayout/tests/concrete/test_canvas_lifecycle.py
T

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"