widget_tester.dart 16 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 18 19
import 'finders.dart';
import 'test_async_utils.dart';

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

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

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

114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144
/// 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,
  bool verbose: false,
  dynamic formatter
}) {
  TestAsyncUtils.guardSync();
  test_package.expect(actual, matcher, reason: reason, verbose: verbose, formatter: formatter);
}

/// 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,
  bool verbose: false,
  dynamic formatter
}) {
  test_package.expect(actual, matcher, reason: reason, verbose: verbose, formatter: formatter);
}

145
/// Class that programmatically interacts with widgets and the test environment.
146 147 148 149
///
/// 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 {
150 151 152 153
  WidgetTester._(TestWidgetsFlutterBinding binding) : super(binding) {
    if (binding is LiveTestWidgetsFlutterBinding)
      binding.deviceEventDispatcher = this;
  }
154

155 156 157
  /// The binding instance used by the testing framework.
  @override
  TestWidgetsFlutterBinding get binding => super.binding;
158 159 160

  /// Renders the UI from the given [widget].
  ///
161 162 163 164
  /// 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.
165 166 167 168
  ///
  /// 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.
169 170 171 172 173
  Future<Null> pumpWidget(Widget widget, [
    Duration duration,
    EnginePhase phase = EnginePhase.sendSemanticsTree
  ]) {
    return TestAsyncUtils.guard(() {
174 175
      binding.attachRootWidget(widget);
      binding.scheduleFrame();
176 177
      return binding.pump(duration, phase);
    });
178 179
  }

180 181 182 183 184
  /// 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.
185
  ///
186 187
  /// This is a convenience function that just calls
  /// [TestWidgetsFlutterBinding.pump].
188
  @override
189 190 191 192 193
  Future<Null> pump([
    Duration duration,
    EnginePhase phase = EnginePhase.sendSemanticsTree
  ]) {
    return TestAsyncUtils.guard(() => binding.pump(duration, phase));
194
  }
195

196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214
  /// Repeatedly calls [pump] with the given `duration` until there are no
  /// longer any transient callbacks scheduled. If no transient callbacks are
  /// scheduled when the function is called, it returns without calling [pump].
  ///
  /// This essentially waits for all animations to have completed.
  ///
  /// This function will never return (and the test will hang and eventually
  /// time out and fail) if there is an infinite animation in progress (for
  /// example, if there is an indeterminate progress indicator spinning).
  ///
  /// 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.
215 216 217 218
  Future<int> pumpUntilNoTransientCallbacks(
    Duration duration, [
      EnginePhase phase = EnginePhase.sendSemanticsTree
    ]) {
219 220 221 222 223 224 225 226
    assert(duration != null);
    assert(duration > Duration.ZERO);
    int count = 0;
    return TestAsyncUtils.guard(() async {
      while (binding.transientCallbackCount > 0) {
        await binding.pump(duration, phase);
        count += 1;
      }
227
    }).then<int>((Null _) => count);
228 229
  }

230 231 232 233 234 235
  @override
  HitTestResult hitTestOnBinding(Point location) {
    location = binding.localToGlobal(location);
    return super.hitTestOnBinding(location);
  }

236 237 238 239 240 241 242 243
  @override
  Future<Null> sendEventToBinding(PointerEvent event, HitTestResult result) {
    return TestAsyncUtils.guard(() async {
      binding.dispatchEvent(event, result, source: TestBindingEventSource.test);
      return null;
    });
  }

244 245 246 247 248 249 250 251 252 253
  /// 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;
254
      final Element innerTargetElement = collectAllElementsFrom(binding.renderViewElement, skipOffstage: true)
255 256 257 258 259 260 261 262 263 264 265
        .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;
266
      debugPrint('Some possible finders for the widgets at ${binding.globalToLocal(event.position)}:');
267 268 269 270 271 272 273 274 275 276 277
      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) {
278
            debugPrint('  find.text(\'${widget.data}\')');
279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295
            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) {
296
              debugPrint('  find.byKey($keyLabel)');
297 298 299 300 301 302 303 304 305
              continue;
            }
          }
        }

        if (!_isPrivate(element.widget.runtimeType)) {
          if (numberOfTypes < 5) {
            final Iterable<Element> matches = find.byType(element.widget.runtimeType).evaluate();
            if (matches.length == 1) {
306
              debugPrint('  find.byType(${element.widget.runtimeType})');
307 308 309 310 311 312 313 314
              numberOfTypes += 1;
              continue;
            }
          }

          if (descendantText != null && numberOfWithTexts < 5) {
            final Iterable<Element> matches = find.widgetWithText(element.widget.runtimeType, descendantText).evaluate();
            if (matches.length == 1) {
315
              debugPrint('  find.widgetWithText(${element.widget.runtimeType}, \'$descendantText\')');
316 317 318 319 320 321 322 323 324
              numberOfWithTexts += 1;
              continue;
            }
          }
        }

        if (!_isPrivate(element.runtimeType)) {
          final Iterable<Element> matches = find.byElementType(element.runtimeType).evaluate();
          if (matches.length == 1) {
325
            debugPrint('  find.byElementType(${element.runtimeType})');
326 327 328 329 330 331 332
            continue;
          }
        }

        totalNumber -= 1; // if we got here, we didn't actually find something to say about it
      }
      if (totalNumber == 0)
333
        debugPrint('  <could not come up with any unique finders>');
334 335 336 337
    }
  }

  bool _isPrivate(Type type) {
338
    // used above so that we don't suggest matchers for private types
339 340 341
    return '_'.matchAsPrefix(type.toString()) != null;
  }

342 343
  /// Returns the exception most recently caught by the Flutter framework.
  ///
344
  /// See [TestWidgetsFlutterBinding.takeException] for details.
345
  dynamic takeException() {
346
    return binding.takeException();
347 348
  }

349 350
  /// Acts as if the application went idle.
  ///
351 352
  /// Runs all remaining microtasks, including those scheduled as a result of
  /// running them, until there are no more microtasks scheduled.
353
  ///
354 355
  /// Does not run timers. May result in an infinite loop or run out of memory
  /// if microtasks continue to recursively schedule new microtasks.
356 357
  Future<Null> idle() {
    return TestAsyncUtils.guard(() => binding.idle());
358
  }
359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416

  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');
  }
}

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