Codemod tests to waitFor pattern (6/?) (#26305)

This converts some of our test suite to use the `waitFor` test pattern,
instead of the `expect(Scheduler).toFlushAndYield` pattern. Most of
these changes are automated with jscodeshift, with some slight manual
cleanup in certain cases.

See #26285 for full context.
This commit is contained in:
Andrew Clark
2023-03-04 11:45:26 -05:00
committed by GitHub
parent 9a52cc8bcd
commit e98695db91
10 changed files with 517 additions and 556 deletions
+44 -48
View File
@@ -18,6 +18,8 @@ let ReactNoop;
let Suspense;
let Scheduler;
let act;
let waitForAll;
let assertLog;
describe('memo', () => {
beforeEach(() => {
@@ -29,6 +31,10 @@ describe('memo', () => {
Scheduler = require('scheduler');
act = require('jest-react').act;
({Suspense} = React);
const InternalTestUtils = require('internal-test-utils');
waitForAll = InternalTestUtils.waitForAll;
assertLog = InternalTestUtils.assertLog;
});
function Text(props) {
@@ -105,9 +111,7 @@ describe('memo', () => {
<Counter count={0} />
</Suspense>,
);
expect(Scheduler).toFlushAndYield(['Loading...']);
await Promise.resolve();
expect(Scheduler).toFlushAndYield([0]);
await waitForAll(['Loading...', 0]);
expect(ReactNoop).toMatchRenderedOutput(<span prop={0} />);
// Should bail out because props have not changed
@@ -116,7 +120,7 @@ describe('memo', () => {
<Counter count={0} />
</Suspense>,
);
expect(Scheduler).toFlushAndYield([]);
await waitForAll([]);
expect(ReactNoop).toMatchRenderedOutput(<span prop={0} />);
// Should update because count prop changed
@@ -125,7 +129,7 @@ describe('memo', () => {
<Counter count={1} />
</Suspense>,
);
expect(Scheduler).toFlushAndYield([1]);
await waitForAll([1]);
expect(ReactNoop).toMatchRenderedOutput(<span prop={1} />);
});
@@ -160,19 +164,17 @@ describe('memo', () => {
const parent = React.createRef(null);
ReactNoop.render(<Parent ref={parent} />);
expect(Scheduler).toFlushAndYield(['Loading...']);
await Promise.resolve();
expect(Scheduler).toFlushAndYield(['Count: 0']);
await waitForAll(['Loading...', 'Count: 0']);
expect(ReactNoop).toMatchRenderedOutput(<span prop="Count: 0" />);
// Should bail out because props have not changed
ReactNoop.render(<Parent ref={parent} />);
expect(Scheduler).toFlushAndYield([]);
await waitForAll([]);
expect(ReactNoop).toMatchRenderedOutput(<span prop="Count: 0" />);
// Should update because there was a context change
parent.current.setState({count: 1});
expect(Scheduler).toFlushAndYield(['Count: 1']);
await waitForAll(['Count: 1']);
expect(ReactNoop).toMatchRenderedOutput(<span prop="Count: 1" />);
});
@@ -273,7 +275,7 @@ describe('memo', () => {
await act(async () => {
root.render(<App prop="A" />);
});
expect(Scheduler).toHaveYielded([
assertLog([
'SimpleMemo [A0]',
'ComplexMemo [A0]',
'MemoWithIndirection [A0]',
@@ -283,7 +285,7 @@ describe('memo', () => {
await act(async () => {
root.render(<App prop="B" />);
});
expect(Scheduler).toHaveYielded([
assertLog([
'SimpleMemo [B0]',
'ComplexMemo [B0]',
'MemoWithIndirection [B0]',
@@ -298,7 +300,7 @@ describe('memo', () => {
root.render(<App prop="B" />);
});
// Nothing re-renders
expect(Scheduler).toHaveYielded([]);
assertLog([]);
// Demonstrate what happens when the prop object changes, it bails out
// because all the props are the same, but we still render the
@@ -309,7 +311,7 @@ describe('memo', () => {
});
// The components should re-render with the new local state, but none
// of the props objects should have changed
expect(Scheduler).toHaveYielded([
assertLog([
'SimpleMemo [B1]',
'ComplexMemo [B1]',
'MemoWithIndirection [B1]',
@@ -322,7 +324,7 @@ describe('memo', () => {
});
// The components should re-render with the new local state, but none
// of the props objects should have changed
expect(Scheduler).toHaveYielded([
assertLog([
'SimpleMemo [B2]',
'ComplexMemo [B2]',
'MemoWithIndirection [B2]',
@@ -345,9 +347,7 @@ describe('memo', () => {
<Counter count={0} />
</Suspense>,
);
expect(Scheduler).toFlushAndYield(['Loading...']);
await Promise.resolve();
expect(Scheduler).toFlushAndYield([0]);
await waitForAll(['Loading...', 0]);
expect(ReactNoop).toMatchRenderedOutput(<span prop={0} />);
// Should bail out because props have not changed
@@ -356,7 +356,7 @@ describe('memo', () => {
<Counter count={0} />
</Suspense>,
);
expect(Scheduler).toFlushAndYield(['Old count: 0, New count: 0']);
await waitForAll(['Old count: 0, New count: 0']);
expect(ReactNoop).toMatchRenderedOutput(<span prop={0} />);
// Should update because count prop changed
@@ -365,7 +365,7 @@ describe('memo', () => {
<Counter count={1} />
</Suspense>,
);
expect(Scheduler).toFlushAndYield(['Old count: 0, New count: 1', 1]);
await waitForAll(['Old count: 0, New count: 1', 1]);
expect(ReactNoop).toMatchRenderedOutput(<span prop={1} />);
});
@@ -383,9 +383,7 @@ describe('memo', () => {
<Counter count={0} />
</Suspense>,
);
expect(Scheduler).toFlushAndYield(['Loading...']);
await Promise.resolve();
expect(Scheduler).toFlushAndYield(['0!']);
await waitForAll(['Loading...', '0!']);
expect(ReactNoop).toMatchRenderedOutput(<span prop="0!" />);
// Should bail out because props have not changed
@@ -394,7 +392,7 @@ describe('memo', () => {
<Counter count={0} />
</Suspense>,
);
expect(Scheduler).toFlushAndYield([]);
await waitForAll([]);
expect(ReactNoop).toMatchRenderedOutput(<span prop="0!" />);
// Should update because count prop changed
@@ -403,7 +401,7 @@ describe('memo', () => {
<Counter count={1} />
</Suspense>,
);
expect(Scheduler).toFlushAndYield(['1!']);
await waitForAll(['1!']);
expect(ReactNoop).toMatchRenderedOutput(<span prop="1!" />);
});
@@ -436,10 +434,8 @@ describe('memo', () => {
<Counter e={5} />
</Suspense>,
);
expect(Scheduler).toFlushAndYield(['Loading...']);
await Promise.resolve();
expect(() => {
expect(Scheduler).toFlushAndYield([15]);
await expect(async () => {
await waitForAll(['Loading...', 15]);
}).toErrorDev([
'Counter: Support for defaultProps will be removed from memo components in a future major release. Use JavaScript default parameters instead.',
]);
@@ -451,7 +447,7 @@ describe('memo', () => {
<Counter e={5} />
</Suspense>,
);
expect(Scheduler).toFlushAndYield([]);
await waitForAll([]);
expect(ReactNoop).toMatchRenderedOutput(<span prop={15} />);
// Should update because count prop changed
@@ -460,7 +456,7 @@ describe('memo', () => {
<Counter e={10} />
</Suspense>,
);
expect(Scheduler).toFlushAndYield([20]);
await waitForAll([20]);
expect(ReactNoop).toMatchRenderedOutput(<span prop={20} />);
});
@@ -480,7 +476,7 @@ describe('memo', () => {
);
});
it('validates propTypes declared on the inner component', () => {
it('validates propTypes declared on the inner component', async () => {
function FnInner(props) {
return props.inner;
}
@@ -488,23 +484,23 @@ describe('memo', () => {
const Fn = React.memo(FnInner);
// Mount
expect(() => {
await expect(async () => {
ReactNoop.render(<Fn inner="2" />);
expect(Scheduler).toFlushWithoutYielding();
await waitForAll([]);
}).toErrorDev(
'Invalid prop `inner` of type `string` supplied to `FnInner`, expected `number`.',
);
// Update
expect(() => {
await expect(async () => {
ReactNoop.render(<Fn inner={false} />);
expect(Scheduler).toFlushWithoutYielding();
await waitForAll([]);
}).toErrorDev(
'Invalid prop `inner` of type `boolean` supplied to `FnInner`, expected `number`.',
);
});
it('validates propTypes declared on the outer component', () => {
it('validates propTypes declared on the outer component', async () => {
function FnInner(props) {
return props.outer;
}
@@ -512,25 +508,25 @@ describe('memo', () => {
Fn.propTypes = {outer: PropTypes.number.isRequired};
// Mount
expect(() => {
await expect(async () => {
ReactNoop.render(<Fn outer="3" />);
expect(Scheduler).toFlushWithoutYielding();
await waitForAll([]);
}).toErrorDev(
// Outer props are checked in createElement
'Invalid prop `outer` of type `string` supplied to `FnInner`, expected `number`.',
);
// Update
expect(() => {
await expect(async () => {
ReactNoop.render(<Fn outer={false} />);
expect(Scheduler).toFlushWithoutYielding();
await waitForAll([]);
}).toErrorDev(
// Outer props are checked in createElement
'Invalid prop `outer` of type `boolean` supplied to `FnInner`, expected `number`.',
);
});
it('validates nested propTypes declarations', () => {
it('validates nested propTypes declarations', async () => {
function Inner(props) {
return props.inner + props.middle + props.outer;
}
@@ -549,20 +545,20 @@ describe('memo', () => {
<Outer />
</div>,
);
expect(() => {
expect(Scheduler).toFlushWithoutYielding();
await expect(async () => {
await waitForAll([]);
}).toErrorDev([
'Inner: Support for defaultProps will be removed from memo components in a future major release. Use JavaScript default parameters instead.',
]);
// Mount
expect(() => {
await expect(async () => {
ReactNoop.render(
<div>
<Outer inner="2" middle="3" outer="4" />
</div>,
);
expect(Scheduler).toFlushWithoutYielding();
await waitForAll([]);
}).toErrorDev([
'Invalid prop `outer` of type `string` supplied to `Inner`, expected `number`.',
'Invalid prop `middle` of type `string` supplied to `Inner`, expected `number`.',
@@ -570,13 +566,13 @@ describe('memo', () => {
]);
// Update
expect(() => {
await expect(async () => {
ReactNoop.render(
<div>
<Outer inner={false} middle={false} outer={false} />
</div>,
);
expect(Scheduler).toFlushWithoutYielding();
await waitForAll([]);
}).toErrorDev([
'Invalid prop `outer` of type `boolean` supplied to `Inner`, expected `number`.',
'Invalid prop `middle` of type `boolean` supplied to `Inner`, expected `number`.',
@@ -14,6 +14,8 @@ let useContext;
let ReactNoop;
let Scheduler;
let gen;
let waitForAll;
let waitFor;
describe('ReactNewContext', () => {
beforeEach(() => {
@@ -24,6 +26,10 @@ describe('ReactNewContext', () => {
ReactNoop = require('react-noop-renderer');
Scheduler = require('scheduler');
gen = require('random-seed');
const InternalTestUtils = require('internal-test-utils');
waitForAll = InternalTestUtils.waitForAll;
waitFor = InternalTestUtils.waitFor;
});
afterEach(() => {
@@ -110,7 +116,7 @@ describe('ReactNewContext', () => {
function sharedContextTests(label, getConsumer) {
describe(`reading context with ${label}`, () => {
it('simple mount and update', () => {
it('simple mount and update', async () => {
const Context = React.createContext(1);
const Consumer = getConsumer(Context);
@@ -131,16 +137,16 @@ describe('ReactNewContext', () => {
}
ReactNoop.render(<App value={2} />);
expect(Scheduler).toFlushWithoutYielding();
await waitForAll([]);
expect(ReactNoop).toMatchRenderedOutput(<span prop="Result: 2" />);
// Update
ReactNoop.render(<App value={3} />);
expect(Scheduler).toFlushWithoutYielding();
await waitForAll([]);
expect(ReactNoop).toMatchRenderedOutput(<span prop="Result: 3" />);
});
it('propagates through shouldComponentUpdate false', () => {
it('propagates through shouldComponentUpdate false', async () => {
const Context = React.createContext(1);
const ContextConsumer = getConsumer(Context);
@@ -189,7 +195,7 @@ describe('ReactNewContext', () => {
}
ReactNoop.render(<App value={2} />);
expect(Scheduler).toFlushAndYield([
await waitForAll([
'App',
'Provider',
'Indirection',
@@ -201,15 +207,11 @@ describe('ReactNewContext', () => {
// Update
ReactNoop.render(<App value={3} />);
expect(Scheduler).toFlushAndYield([
'App',
'Provider',
'Consumer render prop',
]);
await waitForAll(['App', 'Provider', 'Consumer render prop']);
expect(ReactNoop).toMatchRenderedOutput(<span prop="Result: 3" />);
});
it('consumers bail out if context value is the same', () => {
it('consumers bail out if context value is the same', async () => {
const Context = React.createContext(1);
const ContextConsumer = getConsumer(Context);
@@ -258,7 +260,7 @@ describe('ReactNewContext', () => {
}
ReactNoop.render(<App value={2} />);
expect(Scheduler).toFlushAndYield([
await waitForAll([
'App',
'Provider',
'Indirection',
@@ -270,7 +272,7 @@ describe('ReactNewContext', () => {
// Update with the same context value
ReactNoop.render(<App value={2} />);
expect(Scheduler).toFlushAndYield([
await waitForAll([
'App',
'Provider',
// Don't call render prop again
@@ -278,7 +280,7 @@ describe('ReactNewContext', () => {
expect(ReactNoop).toMatchRenderedOutput(<span prop="Result: 2" />);
});
it('nested providers', () => {
it('nested providers', async () => {
const Context = React.createContext(1);
const Consumer = getConsumer(Context);
@@ -325,16 +327,16 @@ describe('ReactNewContext', () => {
}
ReactNoop.render(<App value={2} />);
expect(Scheduler).toFlushWithoutYielding();
await waitForAll([]);
expect(ReactNoop).toMatchRenderedOutput(<span prop="Result: 8" />);
// Update
ReactNoop.render(<App value={3} />);
expect(Scheduler).toFlushWithoutYielding();
await waitForAll([]);
expect(ReactNoop).toMatchRenderedOutput(<span prop="Result: 12" />);
});
it('should provide the correct (default) values to consumers outside of a provider', () => {
it('should provide the correct (default) values to consumers outside of a provider', async () => {
const FooContext = React.createContext({value: 'foo-initial'});
const BarContext = React.createContext({value: 'bar-initial'});
const FooConsumer = getConsumer(FooContext);
@@ -369,10 +371,10 @@ describe('ReactNewContext', () => {
</BarConsumer>
</>,
);
expect(Scheduler).toFlushWithoutYielding();
await waitForAll([]);
});
it('multiple consumers in different branches', () => {
it('multiple consumers in different branches', async () => {
const Context = React.createContext(1);
const Consumer = getConsumer(Context);
@@ -420,7 +422,7 @@ describe('ReactNewContext', () => {
}
ReactNoop.render(<App value={2} />);
expect(Scheduler).toFlushWithoutYielding();
await waitForAll([]);
expect(ReactNoop).toMatchRenderedOutput(
<>
<span prop="Result: 4" />
@@ -430,7 +432,7 @@ describe('ReactNewContext', () => {
// Update
ReactNoop.render(<App value={3} />);
expect(Scheduler).toFlushWithoutYielding();
await waitForAll([]);
expect(ReactNoop).toMatchRenderedOutput(
<>
<span prop="Result: 6" />
@@ -440,7 +442,7 @@ describe('ReactNewContext', () => {
// Another update
ReactNoop.render(<App value={4} />);
expect(Scheduler).toFlushWithoutYielding();
await waitForAll([]);
expect(ReactNoop).toMatchRenderedOutput(
<>
<span prop="Result: 8" />
@@ -449,7 +451,7 @@ describe('ReactNewContext', () => {
);
});
it('compares context values with Object.is semantics', () => {
it('compares context values with Object.is semantics', async () => {
const Context = React.createContext(1);
const ContextConsumer = getConsumer(Context);
@@ -498,7 +500,7 @@ describe('ReactNewContext', () => {
}
ReactNoop.render(<App value={NaN} />);
expect(Scheduler).toFlushAndYield([
await waitForAll([
'App',
'Provider',
'Indirection',
@@ -510,7 +512,7 @@ describe('ReactNewContext', () => {
// Update
ReactNoop.render(<App value={NaN} />);
expect(Scheduler).toFlushAndYield([
await waitForAll([
'App',
'Provider',
// Consumer should not re-render again
@@ -519,7 +521,7 @@ describe('ReactNewContext', () => {
expect(ReactNoop).toMatchRenderedOutput(<span prop="Result: NaN" />);
});
it('context unwinds when interrupted', () => {
it('context unwinds when interrupted', async () => {
const Context = React.createContext('Default');
const ContextConsumer = getConsumer(Context);
@@ -564,14 +566,14 @@ describe('ReactNewContext', () => {
}
ReactNoop.render(<App value="A" />);
expect(Scheduler).toFlushWithoutYielding();
await waitForAll([]);
expect(ReactNoop).toMatchRenderedOutput(
// The second provider should use the default value.
<span prop="Result: Does not unwind" />,
);
});
it("does not re-render if there's an update in a child", () => {
it("does not re-render if there's an update in a child", async () => {
const Context = React.createContext(0);
const Consumer = getConsumer(Context);
@@ -603,19 +605,19 @@ describe('ReactNewContext', () => {
// Initial mount
ReactNoop.render(<App value={1} />);
expect(Scheduler).toFlushAndYield(['Consumer render prop', 'Child']);
await waitForAll(['Consumer render prop', 'Child']);
expect(ReactNoop).toMatchRenderedOutput(
<span prop="Context: 1, Step: 0" />,
);
child.setState({step: 1});
expect(Scheduler).toFlushAndYield(['Child']);
await waitForAll(['Child']);
expect(ReactNoop).toMatchRenderedOutput(
<span prop="Context: 1, Step: 1" />,
);
});
it('consumer bails out if value is unchanged and something above bailed out', () => {
it('consumer bails out if value is unchanged and something above bailed out', async () => {
const Context = React.createContext(0);
const Consumer = getConsumer(Context);
@@ -660,7 +662,7 @@ describe('ReactNewContext', () => {
// Initial mount
ReactNoop.render(<App value={1} />);
expect(Scheduler).toFlushAndYield([
await waitForAll([
'App',
'PureIndirection',
'ChildWithInlineRenderCallback',
@@ -677,7 +679,7 @@ describe('ReactNewContext', () => {
// Update (bailout)
ReactNoop.render(<App value={1} />);
expect(Scheduler).toFlushAndYield(['App']);
await waitForAll(['App']);
expect(ReactNoop).toMatchRenderedOutput(
<>
<span prop={1} />
@@ -687,7 +689,7 @@ describe('ReactNewContext', () => {
// Update (no bailout)
ReactNoop.render(<App value={2} />);
expect(Scheduler).toFlushAndYield(['App', 'Consumer', 'Consumer']);
await waitForAll(['App', 'Consumer', 'Consumer']);
expect(ReactNoop).toMatchRenderedOutput(
<>
<span prop={2} />
@@ -697,7 +699,7 @@ describe('ReactNewContext', () => {
});
// @gate www
it("context consumer doesn't bail out inside hidden subtree", () => {
it("context consumer doesn't bail out inside hidden subtree", async () => {
const Context = React.createContext('dark');
const Consumer = getConsumer(Context);
@@ -712,7 +714,7 @@ describe('ReactNewContext', () => {
}
ReactNoop.render(<App theme="dark" />);
expect(Scheduler).toFlushAndYield(['dark']);
await waitForAll(['dark']);
expect(ReactNoop.getChildrenAsJSX()).toEqual(
<div hidden={true}>
<span prop="dark" />
@@ -720,7 +722,7 @@ describe('ReactNewContext', () => {
);
ReactNoop.render(<App theme="light" />);
expect(Scheduler).toFlushAndYield(['light']);
await waitForAll(['light']);
expect(ReactNoop.getChildrenAsJSX()).toEqual(
<div hidden={true}>
<span prop="light" />
@@ -729,7 +731,7 @@ describe('ReactNewContext', () => {
});
// This is a regression case for https://github.com/facebook/react/issues/12389.
it('does not run into an infinite loop', () => {
it('does not run into an infinite loop', async () => {
const Context = React.createContext(null);
const Consumer = getConsumer(Context);
@@ -759,15 +761,15 @@ describe('ReactNewContext', () => {
}
ReactNoop.render(<App reverse={false} />);
expect(Scheduler).toFlushWithoutYielding();
await waitForAll([]);
ReactNoop.render(<App reverse={true} />);
expect(Scheduler).toFlushWithoutYielding();
await waitForAll([]);
ReactNoop.render(<App reverse={false} />);
expect(Scheduler).toFlushWithoutYielding();
await waitForAll([]);
});
// This is a regression case for https://github.com/facebook/react/issues/12686
it('does not skip some siblings', () => {
it('does not skip some siblings', async () => {
const Context = React.createContext(0);
const ContextConsumer = getConsumer(Context);
@@ -816,7 +818,7 @@ describe('ReactNewContext', () => {
// Initial mount
let inst;
ReactNoop.render(<App ref={ref => (inst = ref)} />);
expect(Scheduler).toFlushAndYield(['App']);
await waitForAll(['App']);
expect(ReactNoop).toMatchRenderedOutput(
<>
<span prop="static 1" />
@@ -825,7 +827,7 @@ describe('ReactNewContext', () => {
);
// Update the first time
inst.setState({step: 1});
expect(Scheduler).toFlushAndYield(['App', 'Consumer']);
await waitForAll(['App', 'Consumer']);
expect(ReactNoop).toMatchRenderedOutput(
<>
<span prop="static 1" />
@@ -835,7 +837,7 @@ describe('ReactNewContext', () => {
);
// Update the second time
inst.setState({step: 2});
expect(Scheduler).toFlushAndYield(['App', 'Consumer']);
await waitForAll(['App', 'Consumer']);
expect(ReactNoop).toMatchRenderedOutput(
<>
<span prop="static 1" />
@@ -863,7 +865,7 @@ describe('ReactNewContext', () => {
);
});
it('warns if multiple renderers concurrently render the same context', () => {
it('warns if multiple renderers concurrently render the same context', async () => {
spyOnDev(console, 'error').mockImplementation(() => {});
const Context = React.createContext(0);
@@ -885,17 +887,20 @@ describe('ReactNewContext', () => {
ReactNoop.render(<App value={1} />);
});
// Render past the Provider, but don't commit yet
expect(Scheduler).toFlushAndYieldThrough(['Foo']);
await waitFor(['Foo']);
// Get a new copy of ReactNoop
jest.resetModules();
React = require('react');
ReactNoop = require('react-noop-renderer');
Scheduler = require('scheduler');
const InternalTestUtils = require('internal-test-utils');
waitForAll = InternalTestUtils.waitForAll;
waitFor = InternalTestUtils.waitFor;
// Render the provider again using a different renderer
ReactNoop.render(<App value={1} />);
expect(Scheduler).toFlushAndYield(['Foo', 'Foo']);
await waitForAll(['Foo', 'Foo']);
if (__DEV__) {
expect(console.error.mock.calls[0][0]).toContain(
@@ -905,7 +910,7 @@ describe('ReactNewContext', () => {
}
});
it('does not warn if multiple renderers use the same context sequentially', () => {
it('does not warn if multiple renderers use the same context sequentially', async () => {
spyOnDev(console, 'error');
const Context = React.createContext(0);
@@ -926,24 +931,27 @@ describe('ReactNewContext', () => {
React.startTransition(() => {
ReactNoop.render(<App value={1} />);
});
expect(Scheduler).toFlushAndYield(['Foo', 'Foo']);
await waitForAll(['Foo', 'Foo']);
// Get a new copy of ReactNoop
jest.resetModules();
React = require('react');
ReactNoop = require('react-noop-renderer');
Scheduler = require('scheduler');
const InternalTestUtils = require('internal-test-utils');
waitForAll = InternalTestUtils.waitForAll;
waitFor = InternalTestUtils.waitFor;
// Render the provider again using a different renderer
ReactNoop.render(<App value={1} />);
expect(Scheduler).toFlushAndYield(['Foo', 'Foo']);
await waitForAll(['Foo', 'Foo']);
if (__DEV__) {
expect(console.error).not.toHaveBeenCalled();
}
});
it('provider bails out if children and value are unchanged (like sCU)', () => {
it('provider bails out if children and value are unchanged (like sCU)', async () => {
const Context = React.createContext(0);
function Child() {
@@ -962,19 +970,19 @@ describe('ReactNewContext', () => {
// Initial mount
ReactNoop.render(<App value={1} />);
expect(Scheduler).toFlushAndYield(['App', 'Child']);
await waitForAll(['App', 'Child']);
expect(ReactNoop).toMatchRenderedOutput(<span prop="Child" />);
// Update
ReactNoop.render(<App value={1} />);
expect(Scheduler).toFlushAndYield([
await waitForAll([
'App',
// Child does not re-render
]);
expect(ReactNoop).toMatchRenderedOutput(<span prop="Child" />);
});
it('provider does not bail out if legacy context changed above', () => {
it('provider does not bail out if legacy context changed above', async () => {
const Context = React.createContext(0);
function Child() {
@@ -1021,22 +1029,22 @@ describe('ReactNewContext', () => {
</App>
</LegacyProvider>,
);
expect(Scheduler).toFlushAndYield(['LegacyProvider', 'App', 'Child']);
await waitForAll(['LegacyProvider', 'App', 'Child']);
expect(ReactNoop).toMatchRenderedOutput(<span prop="Child" />);
// Update App with same value (should bail out)
appRef.current.setState({value: 1});
expect(Scheduler).toFlushAndYield(['App']);
await waitForAll(['App']);
expect(ReactNoop).toMatchRenderedOutput(<span prop="Child" />);
// Update LegacyProvider (should not bail out)
legacyProviderRef.current.setState({value: 1});
expect(Scheduler).toFlushAndYield(['LegacyProvider', 'App', 'Child']);
await waitForAll(['LegacyProvider', 'App', 'Child']);
expect(ReactNoop).toMatchRenderedOutput(<span prop="Child" />);
// Update App with same value (should bail out)
appRef.current.setState({value: 1});
expect(Scheduler).toFlushAndYield(['App']);
await waitForAll(['App']);
expect(ReactNoop).toMatchRenderedOutput(<span prop="Child" />);
});
});
@@ -1055,7 +1063,7 @@ describe('ReactNewContext', () => {
}
});
it('can read other contexts inside consumer render prop', () => {
it('can read other contexts inside consumer render prop', async () => {
const FooContext = React.createContext(0);
const BarContext = React.createContext(0);
@@ -1092,17 +1100,17 @@ describe('ReactNewContext', () => {
}
ReactNoop.render(<App foo={1} bar={1} />);
expect(Scheduler).toFlushAndYield(['Foo: 1, Bar: 1']);
await waitForAll(['Foo: 1, Bar: 1']);
expect(ReactNoop).toMatchRenderedOutput(<span prop="Foo: 1, Bar: 1" />);
// Update foo
ReactNoop.render(<App foo={2} bar={1} />);
expect(Scheduler).toFlushAndYield(['Foo: 2, Bar: 1']);
await waitForAll(['Foo: 2, Bar: 1']);
expect(ReactNoop).toMatchRenderedOutput(<span prop="Foo: 2, Bar: 1" />);
// Update bar
ReactNoop.render(<App foo={2} bar={2} />);
expect(Scheduler).toFlushAndYield(['Foo: 2, Bar: 2']);
await waitForAll(['Foo: 2, Bar: 2']);
expect(ReactNoop).toMatchRenderedOutput(<span prop="Foo: 2, Bar: 2" />);
});
@@ -1111,7 +1119,7 @@ describe('ReactNewContext', () => {
// If we bailed out on referential equality, it would be confusing that you
// can call this.setState(), but an autobound render callback "blocked" the update.
// https://github.com/facebook/react/pull/12470#issuecomment-376917711
it('consumer does not bail out if there were no bailouts above it', () => {
it('consumer does not bail out if there were no bailouts above it', async () => {
const Context = React.createContext(0);
const Consumer = Context.Consumer;
@@ -1138,12 +1146,12 @@ describe('ReactNewContext', () => {
// Initial mount
let inst;
ReactNoop.render(<App value={1} ref={ref => (inst = ref)} />);
expect(Scheduler).toFlushAndYield(['App', 'App#renderConsumer']);
await waitForAll(['App', 'App#renderConsumer']);
expect(ReactNoop).toMatchRenderedOutput(<span prop="hello" />);
// Update
inst.setState({text: 'goodbye'});
expect(Scheduler).toFlushAndYield(['App', 'App#renderConsumer']);
await waitForAll(['App', 'App#renderConsumer']);
expect(ReactNoop).toMatchRenderedOutput(<span prop="goodbye" />);
});
});
@@ -1152,7 +1160,7 @@ describe('ReactNewContext', () => {
// Unstable changedBits API was removed. Port this test to context selectors
// once that exists.
// @gate FIXME
it('can read the same context multiple times in the same function', () => {
it('can read the same context multiple times in the same function', async () => {
const Context = React.createContext({foo: 0, bar: 0, baz: 0}, (a, b) => {
let result = 0;
if (a.foo !== b.foo) {
@@ -1212,7 +1220,7 @@ describe('ReactNewContext', () => {
}
ReactNoop.render(<App foo={1} bar={1} baz={1} />);
expect(Scheduler).toFlushAndYield(['Foo: 1, Bar: 1', 'Baz: 1']);
await waitForAll(['Foo: 1, Bar: 1', 'Baz: 1']);
expect(ReactNoop).toMatchRenderedOutput([
<span prop="Foo: 1, Bar: 1" />,
<span prop="Baz: 1" />,
@@ -1220,7 +1228,7 @@ describe('ReactNewContext', () => {
// Update only foo
ReactNoop.render(<App foo={2} bar={1} baz={1} />);
expect(Scheduler).toFlushAndYield(['Foo: 2, Bar: 1']);
await waitForAll(['Foo: 2, Bar: 1']);
expect(ReactNoop).toMatchRenderedOutput([
<span prop="Foo: 2, Bar: 1" />,
<span prop="Baz: 1" />,
@@ -1228,7 +1236,7 @@ describe('ReactNewContext', () => {
// Update only bar
ReactNoop.render(<App foo={2} bar={2} baz={1} />);
expect(Scheduler).toFlushAndYield(['Foo: 2, Bar: 2']);
await waitForAll(['Foo: 2, Bar: 2']);
expect(ReactNoop).toMatchRenderedOutput([
<span prop="Foo: 2, Bar: 2" />,
<span prop="Baz: 1" />,
@@ -1236,7 +1244,7 @@ describe('ReactNewContext', () => {
// Update only baz
ReactNoop.render(<App foo={2} bar={2} baz={2} />);
expect(Scheduler).toFlushAndYield(['Baz: 2']);
await waitForAll(['Baz: 2']);
expect(ReactNoop).toMatchRenderedOutput([
<span prop="Foo: 2, Bar: 2" />,
<span prop="Baz: 2" />,
@@ -1248,7 +1256,7 @@ describe('ReactNewContext', () => {
// If we bailed out on referential equality, it would be confusing that you
// can call this.setState(), but an autobound render callback "blocked" the update.
// https://github.com/facebook/react/pull/12470#issuecomment-376917711
it('does not bail out if there were no bailouts above it', () => {
it('does not bail out if there were no bailouts above it', async () => {
const Context = React.createContext(0);
class Consumer extends React.Component {
@@ -1281,12 +1289,12 @@ describe('ReactNewContext', () => {
// Initial mount
let inst;
ReactNoop.render(<App value={1} ref={ref => (inst = ref)} />);
expect(Scheduler).toFlushAndYield(['App', 'App#renderConsumer']);
await waitForAll(['App', 'App#renderConsumer']);
expect(ReactNoop).toMatchRenderedOutput(<span prop="hello" />);
// Update
inst.setState({text: 'goodbye'});
expect(Scheduler).toFlushAndYield(['App', 'App#renderConsumer']);
await waitForAll(['App', 'App#renderConsumer']);
expect(ReactNoop).toMatchRenderedOutput(<span prop="goodbye" />);
});
@@ -1361,7 +1369,7 @@ describe('ReactNewContext', () => {
// If we bailed out on referential equality, it would be confusing that you
// can call this.setState(), but an autobound render callback "blocked" the update.
// https://github.com/facebook/react/pull/12470#issuecomment-376917711
it('does not bail out if there were no bailouts above it', () => {
it('does not bail out if there were no bailouts above it', async () => {
const Context = React.createContext(0);
function Consumer({children}) {
@@ -1392,17 +1400,17 @@ describe('ReactNewContext', () => {
// Initial mount
let inst;
ReactNoop.render(<App value={1} ref={ref => (inst = ref)} />);
expect(Scheduler).toFlushAndYield(['App', 'App#renderConsumer']);
await waitForAll(['App', 'App#renderConsumer']);
expect(ReactNoop).toMatchRenderedOutput(<span prop="hello" />);
// Update
inst.setState({text: 'goodbye'});
expect(Scheduler).toFlushAndYield(['App', 'App#renderConsumer']);
await waitForAll(['App', 'App#renderConsumer']);
expect(ReactNoop).toMatchRenderedOutput(<span prop="goodbye" />);
});
});
it('unwinds after errors in complete phase', () => {
it('unwinds after errors in complete phase', async () => {
const Context = React.createContext(0);
// This is a regression test for stack misalignment
@@ -1419,7 +1427,7 @@ describe('ReactNewContext', () => {
<Context.Consumer>{value => <span prop={value} />}</Context.Consumer>
</Context.Provider>,
);
expect(Scheduler).toFlushWithoutYielding();
await waitForAll([]);
expect(ReactNoop).toMatchRenderedOutput(<span prop={10} />);
});
@@ -1631,7 +1639,7 @@ Context fuzz tester error! Copy and paste the following line into the test suite
});
});
it('should warn with an error message when using context as a consumer in DEV', () => {
it('should warn with an error message when using context as a consumer in DEV', async () => {
const BarContext = React.createContext({value: 'bar-initial'});
const BarConsumer = BarContext;
@@ -1647,9 +1655,9 @@ Context fuzz tester error! Copy and paste the following line into the test suite
);
}
expect(() => {
await expect(async () => {
ReactNoop.render(<Component />);
expect(Scheduler).toFlushWithoutYielding();
await waitForAll([]);
}).toErrorDev(
'Rendering <Context> directly is not supported and will be removed in ' +
'a future major release. Did you mean to render <Context.Consumer> instead?',
@@ -1657,7 +1665,7 @@ Context fuzz tester error! Copy and paste the following line into the test suite
});
// False positive regression test.
it('should not warn when using Consumer from React < 16.6 with newer renderer', () => {
it('should not warn when using Consumer from React < 16.6 with newer renderer', async () => {
const BarContext = React.createContext({value: 'bar-initial'});
// React 16.5 and earlier didn't have a separate object.
BarContext.Consumer = BarContext;
@@ -1675,10 +1683,10 @@ Context fuzz tester error! Copy and paste the following line into the test suite
}
ReactNoop.render(<Component />);
expect(Scheduler).toFlushWithoutYielding();
await waitForAll([]);
});
it('should warn with an error message when using nested context consumers in DEV', () => {
it('should warn with an error message when using nested context consumers in DEV', async () => {
const BarContext = React.createContext({value: 'bar-initial'});
const BarConsumer = BarContext;
@@ -1694,16 +1702,16 @@ Context fuzz tester error! Copy and paste the following line into the test suite
);
}
expect(() => {
await expect(async () => {
ReactNoop.render(<Component />);
expect(Scheduler).toFlushWithoutYielding();
await waitForAll([]);
}).toErrorDev(
'Rendering <Context.Consumer.Consumer> is not supported and will be removed in ' +
'a future major release. Did you mean to render <Context.Consumer> instead?',
);
});
it('should warn with an error message when using Context.Consumer.Provider DEV', () => {
it('should warn with an error message when using Context.Consumer.Provider DEV', async () => {
const BarContext = React.createContext({value: 'bar-initial'});
function Component() {
@@ -1718,9 +1726,9 @@ Context fuzz tester error! Copy and paste the following line into the test suite
);
}
expect(() => {
await expect(async () => {
ReactNoop.render(<Component />);
expect(Scheduler).toFlushWithoutYielding();
await waitForAll([]);
}).toErrorDev(
'Rendering <Context.Consumer.Provider> is not supported and will be removed in ' +
'a future major release. Did you mean to render <Context.Provider> instead?',
@@ -13,6 +13,7 @@ const React = require('react');
const ReactNoop = require('react-noop-renderer');
const Scheduler = require('scheduler');
const act = require('jest-react').act;
const {assertLog, waitForAll} = require('internal-test-utils');
// TODO: These tests are no longer specific to the noop renderer
// implementation. They test the internal implementation we use in the React
@@ -34,7 +35,7 @@ describe('internal act()', () => {
/>,
);
});
expect(Scheduler).toFlushWithoutYielding();
await waitForAll([]);
expect(calledLog).toEqual([0]);
});
@@ -56,8 +57,8 @@ describe('internal act()', () => {
await act(async () => {
ReactNoop.render(<App />);
});
expect(Scheduler).toHaveYielded(['stage 1', 'stage 2']);
expect(Scheduler).toFlushWithoutYielding();
assertLog(['stage 1', 'stage 2']);
await waitForAll([]);
expect(ReactNoop).toMatchRenderedOutput('1');
});
});
+100 -115
View File
@@ -10,6 +10,9 @@ let useEffect;
let useMemo;
let useRef;
let startTransition;
let waitForPaint;
let waitFor;
let assertLog;
describe('ReactOffscreen', () => {
beforeEach(() => {
@@ -27,6 +30,11 @@ describe('ReactOffscreen', () => {
useMemo = React.useMemo;
useRef = React.useRef;
startTransition = React.startTransition;
const InternalTestUtils = require('internal-test-utils');
waitForPaint = InternalTestUtils.waitForPaint;
waitFor = InternalTestUtils.waitFor;
assertLog = InternalTestUtils.assertLog;
});
function Text(props) {
@@ -68,10 +76,10 @@ describe('ReactOffscreen', () => {
const root = ReactNoop.createRoot();
await act(async () => {
root.render(<App mode="unstable-defer-without-hiding" />);
expect(Scheduler).toFlushUntilNextPaint(['Normal']);
await waitForPaint(['Normal']);
expect(root).toMatchRenderedOutput(<span prop="Normal" />);
});
expect(Scheduler).toHaveYielded(['Deferred']);
assertLog(['Deferred']);
expect(root).toMatchRenderedOutput(
<>
<span prop="Normal" />
@@ -83,7 +91,7 @@ describe('ReactOffscreen', () => {
await act(async () => {
root.render(<App mode="visible" />);
});
expect(Scheduler).toHaveYielded(['Normal', 'Deferred']);
assertLog(['Normal', 'Deferred']);
expect(root).toMatchRenderedOutput(
<>
<span prop="Normal" />
@@ -93,7 +101,7 @@ describe('ReactOffscreen', () => {
await act(async () => {
root.render(<App mode="unstable-defer-without-hiding" />);
expect(Scheduler).toFlushUntilNextPaint(['Normal']);
await waitForPaint(['Normal']);
expect(root).toMatchRenderedOutput(
<>
<span prop="Normal" />
@@ -101,7 +109,7 @@ describe('ReactOffscreen', () => {
</>,
);
});
expect(Scheduler).toHaveYielded(['Deferred']);
assertLog(['Deferred']);
expect(root).toMatchRenderedOutput(
<>
<span prop="Normal" />
@@ -133,7 +141,7 @@ describe('ReactOffscreen', () => {
ReactNoop.flushSync();
// Should not defer the hidden tree
expect(Scheduler).toHaveYielded(['A', 'Outside']);
assertLog(['A', 'Outside']);
});
expect(root).toMatchRenderedOutput(
<>
@@ -146,7 +154,7 @@ describe('ReactOffscreen', () => {
await act(async () => {
setState('B');
});
expect(Scheduler).toHaveYielded(['B']);
assertLog(['B']);
expect(root).toMatchRenderedOutput(
<>
<span prop="B" />
@@ -175,11 +183,11 @@ describe('ReactOffscreen', () => {
</>,
);
// Should defer the hidden tree.
expect(Scheduler).toFlushUntilNextPaint(['Outside']);
await waitForPaint(['Outside']);
});
// The hidden tree was rendered at lower priority.
expect(Scheduler).toHaveYielded(['A']);
assertLog(['A']);
expect(root).toMatchRenderedOutput(
<>
@@ -192,7 +200,7 @@ describe('ReactOffscreen', () => {
await act(async () => {
setState('B');
});
expect(Scheduler).toHaveYielded(['B']);
assertLog(['B']);
expect(root).toMatchRenderedOutput(
<>
<span prop="B" />
@@ -224,7 +232,7 @@ describe('ReactOffscreen', () => {
);
});
// No layout effect.
expect(Scheduler).toHaveYielded(['Child']);
assertLog(['Child']);
expect(root).toMatchRenderedOutput(<span hidden={true} prop="Child" />);
// Unhide the tree. The layout effect is mounted.
@@ -235,7 +243,7 @@ describe('ReactOffscreen', () => {
</Offscreen>,
);
});
expect(Scheduler).toHaveYielded(['Child', 'Mount layout']);
assertLog(['Child', 'Mount layout']);
expect(root).toMatchRenderedOutput(<span prop="Child" />);
});
@@ -259,7 +267,7 @@ describe('ReactOffscreen', () => {
</Offscreen>,
);
});
expect(Scheduler).toHaveYielded(['Child', 'Mount layout']);
assertLog(['Child', 'Mount layout']);
expect(root).toMatchRenderedOutput(<span prop="Child" />);
// Hide the tree. The layout effect is unmounted.
@@ -270,7 +278,7 @@ describe('ReactOffscreen', () => {
</Offscreen>,
);
});
expect(Scheduler).toHaveYielded(['Unmount layout', 'Child']);
assertLog(['Unmount layout', 'Child']);
expect(root).toMatchRenderedOutput(<span hidden={true} prop="Child" />);
// Unhide the tree. The layout effect is re-mounted.
@@ -281,7 +289,7 @@ describe('ReactOffscreen', () => {
</Offscreen>,
);
});
expect(Scheduler).toHaveYielded(['Child', 'Mount layout']);
assertLog(['Child', 'Mount layout']);
expect(root).toMatchRenderedOutput(<span prop="Child" />);
});
@@ -316,7 +324,7 @@ describe('ReactOffscreen', () => {
);
});
expect(Scheduler).toHaveYielded(['child']);
assertLog(['child']);
expect(root).toMatchRenderedOutput(<span hidden={true} prop="child" />);
await act(async () => {
@@ -330,7 +338,7 @@ describe('ReactOffscreen', () => {
);
});
expect(Scheduler).toHaveYielded(['child']);
assertLog(['child']);
expect(root).toMatchRenderedOutput(<span hidden={true} prop="child" />);
await act(async () => {
@@ -344,7 +352,7 @@ describe('ReactOffscreen', () => {
);
});
expect(Scheduler).toHaveYielded(['child']);
assertLog(['child']);
expect(root).toMatchRenderedOutput(<span hidden={true} prop="child" />);
await act(async () => {
@@ -372,7 +380,7 @@ describe('ReactOffscreen', () => {
);
});
expect(Scheduler).toHaveYielded(['child']);
assertLog(['child']);
await act(async () => {
// Outer offscreen is hidden.
@@ -386,7 +394,7 @@ describe('ReactOffscreen', () => {
);
});
expect(Scheduler).toHaveYielded(['child']);
assertLog(['child']);
});
// @gate enableOffscreen
@@ -410,7 +418,7 @@ describe('ReactOffscreen', () => {
</Offscreen>,
);
});
expect(Scheduler).toHaveYielded(['Child']);
assertLog(['Child']);
expect(root).toMatchRenderedOutput(<span hidden={true} prop="Child" />);
// Show the tree. The layout effect is mounted.
@@ -421,7 +429,7 @@ describe('ReactOffscreen', () => {
</Offscreen>,
);
});
expect(Scheduler).toHaveYielded(['Child', 'Mount layout']);
assertLog(['Child', 'Mount layout']);
expect(root).toMatchRenderedOutput(<span prop="Child" />);
// Hide the tree again. The layout effect is un-mounted.
@@ -432,7 +440,7 @@ describe('ReactOffscreen', () => {
</Offscreen>,
);
});
expect(Scheduler).toHaveYielded(['Unmount layout', 'Child']);
assertLog(['Unmount layout', 'Child']);
expect(root).toMatchRenderedOutput(<span hidden={true} prop="Child" />);
});
@@ -458,7 +466,7 @@ describe('ReactOffscreen', () => {
</Offscreen>,
);
});
expect(Scheduler).toHaveYielded(['Child', 'Mount layout']);
assertLog(['Child', 'Mount layout']);
expect(root).toMatchRenderedOutput(<span prop="Child" />);
// Hide the tree. The layout effect is unmounted.
@@ -469,7 +477,7 @@ describe('ReactOffscreen', () => {
</Offscreen>,
);
});
expect(Scheduler).toHaveYielded(['Unmount layout', 'Child']);
assertLog(['Unmount layout', 'Child']);
// After the layout effect is unmounted, the child is hidden.
expect(root).toMatchRenderedOutput(<span hidden={true} prop="Child" />);
@@ -497,7 +505,7 @@ describe('ReactOffscreen', () => {
</LegacyHidden>,
);
});
expect(Scheduler).toHaveYielded(['Child', 'Mount layout']);
assertLog(['Child', 'Mount layout']);
await act(async () => {
root.render(
@@ -506,7 +514,7 @@ describe('ReactOffscreen', () => {
</LegacyHidden>,
);
});
expect(Scheduler).toHaveYielded(['Child']);
assertLog(['Child']);
await act(async () => {
root.render(
@@ -515,12 +523,12 @@ describe('ReactOffscreen', () => {
</LegacyHidden>,
);
});
expect(Scheduler).toHaveYielded(['Child']);
assertLog(['Child']);
await act(async () => {
root.render(null);
});
expect(Scheduler).toHaveYielded(['Unmount layout']);
assertLog(['Unmount layout']);
});
// @gate enableOffscreen
@@ -665,7 +673,7 @@ describe('ReactOffscreen', () => {
await act(async () => {
root.render(<App show={false} />);
});
expect(Scheduler).toHaveYielded(['Outer: 0', 'Inner: 0']);
assertLog(['Outer: 0', 'Inner: 0']);
expect(root).toMatchRenderedOutput(
<>
<span prop="Outer: 0" />
@@ -681,7 +689,7 @@ describe('ReactOffscreen', () => {
setInner(1);
// Only the outer updates finishes because the inner update is inside a
// hidden tree. The outer update is deferred to a later render.
expect(Scheduler).toFlushUntilNextPaint(['Outer: 1']);
await waitForPaint(['Outer: 1']);
expect(root).toMatchRenderedOutput(
<>
<span prop="Outer: 1" />
@@ -706,7 +714,7 @@ describe('ReactOffscreen', () => {
root.render(<App show={true} />);
});
expect(Scheduler).toHaveYielded([
assertLog([
'Outer: 1',
// There are two pending updates on Inner, but only the first one
@@ -723,7 +731,7 @@ describe('ReactOffscreen', () => {
);
expect(areOuterAndInnerConsistent()).toBe(true);
});
expect(Scheduler).toHaveYielded(['Outer: 2', 'Inner: 2']);
assertLog(['Outer: 2', 'Inner: 2']);
expect(root).toMatchRenderedOutput(
<>
<span prop="Outer: 2" />
@@ -746,7 +754,7 @@ describe('ReactOffscreen', () => {
await act(async () => {
root.render(<Offscreen hidden={false} />);
});
expect(Scheduler).toHaveYielded([]);
assertLog([]);
expect(root).toMatchRenderedOutput(null);
await act(async () => {
@@ -759,7 +767,7 @@ describe('ReactOffscreen', () => {
</Offscreen>,
);
});
expect(Scheduler).toFlushAndYieldThrough(['Initial']);
await waitFor(['Initial']);
// Before it finishes rendering, the whole tree gets deleted
ReactNoop.flushSync(() => {
@@ -802,7 +810,7 @@ describe('ReactOffscreen', () => {
</Offscreen>,
);
});
expect(Scheduler).toHaveYielded(['A']);
assertLog(['A']);
expect(root).toMatchRenderedOutput(<span hidden={true} prop="A" />);
// Schedule an update to a hidden class component. The update will finish
@@ -813,7 +821,7 @@ describe('ReactOffscreen', () => {
Scheduler.unstable_yieldValue('B update finished');
});
});
expect(Scheduler).toHaveYielded(['B']);
assertLog(['B']);
expect(root).toMatchRenderedOutput(<span hidden={true} prop="B" />);
// Now reveal the hidden component. Simultaneously, schedule another
@@ -829,11 +837,7 @@ describe('ReactOffscreen', () => {
Scheduler.unstable_yieldValue('C update finished');
});
});
expect(Scheduler).toHaveYielded([
'C',
'B update finished',
'C update finished',
]);
assertLog(['C', 'B update finished', 'C update finished']);
expect(root).toMatchRenderedOutput(<span prop="C" />);
});
@@ -863,7 +867,7 @@ describe('ReactOffscreen', () => {
</Offscreen>,
);
});
expect(Scheduler).toHaveYielded(['componentDidMount']);
assertLog(['componentDidMount']);
// Hide the class component
await act(async () => {
@@ -873,7 +877,7 @@ describe('ReactOffscreen', () => {
</Offscreen>,
);
});
expect(Scheduler).toHaveYielded(['componentWillUnmount']);
assertLog(['componentWillUnmount']);
// Reappear the class component. componentDidMount should fire, not
// componentDidUpdate.
@@ -884,7 +888,7 @@ describe('ReactOffscreen', () => {
</Offscreen>,
);
});
expect(Scheduler).toHaveYielded(['componentDidMount']);
assertLog(['componentDidMount']);
});
// @gate enableOffscreen
@@ -911,7 +915,7 @@ describe('ReactOffscreen', () => {
</Offscreen>,
);
});
expect(Scheduler).toHaveYielded(['Mount B']);
assertLog(['Mount B']);
// Hide the component
await act(async () => {
@@ -921,7 +925,7 @@ describe('ReactOffscreen', () => {
</Offscreen>,
);
});
expect(Scheduler).toHaveYielded(['Unmount B']);
assertLog(['Unmount B']);
// Reappear the component and also add some new siblings.
await act(async () => {
@@ -936,7 +940,7 @@ describe('ReactOffscreen', () => {
// B's effect should fire in between A and C even though it's been reused
// from a previous render. In other words, it's the same order as if all
// three siblings were brand new.
expect(Scheduler).toHaveYielded(['Mount A', 'Mount B', 'Mount C']);
assertLog(['Mount A', 'Mount B', 'Mount C']);
},
);
@@ -967,7 +971,7 @@ describe('ReactOffscreen', () => {
</Offscreen>,
);
});
expect(Scheduler).toHaveYielded(['Mount B']);
assertLog(['Mount B']);
// We're going to schedule an update on a hidden component, so stash a
// reference to its setState before the ref gets detached
@@ -981,7 +985,7 @@ describe('ReactOffscreen', () => {
</Offscreen>,
);
});
expect(Scheduler).toHaveYielded(['Unmount B']);
assertLog(['Unmount B']);
// Reappear the component and also add some new siblings.
await act(async () => {
@@ -999,12 +1003,7 @@ describe('ReactOffscreen', () => {
// B's effect should fire in between A and C even though it's been reused
// from a previous render. In other words, it's the same order as if all
// three siblings were brand new.
expect(Scheduler).toHaveYielded([
'Mount A',
'Mount B',
'setState callback B',
'Mount C',
]);
assertLog(['Mount A', 'Mount B', 'setState callback B', 'Mount C']);
},
);
@@ -1037,7 +1036,7 @@ describe('ReactOffscreen', () => {
await act(async () => {
root.render(<App showMore={false} />);
});
expect(Scheduler).toHaveYielded([
assertLog([
// First mount the outer visible shell
'Shell',
'Mount Shell',
@@ -1060,7 +1059,7 @@ describe('ReactOffscreen', () => {
await act(async () => {
root.render(<App showMore={true} />);
});
expect(Scheduler).toHaveYielded([
assertLog([
'Shell',
'More',
@@ -1100,7 +1099,7 @@ describe('ReactOffscreen', () => {
await act(async () => {
root.render(<App showMore={false} />);
});
expect(Scheduler).toHaveYielded([
assertLog([
// First mount the outer visible shell
'Shell',
'Mount Shell',
@@ -1123,7 +1122,7 @@ describe('ReactOffscreen', () => {
await act(async () => {
root.render(<App showMore={true} />);
});
expect(Scheduler).toHaveYielded(['Shell', 'More']);
assertLog(['Shell', 'More']);
});
// @gate enableOffscreen
@@ -1155,14 +1154,14 @@ describe('ReactOffscreen', () => {
await act(async () => {
root.render(<App show={true} step={1} />);
});
expect(Scheduler).toHaveYielded([1, 'Commit mount [1]']);
assertLog([1, 'Commit mount [1]']);
expect(root).toMatchRenderedOutput(<span prop={1} />);
// Hide the tree. This will unmount the effect.
await act(async () => {
root.render(<App show={false} step={1} />);
});
expect(Scheduler).toHaveYielded(['Commit unmount [1]']);
assertLog(['Commit unmount [1]']);
expect(root).toMatchRenderedOutput(<span hidden={true} prop={1} />);
// Update.
@@ -1170,7 +1169,7 @@ describe('ReactOffscreen', () => {
root.render(<App show={false} step={2} />);
});
// The update is prerendered but no effects are fired
expect(Scheduler).toHaveYielded([2]);
assertLog([2]);
expect(root).toMatchRenderedOutput(<span hidden={true} prop={2} />);
// Reveal the tree.
@@ -1179,7 +1178,7 @@ describe('ReactOffscreen', () => {
});
// The update doesn't render because it was already prerendered, but we do
// fire the effect.
expect(Scheduler).toHaveYielded(['Commit mount [2]']);
assertLog(['Commit mount [2]']);
expect(root).toMatchRenderedOutput(<span prop={2} />);
});
@@ -1216,24 +1215,24 @@ describe('ReactOffscreen', () => {
await act(async () => {
root.render(<App show={true} />);
});
expect(Scheduler).toHaveYielded(['Mount Child', 'Mount Parent']);
assertLog(['Mount Child', 'Mount Parent']);
// First demonstrate what happens during a normal deletion
await act(async () => {
root.render(null);
});
expect(Scheduler).toHaveYielded(['Unmount Parent', 'Unmount Child']);
assertLog(['Unmount Parent', 'Unmount Child']);
// Now redo the same thing but hide instead of deleting
await act(async () => {
root.render(<App show={true} />);
});
expect(Scheduler).toHaveYielded(['Mount Child', 'Mount Parent']);
assertLog(['Mount Child', 'Mount Parent']);
await act(async () => {
root.render(<App show={false} />);
});
// The order is the same as during a deletion: parent before child
expect(Scheduler).toHaveYielded(['Unmount Parent', 'Unmount Child']);
assertLog(['Unmount Parent', 'Unmount Child']);
});
// TODO: As of now, there's no way to hide a tree without also unmounting its
@@ -1270,12 +1269,7 @@ describe('ReactOffscreen', () => {
await act(async () => {
root.render(<App showMore={true} step={1} />);
});
expect(Scheduler).toHaveYielded([
'Shell 1',
'More 1',
'Mount Shell 1',
'Mount More 1',
]);
assertLog(['Shell 1', 'More 1', 'Mount Shell 1', 'Mount More 1']);
expect(root).toMatchRenderedOutput(
<>
<span prop="Shell 1" />
@@ -1287,7 +1281,7 @@ describe('ReactOffscreen', () => {
await act(async () => {
root.render(<App showMore={false} step={2} />);
});
expect(Scheduler).toHaveYielded([
assertLog([
// First update the outer visible shell
'Shell 2',
'Unmount Shell 1',
@@ -1342,7 +1336,7 @@ describe('ReactOffscreen', () => {
await act(async () => {
root.render(<App showOuter={false} showInner={false} />);
});
expect(Scheduler).toHaveYielded(['Outer']);
assertLog(['Outer']);
expect(root).toMatchRenderedOutput(
<div hidden={true}>
<span prop="Outer" />
@@ -1353,7 +1347,7 @@ describe('ReactOffscreen', () => {
await act(async () => {
root.render(<App showOuter={false} showInner={true} />);
});
expect(Scheduler).toHaveYielded(['Outer', 'Inner']);
assertLog(['Outer', 'Inner']);
expect(root).toMatchRenderedOutput(
<div hidden={true}>
<span prop="Outer" />
@@ -1369,7 +1363,7 @@ describe('ReactOffscreen', () => {
});
// The effects fire, but the tree is not re-rendered because it already
// prerendered.
expect(Scheduler).toHaveYielded(['Mount Outer', 'Mount Inner']);
assertLog(['Mount Outer', 'Mount Inner']);
expect(root).toMatchRenderedOutput(
<div>
<span prop="Outer" />
@@ -1418,7 +1412,7 @@ describe('ReactOffscreen', () => {
await act(async () => {
root.render(<App showOuter={false} showInner={false} />);
});
expect(Scheduler).toHaveYielded(['Outer', 'Inner']);
assertLog(['Outer', 'Inner']);
// Both the inner and the outer tree should be hidden. Hiding the inner tree
// is arguably redundant, but the advantage of hiding both is that later you
// can reveal the outer tree without having to examine the inner one.
@@ -1435,7 +1429,7 @@ describe('ReactOffscreen', () => {
await act(async () => {
root.render(<App showOuter={true} showInner={false} />);
});
expect(Scheduler).toHaveYielded(['Mount Outer']);
assertLog(['Mount Outer']);
expect(root).toMatchRenderedOutput(
<div>
<span prop="Outer" />
@@ -1535,7 +1529,7 @@ describe('ReactOffscreen', () => {
root.render(<App />);
});
expect(Scheduler).toHaveYielded(['HighPriorityComponent 0', 'Child 0']);
assertLog(['HighPriorityComponent 0', 'Child 0']);
expect(root).toMatchRenderedOutput(
<>
<span prop="HighPriorityComponent 0" />
@@ -1549,10 +1543,7 @@ describe('ReactOffscreen', () => {
await act(async () => {
updateChildState(1);
updateHighPriorityComponentState(1);
expect(Scheduler).toFlushUntilNextPaint([
'HighPriorityComponent 1',
'Child 1',
]);
await waitForPaint(['HighPriorityComponent 1', 'Child 1']);
expect(root).toMatchRenderedOutput(
<>
<span prop="HighPriorityComponent 1" />
@@ -1569,7 +1560,7 @@ describe('ReactOffscreen', () => {
await act(async () => {
updateChildState(2);
updateHighPriorityComponentState(2);
expect(Scheduler).toFlushUntilNextPaint(['HighPriorityComponent 2']);
await waitForPaint(['HighPriorityComponent 2']);
expect(root).toMatchRenderedOutput(
<>
<span prop="HighPriorityComponent 2" />
@@ -1578,7 +1569,7 @@ describe('ReactOffscreen', () => {
);
});
expect(Scheduler).toHaveYielded(['Child 2']);
assertLog(['Child 2']);
expect(root).toMatchRenderedOutput(
<>
<span prop="HighPriorityComponent 2" />
@@ -1594,10 +1585,7 @@ describe('ReactOffscreen', () => {
await act(async () => {
updateChildState(3);
updateHighPriorityComponentState(3);
expect(Scheduler).toFlushUntilNextPaint([
'HighPriorityComponent 3',
'Child 3',
]);
await waitForPaint(['HighPriorityComponent 3', 'Child 3']);
expect(root).toMatchRenderedOutput(
<>
<span prop="HighPriorityComponent 3" />
@@ -1666,7 +1654,7 @@ describe('ReactOffscreen', () => {
root.render(<App />);
});
expect(Scheduler).toHaveYielded(['HighPriorityComponent 0', 'Child 0']);
assertLog(['HighPriorityComponent 0', 'Child 0']);
nextRenderTriggerDetach = true;
@@ -1675,7 +1663,7 @@ describe('ReactOffscreen', () => {
await act(async () => {
updateChildState(1);
updateHighPriorityComponentState(1);
expect(Scheduler).toFlushUntilNextPaint([
await waitForPaint([
'HighPriorityComponent 1',
'Child 1',
'HighPriorityComponent 2',
@@ -1688,7 +1676,7 @@ describe('ReactOffscreen', () => {
);
});
expect(Scheduler).toHaveYielded(['Child 2']);
assertLog(['Child 2']);
expect(root).toMatchRenderedOutput(
<>
<span prop="HighPriorityComponent 2" />
@@ -1703,10 +1691,7 @@ describe('ReactOffscreen', () => {
await act(async () => {
updateChildState(3);
updateHighPriorityComponentState(3);
expect(Scheduler).toFlushUntilNextPaint([
'HighPriorityComponent 3',
'Child 3',
]);
await waitForPaint(['HighPriorityComponent 3', 'Child 3']);
expect(root).toMatchRenderedOutput(
<>
<span prop="HighPriorityComponent 3" />
@@ -1865,33 +1850,33 @@ describe('ReactOffscreen', () => {
expect(offscreenRef).not.toBeNull();
expect(spanRef.current).not.toBeNull();
expect(Scheduler).toHaveYielded(['Mount Layout Child', 'Mount Child']);
assertLog(['Mount Layout Child', 'Mount Child']);
await act(async () => {
offscreenRef.detach();
});
expect(spanRef.current).toBeNull();
expect(Scheduler).toHaveYielded(['Unmount Layout Child', 'Unmount Child']);
assertLog(['Unmount Layout Child', 'Unmount Child']);
// Calling attach on already attached Offscreen.
await act(async () => {
offscreenRef.detach();
});
expect(Scheduler).toHaveYielded([]);
assertLog([]);
await act(async () => {
offscreenRef.attach();
});
expect(spanRef.current).not.toBeNull();
expect(Scheduler).toHaveYielded(['Mount Layout Child', 'Mount Child']);
assertLog(['Mount Layout Child', 'Mount Child']);
// Calling attach on already attached Offscreen
offscreenRef.attach();
expect(Scheduler).toHaveYielded([]);
assertLog([]);
});
// @gate enableOffscreen
@@ -1919,7 +1904,7 @@ describe('ReactOffscreen', () => {
root.render(<App />);
});
expect(Scheduler).toHaveYielded([
assertLog([
'outer',
'middle',
'inner',
@@ -1940,7 +1925,7 @@ describe('ReactOffscreen', () => {
expect(innerOffscreen).toBeNull();
expect(Scheduler).toHaveYielded([
assertLog([
'unmount layout middle',
'unmount layout inner',
'unmount middle',
@@ -1951,7 +1936,7 @@ describe('ReactOffscreen', () => {
outerOffscreen.attach();
});
expect(Scheduler).toHaveYielded([
assertLog([
'mount layout inner',
'mount layout middle',
'mount inner',
@@ -1962,27 +1947,27 @@ describe('ReactOffscreen', () => {
innerOffscreen.detach();
});
expect(Scheduler).toHaveYielded(['unmount layout inner', 'unmount inner']);
assertLog(['unmount layout inner', 'unmount inner']);
// Calling detach on already detached Offscreen.
await act(async () => {
innerOffscreen.detach();
});
expect(Scheduler).toHaveYielded([]);
assertLog([]);
await act(async () => {
innerOffscreen.attach();
});
expect(Scheduler).toHaveYielded(['mount layout inner', 'mount inner']);
assertLog(['mount layout inner', 'mount inner']);
await act(async () => {
innerOffscreen.detach();
outerOffscreen.attach();
});
expect(Scheduler).toHaveYielded(['unmount layout inner', 'unmount inner']);
assertLog(['unmount layout inner', 'unmount inner']);
});
// @gate enableOffscreen
@@ -2011,7 +1996,7 @@ describe('ReactOffscreen', () => {
root.render(<App />);
});
expect(Scheduler).toHaveYielded(['attach child']);
assertLog(['attach child']);
await act(async () => {
const instance = offscreen.current;
@@ -2020,14 +2005,14 @@ describe('ReactOffscreen', () => {
instance.attach();
});
expect(Scheduler).toHaveYielded([]);
assertLog([]);
await act(async () => {
const instance = offscreen.current;
instance.detach();
});
expect(Scheduler).toHaveYielded(['detach child']);
assertLog(['detach child']);
await act(async () => {
const instance = offscreen.current;
@@ -2036,7 +2021,7 @@ describe('ReactOffscreen', () => {
instance.detach();
});
expect(Scheduler).toHaveYielded([]);
assertLog([]);
});
// @gate enableOffscreen
@@ -2070,6 +2055,6 @@ describe('ReactOffscreen', () => {
await act(() => {
root.render(<App />);
});
expect(Scheduler).toHaveYielded(['attach child']);
assertLog(['attach child']);
});
});
@@ -9,6 +9,8 @@ let useState;
let useEffect;
let startTransition;
let textCache;
let waitForPaint;
let assertLog;
describe('ReactOffscreen', () => {
beforeEach(() => {
@@ -25,6 +27,10 @@ describe('ReactOffscreen', () => {
useEffect = React.useEffect;
startTransition = React.startTransition;
const InternalTestUtils = require('internal-test-utils');
waitForPaint = InternalTestUtils.waitForPaint;
assertLog = InternalTestUtils.assertLog;
textCache = new Map();
});
@@ -115,7 +121,7 @@ describe('ReactOffscreen', () => {
await act(async () => {
root.render(<App />);
});
expect(Scheduler).toHaveYielded(['Visible', 'Suspend! [Hidden]']);
assertLog(['Visible', 'Suspend! [Hidden]']);
expect(root).toMatchRenderedOutput(<span>Visible</span>);
// When the data resolves, we should be able to finish prerendering
@@ -123,7 +129,7 @@ describe('ReactOffscreen', () => {
await act(async () => {
await resolveText('Hidden');
});
expect(Scheduler).toHaveYielded(['Hidden']);
assertLog(['Hidden']);
expect(root).toMatchRenderedOutput(
<>
<span>Visible</span>
@@ -155,11 +161,7 @@ describe('ReactOffscreen', () => {
await act(async () => {
root.render(<App />);
});
expect(Scheduler).toHaveYielded([
'Visible',
'Suspend! [Hidden]',
'Loading...',
]);
assertLog(['Visible', 'Suspend! [Hidden]', 'Loading...']);
// Nearest Suspense boundary switches to a fallback even though the
// suspended content is hidden.
expect(root).toMatchRenderedOutput(
@@ -197,7 +199,7 @@ describe('ReactOffscreen', () => {
</Details>,
);
});
expect(Scheduler).toHaveYielded(['Closed', 'Suspend! [Async]']);
assertLog(['Closed', 'Suspend! [Async]']);
expect(root).toMatchRenderedOutput(<span>Closed</span>);
// But when we switch the boundary from hidden to visible, it should
@@ -211,7 +213,7 @@ describe('ReactOffscreen', () => {
);
});
});
expect(Scheduler).toHaveYielded(['Open', 'Suspend! [Async]', 'Loading...']);
assertLog(['Open', 'Suspend! [Async]', 'Loading...']);
// It should suspend with delay to prevent the already-visible Suspense
// boundary from switching to a fallback
expect(root).toMatchRenderedOutput(<span>Closed</span>);
@@ -220,7 +222,7 @@ describe('ReactOffscreen', () => {
await act(async () => {
await resolveText('Async');
});
expect(Scheduler).toHaveYielded(['Open', 'Async']);
assertLog(['Open', 'Async']);
expect(root).toMatchRenderedOutput(
<>
<span>Open</span>
@@ -254,7 +256,7 @@ describe('ReactOffscreen', () => {
</Details>,
);
});
expect(Scheduler).toHaveYielded(['Open', '(empty)']);
assertLog(['Open', '(empty)']);
expect(root).toMatchRenderedOutput(
<>
<span>Open</span>
@@ -272,7 +274,7 @@ describe('ReactOffscreen', () => {
);
});
});
expect(Scheduler).toHaveYielded(['Open', 'Suspend! [Async]', 'Loading...']);
assertLog(['Open', 'Suspend! [Async]', 'Loading...']);
// It should suspend with delay to prevent the already-visible Suspense
// boundary from switching to a fallback
expect(root).toMatchRenderedOutput(
@@ -294,7 +296,7 @@ describe('ReactOffscreen', () => {
});
// Now the visible part of the tree can commit without being blocked
// by the suspended content, which is hidden.
expect(Scheduler).toHaveYielded(['Closed', 'Suspend! [Async]']);
assertLog(['Closed', 'Suspend! [Async]']);
expect(root).toMatchRenderedOutput(
<>
<span>Closed</span>
@@ -306,7 +308,7 @@ describe('ReactOffscreen', () => {
await act(async () => {
await resolveText('Async');
});
expect(Scheduler).toHaveYielded(['Async']);
assertLog(['Async']);
expect(root).toMatchRenderedOutput(
<>
<span>Closed</span>
@@ -339,7 +341,7 @@ describe('ReactOffscreen', () => {
await act(async () => {
root.render(<App show={false} />);
});
expect(Scheduler).toHaveYielded(['A']);
assertLog(['A']);
await act(async () => {
startTransition(() => {
@@ -377,7 +379,7 @@ describe('ReactOffscreen', () => {
await act(async () => {
root.render(<App show={false} />);
});
expect(Scheduler).toHaveYielded(['A0']);
assertLog(['A0']);
expect(root).toMatchRenderedOutput(<span hidden={true}>A0</span>);
await act(async () => {
@@ -388,7 +390,7 @@ describe('ReactOffscreen', () => {
setText('B');
});
});
expect(Scheduler).toHaveYielded([
assertLog([
// The high priority render suspends again
'Suspend! [B0]',
// There's still pending work in another lane, so we should attempt
@@ -401,7 +403,7 @@ describe('ReactOffscreen', () => {
await act(async () => {
resolveText('B1');
});
expect(Scheduler).toHaveYielded(['B1']);
assertLog(['B1']);
expect(root).toMatchRenderedOutput(<span hidden={true}>B1</span>);
});
@@ -465,7 +467,7 @@ describe('ReactOffscreen', () => {
await act(async () => {
root.render(<App show={true} />);
});
expect(Scheduler).toHaveYielded([
assertLog([
'Outer: 0',
'Inner: 0',
'Async: 0',
@@ -487,7 +489,7 @@ describe('ReactOffscreen', () => {
// In the same render, also hide the offscreen tree.
root.render(<App show={false} />);
expect(Scheduler).toFlushUntilNextPaint([
await waitForPaint([
// The outer update will commit, but the inner update is deferred until
// a later render.
'Outer: 1',
@@ -535,7 +537,7 @@ describe('ReactOffscreen', () => {
ReactNoop.flushSync(() => {
root.render(<App show={true} />);
});
expect(Scheduler).toHaveYielded([
assertLog([
'Outer: 1',
// There are two pending updates on Inner, but only the first one
@@ -549,7 +551,7 @@ describe('ReactOffscreen', () => {
'Inner and outer are consistent',
]);
});
expect(Scheduler).toHaveYielded([
assertLog([
'Outer: 2',
'Inner: 2',
'Suspend! [Async: 2]',
@@ -12,7 +12,7 @@
let React;
let ReactNoopPersistent;
let Scheduler;
let waitForAll;
describe('ReactPersistent', () => {
beforeEach(() => {
@@ -20,7 +20,8 @@ describe('ReactPersistent', () => {
React = require('react');
ReactNoopPersistent = require('react-noop-renderer/persistent');
Scheduler = require('scheduler');
const InternalTestUtils = require('internal-test-utils');
waitForAll = InternalTestUtils.waitForAll;
});
// Inlined from shared folder so we can run this test on a bundle.
@@ -57,7 +58,7 @@ describe('ReactPersistent', () => {
return ReactNoopPersistent.dangerouslyGetChildren();
}
it('can update child nodes of a host instance', () => {
it('can update child nodes of a host instance', async () => {
function Bar(props) {
return <span>{props.text}</span>;
}
@@ -72,19 +73,19 @@ describe('ReactPersistent', () => {
}
render(<Foo text="Hello" />);
expect(Scheduler).toFlushWithoutYielding();
await waitForAll([]);
const originalChildren = dangerouslyGetChildren();
expect(originalChildren).toEqual([div(span())]);
render(<Foo text="World" />);
expect(Scheduler).toFlushWithoutYielding();
await waitForAll([]);
const newChildren = dangerouslyGetChildren();
expect(newChildren).toEqual([div(span(), span())]);
expect(originalChildren).toEqual([div(span())]);
});
it('can reuse child nodes between updates', () => {
it('can reuse child nodes between updates', async () => {
function Baz(props) {
return <span prop={props.text} />;
}
@@ -106,12 +107,12 @@ describe('ReactPersistent', () => {
}
render(<Foo text="Hello" />);
expect(Scheduler).toFlushWithoutYielding();
await waitForAll([]);
const originalChildren = dangerouslyGetChildren();
expect(originalChildren).toEqual([div(span('Hello'))]);
render(<Foo text="World" />);
expect(Scheduler).toFlushWithoutYielding();
await waitForAll([]);
const newChildren = dangerouslyGetChildren();
expect(newChildren).toEqual([div(span('Hello'), span('World'))]);
@@ -121,7 +122,7 @@ describe('ReactPersistent', () => {
expect(newChildren[0].children[0]).toBe(originalChildren[0].children[0]);
});
it('can update child text nodes', () => {
it('can update child text nodes', async () => {
function Foo(props) {
return (
<div>
@@ -132,19 +133,19 @@ describe('ReactPersistent', () => {
}
render(<Foo text="Hello" />);
expect(Scheduler).toFlushWithoutYielding();
await waitForAll([]);
const originalChildren = dangerouslyGetChildren();
expect(originalChildren).toEqual([div('Hello', span())]);
render(<Foo text="World" />);
expect(Scheduler).toFlushWithoutYielding();
await waitForAll([]);
const newChildren = dangerouslyGetChildren();
expect(newChildren).toEqual([div('World', span())]);
expect(originalChildren).toEqual([div('Hello', span())]);
});
it('supports portals', () => {
it('supports portals', async () => {
function Parent(props) {
return <div>{props.children}</div>;
}
@@ -173,7 +174,7 @@ describe('ReactPersistent', () => {
const portalContainer = {rootID: 'persistent-portal-test', children: []};
const emptyPortalChildSet = portalContainer.children;
render(<Parent>{createPortal(<Child />, portalContainer, null)}</Parent>);
expect(Scheduler).toFlushWithoutYielding();
await waitForAll([]);
expect(emptyPortalChildSet).toEqual([]);
@@ -187,7 +188,7 @@ describe('ReactPersistent', () => {
{createPortal(<Child>Hello {'World'}</Child>, portalContainer, null)}
</Parent>,
);
expect(Scheduler).toFlushWithoutYielding();
await waitForAll([]);
const newChildren = dangerouslyGetChildren();
expect(newChildren).toEqual([div()]);
@@ -204,7 +205,7 @@ describe('ReactPersistent', () => {
// Deleting the Portal, should clear its children
render(<Parent />);
expect(Scheduler).toFlushWithoutYielding();
await waitForAll([]);
const clearedPortalChildren = portalContainer.children;
expect(clearedPortalChildren).toEqual([]);
@@ -18,6 +18,10 @@ let NormalPriority;
let IdlePriority;
let runWithPriority;
let startTransition;
let waitForAll;
let waitForPaint;
let assertLog;
let waitFor;
describe('ReactSchedulerIntegration', () => {
beforeEach(() => {
@@ -31,6 +35,12 @@ describe('ReactSchedulerIntegration', () => {
IdlePriority = Scheduler.unstable_IdlePriority;
runWithPriority = Scheduler.unstable_runWithPriority;
startTransition = React.startTransition;
const InternalTestUtils = require('internal-test-utils');
waitForAll = InternalTestUtils.waitForAll;
waitForPaint = InternalTestUtils.waitForPaint;
assertLog = InternalTestUtils.assertLog;
waitFor = InternalTestUtils.waitFor;
});
// Note: This is based on a similar component we use in www. We can delete
@@ -76,11 +86,11 @@ describe('ReactSchedulerIntegration', () => {
await act(async () => {
ReactNoop.render(<CleanupEffect />);
});
expect(Scheduler).toHaveYielded([]);
assertLog([]);
await act(async () => {
ReactNoop.render(<Effects />);
});
expect(Scheduler).toHaveYielded([
assertLog([
'Cleanup Layout Effect',
'Layout Effect',
'Passive Effect',
@@ -90,12 +100,12 @@ describe('ReactSchedulerIntegration', () => {
]);
});
it('requests a paint after committing', () => {
it('requests a paint after committing', async () => {
const scheduleCallback = Scheduler.unstable_scheduleCallback;
const root = ReactNoop.createRoot();
root.render('Initial');
Scheduler.unstable_flushAll();
await waitForAll([]);
scheduleCallback(NormalPriority, () => Scheduler.unstable_yieldValue('A'));
scheduleCallback(NormalPriority, () => Scheduler.unstable_yieldValue('B'));
@@ -115,7 +125,7 @@ describe('ReactSchedulerIntegration', () => {
// Flush everything up to the next paint. Should yield after the
// React commit.
Scheduler.unstable_flushUntilNextPaint();
expect(Scheduler).toHaveYielded(['A', 'B', 'C']);
assertLog(['A', 'B', 'C']);
});
// @gate www
@@ -141,7 +151,7 @@ describe('ReactSchedulerIntegration', () => {
root.render(<App label="A" />);
// Commit the visible content
expect(Scheduler).toFlushUntilNextPaint(['Visible: A']);
await waitForPaint(['Visible: A']);
expect(root).toMatchRenderedOutput(
<>
Visible: A
@@ -156,7 +166,7 @@ describe('ReactSchedulerIntegration', () => {
});
// The next commit should only include the visible content
expect(Scheduler).toFlushUntilNextPaint(['Visible: B']);
await waitForPaint(['Visible: B']);
expect(root).toMatchRenderedOutput(
<>
Visible: B
@@ -166,7 +176,7 @@ describe('ReactSchedulerIntegration', () => {
});
// The hidden content commits later
expect(Scheduler).toHaveYielded(['Hidden: B']);
assertLog(['Hidden: B']);
expect(root).toMatchRenderedOutput(
<>
Visible: B<div hidden={true}>Hidden: B</div>
@@ -201,6 +211,12 @@ describe(
ReactNoop = require('react-noop-renderer');
Scheduler = require('scheduler');
startTransition = React.startTransition;
const InternalTestUtils = require('internal-test-utils');
waitForAll = InternalTestUtils.waitForAll;
waitForPaint = InternalTestUtils.waitForPaint;
assertLog = InternalTestUtils.assertLog;
waitFor = InternalTestUtils.waitFor;
});
afterEach(() => {
@@ -244,8 +260,8 @@ describe(
await act(async () => {
ReactNoop.render(<App />);
expect(Scheduler).toFlushUntilNextPaint([]);
expect(Scheduler).toFlushUntilNextPaint([]);
await waitForPaint([]);
await waitForPaint([]);
});
});
@@ -276,13 +292,13 @@ describe(
startTransition(() => {
ReactNoop.render(<App />);
});
expect(Scheduler).toFlushAndYieldThrough(['A']);
await waitFor(['A']);
// Start logging whenever shouldYield is called
logDuringShouldYield = true;
// Let's call it once to confirm the mock actually works
Scheduler.unstable_shouldYield();
expect(Scheduler).toHaveYielded(['shouldYield']);
assertLog(['shouldYield']);
// Expire the task
Scheduler.unstable_advanceTime(10000);
@@ -299,7 +315,7 @@ describe(
// Because the render expired, React should finish the tree without
// consulting `shouldYield` again
Scheduler.unstable_flushNumberOfYields(1);
expect(Scheduler).toHaveYielded(['B', 'C']);
assertLog(['B', 'C']);
});
});
},
@@ -8,6 +8,7 @@ let getCacheForType;
let caches;
let seededCache;
let assertLog;
describe('ReactSuspenseWithNoopRenderer', () => {
beforeEach(() => {
@@ -22,6 +23,9 @@ describe('ReactSuspenseWithNoopRenderer', () => {
getCacheForType = React.unstable_getCacheForType;
const InternalTestUtils = require('internal-test-utils');
assertLog = InternalTestUtils.assertLog;
caches = [];
seededCache = null;
});
@@ -160,7 +164,7 @@ describe('ReactSuspenseWithNoopRenderer', () => {
await act(async () => {
root.render(<App />);
});
expect(Scheduler).toHaveYielded(['Suspend! [Async]']);
assertLog(['Suspend! [Async]']);
expect(root).toMatchRenderedOutput('Loading...');
// When the promise resolves, a passive static effect flag is added. In the
@@ -169,7 +173,7 @@ describe('ReactSuspenseWithNoopRenderer', () => {
await act(async () => {
resolveText('Async');
});
expect(Scheduler).toHaveYielded(['Async', 'Effect']);
assertLog(['Async', 'Effect']);
expect(root).toMatchRenderedOutput('Async');
});
});
@@ -9,6 +9,11 @@ let act;
let TextResource;
let textResourceShouldFail;
let assertLog;
let waitForPaint;
let waitForAll;
let waitFor;
describe('ReactSuspense', () => {
beforeEach(() => {
jest.resetModules();
@@ -23,6 +28,12 @@ describe('ReactSuspense', () => {
Suspense = React.Suspense;
const InternalTestUtils = require('internal-test-utils');
waitForAll = InternalTestUtils.waitForAll;
waitForPaint = InternalTestUtils.waitForPaint;
assertLog = InternalTestUtils.assertLog;
waitFor = InternalTestUtils.waitFor;
TextResource = ReactCache.unstable_createResource(
([text, ms = 0]) => {
let listeners = null;
@@ -94,7 +105,7 @@ describe('ReactSuspense', () => {
}
}
it('suspends rendering and continues later', () => {
it('suspends rendering and continues later', async () => {
function Bar(props) {
Scheduler.unstable_yieldValue('Bar');
return props.children;
@@ -119,7 +130,7 @@ describe('ReactSuspense', () => {
unstable_isConcurrent: true,
});
expect(Scheduler).toFlushAndYield(['Foo']);
await waitForAll(['Foo']);
expect(root).toMatchRenderedOutput(null);
// Navigate the shell to now render the child content.
@@ -128,7 +139,7 @@ describe('ReactSuspense', () => {
root.update(<Foo renderBar={true} />);
});
expect(Scheduler).toFlushAndYield([
await waitForAll([
'Foo',
'Bar',
// A suspends
@@ -142,18 +153,18 @@ describe('ReactSuspense', () => {
// Flush some of the time
jest.advanceTimersByTime(50);
// Still nothing...
expect(Scheduler).toFlushWithoutYielding();
await waitForAll([]);
expect(root).toMatchRenderedOutput(null);
// Flush the promise completely
jest.advanceTimersByTime(50);
// Renders successfully
expect(Scheduler).toHaveYielded(['Promise resolved [A]']);
expect(Scheduler).toFlushAndYield(['Foo', 'Bar', 'A', 'B']);
assertLog(['Promise resolved [A]']);
await waitForAll(['Foo', 'Bar', 'A', 'B']);
expect(root).toMatchRenderedOutput('AB');
});
it('suspends siblings and later recovers each independently', () => {
it('suspends siblings and later recovers each independently', async () => {
// Render two sibling Suspense components
const root = ReactTestRenderer.create(
<>
@@ -169,7 +180,7 @@ describe('ReactSuspense', () => {
},
);
expect(Scheduler).toFlushAndYield([
await waitForAll([
'Suspend! [A]',
'Loading A...',
'Suspend! [B]',
@@ -182,19 +193,19 @@ describe('ReactSuspense', () => {
// show the placeholder
jest.advanceTimersByTime(5000);
// TODO: Should we throw if you forget to call toHaveYielded?
expect(Scheduler).toHaveYielded(['Promise resolved [A]']);
expect(Scheduler).toFlushAndYield(['A']);
assertLog(['Promise resolved [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);
expect(Scheduler).toHaveYielded(['Promise resolved [B]']);
expect(Scheduler).toFlushAndYield(['B']);
assertLog(['Promise resolved [B]']);
await waitForAll(['B']);
expect(root).toMatchRenderedOutput('AB');
});
it('interrupts current render if promise resolves before current render phase', () => {
it('interrupts current render if promise resolves before current render phase', async () => {
let didResolve = false;
const listeners = [];
@@ -231,7 +242,7 @@ describe('ReactSuspense', () => {
unstable_isConcurrent: true,
},
);
expect(Scheduler).toFlushAndYield(['Initial']);
await waitForAll(['Initial']);
expect(root).toMatchRenderedOutput('Initial');
// The update will suspend.
@@ -247,19 +258,15 @@ describe('ReactSuspense', () => {
);
});
// Yield past the Suspense boundary but don't complete the last sibling.
expect(Scheduler).toFlushAndYieldThrough([
'Suspend!',
'Loading...',
'After Suspense',
]);
await waitFor(['Suspend!', 'Loading...', 'After Suspense']);
// The promise resolves before the current render phase has completed
resolveThenable();
expect(Scheduler).toHaveYielded([]);
assertLog([]);
expect(root).toMatchRenderedOutput('Initial');
// Start over from the root, instead of continuing.
expect(Scheduler).toFlushAndYield([
await waitForAll([
// Async renders again *before* Sibling
'Async',
'After Suspense',
@@ -268,7 +275,7 @@ describe('ReactSuspense', () => {
expect(root).toMatchRenderedOutput('AsyncAfter SuspenseSibling');
});
it('throttles fallback committing globally', () => {
it('throttles fallback committing globally', async () => {
function Foo() {
Scheduler.unstable_yieldValue('Foo');
return (
@@ -290,7 +297,7 @@ describe('ReactSuspense', () => {
unstable_isConcurrent: true,
});
expect(Scheduler).toFlushAndYield([
await waitForAll([
'Foo',
'Suspend! [A]',
'Suspend! [B]',
@@ -302,8 +309,8 @@ describe('ReactSuspense', () => {
// Resolve A.
jest.advanceTimersByTime(200);
Scheduler.unstable_advanceTime(200);
expect(Scheduler).toHaveYielded(['Promise resolved [A]']);
expect(Scheduler).toFlushAndYield(['A', 'Suspend! [B]', 'Loading more...']);
assertLog(['Promise resolved [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
@@ -313,17 +320,17 @@ describe('ReactSuspense', () => {
// Resolve B.
jest.advanceTimersByTime(100);
Scheduler.unstable_advanceTime(100);
expect(Scheduler).toHaveYielded(['Promise resolved [B]']);
assertLog(['Promise resolved [B]']);
// By this point, B has resolved.
// We're still showing the outer fallback.
expect(root).toMatchRenderedOutput('Loading...');
expect(Scheduler).toFlushAndYield(['A', 'B']);
await waitForAll(['A', 'B']);
// Then contents of both should pop in together.
expect(root).toMatchRenderedOutput('AB');
});
it('does not throttle fallback committing for too long', () => {
it('does not throttle fallback committing for too long', async () => {
function Foo() {
Scheduler.unstable_yieldValue('Foo');
return (
@@ -345,7 +352,7 @@ describe('ReactSuspense', () => {
unstable_isConcurrent: true,
});
expect(Scheduler).toFlushAndYield([
await waitForAll([
'Foo',
'Suspend! [A]',
'Suspend! [B]',
@@ -357,8 +364,8 @@ describe('ReactSuspense', () => {
// Resolve A.
jest.advanceTimersByTime(200);
Scheduler.unstable_advanceTime(200);
expect(Scheduler).toHaveYielded(['Promise resolved [A]']);
expect(Scheduler).toFlushAndYield(['A', 'Suspend! [B]', 'Loading more...']);
assertLog(['Promise resolved [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
@@ -374,8 +381,8 @@ describe('ReactSuspense', () => {
// Resolve B.
jest.advanceTimersByTime(500);
Scheduler.unstable_advanceTime(500);
expect(Scheduler).toHaveYielded(['Promise resolved [B]']);
expect(Scheduler).toFlushAndYield(['B']);
assertLog(['Promise resolved [B]']);
await waitForAll(['B']);
expect(root).toMatchRenderedOutput('AB');
});
@@ -404,16 +411,16 @@ describe('ReactSuspense', () => {
</Suspense>,
);
expect(Scheduler).toHaveYielded(['Loading...']);
assertLog(['Loading...']);
expect(root).toMatchRenderedOutput('Loading...');
await LazyClass;
expect(Scheduler).toFlushUntilNextPaint(['Hi', 'Did mount: Hi']);
await waitForPaint(['Hi', 'Did mount: Hi']);
expect(root).toMatchRenderedOutput('Hi');
});
it('updates memoized child of suspense component when context updates (simple memo)', () => {
it('updates memoized child of suspense component when context updates (simple memo)', async () => {
const {useContext, createContext, useState, memo} = React;
const ValueContext = createContext(null);
@@ -451,23 +458,23 @@ describe('ReactSuspense', () => {
const root = ReactTestRenderer.create(<App />, {
unstable_isConcurrent: true,
});
expect(Scheduler).toFlushAndYield(['Suspend! [default]', 'Loading...']);
await waitForAll(['Suspend! [default]', 'Loading...']);
jest.advanceTimersByTime(1000);
expect(Scheduler).toHaveYielded(['Promise resolved [default]']);
expect(Scheduler).toFlushAndYield(['default']);
assertLog(['Promise resolved [default]']);
await waitForAll(['default']);
expect(root).toMatchRenderedOutput('default');
act(() => setValue('new value'));
expect(Scheduler).toHaveYielded(['Suspend! [new value]', 'Loading...']);
assertLog(['Suspend! [new value]', 'Loading...']);
jest.advanceTimersByTime(1000);
expect(Scheduler).toHaveYielded(['Promise resolved [new value]']);
expect(Scheduler).toFlushAndYield(['new value']);
assertLog(['Promise resolved [new value]']);
await waitForAll(['new value']);
expect(root).toMatchRenderedOutput('new value');
});
it('updates memoized child of suspense component when context updates (manual memo)', () => {
it('updates memoized child of suspense component when context updates (manual memo)', async () => {
const {useContext, createContext, useState, memo} = React;
const ValueContext = createContext(null);
@@ -510,23 +517,23 @@ describe('ReactSuspense', () => {
const root = ReactTestRenderer.create(<App />, {
unstable_isConcurrent: true,
});
expect(Scheduler).toFlushAndYield(['Suspend! [default]', 'Loading...']);
await waitForAll(['Suspend! [default]', 'Loading...']);
jest.advanceTimersByTime(1000);
expect(Scheduler).toHaveYielded(['Promise resolved [default]']);
expect(Scheduler).toFlushAndYield(['default']);
assertLog(['Promise resolved [default]']);
await waitForAll(['default']);
expect(root).toMatchRenderedOutput('default');
act(() => setValue('new value'));
expect(Scheduler).toHaveYielded(['Suspend! [new value]', 'Loading...']);
assertLog(['Suspend! [new value]', 'Loading...']);
jest.advanceTimersByTime(1000);
expect(Scheduler).toHaveYielded(['Promise resolved [new value]']);
expect(Scheduler).toFlushAndYield(['new value']);
assertLog(['Promise resolved [new value]']);
await waitForAll(['new value']);
expect(root).toMatchRenderedOutput('new value');
});
it('updates memoized child of suspense component when context updates (function)', () => {
it('updates memoized child of suspense component when context updates (function)', async () => {
const {useContext, createContext, useState} = React;
const ValueContext = createContext(null);
@@ -567,23 +574,23 @@ describe('ReactSuspense', () => {
unstable_isConcurrent: true,
},
);
expect(Scheduler).toFlushAndYield(['Suspend! [default]', 'Loading...']);
await waitForAll(['Suspend! [default]', 'Loading...']);
jest.advanceTimersByTime(1000);
expect(Scheduler).toHaveYielded(['Promise resolved [default]']);
expect(Scheduler).toFlushAndYield(['default']);
assertLog(['Promise resolved [default]']);
await waitForAll(['default']);
expect(root).toMatchRenderedOutput('default');
act(() => setValue('new value'));
expect(Scheduler).toHaveYielded(['Suspend! [new value]', 'Loading...']);
assertLog(['Suspend! [new value]', 'Loading...']);
jest.advanceTimersByTime(1000);
expect(Scheduler).toHaveYielded(['Promise resolved [new value]']);
expect(Scheduler).toFlushAndYield(['new value']);
assertLog(['Promise resolved [new value]']);
await waitForAll(['new value']);
expect(root).toMatchRenderedOutput('new value');
});
it('updates memoized child of suspense component when context updates (forwardRef)', () => {
it('updates memoized child of suspense component when context updates (forwardRef)', async () => {
const {forwardRef, useContext, createContext, useState} = React;
const ValueContext = createContext(null);
@@ -624,23 +631,23 @@ describe('ReactSuspense', () => {
unstable_isConcurrent: true,
},
);
expect(Scheduler).toFlushAndYield(['Suspend! [default]', 'Loading...']);
await waitForAll(['Suspend! [default]', 'Loading...']);
jest.advanceTimersByTime(1000);
expect(Scheduler).toHaveYielded(['Promise resolved [default]']);
expect(Scheduler).toFlushAndYield(['default']);
assertLog(['Promise resolved [default]']);
await waitForAll(['default']);
expect(root).toMatchRenderedOutput('default');
act(() => setValue('new value'));
expect(Scheduler).toHaveYielded(['Suspend! [new value]', 'Loading...']);
assertLog(['Suspend! [new value]', 'Loading...']);
jest.advanceTimersByTime(1000);
expect(Scheduler).toHaveYielded(['Promise resolved [new value]']);
expect(Scheduler).toFlushAndYield(['new value']);
assertLog(['Promise resolved [new value]']);
await waitForAll(['new value']);
expect(root).toMatchRenderedOutput('new value');
});
it('re-fires layout effects when re-showing Suspense', () => {
it('re-fires layout effects when re-showing Suspense', async () => {
function TextWithLayout(props) {
Scheduler.unstable_yieldValue(props.text);
React.useLayoutEffect(() => {
@@ -668,28 +675,21 @@ describe('ReactSuspense', () => {
unstable_isConcurrent: true,
});
expect(Scheduler).toFlushAndYield(['Child 1', 'create layout']);
await waitForAll(['Child 1', 'create layout']);
expect(root).toMatchRenderedOutput('Child 1');
act(() => {
_setShow(true);
});
expect(Scheduler).toHaveYielded([
'Child 1',
'Suspend! [Child 2]',
'Loading...',
]);
assertLog(['Child 1', 'Suspend! [Child 2]', 'Loading...']);
jest.advanceTimersByTime(1000);
expect(Scheduler).toHaveYielded([
'destroy layout',
'Promise resolved [Child 2]',
]);
expect(Scheduler).toFlushAndYield(['Child 1', 'Child 2', 'create layout']);
assertLog(['destroy layout', 'Promise resolved [Child 2]']);
await waitForAll(['Child 1', 'Child 2', 'create layout']);
expect(root).toMatchRenderedOutput(['Child 1', 'Child 2'].join(''));
});
describe('outside concurrent mode', () => {
it('a mounted class component can suspend without losing state', () => {
it('a mounted class component can suspend without losing state', async () => {
class TextWithLifecycle extends React.Component {
componentDidMount() {
Scheduler.unstable_yieldValue(`Mount [${this.props.text}]`);
@@ -754,7 +754,7 @@ describe('ReactSuspense', () => {
const root = ReactTestRenderer.create(<App />);
expect(Scheduler).toHaveYielded([
assertLog([
'A',
'Suspend! [B:1]',
'C',
@@ -770,8 +770,8 @@ describe('ReactSuspense', () => {
jest.advanceTimersByTime(100);
expect(Scheduler).toHaveYielded(['Promise resolved [B:1]']);
expect(Scheduler).toFlushUntilNextPaint([
assertLog(['Promise resolved [B:1]']);
await waitForPaint([
'B:1',
'Unmount [Loading...]',
// Should be a mount, not an update
@@ -780,25 +780,17 @@ describe('ReactSuspense', () => {
expect(root).toMatchRenderedOutput('AB:1C');
instance.setState({step: 2});
expect(Scheduler).toHaveYielded([
'Suspend! [B:2]',
'Loading...',
'Mount [Loading...]',
]);
assertLog(['Suspend! [B:2]', 'Loading...', 'Mount [Loading...]']);
expect(root).toMatchRenderedOutput('Loading...');
jest.advanceTimersByTime(100);
expect(Scheduler).toHaveYielded(['Promise resolved [B:2]']);
expect(Scheduler).toFlushUntilNextPaint([
'B:2',
'Unmount [Loading...]',
'Update [B:2]',
]);
assertLog(['Promise resolved [B:2]']);
await waitForPaint(['B:2', 'Unmount [Loading...]', 'Update [B:2]']);
expect(root).toMatchRenderedOutput('AB:2C');
});
it('bails out on timed-out primary children even if they receive an update', () => {
it('bails out on timed-out primary children even if they receive an update', async () => {
let instance;
class Stateful extends React.Component {
state = {step: 1};
@@ -819,38 +811,30 @@ describe('ReactSuspense', () => {
const root = ReactTestRenderer.create(<App text="A" />);
expect(Scheduler).toHaveYielded([
'Stateful: 1',
'Suspend! [A]',
'Loading...',
]);
assertLog(['Stateful: 1', 'Suspend! [A]', 'Loading...']);
jest.advanceTimersByTime(1000);
expect(Scheduler).toHaveYielded(['Promise resolved [A]']);
expect(Scheduler).toFlushUntilNextPaint(['A']);
assertLog(['Promise resolved [A]']);
await waitForPaint(['A']);
expect(root).toMatchRenderedOutput('Stateful: 1A');
root.update(<App text="B" />);
expect(Scheduler).toHaveYielded([
'Stateful: 1',
'Suspend! [B]',
'Loading...',
]);
assertLog(['Stateful: 1', 'Suspend! [B]', 'Loading...']);
expect(root).toMatchRenderedOutput('Loading...');
instance.setState({step: 2});
expect(Scheduler).toHaveYielded(['Stateful: 2', 'Suspend! [B]']);
assertLog(['Stateful: 2', 'Suspend! [B]']);
expect(root).toMatchRenderedOutput('Loading...');
jest.advanceTimersByTime(1000);
expect(Scheduler).toHaveYielded(['Promise resolved [B]']);
expect(Scheduler).toFlushUntilNextPaint(['B']);
assertLog(['Promise resolved [B]']);
await waitForPaint(['B']);
expect(root).toMatchRenderedOutput('Stateful: 2B');
});
it('when updating a timed-out tree, always retries the suspended component', () => {
it('when updating a timed-out tree, always retries the suspended component', async () => {
let instance;
class Stateful extends React.Component {
state = {step: 1};
@@ -879,28 +863,20 @@ describe('ReactSuspense', () => {
const root = ReactTestRenderer.create(<App text="A" />);
expect(Scheduler).toHaveYielded([
'Stateful: 1',
'Suspend! [A]',
'Loading...',
]);
assertLog(['Stateful: 1', 'Suspend! [A]', 'Loading...']);
jest.advanceTimersByTime(1000);
expect(Scheduler).toHaveYielded(['Promise resolved [A]']);
expect(Scheduler).toFlushUntilNextPaint(['A']);
assertLog(['Promise resolved [A]']);
await waitForPaint(['A']);
expect(root).toMatchRenderedOutput('Stateful: 1A');
root.update(<App text="B" />);
expect(Scheduler).toHaveYielded([
'Stateful: 1',
'Suspend! [B]',
'Loading...',
]);
assertLog(['Stateful: 1', 'Suspend! [B]', 'Loading...']);
expect(root).toMatchRenderedOutput('Loading...');
instance.setState({step: 2});
expect(Scheduler).toHaveYielded([
assertLog([
'Stateful: 2',
// The suspended component should suspend again. If it doesn't, the
@@ -912,8 +888,8 @@ describe('ReactSuspense', () => {
jest.advanceTimersByTime(1000);
expect(Scheduler).toHaveYielded(['Promise resolved [B]']);
expect(Scheduler).toFlushUntilNextPaint(['B']);
assertLog(['Promise resolved [B]']);
await waitForPaint(['B']);
expect(root).toMatchRenderedOutput('Stateful: 2B');
});
@@ -949,14 +925,14 @@ describe('ReactSuspense', () => {
}
const root = ReactTestRenderer.create(<App text="A" />);
expect(Scheduler).toHaveYielded(['Suspend! [A]', 'Loading...']);
assertLog(['Suspend! [A]', 'Loading...']);
root.update(<Text text="B" />);
// Should not fire componentWillUnmount
expect(Scheduler).toHaveYielded(['B']);
assertLog(['B']);
expect(root).toMatchRenderedOutput('B');
});
it('suspends in a component that also contains useEffect', () => {
it('suspends in a component that also contains useEffect', async () => {
const {useLayoutEffect} = React;
function AsyncTextWithEffect(props) {
@@ -989,14 +965,14 @@ describe('ReactSuspense', () => {
}
ReactTestRenderer.create(<App text="A" />);
expect(Scheduler).toHaveYielded(['Suspend! [A]', 'Loading...']);
assertLog(['Suspend! [A]', 'Loading...']);
jest.advanceTimersByTime(500);
expect(Scheduler).toHaveYielded(['Promise resolved [A]']);
expect(Scheduler).toFlushUntilNextPaint(['A', 'Did commit: A']);
assertLog(['Promise resolved [A]']);
await waitForPaint(['A', 'Did commit: A']);
});
it('retries when an update is scheduled on a timed out tree', () => {
it('retries when an update is scheduled on a timed out tree', async () => {
let instance;
class Stateful extends React.Component {
state = {step: 1};
@@ -1019,33 +995,30 @@ describe('ReactSuspense', () => {
});
// Initial render
expect(Scheduler).toFlushAndYield(['Suspend! [Step: 1]', 'Loading...']);
await waitForAll(['Suspend! [Step: 1]', 'Loading...']);
jest.advanceTimersByTime(1000);
expect(Scheduler).toHaveYielded(['Promise resolved [Step: 1]']);
expect(Scheduler).toFlushAndYield(['Step: 1']);
assertLog(['Promise resolved [Step: 1]']);
await waitForAll(['Step: 1']);
expect(root).toMatchRenderedOutput('Step: 1');
// Update that suspends
instance.setState({step: 2});
expect(Scheduler).toFlushAndYield(['Suspend! [Step: 2]', 'Loading...']);
await waitForAll(['Suspend! [Step: 2]', 'Loading...']);
jest.advanceTimersByTime(500);
expect(root).toMatchRenderedOutput('Loading...');
// Update while still suspended
instance.setState({step: 3});
expect(Scheduler).toFlushAndYield(['Suspend! [Step: 3]']);
await waitForAll(['Suspend! [Step: 3]']);
expect(root).toMatchRenderedOutput('Loading...');
jest.advanceTimersByTime(1000);
expect(Scheduler).toHaveYielded([
'Promise resolved [Step: 2]',
'Promise resolved [Step: 3]',
]);
expect(Scheduler).toFlushAndYield(['Step: 3']);
assertLog(['Promise resolved [Step: 2]', 'Promise resolved [Step: 3]']);
await waitForAll(['Step: 3']);
expect(root).toMatchRenderedOutput('Step: 3');
});
it('does not remount the fallback while suspended children resolve in legacy mode', () => {
it('does not remount the fallback while suspended children resolve in legacy mode', async () => {
let mounts = 0;
class ShouldMountOnce extends React.Component {
componentDidMount() {
@@ -1069,18 +1042,18 @@ describe('ReactSuspense', () => {
const root = ReactTestRenderer.create(<App />);
// Initial render
expect(Scheduler).toHaveYielded([
assertLog([
'Suspend! [Child 1]',
'Suspend! [Child 2]',
'Suspend! [Child 3]',
'Loading...',
]);
expect(Scheduler).toFlushAndYield([]);
await waitForAll([]);
jest.advanceTimersByTime(1000);
expect(Scheduler).toHaveYielded(['Promise resolved [Child 1]']);
expect(Scheduler).toFlushUntilNextPaint([
assertLog(['Promise resolved [Child 1]']);
await waitForPaint([
'Child 1',
'Suspend! [Child 2]',
'Suspend! [Child 3]',
@@ -1088,23 +1061,20 @@ describe('ReactSuspense', () => {
jest.advanceTimersByTime(1000);
expect(Scheduler).toHaveYielded(['Promise resolved [Child 2]']);
expect(Scheduler).toFlushUntilNextPaint([
'Child 2',
'Suspend! [Child 3]',
]);
assertLog(['Promise resolved [Child 2]']);
await waitForPaint(['Child 2', 'Suspend! [Child 3]']);
jest.advanceTimersByTime(1000);
expect(Scheduler).toHaveYielded(['Promise resolved [Child 3]']);
expect(Scheduler).toFlushUntilNextPaint(['Child 3']);
assertLog(['Promise resolved [Child 3]']);
await waitForPaint(['Child 3']);
expect(root).toMatchRenderedOutput(
['Child 1', 'Child 2', 'Child 3'].join(''),
);
expect(mounts).toBe(1);
});
it('does not get stuck with fallback in concurrent mode for a large delay', () => {
it('does not get stuck with fallback in concurrent mode for a large delay', async () => {
function App(props) {
return (
<Suspense fallback={<Text text="Loading..." />}>
@@ -1118,21 +1088,21 @@ describe('ReactSuspense', () => {
unstable_isConcurrent: true,
});
expect(Scheduler).toFlushAndYield([
await waitForAll([
'Suspend! [Child 1]',
'Suspend! [Child 2]',
'Loading...',
]);
jest.advanceTimersByTime(1000);
expect(Scheduler).toHaveYielded(['Promise resolved [Child 1]']);
expect(Scheduler).toFlushAndYield(['Child 1', 'Suspend! [Child 2]']);
assertLog(['Promise resolved [Child 1]']);
await waitForAll(['Child 1', 'Suspend! [Child 2]']);
jest.advanceTimersByTime(6000);
expect(Scheduler).toHaveYielded(['Promise resolved [Child 2]']);
expect(Scheduler).toFlushAndYield(['Child 1', 'Child 2']);
assertLog(['Promise resolved [Child 2]']);
await waitForAll(['Child 1', 'Child 2']);
expect(root).toMatchRenderedOutput(['Child 1', 'Child 2'].join(''));
});
it('reuses effects, including deletions, from the suspended tree', () => {
it('reuses effects, including deletions, from the suspended tree', async () => {
const {useState} = React;
let setTab;
@@ -1149,46 +1119,34 @@ describe('ReactSuspense', () => {
}
const root = ReactTestRenderer.create(<App />);
expect(Scheduler).toHaveYielded([
'Suspend! [Tab: 0]',
' + sibling',
'Loading...',
]);
assertLog(['Suspend! [Tab: 0]', ' + sibling', 'Loading...']);
expect(root).toMatchRenderedOutput('Loading...');
jest.advanceTimersByTime(1000);
expect(Scheduler).toHaveYielded(['Promise resolved [Tab: 0]']);
expect(Scheduler).toFlushUntilNextPaint(['Tab: 0']);
assertLog(['Promise resolved [Tab: 0]']);
await waitForPaint(['Tab: 0']);
expect(root).toMatchRenderedOutput('Tab: 0 + sibling');
act(() => setTab(1));
expect(Scheduler).toHaveYielded([
'Suspend! [Tab: 1]',
' + sibling',
'Loading...',
]);
assertLog(['Suspend! [Tab: 1]', ' + sibling', 'Loading...']);
expect(root).toMatchRenderedOutput('Loading...');
jest.advanceTimersByTime(1000);
expect(Scheduler).toHaveYielded(['Promise resolved [Tab: 1]']);
expect(Scheduler).toFlushUntilNextPaint(['Tab: 1']);
assertLog(['Promise resolved [Tab: 1]']);
await waitForPaint(['Tab: 1']);
expect(root).toMatchRenderedOutput('Tab: 1 + sibling');
act(() => setTab(2));
expect(Scheduler).toHaveYielded([
'Suspend! [Tab: 2]',
' + sibling',
'Loading...',
]);
assertLog(['Suspend! [Tab: 2]', ' + sibling', 'Loading...']);
expect(root).toMatchRenderedOutput('Loading...');
jest.advanceTimersByTime(1000);
expect(Scheduler).toHaveYielded(['Promise resolved [Tab: 2]']);
expect(Scheduler).toFlushUntilNextPaint(['Tab: 2']);
assertLog(['Promise resolved [Tab: 2]']);
await waitForPaint(['Tab: 2']);
expect(root).toMatchRenderedOutput('Tab: 2 + sibling');
});
it('does not warn if an mounted component is pinged', () => {
it('does not warn if an mounted component is pinged', async () => {
const {useState} = React;
const root = ReactTestRenderer.create(null);
@@ -1218,23 +1176,23 @@ describe('ReactSuspense', () => {
</Suspense>,
);
expect(Scheduler).toHaveYielded(['Suspend! [A:0]', 'Loading...']);
assertLog(['Suspend! [A:0]', 'Loading...']);
jest.advanceTimersByTime(1000);
expect(Scheduler).toHaveYielded(['Promise resolved [A:0]']);
expect(Scheduler).toFlushUntilNextPaint(['A:0']);
assertLog(['Promise resolved [A:0]']);
await waitForPaint(['A:0']);
expect(root).toMatchRenderedOutput('A:0');
act(() => setStep(1));
expect(Scheduler).toHaveYielded(['Suspend! [A:1]', 'Loading...']);
assertLog(['Suspend! [A:1]', 'Loading...']);
expect(root).toMatchRenderedOutput('Loading...');
root.update(null);
expect(Scheduler).toFlushWithoutYielding();
await waitForAll([]);
jest.advanceTimersByTime(1000);
});
it('memoizes promise listeners per thread ID to prevent redundant renders', () => {
it('memoizes promise listeners per thread ID to prevent redundant renders', async () => {
function App() {
return (
<Suspense fallback={<Text text="Loading..." />}>
@@ -1249,18 +1207,13 @@ describe('ReactSuspense', () => {
root.update(<App />);
expect(Scheduler).toHaveYielded([
'Suspend! [A]',
'Suspend! [B]',
'Suspend! [C]',
'Loading...',
]);
assertLog(['Suspend! [A]', 'Suspend! [B]', 'Suspend! [C]', 'Loading...']);
// Resolve A
jest.advanceTimersByTime(1000);
expect(Scheduler).toHaveYielded(['Promise resolved [A]']);
expect(Scheduler).toFlushUntilNextPaint([
assertLog(['Promise resolved [A]']);
await waitForPaint([
'A',
// The promises for B and C have now been thrown twice
'Suspend! [B]',
@@ -1270,8 +1223,8 @@ describe('ReactSuspense', () => {
// Resolve B
jest.advanceTimersByTime(1000);
expect(Scheduler).toHaveYielded(['Promise resolved [B]']);
expect(Scheduler).toFlushUntilNextPaint([
assertLog(['Promise resolved [B]']);
await waitForPaint([
// Even though the promise for B was thrown twice, we should only
// re-render once.
'B',
@@ -1282,8 +1235,8 @@ describe('ReactSuspense', () => {
// Resolve C
jest.advanceTimersByTime(1000);
expect(Scheduler).toHaveYielded(['Promise resolved [C]']);
expect(Scheduler).toFlushUntilNextPaint([
assertLog(['Promise resolved [C]']);
await waitForPaint([
// Even though the promise for C was thrown three times, we should only
// re-render once.
'C',
@@ -1328,7 +1281,7 @@ describe('ReactSuspense', () => {
jest.advanceTimersByTime(1000);
});
it('updates memoized child of suspense component when context updates (simple memo)', () => {
it('updates memoized child of suspense component when context updates (simple memo)', async () => {
const {useContext, createContext, useState, memo} = React;
const ValueContext = createContext(null);
@@ -1364,23 +1317,23 @@ describe('ReactSuspense', () => {
}
const root = ReactTestRenderer.create(<App />);
expect(Scheduler).toHaveYielded(['Suspend! [default]', 'Loading...']);
assertLog(['Suspend! [default]', 'Loading...']);
jest.advanceTimersByTime(1000);
expect(Scheduler).toHaveYielded(['Promise resolved [default]']);
expect(Scheduler).toFlushUntilNextPaint(['default']);
assertLog(['Promise resolved [default]']);
await waitForPaint(['default']);
expect(root).toMatchRenderedOutput('default');
act(() => setValue('new value'));
expect(Scheduler).toHaveYielded(['Suspend! [new value]', 'Loading...']);
assertLog(['Suspend! [new value]', 'Loading...']);
jest.advanceTimersByTime(1000);
expect(Scheduler).toHaveYielded(['Promise resolved [new value]']);
expect(Scheduler).toFlushUntilNextPaint(['new value']);
assertLog(['Promise resolved [new value]']);
await waitForPaint(['new value']);
expect(root).toMatchRenderedOutput('new value');
});
it('updates memoized child of suspense component when context updates (manual memo)', () => {
it('updates memoized child of suspense component when context updates (manual memo)', async () => {
const {useContext, createContext, useState, memo} = React;
const ValueContext = createContext(null);
@@ -1421,23 +1374,23 @@ describe('ReactSuspense', () => {
}
const root = ReactTestRenderer.create(<App />);
expect(Scheduler).toHaveYielded(['Suspend! [default]', 'Loading...']);
assertLog(['Suspend! [default]', 'Loading...']);
jest.advanceTimersByTime(1000);
expect(Scheduler).toHaveYielded(['Promise resolved [default]']);
expect(Scheduler).toFlushUntilNextPaint(['default']);
assertLog(['Promise resolved [default]']);
await waitForPaint(['default']);
expect(root).toMatchRenderedOutput('default');
act(() => setValue('new value'));
expect(Scheduler).toHaveYielded(['Suspend! [new value]', 'Loading...']);
assertLog(['Suspend! [new value]', 'Loading...']);
jest.advanceTimersByTime(1000);
expect(Scheduler).toHaveYielded(['Promise resolved [new value]']);
expect(Scheduler).toFlushUntilNextPaint(['new value']);
assertLog(['Promise resolved [new value]']);
await waitForPaint(['new value']);
expect(root).toMatchRenderedOutput('new value');
});
it('updates memoized child of suspense component when context updates (function)', () => {
it('updates memoized child of suspense component when context updates (function)', async () => {
const {useContext, createContext, useState} = React;
const ValueContext = createContext(null);
@@ -1477,23 +1430,23 @@ describe('ReactSuspense', () => {
</Suspense>
</App>,
);
expect(Scheduler).toHaveYielded(['Suspend! [default]', 'Loading...']);
assertLog(['Suspend! [default]', 'Loading...']);
jest.advanceTimersByTime(1000);
expect(Scheduler).toHaveYielded(['Promise resolved [default]']);
expect(Scheduler).toFlushUntilNextPaint(['default']);
assertLog(['Promise resolved [default]']);
await waitForPaint(['default']);
expect(root).toMatchRenderedOutput('default');
act(() => setValue('new value'));
expect(Scheduler).toHaveYielded(['Suspend! [new value]', 'Loading...']);
assertLog(['Suspend! [new value]', 'Loading...']);
jest.advanceTimersByTime(1000);
expect(Scheduler).toHaveYielded(['Promise resolved [new value]']);
expect(Scheduler).toFlushUntilNextPaint(['new value']);
assertLog(['Promise resolved [new value]']);
await waitForPaint(['new value']);
expect(root).toMatchRenderedOutput('new value');
});
it('updates memoized child of suspense component when context updates (forwardRef)', () => {
it('updates memoized child of suspense component when context updates (forwardRef)', async () => {
const {forwardRef, useContext, createContext, useState} = React;
const ValueContext = createContext(null);
@@ -1529,19 +1482,19 @@ describe('ReactSuspense', () => {
}
const root = ReactTestRenderer.create(<App />);
expect(Scheduler).toHaveYielded(['Suspend! [default]', 'Loading...']);
assertLog(['Suspend! [default]', 'Loading...']);
jest.advanceTimersByTime(1000);
expect(Scheduler).toHaveYielded(['Promise resolved [default]']);
expect(Scheduler).toFlushUntilNextPaint(['default']);
assertLog(['Promise resolved [default]']);
await waitForPaint(['default']);
expect(root).toMatchRenderedOutput('default');
act(() => setValue('new value'));
expect(Scheduler).toHaveYielded(['Suspend! [new value]', 'Loading...']);
assertLog(['Suspend! [new value]', 'Loading...']);
jest.advanceTimersByTime(1000);
expect(Scheduler).toHaveYielded(['Promise resolved [new value]']);
expect(Scheduler).toFlushUntilNextPaint(['new value']);
assertLog(['Promise resolved [new value]']);
await waitForPaint(['new value']);
expect(root).toMatchRenderedOutput('new value');
});
@@ -1587,24 +1540,15 @@ describe('ReactSuspense', () => {
}
const root = ReactTestRenderer.create(<App />);
expect(Scheduler).toHaveYielded([
'Received context value [default]',
'default',
]);
assertLog(['Received context value [default]', 'default']);
expect(root).toMatchRenderedOutput('default');
act(() => setValue('new value'));
expect(Scheduler).toHaveYielded([
'Received context value [new value]',
'Loading...',
]);
assertLog(['Received context value [new value]', 'Loading...']);
expect(root).toMatchRenderedOutput('Loading...');
act(() => setValue('default'));
expect(Scheduler).toHaveYielded([
'Received context value [default]',
'default',
]);
assertLog(['Received context value [default]', 'default']);
expect(root).toMatchRenderedOutput('default');
});
});
@@ -12,6 +12,7 @@
let React;
let ReactNoop;
let Scheduler;
let waitForAll;
describe('ReactSuspense', () => {
beforeEach(() => {
@@ -20,6 +21,9 @@ describe('ReactSuspense', () => {
React = require('react');
ReactNoop = require('react-noop-renderer');
Scheduler = require('scheduler');
const InternalTestUtils = require('internal-test-utils');
waitForAll = InternalTestUtils.waitForAll;
});
function createThenable() {
@@ -83,13 +87,13 @@ describe('ReactSuspense', () => {
);
ReactNoop.render(element);
expect(Scheduler).toFlushWithoutYielding();
await waitForAll([]);
expect(ReactNoop).toMatchRenderedOutput('Waiting');
expect(ops).toEqual([new Set([promise])]);
ops = [];
await resolve();
expect(Scheduler).toFlushWithoutYielding();
await waitForAll([]);
expect(ReactNoop).toMatchRenderedOutput('Done');
expect(ops).toEqual([]);
});
@@ -122,27 +126,27 @@ describe('ReactSuspense', () => {
);
ReactNoop.render(element);
expect(Scheduler).toFlushWithoutYielding();
await waitForAll([]);
expect(ReactNoop).toMatchRenderedOutput('Waiting Tier 1');
expect(ops).toEqual([new Set([promise1, promise2])]);
ops = [];
await resolve1();
ReactNoop.render(element);
expect(Scheduler).toFlushWithoutYielding();
await waitForAll([]);
expect(ReactNoop).toMatchRenderedOutput('Waiting Tier 1');
expect(ops).toEqual([new Set([promise2])]);
ops = [];
await resolve2();
ReactNoop.render(element);
expect(Scheduler).toFlushWithoutYielding();
await waitForAll([]);
expect(ReactNoop).toMatchRenderedOutput('DoneDone');
expect(ops).toEqual([]);
});
// @gate www
it('nested suspense promises are reported only for their tier', () => {
it('nested suspense promises are reported only for their tier', async () => {
const {promise, PromiseComp} = createThenable();
const ops1 = [];
@@ -167,7 +171,7 @@ describe('ReactSuspense', () => {
);
ReactNoop.render(element);
expect(Scheduler).toFlushWithoutYielding();
await waitForAll([]);
expect(ReactNoop).toMatchRenderedOutput('Waiting Tier 2');
expect(ops1).toEqual([]);
expect(ops2).toEqual([new Set([promise])]);
@@ -209,7 +213,7 @@ describe('ReactSuspense', () => {
);
ReactNoop.render(element);
expect(Scheduler).toFlushWithoutYielding();
await waitForAll([]);
expect(ReactNoop).toMatchRenderedOutput('Waiting Tier 1');
expect(ops1).toEqual([new Set([promise1])]);
expect(ops2).toEqual([]);
@@ -218,7 +222,7 @@ describe('ReactSuspense', () => {
await resolve1();
ReactNoop.render(element);
expect(Scheduler).toFlushWithoutYielding();
await waitForAll([]);
// Force fallback to commit.
// TODO: Should be able to use `act` here.
@@ -232,7 +236,7 @@ describe('ReactSuspense', () => {
await resolve2();
ReactNoop.render(element);
expect(Scheduler).toFlushWithoutYielding();
await waitForAll([]);
expect(ReactNoop).toMatchRenderedOutput('DoneDone');
expect(ops1).toEqual([]);
expect(ops2).toEqual([]);