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)
| Layer | Tool | What it proves | TDD role |
|---|---|---|---|
| E2E | Maestro (.maestro/*.yaml) | the real app on a device/emulator, black-box user journeys | write the e2e flow FIRST (red), build feature to green |
| Flow / widget + gallery | flow_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 spec | flow test written alongside the feature; success + each failure branch |
| Unit | flutter_test + MockClient (client_sdk_testing) | blocs, repositories, SDK services in isolation | red → green per rule |
| Debug | bloc_devtools_extension | live 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 inappRunner, passed only throughappBuilder. - Mock at the outside layer: repositories AND effect providers are mocktail
Mocks; the analytics effect is aFake. AMocksContainer(theMgeneric) holdsrepositories(*_All) +effectProviders(*_All) +effects+ amockEffectProviderGetEffectMethods()that stubswhen(provider.getEffect)….testAppBuilderstubs effects then calls the realappBuilder. - Two trips (light + dark themes) per
flowTestviacreatedMockedApps. - Steps are
tester.screenshot(...)with fixed orderarrangeBeforeActions → actions → arrangeAfterActions → golden → expectations → expectedEvents. Mocks are arranged just-in-time inarrangeBeforeActions. expectedEventsis the spec: a strict, ordered, exhaustive transcript of bloc-event runtime types + formatted log lines ([logger] LEVEL: msg, analytics rewritten to[ANALYTIC] …). ATestBlocObservercaptures BOTH bloc events and root-Loggerlines into oneeventslist;onListItemRegexescolors 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=trueto emitapp/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:
successflowTest + afailuregroup (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+ aStatusenum + explicittoJson()/fromJson(+part 'state.g.dart', codegen run). AtoStringpayload is invalid JSON-to-Map → the extension silently drops it (no diff/tree view). This is the #1 fidelity rule. - The observer (
Blocs_Observer) postsext.bde_bloc_devtools.bloc_eventwith{bloc_name, bloc_state: jsonEncode(state), event_name, logs: jsonEncode(records)}on create/transition/change(cubit)/close, bufferingloggingrecords between events;ext.bde_bloc_devtools.reseton construct. Registered globally asBloc.observerinappRunner(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_Logging—loggetter →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.*_Allcontainers (getList()/createProviders()/initialize()) registering the CONCRETE types intoappBuilder'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)
- Devx foundation + serialization —
SharedMixin_Logging,*_Base,*_Allwired through runner/appBuilder;@JsonSerializableon all bloc states + codegen; correctBlocs_Observerpayload. (Makes devtools actually work + sets the DI seam.) - flow_test harness, gadfly-style — full
MocksContainer(repos + effect providers + effects + getEffect stub),createFlowConfig(light/dark trips, dual-captureTestBlocObserver, regexes), warps, the canonical sign-in→home flow withexpectedEvents, the mkdocs Test Gallery. - Maestro + TDD —
.maestro/scaffolding, first e2e flow as a red TDD spec, the run docs; thereafter every feature starts with a Maestro flow.