|
| 1 | +# Testing your document — from "I just authored it" to "CI guards it" |
| 2 | + |
| 3 | +A short, end-to-end recipe for protecting a GraphCompose document |
| 4 | +(template, preset, or one-off layout) with automated tests, so any |
| 5 | +future change to the engine or your own code shows up as a red CI |
| 6 | +run, not a silent visual regression. |
| 7 | + |
| 8 | +If you want the deep reference, jump to |
| 9 | +[Layout snapshot testing](./layout-snapshot-testing.md). This page |
| 10 | +is the "Hello world" — start here, link there when you need detail. |
| 11 | + |
| 12 | +--- |
| 13 | + |
| 14 | +## The three protection layers |
| 15 | + |
| 16 | +GraphCompose offers three test layers, ordered cheap → expensive: |
| 17 | + |
| 18 | +| Layer | Catches | Where the baseline lives | Test class pattern | |
| 19 | +|---|---|---|---| |
| 20 | +| **1. Smoke** | Does the document compile + render at all? | _no baseline, exit code only_ | `*SmokeTest` | |
| 21 | +| **2. Layout snapshot** | Geometry — coordinates, sibling order, page breaks, layer/z-index | JSON file (deterministic, cross-machine stable) | `*LayoutSnapshotTest` | |
| 22 | +| **3. Pixel-level visual** | Final render — fonts, colours, anti-aliasing | PNG file (per-pixel diff, tolerance budget) | `*VisualParityTest` / `*DemoTest` | |
| 23 | + |
| 24 | +In day-to-day work **layout snapshots are the workhorse**: deterministic, |
| 25 | +diff-able, fast. Pixel-level visual catches the "looks wrong in PDF |
| 26 | +but the math is right" class, but it is slower to inspect and more |
| 27 | +sensitive to font/renderer drift between OS — keep it for templates |
| 28 | +and presets you ship to others. |
| 29 | + |
| 30 | +--- |
| 31 | + |
| 32 | +## End-to-end recipe (Layout snapshot) |
| 33 | + |
| 34 | +Five steps. First three are once-per-document; the rest is automatic. |
| 35 | + |
| 36 | +### 1. Author your document |
| 37 | + |
| 38 | +```java |
| 39 | +import com.demcha.compose.GraphCompose; |
| 40 | +import com.demcha.compose.document.api.DocumentPageSize; |
| 41 | +import com.demcha.compose.document.api.DocumentSession; |
| 42 | + |
| 43 | +try (DocumentSession session = GraphCompose.document() |
| 44 | + .pageSize(DocumentPageSize.A4) |
| 45 | + .margin(22, 22, 22, 22) |
| 46 | + .create()) { |
| 47 | + |
| 48 | + session.pageFlow(page -> page |
| 49 | + .module("Hello", module -> module |
| 50 | + .paragraph("First report — GraphCompose layout demo"))); |
| 51 | + |
| 52 | + session.buildPdf(); // optional — for visual inspection |
| 53 | +} |
| 54 | +``` |
| 55 | + |
| 56 | +### 2. Add a layout snapshot test next to your document |
| 57 | + |
| 58 | +```java |
| 59 | +import com.demcha.compose.GraphCompose; |
| 60 | +import com.demcha.compose.document.api.DocumentPageSize; |
| 61 | +import com.demcha.compose.document.api.DocumentSession; |
| 62 | +import com.demcha.compose.testing.layout.LayoutSnapshotAssertions; |
| 63 | +import org.junit.jupiter.api.Test; |
| 64 | + |
| 65 | +class MyReportLayoutSnapshotTest { |
| 66 | + |
| 67 | + @Test |
| 68 | + void shouldKeepReportLayoutStable() throws Exception { |
| 69 | + try (DocumentSession session = GraphCompose.document() |
| 70 | + .pageSize(DocumentPageSize.A4) |
| 71 | + .margin(22, 22, 22, 22) |
| 72 | + .create()) { |
| 73 | + |
| 74 | + session.pageFlow(page -> page |
| 75 | + .module("Hello", module -> module |
| 76 | + .paragraph("First report — GraphCompose layout demo"))); |
| 77 | + |
| 78 | + LayoutSnapshotAssertions.assertMatches( |
| 79 | + session, |
| 80 | + "my_reports/report_v1_layout"); // baseline path (no extension) |
| 81 | + } |
| 82 | + } |
| 83 | +} |
| 84 | +``` |
| 85 | + |
| 86 | +`LayoutSnapshotAssertions.assertMatches(session, name)` resolves the |
| 87 | +baseline at: |
| 88 | + |
| 89 | +``` |
| 90 | +src/test/resources/layout-snapshots/my_reports/report_v1_layout.json |
| 91 | +``` |
| 92 | + |
| 93 | +The first run will fail because the baseline does not exist yet — |
| 94 | +that's expected. Go to step 3. |
| 95 | + |
| 96 | +### 3. Bless the first baseline |
| 97 | + |
| 98 | +Once. Run the test in **update mode** so it writes the baseline JSON: |
| 99 | + |
| 100 | +```bash |
| 101 | +./mvnw test -Dgraphcompose.updateSnapshots=true \ |
| 102 | + -Dtest=MyReportLayoutSnapshotTest -pl . |
| 103 | +``` |
| 104 | + |
| 105 | +The baseline JSON appears under `src/test/resources/layout-snapshots/`. |
| 106 | +Commit it alongside your test class — the baseline is part of the |
| 107 | +test, not generated output. |
| 108 | + |
| 109 | +### 4. Day-to-day: just run the suite |
| 110 | + |
| 111 | +```bash |
| 112 | +./mvnw test -pl . |
| 113 | +``` |
| 114 | + |
| 115 | +The test now passes deterministically. Any change that drifts the |
| 116 | +layout — a margin tweak, a new module insertion, a builder behaviour |
| 117 | +change deep in the engine — fails this test immediately, with a |
| 118 | +specific path / coordinate / page diff in the failure message and a |
| 119 | +generated `*.actual.json` under `target/visual-tests/layout-snapshots/` |
| 120 | +that you can diff against the committed baseline. |
| 121 | + |
| 122 | +### 5. You changed something on purpose. Re-bless. |
| 123 | + |
| 124 | +```bash |
| 125 | +./mvnw test -Dgraphcompose.updateSnapshots=true \ |
| 126 | + -Dtest=MyReportLayoutSnapshotTest -pl . |
| 127 | +``` |
| 128 | + |
| 129 | +The baseline is overwritten with the new layout. **Commit the updated |
| 130 | +JSON in the same change as the production code** — the baseline diff |
| 131 | +in the PR is itself part of the review (a senior reviewer should look |
| 132 | +at the JSON diff to confirm the layout change is what you intended). |
| 133 | + |
| 134 | +--- |
| 135 | + |
| 136 | +## What a snapshot file looks like |
| 137 | + |
| 138 | +```json |
| 139 | +{ |
| 140 | + "formatVersion": 1, |
| 141 | + "canvas": { "width": 595.276, "height": 841.89 }, |
| 142 | + "totalPages": 1, |
| 143 | + "nodes": [ |
| 144 | + { |
| 145 | + "path": "module[Hello]/paragraph[0]", |
| 146 | + "depth": 2, |
| 147 | + "layer": 0, |
| 148 | + "computedX": 22.0, |
| 149 | + "computedY": 22.0, |
| 150 | + "placementX": 22.0, |
| 151 | + "placementY": 22.0, |
| 152 | + "width": 551.276, |
| 153 | + "height": 14.4, |
| 154 | + "startPage": 0, |
| 155 | + "endPage": 0 |
| 156 | + } |
| 157 | + ] |
| 158 | +} |
| 159 | +``` |
| 160 | + |
| 161 | +Stable fields only — coordinates, dimensions, structure, paging. No |
| 162 | +UUIDs, no text payload, no colours. That is by design: small, |
| 163 | +content-agnostic diffs that a human can review in a PR. |
| 164 | + |
| 165 | +If you want to also assert text content or colour, drive those |
| 166 | +checks separately with regular unit tests — snapshot is for geometry. |
| 167 | + |
| 168 | +--- |
| 169 | + |
| 170 | +## When a snapshot fails — debugging recipe |
| 171 | + |
| 172 | +1. The failure message points at the actual file: |
| 173 | + `target/visual-tests/layout-snapshots/<name>.actual.json` |
| 174 | +2. Compare the actual against the committed baseline under |
| 175 | + `src/test/resources/layout-snapshots/<name>.json`. Most diff tools |
| 176 | + highlight a single field-level change. |
| 177 | +3. Decide what you're looking at: |
| 178 | + - **`computedY` / `placementY` shifted by a few units** → a margin |
| 179 | + or padding change upstream, or a font swap that changed text |
| 180 | + height. |
| 181 | + - **`startPage` / `endPage` changed** → page-break shifted; check |
| 182 | + pagination tolerance and whether you added content before the |
| 183 | + break. |
| 184 | + - **A node appeared / disappeared** → semantic graph changed; check |
| 185 | + conditional `if (...)` branches in your document author code. |
| 186 | + - **Sibling order changed** → composition order in your DSL changed. |
| 187 | +4. If the change is intentional: re-bless (step 5 above) and commit |
| 188 | + the baseline diff in the same PR. |
| 189 | +5. If the change is *not* intentional: investigate the layout math |
| 190 | + before you trust the PDF output. |
| 191 | + |
| 192 | +--- |
| 193 | + |
| 194 | +## Where every file lives |
| 195 | + |
| 196 | +``` |
| 197 | +src/test/java/com/example/MyReportLayoutSnapshotTest.java ← your test |
| 198 | +src/test/resources/layout-snapshots/my_reports/ |
| 199 | + report_v1_layout.json ← committed baseline |
| 200 | +target/visual-tests/layout-snapshots/my_reports/ |
| 201 | + report_v1_layout.actual.json ← generated on mismatch |
| 202 | +``` |
| 203 | + |
| 204 | +--- |
| 205 | + |
| 206 | +## CI behaviour |
| 207 | + |
| 208 | +CI **never** sets `graphcompose.updateSnapshots=true`. Snapshot tests |
| 209 | +in CI run in strict comparison mode — any drift fails the build and |
| 210 | +writes the `.actual.json` artifact for download. This is the property |
| 211 | +that prevents accidental baseline drift on a busy main branch. |
| 212 | + |
| 213 | +--- |
| 214 | + |
| 215 | +## Pixel-level visual gate |
| 216 | + |
| 217 | +When the math is right but the PDF looks wrong — wrong font shape, |
| 218 | +wrong colour, anti-aliasing artefacts — the layout snapshot does not |
| 219 | +catch it. GraphCompose uses a pixel-diff visual parity gate for each |
| 220 | +shipped CV / cover-letter preset and for the engine showcase tests |
| 221 | +(see `CvV2VisualParityTest`, `CoverLetterV2VisualParityTest`, |
| 222 | +`TableRowSpanDemoTest` and friends). |
| 223 | + |
| 224 | +The harness behind those tests |
| 225 | +(`com.demcha.testing.visual.PdfVisualRegression` + |
| 226 | +`ImageDiff`) is currently **test-only** inside the GraphCompose |
| 227 | +build. Promoting it to a public `com.demcha.compose.testing.visual.*` |
| 228 | +API so library consumers can adopt the same pixel-level gate against |
| 229 | +their own presets is queued as **v1.6.8 / v1.7.0 Track N** — see the |
| 230 | +release-readiness taskboard. Until that ships, the recommended |
| 231 | +public path is layout snapshot above; for pixel-level work, copy |
| 232 | +the pattern from `PdfVisualRegression` (it builds on the public |
| 233 | +`com.demcha.compose.devtool.PdfRenderBridge` for PDF page → image |
| 234 | +conversion). |
| 235 | + |
| 236 | +--- |
| 237 | + |
| 238 | +## When to use which layer |
| 239 | + |
| 240 | +| You want to know that… | Use | |
| 241 | +|---|---| |
| 242 | +| The document compiles + renders at all | smoke (just call `buildPdf()` in a test) | |
| 243 | +| The semantic graph and resolved coordinates are stable across engine refactors | **layout snapshot** | |
| 244 | +| The PDF visually looks identical, fonts/colours and all | pixel-level visual (Track N) | |
| 245 | +| A specific layout math rule holds | a focused unit test | |
| 246 | + |
| 247 | +The advice scales: a flagship template or a preset you publish to |
| 248 | +others deserves all three. A one-off internal report needs smoke + |
| 249 | +layout snapshot — that catches 95% of the regressions you'd care |
| 250 | +about, at near-zero cost per run. |
| 251 | + |
| 252 | +--- |
| 253 | + |
| 254 | +## Deeper reference |
| 255 | + |
| 256 | +- [Layout snapshot testing](./layout-snapshot-testing.md) — |
| 257 | + full reference: pipeline position, snapshot contents, |
| 258 | + determinism guarantees, downstream-project adoption, CI policy, |
| 259 | + what NOT to snapshot. |
| 260 | +- [`LayoutSnapshotPublicApiDogfoodTest`](../../src/test/java/com/demcha/testing/layout/LayoutSnapshotPublicApiDogfoodTest.java) |
| 261 | + — a working integration test that drives the snapshot API |
| 262 | + entirely through the published surface. Copyable starting point. |
| 263 | +- [`CvV2VisualParityTest`](../../src/test/java/com/demcha/compose/document/templates/cv/v2/presets/CvV2VisualParityTest.java) |
| 264 | + — example of the pixel-level pattern (currently test-only; |
| 265 | + becoming public via Track N). |
0 commit comments