element_tree_tester.dart 7.29 KB
Newer Older
1 2 3 4 5 6
// 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;

7
import 'package:flutter/foundation.dart';
8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171
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 {

  /// 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() {
    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();
    }
  }
}

/// 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()) {
    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();
  }

  @override
  void dispatchEvent(PointerEvent event, HitTestResult result) {
    super.dispatchEvent(event, result);
    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;
172
    ElementTreeTester tester = new ElementTreeTester._(async);
173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198
    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.
199
      if (tester._pendingException != null)
200 201 202
        throw 'An exception (shown above) was thrown during the test.';
    } finally {
      FlutterError.onError = oldHandler;
203 204 205 206
      if (tester._pendingException != null) {
        FlutterError.dumpErrorToConsole(tester._pendingException);
        tester._pendingException = null;
      }
207 208 209
    }
  });
}