• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1// Copyright 2016 The Chromium Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5import 'dart:async';
6
7import 'package:flutter/gestures.dart';
8import 'package:flutter/rendering.dart';
9import 'package:flutter/widgets.dart';
10
11import 'all_elements.dart';
12import 'finders.dart';
13import 'test_async_utils.dart';
14import 'test_pointer.dart';
15
16/// The default drag touch slop used to break up a large drag into multiple
17/// smaller moves.
18///
19/// This value must be greater than [kTouchSlop].
20const double kDragSlopDefault = 20.0;
21
22/// Class that programmatically interacts with widgets.
23///
24/// For a variant of this class suited specifically for unit tests, see
25/// [WidgetTester]. For one suitable for live tests on a device, consider
26/// [LiveWidgetController].
27///
28/// Concrete subclasses must implement the [pump] method.
29abstract class WidgetController {
30  /// Creates a widget controller that uses the given binding.
31  WidgetController(this.binding);
32
33  /// A reference to the current instance of the binding.
34  final WidgetsBinding binding;
35
36  // FINDER API
37
38  // TODO(ianh): verify that the return values are of type T and throw
39  // a good message otherwise, in all the generic methods below
40
41  /// Checks if `finder` exists in the tree.
42  bool any(Finder finder) {
43    TestAsyncUtils.guardSync();
44    return finder.evaluate().isNotEmpty;
45  }
46
47  /// All widgets currently in the widget tree (lazy pre-order traversal).
48  ///
49  /// Can contain duplicates, since widgets can be used in multiple
50  /// places in the widget tree.
51  Iterable<Widget> get allWidgets {
52    TestAsyncUtils.guardSync();
53    return allElements.map<Widget>((Element element) => element.widget);
54  }
55
56  /// The matching widget in the widget tree.
57  ///
58  /// Throws a [StateError] if `finder` is empty or matches more than
59  /// one widget.
60  ///
61  /// * Use [firstWidget] if you expect to match several widgets but only want the first.
62  /// * Use [widgetList] if you expect to match several widgets and want all of them.
63  T widget<T extends Widget>(Finder finder) {
64    TestAsyncUtils.guardSync();
65    return finder.evaluate().single.widget;
66  }
67
68  /// The first matching widget according to a depth-first pre-order
69  /// traversal of the widget tree.
70  ///
71  /// Throws a [StateError] if `finder` is empty.
72  ///
73  /// * Use [widget] if you only expect to match one widget.
74  T firstWidget<T extends Widget>(Finder finder) {
75    TestAsyncUtils.guardSync();
76    return finder.evaluate().first.widget;
77  }
78
79  /// The matching widgets in the widget tree.
80  ///
81  /// * Use [widget] if you only expect to match one widget.
82  /// * Use [firstWidget] if you expect to match several but only want the first.
83  Iterable<T> widgetList<T extends Widget>(Finder finder) {
84    TestAsyncUtils.guardSync();
85    return finder.evaluate().map<T>((Element element) {
86      final T result = element.widget;
87      return result;
88    });
89  }
90
91  /// All elements currently in the widget tree (lazy pre-order traversal).
92  ///
93  /// The returned iterable is lazy. It does not walk the entire widget tree
94  /// immediately, but rather a chunk at a time as the iteration progresses
95  /// using [Iterator.moveNext].
96  Iterable<Element> get allElements {
97    TestAsyncUtils.guardSync();
98    return collectAllElementsFrom(binding.renderViewElement, skipOffstage: false);
99  }
100
101  /// The matching element in the widget tree.
102  ///
103  /// Throws a [StateError] if `finder` is empty or matches more than
104  /// one element.
105  ///
106  /// * Use [firstElement] if you expect to match several elements but only want the first.
107  /// * Use [elementList] if you expect to match several elements and want all of them.
108  T element<T extends Element>(Finder finder) {
109    TestAsyncUtils.guardSync();
110    return finder.evaluate().single;
111  }
112
113  /// The first matching element according to a depth-first pre-order
114  /// traversal of the widget tree.
115  ///
116  /// Throws a [StateError] if `finder` is empty.
117  ///
118  /// * Use [element] if you only expect to match one element.
119  T firstElement<T extends Element>(Finder finder) {
120    TestAsyncUtils.guardSync();
121    return finder.evaluate().first;
122  }
123
124  /// The matching elements in the widget tree.
125  ///
126  /// * Use [element] if you only expect to match one element.
127  /// * Use [firstElement] if you expect to match several but only want the first.
128  Iterable<T> elementList<T extends Element>(Finder finder) {
129    TestAsyncUtils.guardSync();
130    return finder.evaluate();
131  }
132
133  /// All states currently in the widget tree (lazy pre-order traversal).
134  ///
135  /// The returned iterable is lazy. It does not walk the entire widget tree
136  /// immediately, but rather a chunk at a time as the iteration progresses
137  /// using [Iterator.moveNext].
138  Iterable<State> get allStates {
139    TestAsyncUtils.guardSync();
140    return allElements.whereType<StatefulElement>().map<State>((StatefulElement element) => element.state);
141  }
142
143  /// The matching state in the widget tree.
144  ///
145  /// Throws a [StateError] if `finder` is empty, matches more than
146  /// one state, or matches a widget that has no state.
147  ///
148  /// * Use [firstState] if you expect to match several states but only want the first.
149  /// * Use [stateList] if you expect to match several states and want all of them.
150  T state<T extends State>(Finder finder) {
151    TestAsyncUtils.guardSync();
152    return _stateOf<T>(finder.evaluate().single, finder);
153  }
154
155  /// The first matching state according to a depth-first pre-order
156  /// traversal of the widget tree.
157  ///
158  /// Throws a [StateError] if `finder` is empty or if the first
159  /// matching widget has no state.
160  ///
161  /// * Use [state] if you only expect to match one state.
162  T firstState<T extends State>(Finder finder) {
163    TestAsyncUtils.guardSync();
164    return _stateOf<T>(finder.evaluate().first, finder);
165  }
166
167  /// The matching states in the widget tree.
168  ///
169  /// Throws a [StateError] if any of the elements in `finder` match a widget
170  /// that has no state.
171  ///
172  /// * Use [state] if you only expect to match one state.
173  /// * Use [firstState] if you expect to match several but only want the first.
174  Iterable<T> stateList<T extends State>(Finder finder) {
175    TestAsyncUtils.guardSync();
176    return finder.evaluate().map<T>((Element element) => _stateOf<T>(element, finder));
177  }
178
179  T _stateOf<T extends State>(Element element, Finder finder) {
180    TestAsyncUtils.guardSync();
181    if (element is StatefulElement)
182      return element.state;
183    throw StateError('Widget of type ${element.widget.runtimeType}, with ${finder.description}, is not a StatefulWidget.');
184  }
185
186  /// Render objects of all the widgets currently in the widget tree
187  /// (lazy pre-order traversal).
188  ///
189  /// This will almost certainly include many duplicates since the
190  /// render object of a [StatelessWidget] or [StatefulWidget] is the
191  /// render object of its child; only [RenderObjectWidget]s have
192  /// their own render object.
193  Iterable<RenderObject> get allRenderObjects {
194    TestAsyncUtils.guardSync();
195    return allElements.map<RenderObject>((Element element) => element.renderObject);
196  }
197
198  /// The render object of the matching widget in the widget tree.
199  ///
200  /// Throws a [StateError] if `finder` is empty or matches more than
201  /// one widget (even if they all have the same render object).
202  ///
203  /// * Use [firstRenderObject] if you expect to match several render objects but only want the first.
204  /// * Use [renderObjectList] if you expect to match several render objects and want all of them.
205  T renderObject<T extends RenderObject>(Finder finder) {
206    TestAsyncUtils.guardSync();
207    return finder.evaluate().single.renderObject;
208  }
209
210  /// The render object of the first matching widget according to a
211  /// depth-first pre-order traversal of the widget tree.
212  ///
213  /// Throws a [StateError] if `finder` is empty.
214  ///
215  /// * Use [renderObject] if you only expect to match one render object.
216  T firstRenderObject<T extends RenderObject>(Finder finder) {
217    TestAsyncUtils.guardSync();
218    return finder.evaluate().first.renderObject;
219  }
220
221  /// The render objects of the matching widgets in the widget tree.
222  ///
223  /// * Use [renderObject] if you only expect to match one render object.
224  /// * Use [firstRenderObject] if you expect to match several but only want the first.
225  Iterable<T> renderObjectList<T extends RenderObject>(Finder finder) {
226    TestAsyncUtils.guardSync();
227    return finder.evaluate().map<T>((Element element) {
228      final T result = element.renderObject;
229      return result;
230    });
231  }
232
233  /// Returns a list of all the [Layer] objects in the rendering.
234  List<Layer> get layers => _walkLayers(binding.renderView.debugLayer).toList();
235  Iterable<Layer> _walkLayers(Layer layer) sync* {
236    TestAsyncUtils.guardSync();
237    yield layer;
238    if (layer is ContainerLayer) {
239      final ContainerLayer root = layer;
240      Layer child = root.firstChild;
241      while (child != null) {
242        yield* _walkLayers(child);
243        child = child.nextSibling;
244      }
245    }
246  }
247
248  // INTERACTION
249
250  /// Dispatch a pointer down / pointer up sequence at the center of
251  /// the given widget, assuming it is exposed.
252  ///
253  /// If the center of the widget is not exposed, this might send events to
254  /// another object.
255  Future<void> tap(Finder finder, {int pointer, int buttons = kPrimaryButton}) {
256    return tapAt(getCenter(finder), pointer: pointer, buttons: buttons);
257  }
258
259  /// Dispatch a pointer down / pointer up sequence at the given location.
260  Future<void> tapAt(Offset location, {int pointer, int buttons = kPrimaryButton}) {
261    return TestAsyncUtils.guard<void>(() async {
262      final TestGesture gesture = await startGesture(location, pointer: pointer, buttons: buttons);
263      await gesture.up();
264    });
265  }
266
267  /// Dispatch a pointer down at the center of the given widget, assuming it is
268  /// exposed.
269  ///
270  /// If the center of the widget is not exposed, this might send events to
271  /// another object.
272  Future<TestGesture> press(Finder finder, {int pointer, int buttons = kPrimaryButton}) {
273    return TestAsyncUtils.guard<TestGesture>(() {
274      return startGesture(getCenter(finder), pointer: pointer, buttons: buttons);
275    });
276  }
277
278  /// Dispatch a pointer down / pointer up sequence (with a delay of
279  /// [kLongPressTimeout] + [kPressTimeout] between the two events) at the
280  /// center of the given widget, assuming it is exposed.
281  ///
282  /// If the center of the widget is not exposed, this might send events to
283  /// another object.
284  Future<void> longPress(Finder finder, {int pointer, int buttons = kPrimaryButton}) {
285    return longPressAt(getCenter(finder), pointer: pointer, buttons: buttons);
286  }
287
288  /// Dispatch a pointer down / pointer up sequence at the given location with
289  /// a delay of [kLongPressTimeout] + [kPressTimeout] between the two events.
290  Future<void> longPressAt(Offset location, {int pointer, int buttons = kPrimaryButton}) {
291    return TestAsyncUtils.guard<void>(() async {
292      final TestGesture gesture = await startGesture(location, pointer: pointer, buttons: buttons);
293      await pump(kLongPressTimeout + kPressTimeout);
294      await gesture.up();
295    });
296  }
297
298  /// Attempts a fling gesture starting from the center of the given
299  /// widget, moving the given distance, reaching the given speed.
300  ///
301  /// If the middle of the widget is not exposed, this might send
302  /// events to another object.
303  ///
304  /// This can pump frames. See [flingFrom] for a discussion of how the
305  /// `offset`, `velocity` and `frameInterval` arguments affect this.
306  ///
307  /// The `speed` is in pixels per second in the direction given by `offset`.
308  ///
309  /// A fling is essentially a drag that ends at a particular speed. If you
310  /// just want to drag and end without a fling, use [drag].
311  ///
312  /// The `initialOffset` argument, if non-zero, causes the pointer to first
313  /// apply that offset, then pump a delay of `initialOffsetDelay`. This can be
314  /// used to simulate a drag followed by a fling, including dragging in the
315  /// opposite direction of the fling (e.g. dragging 200 pixels to the right,
316  /// then fling to the left over 200 pixels, ending at the exact point that the
317  /// drag started).
318  Future<void> fling(
319    Finder finder,
320    Offset offset,
321    double speed, {
322    int pointer,
323    int buttons = kPrimaryButton,
324    Duration frameInterval = const Duration(milliseconds: 16),
325    Offset initialOffset = Offset.zero,
326    Duration initialOffsetDelay = const Duration(seconds: 1),
327  }) {
328    return flingFrom(
329      getCenter(finder),
330      offset,
331      speed,
332      pointer: pointer,
333      buttons: buttons,
334      frameInterval: frameInterval,
335      initialOffset: initialOffset,
336      initialOffsetDelay: initialOffsetDelay,
337    );
338  }
339
340  /// Attempts a fling gesture starting from the given location, moving the
341  /// given distance, reaching the given speed.
342  ///
343  /// Exactly 50 pointer events are synthesized.
344  ///
345  /// The offset and speed control the interval between each pointer event. For
346  /// example, if the offset is 200 pixels down, and the speed is 800 pixels per
347  /// second, the pointer events will be sent for each increment of 4 pixels
348  /// (200/50), over 250ms (200/800), meaning events will be sent every 1.25ms
349  /// (250/200).
350  ///
351  /// To make tests more realistic, frames may be pumped during this time (using
352  /// calls to [pump]). If the total duration is longer than `frameInterval`,
353  /// then one frame is pumped each time that amount of time elapses while
354  /// sending events, or each time an event is synthesized, whichever is rarer.
355  ///
356  /// A fling is essentially a drag that ends at a particular speed. If you
357  /// just want to drag and end without a fling, use [dragFrom].
358  ///
359  /// The `initialOffset` argument, if non-zero, causes the pointer to first
360  /// apply that offset, then pump a delay of `initialOffsetDelay`. This can be
361  /// used to simulate a drag followed by a fling, including dragging in the
362  /// opposite direction of the fling (e.g. dragging 200 pixels to the right,
363  /// then fling to the left over 200 pixels, ending at the exact point that the
364  /// drag started).
365  Future<void> flingFrom(
366    Offset startLocation,
367    Offset offset,
368    double speed, {
369    int pointer,
370    int buttons = kPrimaryButton,
371    Duration frameInterval = const Duration(milliseconds: 16),
372    Offset initialOffset = Offset.zero,
373    Duration initialOffsetDelay = const Duration(seconds: 1),
374  }) {
375    assert(offset.distance > 0.0);
376    assert(speed > 0.0); // speed is pixels/second
377    return TestAsyncUtils.guard<void>(() async {
378      final TestPointer testPointer = TestPointer(pointer ?? _getNextPointer(), PointerDeviceKind.touch, null, buttons);
379      final HitTestResult result = hitTestOnBinding(startLocation);
380      const int kMoveCount = 50; // Needs to be >= kHistorySize, see _LeastSquaresVelocityTrackerStrategy
381      final double timeStampDelta = 1000.0 * offset.distance / (kMoveCount * speed);
382      double timeStamp = 0.0;
383      double lastTimeStamp = timeStamp;
384      await sendEventToBinding(testPointer.down(startLocation, timeStamp: Duration(milliseconds: timeStamp.round())), result);
385      if (initialOffset.distance > 0.0) {
386        await sendEventToBinding(testPointer.move(startLocation + initialOffset, timeStamp: Duration(milliseconds: timeStamp.round())), result);
387        timeStamp += initialOffsetDelay.inMilliseconds;
388        await pump(initialOffsetDelay);
389      }
390      for (int i = 0; i <= kMoveCount; i += 1) {
391        final Offset location = startLocation + initialOffset + Offset.lerp(Offset.zero, offset, i / kMoveCount);
392        await sendEventToBinding(testPointer.move(location, timeStamp: Duration(milliseconds: timeStamp.round())), result);
393        timeStamp += timeStampDelta;
394        if (timeStamp - lastTimeStamp > frameInterval.inMilliseconds) {
395          await pump(Duration(milliseconds: (timeStamp - lastTimeStamp).truncate()));
396          lastTimeStamp = timeStamp;
397        }
398      }
399      await sendEventToBinding(testPointer.up(timeStamp: Duration(milliseconds: timeStamp.round())), result);
400    });
401  }
402
403  /// Called to indicate that time should advance.
404  ///
405  /// This is invoked by [flingFrom], for instance, so that the sequence of
406  /// pointer events occurs over time.
407  ///
408  /// The [WidgetTester] subclass implements this by deferring to the [binding].
409  ///
410  /// See also [SchedulerBinding.endOfFrame], which returns a future that could
411  /// be appropriate to return in the implementation of this method.
412  Future<void> pump(Duration duration);
413
414  /// Attempts to drag the given widget by the given offset, by
415  /// starting a drag in the middle of the widget.
416  ///
417  /// If the middle of the widget is not exposed, this might send
418  /// events to another object.
419  ///
420  /// If you want the drag to end with a speed so that the gesture recognition
421  /// system identifies the gesture as a fling, consider using [fling] instead.
422  ///
423  /// {@template flutter.flutter_test.drag}
424  /// By default, if the x or y component of offset is greater than [kTouchSlop], the
425  /// gesture is broken up into two separate moves calls. Changing 'touchSlopX' or
426  /// `touchSlopY` will change the minimum amount of movement in the respective axis
427  /// before the drag will be broken into multiple calls. To always send the
428  /// drag with just a single call to [TestGesture.moveBy], `touchSlopX` and `touchSlopY`
429  /// should be set to 0.
430  ///
431  /// Breaking the drag into multiple moves is necessary for accurate execution
432  /// of drag update calls with a [DragStartBehavior] variable set to
433  /// [DragStartBehavior.start]. Without such a change, the dragUpdate callback
434  /// from a drag recognizer will never be invoked.
435  ///
436  /// To force this function to a send a single move event, the 'touchSlopX' and
437  /// 'touchSlopY' variables should be set to 0. However, generally, these values
438  /// should be left to their default values.
439  /// {@end template}
440  Future<void> drag(
441    Finder finder,
442    Offset offset, {
443    int pointer,
444    int buttons = kPrimaryButton,
445    double touchSlopX = kDragSlopDefault,
446    double touchSlopY = kDragSlopDefault,
447  }) {
448    assert(kDragSlopDefault > kTouchSlop);
449    return dragFrom(
450      getCenter(finder),
451      offset,
452      pointer: pointer,
453      buttons: buttons,
454      touchSlopX: touchSlopX,
455      touchSlopY: touchSlopY,
456    );
457  }
458
459  /// Attempts a drag gesture consisting of a pointer down, a move by
460  /// the given offset, and a pointer up.
461  ///
462  /// If you want the drag to end with a speed so that the gesture recognition
463  /// system identifies the gesture as a fling, consider using [flingFrom]
464  /// instead.
465  ///
466  /// {@macro flutter.flutter_test.drag}
467  Future<void> dragFrom(
468    Offset startLocation,
469    Offset offset, {
470    int pointer,
471    int buttons = kPrimaryButton,
472    double touchSlopX = kDragSlopDefault,
473    double touchSlopY = kDragSlopDefault,
474  }) {
475    assert(kDragSlopDefault > kTouchSlop);
476    return TestAsyncUtils.guard<void>(() async {
477      final TestGesture gesture = await startGesture(startLocation, pointer: pointer, buttons: buttons);
478      assert(gesture != null);
479
480      final double xSign = offset.dx.sign;
481      final double ySign = offset.dy.sign;
482
483      final double offsetX = offset.dx;
484      final double offsetY = offset.dy;
485
486      final bool separateX = offset.dx.abs() > touchSlopX && touchSlopX > 0;
487      final bool separateY = offset.dy.abs() > touchSlopY && touchSlopY > 0;
488
489      if (separateY || separateX) {
490        final double offsetSlope = offsetY / offsetX;
491        final double inverseOffsetSlope = offsetX / offsetY;
492        final double slopSlope = touchSlopY / touchSlopX;
493        final double absoluteOffsetSlope = offsetSlope.abs();
494        final double signedSlopX = touchSlopX * xSign;
495        final double signedSlopY = touchSlopY * ySign;
496        if (absoluteOffsetSlope != slopSlope) {
497          // The drag goes through one or both of the extents of the edges of the box.
498          if (absoluteOffsetSlope < slopSlope) {
499            assert(offsetX.abs() > touchSlopX);
500            // The drag goes through the vertical edge of the box.
501            // It is guaranteed that the |offsetX| > touchSlopX.
502            final double diffY = offsetSlope.abs() * touchSlopX * ySign;
503
504            // The vector from the origin to the vertical edge.
505            await gesture.moveBy(Offset(signedSlopX, diffY));
506            if (offsetY.abs() <= touchSlopY) {
507              // The drag ends on or before getting to the horizontal extension of the horizontal edge.
508              await gesture.moveBy(Offset(offsetX - signedSlopX, offsetY - diffY));
509            } else {
510              final double diffY2 = signedSlopY - diffY;
511              final double diffX2 = inverseOffsetSlope * diffY2;
512
513              // The vector from the edge of the box to the horizontal extension of the horizontal edge.
514              await gesture.moveBy(Offset(diffX2, diffY2));
515              await gesture.moveBy(Offset(offsetX - diffX2 - signedSlopX, offsetY - signedSlopY));
516            }
517          } else {
518            assert(offsetY.abs() > touchSlopY);
519            // The drag goes through the horizontal edge of the box.
520            // It is guaranteed that the |offsetY| > touchSlopY.
521            final double diffX = inverseOffsetSlope.abs() * touchSlopY * xSign;
522
523            // The vector from the origin to the vertical edge.
524            await gesture.moveBy(Offset(diffX, signedSlopY));
525            if (offsetX.abs() <= touchSlopX) {
526              // The drag ends on or before getting to the vertical extension of the vertical edge.
527              await gesture.moveBy(Offset(offsetX - diffX, offsetY - signedSlopY));
528            } else {
529              final double diffX2 = signedSlopX - diffX;
530              final double diffY2 = offsetSlope * diffX2;
531
532              // The vector from the edge of the box to the vertical extension of the vertical edge.
533              await gesture.moveBy(Offset(diffX2, diffY2));
534              await gesture.moveBy(Offset(offsetX - signedSlopX, offsetY - diffY2 - signedSlopY));
535            }
536          }
537        } else { // The drag goes through the corner of the box.
538          await gesture.moveBy(Offset(signedSlopX, signedSlopY));
539          await gesture.moveBy(Offset(offsetX - signedSlopX, offsetY - signedSlopY));
540        }
541      } else { // The drag ends inside the box.
542        await gesture.moveBy(offset);
543      }
544      await gesture.up();
545    });
546  }
547
548  /// The next available pointer identifier.
549  ///
550  /// This is the default pointer identifier that will be used the next time the
551  /// [startGesture] method is called without an explicit pointer identifier.
552  int nextPointer = 1;
553
554  int _getNextPointer() {
555    final int result = nextPointer;
556    nextPointer += 1;
557    return result;
558  }
559
560  /// Creates gesture and returns the [TestGesture] object which you can use
561  /// to continue the gesture using calls on the [TestGesture] object.
562  ///
563  /// You can use [startGesture] instead if your gesture begins with a down
564  /// event.
565  Future<TestGesture> createGesture({
566    int pointer,
567    PointerDeviceKind kind = PointerDeviceKind.touch,
568    int buttons = kPrimaryButton,
569  }) async {
570    return TestGesture(
571      hitTester: hitTestOnBinding,
572      dispatcher: sendEventToBinding,
573      kind: kind,
574      pointer: pointer ?? _getNextPointer(),
575      buttons: buttons,
576    );
577  }
578
579  /// Creates a gesture with an initial down gesture at a particular point, and
580  /// returns the [TestGesture] object which you can use to continue the
581  /// gesture.
582  ///
583  /// You can use [createGesture] if your gesture doesn't begin with an initial
584  /// down gesture.
585  Future<TestGesture> startGesture(
586    Offset downLocation, {
587    int pointer,
588    PointerDeviceKind kind = PointerDeviceKind.touch,
589    int buttons = kPrimaryButton,
590  }) async {
591    final TestGesture result = await createGesture(
592      pointer: pointer,
593      kind: kind,
594      buttons: buttons,
595    );
596    await result.down(downLocation);
597    return result;
598  }
599
600  /// Forwards the given location to the binding's hitTest logic.
601  HitTestResult hitTestOnBinding(Offset location) {
602    final HitTestResult result = HitTestResult();
603    binding.hitTest(result, location);
604    return result;
605  }
606
607  /// Forwards the given pointer event to the binding.
608  Future<void> sendEventToBinding(PointerEvent event, HitTestResult result) {
609    return TestAsyncUtils.guard<void>(() async {
610      binding.dispatchEvent(event, result);
611    });
612  }
613
614  // GEOMETRY
615
616  /// Returns the point at the center of the given widget.
617  Offset getCenter(Finder finder) {
618    return _getElementPoint(finder, (Size size) => size.center(Offset.zero));
619  }
620
621  /// Returns the point at the top left of the given widget.
622  Offset getTopLeft(Finder finder) {
623    return _getElementPoint(finder, (Size size) => Offset.zero);
624  }
625
626  /// Returns the point at the top right of the given widget. This
627  /// point is not inside the object's hit test area.
628  Offset getTopRight(Finder finder) {
629    return _getElementPoint(finder, (Size size) => size.topRight(Offset.zero));
630  }
631
632  /// Returns the point at the bottom left of the given widget. This
633  /// point is not inside the object's hit test area.
634  Offset getBottomLeft(Finder finder) {
635    return _getElementPoint(finder, (Size size) => size.bottomLeft(Offset.zero));
636  }
637
638  /// Returns the point at the bottom right of the given widget. This
639  /// point is not inside the object's hit test area.
640  Offset getBottomRight(Finder finder) {
641    return _getElementPoint(finder, (Size size) => size.bottomRight(Offset.zero));
642  }
643
644  Offset _getElementPoint(Finder finder, Offset sizeToPoint(Size size)) {
645    TestAsyncUtils.guardSync();
646    final Element element = finder.evaluate().single;
647    final RenderBox box = element.renderObject;
648    assert(box != null);
649    return box.localToGlobal(sizeToPoint(box.size));
650  }
651
652  /// Returns the size of the given widget. This is only valid once
653  /// the widget's render object has been laid out at least once.
654  Size getSize(Finder finder) {
655    TestAsyncUtils.guardSync();
656    final Element element = finder.evaluate().single;
657    final RenderBox box = element.renderObject;
658    assert(box != null);
659    return box.size;
660  }
661
662  /// Returns the rect of the given widget. This is only valid once
663  /// the widget's render object has been laid out at least once.
664  Rect getRect(Finder finder) => getTopLeft(finder) & getSize(finder);
665}
666
667/// Variant of [WidgetController] that can be used in tests running
668/// on a device.
669///
670/// This is used, for instance, by [FlutterDriver].
671class LiveWidgetController extends WidgetController {
672  /// Creates a widget controller that uses the given binding.
673  LiveWidgetController(WidgetsBinding binding) : super(binding);
674
675  @override
676  Future<void> pump(Duration duration) async {
677    if (duration != null)
678      await Future<void>.delayed(duration);
679    binding.scheduleFrame();
680    await binding.endOfFrame;
681  }
682}
683