@@ -13,9 +13,14 @@ let ReactNoop;
1313let Scheduler ;
1414let act ;
1515
16+ let getCacheForType ;
1617let useState ;
18+ let Suspense ;
1719let startTransition ;
1820
21+ let caches ;
22+ let seededCache ;
23+
1924describe ( 'ReactInteractionTracing' , ( ) => {
2025 beforeEach ( ( ) => {
2126 jest . resetModules ( ) ;
@@ -28,13 +33,121 @@ describe('ReactInteractionTracing', () => {
2833
2934 useState = React . useState ;
3035 startTransition = React . startTransition ;
36+ Suspense = React . Suspense ;
37+
38+ getCacheForType = React . unstable_getCacheForType ;
39+
40+ caches = [ ] ;
41+ seededCache = null ;
3142 } ) ;
3243
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+
33134 function Text ( { text} ) {
34135 Scheduler . unstable_yieldValue ( text ) ;
35136 return text ;
36137 }
37138
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+
38151 function advanceTimers ( ms ) {
39152 // Note: This advances Jest's virtual time but not React's. Use
40153 // ReactNoop.expire for that.
@@ -98,4 +211,72 @@ describe('ReactInteractionTracing', () => {
98211 } ) ;
99212 } ) ;
100213 } ) ;
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+ } ) ;
101282} ) ;
0 commit comments