controller.dart 53.9 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
  /// 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 {
101
    TestAsyncUtils.guardSync();
102
    return collectAllElementsFrom(binding.renderViewElement!, skipOffstage: false);
103 104 105 106 107 108
  }

  /// The matching element in the widget tree.
  ///
  /// Throws a [StateError] if `finder` is empty or matches more than
  /// one element.
109 110 111
  ///
  /// * 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.
112
  T element<T extends Element>(Finder finder) {
113
    TestAsyncUtils.guardSync();
114
    return finder.evaluate().single as T;
115 116 117 118 119 120
  }

  /// The first matching element according to a depth-first pre-order
  /// traversal of the widget tree.
  ///
  /// Throws a [StateError] if `finder` is empty.
121 122
  ///
  /// * Use [element] if you only expect to match one element.
123
  T firstElement<T extends Element>(Finder finder) {
124
    TestAsyncUtils.guardSync();
125
    return finder.evaluate().first as T;
126 127
  }

128 129 130 131
  /// 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.
132
  Iterable<T> elementList<T extends Element>(Finder finder) {
133
    TestAsyncUtils.guardSync();
134
    return finder.evaluate().cast<T>();
135 136
  }

137 138 139 140 141 142
  /// 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 {
143
    TestAsyncUtils.guardSync();
144
    return allElements.whereType<StatefulElement>().map<State>((StatefulElement element) => element.state);
145 146 147 148 149 150
  }

  /// 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.
151 152 153
  ///
  /// * 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.
154
  T state<T extends State>(Finder finder) {
155
    TestAsyncUtils.guardSync();
156
    return _stateOf<T>(finder.evaluate().single, finder);
157 158 159 160 161 162 163
  }

  /// 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.
164 165
  ///
  /// * Use [state] if you only expect to match one state.
166
  T firstState<T extends State>(Finder finder) {
167
    TestAsyncUtils.guardSync();
168
    return _stateOf<T>(finder.evaluate().first, finder);
169 170
  }

171 172 173 174 175 176 177
  /// 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.
178
  Iterable<T> stateList<T extends State>(Finder finder) {
179
    TestAsyncUtils.guardSync();
180
    return finder.evaluate().map<T>((Element element) => _stateOf<T>(element, finder));
181 182
  }

183
  T _stateOf<T extends State>(Element element, Finder finder) {
184
    TestAsyncUtils.guardSync();
185
    if (element is StatefulElement)
186
      return element.state as T;
187
    throw StateError('Widget of type ${element.widget.runtimeType}, with ${finder.description}, is not a StatefulWidget.');
188 189 190 191 192 193 194 195 196 197
  }

  /// 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 {
198
    TestAsyncUtils.guardSync();
199
    return allElements.map<RenderObject>((Element element) => element.renderObject!);
200 201 202 203 204 205
  }

  /// 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).
206 207 208
  ///
  /// * 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.
209
  T renderObject<T extends RenderObject>(Finder finder) {
210
    TestAsyncUtils.guardSync();
211
    return finder.evaluate().single.renderObject! as T;
212 213 214 215 216 217
  }

  /// 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.
218 219
  ///
  /// * Use [renderObject] if you only expect to match one render object.
220
  T firstRenderObject<T extends RenderObject>(Finder finder) {
221
    TestAsyncUtils.guardSync();
222
    return finder.evaluate().first.renderObject! as T;
223 224
  }

225 226 227 228
  /// 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.
229
  Iterable<T> renderObjectList<T extends RenderObject>(Finder finder) {
230
    TestAsyncUtils.guardSync();
231
    return finder.evaluate().map<T>((Element element) {
232
      final T result = element.renderObject! as T;
233 234
      return result;
    });
235 236
  }

237
  /// Returns a list of all the [Layer] objects in the rendering.
238
  List<Layer> get layers => _walkLayers(binding.renderView.debugLayer!).toList();
239
  Iterable<Layer> _walkLayers(Layer layer) sync* {
240
    TestAsyncUtils.guardSync();
241 242
    yield layer;
    if (layer is ContainerLayer) {
243
      final ContainerLayer root = layer;
244
      Layer? child = root.firstChild;
245 246 247 248 249 250 251 252 253 254
      while (child != null) {
        yield* _walkLayers(child);
        child = child.nextSibling;
      }
    }
  }

  // INTERACTION

  /// Dispatch a pointer down / pointer up sequence at the center of
255 256
  /// the given widget, assuming it is exposed.
  ///
257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273
  /// {@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);
274 275
  }

276
  /// Dispatch a pointer down / pointer up sequence at the given location.
277
  Future<void> tapAt(Offset location, {int? pointer, int buttons = kPrimaryButton}) {
278
    return TestAsyncUtils.guard<void>(() async {
279
      final TestGesture gesture = await startGesture(location, pointer: pointer, buttons: buttons);
280 281
      await gesture.up();
    });
282 283
  }

284 285 286
  /// Dispatch a pointer down at the center of the given widget, assuming it is
  /// exposed.
  ///
287 288 289 290 291 292 293 294 295 296 297
  /// {@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}) {
298
    return TestAsyncUtils.guard<TestGesture>(() {
299
      return startGesture(getCenter(finder, warnIfMissed: warnIfMissed, callee: 'press'), pointer: pointer, buttons: buttons);
300 301 302
    });
  }

303 304
  /// Dispatch a pointer down / pointer up sequence (with a delay of
  /// [kLongPressTimeout] + [kPressTimeout] between the two events) at the
305 306
  /// center of the given widget, assuming it is exposed.
  ///
307 308 309 310 311 312 313 314 315 316
  /// {@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);
317 318 319 320
  }

  /// Dispatch a pointer down / pointer up sequence at the given location with
  /// a delay of [kLongPressTimeout] + [kPressTimeout] between the two events.
321
  Future<void> longPressAt(Offset location, {int? pointer, int buttons = kPrimaryButton}) {
322
    return TestAsyncUtils.guard<void>(() async {
323
      final TestGesture gesture = await startGesture(location, pointer: pointer, buttons: buttons);
324 325 326 327 328
      await pump(kLongPressTimeout + kPressTimeout);
      await gesture.up();
    });
  }

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

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

432 433 434 435 436 437
  /// 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.
  ///
438 439 440 441 442 443 444
  /// 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,
445
  /// but a very small portion may have a tiny negative value for about tens of
446 447 448
  /// 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
449 450 451 452 453
  /// `records`.
  ///
  /// See [PointerEventRecord].
  Future<List<Duration>> handlePointerEventRecord(List<PointerEventRecord> records);

454 455 456 457 458
  /// 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.
459 460 461 462
  ///
  /// This is invoked by [flingFrom], for instance, so that the sequence of
  /// pointer events occurs over time.
  ///
463
  /// The [WidgetTester] subclass implements this by deferring to the [binding].
464 465 466
  ///
  /// See also [SchedulerBinding.endOfFrame], which returns a future that could
  /// be appropriate to return in the implementation of this method.
467
  Future<void> pump([Duration duration]);
468

469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496
  /// 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),
  ]);

497 498 499
  /// Attempts to drag the given widget by the given offset, by
  /// starting a drag in the middle of the widget.
  ///
500
  /// {@macro flutter.flutter_test.WidgetController.tap.warnIfMissed}
501 502 503
  ///
  /// 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.
504
  ///
505 506 507
  /// The operation happens at once. If you want the drag to last for a period
  /// of time, consider using [timedDrag].
  ///
508
  /// {@template flutter.flutter_test.WidgetController.drag}
509 510 511 512 513 514
  /// 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.
515 516 517 518 519 520
  ///
  /// 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.
  ///
521 522
  /// 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
523
  /// should be left to their default values.
524
  /// {@endtemplate}
525 526 527
  Future<void> drag(
    Finder finder,
    Offset offset, {
528
    int? pointer,
529 530 531
    int buttons = kPrimaryButton,
    double touchSlopX = kDragSlopDefault,
    double touchSlopY = kDragSlopDefault,
532
    bool warnIfMissed = true,
533
    PointerDeviceKind kind = PointerDeviceKind.touch,
534 535
  }) {
    return dragFrom(
536
      getCenter(finder, warnIfMissed: warnIfMissed, callee: 'drag'),
537 538 539 540 541
      offset,
      pointer: pointer,
      buttons: buttons,
      touchSlopX: touchSlopX,
      touchSlopY: touchSlopY,
542
      kind: kind,
543
    );
544 545 546 547
  }

  /// Attempts a drag gesture consisting of a pointer down, a move by
  /// the given offset, and a pointer up.
548 549 550 551
  ///
  /// 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.
552
  ///
553 554 555
  /// The operation happens at once. If you want the drag to last for a period
  /// of time, consider using [timedDragFrom].
  ///
556
  /// {@macro flutter.flutter_test.WidgetController.drag}
557 558 559
  Future<void> dragFrom(
    Offset startLocation,
    Offset offset, {
560
    int? pointer,
561 562 563
    int buttons = kPrimaryButton,
    double touchSlopX = kDragSlopDefault,
    double touchSlopY = kDragSlopDefault,
564
    PointerDeviceKind kind = PointerDeviceKind.touch,
565
  }) {
566
    assert(kDragSlopDefault > kTouchSlop);
567
    return TestAsyncUtils.guard<void>(() async {
568
      final TestGesture gesture = await startGesture(startLocation, pointer: pointer, buttons: buttons, kind: kind);
569
      assert(gesture != null);
570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 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

      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);
      }
635 636
      await gesture.up();
    });
637 638
  }

639 640 641
  /// Attempts to drag the given widget by the given offset in the `duration`
  /// time, starting in the middle of the widget.
  ///
642
  /// {@macro flutter.flutter_test.WidgetController.tap.warnIfMissed}
643 644 645 646 647
  ///
  /// 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`.
  ///
648
  /// {@template flutter.flutter_test.WidgetController.timedDrag}
649 650 651 652 653 654 655 656 657 658 659 660
  /// 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, {
661
    int? pointer,
662 663
    int buttons = kPrimaryButton,
    double frequency = 60.0,
664
    bool warnIfMissed = true,
665 666
  }) {
    return timedDragFrom(
667
      getCenter(finder, warnIfMissed: warnIfMissed, callee: 'timedDrag'),
668 669 670 671 672 673 674 675 676 677 678 679 680 681 682
      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`.
  ///
683
  /// {@macro flutter.flutter_test.WidgetController.timedDrag}
684 685 686 687
  Future<void> timedDragFrom(
    Offset startLocation,
    Offset offset,
    Duration duration, {
688
    int? pointer,
689 690 691 692 693 694
    int buttons = kPrimaryButton,
    double frequency = 60.0,
  }) {
    assert(frequency > 0);
    final int intervals = duration.inMicroseconds * frequency ~/ 1E6;
    assert(intervals > 1);
695
    pointer ??= _getNextPointer();
696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734
    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(
            timeStamp: Duration.zero,
            position: startLocation,
          ),
          PointerDownEvent(
            timeStamp: Duration.zero,
            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,
            )
          ]),
      ],
      PointerEventRecord(duration, <PointerEvent>[
        PointerUpEvent(
          timeStamp: duration,
          position: offsets.last,
          pointer: pointer,
735 736
          // The PointerData received from the engine with
          // change = PointerChange.up, which translates to PointerUpEvent,
737 738 739 740 741 742
          // doesn't provide the button field.
          // buttons: buttons,
        )
      ]),
    ];
    return TestAsyncUtils.guard<void>(() async {
743
      await handlePointerEventRecord(records);
744 745 746
    });
  }

747 748 749 750
  /// 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.
751
  int get nextPointer => _nextPointer;
752

753 754 755 756 757
  static int _nextPointer = 1;

  static int _getNextPointer() {
    final int result = _nextPointer;
    _nextPointer += 1;
758 759 760
    return result;
  }

761 762 763 764 765
  /// 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.
766
  Future<TestGesture> createGesture({
767
    int? pointer,
768 769 770 771
    PointerDeviceKind kind = PointerDeviceKind.touch,
    int buttons = kPrimaryButton,
  }) async {
    return TestGesture(
772
      dispatcher: sendEventToBinding,
773 774
      kind: kind,
      pointer: pointer ?? _getNextPointer(),
775
      buttons: buttons,
776
    );
777 778
  }

779 780 781 782 783 784
  /// 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.
785 786
  Future<TestGesture> startGesture(
    Offset downLocation, {
787
    int? pointer,
788
    PointerDeviceKind kind = PointerDeviceKind.touch,
789
    int buttons = kPrimaryButton,
790
  }) async {
791
    assert(downLocation != null);
792 793 794 795 796
    final TestGesture result = await createGesture(
      pointer: pointer,
      kind: kind,
      buttons: buttons,
    );
797 798 799 800
    await result.down(downLocation);
    return result;
  }

801
  /// Forwards the given location to the binding's hitTest logic.
802
  HitTestResult hitTestOnBinding(Offset location) {
803
    final HitTestResult result = HitTestResult();
804 805 806 807
    binding.hitTest(result, location);
    return result;
  }

808
  /// Forwards the given pointer event to the binding.
809
  Future<void> sendEventToBinding(PointerEvent event) {
810
    return TestAsyncUtils.guard<void>(() async {
811
      binding.handlePointerEvent(event);
812
    });
813 814
  }

815 816 817 818 819 820 821 822 823 824
  /// 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);
  }

825 826 827
  // GEOMETRY

  /// Returns the point at the center of the given widget.
828 829 830 831 832 833 834 835 836 837 838 839 840 841
  ///
  /// {@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);
842 843 844
  }

  /// Returns the point at the top left of the given widget.
845 846 847 848
  ///
  /// {@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);
849 850 851 852
  }

  /// Returns the point at the top right of the given widget. This
  /// point is not inside the object's hit test area.
853 854 855 856
  ///
  /// {@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);
857 858 859 860
  }

  /// Returns the point at the bottom left of the given widget. This
  /// point is not inside the object's hit test area.
861 862 863 864
  ///
  /// {@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);
865 866 867 868
  }

  /// Returns the point at the bottom right of the given widget. This
  /// point is not inside the object's hit test area.
869 870 871 872
  ///
  /// {@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);
873 874
  }

875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897
  /// 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;

898
  Offset _getElementPoint(Finder finder, Offset Function(Size size) sizeToPoint, { required bool warnIfMissed, required String callee }) {
899
    TestAsyncUtils.guardSync();
900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917
    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"). '
918
        'Unfortunately "$callee()" only supports targeting widgets that correspond to RenderBox objects in the rendering.'
919 920
      );
    }
921
    final RenderBox box = element.renderObject! as RenderBox;
922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 946 947 948 949 950 951 952 953 954 955 956 957 958 959
    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'
960
          'To make this warning fatal, set WidgetController.hitTestWarningShouldBeFatal to true.\n',
961 962 963 964
        );
      }
    }
    return location;
965 966 967 968 969
  }

  /// 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) {
970
    TestAsyncUtils.guardSync();
971
    final Element element = finder.evaluate().single;
972
    final RenderBox box = element.renderObject! as RenderBox;
973 974
    return box.size;
  }
975

976
  /// Simulates sending physical key down and up events.
977 978 979 980 981
  ///
  /// This only simulates key events coming from a physical keyboard, not from a
  /// soft keyboard.
  ///
  /// Specify `platform` as one of the platforms allowed in
982 983 984 985
  /// [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.
986
  ///
987 988 989
  /// Whether the event is sent through [RawKeyEvent] or [KeyEvent] is
  /// controlled by [debugKeyEventSimulatorTransitModeOverride].
  ///
990 991 992 993 994 995
  /// 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].
  ///
996 997
  /// Returns true if the key down event was handled by the framework.
  ///
998 999 1000 1001
  /// See also:
  ///
  ///  - [sendKeyDownEvent] to simulate only a key down event.
  ///  - [sendKeyUpEvent] to simulate only a key up event.
Tong Mu's avatar
Tong Mu committed
1002
  Future<bool> sendKeyEvent(LogicalKeyboardKey key, { String platform = _defaultPlatform }) async {
1003
    assert(platform != null);
1004
    final bool handled = await simulateKeyDownEvent(key, platform: platform);
1005
    // Internally wrapped in async guard.
1006 1007
    await simulateKeyUpEvent(key, platform: platform);
    return handled;
1008 1009
  }

1010
  /// Simulates sending a physical key down event.
1011 1012 1013 1014 1015
  ///
  /// 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
1016 1017 1018 1019
  /// [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.
1020
  ///
1021 1022 1023
  /// Whether the event is sent through [RawKeyEvent] or [KeyEvent] is
  /// controlled by [debugKeyEventSimulatorTransitModeOverride].
  ///
1024 1025
  /// Keys that are down when the test completes are cleared after each test.
  ///
1026 1027
  /// Returns true if the key event was handled by the framework.
  ///
1028 1029
  /// See also:
  ///
1030 1031
  ///  - [sendKeyUpEvent] and [sendKeyRepeatEvent] to simulate the corresponding
  ///    key up and repeat event.
1032
  ///  - [sendKeyEvent] to simulate both the key up and key down in the same call.
Tong Mu's avatar
Tong Mu committed
1033
  Future<bool> sendKeyDownEvent(LogicalKeyboardKey key, { String? character, String platform = _defaultPlatform }) async {
1034 1035
    assert(platform != null);
    // Internally wrapped in async guard.
Tong Mu's avatar
Tong Mu committed
1036
    return simulateKeyDownEvent(key, character: character, platform: platform);
1037 1038 1039 1040 1041 1042 1043 1044
  }

  /// 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
1045 1046 1047
  /// [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.
1048
  ///
1049 1050 1051
  /// Whether the event is sent through [RawKeyEvent] or [KeyEvent] is
  /// controlled by [debugKeyEventSimulatorTransitModeOverride].
  ///
1052 1053
  /// Returns true if the key event was handled by the framework.
  ///
1054 1055
  /// See also:
  ///
1056 1057
  ///  - [sendKeyDownEvent] and [sendKeyRepeatEvent] to simulate the
  ///    corresponding key down and repeat event.
1058
  ///  - [sendKeyEvent] to simulate both the key up and key down in the same call.
Tong Mu's avatar
Tong Mu committed
1059
  Future<bool> sendKeyUpEvent(LogicalKeyboardKey key, { String platform = _defaultPlatform }) async {
1060 1061 1062 1063 1064
    assert(platform != null);
    // Internally wrapped in async guard.
    return simulateKeyUpEvent(key, platform: platform);
  }

1065 1066 1067 1068 1069 1070 1071 1072 1073 1074 1075 1076 1077 1078 1079 1080 1081 1082 1083 1084 1085 1086 1087 1088 1089 1090 1091 1092 1093
  /// Simulates sending a physical key repeat event.
  ///
  /// 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.
  ///
  /// 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.
  Future<bool> sendKeyRepeatEvent(LogicalKeyboardKey key, { String? character, String platform = _defaultPlatform }) async {
    assert(platform != null);
    // Internally wrapped in async guard.
    return simulateKeyRepeatEvent(key, character: character, platform: platform);
  }

1094 1095 1096
  /// Returns the rect of the given widget. This is only valid once
  /// the widget's render object has been laid out at least once.
  Rect getRect(Finder finder) => getTopLeft(finder) & getSize(finder);
1097

1098 1099 1100 1101 1102 1103 1104 1105 1106 1107 1108 1109 1110 1111 1112 1113 1114 1115 1116 1117 1118 1119 1120 1121 1122
  /// 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) {
    if (binding.pipelineOwner.semanticsOwner == null)
      throw StateError('Semantics are not enabled.');
    final Iterable<Element> candidates = finder.evaluate();
    if (candidates.isEmpty) {
      throw StateError('Finder returned no matching elements.');
    }
    if (candidates.length > 1) {
      throw StateError('Finder returned more than one element.');
    }
    final Element element = candidates.single;
1123 1124
    RenderObject? renderObject = element.findRenderObject();
    SemanticsNode? result = renderObject?.debugSemantics;
1125
    while (renderObject != null && (result == null || result.isMergedIntoParent)) {
1126
      renderObject = renderObject.parent as RenderObject?;
1127 1128 1129 1130 1131 1132 1133 1134 1135 1136 1137 1138 1139 1140
      result = renderObject?.debugSemantics;
    }
    if (result == null)
      throw StateError('No Semantics data found.');
    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();
  }

1141 1142 1143
  /// 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.
  ///
1144 1145 1146
  /// Usually the `finder` for this method should be labeled `skipOffstage:
  /// false`, so that the [Finder] deals with widgets that are off the screen
  /// correctly.
1147
  ///
1148 1149 1150
  /// 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.
1151
  ///
1152 1153 1154 1155
  /// See also:
  ///
  ///  * [Scrollable.ensureVisible], which is the production API used to
  ///    implement this method.
1156
  Future<void> ensureVisible(Finder finder) => Scrollable.ensureVisible(element(finder));
1157

1158
  /// Repeatedly scrolls a [Scrollable] by `delta` in the
1159 1160
  /// [Scrollable.axisDirection] direction until a widget matching `finder` is
  /// visible.
1161
  ///
1162
  /// Between each scroll, advances the clock by `duration` time.
1163
  ///
1164 1165
  /// Scrolling is performed until the start of the `finder` is visible. This is
  /// due to the default parameter values of the [Scrollable.ensureVisible] method.
1166
  ///
1167 1168 1169 1170
  /// 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.
1171 1172
  ///
  /// This is different from [ensureVisible] in that this allows looking for
1173
  /// `finder` that is not yet built. The caller must specify the scrollable
1174
  /// that will build child specified by `finder` when there are multiple
1175
  /// [Scrollable]s.
1176
  ///
1177
  /// See also:
1178
  ///
1179
  ///  * [dragUntilVisible], which implements the body of this method.
1180 1181 1182
  Future<void> scrollUntilVisible(
    Finder finder,
    double delta, {
1183
      Finder? scrollable,
1184 1185 1186 1187 1188
      int maxScrolls = 50,
      Duration duration = const Duration(milliseconds: 50),
    }
  ) {
    assert(maxScrolls > 0);
1189
    scrollable ??= find.byType(Scrollable);
1190 1191
    return TestAsyncUtils.guard<void>(() async {
      Offset moveStep;
1192
      switch (widget<Scrollable>(scrollable!).axisDirection) {
1193 1194 1195 1196 1197 1198 1199 1200 1201 1202 1203 1204 1205
        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;
      }
1206 1207 1208 1209 1210
      await dragUntilVisible(
        finder,
        scrollable,
        moveStep,
        maxIteration: maxScrolls,
1211 1212
        duration: duration,
      );
1213 1214 1215
    });
  }

1216 1217 1218
  /// Repeatedly drags `view` by `moveStep` until `finder` is visible.
  ///
  /// Between each drag, advances the clock by `duration`.
1219
  ///
1220 1221 1222 1223
  /// Throws a [StateError] if `finder` is not found after `maxIteration`
  /// drags.
  ///
  /// See also:
1224
  ///
1225 1226
  ///  * [scrollUntilVisible], which wraps this method with an API that is more
  ///    convenient when dealing with a [Scrollable].
1227 1228 1229 1230 1231 1232 1233 1234
  Future<void> dragUntilVisible(
    Finder finder,
    Finder view,
    Offset moveStep, {
      int maxIteration = 50,
      Duration duration = const Duration(milliseconds: 50),
  }) {
    return TestAsyncUtils.guard<void>(() async {
1235
      while (maxIteration > 0 && finder.evaluate().isEmpty) {
1236
        await drag(view, moveStep);
1237
        await pump(duration);
1238
        maxIteration -= 1;
1239 1240 1241 1242
      }
      await Scrollable.ensureVisible(element(finder));
    });
  }
1243
}
1244 1245 1246 1247 1248 1249 1250 1251 1252 1253

/// 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.
  LiveWidgetController(WidgetsBinding binding) : super(binding);

  @override
1254
  Future<void> pump([Duration? duration]) async {
1255
    if (duration != null)
1256
      await Future<void>.delayed(duration);
1257 1258 1259
    binding.scheduleFrame();
    await binding.endOfFrame;
  }
1260

1261 1262 1263 1264 1265 1266 1267 1268 1269 1270 1271 1272 1273 1274 1275 1276
  @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;
    });
  }

1277 1278
  @override
  Future<List<Duration>> handlePointerEventRecord(List<PointerEventRecord> records) {
1279 1280 1281 1282 1283 1284 1285
    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>[];
1286
      DateTime? startTime;
1287 1288 1289 1290 1291 1292 1293 1294 1295 1296
      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);
1297
          record.events.forEach(binding.handlePointerEvent);
1298 1299 1300 1301 1302 1303 1304 1305
        } 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,
          );
1306
          record.events.forEach(binding.handlePointerEvent);
1307 1308 1309 1310 1311 1312 1313 1314
        }
      }
      // This makes sure that a gesture is completed, with no more pointers
      // active.
      assert(hitTestHistory.isEmpty);
      return handleTimeStampDiff;
    });
  }
1315
}