let React;
let ReactDOMClient;
let Scheduler;
let act;
let container;
let assertLog;
jest.useRealTimers();
global.IS_REACT_ACT_ENVIRONMENT = true;
function sleep(period) {
return new Promise(resolve => {
setTimeout(() => {
resolve(true);
}, period);
});
}
describe('React.act()', () => {
afterEach(() => {
jest.restoreAllMocks();
});
let root = null;
const renderConcurrent = (el, dom) => {
root = ReactDOMClient.createRoot(dom);
if (__DEV__) {
act(() => root.render(el));
} else {
root.render(el);
}
};
const unmountConcurrent = _dom => {
if (__DEV__) {
act(() => {
if (root !== null) {
root.unmount();
root = null;
}
});
} else {
if (root !== null) {
root.unmount();
root = null;
}
}
};
const rerenderConcurrent = el => {
act(() => root.render(el));
};
runActTests(renderConcurrent, unmountConcurrent, rerenderConcurrent);
describe('unacted effects', () => {
function App() {
React.useEffect(() => {}, []);
return null;
}
it('does not warn', () => {
root = ReactDOMClient.createRoot(document.createElement('div'));
act(() => root.render(<App />));
});
});
});
function runActTests(render, unmount, rerender) {
describe('concurrent render', () => {
beforeEach(() => {
jest.resetModules();
React = require('react');
ReactDOMClient = require('react-dom/client');
Scheduler = require('scheduler');
act = React.act;
const InternalTestUtils = require('internal-test-utils');
assertLog = InternalTestUtils.assertLog;
container = document.createElement('div');
document.body.appendChild(container);
});
afterEach(() => {
unmount(container);
document.body.removeChild(container);
});
describe('sync', () => {
it('can use act to flush effects', () => {
function App() {
React.useEffect(() => {
Scheduler.log(100);
});
return null;
}
act(() => {
render(<App />, container);
});
assertLog([100]);
});
it('flushes effects on every call', async () => {
function App() {
const [ctr, setCtr] = React.useState(0);
React.useEffect(() => {
Scheduler.log(ctr);
});
return (
<button id="button" onClick={() => setCtr(x => x + 1)}>
{ctr}
</button>
);
}
act(() => {
render(<App />, container);
});
assertLog([0]);
const button = container.querySelector('#button');
function click() {
button.dispatchEvent(new MouseEvent('click', {bubbles: true}));
}
await act(async () => {
click();
click();
click();
});
assertLog([3]);
await act(async () => click());
assertLog([4]);
await act(async () => click());
assertLog([5]);
expect(button.innerHTML).toBe('5');
});
it("should keep flushing effects until they're done", () => {
function App() {
const [ctr, setCtr] = React.useState(0);
React.useEffect(() => {
if (ctr < 5) {
setCtr(x => x + 1);
}
});
return ctr;
}
act(() => {
render(<App />, container);
});
expect(container.innerHTML).toBe('5');
});
it('should flush effects only on exiting the outermost act', () => {
function App() {
React.useEffect(() => {
Scheduler.log(0);
});
return null;
}
act(() => {
act(() => {
render(<App />, container);
});
assertLog([]);
});
assertLog([0]);
});
it('warns if a setState is called outside of act(...)', () => {
let setValue = null;
function App() {
const [value, _setValue] = React.useState(0);
setValue = _setValue;
return value;
}
act(() => {
render(<App />, container);
});
expect(() => setValue(1)).toErrorDev([
'An update to App inside a test was not wrapped in act(...).',
]);
});
it('warns if a setState is called outside of act(...) after a component threw', () => {
let setValue = null;
function App({defaultValue}) {
if (defaultValue === undefined) {
throw new Error('some error');
}
const [value, _setValue] = React.useState(defaultValue);
setValue = _setValue;
return value;
}
expect(() => {
act(() => {
render(<App defaultValue={undefined} />, container);
});
}).toThrow('some error');
act(() => {
rerender(<App defaultValue={0} />, container);
});
expect(() => setValue(1)).toErrorDev([
'An update to App inside a test was not wrapped in act(...).',
]);
});
it('does not warn if IS_REACT_ACT_ENVIRONMENT is set to false', () => {
let setState;
function App() {
const [state, _setState] = React.useState(0);
setState = _setState;
return state;
}
act(() => {
render(<App />, container);
});
expect(() => setState(1)).toErrorDev(
'An update to App inside a test was not wrapped in act(...)',
);
const prevIsActEnvironment = global.IS_REACT_ACT_ENVIRONMENT;
global.IS_REACT_ACT_ENVIRONMENT = false;
try {
setState(2);
} finally {
global.IS_REACT_ACT_ENVIRONMENT = prevIsActEnvironment;
}
expect(() => setState(3)).toErrorDev(
'An update to App inside a test was not wrapped in act(...)',
);
});
describe('fake timers', () => {
beforeEach(() => {
jest.useFakeTimers();
});
afterEach(() => {
jest.useRealTimers();
});
it('lets a ticker update', () => {
function App() {
const [toggle, setToggle] = React.useState(0);
React.useEffect(() => {
const timeout = setTimeout(() => {
setToggle(1);
}, 200);
return () => clearTimeout(timeout);
}, []);
return toggle;
}
act(() => {
render(<App />, container);
});
act(() => {
jest.runAllTimers();
});
expect(container.innerHTML).toBe('1');
});
it('can use the async version to catch microtasks', async () => {
function App() {
const [toggle, setToggle] = React.useState(0);
React.useEffect(() => {
sleep(200).then(() => setToggle(1));
}, []);
return toggle;
}
act(() => {
render(<App />, container);
});
await act(async () => {
jest.runAllTimers();
});
expect(container.innerHTML).toBe('1');
});
it('can handle cascading promises with fake timers', async () => {
function App() {
const [state, setState] = React.useState(0);
async function ticker() {
await null;
setState(x => x + 1);
}
React.useEffect(() => {
ticker();
}, [Math.min(state, 4)]);
return state;
}
await act(async () => {
render(<App />, container);
});
expect(container.innerHTML).toBe('5');
});
it('flushes immediate re-renders with act', () => {
function App() {
const [ctr, setCtr] = React.useState(0);
React.useEffect(() => {
if (ctr === 0) {
setCtr(1);
}
const timeout = setTimeout(() => setCtr(2), 1000);
return () => clearTimeout(timeout);
});
return ctr;
}
act(() => {
render(<App />, container);
jest.runAllTimers();
});
expect(container.innerHTML).toBe('1');
act(() => {
jest.runAllTimers();
});
expect(container.innerHTML).toBe('2');
});
});
});
describe('asynchronous tests', () => {
it('works with timeouts', async () => {
function App() {
const [ctr, setCtr] = React.useState(0);
function doSomething() {
setTimeout(() => {
setCtr(1);
}, 50);
}
React.useEffect(() => {
doSomething();
}, []);
return ctr;
}
await act(async () => {
render(<App />, container);
});
expect(container.innerHTML).toBe('0');
await act(async () => {
await sleep(100);
});
expect(container.innerHTML).toBe('1');
});
it('flushes microtasks before exiting (async function)', async () => {
function App() {
const [ctr, setCtr] = React.useState(0);
async function someAsyncFunction() {
await null;
await null;
await null;
setCtr(1);
}
React.useEffect(() => {
someAsyncFunction();
}, []);
return ctr;
}
await act(async () => {
render(<App />, container);
});
expect(container.innerHTML).toEqual('1');
});
it('flushes microtasks before exiting (sync function)', async () => {
function App() {
const [ctr, setCtr] = React.useState(0);
async function someAsyncFunction() {
await null;
await null;
await null;
setCtr(1);
}
React.useEffect(() => {
someAsyncFunction();
}, []);
return ctr;
}
await act(() => {
render(<App />, container);
});
expect(container.innerHTML).toEqual('1');
});
it('warns if you do not await an act call', async () => {
spyOnDevAndProd(console, 'error').mockImplementation(() => {});
act(async () => {});
await sleep(0);
if (__DEV__) {
expect(console.error).toHaveBeenCalledTimes(1);
expect(console.error.mock.calls[0][0]).toMatch(
'You called act(async () => ...) without await.',
);
}
});
it('warns if you try to interleave multiple act calls', async () => {
spyOnDevAndProd(console, 'error').mockImplementation(() => {});
await Promise.all([
act(async () => {
await sleep(50);
}),
act(async () => {
await sleep(100);
}),
]);
await sleep(150);
if (__DEV__) {
expect(console.error).toHaveBeenCalledTimes(2);
expect(console.error.mock.calls[0][0]).toMatch(
'You seem to have overlapping act() calls',
);
expect(console.error.mock.calls[1][0]).toMatch(
'You seem to have overlapping act() calls',
);
}
});
it('async commits and effects are guaranteed to be flushed', async () => {
function App() {
const [state, setState] = React.useState(0);
async function something() {
await null;
setState(1);
}
React.useEffect(() => {
something();
}, []);
React.useEffect(() => {
Scheduler.log(state);
});
return state;
}
await act(async () => {
render(<App />, container);
});
assertLog([0, 1]);
expect(container.innerHTML).toBe('1');
});
it('can handle cascading promises', async () => {
function App() {
const [state, setState] = React.useState(0);
async function ticker() {
await null;
setState(x => x + 1);
}
React.useEffect(() => {
Scheduler.log(state);
ticker();
}, [Math.min(state, 4)]);
return state;
}
await act(async () => {
render(<App />, container);
});
assertLog([0, 1, 2, 3, 4]);
expect(container.innerHTML).toBe('5');
});
});
describe('error propagation', () => {
it('propagates errors - sync', () => {
let err;
try {
act(() => {
throw new Error('some error');
});
} catch (_err) {
err = _err;
} finally {
expect(err instanceof Error).toBe(true);
expect(err.message).toBe('some error');
}
});
it('should propagate errors from effects - sync', () => {
function App() {
React.useEffect(() => {
throw new Error('oh no');
});
return null;
}
let error;
try {
act(() => {
render(<App />, container);
});
} catch (_error) {
error = _error;
} finally {
expect(error instanceof Error).toBe(true);
expect(error.message).toBe('oh no');
}
});
it('propagates errors - async', async () => {
let err;
try {
await act(async () => {
await sleep(100);
throw new Error('some error');
});
} catch (_err) {
err = _err;
} finally {
expect(err instanceof Error).toBe(true);
expect(err.message).toBe('some error');
}
});
it('should cleanup after errors - sync', () => {
function App() {
React.useEffect(() => {
Scheduler.log('oh yes');
});
return null;
}
let error;
try {
act(() => {
throw new Error('oh no');
});
} catch (_error) {
error = _error;
} finally {
expect(error instanceof Error).toBe(true);
expect(error.message).toBe('oh no');
act(() => {
render(<App />, container);
});
assertLog(['oh yes']);
}
});
it('should cleanup after errors - async', async () => {
function App() {
async function somethingAsync() {
await null;
Scheduler.log('oh yes');
}
React.useEffect(() => {
somethingAsync();
});
return null;
}
let error;
try {
await act(async () => {
await sleep(100);
throw new Error('oh no');
});
} catch (_error) {
error = _error;
} finally {
expect(error instanceof Error).toBe(true);
expect(error.message).toBe('oh no');
await act(async () => {
render(<App />, container);
});
assertLog(['oh yes']);
}
});
});
describe('suspense', () => {
if (__DEV__ && __EXPERIMENTAL__) {
it('triggers fallbacks if available', async () => {
let resolved = false;
let resolve;
const promise = new Promise(_resolve => {
resolve = _resolve;
});
function Suspends() {
if (resolved) {
return 'was suspended';
}
throw promise;
}
function App(props) {
return (
<React.Suspense
fallback={<span data-test-id="spinner">loading...</span>}>
{props.suspend ? <Suspends /> : 'content'}
</React.Suspense>
);
}
act(() => {
render(<App suspend={false} />, container);
});
act(() => {
rerender(<App suspend={true} />);
});
expect(
document.querySelector('[data-test-id=spinner]'),
).not.toBeNull();
act(() => {
rerender(<App suspend={false} />);
});
expect(document.querySelector('[data-test-id=spinner]')).toBeNull();
React.startTransition(() => {
act(() => {
rerender(<App suspend={true} />);
});
});
expect(document.querySelector('[data-test-id=spinner]')).toBeNull();
await act(async () => {
resolved = true;
resolve();
});
expect(document.querySelector('[data-test-id=spinner]')).toBeNull();
expect(container.textContent).toBe('was suspended');
});
}
});
describe('throw in prod mode', () => {
it('warns if you try to use act() in prod mode', () => {
expect(() => act(() => {})).toThrow(
'act(...) is not supported in production builds of React',
);
});
});
});
}