Unverified Commit 8860627b authored by Todd Volkert's avatar Todd Volkert Committed by GitHub

Add WidgetTester.runAsync() (#17029)

This will allow callers to run real async code within a widget test.

https://github.com/flutter/flutter/issues/16859
parent 8b99d1d8
......@@ -182,6 +182,25 @@ abstract class TestWidgetsFlutterBinding extends BindingBase
/// this method works when the test is run with `flutter run`.
Future<Null> pump([ Duration duration, EnginePhase newPhase = EnginePhase.sendSemanticsUpdate ]);
/// 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 [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());
/// Artificially calls dispatchLocaleChanged on the Widget binding,
/// then flushes microtasks.
Future<Null> setLocale(String languageCode, String countryCode) {
......@@ -541,6 +560,7 @@ class AutomatedTestWidgetsFlutterBinding extends TestWidgetsFlutterBinding {
}
FakeAsync _fakeAsync;
Completer<void> _pendingAsyncTasks;
@override
Clock get clock => _clock;
......@@ -582,6 +602,41 @@ class AutomatedTestWidgetsFlutterBinding extends TestWidgetsFlutterBinding {
});
}
@override
Future<T> runAsync<T>(Future<T> callback()) {
assert(() {
if (_pendingAsyncTasks == null)
return true;
throw new test_package.TestFailure(
'Reentrant call to runAsync() denied.\n'
'runAsync() was called, then before its future completed, it '
'was called again. You must wait for the first returned future '
'to complete before calling runAsync() again.'
);
}());
return Zone.root.run(() {
_pendingAsyncTasks = new Completer<void>();
return callback().catchError((dynamic exception, StackTrace stack) {
FlutterError.reportError(new FlutterErrorDetails(
exception: exception,
stack: stack,
library: 'Flutter test framework',
context: 'while running async test code',
));
return null;
}).whenComplete(() {
// We complete the _pendingAsyncTasks future successfully regardless of
// whether an exception occurred because in the case of an exception,
// we already reported the exception to FlutterError. Moreover,
// completing the future with an error would trigger an unhandled
// exception due to zone error boundaries.
_pendingAsyncTasks.complete();
_pendingAsyncTasks = null;
});
});
}
@override
void scheduleWarmUpFrame() {
// We override the default version of this so that the application-startup warm-up frame
......@@ -646,12 +701,22 @@ class AutomatedTestWidgetsFlutterBinding extends TestWidgetsFlutterBinding {
testBodyResult = _runTest(testBody, invariantTester, description);
assert(inTest);
});
// testBodyResult is a Future that was created in the Zone of the fakeAsync.
// This means that if we call .then() on it (as the test framework is about to),
// it will register a microtask to handle the future _in the fake async zone_.
// To avoid this, we wrap it in a Future that we've created _outside_ the fake
// async zone.
return new Future<Null>.value(testBodyResult);
return new Future<Null>.microtask(() async {
// Resolve interplay between fake async and real async calls.
_fakeAsync.flushMicrotasks();
while (_pendingAsyncTasks != null) {
await _pendingAsyncTasks.future;
_fakeAsync.flushMicrotasks();
}
// testBodyResult is a Future that was created in the Zone of the
// fakeAsync. This means that if we await it here, it will register a
// microtask to handle the future _in the fake async zone_. We avoid this
// by returning the wrapped microtask future that we've created _outside_
// the fake async zone.
return testBodyResult;
});
}
@override
......@@ -779,6 +844,7 @@ class LiveTestWidgetsFlutterBinding extends TestWidgetsFlutterBinding {
Completer<Null> _pendingFrame;
bool _expectingFrame = false;
bool _viewNeedsPaint = false;
bool _runningAsyncTasks = false;
/// Whether to have [pump] with a duration only pump a single frame
/// (as would happen in a normal test environment using
......@@ -949,6 +1015,35 @@ class LiveTestWidgetsFlutterBinding extends TestWidgetsFlutterBinding {
});
}
@override
Future<T> runAsync<T>(Future<T> callback()) async {
assert(() {
if (!_runningAsyncTasks)
return true;
throw new test_package.TestFailure(
'Reentrant call to runAsync() denied.\n'
'runAsync() was called, then before its future completed, it '
'was called again. You must wait for the first returned future '
'to complete before calling runAsync() again.'
);
}());
_runningAsyncTasks = true;
try {
return await callback();
} catch (error, stack) {
FlutterError.reportError(new FlutterErrorDetails(
exception: error,
stack: stack,
library: 'Flutter test framework',
context: 'while running async test code',
));
return null;
} finally {
_runningAsyncTasks = false;
}
}
@override
Future<Null> runTest(Future<Null> testBody(), VoidCallback invariantTester, { String description: '' }) async {
assert(description != null);
......
......@@ -292,6 +292,28 @@ class WidgetTester extends WidgetController implements HitTestDispatcher, Ticker
}).then<int>((Null _) => 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()) => binding.runAsync(callback);
/// Whether there are any any transient callbacks scheduled.
///
/// This essentially checks whether all animations have completed.
......
......@@ -3,6 +3,7 @@
// found in the LICENSE file.
import 'dart:async';
import 'dart:io';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
......@@ -451,6 +452,49 @@ void main() {
controller.forward();
expect(await tester.pumpAndSettle(const Duration(milliseconds: 300)), 5); // 0, 300, 600, 900, 1200ms
});
group('runAsync', () {
testWidgets('works with no async calls', (WidgetTester tester) async {
String value;
await tester.runAsync(() async {
value = '123';
});
expect(value, '123');
});
testWidgets('works with real async calls', (WidgetTester tester) async {
final StringBuffer buf = new StringBuffer('1');
await tester.runAsync(() async {
buf.write('2');
await Directory.current.stat();
buf.write('3');
});
buf.write('4');
expect(buf.toString(), '1234');
});
testWidgets('propagates return values', (WidgetTester tester) async {
final String value = await tester.runAsync<String>(() async {
return '123';
});
expect(value, '123');
});
testWidgets('reports errors via framework', (WidgetTester tester) async {
final String value = await tester.runAsync<String>(() async {
throw new ArgumentError();
});
expect(value, isNull);
expect(tester.takeException(), isArgumentError);
});
testWidgets('disallows re-entry', (WidgetTester tester) async {
final Completer<void> completer = new Completer<void>();
tester.runAsync<void>(() => completer.future);
expect(() => tester.runAsync(() async {}), throwsA(const isInstanceOf<TestFailure>()));
completer.complete();
});
});
}
class FakeMatcher extends AsyncMatcher {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment