let React;
let ReactNoop;
let act;
let use;
let Suspense;
let DiscreteEventPriority;
let startTransition;
describe('isomorphic act()', () => {
beforeEach(() => {
React = require('react');
ReactNoop = require('react-noop-renderer');
DiscreteEventPriority =
require('react-reconciler/constants').DiscreteEventPriority;
act = React.unstable_act;
use = React.use;
Suspense = React.Suspense;
startTransition = React.startTransition;
});
beforeEach(() => {
global.IS_REACT_ACT_ENVIRONMENT = true;
});
afterEach(() => {
jest.restoreAllMocks();
});
test('bypasses queueMicrotask', async () => {
const root = ReactNoop.createRoot();
global.IS_REACT_ACT_ENVIRONMENT = false;
ReactNoop.unstable_runWithPriority(DiscreteEventPriority, () => {
root.render('A');
});
expect(root).toMatchRenderedOutput(null);
await null;
expect(root).toMatchRenderedOutput('A');
global.IS_REACT_ACT_ENVIRONMENT = true;
act(() => {
ReactNoop.unstable_runWithPriority(DiscreteEventPriority, () => {
root.render('B');
});
});
expect(root).toMatchRenderedOutput('B');
});
test('return value – sync callback', async () => {
expect(await act(() => 'hi')).toEqual('hi');
});
test('return value – sync callback, nested', async () => {
const returnValue = await act(() => {
return act(() => 'hi');
});
expect(returnValue).toEqual('hi');
});
test('return value – async callback', async () => {
const returnValue = await act(async () => {
return await Promise.resolve('hi');
});
expect(returnValue).toEqual('hi');
});
test('return value – async callback, nested', async () => {
const returnValue = await act(async () => {
return await act(async () => {
return await Promise.resolve('hi');
});
});
expect(returnValue).toEqual('hi');
});
test('in legacy mode, updates are batched', () => {
const root = ReactNoop.createLegacyRoot();
root.render('A');
expect(root).toMatchRenderedOutput('A');
act(() => {
root.render('B');
expect(root).toMatchRenderedOutput('A');
ReactNoop.batchedUpdates(() => {
root.render('C');
});
expect(root).toMatchRenderedOutput('A');
});
expect(root).toMatchRenderedOutput('C');
});
test('in legacy mode, in an async scope, updates are batched until the first `await`', async () => {
const root = ReactNoop.createLegacyRoot();
await act(async () => {
root.render('A');
root.render('B');
expect(root).toMatchRenderedOutput(null);
await null;
expect(root).toMatchRenderedOutput('B');
root.render('C');
expect(root).toMatchRenderedOutput('C');
});
});
test('unwraps promises by yielding to microtasks (async act scope)', async () => {
const promise = Promise.resolve('Async');
function Fallback() {
throw new Error('Fallback should never be rendered');
}
function App() {
return use(promise);
}
const root = ReactNoop.createRoot();
await act(async () => {
startTransition(() => {
root.render(
<Suspense fallback={<Fallback />}>
<App />
</Suspense>,
);
});
});
expect(root).toMatchRenderedOutput('Async');
});
test('unwraps promises by yielding to microtasks (non-async act scope)', async () => {
const promise = Promise.resolve('Async');
function Fallback() {
throw new Error('Fallback should never be rendered');
}
function App() {
return use(promise);
}
const root = ReactNoop.createRoot();
await act(() => {
startTransition(() => {
root.render(
<Suspense fallback={<Fallback />}>
<App />
</Suspense>,
);
});
});
expect(root).toMatchRenderedOutput('Async');
});
test('warns if a promise is used in a non-awaited `act` scope', async () => {
const promise = new Promise(() => {});
function Fallback() {
throw new Error('Fallback should never be rendered');
}
function App() {
return use(promise);
}
spyOnDev(console, 'error').mockImplementation(() => {});
const root = ReactNoop.createRoot();
act(() => {
startTransition(() => {
root.render(
<Suspense fallback={<Fallback />}>
<App />
</Suspense>,
);
});
});
await null;
await null;
await null;
expect(console.error).toHaveBeenCalledTimes(1);
expect(console.error.mock.calls[0][0]).toContain(
'Warning: A component suspended inside an `act` scope, but the `act` ' +
'call was not awaited. When testing React components that ' +
'depend on asynchronous data, you must await the result:\n\n' +
'await act(() => ...)',
);
});
test('does not warn when suspending via legacy `throw` API in non-awaited `act` scope', async () => {
let didResolve = false;
let resolvePromise;
const promise = new Promise(r => {
resolvePromise = () => {
didResolve = true;
r();
};
});
function Fallback() {
return 'Loading...';
}
function App() {
if (!didResolve) {
throw promise;
}
return 'Async';
}
spyOnDev(console, 'error').mockImplementation(() => {});
const root = ReactNoop.createRoot();
act(() => {
startTransition(() => {
root.render(
<Suspense fallback={<Fallback />}>
<App />
</Suspense>,
);
});
});
expect(root).toMatchRenderedOutput('Loading...');
await null;
await null;
await null;
expect(console.error).toHaveBeenCalledTimes(0);
await act(async () => {
resolvePromise();
});
expect(root).toMatchRenderedOutput('Async');
});
});