diff --git a/packages/react-reconciler/src/ReactFiber.new.js b/packages/react-reconciler/src/ReactFiber.new.js index 560a3d0ec3321..1ebeebbb8975b 100644 --- a/packages/react-reconciler/src/ReactFiber.new.js +++ b/packages/react-reconciler/src/ReactFiber.new.js @@ -772,6 +772,9 @@ export function createFiberFromTracingMarker( const tracingMarkerInstance: TracingMarkerInstance = { transitions: null, pendingBoundaries: null, + deletions: null, + parents: null, + name: pendingProps.name, }; fiber.stateNode = tracingMarkerInstance; return fiber; diff --git a/packages/react-reconciler/src/ReactFiberBeginWork.new.js b/packages/react-reconciler/src/ReactFiberBeginWork.new.js index 1d3ca68b05b5b..3a37a7134c901 100644 --- a/packages/react-reconciler/src/ReactFiberBeginWork.new.js +++ b/packages/react-reconciler/src/ReactFiberBeginWork.new.js @@ -979,6 +979,7 @@ function updateTracingMarkerComponent( transitions: new Set(currentTransitions), pendingBoundaries: new Map(), name: workInProgress.pendingProps.name, + deletions: null, }; workInProgress.stateNode = markerInstance; } diff --git a/packages/react-reconciler/src/ReactFiberCommitWork.new.js b/packages/react-reconciler/src/ReactFiberCommitWork.new.js index 3f7417fc626d8..124667aa5eadb 100644 --- a/packages/react-reconciler/src/ReactFiberCommitWork.new.js +++ b/packages/react-reconciler/src/ReactFiberCommitWork.new.js @@ -30,7 +30,10 @@ import type { import type {HookFlags} from './ReactHookEffectTags'; import type {Cache} from './ReactFiberCacheComponent.new'; import type {RootState} from './ReactFiberRoot.new'; -import type {Transition} from './ReactFiberTracingMarkerComponent.new'; +import type { + Transition, + TracingMarkerInstance, +} from './ReactFiberTracingMarkerComponent.new'; import { enableCreateEventHandleAPI, @@ -146,6 +149,7 @@ import { addTransitionProgressCallbackToPendingTransition, addTransitionCompleteCallbackToPendingTransition, addMarkerProgressCallbackToPendingTransition, + addMarkerIncompleteCallbackToPendingTransition, addMarkerCompleteCallbackToPendingTransition, setIsRunningInsertionEffect, } from './ReactFiberWorkLoop.new'; @@ -1132,6 +1136,98 @@ function commitLayoutEffectOnFiber( } } +function abortParentMarkerTransitions( + fiber, + deletedFiber, + deletion, + isDeleted, +) { + const instance = deletedFiber.stateNode; + switch (fiber.tag) { + case TracingMarkerComponent: + const transitions = Array.from(deletedFiber.stateNode.transitions); + + const markerInstance = fiber.stateNode; + const markerTransitions = markerInstance.transitions; + const abortMarker = transitions.some(transition => + markerTransitions.has(transition), + ); + + if (abortMarker) { + if (markerInstance.deletions === null) { + markerInstance.deletions = new Set(); + } + + markerInstance.deletions.add(deletion); + addMarkerIncompleteCallbackToPendingTransition( + fiber.memoizedProps.name, + transitions, + markerInstance.deletions, + ); + + if ( + !isDeleted && + markerInstance.pendingBoundaries !== null && + markerInstance.pendingBoundaries.has(instance) + ) { + markerInstance.pendingBoundaries.delete(instance); + + addMarkerProgressCallbackToPendingTransition( + markerInstance.name, + transitions, + markerInstance.pendingBoundaries, + ); + } + } + break; + case HostRoot: + const root = fiber.stateNode; + const incompleteTransitions = root.incompleteTransitions; + + instance.transitions.forEach(transition => { + if (incompleteTransitions.has(transition)) { + const transitionInstance = incompleteTransitions.get(transition); + if (transitionInstance.deletions === null) { + transitionInstance.deletions = []; + } + transitionInstance.deletions.push(deletion); + + if ( + transitionInstance.pendingBoundaries !== null && + transitionInstance.pendingBoundaries.has(instance) + ) { + transitionInstance.pendingBoundaries.delete(instance); + } + } + }); + break; + default: + break; + } +} + +function recursivelyAbortParentMarkerTransitions( + deletedFiber: Fiber, + nearestMountedAncestor, + deletion, +) { + let fiber = deletedFiber; + while (fiber !== null) { + abortParentMarkerTransitions(fiber, deletedFiber, deletion, true); + if (nearestMountedAncestor.deletions.includes(fiber)) { + break; + } else { + fiber = fiber.return; + } + } + + fiber = nearestMountedAncestor; + while (fiber !== null) { + abortParentMarkerTransitions(fiber, deletedFiber, deletion, false); + fiber = fiber.return; + } +} + function commitTransitionProgress(offscreenFiber: Fiber) { if (enableTransitionTracing) { // This function adds suspense boundaries to the root @@ -1987,6 +2083,20 @@ function commitDeletionEffectsOnFiber( const prevOffscreenSubtreeWasHidden = offscreenSubtreeWasHidden; offscreenSubtreeWasHidden = prevOffscreenSubtreeWasHidden || deletedFiber.memoizedState !== null; + + if (enableTransitionTracing) { + // We need to mark this fiber's parents as deleted + const instance: OffscreenInstance = deletedFiber.stateNode; + const markers = instance.pendingMarkers; + if (markers !== null) { + markers.forEach(marker => { + if (marker.pendingBoundaries.has(instance)) { + marker.pendingBoundaries.delete(instance); + } + }); + } + } + recursivelyTraverseDeletionEffects( finishedRoot, nearestMountedAncestor, @@ -2002,6 +2112,30 @@ function commitDeletionEffectsOnFiber( } break; } + case TracingMarkerComponent: { + if (enableTransitionTracing) { + // We need to mark this fiber's parents as deleted + const instance: TracingMarkerInstance = deletedFiber.stateNode; + const transitions = instance.transitions; + if (transitions !== null) { + const deletion = { + type: 'marker', + name: deletedFiber.memoizedProps.name, + }; + recursivelyAbortParentMarkerTransitions( + deletedFiber, + nearestMountedAncestor, + deletion, + ); + } + } + recursivelyTraverseDeletionEffects( + finishedRoot, + nearestMountedAncestor, + deletedFiber, + ); + return; + } default: { recursivelyTraverseDeletionEffects( finishedRoot, @@ -2987,6 +3121,11 @@ function commitOffscreenPassiveMountEffects( } commitTransitionProgress(finishedWork); + + if (!isHidden) { + instance.transitions = null; + instance.pendingMarkers = null; + } } } @@ -3023,14 +3162,16 @@ function commitTracingMarkerPassiveMountEffect(finishedWork: Fiber) { (instance.pendingBoundaries === null || instance.pendingBoundaries.size === 0) ) { - instance.transitions.forEach(transition => { + if (instance.deletions === null) { addMarkerCompleteCallbackToPendingTransition( finishedWork.memoizedProps.name, instance.transitions, ); - }); + } instance.transitions = null; instance.pendingBoundaries = null; + instance.deletions = null; + instance.name = null; } } @@ -3146,7 +3287,9 @@ function commitPassiveMountOnFiber( incompleteTransitions.forEach((markerInstance, transition) => { const pendingBoundaries = markerInstance.pendingBoundaries; if (pendingBoundaries === null || pendingBoundaries.size === 0) { - addTransitionCompleteCallbackToPendingTransition(transition); + if (markerInstance.deletions === null) { + addTransitionCompleteCallbackToPendingTransition(transition); + } incompleteTransitions.delete(transition); } }); diff --git a/packages/react-reconciler/src/ReactFiberTracingMarkerComponent.new.js b/packages/react-reconciler/src/ReactFiberTracingMarkerComponent.new.js index fea28213ff82a..27b8d04492777 100644 --- a/packages/react-reconciler/src/ReactFiberTracingMarkerComponent.new.js +++ b/packages/react-reconciler/src/ReactFiberTracingMarkerComponent.new.js @@ -21,7 +21,14 @@ export type PendingTransitionCallbacks = { transitionStart: Array | null, transitionProgress: Map | null, transitionComplete: Array | null, - markerProgress: Map | null, + markerProgress: Map< + string, + {pendingBoundaries: PendingBoundaries, transitions: Set}, + > | null, + markerIncomplete: Map< + string, + {deletions: Array, transitions: Set}, + > | null, markerComplete: Map> | null, }; @@ -39,7 +46,14 @@ export type BatchConfigTransition = { export type TracingMarkerInstance = {| pendingBoundaries: PendingBoundaries | null, transitions: Set | null, - name?: string, + deletions: Array | null, + name: string | null, +|}; + +export type TransitionDeletion = {| + type: 'error' | 'unknown' | 'marker' | 'suspense', + name?: string | null, + transitions: Set, |}; export type PendingBoundaries = Map; @@ -64,6 +78,7 @@ export function processTransitionCallbacks( if (onMarkerProgress != null && markerProgress !== null) { markerProgress.forEach((markerInstance, markerName) => { if (markerInstance.transitions !== null) { + // TODO: Clone the suspense object so users can't modify it const pending = markerInstance.pendingBoundaries !== null ? Array.from(markerInstance.pendingBoundaries.values()) @@ -96,6 +111,31 @@ export function processTransitionCallbacks( }); } + const markerIncomplete = pendingTransitions.markerIncomplete; + const onMarkerIncomplete = callbacks.onMarkerIncomplete; + if (onMarkerIncomplete != null && markerIncomplete !== null) { + markerIncomplete.forEach(({transitions, deletions}, markerName) => { + transitions.forEach(transition => { + const filteredDeletions = []; + deletions.forEach(deletion => { + const filteredDeletion = getFilteredDeletion(deletion, endTime); + if (filteredDeletion !== null) { + filteredDeletions.push(filteredDeletion); + } + }); + + if (filteredDeletions.length > 0) { + onMarkerIncomplete( + transition.name, + markerName, + transition.startTime, + filteredDeletions, + ); + } + }); + }); + } + const transitionProgress = pendingTransitions.transitionProgress; const onTransitionProgress = callbacks.onTransitionProgress; if (onTransitionProgress != null && transitionProgress !== null) { @@ -120,6 +160,21 @@ export function processTransitionCallbacks( } } +function getFilteredDeletion(deletion: TransitionDeletion, endTime: number) { + switch (deletion.type) { + case 'marker': { + return { + type: deletion.type, + name: deletion.name, + endTime, + }; + } + default: { + return null; + } + } +} + // For every tracing marker, store a pointer to it. We will later access it // to get the set of suspense boundaries that need to resolve before the // tracing marker can be logged as complete @@ -148,6 +203,9 @@ export function pushRootMarkerInstance(workInProgress: Fiber): void { const markerInstance: TracingMarkerInstance = { transitions: new Set([transition]), pendingBoundaries: null, + deletions: null, + parents: null, + name: 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 4e965bb3f42d4..77b2ddf43c897 100644 --- a/packages/react-reconciler/src/ReactFiberWorkLoop.new.js +++ b/packages/react-reconciler/src/ReactFiberWorkLoop.new.js @@ -18,6 +18,7 @@ import type { PendingTransitionCallbacks, PendingBoundaries, Transition, + TransitionDeletion, } from './ReactFiberTracingMarkerComponent.new'; import type {OffscreenInstance} from './ReactFiberOffscreenComponent'; @@ -342,6 +343,7 @@ export function addTransitionStartCallbackToPendingTransition( transitionProgress: null, transitionComplete: null, markerProgress: null, + markerIncomplete: null, markerComplete: null, }; } @@ -357,7 +359,7 @@ export function addTransitionStartCallbackToPendingTransition( export function addMarkerProgressCallbackToPendingTransition( markerName: string, transitions: Set, - pendingBoundaries: PendingBoundaries | null, + pendingBoundaries: PendingBoundaries, ) { if (enableTransitionTracing) { if (currentPendingTransitionCallbacks === null) { @@ -366,6 +368,7 @@ export function addMarkerProgressCallbackToPendingTransition( transitionProgress: null, transitionComplete: null, markerProgress: new Map(), + markerIncomplete: null, markerComplete: null, }; } @@ -381,6 +384,34 @@ export function addMarkerProgressCallbackToPendingTransition( } } +export function addMarkerIncompleteCallbackToPendingTransition( + markerName: string, + transitions: Set, + deletions: Array, +) { + if (enableTransitionTracing) { + if (currentPendingTransitionCallbacks === null) { + currentPendingTransitionCallbacks = { + transitionStart: null, + transitionProgress: null, + transitionComplete: null, + markerProgress: null, + markerIncomplete: new Map(), + markerComplete: null, + }; + } + + if (currentPendingTransitionCallbacks.markerIncomplete === null) { + currentPendingTransitionCallbacks.markerIncomplete = new Map(); + } + + currentPendingTransitionCallbacks.markerIncomplete.set(markerName, { + transitions, + deletions, + }); + } +} + export function addMarkerCompleteCallbackToPendingTransition( markerName: string, transitions: Set, @@ -392,6 +423,7 @@ export function addMarkerCompleteCallbackToPendingTransition( transitionProgress: null, transitionComplete: null, markerProgress: null, + markerIncomplete: null, markerComplete: new Map(), }; } @@ -418,6 +450,7 @@ export function addTransitionProgressCallbackToPendingTransition( transitionProgress: new Map(), transitionComplete: null, markerProgress: null, + markerIncomplete: null, markerComplete: null, }; } @@ -443,6 +476,7 @@ export function addTransitionCompleteCallbackToPendingTransition( transitionProgress: null, transitionComplete: [], markerProgress: null, + markerIncomplete: null, markerComplete: null, }; } diff --git a/packages/react-reconciler/src/ReactInternalTypes.js b/packages/react-reconciler/src/ReactInternalTypes.js index e017674dda205..6a83f822c6a26 100644 --- a/packages/react-reconciler/src/ReactInternalTypes.js +++ b/packages/react-reconciler/src/ReactInternalTypes.js @@ -292,7 +292,6 @@ export type TransitionTracingCallbacks = { deletions: Array<{ type: string, name?: string, - newName?: string, endTime: number, }>, ) => void, @@ -315,7 +314,6 @@ export type TransitionTracingCallbacks = { deletions: Array<{ type: string, name?: string, - newName?: string, endTime: number, }>, ) => void, diff --git a/packages/react-reconciler/src/__tests__/ReactTransitionTracing-test.js b/packages/react-reconciler/src/__tests__/ReactTransitionTracing-test.js index ae4812afa8ec2..1089f0f7e0d0a 100644 --- a/packages/react-reconciler/src/__tests__/ReactTransitionTracing-test.js +++ b/packages/react-reconciler/src/__tests__/ReactTransitionTracing-test.js @@ -23,6 +23,17 @@ let caches; let seededCache; describe('ReactInteractionTracing', () => { + function stringifyDeletions(deletions) { + return deletions + .map( + d => + `{${Object.keys(d) + .map(key => `${key}: ${d[key]}`) + .sort() + .join(', ')}}`, + ) + .join(', '); + } beforeEach(() => { jest.resetModules(); @@ -1283,6 +1294,410 @@ describe('ReactInteractionTracing', () => { }); }); + // @gate enableTransitionTracing + it.skip('warn and calls marker incomplete if name changes before transition completes', 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})`, + ); + }, + onMarkerProgress: ( + transitioName, + markerName, + startTime, + currentTime, + pending, + ) => { + const suspenseNames = pending.map(p => p.name || '').join(', '); + Scheduler.unstable_yieldValue( + `onMarkerProgress(${transitioName}, ${markerName}, ${startTime}, ${currentTime}, [${suspenseNames}])`, + ); + }, + onMarkerIncomplete: ( + transitionName, + markerName, + startTime, + deletions, + ) => { + Scheduler.unstable_yieldValue( + `onMarkerIncomplete(${transitionName}, ${markerName}, ${startTime}, [${stringifyDeletions( + deletions, + )}])`, + ); + }, + onMarkerComplete: (transitioName, markerName, startTime, endTime) => { + Scheduler.unstable_yieldValue( + `onMarkerComplete(${transitioName}, ${markerName}, ${startTime}, ${endTime})`, + ); + }, + }; + + function App({navigate, markerName}) { + return ( +
+ {navigate ? ( + + }> + + + + ) : ( + + )} +
+ ); + } + + const root = ReactNoop.createRoot({ + unstable_transitionCallbacks: transitionCallbacks, + }); + await act(async () => { + root.render(); + ReactNoop.expire(1000); + await advanceTimers(1000); + expect(Scheduler).toFlushAndYield(['Page One']); + + startTransition( + () => root.render(), + { + name: 'transition one', + }, + ); + ReactNoop.expire(1000); + await advanceTimers(1000); + + expect(Scheduler).toFlushAndYield([ + 'Suspend [Page Two]', + 'Loading...', + 'onTransitionStart(transition one, 1000)', + 'onMarkerProgress(transition one, marker one, 1000, 2000, [])', + 'onTransitionProgress(transition one, 1000, 2000, [])', + ]); + + root.render(); + ReactNoop.expire(1000); + await advanceTimers(1000); + expect(() => + expect(Scheduler).toFlushAndYield([ + 'Suspend [Page Two]', + 'Loading...', + 'onMarkerIncomplete(transition one, marker one, 1000, [{endTime: 3000, name: marker one, newName: marker two, type: marker}])', + ]), + ).toErrorDev(''); + + resolveText('Page Two'); + ReactNoop.expire(1000); + await advanceTimers(1000); + expect(Scheduler).toFlushAndYield([ + 'Page Two', + 'onMarkerProgress(transition one, marker one, 1000, 4000, [])', + 'onTransitionProgress(transition one, 1000, 4000, [])', + 'onTransitionComplete(transition one, 1000, 4000)', + ]); + }); + }); + + // @gate enableTransitionTracing + it('marker incomplete for tree with parent and sibling tracing markers', 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})`, + ); + }, + onMarkerProgress: ( + transitioName, + markerName, + startTime, + currentTime, + pending, + ) => { + const suspenseNames = pending.map(p => p.name || '').join(', '); + Scheduler.unstable_yieldValue( + `onMarkerProgress(${transitioName}, ${markerName}, ${startTime}, ${currentTime}, [${suspenseNames}])`, + ); + }, + onMarkerIncomplete: ( + transitionName, + markerName, + startTime, + deletions, + ) => { + Scheduler.unstable_yieldValue( + `onMarkerIncomplete(${transitionName}, ${markerName}, ${startTime}, [${stringifyDeletions( + deletions, + )}])`, + ); + }, + onMarkerComplete: (transitioName, markerName, startTime, endTime) => { + Scheduler.unstable_yieldValue( + `onMarkerComplete(${transitioName}, ${markerName}, ${startTime}, ${endTime})`, + ); + }, + }; + + function App({navigate, showMarker}) { + return ( +
+ {navigate ? ( + + {showMarker ? ( + + }> + + + + ) : ( + }> + + + )} + + }> + + + + + ) : ( + + )} +
+ ); + } + + const root = ReactNoop.createRoot({ + unstable_transitionCallbacks: transitionCallbacks, + }); + await act(async () => { + root.render(); + ReactNoop.expire(1000); + await advanceTimers(1000); + expect(Scheduler).toFlushAndYield(['Page One']); + + startTransition( + () => root.render(), + { + name: 'transition one', + }, + ); + ReactNoop.expire(1000); + await advanceTimers(1000); + expect(Scheduler).toFlushAndYield([ + 'Suspend [Page Two]', + 'Loading...', + 'Suspend [Sibling Text]', + 'Sibling Loading...', + 'onTransitionStart(transition one, 1000)', + 'onMarkerProgress(transition one, parent, 1000, 2000, [suspense page, suspense sibling])', + 'onMarkerProgress(transition one, marker one, 1000, 2000, [suspense page])', + 'onMarkerProgress(transition one, sibling, 1000, 2000, [suspense sibling])', + 'onTransitionProgress(transition one, 1000, 2000, [suspense page, suspense sibling])', + ]); + root.render(); + + ReactNoop.expire(1000); + await advanceTimers(1000); + expect(Scheduler).toFlushAndYield([ + 'Suspend [Page Two]', + 'Loading...', + 'Suspend [Sibling Text]', + 'Sibling Loading...', + 'onMarkerIncomplete(transition one, marker one, 1000, [{endTime: 3000, name: marker one, type: marker}])', + 'onMarkerIncomplete(transition one, parent, 1000, [{endTime: 3000, name: marker one, type: marker}])', + ]); + + root.render(); + ReactNoop.expire(1000); + await advanceTimers(1000); + expect(Scheduler).toFlushAndYield([ + 'Suspend [Page Two]', + 'Loading...', + 'Suspend [Sibling Text]', + 'Sibling Loading...', + ]); + }); + + resolveText('Page Two'); + ReactNoop.expire(1000); + await advanceTimers(1000); + expect(Scheduler).toFlushAndYield(['Page Two']); + + resolveText('Sibling Text'); + ReactNoop.expire(1000); + await advanceTimers(1000); + expect(Scheduler).toFlushAndYield([ + 'Sibling Text', + 'onMarkerProgress(transition one, parent, 1000, 6000, [])', + 'onMarkerProgress(transition one, sibling, 1000, 6000, [])', + // Calls markerComplete and transitionComplete for all parents + 'onMarkerComplete(transition one, sibling, 1000, 6000)', + 'onTransitionProgress(transition one, 1000, 6000, [])', + ]); + }); + + // @gate enableTransitionTracing + it('marker gets deleted', 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})`, + ); + }, + onMarkerProgress: ( + transitioName, + markerName, + startTime, + currentTime, + pending, + ) => { + const suspenseNames = pending.map(p => p.name || '').join(', '); + Scheduler.unstable_yieldValue( + `onMarkerProgress(${transitioName}, ${markerName}, ${startTime}, ${currentTime}, [${suspenseNames}])`, + ); + }, + onMarkerIncomplete: ( + transitionName, + markerName, + startTime, + deletions, + ) => { + Scheduler.unstable_yieldValue( + `onMarkerIncomplete(${transitionName}, ${markerName}, ${startTime}, [${stringifyDeletions( + deletions, + )}])`, + ); + }, + onMarkerComplete: (transitioName, markerName, startTime, endTime) => { + Scheduler.unstable_yieldValue( + `onMarkerComplete(${transitioName}, ${markerName}, ${startTime}, ${endTime})`, + ); + }, + }; + + function App({navigate, deleteOne}) { + return ( +
+ {navigate ? ( + + {!deleteOne ? ( +
+ + }> + + + +
+ ) : null} + + }> + + + +
+ ) : ( + + )} +
+ ); + } + const root = ReactNoop.createRoot({ + unstable_transitionCallbacks: transitionCallbacks, + }); + await act(async () => { + root.render(); + ReactNoop.expire(1000); + await advanceTimers(1000); + expect(Scheduler).toFlushAndYield(['Page One']); + + startTransition( + () => root.render(), + { + name: 'transition', + }, + ); + ReactNoop.expire(1000); + await advanceTimers(1000); + expect(Scheduler).toFlushAndYield([ + 'Suspend [Page One]', + 'Loading One...', + 'Suspend [Page Two]', + 'Loading Two...', + 'onTransitionStart(transition, 1000)', + 'onMarkerProgress(transition, parent, 1000, 2000, [suspense one, suspense two])', + 'onMarkerProgress(transition, one, 1000, 2000, [suspense one])', + 'onMarkerProgress(transition, two, 1000, 2000, [suspense two])', + 'onTransitionProgress(transition, 1000, 2000, [suspense one, suspense two])', + ]); + + root.render(); + ReactNoop.expire(1000); + await advanceTimers(1000); + expect(Scheduler).toFlushAndYield([ + 'Suspend [Page Two]', + 'Loading Two...', + 'onMarkerIncomplete(transition, one, 1000, [{endTime: 3000, name: one, type: marker}])', + 'onMarkerIncomplete(transition, parent, 1000, [{endTime: 3000, name: one, type: marker}])', + ]); + + await resolveText('Page Two'); + ReactNoop.expire(1000); + await advanceTimers(1000); + expect(Scheduler).toFlushAndYield([ + 'Page Two', + // Marker progress will still get called after incomplete but not marker complete + 'onMarkerProgress(transition, parent, 1000, 4000, [])', + 'onMarkerProgress(transition, two, 1000, 4000, [])', + 'onMarkerComplete(transition, two, 1000, 4000)', + // Transition progress will still get called after incomplete but not transition complete + 'onTransitionProgress(transition, 1000, 4000, [])', + ]); + }); + }); + // @gate enableTransitionTracing it('warns when marker name changes', async () => { const transitionCallbacks = { @@ -1296,6 +1711,18 @@ describe('ReactInteractionTracing', () => { `onTransitionComplete(${name}, ${startTime}, ${endTime})`, ); }, + onMarkerIncomplete: ( + transitionName, + markerName, + startTime, + deletions, + ) => { + Scheduler.unstable_yieldValue( + `onMarkerIncomplete(${transitionName}, ${markerName}, ${startTime}, [${stringifyDeletions( + deletions, + )}])`, + ); + }, onMarkerComplete: (transitioName, markerName, startTime, endTime) => { Scheduler.unstable_yieldValue( `onMarkerComplete(${transitioName}, ${markerName}, ${startTime}, ${endTime})`,