rendering_tester.dart 8.7 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
import 'package:flutter/foundation.dart';
Ian Hickson's avatar
Ian Hickson committed
6
import 'package:flutter/gestures.dart';
7
import 'package:flutter/rendering.dart';
Ian Hickson's avatar
Ian Hickson committed
8 9
import 'package:flutter/scheduler.dart';
import 'package:flutter/services.dart';
10
import 'package:flutter_test/flutter_test.dart' show EnginePhase, fail;
11

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

15
class TestRenderingFlutterBinding extends BindingBase with ServicesBinding, GestureBinding, SchedulerBinding, PaintingBinding, SemanticsBinding, RendererBinding {
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
  /// 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].
  TestRenderingFlutterBinding({ this.onErrors });

  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
73 74
  EnginePhase phase = EnginePhase.composite;

75
  @override
76
  void drawFrame() {
77
    assert(phase != EnginePhase.build, 'rendering_tester does not support testing the build phase; use flutter_test instead');
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
    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
115 116 117 118
  }
}

TestRenderingFlutterBinding _renderer;
119
TestRenderingFlutterBinding get renderer {
120
  _renderer ??= TestRenderingFlutterBinding();
121 122
  return _renderer;
}
123

124 125
/// Place the box in the render tree, at the given size and with the given
/// alignment on the screen.
126 127 128 129 130 131 132
///
/// 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).
133 134 135
///
/// The EnginePhase must not be [EnginePhase.build], since the rendering layer
/// has no build phase.
136 137
///
/// If `onErrors` is not null, it is set as [TestRenderingFlutterBinding.onError].
138 139
void layout(
  RenderBox box, {
140
  BoxConstraints constraints,
141 142
  Alignment alignment = Alignment.center,
  EnginePhase phase = EnginePhase.layout,
143
  VoidCallback onErrors,
144
}) {
145 146
  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
147

148
  renderer.renderView.child = null;
149
  if (constraints != null) {
150
    box = RenderPositionedBox(
151
      alignment: alignment,
152
      child: RenderConstrainedBox(
153
        additionalConstraints: constraints,
154 155
        child: box,
      ),
156 157
    );
  }
Ian Hickson's avatar
Ian Hickson committed
158
  renderer.renderView.child = box;
Hixie's avatar
Hixie committed
159

160
  pumpFrame(phase: phase, onErrors: onErrors);
Hixie's avatar
Hixie committed
161 162
}

163 164 165 166
/// 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
167
  assert(renderer != null);
168 169
  assert(renderer.renderView != null);
  assert(renderer.renderView.child != null); // call layout() first!
170 171 172 173 174

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

Ian Hickson's avatar
Ian Hickson committed
175
  renderer.phase = phase;
176
  renderer.drawFrame();
177
}
178 179 180 181 182 183

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

  final VoidCallback onPaint;

184
  @override
185 186 187 188
  void paint(Canvas canvas, Size size) {
    onPaint();
  }

189
  @override
190 191
  bool shouldRepaint(TestCallbackPainter oldPainter) => true;
}
192 193 194 195 196 197 198

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

  final Size _size;

  @override
199
  double computeMinIntrinsicWidth(double height) {
200 201 202 203
    return _size.width;
  }

  @override
204
  double computeMaxIntrinsicWidth(double height) {
205 206 207 208
    return _size.width;
  }

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

  @override
214
  double computeMaxIntrinsicHeight(double width) {
215 216 217 218 219 220 221 222
    return _size.height;
  }

  @override
  bool get sizedByParent => true;

  @override
  void performResize() {
223 224
    size = constraints.constrain(_size);
  }
225 226 227

  @override
  void performLayout() { }
228 229

  @override
230
  bool hitTestSelf(Offset position) => true;
231
}
232 233 234 235 236 237 238 239 240 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

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