rendering_tester.dart 9.99 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
  ///
  /// 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 108 109 110 111 112 113
    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) {
114
          onErrors!();
115 116 117 118 119 120 121 122 123 124
          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 129 130
late final TestRenderingFlutterBinding _renderer = TestRenderingFlutterBinding();
TestRenderingFlutterBinding get renderer => _renderer;

131

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

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

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

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

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

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

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

  final VoidCallback onPaint;

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

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

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

  final Size _size;

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

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

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

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

  @override
  bool get sizedByParent => true;

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

  @override
  void performLayout() { }
236 237

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

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

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

  @override
  void absorbTicker(Ticker originalTicker) { }

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

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

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

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

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

  @override
  void dispose() { }

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

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

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

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

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

  Clip clipBehavior = Clip.none;
}

void expectOverflowedErrors() {
316
  final FlutterErrorDetails errorDetails = renderer.takeFlutterErrorDetails()!;
317 318 319 320 321 322 323 324
  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));