@cevek/screentest
v0.3.5
Published
Local desktop tool for visual screenshot-test review — CLI, runner helpers, and review UI shipped as a single package.
Downloads
151
Readme
@cevek/screentest
Local desktop tool for visual screenshot-test review plus the runner helpers and Docker-based browser harness needed to capture the screenshots. Everything ships in one npm package — no copy-paste.
Install
npm i -D @cevek/screentest playwright vitest && npx playwright install firefox
# pnpm add -D @cevek/screentest playwright vitest && pnpm exec playwright install firefox
# yarn add -D @cevek/screentest playwright vitest && yarn playwright install firefoxplaywright and vitest are peer-deps — keep them in your devDependencies.
Quick start
# 1. Scaffold vitest.screentest.config.ts + tests/example.test.ts
npx screentest init
# 2. Start the shared Firefox daemon (one-time per machine — image builds
# on first run, ~2 min; cached forever after).
npx screentest serve
# 3. Run.
APP_URL=http://localhost:5050 npx vitest run --config vitest.screentest.config.tsAdd
"screentest": "vitest run --config vitest.screentest.config.ts"to your package.json scripts so you can justnpm run screentest.
The daemon lives outside your project — one container serves every
project on your machine. It listens on ws://localhost:5180, holds
~300MB RAM at idle, and stays up across reboots-of-your-app until you
screentest stop it.
Your tests run natively on the host (your normal vitest + TS + path aliases + watch mode); only the Firefox rendering happens in the container, over a WebSocket. Editing tests never needs an image rebuild.
screentest init creates:
vitest.screentest.config.ts— separate vitest config wired with the package's globalSetup. Kept under its own name so your normalvitest.config.ts(unit tests) is untouched.tests/example.test.ts— minimal Playwright test using the runner helpers
Existing files are not overwritten — re-run init safely.
Pass --config vitest.screentest.config.ts to keep the screenshot run
separate from your normal vitest run. The screenshot config wires up
our globalSetup; plain vitest run skips it entirely — no Firefox launch.
Commands
screentest serve # start the shared Firefox browser-server daemon
screentest stop # stop it
screentest status # is it running?npx vitest run --config vitest.screentest.config.tsThat's the test run — vitest on the host, our globalSetup connects to
the daemon. On failure (or new snapshots) the UI auto-launches in your
browser. Set CI=1 to skip the auto-UI and just propagate the exit
code; SCREENTEST_NO_UI=1 does the same locally.
screentest reviewSkip vitest, regenerate doc.json from cached actuals, open the UI.
screentest initScaffold vitest config + example test (see above).
screentest <doc-json-path> [--port N] [--no-open]
[--worker-url URL] [--token TOKEN]Open the review UI on a pre-built doc.json. Used internally by the
globalSetup hook and by screentest review. With no --port, picks a
random free port in [40000, 50000) so parallel test runs don't clash.
The UI also beacons a shutdown back to the server on tab close, so you
don't need Ctrl+C in the terminal.
Writing tests
import { afterAll, beforeAll, describe, it } from 'vitest';
import type { Page } from 'playwright';
import { compareSnapshot, freezeDate, newPage } from '@cevek/screentest/runner';
const APP_URL = (process.env.APP_URL || 'http://localhost:5050').replace(/\/+$/, '');
let page: Page;
let cleanup: () => Promise<void>;
beforeAll(async () => {
const r = await newPage();
await freezeDate(r.ctx, '2024-01-15T12:00:00Z'); // optional
page = r.page;
cleanup = r.cleanup;
});
afterAll(() => cleanup?.());
describe('team', () => {
it('login flow', async () => {
await page.goto(`${APP_URL}/team`);
await page.locator('input[name="username"]').waitFor({ state: 'visible' });
await compareSnapshot(page, 'login'); // → team / login flow / login
// Snapshot just one element instead of the whole page:
await compareSnapshot(page.locator('header'), 'header');
});
});Snapshot paths are auto-derived from describe(...) + it(...) plus the
compareSnapshot leaf name. Hashes go into tests/snapshot.json by
default; override with SCREENTEST_SNAPSHOT_FILE=path/to/snapshot.json.
Actuals + the generated doc.json go to
node_modules/.cache/screentest/ (gitignored by convention).
Upgrading from < 0.3: an existing
<projectRoot>/snapshot.jsonkeeps working — the runner picks it up iftests/snapshot.jsonisn't there yet.
Docker Desktop host networking
The daemon container uses --network=host so its Firefox can reach
your app at localhost:*. On macOS / Windows Docker Desktop enable it
once: Settings → Resources → Network → Enable host networking. On
Linux Docker (CI) it works out of the box.
Cloudflare Worker (blob store)
Accepted screenshots are stored in your Cloudflare Worker (R2-backed).
See the worker template
for a 100-line implementation + wrangler deploy instructions.
Once deployed:
export CLOUDFLARE_WORKER_URL="https://screentests.your-account.workers.dev"
export CLOUDFLARE_TOKEN="<UPLOAD_TOKEN secret you set with wrangler>"Or pass --worker-url / --token to screentest directly.
CI
- run: npx screentest serve # build image + start daemon
- run: CI=1 npx vitest run --config vitest.screentest.config.tsIn CI=1 the globalSetup exits with vitest's code and never tries to
open the UI. Full recipe (GitHub Actions example, docker layer caching
to skip the ~2 min cold-start build, downloading actuals for offline
review, pnpm/yarn variants) is in the CI guide on
GitHub.
Input format reference
If you need to construct your own doc.json for the UI (instead of going
through vitest run / screentest review), see
the format documentation in the source repo.
Security
- The UI server listens only on
127.0.0.1. Never on0.0.0.0. actualfiles are served strictly from a whitelist computed fromdoc.jsonat startup — no path traversal possible.
