rendering_tester.dart 12.2 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 41 42 43 44 45 46 47 48 49 50 51
  /// The current [TestRenderingFlutterBinding], if one has been created.
  ///
  /// Provides access to the features exposed by this binding. The binding must
  /// be initialized before using this getter; this is typically done by calling
  /// [TestRenderingFlutterBinding.ensureInitialized].
  static TestRenderingFlutterBinding get instance => BindingBase.checkInstance(_instance);
  static TestRenderingFlutterBinding? _instance;

  @override
  void initInstances() {
    super.initInstances();
    _instance = this;
  }

  /// Creates and initializes the binding. This function is
  /// idempotent; calling it a second time will just return the
  /// previously-created instance.
  static TestRenderingFlutterBinding ensureInitialized({ VoidCallback? onErrors }) {
52
    if (_instance != null) {
53
      return _instance!;
54
    }
55 56 57
    return TestRenderingFlutterBinding(onErrors: onErrors);
  }

58 59 60 61 62 63 64
  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.
65
  VoidCallback? onErrors;
66 67 68 69 70 71

  /// 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.
72
  FlutterErrorDetails? takeFlutterErrorDetails() {
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
    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
107 108
  EnginePhase phase = EnginePhase.composite;

109 110 111 112 113 114 115 116 117 118
  /// Pumps a frame and runs its entire life cycle.
  ///
  /// This method runs all of the [SchedulerPhase]s in a frame, this is useful
  /// to test [SchedulerPhase.postFrameCallbacks].
  void pumpCompleteFrame() {
    final FlutterExceptionHandler? oldErrorHandler = FlutterError.onError;
    FlutterError.onError = (FlutterErrorDetails details) {
      _errors.add(details);
    };
    try {
119 120
      TestRenderingFlutterBinding.instance.handleBeginFrame(null);
      TestRenderingFlutterBinding.instance.handleDrawFrame();
121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137
    } 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.');
        }
      }
    }
  }

138
  @override
139
  void drawFrame() {
140
    assert(phase != EnginePhase.build, 'rendering_tester does not support testing the build phase; use flutter_test instead');
141
    final FlutterExceptionHandler? oldErrorHandler = FlutterError.onError;
142 143 144 145 146
    FlutterError.onError = (FlutterErrorDetails details) {
      _errors.add(details);
    };
    try {
      pipelineOwner.flushLayout();
147
      if (phase == EnginePhase.layout) {
148
        return;
149
      }
150
      pipelineOwner.flushCompositingBits();
151
      if (phase == EnginePhase.compositingBits) {
152
        return;
153
      }
154
      pipelineOwner.flushPaint();
155
      if (phase == EnginePhase.paint) {
156
        return;
157
      }
158
      renderView.compositeFrame();
159
      if (phase == EnginePhase.composite) {
160
        return;
161
      }
162
      pipelineOwner.flushSemantics();
163
      if (phase == EnginePhase.flushSemantics) {
164
        return;
165
      }
166
      assert(phase == EnginePhase.flushSemantics || phase == EnginePhase.sendSemanticsUpdate);
167 168 169 170
    } finally {
      FlutterError.onError = oldErrorHandler;
      if (_errors.isNotEmpty) {
        if (onErrors != null) {
171
          onErrors!();
172 173 174 175 176 177 178 179 180 181
          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
182 183 184
  }
}

185 186
/// Place the box in the render tree, at the given size and with the given
/// alignment on the screen.
187 188 189 190 191 192 193
///
/// 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).
194 195 196
///
/// The EnginePhase must not be [EnginePhase.build], since the rendering layer
/// has no build phase.
197 198
///
/// If `onErrors` is not null, it is set as [TestRenderingFlutterBinding.onError].
199 200
void layout(
  RenderBox box, {
201
  BoxConstraints? constraints,
202 203
  Alignment alignment = Alignment.center,
  EnginePhase phase = EnginePhase.layout,
204
  VoidCallback? onErrors,
205
}) {
206 207
  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
208

209
  TestRenderingFlutterBinding.instance.renderView.child = null;
210
  if (constraints != null) {
211
    box = RenderPositionedBox(
212
      alignment: alignment,
213
      child: RenderConstrainedBox(
214
        additionalConstraints: constraints,
215 216
        child: box,
      ),
217 218
    );
  }
219
  TestRenderingFlutterBinding.instance.renderView.child = box;
Hixie's avatar
Hixie committed
220

221
  pumpFrame(phase: phase, onErrors: onErrors);
Hixie's avatar
Hixie committed
222 223
}

224 225 226
/// Pumps a single frame.
///
/// If `onErrors` is not null, it is set as [TestRenderingFlutterBinding.onError].
227
void pumpFrame({ EnginePhase phase = EnginePhase.layout, VoidCallback? onErrors }) {
228 229 230
  assert(TestRenderingFlutterBinding.instance != null);
  assert(TestRenderingFlutterBinding.instance.renderView != null);
  assert(TestRenderingFlutterBinding.instance.renderView.child != null); // call layout() first!
231 232

  if (onErrors != null) {
233
    TestRenderingFlutterBinding.instance.onErrors = onErrors;
234 235
  }

236 237
  TestRenderingFlutterBinding.instance.phase = phase;
  TestRenderingFlutterBinding.instance.drawFrame();
238
}
239 240

class TestCallbackPainter extends CustomPainter {
241
  const TestCallbackPainter({ required this.onPaint });
242 243 244

  final VoidCallback onPaint;

245
  @override
246 247 248 249
  void paint(Canvas canvas, Size size) {
    onPaint();
  }

250
  @override
251 252
  bool shouldRepaint(TestCallbackPainter oldPainter) => true;
}
253 254 255 256 257 258 259

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

  final Size _size;

  @override
260
  double computeMinIntrinsicWidth(double height) {
261 262 263 264
    return _size.width;
  }

  @override
265
  double computeMaxIntrinsicWidth(double height) {
266 267 268 269
    return _size.width;
  }

  @override
270
  double computeMinIntrinsicHeight(double width) {
271 272 273 274
    return _size.height;
  }

  @override
275
  double computeMaxIntrinsicHeight(double width) {
276 277 278 279 280 281 282 283
    return _size.height;
  }

  @override
  bool get sizedByParent => true;

  @override
  void performResize() {
284 285
    size = constraints.constrain(_size);
  }
286 287 288

  @override
  void performLayout() { }
289 290

  @override
291
  bool hitTestSelf(Offset position) => true;
292
}
293 294 295 296 297 298 299 300 301 302

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

class FakeTicker implements Ticker {
  @override
303
  bool muted = false;
304 305 306 307 308

  @override
  void absorbTicker(Ticker originalTicker) { }

  @override
309
  String? get debugLabel => null;
310 311

  @override
312
  bool get isActive => throw UnimplementedError();
313 314

  @override
315
  bool get isTicking => throw UnimplementedError();
316 317

  @override
318
  bool get scheduled => throw UnimplementedError();
319 320

  @override
321
  bool get shouldScheduleTick => throw UnimplementedError();
322 323 324 325 326 327 328 329 330

  @override
  void dispose() { }

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

  @override
  TickerFuture start() {
331
    throw UnimplementedError();
332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347
  }

  @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);
  }
}
348 349 350 351 352

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

  @override
353 354 355 356 357 358 359 360
  ClipRectLayer? pushClipRect(
    bool needsCompositing,
    Offset offset,
    Rect clipRect,
    PaintingContextCallback painter, {
    Clip clipBehavior = Clip.hardEdge,
    ClipRectLayer? oldLayer,
  }) {
361
    this.clipBehavior = clipBehavior;
362
    return null;
363 364 365 366 367 368
  }

  Clip clipBehavior = Clip.none;
}

void expectOverflowedErrors() {
369
  final FlutterErrorDetails errorDetails = TestRenderingFlutterBinding.instance.takeFlutterErrorDetails()!;
370 371 372 373 374 375 376 377
  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));