Skip to content

Latest commit

 

History

History
96 lines (75 loc) · 3.93 KB

File metadata and controls

96 lines (75 loc) · 3.93 KB

Layout snapshot testing: catch re-flows before they ship

GraphCompose's layout engine is deterministic: the same document produces the same resolved geometry, every run, on every machine. That makes layout itself testable — DocumentSession.layoutSnapshot() captures the page count, canvas, and the depth-first list of every node's resolved bounds and metadata as stable JSON, deliberately leaving out renderer-specific bytes (font embedding, PDFBox object IDs, timestamps). When a change re-flows something it shouldn't have, the JSON diff shows it instantly — no PDF diffing, no golden images.

A snapshot test in three lines

import com.demcha.compose.testing.layout.LayoutSnapshotAssertions;

@Test
void invoiceLayoutIsStable() throws Exception {
    try (DocumentSession document = GraphCompose.document()
            .pageSize(DocumentPageSize.A4)
            .margin(DocumentInsets.of(28))
            .create()) {
        ModernInvoice.create().compose(document, sampleInvoice());

        LayoutSnapshotAssertions.assertMatches(document, "templates/invoice/invoice_baseline");
    }
}

The slash-delimited key is a logical path: this example compares against core/src/test/resources/layout-snapshots/templates/invoice/invoice_baseline.json. LayoutSnapshotAssertions ships in the graph-compose-testing artifact (io.github.demchaav:graph-compose-testing), split out of the engine jar in 2.0. Add it at test scope alongside graph-compose; the import path is unchanged.

First run and updates

On the first run the baseline does not exist: the assertion fails, writes the actual snapshot, and tells you how to accept it. Accepting — and updating after any deliberate layout change — is one flag:

./mvnw test -Dtest=YourSnapshotTest -Dgraphcompose.updateSnapshots=true

This overwrites the committed baseline with the current layout. Review the JSON diff before committing: that diff is the layout change.

On mismatch

A failed comparison writes the offending snapshot to target/visual-tests/layout-snapshots/<path>.actual.json and the assertion message names both files:

Layout snapshot mismatch for invoice_baseline.
Expected: src/test/resources/layout-snapshots/templates/invoice/invoice_baseline.json
Actual:   target/visual-tests/layout-snapshots/templates/invoice/invoice_baseline.actual.json
Re-run with -Dgraphcompose.updateSnapshots=true to update the baseline.

Diff the two to see exactly which node moved, grew, or paginated differently. A passing run cleans up any stale .actual.json.

Using it in consumer projects

This is not just an internal tool — if you build templates on GraphCompose, snapshot tests are the cheapest regression net for them: compose the template with fixed sample data, assert the snapshot, and a GraphCompose upgrade (or your own refactor) that shifts the layout fails loudly instead of silently re-flowing a customer document.

Baselines default to core/src/test/resources/layout-snapshots; overloads take explicit roots when your project keeps them elsewhere:

LayoutSnapshotAssertions.assertMatches(document,
        Path.of("src", "test", "resources", "my-baselines"),
        Path.of("target", "snapshot-failures"),
        "quotes/quote_standard");

For non-JUnit flows, the underlying pieces are public too: document.layoutSnapshot() returns the snapshot and LayoutSnapshotJson.toJson(snapshot) serialises it, so you can wire the same check into any harness.

Pair the snapshot with a rendered PDF from the same session (document.toPdfBytes()) when you want human-reviewable output next to the machine check.

Runnable walkthrough of the full workflow: LayoutSnapshotRegressionExample. A real in-tree test using the production pattern: ShapeContainerLayoutSnapshotTest.