Manifest
The format
The normative description is the JSON Schema (draft 2020-12) at https://visual-regression.dev/schema/v1.json, shipped as schema.json (in the package, and in the repository under src/) and importable as visual-regression-manifest/schema.json or the manifestSchema export.
The normative description is the JSON Schema (draft 2020-12) at https://visual-regression.dev/schema/v1.json, shipped as schema.json (in the package, and in the repository under src/) and importable as visual-regression-manifest/schema.json or the manifestSchema export. The TypeScript types (Manifest, ManifestEntry, ...) mirror it. A trimmed example:
{
"reportFormat": "visual-regression-manifest",
"specVersion": "1.0.0-beta.1",
"createdAt": "2026-09-22T10:00:00.000Z",
"updatedAt": "2026-09-22T10:03:12.481Z",
"projectRoot": "/home/runner/work/app/app", // every relative path below resolves against it
"platform": {
"osPlatform": "linux",
"arch": "x64",
"osRelease": "6.8.0-1021-azure",
},
"ci": {
"provider": "github",
"repositoryName": "acme/app",
"repositoryUrl": "https://github.com/acme/app",
"commit": "0f1e2d…", // on pull_request events: the merge commit
"pullRequest": {
"number": 42,
"headSha": "9a8b7c…",
"headRef": "feat/new-header",
"baseRef": "main",
},
"buildId": "1234567890",
"buildName": "CI",
"buildUrl": "https://github.com/acme/app/actions/runs/1234567890/attempts/1",
"runAttempt": "1",
"workspace": "/home/runner/work/app/app", // the checkout directory
},
"tool": {
"name": "@pixsame/cypress-plugin-visual-regression-diff",
"version": "4.3.0",
}, // what wrote the file
"options": { "updateImages": "failures-only" }, // the tool's global options, in its own vocabulary
"runner": {
"name": "cypress",
"version": "16.1.0",
"testingType": "e2e",
"mode": "run",
}, // what ran the tests; only `name` is required
"summary": {
"entries": 1,
"passed": 0,
"failed": 1,
"missingBaseline": 0,
"created": 0,
"updated": 0,
"approved": 0,
"needsHuman": 1,
},
"entries": [
{
"name": "home page renders_#0", // the baseline file stem
"test": {
"filePath": "cypress/e2e/home.cy.ts",
"titlePath": ["home page", "renders"],
"retry": 0,
},
"status": "failed",
"comparison": { "diffRatio": 0.0231, "threshold": 0.01 },
"images": {
"baseline": {
"path": "cypress/e2e/__image_snapshots__/home page renders_#0.png",
"width": 1280,
"height": 720,
},
"actual": {
"path": "cypress/e2e/__image_snapshots__/home page renders_#0.actual.png",
"width": 1280,
"height": 720,
},
"diff": {
"path": "cypress/e2e/__image_snapshots__/home page renders_#0.diff.png",
},
},
"baselineWritten": false,
"recordedAt": "2026-09-22T10:03:12.480Z",
"duration": 840, // ms, when the writer measured it
"platform": {
"osPlatform": "linux",
"arch": "x64",
"browser": { "name": "chrome", "version": "130.0.0.0" },
},
"viewport": { "width": 1280, "height": 720 },
"renderer": {
"backend": "native",
"browser": "chrome",
"browserVersion": "130.0.0.0",
},
"hashes": { "baseline": "3b7e…", "actual": "9d21…", "diff": "c0ff…" }, // sha256, only for files that exist
"message": "Image diff factor (2.31%) is bigger than maximum threshold option 1%.",
},
],
}
status | meaning | images.actual.path | images.diff.path | as CTRF status |
|---|---|---|---|---|
passed | within threshold | null | null | passed |
failed | above threshold, actual and diff images kept for review | path | path (if any) | failed |
missing-baseline | no baseline and the tool was told not to create one | path | null | failed |
created | no baseline, the actual image became the baseline | null | null | passed |
updated | baseline overwritten on request | null | null | passed |
approved | baseline replaced from a review UI | null | null | passed |
Rules every writer and reader follows:
- Paths are relative to
projectRootwith/separators and may point outside the project (../…).projectRootis the tested project, not necessarily the repository root:ci.workspaceis the checkout directory. failedandmissing-baselineneed a human. Copyingimages.actual.pathoverimages.baseline.pathapproves them.baselineWrittensays whether the working tree changed, whatever the status.summary.needsHumancounts them, so a consumer can decide without reading the entries.toolwrote the file,runnerran the tests. A Cypress plugin reports itself astooland Cypress asrunner; a converter reports itself astooland the tool it converted from asrunner.platformis where the test ran,rendereris where the pixels came from. Undernativethe renderer is the test browser itself. Entries with different renderers are different baselines.- Browsers are optional. Desktop and mobile tools leave
platform.browserout and describe what they captured in the entry'starget(device, OS version, display density and orientation, locale, theme, app, driver). The run-levelplatformis the host that ran the tests, not necessarily the device.renderer.backendis an open string (native,docker,cloud, or your own, e.g.device);renderer.namenames what rendered the pixels. variantis the baseline identity you control. Entries of the same screenshot name with a differentvariant(iphone-16-ios18-dark) never overwrite each other when manifests are merged. Without one, identity isplatform.osPlatform+platform.browser.name+ renderer.subjectsays what was captured,extracarries tool-specific detail.subjectis a tool-neutral{ id, kind }(for example a Storybook story id with kindstory) for tools that group or match entries by the thing shown rather than by the test.extrais an open object keyed by namespace, on the run, an entry, an image or an attachment:design(tool,file,node,versionof the design-software source a baseline was exported from) andstorybook(args,globals) are the conventional ones;visualRegressionis reserved for the CTRF converters. Neither takes part in baseline identity, and readers ignore namespaces they do not know.attachmentsare any other files of an entry (a trace, a video, a DOM snapshot), in CTRF's attachment shape (name,contentType,path).- Baseline, actual and diff share one image extension (PNG for every current writer);
.actualand.diffgo before it. - Every entry is self-sufficient (its own
platform,viewport,renderer), so the entries of several manifests (a matrix of machines, one file per worker, e2e + component) can be merged. - One file per writer process, named
visual-regression-manifest[.<label>].json, rewritten after every comparison, so it is complete even when the run is aborted.**/visual-regression-manifest{,.*}.json(MANIFEST_FILE_GLOB) finds all of them in an artifact and nothing named merely alike. Merging warns when one run reports the same screenshot identity twice with different results; give such entries a distinctvariant. - Consumers ignore keys they do not know. The major of
specVersionis bumped on breaking changes only; a reader accepts every version with the major it knows. - On GitHub
pull_requesteventsci.commitis the temporary merge commit; useci.pullRequest.headSha/headRefto write to the branch.ci.buildIdandci.runAttemptlocate the workflow artifact.