greenroom 0.3.1
greenroom: ^0.3.1 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 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).
Agent skill #
skills/flutter-widget-previews/SKILL.md ships with the package: how to
write @Preview annotations, verify them, wire CI, supply ambient
dependencies, and read failures. Point your agent at it, or copy the directory into wherever your agent
loads skills from (for Claude Code, .claude/skills/).
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).
- Previews returning
WidgetBuilderare mounted through aBuilder. - 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).