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:
- 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). - Load real fonts before tests run, in
test/flutter_test_config.dart'stestExecutable, usingFontLoader('YourFont')..addFont(...)— otherwise text renders as the Ahem block font (solid rectangles). - 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. - Kill nondeterminism before capturing:
FocusManager.instance.primaryFocus ?.unfocus()to drop the blinking cursor; pump exact durations (tester.pump(Duration(...))), neverpumpAndSettle, when a repeating ticker/animation would never settle; advance past auto-dismissing toasts; ensure timers are owned and disposed (a pendingTimerfails the test). - Assert with
expectLater(find.byType(MaterialApp), matchesGoldenFile('goldens/<name>.png')). - 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. - From then on, plain
flutter testverifies 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.