blob: 7f0476865973a0b472106b585243450a0e05427d [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 '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()}'
),
);
});
});
}