Skip to content

Commit c4f54d0

Browse files
authored
docs(testing): add Testing your document quickstart guide (#116)
New docs/operations/test-your-document.md — a tutorial-style end-to-end recipe for protecting a custom document or template with automated tests. Five steps from "author the document" to "CI guards the shape on every PR", with a quick layer-selection table (smoke / layout snapshot / pixel-level visual). Complements the existing reference-style layout-snapshot-testing.md (deep dive on the public API, pipeline position, downstream-project adoption, CI policy). The new doc is the "where do I start" landing page; the existing doc remains the lookup-friendly reference. - README's "What can I do with this?" table row now links to BOTH docs so the quickstart is the first thing a new reader finds. - CHANGELOG entry under v1.6.8 Planned § Documentation. - Pixel-level visual gate is mentioned as "queued for v1.6.8 / v1.7.0 Track N" — accurate per the private taskboard; the guide does not block on N1 because everything it documents for the layout-snapshot path uses already-public API (LayoutSnapshotAssertions, LayoutSnapshotJson, the -Dgraphcompose.updateSnapshots flag).
1 parent 9e47443 commit c4f54d0

3 files changed

Lines changed: 280 additions & 1 deletion

File tree

CHANGELOG.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,20 @@ follow-ups carried over from the v1.6.7 senior review (see
1515
[ROADMAP.md](ROADMAP.md) and the private taskboard). No breaking
1616
changes are planned.
1717

18+
### Documentation
19+
20+
- New quickstart guide
21+
[Testing your document](docs/operations/test-your-document.md)
22+
end-to-end recipe (author the document → add a layout
23+
snapshot test → bless the baseline → CI guards the
24+
shape on every PR), with a "when to use which layer" table for
25+
the three protection tiers (smoke / layout snapshot / pixel-level
26+
visual). Complements the existing
27+
[layout-snapshot-testing.md](docs/operations/layout-snapshot-testing.md)
28+
reference: that one is reference-style, the new one is
29+
tutorial-style. README's "What can I do with this?" table row
30+
now links to both.
31+
1832
## v1.6.7 — 2026-06-01
1933

2034
**Transitive dependency cleanup.** v1.6.7 narrows the runtime

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ GraphCompose uses PDFBox under the hood as the rendering backend — the com
8989
| Generate a one-off PDF programmatically | DSL | `GraphCompose.document(...).pageFlow(...)` — see [Hello world](#hello-world) below |
9090
| Generate a CV / cover letter from data | Layered templates | `ModernProfessional.create().compose(session, cvDocument)` — see [layered templates](./docs/templates/v2-layered/README.md) |
9191
| Add a custom visual primitive | Engine extension | `NodeDefinition` + `PdfFragmentRenderHandler` — see [extension guide](./docs/contributing/extension-guide.md) |
92-
| Regression-test generated layouts | Layout snapshots | `DocumentSession#layoutSnapshot()` — see [snapshot testing](./docs/operations/layout-snapshot-testing.md) |
92+
| 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) |
9393

9494
## Installation
9595

Lines changed: 265 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,265 @@
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

Comments
 (0)