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%.",
    },
  ],
}
statusmeaningimages.actual.pathimages.diff.pathas CTRF status
passedwithin thresholdnullnullpassed
failedabove threshold, actual and diff images kept for reviewpathpath (if any)failed
missing-baselineno baseline and the tool was told not to create onepathnullfailed
createdno baseline, the actual image became the baselinenullnullpassed
updatedbaseline overwritten on requestnullnullpassed
approvedbaseline replaced from a review UInullnullpassed

Rules every writer and reader follows:

  • Paths are relative to projectRoot with / separators and may point outside the project (../…). projectRoot is the tested project, not necessarily the repository root: ci.workspace is the checkout directory.
  • failed and missing-baseline need a human. Copying images.actual.path over images.baseline.path approves them. baselineWritten says whether the working tree changed, whatever the status. summary.needsHuman counts them, so a consumer can decide without reading the entries.
  • tool wrote the file, runner ran the tests. A Cypress plugin reports itself as tool and Cypress as runner; a converter reports itself as tool and the tool it converted from as runner.
  • platform is where the test ran, renderer is where the pixels came from. Under native the renderer is the test browser itself. Entries with different renderers are different baselines.
  • Browsers are optional. Desktop and mobile tools leave platform.browser out and describe what they captured in the entry's target (device, OS version, display density and orientation, locale, theme, app, driver). The run-level platform is the host that ran the tests, not necessarily the device. renderer.backend is an open string (native, docker, cloud, or your own, e.g. device); renderer.name names what rendered the pixels.
  • variant is the baseline identity you control. Entries of the same screenshot name with a different variant (iphone-16-ios18-dark) never overwrite each other when manifests are merged. Without one, identity is platform.osPlatform + platform.browser.name + renderer.
  • subject says what was captured, extra carries tool-specific detail. subject is a tool-neutral { id, kind } (for example a Storybook story id with kind story) for tools that group or match entries by the thing shown rather than by the test. extra is an open object keyed by namespace, on the run, an entry, an image or an attachment: design (tool, file, node, version of the design-software source a baseline was exported from) and storybook (args, globals) are the conventional ones; visualRegression is reserved for the CTRF converters. Neither takes part in baseline identity, and readers ignore namespaces they do not know.
  • attachments are 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); .actual and .diff go 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 distinct variant.
  • Consumers ignore keys they do not know. The major of specVersion is bumped on breaking changes only; a reader accepts every version with the major it knows.
  • On GitHub pull_request events ci.commit is the temporary merge commit; use ci.pullRequest.headSha / headRef to write to the branch. ci.buildId and ci.runAttempt locate the workflow artifact.