rendering_tester.dart 9.04 KB
Newer Older
Ian Hickson's avatar
Ian Hickson committed
1
// Copyright 2014 The Flutter Authors. All rights reserved.
Ian Hickson's avatar
Ian Hickson committed
2 3 4
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

5 6
import 'dart:async';

7
import 'package:flutter/foundation.dart';
Ian Hickson's avatar
Ian Hickson committed
8
import 'package:flutter/gestures.dart';
9
import 'package:flutter/rendering.dart';
Ian Hickson's avatar
Ian Hickson committed
10 11
import 'package:flutter/scheduler.dart';
import 'package:flutter/services.dart';
12
import 'package:flutter_test/flutter_test.dart' show EnginePhase, fail;
13

14
export 'package:flutter/foundation.dart' show FlutterError, FlutterErrorDetails;
15
export 'package:flutter_test/flutter_test.dart' show EnginePhase;
16

17
class TestRenderingFlutterBinding extends BindingBase with SchedulerBinding, ServicesBinding, GestureBinding, PaintingBinding, SemanticsBinding, RendererBinding {
18 19 20 21 22 23
  /// Creates a binding for testing rendering library functionality.
  ///
  /// If [onErrors] is not null, it is called if [FlutterError] caught any errors
  /// while drawing the frame. If [onErrors] is null and [FlutterError] caught at least
  /// one error, this function fails the test. A test may override [onErrors] and
  /// inspect errors using [takeFlutterErrorDetails].
24 25 26 27 28 29 30 31 32
  ///
  /// Errors caught between frames will cause the test to fail unless
  /// [FlutterError.onError] has been overridden.
  TestRenderingFlutterBinding({ this.onErrors }) {
    FlutterError.onError = (FlutterErrorDetails details) {
      FlutterError.dumpErrorToConsole(details);
      Zone.current.parent.handleUncaughtError(details.exception, details.stack);
    };
  }
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

  final List<FlutterErrorDetails> _errors = <FlutterErrorDetails>[];

  /// A function called after drawing a frame if [FlutterError] caught any errors.
  ///
  /// This function is expected to inspect these errors and decide whether they
  /// are expected or not. Use [takeFlutterErrorDetails] to take one error at a
  /// time, or [takeAllFlutterErrorDetails] to iterate over all errors.
  VoidCallback onErrors;

  /// Returns the error least recently caught by [FlutterError] and removes it
  /// from the list of captured errors.
  ///
  /// Returns null if no errors were captures, or if the list was exhausted by
  /// calling this method repeatedly.
  FlutterErrorDetails takeFlutterErrorDetails() {
    if (_errors.isEmpty) {
      return null;
    }
    return _errors.removeAt(0);
  }

  /// Returns all error details caught by [FlutterError] from least recently caught to
  /// most recently caught, and removes them from the list of captured errors.
  ///
  /// The returned iterable takes errors lazily. If, for example, you iterate over 2
  /// errors, but there are 5 errors total, this binding will still fail the test.
  /// Tests are expected to take and inspect all errors.
  Iterable<FlutterErrorDetails> takeAllFlutterErrorDetails() sync* {
    // sync* and yield are used for lazy evaluation. Otherwise, the list would be
    // drained eagerly and allow a test pass with unexpected errors.
    while (_errors.isNotEmpty) {
      yield _errors.removeAt(0);
    }
  }

  /// Returns all exceptions caught by [FlutterError] from least recently caught to
  /// most recently caught, and removes them from the list of captured errors.
  ///
  /// The returned iterable takes errors lazily. If, for example, you iterate over 2
  /// errors, but there are 5 errors total, this binding will still fail the test.
  /// Tests are expected to take and inspect all errors.
  Iterable<dynamic> takeAllFlutterExceptions() sync* {
    // sync* and yield are used for lazy evaluation. Otherwise, the list would be
    // drained eagerly and allow a test pass with unexpected errors.
    while (_errors.isNotEmpty) {
      yield _errors.removeAt(0).exception;
    }
  }

Ian Hickson's avatar
Ian Hickson committed
83 84
  EnginePhase phase = EnginePhase.composite;

85
  @override
86
  void drawFrame() {
87
    assert(phase != EnginePhase.build, 'rendering_tester does not support testing the build phase; use flutter_test instead');
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
    final FlutterExceptionHandler oldErrorHandler = FlutterError.onError;
    FlutterError.onError = (FlutterErrorDetails details) {
      _errors.add(details);
    };
    try {
      pipelineOwner.flushLayout();
      if (phase == EnginePhase.layout)
        return;
      pipelineOwner.flushCompositingBits();
      if (phase == EnginePhase.compositingBits)
        return;
      pipelineOwner.flushPaint();
      if (phase == EnginePhase.paint)
        return;
      renderView.compositeFrame();
      if (phase == EnginePhase.composite)
        return;
      pipelineOwner.flushSemantics();
      if (phase == EnginePhase.flushSemantics)
        return;
      assert(phase == EnginePhase.flushSemantics ||
            phase == EnginePhase.sendSemanticsUpdate);
    } finally {
      FlutterError.onError = oldErrorHandler;
      if (_errors.isNotEmpty) {
        if (onErrors != null) {
          onErrors();
          if (_errors.isNotEmpty) {
            _errors.forEach(FlutterError.dumpErrorToConsole);
            fail('There are more errors than the test inspected using TestRenderingFlutterBinding.takeFlutterErrorDetails.');
          }
        } else {
          _errors.forEach(FlutterError.dumpErrorToConsole);
          fail('Caught error while rendering frame. See preceding logs for details.');
        }
      }
    }
Ian Hickson's avatar
Ian Hickson committed
125 126 127 128
  }
}

TestRenderingFlutterBinding _renderer;
129
TestRenderingFlutterBinding get renderer {
130
  _renderer ??= TestRenderingFlutterBinding();
131 132
  return _renderer;
}
133

134 135
/// Place the box in the render tree, at the given size and with the given
/// alignment on the screen.
136 137 138 139 140 141 142
///
/// If you've updated `box` and want to lay it out again, use [pumpFrame].
///
/// Once a particular [RenderBox] has been passed to [layout], it cannot easily
/// be put in a different place in the tree or passed to [layout] again, because
/// [layout] places the given object into another [RenderBox] which you would
/// need to unparent it from (but that box isn't itself made available).
143 144 145
///
/// The EnginePhase must not be [EnginePhase.build], since the rendering layer
/// has no build phase.
146 147
///
/// If `onErrors` is not null, it is set as [TestRenderingFlutterBinding.onError].
148 149
void layout(
  RenderBox box, {
150
  BoxConstraints constraints,
151 152
  Alignment alignment = Alignment.center,
  EnginePhase phase = EnginePhase.layout,
153
  VoidCallback onErrors,
154
}) {
155 156
  assert(box != null); // If you want to just repump the last box, call pumpFrame().
  assert(box.parent == null); // We stick the box in another, so you can't reuse it easily, sorry.
Hixie's avatar
Hixie committed
157

158
  renderer.renderView.child = null;
159
  if (constraints != null) {
160
    box = RenderPositionedBox(
161
      alignment: alignment,
162
      child: RenderConstrainedBox(
163
        additionalConstraints: constraints,
164 165
        child: box,
      ),
166 167
    );
  }
Ian Hickson's avatar
Ian Hickson committed
168
  renderer.renderView.child = box;
Hixie's avatar
Hixie committed
169

170
  pumpFrame(phase: phase, onErrors: onErrors);
Hixie's avatar
Hixie committed
171 172
}

173 174 175 176
/// Pumps a single frame.
///
/// If `onErrors` is not null, it is set as [TestRenderingFlutterBinding.onError].
void pumpFrame({ EnginePhase phase = EnginePhase.layout, VoidCallback onErrors }) {
Ian Hickson's avatar
Ian Hickson committed
177
  assert(renderer != null);
178 179
  assert(renderer.renderView != null);
  assert(renderer.renderView.child != null); // call layout() first!
180 181 182 183 184

  if (onErrors != null) {
    renderer.onErrors = onErrors;
  }

Ian Hickson's avatar
Ian Hickson committed
185
  renderer.phase = phase;
186
  renderer.drawFrame();
187
}
188 189 190 191 192 193

class TestCallbackPainter extends CustomPainter {
  const TestCallbackPainter({ this.onPaint });

  final VoidCallback onPaint;

194
  @override
195 196 197 198
  void paint(Canvas canvas, Size size) {
    onPaint();
  }

199
  @override
200 201
  bool shouldRepaint(TestCallbackPainter oldPainter) => true;
}
202 203 204 205 206 207 208

class RenderSizedBox extends RenderBox {
  RenderSizedBox(this._size);

  final Size _size;

  @override
209
  double computeMinIntrinsicWidth(double height) {
210 211 212 213
    return _size.width;
  }

  @override
214
  double computeMaxIntrinsicWidth(double height) {
215 216 217 218
    return _size.width;
  }

  @override
219
  double computeMinIntrinsicHeight(double width) {
220 221 222 223
    return _size.height;
  }

  @override
224
  double computeMaxIntrinsicHeight(double width) {
225 226 227 228 229 230 231 232
    return _size.height;
  }

  @override
  bool get sizedByParent => true;

  @override
  void performResize() {
233 234
    size = constraints.constrain(_size);
  }
235 236 237

  @override
  void performLayout() { }
238 239

  @override
240
  bool hitTestSelf(Offset position) => true;
241
}
242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296

class FakeTickerProvider implements TickerProvider {
  @override
  Ticker createTicker(TickerCallback onTick, [ bool disableAnimations = false ]) {
    return FakeTicker();
  }
}

class FakeTicker implements Ticker {
  @override
  bool muted;

  @override
  void absorbTicker(Ticker originalTicker) { }

  @override
  String get debugLabel => null;

  @override
  bool get isActive => null;

  @override
  bool get isTicking => null;

  @override
  bool get scheduled => null;

  @override
  bool get shouldScheduleTick => null;

  @override
  void dispose() { }

  @override
  void scheduleTick({ bool rescheduling = false }) { }

  @override
  TickerFuture start() {
    return null;
  }

  @override
  void stop({ bool canceled = false }) { }

  @override
  void unscheduleTick() { }

  @override
  String toString({ bool debugIncludeStack = false }) => super.toString();

  @override
  DiagnosticsNode describeForError(String name) {
    return DiagnosticsProperty<Ticker>(name, this, style: DiagnosticsTreeStyle.errorProperty);
  }
}