Skip to content

Commit b33b99d

Browse files
committed
more invariants
1 parent b2a9261 commit b33b99d

2 files changed

Lines changed: 317 additions & 0 deletions

File tree

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
import 'package:flutter/material.dart';
2+
import 'package:flutter_test/flutter_test.dart';
3+
4+
Widget buildTree(Function meBuilder) {
5+
return Directionality(
6+
textDirection: TextDirection.ltr,
7+
child: SizedBox(
8+
key: const ValueKey('root'),
9+
child: Row(
10+
key: const ValueKey('grandparent'),
11+
children: [
12+
const SizedBox(
13+
key: ValueKey('older pibling'),
14+
),
15+
Row(
16+
key: const ValueKey('parent'),
17+
children: [
18+
const SizedBox(
19+
key: ValueKey('older sibling'),
20+
child: SizedBox(
21+
key: ValueKey('older nibling'),
22+
),
23+
),
24+
Builder(
25+
key: const ValueKey('me'),
26+
builder: (BuildContext context) {
27+
const Widget child = SizedBox(
28+
key: ValueKey('child'),
29+
child: SizedBox(
30+
key: ValueKey('grandchild'),
31+
)
32+
);
33+
return meBuilder(context, child);
34+
},
35+
),
36+
const SizedBox(
37+
key: ValueKey('younger sibling'),
38+
child: SizedBox(
39+
key: ValueKey('younger nibling'),
40+
),
41+
),
42+
],
43+
),
44+
const SizedBox(
45+
key: ValueKey('younger pibling'),
46+
),
47+
],
48+
),
49+
),
50+
);
51+
}
52+
53+
void testMeCanMarkRenderObjectDirtyDuringBuild(String relative) {
54+
testWidgets('Me can mark RenderObject "$relative" as dirty during build.', (WidgetTester tester) async {
55+
bool dirtyTree = false;
56+
await tester.pumpWidget(buildTree((BuildContext context, Widget child) {
57+
if (dirtyTree) {
58+
markRenderObjectDirty(tester, relative);
59+
}
60+
return child;
61+
}));
62+
63+
dirtyTree = true;
64+
markWidgetDirty(tester, 'me');
65+
await tester.pump();
66+
});
67+
}
68+
69+
void main() {
70+
testMeCanMarkRenderObjectDirtyDuringBuild('root');
71+
testMeCanMarkRenderObjectDirtyDuringBuild('grandparent');
72+
testMeCanMarkRenderObjectDirtyDuringBuild('older pibling');
73+
testMeCanMarkRenderObjectDirtyDuringBuild('parent');
74+
testMeCanMarkRenderObjectDirtyDuringBuild('younger pibling');
75+
testMeCanMarkRenderObjectDirtyDuringBuild('older sibling');
76+
testMeCanMarkRenderObjectDirtyDuringBuild('me');
77+
testMeCanMarkRenderObjectDirtyDuringBuild('younger sibling');
78+
testMeCanMarkRenderObjectDirtyDuringBuild('older nibling');
79+
testMeCanMarkRenderObjectDirtyDuringBuild('child');
80+
testMeCanMarkRenderObjectDirtyDuringBuild('younger nibling');
81+
testMeCanMarkRenderObjectDirtyDuringBuild('grandchild');
82+
}
83+
84+
void markWidgetDirty(WidgetTester tester, String key) {
85+
tester.element(find.byKey(ValueKey(key))).markNeedsBuild();
86+
}
87+
88+
void markRenderObjectDirty(WidgetTester tester, String key) {
89+
tester.renderObject(find.byKey(ValueKey(key))).markNeedsLayout();
90+
}
Lines changed: 227 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,227 @@
1+
import 'package:flutter/material.dart';
2+
import 'package:flutter/rendering.dart';
3+
import 'package:flutter_test/flutter_test.dart';
4+
5+
Widget buildTree({VoidCallback? layoutCallback, VoidCallback? paintCallback}) {
6+
return Directionality(
7+
textDirection: TextDirection.ltr,
8+
child: SizedBox(
9+
key: const ValueKey('root'),
10+
child: Row(
11+
key: const ValueKey('grandparent'),
12+
children: [
13+
const SizedBox(
14+
key: ValueKey('older pibling'),
15+
),
16+
Row(
17+
key: const ValueKey('parent'),
18+
children: [
19+
const SizedBox(
20+
key: ValueKey('older sibling'),
21+
child: SizedBox(
22+
key: ValueKey('older nibling'),
23+
),
24+
),
25+
TestRenderWidget(
26+
key: const ValueKey('me'),
27+
layoutCallback: layoutCallback,
28+
paintCallback: paintCallback,
29+
child: const SizedBox(
30+
key: ValueKey('child'),
31+
child: SizedBox(
32+
key: ValueKey('grandchild'),
33+
),
34+
),
35+
),
36+
const SizedBox(
37+
key: ValueKey('younger sibling'),
38+
child: SizedBox(
39+
key: ValueKey('younger nibling'),
40+
),
41+
),
42+
],
43+
),
44+
const SizedBox(
45+
key: ValueKey('younger pibling'),
46+
),
47+
],
48+
),
49+
),
50+
);
51+
}
52+
53+
void testMeCannotMarkDirtyDuringLayout(String relative, FailureReason reason) {
54+
final String error;
55+
switch (reason) {
56+
case FailureReason.ancestor:
57+
error = 'The RenderObject was mutated when none of its ancestors is actively performing layout.';
58+
break;
59+
case FailureReason.self:
60+
error = 'A RenderObject must not re-dirty itself while still being laid out.';
61+
break;
62+
case FailureReason.subtree:
63+
error = 'A RenderObject must not mutate another RenderObject from a different render subtree in its performLayout method.';
64+
break;
65+
case FailureReason.descendant:
66+
error = 'A RenderObject must not mutate its descendants in its performLayout method.';
67+
break;
68+
}
69+
70+
testWidgets('Me cannot mark RenderObject "$relative" as dirty during layout.', (WidgetTester tester) async {
71+
bool dirtyTree = false;
72+
int control = 0;
73+
74+
void callback() {
75+
if (dirtyTree) {
76+
control++;
77+
expect(
78+
() => markRenderObjectDirtyForLayout(tester, relative),
79+
throwsAFlutterError(error),
80+
);
81+
82+
83+
}
84+
}
85+
86+
await tester.pumpWidget(buildTree(
87+
layoutCallback: callback,
88+
));
89+
90+
dirtyTree = true;
91+
markRenderObjectDirtyForLayout(tester, 'me');
92+
expect(control, 0);
93+
await tester.pump();
94+
expect(control, 1);
95+
});
96+
}
97+
98+
void testMeCannotMarkDirtyDuringPaint(String relative) {
99+
testWidgets('Me cannot mark RenderObject "$relative" as dirty during paint.', (WidgetTester tester) async {
100+
bool dirtyTree = false;
101+
int control = 0;
102+
103+
void callback() {
104+
if (dirtyTree) {
105+
control++;
106+
markRenderObjectDirtyForLayout(tester, relative);
107+
}
108+
}
109+
110+
await tester.pumpWidget(buildTree(
111+
paintCallback: callback,
112+
));
113+
114+
dirtyTree = true;
115+
markRenderObjectDirtyForLayout(tester, 'me');
116+
expect(control, 0);
117+
await tester.pump();
118+
expect(control, 1);
119+
});
120+
}
121+
122+
void main() {
123+
testMeCannotMarkDirtyDuringLayout('root', FailureReason.ancestor);
124+
testMeCannotMarkDirtyDuringLayout('grandparent', FailureReason.self);
125+
testMeCannotMarkDirtyDuringLayout('older pibling', FailureReason.subtree);
126+
testMeCannotMarkDirtyDuringLayout('parent', FailureReason.self);
127+
testMeCannotMarkDirtyDuringLayout('younger pibling', FailureReason.subtree);
128+
testMeCannotMarkDirtyDuringLayout('older sibling', FailureReason.subtree);
129+
testMeCannotMarkDirtyDuringLayout('me', FailureReason.self);
130+
testMeCannotMarkDirtyDuringLayout('younger sibling', FailureReason.subtree);
131+
testMeCannotMarkDirtyDuringLayout('older nibling', FailureReason.subtree);
132+
testMeCannotMarkDirtyDuringLayout('child', FailureReason.descendant);
133+
testMeCannotMarkDirtyDuringLayout('younger nibling', FailureReason.subtree);
134+
testMeCannotMarkDirtyDuringLayout('grandchild', FailureReason.descendant);
135+
136+
// These should all fail with an assertion error, need to figure out how to catch.
137+
testMeCannotMarkDirtyDuringPaint('root');
138+
testMeCannotMarkDirtyDuringPaint('grandparent');
139+
testMeCannotMarkDirtyDuringPaint('older pibling');
140+
testMeCannotMarkDirtyDuringPaint('parent');
141+
testMeCannotMarkDirtyDuringPaint('younger pibling');
142+
testMeCannotMarkDirtyDuringPaint('older sibling');
143+
testMeCannotMarkDirtyDuringPaint('me');
144+
testMeCannotMarkDirtyDuringPaint('younger sibling');
145+
testMeCannotMarkDirtyDuringPaint('older nibling');
146+
testMeCannotMarkDirtyDuringPaint('child');
147+
testMeCannotMarkDirtyDuringPaint('younger nibling');
148+
testMeCannotMarkDirtyDuringPaint('grandchild');
149+
}
150+
151+
void markWidgetDirty(WidgetTester tester, String key) {
152+
tester.element(find.byKey(ValueKey(key))).markNeedsBuild();
153+
}
154+
155+
void markRenderObjectDirtyForLayout(WidgetTester tester, String key) {
156+
tester.renderObject(find.byKey(ValueKey(key))).markNeedsLayout();
157+
}
158+
159+
void markRenderObjectDirtyForPaint(WidgetTester tester, String key) {
160+
tester.renderObject(find.byKey(ValueKey(key))).markNeedsPaint();
161+
}
162+
163+
Matcher throwsAFlutterError(String message) {
164+
return throwsA(isA<FlutterError>().having((e) => e.toString(), 'message', contains(message)));
165+
}
166+
167+
class TestRenderWidget extends SingleChildRenderObjectWidget {
168+
const TestRenderWidget({super.key, this.layoutCallback, this.paintCallback, super.child});
169+
170+
final VoidCallback? layoutCallback;
171+
final VoidCallback? paintCallback;
172+
173+
@override
174+
TestRender createRenderObject(BuildContext context) {
175+
return TestRender(layoutCallback: layoutCallback, paintCallback: paintCallback);
176+
}
177+
178+
@override
179+
void updateRenderObject(BuildContext context, TestRender renderObject) {
180+
renderObject
181+
..layoutCallback = layoutCallback
182+
..paintCallback = paintCallback;
183+
}
184+
}
185+
186+
class TestRender extends RenderProxyBox {
187+
TestRender({VoidCallback? layoutCallback, VoidCallback? paintCallback}) : _layoutCallback = layoutCallback, _paintCallback = paintCallback;
188+
189+
VoidCallback? get layoutCallback => _layoutCallback;
190+
VoidCallback? _layoutCallback;
191+
set layoutCallback(VoidCallback? callback) {
192+
if (callback == _layoutCallback) {
193+
return;
194+
}
195+
_layoutCallback = callback;
196+
markNeedsLayout();
197+
}
198+
199+
VoidCallback? get paintCallback => _paintCallback;
200+
VoidCallback? _paintCallback;
201+
set paintCallback(VoidCallback? callback) {
202+
if (callback == _paintCallback) {
203+
return;
204+
}
205+
_paintCallback = callback;
206+
markNeedsPaint();
207+
}
208+
209+
@override
210+
void performLayout() {
211+
layoutCallback?.call();
212+
super.performLayout();
213+
}
214+
215+
@override
216+
void paint(PaintingContext context, Offset offset) {
217+
paintCallback?.call();
218+
super.paint(context, offset);
219+
}
220+
}
221+
222+
enum FailureReason {
223+
ancestor,
224+
self,
225+
subtree,
226+
descendant,
227+
}

0 commit comments

Comments
 (0)