Snapshot testing
Use bun:test's `toMatchSnapshot()` helper for golden-file tests, plus `--update-snapshots` to regenerate after intentional UI or contract changes.
On this page
Snapshot testing
Mandu doesn't reinvent snapshots — it uses bun:test's built-in
toMatchSnapshot() matcher and adds the convenience flag
--update-snapshots (equivalent to bun test --update-snapshots) so
the same gesture works through mandu test.
Quick start
# Run tests; failing snapshots produce an inline diff
mandu test
# Regenerate every snapshot after an intentional change
mandu test --update-snapshots
# Regenerate only one file
mandu test --update-snapshots --filter "home page renders"
The shape of a snapshot test
// src/render/home.test.ts
import { test, expect } from "bun:test";
import { renderHome } from "./home";
test("home page renders", () => {
const html = renderHome({ user: { name: "Alice" } });
expect(html).toMatchSnapshot();
});
First run writes __snapshots__/home.test.snap:
// __snapshots__/home.test.snap
exports[\`home page renders 1\`] =
\`
<main>
<h1>Welcome, Alice</h1>
</main>
\`;
Later runs diff against that file. On a mismatch you get a terminal
diff — review it, then either fix the code or re-run with
--update-snapshots if the change was intentional.
Golden-file conventions
Snapshots work best when their inputs and outputs are deterministic.
A snapshot that embeds Date.now(), a random UUID, or an IP address
will flap in CI.
Normalize volatile values before the assertion:
import { normalizeSnapshot } from "./test-utils";
test("user JSON response shape", async () => {
const res = await testFilling(handler, { /* ... */ });
const body = await res.json();
expect(normalizeSnapshot(body)).toMatchSnapshot();
});
// src/shared/test-utils.ts
export function normalizeSnapshot<T>(value: T): T {
const replaced = JSON.parse(
JSON.stringify(value)
.replace(/"createdAt":"[^"]+"/g, '"createdAt":"<ISO-TIMESTAMP>"')
.replace(/"id":"[a-f0-9-]{36}"/g, '"id":"<UUID>"'),
);
return replaced;
}
When to use (and not to)
| Use snapshots for | Do NOT use snapshots for |
|---|---|
| Rendered HTML / JSON whose shape is stable | Anything with timestamps, random IDs, session tokens |
CLI stdout you control (mandu routes list --json) |
Playwright screenshots (see docs/testing/e2e) |
| Error banners / help text you want to keep stable | Very large payloads — snapshot readability matters |
| Config file round-trips (lockfile, manifest) | Logical assertions (use toEqual / toBe) |
Reviewing changes
When a snapshot changes, treat it like any code change: the diff belongs in the same PR as the source change, and a reviewer should read it.
# Surface only snapshot diffs in a PR review
git diff main -- '**/__snapshots__/**'
A large, unexpected snapshot diff is a strong signal that you accidentally changed the rendered output — revert, or if intentional, add a note to the PR body explaining what changed.
Integration with coverage
Snapshots themselves don't register coverage lines, but the code that
produces the snapshot does. Running mandu test --coverage alongside
snapshot tests gives you both signals in one shot.
Common pitfalls
snapshot 'x' does not match in CI only — your snapshot includes
a CI-only value (e.g. process.env.CI === "true" branching). Normalize
it before the assertion.
Snapshot file is huge — extract the interesting subset before asserting. A 4KB snapshot is harder to diff than a focused 20-line one.
--update-snapshots wrote new snapshots I didn't expect —
--update-snapshots regenerates every failing snapshot. If you only
wanted one, add a --filter alongside.
🤖 Agent Prompt
Apply the guidance from the Mandu docs page at https://mandujs.com/docs/testing/snapshot to my project.
Summary of the page:
Snapshot testing uses `expect(x).toMatchSnapshot()` from bun:test. Snapshots live in `__snapshots__/<file>.snap` next to the test. Run `--update-snapshots` after an intentional change. Keep snapshots deterministic — strip timestamps/IDs first.
Required invariants — must hold after your changes:
- Snapshot files live at `__snapshots__/<test-file>.snap` next to the test
- `--update-snapshots` regenerates snapshots in place — always review the diff before committing
- `toMatchSnapshot()` is provided by `bun:test`; no separate dep is required
- Snapshots must be deterministic — serialize timestamps/IDs through a helper before the assertion
Then:
1. Make the change in my codebase consistent with the page.
2. Run `bun run guard` and `bun run check` to verify nothing
in src/ or app/ breaks Mandu's invariants.
3. Show me the diff and any guard violations.
Related
- Unit tests — snapshot tests are a kind of unit test.
- Watch mode —
--update-snapshotsis forwarded to every re-run under--watch, which is handy when iterating on UI shape. - bun:test docs — upstream reference.
For Agents
{
"schema": "mandu.testing.snapshot/v0.24",
"matcher": "toMatchSnapshot",
"snapshot_path_convention": "__snapshots__/<test-file>.snap",
"update_flag": "--update-snapshots",
"determinism_required": true,
"rules": [
"Normalize timestamps/UUIDs/tokens before `toMatchSnapshot`",
"Review every snapshot diff in PRs — treat as code change",
"Use `--filter` with `--update-snapshots` to regenerate one target only"
]
}For Agents
Snapshot testing uses `expect(x).toMatchSnapshot()` from bun:test. Snapshots live in `__snapshots__/<file>.snap` next to the test. Run `--update-snapshots` after an intentional change. Keep snapshots deterministic — strip timestamps/IDs first.
- Snapshot files live at `__snapshots__/<test-file>.snap` next to the test
- `--update-snapshots` regenerates snapshots in place — always review the diff before committing
- `toMatchSnapshot()` is provided by `bun:test`; no separate dep is required
- Snapshots must be deterministic — serialize timestamps/IDs through a helper before the assertion