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 descriptionsize: 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 = unconstrainedtextScaleFactor:→ MediaQuery text scalingbrightness:→ MediaQuery platform brightnesswrapper:→ applied around the entry, resolvable from ANY librarytheme:→ applied (newer SDKs'PreviewThemeData.apply, with a runtime bridge to older SDKs' material/cupertino theme fields)localizations:→ applied as aLocalizationsoverridePreview/MultiPreviewSUBCLASSES → 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/MultiPreviewSUBCLASSES are expanded AT RUNTIME (const Sub().transform()), the official previewer's own architecture — closures and computed previews included.- Annotation fields resolve through constants (
const kPhoneSizeworks, not just literals). - Widget detection is transitive (
class Foo extends MyBaseCardis 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).