act<T> method

Future<T> act<T>(
  1. String description,
  2. FutureOr<T> body(), {
  3. List<String> tags = const [],
  4. Settle? settle,
  5. Duration timeout = const Duration(seconds: 10),
  6. bool shot = true,
})

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),
);