@@ -13,9 +13,14 @@ let ReactNoop;
13
13
let Scheduler ;
14
14
let act ;
15
15
16
+ let getCacheForType ;
16
17
let useState ;
18
+ let Suspense ;
17
19
let startTransition ;
18
20
21
+ let caches ;
22
+ let seededCache ;
23
+
19
24
describe ( 'ReactInteractionTracing' , ( ) => {
20
25
beforeEach ( ( ) => {
21
26
jest . resetModules ( ) ;
@@ -28,13 +33,121 @@ describe('ReactInteractionTracing', () => {
28
33
29
34
useState = React . useState ;
30
35
startTransition = React . startTransition ;
36
+ Suspense = React . Suspense ;
37
+
38
+ getCacheForType = React . unstable_getCacheForType ;
39
+
40
+ caches = [ ] ;
41
+ seededCache = null ;
31
42
} ) ;
32
43
44
+ function createTextCache ( ) {
45
+ if ( seededCache !== null ) {
46
+ const cache = seededCache ;
47
+ seededCache = null ;
48
+ return cache ;
49
+ }
50
+
51
+ const data = new Map ( ) ;
52
+ const cache = {
53
+ data,
54
+ resolve ( text ) {
55
+ const record = data . get ( text ) ;
56
+
57
+ if ( record === undefined ) {
58
+ const newRecord = {
59
+ status : 'resolved' ,
60
+ value : text ,
61
+ } ;
62
+ data . set ( text , newRecord ) ;
63
+ } else if ( record . status === 'pending' ) {
64
+ const thenable = record . value ;
65
+ record . status = 'resolved' ;
66
+ record . value = text ;
67
+ thenable . pings . forEach ( t => t ( ) ) ;
68
+ }
69
+ } ,
70
+ reject ( text , error ) {
71
+ const record = data . get ( text ) ;
72
+ if ( record === undefined ) {
73
+ const newRecord = {
74
+ status : 'rejected' ,
75
+ value : error ,
76
+ } ;
77
+ data . set ( text , newRecord ) ;
78
+ } else if ( record . status === 'pending' ) {
79
+ const thenable = record . value ;
80
+ record . status = 'rejected' ;
81
+ record . value = error ;
82
+ thenable . pings . forEach ( t => t ( ) ) ;
83
+ }
84
+ } ,
85
+ } ;
86
+ caches . push ( cache ) ;
87
+ return cache ;
88
+ }
89
+
90
+ function readText ( text ) {
91
+ const textCache = getCacheForType ( createTextCache ) ;
92
+ const record = textCache . data . get ( text ) ;
93
+ if ( record !== undefined ) {
94
+ switch ( record . status ) {
95
+ case 'pending' :
96
+ Scheduler . unstable_yieldValue ( `Suspend [${ text } ]` ) ;
97
+ throw record . value ;
98
+ case 'rejected' :
99
+ Scheduler . unstable_yieldValue ( `Error [${ text } ]` ) ;
100
+ throw record . value ;
101
+ case 'resolved' :
102
+ return record . value ;
103
+ }
104
+ } else {
105
+ Scheduler . unstable_yieldValue ( `Suspend [${ text } ]` ) ;
106
+
107
+ const thenable = {
108
+ pings : [ ] ,
109
+ then ( resolve ) {
110
+ if ( newRecord . status === 'pending' ) {
111
+ thenable . pings . push ( resolve ) ;
112
+ } else {
113
+ Promise . resolve ( ) . then ( ( ) => resolve ( newRecord . value ) ) ;
114
+ }
115
+ } ,
116
+ } ;
117
+
118
+ const newRecord = {
119
+ status : 'pending' ,
120
+ value : thenable ,
121
+ } ;
122
+ textCache . data . set ( text , newRecord ) ;
123
+
124
+ throw thenable ;
125
+ }
126
+ }
127
+
128
+ function AsyncText ( { text} ) {
129
+ const fullText = readText ( text ) ;
130
+ Scheduler . unstable_yieldValue ( fullText ) ;
131
+ return fullText ;
132
+ }
133
+
33
134
function Text ( { text} ) {
34
135
Scheduler . unstable_yieldValue ( text ) ;
35
136
return text ;
36
137
}
37
138
139
+ function resolveMostRecentTextCache ( text ) {
140
+ if ( caches . length === 0 ) {
141
+ throw Error ( 'Cache does not exist' ) ;
142
+ } else {
143
+ // Resolve the most recently created cache. An older cache can by
144
+ // resolved with `caches[index].resolve(text)`.
145
+ caches [ caches . length - 1 ] . resolve ( text ) ;
146
+ }
147
+ }
148
+
149
+ const resolveText = resolveMostRecentTextCache ;
150
+
38
151
function advanceTimers ( ms ) {
39
152
// Note: This advances Jest's virtual time but not React's. Use
40
153
// ReactNoop.expire for that.
@@ -98,4 +211,72 @@ describe('ReactInteractionTracing', () => {
98
211
} ) ;
99
212
} ) ;
100
213
} ) ;
214
+
215
+ // @gate enableTransitionTracing
216
+ it ( 'should correctly trace interactions for async roots' , async ( ) => {
217
+ const transitionCallbacks = {
218
+ onTransitionStart : ( name , startTime ) => {
219
+ Scheduler . unstable_yieldValue (
220
+ `onTransitionStart(${ name } , ${ startTime } )` ,
221
+ ) ;
222
+ } ,
223
+ onTransitionComplete : ( name , startTime , endTime ) => {
224
+ Scheduler . unstable_yieldValue (
225
+ `onTransitionComplete(${ name } , ${ startTime } , ${ endTime } )` ,
226
+ ) ;
227
+ } ,
228
+ } ;
229
+ let navigateToPageTwo ;
230
+ function App ( ) {
231
+ const [ navigate , setNavigate ] = useState ( false ) ;
232
+ navigateToPageTwo = ( ) => {
233
+ setNavigate ( true ) ;
234
+ } ;
235
+
236
+ return (
237
+ < div >
238
+ { navigate ? (
239
+ < Suspense
240
+ fallback = { < Text text = "Loading..." /> }
241
+ name = "suspense page" >
242
+ < AsyncText text = "Page Two" />
243
+ </ Suspense >
244
+ ) : (
245
+ < Text text = "Page One" />
246
+ ) }
247
+ </ div >
248
+ ) ;
249
+ }
250
+
251
+ const root = ReactNoop . createRoot ( { transitionCallbacks} ) ;
252
+ await act ( async ( ) => {
253
+ root . render ( < App /> ) ;
254
+ ReactNoop . expire ( 1000 ) ;
255
+ await advanceTimers ( 1000 ) ;
256
+
257
+ expect ( Scheduler ) . toFlushAndYield ( [ 'Page One' ] ) ;
258
+ } ) ;
259
+
260
+ await act ( async ( ) => {
261
+ startTransition ( ( ) => navigateToPageTwo ( ) , { name : 'page transition' } ) ;
262
+
263
+ ReactNoop . expire ( 1000 ) ;
264
+ await advanceTimers ( 1000 ) ;
265
+
266
+ expect ( Scheduler ) . toFlushAndYield ( [
267
+ 'Suspend [Page Two]' ,
268
+ 'Loading...' ,
269
+ 'onTransitionStart(page transition, 1000)' ,
270
+ ] ) ;
271
+
272
+ ReactNoop . expire ( 1000 ) ;
273
+ await advanceTimers ( 1000 ) ;
274
+ await resolveText ( 'Page Two' ) ;
275
+
276
+ expect ( Scheduler ) . toFlushAndYield ( [
277
+ 'Page Two' ,
278
+ 'onTransitionComplete(page transition, 1000, 3000)' ,
279
+ ] ) ;
280
+ } ) ;
281
+ } ) ;
101
282
} ) ;
0 commit comments