scrollTo method

Future<DriveStep> scrollTo(
  1. dynamic target, {
  2. dynamic within,
  3. double step = 200,
  4. int maxScrolls = 50,
  5. Duration? settle,
})

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