blob: 5049405560ac317bb3df6a1aa3a9f5390c01a961 [file] [log] [blame]
// 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:ui' as ui;
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter_test/flutter_test.dart';
import 'rendering_tester.dart';
void main() {
TestRenderingFlutterBinding.ensureInitialized();
test('ensure frame is scheduled for markNeedsSemanticsUpdate', () {
// Initialize all bindings because owner.flushSemantics() requires a window
final TestRenderObject renderObject = TestRenderObject();
int onNeedVisualUpdateCallCount = 0;
final PipelineOwner owner = PipelineOwner(onNeedVisualUpdate: () {
onNeedVisualUpdateCallCount +=1;
});
owner.ensureSemantics();
renderObject.attach(owner);
renderObject.layout(const BoxConstraints.tightForFinite()); // semantics are only calculated if layout information is up to date.
owner.flushSemantics();
expect(onNeedVisualUpdateCallCount, 1);
renderObject.markNeedsSemanticsUpdate();
expect(onNeedVisualUpdateCallCount, 2);
});
test('detached RenderObject does not do semantics', () {
final TestRenderObject renderObject = TestRenderObject();
expect(renderObject.attached, isFalse);
expect(renderObject.describeSemanticsConfigurationCallCount, 0);
renderObject.markNeedsSemanticsUpdate();
expect(renderObject.describeSemanticsConfigurationCallCount, 0);
});
test('ensure errors processing render objects are well formatted', () {
late FlutterErrorDetails errorDetails;
final FlutterExceptionHandler? oldHandler = FlutterError.onError;
FlutterError.onError = (FlutterErrorDetails details) {
errorDetails = details;
};
final PipelineOwner owner = PipelineOwner();
final TestThrowingRenderObject renderObject = TestThrowingRenderObject();
try {
renderObject.attach(owner);
renderObject.layout(const BoxConstraints());
} finally {
FlutterError.onError = oldHandler;
}
expect(errorDetails, isNotNull);
expect(errorDetails.stack, isNotNull);
// Check the ErrorDetails without the stack trace
final List<String> lines = errorDetails.toString().split('\n');
// The lines in the middle of the error message contain the stack trace
// which will change depending on where the test is run.
expect(lines.length, greaterThan(8));
expect(
lines.take(4).join('\n'),
equalsIgnoringHashCodes(
'══╡ EXCEPTION CAUGHT BY RENDERING LIBRARY ╞══════════════════════\n'
'The following assertion was thrown during performLayout():\n'
'TestThrowingRenderObject does not support performLayout.\n',
),
);
expect(
lines.getRange(lines.length - 8, lines.length).join('\n'),
equalsIgnoringHashCodes(
'\n'
'The following RenderObject was being processed when the exception was fired:\n'
' TestThrowingRenderObject#00000 NEEDS-PAINT:\n'
' parentData: MISSING\n'
' constraints: BoxConstraints(unconstrained)\n'
'This RenderObject has no descendants.\n'
'═════════════════════════════════════════════════════════════════\n',
),
);
});
test('ContainerParentDataMixin requires nulled out pointers to siblings before detach', () {
expect(() => TestParentData().detach(), isNot(throwsAssertionError));
final TestParentData data1 = TestParentData()
..nextSibling = RenderOpacity()
..previousSibling = RenderOpacity();
expect(() => data1.detach(), throwsAssertionError);
final TestParentData data2 = TestParentData()
..previousSibling = RenderOpacity();
expect(() => data2.detach(), throwsAssertionError);
final TestParentData data3 = TestParentData()
..nextSibling = RenderOpacity();
expect(() => data3.detach(), throwsAssertionError);
});
test('RenderObject.getTransformTo asserts is argument is not descendant', () {
final PipelineOwner owner = PipelineOwner();
final TestRenderObject renderObject1 = TestRenderObject();
renderObject1.attach(owner);
final TestRenderObject renderObject2 = TestRenderObject();
renderObject2.attach(owner);
expect(() => renderObject1.getTransformTo(renderObject2), throwsAssertionError);
});
test('PaintingContext.pushClipRect reuses the layer', () {
_testPaintingContextLayerReuse<ClipRectLayer>((PaintingContextCallback painter, PaintingContext context, Offset offset, Layer? oldLayer) {
return context.pushClipRect(true, offset, Rect.zero, painter, oldLayer: oldLayer as ClipRectLayer?);
});
});
test('PaintingContext.pushClipRRect reuses the layer', () {
_testPaintingContextLayerReuse<ClipRRectLayer>((PaintingContextCallback painter, PaintingContext context, Offset offset, Layer? oldLayer) {
return context.pushClipRRect(true, offset, Rect.zero, RRect.fromRectAndRadius(Rect.zero, const Radius.circular(1.0)), painter, oldLayer: oldLayer as ClipRRectLayer?);
});
});
test('PaintingContext.pushClipPath reuses the layer', () {
_testPaintingContextLayerReuse<ClipPathLayer>((PaintingContextCallback painter, PaintingContext context, Offset offset, Layer? oldLayer) {
return context.pushClipPath(true, offset, Rect.zero, Path(), painter, oldLayer: oldLayer as ClipPathLayer?);
});
});
test('PaintingContext.pushColorFilter reuses the layer', () {
_testPaintingContextLayerReuse<ColorFilterLayer>((PaintingContextCallback painter, PaintingContext context, Offset offset, Layer? oldLayer) {
return context.pushColorFilter(offset, const ColorFilter.mode(Color.fromRGBO(0, 0, 0, 1.0), BlendMode.clear), painter, oldLayer: oldLayer as ColorFilterLayer?);
});
});
test('PaintingContext.pushTransform reuses the layer', () {
_testPaintingContextLayerReuse<TransformLayer>((PaintingContextCallback painter, PaintingContext context, Offset offset, Layer? oldLayer) {
return context.pushTransform(true, offset, Matrix4.identity(), painter, oldLayer: oldLayer as TransformLayer?);
});
});
test('PaintingContext.pushOpacity reuses the layer', () {
_testPaintingContextLayerReuse<OpacityLayer>((PaintingContextCallback painter, PaintingContext context, Offset offset, Layer? oldLayer) {
return context.pushOpacity(offset, 100, painter, oldLayer: oldLayer as OpacityLayer?);
});
});
test('RenderObject.dispose sets debugDisposed to true', () {
final TestRenderObject renderObject = TestRenderObject();
expect(renderObject.debugDisposed, false);
renderObject.dispose();
expect(renderObject.debugDisposed, true);
expect(renderObject.toStringShort(), contains('DISPOSED'));
});
test('Leader layer can switch to a different render object within one frame', () {
List<FlutterErrorDetails?>? caughtErrors;
TestRenderingFlutterBinding.instance.onErrors = () {
caughtErrors = TestRenderingFlutterBinding.instance.takeAllFlutterErrorDetails().toList();
};
final LayerLink layerLink = LayerLink();
// renderObject1 paints the leader layer first.
final LeaderLayerRenderObject renderObject1 = LeaderLayerRenderObject();
renderObject1.layerLink = layerLink;
renderObject1.attach(TestRenderingFlutterBinding.instance.pipelineOwner);
final OffsetLayer rootLayer1 = OffsetLayer();
rootLayer1.attach(renderObject1);
renderObject1.scheduleInitialPaint(rootLayer1);
renderObject1.layout(const BoxConstraints.tightForFinite());
final LeaderLayerRenderObject renderObject2 = LeaderLayerRenderObject();
final OffsetLayer rootLayer2 = OffsetLayer();
rootLayer2.attach(renderObject2);
renderObject2.attach(TestRenderingFlutterBinding.instance.pipelineOwner);
renderObject2.scheduleInitialPaint(rootLayer2);
renderObject2.layout(const BoxConstraints.tightForFinite());
TestRenderingFlutterBinding.instance.pumpCompleteFrame();
// Swap the layer link to renderObject2 in the same frame
renderObject1.layerLink = null;
renderObject1.markNeedsPaint();
renderObject2.layerLink = layerLink;
renderObject2.markNeedsPaint();
TestRenderingFlutterBinding.instance.pumpCompleteFrame();
// Swap the layer link to renderObject1 in the same frame
renderObject1.layerLink = layerLink;
renderObject1.markNeedsPaint();
renderObject2.layerLink = null;
renderObject2.markNeedsPaint();
TestRenderingFlutterBinding.instance.pumpCompleteFrame();
TestRenderingFlutterBinding.instance.onErrors = null;
expect(caughtErrors, isNull);
});
test('Leader layer append to two render objects does crash', () {
List<FlutterErrorDetails?>? caughtErrors;
TestRenderingFlutterBinding.instance.onErrors = () {
caughtErrors = TestRenderingFlutterBinding.instance.takeAllFlutterErrorDetails().toList();
};
final LayerLink layerLink = LayerLink();
// renderObject1 paints the leader layer first.
final LeaderLayerRenderObject renderObject1 = LeaderLayerRenderObject();
renderObject1.layerLink = layerLink;
renderObject1.attach(TestRenderingFlutterBinding.instance.pipelineOwner);
final OffsetLayer rootLayer1 = OffsetLayer();
rootLayer1.attach(renderObject1);
renderObject1.scheduleInitialPaint(rootLayer1);
renderObject1.layout(const BoxConstraints.tightForFinite());
final LeaderLayerRenderObject renderObject2 = LeaderLayerRenderObject();
renderObject2.layerLink = layerLink;
final OffsetLayer rootLayer2 = OffsetLayer();
rootLayer2.attach(renderObject2);
renderObject2.attach(TestRenderingFlutterBinding.instance.pipelineOwner);
renderObject2.scheduleInitialPaint(rootLayer2);
renderObject2.layout(const BoxConstraints.tightForFinite());
TestRenderingFlutterBinding.instance.pumpCompleteFrame();
TestRenderingFlutterBinding.instance.onErrors = null;
expect(caughtErrors!.isNotEmpty, isTrue);
});
test('RenderObject.dispose null the layer on repaint boundaries', () {
final TestRenderObject renderObject = TestRenderObject(allowPaintBounds: true);
// Force a layer to get set.
renderObject.isRepaintBoundary = true;
PaintingContext.repaintCompositedChild(renderObject, debugAlsoPaintedParent: true);
expect(renderObject.debugLayer, isA<OffsetLayer>());
// Dispose with repaint boundary still being true.
renderObject.dispose();
expect(renderObject.debugLayer, null);
});
test('RenderObject.dispose nulls the layer on non-repaint boundaries', () {
final TestRenderObject renderObject = TestRenderObject(allowPaintBounds: true);
// Force a layer to get set.
renderObject.isRepaintBoundary = true;
PaintingContext.repaintCompositedChild(renderObject, debugAlsoPaintedParent: true);
// Dispose with repaint boundary being false.
renderObject.isRepaintBoundary = false;
renderObject.dispose();
expect(renderObject.debugLayer, null);
});
test('Add composition callback works', () {
final ContainerLayer root = ContainerLayer();
final PaintingContext context = PaintingContext(root, Rect.zero);
bool calledBack = false;
final TestObservingRenderObject object = TestObservingRenderObject((Layer layer) {
expect(layer, root);
calledBack = true;
});
expect(calledBack, false);
object.paint(context, Offset.zero);
expect(calledBack, false);
root.buildScene(ui.SceneBuilder()).dispose();
expect(calledBack, true);
});
}
class TestObservingRenderObject extends RenderBox {
TestObservingRenderObject(this.callback);
final CompositionCallback callback;
@override
bool get sizedByParent => true;
@override
void paint(PaintingContext context, Offset offset) {
context.addCompositionCallback(callback);
}
}
// Tests the create-update cycle by pumping two frames. The first frame has no
// prior layer and forces the painting context to create a new one. The second
// frame reuses the layer painted on the first frame.
void _testPaintingContextLayerReuse<L extends Layer>(_LayerTestPaintCallback painter) {
final _TestCustomLayerBox box = _TestCustomLayerBox(painter);
layout(box, phase: EnginePhase.paint);
// Force a repaint. Otherwise, pumpFrame is a noop.
box.markNeedsPaint();
pumpFrame(phase: EnginePhase.paint);
expect(box.paintedLayers, hasLength(2));
expect(box.paintedLayers[0], isA<L>());
expect(box.paintedLayers[0], same(box.paintedLayers[1]));
}
typedef _LayerTestPaintCallback = Layer? Function(PaintingContextCallback painter, PaintingContext context, Offset offset, Layer? oldLayer);
class _TestCustomLayerBox extends RenderBox {
_TestCustomLayerBox(this.painter);
final _LayerTestPaintCallback painter;
final List<Layer> paintedLayers = <Layer>[];
@override
bool get isRepaintBoundary => false;
@override
void performLayout() {
size = constraints.smallest;
}
@override
void paint(PaintingContext context, Offset offset) {
final Layer paintedLayer = painter(super.paint, context, offset, layer)!;
paintedLayers.add(paintedLayer);
layer = paintedLayer as ContainerLayer;
}
}
class TestParentData extends ParentData with ContainerParentDataMixin<RenderBox> { }
class TestRenderObject extends RenderObject {
TestRenderObject({this.allowPaintBounds = false});
final bool allowPaintBounds;
@override
bool isRepaintBoundary = false;
@override
void debugAssertDoesMeetConstraints() { }
@override
Rect get paintBounds {
assert(allowPaintBounds); // For some tests, this should not get called.
return Rect.zero;
}
@override
void performLayout() { }
@override
void performResize() { }
@override
Rect get semanticBounds => const Rect.fromLTWH(0.0, 0.0, 10.0, 20.0);
int describeSemanticsConfigurationCallCount = 0;
@override
void describeSemanticsConfiguration(SemanticsConfiguration config) {
super.describeSemanticsConfiguration(config);
config.isSemanticBoundary = true;
describeSemanticsConfigurationCallCount++;
}
}
class LeaderLayerRenderObject extends RenderObject {
LeaderLayerRenderObject();
LayerLink? layerLink;
@override
bool isRepaintBoundary = true;
@override
void debugAssertDoesMeetConstraints() { }
@override
Rect get paintBounds {
return Rect.zero;
}
@override
void paint(PaintingContext context, Offset offset) {
if (layerLink != null) {
context.pushLayer(LeaderLayer(link: layerLink!), super.paint, offset);
}
}
@override
void performLayout() { }
@override
void performResize() { }
@override
Rect get semanticBounds => const Rect.fromLTWH(0.0, 0.0, 10.0, 20.0);
}
class TestThrowingRenderObject extends RenderObject {
@override
void performLayout() {
throw FlutterError('TestThrowingRenderObject does not support performLayout.');
}
@override
void debugAssertDoesMeetConstraints() { }
@override
Rect get paintBounds {
assert(false); // The test shouldn't call this.
return Rect.zero;
}
@override
void performResize() { }
@override
Rect get semanticBounds {
assert(false); // The test shouldn't call this.
return Rect.zero;
}
}