Snapvisor Docs

Migrate from BackstopJS

Move a BackstopJS suite to Snapvisor - how backstop.json scenarios, viewports, selectors, hidden elements, mismatch thresholds, reference images and backstop approve translate.

This guide moves a BackstopJS suite, a backstop.json with scenarios and viewports, to Snapvisor. Your pages and your CI stay. What changes is where scenarios are defined, where baselines live, and how changes are reviewed. The examples use Playwright, which BackstopJS can also use as its engine, and the other SDKs follow the same pattern.

The one change that matters

BackstopJS keeps its own folder of images. backstop reference writes reference images to backstop_data/bitmaps_reference (the default path), backstop test writes new ones next to them and builds a report, and backstop approve promotes the latest test images to the reference collection. Teams commonly commit the reference folder to Git.

Snapvisor stores baselines server-side, per reference branch. Your tests capture screenshots, your pipeline uploads them, and Snapvisor compares them with the approved screenshots for the branch. You approve changes in the review UI, and they become the new baseline for that branch. See Baseline builds.

Your reference images do not carry over, and there is no import path. Your first Snapvisor build on your base branch becomes the baseline. Plan the switch for a moment when your base branch is visually correct.

Concept mapping

BackstopJSSnapvisor equivalent
A scenario in backstop.json (label, url)A test that visits the URL and calls argosScreenshot(page, "label") from @snapvisor/playwright. See Turn a scenario into a test.
viewports (label, width, height)The viewports option, which takes sizes such as { width: 1280, height: 800 } or presets such as "iphone-6".
selectors (default: the whole document)Page captures are full page by default. For one element, pass element: page.locator(".card").
selectors: ["viewport"]fullPage: false, a viewport-sized capture.
hideSelectors (visibility: hidden)argosCSS, or a mask. See Hide, remove and mask.
removeSelectors (display: none)argosCSS with display: none.
misMatchThreshold (percent of differing pixels allowed)threshold. They are not the same scale. See Thresholds.
readySelector, readyEvent, readyTimeoutYour test runner's own wait, such as await page.locator(".loaded").waitFor(), before the capture call.
delay, postInteractionWaitPrefer waiting for a condition. beforeScreenshot runs your code right before the capture.
onBeforeScript, onReadyScript (in engine_scripts)Ordinary test code before the capture call: cookies, clicks, hovers, scrolling.
clickSelector, hoverSelector, keyPressSelectors, scrollToSelectorPlaywright's click, hover, press and scrollIntoViewIfNeeded in the test.
referenceUrl (compare against another URL)Not needed. The baseline is the approved screenshot for the branch, not a second URL.
backstop referenceThe first build on your base branch.
backstop testYour test run plus the upload. A report is the build in the Snapvisor app.
backstop approveApprove the change in the review UI. See Review workflow.
backstop_data/bitmaps_reference in GitNothing in your repository. Baselines live on the server.
backstop test --dockerNot required for baselines, since they are not machine files. Keep CI in one consistent environment.
asyncCaptureLimit, asyncCompareLimitYour runner's workers setting, and parallel testing to split a large suite.

Turn a scenario into a test

A BackstopJS scenario describes a page and what to do before capturing it:

{
  "scenarios": [
    {
      "label": "Pricing",
      "url": "http://localhost:3000/pricing",
      "readySelector": ".plans",
      "hideSelectors": [".cookie-banner"],
      "removeSelectors": [".live-chat"],
      "misMatchThreshold": 0.1
    }
  ],
  "viewports": [
    { "label": "phone", "width": 375, "height": 667 },
    { "label": "desktop", "width": 1280, "height": 800 }
  ]
}

The same scenario as a Playwright test:

// tests/visual.spec.ts
import { test } from "@playwright/test";
import { argosScreenshot } from "@snapvisor/playwright";

test("pricing", async ({ page }) => {
  await page.goto("http://localhost:3000/pricing");
  await page.locator(".plans").waitFor();
  await argosScreenshot(page, "pricing", {
    viewports: [
      { width: 375, height: 667 },
      { width: 1280, height: 800 },
    ],
    argosCSS: `.cookie-banner { visibility: hidden; } .live-chat { display: none; }`,
  });
});

Repeat for each scenario, or loop over a list of routes to keep the file short. The screenshot name must be unique within a test run.

argosScreenshot stabilizes the page before it captures. It waits for web fonts, images and aria-busy regions to settle, hides carets and scrollbars, and normalizes font anti-aliasing. It is on by default, so many delay values can go. Animations are also disabled by default; pass animations: "allow" to opt out. Page captures are full page by default, which matches BackstopJS capturing the whole document when selectors is not set. See Flaky test detection for how recurring changes are handled.

Install and report

npm install --save-dev @snapvisor/playwright

Register the reporter, which uploads screenshots when the run finishes:

// playwright.config.ts
import { defineConfig } from "@playwright/test";
import { createArgosReporterOptions } from "@snapvisor/playwright/reporter";

export default defineConfig({
  reporter: [
    process.env.CI ? ["dot"] : ["list"],
    [
      "@snapvisor/playwright/reporter",
      createArgosReporterOptions({ uploadToArgos: !!process.env.CI }),
    ],
  ],
});

Other ways to capture

You are not limited to Playwright:

  • Cypress. Register registerArgosTask from @snapvisor/cypress/task in your config, add import "@snapvisor/cypress/support" to your support file, and call cy.argosScreenshot("name").
  • Storybook. If your scenarios were really components, capture stories instead. Use argosVitestPlugin from @snapvisor/storybook/vitest-plugin, or argosScreenshot from @snapvisor/storybook/test-runner in postVisit.
  • Puppeteer. argosScreenshot from @snapvisor/puppeteer saves screenshots locally, and you upload them with the CLI.
  • Any other source of images. Write PNGs to a folder and upload it with npx @snapvisor/cli upload ./screenshots.

See SDKs for the options of each.

Hide, remove and mask

BackstopJS has two ways to take an element out of a capture: hideSelectors sets visibility: hidden, so the space stays, and removeSelectors sets display: none, so the layout reflows. Use argosCSS to do the same:

await argosScreenshot(page, "home", {
  argosCSS: `.cookie-banner { visibility: hidden; } .live-chat { display: none; }`,
});

To hide the area in the screenshot without changing the page, use Playwright's mask:

await argosScreenshot(page, "dashboard", {
  mask: [page.locator(".clock"), page.locator("[data-testid=avatar]")],
});

A mask covers the area, so changes inside it are not compared. Use it for clocks, ads and random data rather than raising the threshold. Passing your own mask replaces Snapvisor's default mask, [data-visual-test="blackout"], so include that selector too if you rely on it.

Thresholds

misMatchThreshold is the percentage of pixels that may differ before a scenario fails. Snapvisor has one setting, threshold, a sensitivity between 0 and 1 that defaults to 0.5. Higher means less sensitive. It does not mean the same thing as misMatchThreshold, so do not copy the number over:

await argosScreenshot(page, "chart", { threshold: 0.7 });

Start from the default and adjust only when a specific screenshot is noisy. If you raised misMatchThreshold to absorb font or anti-aliasing differences between machines, remove it and try the default first. A change is flagged for review, not failed, so a small difference is a decision for a reviewer and not a red build.

The CI change

Before, the job ran BackstopJS, often in Docker, and a changed scenario failed the job:

- run: npm ci
- run: npx backstop test --docker

After, the job runs your tests with the project token. A changed screenshot no longer fails the job:

- run: npm ci
- run: npx playwright install --with-deps chromium
- run: npx playwright test # captures via argosScreenshot, uploads via the reporter
  env:
    ARGOS_TOKEN: ${{ secrets.ARGOS_TOKEN }}

Copy the project token from your project's settings in the app, and store it as a CI secret, never in the repository. See Access tokens. On GitHub Actions you can drop the secret with tokenless authentication. More workflows, including merge queues and fork pull requests, are in GitHub Actions recipes.

Run both side by side

You do not have to switch in one pull request. Keep backstop test running and add the Snapvisor capture calls next to your existing setup:

  1. Add the Snapvisor tests and reporter, and merge them to your base branch so the first Snapvisor build becomes the baseline.
  2. Keep BackstopJS as your gate, and the Snapvisor check optional, while you compare results.
  3. When the Snapvisor builds match your expectations on a few pull requests, make the Snapvisor check required, then remove BackstopJS.

Make the Snapvisor check the gate

upload exits 0 once the build has been created, whether or not it contains changes. A non-zero exit means the upload itself failed (bad token, unresolvable commit, network, missing parallel arguments). The review gate is the commit status check Snapvisor posts to the pull request, which asks for review until someone approves or rejects the changes.

If the check is optional, nothing blocks a merge. A green test job means the screenshots arrived, not that they are unchanged. Require the check in branch protection — see GitHub or GitLab.

What you can delete

Once builds are landing in Snapvisor and the check is required:

  1. backstop.json (or backstop.js) and the backstop dependency.
  2. The backstop_data folder: bitmaps_reference, bitmaps_test, the report folders and engine_scripts. Remove it from Git LFS too if you stored references there.
  3. The .gitignore rules for bitmaps_test and the HTML and JSON reports.
  4. The backstop reference, backstop test and backstop approve scripts in package.json, docs and CI.
  5. The Docker image or job that existed only to render BackstopJS references on one machine.
  6. Any job that published the HTML report as an artifact. The build in the Snapvisor app replaces it.

Your pull requests stop carrying binary image changes. Reviewers see the before and after in the review UI instead.

First-build checklist

After the first Snapvisor build on your base branch:

  1. Open the build and confirm it has a screenshot for each scenario and viewport you replaced.
  2. Look through the screenshots for fonts, images and animations that did not settle. Fix those first, before any threshold change.
  3. Run the same build twice on one commit and check that nothing shows as changed.
  4. Open a pull request with one small visual change. Confirm the Snapvisor status check appears, asks for review, and clears after you approve.
  5. Make the Snapvisor check required in branch protection.

Frequently asked questions

Do I have to rewrite my scenarios as tests?
Yes, each scenario becomes a test or a story that calls the Snapvisor capture function. BackstopJS reads scenarios from backstop.json. Snapvisor captures from the test runner you already use, such as Playwright, Cypress, Storybook or Puppeteer, so a scenario becomes a page visit plus a capture call.
Can I import my bitmaps_reference folder as baselines?
No. There is no import path. Your first Snapvisor build on your base branch becomes the new baseline: it has nothing to compare against, so it is approved automatically. Run it when your base branch is visually correct.
What replaces backstop approve?
Approving in the Snapvisor web app. You review the changes in a build and approve or reject them there. Nothing is copied between folders and no image files are committed.
Can I run BackstopJS and Snapvisor side by side while I switch?
Yes. They are independent. Keep backstop test running, add the Snapvisor capture calls next to your existing setup, and remove BackstopJS once the Snapvisor check is your gate.

On this page