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