| // 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. |
| |
| import 'dart:async'; |
| |
| import 'package:flutter/cupertino.dart'; |
| import 'package:flutter/gestures.dart'; |
| import 'package:flutter/material.dart'; |
| import 'package:flutter/rendering.dart'; |
| import 'package:flutter/scheduler.dart'; |
| import 'package:flutter/widgets.dart'; |
| import 'package:meta/meta.dart'; |
| import 'package:test_api/test_api.dart' as test_package; |
| |
| import 'all_elements.dart'; |
| import 'binding.dart'; |
| import 'controller.dart'; |
| import 'finders.dart'; |
| import 'matchers.dart'; |
| import 'test_async_utils.dart'; |
| import 'test_compat.dart'; |
| import 'test_text_input.dart'; |
| |
| /// Keep users from needing multiple imports to test semantics. |
| export 'package:flutter/rendering.dart' show SemanticsHandle; |
| |
| /// Hide these imports so that they do not conflict with our own implementations in |
| /// test_compat.dart. This handles setting up a declarer when one is not defined, which |
| /// can happen when a test is executed via flutter_run. |
| export 'package:test_api/test_api.dart' hide |
| test, |
| group, |
| setUpAll, |
| tearDownAll, |
| setUp, |
| tearDown, |
| 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 |
| |
| /// Signature for callback to [testWidgets] and [benchmarkWidgets]. |
| typedef WidgetTesterCallback = Future<void> Function(WidgetTester widgetTester); |
| |
| /// Runs the [callback] inside the Flutter test environment. |
| /// |
| /// Use this function for testing custom [StatelessWidget]s and |
| /// [StatefulWidget]s. |
| /// |
| /// The callback can be asynchronous (using `async`/`await` or |
| /// using explicit [Future]s). |
| /// |
| /// There are two kinds of timeouts that can be specified. The `timeout` |
| /// argument specifies the backstop timeout implemented by the `test` package. |
| /// If set, it should be relatively large (minutes). It defaults to ten minutes |
| /// for tests run by `flutter test`, and is unlimited for tests run by `flutter |
| /// run`; specifically, it defaults to |
| /// [TestWidgetsFlutterBinding.defaultTestTimeout]. |
| /// |
| /// The `initialTimeout` argument specifies the timeout implemented by the |
| /// `flutter_test` package itself. If set, it may be relatively small (seconds), |
| /// as it is automatically increased for some expensive operations, and can also |
| /// be manually increased by calling |
| /// [AutomatedTestWidgetsFlutterBinding.addTime]. The effective maximum value of |
| /// this timeout (even after calling `addTime`) is the one specified by the |
| /// `timeout` argument. |
| /// |
| /// In general, timeouts are race conditions and cause flakes, so best practice |
| /// is to avoid the use of timeouts in tests. |
| /// |
| /// If the `semanticsEnabled` parameter is set to `true`, |
| /// [WidgetTester.ensureSemantics] will have been called before the tester is |
| /// passed to the `callback`, and that handle will automatically be disposed |
| /// after the callback is finished. |
| /// |
| /// 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]. |
| /// |
| /// See also: |
| /// |
| /// * [AutomatedTestWidgetsFlutterBinding.addTime] to learn more about |
| /// timeout and how to manually increase timeouts. |
| /// |
| /// ## Sample code |
| /// |
| /// ```dart |
| /// testWidgets('MyWidget', (WidgetTester tester) async { |
| /// await tester.pumpWidget(new MyWidget()); |
| /// await tester.tap(find.text('Save')); |
| /// expect(find.text('Success'), findsOneWidget); |
| /// }); |
| /// ``` |
| @isTest |
| void testWidgets( |
| String description, |
| WidgetTesterCallback callback, { |
| bool skip = false, |
| test_package.Timeout timeout, |
| Duration initialTimeout, |
| bool semanticsEnabled = false, |
| }) { |
| final TestWidgetsFlutterBinding binding = TestWidgetsFlutterBinding.ensureInitialized(); |
| final WidgetTester tester = WidgetTester._(binding); |
| test( |
| description, |
| () { |
| SemanticsHandle semanticsHandle; |
| if (semanticsEnabled == true) { |
| semanticsHandle = tester.ensureSemantics(); |
| } |
| tester._recordNumberOfSemanticsHandles(); |
| test_package.addTearDown(binding.postTest); |
| return binding.runTest( |
| () async { |
| await callback(tester); |
| semanticsHandle?.dispose(); |
| }, |
| tester._endOfTestVerifications, |
| description: description ?? '', |
| timeout: initialTimeout, |
| ); |
| }, |
| skip: skip, |
| timeout: timeout ?? binding.defaultTestTimeout, |
| ); |
| } |
| |
| /// 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 |
| /// 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! |
| /// |
| /// If the `semanticsEnabled` parameter is set to `true`, |
| /// [WidgetTester.ensureSemantics] will have been called before the tester is |
| /// passed to the `callback`, and that handle will automatically be disposed |
| /// after the callback is finished. |
| /// |
| /// Benchmarks must not be run in checked mode, because the performance is not |
| /// representative. To avoid this, this function will print a big message if it |
| /// is run in checked mode. Unit tests of this method pass `mayRunWithAsserts`, |
| /// but it should not be used for actual benchmarking. |
| /// |
| /// Example: |
| /// |
| /// main() async { |
| /// assert(false); // fail in checked mode |
| /// await benchmarkWidgets((WidgetTester tester) async { |
| /// await tester.pumpWidget(new MyWidget()); |
| /// final Stopwatch timer = new Stopwatch()..start(); |
| /// for (int index = 0; index < 10000; index += 1) { |
| /// await tester.tap(find.text('Tap me')); |
| /// await tester.pump(); |
| /// } |
| /// timer.stop(); |
| /// debugPrint('Time taken: ${timer.elapsedMilliseconds}ms'); |
| /// }); |
| /// exit(0); |
| /// } |
| Future<void> benchmarkWidgets( |
| WidgetTesterCallback callback, { |
| bool mayRunWithAsserts = false, |
| bool semanticsEnabled = true, |
| }) { |
| assert(() { |
| if (mayRunWithAsserts) |
| return true; |
| |
| 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 ┆'); |
| print('│ line: flutter run --release benchmark.dart ┊'); |
| print('│ '); |
| print('└─────────────────────────────────────────────────╌┄┈ 🐢'); |
| return true; |
| }()); |
| final TestWidgetsFlutterBinding binding = TestWidgetsFlutterBinding.ensureInitialized(); |
| assert(binding is! AutomatedTestWidgetsFlutterBinding); |
| final WidgetTester tester = WidgetTester._(binding); |
| SemanticsHandle semanticsHandle; |
| if (semanticsEnabled == true) { |
| semanticsHandle = tester.ensureSemantics(); |
| } |
| tester._recordNumberOfSemanticsHandles(); |
| return binding.runTest( |
| () async { |
| await callback(tester); |
| semanticsHandle?.dispose(); |
| }, |
| tester._endOfTestVerifications, |
| ) ?? Future<void>.value(); |
| } |
| |
| /// 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. |
| /// |
| /// See also: |
| /// |
| /// * [expectLater] for use with asynchronous matchers. |
| void expect( |
| dynamic actual, |
| dynamic matcher, { |
| String reason, |
| dynamic skip, // true or a String |
| }) { |
| TestAsyncUtils.guardSync(); |
| test_package.expect(actual, matcher, reason: reason, skip: skip); |
| } |
| |
| /// 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, |
| }) { |
| test_package.expect(actual, matcher, reason: reason); |
| } |
| |
| /// 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 |
| }) { |
| // 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(); |
| return test_package.expectLater(actual, matcher, reason: reason, skip: skip) |
| .then<void>((dynamic value) => null); |
| } |
| |
| /// Class that programmatically interacts with widgets and the test environment. |
| /// |
| /// 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 { |
| WidgetTester._(TestWidgetsFlutterBinding binding) : super(binding) { |
| if (binding is LiveTestWidgetsFlutterBinding) |
| binding.deviceEventDispatcher = this; |
| } |
| |
| /// The binding instance used by the testing framework. |
| @override |
| TestWidgetsFlutterBinding get binding => super.binding; |
| |
| /// Renders the UI from the given [widget]. |
| /// |
| /// 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. |
| /// |
| /// 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. |
| /// |
| /// This method should not be used as the first parameter to an [expect] or |
| /// [expectLater] call to test that a widget throws an exception. Instead, use |
| /// [TestWidgetsFlutterBinding.takeException]. |
| /// |
| /// {@tool sample} |
| /// ```dart |
| /// testWidgets('MyWidget asserts invalid bounds', (WidgetTester tester) async { |
| /// await tester.pumpWidget(MyWidget(-1)); |
| /// expect(tester.takeException(), isAssertionError); // or isNull, as appropriate. |
| /// }); |
| /// ``` |
| /// {@end-tool} |
| /// |
| /// See also [LiveTestWidgetsFlutterBindingFramePolicy], which affects how |
| /// this method works when the test is run with `flutter run`. |
| Future<void> pumpWidget( |
| Widget widget, [ |
| Duration duration, |
| EnginePhase phase = EnginePhase.sendSemanticsUpdate, |
| ]) { |
| return TestAsyncUtils.guard<void>(() { |
| binding.attachRootWidget(widget); |
| binding.scheduleFrame(); |
| return binding.pump(duration, phase); |
| }); |
| } |
| |
| /// 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. |
| /// |
| /// This is a convenience function that just calls |
| /// [TestWidgetsFlutterBinding.pump]. |
| /// |
| /// See also [LiveTestWidgetsFlutterBindingFramePolicy], which affects how |
| /// this method works when the test is run with `flutter run`. |
| @override |
| Future<void> pump([ |
| Duration duration, |
| EnginePhase phase = EnginePhase.sendSemanticsUpdate, |
| ]) { |
| return TestAsyncUtils.guard<void>(() => binding.pump(duration, phase)); |
| } |
| |
| /// Triggers a frame after `duration` amount of time, return as soon as the frame is drawn. |
| /// |
| /// This enables driving an artificially high CPU load by rendering frames in |
| /// a tight loop. It must be used with the frame policy set to |
| /// [LiveTestWidgetsFlutterBindingFramePolicy.benchmark]. |
| /// |
| /// Similarly to [pump], this doesn't actually wait for `duration`, just |
| /// advances the clock. |
| Future<void> pumpBenchmark(Duration duration) async { |
| assert(() { |
| final TestWidgetsFlutterBinding widgetsBinding = binding; |
| return widgetsBinding is LiveTestWidgetsFlutterBinding && |
| widgetsBinding.framePolicy == LiveTestWidgetsFlutterBindingFramePolicy.benchmark; |
| }()); |
| |
| dynamic caughtException; |
| void handleError(dynamic error, StackTrace stackTrace) => caughtException ??= error; |
| |
| await Future<void>.microtask(() { binding.handleBeginFrame(duration); }).catchError(handleError); |
| await idle(); |
| await Future<void>.microtask(() { binding.handleDrawFrame(); }).catchError(handleError); |
| await idle(); |
| |
| if (caughtException != null) { |
| throw caughtException; |
| } |
| } |
| |
| /// Repeatedly calls [pump] with the given `duration` until there are no |
| /// 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. |
| /// |
| /// This essentially waits for all animations to have completed. |
| /// |
| /// 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. |
| /// |
| /// 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. |
| Future<int> pumpAndSettle([ |
| Duration duration = const Duration(milliseconds: 100), |
| EnginePhase phase = EnginePhase.sendSemanticsUpdate, |
| Duration timeout = const Duration(minutes: 10), |
| ]) { |
| assert(duration != null); |
| assert(duration > Duration.zero); |
| assert(timeout != null); |
| assert(timeout > Duration.zero); |
| 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; |
| }()); |
| int count = 0; |
| return TestAsyncUtils.guard<void>(() async { |
| final DateTime endTime = binding.clock.fromNowBy(timeout); |
| do { |
| if (binding.clock.now().isAfter(endTime)) |
| throw FlutterError('pumpAndSettle timed out'); |
| await binding.pump(duration, phase); |
| count += 1; |
| } while (binding.hasScheduledFrame); |
| }).then<int>((_) => count); |
| } |
| |
| /// 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. |
| Future<T> runAsync<T>( |
| Future<T> callback(), { |
| Duration additionalTime = const Duration(milliseconds: 1000), |
| }) => binding.runAsync<T>(callback, additionalTime: additionalTime); |
| |
| /// Whether there are any any transient callbacks scheduled. |
| /// |
| /// This essentially checks whether all animations have completed. |
| /// |
| /// 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. |
| bool get hasRunningAnimations => binding.transientCallbackCount > 0; |
| |
| @override |
| HitTestResult hitTestOnBinding(Offset location) { |
| location = binding.localToGlobal(location); |
| return super.hitTestOnBinding(location); |
| } |
| |
| @override |
| Future<void> sendEventToBinding(PointerEvent event, HitTestResult result) { |
| return TestAsyncUtils.guard<void>(() async { |
| binding.dispatchEvent(event, result, source: TestBindingEventSource.test); |
| }); |
| } |
| |
| /// 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, |
| ).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; |
| } |
| 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; |
| debugPrint('Some possible finders for the widgets at ${binding.globalToLocal(event.position)}:'); |
| for (Element element in candidates) { |
| if (totalNumber > 13) // an arbitrary number of finders that feels useful without being overwhelming |
| break; |
| 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; |
| } |
| } |
| |
| 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) { |
| debugPrint(' find.text(\'${widget.data}\')'); |
| 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 Key(\'${key.value}\')'; |
| } |
| if (keyLabel != null) { |
| final Iterable<Element> matches = find.byKey(key).evaluate(); |
| if (matches.length == 1) { |
| debugPrint(' find.byKey($keyLabel)'); |
| continue; |
| } |
| } |
| } |
| |
| if (!_isPrivate(element.widget.runtimeType)) { |
| if (numberOfTypes < 5) { |
| final Iterable<Element> matches = find.byType(element.widget.runtimeType).evaluate(); |
| if (matches.length == 1) { |
| debugPrint(' find.byType(${element.widget.runtimeType})'); |
| numberOfTypes += 1; |
| continue; |
| } |
| } |
| |
| if (descendantText != null && numberOfWithTexts < 5) { |
| final Iterable<Element> matches = find.widgetWithText(element.widget.runtimeType, descendantText).evaluate(); |
| if (matches.length == 1) { |
| debugPrint(' find.widgetWithText(${element.widget.runtimeType}, \'$descendantText\')'); |
| numberOfWithTexts += 1; |
| continue; |
| } |
| } |
| } |
| |
| if (!_isPrivate(element.runtimeType)) { |
| final Iterable<Element> matches = find.byElementType(element.runtimeType).evaluate(); |
| if (matches.length == 1) { |
| debugPrint(' find.byElementType(${element.runtimeType})'); |
| continue; |
| } |
| } |
| |
| totalNumber -= 1; // if we got here, we didn't actually find something to say about it |
| } |
| if (totalNumber == 0) |
| debugPrint(' <could not come up with any unique finders>'); |
| } |
| } |
| |
| bool _isPrivate(Type type) { |
| // used above so that we don't suggest matchers for private types |
| return '_'.matchAsPrefix(type.toString()) != null; |
| } |
| |
| /// Returns the exception most recently caught by the Flutter framework. |
| /// |
| /// See [TestWidgetsFlutterBinding.takeException] for details. |
| dynamic takeException() { |
| return binding.takeException(); |
| } |
| |
| /// Acts as if the application went idle. |
| /// |
| /// Runs all remaining microtasks, including those scheduled as a result of |
| /// running them, until there are no more microtasks scheduled. |
| /// |
| /// Does not run timers. May result in an infinite loop or run out of memory |
| /// if microtasks continue to recursively schedule new microtasks. |
| Future<void> idle() { |
| return TestAsyncUtils.guard<void>(() => binding.idle()); |
| } |
| |
| Set<Ticker> _tickers; |
| |
| @override |
| Ticker createTicker(TickerCallback onTick) { |
| _tickers ??= <_TestTicker>{}; |
| final _TestTicker result = _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 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'); |
| _verifySemanticsHandlesWereDisposed(); |
| } |
| |
| void _verifySemanticsHandlesWereDisposed() { |
| assert(_lastRecordedSemanticsHandles != null); |
| if (binding.pipelineOwner.debugOutstandingSemanticsHandles > _lastRecordedSemanticsHandles) { |
| throw 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.' |
| ); |
| } |
| _lastRecordedSemanticsHandles = null; |
| } |
| |
| int _lastRecordedSemanticsHandles; |
| |
| void _recordNumberOfSemanticsHandles() { |
| _lastRecordedSemanticsHandles = binding.pipelineOwner.debugOutstandingSemanticsHandles; |
| } |
| |
| /// Returns the TestTextInput singleton. |
| /// |
| /// Typical app tests will not need to use this value. To add text to widgets |
| /// like [TextField] or [TextFormField], call [enterText]. |
| TestTextInput get testTextInput => binding.testTextInput; |
| |
| /// Give the text input widget specified by [finder] the focus, as if the |
| /// onscreen keyboard had appeared. |
| /// |
| /// Implies a call to [pump]. |
| /// |
| /// The widget specified by [finder] must be an [EditableText] or have |
| /// an [EditableText] descendant. For example `find.byType(TextField)` |
| /// or `find.byType(TextFormField)`, or `find.byType(EditableText)`. |
| /// |
| /// Tests that just need to add text to widgets like [TextField] |
| /// or [TextFormField] only need to call [enterText]. |
| Future<void> showKeyboard(Finder finder) async { |
| return TestAsyncUtils.guard<void>(() async { |
| final EditableTextState editable = state<EditableTextState>( |
| find.descendant( |
| of: finder, |
| matching: find.byType(EditableText), |
| matchRoot: true, |
| ), |
| ); |
| binding.focusedEditable = editable; |
| await pump(); |
| }); |
| } |
| |
| /// Give the text input widget specified by [finder] the focus and |
| /// enter [text] as if it been provided by the onscreen keyboard. |
| /// |
| /// The widget specified by [finder] must be an [EditableText] or have |
| /// an [EditableText] descendant. For example `find.byType(TextField)` |
| /// or `find.byType(TextFormField)`, or `find.byType(EditableText)`. |
| /// |
| /// To just give [finder] the focus without entering any text, |
| /// see [showKeyboard]. |
| Future<void> enterText(Finder finder, String text) async { |
| return TestAsyncUtils.guard<void>(() async { |
| await showKeyboard(finder); |
| testTextInput.enterText(text); |
| await idle(); |
| }); |
| } |
| |
| /// 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 { |
| return TestAsyncUtils.guard<void>(() async { |
| Finder backButton = find.byTooltip('Back'); |
| if (backButton.evaluate().isEmpty) { |
| backButton = find.byType(CupertinoNavigationBarBackButton); |
| } |
| |
| expectSync(backButton, findsOneWidget, reason: 'One back button expected on screen'); |
| |
| await tap(backButton); |
| }); |
| } |
| |
| /// Attempts to find the [SemanticsNode] of first result from `finder`. |
| /// |
| /// 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. |
| /// 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. |
| SemanticsNode getSemantics(Finder finder) { |
| if (binding.pipelineOwner.semanticsOwner == null) |
| throw StateError('Semantics are not enabled.'); |
| final Iterable<Element> candidates = finder.evaluate(); |
| if (candidates.isEmpty) { |
| throw StateError('Finder returned no matching elements.'); |
| } |
| if (candidates.length > 1) { |
| throw 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 StateError('No Semantics data found.'); |
| return result; |
| } |
| |
| /// 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(); |
| } |
| |
| /// Given a widget `W` specified by [finder] and a [Scrollable] widget `S` in |
| /// its ancestry tree, this scrolls `S` so as to make `W` visible. |
| /// |
| /// Shorthand for `Scrollable.ensureVisible(tester.element(finder))` |
| Future<void> ensureVisible(Finder finder) => Scrollable.ensureVisible(element(finder)); |
| } |
| |
| typedef _TickerDisposeCallback = void Function(_TestTicker ticker); |
| |
| class _TestTicker extends Ticker { |
| _TestTicker(TickerCallback onTick, this._onDispose) : super(onTick); |
| |
| final _TickerDisposeCallback _onDispose; |
| |
| @override |
| void dispose() { |
| if (_onDispose != null) |
| _onDispose(this); |
| super.dispose(); |
| } |
| } |