controller.dart 20.1 KB
Newer Older
1 2 3 4
// Copyright 2016 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

5 6
import 'dart:async';

7 8 9 10 11
import 'package:flutter/gestures.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart';

import 'all_elements.dart';
12 13 14
import 'finders.dart';
import 'test_async_utils.dart';
import 'test_pointer.dart';
15 16 17 18 19

/// Class that programmatically interacts with widgets.
///
/// For a variant of this class suited specifically for unit tests, see [WidgetTester].
class WidgetController {
20
  /// Creates a widget controller that uses the given binding.
21 22
  WidgetController(this.binding);

23
  /// A reference to the current instance of the binding.
24 25 26 27 28 29 30 31
  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.
32 33 34 35
  bool any(Finder finder) {
    TestAsyncUtils.guardSync();
    return finder.evaluate().isNotEmpty;
  }
36

37

38 39 40 41 42
  /// 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 {
43
    TestAsyncUtils.guardSync();
44 45 46 47 48 49 50 51
    return allElements
           .map((Element element) => element.widget);
  }

  /// The matching widget in the widget tree.
  ///
  /// Throws a [StateError] if `finder` is empty or matches more than
  /// one widget.
52 53 54
  ///
  /// * 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.
55
  T widget<T extends Widget>(Finder finder) {
56
    TestAsyncUtils.guardSync();
57 58 59 60 61 62 63
    return finder.evaluate().single.widget;
  }

  /// The first matching widget according to a depth-first pre-order
  /// traversal of the widget tree.
  ///
  /// Throws a [StateError] if `finder` is empty.
64 65
  ///
  /// * Use [widget] if you only expect to match one widget.
66
  T firstWidget<T extends Widget>(Finder finder) {
67
    TestAsyncUtils.guardSync();
68 69 70
    return finder.evaluate().first.widget;
  }

71 72 73 74
  /// 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.
75
  Iterable<T> widgetList<T extends Widget>(Finder finder) {
76
    TestAsyncUtils.guardSync();
77
    return finder.evaluate().map<T>((Element element) {
78
      final T result = element.widget;
79 80
      return result;
    });
81 82 83
  }


84 85 86 87 88 89
  /// 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 {
90
    TestAsyncUtils.guardSync();
91
    return collectAllElementsFrom(binding.renderViewElement, skipOffstage: false);
92 93 94 95 96 97
  }

  /// The matching element in the widget tree.
  ///
  /// Throws a [StateError] if `finder` is empty or matches more than
  /// one element.
98 99 100
  ///
  /// * 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.
101
  T element<T extends Element>(Finder finder) {
102
    TestAsyncUtils.guardSync();
103 104 105 106 107 108 109
    return finder.evaluate().single;
  }

  /// The first matching element according to a depth-first pre-order
  /// traversal of the widget tree.
  ///
  /// Throws a [StateError] if `finder` is empty.
110 111
  ///
  /// * Use [element] if you only expect to match one element.
112
  T firstElement<T extends Element>(Finder finder) {
113
    TestAsyncUtils.guardSync();
114 115 116
    return finder.evaluate().first;
  }

117 118 119 120
  /// 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.
121
  Iterable<T> elementList<T extends Element>(Finder finder) {
122 123 124 125 126
    TestAsyncUtils.guardSync();
    return finder.evaluate();
  }


127 128 129 130 131 132
  /// 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 {
133
    TestAsyncUtils.guardSync();
134
    return allElements
135
           // TODO(vegorov) replace with Iterable.whereType, when it is available. https://github.com/dart-lang/sdk/issues/27827
136
           .where((Element element) => element is StatefulElement)
137 138 139 140
           .map((Element element) {
             final StatefulElement statefulElement = element;
             return statefulElement.state;
           });
141 142 143 144 145 146
  }

  /// 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.
147 148 149
  ///
  /// * 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.
150
  T state<T extends State<StatefulWidget>>(Finder finder) { // TODO(leafp): remove '<StatefulWidget>' when https://github.com/dart-lang/sdk/issues/28580 is fixed
151
    TestAsyncUtils.guardSync();
152
    return _stateOf<T>(finder.evaluate().single, finder);
153 154 155 156 157 158 159
  }

  /// 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.
160 161
  ///
  /// * Use [state] if you only expect to match one state.
162
  T firstState<T extends State<StatefulWidget>>(Finder finder) { // TODO(leafp): remove '<StatefulWidget>' when https://github.com/dart-lang/sdk/issues/28580 is fixed
163
    TestAsyncUtils.guardSync();
164
    return _stateOf<T>(finder.evaluate().first, finder);
165 166
  }

167 168 169 170 171 172 173
  /// 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.
174
  Iterable<T> stateList<T extends State<StatefulWidget>>(Finder finder) { // TODO(leafp): remove '<StatefulWidget>' when https://github.com/dart-lang/sdk/issues/28580 is fixed
175
    TestAsyncUtils.guardSync();
176
    return finder.evaluate().map((Element element) => _stateOf<T>(element, finder));
177 178
  }

179
  T _stateOf<T extends State<StatefulWidget>>(Element element, Finder finder) { // TODO(leafp): remove '<StatefulWidget>' when https://github.com/dart-lang/sdk/issues/28580 is fixed
180
    TestAsyncUtils.guardSync();
181 182 183 184 185
    if (element is StatefulElement)
      return element.state;
    throw new StateError('Widget of type ${element.widget.runtimeType}, with ${finder.description}, is not a StatefulWidget.');
  }

186

187 188 189 190 191 192 193 194
  /// 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 {
195
    TestAsyncUtils.guardSync();
196 197 198 199 200 201 202 203
    return allElements
           .map((Element element) => element.renderObject);
  }

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

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

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

235 236 237 238

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


  // INTERACTION

  /// Dispatch a pointer down / pointer up sequence at the center of
255 256 257 258 259
  /// the given widget, assuming it is exposed.
  ///
  /// If the center of the widget is not exposed, this might send events to
  /// another object.
  Future<Null> tap(Finder finder, { int pointer }) {
260
    return tapAt(getCenter(finder), pointer: pointer);
261 262
  }

263
  /// Dispatch a pointer down / pointer up sequence at the given location.
264
  Future<Null> tapAt(Offset location, { int pointer }) {
265
    return TestAsyncUtils.guard(() async {
266
      final TestGesture gesture = await startGesture(location, pointer: pointer);
267 268 269
      await gesture.up();
      return null;
    });
270 271
  }

272 273
  /// Dispatch a pointer down / pointer up sequence (with a delay of
  /// [kLongPressTimeout] + [kPressTimeout] between the two events) at the
274 275 276 277 278
  /// center of the given widget, assuming it is exposed.
  ///
  /// If the center of the widget is not exposed, this might send events to
  /// another object.
  Future<Null> longPress(Finder finder, { int pointer }) {
279 280 281 282 283
    return longPressAt(getCenter(finder), pointer: pointer);
  }

  /// Dispatch a pointer down / pointer up sequence at the given location with
  /// a delay of [kLongPressTimeout] + [kPressTimeout] between the two events.
284
  Future<Null> longPressAt(Offset location, { int pointer }) {
285
    return TestAsyncUtils.guard(() async {
286
      final TestGesture gesture = await startGesture(location, pointer: pointer);
287 288 289 290 291 292
      await pump(kLongPressTimeout + kPressTimeout);
      await gesture.up();
      return null;
    });
  }

293
  /// Attempts a fling gesture starting from the center of the given
294
  /// widget, moving the given distance, reaching the given speed.
295 296 297
  ///
  /// If the middle of the widget is not exposed, this might send
  /// events to another object.
298 299 300
  ///
  /// This can pump frames. See [flingFrom] for a discussion of how the
  /// `offset`, `velocity` and `frameInterval` arguments affect this.
301 302 303 304 305
  ///
  /// The `speed` is in pixels per second in the direction given by `offset`.
  ///
  /// 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].
Ian Hickson's avatar
Ian Hickson committed
306 307 308 309 310 311 312
  ///
  /// 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).
313 314 315
  Future<Null> fling(Finder finder, Offset offset, double speed, {
    int pointer,
    Duration frameInterval: const Duration(milliseconds: 16),
Ian Hickson's avatar
Ian Hickson committed
316 317
    Offset initialOffset: Offset.zero,
    Duration initialOffsetDelay: const Duration(seconds: 1),
318
  }) {
Ian Hickson's avatar
Ian Hickson committed
319 320 321 322 323 324 325 326 327
    return flingFrom(
      getCenter(finder),
      offset,
      speed,
      pointer: pointer,
      frameInterval: frameInterval,
      initialOffset: initialOffset,
      initialOffsetDelay: initialOffsetDelay,
    );
328 329
  }

330 331
  /// Attempts a fling gesture starting from the given location, moving the
  /// given distance, reaching the given speed.
332 333 334
  ///
  /// Exactly 50 pointer events are synthesized.
  ///
335 336 337
  /// 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
338 339 340 341 342 343
  /// (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
344
  /// sending events, or each time an event is synthesized, whichever is rarer.
345 346 347
  ///
  /// 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].
Ian Hickson's avatar
Ian Hickson committed
348 349 350 351 352 353 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).
  Future<Null> flingFrom(Offset startLocation, Offset offset, double speed, {
    int pointer,
    Duration frameInterval: const Duration(milliseconds: 16),
    Offset initialOffset: Offset.zero,
    Duration initialOffsetDelay: const Duration(seconds: 1),
  }) {
361
    assert(offset.distance > 0.0);
362
    assert(speed > 0.0); // speed is pixels/second
363
    return TestAsyncUtils.guard(() async {
364
      final TestPointer testPointer = new TestPointer(pointer ?? _getNextPointer());
365
      final HitTestResult result = hitTestOnBinding(startLocation);
366
      const int kMoveCount = 50; // Needs to be >= kHistorySize, see _LeastSquaresVelocityTrackerStrategy
367
      final double timeStampDelta = 1000.0 * offset.distance / (kMoveCount * speed);
368
      double timeStamp = 0.0;
369
      double lastTimeStamp = timeStamp;
370
      await sendEventToBinding(testPointer.down(startLocation, timeStamp: new Duration(milliseconds: timeStamp.round())), result);
Ian Hickson's avatar
Ian Hickson committed
371 372 373 374 375
      if (initialOffset.distance > 0.0) {
        await sendEventToBinding(testPointer.move(startLocation + initialOffset, timeStamp: new Duration(milliseconds: timeStamp.round())), result);
        timeStamp += initialOffsetDelay.inMilliseconds;
        await pump(initialOffsetDelay);
      }
376
      for (int i = 0; i <= kMoveCount; i += 1) {
Ian Hickson's avatar
Ian Hickson committed
377
        final Offset location = startLocation + initialOffset + Offset.lerp(Offset.zero, offset, i / kMoveCount);
378
        await sendEventToBinding(testPointer.move(location, timeStamp: new Duration(milliseconds: timeStamp.round())), result);
379
        timeStamp += timeStampDelta;
380 381 382 383
        if (timeStamp - lastTimeStamp > frameInterval.inMilliseconds) {
          await pump(new Duration(milliseconds: (timeStamp - lastTimeStamp).truncate()));
          lastTimeStamp = timeStamp;
        }
384
      }
385
      await sendEventToBinding(testPointer.up(timeStamp: new Duration(milliseconds: timeStamp.round())), result);
386 387
      return null;
    });
388 389
  }

390 391 392 393 394 395 396 397 398 399
  /// Called to indicate that time should advance.
  ///
  /// This is invoked by [flingFrom], for instance, so that the sequence of
  /// pointer events occurs over time.
  ///
  /// The default implementation does nothing.
  ///
  /// The [WidgetTester] subclass implements this by deferring to the [binding].
  Future<Null> pump(Duration duration) => new Future<Null>.value(null);

400 401 402 403 404
  /// Attempts to drag the given widget by the given offset, by
  /// starting a drag in the middle of the widget.
  ///
  /// If the middle of the widget is not exposed, this might send
  /// events to another object.
405 406 407 408 409
  ///
  /// 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.
  Future<Null> drag(Finder finder, Offset offset, { int pointer }) {
    return dragFrom(getCenter(finder), offset, pointer: pointer);
410 411 412 413
  }

  /// Attempts a drag gesture consisting of a pointer down, a move by
  /// the given offset, and a pointer up.
414 415 416 417
  ///
  /// 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.
418
  Future<Null> dragFrom(Offset startLocation, Offset offset, { int pointer }) {
419
    return TestAsyncUtils.guard(() async {
420
      final TestGesture gesture = await startGesture(startLocation, pointer: pointer);
421
      assert(gesture != null);
422 423 424 425
      await gesture.moveBy(offset);
      await gesture.up();
      return null;
    });
426 427
  }

428 429 430 431 432 433 434 435 436 437 438 439
  /// 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.
  int nextPointer = 1;

  int _getNextPointer() {
    final int result = nextPointer;
    nextPointer += 1;
    return result;
  }

440 441
  /// Begins a gesture at a particular point, and returns the
  /// [TestGesture] object which you can use to continue the gesture.
442
  Future<TestGesture> startGesture(Offset downLocation, { int pointer }) {
443 444 445 446 447 448
    return TestGesture.down(
      downLocation,
      pointer: pointer ?? _getNextPointer(),
      hitTester: hitTestOnBinding,
      dispatcher: sendEventToBinding,
    );
449 450
  }

451
  /// Forwards the given location to the binding's hitTest logic.
452
  HitTestResult hitTestOnBinding(Offset location) {
453 454 455 456 457
    final HitTestResult result = new HitTestResult();
    binding.hitTest(result, location);
    return result;
  }

458 459 460 461 462 463
  /// Forwards the given pointer event to the binding.
  Future<Null> sendEventToBinding(PointerEvent event, HitTestResult result) {
    return TestAsyncUtils.guard(() async {
      binding.dispatchEvent(event, result);
      return null;
    });
464 465
  }

466 467 468 469

  // GEOMETRY

  /// Returns the point at the center of the given widget.
470 471
  Offset getCenter(Finder finder) {
    return _getElementPoint(finder, (Size size) => size.center(Offset.zero));
472 473 474
  }

  /// Returns the point at the top left of the given widget.
475 476
  Offset getTopLeft(Finder finder) {
    return _getElementPoint(finder, (Size size) => Offset.zero);
477 478 479 480
  }

  /// Returns the point at the top right of the given widget. This
  /// point is not inside the object's hit test area.
481 482
  Offset getTopRight(Finder finder) {
    return _getElementPoint(finder, (Size size) => size.topRight(Offset.zero));
483 484 485 486
  }

  /// Returns the point at the bottom left of the given widget. This
  /// point is not inside the object's hit test area.
487 488
  Offset getBottomLeft(Finder finder) {
    return _getElementPoint(finder, (Size size) => size.bottomLeft(Offset.zero));
489 490 491 492
  }

  /// Returns the point at the bottom right of the given widget. This
  /// point is not inside the object's hit test area.
493 494
  Offset getBottomRight(Finder finder) {
    return _getElementPoint(finder, (Size size) => size.bottomRight(Offset.zero));
495 496
  }

497
  Offset _getElementPoint(Finder finder, Offset sizeToPoint(Size size)) {
498
    TestAsyncUtils.guardSync();
499 500
    final Element element = finder.evaluate().single;
    final RenderBox box = element.renderObject;
501 502 503 504 505 506 507
    assert(box != null);
    return box.localToGlobal(sizeToPoint(box.size));
  }

  /// 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) {
508
    TestAsyncUtils.guardSync();
509 510
    final Element element = finder.evaluate().single;
    final RenderBox box = element.renderObject;
511 512 513
    assert(box != null);
    return box.size;
  }
514 515 516 517

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