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