greenroom

A code generator that turns Flutter's own @Preview annotations into widget tests proving every preview mounts — plus screenshots and a machine-readable verdicts file as test artifacts. A candidate version of your app waits in the green room; it goes on stage only when it's green.

$ dart run greenroom          # (re)generate test/greenroom.g_test.dart
$ flutter test                # the gate: green = every preview mounts

No config file, no runtime dependency, no annotations of its own — the @Preview annotation (package:flutter/widget_previews.dart) is the configuration, and the generated test file is fully self-contained.

What gets generated

One testWidgets per @Preview annotation (stacked annotations = one test each), honoring the annotation's own fields:

  • name: → test description
  • size: Size(w, h) → applied EXACTLY as the official previewer does (verified against flutter_tools' widget_preview_scaffold): a SizedBox constraint plus a MediaQueryData.size override, infinity = unconstrained
  • textScaleFactor: → MediaQuery text scaling
  • brightness: → MediaQuery platform brightness
  • wrapper: → applied around the entry, resolvable from ANY library
  • theme: → applied (newer SDKs' PreviewThemeData.apply, with a runtime bridge to older SDKs' material/cupertino theme fields)
  • localizations: → applied as a Localizations override
  • Preview/MultiPreview SUBCLASSES → expanded at runtime (const Sub().transform()), one verified mount per produced preview

Plus: bare mounts for public widgets with a no-required-param default constructor and no entry of their own; failing obligation tests for widget classes that can be neither entered nor bare-mounted ("add a @Preview entry, e.g. …"); a PNG screenshot per passing test under .dart_tool/greenroom/shots/; and a tearDownAll that writes .dart_tool/greenroom/verdicts.json (per-entry ok/error/attribution/hint, hash-stamped).

Everything mounts under your project shell when lib/preview/shell.dart exports Widget shell(Widget child) — the app's real ambient wrappers plus whatever fake services your previews need — else a bare MaterialApp.

What a failure looks like

Flutter's COMPLETE native error report — its stack, widget creation locations, its own remediation paragraphs. Nothing added, nothing summarized away:

The following UnimplementedError was thrown building ProfileHeader:
UnimplementedError: LiveUserService not wired — missing fake?
#0  LiveUserService.displayName (package:example_app/services/user_service.dart:13:7)
#1  ProfileHeader.build (package:example_app/ui/profile_header.dart:20:39)
...

Overflow IS a mount failure (A RenderFlex overflowed… fails the test, with Flutter's own advice intact). verdicts.json additionally carries the short structured form per entry: first error line, cascade count, and the attributed file:line:col (first project stack frame, or the widget's creation location for layout errors with framework-only stacks).

CI

$ dart run greenroom --check && flutter test

--check regenerates in memory and fails (exit 1) if the committed file is stale — the same drift-guard pattern as any committed codegen. Commit the generated file.

Provenance & scope

Discovery is fully RESOLVED (the target must be pub get-able): an annotation counts iff its class is declared in Flutter's widget_previews library — import spelling is irrelevant and name-squatting is impossible. The same annotations remain fully consumable by Flutter's interactive flutter widget-preview start; greenroom is its missing verification half.

  • EVERY annotation is re-instantiated by the generated test with resolved imports (identifiers in annotation arguments may live in any library), and its fields are applied at runtime — the official previewer's own architecture.
  • Preview/MultiPreview SUBCLASSES are expanded AT RUNTIME (const Sub().transform()), the official previewer's own architecture — closures and computed previews included.
  • Annotation fields resolve through constants (const kPhoneSize works, not just literals).
  • Widget detection is transitive (class Foo extends MyBaseCard is seen); InheritedWidgets are treated as supplies, never preview subjects.

Supported versions (all verified end-to-end)

Dart SDK ^3.9.0 (Flutter 3.35, where @Preview shipped) — tested on 3.10.4 (Flutter 3.38.5) and 3.13.0 (Flutter 3.47.0)
analyzer >=12.1.0 <15.0.0 — tested at 12.1.0, 13.3.0, 14.0.0, 14.1.0

Known limits

  • Annotation arguments referencing PRIVATE declarations cannot be re-emitted — the entry mounts with no fields applied (noted, not silent).
  • Mount tests prove the FIRST FRAME with fixture data; interaction-time behavior needs ordinary widget tests, which ride the same flutter test.
  • Screenshots render with real fonts at the annotation's size or a default 800×600 surface.

Requires the Flutter SDK on PATH (developed against 3.47.0).

Libraries

discovery
RESOLVED discovery of preview entries in a target project's lib/.
test_gen
The product: emit test/greenroom_test.g.dart — a SELF-CONTAINED test file proving every preview mounts. No runtime dependency on greenroom; flutter test is the runner and the gate.