Set up visual regression testing in Bun
Write visual tests on bun test with @uiverify/bun, upload the archive, and wire CI so every pull request gets a screenshot diff. Captures with your installed Chrome.
Set it up with your coding agent
Paste this into Claude Code, Cursor, or any coding agent and it wires up UI Verify from these docs.
Set up UI Verify visual regression testing for my Bun project. Follow https://uiverify.ai/docs/quickstart-bun to install the packages and add the config. https://uiverify.ai/llms-full.txt has the same docs as one plain-text reference if you want to read them without fetching each page. Then run one build and upload it right now, locally - no CI or GitHub App needed for this - so my first build lands in the dashboard and we can confirm the setup works before wiring anything else. Use the capture and upload commands from the quickstart. Open the build page it prints in my browser so I can see the captured stories. Ask me for my UIVERIFY_API_KEY when you need it for the upload. Install the UI Verify skills for this project - the making-ui-changes playbook, the economical-visual-tests, triage, and check-visual-changes playbooks, plus the deterministic-capture guide for this framework - so you can change UI safely, author cheap stable stories, preview an edit's visual impact before pushing, and review builds yourself: npx skills add uiverify/uiverify \ --skill making-ui-changes \ --skill economical-visual-tests \ --skill triage-visual-changes \ --skill check-visual-changes \ --skill bun-visual-testing Then add a short rule to my AGENTS.md and CLAUDE.md so you read the making-ui-changes skill before any UI change from now on: `Before changing any component, page, or styles, read the making-ui-changes skill and follow it - reuse before you create, add/update the story or capture in the same change, check the blast radius on shared components, and prove the change with a visual test.` After installing, restart this session (or reload the window) so the new skills load - you will not have them until I do. Once that first build is in and looks right, wire the GitHub Actions workflow from the quickstart so every push is checked (add UIVERIFY_API_KEY as a repository secret), and remind me to install the UI Verify GitHub App from my setup page so the check and PR comment post - that's the one step only I can do, and it isn't needed for the first upload. If this repo is public and takes pull requests from forks, use the two-workflow setup from https://uiverify.ai/docs/open-source-and-forks instead, so fork pull requests get the check too without exposing the key.
@uiverify/bun is UI Verify's capture SDK for Bun. Your visual tests run on bun test and drive the Chrome you have installed through Bun.WebView, and each test archives its final page: the serialized DOM plus the bytes of its images, fonts and stylesheets. UI Verify replays that archive in one pinned browser, screenshots it, and diffs it against the baseline. It is the same archive-replay engine the Playwright and Vitest integrations use, and there is no Playwright or Node test runner to install.
@uiverify/bun into a small project with this exact setup and its own CI workflow.BUN_CHROME_PATH to use another Chrome). The uiverify CLI that uploads the archive runs on Node 24 or later.1. Get a project API key
Sign up at uiverify.ai, create a Bun project, and store its API key in CI as UIVERIFY_API_KEY.
2. Install the capture SDK
bun add -d @uiverify/bun@0.1.03. Define a suite
A suite sets what every test starts from: the viewports each test runs at, where the page clock starts (now, default 2026-01-01T00:00:00Z), and the color scheme. An optional second argument runs at the start of every test, which is where you stub the requests your pages make with page.route().
// tests/visual/fixtures.ts
import { DEFAULT_VIEWPORT, defineSuite } from "@uiverify/bun";
export const visualSuite = defineSuite({ viewports: { "": DEFAULT_VIEWPORT } });4. Write a visual test
Each *.visual.ts file is a visual test file. A test drives the page, and a test that never calls takeSnapshot() is snapshotted once when it ends. A snapshot's id is <file> > <describe> > <test>, plus ::<name> for a named snapshot and ::<viewport key> for a non-default viewport.
// tests/visual/home.visual.ts
import { expect, takeSnapshot } from "@uiverify/bun";
import { visualSuite } from "./fixtures";
const { test } = visualSuite(import.meta.path);
test("menu", async (page) => {
await page.goto("/");
await page.getByRole("button", { name: "Menu" }).click();
await expect(page.getByRole("menu")).toBeVisible();
await takeSnapshot("open"); // optional; a test with no takeSnapshot() is archived at its end
});expect is bun:test's, with retrying assertions for a locator (toBeVisible, toBeAttached, toHaveCount, toBeDisabled, toHaveValue, toHaveAttribute, and .not). Await them.
5. Add the runner script
run() starts your app's web servers (with PORT and UI_VERIFY=1 set), runs every *.visual.ts in its own bun test process, and writes the archive's index to ./uiverify-archive. Its test processes ignore your app's bunfig.toml, so a test preload such as happy-dom never replaces the test process's fetch. Keep your fixtures free of modules that need a loader or define registered there.
// tests/visual/run.ts
import { run } from "@uiverify/bun/run";
process.exitCode = await run({
webServer: { command: ["bun", "server.ts"], port: 3000 }, // your app's start command; it gets PORT
});Test components without running your app
To capture components rather than pages of a running app, build a preview page with serveBundle() and point the suite at it. The preview is an ordinary HTML page named index.html whose script mounts the components you capture. Pass the Bun.build plugins and constants your components need (Bun.build inlines no env, so a process.env.PUBLIC_* a component reads goes in define). With only component tests, drop webServer from the runner script: run() then starts no app.
import { afterAll } from "bun:test";
import { expect, serveBundle } from "@uiverify/bun";
import { visualSuite } from "./fixtures";
const preview = await serveBundle({
entrypoint: `${import.meta.dir}/preview/index.html`,
define: { "process.env.PUBLIC_API_URL": '"https://api.example.com"' },
});
afterAll(() => preview.stop());
const { test } = visualSuite(import.meta.path, { baseUrl: preview.url });
test("primary button", async (page) => {
await page.goto("/");
await expect(page.getByRole("button", { name: "Save" })).toBeVisible();
});6. Run your tests and upload the archive
bun tests/visual/run.ts # writes ./uiverify-archive
UIVERIFY_API_KEY=your_key npx -y uiverify@1.8.0 upload --static-dir ./uiverify-archive7. Catch a change locally
You do not need CI or a pull request to see a diff. Change something visible in a page or component, such as a color or a label, then run the tests and upload again on the same commit. UI Verify diffs the new build against your first one and flags what moved. Open the build page it prints, where you accept or reject the change. Accepting promotes the new screenshot to the baseline for that branch.
8. Wire it into CI
GitHub's Ubuntu runners ship with Google Chrome, so the job sets up Bun, installs your dependencies, runs the visual tests and uploads. It also sets up Node for the uiverify CLI.
name: UI Verify
on:
push:
branches: [main]
pull_request:
jobs:
visual:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
with:
fetch-depth: 0 # full history so the baseline can be found
- uses: oven-sh/setup-bun@v2
- uses: actions/setup-node@v7
with:
node-version: 24
- run: bun install --frozen-lockfile
- run: bun tests/visual/run.ts
- run: |
npx -y uiverify@1.8.0 upload \
--static-dir uiverify-archive
env:
UIVERIFY_API_KEY: ${{ secrets.UIVERIFY_API_KEY }}push trigger on your default branch matters as much as pull_request: it runs the job once on your default branch so the first build becomes the baseline every PR diffs against. Change [main] to whatever your default branch is (master, develop), or a repo whose default is not main gets a trigger that never fires. Accept that first build once, or add --auto-accept-changes to the upload on your default branch to seed it automatically. Without it, every PR shows all-new until a merge happens.@uiverify/bun writes no dependency graph, which UI Verify needs to skip unchanged tests.UI Verify: <slug> a required status check, or add the auto-rerun workflow that clears the job the moment you accept. Both are in Require the check and auto-clear the CI job.9. Install the visual-testing skills
Give your coding agent the visual-testing playbooks so it can do the work in your repo: triage a build's changes, write economical visual tests, check an edit's visual impact before pushing, and keep captures deterministic. This installs just these skills, not the whole bundle. Restart the agent session afterward so they load.
npx skills add uiverify/uiverify \
--skill making-ui-changes \
--skill economical-visual-tests \
--skill triage-visual-changes \
--skill check-visual-changes \
--skill bun-visual-testing10. Review changes from your coding agent
Connect the UI Verify MCP and your agent can pull a build's changes into the conversation, look at each diff, read the AI judge's verdict, and accept the intended ones. See Triage visual changes from your coding agent.
claude mcp add --scope project --transport http uiverify \
https://uiverify.ai/mcpThen log in once: restart Claude Code, run /mcp, pick uiverify, and log in to UI Verify in your browser. After that the agent can reach every project in every team you are a member of. Project scope writes the server to your committed .mcp.json, so the whole team gets it, and the file holds no secret: each teammate logs in with their own account. Using Cursor, Codex, or another client? See UI Verify MCP server for coding agents for its setup.
11. Install the GitHub App
Install the UI Verify GitHub App from your project's setup page and point it at your repo, so it can post a check and a comment on each pull request. This is the one step your coding agent cannot do for you. It does not block getting started: your first upload and baselines work without it, so add it when you want the results to show up on GitHub.
What a Bun capture pins for you
Each capture starts from the same state, so a screenshot changes only when your UI does:
- Time and randomness. The page clock starts at the suite's
nowand runs untilpage.pauseClock(), andMath.randomis seeded. - Motion. Animations and transitions finish instantly, and
prefers-reduced-motionis set. - Locale. The timezone is UTC, the locale en-US, and the color scheme the suite's
colorScheme. - Loading. Before a snapshot it waits for the fonts in use, then for every image and CSS background, then for the page to stop moving.
- Isolation. Each test starts on a blank page with no routes or init scripts, no cookies, no history, and the storage of every origin the previous test touched cleared.
What it cannot pin is yours to fix at the source. A page whose elements keep moving on their own (a requestAnimationFrame loop, a Lottie) fails after 10 seconds, and a canvas that keeps redrawing is captured mid-frame: stop either while UI Verify captures with isUIVerify(). Live data belongs in fixtures, served with page.route(). A cross-site iframe runs in its own process, so its requests are not routed and it gets no clock or init scripts.
Deterministic Bun captures
Stop Bun visual-test diffs that flake without a real change.
Visual testing for agents
UI Verify is visual regression testing for Playwright, Storybook, Vitest and Bun. Your coding agent sees what its UI change did and fixes it before it ships. See how it works.
Get started