/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @emails react-core
*/
// sanity tests to make sure act() works without a mocked scheduler
let React;
let ReactDOM;
let act;
let container;
let yields;
function clearLog() {
try {
return yields;
} finally {
yields = [];
}
}
function render(el, dom) {
ReactDOM.render(el, dom);
}
function unmount(dom) {
ReactDOM.unmountComponentAtNode(dom);
}
beforeEach(() => {
jest.resetModules();
jest.unmock('scheduler');
yields = [];
React = require('react');
ReactDOM = require('react-dom');
act = require('react-dom/test-utils').act;
container = document.createElement('div');
document.body.appendChild(container);
});
afterEach(() => {
unmount(container);
document.body.removeChild(container);
});
// @gate __DEV__
it('can use act to flush effects', () => {
function App() {
React.useEffect(() => {
yields.push(100);
});
return null;
}
act(() => {
render(<App />, container);
});
expect(clearLog()).toEqual([100]);
});
// @gate __DEV__
it('flushes effects on every call', () => {
function App() {
const [ctr, setCtr] = React.useState(0);
React.useEffect(() => {
yields.push(ctr);
});
return (
<button id="button" onClick={() => setCtr(x => x + 1)}>
{ctr}
</button>
);
}
act(() => {
render(<App />, container);
});
expect(clearLog()).toEqual([0]);
const button = container.querySelector('#button');
function click() {
button.dispatchEvent(new MouseEvent('click', {bubbles: true}));
}
act(() => {
click();
click();
click();
});
// it consolidates the 3 updates, then fires the effect
expect(clearLog()).toEqual([3]);
act(click);
expect(clearLog()).toEqual([4]);
act(click);
expect(clearLog()).toEqual([5]);
expect(button.innerHTML).toEqual('5');
});
// @gate __DEV__
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).toEqual('5');
});
// @gate __DEV__
it('should flush effects only on exiting the outermost act', () => {
function App() {
React.useEffect(() => {
yields.push(0);
});
return null;
}
// let's nest a couple of act() calls
act(() => {
act(() => {
render(<App />, container);
});
// the effect wouldn't have yielded yet because
// we're still inside an act() scope
expect(clearLog()).toEqual([]);
});
// but after exiting the last one, effects get flushed
expect(clearLog()).toEqual([0]);
});
// @gate __DEV__
it('can handle cascading promises', async () => {
// this component triggers an effect, that waits a tick,
// then sets state. repeats this 5 times.
function App() {
const [state, setState] = React.useState(0);
async function ticker() {
await null;
setState(x => x + 1);
}
React.useEffect(() => {
yields.push(state);
ticker();
}, [Math.min(state, 4)]);
return state;
}
await act(async () => {
render(<App />, container);
});
// all 5 ticks present and accounted for
expect(clearLog()).toEqual([0, 1, 2, 3, 4]);
expect(container.innerHTML).toBe('5');
});