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);
+ });
});
});
});