blob: 4cd2a38ef46c139b34d5bc68ac9e718ce80d23bc [file] [log] [blame]
// Copyright 2014 The Flutter 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/foundation.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/scheduler.dart';
import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';
import 'package:meta/meta.dart';
// ignore: deprecated_member_use
import 'package:test_api/test_api.dart' as test_package;
import 'all_elements.dart';
import 'binding.dart';
import 'controller.dart';
import 'event_simulation.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;
// ignore: deprecated_member_use
/// 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. It defaults to true.
///
/// 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].
///
/// When the [variant] argument is set, [testWidgets] will run the test once for
/// each value of the [TestVariant.values]. If [variant] is not set, the test
/// will be run once using the base test environment.
///
/// 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 = true,
TestVariant<Object> variant = const DefaultTestVariant(),
}) {
assert(variant != null);
assert(variant.values.isNotEmpty, 'There must be at least on value to test in the testing variant');
final TestWidgetsFlutterBinding binding = TestWidgetsFlutterBinding.ensureInitialized() as TestWidgetsFlutterBinding;
final WidgetTester tester = WidgetTester._(binding);
for (final dynamic value in variant.values) {
final String variationDescription = variant.describeValue(value);
final String combinedDescription = variationDescription.isNotEmpty ? '$description ($variationDescription)' : description;
test(
combinedDescription,
() {
tester._testDescription = combinedDescription;
SemanticsHandle semanticsHandle;
if (semanticsEnabled == true) {
semanticsHandle = tester.ensureSemantics();
}
tester._recordNumberOfSemanticsHandles();
test_package.addTearDown(binding.postTest);
return binding.runTest(
() async {
debugResetSemanticsIdCounter();
tester.resetTestTextInput();
Object memento;
try {
memento = await variant.setUp(value);
await callback(tester);
} finally {
await variant.tearDown(value, memento);
}
semanticsHandle?.dispose();
},
tester._endOfTestVerifications,
description: combinedDescription ?? '',
timeout: initialTimeout,
);
},
skip: skip,
timeout: timeout ?? binding.defaultTestTimeout,
);
}
}
/// An abstract base class for describing test environment variants.
///
/// These serve as elements of the `variants` argument to [testWidgets].
///
/// Use care when adding more testing variants: it multiplies the number of
/// tests which run. This can drastically increase the time it takes to run all
/// the tests.
abstract class TestVariant<T> {
/// A const constructor so that subclasses can be const.
const TestVariant();
/// Returns an iterable of the variations that this test dimension represents.
///
/// The variations returned should be unique so that the same variation isn't
/// needlessly run twice.
Iterable<T> get values;
/// Returns the string that will be used to both add to the test description, and
/// be printed when a test fails for this variation.
String describeValue(T value);
/// A function that will be called before each value is tested, with the
/// value that will be tested.
///
/// This function should preserve any state needed to restore the testing
/// environment back to its base state when [tearDown] is called in the
/// `Object` that is returned. The returned object will then be passed to
/// [tearDown] as a `memento` when the test is complete.
Future<Object> setUp(T value);
/// A function that is guaranteed to be called after a value is tested, even
/// if it throws an exception.
///
/// Calling this function must return the testing environment back to the base
/// state it was in before [setUp] was called. The [memento] is the object
/// returned from [setUp] when it was called.
Future<void> tearDown(T value, covariant Object memento);
}
/// The [TestVariant] that represents the "default" test that is run if no
/// `variants` iterable is specified for [testWidgets].
///
/// This variant can be added into a list of other test variants to provide
/// a "control" test where nothing is changed from the base test environment.
class DefaultTestVariant extends TestVariant<void> {
/// A const constructor for a [DefaultTestVariant].
const DefaultTestVariant();
@override
Iterable<void> get values => const <void>[null];
@override
String describeValue(void value) => '';
@override
Future<void> setUp(void value) async {}
@override
Future<void> tearDown(void value, void memento) async {}
}
/// A [TestVariant] that runs tests with [debugDefaultTargetPlatformOverride]
/// set to different values of [TargetPlatform].
class TargetPlatformVariant extends TestVariant<TargetPlatform> {
/// Creates a [TargetPlatformVariant] that tests the given [values].
const TargetPlatformVariant(this.values);
/// Creates a [TargetPlatformVariant] that tests all values from
/// the [TargetPlatform] enum.
TargetPlatformVariant.all() : values = TargetPlatform.values.toSet();
/// Creates a [TargetPlatformVariant] that tests only the given value of
/// [TargetPlatform].
TargetPlatformVariant.only(TargetPlatform platform) : values = <TargetPlatform>{platform};
@override
final Set<TargetPlatform> values;
@override
String describeValue(TargetPlatform value) => value.toString();
@override
Future<TargetPlatform> setUp(TargetPlatform value) async {
final TargetPlatform previousTargetPlatform = debugDefaultTargetPlatformOverride;
debugDefaultTargetPlatformOverride = value;
return previousTargetPlatform;
}
@override
Future<void> tearDown(TargetPlatform value, TargetPlatform memento) async {
debugDefaultTargetPlatformOverride = memento;
}
}
/// 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 = false,
}) {
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() as TestWidgetsFlutterBinding;
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 description string of the test currently being run.
String get testDescription => _testDescription;
String _testDescription = '';
/// The binding instance used by the testing framework.
@override
TestWidgetsFlutterBinding get binding => super.binding as TestWidgetsFlutterBinding;
/// 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 snippet}
/// ```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 "Vsync" 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
.map((HitTestEntry candidate) => candidate.target)
.whereType<RenderObject>()
.first;
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 (final 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
final Widget widget = element.widget;
if (widget is Tooltip) {
final Iterable<Element> matches = find.byTooltip(widget.message).evaluate();
if (matches.length == 1) {
debugPrint(" find.byTooltip('${widget.message}')");
continue;
}
}
if (widget is Text) {
assert(descendantText == null);
final Iterable<Element> matches = find.text(widget.data).evaluate();
descendantText = widget.data;
if (matches.length == 1) {
debugPrint(" find.text('${widget.data}')");
continue;
}
}
final Key key = widget.key;
if (key is ValueKey<dynamic>) {
String keyLabel;
if (key is ValueKey<int> ||
key is ValueKey<double> ||
key is ValueKey<bool>) {
keyLabel = 'const ${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(widget.runtimeType)) {
if (numberOfTypes < 5) {
final Iterable<Element> matches = find.byType(widget.runtimeType).evaluate();
if (matches.length == 1) {
debugPrint(' find.byType(${widget.runtimeType})');
numberOfTypes += 1;
continue;
}
}
if (descendantText != null && numberOfWithTexts < 5) {
final Iterable<Element> matches = find.widgetWithText(widget.runtimeType, descendantText).evaluate();
if (matches.length == 1) {
debugPrint(" find.widgetWithText(${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 (final Ticker ticker in _tickers) {
if (ticker.isActive) {
throw FlutterError.fromParts(<DiagnosticsNode>[
ErrorSummary('A Ticker was active $when.'),
ErrorDescription('All Tickers must be disposed.'),
ErrorHint(
'Tickers used by AnimationControllers '
'should be disposed by calling dispose() on the AnimationController itself. '
'Otherwise, the ticker will leak.'
),
ticker.describeForError('The offending ticker was')
]);
}
}
}
}
void _endOfTestVerifications() {
verifyTickersWereDisposed('at the end of the test');
_verifySemanticsHandlesWereDisposed();
}
void _verifySemanticsHandlesWereDisposed() {
assert(_lastRecordedSemanticsHandles != null);
if (binding.pipelineOwner.debugOutstandingSemanticsHandles > _lastRecordedSemanticsHandles) {
throw FlutterError.fromParts(<DiagnosticsNode>[
ErrorSummary('A SemanticsHandle was active at the end of the test.'),
ErrorDescription(
'All SemanticsHandle instances must be disposed by calling dispose() on '
'the SemanticsHandle.'
),
ErrorHint(
'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;
/// Ensures that [testTextInput] is registered and [TestTextInput.log] is
/// reset.
///
/// This is called by the testing framework before test runs, so that if a
/// previous test has set its own handler on [SystemChannels.textInput], the
/// [testTextInput] regains control and the log is fresh for the new test.
/// It should not typically need to be called by tests.
void resetTestTextInput() {
testTextInput.resetAndRegister();
}
/// 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();
});
}
/// Simulates sending physical key down and up events through the system channel.
///
/// This only simulates key events coming from a physical keyboard, not from a
/// soft keyboard.
///
/// Specify `platform` as one of the platforms allowed in
/// [Platform.operatingSystem] to make the event appear to be from that type
/// of system. Defaults to "android". Must not be null. Some platforms (e.g.
/// Windows, iOS) are not yet supported.
///
/// Keys that are down when the test completes are cleared after each test.
///
/// This method sends both the key down and the key up events, to simulate a
/// key press. To simulate individual down and/or up events, see
/// [sendKeyDownEvent] and [sendKeyUpEvent].
///
/// See also:
///
/// - [sendKeyDownEvent] to simulate only a key down event.
/// - [sendKeyUpEvent] to simulate only a key up event.
Future<void> sendKeyEvent(LogicalKeyboardKey key, { String platform = 'android' }) async {
assert(platform != null);
await simulateKeyDownEvent(key, platform: platform);
// Internally wrapped in async guard.
return simulateKeyUpEvent(key, platform: platform);
}
/// Simulates sending a physical key down event through the system channel.
///
/// This only simulates key down events coming from a physical keyboard, not
/// from a soft keyboard.
///
/// Specify `platform` as one of the platforms allowed in
/// [Platform.operatingSystem] to make the event appear to be from that type
/// of system. Defaults to "android". Must not be null. Some platforms (e.g.
/// Windows, iOS) are not yet supported.
///
/// Keys that are down when the test completes are cleared after each test.
///
/// See also:
///
/// - [sendKeyUpEvent] to simulate the corresponding key up event.
/// - [sendKeyEvent] to simulate both the key up and key down in the same call.
Future<void> sendKeyDownEvent(LogicalKeyboardKey key, { String platform = 'android' }) async {
assert(platform != null);
// Internally wrapped in async guard.
return simulateKeyDownEvent(key, platform: platform);
}
/// Simulates sending a physical key up event through the system channel.
///
/// This only simulates key up events coming from a physical keyboard,
/// not from a soft keyboard.
///
/// Specify `platform` as one of the platforms allowed in
/// [Platform.operatingSystem] to make the event appear to be from that type
/// of system. Defaults to "android". May not be null.
///
/// See also:
///
/// - [sendKeyDownEvent] to simulate the corresponding key down event.
/// - [sendKeyEvent] to simulate both the key up and key down in the same call.
Future<void> sendKeyUpEvent(LogicalKeyboardKey key, { String platform = 'android' }) async {
assert(platform != null);
// Internally wrapped in async guard.
return simulateKeyUpEvent(key, platform: platform);
}
/// 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 as RenderObject;
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();
}
}