let React;
let ReactNoop;
let act;
let use;
let Suspense;
let DiscreteEventPriority;
let startTransition;
let waitForMicrotasks;
let Scheduler;
let assertLog;
describe('isomorphic act()', () => {
beforeEach(() => {
React = require('react');
Scheduler = require('scheduler');
ReactNoop = require('react-noop-renderer');
DiscreteEventPriority =
require('react-reconciler/constants').DiscreteEventPriority;
act = React.act;
use = React.use;
Suspense = React.Suspense;
startTransition = React.startTransition;
waitForMicrotasks = require('internal-test-utils').waitForMicrotasks;
assertLog = require('internal-test-utils').assertLog;
});
beforeEach(() => {
global.IS_REACT_ACT_ENVIRONMENT = true;
});
afterEach(() => {
jest.restoreAllMocks();
});
function Text({text}) {
Scheduler.log(text);
return text;
}
it('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 waitForMicrotasks();
expect(root).toMatchRenderedOutput('A');
global.IS_REACT_ACT_ENVIRONMENT = true;
act(() => {
ReactNoop.unstable_runWithPriority(DiscreteEventPriority, () => {
root.render('B');
});
});
expect(root).toMatchRenderedOutput('B');
});
it('return value – sync callback', async () => {
expect(await act(() => 'hi')).toEqual('hi');
});
it('return value – sync callback, nested', async () => {
const returnValue = await act(() => {
return act(() => 'hi');
});
expect(returnValue).toEqual('hi');
});
it('return value – async callback', async () => {
const returnValue = await act(async () => {
return await Promise.resolve('hi');
});
expect(returnValue).toEqual('hi');
});
it('return value – async callback, nested', async () => {
const returnValue = await act(async () => {
return await act(async () => {
return await Promise.resolve('hi');
});
});
expect(returnValue).toEqual('hi');
});
it('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');
});
it('in legacy mode, in an async scope, updates are batched until the first `await`', async () => {
const root = ReactNoop.createLegacyRoot();
await act(async () => {
queueMicrotask(() => {
Scheduler.log('Current tree in microtask: ' + root.getChildrenAsJSX());
root.render(<Text text="C" />);
});
root.render(<Text text="A" />);
root.render(<Text text="B" />);
await null;
assertLog([
'B',
'Current tree in microtask: B',
'C',
]);
root.render(<Text text="D" />);
root.render(<Text text="E" />);
assertLog(['D', 'E']);
});
});
it('in legacy mode, in an async scope, updates are batched until the first `await` (regression test: batchedUpdates)', async () => {
const root = ReactNoop.createLegacyRoot();
await act(async () => {
queueMicrotask(() => {
Scheduler.log('Current tree in microtask: ' + root.getChildrenAsJSX());
root.render(<Text text="C" />);
});
ReactNoop.batchedUpdates(() => {
root.render(<Text text="A" />);
root.render(<Text text="B" />);
});
await null;
assertLog([
'B',
'Current tree in microtask: B',
'C',
]);
root.render(<Text text="D" />);
root.render(<Text text="E" />);
assertLog(['D', 'E']);
});
});
it('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');
});
it('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');
});
it('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 waitForMicrotasks();
expect(console.error).toHaveBeenCalledTimes(1);
expect(console.error.mock.calls[0][0]).toContain(
'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(() => ...)',
);
});
it('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 waitForMicrotasks();
expect(console.error).toHaveBeenCalledTimes(0);
await act(async () => {
resolvePromise();
});
expect(root).toMatchRenderedOutput('Async');
});
});