diff --git a/CHANGELOG.md b/CHANGELOG.md index 6e04e80e..0852ef58 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,20 @@ follow-ups carried over from the v1.6.7 senior review (see [ROADMAP.md](ROADMAP.md) and the private taskboard). No breaking changes are planned. +### Documentation + +- New quickstart guide + [Testing your document](docs/operations/test-your-document.md) — + end-to-end recipe (author the document → add a layout + snapshot test → bless the baseline → CI guards the + shape on every PR), with a "when to use which layer" table for + the three protection tiers (smoke / layout snapshot / pixel-level + visual). Complements the existing + [layout-snapshot-testing.md](docs/operations/layout-snapshot-testing.md) + reference: that one is reference-style, the new one is + tutorial-style. README's "What can I do with this?" table row + now links to both. + ## v1.6.7 — 2026-06-01 **Transitive dependency cleanup.** v1.6.7 narrows the runtime diff --git a/README.md b/README.md index cc224666..b586f2da 100644 --- a/README.md +++ b/README.md @@ -89,7 +89,7 @@ GraphCompose uses PDFBox under the hood as the rendering backend — the com | Generate a one-off PDF programmatically | DSL | `GraphCompose.document(...).pageFlow(...)` — see [Hello world](#hello-world) below | | Generate a CV / cover letter from data | Layered templates | `ModernProfessional.create().compose(session, cvDocument)` — see [layered templates](./docs/templates/v2-layered/README.md) | | Add a custom visual primitive | Engine extension | `NodeDefinition` + `PdfFragmentRenderHandler` — see [extension guide](./docs/contributing/extension-guide.md) | -| Regression-test generated layouts | Layout snapshots | `DocumentSession#layoutSnapshot()` — see [snapshot testing](./docs/operations/layout-snapshot-testing.md) | +| Regression-test generated layouts | Layout snapshots | `DocumentSession#layoutSnapshot()` — quickstart at [Testing your document](./docs/operations/test-your-document.md); full reference at [snapshot testing](./docs/operations/layout-snapshot-testing.md) | ## Installation diff --git a/docs/operations/test-your-document.md b/docs/operations/test-your-document.md new file mode 100644 index 00000000..0901a292 --- /dev/null +++ b/docs/operations/test-your-document.md @@ -0,0 +1,265 @@ +# Testing your document — from "I just authored it" to "CI guards it" + +A short, end-to-end recipe for protecting a GraphCompose document +(template, preset, or one-off layout) with automated tests, so any +future change to the engine or your own code shows up as a red CI +run, not a silent visual regression. + +If you want the deep reference, jump to +[Layout snapshot testing](./layout-snapshot-testing.md). This page +is the "Hello world" — start here, link there when you need detail. + +--- + +## The three protection layers + +GraphCompose offers three test layers, ordered cheap → expensive: + +| Layer | Catches | Where the baseline lives | Test class pattern | +|---|---|---|---| +| **1. Smoke** | Does the document compile + render at all? | _no baseline, exit code only_ | `*SmokeTest` | +| **2. Layout snapshot** | Geometry — coordinates, sibling order, page breaks, layer/z-index | JSON file (deterministic, cross-machine stable) | `*LayoutSnapshotTest` | +| **3. Pixel-level visual** | Final render — fonts, colours, anti-aliasing | PNG file (per-pixel diff, tolerance budget) | `*VisualParityTest` / `*DemoTest` | + +In day-to-day work **layout snapshots are the workhorse**: deterministic, +diff-able, fast. Pixel-level visual catches the "looks wrong in PDF +but the math is right" class, but it is slower to inspect and more +sensitive to font/renderer drift between OS — keep it for templates +and presets you ship to others. + +--- + +## End-to-end recipe (Layout snapshot) + +Five steps. First three are once-per-document; the rest is automatic. + +### 1. Author your document + +```java +import com.demcha.compose.GraphCompose; +import com.demcha.compose.document.api.DocumentPageSize; +import com.demcha.compose.document.api.DocumentSession; + +try (DocumentSession session = GraphCompose.document() + .pageSize(DocumentPageSize.A4) + .margin(22, 22, 22, 22) + .create()) { + + session.pageFlow(page -> page + .module("Hello", module -> module + .paragraph("First report — GraphCompose layout demo"))); + + session.buildPdf(); // optional — for visual inspection +} +``` + +### 2. Add a layout snapshot test next to your document + +```java +import com.demcha.compose.GraphCompose; +import com.demcha.compose.document.api.DocumentPageSize; +import com.demcha.compose.document.api.DocumentSession; +import com.demcha.compose.testing.layout.LayoutSnapshotAssertions; +import org.junit.jupiter.api.Test; + +class MyReportLayoutSnapshotTest { + + @Test + void shouldKeepReportLayoutStable() throws Exception { + try (DocumentSession session = GraphCompose.document() + .pageSize(DocumentPageSize.A4) + .margin(22, 22, 22, 22) + .create()) { + + session.pageFlow(page -> page + .module("Hello", module -> module + .paragraph("First report — GraphCompose layout demo"))); + + LayoutSnapshotAssertions.assertMatches( + session, + "my_reports/report_v1_layout"); // baseline path (no extension) + } + } +} +``` + +`LayoutSnapshotAssertions.assertMatches(session, name)` resolves the +baseline at: + +``` +src/test/resources/layout-snapshots/my_reports/report_v1_layout.json +``` + +The first run will fail because the baseline does not exist yet — +that's expected. Go to step 3. + +### 3. Bless the first baseline + +Once. Run the test in **update mode** so it writes the baseline JSON: + +```bash +./mvnw test -Dgraphcompose.updateSnapshots=true \ + -Dtest=MyReportLayoutSnapshotTest -pl . +``` + +The baseline JSON appears under `src/test/resources/layout-snapshots/`. +Commit it alongside your test class — the baseline is part of the +test, not generated output. + +### 4. Day-to-day: just run the suite + +```bash +./mvnw test -pl . +``` + +The test now passes deterministically. Any change that drifts the +layout — a margin tweak, a new module insertion, a builder behaviour +change deep in the engine — fails this test immediately, with a +specific path / coordinate / page diff in the failure message and a +generated `*.actual.json` under `target/visual-tests/layout-snapshots/` +that you can diff against the committed baseline. + +### 5. You changed something on purpose. Re-bless. + +```bash +./mvnw test -Dgraphcompose.updateSnapshots=true \ + -Dtest=MyReportLayoutSnapshotTest -pl . +``` + +The baseline is overwritten with the new layout. **Commit the updated +JSON in the same change as the production code** — the baseline diff +in the PR is itself part of the review (a senior reviewer should look +at the JSON diff to confirm the layout change is what you intended). + +--- + +## What a snapshot file looks like + +```json +{ + "formatVersion": 1, + "canvas": { "width": 595.276, "height": 841.89 }, + "totalPages": 1, + "nodes": [ + { + "path": "module[Hello]/paragraph[0]", + "depth": 2, + "layer": 0, + "computedX": 22.0, + "computedY": 22.0, + "placementX": 22.0, + "placementY": 22.0, + "width": 551.276, + "height": 14.4, + "startPage": 0, + "endPage": 0 + } + ] +} +``` + +Stable fields only — coordinates, dimensions, structure, paging. No +UUIDs, no text payload, no colours. That is by design: small, +content-agnostic diffs that a human can review in a PR. + +If you want to also assert text content or colour, drive those +checks separately with regular unit tests — snapshot is for geometry. + +--- + +## When a snapshot fails — debugging recipe + +1. The failure message points at the actual file: + `target/visual-tests/layout-snapshots/.actual.json` +2. Compare the actual against the committed baseline under + `src/test/resources/layout-snapshots/.json`. Most diff tools + highlight a single field-level change. +3. Decide what you're looking at: + - **`computedY` / `placementY` shifted by a few units** → a margin + or padding change upstream, or a font swap that changed text + height. + - **`startPage` / `endPage` changed** → page-break shifted; check + pagination tolerance and whether you added content before the + break. + - **A node appeared / disappeared** → semantic graph changed; check + conditional `if (...)` branches in your document author code. + - **Sibling order changed** → composition order in your DSL changed. +4. If the change is intentional: re-bless (step 5 above) and commit + the baseline diff in the same PR. +5. If the change is *not* intentional: investigate the layout math + before you trust the PDF output. + +--- + +## Where every file lives + +``` +src/test/java/com/example/MyReportLayoutSnapshotTest.java ← your test +src/test/resources/layout-snapshots/my_reports/ + report_v1_layout.json ← committed baseline +target/visual-tests/layout-snapshots/my_reports/ + report_v1_layout.actual.json ← generated on mismatch +``` + +--- + +## CI behaviour + +CI **never** sets `graphcompose.updateSnapshots=true`. Snapshot tests +in CI run in strict comparison mode — any drift fails the build and +writes the `.actual.json` artifact for download. This is the property +that prevents accidental baseline drift on a busy main branch. + +--- + +## Pixel-level visual gate + +When the math is right but the PDF looks wrong — wrong font shape, +wrong colour, anti-aliasing artefacts — the layout snapshot does not +catch it. GraphCompose uses a pixel-diff visual parity gate for each +shipped CV / cover-letter preset and for the engine showcase tests +(see `CvV2VisualParityTest`, `CoverLetterV2VisualParityTest`, +`TableRowSpanDemoTest` and friends). + +The harness behind those tests +(`com.demcha.testing.visual.PdfVisualRegression` + +`ImageDiff`) is currently **test-only** inside the GraphCompose +build. Promoting it to a public `com.demcha.compose.testing.visual.*` +API so library consumers can adopt the same pixel-level gate against +their own presets is queued as **v1.6.8 / v1.7.0 Track N** — see the +release-readiness taskboard. Until that ships, the recommended +public path is layout snapshot above; for pixel-level work, copy +the pattern from `PdfVisualRegression` (it builds on the public +`com.demcha.compose.devtool.PdfRenderBridge` for PDF page → image +conversion). + +--- + +## When to use which layer + +| You want to know that… | Use | +|---|---| +| The document compiles + renders at all | smoke (just call `buildPdf()` in a test) | +| The semantic graph and resolved coordinates are stable across engine refactors | **layout snapshot** | +| The PDF visually looks identical, fonts/colours and all | pixel-level visual (Track N) | +| A specific layout math rule holds | a focused unit test | + +The advice scales: a flagship template or a preset you publish to +others deserves all three. A one-off internal report needs smoke + +layout snapshot — that catches 95% of the regressions you'd care +about, at near-zero cost per run. + +--- + +## Deeper reference + +- [Layout snapshot testing](./layout-snapshot-testing.md) — + full reference: pipeline position, snapshot contents, + determinism guarantees, downstream-project adoption, CI policy, + what NOT to snapshot. +- [`LayoutSnapshotPublicApiDogfoodTest`](../../src/test/java/com/demcha/testing/layout/LayoutSnapshotPublicApiDogfoodTest.java) + — a working integration test that drives the snapshot API + entirely through the published surface. Copyable starting point. +- [`CvV2VisualParityTest`](../../src/test/java/com/demcha/compose/document/templates/cv/v2/presets/CvV2VisualParityTest.java) + — example of the pixel-level pattern (currently test-only; + becoming public via Track N).