Ian Hickson 91dd969966 Refactor the test framework (#3622)
* 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
2016-04-29 13:23:27 -07:00

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;
}
}