blob: 41d579c9292288c9b828c63b1cda3b1ec7060502 [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:ui' as ui show window;
import 'package:flutter/foundation.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/scheduler.dart';
import 'package:flutter/widgets.dart';
import 'package:quiver/testing/async.dart';
import 'package:quiver/time.dart';
import 'instrumentation.dart';
/// Enumeration of possible phases to reach in pumpWidget.
enum EnginePhase {
layout,
compositingBits,
paint,
composite,
flushSemantics,
sendSemanticsTree
}
class _SteppedWidgetFlutterBinding extends WidgetFlutterBinding { // TODO(ianh): refactor so we're not extending a concrete binding
_SteppedWidgetFlutterBinding(this.async);
final FakeAsync async;
/// Creates and initializes the binding. This constructor is
/// idempotent; calling it a second time will just return the
/// previously-created instance.
static Widgeteer ensureInitialized(FakeAsync async) {
if (Widgeteer.instance == null)
new _SteppedWidgetFlutterBinding(async);
return Widgeteer.instance;
}
EnginePhase phase = EnginePhase.sendSemanticsTree;
// Pump the rendering pipeline up to the given phase.
@override
void beginFrame() {
buildOwner.buildDirtyElements();
_beginFrame();
buildOwner.finalizeTree();
}
// Cloned from Renderer.beginFrame() but with early-exit semantics.
void _beginFrame() {
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) {
super.dispatchEvent(event, result);
async.flushMicrotasks();
}
}
/// Helper class for flutter tests providing fake async.
///
/// This class extends Instrumentation to also abstract away the beginFrame
/// and async/clock access to allow writing tests which depend on the passage
/// of time without actually moving the clock forward.
class ElementTreeTester extends Instrumentation {
ElementTreeTester._(FakeAsync async)
: async = async,
clock = async.getClock(new DateTime.utc(2015, 1, 1)),
super(binding: _SteppedWidgetFlutterBinding.ensureInitialized(async)) {
timeDilation = 1.0;
ui.window.onBeginFrame = null;
debugPrint = _synchronousDebugPrint;
}
void _synchronousDebugPrint(String message, { int wrapWidth }) {
if (wrapWidth != null) {
print(message.split('\n').expand((String line) => debugWordWrap(line, wrapWidth)).join('\n'));
} else {
print(message);
}
}
final FakeAsync async;
final Clock clock;
/// Calls [runApp] with the given widget, then triggers a frame sequence 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.
void pumpWidget(Widget widget, [ Duration duration, EnginePhase phase ]) {
runApp(widget);
pump(duration, phase);
}
/// 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 phase ]) {
if (duration != null)
async.elapse(duration);
if (binding is _SteppedWidgetFlutterBinding) {
// Some tests call WidgetFlutterBinding.ensureInitialized() manually, so
// we can't actually be sure we have a stepped binding.
_SteppedWidgetFlutterBinding steppedBinding = binding;
steppedBinding.phase = phase ?? EnginePhase.sendSemanticsTree;
} else {
// Can't step to a given phase in that case
assert(phase == null);
}
binding.handleBeginFrame(new Duration(
milliseconds: clock.now().millisecondsSinceEpoch)
);
async.flushMicrotasks();
}
/// Artificially calls dispatchLocaleChanged on the Widget binding,
/// then flushes microtasks.
void setLocale(String languageCode, String countryCode) {
Locale locale = new Locale(languageCode, countryCode);
binding.dispatchLocaleChanged(locale);
async.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() {
dynamic result = _pendingException;
_pendingException = null;
return result;
}
dynamic _pendingException;
}
void testElementTree(callback(ElementTreeTester tester)) {
new FakeAsync().run((FakeAsync async) {
FlutterExceptionHandler oldHandler = FlutterError.onError;
ElementTreeTester tester = new ElementTreeTester._(async);
try {
FlutterError.onError = (FlutterErrorDetails details) {
if (tester._pendingException != null) {
FlutterError.dumpErrorToConsole(tester._pendingException);
FlutterError.dumpErrorToConsole(details.exception);
tester._pendingException = 'An uncaught exception was thrown.';
throw details.exception;
}
tester._pendingException = details;
};
runApp(new Container(key: new UniqueKey())); // Reset the tree to a known state.
callback(tester);
runApp(new Container(key: new UniqueKey())); // Unmount any remaining widgets.
async.flushMicrotasks();
assert(Scheduler.instance.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 async.periodicTimerCount == 0;
});
assert(() {
'A Timer is still running even after the widget tree was disposed.';
return async.nonPeriodicTimerCount == 0;
});
assert(async.microtaskCount == 0); // Shouldn't be possible.
if (tester._pendingException != null)
throw 'An exception (shown above) was thrown during the test.';
} finally {
FlutterError.onError = oldHandler;
if (tester._pendingException != null) {
FlutterError.dumpErrorToConsole(tester._pendingException);
tester._pendingException = null;
}
}
});
}