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