From 80208e7696b8e391fe301ab312c73cd5f500bd4c Mon Sep 17 00:00:00 2001 From: Luna Ruan Date: Fri, 8 Jul 2022 12:13:29 -0400 Subject: [PATCH] [Transition Tracing] Add onTransitionProgress Callback (#24833) This PR adds support for `onTransitionProgress` (`onTransitionProgress(transitionName: string, startTime: number, currentTime: number, pending: Array<{name: null | string}>)`) We call this callback when: * When **a child suspense boundary of the transition commits in a fallback state**. Only the suspense boundaries that are triggered and commit in a fallback state when the transition first occurs (and all subsequent suspense boundaries in the initial suspense boundary's subtree) are considered a part of the transition * **A child suspense boundary of the transition resolves** When we call `onTransitionProgress`, we call the function with a `pending` array. This array contains the names of the transition's suspense boundaries that are still in a fallback state --- .../src/ReactFiberCommitWork.new.js | 61 ++-- .../src/ReactFiberCommitWork.old.js | 61 ++-- .../src/ReactFiberOffscreenComponent.js | 3 +- .../ReactFiberTracingMarkerComponent.new.js | 19 +- .../ReactFiberTracingMarkerComponent.old.js | 19 +- .../src/ReactFiberWorkLoop.new.js | 29 ++ .../src/ReactFiberWorkLoop.old.js | 29 ++ .../__tests__/ReactTransitionTracing-test.js | 328 +++++++++++++++++- 8 files changed, 498 insertions(+), 51 deletions(-) diff --git a/packages/react-reconciler/src/ReactFiberCommitWork.new.js b/packages/react-reconciler/src/ReactFiberCommitWork.new.js index 5c46585431b99..6f46abdcddcde 100644 --- a/packages/react-reconciler/src/ReactFiberCommitWork.new.js +++ b/packages/react-reconciler/src/ReactFiberCommitWork.new.js @@ -143,6 +143,7 @@ import { enqueuePendingPassiveProfilerEffect, restorePendingUpdaters, addTransitionStartCallbackToPendingTransition, + addTransitionProgressCallbackToPendingTransition, addTransitionCompleteCallbackToPendingTransition, addMarkerCompleteCallbackToPendingTransition, setIsRunningInsertionEffect, @@ -1119,10 +1120,24 @@ function commitTransitionProgress(offscreenFiber: Fiber) { // The suspense boundaries was just hidden. Add the boundary // to the pending boundary set if it's there if (pendingMarkers !== null) { - pendingMarkers.forEach(pendingBoundaries => { - pendingBoundaries.set(offscreenInstance, { - name, - }); + pendingMarkers.forEach(markerInstance => { + const pendingBoundaries = markerInstance.pendingSuspenseBoundaries; + if ( + pendingBoundaries !== null && + !pendingBoundaries.has(offscreenInstance) + ) { + pendingBoundaries.set(offscreenInstance, { + name, + }); + if (markerInstance.transitions !== null) { + markerInstance.transitions.forEach(transition => { + addTransitionProgressCallbackToPendingTransition( + transition, + pendingBoundaries, + ); + }); + } + } }); } } else if (wasHidden && !isHidden) { @@ -1130,9 +1145,21 @@ function commitTransitionProgress(offscreenFiber: Fiber) { // the boundary from the pending suspense boundaries set // if it's there if (pendingMarkers !== null) { - pendingMarkers.forEach(pendingBoundaries => { - if (pendingBoundaries.has(offscreenInstance)) { + pendingMarkers.forEach(markerInstance => { + const pendingBoundaries = markerInstance.pendingSuspenseBoundaries; + if ( + pendingBoundaries !== null && + pendingBoundaries.has(offscreenInstance) + ) { pendingBoundaries.delete(offscreenInstance); + if (markerInstance.transitions !== null) { + markerInstance.transitions.forEach(transition => { + addTransitionProgressCallbackToPendingTransition( + transition, + pendingBoundaries, + ); + }); + } } }); } @@ -2888,17 +2915,13 @@ function commitPassiveMountOnFiber( clearTransitionsForLanes(finishedRoot, committedLanes); } - incompleteTransitions.forEach( - ({pendingSuspenseBoundaries}, transition) => { - if ( - pendingSuspenseBoundaries === null || - pendingSuspenseBoundaries.size === 0 - ) { - addTransitionCompleteCallbackToPendingTransition(transition); - incompleteTransitions.delete(transition); - } - }, - ); + incompleteTransitions.forEach((markerInstance, transition) => { + const pendingBoundaries = markerInstance.pendingSuspenseBoundaries; + if (pendingBoundaries === null || pendingBoundaries.size === 0) { + addTransitionCompleteCallbackToPendingTransition(transition); + incompleteTransitions.delete(transition); + } + }); clearTransitionsForLanes(finishedRoot, committedLanes); } @@ -2975,9 +2998,7 @@ function commitPassiveMountOnFiber( instance.pendingMarkers = new Set(); } - instance.pendingMarkers.add( - markerInstance.pendingSuspenseBoundaries, - ); + instance.pendingMarkers.add(markerInstance); } }); } diff --git a/packages/react-reconciler/src/ReactFiberCommitWork.old.js b/packages/react-reconciler/src/ReactFiberCommitWork.old.js index 7415997335ea7..bc7fc9ae36a6d 100644 --- a/packages/react-reconciler/src/ReactFiberCommitWork.old.js +++ b/packages/react-reconciler/src/ReactFiberCommitWork.old.js @@ -143,6 +143,7 @@ import { enqueuePendingPassiveProfilerEffect, restorePendingUpdaters, addTransitionStartCallbackToPendingTransition, + addTransitionProgressCallbackToPendingTransition, addTransitionCompleteCallbackToPendingTransition, addMarkerCompleteCallbackToPendingTransition, setIsRunningInsertionEffect, @@ -1119,10 +1120,24 @@ function commitTransitionProgress(offscreenFiber: Fiber) { // The suspense boundaries was just hidden. Add the boundary // to the pending boundary set if it's there if (pendingMarkers !== null) { - pendingMarkers.forEach(pendingBoundaries => { - pendingBoundaries.set(offscreenInstance, { - name, - }); + pendingMarkers.forEach(markerInstance => { + const pendingBoundaries = markerInstance.pendingSuspenseBoundaries; + if ( + pendingBoundaries !== null && + !pendingBoundaries.has(offscreenInstance) + ) { + pendingBoundaries.set(offscreenInstance, { + name, + }); + if (markerInstance.transitions !== null) { + markerInstance.transitions.forEach(transition => { + addTransitionProgressCallbackToPendingTransition( + transition, + pendingBoundaries, + ); + }); + } + } }); } } else if (wasHidden && !isHidden) { @@ -1130,9 +1145,21 @@ function commitTransitionProgress(offscreenFiber: Fiber) { // the boundary from the pending suspense boundaries set // if it's there if (pendingMarkers !== null) { - pendingMarkers.forEach(pendingBoundaries => { - if (pendingBoundaries.has(offscreenInstance)) { + pendingMarkers.forEach(markerInstance => { + const pendingBoundaries = markerInstance.pendingSuspenseBoundaries; + if ( + pendingBoundaries !== null && + pendingBoundaries.has(offscreenInstance) + ) { pendingBoundaries.delete(offscreenInstance); + if (markerInstance.transitions !== null) { + markerInstance.transitions.forEach(transition => { + addTransitionProgressCallbackToPendingTransition( + transition, + pendingBoundaries, + ); + }); + } } }); } @@ -2888,17 +2915,13 @@ function commitPassiveMountOnFiber( clearTransitionsForLanes(finishedRoot, committedLanes); } - incompleteTransitions.forEach( - ({pendingSuspenseBoundaries}, transition) => { - if ( - pendingSuspenseBoundaries === null || - pendingSuspenseBoundaries.size === 0 - ) { - addTransitionCompleteCallbackToPendingTransition(transition); - incompleteTransitions.delete(transition); - } - }, - ); + incompleteTransitions.forEach((markerInstance, transition) => { + const pendingBoundaries = markerInstance.pendingSuspenseBoundaries; + if (pendingBoundaries === null || pendingBoundaries.size === 0) { + addTransitionCompleteCallbackToPendingTransition(transition); + incompleteTransitions.delete(transition); + } + }); clearTransitionsForLanes(finishedRoot, committedLanes); } @@ -2975,9 +2998,7 @@ function commitPassiveMountOnFiber( instance.pendingMarkers = new Set(); } - instance.pendingMarkers.add( - markerInstance.pendingSuspenseBoundaries, - ); + instance.pendingMarkers.add(markerInstance); } }); } diff --git a/packages/react-reconciler/src/ReactFiberOffscreenComponent.js b/packages/react-reconciler/src/ReactFiberOffscreenComponent.js index 0c989dd53fe67..ca1a3938410f7 100644 --- a/packages/react-reconciler/src/ReactFiberOffscreenComponent.js +++ b/packages/react-reconciler/src/ReactFiberOffscreenComponent.js @@ -12,7 +12,6 @@ import type {Lanes} from './ReactFiberLane.old'; import type {SpawnedCachePool} from './ReactFiberCacheComponent.new'; import type { Transition, - PendingSuspenseBoundaries, TracingMarkerInstance, } from './ReactFiberTracingMarkerComponent.new'; @@ -45,7 +44,7 @@ export type OffscreenQueue = {| export type OffscreenInstance = {| isHidden: boolean, - pendingMarkers: Set | null, + pendingMarkers: Set | null, transitions: Set | null, retryCache: WeakSet | Set | null, |}; diff --git a/packages/react-reconciler/src/ReactFiberTracingMarkerComponent.new.js b/packages/react-reconciler/src/ReactFiberTracingMarkerComponent.new.js index e7522fd93fa4a..8e1a60d38caba 100644 --- a/packages/react-reconciler/src/ReactFiberTracingMarkerComponent.new.js +++ b/packages/react-reconciler/src/ReactFiberTracingMarkerComponent.new.js @@ -24,6 +24,7 @@ export type MarkerTransition = { export type PendingTransitionCallbacks = { transitionStart: Array | null, + transitionProgress: Map | null, transitionComplete: Array | null, markerComplete: Array | null, }; @@ -76,6 +77,19 @@ export function processTransitionCallbacks( }); } + const transitionProgress = pendingTransitions.transitionProgress; + const onTransitionProgress = callbacks.onTransitionProgress; + if (onTransitionProgress != null && transitionProgress !== null) { + transitionProgress.forEach((pending, transition) => { + onTransitionProgress( + transition.name, + transition.startTime, + endTime, + Array.from(pending.values()), + ); + }); + } + const transitionComplete = pendingTransitions.transitionComplete; if (transitionComplete !== null) { transitionComplete.forEach(transition => { @@ -117,10 +131,11 @@ export function pushRootMarkerInstance(workInProgress: Fiber): void { if (transitions !== null) { transitions.forEach(transition => { if (!root.incompleteTransitions.has(transition)) { - root.incompleteTransitions.set(transition, { + const markerInstance: TracingMarkerInstance = { transitions: new Set([transition]), pendingSuspenseBoundaries: null, - }); + }; + root.incompleteTransitions.set(transition, markerInstance); } }); } diff --git a/packages/react-reconciler/src/ReactFiberTracingMarkerComponent.old.js b/packages/react-reconciler/src/ReactFiberTracingMarkerComponent.old.js index 0aba8780968af..0a5693823edad 100644 --- a/packages/react-reconciler/src/ReactFiberTracingMarkerComponent.old.js +++ b/packages/react-reconciler/src/ReactFiberTracingMarkerComponent.old.js @@ -24,6 +24,7 @@ export type MarkerTransition = { export type PendingTransitionCallbacks = { transitionStart: Array | null, + transitionProgress: Map | null, transitionComplete: Array | null, markerComplete: Array | null, }; @@ -76,6 +77,19 @@ export function processTransitionCallbacks( }); } + const transitionProgress = pendingTransitions.transitionProgress; + const onTransitionProgress = callbacks.onTransitionProgress; + if (onTransitionProgress != null && transitionProgress !== null) { + transitionProgress.forEach((pending, transition) => { + onTransitionProgress( + transition.name, + transition.startTime, + endTime, + Array.from(pending.values()), + ); + }); + } + const transitionComplete = pendingTransitions.transitionComplete; if (transitionComplete !== null) { transitionComplete.forEach(transition => { @@ -117,10 +131,11 @@ export function pushRootMarkerInstance(workInProgress: Fiber): void { if (transitions !== null) { transitions.forEach(transition => { if (!root.incompleteTransitions.has(transition)) { - root.incompleteTransitions.set(transition, { + const markerInstance: TracingMarkerInstance = { transitions: new Set([transition]), pendingSuspenseBoundaries: null, - }); + }; + root.incompleteTransitions.set(transition, markerInstance); } }); } diff --git a/packages/react-reconciler/src/ReactFiberWorkLoop.new.js b/packages/react-reconciler/src/ReactFiberWorkLoop.new.js index f3c141d76cf54..11d41148abb88 100644 --- a/packages/react-reconciler/src/ReactFiberWorkLoop.new.js +++ b/packages/react-reconciler/src/ReactFiberWorkLoop.new.js @@ -17,6 +17,7 @@ import type {EventPriority} from './ReactEventPriorities.new'; import type { PendingTransitionCallbacks, MarkerTransition, + PendingSuspenseBoundaries, Transition, } from './ReactFiberTracingMarkerComponent.new'; import type {OffscreenInstance} from './ReactFiberOffscreenComponent'; @@ -339,6 +340,7 @@ export function addTransitionStartCallbackToPendingTransition( if (currentPendingTransitionCallbacks === null) { currentPendingTransitionCallbacks = { transitionStart: [], + transitionProgress: null, transitionComplete: null, markerComplete: null, }; @@ -359,6 +361,7 @@ export function addMarkerCompleteCallbackToPendingTransition( if (currentPendingTransitionCallbacks === null) { currentPendingTransitionCallbacks = { transitionStart: null, + transitionProgress: null, transitionComplete: null, markerComplete: [], }; @@ -372,6 +375,31 @@ export function addMarkerCompleteCallbackToPendingTransition( } } +export function addTransitionProgressCallbackToPendingTransition( + transition: Transition, + boundaries: PendingSuspenseBoundaries, +) { + if (enableTransitionTracing) { + if (currentPendingTransitionCallbacks === null) { + currentPendingTransitionCallbacks = { + transitionStart: null, + transitionProgress: new Map(), + transitionComplete: null, + markerComplete: null, + }; + } + + if (currentPendingTransitionCallbacks.transitionProgress === null) { + currentPendingTransitionCallbacks.transitionProgress = new Map(); + } + + currentPendingTransitionCallbacks.transitionProgress.set( + transition, + boundaries, + ); + } +} + export function addTransitionCompleteCallbackToPendingTransition( transition: Transition, ) { @@ -379,6 +407,7 @@ export function addTransitionCompleteCallbackToPendingTransition( if (currentPendingTransitionCallbacks === null) { currentPendingTransitionCallbacks = { transitionStart: null, + transitionProgress: null, transitionComplete: [], markerComplete: null, }; diff --git a/packages/react-reconciler/src/ReactFiberWorkLoop.old.js b/packages/react-reconciler/src/ReactFiberWorkLoop.old.js index 235d0523cbbf4..539d7e2f7731a 100644 --- a/packages/react-reconciler/src/ReactFiberWorkLoop.old.js +++ b/packages/react-reconciler/src/ReactFiberWorkLoop.old.js @@ -17,6 +17,7 @@ import type {EventPriority} from './ReactEventPriorities.old'; import type { PendingTransitionCallbacks, MarkerTransition, + PendingSuspenseBoundaries, Transition, } from './ReactFiberTracingMarkerComponent.old'; import type {OffscreenInstance} from './ReactFiberOffscreenComponent'; @@ -339,6 +340,7 @@ export function addTransitionStartCallbackToPendingTransition( if (currentPendingTransitionCallbacks === null) { currentPendingTransitionCallbacks = { transitionStart: [], + transitionProgress: null, transitionComplete: null, markerComplete: null, }; @@ -359,6 +361,7 @@ export function addMarkerCompleteCallbackToPendingTransition( if (currentPendingTransitionCallbacks === null) { currentPendingTransitionCallbacks = { transitionStart: null, + transitionProgress: null, transitionComplete: null, markerComplete: [], }; @@ -372,6 +375,31 @@ export function addMarkerCompleteCallbackToPendingTransition( } } +export function addTransitionProgressCallbackToPendingTransition( + transition: Transition, + boundaries: PendingSuspenseBoundaries, +) { + if (enableTransitionTracing) { + if (currentPendingTransitionCallbacks === null) { + currentPendingTransitionCallbacks = { + transitionStart: null, + transitionProgress: new Map(), + transitionComplete: null, + markerComplete: null, + }; + } + + if (currentPendingTransitionCallbacks.transitionProgress === null) { + currentPendingTransitionCallbacks.transitionProgress = new Map(); + } + + currentPendingTransitionCallbacks.transitionProgress.set( + transition, + boundaries, + ); + } +} + export function addTransitionCompleteCallbackToPendingTransition( transition: Transition, ) { @@ -379,6 +407,7 @@ export function addTransitionCompleteCallbackToPendingTransition( if (currentPendingTransitionCallbacks === null) { currentPendingTransitionCallbacks = { transitionStart: null, + transitionProgress: null, transitionComplete: [], markerComplete: null, }; diff --git a/packages/react-reconciler/src/__tests__/ReactTransitionTracing-test.js b/packages/react-reconciler/src/__tests__/ReactTransitionTracing-test.js index 1926f47fb64d3..c590741094bba 100644 --- a/packages/react-reconciler/src/__tests__/ReactTransitionTracing-test.js +++ b/packages/react-reconciler/src/__tests__/ReactTransitionTracing-test.js @@ -168,6 +168,12 @@ describe('ReactInteractionTracing', () => { `onTransitionStart(${name}, ${startTime})`, ); }, + onTransitionProgress: (name, startTime, endTime, pending) => { + const suspenseNames = pending.map(p => p.name || '').join(', '); + Scheduler.unstable_yieldValue( + `onTransitionProgress(${name}, ${startTime}, ${endTime}, [${suspenseNames}])`, + ); + }, onTransitionComplete: (name, startTime, endTime) => { Scheduler.unstable_yieldValue( `onTransitionComplete(${name}, ${startTime}, ${endTime})`, @@ -283,6 +289,12 @@ describe('ReactInteractionTracing', () => { `onTransitionStart(${name}, ${startTime})`, ); }, + onTransitionProgress: (name, startTime, endTime, pending) => { + const suspenseNames = pending.map(p => p.name || '').join(', '); + Scheduler.unstable_yieldValue( + `onTransitionProgress(${name}, ${startTime}, ${endTime}, [${suspenseNames}])`, + ); + }, onTransitionComplete: (name, startTime, endTime) => { Scheduler.unstable_yieldValue( `onTransitionComplete(${name}, ${startTime}, ${endTime})`, @@ -301,7 +313,7 @@ describe('ReactInteractionTracing', () => { {navigate ? ( } - name="suspense page"> + unstable_name="suspense page"> ) : ( @@ -330,6 +342,7 @@ describe('ReactInteractionTracing', () => { 'Suspend [Page Two]', 'Loading...', 'onTransitionStart(page transition, 1000)', + 'onTransitionProgress(page transition, 1000, 2000, [suspense page])', ]); ReactNoop.expire(1000); @@ -338,6 +351,7 @@ describe('ReactInteractionTracing', () => { expect(Scheduler).toFlushAndYield([ 'Page Two', + 'onTransitionProgress(page transition, 1000, 3000, [])', 'onTransitionComplete(page transition, 1000, 3000)', ]); }); @@ -351,6 +365,12 @@ describe('ReactInteractionTracing', () => { `onTransitionStart(${name}, ${startTime})`, ); }, + onTransitionProgress: (name, startTime, endTime, pending) => { + const suspenseNames = pending.map(p => p.name || '').join(', '); + Scheduler.unstable_yieldValue( + `onTransitionProgress(${name}, ${startTime}, ${endTime}, [${suspenseNames}])`, + ); + }, onTransitionComplete: (name, startTime, endTime) => { Scheduler.unstable_yieldValue( `onTransitionComplete(${name}, ${startTime}, ${endTime})`, @@ -377,13 +397,15 @@ describe('ReactInteractionTracing', () => { {navigate ? ( <> {showText ? ( - }> + }> ) : null} } - name="suspense page"> + unstable_name="suspense page"> @@ -410,6 +432,7 @@ describe('ReactInteractionTracing', () => { 'Suspend [Page Two]', 'Loading...', 'onTransitionStart(page transition, 1000)', + 'onTransitionProgress(page transition, 1000, 1000, [suspense page])', ]); await resolveText('Page Two'); @@ -417,6 +440,7 @@ describe('ReactInteractionTracing', () => { await advanceTimers(1000); expect(Scheduler).toFlushAndYield([ 'Page Two', + 'onTransitionProgress(page transition, 1000, 2000, [])', 'onTransitionComplete(page transition, 1000, 2000)', ]); @@ -426,6 +450,7 @@ describe('ReactInteractionTracing', () => { 'Show Text Loading...', 'Page Two', 'onTransitionStart(text transition, 2000)', + 'onTransitionProgress(text transition, 2000, 2000, [show text])', ]); await resolveText('Show Text'); @@ -433,6 +458,7 @@ describe('ReactInteractionTracing', () => { await advanceTimers(1000); expect(Scheduler).toFlushAndYield([ 'Show Text', + 'onTransitionProgress(text transition, 2000, 3000, [])', 'onTransitionComplete(text transition, 2000, 3000)', ]); }); @@ -446,6 +472,12 @@ describe('ReactInteractionTracing', () => { `onTransitionStart(${name}, ${startTime})`, ); }, + onTransitionProgress: (name, startTime, endTime, pending) => { + const suspenseNames = pending.map(p => p.name || '').join(', '); + Scheduler.unstable_yieldValue( + `onTransitionProgress(${name}, ${startTime}, ${endTime}, [${suspenseNames}])`, + ); + }, onTransitionComplete: (name, startTime, endTime) => { Scheduler.unstable_yieldValue( `onTransitionComplete(${name}, ${startTime}, ${endTime})`, @@ -470,13 +502,15 @@ describe('ReactInteractionTracing', () => { {navigate ? ( <> {showText ? ( - }> + }> ) : null} } - name="suspense page"> + unstable_name="suspense page"> @@ -505,6 +539,7 @@ describe('ReactInteractionTracing', () => { 'Suspend [Page Two]', 'Loading...', 'onTransitionStart(page transition, 1000)', + 'onTransitionProgress(page transition, 1000, 2000, [suspense page])', ]); }); @@ -517,6 +552,7 @@ describe('ReactInteractionTracing', () => { 'Suspend [Page Two]', 'Loading...', 'onTransitionStart(show text, 2000)', + 'onTransitionProgress(show text, 2000, 2000, [show text])', ]); }); @@ -527,6 +563,7 @@ describe('ReactInteractionTracing', () => { expect(Scheduler).toFlushAndYield([ 'Page Two', + 'onTransitionProgress(page transition, 1000, 3000, [])', 'onTransitionComplete(page transition, 1000, 3000)', ]); @@ -536,11 +573,292 @@ describe('ReactInteractionTracing', () => { expect(Scheduler).toFlushAndYield([ 'Show Text', + 'onTransitionProgress(show text, 2000, 4000, [])', 'onTransitionComplete(show text, 2000, 4000)', ]); }); }); + // @gate enableTransitionTracing + it('trace interaction with nested and sibling suspense boundaries', async () => { + const transitionCallbacks = { + onTransitionStart: (name, startTime) => { + Scheduler.unstable_yieldValue( + `onTransitionStart(${name}, ${startTime})`, + ); + }, + onTransitionProgress: (name, startTime, endTime, pending) => { + const suspenseNames = pending.map(p => p.name || '').join(', '); + Scheduler.unstable_yieldValue( + `onTransitionProgress(${name}, ${startTime}, ${endTime}, [${suspenseNames}])`, + ); + }, + onTransitionComplete: (name, startTime, endTime) => { + Scheduler.unstable_yieldValue( + `onTransitionComplete(${name}, ${startTime}, ${endTime})`, + ); + }, + }; + + let navigateToPageTwo; + function App() { + const [navigate, setNavigate] = useState(false); + navigateToPageTwo = () => { + setNavigate(true); + }; + + return ( +
+ {navigate ? ( + <> + } + unstable_name="suspense page"> + + }> + + +
+ }> + + +
+
+ + ) : ( + + )} +
+ ); + } + + const root = ReactNoop.createRoot({transitionCallbacks}); + await act(async () => { + root.render(); + ReactNoop.expire(1000); + await advanceTimers(1000); + + expect(Scheduler).toFlushAndYield(['Page One']); + }); + + await act(async () => { + startTransition(() => navigateToPageTwo(), {name: 'page transition'}); + ReactNoop.expire(1000); + await advanceTimers(1000); + + expect(Scheduler).toFlushAndYield([ + 'Suspend [Page Two]', + 'Suspend [Show Text One]', + 'Show Text One Loading...', + 'Suspend [Show Text Two]', + 'Show Text Two Loading...', + 'Loading...', + 'onTransitionStart(page transition, 1000)', + 'onTransitionProgress(page transition, 1000, 2000, [suspense page])', + ]); + + resolveText('Page Two'); + ReactNoop.expire(1000); + await advanceTimers(1000); + + expect(Scheduler).toFlushAndYield([ + 'Page Two', + 'Suspend [Show Text One]', + 'Show Text One Loading...', + 'Suspend [Show Text Two]', + 'Show Text Two Loading...', + 'onTransitionProgress(page transition, 1000, 3000, [show text one, show text two])', + ]); + + resolveText('Show Text One'); + ReactNoop.expire(1000); + await advanceTimers(1000); + + expect(Scheduler).toFlushAndYield([ + 'Show Text One', + 'onTransitionProgress(page transition, 1000, 4000, [show text two])', + ]); + + resolveText('Show Text Two'); + ReactNoop.expire(1000); + await advanceTimers(1000); + + expect(Scheduler).toFlushAndYield([ + 'Show Text Two', + 'onTransitionProgress(page transition, 1000, 5000, [])', + 'onTransitionComplete(page transition, 1000, 5000)', + ]); + }); + }); + + // @gate enableTransitionTracing + it('trace interactions with the same child suspense boundaries', async () => { + const transitionCallbacks = { + onTransitionStart: (name, startTime) => { + Scheduler.unstable_yieldValue( + `onTransitionStart(${name}, ${startTime})`, + ); + }, + onTransitionProgress: (name, startTime, endTime, pending) => { + const suspenseNames = pending.map(p => p.name || '').join(', '); + Scheduler.unstable_yieldValue( + `onTransitionProgress(${name}, ${startTime}, ${endTime}, [${suspenseNames}])`, + ); + }, + onTransitionComplete: (name, startTime, endTime) => { + Scheduler.unstable_yieldValue( + `onTransitionComplete(${name}, ${startTime}, ${endTime})`, + ); + }, + }; + + let setNavigate; + let setShowTextOne; + let setShowTextTwo; + function App() { + const [navigate, _setNavigate] = useState(false); + const [showTextOne, _setShowTextOne] = useState(false); + const [showTextTwo, _setShowTextTwo] = useState(false); + + setNavigate = () => _setNavigate(true); + setShowTextOne = () => _setShowTextOne(true); + setShowTextTwo = () => _setShowTextTwo(true); + + return ( +
+ {navigate ? ( + <> + } + unstable_name="suspense page"> + + {/* showTextOne is entangled with navigate */} + {showTextOne ? ( + }> + + + ) : null} + }> + + + {/* showTextTwo's suspense boundaries shouldn't stop navigate's suspense boundaries + from completing */} + {showTextTwo ? ( + }> + + + ) : null} + + + ) : ( + + )} +
+ ); + } + + const root = ReactNoop.createRoot({transitionCallbacks}); + await act(async () => { + root.render(); + ReactNoop.expire(1000); + await advanceTimers(1000); + + expect(Scheduler).toFlushAndYield(['Page One']); + }); + + await act(async () => { + startTransition(() => setNavigate(), {name: 'navigate'}); + startTransition(() => setShowTextOne(), {name: 'show text one'}); + ReactNoop.expire(1000); + await advanceTimers(1000); + + expect(Scheduler).toFlushAndYield([ + 'Suspend [Page Two]', + 'Suspend [Show Text One]', + 'Show Text One Loading...', + 'Suspend [Show Text]', + 'Show Text Loading...', + 'Loading...', + 'onTransitionStart(navigate, 1000)', + 'onTransitionStart(show text one, 1000)', + 'onTransitionProgress(navigate, 1000, 2000, [suspense page])', + 'onTransitionProgress(show text one, 1000, 2000, [suspense page])', + ]); + + resolveText('Page Two'); + ReactNoop.expire(1000); + await advanceTimers(1000); + expect(Scheduler).toFlushAndYield([ + 'Page Two', + 'Suspend [Show Text One]', + 'Show Text One Loading...', + 'Suspend [Show Text]', + 'Show Text Loading...', + 'onTransitionProgress(navigate, 1000, 3000, [show text one, ])', + 'onTransitionProgress(show text one, 1000, 3000, [show text one, ])', + ]); + + startTransition(() => setShowTextTwo(), {name: 'show text two'}); + ReactNoop.expire(1000); + await advanceTimers(1000); + + expect(Scheduler).toFlushAndYield([ + 'Page Two', + 'Suspend [Show Text One]', + 'Show Text One Loading...', + 'Suspend [Show Text]', + 'Show Text Loading...', + 'Suspend [Show Text Two]', + 'Show Text Two Loading...', + 'onTransitionStart(show text two, 3000)', + 'onTransitionProgress(show text two, 3000, 4000, [show text two])', + ]); + + // This should not cause navigate to finish because it's entangled with + // show text one + resolveText('Show Text'); + ReactNoop.expire(1000); + await advanceTimers(1000); + + expect(Scheduler).toFlushAndYield([ + 'Show Text', + 'onTransitionProgress(navigate, 1000, 5000, [show text one])', + 'onTransitionProgress(show text one, 1000, 5000, [show text one])', + ]); + + // This should not cause show text two to finish but nothing else + resolveText('Show Text Two'); + ReactNoop.expire(1000); + await advanceTimers(1000); + expect(Scheduler).toFlushAndYield([ + 'Show Text Two', + 'onTransitionProgress(show text two, 3000, 6000, [])', + 'onTransitionComplete(show text two, 3000, 6000)', + ]); + + // This should cause everything to finish + resolveText('Show Text One'); + ReactNoop.expire(1000); + await advanceTimers(1000); + + expect(Scheduler).toFlushAndYield([ + 'Show Text One', + 'onTransitionProgress(navigate, 1000, 7000, [])', + 'onTransitionProgress(show text one, 1000, 7000, [])', + 'onTransitionComplete(navigate, 1000, 7000)', + 'onTransitionComplete(show text one, 1000, 7000)', + ]); + }); + }); + // @gate enableTransitionTracing it('should correctly trace interactions for tracing markers complete', async () => { const transitionCallbacks = {