scrollTo method
Scrolls until target is on screen. Same contract as the scenario verb:
the target may match nothing yet — being off screen is the reason to call
it — and a target already on screen is a no-op, whether or not anything
scrolls.
Implementation
Future<DriveStep> scrollTo(
dynamic target, {
dynamic within,
double step = 200,
int maxScrolls = 50,
Duration? settle,
}) async {
var watch = Stopwatch()..start();
// Guarded for the same reason as the scenario verb's: the walk evaluates
// it on every step, and a `.first` over a row not built yet throws.
var finder = emptyWhenAbsent(finderForTarget(target));
var scrollable = within == null
? find.byType(Scrollable)
: find.descendant(
of: finderForTarget(within),
matching: find.byType(Scrollable),
matchRoot: true,
);
if (scrollable.evaluate().isEmpty) {
var refusal = refusalWhenNothingScrolls(
finder,
describeTarget(target),
within,
_resolver.messages,
);
if (refusal != null) throw refusal;
// Already on screen: nothing to walk, nothing to do — the same no-op
// the scenario verb makes, so a flow ported between the two engines
// keeps working on its short pages.
var settled = await lane.settle(settle ?? settleBudget);
return DriveStep(
verb: 'scrollTo',
target: describeTarget(target),
settle: settled,
elapsed: watch.elapsed,
);
}
// Built but behind the viewport: the walk only drags one way, so jump —
// same reasoning, same helper as the scenario verb. The recheck waits
// for a frame first, since the reveal is only geometry after layout.
if (finder.evaluate().isEmpty) {
if (scrolledPastTarget(finder, scrollable) case var behind?) {
await Scrollable.ensureVisible(behind);
var settled = await lane.settle(settle ?? settleBudget);
if (finder.evaluate().isNotEmpty) {
return DriveStep(
verb: 'scrollTo',
target: describeTarget(target),
settle: settled,
elapsed: watch.elapsed,
);
}
}
}
try {
await controller.scrollUntilVisible(
finder,
step,
scrollable: scrollable.first,
maxScrolls: maxScrolls,
);
} on StateError {
throw TargetError(
TargetFailure.notFound,
_resolver.messages.scrollExhausted(
maxScrolls,
step,
describeTarget(target),
),
);
}
var result = await lane.settle(settle ?? settleBudget);
return DriveStep(
verb: 'scrollTo',
target: describeTarget(target),
settle: result,
elapsed: watch.elapsed,
);
}