widget_tester.dart 20.5 KB
Newer Older
Hixie's avatar
Hixie committed
1 2 3 4
// Copyright 2015 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
import 'dart:async';
6

7
import 'package:flutter/gestures.dart';
8
import 'package:flutter/material.dart';
9
import 'package:flutter/rendering.dart';
10
import 'package:flutter/scheduler.dart';
11
import 'package:flutter/widgets.dart';
12
import 'package:test/test.dart' as test_package;
13

14
import 'all_elements.dart';
15
import 'binding.dart';
16
import 'controller.dart';
17 18
import 'finders.dart';
import 'test_async_utils.dart';
19
import 'test_text_input.dart';
20 21

export 'package:test/test.dart' hide expect;
22

23 24
/// Signature for callback to [testWidgets] and [benchmarkWidgets].
typedef Future<Null> WidgetTesterCallback(WidgetTester widgetTester);
25

26 27 28 29 30
/// Runs the [callback] inside the Flutter test environment.
///
/// Use this function for testing custom [StatelessWidget]s and
/// [StatefulWidget]s.
///
31 32 33 34 35 36 37 38 39
/// The callback can be asynchronous (using `async`/`await` or
/// using explicit [Future]s).
///
/// This function uses the [test] function in the test package to
/// register the given callback as a test. The callback, when run,
/// will be given a new instance of [WidgetTester]. The [find] object
/// provides convenient widget [Finder]s for use with the
/// [WidgetTester].
///
40
/// ## Sample code
41
///
42
/// ```dart
43 44 45
///     testWidgets('MyWidget', (WidgetTester tester) async {
///       await tester.pumpWidget(new MyWidget());
///       await tester.tap(find.text('Save'));
46
///       expect(find.text('Success'), findsOneWidget);
47
///     });
48
/// ```
49
void testWidgets(String description, WidgetTesterCallback callback, {
50 51
  bool skip: false,
  test_package.Timeout timeout
52
}) {
53 54
  final TestWidgetsFlutterBinding binding = TestWidgetsFlutterBinding.ensureInitialized();
  final WidgetTester tester = new WidgetTester._(binding);
55 56
  timeout ??= binding.defaultTestTimeout;
  test_package.group('-', () {
57 58 59 60 61 62 63 64 65 66 67
    test_package.test(
      description,
      () {
        return binding.runTest(
          () => callback(tester),
          tester._endOfTestVerifications,
          description: description ?? '',
        );
      },
      skip: skip,
    );
68
    test_package.tearDown(binding.postTest);
69 70 71 72 73 74 75 76 77 78 79 80 81 82
  }, timeout: timeout);
}

/// Runs the [callback] inside the Flutter benchmark environment.
///
/// Use this function for benchmarking custom [StatelessWidget]s and
/// [StatefulWidget]s when you want to be able to use features from
/// [TestWidgetsFlutterBinding]. The callback, when run, will be given
/// a new instance of [WidgetTester]. The [find] object provides
/// convenient widget [Finder]s for use with the [WidgetTester].
///
/// The callback can be asynchronous (using `async`/`await` or using
/// explicit [Future]s). If it is, then [benchmarkWidgets] will return
/// a [Future] that completes when the callback's does. Otherwise, it
83 84 85 86
/// will return a Future that is always complete.
///
/// If the callback is asynchronous, make sure you `await` the call
/// to [benchmarkWidgets], otherwise it won't run!
87 88
///
/// Benchmarks must not be run in checked mode. To avoid this, this
89
/// function will print a big message if it is run in checked mode.
90 91 92 93
///
/// Example:
///
///     main() async {
94
///       assert(false); // fail in checked mode
95 96
///       await benchmarkWidgets((WidgetTester tester) async {
///         await tester.pumpWidget(new MyWidget());
97 98
///         final Stopwatch timer = new Stopwatch()..start();
///         for (int index = 0; index < 10000; index += 1) {
99 100
///           await tester.tap(find.text('Tap me'));
///           await tester.pump();
101 102
///         }
///         timer.stop();
103
///         debugPrint('Time taken: ${timer.elapsedMilliseconds}ms');
104 105 106 107
///       });
///       exit(0);
///     }
Future<Null> benchmarkWidgets(WidgetTesterCallback callback) {
108 109 110 111 112 113 114 115 116
  assert(() {
    print('┏╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍┓');
    print('┇ ⚠ THIS BENCHMARK IS BEING RUN WITH ASSERTS ENABLED ⚠  ┇');
    print('┡╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍┦');
    print('│                                                       │');
    print('│  Numbers obtained from a benchmark while asserts are  │');
    print('│  enabled will not accurately reflect the performance  │');
    print('│  that will be experienced by end users using release  ╎');
    print('│  builds. Benchmarks should be run using this command  ┆');
117
    print('│  line:  flutter run --release benchmark.dart          ┊');
118 119 120
    print('│                                                        ');
    print('└─────────────────────────────────────────────────╌┄┈  🐢');
    return true;
121
  }());
122
  final TestWidgetsFlutterBinding binding = TestWidgetsFlutterBinding.ensureInitialized();
123
  assert(binding is! AutomatedTestWidgetsFlutterBinding);
124
  final WidgetTester tester = new WidgetTester._(binding);
125 126 127 128
  return binding.runTest(
    () => callback(tester),
    tester._endOfTestVerifications,
  ) ?? new Future<Null>.value();
129
}
130

131 132 133 134 135 136 137
/// Assert that `actual` matches `matcher`.
///
/// See [test_package.expect] for details. This is a variant of that function
/// that additionally verifies that there are no asynchronous APIs
/// that have not yet resolved.
void expect(dynamic actual, dynamic matcher, {
  String reason,
Ian Hickson's avatar
Ian Hickson committed
138
  dynamic skip, // true or a String
139 140
}) {
  TestAsyncUtils.guardSync();
Ian Hickson's avatar
Ian Hickson committed
141
  test_package.expect(actual, matcher, reason: reason, skip: skip);
142 143 144 145 146 147 148 149 150 151 152 153 154 155
}

/// Assert that `actual` matches `matcher`.
///
/// See [test_package.expect] for details. This variant will _not_ check that
/// there are no outstanding asynchronous API requests. As such, it can be
/// called from, e.g., callbacks that are run during build or layout, or in the
/// completion handlers of futures that execute in response to user input.
///
/// Generally, it is better to use [expect], which does include checks to ensure
/// that asynchronous APIs are not being called.
void expectSync(dynamic actual, dynamic matcher, {
  String reason,
}) {
156
  test_package.expect(actual, matcher, reason: reason);
157 158
}

159
/// Class that programmatically interacts with widgets and the test environment.
160 161 162 163
///
/// For convenience, instances of this class (such as the one provided by
/// `testWidget`) can be used as the `vsync` for `AnimationController` objects.
class WidgetTester extends WidgetController implements HitTestDispatcher, TickerProvider {
164 165 166 167
  WidgetTester._(TestWidgetsFlutterBinding binding) : super(binding) {
    if (binding is LiveTestWidgetsFlutterBinding)
      binding.deviceEventDispatcher = this;
  }
168

169 170 171
  /// The binding instance used by the testing framework.
  @override
  TestWidgetsFlutterBinding get binding => super.binding;
172 173 174

  /// Renders the UI from the given [widget].
  ///
175 176 177 178
  /// Calls [runApp] with the given widget, then triggers a frame and flushes
  /// microtasks, by calling [pump] with the same `duration` (if any). The
  /// supplied [EnginePhase] is the final phase reached during the pump pass; if
  /// not supplied, the whole pass is executed.
179 180 181 182
  ///
  /// Subsequent calls to this is different from [pump] in that it forces a full
  /// rebuild of the tree, even if [widget] is the same as the previous call.
  /// [pump] will only rebuild the widgets that have changed.
183 184 185
  ///
  /// See also [LiveTestWidgetsFlutterBindingFramePolicy], which affects how
  /// this method works when the test is run with `flutter run`.
186 187
  Future<Null> pumpWidget(Widget widget, [
    Duration duration,
188
    EnginePhase phase = EnginePhase.sendSemanticsUpdate,
189 190
  ]) {
    return TestAsyncUtils.guard(() {
191 192
      binding.attachRootWidget(widget);
      binding.scheduleFrame();
193 194
      return binding.pump(duration, phase);
    });
195 196
  }

197 198 199 200 201
  /// Triggers a frame after `duration` amount of time.
  ///
  /// This makes the framework act as if the application had janked (missed
  /// frames) for `duration` amount of time, and then received a v-sync signal
  /// to paint the application.
202
  ///
203 204
  /// This is a convenience function that just calls
  /// [TestWidgetsFlutterBinding.pump].
205 206 207
  ///
  /// See also [LiveTestWidgetsFlutterBindingFramePolicy], which affects how
  /// this method works when the test is run with `flutter run`.
208
  @override
209 210
  Future<Null> pump([
    Duration duration,
211
    EnginePhase phase = EnginePhase.sendSemanticsUpdate,
212 213
  ]) {
    return TestAsyncUtils.guard(() => binding.pump(duration, phase));
214
  }
215

216
  /// Repeatedly calls [pump] with the given `duration` until there are no
217 218 219
  /// 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.
220 221 222
  ///
  /// This essentially waits for all animations to have completed.
  ///
223 224 225 226 227 228 229
  /// 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.
230 231 232 233 234 235 236 237 238 239
  ///
  /// 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.
240
  Future<int> pumpAndSettle([
241
      Duration duration = const Duration(milliseconds: 100),
242
      EnginePhase phase = EnginePhase.sendSemanticsUpdate,
243
      Duration timeout = const Duration(minutes: 10),
244
    ]) {
245 246
    assert(duration != null);
    assert(duration > Duration.ZERO);
247 248
    assert(timeout != null);
    assert(timeout > Duration.ZERO);
249 250 251 252 253 254 255 256 257 258 259
    assert(() {
      final WidgetsBinding binding = this.binding;
      if (binding is LiveTestWidgetsFlutterBinding &&
          binding.framePolicy == LiveTestWidgetsFlutterBindingFramePolicy.benchmark) {
        throw 'When using LiveTestWidgetsFlutterBindingFramePolicy.benchmark, '
              'hasScheduledFrame is never set to true. This means that pumpAndSettle() '
              'cannot be used, because it has no way to know if the application has '
              'stopped registering new frames.';
      }
      return true;
    }());
260 261
    int count = 0;
    return TestAsyncUtils.guard(() async {
262
      final DateTime endTime = binding.clock.fromNowBy(timeout);
263
      do {
264 265
        if (binding.clock.now().isAfter(endTime))
          throw new FlutterError('pumpAndSettle timed out');
266 267
        await binding.pump(duration, phase);
        count += 1;
268
      } while (binding.hasScheduledFrame);
269
    }).then<int>((Null _) => count);
270 271
  }

272
  /// Whether there are any any transient callbacks scheduled.
273 274
  ///
  /// This essentially checks whether all animations have completed.
275 276 277 278 279 280 281 282 283 284 285
  ///
  /// See also:
  ///
  ///  * [pumpAndSettle], which essentially calls [pump] until there are no
  ///    scheduled frames.
  ///  * [SchedulerBinding.transientCallbackCount], which is the value on which
  ///    this is based.
  ///  * [SchedulerBinding.hasScheduledFrame], which is true whenever a frame is
  ///    pending. [SchedulerBinding.hasScheduledFrame] is made true when a
  ///    widget calls [State.setState], even if there are no transient callbacks
  ///    scheduled. This is what [pumpAndSettle] uses.
286 287
  bool get hasRunningAnimations => binding.transientCallbackCount > 0;

288
  @override
289
  HitTestResult hitTestOnBinding(Offset location) {
290 291 292 293
    location = binding.localToGlobal(location);
    return super.hitTestOnBinding(location);
  }

294 295 296 297 298 299 300 301
  @override
  Future<Null> sendEventToBinding(PointerEvent event, HitTestResult result) {
    return TestAsyncUtils.guard(() async {
      binding.dispatchEvent(event, result, source: TestBindingEventSource.test);
      return null;
    });
  }

302 303 304 305 306 307
  /// Handler for device events caught by the binding in live test mode.
  @override
  void dispatchEvent(PointerEvent event, HitTestResult result) {
    if (event is PointerDownEvent) {
      final RenderObject innerTarget = result.path.firstWhere(
        (HitTestEntry candidate) => candidate.target is RenderObject,
308 309 310 311 312 313 314 315 316 317 318 319
      ).target;
      final Element innerTargetElement = collectAllElementsFrom(
        binding.renderViewElement,
        skipOffstage: true,
      ).lastWhere(
        (Element element) => element.renderObject == innerTarget,
        orElse: () => null,
      );
      if (innerTargetElement == null) {
        debugPrint('No widgets found at ${binding.globalToLocal(event.position)}.');
        return;
      }
320 321 322 323 324 325 326 327 328 329
      final List<Element> candidates = <Element>[];
      innerTargetElement.visitAncestorElements((Element element) {
        candidates.add(element);
        return true;
      });
      assert(candidates.isNotEmpty);
      String descendantText;
      int numberOfWithTexts = 0;
      int numberOfTypes = 0;
      int totalNumber = 0;
330
      debugPrint('Some possible finders for the widgets at ${binding.globalToLocal(event.position)}:');
331
      for (Element element in candidates) {
332
        if (totalNumber > 13) // an arbitrary number of finders that feels useful without being overwhelming
333
          break;
334 335 336 337 338 339 340 341 342 343
        totalNumber += 1; // optimistically assume we'll be able to describe it

        if (element.widget is Tooltip) {
          final Tooltip widget = element.widget;
          final Iterable<Element> matches = find.byTooltip(widget.message).evaluate();
          if (matches.length == 1) {
            debugPrint('  find.byTooltip(\'${widget.message}\')');
            continue;
          }
        }
344 345 346 347 348 349 350

        if (element.widget is Text) {
          assert(descendantText == null);
          final Text widget = element.widget;
          final Iterable<Element> matches = find.text(widget.data).evaluate();
          descendantText = widget.data;
          if (matches.length == 1) {
351
            debugPrint('  find.text(\'${widget.data}\')');
352 353 354 355 356 357 358 359 360 361 362 363
            continue;
          }
        }

        if (element.widget.key is ValueKey<dynamic>) {
          final ValueKey<dynamic> key = element.widget.key;
          String keyLabel;
          if ((key is ValueKey<int> ||
               key is ValueKey<double> ||
               key is ValueKey<bool>)) {
            keyLabel = 'const ${element.widget.key.runtimeType}(${key.value})';
          } else if (key is ValueKey<String>) {
364
            keyLabel = 'const Key(\'${key.value}\')';
365 366 367 368
          }
          if (keyLabel != null) {
            final Iterable<Element> matches = find.byKey(key).evaluate();
            if (matches.length == 1) {
369
              debugPrint('  find.byKey($keyLabel)');
370 371 372 373 374 375 376 377 378
              continue;
            }
          }
        }

        if (!_isPrivate(element.widget.runtimeType)) {
          if (numberOfTypes < 5) {
            final Iterable<Element> matches = find.byType(element.widget.runtimeType).evaluate();
            if (matches.length == 1) {
379
              debugPrint('  find.byType(${element.widget.runtimeType})');
380 381 382 383 384 385 386 387
              numberOfTypes += 1;
              continue;
            }
          }

          if (descendantText != null && numberOfWithTexts < 5) {
            final Iterable<Element> matches = find.widgetWithText(element.widget.runtimeType, descendantText).evaluate();
            if (matches.length == 1) {
388
              debugPrint('  find.widgetWithText(${element.widget.runtimeType}, \'$descendantText\')');
389 390 391 392 393 394 395 396 397
              numberOfWithTexts += 1;
              continue;
            }
          }
        }

        if (!_isPrivate(element.runtimeType)) {
          final Iterable<Element> matches = find.byElementType(element.runtimeType).evaluate();
          if (matches.length == 1) {
398
            debugPrint('  find.byElementType(${element.runtimeType})');
399 400 401 402 403 404 405
            continue;
          }
        }

        totalNumber -= 1; // if we got here, we didn't actually find something to say about it
      }
      if (totalNumber == 0)
406
        debugPrint('  <could not come up with any unique finders>');
407 408 409 410
    }
  }

  bool _isPrivate(Type type) {
411
    // used above so that we don't suggest matchers for private types
412 413 414
    return '_'.matchAsPrefix(type.toString()) != null;
  }

415 416
  /// Returns the exception most recently caught by the Flutter framework.
  ///
417
  /// See [TestWidgetsFlutterBinding.takeException] for details.
418
  dynamic takeException() {
419
    return binding.takeException();
420 421
  }

422 423
  /// Acts as if the application went idle.
  ///
424 425
  /// Runs all remaining microtasks, including those scheduled as a result of
  /// running them, until there are no more microtasks scheduled.
426
  ///
427 428
  /// Does not run timers. May result in an infinite loop or run out of memory
  /// if microtasks continue to recursively schedule new microtasks.
429 430
  Future<Null> idle() {
    return TestAsyncUtils.guard(() => binding.idle());
431
  }
432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474

  Set<Ticker> _tickers;

  @override
  Ticker createTicker(TickerCallback onTick) {
    _tickers ??= new Set<_TestTicker>();
    final _TestTicker result = new _TestTicker(onTick, _removeTicker);
    _tickers.add(result);
    return result;
  }

  void _removeTicker(_TestTicker ticker) {
    assert(_tickers != null);
    assert(_tickers.contains(ticker));
    _tickers.remove(ticker);
  }

  /// Throws an exception if any tickers created by the [WidgetTester] are still
  /// active when the method is called.
  ///
  /// An argument can be specified to provide a string that will be used in the
  /// error message. It should be an adverbial phrase describing the current
  /// situation, such as "at the end of the test".
  void verifyTickersWereDisposed([ String when = 'when none should have been' ]) {
    assert(when != null);
    if (_tickers != null) {
      for (Ticker ticker in _tickers) {
        if (ticker.isActive) {
          throw new FlutterError(
            'A Ticker was active $when.\n'
            'All Tickers must be disposed. Tickers used by AnimationControllers '
            'should be disposed by calling dispose() on the AnimationController itself. '
            'Otherwise, the ticker will leak.\n'
            'The offending ticker was: ${ticker.toString(debugIncludeStack: true)}'
          );
        }
      }
    }
  }

  void _endOfTestVerifications() {
    verifyTickersWereDisposed('at the end of the test');
  }
475 476 477 478

  /// Returns the TestTextInput singleton.
  ///
  /// Typical app tests will not need to use this value. To add text to widgets
479
  /// like [TextField] or [TextFormField], call [enterText].
480 481
  TestTextInput get testTextInput => binding.testTextInput;

482
  /// Give the text input widget specified by [finder] the focus, as if the
483 484
  /// onscreen keyboard had appeared.
  ///
485 486
  /// The widget specified by [finder] must be an [EditableText] or have
  /// an [EditableText] descendant. For example `find.byType(TextField)`
487
  /// or `find.byType(TextFormField)`, or `find.byType(EditableText)`.
488 489
  ///
  /// Tests that just need to add text to widgets like [TextField]
490
  /// or [TextFormField] only need to call [enterText].
491
  Future<Null> showKeyboard(Finder finder) async {
492
    return TestAsyncUtils.guard(() async {
493 494 495 496 497
      final EditableTextState editable = state(find.descendant(
        of: finder,
        matching: find.byType(EditableText),
        matchRoot: true,
      ));
498 499 500 501 502
      if (editable != binding.focusedEditable) {
        binding.focusedEditable = editable;
        await pump();
      }
    });
503 504
  }

505
  /// Give the text input widget specified by [finder] the focus and
506
  /// enter [text] as if it been provided by the onscreen keyboard.
507 508 509
  ///
  /// The widget specified by [finder] must be an [EditableText] or have
  /// an [EditableText] descendant. For example `find.byType(TextField)`
510
  /// or `find.byType(TextFormField)`, or `find.byType(EditableText)`.
511 512 513
  ///
  /// To just give [finder] the focus without entering any text,
  /// see [showKeyboard].
514
  Future<Null> enterText(Finder finder, String text) async {
515 516 517 518 519
    return TestAsyncUtils.guard(() async {
      await showKeyboard(finder);
      testTextInput.enterText(text);
      await idle();
    });
520
  }
521 522 523 524 525 526 527 528 529 530 531 532 533 534 535
}

typedef void _TickerDisposeCallback(_TestTicker ticker);

class _TestTicker extends Ticker {
  _TestTicker(TickerCallback onTick, this._onDispose) : super(onTick);

  _TickerDisposeCallback _onDispose;

  @override
  void dispose() {
    if (_onDispose != null)
      _onDispose(this);
    super.dispose();
  }
536
}