skills/vitest-snap-write-snapshot/SKILL.md
Write snapshot tests with toJsonSnapshot, toYamlSnapshot, toMarkdownSnapshot, toTextSnapshot. Covers SnapOptions (dir, name, fileExtension, args, indent), auto-naming from test name, parametric snapshots with test.each and args, Markdown table rendering rules, and yaml peer dependency for toYamlSnapshot. Use when writing any vitest-snap matcher call.
npx skillsauth add Odonno/vitest-snap vitest-snap-write-snapshotInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Install and register matchers via side-effect import in vitest setup file:
// vitest.setup.ts
import "vitest-snap";
// vitest.config.ts
import { defineConfig } from "vitest/config";
export default defineConfig({
test: {
setupFiles: ["./vitest.setup.ts"],
},
});
For toYamlSnapshot, install the yaml peer dependency:
bun add yaml
Minimum working test:
import { describe, expect, test } from "vitest";
// matchers registered via side-effect import in vitest.setup.ts
test("user object", async () => {
const user = { id: 1, name: "Alice", createdAt: new Date() };
await expect(user).toJsonSnapshot();
// writes: ./snapshots/user-object
// { "id": 1, "name": "Alice", "createdAt": "[Date_1]" }
});
toJsonSnapshot(options?: SnapJsonOptions<T>): Promise<void>
toYamlSnapshot(options?: SnapYamlOptions<T>): Promise<void>
toMarkdownSnapshot(options?: SnapMarkdownOptions<T>): Promise<void>
toTextSnapshot(options?: SnapTextOptions): Promise<void>
Options available per matcher:
| Option | JSON | YAML | Markdown | Text |
| --------------- | ---- | ---- | -------- | ---- |
| dir | ✓ | ✓ | ✓ | ✓ |
| name | ✓ | ✓ | ✓ | ✓ |
| fileExtension | ✓ | ✓ | ✓ | ✓ |
| args | ✓ | ✓ | ✓ | ✓ |
| filters | ✓ | ✓ | ✓ | ✗ |
| redactions | ✓ | ✓ | ✓ | ✗ |
| indent | ✓ | ✓ | ✗ | ✗ |
When no name is provided, the snapshot filename is derived from the slugified current test name:
test("user profile data", async () => {
await expect(user).toJsonSnapshot();
// writes: ./snapshots/user-profile-data
});
describe("users", () => {
test("admin role", async () => {
await expect(admin).toJsonSnapshot();
// writes: ./snapshots/admin-role
});
});
The slug is computed from the leaf test name only (not the describe block name).
test.each([10, 50, 100])("pagination page-size %i", async (pageSize) => {
const result = await fetchUsers({ pageSize });
await expect(result).toJsonSnapshot({ name: "page-size", args: [pageSize] });
// writes: ./snapshots/page-size_10
// writes: ./snapshots/page-size_50
// writes: ./snapshots/page-size_100
});
args values are appended to the filename with _ separator. Always provide name + args together in test.each to decouple snapshot filenames from test description strings.
test("config", async () => {
await expect(config).toJsonSnapshot({
name: "app-config",
fileExtension: "json",
dir: "./__snapshots__",
});
// writes: ./__snapshots__/app-config.json
});
Default file extension is empty (no extension). The dir option is relative to the test file location.
await expect(data).toJsonSnapshot({ indent: 4 });
// uses 4-space indent instead of default 2
await expect(data).toYamlSnapshot({ indent: 2 });
// array of objects → one row per element, columns = union of all keys
await expect(users).toMarkdownSnapshot();
// | id | name | role |
// | -- | ----- | ------ |
// | 1 | Alice | admin |
// | 2 | Bob | viewer |
| Input | Output |
| ----------------------- | ------------------------------------------------ |
| Array of objects | One row per element; columns = union of all keys |
| Single object | One row; columns = object keys |
| Primitive | Single-column table with header value |
| null/undefined cell | Empty string |
| Nested object cell | JSON.stringify(value) |
| Date cell | .toISOString() |
test("server config", async () => {
await expect(serverConfig).toYamlSnapshot();
// writes: ./snapshots/server-config
// host: localhost
// port: 8080
});
Requires yaml peer dependency installed. Serialization uses the yaml package internally.
test("csv export", async () => {
const csv = generateCsv(data);
await expect(csv).toTextSnapshot();
// writes: ./snapshots/csv-export
});
toTextSnapshot expects a string value. No serialization is applied — the string is written as-is.
Wrong:
test("user", () => {
expect(user).toJsonSnapshot(); // no await
});
Correct:
test("user", async () => {
await expect(user).toJsonSnapshot();
});
All four matchers return Promise<void>; omitting await makes the test pass without writing or diffing the snapshot file.
Source: src/index.ts
Wrong:
await expect(user).toTextSnapshot({
filters: [new ExcludeFilter(".password")], // silently ignored
});
Correct:
await expect(user).toJsonSnapshot({
filters: [new ExcludeFilter(".password")],
});
SnapTextOptions omits filters and redactions — passing them has no effect. Use toJsonSnapshot for transformation pipelines.
Source: src/options/types.ts — SnapTextOptions
Wrong:
test.each([10, 50])("page size %i", async (pageSize) => {
// slug changes if test name changes
await expect(result).toJsonSnapshot();
});
Correct:
test.each([10, 50])("page size %i", async (pageSize) => {
await expect(result).toJsonSnapshot({ name: "page-size", args: [pageSize] });
});
Without args, the slug is derived from the full interpolated test name. Renaming the test renames all snapshot files.
Source: src/options/file.ts; docs/content/docs/matchers/to-json-snapshot.mdx
Wrong:
await expect(data).toJsonSnapshot({ fileExtension: "..json" }); // → file..json
Correct:
await expect(data).toJsonSnapshot({ fileExtension: "json" });
// or
await expect(data).toJsonSnapshot({ fileExtension: ".json" });
detectGeneratedFilePath prepends . only when the extension does not already start with .. ..json produces filename..json.
Source: src/options/file.ts
Default: ./snapshots/<slug> relative to the test file.
Override with dir:
// absolute path
await expect(data).toJsonSnapshot({ dir: "/tmp/snapshots" });
// relative to test file
await expect(data).toJsonSnapshot({ dir: "./custom-snapshots" });
Run vitest with the --update flag to regenerate snapshot files:
bunx vitest run --update
# or
bunx vitest run -u
testing
Install vitest-snap, register matchers via side-effect import in Vitest setupFiles, configure global defaults with configureGlobalSnapOptions. Covers option merge order (defaults → global → per-call), yaml peer dependency for toYamlSnapshot, UndefinedFilter/Date redaction defaults, and snapshot file migration when changing global dir.
testing
Apply redactions to replace or stabilize values before snapshotting: ValueRedaction(predicate, replaced, counting), ReplacedRedaction(selector, replaced, counting), SortedRedaction(selector). Covers counting behaviour ([Tag] → [Tag_1], [Tag_2]), deepest-first traversal order, default Date redaction override (custom array replaces it), .** selector root replacement risk, and Selector<T> typed paths. Use when configuring the redactions option on any matcher.
testing
End-to-end guide for adding vitest-snap to an existing Vitest project: install package, register matchers in setupFiles via side-effect import, write first toJsonSnapshot call, understand auto-naming and built-in defaults (UndefinedFilter, Date redaction), then add a filter or redaction. Use when bootstrapping vitest-snap from scratch.
testing
Apply filters to remove nodes before snapshotting: UndefinedFilter (default), NullFilter, ExcludeFilter(selector), IncludeFilter(selector[]). Covers Selector<T> typed path DSL, bare selector string shorthand for IncludeFilter, adjacent IncludeFilter auto-merge via regroupFilters, default filters override behaviour (custom array replaces UndefinedFilter), and array slot preservation semantics. Use when configuring the filters option on any matcher.