greenroom 0.3.1 copy "greenroom: ^0.3.1" to clipboard
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 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).

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/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).
  • Previews returning WidgetBuilder are mounted through a Builder.
  • 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).

1
likes
150
points
169
downloads

Documentation

API reference

Publisher

verified publisherwenkaifan.com

Weekly Downloads

Turns Flutter's own @Preview annotations into widget tests proving every preview mounts — with screenshots and a machine-readable verdicts file.

License

MIT (license)

Dependencies

analyzer, crypto, dart_style, path

More

Packages that depend on greenroom