controller.dart 68.4 KB
Newer Older
Ian Hickson's avatar
Ian Hickson committed
1
// Copyright 2014 The Flutter Authors. All rights reserved.
2 3 4
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

5
import 'package:clock/clock.dart';
6
import 'package:flutter/foundation.dart';
7 8
import 'package:flutter/gestures.dart';
import 'package:flutter/rendering.dart';
9
import 'package:flutter/services.dart';
10 11 12
import 'package:flutter/widgets.dart';

import 'all_elements.dart';
13
import 'event_simulation.dart';
14 15 16
import 'finders.dart';
import 'test_async_utils.dart';
import 'test_pointer.dart';
17

18 19 20 21 22 23
/// The default drag touch slop used to break up a large drag into multiple
/// smaller moves.
///
/// This value must be greater than [kTouchSlop].
const double kDragSlopDefault = 20.0;

Tong Mu's avatar
Tong Mu committed
24 25
const String _defaultPlatform = kIsWeb ? 'web' : 'android';

26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221
/// Class that programatically interacts with the [Semantics] tree.
///
/// Allows for testing of the [Semantics] tree, which is used by assistive
/// technology, search engines, and other analysis software to determine the
/// meaning of an application.
///
/// Should be accessed through [WidgetController.semantics]. If no custom
/// implementation is provided, a default [SemanticsController] will be created.
class SemanticsController {
  /// Creates a [SemanticsController] that uses the given binding. Will be
  /// automatically created as part of instantiating a [WidgetController], but
  /// a custom implementation can be passed via the [WidgetController] constructor.
  SemanticsController._(WidgetsBinding binding) : _binding = binding;

  static final int _scrollingActions =
    SemanticsAction.scrollUp.index |
    SemanticsAction.scrollDown.index |
    SemanticsAction.scrollLeft.index |
    SemanticsAction.scrollRight.index;

  /// Based on Android's FOCUSABLE_FLAGS. See [flutter/engine/AccessibilityBridge.java](https://github.com/flutter/engine/blob/main/shell/platform/android/io/flutter/view/AccessibilityBridge.java).
  static final int _importantFlagsForAccessibility =
    SemanticsFlag.hasCheckedState.index |
    SemanticsFlag.hasToggledState.index |
    SemanticsFlag.hasEnabledState.index |
    SemanticsFlag.isButton.index |
    SemanticsFlag.isTextField.index |
    SemanticsFlag.isFocusable.index |
    SemanticsFlag.isSlider.index |
    SemanticsFlag.isInMutuallyExclusiveGroup.index;

  final WidgetsBinding _binding;

  /// Attempts to find the [SemanticsNode] of first result from `finder`.
  ///
  /// If the object identified by the finder doesn't own its semantic node,
  /// this will return the semantics data of the first ancestor with semantics.
  /// The ancestor's semantic data will include the child's as well as
  /// other nodes that have been merged together.
  ///
  /// If the [SemanticsNode] of the object identified by the finder is
  /// force-merged into an ancestor (e.g. via the [MergeSemantics] widget)
  /// the node into which it is merged is returned. That node will include
  /// all the semantics information of the nodes merged into it.
  ///
  /// Will throw a [StateError] if the finder returns more than one element or
  /// if no semantics are found or are not enabled.
  SemanticsNode find(Finder finder) {
    TestAsyncUtils.guardSync();
    if (_binding.pipelineOwner.semanticsOwner == null) {
      throw StateError('Semantics are not enabled.');
    }
    final Iterable<Element> candidates = finder.evaluate();
    if (candidates.isEmpty) {
      throw StateError('Finder returned no matching elements.');
    }
    if (candidates.length > 1) {
      throw StateError('Finder returned more than one element.');
    }
    final Element element = candidates.single;
    RenderObject? renderObject = element.findRenderObject();
    SemanticsNode? result = renderObject?.debugSemantics;
    while (renderObject != null && (result == null || result.isMergedIntoParent)) {
      renderObject = renderObject.parent as RenderObject?;
      result = renderObject?.debugSemantics;
    }
    if (result == null) {
      throw StateError('No Semantics data found.');
    }
    return result;
  }

  /// Simulates a traversal of the currently visible semantics tree as if by
  /// assistive technologies.
  ///
  /// Starts at the node for `start`. If `start` is not provided, then the
  /// traversal begins with the first accessible node in the tree. If `start`
  /// finds zero elements or more than one element, a [StateError] will be
  /// thrown.
  ///
  /// Ends at the node for `end`, inclusive. If `end` is not provided, then the
  /// traversal ends with the last accessible node in the currently available
  /// tree. If `end` finds zero elements or more than one element, a
  /// [StateError] will be thrown.
  ///
  /// Since the order is simulated, edge cases that differ between platforms
  /// (such as how the last visible item in a scrollable list is handled) may be
  /// inconsistent with platform behavior, but are expected to be sufficient for
  /// testing order, availability to assistive technologies, and interactions.
  ///
  /// ## Sample Code
  ///
  /// ```
  /// testWidgets('MyWidget', (WidgetTester tester) async {
  ///   await tester.pumpWidget(MyWidget());
  ///
  ///   expect(
  ///     tester.semantics.simulatedAccessibilityTraversal(),
  ///     containsAllInOrder([
  ///       containsSemantics(label: 'My Widget'),
  ///       containsSemantics(label: 'is awesome!', isChecked: true),
  ///     ]),
  ///   );
  /// });
  /// ```
  ///
  /// See also:
  ///
  /// * [containsSemantics] and [matchesSemantics], which can be used to match
  ///   against a single node in the traversal
  /// * [containsAllInOrder], which can be given an [Iterable<Matcher>] to fuzzy
  ///   match the order allowing extra nodes before after and between matching
  ///   parts of the traversal
  /// * [orderedEquals], which can be given an [Iterable<Matcher>] to exactly
  ///   match the order of the traversal
  Iterable<SemanticsNode> simulatedAccessibilityTraversal({Finder? start, Finder? end}) {
    TestAsyncUtils.guardSync();
    final List<SemanticsNode> traversal = <SemanticsNode>[];
    _traverse(_binding.pipelineOwner.semanticsOwner!.rootSemanticsNode!, traversal);

    int startIndex = 0;
    int endIndex = traversal.length - 1;

    if (start != null) {
      final SemanticsNode startNode = find(start);
      startIndex = traversal.indexOf(startNode);
      if (startIndex == -1) {
        throw StateError(
          'The expected starting node was not found.\n'
          'Finder: ${start.description}\n\n'
          'Expected Start Node: $startNode\n\n'
          'Traversal: [\n  ${traversal.join('\n  ')}\n]');
      }
    }

    if (end != null) {
      final SemanticsNode endNode = find(end);
      endIndex = traversal.indexOf(endNode);
      if (endIndex == -1) {
        throw StateError(
          'The expected ending node was not found.\n'
          'Finder: ${end.description}\n\n'
          'Expected End Node: $endNode\n\n'
          'Traversal: [\n  ${traversal.join('\n  ')}\n]');
      }
    }

    return traversal.getRange(startIndex, endIndex + 1);
  }

  /// Recursive depth first traversal of the specified `node`, adding nodes
  /// that are important for semantics to the `traversal` list.
  void _traverse(SemanticsNode node, List<SemanticsNode> traversal){
    if (_isImportantForAccessibility(node)) {
      traversal.add(node);
    }

    final List<SemanticsNode> children = node.debugListChildrenInOrder(DebugSemanticsDumpOrder.traversalOrder);
    for (final SemanticsNode child in children) {
      _traverse(child, traversal);
    }
  }

  /// Whether or not the node is important for semantics. Should match most cases
  /// on the platforms, but certain edge cases will be inconsisent.
  ///
  /// Based on:
  ///
  /// * [flutter/engine/AccessibilityBridge.java#SemanticsNode.isFocusable()](https://github.com/flutter/engine/blob/main/shell/platform/android/io/flutter/view/AccessibilityBridge.java#L2641)
  /// * [flutter/engine/SemanticsObject.mm#SemanticsObject.isAccessibilityElement](https://github.com/flutter/engine/blob/main/shell/platform/darwin/ios/framework/Source/SemanticsObject.mm#L449)
  bool _isImportantForAccessibility(SemanticsNode node) {
    // If the node scopes a route, it doesn't matter what other flags/actions it
    // has, it is _not_ important for accessibility, so we short circuit.
    if (node.hasFlag(SemanticsFlag.scopesRoute)) {
      return false;
    }

    final bool hasNonScrollingAction = node.getSemanticsData().actions & ~_scrollingActions != 0;
    if (hasNonScrollingAction) {
      return true;
    }

    final bool hasImportantFlag = node.getSemanticsData().flags & _importantFlagsForAccessibility != 0;
    if (hasImportantFlag) {
      return true;
    }

    final bool hasContent = node.label.isNotEmpty || node.value.isNotEmpty || node.hint.isNotEmpty;
    if (hasContent) {
      return true;
    }

    return false;
  }
}

222 223
/// Class that programmatically interacts with widgets.
///
224 225 226 227 228 229
/// For a variant of this class suited specifically for unit tests, see
/// [WidgetTester]. For one suitable for live tests on a device, consider
/// [LiveWidgetController].
///
/// Concrete subclasses must implement the [pump] method.
abstract class WidgetController {
230
  /// Creates a widget controller that uses the given binding.
231 232
  WidgetController(this.binding)
    : _semantics = SemanticsController._(binding);
233

234
  /// A reference to the current instance of the binding.
235 236
  final WidgetsBinding binding;

237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254
  /// Provides access to a [SemanticsController] for testing anything related to
  /// the [Semantics] tree.
  ///
  /// Assistive technologies, search engines, and other analysis tools all make
  /// use of the [Semantics] tree to determine the meaning of an application.
  /// If semantics has been disabled for the test, this will throw a [StateError].
  SemanticsController get semantics {
    if (binding.pipelineOwner.semanticsOwner == null) {
      throw StateError(
        'Semantics are not enabled. Enable them by passing '
        '`semanticsEnabled: true` to `testWidgets`, or by manually creating a '
        '`SemanticsHandle` with `WidgetController.ensureSemantics()`.');
    }

    return _semantics;
  }
  final SemanticsController _semantics;

255 256 257 258 259 260
  // FINDER API

  // TODO(ianh): verify that the return values are of type T and throw
  // a good message otherwise, in all the generic methods below

  /// Checks if `finder` exists in the tree.
261 262 263 264
  bool any(Finder finder) {
    TestAsyncUtils.guardSync();
    return finder.evaluate().isNotEmpty;
  }
265 266 267 268 269 270

  /// All widgets currently in the widget tree (lazy pre-order traversal).
  ///
  /// Can contain duplicates, since widgets can be used in multiple
  /// places in the widget tree.
  Iterable<Widget> get allWidgets {
271
    TestAsyncUtils.guardSync();
272
    return allElements.map<Widget>((Element element) => element.widget);
273 274 275 276 277 278
  }

  /// The matching widget in the widget tree.
  ///
  /// Throws a [StateError] if `finder` is empty or matches more than
  /// one widget.
279 280 281
  ///
  /// * Use [firstWidget] if you expect to match several widgets but only want the first.
  /// * Use [widgetList] if you expect to match several widgets and want all of them.
282
  T widget<T extends Widget>(Finder finder) {
283
    TestAsyncUtils.guardSync();
284
    return finder.evaluate().single.widget as T;
285 286 287 288 289 290
  }

  /// The first matching widget according to a depth-first pre-order
  /// traversal of the widget tree.
  ///
  /// Throws a [StateError] if `finder` is empty.
291 292
  ///
  /// * Use [widget] if you only expect to match one widget.
293
  T firstWidget<T extends Widget>(Finder finder) {
294
    TestAsyncUtils.guardSync();
295
    return finder.evaluate().first.widget as T;
296 297
  }

298 299 300 301
  /// The matching widgets in the widget tree.
  ///
  /// * Use [widget] if you only expect to match one widget.
  /// * Use [firstWidget] if you expect to match several but only want the first.
302
  Iterable<T> widgetList<T extends Widget>(Finder finder) {
303
    TestAsyncUtils.guardSync();
304
    return finder.evaluate().map<T>((Element element) {
305
      final T result = element.widget as T;
306 307
      return result;
    });
308 309
  }

310 311 312 313 314 315 316 317 318 319 320 321 322 323 324
  /// Find all layers that are children of the provided [finder].
  ///
  /// The [finder] must match exactly one element.
  Iterable<Layer> layerListOf(Finder finder) {
    TestAsyncUtils.guardSync();
    final Element element = finder.evaluate().single;
    final RenderObject object = element.renderObject!;
    RenderObject current = object;
    while (current.debugLayer == null) {
      current = current.parent! as RenderObject;
    }
    final ContainerLayer layer = current.debugLayer!;
    return _walkLayers(layer);
  }

325 326 327 328 329 330
  /// All elements currently in the widget tree (lazy pre-order traversal).
  ///
  /// The returned iterable is lazy. It does not walk the entire widget tree
  /// immediately, but rather a chunk at a time as the iteration progresses
  /// using [Iterator.moveNext].
  Iterable<Element> get allElements {
331
    TestAsyncUtils.guardSync();
332
    return collectAllElementsFrom(binding.renderViewElement!, skipOffstage: false);
333 334 335 336 337 338
  }

  /// The matching element in the widget tree.
  ///
  /// Throws a [StateError] if `finder` is empty or matches more than
  /// one element.
339 340 341
  ///
  /// * Use [firstElement] if you expect to match several elements but only want the first.
  /// * Use [elementList] if you expect to match several elements and want all of them.
342
  T element<T extends Element>(Finder finder) {
343
    TestAsyncUtils.guardSync();
344
    return finder.evaluate().single as T;
345 346 347 348 349 350
  }

  /// The first matching element according to a depth-first pre-order
  /// traversal of the widget tree.
  ///
  /// Throws a [StateError] if `finder` is empty.
351 352
  ///
  /// * Use [element] if you only expect to match one element.
353
  T firstElement<T extends Element>(Finder finder) {
354
    TestAsyncUtils.guardSync();
355
    return finder.evaluate().first as T;
356 357
  }

358 359 360 361
  /// The matching elements in the widget tree.
  ///
  /// * Use [element] if you only expect to match one element.
  /// * Use [firstElement] if you expect to match several but only want the first.
362
  Iterable<T> elementList<T extends Element>(Finder finder) {
363
    TestAsyncUtils.guardSync();
364
    return finder.evaluate().cast<T>();
365 366
  }

367 368 369 370 371 372
  /// All states currently in the widget tree (lazy pre-order traversal).
  ///
  /// The returned iterable is lazy. It does not walk the entire widget tree
  /// immediately, but rather a chunk at a time as the iteration progresses
  /// using [Iterator.moveNext].
  Iterable<State> get allStates {
373
    TestAsyncUtils.guardSync();
374
    return allElements.whereType<StatefulElement>().map<State>((StatefulElement element) => element.state);
375 376 377 378 379 380
  }

  /// The matching state in the widget tree.
  ///
  /// Throws a [StateError] if `finder` is empty, matches more than
  /// one state, or matches a widget that has no state.
381 382 383
  ///
  /// * Use [firstState] if you expect to match several states but only want the first.
  /// * Use [stateList] if you expect to match several states and want all of them.
384
  T state<T extends State>(Finder finder) {
385
    TestAsyncUtils.guardSync();
386
    return _stateOf<T>(finder.evaluate().single, finder);
387 388 389 390 391 392 393
  }

  /// The first matching state according to a depth-first pre-order
  /// traversal of the widget tree.
  ///
  /// Throws a [StateError] if `finder` is empty or if the first
  /// matching widget has no state.
394 395
  ///
  /// * Use [state] if you only expect to match one state.
396
  T firstState<T extends State>(Finder finder) {
397
    TestAsyncUtils.guardSync();
398
    return _stateOf<T>(finder.evaluate().first, finder);
399 400
  }

401 402 403 404 405 406 407
  /// The matching states in the widget tree.
  ///
  /// Throws a [StateError] if any of the elements in `finder` match a widget
  /// that has no state.
  ///
  /// * Use [state] if you only expect to match one state.
  /// * Use [firstState] if you expect to match several but only want the first.
408
  Iterable<T> stateList<T extends State>(Finder finder) {
409
    TestAsyncUtils.guardSync();
410
    return finder.evaluate().map<T>((Element element) => _stateOf<T>(element, finder));
411 412
  }

413
  T _stateOf<T extends State>(Element element, Finder finder) {
414
    TestAsyncUtils.guardSync();
415
    if (element is StatefulElement) {
416
      return element.state as T;
417
    }
418
    throw StateError('Widget of type ${element.widget.runtimeType}, with ${finder.description}, is not a StatefulWidget.');
419 420 421 422 423 424 425 426 427 428
  }

  /// Render objects of all the widgets currently in the widget tree
  /// (lazy pre-order traversal).
  ///
  /// This will almost certainly include many duplicates since the
  /// render object of a [StatelessWidget] or [StatefulWidget] is the
  /// render object of its child; only [RenderObjectWidget]s have
  /// their own render object.
  Iterable<RenderObject> get allRenderObjects {
429
    TestAsyncUtils.guardSync();
430
    return allElements.map<RenderObject>((Element element) => element.renderObject!);
431 432 433 434 435 436
  }

  /// The render object of the matching widget in the widget tree.
  ///
  /// Throws a [StateError] if `finder` is empty or matches more than
  /// one widget (even if they all have the same render object).
437 438 439
  ///
  /// * Use [firstRenderObject] if you expect to match several render objects but only want the first.
  /// * Use [renderObjectList] if you expect to match several render objects and want all of them.
440
  T renderObject<T extends RenderObject>(Finder finder) {
441
    TestAsyncUtils.guardSync();
442
    return finder.evaluate().single.renderObject! as T;
443 444 445 446 447 448
  }

  /// The render object of the first matching widget according to a
  /// depth-first pre-order traversal of the widget tree.
  ///
  /// Throws a [StateError] if `finder` is empty.
449 450
  ///
  /// * Use [renderObject] if you only expect to match one render object.
451
  T firstRenderObject<T extends RenderObject>(Finder finder) {
452
    TestAsyncUtils.guardSync();
453
    return finder.evaluate().first.renderObject! as T;
454 455
  }

456 457 458 459
  /// The render objects of the matching widgets in the widget tree.
  ///
  /// * Use [renderObject] if you only expect to match one render object.
  /// * Use [firstRenderObject] if you expect to match several but only want the first.
460
  Iterable<T> renderObjectList<T extends RenderObject>(Finder finder) {
461
    TestAsyncUtils.guardSync();
462
    return finder.evaluate().map<T>((Element element) {
463
      final T result = element.renderObject! as T;
464 465
      return result;
    });
466 467
  }

468
  /// Returns a list of all the [Layer] objects in the rendering.
469
  List<Layer> get layers => _walkLayers(binding.renderView.debugLayer!).toList();
470
  Iterable<Layer> _walkLayers(Layer layer) sync* {
471
    TestAsyncUtils.guardSync();
472 473
    yield layer;
    if (layer is ContainerLayer) {
474
      final ContainerLayer root = layer;
475
      Layer? child = root.firstChild;
476 477 478 479 480 481 482 483 484 485
      while (child != null) {
        yield* _walkLayers(child);
        child = child.nextSibling;
      }
    }
  }

  // INTERACTION

  /// Dispatch a pointer down / pointer up sequence at the center of
486 487
  /// the given widget, assuming it is exposed.
  ///
488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504
  /// {@template flutter.flutter_test.WidgetController.tap.warnIfMissed}
  /// The `warnIfMissed` argument, if true (the default), causes a warning to be
  /// displayed on the console if the specified [Finder] indicates a widget and
  /// location that, were a pointer event to be sent to that location, would not
  /// actually send any events to the widget (e.g. because the widget is
  /// obscured, or the location is off-screen, or the widget is transparent to
  /// pointer events).
  ///
  /// Set the argument to false to silence that warning if you intend to not
  /// actually hit the specified element.
  /// {@endtemplate}
  ///
  /// For example, a test that verifies that tapping a disabled button does not
  /// trigger the button would set `warnIfMissed` to false, because the button
  /// would ignore the tap.
  Future<void> tap(Finder finder, {int? pointer, int buttons = kPrimaryButton, bool warnIfMissed = true}) {
    return tapAt(getCenter(finder, warnIfMissed: warnIfMissed, callee: 'tap'), pointer: pointer, buttons: buttons);
505 506
  }

507
  /// Dispatch a pointer down / pointer up sequence at the given location.
508
  Future<void> tapAt(Offset location, {int? pointer, int buttons = kPrimaryButton}) {
509
    return TestAsyncUtils.guard<void>(() async {
510
      final TestGesture gesture = await startGesture(location, pointer: pointer, buttons: buttons);
511 512
      await gesture.up();
    });
513 514
  }

515 516 517
  /// Dispatch a pointer down at the center of the given widget, assuming it is
  /// exposed.
  ///
518 519 520 521 522 523 524 525 526 527 528
  /// {@macro flutter.flutter_test.WidgetController.tap.warnIfMissed}
  ///
  /// The return value is a [TestGesture] object that can be used to continue the
  /// gesture (e.g. moving the pointer or releasing it).
  ///
  /// See also:
  ///
  ///  * [tap], which presses and releases a pointer at the given location.
  ///  * [longPress], which presses and releases a pointer with a gap in
  ///    between long enough to trigger the long-press gesture.
  Future<TestGesture> press(Finder finder, {int? pointer, int buttons = kPrimaryButton, bool warnIfMissed = true}) {
529
    return TestAsyncUtils.guard<TestGesture>(() {
530
      return startGesture(getCenter(finder, warnIfMissed: warnIfMissed, callee: 'press'), pointer: pointer, buttons: buttons);
531 532 533
    });
  }

534 535
  /// Dispatch a pointer down / pointer up sequence (with a delay of
  /// [kLongPressTimeout] + [kPressTimeout] between the two events) at the
536 537
  /// center of the given widget, assuming it is exposed.
  ///
538 539 540 541 542 543 544 545 546 547
  /// {@macro flutter.flutter_test.WidgetController.tap.warnIfMissed}
  ///
  /// For example, consider a widget that, when long-pressed, shows an overlay
  /// that obscures the original widget. A test for that widget might first
  /// long-press that widget with `warnIfMissed` at its default value true, then
  /// later verify that long-pressing the same location (using the same finder)
  /// has no effect (since the widget is now obscured), setting `warnIfMissed`
  /// to false on that second call.
  Future<void> longPress(Finder finder, {int? pointer, int buttons = kPrimaryButton, bool warnIfMissed = true}) {
    return longPressAt(getCenter(finder, warnIfMissed: warnIfMissed, callee: 'longPress'), pointer: pointer, buttons: buttons);
548 549 550 551
  }

  /// Dispatch a pointer down / pointer up sequence at the given location with
  /// a delay of [kLongPressTimeout] + [kPressTimeout] between the two events.
552
  Future<void> longPressAt(Offset location, {int? pointer, int buttons = kPrimaryButton}) {
553
    return TestAsyncUtils.guard<void>(() async {
554
      final TestGesture gesture = await startGesture(location, pointer: pointer, buttons: buttons);
555 556 557 558 559
      await pump(kLongPressTimeout + kPressTimeout);
      await gesture.up();
    });
  }

560
  /// Attempts a fling gesture starting from the center of the given
561
  /// widget, moving the given distance, reaching the given speed.
562
  ///
563
  /// {@macro flutter.flutter_test.WidgetController.tap.warnIfMissed}
564
  ///
565 566 567 568 569 570 571 572 573 574
  /// {@template flutter.flutter_test.WidgetController.fling.offset}
  /// The `offset` represents a distance the pointer moves in the global
  /// coordinate system of the screen.
  ///
  /// Positive [Offset.dy] values mean the pointer moves downward. Negative
  /// [Offset.dy] values mean the pointer moves upwards. Accordingly, positive
  /// [Offset.dx] values mean the pointer moves towards the right. Negative
  /// [Offset.dx] values mean the pointer moves towards left.
  /// {@endtemplate}
  ///
575
  /// {@template flutter.flutter_test.WidgetController.fling}
576 577 578
  /// This can pump frames.
  ///
  /// Exactly 50 pointer events are synthesized.
579 580 581
  ///
  /// The `speed` is in pixels per second in the direction given by `offset`.
  ///
582 583 584 585 586 587 588 589 590 591 592 593 594
  /// The `offset` and `speed` control the interval between each pointer event.
  /// For example, if the `offset` is 200 pixels down, and the `speed` is 800
  /// pixels per second, the pointer events will be sent for each increment
  /// of 4 pixels (200/50), over 250ms (200/800), meaning events will be sent
  /// every 1.25ms (250/200).
  ///
  /// To make tests more realistic, frames may be pumped during this time (using
  /// calls to [pump]). If the total duration is longer than `frameInterval`,
  /// then one frame is pumped each time that amount of time elapses while
  /// sending events, or each time an event is synthesized, whichever is rarer.
  ///
  /// See [LiveTestWidgetsFlutterBindingFramePolicy.benchmarkLive] if the method
  /// is used in a live environment and accurate time control is important.
Ian Hickson's avatar
Ian Hickson committed
595 596 597 598 599 600 601
  ///
  /// The `initialOffset` argument, if non-zero, causes the pointer to first
  /// apply that offset, then pump a delay of `initialOffsetDelay`. This can be
  /// used to simulate a drag followed by a fling, including dragging in the
  /// opposite direction of the fling (e.g. dragging 200 pixels to the right,
  /// then fling to the left over 200 pixels, ending at the exact point that the
  /// drag started).
602 603 604 605
  /// {@endtemplate}
  ///
  /// A fling is essentially a drag that ends at a particular speed. If you
  /// just want to drag and end without a fling, use [drag].
606 607 608 609
  Future<void> fling(
    Finder finder,
    Offset offset,
    double speed, {
610
    int? pointer,
611
    int buttons = kPrimaryButton,
612 613 614
    Duration frameInterval = const Duration(milliseconds: 16),
    Offset initialOffset = Offset.zero,
    Duration initialOffsetDelay = const Duration(seconds: 1),
615
    bool warnIfMissed = true,
616
    PointerDeviceKind deviceKind = PointerDeviceKind.touch,
617
  }) {
Ian Hickson's avatar
Ian Hickson committed
618
    return flingFrom(
619
      getCenter(finder, warnIfMissed: warnIfMissed, callee: 'fling'),
Ian Hickson's avatar
Ian Hickson committed
620 621 622
      offset,
      speed,
      pointer: pointer,
623
      buttons: buttons,
Ian Hickson's avatar
Ian Hickson committed
624 625 626
      frameInterval: frameInterval,
      initialOffset: initialOffset,
      initialOffsetDelay: initialOffsetDelay,
627
      deviceKind: deviceKind,
Ian Hickson's avatar
Ian Hickson committed
628
    );
629 630
  }

631 632
  /// Attempts a fling gesture starting from the given location, moving the
  /// given distance, reaching the given speed.
633
  ///
634
  /// {@macro flutter.flutter_test.WidgetController.fling}
635 636 637
  ///
  /// A fling is essentially a drag that ends at a particular speed. If you
  /// just want to drag and end without a fling, use [dragFrom].
638 639 640 641
  Future<void> flingFrom(
    Offset startLocation,
    Offset offset,
    double speed, {
642
    int? pointer,
643
    int buttons = kPrimaryButton,
644 645 646
    Duration frameInterval = const Duration(milliseconds: 16),
    Offset initialOffset = Offset.zero,
    Duration initialOffsetDelay = const Duration(seconds: 1),
647
    PointerDeviceKind deviceKind = PointerDeviceKind.touch,
Ian Hickson's avatar
Ian Hickson committed
648
  }) {
649
    assert(offset.distance > 0.0);
650
    assert(speed > 0.0); // speed is pixels/second
651
    return TestAsyncUtils.guard<void>(() async {
652
      final TestPointer testPointer = TestPointer(pointer ?? _getNextPointer(), deviceKind, null, buttons);
653
      const int kMoveCount = 50; // Needs to be >= kHistorySize, see _LeastSquaresVelocityTrackerStrategy
654
      final double timeStampDelta = 1000000.0 * offset.distance / (kMoveCount * speed);
655
      double timeStamp = 0.0;
656
      double lastTimeStamp = timeStamp;
657
      await sendEventToBinding(testPointer.down(startLocation, timeStamp: Duration(microseconds: timeStamp.round())));
Ian Hickson's avatar
Ian Hickson committed
658
      if (initialOffset.distance > 0.0) {
659
        await sendEventToBinding(testPointer.move(startLocation + initialOffset, timeStamp: Duration(microseconds: timeStamp.round())));
660
        timeStamp += initialOffsetDelay.inMicroseconds;
Ian Hickson's avatar
Ian Hickson committed
661 662
        await pump(initialOffsetDelay);
      }
663
      for (int i = 0; i <= kMoveCount; i += 1) {
664
        final Offset location = startLocation + initialOffset + Offset.lerp(Offset.zero, offset, i / kMoveCount)!;
665
        await sendEventToBinding(testPointer.move(location, timeStamp: Duration(microseconds: timeStamp.round())));
666
        timeStamp += timeStampDelta;
667 668
        if (timeStamp - lastTimeStamp > frameInterval.inMicroseconds) {
          await pump(Duration(microseconds: (timeStamp - lastTimeStamp).truncate()));
669 670
          lastTimeStamp = timeStamp;
        }
671
      }
672
      await sendEventToBinding(testPointer.up(timeStamp: Duration(microseconds: timeStamp.round())));
673
    });
674 675
  }

676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753
  /// Attempts a trackpad fling gesture starting from the center of the given
  /// widget, moving the given distance, reaching the given speed. A trackpad
  /// fling sends PointerPanZoom events instead of a sequence of touch events.
  ///
  /// {@macro flutter.flutter_test.WidgetController.tap.warnIfMissed}
  ///
  /// {@macro flutter.flutter_test.WidgetController.fling}
  ///
  /// A fling is essentially a drag that ends at a particular speed. If you
  /// just want to drag and end without a fling, use [drag].
  Future<void> trackpadFling(
    Finder finder,
    Offset offset,
    double speed, {
    int? pointer,
    int buttons = kPrimaryButton,
    Duration frameInterval = const Duration(milliseconds: 16),
    Offset initialOffset = Offset.zero,
    Duration initialOffsetDelay = const Duration(seconds: 1),
    bool warnIfMissed = true,
  }) {
    return trackpadFlingFrom(
      getCenter(finder, warnIfMissed: warnIfMissed, callee: 'fling'),
      offset,
      speed,
      pointer: pointer,
      buttons: buttons,
      frameInterval: frameInterval,
      initialOffset: initialOffset,
      initialOffsetDelay: initialOffsetDelay,
    );
  }

  /// Attempts a fling gesture starting from the given location, moving the
  /// given distance, reaching the given speed. A trackpad fling sends
  /// PointerPanZoom events instead of a sequence of touch events.
  ///
  /// {@macro flutter.flutter_test.WidgetController.fling}
  ///
  /// A fling is essentially a drag that ends at a particular speed. If you
  /// just want to drag and end without a fling, use [dragFrom].
  Future<void> trackpadFlingFrom(
    Offset startLocation,
    Offset offset,
    double speed, {
    int? pointer,
    int buttons = kPrimaryButton,
    Duration frameInterval = const Duration(milliseconds: 16),
    Offset initialOffset = Offset.zero,
    Duration initialOffsetDelay = const Duration(seconds: 1),
  }) {
    assert(offset.distance > 0.0);
    assert(speed > 0.0); // speed is pixels/second
    return TestAsyncUtils.guard<void>(() async {
      final TestPointer testPointer = TestPointer(pointer ?? _getNextPointer(), PointerDeviceKind.trackpad, null, buttons);
      const int kMoveCount = 50; // Needs to be >= kHistorySize, see _LeastSquaresVelocityTrackerStrategy
      final double timeStampDelta = 1000000.0 * offset.distance / (kMoveCount * speed);
      double timeStamp = 0.0;
      double lastTimeStamp = timeStamp;
      await sendEventToBinding(testPointer.panZoomStart(startLocation, timeStamp: Duration(microseconds: timeStamp.round())));
      if (initialOffset.distance > 0.0) {
        await sendEventToBinding(testPointer.panZoomUpdate(startLocation, pan: initialOffset, timeStamp: Duration(microseconds: timeStamp.round())));
        timeStamp += initialOffsetDelay.inMicroseconds;
        await pump(initialOffsetDelay);
      }
      for (int i = 0; i <= kMoveCount; i += 1) {
        final Offset pan = initialOffset + Offset.lerp(Offset.zero, offset, i / kMoveCount)!;
        await sendEventToBinding(testPointer.panZoomUpdate(startLocation, pan: pan, timeStamp: Duration(microseconds: timeStamp.round())));
        timeStamp += timeStampDelta;
        if (timeStamp - lastTimeStamp > frameInterval.inMicroseconds) {
          await pump(Duration(microseconds: (timeStamp - lastTimeStamp).truncate()));
          lastTimeStamp = timeStamp;
        }
      }
      await sendEventToBinding(testPointer.panZoomEnd(timeStamp: Duration(microseconds: timeStamp.round())));
    });
  }

754 755 756 757 758 759
  /// A simulator of how the framework handles a series of [PointerEvent]s
  /// received from the Flutter engine.
  ///
  /// The [PointerEventRecord.timeDelay] is used as the time delay of the events
  /// injection relative to the starting point of the method call.
  ///
760 761 762 763 764 765 766
  /// Returns a list of the difference between the real delay time when the
  /// [PointerEventRecord.events] are processed and
  /// [PointerEventRecord.timeDelay].
  /// - For [AutomatedTestWidgetsFlutterBinding] where the clock is fake, the
  ///   return value should be exact zeros.
  /// - For [LiveTestWidgetsFlutterBinding], the values are typically small
  /// positives, meaning the event happens a little later than the set time,
767
  /// but a very small portion may have a tiny negative value for about tens of
768 769 770
  /// microseconds. This is due to the nature of [Future.delayed].
  ///
  /// The closer the return values are to zero the more faithful it is to the
771 772 773 774 775
  /// `records`.
  ///
  /// See [PointerEventRecord].
  Future<List<Duration>> handlePointerEventRecord(List<PointerEventRecord> records);

776 777 778 779 780
  /// Called to indicate that there should be a new frame after an optional
  /// delay.
  ///
  /// The frame is pumped after a delay of [duration] if [duration] is not null,
  /// or immediately otherwise.
781 782 783 784
  ///
  /// This is invoked by [flingFrom], for instance, so that the sequence of
  /// pointer events occurs over time.
  ///
785
  /// The [WidgetTester] subclass implements this by deferring to the [binding].
786 787 788
  ///
  /// See also [SchedulerBinding.endOfFrame], which returns a future that could
  /// be appropriate to return in the implementation of this method.
789
  Future<void> pump([Duration duration]);
790

791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818
  /// Repeatedly calls [pump] with the given `duration` until there are no
  /// longer any frames scheduled. This will call [pump] at least once, even if
  /// no frames are scheduled when the function is called, to flush any pending
  /// microtasks which may themselves schedule a frame.
  ///
  /// This essentially waits for all animations to have completed.
  ///
  /// If it takes longer that the given `timeout` to settle, then the test will
  /// fail (this method will throw an exception). In particular, this means that
  /// if there is an infinite animation in progress (for example, if there is an
  /// indeterminate progress indicator spinning), this method will throw.
  ///
  /// The default timeout is ten minutes, which is longer than most reasonable
  /// finite animations would last.
  ///
  /// If the function returns, it returns the number of pumps that it performed.
  ///
  /// In general, it is better practice to figure out exactly why each frame is
  /// needed, and then to [pump] exactly as many frames as necessary. This will
  /// help catch regressions where, for instance, an animation is being started
  /// one frame later than it should.
  ///
  /// Alternatively, one can check that the return value from this function
  /// matches the expected number of pumps.
  Future<int> pumpAndSettle([
    Duration duration = const Duration(milliseconds: 100),
  ]);

819 820 821
  /// Attempts to drag the given widget by the given offset, by
  /// starting a drag in the middle of the widget.
  ///
822
  /// {@macro flutter.flutter_test.WidgetController.tap.warnIfMissed}
823 824 825
  ///
  /// If you want the drag to end with a speed so that the gesture recognition
  /// system identifies the gesture as a fling, consider using [fling] instead.
826
  ///
827 828 829
  /// The operation happens at once. If you want the drag to last for a period
  /// of time, consider using [timedDrag].
  ///
830 831
  /// {@macro flutter.flutter_test.WidgetController.fling.offset}
  ///
832
  /// {@template flutter.flutter_test.WidgetController.drag}
833 834 835 836 837 838
  /// By default, if the x or y component of offset is greater than
  /// [kDragSlopDefault], the gesture is broken up into two separate moves
  /// calls. Changing `touchSlopX` or `touchSlopY` will change the minimum
  /// amount of movement in the respective axis before the drag will be broken
  /// into multiple calls. To always send the drag with just a single call to
  /// [TestGesture.moveBy], `touchSlopX` and `touchSlopY` should be set to 0.
839 840 841 842 843 844
  ///
  /// Breaking the drag into multiple moves is necessary for accurate execution
  /// of drag update calls with a [DragStartBehavior] variable set to
  /// [DragStartBehavior.start]. Without such a change, the dragUpdate callback
  /// from a drag recognizer will never be invoked.
  ///
845 846
  /// To force this function to a send a single move event, the `touchSlopX` and
  /// `touchSlopY` variables should be set to 0. However, generally, these values
847
  /// should be left to their default values.
848
  /// {@endtemplate}
849 850 851
  Future<void> drag(
    Finder finder,
    Offset offset, {
852
    int? pointer,
853 854 855
    int buttons = kPrimaryButton,
    double touchSlopX = kDragSlopDefault,
    double touchSlopY = kDragSlopDefault,
856
    bool warnIfMissed = true,
857
    PointerDeviceKind kind = PointerDeviceKind.touch,
858 859
  }) {
    return dragFrom(
860
      getCenter(finder, warnIfMissed: warnIfMissed, callee: 'drag'),
861 862 863 864 865
      offset,
      pointer: pointer,
      buttons: buttons,
      touchSlopX: touchSlopX,
      touchSlopY: touchSlopY,
866
      kind: kind,
867
    );
868 869 870 871
  }

  /// Attempts a drag gesture consisting of a pointer down, a move by
  /// the given offset, and a pointer up.
872 873 874 875
  ///
  /// If you want the drag to end with a speed so that the gesture recognition
  /// system identifies the gesture as a fling, consider using [flingFrom]
  /// instead.
876
  ///
877 878 879
  /// The operation happens at once. If you want the drag to last for a period
  /// of time, consider using [timedDragFrom].
  ///
880
  /// {@macro flutter.flutter_test.WidgetController.drag}
881 882 883
  Future<void> dragFrom(
    Offset startLocation,
    Offset offset, {
884
    int? pointer,
885 886 887
    int buttons = kPrimaryButton,
    double touchSlopX = kDragSlopDefault,
    double touchSlopY = kDragSlopDefault,
888
    PointerDeviceKind kind = PointerDeviceKind.touch,
889
  }) {
890
    assert(kDragSlopDefault > kTouchSlop);
891
    return TestAsyncUtils.guard<void>(() async {
892
      final TestGesture gesture = await startGesture(startLocation, pointer: pointer, buttons: buttons, kind: kind);
893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 946 947 948 949 950 951 952 953 954 955 956 957

      final double xSign = offset.dx.sign;
      final double ySign = offset.dy.sign;

      final double offsetX = offset.dx;
      final double offsetY = offset.dy;

      final bool separateX = offset.dx.abs() > touchSlopX && touchSlopX > 0;
      final bool separateY = offset.dy.abs() > touchSlopY && touchSlopY > 0;

      if (separateY || separateX) {
        final double offsetSlope = offsetY / offsetX;
        final double inverseOffsetSlope = offsetX / offsetY;
        final double slopSlope = touchSlopY / touchSlopX;
        final double absoluteOffsetSlope = offsetSlope.abs();
        final double signedSlopX = touchSlopX * xSign;
        final double signedSlopY = touchSlopY * ySign;
        if (absoluteOffsetSlope != slopSlope) {
          // The drag goes through one or both of the extents of the edges of the box.
          if (absoluteOffsetSlope < slopSlope) {
            assert(offsetX.abs() > touchSlopX);
            // The drag goes through the vertical edge of the box.
            // It is guaranteed that the |offsetX| > touchSlopX.
            final double diffY = offsetSlope.abs() * touchSlopX * ySign;

            // The vector from the origin to the vertical edge.
            await gesture.moveBy(Offset(signedSlopX, diffY));
            if (offsetY.abs() <= touchSlopY) {
              // The drag ends on or before getting to the horizontal extension of the horizontal edge.
              await gesture.moveBy(Offset(offsetX - signedSlopX, offsetY - diffY));
            } else {
              final double diffY2 = signedSlopY - diffY;
              final double diffX2 = inverseOffsetSlope * diffY2;

              // The vector from the edge of the box to the horizontal extension of the horizontal edge.
              await gesture.moveBy(Offset(diffX2, diffY2));
              await gesture.moveBy(Offset(offsetX - diffX2 - signedSlopX, offsetY - signedSlopY));
            }
          } else {
            assert(offsetY.abs() > touchSlopY);
            // The drag goes through the horizontal edge of the box.
            // It is guaranteed that the |offsetY| > touchSlopY.
            final double diffX = inverseOffsetSlope.abs() * touchSlopY * xSign;

            // The vector from the origin to the vertical edge.
            await gesture.moveBy(Offset(diffX, signedSlopY));
            if (offsetX.abs() <= touchSlopX) {
              // The drag ends on or before getting to the vertical extension of the vertical edge.
              await gesture.moveBy(Offset(offsetX - diffX, offsetY - signedSlopY));
            } else {
              final double diffX2 = signedSlopX - diffX;
              final double diffY2 = offsetSlope * diffX2;

              // The vector from the edge of the box to the vertical extension of the vertical edge.
              await gesture.moveBy(Offset(diffX2, diffY2));
              await gesture.moveBy(Offset(offsetX - signedSlopX, offsetY - diffY2 - signedSlopY));
            }
          }
        } else { // The drag goes through the corner of the box.
          await gesture.moveBy(Offset(signedSlopX, signedSlopY));
          await gesture.moveBy(Offset(offsetX - signedSlopX, offsetY - signedSlopY));
        }
      } else { // The drag ends inside the box.
        await gesture.moveBy(offset);
      }
958 959
      await gesture.up();
    });
960 961
  }

962 963 964
  /// Attempts to drag the given widget by the given offset in the `duration`
  /// time, starting in the middle of the widget.
  ///
965
  /// {@macro flutter.flutter_test.WidgetController.tap.warnIfMissed}
966
  ///
967 968
  /// {@macro flutter.flutter_test.WidgetController.fling.offset}
  ///
969 970 971 972
  /// This is the timed version of [drag]. This may or may not result in a
  /// [fling] or ballistic animation, depending on the speed from
  /// `offset/duration`.
  ///
973
  /// {@template flutter.flutter_test.WidgetController.timedDrag}
974 975 976 977 978 979 980 981 982 983 984 985
  /// The move events are sent at a given `frequency` in Hz (or events per
  /// second). It defaults to 60Hz.
  ///
  /// The movement is linear in time.
  ///
  /// See also [LiveTestWidgetsFlutterBindingFramePolicy.benchmarkLive] for
  /// more accurate time control.
  /// {@endtemplate}
  Future<void> timedDrag(
    Finder finder,
    Offset offset,
    Duration duration, {
986
    int? pointer,
987 988
    int buttons = kPrimaryButton,
    double frequency = 60.0,
989
    bool warnIfMissed = true,
990 991
  }) {
    return timedDragFrom(
992
      getCenter(finder, warnIfMissed: warnIfMissed, callee: 'timedDrag'),
993 994 995 996 997 998 999 1000 1001 1002 1003 1004 1005 1006 1007
      offset,
      duration,
      pointer: pointer,
      buttons: buttons,
      frequency: frequency,
    );
  }

  /// Attempts a series of [PointerEvent]s to simulate a drag operation in the
  /// `duration` time.
  ///
  /// This is the timed version of [dragFrom]. This may or may not result in a
  /// [flingFrom] or ballistic animation, depending on the speed from
  /// `offset/duration`.
  ///
1008
  /// {@macro flutter.flutter_test.WidgetController.timedDrag}
1009 1010 1011 1012
  Future<void> timedDragFrom(
    Offset startLocation,
    Offset offset,
    Duration duration, {
1013
    int? pointer,
1014 1015 1016 1017 1018 1019
    int buttons = kPrimaryButton,
    double frequency = 60.0,
  }) {
    assert(frequency > 0);
    final int intervals = duration.inMicroseconds * frequency ~/ 1E6;
    assert(intervals > 1);
1020
    pointer ??= _getNextPointer();
1021 1022 1023 1024 1025 1026 1027 1028 1029 1030 1031 1032 1033 1034 1035 1036 1037 1038 1039 1040 1041 1042 1043 1044 1045 1046 1047 1048 1049
    final List<Duration> timeStamps = <Duration>[
      for (int t = 0; t <= intervals; t += 1)
        duration * t ~/ intervals,
    ];
    final List<Offset> offsets = <Offset>[
      startLocation,
      for (int t = 0; t <= intervals; t += 1)
        startLocation + offset * (t / intervals),
    ];
    final List<PointerEventRecord> records = <PointerEventRecord>[
      PointerEventRecord(Duration.zero, <PointerEvent>[
          PointerAddedEvent(
            position: startLocation,
          ),
          PointerDownEvent(
            position: startLocation,
            pointer: pointer,
            buttons: buttons,
          ),
        ]),
      ...<PointerEventRecord>[
        for(int t = 0; t <= intervals; t += 1)
          PointerEventRecord(timeStamps[t], <PointerEvent>[
            PointerMoveEvent(
              timeStamp: timeStamps[t],
              position: offsets[t+1],
              delta: offsets[t+1] - offsets[t],
              pointer: pointer,
              buttons: buttons,
1050
            ),
1051 1052 1053 1054 1055 1056 1057
          ]),
      ],
      PointerEventRecord(duration, <PointerEvent>[
        PointerUpEvent(
          timeStamp: duration,
          position: offsets.last,
          pointer: pointer,
1058 1059
          // The PointerData received from the engine with
          // change = PointerChange.up, which translates to PointerUpEvent,
1060 1061
          // doesn't provide the button field.
          // buttons: buttons,
1062
        ),
1063 1064 1065
      ]),
    ];
    return TestAsyncUtils.guard<void>(() async {
1066
      await handlePointerEventRecord(records);
1067 1068 1069
    });
  }

1070 1071 1072 1073
  /// The next available pointer identifier.
  ///
  /// This is the default pointer identifier that will be used the next time the
  /// [startGesture] method is called without an explicit pointer identifier.
1074
  int get nextPointer => _nextPointer;
1075

1076 1077 1078 1079 1080
  static int _nextPointer = 1;

  static int _getNextPointer() {
    final int result = _nextPointer;
    _nextPointer += 1;
1081 1082 1083
    return result;
  }

1084 1085 1086 1087 1088
  /// Creates gesture and returns the [TestGesture] object which you can use
  /// to continue the gesture using calls on the [TestGesture] object.
  ///
  /// You can use [startGesture] instead if your gesture begins with a down
  /// event.
1089
  Future<TestGesture> createGesture({
1090
    int? pointer,
1091 1092 1093 1094
    PointerDeviceKind kind = PointerDeviceKind.touch,
    int buttons = kPrimaryButton,
  }) async {
    return TestGesture(
1095
      dispatcher: sendEventToBinding,
1096 1097
      kind: kind,
      pointer: pointer ?? _getNextPointer(),
1098
      buttons: buttons,
1099
    );
1100 1101
  }

1102 1103 1104 1105 1106
  /// Creates a gesture with an initial appropriate starting gesture at a
  /// particular point, and returns the [TestGesture] object which you can use
  /// to continue the gesture. Usually, the starting gesture will be a down event,
  /// but if [kind] is set to [PointerDeviceKind.trackpad], the gesture will start
  /// with a panZoomStart gesture.
1107 1108
  ///
  /// You can use [createGesture] if your gesture doesn't begin with an initial
1109
  /// down or panZoomStart gesture.
1110 1111 1112 1113 1114 1115
  ///
  /// See also:
  ///  * [WidgetController.drag], a method to simulate a drag.
  ///  * [WidgetController.timedDrag], a method to simulate the drag of a given widget in a given duration.
  ///    It sends move events at a given frequency and it is useful when there are listeners involved.
  ///  * [WidgetController.fling], a method to simulate a fling.
1116 1117
  Future<TestGesture> startGesture(
    Offset downLocation, {
1118
    int? pointer,
1119
    PointerDeviceKind kind = PointerDeviceKind.touch,
1120
    int buttons = kPrimaryButton,
1121
  }) async {
1122 1123 1124 1125 1126
    final TestGesture result = await createGesture(
      pointer: pointer,
      kind: kind,
      buttons: buttons,
    );
1127 1128 1129 1130 1131
    if (kind == PointerDeviceKind.trackpad) {
      await result.panZoomStart(downLocation);
    } else {
      await result.down(downLocation);
    }
1132 1133 1134
    return result;
  }

1135
  /// Forwards the given location to the binding's hitTest logic.
1136
  HitTestResult hitTestOnBinding(Offset location) {
1137
    final HitTestResult result = HitTestResult();
1138 1139 1140 1141
    binding.hitTest(result, location);
    return result;
  }

1142
  /// Forwards the given pointer event to the binding.
1143
  Future<void> sendEventToBinding(PointerEvent event) {
1144
    return TestAsyncUtils.guard<void>(() async {
1145
      binding.handlePointerEvent(event);
1146
    });
1147 1148
  }

1149 1150 1151 1152 1153 1154 1155 1156 1157 1158
  /// Calls [debugPrint] with the given message.
  ///
  /// This is overridden by the WidgetTester subclass to use the test binding's
  /// [TestWidgetsFlutterBinding.debugPrintOverride], so that it appears on the
  /// console even if the test is logging output from the application.
  @protected
  void printToConsole(String message) {
    debugPrint(message);
  }

1159 1160 1161
  // GEOMETRY

  /// Returns the point at the center of the given widget.
1162 1163 1164 1165 1166 1167 1168 1169 1170 1171 1172 1173 1174 1175
  ///
  /// {@template flutter.flutter_test.WidgetController.getCenter.warnIfMissed}
  /// If `warnIfMissed` is true (the default is false), then the returned
  /// coordinate is checked to see if a hit test at the returned location would
  /// actually include the specified element in the [HitTestResult], and if not,
  /// a warning is printed to the console.
  ///
  /// The `callee` argument is used to identify the method that should be
  /// referenced in messages regarding `warnIfMissed`. It can be ignored unless
  /// this method is being called from another that is forwarding its own
  /// `warnIfMissed` parameter (see e.g. the implementation of [tap]).
  /// {@endtemplate}
  Offset getCenter(Finder finder, { bool warnIfMissed = false, String callee = 'getCenter' }) {
    return _getElementPoint(finder, (Size size) => size.center(Offset.zero), warnIfMissed: warnIfMissed, callee: callee);
1176 1177 1178
  }

  /// Returns the point at the top left of the given widget.
1179 1180 1181 1182
  ///
  /// {@macro flutter.flutter_test.WidgetController.getCenter.warnIfMissed}
  Offset getTopLeft(Finder finder, { bool warnIfMissed = false, String callee = 'getTopLeft' }) {
    return _getElementPoint(finder, (Size size) => Offset.zero, warnIfMissed: warnIfMissed, callee: callee);
1183 1184 1185 1186
  }

  /// Returns the point at the top right of the given widget. This
  /// point is not inside the object's hit test area.
1187 1188 1189 1190
  ///
  /// {@macro flutter.flutter_test.WidgetController.getCenter.warnIfMissed}
  Offset getTopRight(Finder finder, { bool warnIfMissed = false, String callee = 'getTopRight' }) {
    return _getElementPoint(finder, (Size size) => size.topRight(Offset.zero), warnIfMissed: warnIfMissed, callee: callee);
1191 1192 1193 1194
  }

  /// Returns the point at the bottom left of the given widget. This
  /// point is not inside the object's hit test area.
1195 1196 1197 1198
  ///
  /// {@macro flutter.flutter_test.WidgetController.getCenter.warnIfMissed}
  Offset getBottomLeft(Finder finder, { bool warnIfMissed = false, String callee = 'getBottomLeft' }) {
    return _getElementPoint(finder, (Size size) => size.bottomLeft(Offset.zero), warnIfMissed: warnIfMissed, callee: callee);
1199 1200 1201 1202
  }

  /// Returns the point at the bottom right of the given widget. This
  /// point is not inside the object's hit test area.
1203 1204 1205 1206
  ///
  /// {@macro flutter.flutter_test.WidgetController.getCenter.warnIfMissed}
  Offset getBottomRight(Finder finder, { bool warnIfMissed = false, String callee = 'getBottomRight' }) {
    return _getElementPoint(finder, (Size size) => size.bottomRight(Offset.zero), warnIfMissed: warnIfMissed, callee: callee);
1207 1208
  }

1209 1210 1211 1212 1213 1214 1215 1216 1217 1218 1219 1220 1221 1222 1223 1224 1225 1226 1227 1228 1229 1230 1231
  /// Whether warnings relating to hit tests not hitting their mark should be
  /// fatal (cause the test to fail).
  ///
  /// Some methods, e.g. [tap], have an argument `warnIfMissed` which causes a
  /// warning to be displayed if the specified [Finder] indicates a widget and
  /// location that, were a pointer event to be sent to that location, would not
  /// actually send any events to the widget (e.g. because the widget is
  /// obscured, or the location is off-screen, or the widget is transparent to
  /// pointer events).
  ///
  /// This warning was added in 2021. In ordinary operation this warning is
  /// non-fatal since making it fatal would be a significantly breaking change
  /// for anyone who already has tests relying on the ability to target events
  /// using finders where the events wouldn't reach the widgets specified by the
  /// finders in question.
  ///
  /// However, doing this is usually unintentional. To make the warning fatal,
  /// thus failing any tests where it occurs, this property can be set to true.
  ///
  /// Typically this is done using a `flutter_test_config.dart` file, as described
  /// in the documentation for the [flutter_test] library.
  static bool hitTestWarningShouldBeFatal = false;

1232
  Offset _getElementPoint(Finder finder, Offset Function(Size size) sizeToPoint, { required bool warnIfMissed, required String callee }) {
1233
    TestAsyncUtils.guardSync();
1234 1235 1236 1237 1238 1239 1240 1241 1242 1243 1244 1245 1246 1247 1248 1249 1250 1251
    final Iterable<Element> elements = finder.evaluate();
    if (elements.isEmpty) {
      throw FlutterError('The finder "$finder" (used in a call to "$callee()") could not find any matching widgets.');
    }
    if (elements.length > 1) {
      throw FlutterError('The finder "$finder" (used in a call to "$callee()") ambiguously found multiple matching widgets. The "$callee()" method needs a single target.');
    }
    final Element element = elements.single;
    final RenderObject? renderObject = element.renderObject;
    if (renderObject == null) {
      throw FlutterError(
        'The finder "$finder" (used in a call to "$callee()") found an element, but it does not have a corresponding render object. '
        'Maybe the element has not yet been rendered?'
      );
    }
    if (renderObject is! RenderBox) {
      throw FlutterError(
        'The finder "$finder" (used in a call to "$callee()") found an element whose corresponding render object is not a RenderBox (it is a ${renderObject.runtimeType}: "$renderObject"). '
1252
        'Unfortunately "$callee()" only supports targeting widgets that correspond to RenderBox objects in the rendering.'
1253 1254
      );
    }
1255
    final RenderBox box = element.renderObject! as RenderBox;
1256 1257 1258 1259 1260 1261 1262 1263 1264 1265 1266 1267 1268
    final Offset location = box.localToGlobal(sizeToPoint(box.size));
    if (warnIfMissed) {
      final HitTestResult result = HitTestResult();
      binding.hitTest(result, location);
      bool found = false;
      for (final HitTestEntry entry in result.path) {
        if (entry.target == box) {
          found = true;
          break;
        }
      }
      if (!found) {
        bool outOfBounds = false;
1269
        outOfBounds = !(Offset.zero & binding.renderView.size).contains(location);
1270 1271 1272 1273 1274 1275 1276 1277 1278 1279 1280 1281 1282 1283 1284 1285 1286 1287 1288 1289 1290 1291
        if (hitTestWarningShouldBeFatal) {
          throw FlutterError.fromParts(<DiagnosticsNode>[
            ErrorSummary('Finder specifies a widget that would not receive pointer events.'),
            ErrorDescription('A call to $callee() with finder "$finder" derived an Offset ($location) that would not hit test on the specified widget.'),
            ErrorHint('Maybe the widget is actually off-screen, or another widget is obscuring it, or the widget cannot receive pointer events.'),
            if (outOfBounds)
              ErrorHint('Indeed, $location is outside the bounds of the root of the render tree, ${binding.renderView.size}.'),
            box.toDiagnosticsNode(name: 'The finder corresponds to this RenderBox', style: DiagnosticsTreeStyle.singleLine),
            ErrorDescription('The hit test result at that offset is: $result'),
            ErrorDescription('If you expected this target not to be able to receive pointer events, pass "warnIfMissed: false" to "$callee()".'),
            ErrorDescription('To make this error into a non-fatal warning, set WidgetController.hitTestWarningShouldBeFatal to false.'),
          ]);
        }
        printToConsole(
          '\n'
          'Warning: A call to $callee() with finder "$finder" derived an Offset ($location) that would not hit test on the specified widget.\n'
          'Maybe the widget is actually off-screen, or another widget is obscuring it, or the widget cannot receive pointer events.\n'
          '${outOfBounds ? "Indeed, $location is outside the bounds of the root of the render tree, ${binding.renderView.size}.\n" : ""}'
          'The finder corresponds to this RenderBox: $box\n'
          'The hit test result at that offset is: $result\n'
          '${StackTrace.current}'
          'To silence this warning, pass "warnIfMissed: false" to "$callee()".\n'
1292
          'To make this warning fatal, set WidgetController.hitTestWarningShouldBeFatal to true.\n',
1293 1294 1295 1296
        );
      }
    }
    return location;
1297 1298 1299 1300 1301
  }

  /// Returns the size of the given widget. This is only valid once
  /// the widget's render object has been laid out at least once.
  Size getSize(Finder finder) {
1302
    TestAsyncUtils.guardSync();
1303
    final Element element = finder.evaluate().single;
1304
    final RenderBox box = element.renderObject! as RenderBox;
1305 1306
    return box.size;
  }
1307

1308
  /// Simulates sending physical key down and up events.
1309 1310 1311 1312 1313
  ///
  /// This only simulates key events coming from a physical keyboard, not from a
  /// soft keyboard.
  ///
  /// Specify `platform` as one of the platforms allowed in
1314 1315 1316 1317
  /// [platform.Platform.operatingSystem] to make the event appear to be from
  /// that type of system. Defaults to "web" on web, and "android" everywhere
  /// else. Must not be null. Some platforms (e.g. Windows, iOS) are not yet
  /// supported.
1318
  ///
1319 1320 1321 1322 1323 1324 1325 1326
  /// Specify the `physicalKey` for the event to override what is included in
  /// the simulated event. If not specified, it uses a default from the US
  /// keyboard layout for the corresponding logical `key`.
  ///
  /// Specify the `character` for the event to override what is included in the
  /// simulated event. If not specified, it uses a default derived from the
  /// logical `key`.
  ///
1327 1328 1329
  /// Whether the event is sent through [RawKeyEvent] or [KeyEvent] is
  /// controlled by [debugKeyEventSimulatorTransitModeOverride].
  ///
1330 1331 1332 1333 1334 1335
  /// Keys that are down when the test completes are cleared after each test.
  ///
  /// This method sends both the key down and the key up events, to simulate a
  /// key press. To simulate individual down and/or up events, see
  /// [sendKeyDownEvent] and [sendKeyUpEvent].
  ///
1336 1337
  /// Returns true if the key down event was handled by the framework.
  ///
1338 1339 1340 1341
  /// See also:
  ///
  ///  - [sendKeyDownEvent] to simulate only a key down event.
  ///  - [sendKeyUpEvent] to simulate only a key up event.
1342 1343 1344 1345 1346 1347 1348
  Future<bool> sendKeyEvent(
    LogicalKeyboardKey key, {
    String platform = _defaultPlatform,
    String? character,
    PhysicalKeyboardKey? physicalKey
  }) async {
    final bool handled = await simulateKeyDownEvent(key, platform: platform, character: character, physicalKey: physicalKey);
1349
    // Internally wrapped in async guard.
1350
    await simulateKeyUpEvent(key, platform: platform, physicalKey: physicalKey);
1351
    return handled;
1352 1353
  }

1354
  /// Simulates sending a physical key down event.
1355 1356 1357 1358 1359
  ///
  /// This only simulates key down events coming from a physical keyboard, not
  /// from a soft keyboard.
  ///
  /// Specify `platform` as one of the platforms allowed in
1360 1361 1362 1363
  /// [platform.Platform.operatingSystem] to make the event appear to be from
  /// that type of system. Defaults to "web" on web, and "android" everywhere
  /// else. Must not be null. Some platforms (e.g. Windows, iOS) are not yet
  /// supported.
1364
  ///
1365 1366 1367 1368 1369 1370 1371 1372
  /// Specify the `physicalKey` for the event to override what is included in
  /// the simulated event. If not specified, it uses a default from the US
  /// keyboard layout for the corresponding logical `key`.
  ///
  /// Specify the `character` for the event to override what is included in the
  /// simulated event. If not specified, it uses a default derived from the
  /// logical `key`.
  ///
1373 1374 1375
  /// Whether the event is sent through [RawKeyEvent] or [KeyEvent] is
  /// controlled by [debugKeyEventSimulatorTransitModeOverride].
  ///
1376 1377
  /// Keys that are down when the test completes are cleared after each test.
  ///
1378 1379
  /// Returns true if the key event was handled by the framework.
  ///
1380 1381
  /// See also:
  ///
1382 1383
  ///  - [sendKeyUpEvent] and [sendKeyRepeatEvent] to simulate the corresponding
  ///    key up and repeat event.
1384
  ///  - [sendKeyEvent] to simulate both the key up and key down in the same call.
1385 1386 1387 1388 1389 1390
  Future<bool> sendKeyDownEvent(
    LogicalKeyboardKey key, {
    String platform = _defaultPlatform,
    String? character,
    PhysicalKeyboardKey? physicalKey
  }) async {
1391
    // Internally wrapped in async guard.
1392
    return simulateKeyDownEvent(key, platform: platform, character: character, physicalKey: physicalKey);
1393 1394 1395 1396 1397 1398 1399 1400
  }

  /// Simulates sending a physical key up event through the system channel.
  ///
  /// This only simulates key up events coming from a physical keyboard,
  /// not from a soft keyboard.
  ///
  /// Specify `platform` as one of the platforms allowed in
1401 1402 1403
  /// [platform.Platform.operatingSystem] to make the event appear to be from
  /// that type of system. Defaults to "web" on web, and "android" everywhere
  /// else. May not be null.
1404
  ///
1405 1406 1407 1408
  /// Specify the `physicalKey` for the event to override what is included in
  /// the simulated event. If not specified, it uses a default from the US
  /// keyboard layout for the corresponding logical `key`.
  ///
1409 1410 1411
  /// Whether the event is sent through [RawKeyEvent] or [KeyEvent] is
  /// controlled by [debugKeyEventSimulatorTransitModeOverride].
  ///
1412 1413
  /// Returns true if the key event was handled by the framework.
  ///
1414 1415
  /// See also:
  ///
1416 1417
  ///  - [sendKeyDownEvent] and [sendKeyRepeatEvent] to simulate the
  ///    corresponding key down and repeat event.
1418
  ///  - [sendKeyEvent] to simulate both the key up and key down in the same call.
1419 1420 1421 1422 1423
  Future<bool> sendKeyUpEvent(
      LogicalKeyboardKey key, {
        String platform = _defaultPlatform,
        PhysicalKeyboardKey? physicalKey
      }) async {
1424
    // Internally wrapped in async guard.
1425
    return simulateKeyUpEvent(key, platform: platform, physicalKey: physicalKey);
1426 1427
  }

1428
  /// Simulates sending a key repeat event from a physical keyboard.
1429 1430 1431 1432 1433 1434 1435 1436 1437
  ///
  /// This only simulates key repeat events coming from a physical keyboard, not
  /// from a soft keyboard.
  ///
  /// Specify `platform` as one of the platforms allowed in
  /// [platform.Platform.operatingSystem] to make the event appear to be from that type
  /// of system. Defaults to "web" on web, and "android" everywhere else. Must not be
  /// null. Some platforms (e.g. Windows, iOS) are not yet supported.
  ///
1438 1439 1440 1441 1442 1443 1444 1445
  /// Specify the `physicalKey` for the event to override what is included in
  /// the simulated event. If not specified, it uses a default from the US
  /// keyboard layout for the corresponding logical `key`.
  ///
  /// Specify the `character` for the event to override what is included in the
  /// simulated event. If not specified, it uses a default derived from the
  /// logical `key`.
  ///
1446 1447 1448 1449 1450 1451 1452 1453 1454 1455 1456 1457 1458
  /// Whether the event is sent through [RawKeyEvent] or [KeyEvent] is
  /// controlled by [debugKeyEventSimulatorTransitModeOverride]. If through [RawKeyEvent],
  /// this method is equivalent to [sendKeyDownEvent].
  ///
  /// Keys that are down when the test completes are cleared after each test.
  ///
  /// Returns true if the key event was handled by the framework.
  ///
  /// See also:
  ///
  ///  - [sendKeyDownEvent] and [sendKeyUpEvent] to simulate the corresponding
  ///    key down and up event.
  ///  - [sendKeyEvent] to simulate both the key up and key down in the same call.
1459 1460 1461 1462 1463 1464
  Future<bool> sendKeyRepeatEvent(
      LogicalKeyboardKey key, {
        String platform = _defaultPlatform,
        String? character,
        PhysicalKeyboardKey? physicalKey
      }) async {
1465
    // Internally wrapped in async guard.
1466
    return simulateKeyRepeatEvent(key, platform: platform, character: character, physicalKey: physicalKey);
1467 1468
  }

1469 1470
  /// Returns the rect of the given widget. This is only valid once
  /// the widget's render object has been laid out at least once.
1471
  Rect getRect(Finder finder) => Rect.fromPoints(getTopLeft(finder), getBottomRight(finder));
1472

1473 1474 1475 1476 1477 1478 1479 1480 1481 1482 1483 1484 1485 1486
  /// Attempts to find the [SemanticsNode] of first result from `finder`.
  ///
  /// If the object identified by the finder doesn't own it's semantic node,
  /// this will return the semantics data of the first ancestor with semantics.
  /// The ancestor's semantic data will include the child's as well as
  /// other nodes that have been merged together.
  ///
  /// If the [SemanticsNode] of the object identified by the finder is
  /// force-merged into an ancestor (e.g. via the [MergeSemantics] widget)
  /// the node into which it is merged is returned. That node will include
  /// all the semantics information of the nodes merged into it.
  ///
  /// Will throw a [StateError] if the finder returns more than one element or
  /// if no semantics are found or are not enabled.
1487 1488
  // TODO(pdblasi-google): Deprecate this and point references to semantics.find. See https://github.com/flutter/flutter/issues/112670.
  SemanticsNode getSemantics(Finder finder) => semantics.find(finder);
1489 1490 1491 1492 1493 1494 1495 1496

  /// Enable semantics in a test by creating a [SemanticsHandle].
  ///
  /// The handle must be disposed at the end of the test.
  SemanticsHandle ensureSemantics() {
    return binding.pipelineOwner.ensureSemantics();
  }

1497 1498 1499
  /// Given a widget `W` specified by [finder] and a [Scrollable] widget `S` in
  /// its ancestry tree, this scrolls `S` so as to make `W` visible.
  ///
1500 1501 1502
  /// Usually the `finder` for this method should be labeled `skipOffstage:
  /// false`, so that the [Finder] deals with widgets that are off the screen
  /// correctly.
1503
  ///
1504 1505 1506
  /// This does not work when `S` is long enough, and `W` far away enough from
  /// the displayed part of `S`, that `S` has not yet cached `W`'s element.
  /// Consider using [scrollUntilVisible] in such a situation.
1507
  ///
1508 1509 1510 1511
  /// See also:
  ///
  ///  * [Scrollable.ensureVisible], which is the production API used to
  ///    implement this method.
1512
  Future<void> ensureVisible(Finder finder) => Scrollable.ensureVisible(element(finder));
1513

1514
  /// Repeatedly scrolls a [Scrollable] by `delta` in the
1515 1516
  /// [Scrollable.axisDirection] direction until a widget matching `finder` is
  /// visible.
1517
  ///
1518
  /// Between each scroll, advances the clock by `duration` time.
1519
  ///
1520 1521
  /// Scrolling is performed until the start of the `finder` is visible. This is
  /// due to the default parameter values of the [Scrollable.ensureVisible] method.
1522
  ///
1523 1524 1525 1526
  /// If `scrollable` is `null`, a [Finder] that looks for a [Scrollable] is
  /// used instead.
  ///
  /// Throws a [StateError] if `finder` is not found after `maxScrolls` scrolls.
1527 1528
  ///
  /// This is different from [ensureVisible] in that this allows looking for
1529
  /// `finder` that is not yet built. The caller must specify the scrollable
1530
  /// that will build child specified by `finder` when there are multiple
1531
  /// [Scrollable]s.
1532
  ///
1533
  /// See also:
1534
  ///
1535
  ///  * [dragUntilVisible], which implements the body of this method.
1536 1537 1538
  Future<void> scrollUntilVisible(
    Finder finder,
    double delta, {
1539
      Finder? scrollable,
1540 1541 1542 1543 1544
      int maxScrolls = 50,
      Duration duration = const Duration(milliseconds: 50),
    }
  ) {
    assert(maxScrolls > 0);
1545
    scrollable ??= find.byType(Scrollable);
1546 1547
    return TestAsyncUtils.guard<void>(() async {
      Offset moveStep;
1548
      switch (widget<Scrollable>(scrollable!).axisDirection) {
1549 1550 1551 1552 1553 1554 1555 1556 1557 1558 1559 1560 1561
        case AxisDirection.up:
          moveStep = Offset(0, delta);
          break;
        case AxisDirection.down:
          moveStep = Offset(0, -delta);
          break;
        case AxisDirection.left:
          moveStep = Offset(delta, 0);
          break;
        case AxisDirection.right:
          moveStep = Offset(-delta, 0);
          break;
      }
1562 1563 1564 1565 1566
      await dragUntilVisible(
        finder,
        scrollable,
        moveStep,
        maxIteration: maxScrolls,
1567 1568
        duration: duration,
      );
1569 1570 1571
    });
  }

1572 1573 1574
  /// Repeatedly drags `view` by `moveStep` until `finder` is visible.
  ///
  /// Between each drag, advances the clock by `duration`.
1575
  ///
1576 1577 1578 1579
  /// Throws a [StateError] if `finder` is not found after `maxIteration`
  /// drags.
  ///
  /// See also:
1580
  ///
1581 1582
  ///  * [scrollUntilVisible], which wraps this method with an API that is more
  ///    convenient when dealing with a [Scrollable].
1583 1584 1585 1586 1587 1588 1589 1590
  Future<void> dragUntilVisible(
    Finder finder,
    Finder view,
    Offset moveStep, {
      int maxIteration = 50,
      Duration duration = const Duration(milliseconds: 50),
  }) {
    return TestAsyncUtils.guard<void>(() async {
1591
      while (maxIteration > 0 && finder.evaluate().isEmpty) {
1592
        await drag(view, moveStep);
1593
        await pump(duration);
1594
        maxIteration -= 1;
1595 1596 1597 1598
      }
      await Scrollable.ensureVisible(element(finder));
    });
  }
1599
}
1600 1601 1602 1603 1604 1605 1606

/// Variant of [WidgetController] that can be used in tests running
/// on a device.
///
/// This is used, for instance, by [FlutterDriver].
class LiveWidgetController extends WidgetController {
  /// Creates a widget controller that uses the given binding.
1607
  LiveWidgetController(super.binding);
1608 1609

  @override
1610
  Future<void> pump([Duration? duration]) async {
1611
    if (duration != null) {
1612
      await Future<void>.delayed(duration);
1613
    }
1614 1615 1616
    binding.scheduleFrame();
    await binding.endOfFrame;
  }
1617

1618 1619 1620 1621 1622 1623 1624 1625 1626 1627 1628 1629 1630 1631 1632
  @override
  Future<int> pumpAndSettle([
    Duration duration = const Duration(milliseconds: 100),
  ]) {
    assert(duration > Duration.zero);
    return TestAsyncUtils.guard<int>(() async {
      int count = 0;
      do {
        await pump(duration);
        count += 1;
      } while (binding.hasScheduledFrame);
      return count;
    });
  }

1633 1634
  @override
  Future<List<Duration>> handlePointerEventRecord(List<PointerEventRecord> records) {
1635 1636 1637
    assert(records.isNotEmpty);
    return TestAsyncUtils.guard<List<Duration>>(() async {
      final List<Duration> handleTimeStampDiff = <Duration>[];
1638
      DateTime? startTime;
1639 1640 1641 1642 1643 1644 1645 1646 1647 1648
      for (final PointerEventRecord record in records) {
        final DateTime now = clock.now();
        startTime ??= now;
        // So that the first event is promised to receive a zero timeDiff
        final Duration timeDiff = record.timeDelay - now.difference(startTime);
        if (timeDiff.isNegative) {
          // This happens when something (e.g. GC) takes a long time during the
          // processing of the events.
          // Flush all past events
          handleTimeStampDiff.add(-timeDiff);
1649
          record.events.forEach(binding.handlePointerEvent);
1650 1651 1652 1653 1654 1655 1656 1657
        } else {
          await Future<void>.delayed(timeDiff);
          handleTimeStampDiff.add(
            // Recalculating the time diff for getting exact time when the event
            // packet is sent. For a perfect Future.delayed like the one in a
            // fake async this new diff should be zero.
            clock.now().difference(startTime) - record.timeDelay,
          );
1658
          record.events.forEach(binding.handlePointerEvent);
1659 1660
        }
      }
1661

1662 1663 1664
      return handleTimeStampDiff;
    });
  }
1665
}