---
title: A screenshot that fails the build when your UI drifts from Figma — Flutter golden tests
date: 2026-08-24
tags: [flutter, golden-tests, ui-testing, figma, visual-regression, screenshot-testing]
type: success
status: n/a
reproducible: true
---

# A screenshot that fails the build when your UI drifts from Figma — Flutter golden tests

Plain version first, because this is a Flutter thing but the problem isn't:
you have a handful of screens that match the design *exactly* — the padding,
the font weight, the shade of grey, all approved against Figma. Then you develop
the app for another three months, and those screens quietly drift. Someone nudges
a global spacing token, a shared button gets a new default, and three screens you
thought were "done" are subtly wrong again — and nobody notices until a designer
does. I wanted the screens that are perfect *today* to stay perfect on their own,
without me eyeballing them every week.

The tool for this in Flutter is called a **golden test**, and the trick that
makes it cheap is that it runs **headless** — no phone, no simulator, no emulator.
Flutter can render a screen entirely offscreen: it builds the UI and draws the
pixels straight into memory, the same way it would on a device, but there's no
device — the output is just a PNG. You render a screen once, look at that PNG next
to the Figma frame, and if it's right you save it as the "golden" reference and
commit it to git. From then on, every `flutter test` re-renders the screen headless
and compares it pixel-for-pixel against the saved image. One pixel off → the test
goes red. That's the whole idea: a screenshot that fails the build when it changes.

Getting there had real friction, and it's the same friction anyone hits:

- **Text came out as black boxes.** In headless tests Flutter defaults to a
  placeholder font, so every letter renders as a solid rectangle. You have to
  load the real font files explicitly before the tests run.
- **The image was never the same twice** until I removed every moving part: a
  blinking text cursor, a toast that auto-dismisses, a countdown timer ticking
  once a second. Headless rendering is deterministic only if the UI is — so you
  freeze it (unfocus fields, advance time by exact amounts, make sure timers are
  disposed) before you take the shot.
- **No backend allowed.** To capture "wrong code" or "attempts exhausted" I
  didn't spin up a server — I swapped the app's dependencies for in-memory fakes
  and constructed each state directly. A screenshot test shouldn't need a network.
- **You can't diff against a raw Figma export.** Figma and Flutter's renderer
  draw text slightly differently, so an automatic pixel-diff against a Figma PNG
  never passes. The approval is a *one-time human look*; the automatic lock is
  against your own approved screenshot afterward.

The result: 12 screen states from my auth flow — phone entry (empty, filled,
invalid), the OTP screen in its various states, the biometric modals, the
attempt-limit screen — are each pinned to their Figma frame and locked. Now if
anyone's change shifts one of them by a pixel, `flutter test` fails on that exact
screen before it reaches review. The design stays perfect for free.

## Reproduce this
Prerequisites: a Flutter project (`flutter_test` ships with the SDK — nothing to
install); the screens you want to lock; the Figma frames to approve them against.

Your agent will need from you: which screens/states to cover, your Figma frame
width (e.g. 375 px), and your app's font files.

Steps:
1. Put golden tests under `test/golden/`. Set the test viewport to your Figma
   frame size at 1x — e.g. `tester.view.physicalSize = Size(375, 812);
   tester.view.devicePixelRatio = 1.0;` — so a golden lines up 1:1 with a 1x
   Figma export. Show only app UI (no OS status/nav bars — Figma mocks those).
2. Load real fonts before tests run, in `test/flutter_test_config.dart`'s
   `testExecutable`, using `FontLoader('YourFont')..addFont(...)` — otherwise
   text renders as the Ahem block font (solid rectangles).
3. Make every state constructable without a backend: replace your DI
   registrations (e.g. `get_it`) with in-memory fakes — fake repository, fake
   token store, no-op biometrics/SMS — and pump the screen with the state you
   want.
4. Kill nondeterminism before capturing: `FocusManager.instance.primaryFocus
   ?.unfocus()` to drop the blinking cursor; pump **exact durations**
   (`tester.pump(Duration(...))`), never `pumpAndSettle`, when a repeating
   ticker/animation would never settle; advance past auto-dismissing toasts;
   ensure timers are owned and disposed (a pending `Timer` fails the test).
5. Assert with `expectLater(find.byType(MaterialApp),
   matchesGoldenFile('goldens/<name>.png'))`.
6. Record the references once: `flutter test --update-goldens test/golden`. Open
   each generated PNG next to its Figma frame and approve by eye. Commit the
   goldens together with the code.
7. From then on, plain `flutter test` verifies them. After an *intended* UI
   change, re-run `--update-goldens`, re-approve, and commit the new golden.

Caveat to state up front: **record and verify on the same OS.** Text rasterizes
differently across platforms, so goldens recorded on macOS won't match on a Linux
CI runner — run the golden suite on a matching runner, or treat it as a local
pre-commit gate.

Verify: change a padding by one pixel on a covered screen and run
`flutter test test/golden` — it must go red on exactly that screen's golden.
Revert, and it's green again.
