split method
Forks the scenario: every branch runs, each in its own replay of the whole body — so each branch starts from the exact state this line was reached with, and the flow graph fans out here.
await s.split({
'pay by card': () async {
await s.tap('Pay');
await s.screen('Receipt');
},
'payment fails': () async {
await s.tap('Pay');
await s.screen('Error dialog');
},
});
Steps before the split are captured once and shared; anything after the
call runs per branch, since by then the paths have genuinely diverged.
Splits nest. Under bare flutter test the replays run too, so CI
asserts every path.
What a replay does and does not reset. The widget tree is torn down and rebuilt from nothing, so each path starts from a fresh app. Anything outside the tree is not: a seeded repository, a registered singleton, a mock's recorded calls carry from one path into the next.
The body is what re-runs, so the body is where per-path setup belongs:
scenario('Around the shop', (s) async {
repo.seed(); // every path gets a fresh one
await s.pumpWidget(const ShopApp());
await s.split({ ... });
});
A setUp will not do it. setUp runs once per test and a scenario is
one test however many paths it has — which is package:test's own rule,
kept rather than bent: a setUp that fired three times for one test
would be a surprise nothing in the file could explain, and its
tearDown could not follow it (those run when the test ends).
Implementation
Future<void> split(Map<String, Future<void> Function()> branches) async {
if (branches.isEmpty) return;
var names = branches.keys.toList();
var name = names[_state.plan.choose(names)];
// A new segment: positions restart under the extended choice path, and
// the branch's first capture wears the label.
_ordinal = 0;
_segment++;
_pendingBranch = name;
_branchTrail.add(name);
try {
await branches[name]!();
} catch (error, stack) {
// Annotated where the branch is, so a failure says which path reached
// it. The original stack rides along, so the report still points at the
// user's line; nested splits annotate innermost-first and the outer
// ones leave the message alone.
Error.throwWithStackTrace(_inContext(error), stack);
}
}