@@ -12,6 +12,10 @@ let Scheduler;
12
12
let ReactNoop ;
13
13
let useState ;
14
14
let act ;
15
+ let Suspense ;
16
+ let startTransition ;
17
+ let getCacheForType ;
18
+ let caches ;
15
19
16
20
// These tests are mostly concerned with concurrent roots. The legacy root
17
21
// behavior is covered by other older test suites and is unchanged from
@@ -24,11 +28,110 @@ describe('act warnings', () => {
24
28
ReactNoop = require ( 'react-noop-renderer' ) ;
25
29
act = React . unstable_act ;
26
30
useState = React . useState ;
31
+ Suspense = React . Suspense ;
32
+ startTransition = React . startTransition ;
33
+ getCacheForType = React . unstable_getCacheForType ;
34
+ caches = [ ] ;
27
35
} ) ;
28
36
29
- function Text ( props ) {
30
- Scheduler . unstable_yieldValue ( props . text ) ;
31
- return props . text ;
37
+ function createTextCache ( ) {
38
+ const data = new Map ( ) ;
39
+ const version = caches . length + 1 ;
40
+ const cache = {
41
+ version,
42
+ data,
43
+ resolve ( text ) {
44
+ const record = data . get ( text ) ;
45
+ if ( record === undefined ) {
46
+ const newRecord = {
47
+ status : 'resolved' ,
48
+ value : text ,
49
+ } ;
50
+ data . set ( text , newRecord ) ;
51
+ } else if ( record . status === 'pending' ) {
52
+ const thenable = record . value ;
53
+ record . status = 'resolved' ;
54
+ record . value = text ;
55
+ thenable . pings . forEach ( t => t ( ) ) ;
56
+ }
57
+ } ,
58
+ reject ( text , error ) {
59
+ const record = data . get ( text ) ;
60
+ if ( record === undefined ) {
61
+ const newRecord = {
62
+ status : 'rejected' ,
63
+ value : error ,
64
+ } ;
65
+ data . set ( text , newRecord ) ;
66
+ } else if ( record . status === 'pending' ) {
67
+ const thenable = record . value ;
68
+ record . status = 'rejected' ;
69
+ record . value = error ;
70
+ thenable . pings . forEach ( t => t ( ) ) ;
71
+ }
72
+ } ,
73
+ } ;
74
+ caches . push ( cache ) ;
75
+ return cache ;
76
+ }
77
+
78
+ function readText ( text ) {
79
+ const textCache = getCacheForType ( createTextCache ) ;
80
+ const record = textCache . data . get ( text ) ;
81
+ if ( record !== undefined ) {
82
+ switch ( record . status ) {
83
+ case 'pending' :
84
+ Scheduler . unstable_yieldValue ( `Suspend! [${ text } ]` ) ;
85
+ throw record . value ;
86
+ case 'rejected' :
87
+ Scheduler . unstable_yieldValue ( `Error! [${ text } ]` ) ;
88
+ throw record . value ;
89
+ case 'resolved' :
90
+ return textCache . version ;
91
+ }
92
+ } else {
93
+ Scheduler . unstable_yieldValue ( `Suspend! [${ text } ]` ) ;
94
+
95
+ const thenable = {
96
+ pings : [ ] ,
97
+ then ( resolve ) {
98
+ if ( newRecord . status === 'pending' ) {
99
+ thenable . pings . push ( resolve ) ;
100
+ } else {
101
+ Promise . resolve ( ) . then ( ( ) => resolve ( newRecord . value ) ) ;
102
+ }
103
+ } ,
104
+ } ;
105
+
106
+ const newRecord = {
107
+ status : 'pending' ,
108
+ value : thenable ,
109
+ } ;
110
+ textCache . data . set ( text , newRecord ) ;
111
+
112
+ throw thenable ;
113
+ }
114
+ }
115
+
116
+ function Text ( { text} ) {
117
+ Scheduler . unstable_yieldValue ( text ) ;
118
+ return text ;
119
+ }
120
+
121
+ function AsyncText ( { text} ) {
122
+ readText ( text ) ;
123
+ Scheduler . unstable_yieldValue ( text ) ;
124
+ return text ;
125
+ }
126
+
127
+ function resolveText ( text ) {
128
+ if ( caches . length === 0 ) {
129
+ throw Error ( 'Cache does not exist.' ) ;
130
+ } else {
131
+ // Resolve the most recently created cache. An older cache can by
132
+ // resolved with `caches[index].resolve(text)`.
133
+ caches [ caches . length - 1 ] . resolve ( text ) ;
134
+ }
32
135
}
33
136
34
137
function withActEnvironment ( value , scope ) {
@@ -187,4 +290,72 @@ describe('act warnings', () => {
187
290
expect ( root ) . toMatchRenderedOutput ( '1' ) ;
188
291
} ) ;
189
292
} ) ;
293
+
294
+ // @gate __DEV__
295
+ // @gate enableCache
296
+ test ( 'warns if Suspense retry is not wrapped' , ( ) => {
297
+ function App ( ) {
298
+ return (
299
+ < Suspense fallback = { < Text text = "Loading..." /> } >
300
+ < AsyncText text = "Async" />
301
+ </ Suspense >
302
+ ) ;
303
+ }
304
+
305
+ withActEnvironment ( true , ( ) => {
306
+ const root = ReactNoop . createRoot ( ) ;
307
+ act ( ( ) => {
308
+ root . render ( < App /> ) ;
309
+ } ) ;
310
+ expect ( Scheduler ) . toHaveYielded ( [ 'Suspend! [Async]' , 'Loading...' ] ) ;
311
+ expect ( root ) . toMatchRenderedOutput ( 'Loading...' ) ;
312
+
313
+ // This is a retry, not a ping, because we already showed a fallback.
314
+ expect ( ( ) =>
315
+ resolveText ( 'Async' ) ,
316
+ ) . toErrorDev (
317
+ 'A suspended resource finished loading inside a test, but the event ' +
318
+ 'was not wrapped in act(...)' ,
319
+ { withoutStack : true } ,
320
+ ) ;
321
+ } ) ;
322
+ } ) ;
323
+
324
+ // @gate __DEV__
325
+ // @gate enableCache
326
+ test ( 'warns if Suspense ping is not wrapped' , ( ) => {
327
+ function App ( { showMore} ) {
328
+ return (
329
+ < Suspense fallback = { < Text text = "Loading..." /> } >
330
+ { showMore ? < AsyncText text = "Async" /> : < Text text = "(empty)" /> }
331
+ </ Suspense >
332
+ ) ;
333
+ }
334
+
335
+ withActEnvironment ( true , ( ) => {
336
+ const root = ReactNoop . createRoot ( ) ;
337
+ act ( ( ) => {
338
+ root . render ( < App showMore = { false } /> ) ;
339
+ } ) ;
340
+ expect ( Scheduler ) . toHaveYielded ( [ '(empty)' ] ) ;
341
+ expect ( root ) . toMatchRenderedOutput ( '(empty)' ) ;
342
+
343
+ act ( ( ) => {
344
+ startTransition ( ( ) => {
345
+ root . render ( < App showMore = { true } /> ) ;
346
+ } ) ;
347
+ } ) ;
348
+ expect ( Scheduler ) . toHaveYielded ( [ 'Suspend! [Async]' , 'Loading...' ] ) ;
349
+ expect ( root ) . toMatchRenderedOutput ( '(empty)' ) ;
350
+
351
+ // This is a ping, not a retry, because no fallback is showing.
352
+ expect ( ( ) =>
353
+ resolveText ( 'Async' ) ,
354
+ ) . toErrorDev (
355
+ 'A suspended resource finished loading inside a test, but the event ' +
356
+ 'was not wrapped in act(...)' ,
357
+ { withoutStack : true } ,
358
+ ) ;
359
+ } ) ;
360
+ } ) ;
190
361
} ) ;
0 commit comments