@@ -97,9 +97,10 @@ function runPlaceholderTests(suiteLabel, loadReactNoop) {
9797 textResourceShouldFail = false ;
9898 } ) ;
9999
100- function Text ( props ) {
101- ReactTestRenderer . unstable_yield ( props . text ) ;
102- return props . text ;
100+ function Text ( { fakeRenderDuration = 0 , text = 'Text' } ) {
101+ advanceTimeBy ( fakeRenderDuration ) ;
102+ ReactTestRenderer . unstable_yield ( text ) ;
103+ return text ;
103104 }
104105
105106 function AsyncText ( { fakeRenderDuration = 0 , ms, text} ) {
@@ -250,82 +251,207 @@ function runPlaceholderTests(suiteLabel, loadReactNoop) {
250251 return < AsyncText ms = { 1000 } text = "Loaded" fakeRenderDuration = { 1 } /> ;
251252 } ;
252253
253- const NonSuspending = ( ) => {
254- ReactTestRenderer . unstable_yield ( 'NonSuspending' ) ;
255- advanceTimeBy ( 5 ) ;
256- return 'NonSuspending' ;
257- } ;
258-
259- App = ( ) => {
254+ App = ( { shouldSuspend, text = 'Text' , textRenderDuration = 5 } ) => {
260255 ReactTestRenderer . unstable_yield ( 'App' ) ;
261256 return (
262257 < Profiler id = "root" onRender = { onRender } >
263258 < Suspense maxDuration = { 500 } fallback = { < Fallback /> } >
264- < Suspending />
265- < NonSuspending />
259+ { shouldSuspend && < Suspending /> }
260+ < Text fakeRenderDuration = { textRenderDuration } text = { text } />
266261 </ Suspense >
267262 </ Profiler >
268263 ) ;
269264 } ;
270265 } ) ;
271266
272- it ( 'properly accounts for base durations when a suspended times out in a sync tree' , ( ) => {
273- const root = ReactTestRenderer . create ( < App /> ) ;
274- expect ( root . toJSON ( ) ) . toEqual ( [ 'Loading...' ] ) ;
275- expect ( onRender ) . toHaveBeenCalledTimes ( 1 ) ;
267+ describe ( 'when suspending during mount' , ( ) => {
268+ it ( 'properly accounts for base durations when a suspended times out in a sync tree' , ( ) => {
269+ const root = ReactTestRenderer . create ( < App shouldSuspend = { true } /> ) ;
270+ expect ( root . toJSON ( ) ) . toEqual ( [ 'Loading...' ] ) ;
271+ expect ( onRender ) . toHaveBeenCalledTimes ( 1 ) ;
276272
277- // Initial mount only shows the "Loading..." Fallback.
278- // The treeBaseDuration then should be 10ms spent rendering Fallback,
279- // but the actualDuration should also include the 8ms spent rendering the hidden tree.
280- expect ( onRender . mock . calls [ 0 ] [ 2 ] ) . toBe ( 18 ) ;
281- expect ( onRender . mock . calls [ 0 ] [ 3 ] ) . toBe ( 10 ) ;
273+ // Initial mount only shows the "Loading..." Fallback.
274+ // The treeBaseDuration then should be 10ms spent rendering Fallback,
275+ // but the actualDuration should also include the 8ms spent rendering the hidden tree.
276+ expect ( onRender . mock . calls [ 0 ] [ 2 ] ) . toBe ( 18 ) ;
277+ expect ( onRender . mock . calls [ 0 ] [ 3 ] ) . toBe ( 10 ) ;
282278
283- jest . advanceTimersByTime ( 1000 ) ;
279+ jest . advanceTimersByTime ( 1000 ) ;
284280
285- expect ( root . toJSON ( ) ) . toEqual ( [ 'Loaded' , 'NonSuspending ' ] ) ;
286- expect ( onRender ) . toHaveBeenCalledTimes ( 2 ) ;
281+ expect ( root . toJSON ( ) ) . toEqual ( [ 'Loaded' , 'Text ' ] ) ;
282+ expect ( onRender ) . toHaveBeenCalledTimes ( 2 ) ;
287283
288- // When the suspending data is resolved and our final UI is rendered,
289- // the baseDuration should only include the 1ms re-rendering AsyncText,
290- // but the treeBaseDuration should include the full 8ms spent in the tree.
291- expect ( onRender . mock . calls [ 1 ] [ 2 ] ) . toBe ( 1 ) ;
292- expect ( onRender . mock . calls [ 1 ] [ 3 ] ) . toBe ( 8 ) ;
284+ // When the suspending data is resolved and our final UI is rendered,
285+ // the baseDuration should only include the 1ms re-rendering AsyncText,
286+ // but the treeBaseDuration should include the full 8ms spent in the tree.
287+ expect ( onRender . mock . calls [ 1 ] [ 2 ] ) . toBe ( 1 ) ;
288+ expect ( onRender . mock . calls [ 1 ] [ 3 ] ) . toBe ( 8 ) ;
289+ } ) ;
290+
291+ it ( 'properly accounts for base durations when a suspended times out in a concurrent tree' , ( ) => {
292+ const root = ReactTestRenderer . create ( < App shouldSuspend = { true } /> , {
293+ unstable_isConcurrent : true ,
294+ } ) ;
295+
296+ expect ( root ) . toFlushAndYield ( [
297+ 'App' ,
298+ 'Suspending' ,
299+ 'Suspend! [Loaded]' ,
300+ 'Text' ,
301+ 'Fallback' ,
302+ ] ) ;
303+ expect ( root ) . toMatchRenderedOutput ( null ) ;
304+
305+ // Show the fallback UI.
306+ jest . advanceTimersByTime ( 750 ) ;
307+ expect ( root ) . toMatchRenderedOutput ( 'Loading...' ) ;
308+ expect ( onRender ) . toHaveBeenCalledTimes ( 1 ) ;
309+
310+ // Initial mount only shows the "Loading..." Fallback.
311+ // The treeBaseDuration then should be 10ms spent rendering Fallback,
312+ // but the actualDuration should also include the 8ms spent rendering the hidden tree.
313+ expect ( onRender . mock . calls [ 0 ] [ 2 ] ) . toBe ( 18 ) ;
314+ expect ( onRender . mock . calls [ 0 ] [ 3 ] ) . toBe ( 10 ) ;
315+
316+ // Resolve the pending promise.
317+ jest . advanceTimersByTime ( 250 ) ;
318+ expect ( ReactTestRenderer ) . toHaveYielded ( [
319+ 'Promise resolved [Loaded]' ,
320+ ] ) ;
321+ expect ( root ) . toFlushAndYield ( [ 'Suspending' , 'Loaded' , 'Text' ] ) ;
322+ expect ( root ) . toMatchRenderedOutput ( 'LoadedText' ) ;
323+ expect ( onRender ) . toHaveBeenCalledTimes ( 2 ) ;
324+
325+ // When the suspending data is resolved and our final UI is rendered,
326+ // both times should include the 8ms re-rendering Suspending and AsyncText.
327+ expect ( onRender . mock . calls [ 1 ] [ 2 ] ) . toBe ( 8 ) ;
328+ expect ( onRender . mock . calls [ 1 ] [ 3 ] ) . toBe ( 8 ) ;
329+ } ) ;
293330 } ) ;
294331
295- it ( 'properly accounts for base durations when a suspended times out in a concurrent tree' , ( ) => {
296- const root = ReactTestRenderer . create ( < App /> , {
297- unstable_isConcurrent : true ,
332+ describe ( 'when suspending during update' , ( ) => {
333+ it ( 'properly accounts for base durations when a suspended times out in a sync tree' , ( ) => {
334+ const root = ReactTestRenderer . create (
335+ < App shouldSuspend = { false } textRenderDuration = { 5 } /> ,
336+ ) ;
337+ expect ( root . toJSON ( ) ) . toEqual ( 'Text' ) ;
338+ expect ( onRender ) . toHaveBeenCalledTimes ( 1 ) ;
339+
340+ // Initial mount only shows the "Text" text.
341+ // It should take 5ms to render.
342+ expect ( onRender . mock . calls [ 0 ] [ 2 ] ) . toBe ( 5 ) ;
343+ expect ( onRender . mock . calls [ 0 ] [ 3 ] ) . toBe ( 5 ) ;
344+
345+ root . update ( < App shouldSuspend = { true } textRenderDuration = { 5 } /> ) ;
346+ expect ( root . toJSON ( ) ) . toEqual ( [ 'Loading...' ] ) ;
347+ expect ( onRender ) . toHaveBeenCalledTimes ( 2 ) ;
348+
349+ // The suspense update should only show the "Loading..." Fallback.
350+ // Both durations should include 10ms spent rendering Fallback
351+ // plus the 8ms rendering the (hidden) components.
352+ expect ( onRender . mock . calls [ 1 ] [ 2 ] ) . toBe ( 18 ) ;
353+ expect ( onRender . mock . calls [ 1 ] [ 3 ] ) . toBe ( 18 ) ;
354+
355+ root . update (
356+ < App shouldSuspend = { true } text = "New" textRenderDuration = { 6 } /> ,
357+ ) ;
358+ expect ( root . toJSON ( ) ) . toEqual ( [ 'Loading...' ] ) ;
359+ expect ( onRender ) . toHaveBeenCalledTimes ( 3 ) ;
360+
361+ // If we force another update while still timed out,
362+ // but this time the Text component took 1ms longer to render.
363+ // This should impact both actualDuration and treeBaseDuration.
364+ expect ( onRender . mock . calls [ 2 ] [ 2 ] ) . toBe ( 19 ) ;
365+ expect ( onRender . mock . calls [ 2 ] [ 3 ] ) . toBe ( 19 ) ;
366+
367+ jest . advanceTimersByTime ( 1000 ) ;
368+
369+ // TODO Change expected onRender count to 4.
370+ // At the moment, every time we suspended while rendering will cause a commit.
371+ // This will probably change in the future, but that's why there are two new ones.
372+ expect ( root . toJSON ( ) ) . toEqual ( [ 'Loaded' , 'New' ] ) ;
373+ expect ( onRender ) . toHaveBeenCalledTimes ( 5 ) ;
374+
375+ // When the suspending data is resolved and our final UI is rendered,
376+ // the baseDuration should only include the 1ms re-rendering AsyncText,
377+ // but the treeBaseDuration should include the full 9ms spent in the tree.
378+ expect ( onRender . mock . calls [ 3 ] [ 2 ] ) . toBe ( 1 ) ;
379+ expect ( onRender . mock . calls [ 3 ] [ 3 ] ) . toBe ( 9 ) ;
380+
381+ // TODO Remove these assertions once this commit is gone.
382+ // For now, there was no actual work done during this commit; see above comment.
383+ expect ( onRender . mock . calls [ 4 ] [ 2 ] ) . toBe ( 0 ) ;
384+ expect ( onRender . mock . calls [ 4 ] [ 3 ] ) . toBe ( 9 ) ;
298385 } ) ;
299386
300- expect ( root ) . toFlushAndYield ( [
301- 'App' ,
302- 'Suspending' ,
303- 'Suspend! [Loaded]' ,
304- 'NonSuspending' ,
305- 'Fallback' ,
306- ] ) ;
307- expect ( root ) . toMatchRenderedOutput ( null ) ;
308-
309- jest . advanceTimersByTime ( 1000 ) ;
310-
311- expect ( ReactTestRenderer ) . toHaveYielded ( [ 'Promise resolved [Loaded]' ] ) ;
312- expect ( root ) . toMatchRenderedOutput ( 'Loading...' ) ;
313- expect ( onRender ) . toHaveBeenCalledTimes ( 1 ) ;
314-
315- // Initial mount only shows the "Loading..." Fallback.
316- // The treeBaseDuration then should be 10ms spent rendering Fallback,
317- // but the actualDuration should also include the 8ms spent rendering the hidden tree.
318- expect ( onRender . mock . calls [ 0 ] [ 2 ] ) . toBe ( 18 ) ;
319- expect ( onRender . mock . calls [ 0 ] [ 3 ] ) . toBe ( 10 ) ;
320-
321- expect ( root ) . toFlushAndYield ( [ 'Suspending' , 'Loaded' , 'NonSuspending' ] ) ;
322- expect ( root ) . toMatchRenderedOutput ( 'LoadedNonSuspending' ) ;
323- expect ( onRender ) . toHaveBeenCalledTimes ( 2 ) ;
324-
325- // When the suspending data is resolved and our final UI is rendered,
326- // both times should include the 8ms re-rendering Suspending and AsyncText.
327- expect ( onRender . mock . calls [ 1 ] [ 2 ] ) . toBe ( 8 ) ;
328- expect ( onRender . mock . calls [ 1 ] [ 3 ] ) . toBe ( 8 ) ;
387+ it ( 'properly accounts for base durations when a suspended times out in a concurrent tree' , ( ) => {
388+ const root = ReactTestRenderer . create (
389+ < App shouldSuspend = { false } textRenderDuration = { 5 } /> ,
390+ {
391+ unstable_isConcurrent : true ,
392+ } ,
393+ ) ;
394+
395+ expect ( root ) . toFlushAndYield ( [ 'App' , 'Text' ] ) ;
396+ expect ( root ) . toMatchRenderedOutput ( 'Text' ) ;
397+ expect ( onRender ) . toHaveBeenCalledTimes ( 1 ) ;
398+
399+ // Initial mount only shows the "Text" text.
400+ // It should take 5ms to render.
401+ expect ( onRender . mock . calls [ 0 ] [ 2 ] ) . toBe ( 5 ) ;
402+ expect ( onRender . mock . calls [ 0 ] [ 3 ] ) . toBe ( 5 ) ;
403+
404+ root . update ( < App shouldSuspend = { true } textRenderDuration = { 5 } /> ) ;
405+ expect ( root ) . toFlushAndYield ( [
406+ 'App' ,
407+ 'Suspending' ,
408+ 'Suspend! [Loaded]' ,
409+ 'Text' ,
410+ 'Fallback' ,
411+ ] ) ;
412+ expect ( root ) . toMatchRenderedOutput ( 'Text' ) ;
413+
414+ // Show the fallback UI.
415+ jest . advanceTimersByTime ( 750 ) ;
416+ expect ( root ) . toMatchRenderedOutput ( 'Loading...' ) ;
417+ expect ( onRender ) . toHaveBeenCalledTimes ( 2 ) ;
418+
419+ // The suspense update should only show the "Loading..." Fallback.
420+ // The actual duration should include 10ms spent rendering Fallback,
421+ // plus the 8ms render all of the hidden, suspended subtree.
422+ // But the tree base duration should only include 10ms spent rendering Fallback,
423+ // plus the 5ms rendering the previously committed version of the hidden tree.
424+ expect ( onRender . mock . calls [ 1 ] [ 2 ] ) . toBe ( 18 ) ;
425+ expect ( onRender . mock . calls [ 1 ] [ 3 ] ) . toBe ( 15 ) ;
426+
427+ // Update again while timed out.
428+ root . update (
429+ < App shouldSuspend = { true } text = "New" textRenderDuration = { 6 } /> ,
430+ ) ;
431+ expect ( root ) . toFlushAndYield ( [
432+ 'App' ,
433+ 'Suspending' ,
434+ 'Suspend! [Loaded]' ,
435+ 'New' ,
436+ 'Fallback' ,
437+ ] ) ;
438+ expect ( root ) . toMatchRenderedOutput ( 'Loading...' ) ;
439+ expect ( onRender ) . toHaveBeenCalledTimes ( 2 ) ;
440+
441+ // Resolve the pending promise.
442+ jest . advanceTimersByTime ( 250 ) ;
443+ expect ( ReactTestRenderer ) . toHaveYielded ( [
444+ 'Promise resolved [Loaded]' ,
445+ ] ) ;
446+ expect ( root ) . toFlushAndYield ( [ 'App' , 'Suspending' , 'Loaded' , 'New' ] ) ;
447+ expect ( onRender ) . toHaveBeenCalledTimes ( 3 ) ;
448+
449+ // When the suspending data is resolved and our final UI is rendered,
450+ // both times should include the 6ms rendering Text,
451+ // the 2ms rendering Suspending, and the 1ms rendering AsyncText.
452+ expect ( onRender . mock . calls [ 2 ] [ 2 ] ) . toBe ( 9 ) ;
453+ expect ( onRender . mock . calls [ 2 ] [ 3 ] ) . toBe ( 9 ) ;
454+ } ) ;
329455 } ) ;
330456 } ) ;
331457 } ) ;
0 commit comments