• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1// Copyright 2015 The Chromium Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5import 'package:flutter/foundation.dart';
6import 'package:flutter/gestures.dart';
7import 'package:flutter/rendering.dart';
8import 'package:flutter/scheduler.dart';
9import 'package:flutter/services.dart';
10import 'package:flutter_test/flutter_test.dart' show EnginePhase, fail;
11
12export 'package:flutter/foundation.dart' show FlutterError, FlutterErrorDetails;
13export 'package:flutter_test/flutter_test.dart' show EnginePhase;
14
15class TestRenderingFlutterBinding extends BindingBase with ServicesBinding, GestureBinding, SchedulerBinding, PaintingBinding, SemanticsBinding, RendererBinding {
16  /// Creates a binding for testing rendering library functionality.
17  ///
18  /// If [onErrors] is not null, it is called if [FlutterError] caught any errors
19  /// while drawing the frame. If [onErrors] is null and [FlutterError] caught at least
20  /// one error, this function fails the test. A test may override [onErrors] and
21  /// inspect errors using [takeFlutterErrorDetails].
22  TestRenderingFlutterBinding({ this.onErrors });
23
24  final List<FlutterErrorDetails> _errors = <FlutterErrorDetails>[];
25
26  /// A function called after drawing a frame if [FlutterError] caught any errors.
27  ///
28  /// This function is expected to inspect these errors and decide whether they
29  /// are expected or not. Use [takeFlutterErrorDetails] to take one error at a
30  /// time, or [takeAllFlutterErrorDetails] to iterate over all errors.
31  VoidCallback onErrors;
32
33  /// Returns the error least recently caught by [FlutterError] and removes it
34  /// from the list of captured errors.
35  ///
36  /// Returns null if no errors were captures, or if the list was exhausted by
37  /// calling this method repeatedly.
38  FlutterErrorDetails takeFlutterErrorDetails() {
39    if (_errors.isEmpty) {
40      return null;
41    }
42    return _errors.removeAt(0);
43  }
44
45  /// Returns all error details caught by [FlutterError] from least recently caught to
46  /// most recently caught, and removes them from the list of captured errors.
47  ///
48  /// The returned iterable takes errors lazily. If, for example, you iterate over 2
49  /// errors, but there are 5 errors total, this binding will still fail the test.
50  /// Tests are expected to take and inspect all errors.
51  Iterable<FlutterErrorDetails> takeAllFlutterErrorDetails() sync* {
52    // sync* and yield are used for lazy evaluation. Otherwise, the list would be
53    // drained eagerly and allow a test pass with unexpected errors.
54    while (_errors.isNotEmpty) {
55      yield _errors.removeAt(0);
56    }
57  }
58
59  /// Returns all exceptions caught by [FlutterError] from least recently caught to
60  /// most recently caught, and removes them from the list of captured errors.
61  ///
62  /// The returned iterable takes errors lazily. If, for example, you iterate over 2
63  /// errors, but there are 5 errors total, this binding will still fail the test.
64  /// Tests are expected to take and inspect all errors.
65  Iterable<dynamic> takeAllFlutterExceptions() sync* {
66    // sync* and yield are used for lazy evaluation. Otherwise, the list would be
67    // drained eagerly and allow a test pass with unexpected errors.
68    while (_errors.isNotEmpty) {
69      yield _errors.removeAt(0).exception;
70    }
71  }
72
73  EnginePhase phase = EnginePhase.composite;
74
75  @override
76  void drawFrame() {
77    assert(phase != EnginePhase.build, 'rendering_tester does not support testing the build phase; use flutter_test instead');
78    final FlutterExceptionHandler oldErrorHandler = FlutterError.onError;
79    FlutterError.onError = (FlutterErrorDetails details) {
80      _errors.add(details);
81    };
82    try {
83      pipelineOwner.flushLayout();
84      if (phase == EnginePhase.layout)
85        return;
86      pipelineOwner.flushCompositingBits();
87      if (phase == EnginePhase.compositingBits)
88        return;
89      pipelineOwner.flushPaint();
90      if (phase == EnginePhase.paint)
91        return;
92      renderView.compositeFrame();
93      if (phase == EnginePhase.composite)
94        return;
95      pipelineOwner.flushSemantics();
96      if (phase == EnginePhase.flushSemantics)
97        return;
98      assert(phase == EnginePhase.flushSemantics ||
99            phase == EnginePhase.sendSemanticsUpdate);
100    } finally {
101      FlutterError.onError = oldErrorHandler;
102      if (_errors.isNotEmpty) {
103        if (onErrors != null) {
104          onErrors();
105          if (_errors.isNotEmpty) {
106            _errors.forEach(FlutterError.dumpErrorToConsole);
107            fail('There are more errors than the test inspected using TestRenderingFlutterBinding.takeFlutterErrorDetails.');
108          }
109        } else {
110          _errors.forEach(FlutterError.dumpErrorToConsole);
111          fail('Caught error while rendering frame. See preceding logs for details.');
112        }
113      }
114    }
115  }
116}
117
118TestRenderingFlutterBinding _renderer;
119TestRenderingFlutterBinding get renderer {
120  _renderer ??= TestRenderingFlutterBinding();
121  return _renderer;
122}
123
124/// Place the box in the render tree, at the given size and with the given
125/// alignment on the screen.
126///
127/// If you've updated `box` and want to lay it out again, use [pumpFrame].
128///
129/// Once a particular [RenderBox] has been passed to [layout], it cannot easily
130/// be put in a different place in the tree or passed to [layout] again, because
131/// [layout] places the given object into another [RenderBox] which you would
132/// need to unparent it from (but that box isn't itself made available).
133///
134/// The EnginePhase must not be [EnginePhase.build], since the rendering layer
135/// has no build phase.
136///
137/// If `onErrors` is not null, it is set as [TestRenderingFlutterBinding.onError].
138void layout(
139  RenderBox box, {
140  BoxConstraints constraints,
141  Alignment alignment = Alignment.center,
142  EnginePhase phase = EnginePhase.layout,
143  VoidCallback onErrors,
144}) {
145  assert(box != null); // If you want to just repump the last box, call pumpFrame().
146  assert(box.parent == null); // We stick the box in another, so you can't reuse it easily, sorry.
147
148  renderer.renderView.child = null;
149  if (constraints != null) {
150    box = RenderPositionedBox(
151      alignment: alignment,
152      child: RenderConstrainedBox(
153        additionalConstraints: constraints,
154        child: box,
155      ),
156    );
157  }
158  renderer.renderView.child = box;
159
160  pumpFrame(phase: phase, onErrors: onErrors);
161}
162
163/// Pumps a single frame.
164///
165/// If `onErrors` is not null, it is set as [TestRenderingFlutterBinding.onError].
166void pumpFrame({ EnginePhase phase = EnginePhase.layout, VoidCallback onErrors }) {
167  assert(renderer != null);
168  assert(renderer.renderView != null);
169  assert(renderer.renderView.child != null); // call layout() first!
170
171  if (onErrors != null) {
172    renderer.onErrors = onErrors;
173  }
174
175  renderer.phase = phase;
176  renderer.drawFrame();
177}
178
179class TestCallbackPainter extends CustomPainter {
180  const TestCallbackPainter({ this.onPaint });
181
182  final VoidCallback onPaint;
183
184  @override
185  void paint(Canvas canvas, Size size) {
186    onPaint();
187  }
188
189  @override
190  bool shouldRepaint(TestCallbackPainter oldPainter) => true;
191}
192
193
194class RenderSizedBox extends RenderBox {
195  RenderSizedBox(this._size);
196
197  final Size _size;
198
199  @override
200  double computeMinIntrinsicWidth(double height) {
201    return _size.width;
202  }
203
204  @override
205  double computeMaxIntrinsicWidth(double height) {
206    return _size.width;
207  }
208
209  @override
210  double computeMinIntrinsicHeight(double width) {
211    return _size.height;
212  }
213
214  @override
215  double computeMaxIntrinsicHeight(double width) {
216    return _size.height;
217  }
218
219  @override
220  bool get sizedByParent => true;
221
222  @override
223  void performResize() {
224    size = constraints.constrain(_size);
225  }
226
227  @override
228  void performLayout() { }
229
230  @override
231  bool hitTestSelf(Offset position) => true;
232}
233