rendering_tester.dart 12.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 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56
  /// 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 }) {
    if (_instance != null)
      return _instance!;
    return TestRenderingFlutterBinding(onErrors: onErrors);
  }

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

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

108 109 110 111 112 113 114 115 116 117
  /// 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 {
118 119
      TestRenderingFlutterBinding.instance.handleBeginFrame(null);
      TestRenderingFlutterBinding.instance.handleDrawFrame();
120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136
    } 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.');
        }
      }
    }
  }

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

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

203
  TestRenderingFlutterBinding.instance.renderView.child = null;
204
  if (constraints != null) {
205
    box = RenderPositionedBox(
206
      alignment: alignment,
207
      child: RenderConstrainedBox(
208
        additionalConstraints: constraints,
209 210
        child: box,
      ),
211 212
    );
  }
213
  TestRenderingFlutterBinding.instance.renderView.child = box;
Hixie's avatar
Hixie committed
214

215
  pumpFrame(phase: phase, onErrors: onErrors);
Hixie's avatar
Hixie committed
216 217
}

218 219 220
/// Pumps a single frame.
///
/// If `onErrors` is not null, it is set as [TestRenderingFlutterBinding.onError].
221
void pumpFrame({ EnginePhase phase = EnginePhase.layout, VoidCallback? onErrors }) {
222 223 224
  assert(TestRenderingFlutterBinding.instance != null);
  assert(TestRenderingFlutterBinding.instance.renderView != null);
  assert(TestRenderingFlutterBinding.instance.renderView.child != null); // call layout() first!
225 226

  if (onErrors != null) {
227
    TestRenderingFlutterBinding.instance.onErrors = onErrors;
228 229
  }

230 231
  TestRenderingFlutterBinding.instance.phase = phase;
  TestRenderingFlutterBinding.instance.drawFrame();
232
}
233 234

class TestCallbackPainter extends CustomPainter {
235
  const TestCallbackPainter({ required this.onPaint });
236 237 238

  final VoidCallback onPaint;

239
  @override
240 241 242 243
  void paint(Canvas canvas, Size size) {
    onPaint();
  }

244
  @override
245 246
  bool shouldRepaint(TestCallbackPainter oldPainter) => true;
}
247 248 249 250 251 252 253

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

  final Size _size;

  @override
254
  double computeMinIntrinsicWidth(double height) {
255 256 257 258
    return _size.width;
  }

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

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

  @override
269
  double computeMaxIntrinsicHeight(double width) {
270 271 272 273 274 275 276 277
    return _size.height;
  }

  @override
  bool get sizedByParent => true;

  @override
  void performResize() {
278 279
    size = constraints.constrain(_size);
  }
280 281 282

  @override
  void performLayout() { }
283 284

  @override
285
  bool hitTestSelf(Offset position) => true;
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
297
  bool muted = false;
298 299 300 301 302

  @override
  void absorbTicker(Ticker originalTicker) { }

  @override
303
  String? get debugLabel => null;
304 305

  @override
306
  bool get isActive => throw UnimplementedError();
307 308

  @override
309
  bool get isTicking => throw UnimplementedError();
310 311

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

  @override
315
  bool get shouldScheduleTick => throw UnimplementedError();
316 317 318 319 320 321 322 323 324

  @override
  void dispose() { }

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

  @override
  TickerFuture start() {
325
    throw UnimplementedError();
326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341
  }

  @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);
  }
}
342 343 344 345 346

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

  @override
347 348 349 350 351 352 353 354
  ClipRectLayer? pushClipRect(
    bool needsCompositing,
    Offset offset,
    Rect clipRect,
    PaintingContextCallback painter, {
    Clip clipBehavior = Clip.hardEdge,
    ClipRectLayer? oldLayer,
  }) {
355
    this.clipBehavior = clipBehavior;
356
    return null;
357 358 359 360 361 362
  }

  Clip clipBehavior = Clip.none;
}

void expectOverflowedErrors() {
363
  final FlutterErrorDetails errorDetails = TestRenderingFlutterBinding.instance.takeFlutterErrorDetails()!;
364 365 366 367 368 369 370 371
  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));