Skip to main content

Rewhaven Testing Strategy (2026-06-19)

How we test the app as we build it. Derived from the gadfly_flutter_template (../../gadfly/gadfly_flutter_template/template) — the canonical intended usage of our ported flow_test + bloc_devtools_extension — plus Maestro for device e2e, all driven TDD (red → green → refactor).

The layered loop (outer → inner)

LayerToolWhat it provesTDD role
E2EMaestro (.maestro/*.yaml)the real app on a device/emulator, black-box user journeyswrite the e2e flow FIRST (red), build feature to green
Flow / widget + galleryflow_test (gadfly-style)the widget tree driven through appBuilder with mocked repositories + effect providers; exact event transcript; light+dark goldens; the served Test Gallery doubles as living specflow test written alongside the feature; success + each failure branch
Unitflutter_test + MockClient (client_sdk_testing)blocs, repositories, SDK services in isolationred → green per rule
Debugbloc_devtools_extensionlive bloc event/state/diff stream in DevTools (debug builds)not a gate — the inspection tool while building

Build a feature outside-in: failing Maestro e2e → failing flow test → unit tests → implementation → all green → refactor.

flow_test — the intended (gadfly) contract

  • Single front door: every flow test calls the app's real appBuilder(...) with mocked outside singletons. So the app must keep the inside/outside boundary: mockable singletons (Repository, EffectProvider, ClientProvider) created in appRunner, passed only through appBuilder.
  • Mock at the outside layer: repositories AND effect providers are mocktail Mocks; the analytics effect is a Fake. A MocksContainer (the M generic) holds repositories (*_All) + effectProviders (*_All) + effects + a mockEffectProviderGetEffectMethods() that stubs when(provider.getEffect)…. testAppBuilder stubs effects then calls the real appBuilder.
  • Two trips (light + dark themes) per flowTest via createdMockedApps.
  • Steps are tester.screenshot(...) with fixed order arrangeBeforeActions → actions → arrangeAfterActions → golden → expectations → expectedEvents. Mocks are arranged just-in-time in arrangeBeforeActions.
  • expectedEvents is the spec: a strict, ordered, exhaustive transcript of bloc-event runtime types + formatted log lines ([logger] LEVEL: msg, analytics rewritten to [ANALYTIC] …). A TestBlocObserver captures BOTH bloc events and root-Logger lines into one events list; onListItemRegexes colors them.
  • Warps fast-forward to a screen before the flow under test; tester.setUp(warp:) clears events/records so the warp is invisible to the gallery. One warp file per destination.
  • Test Gallery: run with --update-goldens --dart-define createScreenshots=true to emit app/test/gallery/*.md (EPIC/STORY/AC breadcrumb + per-step User Actions / Expectations / Events tables + light+dark shots); served via mkdocs on a dedicated port. The gallery is the feature's visual documentation + DoD.
  • DoD per feature: success flowTest + a failure group (one flowTest per failure scenario), exact event transcripts, light+dark goldens, all visible in the served gallery.

bloc_devtools_extension — the serialization contract (load-bearing)

The extension jsonDecodes the bloc_state payload and expects a Map. So:

  • Every Bloc/Cubit state is @JsonSerializable() + Equatable + a Status enum + explicit toJson()/fromJson (+ part 'state.g.dart', codegen run). A toString payload is invalid JSON-to-Map → the extension silently drops it (no diff/tree view). This is the #1 fidelity rule.
  • The observer (Blocs_Observer) posts ext.bde_bloc_devtools.bloc_event with {bloc_name, bloc_state: jsonEncode(state), event_name, logs: jsonEncode(records)} on create/transition/change(cubit)/close, buffering logging records between events; ext.bde_bloc_devtools.reset on construct. Registered globally as Bloc.observer in appRunner (no-op in release via VM service).
  • App has the extension as a path dev_dependency + enables it in app/devtools_options.yaml.

Glue the app must provide (gadfly base classes)

  • SharedMixin_Logginglog getter → Logger(runtimeType.snakeCase); mixed into blocs/repos/effects/widgets. Produces the stable [logger_name] strings flow tests assert on + the devtools log records.
  • Bloc_Base, Repository_Base, EffectProvider_Base<T>, ClientProvider_Base — thin bases with the logging mixin + init()/getEffect() contracts.
  • *_All containers (getList()/createProviders()/initialize()) registering the CONCRETE types into appBuilder's providers.

Maestro — device e2e, TDD

  • Flows live in app/.maestro/*.yaml (YAML steps: launchApp, tapOn, inputText, assertVisible, …). Run against the running app on an emulator/device (maestro test .maestro/flow.yaml).
  • TDD: write the e2e flow first (it fails — feature absent), build down through flow_test + unit + implementation until the Maestro flow passes green.
  • Constraint: Maestro needs a device/emulator; it can't run in a headless CI/dev box without one. Flows are authored + committed as executable specs; execution happens on a dev machine with an emulator (document the run command per flow).

Rollout phases (this initiative)

  1. Devx foundation + serializationSharedMixin_Logging, *_Base, *_All wired through runner/appBuilder; @JsonSerializable on all bloc states + codegen; correct Blocs_Observer payload. (Makes devtools actually work + sets the DI seam.)
  2. flow_test harness, gadfly-style — full MocksContainer (repos + effect providers + effects + getEffect stub), createFlowConfig (light/dark trips, dual-capture TestBlocObserver, regexes), warps, the canonical sign-in→home flow with expectedEvents, the mkdocs Test Gallery.
  3. Maestro + TDD.maestro/ scaffolding, first e2e flow as a red TDD spec, the run docs; thereafter every feature starts with a Maestro flow.