mirror of
https://github.com/flutter/flutter.git
synced 2026-02-20 02:29:02 +08:00
* Refactor widget test framework
Instead of:
```dart
test("Card Collection smoke test", () {
testWidgets((WidgetTester tester) {
```
...you now say:
```dart
testWidgets("Card Collection smoke test", (WidgetTester tester) {
```
Instead of:
```dart
expect(tester, hasWidget(find.text('hello')));
```
...you now say:
```dart
expect(find.text('hello'), findsOneWidget);
```
Instead of the previous API (exists, widgets, widget, stateOf,
elementOf, etc), you now have the following comprehensive API. All these
are functions that take a Finder, except the all* properties.
* `any()` - true if anything matches, c.f. `Iterable.any`
* `allWidgets` - all the widgets in the tree
* `widget()` - the one and only widget that matches the finder
* `firstWidget()` - the first widget that matches the finder
* `allElements` - all the elements in the tree
* `element()` - the one and only element that matches the finder
* `firstElement()` - the first element that matches the finder
* `allStates` - all the `State`s in the tree
* `state()` - the one and only state that matches the finder
* `firstState()` - the first state that matches the finder
* `allRenderObjects` - all the render objects in the tree
* `renderObject()` - the one and only render object that matches the finder
* `firstRenderObject()` - the first render object that matches the finder
There's also `layers' which returns the list of current layers.
`tap`, `fling`, getCenter, getSize, etc, take Finders, like the APIs
above, and expect there to only be one matching widget.
The finders are:
* `find.text(String text)`
* `find.widgetWithText(Type widgetType, String text)`
* `find.byKey(Key key)`
* `find.byType(Type type)`
* `find.byElementType(Type type)`
* `find.byConfig(Widget config)`
* `find.byWidgetPredicate(WidgetPredicate predicate)`
* `find.byElementPredicate(ElementPredicate predicate)`
The matchers (for `expect`) are:
* `findsNothing`
* `findsWidgets`
* `findsOneWidget`
* `findsNWidgets(n)`
* `isOnStage`
* `isOffStage`
* `isInCard`
* `isNotInCard`
Benchmarks now use benchmarkWidgets instead of testWidgets.
Also, for those of you using mockers, `serviceMocker` now automatically
handles the binding initialization.
This patch also:
* changes how tests are run so that we can more easily swap the logic
out for a "real" mode instead of FakeAsync.
* introduces CachingIterable.
* changes how flutter_driver interacts with the widget tree to use the
aforementioned new API rather than ElementTreeTester, which is gone.
* removes ElementTreeTester.
* changes the semantics of a test for scrollables because we couldn't
convince ourselves that the old semantics made sense; it only worked
before because flushing the microtasks after every event was broken.
* fixes the flushing of microtasks after every event.
* Reindent the tests
* Fix review comments
267 lines
8.8 KiB
Dart
267 lines
8.8 KiB
Dart
// Copyright 2016 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/foundation.dart';
|
|
import 'package:flutter/gestures.dart';
|
|
import 'package:flutter/rendering.dart';
|
|
import 'package:flutter/scheduler.dart';
|
|
import 'package:flutter/services.dart';
|
|
import 'package:flutter/widgets.dart';
|
|
import 'package:quiver/testing/async.dart';
|
|
import 'package:quiver/time.dart';
|
|
|
|
/// Enumeration of possible phases to reach in
|
|
/// [WidgetTester.pumpWidget] and [TestWidgetsFlutterBinding.pump].
|
|
// TODO(ianh): Merge with identical code in the rendering test code.
|
|
enum EnginePhase {
|
|
layout,
|
|
compositingBits,
|
|
paint,
|
|
composite,
|
|
flushSemantics,
|
|
sendSemanticsTree
|
|
}
|
|
|
|
class TestWidgetsFlutterBinding extends BindingBase with SchedulerBinding, GestureBinding, ServicesBinding, RendererBinding, WidgetsBinding {
|
|
/// Creates and initializes the binding. This constructor is
|
|
/// idempotent; calling it a second time will just return the
|
|
/// previously-created instance.
|
|
static WidgetsBinding ensureInitialized() {
|
|
if (WidgetsBinding.instance == null)
|
|
new TestWidgetsFlutterBinding();
|
|
assert(WidgetsBinding.instance is TestWidgetsFlutterBinding);
|
|
return WidgetsBinding.instance;
|
|
}
|
|
|
|
@override
|
|
void initInstances() {
|
|
timeDilation = 1.0; // just in case the developer has artificially changed it for development
|
|
debugPrint = _synchronousDebugPrint; // TODO(ianh): don't do this when running as 'flutter run'
|
|
super.initInstances();
|
|
}
|
|
|
|
void _synchronousDebugPrint(String message, { int wrapWidth }) {
|
|
if (wrapWidth != null) {
|
|
print(message.split('\n').expand((String line) => debugWordWrap(line, wrapWidth)).join('\n'));
|
|
} else {
|
|
print(message);
|
|
}
|
|
}
|
|
|
|
FakeAsync get fakeAsync => _fakeAsync;
|
|
bool get inTest => fakeAsync != null;
|
|
|
|
FakeAsync _fakeAsync;
|
|
Clock _clock;
|
|
|
|
EnginePhase phase = EnginePhase.sendSemanticsTree;
|
|
|
|
// Pump the rendering pipeline up to the given phase.
|
|
@override
|
|
void beginFrame() {
|
|
assert(inTest);
|
|
buildOwner.buildDirtyElements();
|
|
_beginFrame();
|
|
buildOwner.finalizeTree();
|
|
}
|
|
|
|
// Cloned from RendererBinding.beginFrame() but with early-exit semantics.
|
|
void _beginFrame() {
|
|
assert(inTest);
|
|
assert(renderView != null);
|
|
pipelineOwner.flushLayout();
|
|
if (phase == EnginePhase.layout)
|
|
return;
|
|
pipelineOwner.flushCompositingBits();
|
|
if (phase == EnginePhase.compositingBits)
|
|
return;
|
|
pipelineOwner.flushPaint();
|
|
if (phase == EnginePhase.paint)
|
|
return;
|
|
renderView.compositeFrame(); // this sends the bits to the GPU
|
|
if (phase == EnginePhase.composite)
|
|
return;
|
|
if (SemanticsNode.hasListeners) {
|
|
pipelineOwner.flushSemantics();
|
|
if (phase == EnginePhase.flushSemantics)
|
|
return;
|
|
SemanticsNode.sendSemanticsTree();
|
|
}
|
|
}
|
|
|
|
@override
|
|
void dispatchEvent(PointerEvent event, HitTestResult result) {
|
|
assert(inTest);
|
|
super.dispatchEvent(event, result);
|
|
fakeAsync.flushMicrotasks();
|
|
}
|
|
|
|
/// Triggers a frame sequence (build/layout/paint/etc),
|
|
/// then flushes microtasks.
|
|
///
|
|
/// If duration is set, then advances the clock by that much first.
|
|
/// Doing this flushes microtasks.
|
|
///
|
|
/// The supplied EnginePhase is the final phase reached during the pump pass;
|
|
/// if not supplied, the whole pass is executed.
|
|
void pump([ Duration duration, EnginePhase newPhase = EnginePhase.sendSemanticsTree ]) {
|
|
assert(inTest);
|
|
assert(_clock != null);
|
|
if (duration != null)
|
|
fakeAsync.elapse(duration);
|
|
phase = newPhase;
|
|
handleBeginFrame(new Duration(
|
|
milliseconds: _clock.now().millisecondsSinceEpoch
|
|
));
|
|
fakeAsync.flushMicrotasks();
|
|
}
|
|
|
|
/// Artificially calls dispatchLocaleChanged on the Widget binding,
|
|
/// then flushes microtasks.
|
|
void setLocale(String languageCode, String countryCode) {
|
|
assert(inTest);
|
|
Locale locale = new Locale(languageCode, countryCode);
|
|
dispatchLocaleChanged(locale);
|
|
fakeAsync.flushMicrotasks();
|
|
}
|
|
|
|
/// Returns the exception most recently caught by the Flutter framework.
|
|
///
|
|
/// Call this if you expect an exception during a test. If an exception is
|
|
/// thrown and this is not called, then the exception is rethrown when
|
|
/// the [testWidgets] call completes.
|
|
///
|
|
/// If two exceptions are thrown in a row without the first one being
|
|
/// acknowledged with a call to this method, then when the second exception is
|
|
/// thrown, they are both dumped to the console and then the second is
|
|
/// rethrown from the exception handler. This will likely result in the
|
|
/// framework entering a highly unstable state and everything collapsing.
|
|
///
|
|
/// It's safe to call this when there's no pending exception; it will return
|
|
/// null in that case.
|
|
dynamic takeException() {
|
|
assert(inTest);
|
|
dynamic result = _pendingException?.exception;
|
|
_pendingException = null;
|
|
return result;
|
|
}
|
|
FlutterErrorDetails _pendingException;
|
|
FlutterExceptionHandler _oldHandler;
|
|
int _exceptionCount;
|
|
|
|
/// Called by the [testWidgets] function before a test is executed.
|
|
void preTest() {
|
|
assert(fakeAsync == null);
|
|
assert(_clock == null);
|
|
_fakeAsync = new FakeAsync();
|
|
_clock = fakeAsync.getClock(new DateTime.utc(2015, 1, 1));
|
|
_oldHandler = FlutterError.onError;
|
|
_exceptionCount = 0; // number of un-taken exceptions
|
|
FlutterError.onError = (FlutterErrorDetails details) {
|
|
if (_pendingException != null) {
|
|
if (_exceptionCount == 0) {
|
|
_exceptionCount = 2;
|
|
FlutterError.dumpErrorToConsole(_pendingException, forceReport: true);
|
|
} else {
|
|
_exceptionCount += 1;
|
|
}
|
|
FlutterError.dumpErrorToConsole(details, forceReport: true);
|
|
_pendingException = new FlutterErrorDetails(
|
|
exception: 'Multiple exceptions ($_exceptionCount) were detected during the running of the current test, and at least one was unexpected.',
|
|
library: 'Flutter test framework'
|
|
);
|
|
} else {
|
|
_pendingException = details;
|
|
}
|
|
};
|
|
}
|
|
|
|
/// Invoke the callback inside a [FakeAsync] scope on which [pump] can
|
|
/// advance time.
|
|
///
|
|
/// Returns a future which completes when the test has run.
|
|
///
|
|
/// Called by the [testWidgets] and [benchmarkWidgets] functions to
|
|
/// run a test.
|
|
Future<Null> runTest(Future<Null> callback()) {
|
|
assert(inTest);
|
|
Future<Null> callbackResult;
|
|
fakeAsync.run((FakeAsync fakeAsync) {
|
|
assert(fakeAsync == this.fakeAsync);
|
|
callbackResult = _runTest(callback);
|
|
fakeAsync.flushMicrotasks();
|
|
assert(inTest);
|
|
});
|
|
// callbackResult 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(callbackResult);
|
|
}
|
|
|
|
Future<Null> _runTest(Future<Null> callback()) async {
|
|
assert(inTest);
|
|
|
|
runApp(new Container(key: new UniqueKey())); // Reset the tree to a known state.
|
|
pump();
|
|
|
|
// run the test
|
|
try {
|
|
await callback();
|
|
fakeAsync.flushMicrotasks();
|
|
} catch (exception, stack) {
|
|
// call onError handler above
|
|
FlutterError.reportError(new FlutterErrorDetails(
|
|
exception: exception,
|
|
stack: stack,
|
|
library: 'Flutter test framework'
|
|
));
|
|
}
|
|
|
|
runApp(new Container(key: new UniqueKey())); // Unmount any remaining widgets.
|
|
pump();
|
|
|
|
// verify invariants
|
|
assert(debugAssertNoTransientCallbacks(
|
|
'An animation is still running even after the widget tree was disposed.'
|
|
));
|
|
assert(() {
|
|
'A Timer is still running even after the widget tree was disposed.';
|
|
return fakeAsync.periodicTimerCount == 0;
|
|
});
|
|
assert(() {
|
|
'A Timer is still running even after the widget tree was disposed.';
|
|
return fakeAsync.nonPeriodicTimerCount == 0;
|
|
});
|
|
assert(fakeAsync.microtaskCount == 0); // Shouldn't be possible.
|
|
|
|
// check for unexpected exceptions
|
|
if (_pendingException != null) {
|
|
if (_exceptionCount > 1)
|
|
throw 'Test failed. See exception logs above.';
|
|
throw 'Test failed. See exception log below.';
|
|
}
|
|
|
|
assert(inTest);
|
|
return null;
|
|
}
|
|
|
|
/// Called by the [testWidgets] function after a test is executed.
|
|
void postTest() {
|
|
assert(_fakeAsync != null);
|
|
assert(_clock != null);
|
|
FlutterError.onError = _oldHandler;
|
|
if (_pendingException != null)
|
|
FlutterError.dumpErrorToConsole(_pendingException, forceReport: true);
|
|
_pendingException = null;
|
|
_exceptionCount = null;
|
|
_clock = null;
|
|
_fakeAsync = null;
|
|
}
|
|
|
|
}
|