# squint Visual regression testing and UI review for web applications, driven from the command line and from Claude Code. It answers two questions that ordinary tests cannot: - **Did this change move the page?** — screenshot the app before and after a change and show what moved. - **Does this page follow its own rules?** — screenshot the app and check it against the project's own design guide, with no "before" at all. The second question is the one that matters most in practice. The bugs that get reported by a human looking at a screenshot — three badge sizes on one screen, a button whose label wraps one letter per line, a form whose two password fields sit four pixels apart — were never "a regression". They shipped wrong and stayed wrong. There is no baseline that catches them. ## Decisions Taken 2026-09-15, with James Mills, before any code. ### The browser is borrowed, never vendored squint drives the Chrome already installed on the machine, over the Chrome DevTools Protocol, using `chromedp`. It does not download a browser and it does not ship one. This is the decision the whole tool hangs off. An earlier attempt at visual regression testing inside the Parley project pulled in the `chromedp/headless-shell` container image — about 300MB — and was abandoned for that reason alone. The container was never the requirement: it was an attempt to pin one exact renderer so that stored baseline images would match. squint does not store baseline images (see below), so it does not need to pin a renderer, so it does not need a container. Chrome's command line alone (`--headless --screenshot`, `--dump-dom`) was measured and rejected: it can render a page and dump the DOM, but it cannot inject script into an application it did not author, cannot click, and cannot set a cookie. That rules out every scenario with an action in it — opening a drawer, switching a theme, confirming a dialog. `rod` downloads its own Chromium by default and `playwright-go` needs a Node driver binary; both reintroduce what was rejected. ### Both sides are rendered in the same run There are **no committed golden images**. When comparing, squint renders the baseline and the candidate in the same run, on the same machine, with the same browser binary, seconds apart. This is what makes pixel comparison trustworthy again. Stored goldens fail because the baseline was rendered on one machine months ago and the candidate on another today: font hinting, subpixel rounding and the processor architecture all differ, and every one of those differences reads as "something changed". Rendering both sides in one run cancels all of them exactly — there is nothing left in the difference except the change itself. It also removes, at a stroke: Git LFS, binary files in review, an `-update` ritual, cross-architecture failures, and the question of which machine is allowed to regenerate a baseline. The baseline comes from git on demand — a worktree at `HEAD`, or at any named ref — built and run exactly like the candidate. ### Claude judges; the tool measures The Go tool captures, compares and reports. It does **not** call a model and holds no API key, so it is useful on its own and in continuous integration. Claude is wired in through a skill and a slash command. It reads the tool's JSON result, opens only the images that actually moved, and decides what a difference means: an intended change, a real regression, or rendering noise. In review mode it reads the project's design guide and critiques the screenshots against it. The division matters: a pixel count cannot tell you whether a change is an improvement, and a language model should not be asked to measure pixels. ### Scenarios are YAML, with an escape hatch Pages, viewports, themes and actions are declared in a YAML file in the project under test. A `js:` step runs raw JavaScript in the page for anything the vocabulary does not cover. The step vocabulary of a browser automation tool grows without bound — click, type, scroll, hover, upload, switch frame. Capping it with an escape hatch is cheaper than inventing a language, and YAML keeps squint usable from projects that are not written in Go. ### One consumer until it earns more squint is its own repository from the start, so that the boundary is real and there is no extraction debt later — it reads a config file and shells out, and knows nothing about any particular application. But the only configuration that has to work is Parley's until the design is proven. Generality gets validated against an application with a real history of real bugs, rather than guessed at for consumers that do not exist yet. ### Version one ships both modes Compare and review land together rather than review first. Recorded honestly: review-first was recommended, on the grounds that every bug reported against Parley's UI this week had no "before" to compare against, and that the markup half of the compare question is already answered by an HTML snapshot suite in that project. The decision was to build both, and that is what is being built. ## Shape ``` squint capture --label before # render the matrix once squint check # baseline = HEAD, candidate = working tree squint check --base v0.4.0 # baseline = any ref squint review # no baseline; screenshots + rubric for a reviewer ``` Output lands in `.squint/`: ``` .squint/ before/...png after/... diff/... # overlay: the candidate faded, changed pixels marked report.html # for a person: side by side, worst first result.json # for Claude: per-shot numbers, verdicts, paths ``` ## Non-goals - **Taste.** squint can tell you a screen has three badge sizes on it. It cannot tell you the design is good. - **A hard gate on judgment.** Model review is advisory and not reproducible run to run. Deterministic gating belongs to the project's own tests — in Parley's case an HTML snapshot suite that compares rendered markup and runs in ordinary `go test`. - **Provisioning browsers.** No Chrome, no squint.