Migrate from Lost Pixel
Move a lostpixel.config.js suite to Snapvisor - how shots, masks, breakpoints, and thresholds translate, and why baselines stop living in your git repository.
The Lost Pixel repository is archived and no longer maintained. If you are running it today it still works, but nothing is coming: no Playwright updates, no fixes for the open issues. This guide moves an existing Lost Pixel suite to Snapvisor without rewriting your tests from scratch.
The one change that matters
Lost Pixel keeps baselines as image files on your disk, under imagePathBaseline (.lostpixel/baseline/ by default). Something has to persist that folder between runs, so in practice you either committed thousands of PNGs to git — bloating the repository and producing binary merge conflicts nobody can resolve — or you wrote a CI step to sync it to object storage yourself.
Snapvisor stores baselines server-side, per reference branch. There is no baseline folder, nothing to commit, nothing to sync, and no step that copies this run's images over last run's to accept a change. You approve the change in the review UI and it becomes the new baseline for that branch. See Baseline builds for how the reference build is resolved.
This also means your existing baselines do not carry over, and there is no import path for them. Your first Snapvisor build on your base branch becomes the new baseline — it has nothing to compare against, so it is approved automatically. Plan the switch for a moment when your base branch is visually correct.
Pick a path
Which route you take depends on how Lost Pixel was capturing images.
- You used
pageShotsorstorybookShots— move the capture into Playwright with@snapvisor/playwright, or into@snapvisor/storybook. This is the path that gives you masking, viewports, and stabilization. - You used
customShotsor already had a folder full of PNGs — keep producing them exactly as you do now and hand the folder to@snapvisor/cli upload. Nothing about how you take screenshots has to change.
Both paths end at the same place: a build in Snapvisor, diffed against the branch baseline, reported as a status check on your pull request.
Config mapping
lostpixel.config.js | Snapvisor equivalent |
|---|---|
imagePathBaseline, imagePathCurrent, imagePathDifference | Nothing. Baselines, current shots, and diffs all live server-side. |
pageShots.baseUrl + pageShots.pages | One Playwright test per page, with page.goto() and argosScreenshot(). |
storybookShots | @snapvisor/storybook |
customShots.currentShotsPath | npx @snapvisor/cli upload <directory> |
fullPage | fullPage: true on argosScreenshot (Playwright's own screenshot option, passed through). |
mask: [{ selector: ".foo" }] | mask: [page.locator(".foo")] — Playwright's native masking, passed through unchanged. |
breakpoints: [375, 1280] | viewports on argosScreenshot — full sizes or named presets, not bare widths. |
waitBeforeScreenshot | Automatic stabilization (stabilize, on by default) — see Waiting. |
threshold | threshold, but the units are different — see Thresholds. |
shotConcurrency, compareConcurrency | Nothing to tune. Comparison runs on Snapvisor's workers, not your CI machine. |
compareEngine (pixelmatch / odiff) | Nothing to choose. One perceptual engine, tuned to ignore anti-aliasing and rendering noise. |
generateOnly, failOnDifference | Neither exists. The gate is the commit status check — see Exit codes. |
lost-pixel finalize | npx @snapvisor/cli finalize — but you only need it for manual-mode parallel builds, not on every run. |
Path A: upload a directory
If your pipeline already writes PNGs somewhere, you are three lines from a working build.
npm install --save-dev @snapvisor/cliexport ARGOS_TOKEN="<your project token>"
# run whatever already produced .lostpixel/current/ — then:
npx @snapvisor/cli upload ./screenshotsThe CLI reads branch and commit from your CI environment, uploads everything matching **/*.{png,jpg,jpeg} (override with --files / --ignore), and prints the build URL. Grab the project token from your project's settings in the app.
Path B: capture from Playwright
This replaces pageShots and gives you Lost Pixel's masking and breakpoints back, plus stabilization.
npm install --save-dev @snapvisor/playwrightRegister the reporter so screenshots upload 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 }),
],
],
});A pageShots block like this:
// lostpixel.config.js
export const config = {
pageShots: {
baseUrl: "http://localhost:3000",
pages: [{ path: "/", name: "home" }],
mask: [{ selector: "[data-testid=timestamp]" }],
breakpoints: [375, 1280],
fullPage: true,
},
};becomes a normal Playwright test:
// tests/visual.spec.ts
import { test } from "@playwright/test";
import { argosScreenshot } from "@snapvisor/playwright";
test("home", async ({ page }) => {
await page.goto("http://localhost:3000/");
await argosScreenshot(page, "home", {
mask: [page.locator("[data-testid=timestamp]")],
viewports: [{ width: 375, height: 812 }, { width: 1280, height: 800 }],
fullPage: true,
});
});mask and fullPage are Playwright's own screenshot options — argosScreenshot passes them straight through, so anything you can express in page.screenshot() works here too.
Note that viewports takes complete viewport sizes, not the bare widths Lost Pixel's breakpoints accepted. You can also name a preset instead, e.g. viewports: ["iphone-6", { width: 1280, height: 800 }].
Writing them as tests is more verbose than a list of paths in a config file, and that is the trade: they run in the test runner you already have, so they get retries, fixtures, authentication, and sharding for free instead of reimplementing each one.
Storybook
storybookShots maps to @snapvisor/storybook, which captures stories through Storybook's own test runner rather than crawling a built directory:
// .storybook/test-runner.ts
import { argosScreenshot } from "@snapvisor/storybook/test-runner";
export default {
async postVisit(page, context) {
await argosScreenshot(page, context);
},
};Every story your test runner visits produces a screenshot, so the story list stays in Storybook instead of being duplicated in a visual-testing config. There is also a Vitest browser-mode plugin if that is how you run stories — see SDKs.
Thresholds: do not copy the number over
Both tools call the setting threshold and they mean different things.
- Lost Pixel: a tolerance for how much may differ before a shot is flagged, defaulting to
0— zero tolerance, every changed pixel fails. - Snapvisor: a sensitivity between 0 and 1, defaulting to
0.5. Higher means less sensitive.
So a Lost Pixel threshold: 0 is not a Snapvisor threshold: 0 — the latter is maximum sensitivity, which will flag anti-aliasing noise. Start from the default and adjust only if a specific screenshot is noisy:
await argosScreenshot(page, "chart", { threshold: 0.7 });Or set it for a whole directory upload:
npx @snapvisor/cli upload ./screenshots --threshold 0.7If you turned Lost Pixel's threshold up because rendering noise kept failing your builds, try the default first. Snapvisor's comparison is perceptual and already ignores anti-aliasing and rendering noise, which is usually what the tolerance was compensating for. The threshold is stored with each screenshot and applied by the same engine that produces the diff you review, so what you set is what gates the build.
Waiting and flakiness
Lost Pixel's answer to "the page isn't ready yet" was waitBeforeScreenshot, a fixed sleep — too short and you get flakes, too long and every shot pays for it.
argosScreenshot stabilizes instead. Before capturing it waits for web fonts to load, for images (including srcset candidates) to decode, and for aria-busy regions to settle — then hides carets and scrollbars, disables spellcheck underlines, and normalizes font anti-aliasing. It is on by default, so drop the sleeps. If a particular screenshot needs something else settled first, beforeScreenshot runs your own code at exactly the right moment.
Snapvisor also detects flaky changes across retries, so a screenshot that only sometimes differs is surfaced as flaky rather than failing the build outright. See Flaky test detection.
Playwright versions
Lost Pixel pins playwright-core to an exact version as a direct dependency — 1.47.2 in the final archived release. If your project ran a newer Playwright, you could end up resolving two Playwright versions at once, with shots captured by a browser build your own tests never used. Because the project is archived, that pin is where it stays.
@snapvisor/playwright does not bundle or pin Playwright. It imports @playwright/test from your project at runtime and declares no peer-dependency range, so it uses whatever version you have installed. Upgrade Playwright on your schedule; nothing in Snapvisor holds you back.
Sharding
Lost Pixel had no shard concept — one process took every shot. Snapvisor groups shards into a single build, so a suite split across ten machines is still one thing to review:
npx @snapvisor/cli upload ./screenshots \
--parallel \
--parallel-nonce "$GITHUB_RUN_ID" \
--parallel-total 10 \
--parallel-index "$SHARD_INDEX"--parallel-index starts at 1, and --parallel-nonce has to be identical across every shard of the same run — a CI run id is the natural choice.
Pass --parallel-total -1 when you don't know the shard count up front, then close the build with npx @snapvisor/cli finalize --parallel-nonce "$GITHUB_RUN_ID" in a step that always runs. Full details on Parallel testing.
Exit codes work differently
This is the part that breaks pipelines during a migration, so change it deliberately.
Lost Pixel exits non-zero from the lost-pixel process when differences are found. Your CI step going red was the failure signal.
Snapvisor's CLI does not do this. 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.
So instead of relying on a red step, make the Snapvisor check required:
- GitHub — branch protection, require the Snapvisor status check. See GitHub.
- GitLab — the merge request pipeline status. See GitLab.
If you leave the check optional, nothing blocks a merge. A green CI job after upload means "the screenshots arrived", not "the screenshots are unchanged" — the two were the same thing in Lost Pixel and are not here.
If you were running Lost Pixel outside pull requests — nightly against production, say — that is monitoring mode, where a detected change sets the status to failure and waits on your decision.
A worked CI change
Before — the step goes red when anything moved, and that was the gate:
- run: npm run build
- run: npx lost-pixel # exits 1 on any visual differenceAfter — the step goes red only if the upload failed, and the gate moves to the status check:
- run: npm run build
- run: npx playwright test # captures via argosScreenshot, uploads via the reporter
env:
ARGOS_TOKEN: ${{ secrets.ARGOS_TOKEN }}Then require the Snapvisor check in branch protection. On GitHub Actions you can drop the secret entirely — see tokenless authentication.
Cleaning up
Once builds are landing in Snapvisor:
- Delete
lostpixel.config.jsand the.lostpixel/directory. - Remove the baseline PNGs from git — and from Git LFS, if you put them there.
- Drop
lost-pixelfromdevDependencies, along with anyplaywright-corepin you added to keep it happy. - Remove the old status check from branch protection so it stops blocking merges.
Getting started
Create a Snapvisor project, set your project token, capture screenshots from your existing test suite, and upload your first build.
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.