Skip unchanged stories and tests in visual testing
UI Verify renders only the stories and tests your change can affect and carries the rest forward, on by default when your build has a dependency graph.
On a large suite most pull requests touch a handful of components. Re-rendering all 1,000 stories or tests every time is slow, and on any per-snapshot tool it is expensive. UI Verify renders just the stories and tests your change can affect and carries the unchanged ones forward from their baseline. It happens on every upload whose build carries a dependency graph, so for Storybook, build with --stats-json.
npm run build-storybook -- --stats-json # writes the dependency graph
npx -y uiverify@1.8.0 upload --static-dir ./storybook-staticWhat does a build need to skip unchanged stories?
A module dependency graph: a preview-stats.json file at the root of the directory you upload. It records which source file imports which, so UI Verify can trace a changed file to the stories and tests that use it. How an upload gets one depends on what you upload:
| Upload | Where the graph comes from |
|---|---|
| Storybook | Build with --stats-json (npm run build-storybook -- --stats-json). That is the Storybook 8 and later flag (Storybook's CLI reference, checked 2026-10-17); on Storybook 7 it is --webpack-stats-json. |
| Vitest archive | Automatic. @uiverify/vitest 1.6.0 and later writes it, as long as uiverifyPlugin() is in your root Vitest config. |
| Playwright archive | You add it yourself. See Module graph format. |
Screenshots (--screenshots) | None. Every image is diffed; to do less work, upload only the screens you changed. |
Skipping works the same on every build: pull requests, pushes to your default branch, and uploads from your own machine.
Why does every story render on every build?
Usually because the upload has no dependency graph. When that happens every story or test renders. For Storybook and Vitest the CLI says so on every upload (unless you passed --render-all), and so does the build page, in a warning above the build's counts that names the fix. The CLI's lines:
Every story renders: this Storybook build has no dependency graph (preview-stats.json). Build it with `storybook build --stats-json` and UI Verify renders only what your change affects.
Every test renders: this archive has no module graph (preview-stats.json). Upgrade @uiverify/vitest to 1.6.0 or later and keep uiverifyPlugin() in your root Vitest config, and UI Verify renders only what your change affects.If the log shows none of those lines and everything still rendered, the cause is one of the full-render triggers below.
How does UI Verify decide what changed?
UI Verify walks your change's files up the dependency graph and selects every story or test that imports a changed file, directly or transitively. Everything else is carried forward: its existing baseline is reused as this build's result, with no render and no diff. A changed file the graph proves feeds no story, such as a backend file in the same pull request, is ignored.
The list of changed files comes from the CLI. UI Verify tells it which commits your baselines were last confirmed at, and the CLI runs git diff --name-only against each one in your checkout and sends the file paths with the upload. Only paths leave your machine, never file contents, and UI Verify does not read your repository to work them out. In a shallow clone the CLI fetches the commits it needs by SHA, so fetch-depth: 0 is not required for skipping, though it still helps baseline resolution.
What happens to uncommitted changes?
Files where your checkout differs from the commit count as changed files too, so the stories they affect render. In CI that means tracked files a step edited; on your own machine it also includes new files git does not ignore. The CLI lists them in its log. A build made with uncommitted changes renders and can be accepted like any other, but later builds do not use it as a reference point for skipping. A story whose baseline it changed renders once more on the next build from a clean checkout; the rest keep skipping from the last clean build.
How do I turn off skipping and render everything?
Pass --render-all to the upload. Every story or test renders and nothing is carried forward. It also re-renders a commit whose identical upload already passed, which UI Verify otherwise does not build again.
npx -y uiverify@1.8.0 upload --static-dir ./storybook-static --render-allWhat triggers a full render?
- No
preview-stats.jsonin the upload: a Storybook built without--stats-json, an archive from a@uiverify/vitestolder than 1.6.0, or a Playwright archive you did not add a graph to. --render-allon the upload.- A change to the Storybook
previewor anything it imports, such as a global stylesheet or decorator. - A lockfile,
package.json, or build-config change (Vite, webpack, Vitest, Playwright, Tailwind, PostCSS,tsconfig), which can shift anything. - No verified baseline to carry forward yet, as on a project's first build. A new story renders on its own; the rest still skip.
- A
uiverifyCLI older than 1.8.0. - A baseline commit the CLI cannot read, for example a shallow clone with no credentials to fetch it (
persist-credentials: false). Only the stories on that baseline render; the rest still carry forward.
In each of those cases re-rendering is the correct, safe answer, so it does.
Does skipping work with Vitest tests?
Yes, with nothing to configure. @uiverify/vitest 1.6.0 and later writes the Vite module graph into the archive, so a changed file is traced to the browser-mode tests that import it, directly or transitively, exactly as for Storybook stories. Keep uiverifyPlugin() in your root Vitest config. An archive from an older @uiverify/vitest renders every test; upgrade to skip.
Before it writes the graph, the plugin checks each test's imports against git. A test whose imports pass through files git does not track (bundler output such as the chunks Angular's unit-test builder produces, a workspace package's built dist/, gitignored generated code), or that imports no tracked source file at all, is left out of the graph and always renders. The test run prints [uiverify] The tests in N file(s) always render with the reason. That keeps projects like Angular safe on the default: their tests render every time instead of being skipped on a graph that cannot see your source.
Why did my Vitest run write no module graph?
Sometimes @uiverify/vitest writes no graph for a whole run, so every test renders. When it does, your test output (the CI log of the Vitest step) has one line starting [uiverify] no module graph written, so every test renders: or [uiverify] could not write the module graph. The end of that line names the cause:
git could not list this repository's files: the tests ran outside the git checkout, or git is not installed in that CI step. Run them in the checked-out repository with git available, for example afteractions/checkout.Vitest didn't report both a browser project's tests and its imports: a browser-mode project ran tests, but Vitest gave the plugin no module graph or no test list for it, so its tests can't be checked. Check that this project's tests run under the Vitest config that hasuiverifyPlugin().<file> always renders, but another module in the graph ends with the same path: a test that is left out of the graph has the same path as a file in another project, so it could be matched to the wrong source. Fix why that test always renders (theThe tests in N file(s) always renderline), or give it a path no other project shares.could not write the module graph: writing the file failed. The rest of the line is the error, usually a permission or disk problem in the archive directory.
How do I skip unchanged Playwright tests?
Add a module graph to the archive yourself. A Playwright test visits a page by URL, so nothing in the archive relates your source files to a screenshot, and by default every test renders. If you can say which files each spec depends on, write them as a preview-stats.json in the archive directory before you upload. @uiverify/playwright 1.4.0 and later records each test's spec file, which is how UI Verify finds the test in your graph.
Module graph format
The file is one JSON object with a modules list. Each module has a name and a reasons list naming the modules that import it, so the edges point from a file to the files that use it. A spec reaches its page through a URL rather than an import, so list the spec as an importer of the files the page is built from. In this example a change to Button.tsx or CheckoutPage.tsx renders the checkout test, and a change to anything else leaves it carried forward:
{
"modules": [
{ "name": "./src/components/Button.tsx", "reasons": [{ "moduleName": "./src/pages/CheckoutPage.tsx" }] },
{ "name": "./src/pages/CheckoutPage.tsx", "reasons": [{ "moduleName": "./e2e/checkout.spec.ts" }] },
{ "name": "./e2e/checkout.spec.ts", "reasons": [] }
]
}- Names are paths relative to the repository root; a leading
./is fine. - Every spec file must appear as a module itself, under its full repository path. UI Verify finds a test by matching the end of a module name against the spec's path relative to your Playwright config, so a test no module name ends with always renders, and two specs with the same relative path in different packages are matched together.
- A changed file the graph proves feeds no test is ignored, so list every file a page depends on, including its data. A changed image, font or stylesheet the graph does not list renders everything instead, since a page can load one directly.
- A changed lockfile,
package.jsonor Playwright config renders everything, whatever the graph says. - Write the file to
<archive dir>/preview-stats.jsonafter the tests run and beforeuiverify upload.
Is skipping safe, or could it skip a story that really changed?
It is safe as far as the dependency graph can see: a carried-forward story is one whose inputs your change did not touch, so its pixels should not have moved. The guarantee is only as good as that graph, though, so treat any surprise as a signal: if you ever see a carried-forward story that clearly should have rendered, or a diff you cannot attribute to your own change, investigate the baseline rather than accepting it. When in doubt, upload once with --render-all.
How are skipped stories billed?
A carried-forward story still counts toward your snapshot allowance, at a fifth of a rendered one (0.2), so a typical pull request bills a fraction of your full UI. See How snapshots are counted.
Write economical visual tests
Full visual coverage in the fewest billable snapshots.
How I cut my Chromatic bill 10x (works on any visual testing tool)
Visual regression tools bill per snapshot, so story count is the bill. Two changes to how I write stories cut mine roughly 10x, same coverage, on Chromatic or any per-snapshot tool. Here they are.
Visual testing for agents
UI Verify is visual regression testing for Playwright, Storybook and Vitest. Your coding agent sees what its UI change did and fixes it before users see it. See how it works.
Get started