1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
// 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()}'
),
);
});
});
}