rendering_tester.dart 10.1 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 TestDefaultBinaryMessengerBinding, EnginePhase, fail;
13

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

17
class TestRenderingFlutterBinding extends BindingBase with SchedulerBinding, ServicesBinding, GestureBinding, PaintingBinding, SemanticsBinding, RendererBinding, TestDefaultBinaryMessengerBinding {
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
  ///
  /// 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);
30
      Zone.current.parent!.handleUncaughtError(details.exception, details.stack!);
31 32
    };
  }
33 34 35 36 37 38 39 40

  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.
41
  VoidCallback? onErrors;
42 43 44 45 46 47

  /// 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.
48
  FlutterErrorDetails? takeFlutterErrorDetails() {
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
    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
    final FlutterExceptionHandler? oldErrorHandler = FlutterError.onError;
89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107
    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;
108
      assert(phase == EnginePhase.flushSemantics || phase == EnginePhase.sendSemanticsUpdate);
109 110 111 112
    } finally {
      FlutterError.onError = oldErrorHandler;
      if (_errors.isNotEmpty) {
        if (onErrors != null) {
113
          onErrors!();
114 115 116 117 118 119 120 121 122 123
          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
124 125 126
  }
}

127 128 129
late final TestRenderingFlutterBinding _renderer = TestRenderingFlutterBinding();
TestRenderingFlutterBinding get renderer => _renderer;

130

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

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

167
  pumpFrame(phase: phase, onErrors: onErrors);
Hixie's avatar
Hixie committed
168 169
}

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

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

Ian Hickson's avatar
Ian Hickson committed
182
  renderer.phase = phase;
183
  renderer.drawFrame();
184
}
185 186

class TestCallbackPainter extends CustomPainter {
187
  const TestCallbackPainter({ required this.onPaint });
188 189 190

  final VoidCallback onPaint;

191
  @override
192 193 194 195
  void paint(Canvas canvas, Size size) {
    onPaint();
  }

196
  @override
197 198
  bool shouldRepaint(TestCallbackPainter oldPainter) => true;
}
199 200 201 202 203 204 205

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

  final Size _size;

  @override
206
  double computeMinIntrinsicWidth(double height) {
207 208 209 210
    return _size.width;
  }

  @override
211
  double computeMaxIntrinsicWidth(double height) {
212 213 214 215
    return _size.width;
  }

  @override
216
  double computeMinIntrinsicHeight(double width) {
217 218 219 220
    return _size.height;
  }

  @override
221
  double computeMaxIntrinsicHeight(double width) {
222 223 224 225 226 227 228 229
    return _size.height;
  }

  @override
  bool get sizedByParent => true;

  @override
  void performResize() {
230 231
    size = constraints.constrain(_size);
  }
232 233 234

  @override
  void performLayout() { }
235 236

  @override
237
  bool hitTestSelf(Offset position) => true;
238
}
239 240 241 242 243 244 245 246 247 248

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

class FakeTicker implements Ticker {
  @override
249
  bool muted = false;
250 251 252 253 254

  @override
  void absorbTicker(Ticker originalTicker) { }

  @override
255
  String? get debugLabel => null;
256 257

  @override
258
  bool get isActive => throw UnimplementedError();
259 260

  @override
261
  bool get isTicking => throw UnimplementedError();
262 263

  @override
264
  bool get scheduled => throw UnimplementedError();
265 266

  @override
267
  bool get shouldScheduleTick => throw UnimplementedError();
268 269 270 271 272 273 274 275 276

  @override
  void dispose() { }

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

  @override
  TickerFuture start() {
277
    throw UnimplementedError();
278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293
  }

  @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);
  }
}
294 295 296 297 298

class TestClipPaintingContext extends PaintingContext {
  TestClipPaintingContext() : super(ContainerLayer(), Rect.zero);

  @override
299 300 301 302 303 304 305 306
  ClipRectLayer? pushClipRect(
    bool needsCompositing,
    Offset offset,
    Rect clipRect,
    PaintingContextCallback painter, {
    Clip clipBehavior = Clip.hardEdge,
    ClipRectLayer? oldLayer,
  }) {
307 308 309 310 311 312 313
    this.clipBehavior = clipBehavior;
  }

  Clip clipBehavior = Clip.none;
}

void expectOverflowedErrors() {
314
  final FlutterErrorDetails errorDetails = renderer.takeFlutterErrorDetails()!;
315 316 317 318 319 320 321 322
  final bool overflowed = errorDetails.toString().contains('overflowed');
  if (!overflowed) {
    FlutterError.reportError(errorDetails);
  }
}

RenderConstrainedBox get box200x200 =>
    RenderConstrainedBox(additionalConstraints: const BoxConstraints.tightFor(height: 200.0, width: 200.0));