LangENKO

Snapshot testing

Use bun:test's `toMatchSnapshot()` helper for golden-file tests, plus `--update-snapshots` to regenerate after intentional UI or contract changes.

since v0.24
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

🤖 Agent Prompt — Snapshot testing
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.
  • Unit tests — snapshot tests are a kind of unit test.
  • Watch mode--update-snapshots is 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

AI hint

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.

Invariants
  • 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
Guard scope
testing