// 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; } }