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