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
| BackstopJS | Snapvisor 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, readyTimeout | Your test runner's own wait, such as await page.locator(".loaded").waitFor(), before the capture call. |
delay, postInteractionWait | Prefer 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, scrollToSelector | Playwright'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 reference | The first build on your base branch. |
backstop test | Your test run plus the upload. A report is the build in the Snapvisor app. |
backstop approve | Approve the change in the review UI. See Review workflow. |
backstop_data/bitmaps_reference in Git | Nothing in your repository. Baselines live on the server. |
backstop test --docker | Not required for baselines, since they are not machine files. Keep CI in one consistent environment. |
asyncCaptureLimit, asyncCompareLimit | Your 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/playwrightRegister 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
registerArgosTaskfrom@snapvisor/cypress/taskin your config, addimport "@snapvisor/cypress/support"to your support file, and callcy.argosScreenshot("name"). - Storybook. If your scenarios were really components, capture stories instead. Use
argosVitestPluginfrom@snapvisor/storybook/vitest-plugin, orargosScreenshotfrom@snapvisor/storybook/test-runnerinpostVisit. - Puppeteer.
argosScreenshotfrom@snapvisor/puppeteersaves 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 --dockerAfter, 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:
- Add the Snapvisor tests and reporter, and merge them to your base branch so the first Snapvisor build becomes the baseline.
- Keep BackstopJS as your gate, and the Snapvisor check optional, while you compare results.
- 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.
What you can delete
Once builds are landing in Snapvisor and the check is required:
backstop.json(orbackstop.js) and thebackstopdependency.- The
backstop_datafolder:bitmaps_reference,bitmaps_test, the report folders andengine_scripts. Remove it from Git LFS too if you stored references there. - The
.gitignorerules forbitmaps_testand the HTML and JSON reports. - The
backstop reference,backstop testandbackstop approvescripts inpackage.json, docs and CI. - The Docker image or job that existed only to render BackstopJS references on one machine.
- 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:
- Open the build and confirm it has a screenshot for each scenario and viewport you replaced.
- Look through the screenshots for fonts, images and animations that did not settle. Fix those first, before any threshold change.
- Run the same build twice on one commit and check that nothing shows as changed.
- Open a pull request with one small visual change. Confirm the Snapvisor status check appears, asks for review, and clears after you approve.
- 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.
Migrate from Playwright toHaveScreenshot
Move a Playwright suite from expect(page).toHaveScreenshot and committed snapshot PNGs to Snapvisor - how assertions, baselines, tolerances, masks and per-platform snapshot names translate.
Baseline builds
The baseline is the last approved build on your base branch. Learn how Snapvisor picks the reference build and resolves the base branch for every comparison.