widget_tester.dart 26 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/cupertino.dart';
8
import 'package:flutter/gestures.dart';
9
import 'package:flutter/material.dart';
10
import 'package:flutter/rendering.dart';
11
import 'package:flutter/scheduler.dart';
12
import 'package:flutter/widgets.dart';
13
import 'package:meta/meta.dart';
14
import 'package:test/test.dart' as test_package;
15

16
import 'all_elements.dart';
17
import 'binding.dart';
18
import 'controller.dart';
19
import 'finders.dart';
20
import 'matchers.dart';
21
import 'test_async_utils.dart';
22
import 'test_text_input.dart';
23

24 25 26
/// Keep users from needing multiple imports to test semantics.
export 'package:flutter/rendering.dart' show SemanticsHandle;

27 28 29 30
export 'package:test/test.dart' hide
  expect, // we have our own wrapper below
  TypeMatcher, // matcher's TypeMatcher conflicts with the one in the Flutter framework
  isInstanceOf; // we have our own wrapper in matchers.dart
31

32
/// Signature for callback to [testWidgets] and [benchmarkWidgets].
33
typedef Future<Null> WidgetTesterCallback(WidgetTester widgetTester);
34

35 36 37 38 39
/// Runs the [callback] inside the Flutter test environment.
///
/// Use this function for testing custom [StatelessWidget]s and
/// [StatefulWidget]s.
///
40 41 42 43 44 45 46 47 48
/// 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].
///
49
/// ## Sample code
50
///
51
/// ```dart
52 53 54
///     testWidgets('MyWidget', (WidgetTester tester) async {
///       await tester.pumpWidget(new MyWidget());
///       await tester.tap(find.text('Save'));
55
///       expect(find.text('Success'), findsOneWidget);
56
///     });
57
/// ```
58
@isTest
59
void testWidgets(String description, WidgetTesterCallback callback, {
60
  bool skip = false,
61
  test_package.Timeout timeout
62
}) {
63 64
  final TestWidgetsFlutterBinding binding = TestWidgetsFlutterBinding.ensureInitialized();
  final WidgetTester tester = new WidgetTester._(binding);
65
  timeout ??= binding.defaultTestTimeout;
66
  test_package.test(
67 68
    description,
    () {
69
      tester._recordNumberOfSemanticsHandles();
70 71 72 73 74 75 76 77 78 79
      test_package.addTearDown(binding.postTest);
      return binding.runTest(
        () => callback(tester),
        tester._endOfTestVerifications,
        description: description ?? '',
      );
    },
    skip: skip,
    timeout: timeout
  );
80 81 82 83 84 85 86 87 88 89 90 91 92
}

/// 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
93 94 95 96
/// 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!
97 98
///
/// Benchmarks must not be run in checked mode. To avoid this, this
99
/// function will print a big message if it is run in checked mode.
100 101 102 103
///
/// Example:
///
///     main() async {
104
///       assert(false); // fail in checked mode
105 106
///       await benchmarkWidgets((WidgetTester tester) async {
///         await tester.pumpWidget(new MyWidget());
107 108
///         final Stopwatch timer = new Stopwatch()..start();
///         for (int index = 0; index < 10000; index += 1) {
109 110
///           await tester.tap(find.text('Tap me'));
///           await tester.pump();
111 112
///         }
///         timer.stop();
113
///         debugPrint('Time taken: ${timer.elapsedMilliseconds}ms');
114 115 116 117
///       });
///       exit(0);
///     }
Future<Null> benchmarkWidgets(WidgetTesterCallback callback) {
118 119 120 121 122 123 124 125 126
  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  ┆');
127
    print('│  line:  flutter run --release benchmark.dart          ┊');
128 129 130
    print('│                                                        ');
    print('└─────────────────────────────────────────────────╌┄┈  🐢');
    return true;
131
  }());
132
  final TestWidgetsFlutterBinding binding = TestWidgetsFlutterBinding.ensureInitialized();
133
  assert(binding is! AutomatedTestWidgetsFlutterBinding);
134
  final WidgetTester tester = new WidgetTester._(binding);
135
  tester._recordNumberOfSemanticsHandles();
136 137 138 139
  return binding.runTest(
    () => callback(tester),
    tester._endOfTestVerifications,
  ) ?? new Future<Null>.value();
140
}
141

142 143 144 145 146
/// 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.
147 148 149 150
///
/// See also:
///
///  * [expectLater] for use with asynchronous matchers.
151 152
void expect(dynamic actual, dynamic matcher, {
  String reason,
Ian Hickson's avatar
Ian Hickson committed
153
  dynamic skip, // true or a String
154 155
}) {
  TestAsyncUtils.guardSync();
Ian Hickson's avatar
Ian Hickson committed
156
  test_package.expect(actual, matcher, reason: reason, skip: skip);
157 158 159 160 161 162 163 164 165 166 167 168 169 170
}

/// 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,
}) {
171
  test_package.expect(actual, matcher, reason: reason);
172 173
}

174 175 176 177 178 179 180 181 182 183 184 185
/// Just like [expect], but returns a [Future] that completes when the matcher
/// has finished matching.
///
/// See [test_package.expectLater] for details.
///
/// If the matcher fails asynchronously, that failure is piped to the returned
/// future where it can be handled by user code. If it is not handled by user
/// code, the test will fail.
Future<void> expectLater(dynamic actual, dynamic matcher, {
  String reason,
  dynamic skip, // true or a String
}) {
186 187 188
  // We can't wrap the delegate in a guard, or we'll hit async barriers in
  // [TestWidgetsFlutterBinding] while we're waiting for the matcher to complete
  TestAsyncUtils.guardSync();
189 190
  return test_package.expectLater(actual, matcher, reason: reason, skip: skip)
           .then<void>((dynamic value) => null);
191 192
}

193
/// Class that programmatically interacts with widgets and the test environment.
194 195 196 197
///
/// 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 {
198 199 200 201
  WidgetTester._(TestWidgetsFlutterBinding binding) : super(binding) {
    if (binding is LiveTestWidgetsFlutterBinding)
      binding.deviceEventDispatcher = this;
  }
202

203 204 205
  /// The binding instance used by the testing framework.
  @override
  TestWidgetsFlutterBinding get binding => super.binding;
206 207 208

  /// Renders the UI from the given [widget].
  ///
209 210 211 212
  /// 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.
213 214 215 216
  ///
  /// 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.
217 218 219
  ///
  /// See also [LiveTestWidgetsFlutterBindingFramePolicy], which affects how
  /// this method works when the test is run with `flutter run`.
220 221
  Future<Null> pumpWidget(Widget widget, [
    Duration duration,
222
    EnginePhase phase = EnginePhase.sendSemanticsUpdate,
223 224
  ]) {
    return TestAsyncUtils.guard(() {
225 226
      binding.attachRootWidget(widget);
      binding.scheduleFrame();
227 228
      return binding.pump(duration, phase);
    });
229 230
  }

231 232 233 234 235
  /// 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.
236
  ///
237 238
  /// This is a convenience function that just calls
  /// [TestWidgetsFlutterBinding.pump].
239 240 241
  ///
  /// See also [LiveTestWidgetsFlutterBindingFramePolicy], which affects how
  /// this method works when the test is run with `flutter run`.
242
  @override
243 244
  Future<Null> pump([
    Duration duration,
245
    EnginePhase phase = EnginePhase.sendSemanticsUpdate,
246 247
  ]) {
    return TestAsyncUtils.guard(() => binding.pump(duration, phase));
248
  }
249

250
  /// Repeatedly calls [pump] with the given `duration` until there are no
251 252 253
  /// 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.
254 255 256
  ///
  /// This essentially waits for all animations to have completed.
  ///
257 258 259 260 261 262 263
  /// 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.
264 265 266 267 268 269 270 271 272 273
  ///
  /// 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.
274
  Future<int> pumpAndSettle([
275
      Duration duration = const Duration(milliseconds: 100),
276
      EnginePhase phase = EnginePhase.sendSemanticsUpdate,
277
      Duration timeout = const Duration(minutes: 10),
278
    ]) {
279
    assert(duration != null);
280
    assert(duration > Duration.zero);
281
    assert(timeout != null);
282
    assert(timeout > Duration.zero);
283 284 285 286 287 288 289 290 291 292 293
    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;
    }());
294 295
    int count = 0;
    return TestAsyncUtils.guard(() async {
296
      final DateTime endTime = binding.clock.fromNowBy(timeout);
297
      do {
298 299
        if (binding.clock.now().isAfter(endTime))
          throw new FlutterError('pumpAndSettle timed out');
300 301
        await binding.pump(duration, phase);
        count += 1;
302
      } while (binding.hasScheduledFrame);
303
    }).then<int>((Null _) => count);
304 305
  }

306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325
  /// Runs a [callback] that performs real asynchronous work.
  ///
  /// This is intended for callers who need to call asynchronous methods where
  /// the methods spawn isolates or OS threads and thus cannot be executed
  /// synchronously by calling [pump].
  ///
  /// If callers were to run these types of asynchronous tasks directly in
  /// their test methods, they run the possibility of encountering deadlocks.
  ///
  /// If [callback] completes successfully, this will return the future
  /// returned by [callback].
  ///
  /// If [callback] completes with an error, the error will be caught by the
  /// Flutter framework and made available via [takeException], and this method
  /// will return a future that completes will `null`.
  ///
  /// Re-entrant calls to this method are not allowed; callers of this method
  /// are required to wait for the returned future to complete before calling
  /// this method again. Attempts to do otherwise will result in a
  /// [TestFailure] error being thrown.
326 327 328
  Future<T> runAsync<T>(Future<T> callback(), {
    Duration additionalTime = const Duration(milliseconds: 250),
  }) => binding.runAsync(callback, additionalTime: additionalTime);
329

330
  /// Whether there are any any transient callbacks scheduled.
331 332
  ///
  /// This essentially checks whether all animations have completed.
333 334 335 336 337 338 339 340 341 342 343
  ///
  /// 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.
344 345
  bool get hasRunningAnimations => binding.transientCallbackCount > 0;

346
  @override
347
  HitTestResult hitTestOnBinding(Offset location) {
348 349 350 351
    location = binding.localToGlobal(location);
    return super.hitTestOnBinding(location);
  }

352 353 354 355 356 357 358 359
  @override
  Future<Null> sendEventToBinding(PointerEvent event, HitTestResult result) {
    return TestAsyncUtils.guard(() async {
      binding.dispatchEvent(event, result, source: TestBindingEventSource.test);
      return null;
    });
  }

360 361 362 363 364 365
  /// 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,
366 367 368 369 370 371 372 373 374 375 376 377
      ).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;
      }
378 379 380 381 382 383 384 385 386 387
      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;
388
      debugPrint('Some possible finders for the widgets at ${binding.globalToLocal(event.position)}:');
389
      for (Element element in candidates) {
390
        if (totalNumber > 13) // an arbitrary number of finders that feels useful without being overwhelming
391
          break;
392 393 394 395 396 397 398 399 400 401
        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;
          }
        }
402 403 404 405 406 407 408

        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) {
409
            debugPrint('  find.text(\'${widget.data}\')');
410 411 412 413 414 415 416
            continue;
          }
        }

        if (element.widget.key is ValueKey<dynamic>) {
          final ValueKey<dynamic> key = element.widget.key;
          String keyLabel;
417 418 419
          if (key is ValueKey<int> ||
              key is ValueKey<double> ||
              key is ValueKey<bool>) {
420 421
            keyLabel = 'const ${element.widget.key.runtimeType}(${key.value})';
          } else if (key is ValueKey<String>) {
422
            keyLabel = 'const Key(\'${key.value}\')';
423 424 425 426
          }
          if (keyLabel != null) {
            final Iterable<Element> matches = find.byKey(key).evaluate();
            if (matches.length == 1) {
427
              debugPrint('  find.byKey($keyLabel)');
428 429 430 431 432 433 434 435 436
              continue;
            }
          }
        }

        if (!_isPrivate(element.widget.runtimeType)) {
          if (numberOfTypes < 5) {
            final Iterable<Element> matches = find.byType(element.widget.runtimeType).evaluate();
            if (matches.length == 1) {
437
              debugPrint('  find.byType(${element.widget.runtimeType})');
438 439 440 441 442 443 444 445
              numberOfTypes += 1;
              continue;
            }
          }

          if (descendantText != null && numberOfWithTexts < 5) {
            final Iterable<Element> matches = find.widgetWithText(element.widget.runtimeType, descendantText).evaluate();
            if (matches.length == 1) {
446
              debugPrint('  find.widgetWithText(${element.widget.runtimeType}, \'$descendantText\')');
447 448 449 450 451 452 453 454 455
              numberOfWithTexts += 1;
              continue;
            }
          }
        }

        if (!_isPrivate(element.runtimeType)) {
          final Iterable<Element> matches = find.byElementType(element.runtimeType).evaluate();
          if (matches.length == 1) {
456
            debugPrint('  find.byElementType(${element.runtimeType})');
457 458 459 460 461 462 463
            continue;
          }
        }

        totalNumber -= 1; // if we got here, we didn't actually find something to say about it
      }
      if (totalNumber == 0)
464
        debugPrint('  <could not come up with any unique finders>');
465 466 467 468
    }
  }

  bool _isPrivate(Type type) {
469
    // used above so that we don't suggest matchers for private types
470 471 472
    return '_'.matchAsPrefix(type.toString()) != null;
  }

473 474
  /// Returns the exception most recently caught by the Flutter framework.
  ///
475
  /// See [TestWidgetsFlutterBinding.takeException] for details.
476
  dynamic takeException() {
477
    return binding.takeException();
478 479
  }

480 481
  /// Acts as if the application went idle.
  ///
482 483
  /// Runs all remaining microtasks, including those scheduled as a result of
  /// running them, until there are no more microtasks scheduled.
484
  ///
485 486
  /// Does not run timers. May result in an infinite loop or run out of memory
  /// if microtasks continue to recursively schedule new microtasks.
487 488
  Future<Null> idle() {
    return TestAsyncUtils.guard(() => binding.idle());
489
  }
490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531

  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');
532 533 534 535
    _verifySemanticsHandlesWereDisposed();
  }

  void _verifySemanticsHandlesWereDisposed() {
536 537
    assert(_lastRecordedSemanticsHandles != null);
    if (binding.pipelineOwner.debugOutstandingSemanticsHandles > _lastRecordedSemanticsHandles) {
538 539 540 541 542 543 544 545
      throw new FlutterError(
        'A SemanticsHandle was active at the end of the test.\n'
        'All SemanticsHandle instances must be disposed by calling dispose() on '
        'the SemanticsHandle. If your test uses SemanticsTester, it is '
        'sufficient to call dispose() on SemanticsTester. Otherwise, the '
        'existing handle will leak into another test and alter its behavior.'
      );
    }
546 547 548 549 550 551 552
    _lastRecordedSemanticsHandles = null;
  }

  int _lastRecordedSemanticsHandles;

  void _recordNumberOfSemanticsHandles() {
    _lastRecordedSemanticsHandles = binding.pipelineOwner.debugOutstandingSemanticsHandles;
553
  }
554 555 556 557

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

561
  /// Give the text input widget specified by [finder] the focus, as if the
562 563
  /// onscreen keyboard had appeared.
  ///
564 565
  /// Implies a call to [pump].
  ///
566 567
  /// The widget specified by [finder] must be an [EditableText] or have
  /// an [EditableText] descendant. For example `find.byType(TextField)`
568
  /// or `find.byType(TextFormField)`, or `find.byType(EditableText)`.
569 570
  ///
  /// Tests that just need to add text to widgets like [TextField]
571
  /// or [TextFormField] only need to call [enterText].
572
  Future<Null> showKeyboard(Finder finder) async {
573
    return TestAsyncUtils.guard(() async {
574 575 576 577 578 579 580 581 582
      final EditableTextState editable = state(
        find.descendant(
          of: finder,
          matching: find.byType(EditableText),
          matchRoot: true,
        ),
      );
      binding.focusedEditable = editable;
      await pump();
583
    });
584 585
  }

586
  /// Give the text input widget specified by [finder] the focus and
587
  /// enter [text] as if it been provided by the onscreen keyboard.
588 589 590
  ///
  /// The widget specified by [finder] must be an [EditableText] or have
  /// an [EditableText] descendant. For example `find.byType(TextField)`
591
  /// or `find.byType(TextFormField)`, or `find.byType(EditableText)`.
592 593 594
  ///
  /// To just give [finder] the focus without entering any text,
  /// see [showKeyboard].
595
  Future<Null> enterText(Finder finder, String text) async {
596 597 598 599 600
    return TestAsyncUtils.guard(() async {
      await showKeyboard(finder);
      testTextInput.enterText(text);
      await idle();
    });
601
  }
602 603 604 605 606 607

  /// Makes an effort to dismiss the current page with a Material [Scaffold] or
  /// a [CupertinoPageScaffold].
  ///
  /// Will throw an error if there is no back button in the page.
  Future<void> pageBack() async {
608 609 610
    return TestAsyncUtils.guard(() async {
      Finder backButton = find.byTooltip('Back');
      if (backButton.evaluate().isEmpty) {
611
        backButton = find.byType(CupertinoNavigationBarBackButton);
612
      }
613

614
      expectSync(backButton, findsOneWidget, reason: 'One back button expected on screen');
615

616 617
      await tap(backButton);
    });
618
  }
619 620

  /// Attempts to find the [SemanticsData] of first result from `finder`.
621
  ///
622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656
  /// If the object identified by the finder doesn't own it's semantic node,
  /// this will return the semantics data of the first ancestor with semantics
  /// data. The ancestor's semantic data will include the child's as well as
  /// other nodes that have been merged together.
  ///
  /// Will throw a [StateError] if the finder returns more than one element or
  /// if no semantics are found or are not enabled.
  SemanticsData getSemanticsData(Finder finder) {
    if (binding.pipelineOwner.semanticsOwner == null)
      throw new StateError('Semantics are not enabled.');
    final Iterable<Element> candidates = finder.evaluate();
    if (candidates.isEmpty) {
      throw new StateError('Finder returned no matching elements.');
    }
    if (candidates.length > 1) {
      throw new StateError('Finder returned more than one element.');
    }
    final Element element = candidates.single;
    RenderObject renderObject = element.findRenderObject();
    SemanticsNode result = renderObject.debugSemantics;
    while (renderObject != null && result == null) {
      renderObject = renderObject?.parent;
      result = renderObject?.debugSemantics;
    }
    if (result == null)
      throw new StateError('No Semantics data found.');
    return result.getSemanticsData();
  }

  /// Enable semantics in a test by creating a [SemanticsHandle].
  ///
  /// The handle must be disposed at the end of the test.
  SemanticsHandle ensureSemantics() {
    return binding.pipelineOwner.ensureSemantics();
  }
657 658
}

659
typedef void _TickerDisposeCallback(_TestTicker ticker);
660 661 662 663 664 665 666 667 668 669 670 671

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

  _TickerDisposeCallback _onDispose;

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