Codemod act -> await act (4/?) (#26338)

Similar to the rationale for `waitFor` (see #26285), we should always
await the result of an `act` call so that microtasks have a chance to
fire.

This only affects the internal `act` that we use in our repo, for now.
In the public `act` API, we don't yet require this; however, we
effectively will for any update that triggers suspense once `use` lands.
So we likely will start warning in an upcoming minor.
This commit is contained in:
Andrew Clark 2023-03-08 11:36:05 -05:00 committed by GitHub
parent 9fb2469a63
commit 702fc984e6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 913 additions and 1081 deletions

View File

@ -984,7 +984,7 @@ describe('ReactHooksInspectionIntegration', () => {
children: ['count: ', '1'],
});
act(incrementCount);
await act(async () => incrementCount());
expect(renderer.toJSON()).toEqual({
type: 'div',
props: {},

View File

@ -1069,8 +1069,8 @@ describe('InspectedElement', () => {
});
async function loadPath(path) {
TestUtilsAct(() => {
TestRendererAct(() => {
await TestUtilsAct(async () => {
await TestRendererAct(async () => {
inspectElementPath(path);
jest.runOnlyPendingTimers();
});
@ -1224,8 +1224,8 @@ describe('InspectedElement', () => {
});
async function loadPath(path) {
TestUtilsAct(() => {
TestRendererAct(() => {
await TestUtilsAct(async () => {
await TestRendererAct(async () => {
inspectElementPath(path);
jest.runOnlyPendingTimers();
});
@ -1306,8 +1306,8 @@ describe('InspectedElement', () => {
});
async function loadPath(path) {
TestUtilsAct(() => {
TestRendererAct(() => {
await TestUtilsAct(async () => {
await TestRendererAct(async () => {
inspectElementPath(path);
jest.runOnlyPendingTimers();
});
@ -1375,8 +1375,8 @@ describe('InspectedElement', () => {
}
`);
TestRendererAct(() => {
TestUtilsAct(() => {
await TestRendererAct(async () => {
await TestUtilsAct(async () => {
legacyRender(
<Example
nestedObject={{
@ -1469,8 +1469,8 @@ describe('InspectedElement', () => {
});
async function loadPath(path) {
TestUtilsAct(() => {
TestRendererAct(() => {
await TestUtilsAct(async () => {
await TestRendererAct(async () => {
inspectElementPath(path);
jest.runOnlyPendingTimers();
});
@ -1513,8 +1513,8 @@ describe('InspectedElement', () => {
}
`);
TestRendererAct(() => {
TestUtilsAct(() => {
await TestRendererAct(async () => {
await TestUtilsAct(async () => {
legacyRender(
<Example
nestedObject={{
@ -1596,8 +1596,8 @@ describe('InspectedElement', () => {
});
async function loadPath(path) {
TestUtilsAct(() => {
TestRendererAct(() => {
await TestUtilsAct(async () => {
await TestRendererAct(async () => {
inspectElementPath(path);
jest.runOnlyPendingTimers();
});
@ -1618,7 +1618,7 @@ describe('InspectedElement', () => {
}
`);
TestUtilsAct(() => {
await TestUtilsAct(async () => {
legacyRender(
<Example
nestedObject={{
@ -1640,11 +1640,9 @@ describe('InspectedElement', () => {
expect(inspectedElement.props).toMatchInlineSnapshot(`
{
"nestedObject": {
"a": {
"b": {
"value": 2,
},
"value": 2,
"a": Dehydrated {
"preview_short": {},
"preview_long": {b: {}, value: 2},
},
"value": 2,
},
@ -2833,7 +2831,7 @@ describe('InspectedElement', () => {
};
const toggleError = async forceError => {
await withErrorsOrWarningsIgnored(['ErrorBoundary'], async () => {
await TestUtilsAct(() => {
await TestUtilsAct(async () => {
bridge.send('overrideError', {
id: targetErrorBoundaryID,
rendererID: store.getRendererIDForElement(targetErrorBoundaryID),
@ -2842,7 +2840,7 @@ describe('InspectedElement', () => {
});
});
TestUtilsAct(() => {
await TestUtilsAct(async () => {
jest.runOnlyPendingTimers();
});
};

View File

@ -19,10 +19,8 @@ describe('Store component filters', () => {
let utils;
let internalAct;
const act = (callback: Function) => {
internalAct(() => {
callback();
});
const act = async (callback: Function) => {
internalAct(callback);
jest.runAllTimers(); // Flush Bridge operations
};
@ -42,15 +40,15 @@ describe('Store component filters', () => {
});
// @reactVersion >= 16.0
it('should throw if filters are updated while profiling', () => {
act(() => store.profilerStore.startProfiling());
it('should throw if filters are updated while profiling', async () => {
await act(async () => store.profilerStore.startProfiling());
expect(() => (store.componentFilters = [])).toThrow(
'Cannot modify filter preferences while profiling',
);
});
// @reactVersion >= 16.0
it('should support filtering by element type', () => {
it('should support filtering by element type', async () => {
class ClassComponent extends React.Component<{children: React$Node}> {
render() {
return <div>{this.props.children}</div>;
@ -58,7 +56,7 @@ describe('Store component filters', () => {
}
const FunctionComponent = () => <div>Hi</div>;
act(() =>
await act(async () =>
legacyRender(
<ClassComponent>
<FunctionComponent />
@ -74,8 +72,8 @@ describe('Store component filters', () => {
<div>
`);
act(
() =>
await act(
async () =>
(store.componentFilters = [
utils.createElementTypeFilter(Types.ElementTypeHostComponent),
]),
@ -86,8 +84,8 @@ describe('Store component filters', () => {
<FunctionComponent>
`);
act(
() =>
await act(
async () =>
(store.componentFilters = [
utils.createElementTypeFilter(Types.ElementTypeClass),
]),
@ -99,8 +97,8 @@ describe('Store component filters', () => {
<div>
`);
act(
() =>
await act(
async () =>
(store.componentFilters = [
utils.createElementTypeFilter(Types.ElementTypeClass),
utils.createElementTypeFilter(Types.ElementTypeFunction),
@ -112,8 +110,8 @@ describe('Store component filters', () => {
<div>
`);
act(
() =>
await act(
async () =>
(store.componentFilters = [
utils.createElementTypeFilter(Types.ElementTypeClass, false),
utils.createElementTypeFilter(Types.ElementTypeFunction, false),
@ -127,7 +125,7 @@ describe('Store component filters', () => {
<div>
`);
act(() => (store.componentFilters = []));
await act(async () => (store.componentFilters = []));
expect(store).toMatchInlineSnapshot(`
[root]
<ClassComponent>
@ -138,18 +136,20 @@ describe('Store component filters', () => {
});
// @reactVersion >= 16.0
it('should ignore invalid ElementTypeRoot filter', () => {
it('should ignore invalid ElementTypeRoot filter', async () => {
const Component = () => <div>Hi</div>;
act(() => legacyRender(<Component />, document.createElement('div')));
await act(async () =>
legacyRender(<Component />, document.createElement('div')),
);
expect(store).toMatchInlineSnapshot(`
[root]
<Component>
<div>
`);
act(
() =>
await act(
async () =>
(store.componentFilters = [
utils.createElementTypeFilter(Types.ElementTypeRoot),
]),
@ -163,13 +163,13 @@ describe('Store component filters', () => {
});
// @reactVersion >= 16.2
it('should filter by display name', () => {
it('should filter by display name', async () => {
const Text = ({label}) => label;
const Foo = () => <Text label="foo" />;
const Bar = () => <Text label="bar" />;
const Baz = () => <Text label="baz" />;
act(() =>
await act(async () =>
legacyRender(
<React.Fragment>
<Foo />
@ -189,8 +189,9 @@ describe('Store component filters', () => {
<Text>
`);
act(
() => (store.componentFilters = [utils.createDisplayNameFilter('Foo')]),
await act(
async () =>
(store.componentFilters = [utils.createDisplayNameFilter('Foo')]),
);
expect(store).toMatchInlineSnapshot(`
[root]
@ -201,7 +202,10 @@ describe('Store component filters', () => {
<Text>
`);
act(() => (store.componentFilters = [utils.createDisplayNameFilter('Ba')]));
await act(
async () =>
(store.componentFilters = [utils.createDisplayNameFilter('Ba')]),
);
expect(store).toMatchInlineSnapshot(`
[root]
<Foo>
@ -210,8 +214,9 @@ describe('Store component filters', () => {
<Text>
`);
act(
() => (store.componentFilters = [utils.createDisplayNameFilter('B.z')]),
await act(
async () =>
(store.componentFilters = [utils.createDisplayNameFilter('B.z')]),
);
expect(store).toMatchInlineSnapshot(`
[root]
@ -224,18 +229,20 @@ describe('Store component filters', () => {
});
// @reactVersion >= 16.0
it('should filter by path', () => {
it('should filter by path', async () => {
const Component = () => <div>Hi</div>;
act(() => legacyRender(<Component />, document.createElement('div')));
await act(async () =>
legacyRender(<Component />, document.createElement('div')),
);
expect(store).toMatchInlineSnapshot(`
[root]
<Component>
<div>
`);
act(
() =>
await act(
async () =>
(store.componentFilters = [
utils.createLocationFilter(__filename.replace(__dirname, '')),
]),
@ -243,8 +250,8 @@ describe('Store component filters', () => {
expect(store).toMatchInlineSnapshot(`[root]`);
act(
() =>
await act(
async () =>
(store.componentFilters = [
utils.createLocationFilter('this:is:a:made:up:path'),
]),
@ -258,14 +265,14 @@ describe('Store component filters', () => {
});
// @reactVersion >= 16.0
it('should filter HOCs', () => {
it('should filter HOCs', async () => {
const Component = () => <div>Hi</div>;
const Foo = () => <Component />;
Foo.displayName = 'Foo(Component)';
const Bar = () => <Foo />;
Bar.displayName = 'Bar(Foo(Component))';
act(() => legacyRender(<Bar />, document.createElement('div')));
await act(async () => legacyRender(<Bar />, document.createElement('div')));
expect(store).toMatchInlineSnapshot(`
[root]
<Component> [Bar][Foo]
@ -274,14 +281,18 @@ describe('Store component filters', () => {
<div>
`);
act(() => (store.componentFilters = [utils.createHOCFilter(true)]));
await act(
async () => (store.componentFilters = [utils.createHOCFilter(true)]),
);
expect(store).toMatchInlineSnapshot(`
[root]
<Component>
<div>
`);
act(() => (store.componentFilters = [utils.createHOCFilter(false)]));
await act(
async () => (store.componentFilters = [utils.createHOCFilter(false)]),
);
expect(store).toMatchInlineSnapshot(`
[root]
<Component> [Bar][Foo]
@ -292,29 +303,31 @@ describe('Store component filters', () => {
});
// @reactVersion >= 16.0
it('should not send a bridge update if the set of enabled filters has not changed', () => {
act(() => (store.componentFilters = [utils.createHOCFilter(true)]));
it('should not send a bridge update if the set of enabled filters has not changed', async () => {
await act(
async () => (store.componentFilters = [utils.createHOCFilter(true)]),
);
bridge.addListener('updateComponentFilters', componentFilters => {
throw Error('Unexpected component update');
});
act(
() =>
await act(
async () =>
(store.componentFilters = [
utils.createHOCFilter(false),
utils.createHOCFilter(true),
]),
);
act(
() =>
await act(
async () =>
(store.componentFilters = [
utils.createHOCFilter(true),
utils.createLocationFilter('abc', false),
]),
);
act(
() =>
await act(
async () =>
(store.componentFilters = [
utils.createHOCFilter(true),
utils.createElementTypeFilter(Types.ElementTypeHostComponent, false),
@ -323,7 +336,7 @@ describe('Store component filters', () => {
});
// @reactVersion >= 18.0
it('should not break when Suspense nodes are filtered from the tree', () => {
it('should not break when Suspense nodes are filtered from the tree', async () => {
const promise = new Promise(() => {});
const Loading = () => <div>Loading...</div>;
@ -346,7 +359,9 @@ describe('Store component filters', () => {
];
const container = document.createElement('div');
act(() => legacyRender(<Wrapper shouldSuspend={true} />, container));
await act(async () =>
legacyRender(<Wrapper shouldSuspend={true} />, container),
);
expect(store).toMatchInlineSnapshot(`
[root]
<Wrapper>
@ -354,15 +369,21 @@ describe('Store component filters', () => {
<div>
`);
act(() => legacyRender(<Wrapper shouldSuspend={false} />, container));
await act(async () =>
legacyRender(<Wrapper shouldSuspend={false} />, container),
);
expect(store).toMatchInlineSnapshot(`
1, 0
[root]
<Wrapper>
<Component>
`);
act(() => legacyRender(<Wrapper shouldSuspend={true} />, container));
await act(async () =>
legacyRender(<Wrapper shouldSuspend={true} />, container),
);
expect(store).toMatchInlineSnapshot(`
2, 0
[root]
<Wrapper>
<Loading>
@ -372,7 +393,7 @@ describe('Store component filters', () => {
describe('inline errors and warnings', () => {
// @reactVersion >= 17.0
it('only counts for unfiltered components', () => {
it('only counts for unfiltered components', async () => {
function ComponentWithWarning() {
console.warn('test-only: render warning');
return null;
@ -395,31 +416,29 @@ describe('Store component filters', () => {
require('react-dom');
const container = document.createElement('div');
await act(
async () =>
(store.componentFilters = [
utils.createDisplayNameFilter('Warning'),
utils.createDisplayNameFilter('Error'),
]),
);
utils.withErrorsOrWarningsIgnored(['test-only:'], () => {
act(
() =>
(store.componentFilters = [
utils.createDisplayNameFilter('Warning'),
utils.createDisplayNameFilter('Error'),
]),
);
act(() =>
legacyRender(
<React.Fragment>
<ComponentWithError />
<ComponentWithWarning />
<ComponentWithWarningAndError />
</React.Fragment>,
container,
),
legacyRender(
<React.Fragment>
<ComponentWithError />
<ComponentWithWarning />
<ComponentWithWarningAndError />
</React.Fragment>,
container,
);
});
expect(store).toMatchInlineSnapshot(`[root]`);
expect(store).toMatchInlineSnapshot(``);
expect(store.errorCount).toBe(0);
expect(store.warningCount).toBe(0);
act(() => (store.componentFilters = []));
await act(async () => (store.componentFilters = []));
expect(store).toMatchInlineSnapshot(`
2, 2
[root]
@ -428,8 +447,8 @@ describe('Store component filters', () => {
<ComponentWithWarningAndError>
`);
act(
() =>
await act(
async () =>
(store.componentFilters = [utils.createDisplayNameFilter('Warning')]),
);
expect(store).toMatchInlineSnapshot(`
@ -438,8 +457,8 @@ describe('Store component filters', () => {
<ComponentWithError>
`);
act(
() =>
await act(
async () =>
(store.componentFilters = [utils.createDisplayNameFilter('Error')]),
);
expect(store).toMatchInlineSnapshot(`
@ -448,8 +467,8 @@ describe('Store component filters', () => {
<ComponentWithWarning>
`);
act(
() =>
await act(
async () =>
(store.componentFilters = [
utils.createDisplayNameFilter('Warning'),
utils.createDisplayNameFilter('Error'),
@ -459,7 +478,7 @@ describe('Store component filters', () => {
expect(store.errorCount).toBe(0);
expect(store.warningCount).toBe(0);
act(() => (store.componentFilters = []));
await act(async () => (store.componentFilters = []));
expect(store).toMatchInlineSnapshot(`
2, 2
[root]

View File

@ -48,21 +48,16 @@ module.exports = function (initModules) {
// ====================================
// promisified version of ReactDOM.render()
function asyncReactDOMRender(reactElement, domElement, forceHydrate) {
return new Promise(resolve => {
if (forceHydrate) {
act(() => {
ReactDOM.hydrate(reactElement, domElement);
});
} else {
act(() => {
ReactDOM.render(reactElement, domElement);
});
}
// We can't use the callback for resolution because that will not catch
// errors. They're thrown.
resolve();
});
async function asyncReactDOMRender(reactElement, domElement, forceHydrate) {
if (forceHydrate) {
await act(async () => {
ReactDOM.hydrate(reactElement, domElement);
});
} else {
await act(async () => {
ReactDOM.render(reactElement, domElement);
});
}
}
// performs fn asynchronously and expects count errors logged to console.error.
// will fail the test if the count of errors logged is not equal to count.

View File

@ -2520,7 +2520,7 @@ describe('DOMPluginEventSystem', () => {
});
// @gate www
it('beforeblur and afterblur are called after a focused element is suspended', () => {
it('beforeblur and afterblur are called after a focused element is suspended', async () => {
const log = [];
// We have to persist here because we want to read relatedTarget later.
const onAfterBlur = jest.fn(e => {
@ -2575,7 +2575,7 @@ describe('DOMPluginEventSystem', () => {
const root = ReactDOMClient.createRoot(container2);
act(() => {
await act(async () => {
root.render(<Component />);
});
jest.runAllTimers();
@ -2587,7 +2587,7 @@ describe('DOMPluginEventSystem', () => {
expect(onAfterBlur).toHaveBeenCalledTimes(0);
suspend = true;
act(() => {
await act(async () => {
root.render(<Component />);
});
jest.runAllTimers();
@ -2604,7 +2604,7 @@ describe('DOMPluginEventSystem', () => {
});
// @gate www
it('beforeblur should skip handlers from a deleted subtree after the focused element is suspended', () => {
it('beforeblur should skip handlers from a deleted subtree after the focused element is suspended', async () => {
const onBeforeBlur = jest.fn();
const innerRef = React.createRef();
const innerRef2 = React.createRef();
@ -2661,7 +2661,7 @@ describe('DOMPluginEventSystem', () => {
const root = ReactDOMClient.createRoot(container2);
act(() => {
await act(async () => {
root.render(<Component />);
});
jest.runAllTimers();
@ -2672,7 +2672,7 @@ describe('DOMPluginEventSystem', () => {
expect(onBeforeBlur).toHaveBeenCalledTimes(0);
suspend = true;
act(() => {
await act(async () => {
root.render(<Component />);
});
jest.runAllTimers();
@ -2684,17 +2684,17 @@ describe('DOMPluginEventSystem', () => {
});
// @gate www
it('regression: does not fire beforeblur/afterblur if target is already hidden', () => {
it('regression: does not fire beforeblur/afterblur if target is already hidden', async () => {
const Suspense = React.Suspense;
let suspend = false;
const promise = Promise.resolve();
const fakePromise = {then() {}};
const setBeforeBlurHandle =
ReactDOM.unstable_createEventHandle('beforeblur');
const innerRef = React.createRef();
function Child() {
if (suspend) {
throw promise;
throw fakePromise;
}
return <input ref={innerRef} />;
}
@ -2726,7 +2726,7 @@ describe('DOMPluginEventSystem', () => {
document.body.appendChild(container2);
const root = ReactDOMClient.createRoot(container2);
act(() => {
await act(async () => {
root.render(<Component />);
});
@ -2737,7 +2737,7 @@ describe('DOMPluginEventSystem', () => {
// Suspend. This hides the input node, causing it to lose focus.
suspend = true;
act(() => {
await act(async () => {
root.render(<Component />);
});

View File

@ -20,6 +20,7 @@ let ReactDOMServer;
let act;
let assertLog;
let waitForAll;
let waitForThrow;
describe('ReactHooks', () => {
beforeEach(() => {
@ -36,6 +37,7 @@ describe('ReactHooks', () => {
const InternalTestUtils = require('internal-test-utils');
assertLog = InternalTestUtils.assertLog;
waitForAll = InternalTestUtils.waitForAll;
waitForThrow = InternalTestUtils.waitForThrow;
});
if (__DEV__) {
@ -90,7 +92,7 @@ describe('ReactHooks', () => {
expect(root).toMatchRenderedOutput('0, 0');
// Normal update
act(() => {
await act(async () => {
setCounter1(1);
setCounter2(1);
});
@ -98,12 +100,12 @@ describe('ReactHooks', () => {
assertLog(['Parent: 1, 1', 'Child: 1, 1', 'Effect: 1, 1']);
// Update that bails out.
act(() => setCounter1(1));
await act(async () => setCounter1(1));
assertLog(['Parent: 1, 1']);
// This time, one of the state updates but the other one doesn't. So we
// can't bail out.
act(() => {
await act(async () => {
setCounter1(1);
setCounter2(2);
});
@ -111,7 +113,7 @@ describe('ReactHooks', () => {
assertLog(['Parent: 1, 2', 'Child: 1, 2', 'Effect: 1, 2']);
// Lots of updates that eventually resolve to the current values.
act(() => {
await act(async () => {
setCounter1(9);
setCounter2(3);
setCounter1(4);
@ -125,7 +127,7 @@ describe('ReactHooks', () => {
assertLog(['Parent: 1, 2']);
// prepare to check SameValue
act(() => {
await act(async () => {
setCounter1(0 / -1);
setCounter2(NaN);
});
@ -133,7 +135,7 @@ describe('ReactHooks', () => {
assertLog(['Parent: 0, NaN', 'Child: 0, NaN', 'Effect: 0, NaN']);
// check if re-setting to negative 0 / NaN still bails out
act(() => {
await act(async () => {
setCounter1(0 / -1);
setCounter2(NaN);
setCounter2(Infinity);
@ -143,7 +145,7 @@ describe('ReactHooks', () => {
assertLog(['Parent: 0, NaN']);
// check if changing negative 0 to positive 0 does not bail out
act(() => {
await act(async () => {
setCounter1(0);
});
assertLog(['Parent: 0, NaN', 'Child: 0, NaN', 'Effect: 0, NaN']);
@ -178,7 +180,7 @@ describe('ReactHooks', () => {
expect(root).toMatchRenderedOutput('0, 0 (light)');
// Normal update
act(() => {
await act(async () => {
setCounter1(1);
setCounter2(1);
});
@ -186,12 +188,12 @@ describe('ReactHooks', () => {
assertLog(['Parent: 1, 1 (light)', 'Child: 1, 1 (light)']);
// Update that bails out.
act(() => setCounter1(1));
await act(async () => setCounter1(1));
assertLog(['Parent: 1, 1 (light)']);
// This time, one of the state updates but the other one doesn't. So we
// can't bail out.
act(() => {
await act(async () => {
setCounter1(1);
setCounter2(2);
});
@ -200,7 +202,7 @@ describe('ReactHooks', () => {
// Updates bail out, but component still renders because props
// have changed
act(() => {
await act(async () => {
setCounter1(1);
setCounter2(2);
root.update(<Parent theme="dark" />);
@ -209,7 +211,7 @@ describe('ReactHooks', () => {
assertLog(['Parent: 1, 2 (dark)', 'Child: 1, 2 (dark)']);
// Both props and state bail out
act(() => {
await act(async () => {
setCounter1(1);
setCounter2(2);
root.update(<Parent theme="dark" />);
@ -235,8 +237,8 @@ describe('ReactHooks', () => {
await waitForAll(['Count: 0']);
expect(root).toMatchRenderedOutput('0');
expect(() => {
act(() =>
await expect(async () => {
await act(async () =>
setCounter(1, () => {
throw new Error('Expected to ignore the callback.');
}),
@ -269,8 +271,8 @@ describe('ReactHooks', () => {
await waitForAll(['Count: 0']);
expect(root).toMatchRenderedOutput('0');
expect(() => {
act(() =>
await expect(async () => {
await act(async () =>
dispatch(1, () => {
throw new Error('Expected to ignore the callback.');
}),
@ -321,7 +323,7 @@ describe('ReactHooks', () => {
return <Child text={text} />;
}
const root = ReactTestRenderer.create(null, {unstable_isConcurrent: true});
act(() => {
await act(async () => {
root.update(
<ThemeProvider>
<Parent />
@ -344,18 +346,18 @@ describe('ReactHooks', () => {
expect(root).toMatchRenderedOutput('0 (light)');
// Normal update
act(() => setCounter(1));
await act(async () => setCounter(1));
assertLog(['Parent: 1 (light)', 'Child: 1 (light)', 'Effect: 1 (light)']);
expect(root).toMatchRenderedOutput('1 (light)');
// Update that doesn't change state, so it bails out
act(() => setCounter(1));
await act(async () => setCounter(1));
assertLog(['Parent: 1 (light)']);
expect(root).toMatchRenderedOutput('1 (light)');
// Update that doesn't change state, but the context changes, too, so it
// can't bail out
act(() => {
await act(async () => {
setCounter(1);
setTheme('dark');
});
@ -394,7 +396,7 @@ describe('ReactHooks', () => {
expect(root).toMatchRenderedOutput('0');
// Normal update
act(() => setCounter(1));
await act(async () => setCounter(1));
assertLog(['Parent: 1', 'Child: 1', 'Effect: 1']);
expect(root).toMatchRenderedOutput('1');
@ -402,30 +404,30 @@ describe('ReactHooks', () => {
// because the alternate fiber has pending update priority, so we have to
// enter the render phase before we can bail out. But we bail out before
// rendering the child, and we don't fire any effects.
act(() => setCounter(1));
await act(async () => setCounter(1));
assertLog(['Parent: 1']);
expect(root).toMatchRenderedOutput('1');
// Update to the same state again. This times, neither fiber has pending
// update priority, so we can bail out before even entering the render phase.
act(() => setCounter(1));
await act(async () => setCounter(1));
await waitForAll([]);
expect(root).toMatchRenderedOutput('1');
// This changes the state to something different so it renders normally.
act(() => setCounter(2));
await act(async () => setCounter(2));
assertLog(['Parent: 2', 'Child: 2', 'Effect: 2']);
expect(root).toMatchRenderedOutput('2');
// prepare to check SameValue
act(() => {
await act(async () => {
setCounter(0);
});
assertLog(['Parent: 0', 'Child: 0', 'Effect: 0']);
expect(root).toMatchRenderedOutput('0');
// Update to the same state for the first time to flush the queue
act(() => {
await act(async () => {
setCounter(0);
});
@ -433,14 +435,14 @@ describe('ReactHooks', () => {
expect(root).toMatchRenderedOutput('0');
// Update again to the same state. Should bail out.
act(() => {
await act(async () => {
setCounter(0);
});
await waitForAll([]);
expect(root).toMatchRenderedOutput('0');
// Update to a different state (positive 0 to negative 0)
act(() => {
await act(async () => {
setCounter(0 / -1);
});
assertLog(['Parent: 0', 'Child: 0', 'Effect: 0']);
@ -615,7 +617,7 @@ describe('ReactHooks', () => {
]);
});
it('warns if deps is not an array', () => {
it('warns if deps is not an array', async () => {
const {useEffect, useLayoutEffect, useMemo, useCallback} = React;
function App(props) {
@ -626,8 +628,8 @@ describe('ReactHooks', () => {
return null;
}
expect(() => {
act(() => {
await expect(async () => {
await act(async () => {
ReactTestRenderer.create(<App deps={'hello'} />);
});
}).toErrorDev([
@ -640,8 +642,8 @@ describe('ReactHooks', () => {
'Warning: useCallback received a final argument that is not an array (instead, received `string`). ' +
'When specified, the final argument must be an array.',
]);
expect(() => {
act(() => {
await expect(async () => {
await act(async () => {
ReactTestRenderer.create(<App deps={100500} />);
});
}).toErrorDev([
@ -654,8 +656,8 @@ describe('ReactHooks', () => {
'Warning: useCallback received a final argument that is not an array (instead, received `number`). ' +
'When specified, the final argument must be an array.',
]);
expect(() => {
act(() => {
await expect(async () => {
await act(async () => {
ReactTestRenderer.create(<App deps={{}} />);
});
}).toErrorDev([
@ -669,7 +671,7 @@ describe('ReactHooks', () => {
'When specified, the final argument must be an array.',
]);
act(() => {
await act(async () => {
ReactTestRenderer.create(<App deps={[]} />);
ReactTestRenderer.create(<App deps={null} />);
ReactTestRenderer.create(<App deps={undefined} />);
@ -695,7 +697,7 @@ describe('ReactHooks', () => {
ReactTestRenderer.create(<App deps={undefined} />);
});
it('does not forget render phase useState updates inside an effect', () => {
it('does not forget render phase useState updates inside an effect', async () => {
const {useState, useEffect} = React;
function Counter() {
@ -712,13 +714,13 @@ describe('ReactHooks', () => {
}
const root = ReactTestRenderer.create(null);
act(() => {
await act(async () => {
root.update(<Counter />);
});
expect(root).toMatchRenderedOutput('4');
});
it('does not forget render phase useReducer updates inside an effect with hoisted reducer', () => {
it('does not forget render phase useReducer updates inside an effect with hoisted reducer', async () => {
const {useReducer, useEffect} = React;
const reducer = x => x + 1;
@ -736,13 +738,13 @@ describe('ReactHooks', () => {
}
const root = ReactTestRenderer.create(null);
act(() => {
await act(async () => {
root.update(<Counter />);
});
expect(root).toMatchRenderedOutput('4');
});
it('does not forget render phase useReducer updates inside an effect with inline reducer', () => {
it('does not forget render phase useReducer updates inside an effect with inline reducer', async () => {
const {useReducer, useEffect} = React;
function Counter() {
@ -759,7 +761,7 @@ describe('ReactHooks', () => {
}
const root = ReactTestRenderer.create(null);
act(() => {
await act(async () => {
root.update(<Counter />);
});
expect(root).toMatchRenderedOutput('4');
@ -914,7 +916,7 @@ describe('ReactHooks', () => {
});
// Throws because there's no runtime cost for being strict here.
it('throws when reading context inside useEffect', () => {
it('throws when reading context inside useEffect', async () => {
const {useEffect, createContext} = React;
const ReactCurrentDispatcher =
React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED
@ -928,14 +930,11 @@ describe('ReactHooks', () => {
return null;
}
expect(() => {
act(() => {
ReactTestRenderer.create(<App />);
});
}).toThrow(
await act(async () => {
ReactTestRenderer.create(<App />);
// The exact message doesn't matter, just make sure we don't allow this
'Context can only be read while React is rendering',
);
await waitForThrow('Context can only be read while React is rendering');
});
});
// Throws because there's no runtime cost for being strict here.
@ -1070,7 +1069,7 @@ describe('ReactHooks', () => {
);
});
it('resets warning internal state when interrupted by an error', () => {
it('resets warning internal state when interrupted by an error', async () => {
const ReactCurrentDispatcher =
React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED
.ReactCurrentDispatcher;
@ -1132,7 +1131,7 @@ describe('ReactHooks', () => {
}
// Verify it doesn't think we're still inside a Hook.
// Should have no warnings.
act(() => {
await act(async () => {
ReactTestRenderer.create(<Valid />);
});
@ -1473,7 +1472,7 @@ describe('ReactHooks', () => {
.replace('use', '')
.replace('Helper', '');
it(`warns on using differently ordered hooks (${hookNameA}, ${hookNameB}) on subsequent renders`, () => {
it(`warns on using differently ordered hooks (${hookNameA}, ${hookNameB}) on subsequent renders`, async () => {
function App(props) {
/* eslint-disable no-unused-vars */
if (props.update) {
@ -1489,12 +1488,12 @@ describe('ReactHooks', () => {
/* eslint-enable no-unused-vars */
}
let root;
act(() => {
await act(async () => {
root = ReactTestRenderer.create(<App update={false} />);
});
expect(() => {
await expect(async () => {
try {
act(() => {
await act(async () => {
root.update(<App update={true} />);
});
} catch (error) {
@ -1515,7 +1514,7 @@ describe('ReactHooks', () => {
// further warnings for this component are silenced
try {
act(() => {
await act(async () => {
root.update(<App update={false} />);
});
} catch (error) {
@ -1525,7 +1524,7 @@ describe('ReactHooks', () => {
}
});
it(`warns when more hooks (${hookNameA}, ${hookNameB}) are used during update than mount`, () => {
it(`warns when more hooks (${hookNameA}, ${hookNameB}) are used during update than mount`, async () => {
function App(props) {
/* eslint-disable no-unused-vars */
if (props.update) {
@ -1538,13 +1537,13 @@ describe('ReactHooks', () => {
/* eslint-enable no-unused-vars */
}
let root;
act(() => {
await act(async () => {
root = ReactTestRenderer.create(<App update={false} />);
});
expect(() => {
await expect(async () => {
try {
act(() => {
await act(async () => {
root.update(<App update={true} />);
});
} catch (error) {
@ -1579,7 +1578,7 @@ describe('ReactHooks', () => {
.replace('use', '')
.replace('Helper', '');
it(`warns when fewer hooks (${hookNameA}, ${hookNameB}) are used during update than mount`, () => {
it(`warns when fewer hooks (${hookNameA}, ${hookNameB}) are used during update than mount`, async () => {
function App(props) {
/* eslint-disable no-unused-vars */
if (props.update) {
@ -1592,15 +1591,15 @@ describe('ReactHooks', () => {
/* eslint-enable no-unused-vars */
}
let root;
act(() => {
await act(async () => {
root = ReactTestRenderer.create(<App update={false} />);
});
expect(() => {
act(() => {
await act(async () => {
expect(() => {
root.update(<App update={true} />);
});
}).toThrow('Rendered fewer hooks than expected.');
}).toThrow('Rendered fewer hooks than expected. ');
});
});
});
@ -1726,7 +1725,7 @@ describe('ReactHooks', () => {
});
// Regression test for https://github.com/facebook/react/issues/15057
it('does not fire a false positive warning when previous effect unmounts the component', () => {
it('does not fire a false positive warning when previous effect unmounts the component', async () => {
const {useState, useEffect} = React;
let globalListener;
@ -1762,7 +1761,7 @@ describe('ReactHooks', () => {
return null;
}
act(() => {
await act(async () => {
ReactTestRenderer.create(<A />);
});
@ -1912,7 +1911,7 @@ describe('ReactHooks', () => {
}
let root;
act(() => {
await act(async () => {
root = ReactTestRenderer.create(
<ErrorBoundary>
<Thrower />
@ -1921,7 +1920,7 @@ describe('ReactHooks', () => {
});
expect(root).toMatchRenderedOutput('Throw!');
act(() => setShouldThrow(true));
await act(async () => setShouldThrow(true));
expect(root).toMatchRenderedOutput('Error!');
});
});

View File

@ -294,11 +294,11 @@ describe('ReactHooksWithNoopRenderer', () => {
await waitForAll(['Count: 0']);
expect(ReactNoop).toMatchRenderedOutput(<span prop="Count: 0" />);
act(() => counter.current.updateCount(1));
await act(async () => counter.current.updateCount(1));
assertLog(['Count: 1']);
expect(ReactNoop).toMatchRenderedOutput(<span prop="Count: 1" />);
act(() => counter.current.updateCount(count => count + 10));
await act(async () => counter.current.updateCount(count => count + 10));
assertLog(['Count: 11']);
expect(ReactNoop).toMatchRenderedOutput(<span prop="Count: 11" />);
});
@ -318,7 +318,7 @@ describe('ReactHooksWithNoopRenderer', () => {
await waitForAll(['getInitialState', 'Count: 42']);
expect(ReactNoop).toMatchRenderedOutput(<span prop="Count: 42" />);
act(() => counter.current.updateCount(7));
await act(async () => counter.current.updateCount(7));
assertLog(['Count: 7']);
expect(ReactNoop).toMatchRenderedOutput(<span prop="Count: 7" />);
});
@ -336,10 +336,10 @@ describe('ReactHooksWithNoopRenderer', () => {
await waitForAll(['Count: 0']);
expect(ReactNoop).toMatchRenderedOutput(<span prop="Count: 0" />);
act(() => counter.current.updateCount(7));
await act(async () => counter.current.updateCount(7));
assertLog(['Count: 7']);
act(() => counter.current.updateLabel('Total'));
await act(async () => counter.current.updateLabel('Total'));
assertLog(['Total: 7']);
});
@ -356,13 +356,13 @@ describe('ReactHooksWithNoopRenderer', () => {
const firstUpdater = updater;
act(() => firstUpdater(1));
await act(async () => firstUpdater(1));
assertLog(['Count: 1']);
expect(ReactNoop).toMatchRenderedOutput(<span prop="Count: 1" />);
const secondUpdater = updater;
act(() => firstUpdater(count => count + 10));
await act(async () => firstUpdater(count => count + 10));
assertLog(['Count: 11']);
expect(ReactNoop).toMatchRenderedOutput(<span prop="Count: 11" />);
@ -381,7 +381,7 @@ describe('ReactHooksWithNoopRenderer', () => {
await waitForAll([]);
ReactNoop.render(null);
await waitForAll([]);
act(() => _updateCount(1));
await act(async () => _updateCount(1));
});
it('works with memo', async () => {
@ -401,7 +401,7 @@ describe('ReactHooksWithNoopRenderer', () => {
await waitForAll([]);
expect(ReactNoop).toMatchRenderedOutput(<span prop="Count: 0" />);
act(() => _updateCount(1));
await act(async () => _updateCount(1));
assertLog(['Count: 1']);
expect(ReactNoop).toMatchRenderedOutput(<span prop="Count: 1" />);
});
@ -637,7 +637,7 @@ describe('ReactHooksWithNoopRenderer', () => {
// Test that it works on update, too. This time the log is a bit different
// because we started with reducerB instead of reducerA.
act(() => {
await act(async () => {
counter.current.dispatch('reset');
});
ReactNoop.render(<Counter ref={counter} />);
@ -851,10 +851,10 @@ describe('ReactHooksWithNoopRenderer', () => {
await waitForAll(['Count: 0']);
expect(ReactNoop).toMatchRenderedOutput(<span prop="Count: 0" />);
act(() => counter.current.dispatch(INCREMENT));
await act(async () => counter.current.dispatch(INCREMENT));
assertLog(['Count: 1']);
expect(ReactNoop).toMatchRenderedOutput(<span prop="Count: 1" />);
act(() => {
await act(async () => {
counter.current.dispatch(DECREMENT);
counter.current.dispatch(DECREMENT);
counter.current.dispatch(DECREMENT);
@ -893,11 +893,11 @@ describe('ReactHooksWithNoopRenderer', () => {
await waitForAll(['Init', 'Count: 10']);
expect(ReactNoop).toMatchRenderedOutput(<span prop="Count: 10" />);
act(() => counter.current.dispatch(INCREMENT));
await act(async () => counter.current.dispatch(INCREMENT));
assertLog(['Count: 11']);
expect(ReactNoop).toMatchRenderedOutput(<span prop="Count: 11" />);
act(() => {
await act(async () => {
counter.current.dispatch(DECREMENT);
counter.current.dispatch(DECREMENT);
counter.current.dispatch(DECREMENT);
@ -1427,7 +1427,7 @@ describe('ReactHooksWithNoopRenderer', () => {
return state;
}
act(() => {
await act(async () => {
ReactNoop.renderToRootWithID(<Component />, 'root', () =>
Scheduler.log('Sync effect'),
);
@ -1437,7 +1437,7 @@ describe('ReactHooksWithNoopRenderer', () => {
ReactNoop.unmountRootWithID('root');
await waitForAll(['passive destroy']);
act(() => {
await act(async () => {
updaterFunction(true);
});
});
@ -1624,7 +1624,7 @@ describe('ReactHooksWithNoopRenderer', () => {
expect(ReactNoop).toMatchRenderedOutput(<span prop="Count: 0" />);
// A flush sync doesn't cause the passive effects to fire.
// So we haven't added the other update yet.
act(() => {
await act(async () => {
ReactNoop.flushSync(() => {
_updateCount(2);
});
@ -2256,7 +2256,7 @@ describe('ReactHooksWithNoopRenderer', () => {
// @gate skipUnmountedBoundaries
it('should use the nearest still-mounted boundary if there are no unmounted boundaries', async () => {
act(() => {
await act(async () => {
ReactNoop.render(
<LogOnlyErrorBoundary>
<BrokenUseEffectCleanup />
@ -2269,7 +2269,7 @@ describe('ReactHooksWithNoopRenderer', () => {
'BrokenUseEffectCleanup useEffect',
]);
act(() => {
await act(async () => {
ReactNoop.render(<LogOnlyErrorBoundary />);
});
@ -2294,7 +2294,7 @@ describe('ReactHooksWithNoopRenderer', () => {
}
}
act(() => {
await act(async () => {
ReactNoop.render(
<LogOnlyErrorBoundary>
<Conditional showChildren={true} />
@ -2308,7 +2308,7 @@ describe('ReactHooksWithNoopRenderer', () => {
'BrokenUseEffectCleanup useEffect',
]);
act(() => {
await act(async () => {
ReactNoop.render(
<LogOnlyErrorBoundary>
<Conditional showChildren={false} />
@ -2333,7 +2333,7 @@ describe('ReactHooksWithNoopRenderer', () => {
}
}
act(() => {
await act(async () => {
ReactNoop.render(
<ErrorBoundary>
<Conditional showChildren={true} />
@ -2346,7 +2346,7 @@ describe('ReactHooksWithNoopRenderer', () => {
'BrokenUseEffectCleanup useEffect',
]);
act(() => {
await act(async () => {
ReactNoop.render(
<ErrorBoundary>
<Conditional showChildren={false} />
@ -2381,7 +2381,7 @@ describe('ReactHooksWithNoopRenderer', () => {
}
}
act(() => {
await act(async () => {
ReactNoop.render(<Conditional showChildren={true} />);
});
@ -2390,11 +2390,10 @@ describe('ReactHooksWithNoopRenderer', () => {
'BrokenUseEffectCleanup useEffect',
]);
expect(() => {
act(() => {
ReactNoop.render(<Conditional showChildren={false} />);
});
}).toThrow('Expected error');
await act(async () => {
ReactNoop.render(<Conditional showChildren={false} />);
await waitForThrow('Expected error');
});
assertLog(['BrokenUseEffectCleanup useEffect destroy']);
@ -2425,7 +2424,7 @@ describe('ReactHooksWithNoopRenderer', () => {
prevProps.prop === nextProps.prop;
const MemoizedChild = React.memo(Child, isEqual);
act(() => {
await act(async () => {
ReactNoop.render(
<Wrapper>
<MemoizedChild key={1} />
@ -2435,7 +2434,7 @@ describe('ReactHooksWithNoopRenderer', () => {
assertLog(['render', 'layout create', 'passive create']);
// Include at least one no-op (memoized) update to trigger original bug.
act(() => {
await act(async () => {
ReactNoop.render(
<Wrapper>
<MemoizedChild key={1} />
@ -2444,7 +2443,7 @@ describe('ReactHooksWithNoopRenderer', () => {
});
assertLog([]);
act(() => {
await act(async () => {
ReactNoop.render(
<Wrapper>
<MemoizedChild key={2} />
@ -2459,7 +2458,7 @@ describe('ReactHooksWithNoopRenderer', () => {
'passive create',
]);
act(() => {
await act(async () => {
ReactNoop.render(null);
});
assertLog(['layout destroy', 'passive destroy']);
@ -2492,7 +2491,7 @@ describe('ReactHooksWithNoopRenderer', () => {
prevProps.prop === nextProps.prop;
const MemoizedChild = React.memo(Child, isEqual);
act(() => {
await act(async () => {
ReactNoop.render(
<Wrapper>
<MemoizedChild key={1} />
@ -2502,7 +2501,7 @@ describe('ReactHooksWithNoopRenderer', () => {
assertLog(['render', 'layout create', 'passive create']);
// Include at least one no-op (memoized) update to trigger original bug.
act(() => {
await act(async () => {
ReactNoop.render(
<Wrapper>
<MemoizedChild key={1} />
@ -2511,7 +2510,7 @@ describe('ReactHooksWithNoopRenderer', () => {
});
assertLog([]);
act(() => {
await act(async () => {
ReactNoop.render(
<Wrapper>
<MemoizedChild key={2} />
@ -2526,12 +2525,16 @@ describe('ReactHooksWithNoopRenderer', () => {
'passive create',
]);
act(() => {
await act(async () => {
ReactNoop.render(null);
});
assertLog(['layout destroy', 'passive destroy']);
});
// TODO: This test fails when skipUnmountedBoundaries is disabled. However,
// it's also rolled out to open source already and partially to www. So
// we should probably just land it.
// @gate skipUnmountedBoundaries
it('assumes passive effect destroy function is either a function or undefined', async () => {
function App(props) {
useEffect(() => {
@ -2541,8 +2544,8 @@ describe('ReactHooksWithNoopRenderer', () => {
}
const root1 = ReactNoop.createRoot();
expect(() => {
act(() => {
await expect(async () => {
await act(async () => {
root1.render(<App return={17} />);
});
}).toErrorDev([
@ -2551,8 +2554,8 @@ describe('ReactHooksWithNoopRenderer', () => {
]);
const root2 = ReactNoop.createRoot();
expect(() => {
act(() => {
await expect(async () => {
await act(async () => {
root2.render(<App return={null} />);
});
}).toErrorDev([
@ -2562,8 +2565,8 @@ describe('ReactHooksWithNoopRenderer', () => {
]);
const root3 = ReactNoop.createRoot();
expect(() => {
act(() => {
await expect(async () => {
await act(async () => {
root3.render(<App return={Promise.resolve()} />);
});
}).toErrorDev([
@ -2573,11 +2576,10 @@ describe('ReactHooksWithNoopRenderer', () => {
]);
// Error on unmount because React assumes the value is a function
expect(() =>
act(() => {
root3.unmount();
}),
).toThrow('is not a function');
await act(async () => {
root3.render(null);
await waitForThrow('is not a function');
});
});
});
@ -2921,8 +2923,8 @@ describe('ReactHooksWithNoopRenderer', () => {
}
const root1 = ReactNoop.createRoot();
expect(() => {
act(() => {
await expect(async () => {
await act(async () => {
root1.render(<App return={17} />);
});
}).toErrorDev([
@ -2931,8 +2933,8 @@ describe('ReactHooksWithNoopRenderer', () => {
]);
const root2 = ReactNoop.createRoot();
expect(() => {
act(() => {
await expect(async () => {
await act(async () => {
root2.render(<App return={null} />);
});
}).toErrorDev([
@ -2942,8 +2944,8 @@ describe('ReactHooksWithNoopRenderer', () => {
]);
const root3 = ReactNoop.createRoot();
expect(() => {
act(() => {
await expect(async () => {
await act(async () => {
root3.render(<App return={Promise.resolve()} />);
});
}).toErrorDev([
@ -2953,11 +2955,10 @@ describe('ReactHooksWithNoopRenderer', () => {
]);
// Error on unmount because React assumes the value is a function
expect(() =>
act(() => {
root3.unmount();
}),
).toThrow('is not a function');
await act(async () => {
root3.render(null);
await waitForThrow('is not a function');
});
});
it('warns when setState is called from insertion effect setup', async () => {
@ -2973,17 +2974,16 @@ describe('ReactHooksWithNoopRenderer', () => {
}
const root = ReactNoop.createRoot();
expect(() => {
act(() => {
await expect(async () => {
await act(async () => {
root.render(<App />);
});
}).toErrorDev(['Warning: useInsertionEffect must not schedule updates.']);
expect(() => {
act(() => {
root.render(<App throw={true} />);
});
}).toThrow('No');
await act(async () => {
root.render(<App throw={true} />);
await waitForThrow('No');
});
// Should not warn for regular effects after throw.
function NotInsertion() {
@ -2993,7 +2993,7 @@ describe('ReactHooksWithNoopRenderer', () => {
}, []);
return null;
}
act(() => {
await act(async () => {
root.render(<NotInsertion />);
});
});
@ -3013,20 +3013,19 @@ describe('ReactHooksWithNoopRenderer', () => {
}
const root = ReactNoop.createRoot();
act(() => {
await act(async () => {
root.render(<App foo="hello" />);
});
expect(() => {
act(() => {
await expect(async () => {
await act(async () => {
root.render(<App foo="goodbye" />);
});
}).toErrorDev(['Warning: useInsertionEffect must not schedule updates.']);
expect(() => {
act(() => {
root.render(<App throw={true} />);
});
}).toThrow('No');
await act(async () => {
root.render(<App throw={true} />);
await waitForThrow('No');
});
// Should not warn for regular effects after throw.
function NotInsertion() {
@ -3036,7 +3035,7 @@ describe('ReactHooksWithNoopRenderer', () => {
}, []);
return null;
}
act(() => {
await act(async () => {
root.render(<NotInsertion />);
});
});
@ -3204,8 +3203,8 @@ describe('ReactHooksWithNoopRenderer', () => {
}
const root1 = ReactNoop.createRoot();
expect(() => {
act(() => {
await expect(async () => {
await act(async () => {
root1.render(<App return={17} />);
});
}).toErrorDev([
@ -3214,8 +3213,8 @@ describe('ReactHooksWithNoopRenderer', () => {
]);
const root2 = ReactNoop.createRoot();
expect(() => {
act(() => {
await expect(async () => {
await act(async () => {
root2.render(<App return={null} />);
});
}).toErrorDev([
@ -3225,8 +3224,8 @@ describe('ReactHooksWithNoopRenderer', () => {
]);
const root3 = ReactNoop.createRoot();
expect(() => {
act(() => {
await expect(async () => {
await act(async () => {
root3.render(<App return={Promise.resolve()} />);
});
}).toErrorDev([
@ -3236,11 +3235,10 @@ describe('ReactHooksWithNoopRenderer', () => {
]);
// Error on unmount because React assumes the value is a function
expect(() =>
act(() => {
root3.unmount();
}),
).toThrow('is not a function');
await act(async () => {
root3.render(null);
await waitForThrow('is not a function');
});
});
});
@ -3279,7 +3277,7 @@ describe('ReactHooksWithNoopRenderer', () => {
</>,
);
act(button.current.increment);
await act(async () => button.current.increment());
assertLog([
// Button should not re-render, because its props haven't changed
// 'Increment',
@ -3307,7 +3305,7 @@ describe('ReactHooksWithNoopRenderer', () => {
);
// Callback should have updated
act(button.current.increment);
await act(async () => button.current.increment());
assertLog(['Count: 11']);
expect(ReactNoop).toMatchRenderedOutput(
<>
@ -3425,7 +3423,7 @@ describe('ReactHooksWithNoopRenderer', () => {
expect(ReactNoop).toMatchRenderedOutput(<span prop="Count: 0" />);
expect(counter.current.count).toBe(0);
act(() => {
await act(async () => {
counter.current.dispatch(INCREMENT);
});
assertLog(['Count: 1']);
@ -3455,7 +3453,7 @@ describe('ReactHooksWithNoopRenderer', () => {
expect(ReactNoop).toMatchRenderedOutput(<span prop="Count: 0" />);
expect(counter.current.count).toBe(0);
act(() => {
await act(async () => {
counter.current.dispatch(INCREMENT);
});
assertLog(['Count: 1']);
@ -3492,7 +3490,7 @@ describe('ReactHooksWithNoopRenderer', () => {
expect(counter.current.count).toBe(0);
expect(totalRefUpdates).toBe(1);
act(() => {
await act(async () => {
counter.current.dispatch(INCREMENT);
});
assertLog(['Count: 1']);
@ -3591,7 +3589,7 @@ describe('ReactHooksWithNoopRenderer', () => {
);
}
act(() => {
await act(async () => {
ReactNoop.render(<App />);
});
@ -3689,7 +3687,7 @@ describe('ReactHooksWithNoopRenderer', () => {
<span prop="A: 0, B: 0, C: [not loaded]" />,
);
act(() => {
await act(async () => {
updateA(2);
updateB(3);
});
@ -3751,7 +3749,7 @@ describe('ReactHooksWithNoopRenderer', () => {
ReactNoop.render(<App loadC={true} />);
await waitForAll(['A: 0, B: 0, C: 0']);
expect(ReactNoop).toMatchRenderedOutput(<span prop="A: 0, B: 0, C: 0" />);
act(() => {
await act(async () => {
updateA(2);
updateB(3);
updateC(4);
@ -3857,7 +3855,7 @@ describe('ReactHooksWithNoopRenderer', () => {
expect(ReactNoop).toMatchRenderedOutput('1');
});
act(() => {
await act(async () => {
setCounter(2);
});
assertLog(['Render: 1', 'Effect: 2', 'Reducer: 2', 'Render: 2']);
@ -3892,7 +3890,7 @@ describe('ReactHooksWithNoopRenderer', () => {
await waitForAll(['Render disabled: true', 'Render count: 0']);
expect(ReactNoop).toMatchRenderedOutput('0');
act(() => {
await act(async () => {
// These increments should have no effect, since disabled=true
increment();
increment();
@ -3901,7 +3899,7 @@ describe('ReactHooksWithNoopRenderer', () => {
assertLog(['Render disabled: true', 'Render count: 0']);
expect(ReactNoop).toMatchRenderedOutput('0');
act(() => {
await act(async () => {
// Enabling the updater should *not* replay the previous increment() actions
setDisabled(false);
});
@ -3941,7 +3939,7 @@ describe('ReactHooksWithNoopRenderer', () => {
await waitForAll(['Render disabled: true', 'Render count: 0']);
expect(ReactNoop).toMatchRenderedOutput('0');
act(() => {
await act(async () => {
// These increments should have no effect, since disabled=true
increment();
increment();
@ -3950,7 +3948,7 @@ describe('ReactHooksWithNoopRenderer', () => {
assertLog(['Render count: 0']);
expect(ReactNoop).toMatchRenderedOutput('0');
act(() => {
await act(async () => {
// Enabling the updater should *not* replay the previous increment() actions
setDisabled(false);
});
@ -3990,7 +3988,7 @@ describe('ReactHooksWithNoopRenderer', () => {
await waitForAll(['Render disabled: true', 'Render count: 0']);
expect(ReactNoop).toMatchRenderedOutput('0');
act(() => {
await act(async () => {
// Although the increment happens first (and would seem to do nothing since disabled=true),
// because these calls are in a batch the parent updates first. This should cause the child
// to re-render with disabled=false and *then* process the increment action, which now
@ -4085,7 +4083,7 @@ describe('ReactHooksWithNoopRenderer', () => {
]);
expect(ReactNoop).toMatchRenderedOutput('0');
act(() => dispatch());
await act(async () => dispatch());
assertLog(['Step: 5, Shadow: 5']);
expect(ReactNoop).toMatchRenderedOutput('5');
});
@ -4110,10 +4108,10 @@ describe('ReactHooksWithNoopRenderer', () => {
return `${a ? 'A' : 'a'}${b ? 'B' : 'b'}${c ? 'C' : 'c'}`;
}
act(() => ReactNoop.render(<App />));
await act(async () => ReactNoop.render(<App />));
expect(ReactNoop).toMatchRenderedOutput('abc');
act(() => {
await act(async () => {
updateA(true);
// This update should not get dropped.
updateC(true);
@ -4282,25 +4280,25 @@ describe('ReactHooksWithNoopRenderer', () => {
return <Text text={`Render: ${count}`} />;
}
act(() => {
await act(async () => {
ReactNoop.render(<Test />);
});
assertLog(['Render: 0', 'Effect: 0']);
act(() => {
await act(async () => {
handleClick();
});
assertLog(['Render: 0']);
act(() => {
await act(async () => {
handleClick();
});
assertLog(['Render: 0']);
act(() => {
await act(async () => {
handleClick();
});

View File

@ -32,8 +32,8 @@ describe('ReactOffscreenStrictMode', () => {
}
// @gate __DEV__ && enableOffscreen
it('should trigger strict effects when offscreen is visible', () => {
act(() => {
it('should trigger strict effects when offscreen is visible', async () => {
await act(async () => {
ReactNoop.render(
<React.StrictMode>
<Offscreen mode="visible">
@ -56,8 +56,8 @@ describe('ReactOffscreenStrictMode', () => {
});
// @gate __DEV__ && enableOffscreen && useModernStrictMode
it('should not trigger strict effects when offscreen is hidden', () => {
act(() => {
it('should not trigger strict effects when offscreen is hidden', async () => {
await act(async () => {
ReactNoop.render(
<React.StrictMode>
<Offscreen mode="hidden">
@ -71,7 +71,7 @@ describe('ReactOffscreenStrictMode', () => {
log = [];
act(() => {
await act(async () => {
ReactNoop.render(
<React.StrictMode>
<Offscreen mode="hidden">
@ -86,7 +86,7 @@ describe('ReactOffscreenStrictMode', () => {
log = [];
act(() => {
await act(async () => {
ReactNoop.render(
<React.StrictMode>
<Offscreen mode="visible">
@ -109,7 +109,7 @@ describe('ReactOffscreenStrictMode', () => {
log = [];
act(() => {
await act(async () => {
ReactNoop.render(
<React.StrictMode>
<Offscreen mode="hidden">
@ -127,7 +127,7 @@ describe('ReactOffscreenStrictMode', () => {
]);
});
it('should not cause infinite render loop when StrictMode is used with Suspense and synchronous set states', () => {
it('should not cause infinite render loop when StrictMode is used with Suspense and synchronous set states', async () => {
// This is a regression test, see https://github.com/facebook/react/pull/25179 for more details.
function App() {
const [state, setState] = React.useState(false);
@ -143,7 +143,7 @@ describe('ReactOffscreenStrictMode', () => {
return state;
}
act(() => {
await act(async () => {
ReactNoop.render(
<React.StrictMode>
<React.Suspense>
@ -193,7 +193,7 @@ describe('ReactOffscreenStrictMode', () => {
return null;
}
act(() => {
await act(async () => {
ReactNoop.render(
<React.StrictMode>
<Offscreen mode="visible">

View File

@ -2,12 +2,9 @@ let React;
let ReactTestRenderer;
let ReactFeatureFlags;
let Scheduler;
let ReactCache;
let Suspense;
let act;
let TextResource;
let textResourceShouldFail;
let textCache;
let assertLog;
let waitForPaint;
@ -24,7 +21,6 @@ describe('ReactSuspense', () => {
ReactTestRenderer = require('react-test-renderer');
act = require('jest-react').act;
Scheduler = require('scheduler');
ReactCache = require('react-cache');
Suspense = React.Suspense;
@ -34,73 +30,71 @@ describe('ReactSuspense', () => {
assertLog = InternalTestUtils.assertLog;
waitFor = InternalTestUtils.waitFor;
TextResource = ReactCache.unstable_createResource(
([text, ms = 0]) => {
let listeners = null;
let status = 'pending';
let value = null;
return {
then(resolve, reject) {
switch (status) {
case 'pending': {
if (listeners === null) {
listeners = [{resolve, reject}];
setTimeout(() => {
if (textResourceShouldFail) {
Scheduler.log(`Promise rejected [${text}]`);
status = 'rejected';
value = new Error('Failed to load: ' + text);
listeners.forEach(listener => listener.reject(value));
} else {
Scheduler.log(`Promise resolved [${text}]`);
status = 'resolved';
value = text;
listeners.forEach(listener => listener.resolve(value));
}
}, ms);
} else {
listeners.push({resolve, reject});
}
break;
}
case 'resolved': {
resolve(value);
break;
}
case 'rejected': {
reject(value);
break;
}
}
},
};
},
([text, ms]) => text,
);
textResourceShouldFail = false;
textCache = new Map();
});
function Text(props) {
Scheduler.log(props.text);
return props.text;
function resolveText(text) {
const record = textCache.get(text);
if (record === undefined) {
const newRecord = {
status: 'resolved',
value: text,
};
textCache.set(text, newRecord);
} else if (record.status === 'pending') {
const thenable = record.value;
record.status = 'resolved';
record.value = text;
thenable.pings.forEach(t => t());
}
}
function AsyncText(props) {
const text = props.text;
try {
TextResource.read([props.text, props.ms]);
Scheduler.log(text);
return text;
} catch (promise) {
if (typeof promise.then === 'function') {
Scheduler.log(`Suspend! [${text}]`);
} else {
Scheduler.log(`Error! [${text}]`);
function readText(text) {
const record = textCache.get(text);
if (record !== undefined) {
switch (record.status) {
case 'pending':
Scheduler.log(`Suspend! [${text}]`);
throw record.value;
case 'rejected':
throw record.value;
case 'resolved':
return record.value;
}
throw promise;
} else {
Scheduler.log(`Suspend! [${text}]`);
const thenable = {
pings: [],
then(resolve) {
if (newRecord.status === 'pending') {
thenable.pings.push(resolve);
} else {
Promise.resolve().then(() => resolve(newRecord.value));
}
},
};
const newRecord = {
status: 'pending',
value: thenable,
};
textCache.set(text, newRecord);
throw thenable;
}
}
function Text({text}) {
Scheduler.log(text);
return text;
}
function AsyncText({text}) {
readText(text);
Scheduler.log(text);
return text;
}
it('suspends rendering and continues later', async () => {
function Bar(props) {
Scheduler.log('Bar');
@ -146,16 +140,10 @@ describe('ReactSuspense', () => {
]);
expect(root).toMatchRenderedOutput(null);
// Flush some of the time
jest.advanceTimersByTime(50);
// Still nothing...
await waitForAll([]);
expect(root).toMatchRenderedOutput(null);
// Flush the promise completely
jest.advanceTimersByTime(50);
// Renders successfully
assertLog(['Promise resolved [A]']);
await resolveText('A');
await waitForAll(['Foo', 'Bar', 'A', 'B']);
expect(root).toMatchRenderedOutput('AB');
});
@ -184,19 +172,15 @@ describe('ReactSuspense', () => {
]);
expect(root).toMatchRenderedOutput('Loading A...Loading B...');
// Advance time by enough that the first Suspense's promise resolves and
// switches back to the normal view. The second Suspense should still
// show the placeholder
jest.advanceTimersByTime(5000);
// TODO: Should we throw if you forget to call toHaveYielded?
assertLog(['Promise resolved [A]']);
// Resolve first Suspense's promise and switch back to the normal view. The
// second Suspense should still show the placeholder
await resolveText('A');
await waitForAll(['A']);
expect(root).toMatchRenderedOutput('ALoading B...');
// Advance time by enough that the second Suspense's promise resolves
// and switches back to the normal view
jest.advanceTimersByTime(1000);
assertLog(['Promise resolved [B]']);
// Resolve the second Suspense's promise resolves and switche back to the
// normal view
await resolveText('B');
await waitForAll(['B']);
expect(root).toMatchRenderedOutput('AB');
});
@ -284,11 +268,6 @@ describe('ReactSuspense', () => {
);
}
// Committing fallbacks should be throttled.
// First, advance some time to skip the first threshold.
jest.advanceTimersByTime(600);
Scheduler.unstable_advanceTime(600);
const root = ReactTestRenderer.create(<Foo />, {
unstable_isConcurrent: true,
});
@ -302,10 +281,7 @@ describe('ReactSuspense', () => {
]);
expect(root).toMatchRenderedOutput('Loading...');
// Resolve A.
jest.advanceTimersByTime(200);
Scheduler.unstable_advanceTime(200);
assertLog(['Promise resolved [A]']);
await resolveText('A');
await waitForAll(['A', 'Suspend! [B]', 'Loading more...']);
// By this point, we have enough info to show "A" and "Loading more..."
@ -313,15 +289,12 @@ describe('ReactSuspense', () => {
// showing the inner fallback hoping that B will resolve soon enough.
expect(root).toMatchRenderedOutput('Loading...');
// Resolve B.
jest.advanceTimersByTime(100);
Scheduler.unstable_advanceTime(100);
assertLog(['Promise resolved [B]']);
// By this point, B has resolved.
// We're still showing the outer fallback.
await resolveText('B');
expect(root).toMatchRenderedOutput('Loading...');
await waitForAll(['A', 'B']);
// Then contents of both should pop in together.
expect(root).toMatchRenderedOutput('AB');
});
@ -339,11 +312,6 @@ describe('ReactSuspense', () => {
);
}
// Committing fallbacks should be throttled.
// First, advance some time to skip the first threshold.
jest.advanceTimersByTime(600);
Scheduler.unstable_advanceTime(600);
const root = ReactTestRenderer.create(<Foo />, {
unstable_isConcurrent: true,
});
@ -357,27 +325,20 @@ describe('ReactSuspense', () => {
]);
expect(root).toMatchRenderedOutput('Loading...');
// Resolve A.
jest.advanceTimersByTime(200);
Scheduler.unstable_advanceTime(200);
assertLog(['Promise resolved [A]']);
await resolveText('A');
await waitForAll(['A', 'Suspend! [B]', 'Loading more...']);
// By this point, we have enough info to show "A" and "Loading more..."
// However, we've just shown the outer fallback. So we'll delay
// showing the inner fallback hoping that B will resolve soon enough.
expect(root).toMatchRenderedOutput('Loading...');
// Wait some more. B is still not resolving.
// But if we wait a bit longer, eventually we'll give up and show a
// fallback. The exact value here isn't important. It's a JND ("Just
// Noticeable Difference").
jest.advanceTimersByTime(500);
Scheduler.unstable_advanceTime(500);
// Give up and render A with a spinner for B.
expect(root).toMatchRenderedOutput('ALoading more...');
// Resolve B.
jest.advanceTimersByTime(500);
Scheduler.unstable_advanceTime(500);
assertLog(['Promise resolved [B]']);
await resolveText('B');
await waitForAll(['B']);
expect(root).toMatchRenderedOutput('AB');
});
@ -423,18 +384,7 @@ describe('ReactSuspense', () => {
const MemoizedChild = memo(function MemoizedChild() {
const text = useContext(ValueContext);
try {
TextResource.read([text, 1000]);
Scheduler.log(text);
return text;
} catch (promise) {
if (typeof promise.then === 'function') {
Scheduler.log(`Suspend! [${text}]`);
} else {
Scheduler.log(`Error! [${text}]`);
}
throw promise;
}
return <Text text={readText(text)} />;
});
let setValue;
@ -455,17 +405,15 @@ describe('ReactSuspense', () => {
unstable_isConcurrent: true,
});
await waitForAll(['Suspend! [default]', 'Loading...']);
jest.advanceTimersByTime(1000);
assertLog(['Promise resolved [default]']);
await resolveText('default');
await waitForAll(['default']);
expect(root).toMatchRenderedOutput('default');
act(() => setValue('new value'));
await act(async () => setValue('new value'));
assertLog(['Suspend! [new value]', 'Loading...']);
jest.advanceTimersByTime(1000);
assertLog(['Promise resolved [new value]']);
await resolveText('new value');
await waitForAll(['new value']);
expect(root).toMatchRenderedOutput('new value');
});
@ -478,18 +426,7 @@ describe('ReactSuspense', () => {
const MemoizedChild = memo(
function MemoizedChild() {
const text = useContext(ValueContext);
try {
TextResource.read([text, 1000]);
Scheduler.log(text);
return text;
} catch (promise) {
if (typeof promise.then === 'function') {
Scheduler.log(`Suspend! [${text}]`);
} else {
Scheduler.log(`Error! [${text}]`);
}
throw promise;
}
return <Text text={readText(text)} />;
},
function areEqual(prevProps, nextProps) {
return true;
@ -514,17 +451,15 @@ describe('ReactSuspense', () => {
unstable_isConcurrent: true,
});
await waitForAll(['Suspend! [default]', 'Loading...']);
jest.advanceTimersByTime(1000);
assertLog(['Promise resolved [default]']);
await resolveText('default');
await waitForAll(['default']);
expect(root).toMatchRenderedOutput('default');
act(() => setValue('new value'));
await act(async () => setValue('new value'));
assertLog(['Suspend! [new value]', 'Loading...']);
jest.advanceTimersByTime(1000);
assertLog(['Promise resolved [new value]']);
await resolveText('new value');
await waitForAll(['new value']);
expect(root).toMatchRenderedOutput('new value');
});
@ -536,18 +471,7 @@ describe('ReactSuspense', () => {
function MemoizedChild() {
const text = useContext(ValueContext);
try {
TextResource.read([text, 1000]);
Scheduler.log(text);
return text;
} catch (promise) {
if (typeof promise.then === 'function') {
Scheduler.log(`Suspend! [${text}]`);
} else {
Scheduler.log(`Error! [${text}]`);
}
throw promise;
}
return <Text text={readText(text)} />;
}
let setValue;
@ -571,17 +495,15 @@ describe('ReactSuspense', () => {
},
);
await waitForAll(['Suspend! [default]', 'Loading...']);
jest.advanceTimersByTime(1000);
assertLog(['Promise resolved [default]']);
await resolveText('default');
await waitForAll(['default']);
expect(root).toMatchRenderedOutput('default');
act(() => setValue('new value'));
await act(async () => setValue('new value'));
assertLog(['Suspend! [new value]', 'Loading...']);
jest.advanceTimersByTime(1000);
assertLog(['Promise resolved [new value]']);
await resolveText('new value');
await waitForAll(['new value']);
expect(root).toMatchRenderedOutput('new value');
});
@ -593,18 +515,7 @@ describe('ReactSuspense', () => {
const MemoizedChild = forwardRef(() => {
const text = useContext(ValueContext);
try {
TextResource.read([text, 1000]);
Scheduler.log(text);
return text;
} catch (promise) {
if (typeof promise.then === 'function') {
Scheduler.log(`Suspend! [${text}]`);
} else {
Scheduler.log(`Error! [${text}]`);
}
throw promise;
}
return <Text text={readText(text)} />;
});
let setValue;
@ -628,17 +539,15 @@ describe('ReactSuspense', () => {
},
);
await waitForAll(['Suspend! [default]', 'Loading...']);
jest.advanceTimersByTime(1000);
assertLog(['Promise resolved [default]']);
await resolveText('default');
await waitForAll(['default']);
expect(root).toMatchRenderedOutput('default');
act(() => setValue('new value'));
await act(async () => setValue('new value'));
assertLog(['Suspend! [new value]', 'Loading...']);
jest.advanceTimersByTime(1000);
assertLog(['Promise resolved [new value]']);
await resolveText('new value');
await waitForAll(['new value']);
expect(root).toMatchRenderedOutput('new value');
});
@ -674,12 +583,17 @@ describe('ReactSuspense', () => {
await waitForAll(['Child 1', 'create layout']);
expect(root).toMatchRenderedOutput('Child 1');
act(() => {
await act(async () => {
_setShow(true);
});
assertLog(['Child 1', 'Suspend! [Child 2]', 'Loading...']);
jest.advanceTimersByTime(1000);
assertLog(['destroy layout', 'Promise resolved [Child 2]']);
assertLog([
'Child 1',
'Suspend! [Child 2]',
'Loading...',
'destroy layout',
]);
await resolveText('Child 2');
await waitForAll(['Child 1', 'Child 2', 'create layout']);
expect(root).toMatchRenderedOutput(['Child 1', 'Child 2'].join(''));
});
@ -715,20 +629,8 @@ describe('ReactSuspense', () => {
}
render() {
instance = this;
const text = `${this.props.text}:${this.state.step}`;
const ms = this.props.ms;
try {
TextResource.read([text, ms]);
Scheduler.log(text);
return text;
} catch (promise) {
if (typeof promise.then === 'function') {
Scheduler.log(`Suspend! [${text}]`);
} else {
Scheduler.log(`Error! [${text}]`);
}
throw promise;
}
const text = readText(`${this.props.text}:${this.state.step}`);
return <Text text={text} />;
}
}
@ -758,9 +660,7 @@ describe('ReactSuspense', () => {
]);
expect(root).toMatchRenderedOutput('Loading...');
jest.advanceTimersByTime(100);
assertLog(['Promise resolved [B:1]']);
await resolveText('B:1');
await waitForPaint([
'B:1',
'Unmount [Loading...]',
@ -773,9 +673,7 @@ describe('ReactSuspense', () => {
assertLog(['Suspend! [B:2]', 'Loading...', 'Mount [Loading...]']);
expect(root).toMatchRenderedOutput('Loading...');
jest.advanceTimersByTime(100);
assertLog(['Promise resolved [B:2]']);
await resolveText('B:2');
await waitForPaint(['B:2', 'Unmount [Loading...]', 'Update [B:2]']);
expect(root).toMatchRenderedOutput('AB:2C');
});
@ -803,9 +701,7 @@ describe('ReactSuspense', () => {
assertLog(['Stateful: 1', 'Suspend! [A]', 'Loading...']);
jest.advanceTimersByTime(1000);
assertLog(['Promise resolved [A]']);
await resolveText('A');
await waitForPaint(['A']);
expect(root).toMatchRenderedOutput('Stateful: 1A');
@ -817,9 +713,7 @@ describe('ReactSuspense', () => {
assertLog(['Stateful: 2', 'Suspend! [B]']);
expect(root).toMatchRenderedOutput('Loading...');
jest.advanceTimersByTime(1000);
assertLog(['Promise resolved [B]']);
await resolveText('B');
await waitForPaint(['B']);
expect(root).toMatchRenderedOutput('Stateful: 2B');
});
@ -855,9 +749,7 @@ describe('ReactSuspense', () => {
assertLog(['Stateful: 1', 'Suspend! [A]', 'Loading...']);
jest.advanceTimersByTime(1000);
assertLog(['Promise resolved [A]']);
await resolveText('A');
await waitForPaint(['A']);
expect(root).toMatchRenderedOutput('Stateful: 1A');
@ -876,9 +768,7 @@ describe('ReactSuspense', () => {
]);
expect(root).toMatchRenderedOutput('Loading...');
jest.advanceTimersByTime(1000);
assertLog(['Promise resolved [B]']);
await resolveText('B');
await waitForPaint(['B']);
expect(root).toMatchRenderedOutput('Stateful: 2B');
});
@ -889,20 +779,7 @@ describe('ReactSuspense', () => {
Scheduler.log('will unmount');
}
render() {
const text = this.props.text;
const ms = this.props.ms;
try {
TextResource.read([text, ms]);
Scheduler.log(text);
return text;
} catch (promise) {
if (typeof promise.then === 'function') {
Scheduler.log(`Suspend! [${text}]`);
} else {
Scheduler.log(`Error! [${text}]`);
}
throw promise;
}
return <Text text={readText(this.props.text)} />;
}
}
@ -932,18 +809,7 @@ describe('ReactSuspense', () => {
Scheduler.log('Did commit: ' + text);
}, [text]);
try {
TextResource.read([props.text, props.ms]);
Scheduler.log(text);
return text;
} catch (promise) {
if (typeof promise.then === 'function') {
Scheduler.log(`Suspend! [${text}]`);
} else {
Scheduler.log(`Error! [${text}]`);
}
throw promise;
}
return <Text text={readText(text)} />;
}
function App({text}) {
@ -956,9 +822,7 @@ describe('ReactSuspense', () => {
ReactTestRenderer.create(<App text="A" />);
assertLog(['Suspend! [A]', 'Loading...']);
jest.advanceTimersByTime(500);
assertLog(['Promise resolved [A]']);
await resolveText('A');
await waitForPaint(['A', 'Did commit: A']);
});
@ -986,15 +850,16 @@ describe('ReactSuspense', () => {
// Initial render
await waitForAll(['Suspend! [Step: 1]', 'Loading...']);
jest.advanceTimersByTime(1000);
assertLog(['Promise resolved [Step: 1]']);
await resolveText('Step: 1');
await waitForAll(['Step: 1']);
expect(root).toMatchRenderedOutput('Step: 1');
// Update that suspends
instance.setState({step: 2});
await waitForAll(['Suspend! [Step: 2]', 'Loading...']);
jest.advanceTimersByTime(500);
await act(async () => {
instance.setState({step: 2});
});
assertLog(['Suspend! [Step: 2]', 'Loading...']);
expect(root).toMatchRenderedOutput('Loading...');
// Update while still suspended
@ -1002,8 +867,8 @@ describe('ReactSuspense', () => {
await waitForAll(['Suspend! [Step: 3]']);
expect(root).toMatchRenderedOutput('Loading...');
jest.advanceTimersByTime(1000);
assertLog(['Promise resolved [Step: 2]', 'Promise resolved [Step: 3]']);
await resolveText('Step: 2');
await resolveText('Step: 3');
await waitForAll(['Step: 3']);
expect(root).toMatchRenderedOutput('Step: 3');
});
@ -1040,23 +905,17 @@ describe('ReactSuspense', () => {
]);
await waitForAll([]);
jest.advanceTimersByTime(1000);
assertLog(['Promise resolved [Child 1]']);
await resolveText('Child 1');
await waitForPaint([
'Child 1',
'Suspend! [Child 2]',
'Suspend! [Child 3]',
]);
jest.advanceTimersByTime(1000);
assertLog(['Promise resolved [Child 2]']);
await resolveText('Child 2');
await waitForPaint(['Child 2', 'Suspend! [Child 3]']);
jest.advanceTimersByTime(1000);
assertLog(['Promise resolved [Child 3]']);
await resolveText('Child 3');
await waitForPaint(['Child 3']);
expect(root).toMatchRenderedOutput(
['Child 1', 'Child 2', 'Child 3'].join(''),
@ -1083,11 +942,12 @@ describe('ReactSuspense', () => {
'Suspend! [Child 2]',
'Loading...',
]);
jest.advanceTimersByTime(1000);
assertLog(['Promise resolved [Child 1]']);
await resolveText('Child 1');
await waitForAll(['Child 1', 'Suspend! [Child 2]']);
jest.advanceTimersByTime(6000);
assertLog(['Promise resolved [Child 2]']);
await resolveText('Child 2');
await waitForAll(['Child 1', 'Child 2']);
expect(root).toMatchRenderedOutput(['Child 1', 'Child 2'].join(''));
});
@ -1111,32 +971,29 @@ describe('ReactSuspense', () => {
const root = ReactTestRenderer.create(<App />);
assertLog(['Suspend! [Tab: 0]', ' + sibling', 'Loading...']);
expect(root).toMatchRenderedOutput('Loading...');
jest.advanceTimersByTime(1000);
assertLog(['Promise resolved [Tab: 0]']);
await resolveText('Tab: 0');
await waitForPaint(['Tab: 0']);
expect(root).toMatchRenderedOutput('Tab: 0 + sibling');
act(() => setTab(1));
await act(async () => setTab(1));
assertLog(['Suspend! [Tab: 1]', ' + sibling', 'Loading...']);
expect(root).toMatchRenderedOutput('Loading...');
jest.advanceTimersByTime(1000);
assertLog(['Promise resolved [Tab: 1]']);
await resolveText('Tab: 1');
await waitForPaint(['Tab: 1']);
expect(root).toMatchRenderedOutput('Tab: 1 + sibling');
act(() => setTab(2));
await act(async () => setTab(2));
assertLog(['Suspend! [Tab: 2]', ' + sibling', 'Loading...']);
expect(root).toMatchRenderedOutput('Loading...');
jest.advanceTimersByTime(1000);
assertLog(['Promise resolved [Tab: 2]']);
await resolveText('Tab: 2');
await waitForPaint(['Tab: 2']);
expect(root).toMatchRenderedOutput('Tab: 2 + sibling');
});
it('does not warn if an mounted component is pinged', async () => {
it('does not warn if a mounted component is pinged', async () => {
const {useState} = React;
const root = ReactTestRenderer.create(null);
@ -1146,18 +1003,7 @@ describe('ReactSuspense', () => {
const [step, _setStep] = useState(0);
setStep = _setStep;
const fullText = `${text}:${step}`;
try {
TextResource.read([fullText, ms]);
Scheduler.log(fullText);
return fullText;
} catch (promise) {
if (typeof promise.then === 'function') {
Scheduler.log(`Suspend! [${fullText}]`);
} else {
Scheduler.log(`Error! [${fullText}]`);
}
throw promise;
}
return <Text text={readText(fullText)} />;
}
root.update(
@ -1167,19 +1013,18 @@ describe('ReactSuspense', () => {
);
assertLog(['Suspend! [A:0]', 'Loading...']);
jest.advanceTimersByTime(1000);
assertLog(['Promise resolved [A:0]']);
await resolveText('A:0');
await waitForPaint(['A:0']);
expect(root).toMatchRenderedOutput('A:0');
act(() => setStep(1));
await act(async () => setStep(1));
assertLog(['Suspend! [A:1]', 'Loading...']);
expect(root).toMatchRenderedOutput('Loading...');
root.update(null);
await waitForAll([]);
jest.advanceTimersByTime(1000);
await act(async () => {
root.update(null);
});
});
it('memoizes promise listeners per thread ID to prevent redundant renders', async () => {
@ -1199,10 +1044,7 @@ describe('ReactSuspense', () => {
assertLog(['Suspend! [A]', 'Suspend! [B]', 'Suspend! [C]', 'Loading...']);
// Resolve A
jest.advanceTimersByTime(1000);
assertLog(['Promise resolved [A]']);
await resolveText('A');
await waitForPaint([
'A',
// The promises for B and C have now been thrown twice
@ -1210,10 +1052,7 @@ describe('ReactSuspense', () => {
'Suspend! [C]',
]);
// Resolve B
jest.advanceTimersByTime(1000);
assertLog(['Promise resolved [B]']);
await resolveText('B');
await waitForPaint([
// Even though the promise for B was thrown twice, we should only
// re-render once.
@ -1222,10 +1061,7 @@ describe('ReactSuspense', () => {
'Suspend! [C]',
]);
// Resolve C
jest.advanceTimersByTime(1000);
assertLog(['Promise resolved [C]']);
await resolveText('C');
await waitForPaint([
// Even though the promise for C was thrown three times, we should only
// re-render once.
@ -1233,7 +1069,7 @@ describe('ReactSuspense', () => {
]);
});
it('#14162', () => {
it('#14162', async () => {
const {lazy} = React;
function Hello() {
@ -1267,8 +1103,9 @@ describe('ReactSuspense', () => {
const root = ReactTestRenderer.create(null);
root.update(<App name="world" />);
jest.advanceTimersByTime(1000);
await act(async () => {
root.update(<App name="world" />);
});
});
it('updates memoized child of suspense component when context updates (simple memo)', async () => {
@ -1278,18 +1115,7 @@ describe('ReactSuspense', () => {
const MemoizedChild = memo(function MemoizedChild() {
const text = useContext(ValueContext);
try {
TextResource.read([text, 1000]);
Scheduler.log(text);
return text;
} catch (promise) {
if (typeof promise.then === 'function') {
Scheduler.log(`Suspend! [${text}]`);
} else {
Scheduler.log(`Error! [${text}]`);
}
throw promise;
}
return <Text text={readText(text)} />;
});
let setValue;
@ -1308,17 +1134,15 @@ describe('ReactSuspense', () => {
const root = ReactTestRenderer.create(<App />);
assertLog(['Suspend! [default]', 'Loading...']);
jest.advanceTimersByTime(1000);
assertLog(['Promise resolved [default]']);
await resolveText('default');
await waitForPaint(['default']);
expect(root).toMatchRenderedOutput('default');
act(() => setValue('new value'));
await act(async () => setValue('new value'));
assertLog(['Suspend! [new value]', 'Loading...']);
jest.advanceTimersByTime(1000);
assertLog(['Promise resolved [new value]']);
await resolveText('new value');
await waitForPaint(['new value']);
expect(root).toMatchRenderedOutput('new value');
});
@ -1331,18 +1155,7 @@ describe('ReactSuspense', () => {
const MemoizedChild = memo(
function MemoizedChild() {
const text = useContext(ValueContext);
try {
TextResource.read([text, 1000]);
Scheduler.log(text);
return text;
} catch (promise) {
if (typeof promise.then === 'function') {
Scheduler.log(`Suspend! [${text}]`);
} else {
Scheduler.log(`Error! [${text}]`);
}
throw promise;
}
return <Text text={readText(text)} />;
},
function areEqual(prevProps, nextProps) {
return true;
@ -1365,17 +1178,15 @@ describe('ReactSuspense', () => {
const root = ReactTestRenderer.create(<App />);
assertLog(['Suspend! [default]', 'Loading...']);
jest.advanceTimersByTime(1000);
assertLog(['Promise resolved [default]']);
await resolveText('default');
await waitForPaint(['default']);
expect(root).toMatchRenderedOutput('default');
act(() => setValue('new value'));
await act(async () => setValue('new value'));
assertLog(['Suspend! [new value]', 'Loading...']);
jest.advanceTimersByTime(1000);
assertLog(['Promise resolved [new value]']);
await resolveText('new value');
await waitForPaint(['new value']);
expect(root).toMatchRenderedOutput('new value');
});
@ -1387,18 +1198,7 @@ describe('ReactSuspense', () => {
function MemoizedChild() {
const text = useContext(ValueContext);
try {
TextResource.read([text, 1000]);
Scheduler.log(text);
return text;
} catch (promise) {
if (typeof promise.then === 'function') {
Scheduler.log(`Suspend! [${text}]`);
} else {
Scheduler.log(`Error! [${text}]`);
}
throw promise;
}
return <Text text={readText(text)} />;
}
let setValue;
@ -1421,17 +1221,15 @@ describe('ReactSuspense', () => {
</App>,
);
assertLog(['Suspend! [default]', 'Loading...']);
jest.advanceTimersByTime(1000);
assertLog(['Promise resolved [default]']);
await resolveText('default');
await waitForPaint(['default']);
expect(root).toMatchRenderedOutput('default');
act(() => setValue('new value'));
await act(async () => setValue('new value'));
assertLog(['Suspend! [new value]', 'Loading...']);
jest.advanceTimersByTime(1000);
assertLog(['Promise resolved [new value]']);
await resolveText('new value');
await waitForPaint(['new value']);
expect(root).toMatchRenderedOutput('new value');
});
@ -1443,18 +1241,7 @@ describe('ReactSuspense', () => {
const MemoizedChild = forwardRef(function MemoizedChild() {
const text = useContext(ValueContext);
try {
TextResource.read([text, 1000]);
Scheduler.log(text);
return text;
} catch (promise) {
if (typeof promise.then === 'function') {
Scheduler.log(`Suspend! [${text}]`);
} else {
Scheduler.log(`Error! [${text}]`);
}
throw promise;
}
return <Text text={readText(text)} />;
});
let setValue;
@ -1473,22 +1260,20 @@ describe('ReactSuspense', () => {
const root = ReactTestRenderer.create(<App />);
assertLog(['Suspend! [default]', 'Loading...']);
jest.advanceTimersByTime(1000);
assertLog(['Promise resolved [default]']);
await resolveText('default');
await waitForPaint(['default']);
expect(root).toMatchRenderedOutput('default');
act(() => setValue('new value'));
await act(async () => setValue('new value'));
assertLog(['Suspend! [new value]', 'Loading...']);
jest.advanceTimersByTime(1000);
assertLog(['Promise resolved [new value]']);
await resolveText('new value');
await waitForPaint(['new value']);
expect(root).toMatchRenderedOutput('new value');
});
it('updates context consumer within child of suspended suspense component when context updates', () => {
it('updates context consumer within child of suspended suspense component when context updates', async () => {
const {createContext, useState} = React;
const ValueContext = createContext(null);
@ -1531,11 +1316,11 @@ describe('ReactSuspense', () => {
assertLog(['Received context value [default]', 'default']);
expect(root).toMatchRenderedOutput('default');
act(() => setValue('new value'));
await act(async () => setValue('new value'));
assertLog(['Received context value [new value]', 'Loading...']);
expect(root).toMatchRenderedOutput('Loading...');
act(() => setValue('default'));
await act(async () => setValue('default'));
assertLog(['Received context value [default]', 'default']);
expect(root).toMatchRenderedOutput('default');
});

View File

@ -17,6 +17,7 @@ let caches;
let seededCache;
let ErrorBoundary;
let waitForAll;
let waitFor;
let assertLog;
// TODO: These tests don't pass in persistent mode yet. Need to implement.
@ -35,6 +36,7 @@ describe('ReactSuspenseEffectsSemantics', () => {
const InternalTestUtils = require('internal-test-utils');
waitForAll = InternalTestUtils.waitForAll;
waitFor = InternalTestUtils.waitFor;
assertLog = InternalTestUtils.assertLog;
caches = [];
@ -372,7 +374,7 @@ describe('ReactSuspenseEffectsSemantics', () => {
}
// Mount and suspend.
act(() => {
await act(async () => {
ReactNoop.renderLegacySyncRoot(
<App>
<AsyncText text="Async" ms={1000} />
@ -473,7 +475,7 @@ describe('ReactSuspenseEffectsSemantics', () => {
}
// Mount
act(() => {
await act(async () => {
ReactNoop.renderLegacySyncRoot(<App />);
});
assertLog([
@ -499,7 +501,7 @@ describe('ReactSuspenseEffectsSemantics', () => {
);
// Schedule an update that causes React to suspend.
act(() => {
await act(async () => {
ReactNoop.renderLegacySyncRoot(
<App>
<AsyncText text="Async" ms={1000} />
@ -629,46 +631,46 @@ describe('ReactSuspenseEffectsSemantics', () => {
);
// Schedule an update that causes React to suspend.
act(() => {
await act(async () => {
ReactNoop.render(
<App>
<AsyncText text="Async" ms={1000} />
</App>,
);
await waitFor([
'App render',
'Text:Inside:Before render',
'Suspend:Async',
'Text:Inside:After render',
'Text:Fallback render',
'Text:Outside render',
]);
expect(ReactNoop).toMatchRenderedOutput(
<>
<span prop="Inside:Before" />
<span prop="Inside:After" />
<span prop="Outside" />
</>,
);
await jest.runAllTimers();
// Timing out should commit the fallback and destroy inner layout effects.
assertLog([
'Text:Inside:Before destroy layout',
'Text:Inside:After destroy layout',
'Text:Fallback create layout',
]);
await waitForAll(['Text:Fallback create passive']);
expect(ReactNoop).toMatchRenderedOutput(
<>
<span prop="Inside:Before" hidden={true} />
<span prop="Inside:After" hidden={true} />
<span prop="Fallback" />
<span prop="Outside" />
</>,
);
});
assertLog([
'App render',
'Text:Inside:Before render',
'Suspend:Async',
'Text:Inside:After render',
'Text:Fallback render',
'Text:Outside render',
]);
expect(ReactNoop).toMatchRenderedOutput(
<>
<span prop="Inside:Before" />
<span prop="Inside:After" />
<span prop="Outside" />
</>,
);
await advanceTimers(1000);
// Timing out should commit the fallback and destroy inner layout effects.
assertLog([
'Text:Inside:Before destroy layout',
'Text:Inside:After destroy layout',
'Text:Fallback create layout',
]);
await waitForAll(['Text:Fallback create passive']);
expect(ReactNoop).toMatchRenderedOutput(
<>
<span prop="Inside:Before" hidden={true} />
<span prop="Inside:After" hidden={true} />
<span prop="Fallback" />
<span prop="Outside" />
</>,
);
// Resolving the suspended resource should re-create inner layout effects.
await act(async () => {
@ -783,46 +785,47 @@ describe('ReactSuspenseEffectsSemantics', () => {
);
// Schedule an update that causes React to suspend.
act(() => {
await act(async () => {
ReactNoop.render(
<App>
<AsyncText text="Async" ms={1000} />
</App>,
);
await waitFor([
'App render',
'ClassText:Inside:Before render',
'Suspend:Async',
'ClassText:Inside:After render',
'ClassText:Fallback render',
'ClassText:Outside render',
]);
expect(ReactNoop).toMatchRenderedOutput(
<>
<span prop="Inside:Before" />
<span prop="Inside:After" />
<span prop="Outside" />
</>,
);
await jest.runAllTimers();
// Timing out should commit the fallback and destroy inner layout effects.
assertLog([
'ClassText:Inside:Before componentWillUnmount',
'ClassText:Inside:After componentWillUnmount',
'ClassText:Fallback componentDidMount',
'ClassText:Outside componentDidUpdate',
]);
expect(ReactNoop).toMatchRenderedOutput(
<>
<span prop="Inside:Before" hidden={true} />
<span prop="Inside:After" hidden={true} />
<span prop="Fallback" />
<span prop="Outside" />
</>,
);
});
assertLog([
'App render',
'ClassText:Inside:Before render',
'Suspend:Async',
'ClassText:Inside:After render',
'ClassText:Fallback render',
'ClassText:Outside render',
]);
expect(ReactNoop).toMatchRenderedOutput(
<>
<span prop="Inside:Before" />
<span prop="Inside:After" />
<span prop="Outside" />
</>,
);
await advanceTimers(1000);
// Timing out should commit the fallback and destroy inner layout effects.
assertLog([
'ClassText:Inside:Before componentWillUnmount',
'ClassText:Inside:After componentWillUnmount',
'ClassText:Fallback componentDidMount',
'ClassText:Outside componentDidUpdate',
]);
expect(ReactNoop).toMatchRenderedOutput(
<>
<span prop="Inside:Before" hidden={true} />
<span prop="Inside:After" hidden={true} />
<span prop="Fallback" />
<span prop="Outside" />
</>,
);
// Resolving the suspended resource should re-create inner layout effects.
await act(async () => {
@ -908,43 +911,43 @@ describe('ReactSuspenseEffectsSemantics', () => {
);
// Schedule an update that causes React to suspend.
act(() => {
await act(async () => {
ReactNoop.render(
<App>
<AsyncText text="Async" ms={1000} />
</App>,
);
});
assertLog([
'App render',
'Suspend:Async',
'Text:Outer render',
'Text:Inner render',
'Text:Fallback render',
]);
expect(ReactNoop).toMatchRenderedOutput(
<span prop="Outer">
<span prop="Inner" />
</span>,
);
await advanceTimers(1000);
// Timing out should commit the fallback and destroy inner layout effects.
assertLog([
'Text:Outer destroy layout',
'Text:Inner destroy layout',
'Text:Fallback create layout',
]);
await waitForAll(['Text:Fallback create passive']);
expect(ReactNoop).toMatchRenderedOutput(
<>
<span hidden={true} prop="Outer">
await waitFor([
'App render',
'Suspend:Async',
'Text:Outer render',
'Text:Inner render',
'Text:Fallback render',
]);
expect(ReactNoop).toMatchRenderedOutput(
<span prop="Outer">
<span prop="Inner" />
</span>
<span prop="Fallback" />
</>,
);
</span>,
);
await jest.runAllTimers();
// Timing out should commit the fallback and destroy inner layout effects.
assertLog([
'Text:Outer destroy layout',
'Text:Inner destroy layout',
'Text:Fallback create layout',
]);
await waitForAll(['Text:Fallback create passive']);
expect(ReactNoop).toMatchRenderedOutput(
<>
<span hidden={true} prop="Outer">
<span prop="Inner" />
</span>
<span prop="Fallback" />
</>,
);
});
// Resolving the suspended resource should re-create inner layout effects.
await act(async () => {
@ -1035,44 +1038,44 @@ describe('ReactSuspenseEffectsSemantics', () => {
);
// Schedule an update that causes React to suspend.
act(() => {
await act(async () => {
ReactNoop.render(
<App>
<AsyncText text="Async" ms={1000} />
</App>,
);
});
assertLog([
'App render',
'Suspend:Async',
'Text:Outer render',
// Text:MemoizedInner is memoized
'Text:Fallback render',
]);
expect(ReactNoop).toMatchRenderedOutput(
<span prop="Outer">
<span prop="MemoizedInner" />
</span>,
);
await advanceTimers(1000);
// Timing out should commit the fallback and destroy inner layout effects.
// Even though the innermost layout effects are beneath a hidden HostComponent.
assertLog([
'Text:Outer destroy layout',
'Text:MemoizedInner destroy layout',
'Text:Fallback create layout',
]);
await waitForAll(['Text:Fallback create passive']);
expect(ReactNoop).toMatchRenderedOutput(
<>
<span hidden={true} prop="Outer">
await waitFor([
'App render',
'Suspend:Async',
'Text:Outer render',
// Text:MemoizedInner is memoized
'Text:Fallback render',
]);
expect(ReactNoop).toMatchRenderedOutput(
<span prop="Outer">
<span prop="MemoizedInner" />
</span>
<span prop="Fallback" />
</>,
);
</span>,
);
await jest.runAllTimers();
// Timing out should commit the fallback and destroy inner layout effects.
// Even though the innermost layout effects are beneath a hidden HostComponent.
assertLog([
'Text:Outer destroy layout',
'Text:MemoizedInner destroy layout',
'Text:Fallback create layout',
]);
await waitForAll(['Text:Fallback create passive']);
expect(ReactNoop).toMatchRenderedOutput(
<>
<span hidden={true} prop="Outer">
<span prop="MemoizedInner" />
</span>
<span prop="Fallback" />
</>,
);
});
// Resolving the suspended resource should re-create inner layout effects.
await act(async () => {
@ -1147,12 +1150,11 @@ describe('ReactSuspenseEffectsSemantics', () => {
);
// Suspend the inner Suspense subtree (only inner effects should be destroyed)
act(() => {
await act(async () => {
ReactNoop.render(
<App innerChildren={<AsyncText text="InnerAsync_1" ms={1000} />} />,
);
});
await advanceTimers(1000);
assertLog([
'Text:Outer render',
'Text:Inner render',
@ -1160,8 +1162,8 @@ describe('ReactSuspenseEffectsSemantics', () => {
'Text:InnerFallback render',
'Text:Inner destroy layout',
'Text:InnerFallback create layout',
'Text:InnerFallback create passive',
]);
await waitForAll(['Text:InnerFallback create passive']);
expect(ReactNoop).toMatchRenderedOutput(
<>
<span prop="Outer" />
@ -1172,7 +1174,7 @@ describe('ReactSuspenseEffectsSemantics', () => {
// Suspend the outer Suspense subtree (outer effects and inner fallback effects should be destroyed)
// (This check also ensures we don't destroy effects for mounted inner fallback.)
act(() => {
await act(async () => {
ReactNoop.render(
<App
outerChildren={<AsyncText text="OuterAsync_1" ms={1000} />}
@ -1191,8 +1193,8 @@ describe('ReactSuspenseEffectsSemantics', () => {
'Text:Outer destroy layout',
'Text:InnerFallback destroy layout',
'Text:OuterFallback create layout',
'Text:OuterFallback create passive',
]);
await waitForAll(['Text:OuterFallback create passive']);
expect(ReactNoop).toMatchRenderedOutput(
<>
<span prop="Outer" hidden={true} />
@ -1222,7 +1224,7 @@ describe('ReactSuspenseEffectsSemantics', () => {
);
// Suspend the inner Suspense subtree (no effects should be destroyed)
act(() => {
await act(async () => {
ReactNoop.render(
<App
outerChildren={<AsyncText text="OuterAsync_1" ms={1000} />}
@ -1297,7 +1299,7 @@ describe('ReactSuspenseEffectsSemantics', () => {
);
// Suspend the outer Suspense subtree (all effects should be destroyed)
act(() => {
await act(async () => {
ReactNoop.render(
<App
outerChildren={<AsyncText text="OuterAsync_2" ms={1000} />}
@ -1305,7 +1307,6 @@ describe('ReactSuspenseEffectsSemantics', () => {
/>,
);
});
await advanceTimers(1000);
assertLog([
'Text:Outer render',
'Suspend:OuterAsync_2',
@ -1317,6 +1318,7 @@ describe('ReactSuspenseEffectsSemantics', () => {
'Text:Inner destroy layout',
'AsyncText:InnerAsync_2 destroy layout',
'Text:OuterFallback create layout',
'Text:OuterFallback create passive',
]);
expect(ReactNoop).toMatchRenderedOutput(
<>
@ -1333,7 +1335,6 @@ describe('ReactSuspenseEffectsSemantics', () => {
await resolveText('OuterAsync_2');
});
assertLog([
'Text:OuterFallback create passive',
'Text:Outer render',
'AsyncText:OuterAsync_2 render',
'Text:Inner render',
@ -1390,12 +1391,11 @@ describe('ReactSuspenseEffectsSemantics', () => {
);
// Suspend the inner Suspense subtree (only inner effects should be destroyed)
act(() => {
await act(async () => {
ReactNoop.render(
<App innerChildren={<AsyncText text="InnerAsync_1" ms={1000} />} />,
);
});
await advanceTimers(1000);
assertLog([
'Text:Outer render',
'Text:Inner render',
@ -1403,8 +1403,8 @@ describe('ReactSuspenseEffectsSemantics', () => {
'Text:InnerFallback render',
'Text:Inner destroy layout',
'Text:InnerFallback create layout',
'Text:InnerFallback create passive',
]);
await waitForAll(['Text:InnerFallback create passive']);
expect(ReactNoop).toMatchRenderedOutput(
<>
<span prop="Outer" />
@ -1415,7 +1415,7 @@ describe('ReactSuspenseEffectsSemantics', () => {
// Suspend the outer Suspense subtree (outer effects and inner fallback effects should be destroyed)
// (This check also ensures we don't destroy effects for mounted inner fallback.)
act(() => {
await act(async () => {
ReactNoop.render(
<App
outerChildren={<AsyncText text="OuterAsync_1" ms={1000} />}
@ -1423,7 +1423,6 @@ describe('ReactSuspenseEffectsSemantics', () => {
/>,
);
});
await advanceTimers(1000);
assertLog([
'Text:Outer render',
'Suspend:OuterAsync_1',
@ -1434,8 +1433,8 @@ describe('ReactSuspenseEffectsSemantics', () => {
'Text:Outer destroy layout',
'Text:InnerFallback destroy layout',
'Text:OuterFallback create layout',
'Text:OuterFallback create passive',
]);
await waitForAll(['Text:OuterFallback create passive']);
expect(ReactNoop).toMatchRenderedOutput(
<>
<span prop="Outer" hidden={true} />
@ -1518,88 +1517,88 @@ describe('ReactSuspenseEffectsSemantics', () => {
);
// Suspend the outer shell
act(() => {
await act(async () => {
ReactNoop.render(
<App outerChildren={<AsyncText text="OutsideAsync" ms={1000} />} />,
);
});
assertLog([
'Text:Inside render',
'Suspend:OutsideAsync',
'Text:Fallback:Inside render',
'Text:Fallback:Outside render',
'Text:Outside render',
]);
expect(ReactNoop).toMatchRenderedOutput(
<>
<span prop="Inside" />
<span prop="Outside" />
</>,
);
await waitFor([
'Text:Inside render',
'Suspend:OutsideAsync',
'Text:Fallback:Inside render',
'Text:Fallback:Outside render',
'Text:Outside render',
]);
expect(ReactNoop).toMatchRenderedOutput(
<>
<span prop="Inside" />
<span prop="Outside" />
</>,
);
// Timing out should commit the fallback and destroy inner layout effects.
await advanceTimers(1000);
assertLog([
'Text:Inside destroy layout',
'Text:Fallback:Inside create layout',
'Text:Fallback:Outside create layout',
]);
await waitForAll([
'Text:Fallback:Inside create passive',
'Text:Fallback:Outside create passive',
]);
expect(ReactNoop).toMatchRenderedOutput(
<>
<span prop="Inside" hidden={true} />
<span prop="Fallback:Inside" />
<span prop="Fallback:Outside" />
<span prop="Outside" />
</>,
);
// Timing out should commit the fallback and destroy inner layout effects.
await jest.runAllTimers();
assertLog([
'Text:Inside destroy layout',
'Text:Fallback:Inside create layout',
'Text:Fallback:Outside create layout',
]);
await waitForAll([
'Text:Fallback:Inside create passive',
'Text:Fallback:Outside create passive',
]);
expect(ReactNoop).toMatchRenderedOutput(
<>
<span prop="Inside" hidden={true} />
<span prop="Fallback:Inside" />
<span prop="Fallback:Outside" />
<span prop="Outside" />
</>,
);
});
// Suspend the fallback and verify that it's effects get cleaned up as well
act(() => {
await act(async () => {
ReactNoop.render(
<App
fallbackChildren={<AsyncText text="FallbackAsync" ms={1000} />}
outerChildren={<AsyncText text="OutsideAsync" ms={1000} />}
/>,
);
});
assertLog([
'Text:Inside render',
'Suspend:OutsideAsync',
'Text:Fallback:Inside render',
'Suspend:FallbackAsync',
'Text:Fallback:Fallback render',
'Text:Fallback:Outside render',
'Text:Outside render',
]);
expect(ReactNoop).toMatchRenderedOutput(
<>
<span prop="Inside" hidden={true} />
<span prop="Fallback:Inside" />
<span prop="Fallback:Outside" />
<span prop="Outside" />
</>,
);
await waitFor([
'Text:Inside render',
'Suspend:OutsideAsync',
'Text:Fallback:Inside render',
'Suspend:FallbackAsync',
'Text:Fallback:Fallback render',
'Text:Fallback:Outside render',
'Text:Outside render',
]);
expect(ReactNoop).toMatchRenderedOutput(
<>
<span prop="Inside" hidden={true} />
<span prop="Fallback:Inside" />
<span prop="Fallback:Outside" />
<span prop="Outside" />
</>,
);
// Timing out should commit the inner fallback and destroy outer fallback layout effects.
await advanceTimers(1000);
assertLog([
'Text:Fallback:Inside destroy layout',
'Text:Fallback:Fallback create layout',
]);
await waitForAll(['Text:Fallback:Fallback create passive']);
expect(ReactNoop).toMatchRenderedOutput(
<>
<span prop="Inside" hidden={true} />
<span prop="Fallback:Inside" hidden={true} />
<span prop="Fallback:Fallback" />
<span prop="Fallback:Outside" />
<span prop="Outside" />
</>,
);
// Timing out should commit the inner fallback and destroy outer fallback layout effects.
await jest.runAllTimers();
assertLog([
'Text:Fallback:Inside destroy layout',
'Text:Fallback:Fallback create layout',
]);
await waitForAll(['Text:Fallback:Fallback create passive']);
expect(ReactNoop).toMatchRenderedOutput(
<>
<span prop="Inside" hidden={true} />
<span prop="Fallback:Inside" hidden={true} />
<span prop="Fallback:Fallback" />
<span prop="Fallback:Outside" />
<span prop="Outside" />
</>,
);
});
// Resolving both resources should cleanup fallback effects and recreate main effects
await act(async () => {
@ -1670,7 +1669,7 @@ describe('ReactSuspenseEffectsSemantics', () => {
);
// Suspend both the outer boundary and the fallback
act(() => {
await act(async () => {
ReactNoop.render(
<App
outerChildren={<AsyncText text="OutsideAsync" ms={1000} />}
@ -1678,7 +1677,6 @@ describe('ReactSuspenseEffectsSemantics', () => {
/>,
);
});
await advanceTimers(1000);
assertLog([
'Text:Inside render',
'Suspend:OutsideAsync',
@ -1690,8 +1688,6 @@ describe('ReactSuspenseEffectsSemantics', () => {
'Text:Inside destroy layout',
'Text:Fallback:Fallback create layout',
'Text:Fallback:Outside create layout',
]);
await waitForAll([
'Text:Fallback:Fallback create passive',
'Text:Fallback:Outside create passive',
]);
@ -1795,32 +1791,35 @@ describe('ReactSuspenseEffectsSemantics', () => {
// Suspending a component in the middle of the tree
// should still properly cleanup effects deeper in the tree
act(() => {
await act(async () => {
ReactNoop.render(<App shouldSuspend={true} />);
});
assertLog([
'Suspend:Suspend',
'Text:Fallback render',
'Text:Outside render',
]);
expect(ReactNoop).toMatchRenderedOutput(
<>
<span prop="Inside" />
<span prop="Outside" />
</>,
);
await waitFor([
'Suspend:Suspend',
'Text:Fallback render',
'Text:Outside render',
]);
expect(ReactNoop).toMatchRenderedOutput(
<>
<span prop="Inside" />
<span prop="Outside" />
</>,
);
// Timing out should commit the inner fallback and destroy outer fallback layout effects.
await advanceTimers(1000);
assertLog(['Text:Inside destroy layout', 'Text:Fallback create layout']);
await waitForAll(['Text:Fallback create passive']);
expect(ReactNoop).toMatchRenderedOutput(
<>
<span prop="Inside" hidden={true} />
<span prop="Fallback" />
<span prop="Outside" />
</>,
);
// Timing out should commit the inner fallback and destroy outer fallback layout effects.
await jest.runAllTimers();
assertLog([
'Text:Inside destroy layout',
'Text:Fallback create layout',
]);
await waitForAll(['Text:Fallback create passive']);
expect(ReactNoop).toMatchRenderedOutput(
<>
<span prop="Inside" hidden={true} />
<span prop="Fallback" />
<span prop="Outside" />
</>,
);
});
// Resolving should cleanup.
await act(async () => {
@ -2390,43 +2389,43 @@ describe('ReactSuspenseEffectsSemantics', () => {
);
// Schedule an update that causes React to suspend.
act(() => {
await act(async () => {
ReactNoop.render(
<App>
<AsyncText text="Async_1" ms={1000} />
<AsyncText text="Async_2" ms={2000} />
</App>,
);
await waitFor([
'Text:Function render',
'Suspend:Async_1',
'Suspend:Async_2',
'ClassText:Class render',
'ClassText:Fallback render',
]);
expect(ReactNoop).toMatchRenderedOutput(
<>
<span prop="Function" />
<span prop="Class" />
</>,
);
await jest.runAllTimers();
// Timing out should commit the fallback and destroy inner layout effects.
assertLog([
'Text:Function destroy layout',
'ClassText:Class componentWillUnmount',
'ClassText:Fallback componentDidMount',
]);
expect(ReactNoop).toMatchRenderedOutput(
<>
<span prop="Function" hidden={true} />
<span prop="Class" hidden={true} />
<span prop="Fallback" />
</>,
);
});
assertLog([
'Text:Function render',
'Suspend:Async_1',
'Suspend:Async_2',
'ClassText:Class render',
'ClassText:Fallback render',
]);
expect(ReactNoop).toMatchRenderedOutput(
<>
<span prop="Function" />
<span prop="Class" />
</>,
);
await advanceTimers(1000);
// Timing out should commit the fallback and destroy inner layout effects.
assertLog([
'Text:Function destroy layout',
'ClassText:Class componentWillUnmount',
'ClassText:Fallback componentDidMount',
]);
expect(ReactNoop).toMatchRenderedOutput(
<>
<span prop="Function" hidden={true} />
<span prop="Class" hidden={true} />
<span prop="Fallback" />
</>,
);
// Resolving the suspended resource should re-create inner layout effects.
await act(async () => {
@ -2549,40 +2548,40 @@ describe('ReactSuspenseEffectsSemantics', () => {
// Schedule an update that causes React to suspend.
textToRead = 'A';
act(() => {
await act(async () => {
ReactNoop.render(<App />);
await waitFor([
'Text:Function render',
'Suspender "A" render',
'Suspend:A',
'ClassText:Class render',
'ClassText:Fallback render',
]);
expect(ReactNoop).toMatchRenderedOutput(
<>
<span prop="Function" />
<span prop="Suspender" />
<span prop="Class" />
</>,
);
await jest.runAllTimers();
// Timing out should commit the fallback and destroy inner layout effects.
assertLog([
'Text:Function destroy layout',
'ClassText:Class componentWillUnmount',
'ClassText:Fallback componentDidMount',
]);
expect(ReactNoop).toMatchRenderedOutput(
<>
<span prop="Function" hidden={true} />
<span prop="Suspender" hidden={true} />
<span prop="Class" hidden={true} />
<span prop="Fallback" />
</>,
);
});
assertLog([
'Text:Function render',
'Suspender "A" render',
'Suspend:A',
'ClassText:Class render',
'ClassText:Fallback render',
]);
expect(ReactNoop).toMatchRenderedOutput(
<>
<span prop="Function" />
<span prop="Suspender" />
<span prop="Class" />
</>,
);
await advanceTimers(1000);
// Timing out should commit the fallback and destroy inner layout effects.
assertLog([
'Text:Function destroy layout',
'ClassText:Class componentWillUnmount',
'ClassText:Fallback componentDidMount',
]);
expect(ReactNoop).toMatchRenderedOutput(
<>
<span prop="Function" hidden={true} />
<span prop="Suspender" hidden={true} />
<span prop="Class" hidden={true} />
<span prop="Fallback" />
</>,
);
// Resolving the suspended resource should re-create inner layout effects.
textToRead = 'B';
@ -2712,7 +2711,7 @@ describe('ReactSuspenseEffectsSemantics', () => {
);
}
act(() => {
await act(async () => {
ReactNoop.renderLegacySyncRoot(<App />);
});
assertLog([
@ -2730,7 +2729,7 @@ describe('ReactSuspenseEffectsSemantics', () => {
expect(ReactNoop).toMatchRenderedOutput(null);
// Suspend the inner Suspense subtree (only inner effects should be destroyed)
act(() => {
await act(async () => {
ReactNoop.renderLegacySyncRoot(
<App children={<AsyncText text="Async" ms={1000} />} />,
);
@ -2811,7 +2810,7 @@ describe('ReactSuspenseEffectsSemantics', () => {
);
// Suspend the inner Suspense subtree (only inner effects should be destroyed)
act(() => {
await act(async () => {
ReactNoop.render(
<App children={<AsyncText text="Async" ms={1000} />} />,
);
@ -2829,6 +2828,7 @@ describe('ReactSuspenseEffectsSemantics', () => {
'RefCheckerOuter refCallback value? false',
'RefCheckerInner:refCallback destroy layout ref? false',
'Text:Fallback create layout',
'Text:Fallback create passive',
]);
expect(ReactNoop).toMatchRenderedOutput(
<>
@ -2843,7 +2843,6 @@ describe('ReactSuspenseEffectsSemantics', () => {
await resolveText('Async');
});
assertLog([
'Text:Fallback create passive',
'AsyncText:Async render',
'RefCheckerOuter render',
'RefCheckerInner:refObject render',
@ -2917,7 +2916,7 @@ describe('ReactSuspenseEffectsSemantics', () => {
expect(ReactNoop).toMatchRenderedOutput(null);
// Suspend the inner Suspense subtree (only inner effects should be destroyed)
act(() => {
await act(async () => {
ReactNoop.render(
<App children={<AsyncText text="Async" ms={1000} />} />,
);
@ -2937,6 +2936,7 @@ describe('ReactSuspenseEffectsSemantics', () => {
'RefCheckerOuter refCallback value? false',
'RefCheckerInner:refCallback destroy layout ref? false',
'Text:Fallback create layout',
'Text:Fallback create passive',
]);
expect(ReactNoop).toMatchRenderedOutput(<span prop="Fallback" />);
@ -2945,7 +2945,6 @@ describe('ReactSuspenseEffectsSemantics', () => {
await resolveText('Async');
});
assertLog([
'Text:Fallback create passive',
'AsyncText:Async render',
'RefCheckerOuter render',
'ClassComponent:refObject render',
@ -3021,7 +3020,7 @@ describe('ReactSuspenseEffectsSemantics', () => {
expect(ReactNoop).toMatchRenderedOutput(null);
// Suspend the inner Suspense subtree (only inner effects should be destroyed)
act(() => {
await act(async () => {
ReactNoop.render(
<App children={<AsyncText text="Async" ms={1000} />} />,
);
@ -3041,6 +3040,7 @@ describe('ReactSuspenseEffectsSemantics', () => {
'RefCheckerOuter refCallback value? false',
'RefCheckerInner:refCallback destroy layout ref? false',
'Text:Fallback create layout',
'Text:Fallback create passive',
]);
expect(ReactNoop).toMatchRenderedOutput(<span prop="Fallback" />);
@ -3049,7 +3049,6 @@ describe('ReactSuspenseEffectsSemantics', () => {
await resolveText('Async');
});
assertLog([
'Text:Fallback create passive',
'AsyncText:Async render',
'RefCheckerOuter render',
'FunctionComponent render',
@ -3130,7 +3129,7 @@ describe('ReactSuspenseEffectsSemantics', () => {
expect(ReactNoop).toMatchRenderedOutput(null);
// Suspend the inner Suspense subtree (only inner effects should be destroyed)
act(() => {
await act(async () => {
ReactNoop.render(
<App children={<AsyncText text="Async" ms={1000} />} />,
);
@ -3143,6 +3142,7 @@ describe('ReactSuspenseEffectsSemantics', () => {
'Text:Fallback render',
'RefChecker destroy layout ref? true',
'Text:Fallback create layout',
'Text:Fallback create passive',
]);
expect(ReactNoop).toMatchRenderedOutput(<span prop="Fallback" />);
@ -3151,7 +3151,6 @@ describe('ReactSuspenseEffectsSemantics', () => {
await resolveText('Async');
});
assertLog([
'Text:Fallback create passive',
'AsyncText:Async render',
'RefChecker render',
'Text:Fallback destroy layout',

View File

@ -17,6 +17,7 @@ let act;
let container;
let waitForAll;
let assertLog;
let fakeModuleCache;
describe('ReactSuspenseEffectsSemanticsDOM', () => {
beforeEach(() => {
@ -34,14 +35,53 @@ describe('ReactSuspenseEffectsSemanticsDOM', () => {
container = document.createElement('div');
document.body.appendChild(container);
fakeModuleCache = new Map();
});
afterEach(() => {
document.body.removeChild(container);
});
async function fakeImport(result) {
return {default: result};
async function fakeImport(Component) {
const record = fakeModuleCache.get(Component);
if (record === undefined) {
const newRecord = {
status: 'pending',
value: {default: Component},
pings: [],
then(ping) {
switch (newRecord.status) {
case 'pending': {
newRecord.pings.push(ping);
return;
}
case 'resolved': {
ping(newRecord.value);
return;
}
case 'rejected': {
throw newRecord.value;
}
}
},
};
fakeModuleCache.set(Component, newRecord);
return newRecord;
}
return record;
}
function resolveFakeImport(moduleName) {
const record = fakeModuleCache.get(moduleName);
if (record === undefined) {
throw new Error('Module not found');
}
if (record.status !== 'pending') {
throw new Error('Module already resolved');
}
record.status = 'resolved';
record.pings.forEach(ping => ping(record.value));
}
function Text(props) {
@ -49,7 +89,7 @@ describe('ReactSuspenseEffectsSemanticsDOM', () => {
return props.text;
}
it('should not cause a cycle when combined with a render phase update', () => {
it('should not cause a cycle when combined with a render phase update', async () => {
let scheduleSuspendingUpdate;
function App() {
@ -79,22 +119,22 @@ describe('ReactSuspenseEffectsSemanticsDOM', () => {
return <div ref={setRef} />;
}
const promise = Promise.resolve();
const neverResolves = {then() {}};
function ComponentThatSuspendsOnUpdate({shouldSuspend}) {
if (shouldSuspend) {
// Fake Suspend
throw promise;
throw neverResolves;
}
return null;
}
act(() => {
await act(async () => {
const root = ReactDOMClient.createRoot(container);
root.render(<App />);
});
act(() => {
await act(async () => {
scheduleSuspendingUpdate();
});
});
@ -142,12 +182,12 @@ describe('ReactSuspenseEffectsSemanticsDOM', () => {
}
const root = ReactDOMClient.createRoot(container);
act(() => {
await act(async () => {
root.render(<Parent swap={false} />);
});
assertLog(['Loading...']);
await LazyChildA;
await resolveFakeImport(ChildA);
await waitForAll(['A', 'Ref mount: A']);
expect(container.innerHTML).toBe('<span>A</span>');
@ -160,7 +200,7 @@ describe('ReactSuspenseEffectsSemanticsDOM', () => {
'<span style="display: none;">A</span>Loading...',
);
await LazyChildB;
await resolveFakeImport(ChildB);
await waitForAll(['B', 'Ref mount: B']);
expect(container.innerHTML).toBe('<span>B</span>');
});
@ -202,12 +242,12 @@ describe('ReactSuspenseEffectsSemanticsDOM', () => {
}
const root = ReactDOMClient.createRoot(container);
act(() => {
await act(async () => {
root.render(<Parent swap={false} />);
});
assertLog(['Loading...']);
await LazyChildA;
await resolveFakeImport(ChildA);
await waitForAll(['A', 'Did mount: A']);
expect(container.innerHTML).toBe('A');
@ -218,7 +258,7 @@ describe('ReactSuspenseEffectsSemanticsDOM', () => {
assertLog(['Loading...', 'Will unmount: A']);
expect(container.innerHTML).toBe('Loading...');
await LazyChildB;
await resolveFakeImport(ChildB);
await waitForAll(['B', 'Did mount: B']);
expect(container.innerHTML).toBe('B');
});
@ -254,12 +294,12 @@ describe('ReactSuspenseEffectsSemanticsDOM', () => {
}
const root = ReactDOMClient.createRoot(container);
act(() => {
await act(async () => {
root.render(<Parent swap={false} />);
});
assertLog(['Loading...']);
await LazyChildA;
await resolveFakeImport(ChildA);
await waitForAll(['A', 'Did mount: A']);
expect(container.innerHTML).toBe('A');
@ -321,12 +361,12 @@ describe('ReactSuspenseEffectsSemanticsDOM', () => {
}
const root = ReactDOMClient.createRoot(container);
act(() => {
await act(async () => {
root.render(<Parent swap={false} />);
});
assertLog(['Loading...']);
await LazyChildA;
await resolveFakeImport(ChildA);
await waitForAll(['A', 'Ref mount: A']);
expect(container.innerHTML).toBe('<span>A</span>');
@ -384,12 +424,12 @@ describe('ReactSuspenseEffectsSemanticsDOM', () => {
}
const root = ReactDOMClient.createRoot(container);
act(() => {
await act(async () => {
root.render(<Parent swap={false} />);
});
assertLog(['Loading...']);
await LazyChildA;
await resolveFakeImport(ChildA);
await waitForAll(['A', 'Did mount: A']);
expect(container.innerHTML).toBe('A');

View File

@ -138,14 +138,14 @@ describe('ReactSuspenseFuzz', () => {
return resolvedText;
}
function resolveAllTasks() {
async function resolveAllTasks() {
Scheduler.unstable_flushAllWithoutAsserting();
let elapsedTime = 0;
while (pendingTasks && pendingTasks.size > 0) {
if ((elapsedTime += 1000) > 1000000) {
throw new Error('Something did not resolve properly.');
}
act(() => {
await act(async () => {
ReactNoop.batchedUpdates(() => {
jest.advanceTimersByTime(1000);
});
@ -154,7 +154,7 @@ describe('ReactSuspenseFuzz', () => {
}
}
function testResolvedOutput(unwrappedChildren) {
async function testResolvedOutput(unwrappedChildren) {
const children = (
<Suspense fallback="Loading...">{unwrappedChildren}</Suspense>
);
@ -166,17 +166,15 @@ describe('ReactSuspenseFuzz', () => {
{children}
</ShouldSuspendContext.Provider>,
);
resolveAllTasks();
await resolveAllTasks();
const expectedOutput = expectedRoot.getChildrenAsJSX();
gate(flags => {
resetCache();
ReactNoop.renderLegacySyncRoot(children);
resolveAllTasks();
const legacyOutput = ReactNoop.getChildrenAsJSX();
expect(legacyOutput).toEqual(expectedOutput);
ReactNoop.renderLegacySyncRoot(null);
});
resetCache();
ReactNoop.renderLegacySyncRoot(children);
await resolveAllTasks();
const legacyOutput = ReactNoop.getChildrenAsJSX();
expect(legacyOutput).toEqual(expectedOutput);
ReactNoop.renderLegacySyncRoot(null);
}
function pickRandomWeighted(rand, options) {
@ -298,10 +296,10 @@ describe('ReactSuspenseFuzz', () => {
return {Container, Text, testResolvedOutput, generateTestCase};
}
it('basic cases', () => {
it('basic cases', async () => {
// This demonstrates that the testing primitives work
const {Container, Text, testResolvedOutput} = createFuzzer();
testResolvedOutput(
await testResolvedOutput(
<Container updates={[{remountAfter: 150}]}>
<Text
text="Hi"
@ -312,7 +310,7 @@ describe('ReactSuspenseFuzz', () => {
);
});
it(`generative tests (random seed: ${SEED})`, () => {
it(`generative tests (random seed: ${SEED})`, async () => {
const {generateTestCase, testResolvedOutput} = createFuzzer();
const rand = Random.create(SEED);
@ -323,7 +321,7 @@ describe('ReactSuspenseFuzz', () => {
for (let i = 0; i < NUMBER_OF_TEST_CASES; i++) {
const randomTestCase = generateTestCase(rand, ELEMENTS_PER_CASE);
try {
testResolvedOutput(randomTestCase);
await testResolvedOutput(randomTestCase);
} catch (e) {
console.log(`
Failed fuzzy test case:
@ -339,9 +337,9 @@ Random seed is ${SEED}
});
describe('hard-coded cases', () => {
it('1', () => {
it('1', async () => {
const {Text, testResolvedOutput} = createFuzzer();
testResolvedOutput(
await testResolvedOutput(
<>
<Text
initialDelay={20}
@ -360,9 +358,9 @@ Random seed is ${SEED}
);
});
it('2', () => {
it('2', async () => {
const {Text, Container, testResolvedOutput} = createFuzzer();
testResolvedOutput(
await testResolvedOutput(
<>
<Suspense fallback="Loading...">
<Text initialDelay={7200} text="A" />
@ -378,9 +376,9 @@ Random seed is ${SEED}
);
});
it('3', () => {
it('3', async () => {
const {Text, Container, testResolvedOutput} = createFuzzer();
testResolvedOutput(
await testResolvedOutput(
<>
<Suspense fallback="Loading...">
<Text
@ -412,9 +410,9 @@ Random seed is ${SEED}
);
});
it('4', () => {
it('4', async () => {
const {Text, testResolvedOutput} = createFuzzer();
testResolvedOutput(
await testResolvedOutput(
<React.Suspense fallback="Loading...">
<React.Suspense>
<React.Suspense>

View File

@ -2275,7 +2275,7 @@ describe('ReactSuspenseList', () => {
);
// Update the row adjacent to the list
act(() => updateAdjacent('C'));
await act(async () => updateAdjacent('C'));
assertLog(['C']);
@ -2332,7 +2332,7 @@ describe('ReactSuspenseList', () => {
const previousInst = setAsyncB;
// During an update we suspend on B.
act(() => setAsyncB(true));
await act(async () => setAsyncB(true));
assertLog([
'Suspend! [B]',
@ -2350,7 +2350,7 @@ describe('ReactSuspenseList', () => {
// Before we resolve we'll rerender the whole list.
// This should leave the tree intact.
act(() => ReactNoop.render(<Foo updateList={true} />));
await act(async () => ReactNoop.render(<Foo updateList={true} />));
assertLog(['A', 'Suspend! [B]', 'Loading B']);
@ -2421,7 +2421,7 @@ describe('ReactSuspenseList', () => {
const previousInst = setAsyncB;
// During an update we suspend on B.
act(() => setAsyncB(true));
await act(async () => setAsyncB(true));
assertLog([
'Suspend! [B]',
@ -2439,7 +2439,7 @@ describe('ReactSuspenseList', () => {
// Before we resolve we'll rerender the whole list.
// This should leave the tree intact.
act(() => ReactNoop.render(<Foo updateList={true} />));
await act(async () => ReactNoop.render(<Foo updateList={true} />));
assertLog(['A', 'Suspend! [B]', 'Loading B']);

View File

@ -2086,7 +2086,7 @@ describe('ReactSuspenseWithNoopRenderer', () => {
});
// TODO: assert toErrorDev() when the warning is implemented again.
act(() => {
await act(async () => {
ReactNoop.flushSync(() => _setShow(true));
});
});
@ -2113,7 +2113,7 @@ describe('ReactSuspenseWithNoopRenderer', () => {
});
// TODO: assert toErrorDev() when the warning is implemented again.
act(() => {
await act(async () => {
ReactNoop.flushSync(() => show());
});
});
@ -2142,7 +2142,7 @@ describe('ReactSuspenseWithNoopRenderer', () => {
assertLog(['Suspend! [A]']);
expect(ReactNoop).toMatchRenderedOutput('Loading...');
act(() => {
await act(async () => {
ReactNoop.flushSync(() => showB());
});
@ -2173,7 +2173,7 @@ describe('ReactSuspenseWithNoopRenderer', () => {
});
// TODO: assert toErrorDev() when the warning is implemented again.
act(() => {
await act(async () => {
ReactNoop.flushSync(() => _setShow(true));
});
},

View File

@ -308,7 +308,7 @@ describe('updaters', () => {
expect(allSchedulerTypes).toEqual([[null], [Suspender]]);
expect(resolver).not.toBeNull();
await act(() => {
await act(async () => {
resolver('abc');
return promise;
});

View File

@ -88,7 +88,7 @@ describe('useEffectEvent', () => {
</>,
);
act(button.current.increment);
await act(async () => button.current.increment());
assertLog(['Increment', 'Count: 1']);
expect(ReactNoop).toMatchRenderedOutput(
<>
@ -97,7 +97,7 @@ describe('useEffectEvent', () => {
</>,
);
act(button.current.increment);
await act(async () => button.current.increment());
assertLog([
'Increment',
// Event should use the updated callback function closed over the new value.
@ -121,7 +121,7 @@ describe('useEffectEvent', () => {
);
// Event uses the new prop
act(button.current.increment);
await act(async () => button.current.increment());
assertLog(['Increment', 'Count: 12']);
expect(ReactNoop).toMatchRenderedOutput(
<>
@ -174,7 +174,7 @@ describe('useEffectEvent', () => {
</>,
);
act(button.current.increment);
await act(async () => button.current.increment());
assertLog(['Increment', 'Count: 5']);
expect(ReactNoop).toMatchRenderedOutput(
<>
@ -183,7 +183,7 @@ describe('useEffectEvent', () => {
</>,
);
act(button.current.multiply);
await act(async () => button.current.multiply());
assertLog(['Increment', 'Count: 25']);
expect(ReactNoop).toMatchRenderedOutput(
<>
@ -233,7 +233,7 @@ describe('useEffectEvent', () => {
</>,
);
act(button.current.greet);
await act(async () => button.current.greet());
assertLog(['Say hej', 'Greeting: undefined says hej']);
expect(ReactNoop).toMatchRenderedOutput(
<>
@ -327,7 +327,7 @@ describe('useEffectEvent', () => {
</>,
);
act(button.current.increment);
await act(async () => button.current.increment());
assertLog([
'Increment',
// Effect should not re-run because the dependency hasn't changed.
@ -340,7 +340,7 @@ describe('useEffectEvent', () => {
</>,
);
act(button.current.increment);
await act(async () => button.current.increment());
assertLog([
'Increment',
// Event should use the updated callback function closed over the new value.
@ -370,7 +370,7 @@ describe('useEffectEvent', () => {
);
// Event uses the new prop
act(button.current.increment);
await act(async () => button.current.increment());
assertLog(['Increment', 'Count: 34']);
expect(ReactNoop).toMatchRenderedOutput(
<>
@ -426,7 +426,7 @@ describe('useEffectEvent', () => {
</>,
);
act(button.current.increment);
await act(async () => button.current.increment());
assertLog([
'Increment',
// Effect should not re-run because the dependency hasn't changed.
@ -439,7 +439,7 @@ describe('useEffectEvent', () => {
</>,
);
act(button.current.increment);
await act(async () => button.current.increment());
assertLog([
'Increment',
// Event should use the updated callback function closed over the new value.
@ -469,7 +469,7 @@ describe('useEffectEvent', () => {
);
// Event uses the new prop
act(button.current.increment);
await act(async () => button.current.increment());
assertLog(['Increment', 'Count: 34']);
expect(ReactNoop).toMatchRenderedOutput(
<>
@ -531,7 +531,7 @@ describe('useEffectEvent', () => {
</>,
);
act(button.current.increment);
await act(async () => button.current.increment());
assertLog([
'Increment',
// Effect should not re-run because the dependency hasn't changed.
@ -544,7 +544,7 @@ describe('useEffectEvent', () => {
</>,
);
act(button.current.increment);
await act(async () => button.current.increment());
assertLog([
'Increment',
// Event should use the updated callback function closed over the new value.
@ -574,7 +574,7 @@ describe('useEffectEvent', () => {
);
// Event uses the new prop
act(button.current.increment);
await act(async () => button.current.increment());
assertLog(['Increment', 'Count: 34']);
expect(ReactNoop).toMatchRenderedOutput(
<>
@ -849,7 +849,7 @@ describe('useEffectEvent', () => {
),
);
assertLog(['Add to cart', 'url: /shop/1, numberOfItems: 0']);
act(button.current.addToCart);
await act(async () => button.current.addToCart());
assertLog(['Add to cart']);
await act(async () =>

View File

@ -3783,7 +3783,7 @@ describe('ReactFresh', () => {
}
// This simulates the scenario in https://github.com/facebook/react/issues/17626
it('can inject the runtime after the renderer executes', () => {
it('can inject the runtime after the renderer executes', async () => {
if (__DEV__) {
initFauxDevToolsHook();
@ -3792,7 +3792,8 @@ describe('ReactFresh', () => {
React = require('react');
ReactDOM = require('react-dom');
Scheduler = require('scheduler');
act = require('jest-react').act;
act = require('react-dom/test-utils').act;
internalAct = require('jest-react').act;
// Important! Inject into the global hook *after* ReactDOM runs:
ReactFreshRuntime = require('react-refresh/runtime');

View File

@ -346,7 +346,7 @@ describe(`onRender`, () => {
Scheduler.unstable_advanceTime(20); // 30 -> 50
// Updating a sibling should not report a re-render.
act(updateProfilerSibling);
await act(async () => updateProfilerSibling());
expect(callback).not.toHaveBeenCalled();
});