greenroom 0.2.0
greenroom: ^0.2.0 copied to clipboard
Turns Flutter's own @Preview annotations into widget tests proving every preview mounts — with screenshots and a machine-readable verdicts file.
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 libraryPreview/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 summarized away — plus at most one added line, only where it encodes convention knowledge Flutter cannot have:
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)
...
greenroom: Register a fixture-returning fake in lib/preview/world.dart.
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, the
attributed file:line:col (first project stack frame, or the widget's
creation location for layout errors with framework-only stacks), and the
hint.
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.
wrapper:may live in ANY library — the tear-off's declaring library is imported by the generated test.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 #
theme:/localizations:on direct@Previeware not applied (noted, not silent); subclass annotations get them for free via the runtime path.- Subclass annotations whose ARGUMENTS reference project code mount without fields (only dart:/flutter identifiers are re-emitted); argument-free subclasses — the normal shape — are fully supported.
- 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).