325 lines
10 KiB
Dart
325 lines
10 KiB
Dart
// Copyright 2014 The Flutter Authors. All rights reserved.
|
|
// Use of this source code is governed by a BSD-style license that can be
|
|
// found in the LICENSE file.
|
|
|
|
import 'dart:async';
|
|
|
|
import 'package:flutter/foundation.dart';
|
|
import 'package:flutter/gestures.dart';
|
|
import 'package:flutter/rendering.dart';
|
|
import 'package:flutter/scheduler.dart';
|
|
import 'package:flutter/services.dart';
|
|
import 'package:flutter_test/flutter_test.dart' show EnginePhase, fail;
|
|
|
|
export 'package:flutter/foundation.dart' show FlutterError, FlutterErrorDetails;
|
|
export 'package:flutter_test/flutter_test.dart' show EnginePhase;
|
|
|
|
class TestRenderingFlutterBinding extends BindingBase with SchedulerBinding, ServicesBinding, GestureBinding, PaintingBinding, SemanticsBinding, RendererBinding {
|
|
/// 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].
|
|
///
|
|
/// 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);
|
|
Zone.current.parent!.handleUncaughtError(details.exception, details.stack!);
|
|
};
|
|
}
|
|
|
|
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.
|
|
VoidCallback? onErrors;
|
|
|
|
/// 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.
|
|
FlutterErrorDetails? takeFlutterErrorDetails() {
|
|
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;
|
|
}
|
|
}
|
|
|
|
EnginePhase phase = EnginePhase.composite;
|
|
|
|
@override
|
|
void drawFrame() {
|
|
assert(phase != EnginePhase.build, 'rendering_tester does not support testing the build phase; use flutter_test instead');
|
|
final FlutterExceptionHandler? oldErrorHandler = FlutterError.onError;
|
|
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) {
|
|
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.');
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
late final TestRenderingFlutterBinding _renderer = TestRenderingFlutterBinding();
|
|
TestRenderingFlutterBinding get renderer => _renderer;
|
|
|
|
|
|
/// Place the box in the render tree, at the given size and with the given
|
|
/// alignment on the screen.
|
|
///
|
|
/// 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).
|
|
///
|
|
/// The EnginePhase must not be [EnginePhase.build], since the rendering layer
|
|
/// has no build phase.
|
|
///
|
|
/// If `onErrors` is not null, it is set as [TestRenderingFlutterBinding.onError].
|
|
void layout(
|
|
RenderBox box, {
|
|
BoxConstraints? constraints,
|
|
Alignment alignment = Alignment.center,
|
|
EnginePhase phase = EnginePhase.layout,
|
|
VoidCallback? onErrors,
|
|
}) {
|
|
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.
|
|
|
|
renderer.renderView.child = null;
|
|
if (constraints != null) {
|
|
box = RenderPositionedBox(
|
|
alignment: alignment,
|
|
child: RenderConstrainedBox(
|
|
additionalConstraints: constraints,
|
|
child: box,
|
|
),
|
|
);
|
|
}
|
|
renderer.renderView.child = box;
|
|
|
|
pumpFrame(phase: phase, onErrors: onErrors);
|
|
}
|
|
|
|
/// Pumps a single frame.
|
|
///
|
|
/// If `onErrors` is not null, it is set as [TestRenderingFlutterBinding.onError].
|
|
void pumpFrame({ EnginePhase phase = EnginePhase.layout, VoidCallback? onErrors }) {
|
|
assert(renderer != null);
|
|
assert(renderer.renderView != null);
|
|
assert(renderer.renderView.child != null); // call layout() first!
|
|
|
|
if (onErrors != null) {
|
|
renderer.onErrors = onErrors;
|
|
}
|
|
|
|
renderer.phase = phase;
|
|
renderer.drawFrame();
|
|
}
|
|
|
|
class TestCallbackPainter extends CustomPainter {
|
|
const TestCallbackPainter({ required this.onPaint });
|
|
|
|
final VoidCallback onPaint;
|
|
|
|
@override
|
|
void paint(Canvas canvas, Size size) {
|
|
onPaint();
|
|
}
|
|
|
|
@override
|
|
bool shouldRepaint(TestCallbackPainter oldPainter) => true;
|
|
}
|
|
|
|
class RenderSizedBox extends RenderBox {
|
|
RenderSizedBox(this._size);
|
|
|
|
final Size _size;
|
|
|
|
@override
|
|
double computeMinIntrinsicWidth(double height) {
|
|
return _size.width;
|
|
}
|
|
|
|
@override
|
|
double computeMaxIntrinsicWidth(double height) {
|
|
return _size.width;
|
|
}
|
|
|
|
@override
|
|
double computeMinIntrinsicHeight(double width) {
|
|
return _size.height;
|
|
}
|
|
|
|
@override
|
|
double computeMaxIntrinsicHeight(double width) {
|
|
return _size.height;
|
|
}
|
|
|
|
@override
|
|
bool get sizedByParent => true;
|
|
|
|
@override
|
|
void performResize() {
|
|
size = constraints.constrain(_size);
|
|
}
|
|
|
|
@override
|
|
void performLayout() { }
|
|
|
|
@override
|
|
bool hitTestSelf(Offset position) => true;
|
|
}
|
|
|
|
class FakeTickerProvider implements TickerProvider {
|
|
@override
|
|
Ticker createTicker(TickerCallback onTick, [ bool disableAnimations = false ]) {
|
|
return FakeTicker();
|
|
}
|
|
}
|
|
|
|
class FakeTicker implements Ticker {
|
|
@override
|
|
bool muted = false;
|
|
|
|
@override
|
|
void absorbTicker(Ticker originalTicker) { }
|
|
|
|
@override
|
|
String? get debugLabel => null;
|
|
|
|
@override
|
|
bool get isActive => throw UnimplementedError();
|
|
|
|
@override
|
|
bool get isTicking => throw UnimplementedError();
|
|
|
|
@override
|
|
bool get scheduled => throw UnimplementedError();
|
|
|
|
@override
|
|
bool get shouldScheduleTick => throw UnimplementedError();
|
|
|
|
@override
|
|
void dispose() { }
|
|
|
|
@override
|
|
void scheduleTick({ bool rescheduling = false }) { }
|
|
|
|
@override
|
|
TickerFuture start() {
|
|
throw UnimplementedError();
|
|
}
|
|
|
|
@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);
|
|
}
|
|
}
|
|
|
|
class TestClipPaintingContext extends PaintingContext {
|
|
TestClipPaintingContext() : super(ContainerLayer(), Rect.zero);
|
|
|
|
@override
|
|
ClipRectLayer? pushClipRect(
|
|
bool needsCompositing,
|
|
Offset offset,
|
|
Rect clipRect,
|
|
PaintingContextCallback painter, {
|
|
Clip clipBehavior = Clip.hardEdge,
|
|
ClipRectLayer? oldLayer,
|
|
}) {
|
|
this.clipBehavior = clipBehavior;
|
|
return null;
|
|
}
|
|
|
|
Clip clipBehavior = Clip.none;
|
|
}
|
|
|
|
void expectOverflowedErrors() {
|
|
final FlutterErrorDetails errorDetails = renderer.takeFlutterErrorDetails()!;
|
|
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));
|