controller.dart 72.8 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
import 'package:flutter/widgets.dart';

12
import 'event_simulation.dart';
13 14 15
import 'finders.dart';
import 'test_async_utils.dart';
import 'test_pointer.dart';
16
import 'tree_traversal.dart';
17
import 'window.dart';
18

19 20 21 22 23 24
/// 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
25 26
const String _defaultPlatform = kIsWeb ? 'web' : 'android';

27 28 29
// Examples can assume:
// typedef MyWidget = Placeholder;

Lioness100's avatar
Lioness100 committed
30
/// Class that programmatically interacts with the [Semantics] tree.
31 32 33 34 35 36 37 38 39 40 41
///
/// 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.
42
  SemanticsController._(this._controller);
43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60

  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;

61
  final WidgetController _controller;
62 63 64 65 66 67 68 69 70 71 72 73 74 75 76

  /// 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.
77
  SemanticsNode find(FinderBase<Element> finder) {
78
    TestAsyncUtils.guardSync();
79
    if (!_controller.binding.semanticsEnabled) {
80 81 82 83 84 85 86 87 88 89 90 91 92
      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)) {
93
      renderObject = renderObject.parent;
94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114
      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.
  ///
115 116 117 118 119 120 121
  /// If provided, the nodes for `end` and `start` must be part of the same
  /// semantics tree, i.e. they must be part of the same view.
  ///
  /// If neither `start` or `end` is provided, `view` can be provided to specify
  /// the semantics tree to traverse. If `view` is left unspecified,
  /// [WidgetTester.view] is traversed by default.
  ///
122 123 124 125 126 127 128
  /// 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
  ///
129
  /// ```dart
130
  /// testWidgets('MyWidget', (WidgetTester tester) async {
131
  ///   await tester.pumpWidget(const MyWidget());
132 133 134
  ///
  ///   expect(
  ///     tester.semantics.simulatedAccessibilityTraversal(),
135
  ///     containsAllInOrder(<Matcher>[
136 137 138 139 140 141 142 143 144 145
  ///       containsSemantics(label: 'My Widget'),
  ///       containsSemantics(label: 'is awesome!', isChecked: true),
  ///     ]),
  ///   );
  /// });
  /// ```
  ///
  /// See also:
  ///
  /// * [containsSemantics] and [matchesSemantics], which can be used to match
146
  ///   against a single node in the traversal.
147 148
  /// * [containsAllInOrder], which can be given an [Iterable<Matcher>] to fuzzy
  ///   match the order allowing extra nodes before after and between matching
149
  ///   parts of the traversal.
150
  /// * [orderedEquals], which can be given an [Iterable<Matcher>] to exactly
151
  ///   match the order of the traversal.
152
  Iterable<SemanticsNode> simulatedAccessibilityTraversal({FinderBase<Element>? start, FinderBase<Element>? end, FlutterView? view}) {
153
    TestAsyncUtils.guardSync();
154 155 156 157 158 159 160
    FlutterView? startView;
    FlutterView? endView;
    if (start != null) {
      startView = _controller.viewOf(start);
      if (view != null && startView != view) {
        throw StateError(
          'The start node is not part of the provided view.\n'
161
          'Finder: ${start.toString(describeSelf: true)}\n'
162 163 164 165 166 167 168 169 170 171
          'View of start node: $startView\n'
          'Specified view: $view'
        );
      }
    }
    if (end != null) {
      endView = _controller.viewOf(end);
      if (view != null && endView != view) {
        throw StateError(
          'The end node is not part of the provided view.\n'
172
          'Finder: ${end.toString(describeSelf: true)}\n'
173 174 175 176 177 178 179 180
          'View of end node: $endView\n'
          'Specified view: $view'
        );
      }
    }
    if (endView != null && startView != null && endView != startView) {
      throw StateError(
        'The start and end node are in different views.\n'
181 182
        'Start finder: ${start!.toString(describeSelf: true)}\n'
        'End finder: ${end!.toString(describeSelf: true)}\n'
183 184 185 186 187 188 189 190
        'View of start node: $startView\n'
        'View of end node: $endView'
      );
    }

    final FlutterView actualView = view ?? startView ?? endView ?? _controller.view;
    final RenderView renderView = _controller.binding.renderViews.firstWhere((RenderView r) => r.flutterView == actualView);

191
    final List<SemanticsNode> traversal = <SemanticsNode>[];
192
    _traverse(renderView.owner!.semanticsOwner!.rootSemanticsNode!, traversal);
193 194 195 196 197 198 199 200 201 202

    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'
203
          'Finder: ${start.toString(describeSelf: true)}\n\n'
204 205 206 207 208 209 210 211 212 213 214
          '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'
215
          'Finder: ${end.toString(describeSelf: true)}\n\n'
216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237
          '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
Lioness100's avatar
Lioness100 committed
238
  /// on the platforms, but certain edge cases will be inconsistent.
239 240 241 242 243 244
  ///
  /// 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) {
245 246 247 248 249 250
    if (node.isMergedIntoParent) {
      // If this node is merged, all its information are present on an ancestor
      // node.
      return false;
    }
    final SemanticsData data = node.getSemanticsData();
251 252
    // 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.
253
    if (data.hasFlag(SemanticsFlag.scopesRoute)) {
254 255 256
      return false;
    }

257
    final bool hasNonScrollingAction = data.actions & ~_scrollingActions != 0;
258 259 260 261
    if (hasNonScrollingAction) {
      return true;
    }

262
    final bool hasImportantFlag = data.flags & _importantFlagsForAccessibility != 0;
263 264 265 266
    if (hasImportantFlag) {
      return true;
    }

267
    final bool hasContent = data.label.isNotEmpty || data.value.isNotEmpty || data.hint.isNotEmpty;
268 269 270 271 272 273 274 275
    if (hasContent) {
      return true;
    }

    return false;
  }
}

276 277
/// Class that programmatically interacts with widgets.
///
278 279 280 281 282 283
/// 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 {
284
  /// Creates a widget controller that uses the given binding.
285
  WidgetController(this.binding);
286

287
  /// A reference to the current instance of the binding.
288 289
  final WidgetsBinding binding;

290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306
  /// The [TestPlatformDispatcher] that is being used in this test.
  ///
  /// This will be injected into the framework such that calls to
  /// [WidgetsBinding.platformDispatcher] will use this. This allows
  /// users to change platform specific properties for testing.
  ///
  /// See also:
  ///
  ///   * [TestFlutterView] which allows changing view specific properties
  ///     for testing
  ///   * [view] and [viewOf] which are used to find
  ///     [TestFlutterView]s from the widget tree
  TestPlatformDispatcher get platformDispatcher => binding.platformDispatcher as TestPlatformDispatcher;

  /// The [TestFlutterView] provided by default when testing with
  /// [WidgetTester.pumpWidget].
  ///
307 308 309 310
  /// If the test uses multiple views, this will return the view that is painted
  /// into by [WidgetTester.pumpWidget]. If a different view needs to be
  /// accessed use [viewOf] to ensure that the view related to the widget being
  /// evaluated is the one that gets updated.
311 312 313 314 315 316
  ///
  /// See also:
  ///
  ///   * [viewOf], which can find a [TestFlutterView] related to a given finder.
  ///     This is how to modify view properties for testing when dealing with
  ///     multiple views.
317
  TestFlutterView get view => platformDispatcher.implicitView!;
318

319 320 321 322 323 324 325
  /// 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 {
326
    if (!binding.semanticsEnabled) {
327 328 329 330 331 332 333 334
      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;
  }
335
  late final SemanticsController _semantics = SemanticsController._(this);
336

337 338 339 340 341
  // 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

342 343 344 345 346 347 348 349 350
  /// Finds the [TestFlutterView] that is the closest ancestor of the widget
  /// found by [finder].
  ///
  /// [TestFlutterView] can be used to modify view specific properties for testing.
  ///
  /// See also:
  ///
  ///   * [view] which returns the [TestFlutterView] used when only a single
  ///     view is being used.
351
  TestFlutterView viewOf(FinderBase<Element> finder) {
352 353 354
    return _viewOf(finder) as TestFlutterView;
  }

355
  FlutterView _viewOf(FinderBase<Element> finder) {
356
    return firstWidget<View>(
357 358 359
      find.ancestor(
        of: finder,
        matching: find.byType(View),
360 361
      ),
    ).view;
362 363
  }

364
  /// Checks if `finder` exists in the tree.
365
  bool any(FinderBase<Element> finder) {
366 367 368
    TestAsyncUtils.guardSync();
    return finder.evaluate().isNotEmpty;
  }
369 370 371 372 373 374

  /// 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 {
375
    TestAsyncUtils.guardSync();
376
    return allElements.map<Widget>((Element element) => element.widget);
377 378 379 380 381 382
  }

  /// The matching widget in the widget tree.
  ///
  /// Throws a [StateError] if `finder` is empty or matches more than
  /// one widget.
383 384 385
  ///
  /// * 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.
386
  T widget<T extends Widget>(FinderBase<Element> finder) {
387
    TestAsyncUtils.guardSync();
388
    return finder.evaluate().single.widget as T;
389 390 391 392 393 394
  }

  /// The first matching widget according to a depth-first pre-order
  /// traversal of the widget tree.
  ///
  /// Throws a [StateError] if `finder` is empty.
395 396
  ///
  /// * Use [widget] if you only expect to match one widget.
397
  T firstWidget<T extends Widget>(FinderBase<Element> finder) {
398
    TestAsyncUtils.guardSync();
399
    return finder.evaluate().first.widget as T;
400 401
  }

402 403 404 405
  /// 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.
406
  Iterable<T> widgetList<T extends Widget>(FinderBase<Element> finder) {
407
    TestAsyncUtils.guardSync();
408
    return finder.evaluate().map<T>((Element element) {
409
      final T result = element.widget as T;
410 411
      return result;
    });
412 413
  }

414 415 416
  /// Find all layers that are children of the provided [finder].
  ///
  /// The [finder] must match exactly one element.
417
  Iterable<Layer> layerListOf(FinderBase<Element> finder) {
418 419 420 421 422
    TestAsyncUtils.guardSync();
    final Element element = finder.evaluate().single;
    final RenderObject object = element.renderObject!;
    RenderObject current = object;
    while (current.debugLayer == null) {
423
      current = current.parent!;
424 425 426 427 428
    }
    final ContainerLayer layer = current.debugLayer!;
    return _walkLayers(layer);
  }

429 430 431 432 433 434
  /// 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 {
435
    TestAsyncUtils.guardSync();
436
    return collectAllElementsFrom(binding.rootElement!, skipOffstage: false);
437 438 439 440 441 442
  }

  /// The matching element in the widget tree.
  ///
  /// Throws a [StateError] if `finder` is empty or matches more than
  /// one element.
443 444 445
  ///
  /// * 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.
446
  T element<T extends Element>(FinderBase<Element> finder) {
447
    TestAsyncUtils.guardSync();
448
    return finder.evaluate().single as T;
449 450 451 452 453 454
  }

  /// The first matching element according to a depth-first pre-order
  /// traversal of the widget tree.
  ///
  /// Throws a [StateError] if `finder` is empty.
455 456
  ///
  /// * Use [element] if you only expect to match one element.
457
  T firstElement<T extends Element>(FinderBase<Element> finder) {
458
    TestAsyncUtils.guardSync();
459
    return finder.evaluate().first as T;
460 461
  }

462 463 464 465
  /// 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.
466
  Iterable<T> elementList<T extends Element>(FinderBase<Element> finder) {
467
    TestAsyncUtils.guardSync();
468
    return finder.evaluate().cast<T>();
469 470
  }

471 472 473 474 475 476
  /// 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 {
477
    TestAsyncUtils.guardSync();
478
    return allElements.whereType<StatefulElement>().map<State>((StatefulElement element) => element.state);
479 480 481 482 483 484
  }

  /// 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.
485 486 487
  ///
  /// * 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.
488
  T state<T extends State>(FinderBase<Element> finder) {
489
    TestAsyncUtils.guardSync();
490
    return _stateOf<T>(finder.evaluate().single, finder);
491 492 493 494 495 496 497
  }

  /// 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.
498 499
  ///
  /// * Use [state] if you only expect to match one state.
500
  T firstState<T extends State>(FinderBase<Element> finder) {
501
    TestAsyncUtils.guardSync();
502
    return _stateOf<T>(finder.evaluate().first, finder);
503 504
  }

505 506 507 508 509 510 511
  /// 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.
512
  Iterable<T> stateList<T extends State>(FinderBase<Element> finder) {
513
    TestAsyncUtils.guardSync();
514
    return finder.evaluate().map<T>((Element element) => _stateOf<T>(element, finder));
515 516
  }

517
  T _stateOf<T extends State>(Element element, FinderBase<Element> finder) {
518
    TestAsyncUtils.guardSync();
519
    if (element is StatefulElement) {
520
      return element.state as T;
521
    }
522
    throw StateError('Widget of type ${element.widget.runtimeType}, with ${finder.describeMatch(Plurality.many)}, is not a StatefulWidget.');
523 524 525 526 527 528 529 530 531 532
  }

  /// 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 {
533
    TestAsyncUtils.guardSync();
534
    return allElements.map<RenderObject>((Element element) => element.renderObject!);
535 536 537 538 539 540
  }

  /// 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).
541 542 543
  ///
  /// * 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.
544
  T renderObject<T extends RenderObject>(FinderBase<Element> finder) {
545
    TestAsyncUtils.guardSync();
546
    return finder.evaluate().single.renderObject! as T;
547 548 549 550 551 552
  }

  /// 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.
553 554
  ///
  /// * Use [renderObject] if you only expect to match one render object.
555
  T firstRenderObject<T extends RenderObject>(FinderBase<Element> finder) {
556
    TestAsyncUtils.guardSync();
557
    return finder.evaluate().first.renderObject! as T;
558 559
  }

560 561 562 563
  /// 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.
564
  Iterable<T> renderObjectList<T extends RenderObject>(FinderBase<Element> finder) {
565
    TestAsyncUtils.guardSync();
566
    return finder.evaluate().map<T>((Element element) {
567
      final T result = element.renderObject! as T;
568 569
      return result;
    });
570 571
  }

572
  /// Returns a list of all the [Layer] objects in the rendering.
573 574 575 576 577 578
  List<Layer> get layers {
    return <Layer>[
      for (final RenderView renderView in binding.renderViews)
        ..._walkLayers(renderView.debugLayer!)
    ];
  }
579
  Iterable<Layer> _walkLayers(Layer layer) sync* {
580
    TestAsyncUtils.guardSync();
581 582
    yield layer;
    if (layer is ContainerLayer) {
583
      final ContainerLayer root = layer;
584
      Layer? child = root.firstChild;
585 586 587 588 589 590 591 592 593 594
      while (child != null) {
        yield* _walkLayers(child);
        child = child.nextSibling;
      }
    }
  }

  // INTERACTION

  /// Dispatch a pointer down / pointer up sequence at the center of
595 596
  /// the given widget, assuming it is exposed.
  ///
597 598 599 600 601 602 603 604 605 606 607 608 609 610 611
  /// {@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.
612
  Future<void> tap(FinderBase<Element> finder, {int? pointer, int buttons = kPrimaryButton, bool warnIfMissed = true}) {
613
    return tapAt(getCenter(finder, warnIfMissed: warnIfMissed, callee: 'tap'), pointer: pointer, buttons: buttons);
614 615
  }

616
  /// Dispatch a pointer down / pointer up sequence at the given location.
617
  Future<void> tapAt(Offset location, {int? pointer, int buttons = kPrimaryButton}) {
618
    return TestAsyncUtils.guard<void>(() async {
619
      final TestGesture gesture = await startGesture(location, pointer: pointer, buttons: buttons);
620 621
      await gesture.up();
    });
622 623
  }

624 625 626
  /// Dispatch a pointer down at the center of the given widget, assuming it is
  /// exposed.
  ///
627 628 629 630 631 632 633 634 635 636
  /// {@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.
637
  Future<TestGesture> press(FinderBase<Element> finder, {int? pointer, int buttons = kPrimaryButton, bool warnIfMissed = true}) {
638
    return TestAsyncUtils.guard<TestGesture>(() {
639
      return startGesture(getCenter(finder, warnIfMissed: warnIfMissed, callee: 'press'), pointer: pointer, buttons: buttons);
640 641 642
    });
  }

643 644
  /// Dispatch a pointer down / pointer up sequence (with a delay of
  /// [kLongPressTimeout] + [kPressTimeout] between the two events) at the
645 646
  /// center of the given widget, assuming it is exposed.
  ///
647 648 649 650 651 652 653 654
  /// {@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.
655
  Future<void> longPress(FinderBase<Element> finder, {int? pointer, int buttons = kPrimaryButton, bool warnIfMissed = true}) {
656
    return longPressAt(getCenter(finder, warnIfMissed: warnIfMissed, callee: 'longPress'), pointer: pointer, buttons: buttons);
657 658 659 660
  }

  /// Dispatch a pointer down / pointer up sequence at the given location with
  /// a delay of [kLongPressTimeout] + [kPressTimeout] between the two events.
661
  Future<void> longPressAt(Offset location, {int? pointer, int buttons = kPrimaryButton}) {
662
    return TestAsyncUtils.guard<void>(() async {
663
      final TestGesture gesture = await startGesture(location, pointer: pointer, buttons: buttons);
664 665 666 667 668
      await pump(kLongPressTimeout + kPressTimeout);
      await gesture.up();
    });
  }

669
  /// Attempts a fling gesture starting from the center of the given
670
  /// widget, moving the given distance, reaching the given speed.
671
  ///
672
  /// {@macro flutter.flutter_test.WidgetController.tap.warnIfMissed}
673
  ///
674 675 676 677 678 679 680 681 682 683
  /// {@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}
  ///
684
  /// {@template flutter.flutter_test.WidgetController.fling}
685 686 687
  /// This can pump frames.
  ///
  /// Exactly 50 pointer events are synthesized.
688 689 690
  ///
  /// The `speed` is in pixels per second in the direction given by `offset`.
  ///
691 692 693 694 695 696 697 698 699 700 701 702 703
  /// 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
704 705 706 707 708 709 710
  ///
  /// 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).
711 712 713 714
  /// {@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].
715
  Future<void> fling(
716
    FinderBase<Element> finder,
717 718
    Offset offset,
    double speed, {
719
    int? pointer,
720
    int buttons = kPrimaryButton,
721 722 723
    Duration frameInterval = const Duration(milliseconds: 16),
    Offset initialOffset = Offset.zero,
    Duration initialOffsetDelay = const Duration(seconds: 1),
724
    bool warnIfMissed = true,
725
    PointerDeviceKind deviceKind = PointerDeviceKind.touch,
726
  }) {
Ian Hickson's avatar
Ian Hickson committed
727
    return flingFrom(
728
      getCenter(finder, warnIfMissed: warnIfMissed, callee: 'fling'),
Ian Hickson's avatar
Ian Hickson committed
729 730 731
      offset,
      speed,
      pointer: pointer,
732
      buttons: buttons,
Ian Hickson's avatar
Ian Hickson committed
733 734 735
      frameInterval: frameInterval,
      initialOffset: initialOffset,
      initialOffsetDelay: initialOffsetDelay,
736
      deviceKind: deviceKind,
Ian Hickson's avatar
Ian Hickson committed
737
    );
738 739
  }

740 741
  /// Attempts a fling gesture starting from the given location, moving the
  /// given distance, reaching the given speed.
742
  ///
743
  /// {@macro flutter.flutter_test.WidgetController.fling}
744 745 746
  ///
  /// 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].
747 748 749 750
  Future<void> flingFrom(
    Offset startLocation,
    Offset offset,
    double speed, {
751
    int? pointer,
752
    int buttons = kPrimaryButton,
753 754 755
    Duration frameInterval = const Duration(milliseconds: 16),
    Offset initialOffset = Offset.zero,
    Duration initialOffsetDelay = const Duration(seconds: 1),
756
    PointerDeviceKind deviceKind = PointerDeviceKind.touch,
Ian Hickson's avatar
Ian Hickson committed
757
  }) {
758
    assert(offset.distance > 0.0);
759
    assert(speed > 0.0); // speed is pixels/second
760
    return TestAsyncUtils.guard<void>(() async {
761
      final TestPointer testPointer = TestPointer(pointer ?? _getNextPointer(), deviceKind, null, buttons);
762
      const int kMoveCount = 50; // Needs to be >= kHistorySize, see _LeastSquaresVelocityTrackerStrategy
763
      final double timeStampDelta = 1000000.0 * offset.distance / (kMoveCount * speed);
764
      double timeStamp = 0.0;
765
      double lastTimeStamp = timeStamp;
766
      await sendEventToBinding(testPointer.down(startLocation, timeStamp: Duration(microseconds: timeStamp.round())));
Ian Hickson's avatar
Ian Hickson committed
767
      if (initialOffset.distance > 0.0) {
768
        await sendEventToBinding(testPointer.move(startLocation + initialOffset, timeStamp: Duration(microseconds: timeStamp.round())));
769
        timeStamp += initialOffsetDelay.inMicroseconds;
Ian Hickson's avatar
Ian Hickson committed
770 771
        await pump(initialOffsetDelay);
      }
772
      for (int i = 0; i <= kMoveCount; i += 1) {
773
        final Offset location = startLocation + initialOffset + Offset.lerp(Offset.zero, offset, i / kMoveCount)!;
774
        await sendEventToBinding(testPointer.move(location, timeStamp: Duration(microseconds: timeStamp.round())));
775
        timeStamp += timeStampDelta;
776 777
        if (timeStamp - lastTimeStamp > frameInterval.inMicroseconds) {
          await pump(Duration(microseconds: (timeStamp - lastTimeStamp).truncate()));
778 779
          lastTimeStamp = timeStamp;
        }
780
      }
781
      await sendEventToBinding(testPointer.up(timeStamp: Duration(microseconds: timeStamp.round())));
782
    });
783 784
  }

785 786 787 788 789 790 791 792 793 794 795
  /// 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(
796
    FinderBase<Element> finder,
797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862
    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())));
    });
  }

863 864 865 866 867 868
  /// 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.
  ///
869 870 871 872 873 874 875
  /// 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,
876
  /// but a very small portion may have a tiny negative value for about tens of
877 878 879
  /// 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
880 881 882 883 884
  /// `records`.
  ///
  /// See [PointerEventRecord].
  Future<List<Duration>> handlePointerEventRecord(List<PointerEventRecord> records);

885 886 887 888 889
  /// 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.
890 891 892 893
  ///
  /// This is invoked by [flingFrom], for instance, so that the sequence of
  /// pointer events occurs over time.
  ///
894
  /// The [WidgetTester] subclass implements this by deferring to the [binding].
895
  ///
896 897 898 899
  /// See also:
  ///
  ///  * [SchedulerBinding.endOfFrame], which returns a future that could be
  ///    appropriate to return in the implementation of this method.
900
  Future<void> pump([Duration duration]);
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
  /// 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),
  ]);

930 931 932
  /// Attempts to drag the given widget by the given offset, by
  /// starting a drag in the middle of the widget.
  ///
933
  /// {@macro flutter.flutter_test.WidgetController.tap.warnIfMissed}
934 935 936
  ///
  /// 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.
937
  ///
938 939 940
  /// The operation happens at once. If you want the drag to last for a period
  /// of time, consider using [timedDrag].
  ///
941 942
  /// {@macro flutter.flutter_test.WidgetController.fling.offset}
  ///
943
  /// {@template flutter.flutter_test.WidgetController.drag}
944 945 946 947 948 949
  /// 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.
950 951 952 953 954 955
  ///
  /// 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.
  ///
956 957
  /// 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
958
  /// should be left to their default values.
959
  /// {@endtemplate}
960
  Future<void> drag(
961
    FinderBase<Element> finder,
962
    Offset offset, {
963
    int? pointer,
964 965 966
    int buttons = kPrimaryButton,
    double touchSlopX = kDragSlopDefault,
    double touchSlopY = kDragSlopDefault,
967
    bool warnIfMissed = true,
968
    PointerDeviceKind kind = PointerDeviceKind.touch,
969 970
  }) {
    return dragFrom(
971
      getCenter(finder, warnIfMissed: warnIfMissed, callee: 'drag'),
972 973 974 975 976
      offset,
      pointer: pointer,
      buttons: buttons,
      touchSlopX: touchSlopX,
      touchSlopY: touchSlopY,
977
      kind: kind,
978
    );
979 980 981 982
  }

  /// Attempts a drag gesture consisting of a pointer down, a move by
  /// the given offset, and a pointer up.
983 984 985 986
  ///
  /// 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.
987
  ///
988 989 990
  /// The operation happens at once. If you want the drag to last for a period
  /// of time, consider using [timedDragFrom].
  ///
991
  /// {@macro flutter.flutter_test.WidgetController.drag}
992 993 994
  Future<void> dragFrom(
    Offset startLocation,
    Offset offset, {
995
    int? pointer,
996 997 998
    int buttons = kPrimaryButton,
    double touchSlopX = kDragSlopDefault,
    double touchSlopY = kDragSlopDefault,
999
    PointerDeviceKind kind = PointerDeviceKind.touch,
1000
  }) {
1001
    assert(kDragSlopDefault > kTouchSlop);
1002
    return TestAsyncUtils.guard<void>(() async {
1003
      final TestGesture gesture = await startGesture(startLocation, pointer: pointer, buttons: buttons, kind: kind);
1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019 1020 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 1050 1051 1052 1053 1054 1055 1056 1057 1058 1059 1060 1061 1062 1063 1064 1065 1066 1067 1068

      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);
      }
1069 1070
      await gesture.up();
    });
1071 1072
  }

1073 1074 1075
  /// Attempts to drag the given widget by the given offset in the `duration`
  /// time, starting in the middle of the widget.
  ///
1076
  /// {@macro flutter.flutter_test.WidgetController.tap.warnIfMissed}
1077
  ///
1078 1079
  /// {@macro flutter.flutter_test.WidgetController.fling.offset}
  ///
1080 1081 1082 1083
  /// 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`.
  ///
1084
  /// {@template flutter.flutter_test.WidgetController.timedDrag}
1085 1086 1087 1088 1089 1090 1091 1092 1093
  /// 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(
1094
    FinderBase<Element> finder,
1095 1096
    Offset offset,
    Duration duration, {
1097
    int? pointer,
1098 1099
    int buttons = kPrimaryButton,
    double frequency = 60.0,
1100
    bool warnIfMissed = true,
1101 1102
  }) {
    return timedDragFrom(
1103
      getCenter(finder, warnIfMissed: warnIfMissed, callee: 'timedDrag'),
1104 1105 1106 1107 1108 1109 1110 1111 1112 1113 1114 1115 1116 1117 1118
      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`.
  ///
1119
  /// {@macro flutter.flutter_test.WidgetController.timedDrag}
1120 1121 1122 1123
  Future<void> timedDragFrom(
    Offset startLocation,
    Offset offset,
    Duration duration, {
1124
    int? pointer,
1125 1126 1127 1128 1129 1130
    int buttons = kPrimaryButton,
    double frequency = 60.0,
  }) {
    assert(frequency > 0);
    final int intervals = duration.inMicroseconds * frequency ~/ 1E6;
    assert(intervals > 1);
1131
    pointer ??= _getNextPointer();
1132 1133 1134 1135 1136 1137 1138 1139 1140 1141 1142 1143 1144 1145 1146 1147 1148 1149 1150 1151 1152
    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>[
1153
        for (int t = 0; t <= intervals; t += 1)
1154 1155 1156 1157 1158 1159 1160
          PointerEventRecord(timeStamps[t], <PointerEvent>[
            PointerMoveEvent(
              timeStamp: timeStamps[t],
              position: offsets[t+1],
              delta: offsets[t+1] - offsets[t],
              pointer: pointer,
              buttons: buttons,
1161
            ),
1162 1163 1164 1165 1166 1167 1168
          ]),
      ],
      PointerEventRecord(duration, <PointerEvent>[
        PointerUpEvent(
          timeStamp: duration,
          position: offsets.last,
          pointer: pointer,
1169 1170
          // The PointerData received from the engine with
          // change = PointerChange.up, which translates to PointerUpEvent,
1171 1172
          // doesn't provide the button field.
          // buttons: buttons,
1173
        ),
1174 1175 1176
      ]),
    ];
    return TestAsyncUtils.guard<void>(() async {
1177
      await handlePointerEventRecord(records);
1178 1179 1180
    });
  }

1181 1182 1183 1184
  /// 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.
1185
  int get nextPointer => _nextPointer;
1186

1187 1188 1189 1190 1191
  static int _nextPointer = 1;

  static int _getNextPointer() {
    final int result = _nextPointer;
    _nextPointer += 1;
1192 1193 1194
    return result;
  }

1195 1196 1197 1198 1199 1200 1201 1202 1203 1204 1205 1206 1207
  TestGesture _createGesture({
    int? pointer,
    required PointerDeviceKind kind,
    required int buttons,
  }) {
    return TestGesture(
      dispatcher: sendEventToBinding,
      kind: kind,
      pointer: pointer ?? _getNextPointer(),
      buttons: buttons,
    );
  }

1208 1209 1210 1211 1212
  /// 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.
1213
  Future<TestGesture> createGesture({
1214
    int? pointer,
1215 1216 1217
    PointerDeviceKind kind = PointerDeviceKind.touch,
    int buttons = kPrimaryButton,
  }) async {
1218
    return _createGesture(pointer: pointer, kind: kind, buttons: buttons);
1219 1220
  }

1221 1222 1223 1224 1225
  /// 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.
1226 1227
  ///
  /// You can use [createGesture] if your gesture doesn't begin with an initial
1228
  /// down or panZoomStart gesture.
1229 1230 1231
  ///
  /// See also:
  ///  * [WidgetController.drag], a method to simulate a drag.
1232 1233 1234
  ///  * [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.
1235
  ///  * [WidgetController.fling], a method to simulate a fling.
1236 1237
  Future<TestGesture> startGesture(
    Offset downLocation, {
1238
    int? pointer,
1239
    PointerDeviceKind kind = PointerDeviceKind.touch,
1240
    int buttons = kPrimaryButton,
1241
  }) async {
1242
    final TestGesture result = _createGesture(pointer: pointer, kind: kind, buttons: buttons);
1243 1244 1245 1246 1247
    if (kind == PointerDeviceKind.trackpad) {
      await result.panZoomStart(downLocation);
    } else {
      await result.down(downLocation);
    }
1248 1249 1250
    return result;
  }

1251
  /// Forwards the given location to the binding's hitTest logic.
1252 1253
  HitTestResult hitTestOnBinding(Offset location, { int? viewId }) {
    viewId ??= view.viewId;
1254
    final HitTestResult result = HitTestResult();
1255
    binding.hitTestInView(result, location, viewId);
1256 1257 1258
    return result;
  }

1259
  /// Forwards the given pointer event to the binding.
1260
  Future<void> sendEventToBinding(PointerEvent event) {
1261
    return TestAsyncUtils.guard<void>(() async {
1262
      binding.handlePointerEvent(event);
1263
    });
1264 1265
  }

1266 1267 1268 1269 1270 1271 1272 1273 1274 1275
  /// 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);
  }

1276 1277 1278
  // GEOMETRY

  /// Returns the point at the center of the given widget.
1279 1280 1281 1282 1283 1284 1285 1286 1287 1288 1289 1290
  ///
  /// {@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}
1291
  Offset getCenter(FinderBase<Element> finder, { bool warnIfMissed = false, String callee = 'getCenter' }) {
1292
    return _getElementPoint(finder, (Size size) => size.center(Offset.zero), warnIfMissed: warnIfMissed, callee: callee);
1293 1294 1295
  }

  /// Returns the point at the top left of the given widget.
1296 1297
  ///
  /// {@macro flutter.flutter_test.WidgetController.getCenter.warnIfMissed}
1298
  Offset getTopLeft(FinderBase<Element> finder, { bool warnIfMissed = false, String callee = 'getTopLeft' }) {
1299
    return _getElementPoint(finder, (Size size) => Offset.zero, warnIfMissed: warnIfMissed, callee: callee);
1300 1301 1302 1303
  }

  /// Returns the point at the top right of the given widget. This
  /// point is not inside the object's hit test area.
1304 1305
  ///
  /// {@macro flutter.flutter_test.WidgetController.getCenter.warnIfMissed}
1306
  Offset getTopRight(FinderBase<Element> finder, { bool warnIfMissed = false, String callee = 'getTopRight' }) {
1307
    return _getElementPoint(finder, (Size size) => size.topRight(Offset.zero), warnIfMissed: warnIfMissed, callee: callee);
1308 1309 1310 1311
  }

  /// Returns the point at the bottom left of the given widget. This
  /// point is not inside the object's hit test area.
1312 1313
  ///
  /// {@macro flutter.flutter_test.WidgetController.getCenter.warnIfMissed}
1314
  Offset getBottomLeft(FinderBase<Element> finder, { bool warnIfMissed = false, String callee = 'getBottomLeft' }) {
1315
    return _getElementPoint(finder, (Size size) => size.bottomLeft(Offset.zero), warnIfMissed: warnIfMissed, callee: callee);
1316 1317 1318 1319
  }

  /// Returns the point at the bottom right of the given widget. This
  /// point is not inside the object's hit test area.
1320 1321
  ///
  /// {@macro flutter.flutter_test.WidgetController.getCenter.warnIfMissed}
1322
  Offset getBottomRight(FinderBase<Element> finder, { bool warnIfMissed = false, String callee = 'getBottomRight' }) {
1323
    return _getElementPoint(finder, (Size size) => size.bottomRight(Offset.zero), warnIfMissed: warnIfMissed, callee: callee);
1324 1325
  }

1326 1327 1328 1329 1330 1331 1332 1333 1334 1335 1336 1337 1338 1339 1340 1341 1342 1343 1344 1345 1346 1347 1348
  /// 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;

1349
  Offset _getElementPoint(FinderBase<Element> finder, Offset Function(Size size) sizeToPoint, { required bool warnIfMissed, required String callee }) {
1350
    TestAsyncUtils.guardSync();
1351 1352 1353 1354 1355 1356 1357 1358 1359 1360 1361 1362 1363 1364 1365 1366 1367 1368
    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"). '
1369
        'Unfortunately "$callee()" only supports targeting widgets that correspond to RenderBox objects in the rendering.'
1370 1371
      );
    }
1372
    final RenderBox box = element.renderObject! as RenderBox;
1373 1374
    final Offset location = box.localToGlobal(sizeToPoint(box.size));
    if (warnIfMissed) {
1375
      final FlutterView view = _viewOf(finder);
1376
      final HitTestResult result = HitTestResult();
1377
      binding.hitTestInView(result, location, view.viewId);
1378 1379 1380 1381 1382 1383 1384 1385
      bool found = false;
      for (final HitTestEntry entry in result.path) {
        if (entry.target == box) {
          found = true;
          break;
        }
      }
      if (!found) {
1386
        final RenderView renderView = binding.renderViews.firstWhere((RenderView r) => r.flutterView == view);
1387
        bool outOfBounds = false;
1388
        outOfBounds = !(Offset.zero & renderView.size).contains(location);
1389 1390 1391 1392 1393 1394
        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)
1395
              ErrorHint('Indeed, $location is outside the bounds of the root of the render tree, ${renderView.size}.'),
1396 1397 1398 1399 1400 1401 1402 1403 1404 1405
            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'
1406
          '${outOfBounds ? "Indeed, $location is outside the bounds of the root of the render tree, ${renderView.size}.\n" : ""}'
1407 1408 1409 1410
          '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'
1411
          'To make this warning fatal, set WidgetController.hitTestWarningShouldBeFatal to true.\n',
1412 1413 1414 1415
        );
      }
    }
    return location;
1416 1417 1418 1419
  }

  /// Returns the size of the given widget. This is only valid once
  /// the widget's render object has been laid out at least once.
1420
  Size getSize(FinderBase<Element> finder) {
1421
    TestAsyncUtils.guardSync();
1422
    final Element element = finder.evaluate().single;
1423
    final RenderBox box = element.renderObject! as RenderBox;
1424 1425
    return box.size;
  }
1426

1427
  /// Simulates sending physical key down and up events.
1428 1429 1430 1431 1432
  ///
  /// This only simulates key events coming from a physical keyboard, not from a
  /// soft keyboard.
  ///
  /// Specify `platform` as one of the platforms allowed in
1433 1434
  /// [platform.Platform.operatingSystem] to make the event appear to be from
  /// that type of system. Defaults to "web" on web, and "android" everywhere
1435
  /// else.
1436
  ///
1437 1438 1439 1440 1441 1442 1443 1444
  /// 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`.
  ///
1445 1446 1447
  /// Whether the event is sent through [RawKeyEvent] or [KeyEvent] is
  /// controlled by [debugKeyEventSimulatorTransitModeOverride].
  ///
1448 1449 1450 1451 1452 1453
  /// 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].
  ///
1454 1455
  /// Returns true if the key down event was handled by the framework.
  ///
1456 1457 1458 1459
  /// See also:
  ///
  ///  - [sendKeyDownEvent] to simulate only a key down event.
  ///  - [sendKeyUpEvent] to simulate only a key up event.
1460 1461 1462 1463 1464 1465 1466
  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);
1467
    // Internally wrapped in async guard.
1468
    await simulateKeyUpEvent(key, platform: platform, physicalKey: physicalKey);
1469
    return handled;
1470 1471
  }

1472
  /// Simulates sending a physical key down event.
1473 1474 1475 1476 1477
  ///
  /// 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
1478 1479
  /// [platform.Platform.operatingSystem] to make the event appear to be from
  /// that type of system. Defaults to "web" on web, and "android" everywhere
1480
  /// else.
1481
  ///
1482 1483 1484 1485 1486 1487 1488 1489
  /// 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`.
  ///
1490 1491 1492
  /// Whether the event is sent through [RawKeyEvent] or [KeyEvent] is
  /// controlled by [debugKeyEventSimulatorTransitModeOverride].
  ///
1493 1494
  /// Keys that are down when the test completes are cleared after each test.
  ///
1495 1496
  /// Returns true if the key event was handled by the framework.
  ///
1497 1498
  /// See also:
  ///
1499 1500
  ///  - [sendKeyUpEvent] and [sendKeyRepeatEvent] to simulate the corresponding
  ///    key up and repeat event.
1501
  ///  - [sendKeyEvent] to simulate both the key up and key down in the same call.
1502 1503 1504 1505 1506 1507
  Future<bool> sendKeyDownEvent(
    LogicalKeyboardKey key, {
    String platform = _defaultPlatform,
    String? character,
    PhysicalKeyboardKey? physicalKey
  }) async {
1508
    // Internally wrapped in async guard.
1509
    return simulateKeyDownEvent(key, platform: platform, character: character, physicalKey: physicalKey);
1510 1511 1512 1513 1514 1515 1516 1517
  }

  /// 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
1518 1519 1520
  /// [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.
1521
  ///
1522 1523 1524 1525
  /// 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`.
  ///
1526 1527 1528
  /// Whether the event is sent through [RawKeyEvent] or [KeyEvent] is
  /// controlled by [debugKeyEventSimulatorTransitModeOverride].
  ///
1529 1530
  /// Returns true if the key event was handled by the framework.
  ///
1531 1532
  /// See also:
  ///
1533 1534
  ///  - [sendKeyDownEvent] and [sendKeyRepeatEvent] to simulate the
  ///    corresponding key down and repeat event.
1535
  ///  - [sendKeyEvent] to simulate both the key up and key down in the same call.
1536 1537 1538 1539 1540
  Future<bool> sendKeyUpEvent(
      LogicalKeyboardKey key, {
        String platform = _defaultPlatform,
        PhysicalKeyboardKey? physicalKey
      }) async {
1541
    // Internally wrapped in async guard.
1542
    return simulateKeyUpEvent(key, platform: platform, physicalKey: physicalKey);
1543 1544
  }

1545
  /// Simulates sending a key repeat event from a physical keyboard.
1546 1547 1548 1549 1550 1551 1552
  ///
  /// 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
1553
  /// null.
1554
  ///
1555 1556 1557 1558 1559 1560 1561 1562
  /// 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`.
  ///
1563 1564 1565 1566 1567 1568 1569 1570 1571 1572 1573 1574 1575
  /// 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.
1576 1577 1578 1579 1580 1581
  Future<bool> sendKeyRepeatEvent(
      LogicalKeyboardKey key, {
        String platform = _defaultPlatform,
        String? character,
        PhysicalKeyboardKey? physicalKey
      }) async {
1582
    // Internally wrapped in async guard.
1583
    return simulateKeyRepeatEvent(key, platform: platform, character: character, physicalKey: physicalKey);
1584 1585
  }

1586 1587
  /// Returns the rect of the given widget. This is only valid once
  /// the widget's render object has been laid out at least once.
1588
  Rect getRect(FinderBase<Element> finder) => Rect.fromPoints(getTopLeft(finder), getBottomRight(finder));
1589

1590 1591 1592 1593 1594 1595 1596 1597 1598 1599 1600 1601 1602 1603
  /// 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.
1604
  // TODO(pdblasi-google): Deprecate this and point references to semantics.find. See https://github.com/flutter/flutter/issues/112670.
1605
  SemanticsNode getSemantics(FinderBase<Element> finder) => semantics.find(finder);
1606 1607 1608 1609 1610

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

1614 1615 1616
  /// 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.
  ///
1617 1618 1619
  /// Usually the `finder` for this method should be labeled `skipOffstage:
  /// false`, so that the [Finder] deals with widgets that are off the screen
  /// correctly.
1620
  ///
1621 1622 1623
  /// 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.
1624
  ///
1625 1626 1627 1628
  /// See also:
  ///
  ///  * [Scrollable.ensureVisible], which is the production API used to
  ///    implement this method.
1629
  Future<void> ensureVisible(FinderBase<Element> finder) => Scrollable.ensureVisible(element(finder));
1630

1631
  /// Repeatedly scrolls a [Scrollable] by `delta` in the
1632 1633
  /// [Scrollable.axisDirection] direction until a widget matching `finder` is
  /// visible.
1634
  ///
1635
  /// Between each scroll, advances the clock by `duration` time.
1636
  ///
1637 1638
  /// Scrolling is performed until the start of the `finder` is visible. This is
  /// due to the default parameter values of the [Scrollable.ensureVisible] method.
1639
  ///
1640 1641 1642 1643
  /// 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.
1644 1645
  ///
  /// This is different from [ensureVisible] in that this allows looking for
1646
  /// `finder` that is not yet built. The caller must specify the scrollable
1647
  /// that will build child specified by `finder` when there are multiple
1648
  /// [Scrollable]s.
1649
  ///
1650
  /// See also:
1651
  ///
1652
  ///  * [dragUntilVisible], which implements the body of this method.
1653
  Future<void> scrollUntilVisible(
1654
    FinderBase<Element> finder,
1655
    double delta, {
1656
      FinderBase<Element>? scrollable,
1657 1658 1659 1660 1661
      int maxScrolls = 50,
      Duration duration = const Duration(milliseconds: 50),
    }
  ) {
    assert(maxScrolls > 0);
1662
    scrollable ??= find.byType(Scrollable);
1663 1664
    return TestAsyncUtils.guard<void>(() async {
      Offset moveStep;
1665
      switch (widget<Scrollable>(scrollable!).axisDirection) {
1666 1667 1668 1669 1670 1671 1672 1673 1674
        case AxisDirection.up:
          moveStep = Offset(0, delta);
        case AxisDirection.down:
          moveStep = Offset(0, -delta);
        case AxisDirection.left:
          moveStep = Offset(delta, 0);
        case AxisDirection.right:
          moveStep = Offset(-delta, 0);
      }
1675 1676 1677 1678 1679
      await dragUntilVisible(
        finder,
        scrollable,
        moveStep,
        maxIteration: maxScrolls,
1680 1681
        duration: duration,
      );
1682 1683 1684
    });
  }

1685 1686 1687
  /// Repeatedly drags `view` by `moveStep` until `finder` is visible.
  ///
  /// Between each drag, advances the clock by `duration`.
1688
  ///
1689 1690 1691 1692
  /// Throws a [StateError] if `finder` is not found after `maxIteration`
  /// drags.
  ///
  /// See also:
1693
  ///
1694 1695
  ///  * [scrollUntilVisible], which wraps this method with an API that is more
  ///    convenient when dealing with a [Scrollable].
1696
  Future<void> dragUntilVisible(
1697 1698
    FinderBase<Element> finder,
    FinderBase<Element> view,
1699 1700 1701 1702 1703
    Offset moveStep, {
      int maxIteration = 50,
      Duration duration = const Duration(milliseconds: 50),
  }) {
    return TestAsyncUtils.guard<void>(() async {
1704
      while (maxIteration > 0 && finder.evaluate().isEmpty) {
1705
        await drag(view, moveStep);
1706
        await pump(duration);
1707
        maxIteration -= 1;
1708 1709 1710 1711
      }
      await Scrollable.ensureVisible(element(finder));
    });
  }
1712
}
1713 1714 1715 1716 1717 1718 1719

/// 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.
1720
  LiveWidgetController(super.binding);
1721 1722

  @override
1723
  Future<void> pump([Duration? duration]) async {
1724
    if (duration != null) {
1725
      await Future<void>.delayed(duration);
1726
    }
1727 1728 1729
    binding.scheduleFrame();
    await binding.endOfFrame;
  }
1730

1731 1732 1733 1734 1735 1736 1737 1738 1739 1740 1741 1742 1743 1744 1745
  @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;
    });
  }

1746 1747
  @override
  Future<List<Duration>> handlePointerEventRecord(List<PointerEventRecord> records) {
1748 1749 1750
    assert(records.isNotEmpty);
    return TestAsyncUtils.guard<List<Duration>>(() async {
      final List<Duration> handleTimeStampDiff = <Duration>[];
1751
      DateTime? startTime;
1752 1753 1754
      for (final PointerEventRecord record in records) {
        final DateTime now = clock.now();
        startTime ??= now;
1755
        // So that the first event is promised to receive a zero timeDiff.
1756 1757 1758 1759
        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.
1760
          // Flush all past events.
1761
          handleTimeStampDiff.add(-timeDiff);
1762
          record.events.forEach(binding.handlePointerEvent);
1763 1764 1765 1766 1767 1768 1769 1770
        } 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,
          );
1771
          record.events.forEach(binding.handlePointerEvent);
1772 1773
        }
      }
1774

1775 1776 1777
      return handleTimeStampDiff;
    });
  }
1778
}