widget_tester.dart 19.4 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/rendering.dart';
9
import 'package:flutter/scheduler.dart';
10
import 'package:flutter/widgets.dart';
11
import 'package:test/test.dart' as test_package;
12

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

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

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

25 26 27 28 29
/// Runs the [callback] inside the Flutter test environment.
///
/// Use this function for testing custom [StatelessWidget]s and
/// [StatefulWidget]s.
///
30 31 32 33 34 35 36 37 38
/// 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].
///
39 40
/// Example:
///
41 42 43
///     testWidgets('MyWidget', (WidgetTester tester) async {
///       await tester.pumpWidget(new MyWidget());
///       await tester.tap(find.text('Save'));
44
///       expect(tester, hasWidget(find.text('Success')));
45
///     });
46
void testWidgets(String description, WidgetTesterCallback callback, {
47 48
  bool skip: false,
  test_package.Timeout timeout
49
}) {
50 51
  final TestWidgetsFlutterBinding binding = TestWidgetsFlutterBinding.ensureInitialized();
  final WidgetTester tester = new WidgetTester._(binding);
52 53
  timeout ??= binding.defaultTestTimeout;
  test_package.group('-', () {
54 55 56 57 58 59 60 61 62 63 64
    test_package.test(
      description,
      () {
        return binding.runTest(
          () => callback(tester),
          tester._endOfTestVerifications,
          description: description ?? '',
        );
      },
      skip: skip,
    );
65
    test_package.tearDown(binding.postTest);
66 67 68 69 70 71 72 73 74 75 76 77 78 79
  }, 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
80 81 82 83
/// 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!
84 85
///
/// Benchmarks must not be run in checked mode. To avoid this, this
86
/// function will print a big message if it is run in checked mode.
87 88 89 90
///
/// Example:
///
///     main() async {
91
///       assert(false); // fail in checked mode
92 93
///       await benchmarkWidgets((WidgetTester tester) async {
///         await tester.pumpWidget(new MyWidget());
94 95
///         final Stopwatch timer = new Stopwatch()..start();
///         for (int index = 0; index < 10000; index += 1) {
96 97
///           await tester.tap(find.text('Tap me'));
///           await tester.pump();
98 99
///         }
///         timer.stop();
100
///         debugPrint('Time taken: ${timer.elapsedMilliseconds}ms');
101 102 103 104
///       });
///       exit(0);
///     }
Future<Null> benchmarkWidgets(WidgetTesterCallback callback) {
105 106 107 108 109 110 111 112 113
  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  ┆');
114
    print('│  line:  flutter run --release benchmark.dart          ┊');
115 116 117 118
    print('│                                                        ');
    print('└─────────────────────────────────────────────────╌┄┈  🐢');
    return true;
  });
119
  final TestWidgetsFlutterBinding binding = TestWidgetsFlutterBinding.ensureInitialized();
120
  assert(binding is! AutomatedTestWidgetsFlutterBinding);
121
  final WidgetTester tester = new WidgetTester._(binding);
122 123 124 125
  return binding.runTest(
    () => callback(tester),
    tester._endOfTestVerifications,
  ) ?? new Future<Null>.value();
126
}
127

128 129 130 131 132 133 134 135 136
/// 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,
}) {
  TestAsyncUtils.guardSync();
137
  test_package.expect(actual, matcher, reason: reason);
138 139 140 141 142 143 144 145 146 147 148 149 150 151
}

/// 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,
}) {
152
  test_package.expect(actual, matcher, reason: reason);
153 154
}

155
/// Class that programmatically interacts with widgets and the test environment.
156 157 158 159
///
/// 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 {
160 161 162 163
  WidgetTester._(TestWidgetsFlutterBinding binding) : super(binding) {
    if (binding is LiveTestWidgetsFlutterBinding)
      binding.deviceEventDispatcher = this;
  }
164

165 166 167
  /// The binding instance used by the testing framework.
  @override
  TestWidgetsFlutterBinding get binding => super.binding;
168 169 170

  /// Renders the UI from the given [widget].
  ///
171 172 173 174
  /// 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.
175 176 177 178
  ///
  /// 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.
179 180 181
  ///
  /// See also [LiveTestWidgetsFlutterBindingFramePolicy], which affects how
  /// this method works when the test is run with `flutter run`.
182 183
  Future<Null> pumpWidget(Widget widget, [
    Duration duration,
184
    EnginePhase phase = EnginePhase.sendSemanticsUpdate,
185 186
  ]) {
    return TestAsyncUtils.guard(() {
187 188
      binding.attachRootWidget(widget);
      binding.scheduleFrame();
189 190
      return binding.pump(duration, phase);
    });
191 192
  }

193 194 195 196 197
  /// 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.
198
  ///
199 200
  /// This is a convenience function that just calls
  /// [TestWidgetsFlutterBinding.pump].
201 202 203
  ///
  /// See also [LiveTestWidgetsFlutterBindingFramePolicy], which affects how
  /// this method works when the test is run with `flutter run`.
204
  @override
205 206
  Future<Null> pump([
    Duration duration,
207
    EnginePhase phase = EnginePhase.sendSemanticsUpdate,
208 209
  ]) {
    return TestAsyncUtils.guard(() => binding.pump(duration, phase));
210
  }
211

212
  /// Repeatedly calls [pump] with the given `duration` until there are no
213 214 215
  /// 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.
216 217 218
  ///
  /// This essentially waits for all animations to have completed.
  ///
219 220 221 222 223 224 225
  /// 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.
226 227 228 229 230 231 232 233 234 235
  ///
  /// 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.
236
  Future<int> pumpAndSettle([
237
      Duration duration = const Duration(milliseconds: 100),
238
      EnginePhase phase = EnginePhase.sendSemanticsUpdate,
239
      Duration timeout = const Duration(minutes: 10),
240
    ]) {
241 242
    assert(duration != null);
    assert(duration > Duration.ZERO);
243 244
    assert(timeout != null);
    assert(timeout > Duration.ZERO);
245 246
    int count = 0;
    return TestAsyncUtils.guard(() async {
247
      final DateTime endTime = binding.clock.fromNowBy(timeout);
248
      do {
249 250
        if (binding.clock.now().isAfter(endTime))
          throw new FlutterError('pumpAndSettle timed out');
251 252
        await binding.pump(duration, phase);
        count += 1;
253
      } while (binding.hasScheduledFrame);
254
    }).then<int>((Null _) => count);
255 256
  }

257
  /// Whether there are any any transient callbacks scheduled.
258 259
  ///
  /// This essentially checks whether all animations have completed.
260 261 262 263 264 265 266 267 268 269 270 271 272
  ///
  /// 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.
273 274
  bool get hasRunningAnimations => binding.transientCallbackCount > 0;

275
  @override
276
  HitTestResult hitTestOnBinding(Offset location) {
277 278 279 280
    location = binding.localToGlobal(location);
    return super.hitTestOnBinding(location);
  }

281 282 283 284 285 286 287 288
  @override
  Future<Null> sendEventToBinding(PointerEvent event, HitTestResult result) {
    return TestAsyncUtils.guard(() async {
      binding.dispatchEvent(event, result, source: TestBindingEventSource.test);
      return null;
    });
  }

289 290 291 292 293 294 295 296 297 298
  /// 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,
        orElse: () => null
      )?.target;
      if (innerTarget == null)
        return null;
299
      final Element innerTargetElement = collectAllElementsFrom(binding.renderViewElement, skipOffstage: true)
300 301 302 303 304 305 306 307 308 309 310
        .lastWhere((Element element) => element.renderObject == innerTarget);
      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;
311
      debugPrint('Some possible finders for the widgets at ${binding.globalToLocal(event.position)}:');
312 313 314 315 316 317 318 319 320 321 322
      for (Element element in candidates) {
        if (totalNumber > 10)
          break;
        totalNumber += 1;

        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) {
323
            debugPrint('  find.text(\'${widget.data}\')');
324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340
            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>) {
            keyLabel = 'const ${element.widget.key.runtimeType}(\'${key.value}\')';
          }
          if (keyLabel != null) {
            final Iterable<Element> matches = find.byKey(key).evaluate();
            if (matches.length == 1) {
341
              debugPrint('  find.byKey($keyLabel)');
342 343 344 345 346 347 348 349 350
              continue;
            }
          }
        }

        if (!_isPrivate(element.widget.runtimeType)) {
          if (numberOfTypes < 5) {
            final Iterable<Element> matches = find.byType(element.widget.runtimeType).evaluate();
            if (matches.length == 1) {
351
              debugPrint('  find.byType(${element.widget.runtimeType})');
352 353 354 355 356 357 358 359
              numberOfTypes += 1;
              continue;
            }
          }

          if (descendantText != null && numberOfWithTexts < 5) {
            final Iterable<Element> matches = find.widgetWithText(element.widget.runtimeType, descendantText).evaluate();
            if (matches.length == 1) {
360
              debugPrint('  find.widgetWithText(${element.widget.runtimeType}, \'$descendantText\')');
361 362 363 364 365 366 367 368 369
              numberOfWithTexts += 1;
              continue;
            }
          }
        }

        if (!_isPrivate(element.runtimeType)) {
          final Iterable<Element> matches = find.byElementType(element.runtimeType).evaluate();
          if (matches.length == 1) {
370
            debugPrint('  find.byElementType(${element.runtimeType})');
371 372 373 374 375 376 377
            continue;
          }
        }

        totalNumber -= 1; // if we got here, we didn't actually find something to say about it
      }
      if (totalNumber == 0)
378
        debugPrint('  <could not come up with any unique finders>');
379 380 381 382
    }
  }

  bool _isPrivate(Type type) {
383
    // used above so that we don't suggest matchers for private types
384 385 386
    return '_'.matchAsPrefix(type.toString()) != null;
  }

387 388
  /// Returns the exception most recently caught by the Flutter framework.
  ///
389
  /// See [TestWidgetsFlutterBinding.takeException] for details.
390
  dynamic takeException() {
391
    return binding.takeException();
392 393
  }

394 395
  /// Acts as if the application went idle.
  ///
396 397
  /// Runs all remaining microtasks, including those scheduled as a result of
  /// running them, until there are no more microtasks scheduled.
398
  ///
399 400
  /// Does not run timers. May result in an infinite loop or run out of memory
  /// if microtasks continue to recursively schedule new microtasks.
401 402
  Future<Null> idle() {
    return TestAsyncUtils.guard(() => binding.idle());
403
  }
404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446

  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');
  }
447 448 449 450

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

454
  /// Give the text input widget specified by [finder] the focus, as if the
455 456
  /// onscreen keyboard had appeared.
  ///
457 458
  /// The widget specified by [finder] must be an [EditableText] or have
  /// an [EditableText] descendant. For example `find.byType(TextField)`
459
  /// or `find.byType(TextFormField)`, or `find.byType(EditableText)`.
460 461
  ///
  /// Tests that just need to add text to widgets like [TextField]
462
  /// or [TextFormField] only need to call [enterText].
463
  Future<Null> showKeyboard(Finder finder) async {
464
    return TestAsyncUtils.guard(() async {
465 466 467 468 469
      final EditableTextState editable = state(find.descendant(
        of: finder,
        matching: find.byType(EditableText),
        matchRoot: true,
      ));
470 471 472 473 474
      if (editable != binding.focusedEditable) {
        binding.focusedEditable = editable;
        await pump();
      }
    });
475 476
  }

477
  /// Give the text input widget specified by [finder] the focus and
478
  /// enter [text] as if it been provided by the onscreen keyboard.
479 480 481
  ///
  /// The widget specified by [finder] must be an [EditableText] or have
  /// an [EditableText] descendant. For example `find.byType(TextField)`
482
  /// or `find.byType(TextFormField)`, or `find.byType(EditableText)`.
483 484 485
  ///
  /// To just give [finder] the focus without entering any text,
  /// see [showKeyboard].
486
  Future<Null> enterText(Finder finder, String text) async {
487 488 489 490 491
    return TestAsyncUtils.guard(() async {
      await showKeyboard(finder);
      testTextInput.enterText(text);
      await idle();
    });
492
  }
493 494 495 496 497 498 499 500 501 502 503 504 505 506 507
}

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();
  }
508
}