squint
Visual regression testing and UI review for web applications.
squint review photograph every screen, for a design review
squint check photograph a revision and the working tree, and diff
squint drives the Chrome already installed on the machine. It does not download a browser, it does not ship one, and it commits no baseline images.
Why there are no golden images
Stored baselines are what makes visual regression testing miserable. The baseline was rendered on one machine months ago and the candidate on another today, so font hinting, subpixel rounding and the processor architecture all differ — and every one of those reads as "something changed".
squint renders both sides in the same run, on the same machine, with the same browser binary, seconds apart. Those differences cancel exactly. What is left in the difference is the change itself.
Which also means no Git LFS, no binary files in review, no -update
ritual, and no argument about which machine may regenerate a baseline. The
baseline comes from git on demand — a worktree at HEAD, or any ref.
Configuration
A squint.yaml in the project under test:
app:
build: go build -o server ./cmd/server
run: ./server -http 127.0.0.1:{{.Port}} -data {{.TmpDir}}
ready: "{{.BaseURL}}/healthz"
setup: ./hack/seed.sh {{.BaseURL}} # the most important line in the file
auth:
path: /login
form: {user: "{{.User}}", password: password}
viewports:
desktop: {width: 1280, height: 900}
phone: {width: 390, height: 844}
themes:
light: {}
dark: {cookies: {theme: dark}}
mask:
- ".token-prefix" # hidden before the shutter, keeps its space
scenarios:
- {name: home, path: /}
- {name: settings, path: /settings, login: root}
- name: drawer
path: /
login: root
viewports: [phone]
steps:
- js: "document.getElementById('site-drawer').checked = true"
- wait: 300ms
Scenarios expand across every viewport and theme automatically. Narrowing is opt-in, which is how the phone and dark renderings stop being the ones nobody checks.
Steps are click, type/into, wait_for, wait and js. The last is
the escape hatch: a browser automation vocabulary grows without bound, and
capping it is cheaper than inventing a language.
Determinism is the caller's job
A screenshot of undetermined data differs for reasons that are not the code's. squint gives you two tools and the rest is the fixture's problem:
setup:runs after the application is ready and before the first shot.mask:hides elements whose content is genuinely new every run — a token minted from real randomness, an id, a clock. Hidden withvisibility, so the element keeps its space and the layout is unchanged.
What neither can fix is an application that renders something variable: live error text from a network stack will wrap to a different number of lines and change a page's height. Either pin it in the fixture or do not photograph that screen.
A fixture that reaches the real network is not a fixture.
Output
.squint/
before/<scenario>.<viewport>.<theme>.png
after/...
diff/... # the candidate faded, changed pixels marked
report.html # for a person: side by side, worst first
result.json # for a reviewer: paths, numbers, verdicts
Requirements
Go 1.24+ to build, and a Chrome or Chromium on the machine. Set
SQUINT_CHROME to point at a specific one.
Status
Early. The compare and review commands work and are dogfooded against
Parley. Not yet built: the Claude
Code skill and slash command that read result.json, open only the images
that moved, and say what a difference means.