'use strict';
let React;
let ReactFeatureFlags;
let ReactNoop;
let Scheduler;
let ReactTestRenderer;
let act;
let AdvanceTime;
let assertLog;
let waitFor;
let waitForAll;
let waitForThrow;
function loadModules({
enableProfilerTimer = true,
enableProfilerCommitHooks = true,
enableProfilerNestedUpdatePhase = true,
enableProfilerNestedUpdateScheduledHook = false,
replayFailedUnitOfWorkWithInvokeGuardedCallback = false,
useNoopRenderer = false,
} = {}) {
ReactFeatureFlags = require('shared/ReactFeatureFlags');
ReactFeatureFlags.enableProfilerTimer = enableProfilerTimer;
ReactFeatureFlags.enableProfilerCommitHooks = enableProfilerCommitHooks;
ReactFeatureFlags.enableProfilerNestedUpdatePhase =
enableProfilerNestedUpdatePhase;
ReactFeatureFlags.enableProfilerNestedUpdateScheduledHook =
enableProfilerNestedUpdateScheduledHook;
ReactFeatureFlags.replayFailedUnitOfWorkWithInvokeGuardedCallback =
replayFailedUnitOfWorkWithInvokeGuardedCallback;
React = require('react');
Scheduler = require('scheduler');
act = require('internal-test-utils').act;
if (useNoopRenderer) {
ReactNoop = require('react-noop-renderer');
ReactTestRenderer = null;
} else {
ReactNoop = null;
ReactTestRenderer = require('react-test-renderer');
}
const InternalTestUtils = require('internal-test-utils');
assertLog = InternalTestUtils.assertLog;
waitFor = InternalTestUtils.waitFor;
waitForAll = InternalTestUtils.waitForAll;
waitForThrow = InternalTestUtils.waitForThrow;
AdvanceTime = class extends React.Component {
static defaultProps = {
byAmount: 10,
shouldComponentUpdate: true,
};
shouldComponentUpdate(nextProps) {
return nextProps.shouldComponentUpdate;
}
render() {
Scheduler.unstable_advanceTime(this.props.byAmount);
return this.props.children || null;
}
};
}
describe('Profiler', () => {
describe('works in profiling and non-profiling bundles', () => {
[true, false].forEach(enableProfilerTimer => {
describe(`enableProfilerTimer:${
enableProfilerTimer ? 'enabled' : 'disabled'
}`, () => {
beforeEach(() => {
jest.resetModules();
loadModules({enableProfilerTimer});
});
if (__DEV__ && enableProfilerTimer) {
it('should warn if required params are missing', () => {
expect(() => {
ReactTestRenderer.create(<React.Profiler />);
}).toErrorDev(
'Profiler must specify an "id" of type `string` as a prop. Received the type `undefined` instead.',
{
withoutStack: true,
},
);
});
}
it('should support an empty Profiler (with no children)', () => {
expect(
ReactTestRenderer.create(
<React.Profiler id="label" onRender={jest.fn()} />,
).toJSON(),
).toMatchSnapshot();
expect(
ReactTestRenderer.create(
<div>
<React.Profiler id="label" onRender={jest.fn()} />
</div>,
).toJSON(),
).toMatchSnapshot();
});
it('should render children', () => {
const FunctionComponent = ({label}) => <span>{label}</span>;
const renderer = ReactTestRenderer.create(
<div>
<span>outside span</span>
<React.Profiler id="label" onRender={jest.fn()}>
<span>inside span</span>
<FunctionComponent label="function component" />
</React.Profiler>
</div>,
);
expect(renderer.toJSON()).toMatchSnapshot();
});
it('should support nested Profilers', () => {
const FunctionComponent = ({label}) => <div>{label}</div>;
class ClassComponent extends React.Component {
render() {
return <block>{this.props.label}</block>;
}
}
const renderer = ReactTestRenderer.create(
<React.Profiler id="outer" onRender={jest.fn()}>
<FunctionComponent label="outer function component" />
<React.Profiler id="inner" onRender={jest.fn()}>
<ClassComponent label="inner class component" />
<span>inner span</span>
</React.Profiler>
</React.Profiler>,
);
expect(renderer.toJSON()).toMatchSnapshot();
});
});
});
});
});
describe(`onRender`, () => {
beforeEach(() => {
jest.resetModules();
loadModules();
});
it('should handle errors thrown', () => {
const callback = jest.fn(id => {
if (id === 'throw') {
throw Error('expected');
}
});
let didMount = false;
class ClassComponent extends React.Component {
componentDidMount() {
didMount = true;
}
render() {
return this.props.children;
}
}
expect(() =>
ReactTestRenderer.create(
<ClassComponent>
<React.Profiler id="do-not-throw" onRender={callback}>
<React.Profiler id="throw" onRender={callback}>
<div />
</React.Profiler>
</React.Profiler>
</ClassComponent>,
),
).toThrow('expected');
expect(didMount).toBe(true);
expect(callback).toHaveBeenCalledTimes(2);
});
it('is not invoked until the commit phase', async () => {
const callback = jest.fn();
const Yield = ({value}) => {
Scheduler.log(value);
return null;
};
React.startTransition(() => {
ReactTestRenderer.create(
<React.Profiler id="test" onRender={callback}>
<Yield value="first" />
<Yield value="last" />
</React.Profiler>,
{
unstable_isConcurrent: true,
},
);
});
await waitFor(['first']);
expect(callback).toHaveBeenCalledTimes(0);
await waitForAll(['last']);
expect(callback).toHaveBeenCalledTimes(1);
});
it('does not record times for components outside of Profiler tree', () => {
jest.mock('scheduler', obj => {
const ActualScheduler = jest.requireActual('scheduler/unstable_mock');
return {
...ActualScheduler,
unstable_now: function mockUnstableNow() {
ActualScheduler.log('read current time');
return ActualScheduler.unstable_now();
},
};
});
jest.resetModules();
loadModules();
Scheduler.unstable_clearLog();
ReactTestRenderer.create(
<div>
<AdvanceTime />
<AdvanceTime />
<AdvanceTime />
<AdvanceTime />
<AdvanceTime />
</div>,
);
assertLog([
'read current time',
'read current time',
'read current time',
'read current time',
'read current time',
]);
jest.mock('scheduler', () => jest.requireActual('scheduler/unstable_mock'));
});
it('does not report work done on a sibling', async () => {
const callback = jest.fn();
const DoesNotUpdate = React.memo(
function DoesNotUpdateInner() {
Scheduler.unstable_advanceTime(10);
return null;
},
() => true,
);
let updateProfilerSibling;
function ProfilerSibling() {
const [count, setCount] = React.useState(0);
updateProfilerSibling = () => setCount(count + 1);
return null;
}
function App() {
return (
<React.Fragment>
<React.Profiler id="test" onRender={callback}>
<DoesNotUpdate />
</React.Profiler>
<ProfilerSibling />
</React.Fragment>
);
}
const renderer = ReactTestRenderer.create(<App />);
expect(callback).toHaveBeenCalledTimes(1);
let call = callback.mock.calls[0];
expect(call).toHaveLength(6);
expect(call[0]).toBe('test');
expect(call[1]).toBe('mount');
expect(call[2]).toBe(10);
expect(call[3]).toBe(10);
expect(call[4]).toBe(0);
expect(call[5]).toBe(10);
callback.mockReset();
Scheduler.unstable_advanceTime(20);
renderer.update(<App />);
if (gate(flags => flags.enableUseJSStackToTrackPassiveDurations)) {
expect(callback).not.toHaveBeenCalled();
} else {
expect(callback).toHaveBeenCalledTimes(1);
call = callback.mock.calls[0];
expect(call).toHaveLength(6);
expect(call[0]).toBe('test');
expect(call[1]).toBe('update');
expect(call[2]).toBe(0);
expect(call[3]).toBe(10);
expect(call[4]).toBe(30);
expect(call[5]).toBe(30);
callback.mockReset();
}
Scheduler.unstable_advanceTime(20);
await act(() => updateProfilerSibling());
expect(callback).not.toHaveBeenCalled();
});
it('logs render times for both mount and update', () => {
const callback = jest.fn();
Scheduler.unstable_advanceTime(5);
const renderer = ReactTestRenderer.create(
<React.Profiler id="test" onRender={callback}>
<AdvanceTime />
</React.Profiler>,
);
expect(callback).toHaveBeenCalledTimes(1);
let [call] = callback.mock.calls;
expect(call).toHaveLength(6);
expect(call[0]).toBe('test');
expect(call[1]).toBe('mount');
expect(call[2]).toBe(10);
expect(call[3]).toBe(10);
expect(call[4]).toBe(5);
expect(call[5]).toBe(15);
callback.mockReset();
Scheduler.unstable_advanceTime(20);
renderer.update(
<React.Profiler id="test" onRender={callback}>
<AdvanceTime />
</React.Profiler>,
);
expect(callback).toHaveBeenCalledTimes(1);
[call] = callback.mock.calls;
expect(call).toHaveLength(6);
expect(call[0]).toBe('test');
expect(call[1]).toBe('update');
expect(call[2]).toBe(10);
expect(call[3]).toBe(10);
expect(call[4]).toBe(35);
expect(call[5]).toBe(45);
callback.mockReset();
Scheduler.unstable_advanceTime(20);
renderer.update(
<React.Profiler id="test" onRender={callback}>
<AdvanceTime byAmount={4} />
</React.Profiler>,
);
expect(callback).toHaveBeenCalledTimes(1);
[call] = callback.mock.calls;
expect(call).toHaveLength(6);
expect(call[0]).toBe('test');
expect(call[1]).toBe('update');
expect(call[2]).toBe(4);
expect(call[3]).toBe(4);
expect(call[4]).toBe(65);
expect(call[5]).toBe(69);
});
it('includes render times of nested Profilers in their parent times', () => {
const callback = jest.fn();
Scheduler.unstable_advanceTime(5);
ReactTestRenderer.create(
<React.Fragment>
<React.Profiler id="parent" onRender={callback}>
<AdvanceTime byAmount={10}>
<React.Profiler id="child" onRender={callback}>
<AdvanceTime byAmount={20} />
</React.Profiler>
</AdvanceTime>
</React.Profiler>
</React.Fragment>,
);
expect(callback).toHaveBeenCalledTimes(2);
const [childCall, parentCall] = callback.mock.calls;
expect(childCall[0]).toBe('child');
expect(parentCall[0]).toBe('parent');
expect(childCall[2]).toBe(20);
expect(childCall[3]).toBe(20);
expect(childCall[4]).toBe(15);
expect(childCall[5]).toBe(35);
expect(parentCall[2]).toBe(30);
expect(parentCall[3]).toBe(30);
expect(parentCall[4]).toBe(5);
expect(parentCall[5]).toBe(35);
});
it('traces sibling Profilers separately', () => {
const callback = jest.fn();
Scheduler.unstable_advanceTime(5);
ReactTestRenderer.create(
<React.Fragment>
<React.Profiler id="first" onRender={callback}>
<AdvanceTime byAmount={20} />
</React.Profiler>
<React.Profiler id="second" onRender={callback}>
<AdvanceTime byAmount={5} />
</React.Profiler>
</React.Fragment>,
);
expect(callback).toHaveBeenCalledTimes(2);
const [firstCall, secondCall] = callback.mock.calls;
expect(firstCall[0]).toBe('first');
expect(secondCall[0]).toBe('second');
expect(firstCall[2]).toBe(20);
expect(firstCall[3]).toBe(20);
expect(firstCall[4]).toBe(5);
expect(firstCall[5]).toBe(30);
expect(secondCall[2]).toBe(5);
expect(secondCall[3]).toBe(5);
expect(secondCall[4]).toBe(25);
expect(secondCall[5]).toBe(30);
});
it('does not include time spent outside of profile root', () => {
const callback = jest.fn();
Scheduler.unstable_advanceTime(5);
ReactTestRenderer.create(
<React.Fragment>
<AdvanceTime byAmount={20} />
<React.Profiler id="test" onRender={callback}>
<AdvanceTime byAmount={5} />
</React.Profiler>
<AdvanceTime byAmount={20} />
</React.Fragment>,
);
expect(callback).toHaveBeenCalledTimes(1);
const [call] = callback.mock.calls;
expect(call[0]).toBe('test');
expect(call[2]).toBe(5);
expect(call[3]).toBe(5);
expect(call[4]).toBe(25);
expect(call[5]).toBe(50);
});
it('is not called when blocked by sCU false', () => {
const callback = jest.fn();
let instance;
class Updater extends React.Component {
state = {};
render() {
instance = this;
return this.props.children;
}
}
const renderer = ReactTestRenderer.create(
<React.Profiler id="outer" onRender={callback}>
<Updater>
<React.Profiler id="inner" onRender={callback}>
<div />
</React.Profiler>
</Updater>
</React.Profiler>,
);
expect(callback).toHaveBeenCalledTimes(2);
callback.mockReset();
renderer.unstable_flushSync(() => {
instance.setState({
count: 1,
});
});
expect(callback).toHaveBeenCalledTimes(1);
expect(callback.mock.calls[0][0]).toBe('outer');
});
it('decreases actual time but not base time when sCU prevents an update', () => {
const callback = jest.fn();
Scheduler.unstable_advanceTime(5);
const renderer = ReactTestRenderer.create(
<React.Profiler id="test" onRender={callback}>
<AdvanceTime byAmount={10}>
<AdvanceTime byAmount={13} shouldComponentUpdate={false} />
</AdvanceTime>
</React.Profiler>,
);
expect(callback).toHaveBeenCalledTimes(1);
Scheduler.unstable_advanceTime(30);
renderer.update(
<React.Profiler id="test" onRender={callback}>
<AdvanceTime byAmount={4}>
<AdvanceTime byAmount={7} shouldComponentUpdate={false} />
</AdvanceTime>
</React.Profiler>,
);
expect(callback).toHaveBeenCalledTimes(2);
const [mountCall, updateCall] = callback.mock.calls;
expect(mountCall[1]).toBe('mount');
expect(mountCall[2]).toBe(23);
expect(mountCall[3]).toBe(23);
expect(mountCall[4]).toBe(5);
expect(mountCall[5]).toBe(28);
expect(updateCall[1]).toBe('update');
expect(updateCall[2]).toBe(4);
expect(updateCall[3]).toBe(17);
expect(updateCall[4]).toBe(58);
expect(updateCall[5]).toBe(62);
});
it('includes time spent in render phase lifecycles', () => {
class WithLifecycles extends React.Component {
state = {};
static getDerivedStateFromProps() {
Scheduler.unstable_advanceTime(3);
return null;
}
shouldComponentUpdate() {
Scheduler.unstable_advanceTime(7);
return true;
}
render() {
Scheduler.unstable_advanceTime(5);
return null;
}
}
const callback = jest.fn();
Scheduler.unstable_advanceTime(5);
const renderer = ReactTestRenderer.create(
<React.Profiler id="test" onRender={callback}>
<WithLifecycles />
</React.Profiler>,
);
Scheduler.unstable_advanceTime(15);
renderer.update(
<React.Profiler id="test" onRender={callback}>
<WithLifecycles />
</React.Profiler>,
);
expect(callback).toHaveBeenCalledTimes(2);
const [mountCall, updateCall] = callback.mock.calls;
expect(mountCall[1]).toBe('mount');
expect(mountCall[2]).toBe(8);
expect(mountCall[3]).toBe(8);
expect(mountCall[4]).toBe(5);
expect(mountCall[5]).toBe(13);
expect(updateCall[1]).toBe('update');
expect(updateCall[2]).toBe(15);
expect(updateCall[3]).toBe(15);
expect(updateCall[4]).toBe(28);
expect(updateCall[5]).toBe(43);
});
it('should clear nested-update flag when multiple cascading renders are scheduled', async () => {
loadModules({
useNoopRenderer: true,
});
function Component() {
const [didMount, setDidMount] = React.useState(false);
const [didMountAndUpdate, setDidMountAndUpdate] = React.useState(false);
React.useLayoutEffect(() => {
setDidMount(true);
}, []);
React.useEffect(() => {
if (didMount && !didMountAndUpdate) {
setDidMountAndUpdate(true);
}
}, [didMount, didMountAndUpdate]);
Scheduler.log(`${didMount}:${didMountAndUpdate}`);
return null;
}
const onRender = jest.fn();
await act(() => {
ReactNoop.render(
<React.Profiler id="root" onRender={onRender}>
<Component />
</React.Profiler>,
);
});
assertLog(['false:false', 'true:false', 'true:true']);
expect(onRender).toHaveBeenCalledTimes(3);
expect(onRender.mock.calls[0][1]).toBe('mount');
expect(onRender.mock.calls[1][1]).toBe('nested-update');
expect(onRender.mock.calls[2][1]).toBe('update');
});
it('is properly distinguish updates and nested-updates when there is more than sync remaining work', () => {
loadModules({
useNoopRenderer: true,
});
function Component() {
const [didMount, setDidMount] = React.useState(false);
React.useLayoutEffect(() => {
setDidMount(true);
}, []);
Scheduler.log(didMount);
return didMount;
}
const onRender = jest.fn();
React.startTransition(() =>
ReactNoop.render(
<React.Profiler id="root" onRender={onRender}>
<Component />
</React.Profiler>,
),
);
ReactNoop.flushSync(() => {
ReactNoop.render(
<React.Profiler id="root" onRender={onRender}>
<Component />
</React.Profiler>,
);
});
assertLog([false, true]);
expect(onRender).toHaveBeenCalledTimes(2);
expect(onRender.mock.calls[0][1]).toBe('mount');
expect(onRender.mock.calls[1][1]).toBe('nested-update');
});
describe('with regard to interruptions', () => {
it('should accumulate actual time after a scheduling interruptions', async () => {
const callback = jest.fn();
const Yield = ({renderTime}) => {
Scheduler.unstable_advanceTime(renderTime);
Scheduler.log('Yield:' + renderTime);
return null;
};
Scheduler.unstable_advanceTime(5);
React.startTransition(() => {
ReactTestRenderer.create(
<React.Profiler id="test" onRender={callback}>
<Yield renderTime={2} />
<Yield renderTime={3} />
</React.Profiler>,
{unstable_isConcurrent: true},
);
});
await waitFor(['Yield:2']);
expect(callback).toHaveBeenCalledTimes(0);
await waitForAll(['Yield:3']);
expect(callback).toHaveBeenCalledTimes(1);
const [call] = callback.mock.calls;
expect(call[2]).toBe(5);
expect(call[3]).toBe(5);
expect(call[4]).toBe(5);
expect(call[5]).toBe(10);
});
it('should not include time between frames', async () => {
const callback = jest.fn();
const Yield = ({renderTime}) => {
Scheduler.unstable_advanceTime(renderTime);
Scheduler.log('Yield:' + renderTime);
return null;
};
Scheduler.unstable_advanceTime(5);
React.startTransition(() => {
ReactTestRenderer.create(
<React.Profiler id="outer" onRender={callback}>
<Yield renderTime={5} />
<Yield renderTime={10} />
<React.Profiler id="inner" onRender={callback}>
<Yield renderTime={17} />
</React.Profiler>
</React.Profiler>,
{unstable_isConcurrent: true},
);
});
await waitFor(['Yield:5']);
expect(callback).toHaveBeenCalledTimes(0);
Scheduler.unstable_advanceTime(50);
await waitForAll(['Yield:10', 'Yield:17']);
expect(callback).toHaveBeenCalledTimes(2);
const [innerCall, outerCall] = callback.mock.calls;
expect(innerCall[0]).toBe('inner');
expect(innerCall[2]).toBe(17);
expect(innerCall[3]).toBe(17);
expect(innerCall[4]).toBe(70);
expect(innerCall[5]).toBe(87);
expect(outerCall[0]).toBe('outer');
expect(outerCall[2]).toBe(32);
expect(outerCall[3]).toBe(32);
expect(outerCall[4]).toBe(5);
expect(outerCall[5]).toBe(87);
});
it('should report the expected times when a high-pri update replaces a mount in-progress', async () => {
const callback = jest.fn();
const Yield = ({renderTime}) => {
Scheduler.unstable_advanceTime(renderTime);
Scheduler.log('Yield:' + renderTime);
return null;
};
Scheduler.unstable_advanceTime(5);
let renderer;
React.startTransition(() => {
renderer = ReactTestRenderer.create(
<React.Profiler id="test" onRender={callback}>
<Yield renderTime={10} />
<Yield renderTime={20} />
</React.Profiler>,
{unstable_isConcurrent: true},
);
});
await waitFor(['Yield:10']);
expect(callback).toHaveBeenCalledTimes(0);
Scheduler.unstable_advanceTime(100);
renderer.unstable_flushSync(() => {
renderer.update(
<React.Profiler id="test" onRender={callback}>
<Yield renderTime={5} />
</React.Profiler>,
);
});
assertLog(['Yield:5']);
expect(callback).toHaveBeenCalledTimes(1);
const call = callback.mock.calls[0];
expect(call[2]).toBe(5);
expect(call[3]).toBe(5);
expect(call[4]).toBe(115);
expect(call[5]).toBe(120);
callback.mockReset();
await waitForAll([]);
expect(callback).toHaveBeenCalledTimes(0);
});
it('should report the expected times when a high-priority update replaces a low-priority update', async () => {
const callback = jest.fn();
const Yield = ({renderTime}) => {
Scheduler.unstable_advanceTime(renderTime);
Scheduler.log('Yield:' + renderTime);
return null;
};
Scheduler.unstable_advanceTime(5);
const renderer = ReactTestRenderer.create(
<React.Profiler id="test" onRender={callback}>
<Yield renderTime={6} />
<Yield renderTime={15} />
</React.Profiler>,
{unstable_isConcurrent: true},
);
await waitForAll(['Yield:6', 'Yield:15']);
expect(callback).toHaveBeenCalledTimes(1);
let call = callback.mock.calls[0];
expect(call[2]).toBe(21);
expect(call[3]).toBe(21);
expect(call[4]).toBe(5);
expect(call[5]).toBe(26);
callback.mockReset();
Scheduler.unstable_advanceTime(30);
React.startTransition(() => {
renderer.update(
<React.Profiler id="test" onRender={callback}>
<Yield renderTime={3} />
<Yield renderTime={5} />
<Yield renderTime={9} />
</React.Profiler>,
);
});
await waitFor(['Yield:3']);
expect(callback).toHaveBeenCalledTimes(0);
Scheduler.unstable_advanceTime(100);
await waitFor(['Yield:5']);
expect(callback).toHaveBeenCalledTimes(0);
Scheduler.unstable_advanceTime(100);
renderer.unstable_flushSync(() => {
renderer.update(
<React.Profiler id="test" onRender={callback}>
<Yield renderTime={11} />
</React.Profiler>,
);
});
assertLog(['Yield:11']);
expect(callback).toHaveBeenCalledTimes(1);
call = callback.mock.calls[0];
expect(call[2]).toBe(11);
expect(call[3]).toBe(11);
expect(call[4]).toBe(264);
expect(call[5]).toBe(275);
await waitForAll([]);
expect(callback).toHaveBeenCalledTimes(1);
});
it('should report the expected times when a high-priority update interrupts a low-priority update', async () => {
const callback = jest.fn();
const Yield = ({renderTime}) => {
Scheduler.unstable_advanceTime(renderTime);
Scheduler.log('Yield:' + renderTime);
return null;
};
let first;
class FirstComponent extends React.Component {
state = {renderTime: 1};
render() {
first = this;
Scheduler.unstable_advanceTime(this.state.renderTime);
Scheduler.log('FirstComponent:' + this.state.renderTime);
return <Yield renderTime={4} />;
}
}
let second;
class SecondComponent extends React.Component {
state = {renderTime: 2};
render() {
second = this;
Scheduler.unstable_advanceTime(this.state.renderTime);
Scheduler.log('SecondComponent:' + this.state.renderTime);
return <Yield renderTime={7} />;
}
}
Scheduler.unstable_advanceTime(5);
const renderer = ReactTestRenderer.create(
<React.Profiler id="test" onRender={callback}>
<FirstComponent />
<SecondComponent />
</React.Profiler>,
{unstable_isConcurrent: true},
);
await waitForAll([
'FirstComponent:1',
'Yield:4',
'SecondComponent:2',
'Yield:7',
]);
expect(callback).toHaveBeenCalledTimes(1);
let call = callback.mock.calls[0];
expect(call[2]).toBe(14);
expect(call[3]).toBe(14);
expect(call[4]).toBe(5);
expect(call[5]).toBe(19);
callback.mockClear();
Scheduler.unstable_advanceTime(100);
React.startTransition(() => {
first.setState({renderTime: 10});
});
await waitFor(['FirstComponent:10']);
expect(callback).toHaveBeenCalledTimes(0);
Scheduler.unstable_advanceTime(100);
renderer.unstable_flushSync(() => second.setState({renderTime: 30}));
assertLog(['SecondComponent:30', 'Yield:7']);
expect(callback).toHaveBeenCalledTimes(1);
call = callback.mock.calls[0];
expect(call[2]).toBe(37);
expect(call[3]).toBe(42);
expect(call[4]).toBe(229);
expect(call[5]).toBe(266);
callback.mockClear();
Scheduler.unstable_advanceTime(100);
await waitForAll(['FirstComponent:10', 'Yield:4']);
expect(callback).toHaveBeenCalledTimes(1);
call = callback.mock.calls[0];
expect(call[2]).toBe(14);
expect(call[3]).toBe(51);
expect(call[4]).toBe(366);
expect(call[5]).toBe(380);
});
[true, false].forEach(replayFailedUnitOfWorkWithInvokeGuardedCallback => {
describe(`replayFailedUnitOfWorkWithInvokeGuardedCallback ${
replayFailedUnitOfWorkWithInvokeGuardedCallback ? 'enabled' : 'disabled'
}`, () => {
beforeEach(() => {
jest.resetModules();
loadModules({
replayFailedUnitOfWorkWithInvokeGuardedCallback,
});
});
it('should accumulate actual time after an error handled by componentDidCatch()', () => {
const callback = jest.fn();
const ThrowsError = ({unused}) => {
Scheduler.unstable_advanceTime(3);
throw Error('expected error');
};
class ErrorBoundary extends React.Component {
state = {error: null};
componentDidCatch(error) {
this.setState({error});
}
render() {
Scheduler.unstable_advanceTime(2);
return this.state.error === null ? (
this.props.children
) : (
<AdvanceTime byAmount={20} />
);
}
}
Scheduler.unstable_advanceTime(5);
ReactTestRenderer.create(
<React.Profiler id="test" onRender={callback}>
<ErrorBoundary>
<AdvanceTime byAmount={9} />
<ThrowsError />
</ErrorBoundary>
</React.Profiler>,
);
expect(callback).toHaveBeenCalledTimes(2);
const [mountCall, updateCall] = callback.mock.calls;
expect(mountCall[1]).toBe('mount');
expect(mountCall[2]).toBe(14);
expect(mountCall[3]).toBe(2);
expect(mountCall[4]).toBe(5);
expect(mountCall[5]).toBe(
__DEV__ && replayFailedUnitOfWorkWithInvokeGuardedCallback
? 22
: 19,
);
expect(updateCall[1]).toBe('nested-update');
expect(updateCall[2]).toBe(22);
expect(updateCall[3]).toBe(22);
expect(updateCall[4]).toBe(
__DEV__ && replayFailedUnitOfWorkWithInvokeGuardedCallback
? 22
: 19,
);
expect(updateCall[5]).toBe(
__DEV__ && replayFailedUnitOfWorkWithInvokeGuardedCallback
? 44
: 41,
);
});
it('should accumulate actual time after an error handled by getDerivedStateFromError()', () => {
const callback = jest.fn();
const ThrowsError = ({unused}) => {
Scheduler.unstable_advanceTime(10);
throw Error('expected error');
};
class ErrorBoundary extends React.Component {
state = {error: null};
static getDerivedStateFromError(error) {
return {error};
}
render() {
Scheduler.unstable_advanceTime(2);
return this.state.error === null ? (
this.props.children
) : (
<AdvanceTime byAmount={20} />
);
}
}
Scheduler.unstable_advanceTime(5);
ReactTestRenderer.create(
<React.Profiler id="test" onRender={callback}>
<ErrorBoundary>
<AdvanceTime byAmount={5} />
<ThrowsError />
</ErrorBoundary>
</React.Profiler>,
);
expect(callback).toHaveBeenCalledTimes(1);
const [mountCall] = callback.mock.calls;
expect(mountCall[1]).toBe('mount');
expect(mountCall[2]).toBe(39);
expect(mountCall[3]).toBe(22);
expect(mountCall[4]).toBe(5);
expect(mountCall[5]).toBe(
__DEV__ && replayFailedUnitOfWorkWithInvokeGuardedCallback
? 54
: 44,
);
});
it('should reset the fiber stack correct after a "complete" phase error', async () => {
jest.resetModules();
loadModules({
useNoopRenderer: true,
replayFailedUnitOfWorkWithInvokeGuardedCallback,
});
ReactNoop.render(
<React.Profiler id="profiler" onRender={jest.fn()}>
<errorInCompletePhase>hi</errorInCompletePhase>
</React.Profiler>,
);
await waitForThrow('Error in host config.');
ReactNoop.render(
<React.Profiler id="profiler" onRender={jest.fn()}>
<errorInCompletePhase>
<span>hi</span>
</errorInCompletePhase>
</React.Profiler>,
);
await waitForThrow('Error in host config.');
ReactNoop.render(
<React.Profiler id="profiler" onRender={jest.fn()}>
<span>hi</span>
</React.Profiler>,
);
await waitForAll([]);
});
});
});
});
it('reflects the most recently rendered id value', () => {
const callback = jest.fn();
Scheduler.unstable_advanceTime(5);
const renderer = ReactTestRenderer.create(
<React.Profiler id="one" onRender={callback}>
<AdvanceTime byAmount={2} />
</React.Profiler>,
);
expect(callback).toHaveBeenCalledTimes(1);
Scheduler.unstable_advanceTime(20);
renderer.update(
<React.Profiler id="two" onRender={callback}>
<AdvanceTime byAmount={1} />
</React.Profiler>,
);
expect(callback).toHaveBeenCalledTimes(2);
const [mountCall, updateCall] = callback.mock.calls;
expect(mountCall[0]).toBe('one');
expect(mountCall[1]).toBe('mount');
expect(mountCall[2]).toBe(2);
expect(mountCall[3]).toBe(2);
expect(mountCall[4]).toBe(5);
expect(updateCall[0]).toBe('two');
expect(updateCall[1]).toBe('update');
expect(updateCall[2]).toBe(1);
expect(updateCall[3]).toBe(1);
expect(updateCall[4]).toBe(27);
});
it('should not be called until after mutations', () => {
let classComponentMounted = false;
const callback = jest.fn(
(id, phase, actualDuration, baseDuration, startTime, commitTime) => {
expect(classComponentMounted).toBe(true);
expect(commitTime).toBe(2);
},
);
class ClassComponent extends React.Component {
componentDidMount() {
Scheduler.unstable_advanceTime(5);
classComponentMounted = true;
}
render() {
Scheduler.unstable_advanceTime(2);
return null;
}
}
ReactTestRenderer.create(
<React.Profiler id="test" onRender={callback}>
<ClassComponent />
</React.Profiler>,
);
expect(callback).toHaveBeenCalledTimes(1);
});
});
describe(`onCommit`, () => {
beforeEach(() => {
jest.resetModules();
loadModules();
});
it('should report time spent in layout effects and commit lifecycles', () => {
const callback = jest.fn();
const ComponentWithEffects = () => {
React.useLayoutEffect(() => {
Scheduler.unstable_advanceTime(10);
return () => {
Scheduler.unstable_advanceTime(100);
};
}, []);
React.useLayoutEffect(() => {
Scheduler.unstable_advanceTime(1000);
return () => {
Scheduler.unstable_advanceTime(10000);
};
});
React.useEffect(() => {
Scheduler.unstable_advanceTime(5);
return () => {
Scheduler.unstable_advanceTime(7);
};
});
return null;
};
class ComponentWithCommitHooks extends React.Component {
componentDidMount() {
Scheduler.unstable_advanceTime(100000);
}
componentDidUpdate() {
Scheduler.unstable_advanceTime(1000000);
}
render() {
return null;
}
}
Scheduler.unstable_advanceTime(1);
const renderer = ReactTestRenderer.create(
<React.Profiler id="mount-test" onCommit={callback}>
<ComponentWithEffects />
<ComponentWithCommitHooks />
</React.Profiler>,
);
expect(callback).toHaveBeenCalledTimes(1);
let call = callback.mock.calls[0];
expect(call).toHaveLength(4);
expect(call[0]).toBe('mount-test');
expect(call[1]).toBe('mount');
expect(call[2]).toBe(101010);
expect(call[3]).toBe(1);
Scheduler.unstable_advanceTime(1);
renderer.update(
<React.Profiler id="update-test" onCommit={callback}>
<ComponentWithEffects />
<ComponentWithCommitHooks />
</React.Profiler>,
);
expect(callback).toHaveBeenCalledTimes(2);
call = callback.mock.calls[1];
expect(call).toHaveLength(4);
expect(call[0]).toBe('update-test');
expect(call[1]).toBe('update');
expect(call[2]).toBe(1011000);
expect(call[3]).toBe(101017);
Scheduler.unstable_advanceTime(1);
renderer.update(<React.Profiler id="unmount-test" onCommit={callback} />);
expect(callback).toHaveBeenCalledTimes(3);
call = callback.mock.calls[2];
expect(call).toHaveLength(4);
expect(call[0]).toBe('unmount-test');
expect(call[1]).toBe('update');
expect(call[2]).toBe(10100);
expect(call[3]).toBe(1112030);
});
it('should report time spent in layout effects and commit lifecycles with cascading renders', () => {
const callback = jest.fn();
const ComponentWithEffects = ({shouldCascade}) => {
const [didCascade, setDidCascade] = React.useState(false);
Scheduler.unstable_advanceTime(100000000);
React.useLayoutEffect(() => {
if (shouldCascade && !didCascade) {
setDidCascade(true);
}
Scheduler.unstable_advanceTime(didCascade ? 30 : 10);
return () => {
Scheduler.unstable_advanceTime(100);
};
}, [didCascade, shouldCascade]);
return null;
};
class ComponentWithCommitHooks extends React.Component {
state = {
didCascade: false,
};
componentDidMount() {
Scheduler.unstable_advanceTime(1000);
}
componentDidUpdate() {
Scheduler.unstable_advanceTime(10000);
if (this.props.shouldCascade && !this.state.didCascade) {
this.setState({didCascade: true});
}
}
render() {
Scheduler.unstable_advanceTime(1000000000);
return null;
}
}
Scheduler.unstable_advanceTime(1);
const renderer = ReactTestRenderer.create(
<React.Profiler id="mount-test" onCommit={callback}>
<ComponentWithEffects shouldCascade={true} />
<ComponentWithCommitHooks />
</React.Profiler>,
);
expect(callback).toHaveBeenCalledTimes(2);
let call = callback.mock.calls[0];
expect(call).toHaveLength(4);
expect(call[0]).toBe('mount-test');
expect(call[1]).toBe('mount');
expect(call[2]).toBe(1010);
expect(call[3]).toBe(1100000001);
call = callback.mock.calls[1];
expect(call).toHaveLength(4);
expect(call[0]).toBe('mount-test');
expect(call[1]).toBe('nested-update');
expect(call[2]).toBe(130);
expect(call[3]).toBe(1200001011);
Scheduler.unstable_advanceTime(1);
renderer.update(
<React.Profiler id="update-test" onCommit={callback}>
<ComponentWithEffects />
<ComponentWithCommitHooks shouldCascade={true} />
</React.Profiler>,
);
expect(callback).toHaveBeenCalledTimes(4);
call = callback.mock.calls[2];
expect(call).toHaveLength(4);
expect(call[0]).toBe('update-test');
expect(call[1]).toBe('update');
expect(call[2]).toBe(10130);
expect(call[3]).toBe(2300001142);
call = callback.mock.calls[3];
expect(call).toHaveLength(4);
expect(call[0]).toBe('update-test');
expect(call[1]).toBe('nested-update');
expect(call[2]).toBe(10000);
expect(call[3]).toBe(3300011272);
});
it('should include time spent in ref callbacks', () => {
const callback = jest.fn();
const refSetter = ref => {
if (ref !== null) {
Scheduler.unstable_advanceTime(10);
} else {
Scheduler.unstable_advanceTime(100);
}
};
class ClassComponent extends React.Component {
render() {
return null;
}
}
const Component = () => {
Scheduler.unstable_advanceTime(1000);
return <ClassComponent ref={refSetter} />;
};
Scheduler.unstable_advanceTime(1);
const renderer = ReactTestRenderer.create(
<React.Profiler id="root" onCommit={callback}>
<Component />
</React.Profiler>,
);
expect(callback).toHaveBeenCalledTimes(1);
let call = callback.mock.calls[0];
expect(call).toHaveLength(4);
expect(call[0]).toBe('root');
expect(call[1]).toBe('mount');
expect(call[2]).toBe(10);
expect(call[3]).toBe(1001);
callback.mockClear();
renderer.update(<React.Profiler id="root" onCommit={callback} />);
expect(callback).toHaveBeenCalledTimes(1);
call = callback.mock.calls[0];
expect(call).toHaveLength(4);
expect(call[0]).toBe('root');
expect(call[1]).toBe('update');
expect(call[2]).toBe(100);
expect(call[3]).toBe(1011);
});
it('should bubble time spent in layout effects to higher profilers', async () => {
const callback = jest.fn();
const ComponentWithEffects = ({cleanupDuration, duration, setCountRef}) => {
const setCount = React.useState(0)[1];
if (setCountRef != null) {
setCountRef.current = setCount;
}
React.useLayoutEffect(() => {
Scheduler.unstable_advanceTime(duration);
return () => {
Scheduler.unstable_advanceTime(cleanupDuration);
};
});
Scheduler.unstable_advanceTime(1);
return null;
};
const setCountRef = React.createRef(null);
let renderer = null;
await act(() => {
renderer = ReactTestRenderer.create(
<React.Profiler id="root-mount" onCommit={callback}>
<React.Profiler id="a">
<ComponentWithEffects
duration={10}
cleanupDuration={100}
setCountRef={setCountRef}
/>
</React.Profiler>
<React.Profiler id="b">
<ComponentWithEffects duration={1000} cleanupDuration={10000} />
</React.Profiler>
</React.Profiler>,
);
});
expect(callback).toHaveBeenCalledTimes(1);
let call = callback.mock.calls[0];
expect(call).toHaveLength(4);
expect(call[0]).toBe('root-mount');
expect(call[1]).toBe('mount');
expect(call[2]).toBe(1010);
expect(call[3]).toBe(2);
await act(() => setCountRef.current(count => count + 1));
expect(callback).toHaveBeenCalledTimes(2);
call = callback.mock.calls[1];
expect(call).toHaveLength(4);
expect(call[0]).toBe('root-mount');
expect(call[1]).toBe('update');
expect(call[2]).toBe(110);
expect(call[3]).toBe(1013);
await act(() => {
renderer.update(
<React.Profiler id="root-update" onCommit={callback}>
<React.Profiler id="b">
<ComponentWithEffects duration={1000} cleanupDuration={10000} />
</React.Profiler>
</React.Profiler>,
);
});
expect(callback).toHaveBeenCalledTimes(3);
call = callback.mock.calls[2];
expect(call).toHaveLength(4);
expect(call[0]).toBe('root-update');
expect(call[1]).toBe('update');
expect(call[2]).toBe(1100);
expect(call[3]).toBe(1124);
});
it('should properly report time in layout effects even when there are errors', async () => {
const callback = jest.fn();
class ErrorBoundary extends React.Component {
state = {error: null};
static getDerivedStateFromError(error) {
return {error};
}
render() {
return this.state.error === null
? this.props.children
: this.props.fallback;
}
}
const ComponentWithEffects = ({
cleanupDuration,
duration,
effectDuration,
shouldThrow,
}) => {
React.useLayoutEffect(() => {
Scheduler.unstable_advanceTime(effectDuration);
if (shouldThrow) {
throw Error('expected');
}
return () => {
Scheduler.unstable_advanceTime(cleanupDuration);
};
});
Scheduler.unstable_advanceTime(duration);
return null;
};
Scheduler.unstable_advanceTime(1);
await act(() => {
ReactTestRenderer.create(
<React.Profiler id="root" onCommit={callback}>
<ErrorBoundary
fallback={
<ComponentWithEffects
duration={10000000}
effectDuration={100000000}
cleanupDuration={1000000000}
/>
}>
<ComponentWithEffects
duration={10}
effectDuration={100}
cleanupDuration={1000}
shouldThrow={true}
/>
</ErrorBoundary>
<ComponentWithEffects
duration={10000}
effectDuration={100000}
cleanupDuration={1000000}
/>
</React.Profiler>,
);
});
expect(callback).toHaveBeenCalledTimes(2);
let call = callback.mock.calls[0];
expect(call).toHaveLength(4);
expect(call[0]).toBe('root');
expect(call[1]).toBe('mount');
expect(call[2]).toBe(100100);
expect(call[3]).toBe(10011);
call = callback.mock.calls[1];
expect(call).toHaveLength(4);
expect(call[0]).toBe('root');
expect(call[1]).toBe('nested-update');
expect(call[2]).toBe(100000000);
expect(call[3]).toBe(10110111);
});
it('should properly report time in layout effect cleanup functions even when there are errors', async () => {
const callback = jest.fn();
class ErrorBoundary extends React.Component {
state = {error: null};
static getDerivedStateFromError(error) {
return {error};
}
render() {
return this.state.error === null
? this.props.children
: this.props.fallback;
}
}
const ComponentWithEffects = ({
cleanupDuration,
duration,
effectDuration,
shouldThrow = false,
}) => {
React.useLayoutEffect(() => {
Scheduler.unstable_advanceTime(effectDuration);
return () => {
Scheduler.unstable_advanceTime(cleanupDuration);
if (shouldThrow) {
throw Error('expected');
}
};
});
Scheduler.unstable_advanceTime(duration);
return null;
};
Scheduler.unstable_advanceTime(1);
let renderer = null;
await act(() => {
renderer = ReactTestRenderer.create(
<React.Profiler id="root" onCommit={callback}>
<ErrorBoundary
fallback={
<ComponentWithEffects
duration={10000000}
effectDuration={100000000}
cleanupDuration={1000000000}
/>
}>
<ComponentWithEffects
duration={10}
effectDuration={100}
cleanupDuration={1000}
shouldThrow={true}
/>
</ErrorBoundary>
<ComponentWithEffects
duration={10000}
effectDuration={100000}
cleanupDuration={1000000}
/>
</React.Profiler>,
);
});
expect(callback).toHaveBeenCalledTimes(1);
let call = callback.mock.calls[0];
expect(call).toHaveLength(4);
expect(call[0]).toBe('root');
expect(call[1]).toBe('mount');
expect(call[2]).toBe(100100);
expect(call[3]).toBe(10011);
callback.mockClear();
await act(() => {
renderer.update(
<React.Profiler id="root" onCommit={callback}>
<ErrorBoundary
fallback={
<ComponentWithEffects
duration={10000000}
effectDuration={100000000}
cleanupDuration={1000000000}
/>
}>
<ComponentWithEffects
duration={10}
effectDuration={100}
cleanupDuration={1000}
shouldThrow={false}
/>
</ErrorBoundary>
<ComponentWithEffects
duration={10000}
effectDuration={100000}
cleanupDuration={1000000}
/>
</React.Profiler>,
);
});
expect(callback).toHaveBeenCalledTimes(2);
call = callback.mock.calls[0];
expect(call).toHaveLength(4);
expect(call[0]).toBe('root');
expect(call[1]).toBe('update');
expect(call[2]).toBe(1101100);
expect(call[3]).toBe(120121);
call = callback.mock.calls[1];
expect(call).toHaveLength(4);
expect(call[0]).toBe('root');
expect(call[1]).toBe('nested-update');
expect(call[2]).toBe(100001000);
expect(call[3]).toBe(11221221);
});
});
describe(`onPostCommit`, () => {
beforeEach(() => {
jest.resetModules();
loadModules();
});
it('should report time spent in passive effects', async () => {
const callback = jest.fn();
const ComponentWithEffects = () => {
React.useLayoutEffect(() => {
Scheduler.unstable_advanceTime(5);
return () => {
Scheduler.unstable_advanceTime(7);
};
});
React.useEffect(() => {
Scheduler.unstable_advanceTime(10);
return () => {
Scheduler.unstable_advanceTime(100);
};
}, []);
React.useEffect(() => {
Scheduler.unstable_advanceTime(1000);
return () => {
Scheduler.unstable_advanceTime(10000);
};
});
return null;
};
Scheduler.unstable_advanceTime(1);
let renderer;
await act(() => {
renderer = ReactTestRenderer.create(
<React.Profiler id="mount-test" onPostCommit={callback}>
<ComponentWithEffects />
</React.Profiler>,
);
});
await waitForAll([]);
expect(callback).toHaveBeenCalledTimes(1);
let call = callback.mock.calls[0];
expect(call).toHaveLength(4);
expect(call[0]).toBe('mount-test');
expect(call[1]).toBe('mount');
expect(call[2]).toBe(1010);
expect(call[3]).toBe(1);
Scheduler.unstable_advanceTime(1);
await act(() => {
renderer.update(
<React.Profiler id="update-test" onPostCommit={callback}>
<ComponentWithEffects />
</React.Profiler>,
);
});
await waitForAll([]);
expect(callback).toHaveBeenCalledTimes(2);
call = callback.mock.calls[1];
expect(call).toHaveLength(4);
expect(call[0]).toBe('update-test');
expect(call[1]).toBe('update');
expect(call[2]).toBe(11000);
expect(call[3]).toBe(1017);
Scheduler.unstable_advanceTime(1);
await act(() => {
renderer.update(
<React.Profiler id="unmount-test" onPostCommit={callback} />,
);
});
await waitForAll([]);
expect(callback).toHaveBeenCalledTimes(3);
call = callback.mock.calls[2];
expect(call).toHaveLength(4);
expect(call[0]).toBe('unmount-test');
expect(call[1]).toBe('update');
expect(call[2]).toBe(0);
expect(call[3]).toBe(12030);
});
it('should report time spent in passive effects with cascading renders', async () => {
const callback = jest.fn();
const ComponentWithEffects = () => {
const [didMount, setDidMount] = React.useState(false);
Scheduler.unstable_advanceTime(1000);
React.useEffect(() => {
if (!didMount) {
setDidMount(true);
}
Scheduler.unstable_advanceTime(didMount ? 30 : 10);
return () => {
Scheduler.unstable_advanceTime(100);
};
}, [didMount]);
return null;
};
Scheduler.unstable_advanceTime(1);
await act(() => {
ReactTestRenderer.create(
<React.Profiler id="mount-test" onPostCommit={callback}>
<ComponentWithEffects />
</React.Profiler>,
);
});
expect(callback).toHaveBeenCalledTimes(2);
let call = callback.mock.calls[0];
expect(call).toHaveLength(4);
expect(call[0]).toBe('mount-test');
expect(call[1]).toBe('mount');
expect(call[2]).toBe(10);
expect(call[3]).toBe(1001);
call = callback.mock.calls[1];
expect(call).toHaveLength(4);
expect(call[0]).toBe('mount-test');
expect(call[1]).toBe('update');
expect(call[2]).toBe(130);
expect(call[3]).toBe(2011);
});
it('should bubble time spent in effects to higher profilers', async () => {
const callback = jest.fn();
const ComponentWithEffects = ({cleanupDuration, duration, setCountRef}) => {
const setCount = React.useState(0)[1];
if (setCountRef != null) {
setCountRef.current = setCount;
}
React.useEffect(() => {
Scheduler.unstable_advanceTime(duration);
return () => {
Scheduler.unstable_advanceTime(cleanupDuration);
};
});
Scheduler.unstable_advanceTime(1);
return null;
};
const setCountRef = React.createRef(null);
let renderer = null;
await act(() => {
renderer = ReactTestRenderer.create(
<React.Profiler id="root-mount" onPostCommit={callback}>
<React.Profiler id="a">
<ComponentWithEffects
duration={10}
cleanupDuration={100}
setCountRef={setCountRef}
/>
</React.Profiler>
<React.Profiler id="b">
<ComponentWithEffects duration={1000} cleanupDuration={10000} />
</React.Profiler>
</React.Profiler>,
);
});
expect(callback).toHaveBeenCalledTimes(1);
let call = callback.mock.calls[0];
expect(call).toHaveLength(4);
expect(call[0]).toBe('root-mount');
expect(call[1]).toBe('mount');
expect(call[2]).toBe(1010);
expect(call[3]).toBe(2);
await act(() => setCountRef.current(count => count + 1));
expect(callback).toHaveBeenCalledTimes(2);
call = callback.mock.calls[1];
expect(call).toHaveLength(4);
expect(call[0]).toBe('root-mount');
expect(call[1]).toBe('update');
expect(call[2]).toBe(110);
expect(call[3]).toBe(1013);
await act(() => {
renderer.update(
<React.Profiler id="root-update" onPostCommit={callback}>
<React.Profiler id="b">
<ComponentWithEffects duration={1000} cleanupDuration={10000} />
</React.Profiler>
</React.Profiler>,
);
});
expect(callback).toHaveBeenCalledTimes(3);
call = callback.mock.calls[2];
expect(call).toHaveLength(4);
expect(call[0]).toBe('root-update');
expect(call[1]).toBe('update');
expect(call[2]).toBe(1100);
expect(call[3]).toBe(1124);
});
it('should properly report time in passive effects even when there are errors', async () => {
const callback = jest.fn();
class ErrorBoundary extends React.Component {
state = {error: null};
static getDerivedStateFromError(error) {
return {error};
}
render() {
return this.state.error === null
? this.props.children
: this.props.fallback;
}
}
const ComponentWithEffects = ({
cleanupDuration,
duration,
effectDuration,
shouldThrow,
}) => {
React.useEffect(() => {
Scheduler.unstable_advanceTime(effectDuration);
if (shouldThrow) {
throw Error('expected');
}
return () => {
Scheduler.unstable_advanceTime(cleanupDuration);
};
});
Scheduler.unstable_advanceTime(duration);
return null;
};
Scheduler.unstable_advanceTime(1);
await act(() => {
ReactTestRenderer.create(
<React.Profiler id="root" onPostCommit={callback}>
<ErrorBoundary
fallback={
<ComponentWithEffects
duration={10000000}
effectDuration={100000000}
cleanupDuration={1000000000}
/>
}>
<ComponentWithEffects
duration={10}
effectDuration={100}
cleanupDuration={1000}
shouldThrow={true}
/>
</ErrorBoundary>
<ComponentWithEffects
duration={10000}
effectDuration={100000}
cleanupDuration={1000000}
/>
</React.Profiler>,
);
});
expect(callback).toHaveBeenCalledTimes(2);
let call = callback.mock.calls[0];
expect(call).toHaveLength(4);
expect(call[0]).toBe('root');
expect(call[1]).toBe('mount');
expect(call[2]).toBe(100100);
expect(call[3]).toBe(10011);
call = callback.mock.calls[1];
expect(call).toHaveLength(4);
expect(call[0]).toBe('root');
expect(call[1]).toBe('update');
expect(call[2]).toBe(100000000);
expect(call[3]).toBe(10110111);
});
it('should properly report time in passive effect cleanup functions even when there are errors', async () => {
const callback = jest.fn();
class ErrorBoundary extends React.Component {
state = {error: null};
static getDerivedStateFromError(error) {
return {error};
}
render() {
return this.state.error === null
? this.props.children
: this.props.fallback;
}
}
const ComponentWithEffects = ({
cleanupDuration,
duration,
effectDuration,
shouldThrow = false,
id,
}) => {
React.useEffect(() => {
Scheduler.unstable_advanceTime(effectDuration);
return () => {
Scheduler.unstable_advanceTime(cleanupDuration);
if (shouldThrow) {
throw Error('expected');
}
};
});
Scheduler.unstable_advanceTime(duration);
return null;
};
Scheduler.unstable_advanceTime(1);
let renderer = null;
await act(() => {
renderer = ReactTestRenderer.create(
<React.Profiler id="root" onPostCommit={callback}>
<ErrorBoundary
fallback={
<ComponentWithEffects
duration={10000000}
effectDuration={100000000}
cleanupDuration={1000000000}
/>
}>
<ComponentWithEffects
duration={10}
effectDuration={100}
cleanupDuration={1000}
shouldThrow={true}
/>
</ErrorBoundary>
<ComponentWithEffects
duration={10000}
effectDuration={100000}
cleanupDuration={1000000}
/>
</React.Profiler>,
);
});
expect(callback).toHaveBeenCalledTimes(1);
let call = callback.mock.calls[0];
expect(call).toHaveLength(4);
expect(call[0]).toBe('root');
expect(call[1]).toBe('mount');
expect(call[2]).toBe(100100);
expect(call[3]).toBe(10011);
callback.mockClear();
await act(() => {
renderer.update(
<React.Profiler id="root" onPostCommit={callback}>
<ErrorBoundary
fallback={
<ComponentWithEffects
duration={10000000}
effectDuration={100000000}
cleanupDuration={1000000000}
/>
}>
<ComponentWithEffects
duration={10}
effectDuration={100}
cleanupDuration={1000}
shouldThrow={false}
/>
</ErrorBoundary>
<ComponentWithEffects
duration={10000}
effectDuration={100000}
cleanupDuration={1000000}
/>
</React.Profiler>,
);
});
expect(callback).toHaveBeenCalledTimes(2);
call = callback.mock.calls[0];
expect(call).toHaveLength(4);
expect(call[0]).toBe('root');
expect(call[1]).toBe('update');
expect(call[2]).toBe(1101100);
expect(call[3]).toBe(120121);
call = callback.mock.calls[1];
expect(call).toHaveLength(4);
expect(call[0]).toBe('root');
expect(call[1]).toBe('update');
expect(call[2]).toBe(100000000);
expect(call[3]).toBe(11221221);
});
});
describe(`onNestedUpdateScheduled`, () => {
beforeEach(() => {
jest.resetModules();
loadModules({
enableProfilerNestedUpdateScheduledHook: true,
useNoopRenderer: true,
});
});
it('is not called when the legacy render API is used to schedule an update', () => {
const onNestedUpdateScheduled = jest.fn();
ReactNoop.renderLegacySyncRoot(
<React.Profiler
id="test"
onNestedUpdateScheduled={onNestedUpdateScheduled}>
<div>initial</div>
</React.Profiler>,
);
ReactNoop.renderLegacySyncRoot(
<React.Profiler
id="test"
onNestedUpdateScheduled={onNestedUpdateScheduled}>
<div>update</div>
</React.Profiler>,
);
expect(onNestedUpdateScheduled).not.toHaveBeenCalled();
});
it('is not called when the root API is used to schedule an update', () => {
const onNestedUpdateScheduled = jest.fn();
ReactNoop.render(
<React.Profiler
id="test"
onNestedUpdateScheduled={onNestedUpdateScheduled}>
<div>initial</div>
</React.Profiler>,
);
ReactNoop.render(
<React.Profiler
id="test"
onNestedUpdateScheduled={onNestedUpdateScheduled}>
<div>update</div>
</React.Profiler>,
);
expect(onNestedUpdateScheduled).not.toHaveBeenCalled();
});
it('is called when a function component schedules an update during a layout effect', async () => {
function Component() {
const [didMount, setDidMount] = React.useState(false);
React.useLayoutEffect(() => {
setDidMount(true);
}, []);
Scheduler.log(`Component:${didMount}`);
return didMount;
}
const onNestedUpdateScheduled = jest.fn();
await act(() => {
ReactNoop.render(
<React.Profiler
id="test"
onNestedUpdateScheduled={onNestedUpdateScheduled}>
<Component />
</React.Profiler>,
);
});
assertLog(['Component:false', 'Component:true']);
expect(onNestedUpdateScheduled).toHaveBeenCalledTimes(1);
expect(onNestedUpdateScheduled.mock.calls[0][0]).toBe('test');
});
it('is called when a function component schedules a batched update during a layout effect', async () => {
function Component() {
const [didMount, setDidMount] = React.useState(false);
React.useLayoutEffect(() => {
ReactNoop.batchedUpdates(() => {
setDidMount(true);
});
}, []);
Scheduler.log(`Component:${didMount}`);
return didMount;
}
const onNestedUpdateScheduled = jest.fn();
const onRender = jest.fn();
ReactNoop.render(
<React.Profiler
id="root"
onNestedUpdateScheduled={onNestedUpdateScheduled}
onRender={onRender}>
<Component />
</React.Profiler>,
);
await waitForAll(['Component:false', 'Component:true']);
expect(onRender).toHaveBeenCalledTimes(2);
expect(onRender.mock.calls[0][1]).toBe('mount');
expect(onRender.mock.calls[1][1]).toBe('nested-update');
expect(onNestedUpdateScheduled).toHaveBeenCalledTimes(1);
expect(onNestedUpdateScheduled.mock.calls[0][0]).toBe('root');
});
it('bubbles up and calls all ancestor Profilers', async () => {
function Component() {
const [didMount, setDidMount] = React.useState(false);
React.useLayoutEffect(() => {
setDidMount(true);
}, []);
Scheduler.log(`Component:${didMount}`);
return didMount;
}
const onNestedUpdateScheduledOne = jest.fn();
const onNestedUpdateScheduledTwo = jest.fn();
const onNestedUpdateScheduledThree = jest.fn();
await act(() => {
ReactNoop.render(
<React.Profiler
id="one"
onNestedUpdateScheduled={onNestedUpdateScheduledOne}>
<React.Profiler
id="two"
onNestedUpdateScheduled={onNestedUpdateScheduledTwo}>
<>
<Component />
<React.Profiler
id="three"
onNestedUpdateScheduled={onNestedUpdateScheduledThree}
/>
</>
</React.Profiler>
</React.Profiler>,
);
});
assertLog(['Component:false', 'Component:true']);
expect(onNestedUpdateScheduledOne).toHaveBeenCalledTimes(1);
expect(onNestedUpdateScheduledOne.mock.calls[0][0]).toBe('one');
expect(onNestedUpdateScheduledTwo).toHaveBeenCalledTimes(1);
expect(onNestedUpdateScheduledTwo.mock.calls[0][0]).toBe('two');
expect(onNestedUpdateScheduledThree).not.toHaveBeenCalled();
});
it('is not called when an update is scheduled for another doort during a layout effect', async () => {
const setStateRef = React.createRef(null);
function ComponentRootOne() {
const [state, setState] = React.useState(false);
setStateRef.current = setState;
Scheduler.log(`ComponentRootOne:${state}`);
return state;
}
function ComponentRootTwo() {
React.useLayoutEffect(() => {
setStateRef.current(true);
}, []);
Scheduler.log('ComponentRootTwo');
return null;
}
const onNestedUpdateScheduled = jest.fn();
await act(() => {
ReactNoop.renderToRootWithID(
<React.Profiler
id="test"
onNestedUpdateScheduled={onNestedUpdateScheduled}>
<ComponentRootOne />
</React.Profiler>,
1,
);
ReactNoop.renderToRootWithID(
<React.Profiler
id="test"
onNestedUpdateScheduled={onNestedUpdateScheduled}>
<ComponentRootTwo />
</React.Profiler>,
2,
);
});
assertLog([
'ComponentRootOne:false',
'ComponentRootTwo',
'ComponentRootOne:true',
]);
expect(onNestedUpdateScheduled).not.toHaveBeenCalled();
});
it('is not called when a function component schedules an update during a passive effect', async () => {
function Component() {
const [didMount, setDidMount] = React.useState(false);
React.useEffect(() => {
setDidMount(true);
}, []);
Scheduler.log(`Component:${didMount}`);
return didMount;
}
const onNestedUpdateScheduled = jest.fn();
await act(() => {
ReactNoop.render(
<React.Profiler
id="test"
onNestedUpdateScheduled={onNestedUpdateScheduled}>
<Component />
</React.Profiler>,
);
});
assertLog(['Component:false', 'Component:true']);
expect(onNestedUpdateScheduled).not.toHaveBeenCalled();
});
it('is not called when a function component schedules an update outside of render', async () => {
const updateFnRef = React.createRef(null);
function Component() {
const [state, setState] = React.useState(false);
updateFnRef.current = () => setState(true);
Scheduler.log(`Component:${state}`);
return state;
}
const onNestedUpdateScheduled = jest.fn();
await act(() => {
ReactNoop.render(
<React.Profiler
id="test"
onNestedUpdateScheduled={onNestedUpdateScheduled}>
<Component />
</React.Profiler>,
);
});
assertLog(['Component:false']);
await act(() => {
updateFnRef.current();
});
assertLog(['Component:true']);
expect(onNestedUpdateScheduled).not.toHaveBeenCalled();
});
it('it is not called when a component schedules an update during render', async () => {
function Component() {
const [state, setState] = React.useState(false);
if (state === false) {
setState(true);
}
Scheduler.log(`Component:${state}`);
return state;
}
const onNestedUpdateScheduled = jest.fn();
await act(() => {
ReactNoop.render(
<React.Profiler
id="test"
onNestedUpdateScheduled={onNestedUpdateScheduled}>
<Component />
</React.Profiler>,
);
});
assertLog(['Component:false', 'Component:true']);
expect(onNestedUpdateScheduled).not.toHaveBeenCalled();
});
it('it is called when a component schedules an update from a ref callback', async () => {
function Component({mountChild}) {
const [refAttached, setRefAttached] = React.useState(false);
const [refDetached, setRefDetached] = React.useState(false);
const refSetter = React.useCallback(ref => {
if (ref !== null) {
setRefAttached(true);
} else {
setRefDetached(true);
}
}, []);
Scheduler.log(`Component:${refAttached}:${refDetached}`);
return mountChild ? <div ref={refSetter} /> : null;
}
const onNestedUpdateScheduled = jest.fn();
await act(() => {
ReactNoop.render(
<React.Profiler
id="test"
onNestedUpdateScheduled={onNestedUpdateScheduled}>
<Component mountChild={true} />
</React.Profiler>,
);
});
assertLog(['Component:false:false', 'Component:true:false']);
expect(onNestedUpdateScheduled).toHaveBeenCalledTimes(1);
expect(onNestedUpdateScheduled.mock.calls[0][0]).toBe('test');
await act(() => {
ReactNoop.render(
<React.Profiler
id="test"
onNestedUpdateScheduled={onNestedUpdateScheduled}>
<Component mountChild={false} />
</React.Profiler>,
);
});
assertLog(['Component:true:false', 'Component:true:true']);
expect(onNestedUpdateScheduled).toHaveBeenCalledTimes(2);
expect(onNestedUpdateScheduled.mock.calls[1][0]).toBe('test');
});
it('is called when a class component schedules an update from the componentDidMount lifecycles', async () => {
class Component extends React.Component {
state = {
value: false,
};
componentDidMount() {
this.setState({value: true});
}
render() {
const {value} = this.state;
Scheduler.log(`Component:${value}`);
return value;
}
}
const onNestedUpdateScheduled = jest.fn();
await act(() => {
ReactNoop.render(
<React.Profiler
id="test"
onNestedUpdateScheduled={onNestedUpdateScheduled}>
<Component />
</React.Profiler>,
);
});
assertLog(['Component:false', 'Component:true']);
expect(onNestedUpdateScheduled).toHaveBeenCalledTimes(1);
expect(onNestedUpdateScheduled.mock.calls[0][0]).toBe('test');
});
it('is called when a class component schedules an update from the componentDidUpdate lifecycles', async () => {
class Component extends React.Component {
state = {
nestedUpdateSheduled: false,
};
componentDidUpdate(prevProps, prevState) {
if (
this.props.scheduleNestedUpdate &&
!this.state.nestedUpdateSheduled
) {
this.setState({nestedUpdateSheduled: true});
}
}
render() {
const {scheduleNestedUpdate} = this.props;
const {nestedUpdateSheduled} = this.state;
Scheduler.log(
`Component:${scheduleNestedUpdate}:${nestedUpdateSheduled}`,
);
return nestedUpdateSheduled;
}
}
const onNestedUpdateScheduled = jest.fn();
await act(() => {
ReactNoop.render(
<React.Profiler
id="test"
onNestedUpdateScheduled={onNestedUpdateScheduled}>
<Component scheduleNestedUpdate={false} />
</React.Profiler>,
);
});
assertLog(['Component:false:false']);
expect(onNestedUpdateScheduled).not.toHaveBeenCalled();
await act(() => {
ReactNoop.render(
<React.Profiler
id="test"
onNestedUpdateScheduled={onNestedUpdateScheduled}>
<Component scheduleNestedUpdate={true} />
</React.Profiler>,
);
});
assertLog(['Component:true:false', 'Component:true:true']);
expect(onNestedUpdateScheduled).toHaveBeenCalledTimes(1);
expect(onNestedUpdateScheduled.mock.calls[0][0]).toBe('test');
});
it('is not called when a class component schedules an update outside of render', async () => {
const updateFnRef = React.createRef(null);
class Component extends React.Component {
state = {
value: false,
};
render() {
const {value} = this.state;
updateFnRef.current = () => this.setState({value: true});
Scheduler.log(`Component:${value}`);
return value;
}
}
const onNestedUpdateScheduled = jest.fn();
await act(() => {
ReactNoop.render(
<React.Profiler
id="test"
onNestedUpdateScheduled={onNestedUpdateScheduled}>
<Component />
</React.Profiler>,
);
});
assertLog(['Component:false']);
await act(() => {
updateFnRef.current();
});
assertLog(['Component:true']);
expect(onNestedUpdateScheduled).not.toHaveBeenCalled();
});
});