README.md

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 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/<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.