act<T> method
A step whose cause is not a finger: description says what happened,
body makes it happen, and the screen it produces is captured under
that name.
The verbs above all reach into the widget tree, and a great deal of what moves a real app does not. A push arrives, a deep link lands, a socket pushes a row, a completer the scenario is holding resolves, a fake backend is seeded mid-flow:
await s.act('A photo-ready push arrives', () {
app.notifications.onOpen(data);
});
The waiting was never the gap — settle does that, and did before this
existed. The report was: the screen changed and nothing in the run said
why, so a reader had to infer the cause from the two pictures either
side of it. document and notification are beats that are not
screens; this is the same idea one step earlier, the beat that causes
one.
body may be synchronous or return a future, and whatever it returns
comes back — a handle the rest of the scenario needs is not worth a
variable declared a line above.
It runs under fake time like everything else, and the clock moves for
it. A body that awaits something only the fake clock can finish — a
Future.delayed, a debounce, a repository answering behind a timer, an
animation it started — would otherwise wait forever, since nothing pumps
while it waits. So while it waits on one, the act pumps frames the way a
settle does, until the body completes or timeout of fake time is
spent, and a body still waiting then fails the step with what the clock
still held:
await s.act('The search debounce fires', () => search.query('latte'));
A body that needs no time moves none, and a verb called inside the body moves the clock itself — the act waits for it rather than pumping under it. It is not the place for work that needs the real event loop: runAsync is its own step, so putting one inside this one captures twice, once for what landed and once for the name.
shot: false keeps the step and drops the name. It is captured the way
any verb's automatic step is — labelled act "<description>" in the
flow, collapsed as detail, and under Shots.manual not at all — so it
never reaches scenarios shots or the store export, which keep named
shots only. For a walk that seeds a backend between the screens it is
about: the cause stays in the flow, and out of the exported pictures.
tags belong to the name, and go with it.
Implementation
Future<T> act<T>(
String description,
FutureOr<T> Function() body, {
List<String> tags = const [],
Settle? settle,
Duration timeout = const Duration(seconds: 10),
bool shot = true,
}) => _step(
shot ? Shot(description, tags: tags) : null,
settle,
() => _awaitMovingClock(description, body, timeout),
verb: 'act',
target: shot ? null : describeTarget(description),
);