# 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: ```yaml 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 with `visibility`, 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/...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, # and overflow / overflow_ink per shot ``` ## The one thing squint judges by itself Everything else here measures and leaves the judging to a person. There is one exception, because it needs no taste at all: a page that renders wider than the viewport it was taken for scrolls sideways, and on a phone that is always a bug. A full-page screenshot is captured at the page's own content size, so this falls out of the shot for free. Every shot that does not fit is reported: ``` admin-accounts.phone.light 65px past the 390px viewport, with content out there admin-settings.phone.light 40px past the 390px viewport, nothing drawn out there ``` The second half matters as much as the first. The accounts card had pushed an account's status badge off the screen, which is what a person reported. The settings page was forty pixels wider because of a tooltip bubble that is invisible at rest and still counted -- a real bug, but nothing is out of reach, and a report that presented the two identically would be ignored within a week. `result.json` carries both as `overflow` and `overflow_ink`. ## 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](https://git.mills.io/prologic/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.