| // Copyright 2015 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:quiver/testing/async.dart'; |
| import 'package:quiver/time.dart'; |
| import 'package:flutter/rendering.dart'; |
| import 'package:flutter/scheduler.dart'; |
| import 'package:flutter/widgets.dart'; |
| |
| import 'instrumentation.dart'; |
| |
| /// Enumeration of possible phases to reach in pumpWidget. |
| enum EnginePhase { |
| layout, |
| compositingBits, |
| paint, |
| composite, |
| flushSemantics, |
| sendSemanticsTree |
| } |
| |
| class _SteppedWidgetFlutterBinding extends WidgetFlutterBinding { |
| |
| /// Creates and initializes the binding. This constructor is |
| /// idempotent; calling it a second time will just return the |
| /// previously-created instance. |
| static WidgetFlutterBinding ensureInitialized() { |
| if (WidgetFlutterBinding.instance == null) |
| new _SteppedWidgetFlutterBinding(); |
| return WidgetFlutterBinding.instance; |
| } |
| |
| EnginePhase phase = EnginePhase.sendSemanticsTree; |
| |
| // Pump the rendering pipeline up to the given phase. |
| @override |
| void beginFrame() { |
| buildDirtyElements(); |
| _beginFrame(); |
| Element.finalizeTree(); |
| } |
| |
| // Cloned from Renderer.beginFrame() but with early-exit semantics. |
| void _beginFrame() { |
| assert(renderView != null); |
| RenderObject.flushLayout(); |
| if (phase == EnginePhase.layout) |
| return; |
| RenderObject.flushCompositingBits(); |
| if (phase == EnginePhase.compositingBits) |
| return; |
| RenderObject.flushPaint(); |
| if (phase == EnginePhase.paint) |
| return; |
| renderView.compositeFrame(); // this sends the bits to the GPU |
| if (phase == EnginePhase.composite) |
| return; |
| if (SemanticsNode.hasListeners) { |
| RenderObject.flushSemantics(); |
| if (phase == EnginePhase.flushSemantics) |
| return; |
| SemanticsNode.sendSemanticsTree(); |
| } |
| } |
| } |
| |
| /// 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 WidgetTester extends Instrumentation { |
| WidgetTester._(FakeAsync async) |
| : async = async, |
| clock = async.getClock(new DateTime.utc(2015, 1, 1)), |
| super(binding: _SteppedWidgetFlutterBinding.ensureInitialized()) { |
| timeDilation = 1.0; |
| ui.window.onBeginFrame = null; |
| } |
| |
| final FakeAsync async; |
| final Clock clock; |
| |
| /// Calls [runApp()] with the given widget, then triggers a frame sequent 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 ]) { |
| 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); |
| } |
| runApp(widget); |
| pump(duration); |
| } |
| |
| /// 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(); |
| } |
| |
| /// 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. |
| void pump([ Duration duration ]) { |
| if (duration != null) |
| async.elapse(duration); |
| binding.handleBeginFrame(new Duration( |
| milliseconds: clock.now().millisecondsSinceEpoch) |
| ); |
| async.flushMicrotasks(); |
| } |
| |
| @override |
| void dispatchEvent(PointerEvent event, HitTestResult result) { |
| super.dispatchEvent(event, result); |
| async.flushMicrotasks(); |
| } |
| } |
| |
| void testWidgets(callback(WidgetTester tester)) { |
| new FakeAsync().run((FakeAsync async) { |
| WidgetTester tester = new WidgetTester._(async); |
| 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(() { |
| "An animation is still running even after the widget tree was disposed."; |
| return Scheduler.instance.transientCallbackCount == 0; |
| }); |
| 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. |
| }); |
| } |