// 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 'package:flutter/rendering.dart'; import 'package:flutter_test/flutter_test.dart'; import 'rendering_tester.dart'; class RenderLayoutTestBox extends RenderProxyBox { RenderLayoutTestBox(this.onLayout, { this.onPerformLayout, }); final VoidCallback onLayout; final VoidCallback? onPerformLayout; @override void layout(Constraints constraints, { bool parentUsesSize = false }) { // Doing this in tests is ok, but if you're writing your own // render object, you want to override performLayout(), not // layout(). Overriding layout() would remove many critical // performance optimizations of the rendering system, as well as // many bypassing many checked-mode integrity checks. super.layout(constraints, parentUsesSize: parentUsesSize); onLayout(); } @override bool get sizedByParent => true; @override void performLayout() { child?.layout(constraints, parentUsesSize: true); onPerformLayout?.call(); } } void main() { TestRenderingFlutterBinding.ensureInitialized(); test('moving children', () { RenderBox child1, child2; bool movedChild1 = false; bool movedChild2 = false; final RenderFlex block = RenderFlex(textDirection: TextDirection.ltr); block.add(child1 = RenderLayoutTestBox(() { movedChild1 = true; })); block.add(child2 = RenderLayoutTestBox(() { movedChild2 = true; })); expect(movedChild1, isFalse); expect(movedChild2, isFalse); layout(block); expect(movedChild1, isTrue); expect(movedChild2, isTrue); movedChild1 = false; movedChild2 = false; expect(movedChild1, isFalse); expect(movedChild2, isFalse); pumpFrame(); expect(movedChild1, isFalse); expect(movedChild2, isFalse); block.move(child1, after: child2); expect(movedChild1, isFalse); expect(movedChild2, isFalse); pumpFrame(); expect(movedChild1, isTrue); expect(movedChild2, isTrue); movedChild1 = false; movedChild2 = false; expect(movedChild1, isFalse); expect(movedChild2, isFalse); pumpFrame(); expect(movedChild1, isFalse); expect(movedChild2, isFalse); }); group('Throws when illegal mutations are attempted: ', () { FlutterError catchLayoutError(RenderBox box) { Object? error; layout(box, onErrors: () { error = TestRenderingFlutterBinding.instance.takeFlutterErrorDetails()!.exception; }); expect(error, isFlutterError); return error! as FlutterError; } test('on disposed render objects', () { final RenderBox box = RenderLayoutTestBox(() {}); box.dispose(); Object? error; try { box.markNeedsLayout(); } catch (e) { error = e; } expect(error, isFlutterError); expect( (error! as FlutterError).message, equalsIgnoringWhitespace( 'A disposed RenderObject was mutated.\n' 'The disposed RenderObject was:\n' '${box.toStringShort()}' ) ); }); test('marking itself dirty in performLayout', () { late RenderBox child1; final RenderFlex block = RenderFlex(textDirection: TextDirection.ltr); block.add(child1 = RenderLayoutTestBox(() {}, onPerformLayout: () { child1.markNeedsLayout(); })); expect( catchLayoutError(block).message, equalsIgnoringWhitespace( 'A RenderLayoutTestBox was mutated in its own performLayout implementation.\n' 'A RenderObject must not re-dirty itself while still being laid out.\n' 'The RenderObject being mutated was:\n' '${child1.toStringShort()}\n' 'Consider using the LayoutBuilder widget to dynamically change a subtree during layout.' ) ); }); test('marking a sibling dirty in performLayout', () { late RenderBox child1, child2; final RenderFlex block = RenderFlex(textDirection: TextDirection.ltr); block.add(child1 = RenderLayoutTestBox(() {})); block.add(child2 = RenderLayoutTestBox(() {}, onPerformLayout: () { child1.markNeedsLayout(); })); expect( catchLayoutError(block).message, equalsIgnoringWhitespace( 'A RenderLayoutTestBox was mutated in RenderLayoutTestBox.performLayout.\n' 'A RenderObject must not mutate another RenderObject from a different render subtree in its performLayout method.\n' 'The RenderObject being mutated was:\n' '${child1.toStringShort()}\n' 'The RenderObject that was mutating the said RenderLayoutTestBox was:\n' '${child2.toStringShort()}\n' 'Their common ancestor was:\n' '${block.toStringShort()}\n' 'Mutating the layout of another RenderObject may cause some RenderObjects in its subtree to be laid out more than once. Consider using the LayoutBuilder widget to dynamically mutate a subtree during layout.' ) ); }); test('marking a descendant dirty in performLayout', () { late RenderBox child1; final RenderFlex block = RenderFlex(textDirection: TextDirection.ltr); block.add(child1 = RenderLayoutTestBox(() {})); block.add(RenderLayoutTestBox(child1.markNeedsLayout)); expect( catchLayoutError(block).message, equalsIgnoringWhitespace( 'A RenderLayoutTestBox was mutated in RenderFlex.performLayout.\n' 'A RenderObject must not mutate its descendants in its performLayout method.\n' 'The RenderObject being mutated was:\n' '${child1.toStringShort()}\n' 'The ancestor RenderObject that was mutating the said RenderLayoutTestBox was:\n' '${block.toStringShort()}\n' 'Mutating the layout of another RenderObject may cause some RenderObjects in its subtree to be laid out more than once. Consider using the LayoutBuilder widget to dynamically mutate a subtree during layout.' ), ); }); test('marking an out-of-band mutation in performLayout', () { late RenderProxyBox child1, child11, child2, child21; final RenderFlex block = RenderFlex(textDirection: TextDirection.ltr); block.add(child1 = RenderLayoutTestBox(() {})); block.add(child2 = RenderLayoutTestBox(() {})); child1.child = child11 = RenderLayoutTestBox(() {}); layout(block); expect(block.debugNeedsLayout, false); expect(child1.debugNeedsLayout, false); expect(child11.debugNeedsLayout, false); expect(child2.debugNeedsLayout, false); // Add a new child to child2 which is a relayout boundary. child2.child = child21 = RenderLayoutTestBox(() {}, onPerformLayout: child11.markNeedsLayout); FlutterError? error; pumpFrame(onErrors: () { error = TestRenderingFlutterBinding.instance.takeFlutterErrorDetails()!.exception as FlutterError; }); expect( error?.message, equalsIgnoringWhitespace( 'A RenderLayoutTestBox was mutated in RenderLayoutTestBox.performLayout.\n' 'The RenderObject was mutated when none of its ancestors is actively performing layout.\n' 'The RenderObject being mutated was:\n' '${child11.toStringShort()}\n' 'The RenderObject that was mutating the said RenderLayoutTestBox was:\n' '${child21.toStringShort()}' ), ); }); }); }