controller.dart 56.5 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
/// Class that programmatically interacts with widgets.
///
28 29 30 31 32 33
/// 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 {
34
  /// Creates a widget controller that uses the given binding.
35 36
  WidgetController(this.binding);

37
  /// A reference to the current instance of the binding.
38 39 40 41 42 43 44 45
  final WidgetsBinding binding;

  // 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.
46 47 48 49
  bool any(Finder finder) {
    TestAsyncUtils.guardSync();
    return finder.evaluate().isNotEmpty;
  }
50 51 52 53 54 55

  /// 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 {
56
    TestAsyncUtils.guardSync();
57
    return allElements.map<Widget>((Element element) => element.widget);
58 59 60 61 62 63
  }

  /// The matching widget in the widget tree.
  ///
  /// Throws a [StateError] if `finder` is empty or matches more than
  /// one widget.
64 65 66
  ///
  /// * 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.
67
  T widget<T extends Widget>(Finder finder) {
68
    TestAsyncUtils.guardSync();
69
    return finder.evaluate().single.widget as T;
70 71 72 73 74 75
  }

  /// The first matching widget according to a depth-first pre-order
  /// traversal of the widget tree.
  ///
  /// Throws a [StateError] if `finder` is empty.
76 77
  ///
  /// * Use [widget] if you only expect to match one widget.
78
  T firstWidget<T extends Widget>(Finder finder) {
79
    TestAsyncUtils.guardSync();
80
    return finder.evaluate().first.widget as T;
81 82
  }

83 84 85 86
  /// 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.
87
  Iterable<T> widgetList<T extends Widget>(Finder finder) {
88
    TestAsyncUtils.guardSync();
89
    return finder.evaluate().map<T>((Element element) {
90
      final T result = element.widget as T;
91 92
      return result;
    });
93 94
  }

95 96 97 98 99 100 101 102 103 104 105 106 107 108 109
  /// 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);
  }

110 111 112 113 114 115
  /// 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 {
116
    TestAsyncUtils.guardSync();
117
    return collectAllElementsFrom(binding.renderViewElement!, skipOffstage: false);
118 119 120 121 122 123
  }

  /// The matching element in the widget tree.
  ///
  /// Throws a [StateError] if `finder` is empty or matches more than
  /// one element.
124 125 126
  ///
  /// * 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.
127
  T element<T extends Element>(Finder finder) {
128
    TestAsyncUtils.guardSync();
129
    return finder.evaluate().single as T;
130 131 132 133 134 135
  }

  /// The first matching element according to a depth-first pre-order
  /// traversal of the widget tree.
  ///
  /// Throws a [StateError] if `finder` is empty.
136 137
  ///
  /// * Use [element] if you only expect to match one element.
138
  T firstElement<T extends Element>(Finder finder) {
139
    TestAsyncUtils.guardSync();
140
    return finder.evaluate().first as T;
141 142
  }

143 144 145 146
  /// 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.
147
  Iterable<T> elementList<T extends Element>(Finder finder) {
148
    TestAsyncUtils.guardSync();
149
    return finder.evaluate().cast<T>();
150 151
  }

152 153 154 155 156 157
  /// 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 {
158
    TestAsyncUtils.guardSync();
159
    return allElements.whereType<StatefulElement>().map<State>((StatefulElement element) => element.state);
160 161 162 163 164 165
  }

  /// 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.
166 167 168
  ///
  /// * 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.
169
  T state<T extends State>(Finder finder) {
170
    TestAsyncUtils.guardSync();
171
    return _stateOf<T>(finder.evaluate().single, finder);
172 173 174 175 176 177 178
  }

  /// 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.
179 180
  ///
  /// * Use [state] if you only expect to match one state.
181
  T firstState<T extends State>(Finder finder) {
182
    TestAsyncUtils.guardSync();
183
    return _stateOf<T>(finder.evaluate().first, finder);
184 185
  }

186 187 188 189 190 191 192
  /// 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.
193
  Iterable<T> stateList<T extends State>(Finder finder) {
194
    TestAsyncUtils.guardSync();
195
    return finder.evaluate().map<T>((Element element) => _stateOf<T>(element, finder));
196 197
  }

198
  T _stateOf<T extends State>(Element element, Finder finder) {
199
    TestAsyncUtils.guardSync();
200
    if (element is StatefulElement) {
201
      return element.state as T;
202
    }
203
    throw StateError('Widget of type ${element.widget.runtimeType}, with ${finder.description}, is not a StatefulWidget.');
204 205 206 207 208 209 210 211 212 213
  }

  /// 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 {
214
    TestAsyncUtils.guardSync();
215
    return allElements.map<RenderObject>((Element element) => element.renderObject!);
216 217 218 219 220 221
  }

  /// 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).
222 223 224
  ///
  /// * 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.
225
  T renderObject<T extends RenderObject>(Finder finder) {
226
    TestAsyncUtils.guardSync();
227
    return finder.evaluate().single.renderObject! as T;
228 229 230 231 232 233
  }

  /// 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.
234 235
  ///
  /// * Use [renderObject] if you only expect to match one render object.
236
  T firstRenderObject<T extends RenderObject>(Finder finder) {
237
    TestAsyncUtils.guardSync();
238
    return finder.evaluate().first.renderObject! as T;
239 240
  }

241 242 243 244
  /// 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.
245
  Iterable<T> renderObjectList<T extends RenderObject>(Finder finder) {
246
    TestAsyncUtils.guardSync();
247
    return finder.evaluate().map<T>((Element element) {
248
      final T result = element.renderObject! as T;
249 250
      return result;
    });
251 252
  }

253
  /// Returns a list of all the [Layer] objects in the rendering.
254
  List<Layer> get layers => _walkLayers(binding.renderView.debugLayer!).toList();
255
  Iterable<Layer> _walkLayers(Layer layer) sync* {
256
    TestAsyncUtils.guardSync();
257 258
    yield layer;
    if (layer is ContainerLayer) {
259
      final ContainerLayer root = layer;
260
      Layer? child = root.firstChild;
261 262 263 264 265 266 267 268 269 270
      while (child != null) {
        yield* _walkLayers(child);
        child = child.nextSibling;
      }
    }
  }

  // INTERACTION

  /// Dispatch a pointer down / pointer up sequence at the center of
271 272
  /// the given widget, assuming it is exposed.
  ///
273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289
  /// {@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);
290 291
  }

292
  /// Dispatch a pointer down / pointer up sequence at the given location.
293
  Future<void> tapAt(Offset location, {int? pointer, int buttons = kPrimaryButton}) {
294
    return TestAsyncUtils.guard<void>(() async {
295
      final TestGesture gesture = await startGesture(location, pointer: pointer, buttons: buttons);
296 297
      await gesture.up();
    });
298 299
  }

300 301 302
  /// Dispatch a pointer down at the center of the given widget, assuming it is
  /// exposed.
  ///
303 304 305 306 307 308 309 310 311 312 313
  /// {@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}) {
314
    return TestAsyncUtils.guard<TestGesture>(() {
315
      return startGesture(getCenter(finder, warnIfMissed: warnIfMissed, callee: 'press'), pointer: pointer, buttons: buttons);
316 317 318
    });
  }

319 320
  /// Dispatch a pointer down / pointer up sequence (with a delay of
  /// [kLongPressTimeout] + [kPressTimeout] between the two events) at the
321 322
  /// center of the given widget, assuming it is exposed.
  ///
323 324 325 326 327 328 329 330 331 332
  /// {@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);
333 334 335 336
  }

  /// Dispatch a pointer down / pointer up sequence at the given location with
  /// a delay of [kLongPressTimeout] + [kPressTimeout] between the two events.
337
  Future<void> longPressAt(Offset location, {int? pointer, int buttons = kPrimaryButton}) {
338
    return TestAsyncUtils.guard<void>(() async {
339
      final TestGesture gesture = await startGesture(location, pointer: pointer, buttons: buttons);
340 341 342 343 344
      await pump(kLongPressTimeout + kPressTimeout);
      await gesture.up();
    });
  }

345
  /// Attempts a fling gesture starting from the center of the given
346
  /// widget, moving the given distance, reaching the given speed.
347
  ///
348
  /// {@macro flutter.flutter_test.WidgetController.tap.warnIfMissed}
349
  ///
350
  /// {@template flutter.flutter_test.WidgetController.fling}
351 352 353
  /// This can pump frames.
  ///
  /// Exactly 50 pointer events are synthesized.
354 355 356
  ///
  /// The `speed` is in pixels per second in the direction given by `offset`.
  ///
357 358 359 360 361 362 363 364 365 366 367 368 369
  /// 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
370 371 372 373 374 375 376
  ///
  /// 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).
377 378 379 380
  /// {@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].
381 382 383 384
  Future<void> fling(
    Finder finder,
    Offset offset,
    double speed, {
385
    int? pointer,
386
    int buttons = kPrimaryButton,
387 388 389
    Duration frameInterval = const Duration(milliseconds: 16),
    Offset initialOffset = Offset.zero,
    Duration initialOffsetDelay = const Duration(seconds: 1),
390
    bool warnIfMissed = true,
391
  }) {
Ian Hickson's avatar
Ian Hickson committed
392
    return flingFrom(
393
      getCenter(finder, warnIfMissed: warnIfMissed, callee: 'fling'),
Ian Hickson's avatar
Ian Hickson committed
394 395 396
      offset,
      speed,
      pointer: pointer,
397
      buttons: buttons,
Ian Hickson's avatar
Ian Hickson committed
398 399 400 401
      frameInterval: frameInterval,
      initialOffset: initialOffset,
      initialOffsetDelay: initialOffsetDelay,
    );
402 403
  }

404 405
  /// Attempts a fling gesture starting from the given location, moving the
  /// given distance, reaching the given speed.
406
  ///
407
  /// {@macro flutter.flutter_test.WidgetController.fling}
408 409 410
  ///
  /// 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].
411 412 413 414
  Future<void> flingFrom(
    Offset startLocation,
    Offset offset,
    double speed, {
415
    int? pointer,
416
    int buttons = kPrimaryButton,
417 418 419
    Duration frameInterval = const Duration(milliseconds: 16),
    Offset initialOffset = Offset.zero,
    Duration initialOffsetDelay = const Duration(seconds: 1),
Ian Hickson's avatar
Ian Hickson committed
420
  }) {
421
    assert(offset.distance > 0.0);
422
    assert(speed > 0.0); // speed is pixels/second
423
    return TestAsyncUtils.guard<void>(() async {
424
      final TestPointer testPointer = TestPointer(pointer ?? _getNextPointer(), PointerDeviceKind.touch, null, buttons);
425
      const int kMoveCount = 50; // Needs to be >= kHistorySize, see _LeastSquaresVelocityTrackerStrategy
426
      final double timeStampDelta = 1000000.0 * offset.distance / (kMoveCount * speed);
427
      double timeStamp = 0.0;
428
      double lastTimeStamp = timeStamp;
429
      await sendEventToBinding(testPointer.down(startLocation, timeStamp: Duration(microseconds: timeStamp.round())));
Ian Hickson's avatar
Ian Hickson committed
430
      if (initialOffset.distance > 0.0) {
431
        await sendEventToBinding(testPointer.move(startLocation + initialOffset, timeStamp: Duration(microseconds: timeStamp.round())));
432
        timeStamp += initialOffsetDelay.inMicroseconds;
Ian Hickson's avatar
Ian Hickson committed
433 434
        await pump(initialOffsetDelay);
      }
435
      for (int i = 0; i <= kMoveCount; i += 1) {
436
        final Offset location = startLocation + initialOffset + Offset.lerp(Offset.zero, offset, i / kMoveCount)!;
437
        await sendEventToBinding(testPointer.move(location, timeStamp: Duration(microseconds: timeStamp.round())));
438
        timeStamp += timeStampDelta;
439 440
        if (timeStamp - lastTimeStamp > frameInterval.inMicroseconds) {
          await pump(Duration(microseconds: (timeStamp - lastTimeStamp).truncate()));
441 442
          lastTimeStamp = timeStamp;
        }
443
      }
444
      await sendEventToBinding(testPointer.up(timeStamp: Duration(microseconds: timeStamp.round())));
445
    });
446 447
  }

448 449 450 451 452 453
  /// 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.
  ///
454 455 456 457 458 459 460
  /// 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,
461
  /// but a very small portion may have a tiny negative value for about tens of
462 463 464
  /// 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
465 466 467 468 469
  /// `records`.
  ///
  /// See [PointerEventRecord].
  Future<List<Duration>> handlePointerEventRecord(List<PointerEventRecord> records);

470 471 472 473 474
  /// 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.
475 476 477 478
  ///
  /// This is invoked by [flingFrom], for instance, so that the sequence of
  /// pointer events occurs over time.
  ///
479
  /// The [WidgetTester] subclass implements this by deferring to the [binding].
480 481 482
  ///
  /// See also [SchedulerBinding.endOfFrame], which returns a future that could
  /// be appropriate to return in the implementation of this method.
483
  Future<void> pump([Duration duration]);
484

485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512
  /// 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),
  ]);

513 514 515
  /// Attempts to drag the given widget by the given offset, by
  /// starting a drag in the middle of the widget.
  ///
516
  /// {@macro flutter.flutter_test.WidgetController.tap.warnIfMissed}
517 518 519
  ///
  /// 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.
520
  ///
521 522 523
  /// The operation happens at once. If you want the drag to last for a period
  /// of time, consider using [timedDrag].
  ///
524
  /// {@template flutter.flutter_test.WidgetController.drag}
525 526 527 528 529 530
  /// 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.
531 532 533 534 535 536
  ///
  /// 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.
  ///
537 538
  /// 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
539
  /// should be left to their default values.
540
  /// {@endtemplate}
541 542 543
  Future<void> drag(
    Finder finder,
    Offset offset, {
544
    int? pointer,
545 546 547
    int buttons = kPrimaryButton,
    double touchSlopX = kDragSlopDefault,
    double touchSlopY = kDragSlopDefault,
548
    bool warnIfMissed = true,
549
    PointerDeviceKind kind = PointerDeviceKind.touch,
550 551
  }) {
    return dragFrom(
552
      getCenter(finder, warnIfMissed: warnIfMissed, callee: 'drag'),
553 554 555 556 557
      offset,
      pointer: pointer,
      buttons: buttons,
      touchSlopX: touchSlopX,
      touchSlopY: touchSlopY,
558
      kind: kind,
559
    );
560 561 562 563
  }

  /// Attempts a drag gesture consisting of a pointer down, a move by
  /// the given offset, and a pointer up.
564 565 566 567
  ///
  /// 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.
568
  ///
569 570 571
  /// The operation happens at once. If you want the drag to last for a period
  /// of time, consider using [timedDragFrom].
  ///
572
  /// {@macro flutter.flutter_test.WidgetController.drag}
573 574 575
  Future<void> dragFrom(
    Offset startLocation,
    Offset offset, {
576
    int? pointer,
577 578 579
    int buttons = kPrimaryButton,
    double touchSlopX = kDragSlopDefault,
    double touchSlopY = kDragSlopDefault,
580
    PointerDeviceKind kind = PointerDeviceKind.touch,
581
  }) {
582
    assert(kDragSlopDefault > kTouchSlop);
583
    return TestAsyncUtils.guard<void>(() async {
584
      final TestGesture gesture = await startGesture(startLocation, pointer: pointer, buttons: buttons, kind: kind);
585
      assert(gesture != null);
586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650

      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);
      }
651 652
      await gesture.up();
    });
653 654
  }

655 656 657
  /// Attempts to drag the given widget by the given offset in the `duration`
  /// time, starting in the middle of the widget.
  ///
658
  /// {@macro flutter.flutter_test.WidgetController.tap.warnIfMissed}
659 660 661 662 663
  ///
  /// 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`.
  ///
664
  /// {@template flutter.flutter_test.WidgetController.timedDrag}
665 666 667 668 669 670 671 672 673 674 675 676
  /// 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, {
677
    int? pointer,
678 679
    int buttons = kPrimaryButton,
    double frequency = 60.0,
680
    bool warnIfMissed = true,
681 682
  }) {
    return timedDragFrom(
683
      getCenter(finder, warnIfMissed: warnIfMissed, callee: 'timedDrag'),
684 685 686 687 688 689 690 691 692 693 694 695 696 697 698
      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`.
  ///
699
  /// {@macro flutter.flutter_test.WidgetController.timedDrag}
700 701 702 703
  Future<void> timedDragFrom(
    Offset startLocation,
    Offset offset,
    Duration duration, {
704
    int? pointer,
705 706 707 708 709 710
    int buttons = kPrimaryButton,
    double frequency = 60.0,
  }) {
    assert(frequency > 0);
    final int intervals = duration.inMicroseconds * frequency ~/ 1E6;
    assert(intervals > 1);
711
    pointer ??= _getNextPointer();
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
    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,
741
            ),
742 743 744 745 746 747 748
          ]),
      ],
      PointerEventRecord(duration, <PointerEvent>[
        PointerUpEvent(
          timeStamp: duration,
          position: offsets.last,
          pointer: pointer,
749 750
          // The PointerData received from the engine with
          // change = PointerChange.up, which translates to PointerUpEvent,
751 752
          // doesn't provide the button field.
          // buttons: buttons,
753
        ),
754 755 756
      ]),
    ];
    return TestAsyncUtils.guard<void>(() async {
757
      await handlePointerEventRecord(records);
758 759 760
    });
  }

761 762 763 764
  /// 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.
765
  int get nextPointer => _nextPointer;
766

767 768 769 770 771
  static int _nextPointer = 1;

  static int _getNextPointer() {
    final int result = _nextPointer;
    _nextPointer += 1;
772 773 774
    return result;
  }

775 776 777 778 779
  /// 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.
780
  Future<TestGesture> createGesture({
781
    int? pointer,
782 783 784 785
    PointerDeviceKind kind = PointerDeviceKind.touch,
    int buttons = kPrimaryButton,
  }) async {
    return TestGesture(
786
      dispatcher: sendEventToBinding,
787 788
      kind: kind,
      pointer: pointer ?? _getNextPointer(),
789
      buttons: buttons,
790
    );
791 792
  }

793 794 795 796 797 798
  /// Creates a gesture with an initial down gesture at a particular point, and
  /// returns the [TestGesture] object which you can use to continue the
  /// gesture.
  ///
  /// You can use [createGesture] if your gesture doesn't begin with an initial
  /// down gesture.
799 800 801 802 803 804
  ///
  /// 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.
805 806
  Future<TestGesture> startGesture(
    Offset downLocation, {
807
    int? pointer,
808
    PointerDeviceKind kind = PointerDeviceKind.touch,
809
    int buttons = kPrimaryButton,
810
  }) async {
811
    assert(downLocation != null);
812 813 814 815 816
    final TestGesture result = await createGesture(
      pointer: pointer,
      kind: kind,
      buttons: buttons,
    );
817 818 819 820
    await result.down(downLocation);
    return result;
  }

821
  /// Forwards the given location to the binding's hitTest logic.
822
  HitTestResult hitTestOnBinding(Offset location) {
823
    final HitTestResult result = HitTestResult();
824 825 826 827
    binding.hitTest(result, location);
    return result;
  }

828
  /// Forwards the given pointer event to the binding.
829
  Future<void> sendEventToBinding(PointerEvent event) {
830
    return TestAsyncUtils.guard<void>(() async {
831
      binding.handlePointerEvent(event);
832
    });
833 834
  }

835 836 837 838 839 840 841 842 843 844
  /// 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);
  }

845 846 847
  // GEOMETRY

  /// Returns the point at the center of the given widget.
848 849 850 851 852 853 854 855 856 857 858 859 860 861
  ///
  /// {@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);
862 863 864
  }

  /// Returns the point at the top left of the given widget.
865 866 867 868
  ///
  /// {@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);
869 870 871 872
  }

  /// Returns the point at the top right of the given widget. This
  /// point is not inside the object's hit test area.
873 874 875 876
  ///
  /// {@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);
877 878 879 880
  }

  /// Returns the point at the bottom left of the given widget. This
  /// point is not inside the object's hit test area.
881 882 883 884
  ///
  /// {@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);
885 886 887 888
  }

  /// Returns the point at the bottom right of the given widget. This
  /// point is not inside the object's hit test area.
889 890 891 892
  ///
  /// {@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);
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
  /// 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;

918
  Offset _getElementPoint(Finder finder, Offset Function(Size size) sizeToPoint, { required bool warnIfMissed, required String callee }) {
919
    TestAsyncUtils.guardSync();
920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937
    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"). '
938
        'Unfortunately "$callee()" only supports targeting widgets that correspond to RenderBox objects in the rendering.'
939 940
      );
    }
941
    final RenderBox box = element.renderObject! as RenderBox;
942 943 944 945 946 947 948 949 950 951 952 953 954 955 956 957 958 959 960 961 962 963 964 965 966 967 968 969 970 971 972 973 974 975 976 977 978 979
    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;
        if (binding.renderView != null && binding.renderView.size != null) {
          outOfBounds = !(Offset.zero & binding.renderView.size).contains(location);
        }
        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'
980
          'To make this warning fatal, set WidgetController.hitTestWarningShouldBeFatal to true.\n',
981 982 983 984
        );
      }
    }
    return location;
985 986 987 988 989
  }

  /// 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) {
990
    TestAsyncUtils.guardSync();
991
    final Element element = finder.evaluate().single;
992
    final RenderBox box = element.renderObject! as RenderBox;
993 994
    return box.size;
  }
995

996
  /// Simulates sending physical key down and up events.
997 998 999 1000 1001
  ///
  /// This only simulates key events coming from a physical keyboard, not from a
  /// soft keyboard.
  ///
  /// Specify `platform` as one of the platforms allowed in
1002 1003 1004 1005
  /// [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.
1006
  ///
1007 1008 1009 1010 1011 1012 1013 1014
  /// 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`.
  ///
1015 1016 1017
  /// Whether the event is sent through [RawKeyEvent] or [KeyEvent] is
  /// controlled by [debugKeyEventSimulatorTransitModeOverride].
  ///
1018 1019 1020 1021 1022 1023
  /// 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].
  ///
1024 1025
  /// Returns true if the key down event was handled by the framework.
  ///
1026 1027 1028 1029
  /// See also:
  ///
  ///  - [sendKeyDownEvent] to simulate only a key down event.
  ///  - [sendKeyUpEvent] to simulate only a key up event.
1030 1031 1032 1033 1034 1035
  Future<bool> sendKeyEvent(
    LogicalKeyboardKey key, {
    String platform = _defaultPlatform,
    String? character,
    PhysicalKeyboardKey? physicalKey
  }) async {
1036
    assert(platform != null);
1037
    final bool handled = await simulateKeyDownEvent(key, platform: platform, character: character, physicalKey: physicalKey);
1038
    // Internally wrapped in async guard.
1039
    await simulateKeyUpEvent(key, platform: platform, physicalKey: physicalKey);
1040
    return handled;
1041 1042
  }

1043
  /// Simulates sending a physical key down event.
1044 1045 1046 1047 1048
  ///
  /// 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
1049 1050 1051 1052
  /// [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.
1053
  ///
1054 1055 1056 1057 1058 1059 1060 1061
  /// 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`.
  ///
1062 1063 1064
  /// Whether the event is sent through [RawKeyEvent] or [KeyEvent] is
  /// controlled by [debugKeyEventSimulatorTransitModeOverride].
  ///
1065 1066
  /// Keys that are down when the test completes are cleared after each test.
  ///
1067 1068
  /// Returns true if the key event was handled by the framework.
  ///
1069 1070
  /// See also:
  ///
1071 1072
  ///  - [sendKeyUpEvent] and [sendKeyRepeatEvent] to simulate the corresponding
  ///    key up and repeat event.
1073
  ///  - [sendKeyEvent] to simulate both the key up and key down in the same call.
1074 1075 1076 1077 1078 1079
  Future<bool> sendKeyDownEvent(
    LogicalKeyboardKey key, {
    String platform = _defaultPlatform,
    String? character,
    PhysicalKeyboardKey? physicalKey
  }) async {
1080 1081
    assert(platform != null);
    // Internally wrapped in async guard.
1082
    return simulateKeyDownEvent(key, platform: platform, character: character, physicalKey: physicalKey);
1083 1084 1085 1086 1087 1088 1089 1090
  }

  /// 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
1091 1092 1093
  /// [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.
1094
  ///
1095 1096 1097 1098
  /// 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`.
  ///
1099 1100 1101
  /// Whether the event is sent through [RawKeyEvent] or [KeyEvent] is
  /// controlled by [debugKeyEventSimulatorTransitModeOverride].
  ///
1102 1103
  /// Returns true if the key event was handled by the framework.
  ///
1104 1105
  /// See also:
  ///
1106 1107
  ///  - [sendKeyDownEvent] and [sendKeyRepeatEvent] to simulate the
  ///    corresponding key down and repeat event.
1108
  ///  - [sendKeyEvent] to simulate both the key up and key down in the same call.
1109 1110 1111 1112 1113
  Future<bool> sendKeyUpEvent(
      LogicalKeyboardKey key, {
        String platform = _defaultPlatform,
        PhysicalKeyboardKey? physicalKey
      }) async {
1114 1115
    assert(platform != null);
    // Internally wrapped in async guard.
1116
    return simulateKeyUpEvent(key, platform: platform, physicalKey: physicalKey);
1117 1118
  }

1119
  /// Simulates sending a key repeat event from a physical keyboard.
1120 1121 1122 1123 1124 1125 1126 1127 1128
  ///
  /// 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.
  ///
1129 1130 1131 1132 1133 1134 1135 1136
  /// 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`.
  ///
1137 1138 1139 1140 1141 1142 1143 1144 1145 1146 1147 1148 1149
  /// 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.
1150 1151 1152 1153 1154 1155
  Future<bool> sendKeyRepeatEvent(
      LogicalKeyboardKey key, {
        String platform = _defaultPlatform,
        String? character,
        PhysicalKeyboardKey? physicalKey
      }) async {
1156 1157
    assert(platform != null);
    // Internally wrapped in async guard.
1158
    return simulateKeyRepeatEvent(key, platform: platform, character: character, physicalKey: physicalKey);
1159 1160
  }

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

1165 1166 1167 1168 1169 1170 1171 1172 1173 1174 1175 1176 1177 1178 1179
  /// 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.
  SemanticsNode getSemantics(Finder finder) {
1180
    if (binding.pipelineOwner.semanticsOwner == null) {
1181
      throw StateError('Semantics are not enabled.');
1182
    }
1183 1184 1185 1186 1187 1188 1189 1190
    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;
1191 1192
    RenderObject? renderObject = element.findRenderObject();
    SemanticsNode? result = renderObject?.debugSemantics;
1193
    while (renderObject != null && (result == null || result.isMergedIntoParent)) {
1194
      renderObject = renderObject.parent as RenderObject?;
1195 1196
      result = renderObject?.debugSemantics;
    }
1197
    if (result == null) {
1198
      throw StateError('No Semantics data found.');
1199
    }
1200 1201 1202 1203 1204 1205 1206 1207 1208 1209
    return result;
  }

  /// 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();
  }

1210 1211 1212
  /// 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.
  ///
1213 1214 1215
  /// Usually the `finder` for this method should be labeled `skipOffstage:
  /// false`, so that the [Finder] deals with widgets that are off the screen
  /// correctly.
1216
  ///
1217 1218 1219
  /// 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.
1220
  ///
1221 1222 1223 1224
  /// See also:
  ///
  ///  * [Scrollable.ensureVisible], which is the production API used to
  ///    implement this method.
1225
  Future<void> ensureVisible(Finder finder) => Scrollable.ensureVisible(element(finder));
1226

1227
  /// Repeatedly scrolls a [Scrollable] by `delta` in the
1228 1229
  /// [Scrollable.axisDirection] direction until a widget matching `finder` is
  /// visible.
1230
  ///
1231
  /// Between each scroll, advances the clock by `duration` time.
1232
  ///
1233 1234
  /// Scrolling is performed until the start of the `finder` is visible. This is
  /// due to the default parameter values of the [Scrollable.ensureVisible] method.
1235
  ///
1236 1237 1238 1239
  /// 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.
1240 1241
  ///
  /// This is different from [ensureVisible] in that this allows looking for
1242
  /// `finder` that is not yet built. The caller must specify the scrollable
1243
  /// that will build child specified by `finder` when there are multiple
1244
  /// [Scrollable]s.
1245
  ///
1246
  /// See also:
1247
  ///
1248
  ///  * [dragUntilVisible], which implements the body of this method.
1249 1250 1251
  Future<void> scrollUntilVisible(
    Finder finder,
    double delta, {
1252
      Finder? scrollable,
1253 1254 1255 1256 1257
      int maxScrolls = 50,
      Duration duration = const Duration(milliseconds: 50),
    }
  ) {
    assert(maxScrolls > 0);
1258
    scrollable ??= find.byType(Scrollable);
1259 1260
    return TestAsyncUtils.guard<void>(() async {
      Offset moveStep;
1261
      switch (widget<Scrollable>(scrollable!).axisDirection) {
1262 1263 1264 1265 1266 1267 1268 1269 1270 1271 1272 1273 1274
        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;
      }
1275 1276 1277 1278 1279
      await dragUntilVisible(
        finder,
        scrollable,
        moveStep,
        maxIteration: maxScrolls,
1280 1281
        duration: duration,
      );
1282 1283 1284
    });
  }

1285 1286 1287
  /// Repeatedly drags `view` by `moveStep` until `finder` is visible.
  ///
  /// Between each drag, advances the clock by `duration`.
1288
  ///
1289 1290 1291 1292
  /// Throws a [StateError] if `finder` is not found after `maxIteration`
  /// drags.
  ///
  /// See also:
1293
  ///
1294 1295
  ///  * [scrollUntilVisible], which wraps this method with an API that is more
  ///    convenient when dealing with a [Scrollable].
1296 1297 1298 1299 1300 1301 1302 1303
  Future<void> dragUntilVisible(
    Finder finder,
    Finder view,
    Offset moveStep, {
      int maxIteration = 50,
      Duration duration = const Duration(milliseconds: 50),
  }) {
    return TestAsyncUtils.guard<void>(() async {
1304
      while (maxIteration > 0 && finder.evaluate().isEmpty) {
1305
        await drag(view, moveStep);
1306
        await pump(duration);
1307
        maxIteration -= 1;
1308 1309 1310 1311
      }
      await Scrollable.ensureVisible(element(finder));
    });
  }
1312
}
1313 1314 1315 1316 1317 1318 1319

/// 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.
1320
  LiveWidgetController(super.binding);
1321 1322

  @override
1323
  Future<void> pump([Duration? duration]) async {
1324
    if (duration != null) {
1325
      await Future<void>.delayed(duration);
1326
    }
1327 1328 1329
    binding.scheduleFrame();
    await binding.endOfFrame;
  }
1330

1331 1332 1333 1334 1335 1336 1337 1338 1339 1340 1341 1342 1343 1344 1345 1346
  @override
  Future<int> pumpAndSettle([
    Duration duration = const Duration(milliseconds: 100),
  ]) {
    assert(duration != null);
    assert(duration > Duration.zero);
    return TestAsyncUtils.guard<int>(() async {
      int count = 0;
      do {
        await pump(duration);
        count += 1;
      } while (binding.hasScheduledFrame);
      return count;
    });
  }

1347 1348
  @override
  Future<List<Duration>> handlePointerEventRecord(List<PointerEventRecord> records) {
1349 1350 1351 1352 1353 1354 1355
    assert(records != null);
    assert(records.isNotEmpty);
    return TestAsyncUtils.guard<List<Duration>>(() async {
      // hitTestHistory is an equivalence of _hitTests in [GestureBinding],
      // used as state for all pointers which are currently down.
      final Map<int, HitTestResult> hitTestHistory = <int, HitTestResult>{};
      final List<Duration> handleTimeStampDiff = <Duration>[];
1356
      DateTime? startTime;
1357 1358 1359 1360 1361 1362 1363 1364 1365 1366
      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);
1367
          record.events.forEach(binding.handlePointerEvent);
1368 1369 1370 1371 1372 1373 1374 1375
        } 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,
          );
1376
          record.events.forEach(binding.handlePointerEvent);
1377 1378 1379 1380 1381 1382 1383 1384
        }
      }
      // This makes sure that a gesture is completed, with no more pointers
      // active.
      assert(hitTestHistory.isEmpty);
      return handleTimeStampDiff;
    });
  }
1385
}