blob: ef09dbc4907b4f338eeed1e12fe105d3cfe3bb10 [file] [log] [blame]
// 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;
}
}