diff --git a/packages/react/src/__tests__/ReactProfiler-test.internal.js b/packages/react/src/__tests__/ReactProfiler-test.internal.js index b1237a4e57c55..073c5053ada78 100644 --- a/packages/react/src/__tests__/ReactProfiler-test.internal.js +++ b/packages/react/src/__tests__/ReactProfiler-test.internal.js @@ -46,7 +46,9 @@ function loadModules({ if (useNoopRenderer) { ReactNoop = require('react-noop-renderer'); + ReactTestRenderer = null; } else { + ReactNoop = null; ReactTestRenderer = require('react-test-renderer'); ReactTestRenderer.unstable_setNowImplementation(mockNow); } @@ -1229,12 +1231,13 @@ describe('Profiler', () => { const getWorkForReactThreads = mockFn => mockFn.mock.calls.filter(([interactions, threadID]) => threadID > 0); - beforeEach(() => { + function loadModulesForTracing(params) { jest.resetModules(); loadModules({ - enableSchedulerTracing: true, enableSuspense: true, + enableSchedulerTracing: true, + ...params, }); throwInOnInteractionScheduledWorkCompleted = false; @@ -1274,7 +1277,9 @@ describe('Profiler', () => { onWorkStarted, onWorkStopped, }); - }); + } + + beforeEach(() => loadModulesForTracing()); describe('error handling', () => { it('should cover errors thrown in onWorkScheduled', () => { @@ -2144,24 +2149,12 @@ describe('Profiler', () => { ]); }); - it('traces both the temporary placeholder and the finished render for an interaction', async () => { - jest.resetModules(); - - loadModules({ - useNoopRenderer: true, - enableSuspense: true, - enableSchedulerTracing: true, - }); - - // Re-register since we've reloaded modules - SchedulerTracing.unstable_subscribe({ - onInteractionScheduledWorkCompleted, - onInteractionTraced, - onWorkCanceled, - onWorkScheduled, - onWorkStarted, - onWorkStopped, - }); + describe('suspense', () => { + let AsyncText; + let Text; + let TextResource; + let cache; + let resourcePromise; function awaitableAdvanceTimers(ms) { jest.advanceTimersByTime(ms); @@ -2171,423 +2164,315 @@ describe('Profiler', () => { }); } - const SimpleCacheProvider = require('simple-cache-provider'); - let cache; - function invalidateCache() { - cache = SimpleCacheProvider.createCache(invalidateCache); - } - invalidateCache(); - const TextResource = SimpleCacheProvider.createResource( - ([text, ms = 0]) => { - return new Promise((resolve, reject) => { - setTimeout(() => { - ReactNoop.yield(`Promise resolved [${text}]`); - resolve(text); - }, ms); - }); - }, - ([text, ms]) => text, - ); - - function Text(props) { - ReactNoop.yield(props.text); - return ; - } - - function span(prop) { - return {type: 'span', children: [], prop}; - } - - function AsyncText(props) { - const text = props.text; - try { - TextResource.read(cache, [props.text, props.ms]); - ReactNoop.yield(text); - return ; - } catch (promise) { - if (typeof promise.then === 'function') { - ReactNoop.yield(`Suspend! [${text}]`); - } else { - ReactNoop.yield(`Error! [${text}]`); - } - throw promise; + function yieldForRenderer(value) { + if (ReactNoop) { + ReactNoop.yield(value); + } else { + ReactTestRenderer.unstable_yield(value); } } - const interaction = { - id: 0, - name: 'initial render', - timestamp: mockNow(), - }; - - const onRender = jest.fn(); - SchedulerTracing.unstable_trace(interaction.name, mockNow(), () => { - ReactNoop.render( - - }> - - - - , - ); - }); - - expect(onInteractionTraced).toHaveBeenCalledTimes(1); - expect(onInteractionTraced).toHaveBeenLastNotifiedOfInteraction( - interaction, - ); - expect(onInteractionScheduledWorkCompleted).not.toHaveBeenCalled(); - expect(getWorkForReactThreads(onWorkStarted)).toHaveLength(0); - expect(getWorkForReactThreads(onWorkStopped)).toHaveLength(0); - - expect(ReactNoop.flush()).toEqual([ - 'Suspend! [Async]', - 'Loading...', - 'Sync', - ]); - // The update hasn't expired yet, so we commit nothing. - expect(ReactNoop.getChildren()).toEqual([]); - expect(onRender).not.toHaveBeenCalled(); - - // Advance both React's virtual time and Jest's timers by enough to expire - // the update, but not by enough to flush the suspending promise. - ReactNoop.expire(10000); - await awaitableAdvanceTimers(10000); - // No additional rendering work is required, since we already prepared - // the placeholder. - expect(ReactNoop.flushExpired()).toEqual([]); - // Should have committed the placeholder. - expect(ReactNoop.getChildren()).toEqual([ - span('Loading...'), - span('Sync'), - ]); - expect(onRender).toHaveBeenCalledTimes(1); - - let call = onRender.mock.calls[0]; - expect(call[0]).toEqual('test-profiler'); - expect(call[6]).toMatchInteractions( - ReactFeatureFlags.enableSchedulerTracing ? [interaction] : [], - ); - - expect(onInteractionTraced).toHaveBeenCalledTimes(1); - expect(onInteractionScheduledWorkCompleted).not.toHaveBeenCalled(); - - // Once the promise resolves, we render the suspended view - await awaitableAdvanceTimers(10000); - expect(ReactNoop.flush()).toEqual(['Promise resolved [Async]', 'Async']); - expect(ReactNoop.getChildren()).toEqual([span('Async'), span('Sync')]); - expect(onRender).toHaveBeenCalledTimes(2); - - call = onRender.mock.calls[1]; - expect(call[0]).toEqual('test-profiler'); - expect(call[6]).toMatchInteractions( - ReactFeatureFlags.enableSchedulerTracing ? [interaction] : [], - ); - - expect(onInteractionTraced).toHaveBeenCalledTimes(1); - expect(onInteractionScheduledWorkCompleted).toHaveBeenCalledTimes(1); - expect( - onInteractionScheduledWorkCompleted, - ).toHaveBeenLastNotifiedOfInteraction(interaction); - }); + beforeEach(() => { + const SimpleCacheProvider = require('simple-cache-provider'); + function invalidateCache() { + cache = SimpleCacheProvider.createCache(invalidateCache); + } + invalidateCache(); - it('does not prematurely complete for suspended sync renders', async () => { - const SimpleCacheProvider = require('simple-cache-provider'); - let cache; - function invalidateCache() { - cache = SimpleCacheProvider.createCache(invalidateCache); - } - invalidateCache(); + resourcePromise = null; - let resourcePromise; - const TextResource = SimpleCacheProvider.createResource( - ([text, ms = 0]) => { + TextResource = SimpleCacheProvider.createResource(([text, ms = 0]) => { resourcePromise = new Promise((resolve, reject) => - setTimeout(() => resolve(text), ms), + setTimeout(() => { + yieldForRenderer(`Promise resolved [${text}]`); + resolve(text); + }, ms), ); return resourcePromise; - }, - ([text, ms]) => text, - ); + }, ([text, ms]) => text); + + AsyncText = ({ms, text}) => { + try { + TextResource.read(cache, [text, ms]); + yieldForRenderer(`AsyncText [${text}]`); + return text; + } catch (promise) { + if (typeof promise.then === 'function') { + yieldForRenderer(`Suspend! [${text}]`); + } else { + yieldForRenderer(`Error! [${text}]`); + } + throw promise; + } + }; - function AsyncText({ms, text}) { - TextResource.read(cache, [text, ms]); - return ; - } + Text = ({text}) => { + yieldForRenderer(`Text [${text}]`); + return text; + }; + }); - function Text({text}) { - return text; - } + it('traces both the temporary placeholder and the finished render for an interaction', async () => { + loadModulesForTracing({useNoopRenderer: true}); - const interaction = { - id: 0, - name: 'initial render', - timestamp: mockNow(), - }; + const interaction = { + id: 0, + name: 'initial render', + timestamp: mockNow(), + }; - const onRender = jest.fn(); - SchedulerTracing.unstable_trace( - interaction.name, - interaction.timestamp, - () => { - ReactTestRenderer.create( - - }> - + const onRender = jest.fn(); + SchedulerTracing.unstable_trace(interaction.name, mockNow(), () => { + ReactNoop.render( + + }> + + , ); - }, - ); - - expect(onInteractionScheduledWorkCompleted).not.toHaveBeenCalled(); - - jest.runAllTimers(); - await resourcePromise; - - expect(onInteractionScheduledWorkCompleted).toHaveBeenCalledTimes(1); - expect( - onInteractionScheduledWorkCompleted, - ).toHaveBeenLastNotifiedOfInteraction(interaction); - }); - - it('tracks cascading work after suspended sync renders', async () => { - const SimpleCacheProvider = require('simple-cache-provider'); - let cache; - function invalidateCache() { - cache = SimpleCacheProvider.createCache(invalidateCache); - } - invalidateCache(); + }); - let resourcePromise; - const TextResource = SimpleCacheProvider.createResource( - ([text, ms = 0]) => { - resourcePromise = new Promise((resolve, reject) => - setTimeout(() => resolve(text), ms), - ); - return resourcePromise; - }, - ([text, ms]) => text, - ); + expect(onInteractionTraced).toHaveBeenCalledTimes(1); + expect(onInteractionTraced).toHaveBeenLastNotifiedOfInteraction( + interaction, + ); + expect(onInteractionScheduledWorkCompleted).not.toHaveBeenCalled(); + expect(getWorkForReactThreads(onWorkStarted)).toHaveLength(0); + expect(getWorkForReactThreads(onWorkStopped)).toHaveLength(0); - let wrappedCascadingFn; - class AsyncComponentWithCascadingWork extends React.Component { - state = { - hasMounted: false, - }; + expect(ReactNoop.flush()).toEqual([ + 'Suspend! [Async]', + 'Text [Loading...]', + 'Text [Sync]', + ]); + // The update hasn't expired yet, so we commit nothing. + expect(ReactNoop.getChildren()).toEqual([]); + expect(onRender).not.toHaveBeenCalled(); - componentDidMount() { - wrappedCascadingFn = SchedulerTracing.unstable_wrap(() => { - this.setState({hasMounted: true}); - }); - } + // Advance both React's virtual time and Jest's timers by enough to expire + // the update, but not by enough to flush the suspending promise. + ReactNoop.expire(10000); + await awaitableAdvanceTimers(10000); + // No additional rendering work is required, since we already prepared + // the placeholder. + expect(ReactNoop.flushExpired()).toEqual([]); + // Should have committed the placeholder. + expect(ReactNoop.getChildren()).toEqual([ + {text: 'Loading...'}, + {text: 'Sync'}, + ]); + expect(onRender).toHaveBeenCalledTimes(1); - render() { - const {ms, text} = this.props; - TextResource.read(cache, [text, ms]); - return {this.state.hasMounted}; - } - } + let call = onRender.mock.calls[0]; + expect(call[0]).toEqual('test-profiler'); + expect(call[6]).toMatchInteractions( + ReactFeatureFlags.enableSchedulerTracing ? [interaction] : [], + ); - function Text({text}) { - return text; - } + expect(onInteractionTraced).toHaveBeenCalledTimes(1); + expect(onInteractionScheduledWorkCompleted).not.toHaveBeenCalled(); - const interaction = { - id: 0, - name: 'initial render', - timestamp: mockNow(), - }; + // Once the promise resolves, we render the suspended view + await awaitableAdvanceTimers(10000); + expect(ReactNoop.flush()).toEqual([ + 'Promise resolved [Async]', + 'AsyncText [Async]', + ]); + expect(ReactNoop.getChildren()).toEqual([ + {text: 'Async'}, + {text: 'Sync'}, + ]); + expect(onRender).toHaveBeenCalledTimes(2); - const onRender = jest.fn(); - SchedulerTracing.unstable_trace( - interaction.name, - interaction.timestamp, - () => { - ReactTestRenderer.create( - - }> - - - , - ); - }, - ); + call = onRender.mock.calls[1]; + expect(call[0]).toEqual('test-profiler'); + expect(call[6]).toMatchInteractions( + ReactFeatureFlags.enableSchedulerTracing ? [interaction] : [], + ); - expect(onInteractionScheduledWorkCompleted).not.toHaveBeenCalled(); + expect(onInteractionTraced).toHaveBeenCalledTimes(1); + expect(onInteractionScheduledWorkCompleted).toHaveBeenCalledTimes(1); + expect( + onInteractionScheduledWorkCompleted, + ).toHaveBeenLastNotifiedOfInteraction(interaction); + }); - jest.runAllTimers(); - await resourcePromise; + it('does not prematurely complete for suspended sync renders', async () => { + const interaction = { + id: 0, + name: 'initial render', + timestamp: mockNow(), + }; - expect(onInteractionScheduledWorkCompleted).not.toHaveBeenCalled(); + const onRender = jest.fn(); + SchedulerTracing.unstable_trace( + interaction.name, + interaction.timestamp, + () => { + ReactTestRenderer.create( + + }> + + + , + ); + }, + ); - wrappedCascadingFn(); + expect(onInteractionScheduledWorkCompleted).not.toHaveBeenCalled(); - expect(onInteractionScheduledWorkCompleted).toHaveBeenCalledTimes(1); - expect( - onInteractionScheduledWorkCompleted, - ).toHaveBeenLastNotifiedOfInteraction(interaction); - }); + jest.runAllTimers(); + await resourcePromise; - it('does not prematurely complete for suspended renders that have exceeded their deadline', async () => { - function awaitableAdvanceTimers(ms) { - jest.advanceTimersByTime(ms); - // Wait until the end of the current tick - return new Promise(resolve => { - setImmediate(resolve); - }); - } + expect(onInteractionScheduledWorkCompleted).toHaveBeenCalledTimes(1); + expect( + onInteractionScheduledWorkCompleted, + ).toHaveBeenLastNotifiedOfInteraction(interaction); + }); - const SimpleCacheProvider = require('simple-cache-provider'); - let cache; - function invalidateCache() { - cache = SimpleCacheProvider.createCache(invalidateCache); - } - invalidateCache(); + it('traces cascading work after suspended sync renders', async () => { + let wrappedCascadingFn; + class AsyncComponentWithCascadingWork extends React.Component { + state = { + hasMounted: false, + }; - const TextResource = SimpleCacheProvider.createResource( - ([text, ms = 0]) => { - return new Promise((resolve, reject) => { - setTimeout(() => { - ReactTestRenderer.unstable_yield(`Promise resolved [${text}]`); - resolve(text); - }, ms); - }); - }, - ([text, ms]) => text, - ); + componentDidMount() { + wrappedCascadingFn = SchedulerTracing.unstable_wrap(() => { + this.setState({hasMounted: true}); + }); + } - function AsyncText({ms, text}) { - try { - TextResource.read(cache, [text, ms]); - ReactTestRenderer.unstable_yield(`AsyncText [${text}]`); - return ; - } catch (promise) { - if (typeof promise.then === 'function') { - ReactTestRenderer.unstable_yield(`Suspend! [${text}]`); - } else { - ReactTestRenderer.unstable_yield(`Error! [${text}]`); + render() { + const {ms, text} = this.props; + TextResource.read(cache, [text, ms]); + return {this.state.hasMounted}; } - throw promise; } - } - function Text({text}) { - ReactTestRenderer.unstable_yield(`Text [${text}]`); - return text; - } + const interaction = { + id: 0, + name: 'initial render', + timestamp: mockNow(), + }; - const interaction = { - id: 0, - name: 'initial render', - timestamp: mockNow(), - }; + const onRender = jest.fn(); + SchedulerTracing.unstable_trace( + interaction.name, + interaction.timestamp, + () => { + ReactTestRenderer.create( + + }> + + + , + ); + }, + ); - const onRender = jest.fn(); - let renderer; - SchedulerTracing.unstable_trace( - interaction.name, - interaction.timestamp, - () => { - renderer = ReactTestRenderer.create( - - }> - - - , - { - unstable_isAsync: true, - }, - ); - }, - ); + expect(onInteractionScheduledWorkCompleted).not.toHaveBeenCalled(); - advanceTimeBy(1500); - await awaitableAdvanceTimers(1500); + jest.runAllTimers(); + await resourcePromise; - expect(renderer).toFlushAll(['Suspend! [loaded]', 'Text [loading]']); - expect(onInteractionScheduledWorkCompleted).not.toHaveBeenCalled(); + expect(onInteractionScheduledWorkCompleted).not.toHaveBeenCalled(); - advanceTimeBy(2500); - await awaitableAdvanceTimers(2500); + wrappedCascadingFn(); - expect(renderer).toFlushAll(['AsyncText [loaded]']); - expect(onInteractionScheduledWorkCompleted).toHaveBeenCalledTimes(1); - expect( - onInteractionScheduledWorkCompleted, - ).toHaveBeenLastNotifiedOfInteraction(interaction); - }); + expect(onInteractionScheduledWorkCompleted).toHaveBeenCalledTimes(1); + expect( + onInteractionScheduledWorkCompleted, + ).toHaveBeenLastNotifiedOfInteraction(interaction); + }); - it('decrements interaction count correctly if suspense loads before placeholder is shown', async () => { - const SimpleCacheProvider = require('simple-cache-provider'); - let cache; - function invalidateCache() { - cache = SimpleCacheProvider.createCache(invalidateCache); - } - invalidateCache(); + it('does not prematurely complete for suspended renders that have exceeded their deadline', async () => { + const interaction = { + id: 0, + name: 'initial render', + timestamp: mockNow(), + }; - let resourcePromise; - const TextResource = SimpleCacheProvider.createResource( - ([text, ms = 0]) => { - resourcePromise = new Promise((resolve, reject) => - setTimeout(() => resolve(text), ms), - ); - return resourcePromise; - }, - ([text, ms]) => text, - ); + const onRender = jest.fn(); + let renderer; + SchedulerTracing.unstable_trace( + interaction.name, + interaction.timestamp, + () => { + renderer = ReactTestRenderer.create( + + }> + + + , + { + unstable_isAsync: true, + }, + ); + }, + ); - function AsyncText({ms, text}) { - TextResource.read(cache, [text, ms]); - return ; - } + advanceTimeBy(1500); + await awaitableAdvanceTimers(1500); - function Text({text}) { - return text; - } + expect(renderer).toFlushAll(['Suspend! [loaded]', 'Text [loading]']); + expect(onInteractionScheduledWorkCompleted).not.toHaveBeenCalled(); - const interaction = { - id: 0, - name: 'initial render', - timestamp: mockNow(), - }; + advanceTimeBy(2500); + await awaitableAdvanceTimers(2500); - const onRender = jest.fn(); - let renderer; - SchedulerTracing.unstable_trace( - interaction.name, - interaction.timestamp, - () => { - renderer = ReactTestRenderer.create( - - }> - - - , - {unstable_isAsync: true}, - ); - }, - ); - renderer.unstable_flushAll(); + expect(renderer).toFlushAll(['AsyncText [loaded]']); + expect(onInteractionScheduledWorkCompleted).toHaveBeenCalledTimes(1); + expect( + onInteractionScheduledWorkCompleted, + ).toHaveBeenLastNotifiedOfInteraction(interaction); + }); - expect(onInteractionScheduledWorkCompleted).not.toHaveBeenCalled(); + it('decrements interaction count correctly if suspense loads before placeholder is shown', async () => { + const interaction = { + id: 0, + name: 'initial render', + timestamp: mockNow(), + }; - jest.advanceTimersByTime(1000); - await resourcePromise; - renderer.unstable_flushAll(); + const onRender = jest.fn(); + let renderer; + SchedulerTracing.unstable_trace( + interaction.name, + interaction.timestamp, + () => { + renderer = ReactTestRenderer.create( + + }> + + + , + {unstable_isAsync: true}, + ); + }, + ); + renderer.unstable_flushAll(); - expect(onInteractionScheduledWorkCompleted).toHaveBeenCalledTimes(1); - expect( - onInteractionScheduledWorkCompleted, - ).toHaveBeenLastNotifiedOfInteraction(interaction); + expect(onInteractionScheduledWorkCompleted).not.toHaveBeenCalled(); + + jest.advanceTimersByTime(1000); + await resourcePromise; + renderer.unstable_flushAll(); + + expect(onInteractionScheduledWorkCompleted).toHaveBeenCalledTimes(1); + expect( + onInteractionScheduledWorkCompleted, + ).toHaveBeenLastNotifiedOfInteraction(interaction); + }); }); }); });