Codemod tests to waitFor pattern (4/?) (#26302)

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-03 21:24:22 -05:00 committed by GitHub
parent 06460b6fb5
commit faacefb4d0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 1095 additions and 1274 deletions

View File

@ -18,6 +18,8 @@ describe('SimpleEventPlugin', function () {
let onClick;
let container;
let assertLog;
let waitForAll;
function expectClickThru(element) {
element.click();
@ -43,6 +45,10 @@ describe('SimpleEventPlugin', function () {
ReactDOMClient = require('react-dom/client');
Scheduler = require('scheduler');
const InternalTestUtils = require('internal-test-utils');
assertLog = InternalTestUtils.assertLog;
waitForAll = InternalTestUtils.waitForAll;
onClick = jest.fn();
});
@ -222,12 +228,12 @@ describe('SimpleEventPlugin', function () {
ReactDOM.render(<Button />, container);
expect(button.textContent).toEqual('Count: 0');
expect(Scheduler).toHaveYielded([]);
assertLog([]);
click();
// There should be exactly one update.
expect(Scheduler).toHaveYielded(['didUpdate - Count: 3']);
assertLog(['didUpdate - Count: 3']);
expect(button.textContent).toEqual('Count: 3');
});
@ -240,6 +246,10 @@ describe('SimpleEventPlugin', function () {
ReactDOMClient = require('react-dom/client');
Scheduler = require('scheduler');
const InternalTestUtils = require('internal-test-utils');
assertLog = InternalTestUtils.assertLog;
waitForAll = InternalTestUtils.waitForAll;
act = require('jest-react').act;
});
@ -274,10 +284,10 @@ describe('SimpleEventPlugin', function () {
// Initial mount
root.render(<Button />);
// Should not have flushed yet because it's async
expect(Scheduler).toHaveYielded([]);
assertLog([]);
expect(button).toBe(undefined);
// Flush async work
expect(Scheduler).toFlushAndYield(['render button: enabled']);
await waitForAll(['render button: enabled']);
function click() {
const event = new MouseEvent('click', {
@ -292,7 +302,7 @@ describe('SimpleEventPlugin', function () {
// Click the button to trigger the side-effect
await act(async () => click());
expect(Scheduler).toHaveYielded([
assertLog([
// The handler fired
'Side-effect',
// The component re-rendered synchronously, even in concurrent mode.
@ -301,7 +311,7 @@ describe('SimpleEventPlugin', function () {
// Click the button again
click();
expect(Scheduler).toHaveYielded([
assertLog([
// The event handler was removed from the button, so there's no effect.
]);
@ -312,7 +322,7 @@ describe('SimpleEventPlugin', function () {
click();
click();
click();
expect(Scheduler).toFlushAndYield([]);
await waitForAll([]);
});
// NOTE: This test was written for the old behavior of discrete updates,
@ -345,7 +355,7 @@ describe('SimpleEventPlugin', function () {
// Should not have flushed yet because it's async
expect(button).toBe(undefined);
// Flush async work
Scheduler.unstable_flushAll();
await waitForAll([]);
expect(button.textContent).toEqual('Count: 0');
function click() {
@ -373,7 +383,7 @@ describe('SimpleEventPlugin', function () {
await act(async () => click());
// Flush the remaining work
Scheduler.unstable_flushAll();
await waitForAll([]);
// The counter should equal the total number of clicks
expect(button.textContent).toEqual('Count: 7');
});

View File

@ -18,6 +18,8 @@ let ReactTestRenderer;
let Scheduler;
let ReactDOMServer;
let act;
let assertLog;
let waitForAll;
describe('ReactHooks', () => {
beforeEach(() => {
@ -30,6 +32,10 @@ describe('ReactHooks', () => {
Scheduler = require('scheduler');
ReactDOMServer = require('react-dom/server');
act = require('jest-react').act;
const InternalTestUtils = require('internal-test-utils');
assertLog = InternalTestUtils.assertLog;
waitForAll = InternalTestUtils.waitForAll;
});
if (__DEV__) {
@ -54,7 +60,7 @@ describe('ReactHooks', () => {
});
}
it('bails out in the render phase if all of the state is the same', () => {
it('bails out in the render phase if all of the state is the same', async () => {
const {useState, useLayoutEffect} = React;
function Child({text}) {
@ -80,11 +86,7 @@ describe('ReactHooks', () => {
const root = ReactTestRenderer.create(null, {unstable_isConcurrent: true});
root.update(<Parent />);
expect(Scheduler).toFlushAndYield([
'Parent: 0, 0',
'Child: 0, 0',
'Effect: 0, 0',
]);
await waitForAll(['Parent: 0, 0', 'Child: 0, 0', 'Effect: 0, 0']);
expect(root).toMatchRenderedOutput('0, 0');
// Normal update
@ -93,15 +95,11 @@ describe('ReactHooks', () => {
setCounter2(1);
});
expect(Scheduler).toHaveYielded([
'Parent: 1, 1',
'Child: 1, 1',
'Effect: 1, 1',
]);
assertLog(['Parent: 1, 1', 'Child: 1, 1', 'Effect: 1, 1']);
// Update that bails out.
act(() => setCounter1(1));
expect(Scheduler).toHaveYielded(['Parent: 1, 1']);
assertLog(['Parent: 1, 1']);
// This time, one of the state updates but the other one doesn't. So we
// can't bail out.
@ -110,11 +108,7 @@ describe('ReactHooks', () => {
setCounter2(2);
});
expect(Scheduler).toHaveYielded([
'Parent: 1, 2',
'Child: 1, 2',
'Effect: 1, 2',
]);
assertLog(['Parent: 1, 2', 'Child: 1, 2', 'Effect: 1, 2']);
// Lots of updates that eventually resolve to the current values.
act(() => {
@ -128,7 +122,7 @@ describe('ReactHooks', () => {
// Because the final values are the same as the current values, the
// component bails out.
expect(Scheduler).toHaveYielded(['Parent: 1, 2']);
assertLog(['Parent: 1, 2']);
// prepare to check SameValue
act(() => {
@ -136,11 +130,7 @@ describe('ReactHooks', () => {
setCounter2(NaN);
});
expect(Scheduler).toHaveYielded([
'Parent: 0, NaN',
'Child: 0, NaN',
'Effect: 0, NaN',
]);
assertLog(['Parent: 0, NaN', 'Child: 0, NaN', 'Effect: 0, NaN']);
// check if re-setting to negative 0 / NaN still bails out
act(() => {
@ -150,20 +140,16 @@ describe('ReactHooks', () => {
setCounter2(NaN);
});
expect(Scheduler).toHaveYielded(['Parent: 0, NaN']);
assertLog(['Parent: 0, NaN']);
// check if changing negative 0 to positive 0 does not bail out
act(() => {
setCounter1(0);
});
expect(Scheduler).toHaveYielded([
'Parent: 0, NaN',
'Child: 0, NaN',
'Effect: 0, NaN',
]);
assertLog(['Parent: 0, NaN', 'Child: 0, NaN', 'Effect: 0, NaN']);
});
it('bails out in render phase if all the state is the same and props bail out with memo', () => {
it('bails out in render phase if all the state is the same and props bail out with memo', async () => {
const {useState, memo} = React;
function Child({text}) {
@ -188,10 +174,7 @@ describe('ReactHooks', () => {
const root = ReactTestRenderer.create(null, {unstable_isConcurrent: true});
root.update(<Parent theme="light" />);
expect(Scheduler).toFlushAndYield([
'Parent: 0, 0 (light)',
'Child: 0, 0 (light)',
]);
await waitForAll(['Parent: 0, 0 (light)', 'Child: 0, 0 (light)']);
expect(root).toMatchRenderedOutput('0, 0 (light)');
// Normal update
@ -200,14 +183,11 @@ describe('ReactHooks', () => {
setCounter2(1);
});
expect(Scheduler).toHaveYielded([
'Parent: 1, 1 (light)',
'Child: 1, 1 (light)',
]);
assertLog(['Parent: 1, 1 (light)', 'Child: 1, 1 (light)']);
// Update that bails out.
act(() => setCounter1(1));
expect(Scheduler).toHaveYielded(['Parent: 1, 1 (light)']);
assertLog(['Parent: 1, 1 (light)']);
// This time, one of the state updates but the other one doesn't. So we
// can't bail out.
@ -216,10 +196,7 @@ describe('ReactHooks', () => {
setCounter2(2);
});
expect(Scheduler).toHaveYielded([
'Parent: 1, 2 (light)',
'Child: 1, 2 (light)',
]);
assertLog(['Parent: 1, 2 (light)', 'Child: 1, 2 (light)']);
// Updates bail out, but component still renders because props
// have changed
@ -229,10 +206,7 @@ describe('ReactHooks', () => {
root.update(<Parent theme="dark" />);
});
expect(Scheduler).toHaveYielded([
'Parent: 1, 2 (dark)',
'Child: 1, 2 (dark)',
]);
assertLog(['Parent: 1, 2 (dark)', 'Child: 1, 2 (dark)']);
// Both props and state bail out
act(() => {
@ -241,10 +215,10 @@ describe('ReactHooks', () => {
root.update(<Parent theme="dark" />);
});
expect(Scheduler).toHaveYielded(['Parent: 1, 2 (dark)']);
assertLog(['Parent: 1, 2 (dark)']);
});
it('warns about setState second argument', () => {
it('warns about setState second argument', async () => {
const {useState} = React;
let setCounter;
@ -258,7 +232,7 @@ describe('ReactHooks', () => {
const root = ReactTestRenderer.create(null, {unstable_isConcurrent: true});
root.update(<Counter />);
expect(Scheduler).toFlushAndYield(['Count: 0']);
await waitForAll(['Count: 0']);
expect(root).toMatchRenderedOutput('0');
expect(() => {
@ -274,11 +248,11 @@ describe('ReactHooks', () => {
'declare it in the component body with useEffect().',
{withoutStack: true},
);
expect(Scheduler).toHaveYielded(['Count: 1']);
assertLog(['Count: 1']);
expect(root).toMatchRenderedOutput('1');
});
it('warns about dispatch second argument', () => {
it('warns about dispatch second argument', async () => {
const {useReducer} = React;
let dispatch;
@ -292,7 +266,7 @@ describe('ReactHooks', () => {
const root = ReactTestRenderer.create(null, {unstable_isConcurrent: true});
root.update(<Counter />);
expect(Scheduler).toFlushAndYield(['Count: 0']);
await waitForAll(['Count: 0']);
expect(root).toMatchRenderedOutput('0');
expect(() => {
@ -308,11 +282,11 @@ describe('ReactHooks', () => {
'declare it in the component body with useEffect().',
{withoutStack: true},
);
expect(Scheduler).toHaveYielded(['Count: 1']);
assertLog(['Count: 1']);
expect(root).toMatchRenderedOutput('1');
});
it('never bails out if context has changed', () => {
it('never bails out if context has changed', async () => {
const {useState, useLayoutEffect, useContext} = React;
const ThemeContext = React.createContext('light');
@ -355,7 +329,7 @@ describe('ReactHooks', () => {
);
});
expect(Scheduler).toHaveYielded([
assertLog([
'Theme: light',
'Parent: 0 (light)',
'Child: 0 (light)',
@ -366,21 +340,17 @@ describe('ReactHooks', () => {
// Updating the theme to the same value doesn't cause the consumers
// to re-render.
setTheme('light');
expect(Scheduler).toFlushAndYield([]);
await waitForAll([]);
expect(root).toMatchRenderedOutput('0 (light)');
// Normal update
act(() => setCounter(1));
expect(Scheduler).toHaveYielded([
'Parent: 1 (light)',
'Child: 1 (light)',
'Effect: 1 (light)',
]);
assertLog(['Parent: 1 (light)', 'Child: 1 (light)', 'Effect: 1 (light)']);
expect(root).toMatchRenderedOutput('1 (light)');
// Update that doesn't change state, so it bails out
act(() => setCounter(1));
expect(Scheduler).toHaveYielded(['Parent: 1 (light)']);
assertLog(['Parent: 1 (light)']);
expect(root).toMatchRenderedOutput('1 (light)');
// Update that doesn't change state, but the context changes, too, so it
@ -390,7 +360,7 @@ describe('ReactHooks', () => {
setTheme('dark');
});
expect(Scheduler).toHaveYielded([
assertLog([
'Theme: dark',
'Parent: 1 (dark)',
'Child: 1 (dark)',
@ -399,7 +369,7 @@ describe('ReactHooks', () => {
expect(root).toMatchRenderedOutput('1 (dark)');
});
it('can bail out without calling render phase (as an optimization) if queue is known to be empty', () => {
it('can bail out without calling render phase (as an optimization) if queue is known to be empty', async () => {
const {useState, useLayoutEffect} = React;
function Child({text}) {
@ -420,12 +390,12 @@ describe('ReactHooks', () => {
const root = ReactTestRenderer.create(null, {unstable_isConcurrent: true});
root.update(<Parent />);
expect(Scheduler).toFlushAndYield(['Parent: 0', 'Child: 0', 'Effect: 0']);
await waitForAll(['Parent: 0', 'Child: 0', 'Effect: 0']);
expect(root).toMatchRenderedOutput('0');
// Normal update
act(() => setCounter(1));
expect(Scheduler).toHaveYielded(['Parent: 1', 'Child: 1', 'Effect: 1']);
assertLog(['Parent: 1', 'Child: 1', 'Effect: 1']);
expect(root).toMatchRenderedOutput('1');
// Update to the same state. React doesn't know if the queue is empty
@ -433,25 +403,25 @@ describe('ReactHooks', () => {
// enter the render phase before we can bail out. But we bail out before
// rendering the child, and we don't fire any effects.
act(() => setCounter(1));
expect(Scheduler).toHaveYielded(['Parent: 1']);
assertLog(['Parent: 1']);
expect(root).toMatchRenderedOutput('1');
// Update to the same state again. This times, neither fiber has pending
// update priority, so we can bail out before even entering the render phase.
act(() => setCounter(1));
expect(Scheduler).toFlushAndYield([]);
await waitForAll([]);
expect(root).toMatchRenderedOutput('1');
// This changes the state to something different so it renders normally.
act(() => setCounter(2));
expect(Scheduler).toHaveYielded(['Parent: 2', 'Child: 2', 'Effect: 2']);
assertLog(['Parent: 2', 'Child: 2', 'Effect: 2']);
expect(root).toMatchRenderedOutput('2');
// prepare to check SameValue
act(() => {
setCounter(0);
});
expect(Scheduler).toHaveYielded(['Parent: 0', 'Child: 0', 'Effect: 0']);
assertLog(['Parent: 0', 'Child: 0', 'Effect: 0']);
expect(root).toMatchRenderedOutput('0');
// Update to the same state for the first time to flush the queue
@ -459,25 +429,25 @@ describe('ReactHooks', () => {
setCounter(0);
});
expect(Scheduler).toHaveYielded(['Parent: 0']);
assertLog(['Parent: 0']);
expect(root).toMatchRenderedOutput('0');
// Update again to the same state. Should bail out.
act(() => {
setCounter(0);
});
expect(Scheduler).toFlushAndYield([]);
await waitForAll([]);
expect(root).toMatchRenderedOutput('0');
// Update to a different state (positive 0 to negative 0)
act(() => {
setCounter(0 / -1);
});
expect(Scheduler).toHaveYielded(['Parent: 0', 'Child: 0', 'Effect: 0']);
assertLog(['Parent: 0', 'Child: 0', 'Effect: 0']);
expect(root).toMatchRenderedOutput('0');
});
it('bails out multiple times in a row without entering render phase', () => {
it('bails out multiple times in a row without entering render phase', async () => {
const {useState} = React;
function Child({text}) {
@ -495,7 +465,7 @@ describe('ReactHooks', () => {
const root = ReactTestRenderer.create(null, {unstable_isConcurrent: true});
root.update(<Parent />);
expect(Scheduler).toFlushAndYield(['Parent: 0', 'Child: 0']);
await waitForAll(['Parent: 0', 'Child: 0']);
expect(root).toMatchRenderedOutput('0');
const update = value => {
@ -515,7 +485,7 @@ describe('ReactHooks', () => {
update(3);
});
expect(Scheduler).toHaveYielded([
assertLog([
// The first four updates were eagerly computed, because the queue is
// empty before each one.
'Compute state (0 -> 0)',
@ -527,7 +497,7 @@ describe('ReactHooks', () => {
]);
// Now let's enter the render phase
expect(Scheduler).toFlushAndYield([
await waitForAll([
// We don't need to re-compute the first four updates. Only the final two.
'Compute state (1 -> 2)',
'Compute state (2 -> 3)',
@ -537,7 +507,7 @@ describe('ReactHooks', () => {
expect(root).toMatchRenderedOutput('3');
});
it('can rebase on top of a previously skipped update', () => {
it('can rebase on top of a previously skipped update', async () => {
const {useState} = React;
function Child({text}) {
@ -555,7 +525,7 @@ describe('ReactHooks', () => {
const root = ReactTestRenderer.create(null, {unstable_isConcurrent: true});
root.update(<Parent />);
expect(Scheduler).toFlushAndYield(['Parent: 1', 'Child: 1']);
await waitForAll(['Parent: 1', 'Child: 1']);
expect(root).toMatchRenderedOutput('1');
const update = compute => {
@ -576,13 +546,13 @@ describe('ReactHooks', () => {
ReactTestRenderer.unstable_batchedUpdates(() => update(n => n * 100));
}
// The new state is eagerly computed.
expect(Scheduler).toHaveYielded(['Compute state (1 -> 100)']);
assertLog(['Compute state (1 -> 100)']);
// but before it's flushed, a higher priority update interrupts it.
root.unstable_flushSync(() => {
update(n => n + 5);
});
expect(Scheduler).toHaveYielded([
assertLog([
// The eagerly computed state was completely skipped
'Compute state (1 -> 6)',
'Parent: 6',
@ -593,7 +563,7 @@ describe('ReactHooks', () => {
// Now when we finish the first update, the second update is rebased on top.
// Notice we didn't have to recompute the first update even though it was
// skipped in the previous render.
expect(Scheduler).toFlushAndYield([
await waitForAll([
'Compute state (100 -> 105)',
'Parent: 105',
'Child: 105',
@ -612,7 +582,7 @@ describe('ReactHooks', () => {
return props.dependencies;
}
const root = ReactTestRenderer.create(<App dependencies={['A']} />);
expect(Scheduler).toHaveYielded(['Did commit: A']);
assertLog(['Did commit: A']);
expect(() => {
root.update(<App dependencies={['A', 'B']} />);
}).toErrorDev([
@ -639,7 +609,7 @@ describe('ReactHooks', () => {
const root = ReactTestRenderer.create(null);
root.update(<App text="Hello" hasDeps={true} />);
expect(Scheduler).toHaveYielded(['Compute']);
assertLog(['Compute']);
expect(root).toMatchRenderedOutput('HELLO');
expect(() => {
@ -1841,7 +1811,7 @@ describe('ReactHooks', () => {
);
expect(root).toMatchRenderedOutput('loading');
await Promise.resolve();
Scheduler.unstable_flushAll();
await waitForAll([]);
expect(root).toMatchRenderedOutput('hello');
});
@ -1873,7 +1843,7 @@ describe('ReactHooks', () => {
);
expect(root).toMatchRenderedOutput('loading');
await Promise.resolve();
Scheduler.unstable_flushAll();
await waitForAll([]);
expect(root).toMatchRenderedOutput('hello');
});
@ -1905,7 +1875,7 @@ describe('ReactHooks', () => {
);
expect(root).toMatchRenderedOutput('loading');
await Promise.resolve();
Scheduler.unstable_flushAll();
await waitForAll([]);
expect(root).toMatchRenderedOutput('hello');
});

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -16,6 +16,10 @@ let React;
let ReactNoop;
let Scheduler;
let act;
let assertLog;
let waitForAll;
let waitFor;
let waitForThrow;
describe('ReactIncrementalErrorHandling', () => {
beforeEach(() => {
@ -27,6 +31,12 @@ describe('ReactIncrementalErrorHandling', () => {
ReactNoop = require('react-noop-renderer');
Scheduler = require('scheduler');
act = require('jest-react').act;
const InternalTestUtils = require('internal-test-utils');
assertLog = InternalTestUtils.assertLog;
waitForAll = InternalTestUtils.waitForAll;
waitFor = InternalTestUtils.waitFor;
waitForThrow = InternalTestUtils.waitForThrow;
});
afterEach(() => {
@ -55,7 +65,7 @@ describe('ReactIncrementalErrorHandling', () => {
);
}
it('recovers from errors asynchronously', () => {
it('recovers from errors asynchronously', async () => {
class ErrorBoundary extends React.Component {
state = {error: null};
static getDerivedStateFromError(error) {
@ -104,7 +114,7 @@ describe('ReactIncrementalErrorHandling', () => {
});
// Start rendering asynchronously
expect(Scheduler).toFlushAndYieldThrough([
await waitFor([
'ErrorBoundary (try)',
'Indirection',
'Indirection',
@ -114,9 +124,9 @@ describe('ReactIncrementalErrorHandling', () => {
]);
// Still rendering async...
expect(Scheduler).toFlushAndYieldThrough(['Indirection']);
await waitFor(['Indirection']);
expect(Scheduler).toFlushAndYieldThrough([
await waitFor([
'Indirection',
// Call getDerivedStateFromError and re-render the error boundary, this
@ -153,7 +163,7 @@ describe('ReactIncrementalErrorHandling', () => {
);
});
it('recovers from errors asynchronously (legacy, no getDerivedStateFromError)', () => {
it('recovers from errors asynchronously (legacy, no getDerivedStateFromError)', async () => {
class ErrorBoundary extends React.Component {
state = {error: null};
componentDidCatch(error) {
@ -202,7 +212,7 @@ describe('ReactIncrementalErrorHandling', () => {
});
// Start rendering asynchronously
expect(Scheduler).toFlushAndYieldThrough([
await waitFor([
'ErrorBoundary (try)',
'Indirection',
'Indirection',
@ -212,9 +222,9 @@ describe('ReactIncrementalErrorHandling', () => {
]);
// Still rendering async...
expect(Scheduler).toFlushAndYieldThrough(['Indirection']);
await waitFor(['Indirection']);
expect(Scheduler).toFlushAndYieldThrough([
await waitFor([
'Indirection',
// Now that the tree is complete, and there's no remaining work, React
// reverts to legacy mode to retry one more time before handling the error.
@ -254,7 +264,7 @@ describe('ReactIncrementalErrorHandling', () => {
React.startTransition(() => {
ReactNoop.render(<App isBroken={true} />, onCommit);
});
expect(Scheduler).toFlushAndYieldThrough(['error']);
await waitFor(['error']);
React.startTransition(() => {
// This update is in a separate batch
@ -268,7 +278,7 @@ describe('ReactIncrementalErrorHandling', () => {
// to recover from the error is synchronous, this should be enough to
// finish the rest of the work.
Scheduler.unstable_flushNumberOfYields(1);
expect(Scheduler).toHaveYielded([
assertLog([
'success',
// Nothing commits until the second update completes.
'commit',
@ -280,7 +290,7 @@ describe('ReactIncrementalErrorHandling', () => {
});
// @gate www
it('does not include offscreen work when retrying after an error', () => {
it('does not include offscreen work when retrying after an error', async () => {
function App(props) {
if (props.isBroken) {
Scheduler.unstable_yieldValue('error');
@ -304,7 +314,7 @@ describe('ReactIncrementalErrorHandling', () => {
React.startTransition(() => {
ReactNoop.render(<App isBroken={true} />, onCommit);
});
expect(Scheduler).toFlushAndYieldThrough(['error']);
await waitFor(['error']);
expect(ReactNoop).toMatchRenderedOutput(null);
@ -320,7 +330,7 @@ describe('ReactIncrementalErrorHandling', () => {
// to recover from the error is synchronous, this should be enough to
// finish the rest of the work.
Scheduler.unstable_flushNumberOfYields(1);
expect(Scheduler).toHaveYielded([
assertLog([
'success',
// Nothing commits until the second update completes.
'commit',
@ -335,7 +345,7 @@ describe('ReactIncrementalErrorHandling', () => {
);
// The offscreen content finishes in a subsequent render
expect(Scheduler).toFlushAndYield([]);
await waitForAll([]);
expect(ReactNoop).toMatchRenderedOutput(
<>
Everything is fine
@ -346,7 +356,7 @@ describe('ReactIncrementalErrorHandling', () => {
);
});
it('retries one more time before handling error', () => {
it('retries one more time before handling error', async () => {
function BadRender({unused}) {
Scheduler.unstable_yieldValue('BadRender');
throw new Error('oops');
@ -374,19 +384,14 @@ describe('ReactIncrementalErrorHandling', () => {
});
// Render the bad component asynchronously
expect(Scheduler).toFlushAndYieldThrough(['Parent', 'BadRender']);
await waitFor(['Parent', 'BadRender']);
// Finish the rest of the async work
expect(Scheduler).toFlushAndYieldThrough(['Sibling']);
await waitFor(['Sibling']);
// Old scheduler renders, commits, and throws synchronously
expect(() => Scheduler.unstable_flushNumberOfYields(1)).toThrow('oops');
expect(Scheduler).toHaveYielded([
'Parent',
'BadRender',
'Sibling',
'commit',
]);
assertLog(['Parent', 'BadRender', 'Sibling', 'commit']);
expect(ReactNoop).toMatchRenderedOutput(null);
});
@ -418,7 +423,7 @@ describe('ReactIncrementalErrorHandling', () => {
});
// Render part of the tree
expect(Scheduler).toFlushAndYieldThrough(['A', 'B']);
await waitFor(['A', 'B']);
// Expire the render midway through
Scheduler.unstable_advanceTime(10000);
@ -428,7 +433,7 @@ describe('ReactIncrementalErrorHandling', () => {
ReactNoop.flushSync();
}).toThrow('Oops');
expect(Scheduler).toHaveYielded([
assertLog([
// The render expired, but we shouldn't throw out the partial work.
// Finish the current level.
'Oops',
@ -446,7 +451,7 @@ describe('ReactIncrementalErrorHandling', () => {
expect(ReactNoop).toMatchRenderedOutput(null);
});
it('calls componentDidCatch multiple times for multiple errors', () => {
it('calls componentDidCatch multiple times for multiple errors', async () => {
let id = 0;
class BadMount extends React.Component {
componentDidMount() {
@ -481,7 +486,7 @@ describe('ReactIncrementalErrorHandling', () => {
</ErrorBoundary>,
);
expect(Scheduler).toFlushAndYield([
await waitForAll([
'ErrorBoundary',
'BadMount',
'BadMount',
@ -495,7 +500,7 @@ describe('ReactIncrementalErrorHandling', () => {
);
});
it('catches render error in a boundary during full deferred mounting', () => {
it('catches render error in a boundary during full deferred mounting', async () => {
class ErrorBoundary extends React.Component {
state = {error: null};
componentDidCatch(error) {
@ -520,13 +525,13 @@ describe('ReactIncrementalErrorHandling', () => {
<BrokenRender />
</ErrorBoundary>,
);
expect(Scheduler).toFlushWithoutYielding();
await waitForAll([]);
expect(ReactNoop).toMatchRenderedOutput(
<span prop="Caught an error: Hello." />,
);
});
it('catches render error in a boundary during partial deferred mounting', () => {
it('catches render error in a boundary during partial deferred mounting', async () => {
class ErrorBoundary extends React.Component {
state = {error: null};
componentDidCatch(error) {
@ -558,10 +563,10 @@ describe('ReactIncrementalErrorHandling', () => {
);
});
expect(Scheduler).toFlushAndYieldThrough(['ErrorBoundary render success']);
await waitFor(['ErrorBoundary render success']);
expect(ReactNoop).toMatchRenderedOutput(null);
expect(Scheduler).toFlushAndYield([
await waitForAll([
'BrokenRender',
// React retries one more time
'ErrorBoundary render success',
@ -608,7 +613,7 @@ describe('ReactIncrementalErrorHandling', () => {
);
});
expect(Scheduler).toHaveYielded([
assertLog([
'ErrorBoundary render success',
'BrokenRender',
@ -658,7 +663,7 @@ describe('ReactIncrementalErrorHandling', () => {
);
});
expect(Scheduler).toHaveYielded([
assertLog([
'ErrorBoundary render success',
'BrokenRender',
@ -675,7 +680,7 @@ describe('ReactIncrementalErrorHandling', () => {
);
});
it('propagates an error from a noop error boundary during full deferred mounting', () => {
it('propagates an error from a noop error boundary during full deferred mounting', async () => {
class RethrowErrorBoundary extends React.Component {
componentDidCatch(error) {
Scheduler.unstable_yieldValue('RethrowErrorBoundary componentDidCatch');
@ -698,23 +703,22 @@ describe('ReactIncrementalErrorHandling', () => {
</RethrowErrorBoundary>,
);
expect(() => {
expect(Scheduler).toFlushAndYield([
'RethrowErrorBoundary render',
'BrokenRender',
await waitForThrow('Hello');
assertLog([
'RethrowErrorBoundary render',
'BrokenRender',
// React retries one more time
'RethrowErrorBoundary render',
'BrokenRender',
// React retries one more time
'RethrowErrorBoundary render',
'BrokenRender',
// Errored again on retry. Now handle it.
'RethrowErrorBoundary componentDidCatch',
]);
}).toThrow('Hello');
// Errored again on retry. Now handle it.
'RethrowErrorBoundary componentDidCatch',
]);
expect(ReactNoop.getChildrenAsJSX()).toEqual(null);
});
it('propagates an error from a noop error boundary during partial deferred mounting', () => {
it('propagates an error from a noop error boundary during partial deferred mounting', async () => {
class RethrowErrorBoundary extends React.Component {
componentDidCatch(error) {
Scheduler.unstable_yieldValue('RethrowErrorBoundary componentDidCatch');
@ -739,12 +743,10 @@ describe('ReactIncrementalErrorHandling', () => {
);
});
expect(Scheduler).toFlushAndYieldThrough(['RethrowErrorBoundary render']);
await waitFor(['RethrowErrorBoundary render']);
expect(() => {
expect(Scheduler).toFlushWithoutYielding();
}).toThrow('Hello');
expect(Scheduler).toHaveYielded([
await waitForThrow('Hello');
assertLog([
'BrokenRender',
// React retries one more time
@ -783,7 +785,7 @@ describe('ReactIncrementalErrorHandling', () => {
);
});
}).toThrow('Hello');
expect(Scheduler).toHaveYielded([
assertLog([
'RethrowErrorBoundary render',
'BrokenRender',
@ -826,7 +828,7 @@ describe('ReactIncrementalErrorHandling', () => {
);
});
}).toThrow('Hello');
expect(Scheduler).toHaveYielded([
assertLog([
'RethrowErrorBoundary render',
'BrokenRender',
@ -840,7 +842,7 @@ describe('ReactIncrementalErrorHandling', () => {
expect(ReactNoop).toMatchRenderedOutput(null);
});
it('applies batched updates regardless despite errors in scheduling', () => {
it('applies batched updates regardless despite errors in scheduling', async () => {
ReactNoop.render(<span prop="a:1" />);
expect(() => {
ReactNoop.batchedUpdates(() => {
@ -849,11 +851,11 @@ describe('ReactIncrementalErrorHandling', () => {
throw new Error('Hello');
});
}).toThrow('Hello');
expect(Scheduler).toFlushWithoutYielding();
await waitForAll([]);
expect(ReactNoop).toMatchRenderedOutput(<span prop="a:3" />);
});
it('applies nested batched updates despite errors in scheduling', () => {
it('applies nested batched updates despite errors in scheduling', async () => {
ReactNoop.render(<span prop="a:1" />);
expect(() => {
ReactNoop.batchedUpdates(() => {
@ -866,12 +868,12 @@ describe('ReactIncrementalErrorHandling', () => {
});
});
}).toThrow('Hello');
expect(Scheduler).toFlushWithoutYielding();
await waitForAll([]);
expect(ReactNoop).toMatchRenderedOutput(<span prop="a:5" />);
});
// TODO: Is this a breaking change?
it('defers additional sync work to a separate event after an error', () => {
it('defers additional sync work to a separate event after an error', async () => {
ReactNoop.render(<span prop="a:1" />);
expect(() => {
ReactNoop.flushSync(() => {
@ -882,11 +884,11 @@ describe('ReactIncrementalErrorHandling', () => {
});
});
}).toThrow('Hello');
Scheduler.unstable_flushAll();
await waitForAll([]);
expect(ReactNoop).toMatchRenderedOutput(<span prop="a:3" />);
});
it('can schedule updates after uncaught error in render on mount', () => {
it('can schedule updates after uncaught error in render on mount', async () => {
function BrokenRender({unused}) {
Scheduler.unstable_yieldValue('BrokenRender');
throw new Error('Hello');
@ -898,20 +900,18 @@ describe('ReactIncrementalErrorHandling', () => {
}
ReactNoop.render(<BrokenRender />);
expect(() => {
expect(Scheduler).toFlushWithoutYielding();
}).toThrow('Hello');
await waitForThrow('Hello');
ReactNoop.render(<Foo />);
expect(Scheduler).toHaveYielded([
assertLog([
'BrokenRender',
// React retries one more time
'BrokenRender',
// Errored again on retry
]);
expect(Scheduler).toFlushAndYield(['Foo']);
await waitForAll(['Foo']);
});
it('can schedule updates after uncaught error in render on update', () => {
it('can schedule updates after uncaught error in render on update', async () => {
function BrokenRender({shouldThrow}) {
Scheduler.unstable_yieldValue('BrokenRender');
if (shouldThrow) {
@ -926,13 +926,11 @@ describe('ReactIncrementalErrorHandling', () => {
}
ReactNoop.render(<BrokenRender shouldThrow={false} />);
expect(Scheduler).toFlushAndYield(['BrokenRender']);
await waitForAll(['BrokenRender']);
expect(() => {
ReactNoop.render(<BrokenRender shouldThrow={true} />);
expect(Scheduler).toFlushWithoutYielding();
}).toThrow('Hello');
expect(Scheduler).toHaveYielded([
ReactNoop.render(<BrokenRender shouldThrow={true} />);
await waitForThrow('Hello');
assertLog([
'BrokenRender',
// React retries one more time
'BrokenRender',
@ -940,10 +938,10 @@ describe('ReactIncrementalErrorHandling', () => {
]);
ReactNoop.render(<Foo />);
expect(Scheduler).toFlushAndYield(['Foo']);
await waitForAll(['Foo']);
});
it('can schedule updates after uncaught error during unmounting', () => {
it('can schedule updates after uncaught error during unmounting', async () => {
class BrokenComponentWillUnmount extends React.Component {
render() {
return <div />;
@ -959,19 +957,17 @@ describe('ReactIncrementalErrorHandling', () => {
}
ReactNoop.render(<BrokenComponentWillUnmount />);
expect(Scheduler).toFlushWithoutYielding();
await waitForAll([]);
expect(() => {
ReactNoop.render(<div />);
expect(Scheduler).toFlushWithoutYielding();
}).toThrow('Hello');
ReactNoop.render(<div />);
await waitForThrow('Hello');
ReactNoop.render(<Foo />);
expect(Scheduler).toFlushAndYield(['Foo']);
await waitForAll(['Foo']);
});
// @gate skipUnmountedBoundaries
it('should not attempt to recover an unmounting error boundary', () => {
it('should not attempt to recover an unmounting error boundary', async () => {
class Parent extends React.Component {
componentWillUnmount() {
Scheduler.unstable_yieldValue('Parent componentWillUnmount');
@ -1001,22 +997,21 @@ describe('ReactIncrementalErrorHandling', () => {
}
ReactNoop.render(<Parent />);
expect(Scheduler).toFlushWithoutYielding();
await waitForAll([]);
// Because the error boundary is also unmounting,
// an error in ThrowsOnUnmount should be rethrown.
expect(() => {
ReactNoop.render(null);
expect(Scheduler).toFlushAndYield([
'Parent componentWillUnmount',
'ThrowsOnUnmount componentWillUnmount',
]);
}).toThrow('unmount error');
ReactNoop.render(null);
await waitForThrow('unmount error');
await assertLog([
'Parent componentWillUnmount',
'ThrowsOnUnmount componentWillUnmount',
]);
ReactNoop.render(<Parent />);
});
it('can unmount an error boundary before it is handled', () => {
it('can unmount an error boundary before it is handled', async () => {
let parent;
class Parent extends React.Component {
@ -1045,14 +1040,14 @@ describe('ReactIncrementalErrorHandling', () => {
}
ReactNoop.render(<Parent />);
expect(Scheduler).toFlushWithoutYielding();
await waitForAll([]);
ReactNoop.flushSync(() => {
ReactNoop.render(<Parent />);
});
});
it('continues work on other roots despite caught errors', () => {
it('continues work on other roots despite caught errors', async () => {
class ErrorBoundary extends React.Component {
state = {error: null};
componentDidCatch(error) {
@ -1079,50 +1074,42 @@ describe('ReactIncrementalErrorHandling', () => {
'a',
);
ReactNoop.renderToRootWithID(<span prop="b:1" />, 'b');
expect(Scheduler).toFlushWithoutYielding();
await waitForAll([]);
expect(ReactNoop.getChildrenAsJSX('a')).toEqual(
<span prop="Caught an error: Hello." />,
);
expect(Scheduler).toFlushWithoutYielding();
await waitForAll([]);
expect(ReactNoop.getChildrenAsJSX('b')).toEqual(<span prop="b:1" />);
});
it('continues work on other roots despite uncaught errors', () => {
it('continues work on other roots despite uncaught errors', async () => {
function BrokenRender(props) {
throw new Error(props.label);
}
ReactNoop.renderToRootWithID(<BrokenRender label="a" />, 'a');
expect(() => {
expect(Scheduler).toFlushWithoutYielding();
}).toThrow('a');
await waitForThrow('a');
expect(ReactNoop.getChildrenAsJSX('a')).toEqual(null);
ReactNoop.renderToRootWithID(<BrokenRender label="a" />, 'a');
ReactNoop.renderToRootWithID(<span prop="b:2" />, 'b');
expect(() => {
expect(Scheduler).toFlushWithoutYielding();
}).toThrow('a');
await waitForThrow('a');
expect(Scheduler).toFlushWithoutYielding();
await waitForAll([]);
expect(ReactNoop.getChildrenAsJSX('a')).toEqual(null);
expect(ReactNoop.getChildrenAsJSX('b')).toEqual(<span prop="b:2" />);
ReactNoop.renderToRootWithID(<span prop="a:3" />, 'a');
ReactNoop.renderToRootWithID(<BrokenRender label="b" />, 'b');
expect(() => {
expect(Scheduler).toFlushWithoutYielding();
}).toThrow('b');
await waitForThrow('b');
expect(ReactNoop.getChildrenAsJSX('a')).toEqual(<span prop="a:3" />);
expect(ReactNoop.getChildrenAsJSX('b')).toEqual(null);
ReactNoop.renderToRootWithID(<span prop="a:4" />, 'a');
ReactNoop.renderToRootWithID(<BrokenRender label="b" />, 'b');
ReactNoop.renderToRootWithID(<span prop="c:4" />, 'c');
expect(() => {
expect(Scheduler).toFlushWithoutYielding();
}).toThrow('b');
expect(Scheduler).toFlushWithoutYielding();
await waitForThrow('b');
await waitForAll([]);
expect(ReactNoop.getChildrenAsJSX('a')).toEqual(<span prop="a:4" />);
expect(ReactNoop.getChildrenAsJSX('b')).toEqual(null);
expect(ReactNoop.getChildrenAsJSX('c')).toEqual(<span prop="c:4" />);
@ -1132,10 +1119,8 @@ describe('ReactIncrementalErrorHandling', () => {
ReactNoop.renderToRootWithID(<span prop="c:5" />, 'c');
ReactNoop.renderToRootWithID(<span prop="d:5" />, 'd');
ReactNoop.renderToRootWithID(<BrokenRender label="e" />, 'e');
expect(() => {
expect(Scheduler).toFlushWithoutYielding();
}).toThrow('e');
expect(Scheduler).toFlushWithoutYielding();
await waitForThrow('e');
await waitForAll([]);
expect(ReactNoop.getChildrenAsJSX('a')).toEqual(<span prop="a:5" />);
expect(ReactNoop.getChildrenAsJSX('b')).toEqual(<span prop="b:5" />);
expect(ReactNoop.getChildrenAsJSX('c')).toEqual(<span prop="c:5" />);
@ -1149,17 +1134,11 @@ describe('ReactIncrementalErrorHandling', () => {
ReactNoop.renderToRootWithID(<BrokenRender label="e" />, 'e');
ReactNoop.renderToRootWithID(<span prop="f:6" />, 'f');
expect(() => {
expect(Scheduler).toFlushWithoutYielding();
}).toThrow('a');
expect(() => {
expect(Scheduler).toFlushWithoutYielding();
}).toThrow('c');
expect(() => {
expect(Scheduler).toFlushWithoutYielding();
}).toThrow('e');
await waitForThrow('a');
await waitForThrow('c');
await waitForThrow('e');
expect(Scheduler).toFlushWithoutYielding();
await waitForAll([]);
expect(ReactNoop.getChildrenAsJSX('a')).toEqual(null);
expect(ReactNoop.getChildrenAsJSX('b')).toEqual(<span prop="b:6" />);
expect(ReactNoop.getChildrenAsJSX('c')).toEqual(null);
@ -1173,7 +1152,7 @@ describe('ReactIncrementalErrorHandling', () => {
ReactNoop.unmountRootWithID('d');
ReactNoop.unmountRootWithID('e');
ReactNoop.unmountRootWithID('f');
expect(Scheduler).toFlushWithoutYielding();
await waitForAll([]);
expect(ReactNoop.getChildrenAsJSX('a')).toEqual(null);
expect(ReactNoop.getChildrenAsJSX('b')).toEqual(null);
expect(ReactNoop.getChildrenAsJSX('c')).toEqual(null);
@ -1182,7 +1161,7 @@ describe('ReactIncrementalErrorHandling', () => {
expect(ReactNoop.getChildrenAsJSX('f')).toEqual(null);
});
it('unwinds the context stack correctly on error', () => {
it('unwinds the context stack correctly on error', async () => {
class Provider extends React.Component {
static childContextTypes = {message: PropTypes.string};
static contextTypes = {message: PropTypes.string};
@ -1234,7 +1213,7 @@ describe('ReactIncrementalErrorHandling', () => {
<Connector />
</Provider>,
);
expect(Scheduler).toFlushWithoutYielding();
await waitForAll([]);
// If the context stack does not unwind, span will get 'abcde'
expect(ReactNoop).toMatchRenderedOutput(<span prop="a" />);
@ -1283,7 +1262,7 @@ describe('ReactIncrementalErrorHandling', () => {
);
});
it('catches reconciler errors in a boundary during update', () => {
it('catches reconciler errors in a boundary during update', async () => {
class ErrorBoundary extends React.Component {
state = {error: null};
componentDidCatch(error) {
@ -1307,7 +1286,7 @@ describe('ReactIncrementalErrorHandling', () => {
<BrokenRender fail={false} />
</ErrorBoundary>,
);
expect(Scheduler).toFlushWithoutYielding();
await waitForAll([]);
ReactNoop.render(
<ErrorBoundary>
@ -1334,7 +1313,7 @@ describe('ReactIncrementalErrorHandling', () => {
);
});
it('recovers from uncaught reconciler errors', () => {
it('recovers from uncaught reconciler errors', async () => {
const InvalidType = undefined;
expect(() => ReactNoop.render(<InvalidType />)).toErrorDev(
'Warning: React.createElement: type is invalid -- expected a string',
@ -1350,11 +1329,11 @@ describe('ReactIncrementalErrorHandling', () => {
);
ReactNoop.render(<span prop="hi" />);
expect(Scheduler).toFlushWithoutYielding();
await waitForAll([]);
expect(ReactNoop).toMatchRenderedOutput(<span prop="hi" />);
});
it('unmounts components with uncaught errors', () => {
it('unmounts components with uncaught errors', async () => {
let inst;
class BrokenRenderAndUnmount extends React.Component {
@ -1390,14 +1369,21 @@ describe('ReactIncrementalErrorHandling', () => {
</Parent>
</Parent>,
);
expect(Scheduler).toFlushWithoutYielding();
await waitForAll([]);
inst.setState({fail: true});
expect(() => {
expect(Scheduler).toFlushWithoutYielding();
}).toThrowError('Hello.');
expect(() => {
ReactNoop.flushSync(() => {
inst.setState({fail: true});
});
}).toThrow('Hello.');
expect(Scheduler).toHaveYielded([
// The unmount is queued in a microtask. In order to capture the error
// that occurs during unmount, we can flush it early with `flushSync`.
ReactNoop.flushSync();
}).toThrow('One does not simply unmount me.');
assertLog([
// Attempt to clean up.
// Errors in parents shouldn't stop children from unmounting.
'Parent componentWillUnmount [!]',
@ -1405,13 +1391,9 @@ describe('ReactIncrementalErrorHandling', () => {
'BrokenRenderAndUnmount componentWillUnmount',
]);
expect(ReactNoop).toMatchRenderedOutput(null);
expect(() => {
ReactNoop.flushSync();
}).toThrow('One does not simply unmount me.');
});
it('does not interrupt unmounting if detaching a ref throws', () => {
it('does not interrupt unmounting if detaching a ref throws', async () => {
class Bar extends React.Component {
componentWillUnmount() {
Scheduler.unstable_yieldValue('Bar unmount');
@ -1434,7 +1416,7 @@ describe('ReactIncrementalErrorHandling', () => {
}
ReactNoop.render(<Foo />);
expect(Scheduler).toFlushAndYield(['barRef attach']);
await waitForAll(['barRef attach']);
expect(ReactNoop).toMatchRenderedOutput(
<div>
<span prop="Bar" />
@ -1444,7 +1426,7 @@ describe('ReactIncrementalErrorHandling', () => {
// Unmount
ReactNoop.render(<Foo hide={true} />);
expect(Scheduler).toFlushAndThrow('Detach error');
expect(Scheduler).toHaveYielded([
assertLog([
'barRef detach',
// Bar should unmount even though its ref threw an error while detaching
'Bar unmount',
@ -1465,7 +1447,7 @@ describe('ReactIncrementalErrorHandling', () => {
expect(Scheduler).toFlushAndThrow('Error!');
});
it('error boundaries capture non-errors', () => {
it('error boundaries capture non-errors', async () => {
spyOnProd(console, 'error').mockImplementation(() => {});
spyOnDev(console, 'error').mockImplementation(() => {});
@ -1509,7 +1491,7 @@ describe('ReactIncrementalErrorHandling', () => {
</ErrorBoundary>,
);
expect(Scheduler).toFlushAndYield([
await waitForAll([
'ErrorBoundary (try)',
'Indirection',
'BadRender',
@ -1540,7 +1522,7 @@ describe('ReactIncrementalErrorHandling', () => {
// TODO: Error boundary does not catch promises
it('continues working on siblings of a component that throws', () => {
it('continues working on siblings of a component that throws', async () => {
class ErrorBoundary extends React.Component {
state = {error: null};
componentDidCatch(error) {
@ -1580,7 +1562,7 @@ describe('ReactIncrementalErrorHandling', () => {
</ErrorBoundary>,
);
expect(Scheduler).toFlushAndYield([
await waitForAll([
'ErrorBoundary (try)',
'throw',
// Continue rendering siblings after BadRender throws
@ -1603,7 +1585,7 @@ describe('ReactIncrementalErrorHandling', () => {
);
});
it('calls the correct lifecycles on the error boundary after catching an error (mixed)', () => {
it('calls the correct lifecycles on the error boundary after catching an error (mixed)', async () => {
// This test seems a bit contrived, but it's based on an actual regression
// where we checked for the existence of didUpdate instead of didMount, and
// didMount was not defined.
@ -1632,7 +1614,7 @@ describe('ReactIncrementalErrorHandling', () => {
}
ReactNoop.render(<Parent step={1} />);
expect(Scheduler).toFlushAndYieldThrough([
await waitFor([
'render',
'throw',
'render',
@ -1646,7 +1628,7 @@ describe('ReactIncrementalErrorHandling', () => {
);
});
it('provides component stack to the error boundary with componentDidCatch', () => {
it('provides component stack to the error boundary with componentDidCatch', async () => {
class ErrorBoundary extends React.Component {
state = {error: null, errorInfo: null};
componentDidCatch(error, errorInfo) {
@ -1676,7 +1658,7 @@ describe('ReactIncrementalErrorHandling', () => {
<BrokenRender />
</ErrorBoundary>,
);
expect(Scheduler).toFlushAndYield(['render error message']);
await waitForAll(['render error message']);
expect(ReactNoop).toMatchRenderedOutput(
<span
prop={
@ -1688,7 +1670,7 @@ describe('ReactIncrementalErrorHandling', () => {
);
});
it('does not provide component stack to the error boundary with getDerivedStateFromError', () => {
it('does not provide component stack to the error boundary with getDerivedStateFromError', async () => {
class ErrorBoundary extends React.Component {
state = {error: null};
static getDerivedStateFromError(error, errorInfo) {
@ -1712,13 +1694,13 @@ describe('ReactIncrementalErrorHandling', () => {
<BrokenRender />
</ErrorBoundary>,
);
expect(Scheduler).toFlushWithoutYielding();
await waitForAll([]);
expect(ReactNoop).toMatchRenderedOutput(
<span prop="Caught an error: Hello" />,
);
});
it('provides component stack even if overriding prepareStackTrace', () => {
it('provides component stack even if overriding prepareStackTrace', async () => {
Error.prepareStackTrace = function (error, callsites) {
const stack = ['An error occurred:', error.message];
for (let i = 0; i < callsites.length; i++) {
@ -1762,7 +1744,7 @@ describe('ReactIncrementalErrorHandling', () => {
<BrokenRender />
</ErrorBoundary>,
);
expect(Scheduler).toFlushAndYield(['render error message']);
await waitForAll(['render error message']);
Error.prepareStackTrace = undefined;
expect(ReactNoop).toMatchRenderedOutput(
@ -1822,7 +1804,7 @@ describe('ReactIncrementalErrorHandling', () => {
});
// Render past the component that throws, then yield.
expect(Scheduler).toFlushAndYieldThrough(['Oops']);
await waitFor(['Oops']);
expect(root).toMatchRenderedOutput(null);
// Interleaved update. When the root completes, instead of throwing the
// error, it should try rendering again. This update will cause it to
@ -1868,7 +1850,7 @@ describe('ReactIncrementalErrorHandling', () => {
// Render through just the default pri update. The low pri update remains on
// the queue.
expect(Scheduler).toFlushAndYieldThrough(['Everything is fine.']);
await waitFor(['Everything is fine.']);
// Schedule a discrete update on a child that triggers an error.
// The root should capture this error. But since there's still a pending
@ -1878,7 +1860,7 @@ describe('ReactIncrementalErrorHandling', () => {
});
});
// Should render the final state without throwing the error.
expect(Scheduler).toHaveYielded(['Everything is fine.']);
assertLog(['Everything is fine.']);
expect(root).toMatchRenderedOutput('Everything is fine.');
});

View File

@ -13,6 +13,7 @@
let React;
let ReactNoop;
let Scheduler;
let waitForAll;
describe('ReactIncrementalErrorLogging', () => {
beforeEach(() => {
@ -20,6 +21,9 @@ describe('ReactIncrementalErrorLogging', () => {
React = require('react');
ReactNoop = require('react-noop-renderer');
Scheduler = require('scheduler');
const InternalTestUtils = require('internal-test-utils');
waitForAll = InternalTestUtils.waitForAll;
});
// Note: in this test file we won't be using toErrorDev() matchers
@ -151,7 +155,7 @@ describe('ReactIncrementalErrorLogging', () => {
}).toThrow('logCapturedError error');
});
it('resets instance variables before unmounting failed node', () => {
it('resets instance variables before unmounting failed node', async () => {
class ErrorBoundary extends React.Component {
state = {error: null};
componentDidCatch(error) {
@ -185,7 +189,7 @@ describe('ReactIncrementalErrorLogging', () => {
<Foo />
</ErrorBoundary>,
);
expect(Scheduler).toFlushAndYield(
await waitForAll(
[
'render: 0',

View File

@ -13,6 +13,7 @@
let React;
let ReactNoop;
let Scheduler;
let waitForAll;
describe('ReactIncrementalErrorReplay', () => {
beforeEach(() => {
@ -20,6 +21,9 @@ describe('ReactIncrementalErrorReplay', () => {
React = require('react');
ReactNoop = require('react-noop-renderer');
Scheduler = require('scheduler');
const InternalTestUtils = require('internal-test-utils');
waitForAll = InternalTestUtils.waitForAll;
});
it('should fail gracefully on error in the host environment', () => {
@ -27,7 +31,7 @@ describe('ReactIncrementalErrorReplay', () => {
expect(Scheduler).toFlushAndThrow('Error in host config.');
});
it("should ignore error if it doesn't throw on retry", () => {
it("should ignore error if it doesn't throw on retry", async () => {
let didInit = false;
function badLazyInit() {
@ -45,6 +49,6 @@ describe('ReactIncrementalErrorReplay', () => {
}
}
ReactNoop.render(<App />);
expect(Scheduler).toFlushWithoutYielding();
await waitForAll([]);
});
});

View File

@ -13,6 +13,8 @@
let React;
let ReactNoop;
let Scheduler;
let waitFor;
let waitForAll;
describe('ReactIncrementalReflection', () => {
beforeEach(() => {
@ -21,6 +23,10 @@ describe('ReactIncrementalReflection', () => {
React = require('react');
ReactNoop = require('react-noop-renderer');
Scheduler = require('scheduler');
const InternalTestUtils = require('internal-test-utils');
waitFor = InternalTestUtils.waitFor;
waitForAll = InternalTestUtils.waitForAll;
});
function div(...children) {
@ -34,7 +40,7 @@ describe('ReactIncrementalReflection', () => {
return {type: 'span', children: [], prop, hidden: false};
}
it('handles isMounted even when the initial render is deferred', () => {
it('handles isMounted even when the initial render is deferred', async () => {
const instances = [];
class Component extends React.Component {
@ -68,17 +74,17 @@ describe('ReactIncrementalReflection', () => {
});
// Render part way through but don't yet commit the updates.
expect(Scheduler).toFlushAndYieldThrough(['componentWillMount: false']);
await waitFor(['componentWillMount: false']);
expect(instances[0]._isMounted()).toBe(false);
// Render the rest and commit the updates.
expect(Scheduler).toFlushAndYield(['componentDidMount: true']);
await waitForAll(['componentDidMount: true']);
expect(instances[0]._isMounted()).toBe(true);
});
it('handles isMounted when an unmount is deferred', () => {
it('handles isMounted when an unmount is deferred', async () => {
const instances = [];
class Component extends React.Component {
@ -109,7 +115,7 @@ describe('ReactIncrementalReflection', () => {
}
ReactNoop.render(<Foo mount={true} />);
expect(Scheduler).toFlushAndYield(['Component']);
await waitForAll(['Component']);
expect(instances[0]._isMounted()).toBe(true);
@ -118,17 +124,17 @@ describe('ReactIncrementalReflection', () => {
});
// Render part way through but don't yet commit the updates so it is not
// fully unmounted yet.
expect(Scheduler).toFlushAndYieldThrough(['Other']);
await waitFor(['Other']);
expect(instances[0]._isMounted()).toBe(true);
// Finish flushing the unmount.
expect(Scheduler).toFlushAndYield(['componentWillUnmount: true']);
await waitForAll(['componentWillUnmount: true']);
expect(instances[0]._isMounted()).toBe(false);
});
it('finds no node before insertion and correct node before deletion', () => {
it('finds no node before insertion and correct node before deletion', async () => {
let classInstance = null;
function findInstance(inst) {
@ -202,18 +208,14 @@ describe('ReactIncrementalReflection', () => {
ReactNoop.render(<Foo step={0} />);
});
// Flush past Component but don't complete rendering everything yet.
expect(Scheduler).toFlushAndYieldThrough([
['componentWillMount', null],
'render',
'render sibling',
]);
await waitFor([['componentWillMount', null], 'render', 'render sibling']);
expect(classInstance).toBeDefined();
// The instance has been complete but is still not committed so it should
// not find any host nodes in it.
expect(findInstance(classInstance)).toBe(null);
expect(Scheduler).toFlushAndYield([['componentDidMount', span()]]);
await waitForAll([['componentDidMount', span()]]);
const hostSpan = classInstance.span;
expect(hostSpan).toBeDefined();
@ -223,7 +225,7 @@ describe('ReactIncrementalReflection', () => {
// Flush next step which will cause an update but not yet render a new host
// node.
ReactNoop.render(<Foo step={1} />);
expect(Scheduler).toFlushAndYield([
await waitForAll([
['componentWillUpdate', hostSpan],
'render',
'render sibling',
@ -237,7 +239,7 @@ describe('ReactIncrementalReflection', () => {
React.startTransition(() => {
ReactNoop.render(<Foo step={2} />);
});
expect(Scheduler).toFlushAndYieldThrough([
await waitFor([
['componentWillUpdate', hostSpan],
'render',
'render sibling',
@ -247,7 +249,7 @@ describe('ReactIncrementalReflection', () => {
expect(ReactNoop.findInstance(classInstance)).toBe(hostSpan);
// When we finally flush the tree it will get committed.
expect(Scheduler).toFlushAndYield([['componentDidUpdate', div()]]);
await waitForAll([['componentDidUpdate', div()]]);
const hostDiv = classInstance.div;
expect(hostDiv).toBeDefined();
@ -260,7 +262,7 @@ describe('ReactIncrementalReflection', () => {
React.startTransition(() => {
ReactNoop.render(<Foo step={3} />);
});
expect(Scheduler).toFlushAndYieldThrough([
await waitFor([
['componentWillUpdate', hostDiv],
'render',
'render sibling',
@ -269,14 +271,14 @@ describe('ReactIncrementalReflection', () => {
// This should still be the host div since the deletion is not committed.
expect(ReactNoop.findInstance(classInstance)).toBe(hostDiv);
expect(Scheduler).toFlushAndYield([['componentDidUpdate', null]]);
await waitForAll([['componentDidUpdate', null]]);
// This should still be the host div since the deletion is not committed.
expect(ReactNoop.findInstance(classInstance)).toBe(null);
// Render a div again
ReactNoop.render(<Foo step={4} />);
expect(Scheduler).toFlushAndYield([
await waitForAll([
['componentWillUpdate', null],
'render',
'render sibling',
@ -285,6 +287,6 @@ describe('ReactIncrementalReflection', () => {
// Unmount the component.
ReactNoop.render([]);
expect(Scheduler).toFlushAndYield([['componentWillUnmount', hostDiv]]);
await waitForAll([['componentWillUnmount', hostDiv]]);
});
});

View File

@ -14,6 +14,10 @@ let React;
let ReactNoop;
let Scheduler;
let act;
let waitForAll;
let waitFor;
let assertLog;
let waitForPaint;
describe('ReactIncrementalScheduling', () => {
beforeEach(() => {
@ -23,32 +27,38 @@ describe('ReactIncrementalScheduling', () => {
ReactNoop = require('react-noop-renderer');
Scheduler = require('scheduler');
act = require('jest-react').act;
const InternalTestUtils = require('internal-test-utils');
waitForAll = InternalTestUtils.waitForAll;
waitFor = InternalTestUtils.waitFor;
assertLog = InternalTestUtils.assertLog;
waitForPaint = InternalTestUtils.waitForPaint;
});
it('schedules and flushes deferred work', () => {
it('schedules and flushes deferred work', async () => {
ReactNoop.render(<span prop="1" />);
expect(ReactNoop).toMatchRenderedOutput(null);
expect(Scheduler).toFlushWithoutYielding();
await waitForAll([]);
expect(ReactNoop).toMatchRenderedOutput(<span prop="1" />);
});
it('searches for work on other roots once the current root completes', () => {
it('searches for work on other roots once the current root completes', async () => {
ReactNoop.renderToRootWithID(<span prop="a:1" />, 'a');
ReactNoop.renderToRootWithID(<span prop="b:1" />, 'b');
ReactNoop.renderToRootWithID(<span prop="c:1" />, 'c');
expect(Scheduler).toFlushWithoutYielding();
await waitForAll([]);
expect(ReactNoop.getChildrenAsJSX('a')).toEqual(<span prop="a:1" />);
expect(ReactNoop.getChildrenAsJSX('b')).toEqual(<span prop="b:1" />);
expect(ReactNoop.getChildrenAsJSX('c')).toEqual(<span prop="c:1" />);
});
it('schedules top-level updates in order of priority', () => {
it('schedules top-level updates in order of priority', async () => {
// Initial render.
ReactNoop.render(<span prop={1} />);
expect(Scheduler).toFlushWithoutYielding();
await waitForAll([]);
expect(ReactNoop).toMatchRenderedOutput(<span prop={1} />);
ReactNoop.batchedUpdates(() => {
@ -64,14 +74,14 @@ describe('ReactIncrementalScheduling', () => {
// The terminal value should be the last update that was scheduled,
// regardless of priority. In this case, that's the last sync update.
expect(Scheduler).toFlushWithoutYielding();
await waitForAll([]);
expect(ReactNoop).toMatchRenderedOutput(<span prop={4} />);
});
it('schedules top-level updates with same priority in order of insertion', () => {
it('schedules top-level updates with same priority in order of insertion', async () => {
// Initial render.
ReactNoop.render(<span prop={1} />);
expect(Scheduler).toFlushWithoutYielding();
await waitForAll([]);
expect(ReactNoop).toMatchRenderedOutput(<span prop={1} />);
ReactNoop.render(<span prop={2} />);
@ -79,7 +89,7 @@ describe('ReactIncrementalScheduling', () => {
ReactNoop.render(<span prop={4} />);
ReactNoop.render(<span prop={5} />);
expect(Scheduler).toFlushWithoutYielding();
await waitForAll([]);
expect(ReactNoop).toMatchRenderedOutput(<span prop={5} />);
});
@ -97,20 +107,20 @@ describe('ReactIncrementalScheduling', () => {
ReactNoop.renderToRootWithID(<Text text="b:1" />, 'b');
ReactNoop.renderToRootWithID(<Text text="c:1" />, 'c');
});
expect(Scheduler).toHaveYielded(['a:1', 'b:1', 'c:1']);
assertLog(['a:1', 'b:1', 'c:1']);
expect(ReactNoop.getChildrenAsJSX('a')).toEqual('a:1');
expect(ReactNoop.getChildrenAsJSX('b')).toEqual('b:1');
expect(ReactNoop.getChildrenAsJSX('c')).toEqual('c:1');
// Schedule deferred work in the reverse order
act(() => {
act(async () => {
React.startTransition(() => {
ReactNoop.renderToRootWithID(<Text text="c:2" />, 'c');
ReactNoop.renderToRootWithID(<Text text="b:2" />, 'b');
});
// Ensure it starts in the order it was scheduled
expect(Scheduler).toFlushAndYieldThrough(['c:2']);
await waitFor(['c:2']);
expect(ReactNoop.getChildrenAsJSX('a')).toEqual('a:1');
expect(ReactNoop.getChildrenAsJSX('b')).toEqual('b:1');
@ -122,19 +132,19 @@ describe('ReactIncrementalScheduling', () => {
});
// Keep performing work in the order it was scheduled
expect(Scheduler).toFlushAndYieldThrough(['b:2']);
await waitFor(['b:2']);
expect(ReactNoop.getChildrenAsJSX('a')).toEqual('a:1');
expect(ReactNoop.getChildrenAsJSX('b')).toEqual('b:2');
expect(ReactNoop.getChildrenAsJSX('c')).toEqual('c:2');
expect(Scheduler).toFlushAndYieldThrough(['a:2']);
await waitFor(['a:2']);
expect(ReactNoop.getChildrenAsJSX('a')).toEqual('a:2');
expect(ReactNoop.getChildrenAsJSX('b')).toEqual('b:2');
expect(ReactNoop.getChildrenAsJSX('c')).toEqual('c:2');
});
});
it('schedules sync updates when inside componentDidMount/Update', () => {
it('schedules sync updates when inside componentDidMount/Update', async () => {
let instance;
class Foo extends React.Component {
@ -176,7 +186,7 @@ describe('ReactIncrementalScheduling', () => {
ReactNoop.render(<Foo />);
});
// Render without committing
expect(Scheduler).toFlushAndYieldThrough(['render: 0']);
await waitFor(['render: 0']);
// Do one more unit of work to commit
expect(ReactNoop.flushNextYield()).toEqual([
@ -191,7 +201,7 @@ describe('ReactIncrementalScheduling', () => {
React.startTransition(() => {
instance.setState({tick: 2});
});
expect(Scheduler).toFlushAndYieldThrough(['render: 2']);
await waitFor(['render: 2']);
expect(ReactNoop.flushNextYield()).toEqual([
'componentDidUpdate: 2',
'componentDidUpdate (before setState): 2',
@ -203,7 +213,7 @@ describe('ReactIncrementalScheduling', () => {
]);
});
it('can opt-in to async scheduling inside componentDidMount/Update', () => {
it('can opt-in to async scheduling inside componentDidMount/Update', async () => {
let instance;
class Foo extends React.Component {
state = {tick: 0};
@ -248,7 +258,7 @@ describe('ReactIncrementalScheduling', () => {
ReactNoop.render(<Foo />);
});
// The cDM update should not have flushed yet because it has async priority.
expect(Scheduler).toHaveYielded([
assertLog([
'render: 0',
'componentDidMount (before setState): 0',
'componentDidMount (after setState): 0',
@ -256,14 +266,14 @@ describe('ReactIncrementalScheduling', () => {
expect(ReactNoop).toMatchRenderedOutput(<span prop={0} />);
// Now flush the cDM update.
expect(Scheduler).toFlushAndYield(['render: 1', 'componentDidUpdate: 1']);
await waitForAll(['render: 1', 'componentDidUpdate: 1']);
expect(ReactNoop).toMatchRenderedOutput(<span prop={1} />);
React.startTransition(() => {
instance.setState({tick: 2});
});
expect(Scheduler).toFlushUntilNextPaint([
await waitForPaint([
'render: 2',
'componentDidUpdate: 2',
'componentDidUpdate (before setState): 2',
@ -272,11 +282,11 @@ describe('ReactIncrementalScheduling', () => {
expect(ReactNoop).toMatchRenderedOutput(<span prop={2} />);
// Now flush the cDU update.
expect(Scheduler).toFlushAndYield(['render: 3', 'componentDidUpdate: 3']);
await waitForAll(['render: 3', 'componentDidUpdate: 3']);
expect(ReactNoop).toMatchRenderedOutput(<span prop={3} />);
});
it('performs Task work even after time runs out', () => {
it('performs Task work even after time runs out', async () => {
class Foo extends React.Component {
state = {step: 1};
componentDidMount() {
@ -299,7 +309,7 @@ describe('ReactIncrementalScheduling', () => {
// This should be just enough to complete all the work, but not enough to
// commit it.
expect(Scheduler).toFlushAndYieldThrough(['Foo']);
await waitFor(['Foo']);
expect(ReactNoop).toMatchRenderedOutput(null);
// Do one more unit of work.

View File

@ -13,6 +13,8 @@
let React;
let ReactNoop;
let Scheduler;
let waitForAll;
let waitFor;
describe('ReactIncrementalSideEffects', () => {
beforeEach(() => {
@ -21,6 +23,10 @@ describe('ReactIncrementalSideEffects', () => {
React = require('react');
ReactNoop = require('react-noop-renderer');
Scheduler = require('scheduler');
const InternalTestUtils = require('internal-test-utils');
waitForAll = InternalTestUtils.waitForAll;
waitFor = InternalTestUtils.waitFor;
});
// Note: This is based on a similar component we use in www. We can delete
@ -36,7 +42,7 @@ describe('ReactIncrementalSideEffects', () => {
);
}
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>;
}
@ -51,7 +57,7 @@ describe('ReactIncrementalSideEffects', () => {
}
ReactNoop.render(<Foo text="Hello" />);
expect(Scheduler).toFlushWithoutYielding();
await waitForAll([]);
expect(ReactNoop).toMatchRenderedOutput(
<div>
<span>Hello</span>
@ -59,7 +65,7 @@ describe('ReactIncrementalSideEffects', () => {
);
ReactNoop.render(<Foo text="World" />);
expect(Scheduler).toFlushWithoutYielding();
await waitForAll([]);
expect(ReactNoop).toMatchRenderedOutput(
<div>
<span>World</span>
@ -68,7 +74,7 @@ describe('ReactIncrementalSideEffects', () => {
);
});
it('can update child nodes of a fragment', function () {
it('can update child nodes of a fragment', async function () {
function Bar(props) {
return <span>{props.text}</span>;
}
@ -88,7 +94,7 @@ describe('ReactIncrementalSideEffects', () => {
}
ReactNoop.render(<Foo text="Hello" />);
expect(Scheduler).toFlushWithoutYielding();
await waitForAll([]);
expect(ReactNoop).toMatchRenderedOutput(
<div>
<span>Hello</span>
@ -97,7 +103,7 @@ describe('ReactIncrementalSideEffects', () => {
);
ReactNoop.render(<Foo text="World" />);
expect(Scheduler).toFlushWithoutYielding();
await waitForAll([]);
expect(ReactNoop).toMatchRenderedOutput(
<div>
<span>World</span>
@ -108,7 +114,7 @@ describe('ReactIncrementalSideEffects', () => {
);
ReactNoop.render(<Foo text="Hi" />);
expect(Scheduler).toFlushWithoutYielding();
await waitForAll([]);
expect(ReactNoop).toMatchRenderedOutput(
<div>
<span>Hi</span>
@ -119,7 +125,7 @@ describe('ReactIncrementalSideEffects', () => {
);
});
it('can update child nodes rendering into text nodes', function () {
it('can update child nodes rendering into text nodes', async function () {
function Bar(props) {
return props.text;
}
@ -136,15 +142,15 @@ describe('ReactIncrementalSideEffects', () => {
}
ReactNoop.render(<Foo text="Hello" />);
expect(Scheduler).toFlushWithoutYielding();
await waitForAll([]);
expect(ReactNoop).toMatchRenderedOutput(<div>Hello</div>);
ReactNoop.render(<Foo text="World" />);
expect(Scheduler).toFlushWithoutYielding();
await waitForAll([]);
expect(ReactNoop).toMatchRenderedOutput(<div>WorldWorld!</div>);
});
it('can deletes children either components, host or text', function () {
it('can deletes children either components, host or text', async function () {
function Bar(props) {
return <span prop={props.children} />;
}
@ -160,7 +166,7 @@ describe('ReactIncrementalSideEffects', () => {
}
ReactNoop.render(<Foo show={true} />);
expect(Scheduler).toFlushWithoutYielding();
await waitForAll([]);
expect(ReactNoop).toMatchRenderedOutput(
<div>
<div />
@ -170,11 +176,11 @@ describe('ReactIncrementalSideEffects', () => {
);
ReactNoop.render(<Foo show={false} />);
expect(Scheduler).toFlushWithoutYielding();
await waitForAll([]);
expect(ReactNoop).toMatchRenderedOutput(<div />);
});
it('can delete a child that changes type - implicit keys', function () {
it('can delete a child that changes type - implicit keys', async function () {
let unmounted = false;
class ClassComponent extends React.Component {
@ -206,7 +212,7 @@ describe('ReactIncrementalSideEffects', () => {
}
ReactNoop.render(<Foo useClass={true} />);
expect(Scheduler).toFlushWithoutYielding();
await waitForAll([]);
expect(ReactNoop).toMatchRenderedOutput(
<div>
<span prop="Class" />
@ -217,7 +223,7 @@ describe('ReactIncrementalSideEffects', () => {
expect(unmounted).toBe(false);
ReactNoop.render(<Foo useFunction={true} />);
expect(Scheduler).toFlushWithoutYielding();
await waitForAll([]);
expect(ReactNoop).toMatchRenderedOutput(
<div>
<span prop="Function" />
@ -228,15 +234,15 @@ describe('ReactIncrementalSideEffects', () => {
expect(unmounted).toBe(true);
ReactNoop.render(<Foo useText={true} />);
expect(Scheduler).toFlushWithoutYielding();
await waitForAll([]);
expect(ReactNoop).toMatchRenderedOutput(<div>TextTrail</div>);
ReactNoop.render(<Foo />);
expect(Scheduler).toFlushWithoutYielding();
await waitForAll([]);
expect(ReactNoop).toMatchRenderedOutput(<div>Trail</div>);
});
it('can delete a child that changes type - explicit keys', function () {
it('can delete a child that changes type - explicit keys', async function () {
let unmounted = false;
class ClassComponent extends React.Component {
@ -266,7 +272,7 @@ describe('ReactIncrementalSideEffects', () => {
}
ReactNoop.render(<Foo useClass={true} />);
expect(Scheduler).toFlushWithoutYielding();
await waitForAll([]);
expect(ReactNoop).toMatchRenderedOutput(
<div>
<span prop="Class" />
@ -277,7 +283,7 @@ describe('ReactIncrementalSideEffects', () => {
expect(unmounted).toBe(false);
ReactNoop.render(<Foo useFunction={true} />);
expect(Scheduler).toFlushWithoutYielding();
await waitForAll([]);
expect(ReactNoop).toMatchRenderedOutput(
<div>
<span prop="Function" />
@ -288,11 +294,11 @@ describe('ReactIncrementalSideEffects', () => {
expect(unmounted).toBe(true);
ReactNoop.render(<Foo />);
expect(Scheduler).toFlushWithoutYielding();
await waitForAll([]);
expect(ReactNoop).toMatchRenderedOutput(<div>Trail</div>);
});
it('can delete a child when it unmounts inside a portal', () => {
it('can delete a child when it unmounts inside a portal', async () => {
function Bar(props) {
return <span prop={props.children} />;
}
@ -312,7 +318,7 @@ describe('ReactIncrementalSideEffects', () => {
<Foo show={true} />
</div>,
);
expect(Scheduler).toFlushWithoutYielding();
await waitForAll([]);
expect(ReactNoop).toMatchRenderedOutput(<div />);
expect(ReactNoop.getChildrenAsJSX('portalContainer')).toEqual(
<>
@ -327,7 +333,7 @@ describe('ReactIncrementalSideEffects', () => {
<Foo show={false} />
</div>,
);
expect(Scheduler).toFlushWithoutYielding();
await waitForAll([]);
expect(ReactNoop).toMatchRenderedOutput(<div />);
expect(ReactNoop.getChildrenAsJSX('portalContainer')).toEqual(null);
@ -336,7 +342,7 @@ describe('ReactIncrementalSideEffects', () => {
<Foo show={true} />
</div>,
);
expect(Scheduler).toFlushWithoutYielding();
await waitForAll([]);
expect(ReactNoop).toMatchRenderedOutput(<div />);
expect(ReactNoop.getChildrenAsJSX('portalContainer')).toEqual(
<>
@ -347,17 +353,17 @@ describe('ReactIncrementalSideEffects', () => {
);
ReactNoop.render(null);
expect(Scheduler).toFlushWithoutYielding();
await waitForAll([]);
expect(ReactNoop).toMatchRenderedOutput(null);
expect(ReactNoop.getChildrenAsJSX('portalContainer')).toEqual(null);
ReactNoop.render(<Foo show={false} />);
expect(Scheduler).toFlushWithoutYielding();
await waitForAll([]);
expect(ReactNoop).toMatchRenderedOutput(null);
expect(ReactNoop.getChildrenAsJSX('portalContainer')).toEqual(null);
ReactNoop.render(<Foo show={true} />);
expect(Scheduler).toFlushWithoutYielding();
await waitForAll([]);
expect(ReactNoop).toMatchRenderedOutput(null);
expect(ReactNoop.getChildrenAsJSX('portalContainer')).toEqual(
<>
@ -368,12 +374,12 @@ describe('ReactIncrementalSideEffects', () => {
);
ReactNoop.render(null);
expect(Scheduler).toFlushWithoutYielding();
await waitForAll([]);
expect(ReactNoop).toMatchRenderedOutput(null);
expect(ReactNoop.getChildrenAsJSX('portalContainer')).toEqual(null);
});
it('can delete a child when it unmounts with a portal', () => {
it('can delete a child when it unmounts with a portal', async () => {
function Bar(props) {
return <span prop={props.children} />;
}
@ -393,7 +399,7 @@ describe('ReactIncrementalSideEffects', () => {
<Foo />
</div>,
);
expect(Scheduler).toFlushWithoutYielding();
await waitForAll([]);
expect(ReactNoop).toMatchRenderedOutput(<div />);
expect(ReactNoop.getChildrenAsJSX('portalContainer')).toEqual(
<>
@ -404,12 +410,12 @@ describe('ReactIncrementalSideEffects', () => {
);
ReactNoop.render(null);
expect(Scheduler).toFlushWithoutYielding();
await waitForAll([]);
expect(ReactNoop).toMatchRenderedOutput(null);
expect(ReactNoop.getChildrenAsJSX('portalContainer')).toEqual(null);
ReactNoop.render(<Foo />);
expect(Scheduler).toFlushWithoutYielding();
await waitForAll([]);
expect(ReactNoop).toMatchRenderedOutput(null);
expect(ReactNoop.getChildrenAsJSX('portalContainer')).toEqual(
<>
@ -420,12 +426,12 @@ describe('ReactIncrementalSideEffects', () => {
);
ReactNoop.render(null);
expect(Scheduler).toFlushWithoutYielding();
await waitForAll([]);
expect(ReactNoop).toMatchRenderedOutput(null);
expect(ReactNoop.getChildrenAsJSX('portalContainer')).toEqual(null);
});
it('does not update child nodes if a flush is aborted', () => {
it('does not update child nodes if a flush is aborted', async () => {
function Bar(props) {
Scheduler.unstable_yieldValue('Bar');
return <span prop={props.text} />;
@ -445,7 +451,7 @@ describe('ReactIncrementalSideEffects', () => {
}
ReactNoop.render(<Foo text="Hello" />);
expect(Scheduler).toFlushAndYield(['Foo', 'Bar', 'Bar', 'Bar']);
await waitForAll(['Foo', 'Bar', 'Bar', 'Bar']);
expect(ReactNoop).toMatchRenderedOutput(
<div>
<div>
@ -461,7 +467,7 @@ describe('ReactIncrementalSideEffects', () => {
});
// Flush some of the work without committing
expect(Scheduler).toFlushAndYieldThrough(['Foo', 'Bar']);
await waitFor(['Foo', 'Bar']);
expect(ReactNoop).toMatchRenderedOutput(
<div>
<div>
@ -474,7 +480,7 @@ describe('ReactIncrementalSideEffects', () => {
});
// @gate www
it('preserves a previously rendered node when deprioritized', () => {
it('preserves a previously rendered node when deprioritized', async () => {
function Middle(props) {
Scheduler.unstable_yieldValue('Middle');
return <span prop={props.children} />;
@ -492,7 +498,7 @@ describe('ReactIncrementalSideEffects', () => {
}
ReactNoop.render(<Foo text="foo" />);
expect(Scheduler).toFlushAndYield(['Foo', 'Middle']);
await waitForAll(['Foo', 'Middle']);
expect(ReactNoop.getChildrenAsJSX()).toEqual(
<div>
@ -505,7 +511,7 @@ describe('ReactIncrementalSideEffects', () => {
ReactNoop.render(<Foo text="bar" />, () =>
Scheduler.unstable_yieldValue('commit'),
);
expect(Scheduler).toFlushAndYieldThrough(['Foo', 'commit']);
await waitFor(['Foo', 'commit']);
expect(ReactNoop.getChildrenAsJSX()).toEqual(
<div>
<div hidden={true}>
@ -514,7 +520,7 @@ describe('ReactIncrementalSideEffects', () => {
</div>,
);
expect(Scheduler).toFlushAndYield(['Middle']);
await waitForAll(['Middle']);
expect(ReactNoop.getChildrenAsJSX()).toEqual(
<div>
<div hidden={true}>
@ -525,7 +531,7 @@ describe('ReactIncrementalSideEffects', () => {
});
// @gate www
it('can reuse side-effects after being preempted', () => {
it('can reuse side-effects after being preempted', async () => {
function Bar(props) {
Scheduler.unstable_yieldValue('Bar');
return <span prop={props.children} />;
@ -556,7 +562,7 @@ describe('ReactIncrementalSideEffects', () => {
// Init
ReactNoop.render(<Foo text="foo" step={0} />);
expect(Scheduler).toFlushAndYield(['Foo', 'Bar', 'Bar']);
await waitForAll(['Foo', 'Bar', 'Bar']);
expect(ReactNoop.getChildrenAsJSX()).toEqual(
<div hidden={true}>
@ -572,7 +578,7 @@ describe('ReactIncrementalSideEffects', () => {
ReactNoop.render(<Foo text="bar" step={1} />, () =>
Scheduler.unstable_yieldValue('commit'),
);
expect(Scheduler).toFlushAndYieldThrough(['Foo', 'commit', 'Bar']);
await waitFor(['Foo', 'commit', 'Bar']);
// The tree remains unchanged.
expect(ReactNoop.getChildrenAsJSX()).toEqual(
@ -588,7 +594,7 @@ describe('ReactIncrementalSideEffects', () => {
// render some higher priority work. The middle content will bailout so
// it remains untouched which means that it should reuse it next time.
ReactNoop.render(<Foo text="foo" step={1} />);
expect(Scheduler).toFlushAndYield(['Foo', 'Bar', 'Bar']);
await waitForAll(['Foo', 'Bar', 'Bar']);
// Since we did nothing to the middle subtree during the interruption,
// we should be able to reuse the reconciliation work that we already did
@ -605,7 +611,7 @@ describe('ReactIncrementalSideEffects', () => {
});
// @gate www
it('can reuse side-effects after being preempted, if shouldComponentUpdate is false', () => {
it('can reuse side-effects after being preempted, if shouldComponentUpdate is false', async () => {
class Bar extends React.Component {
shouldComponentUpdate(nextProps) {
return this.props.children !== nextProps.children;
@ -642,7 +648,7 @@ describe('ReactIncrementalSideEffects', () => {
// Init
ReactNoop.render(<Foo text="foo" step={0} />);
expect(Scheduler).toFlushAndYield(['Foo', 'Content', 'Bar', 'Bar']);
await waitForAll(['Foo', 'Content', 'Bar', 'Bar']);
expect(ReactNoop.getChildrenAsJSX()).toEqual(
<div hidden={true}>
@ -656,7 +662,7 @@ describe('ReactIncrementalSideEffects', () => {
// Make a quick update which will schedule low priority work to
// update the middle content.
ReactNoop.render(<Foo text="bar" step={1} />);
expect(Scheduler).toFlushAndYieldThrough(['Foo', 'Content', 'Bar']);
await waitFor(['Foo', 'Content', 'Bar']);
// The tree remains unchanged.
expect(ReactNoop.getChildrenAsJSX()).toEqual(
@ -672,7 +678,7 @@ describe('ReactIncrementalSideEffects', () => {
// render some higher priority work. The middle content will bailout so
// it remains untouched which means that it should reuse it next time.
ReactNoop.render(<Foo text="foo" step={1} />);
expect(Scheduler).toFlushAndYield(['Foo', 'Content', 'Bar', 'Bar']);
await waitForAll(['Foo', 'Content', 'Bar', 'Bar']);
// Since we did nothing to the middle subtree during the interruption,
// we should be able to reuse the reconciliation work that we already did
@ -688,7 +694,7 @@ describe('ReactIncrementalSideEffects', () => {
);
});
it('can update a completed tree before it has a chance to commit', () => {
it('can update a completed tree before it has a chance to commit', async () => {
function Foo(props) {
Scheduler.unstable_yieldValue('Foo');
return <span prop={props.step} />;
@ -697,7 +703,7 @@ describe('ReactIncrementalSideEffects', () => {
ReactNoop.render(<Foo step={1} />);
});
// This should be just enough to complete the tree without committing it
expect(Scheduler).toFlushAndYieldThrough(['Foo']);
await waitFor(['Foo']);
expect(ReactNoop.getChildrenAsJSX()).toEqual(null);
// To confirm, perform one more unit of work. The tree should now
// be flushed.
@ -708,7 +714,7 @@ describe('ReactIncrementalSideEffects', () => {
ReactNoop.render(<Foo step={2} />);
});
// This should be just enough to complete the tree without committing it
expect(Scheduler).toFlushAndYieldThrough(['Foo']);
await waitFor(['Foo']);
expect(ReactNoop.getChildrenAsJSX()).toEqual(<span prop={1} />);
// This time, before we commit the tree, we update the root component with
// new props
@ -723,12 +729,12 @@ describe('ReactIncrementalSideEffects', () => {
expect(ReactNoop.getChildrenAsJSX()).toEqual(<span prop={2} />);
// If we flush the rest of the work, we should get another commit that
// renders 3. If it renders 2 again, that means an update was dropped.
expect(Scheduler).toFlushWithoutYielding();
await waitForAll([]);
expect(ReactNoop.getChildrenAsJSX()).toEqual(<span prop={3} />);
});
// @gate www
it('updates a child even though the old props is empty', () => {
it('updates a child even though the old props is empty', async () => {
function Foo(props) {
return (
<LegacyHiddenDiv mode="hidden">
@ -738,7 +744,7 @@ describe('ReactIncrementalSideEffects', () => {
}
ReactNoop.render(<Foo />);
expect(Scheduler).toFlushWithoutYielding();
await waitForAll([]);
expect(ReactNoop.getChildrenAsJSX()).toEqual(
<div hidden={true}>
<span prop={1} />
@ -746,7 +752,7 @@ describe('ReactIncrementalSideEffects', () => {
);
});
xit('can defer side-effects and resume them later on', () => {
xit('can defer side-effects and resume them later on', async () => {
class Bar extends React.Component {
shouldComponentUpdate(nextProps) {
return this.props.idx !== nextProps.idx;
@ -809,7 +815,7 @@ describe('ReactIncrementalSideEffects', () => {
</div>,
);
ReactNoop.render(<Foo tick={3} idx={1} />);
expect(Scheduler).toFlushWithoutYielding();
await waitForAll([]);
expect(ReactNoop).toMatchRenderedOutput(
<div>
<span prop={3} />
@ -829,7 +835,7 @@ describe('ReactIncrementalSideEffects', () => {
expect(innerSpanA).toBe(innerSpanB);
});
xit('can defer side-effects and reuse them later - complex', function () {
xit('can defer side-effects and reuse them later - complex', async function () {
let ops = [];
class Bar extends React.Component {
@ -892,7 +898,7 @@ describe('ReactIncrementalSideEffects', () => {
expect(ops).toEqual(['Foo']);
ops = [];
expect(Scheduler).toFlushWithoutYielding();
await waitForAll([]);
expect(ReactNoop).toMatchRenderedOutput([
<div>
<span prop={1} />,
@ -959,7 +965,7 @@ describe('ReactIncrementalSideEffects', () => {
// We should now be able to reuse some of the work we've already done
// and replay those side-effects.
expect(Scheduler).toFlushWithoutYielding();
await waitForAll([]);
expect(ReactNoop).toMatchRenderedOutput([
<div>
<span prop={3} />,
@ -979,7 +985,7 @@ describe('ReactIncrementalSideEffects', () => {
});
// @gate www
it('deprioritizes setStates that happens within a deprioritized tree', () => {
it('deprioritizes setStates that happens within a deprioritized tree', async () => {
const barInstances = [];
class Bar extends React.Component {
@ -1010,7 +1016,7 @@ describe('ReactIncrementalSideEffects', () => {
);
}
ReactNoop.render(<Foo tick={0} idx={0} />);
expect(Scheduler).toFlushAndYield(['Foo', 'Bar', 'Bar', 'Bar']);
await waitForAll(['Foo', 'Bar', 'Bar', 'Bar']);
expect(ReactNoop.getChildrenAsJSX()).toEqual(
<div>
<span prop={0} />
@ -1023,7 +1029,7 @@ describe('ReactIncrementalSideEffects', () => {
);
ReactNoop.render(<Foo tick={1} idx={1} />);
expect(Scheduler).toFlushAndYieldThrough(['Foo', 'Bar', 'Bar']);
await waitFor(['Foo', 'Bar', 'Bar']);
expect(ReactNoop.getChildrenAsJSX()).toEqual(
<div>
{/* Updated */}
@ -1041,7 +1047,7 @@ describe('ReactIncrementalSideEffects', () => {
// This should not be enough time to render the content of all the hidden
// items. Including the set state since that is deprioritized.
// ReactNoop.flushDeferredPri(35);
expect(Scheduler).toFlushAndYieldThrough(['Bar']);
await waitFor(['Bar']);
expect(ReactNoop.getChildrenAsJSX()).toEqual(
<div>
{/* Updated */}
@ -1057,7 +1063,7 @@ describe('ReactIncrementalSideEffects', () => {
// However, once we render fully, we will have enough time to finish it all
// at once.
expect(Scheduler).toFlushAndYield(['Bar', 'Bar']);
await waitForAll(['Bar', 'Bar']);
expect(ReactNoop.getChildrenAsJSX()).toEqual(
<div>
<span prop={1} />
@ -1074,7 +1080,7 @@ describe('ReactIncrementalSideEffects', () => {
// moves to "current" without flushing due to having lower priority. Does this
// even happen? Maybe a child doesn't get processed because it is lower prio?
it('calls callback after update is flushed', () => {
it('calls callback after update is flushed', async () => {
let instance;
class Foo extends React.Component {
constructor() {
@ -1088,18 +1094,18 @@ describe('ReactIncrementalSideEffects', () => {
}
ReactNoop.render(<Foo />);
expect(Scheduler).toFlushWithoutYielding();
await waitForAll([]);
expect(ReactNoop).toMatchRenderedOutput(<span prop="foo" />);
let called = false;
instance.setState({text: 'bar'}, () => {
expect(ReactNoop).toMatchRenderedOutput(<span prop="bar" />);
called = true;
});
expect(Scheduler).toFlushWithoutYielding();
await waitForAll([]);
expect(called).toBe(true);
});
it('calls setState callback even if component bails out', () => {
it('calls setState callback even if component bails out', async () => {
let instance;
class Foo extends React.Component {
constructor() {
@ -1116,19 +1122,19 @@ describe('ReactIncrementalSideEffects', () => {
}
ReactNoop.render(<Foo />);
expect(Scheduler).toFlushWithoutYielding();
await waitForAll([]);
expect(ReactNoop).toMatchRenderedOutput(<span prop="foo" />);
let called = false;
instance.setState({}, () => {
called = true;
});
expect(Scheduler).toFlushWithoutYielding();
await waitForAll([]);
expect(called).toBe(true);
});
// TODO: Test that callbacks are not lost if an update is preempted.
it('calls componentWillUnmount after a deletion, even if nested', () => {
it('calls componentWillUnmount after a deletion, even if nested', async () => {
const ops = [];
class Bar extends React.Component {
@ -1170,11 +1176,11 @@ describe('ReactIncrementalSideEffects', () => {
}
ReactNoop.render(<Foo show={true} />);
expect(Scheduler).toFlushWithoutYielding();
await waitForAll([]);
expect(ops).toEqual([]);
ReactNoop.render(<Foo show={false} />);
expect(Scheduler).toFlushWithoutYielding();
await waitForAll([]);
expect(ops).toEqual([
'A',
'Wrapper',
@ -1188,7 +1194,7 @@ describe('ReactIncrementalSideEffects', () => {
]);
});
it('calls componentDidMount/Update after insertion/update', () => {
it('calls componentDidMount/Update after insertion/update', async () => {
let ops = [];
class Bar extends React.Component {
@ -1233,7 +1239,7 @@ describe('ReactIncrementalSideEffects', () => {
}
ReactNoop.render(<Foo />);
expect(Scheduler).toFlushWithoutYielding();
await waitForAll([]);
expect(ops).toEqual([
'mount:A',
'mount:B',
@ -1249,7 +1255,7 @@ describe('ReactIncrementalSideEffects', () => {
ops = [];
ReactNoop.render(<Foo />);
expect(Scheduler).toFlushWithoutYielding();
await waitForAll([]);
expect(ops).toEqual([
'update:A',
'update:B',
@ -1263,7 +1269,7 @@ describe('ReactIncrementalSideEffects', () => {
]);
});
it('invokes ref callbacks after insertion/update/unmount', () => {
it('invokes ref callbacks after insertion/update/unmount', async () => {
let classInstance = null;
let ops = [];
@ -1310,7 +1316,7 @@ describe('ReactIncrementalSideEffects', () => {
// Refs that switch function instances get reinvoked
ReactNoop.render(<Foo show={true} />);
expect(Scheduler).toFlushWithoutYielding();
await waitForAll([]);
expect(ops).toEqual([
// detach all refs that switched handlers first.
null,
@ -1323,7 +1329,7 @@ describe('ReactIncrementalSideEffects', () => {
ops = [];
ReactNoop.render(<Foo show={false} />);
expect(Scheduler).toFlushWithoutYielding();
await waitForAll([]);
expect(ops).toEqual([
// unmount
null,
@ -1334,7 +1340,7 @@ describe('ReactIncrementalSideEffects', () => {
// TODO: Test that mounts, updates, refs, unmounts and deletions happen in the
// expected way for aborted and resumed render life-cycles.
it('supports string refs', () => {
it('supports string refs', async () => {
let fooInstance = null;
class Bar extends React.Component {
@ -1354,8 +1360,8 @@ describe('ReactIncrementalSideEffects', () => {
}
ReactNoop.render(<Foo />);
expect(() => {
expect(Scheduler).toFlushWithoutYielding();
await expect(async () => {
await waitForAll([]);
}).toErrorDev([
'Warning: Component "Foo" contains the string ref "bar". ' +
'Support for string refs will be removed in a future major release. ' +

View File

@ -15,6 +15,9 @@ let ReactNoop;
let Scheduler;
let ContinuousEventPriority;
let act;
let waitForAll;
let waitFor;
let assertLog;
describe('ReactIncrementalUpdates', () => {
beforeEach(() => {
@ -26,6 +29,11 @@ describe('ReactIncrementalUpdates', () => {
act = require('jest-react').act;
ContinuousEventPriority =
require('react-reconciler/constants').ContinuousEventPriority;
const InternalTestUtils = require('internal-test-utils');
waitForAll = InternalTestUtils.waitForAll;
waitFor = InternalTestUtils.waitFor;
assertLog = InternalTestUtils.assertLog;
});
function flushNextRenderIfExpired() {
@ -37,7 +45,7 @@ describe('ReactIncrementalUpdates', () => {
ReactNoop.flushSync();
}
it('applies updates in order of priority', () => {
it('applies updates in order of priority', async () => {
let state;
class Foo extends React.Component {
state = {};
@ -58,14 +66,14 @@ describe('ReactIncrementalUpdates', () => {
}
ReactNoop.render(<Foo />);
expect(Scheduler).toFlushAndYieldThrough(['commit']);
await waitFor(['commit']);
expect(state).toEqual({a: 'a'});
expect(Scheduler).toFlushWithoutYielding();
await waitForAll([]);
expect(state).toEqual({a: 'a', b: 'b', c: 'c'});
});
it('applies updates with equal priority in insertion order', () => {
it('applies updates with equal priority in insertion order', async () => {
let state;
class Foo extends React.Component {
state = {};
@ -82,11 +90,11 @@ describe('ReactIncrementalUpdates', () => {
}
ReactNoop.render(<Foo />);
expect(Scheduler).toFlushWithoutYielding();
await waitForAll([]);
expect(state).toEqual({a: 'a', b: 'b', c: 'c'});
});
it('only drops updates with equal or lesser priority when replaceState is called', () => {
it('only drops updates with equal or lesser priority when replaceState is called', async () => {
let instance;
class Foo extends React.Component {
state = {};
@ -104,7 +112,7 @@ describe('ReactIncrementalUpdates', () => {
}
ReactNoop.render(<Foo />);
expect(Scheduler).toFlushAndYield(['render', 'componentDidMount']);
await waitForAll(['render', 'componentDidMount']);
ReactNoop.flushSync(() => {
React.startTransition(() => {
@ -122,14 +130,14 @@ describe('ReactIncrementalUpdates', () => {
// Even though a replaceState has been already scheduled, it hasn't been
// flushed yet because it has async priority.
expect(instance.state).toEqual({a: 'a', b: 'b'});
expect(Scheduler).toHaveYielded(['render', 'componentDidUpdate']);
assertLog(['render', 'componentDidUpdate']);
expect(Scheduler).toFlushAndYield(['render', 'componentDidUpdate']);
await waitForAll(['render', 'componentDidUpdate']);
// Now the rest of the updates are flushed, including the replaceState.
expect(instance.state).toEqual({c: 'c', d: 'd'});
});
it('can abort an update, schedule additional updates, and resume', () => {
it('can abort an update, schedule additional updates, and resume', async () => {
let instance;
class Foo extends React.Component {
state = {};
@ -140,7 +148,7 @@ describe('ReactIncrementalUpdates', () => {
}
ReactNoop.render(<Foo />);
expect(Scheduler).toFlushWithoutYielding();
await waitForAll([]);
function createUpdate(letter) {
return () => {
@ -159,7 +167,7 @@ describe('ReactIncrementalUpdates', () => {
});
// Begin the updates but don't flush them yet
expect(Scheduler).toFlushAndYieldThrough(['a', 'b', 'c']);
await waitFor(['a', 'b', 'c']);
expect(ReactNoop).toMatchRenderedOutput(<span prop="" />);
// Schedule some more updates at different priorities
@ -174,11 +182,11 @@ describe('ReactIncrementalUpdates', () => {
// The sync updates should have flushed, but not the async ones.
if (gate(flags => flags.enableUnifiedSyncLane)) {
expect(Scheduler).toHaveYielded(['d', 'e', 'f']);
assertLog(['d', 'e', 'f']);
expect(ReactNoop).toMatchRenderedOutput(<span prop="def" />);
} else {
// Update d was dropped and replaced by e.
expect(Scheduler).toHaveYielded(['e', 'f']);
assertLog(['e', 'f']);
expect(ReactNoop).toMatchRenderedOutput(<span prop="ef" />);
}
@ -186,7 +194,7 @@ describe('ReactIncrementalUpdates', () => {
// they should be processed again, to ensure that the terminal state
// is deterministic.
if (gate(flags => !flags.enableUnifiedSyncLane)) {
expect(Scheduler).toFlushAndYield([
await waitForAll([
// Since 'g' is in a transition, we'll process 'd' separately first.
// That causes us to process 'd' with 'e' and 'f' rebased.
'd',
@ -202,7 +210,7 @@ describe('ReactIncrementalUpdates', () => {
'g',
]);
} else {
expect(Scheduler).toFlushAndYield([
await waitForAll([
// Then we'll re-process everything for 'g'.
'a',
'b',
@ -216,7 +224,7 @@ describe('ReactIncrementalUpdates', () => {
expect(ReactNoop).toMatchRenderedOutput(<span prop="abcdefg" />);
});
it('can abort an update, schedule a replaceState, and resume', () => {
it('can abort an update, schedule a replaceState, and resume', async () => {
let instance;
class Foo extends React.Component {
state = {};
@ -227,7 +235,7 @@ describe('ReactIncrementalUpdates', () => {
}
ReactNoop.render(<Foo />);
expect(Scheduler).toFlushWithoutYielding();
await waitForAll([]);
function createUpdate(letter) {
return () => {
@ -246,7 +254,7 @@ describe('ReactIncrementalUpdates', () => {
});
// Begin the updates but don't flush them yet
expect(Scheduler).toFlushAndYieldThrough(['a', 'b', 'c']);
await waitFor(['a', 'b', 'c']);
expect(ReactNoop).toMatchRenderedOutput(<span prop="" />);
// Schedule some more updates at different priorities
@ -264,10 +272,10 @@ describe('ReactIncrementalUpdates', () => {
// The sync updates should have flushed, but not the async ones.
if (gate(flags => flags.enableUnifiedSyncLane)) {
expect(Scheduler).toHaveYielded(['d', 'e', 'f']);
assertLog(['d', 'e', 'f']);
} else {
// Update d was dropped and replaced by e.
expect(Scheduler).toHaveYielded(['e', 'f']);
assertLog(['e', 'f']);
}
expect(ReactNoop).toMatchRenderedOutput(<span prop="f" />);
@ -275,7 +283,7 @@ describe('ReactIncrementalUpdates', () => {
// they should be processed again, to ensure that the terminal state
// is deterministic.
if (gate(flags => !flags.enableUnifiedSyncLane)) {
expect(Scheduler).toFlushAndYield([
await waitForAll([
// Since 'g' is in a transition, we'll process 'd' separately first.
// That causes us to process 'd' with 'e' and 'f' rebased.
'd',
@ -291,7 +299,7 @@ describe('ReactIncrementalUpdates', () => {
'g',
]);
} else {
expect(Scheduler).toFlushAndYield([
await waitForAll([
// Then we'll re-process everything for 'g'.
'a',
'b',
@ -305,7 +313,7 @@ describe('ReactIncrementalUpdates', () => {
expect(ReactNoop).toMatchRenderedOutput(<span prop="fg" />);
});
it('passes accumulation of previous updates to replaceState updater function', () => {
it('passes accumulation of previous updates to replaceState updater function', async () => {
let instance;
class Foo extends React.Component {
state = {};
@ -315,7 +323,7 @@ describe('ReactIncrementalUpdates', () => {
}
}
ReactNoop.render(<Foo />);
expect(Scheduler).toFlushWithoutYielding();
await waitForAll([]);
instance.setState({a: 'a'});
instance.setState({b: 'b'});
@ -324,11 +332,11 @@ describe('ReactIncrementalUpdates', () => {
instance.updater.enqueueReplaceState(instance, previousState => ({
previousState,
}));
expect(Scheduler).toFlushWithoutYielding();
await waitForAll([]);
expect(instance.state).toEqual({previousState: {a: 'a', b: 'b'}});
});
it('does not call callbacks that are scheduled by another callback until a later commit', () => {
it('does not call callbacks that are scheduled by another callback until a later commit', async () => {
class Foo extends React.Component {
state = {};
componentDidMount() {
@ -347,7 +355,7 @@ describe('ReactIncrementalUpdates', () => {
}
ReactNoop.render(<Foo />);
expect(Scheduler).toFlushAndYield([
await waitForAll([
'render',
'did mount',
'render',
@ -357,7 +365,7 @@ describe('ReactIncrementalUpdates', () => {
]);
});
it('gives setState during reconciliation the same priority as whatever level is currently reconciling', () => {
it('gives setState during reconciliation the same priority as whatever level is currently reconciling', async () => {
let instance;
class Foo extends React.Component {
@ -373,7 +381,7 @@ describe('ReactIncrementalUpdates', () => {
}
}
ReactNoop.render(<Foo />);
expect(Scheduler).toFlushAndYield(['render']);
await waitForAll(['render']);
ReactNoop.flushSync(() => {
instance.setState({a: 'a'});
@ -384,17 +392,13 @@ describe('ReactIncrementalUpdates', () => {
expect(instance.state).toEqual({a: 'a', b: 'b'});
if (gate(flags => flags.deferRenderPhaseUpdateToNextBatch)) {
expect(Scheduler).toHaveYielded([
'componentWillReceiveProps',
'render',
'render',
]);
assertLog(['componentWillReceiveProps', 'render', 'render']);
} else {
expect(Scheduler).toHaveYielded(['componentWillReceiveProps', 'render']);
assertLog(['componentWillReceiveProps', 'render']);
}
});
it('updates triggered from inside a class setState updater', () => {
it('updates triggered from inside a class setState updater', async () => {
let instance;
class Foo extends React.Component {
state = {};
@ -406,7 +410,7 @@ describe('ReactIncrementalUpdates', () => {
}
ReactNoop.render(<Foo />);
expect(Scheduler).toFlushAndYield([
await waitForAll([
// Initial render
'render',
]);
@ -451,7 +455,7 @@ describe('ReactIncrementalUpdates', () => {
this.setState({a: 'a'});
return {b: 'b'};
});
expect(Scheduler).toFlushAndYield(
await waitForAll(
gate(flags =>
flags.deferRenderPhaseUpdateToNextBatch
? // In the new reconciler, updates inside the render phase are
@ -529,24 +533,20 @@ describe('ReactIncrementalUpdates', () => {
return null;
}
act(() => {
act(async () => {
React.startTransition(() => {
ReactNoop.render(<App />);
});
flushNextRenderIfExpired();
expect(Scheduler).toHaveYielded([]);
expect(Scheduler).toFlushAndYield([
'Render: 0',
'Commit: 0',
'Render: 1',
]);
assertLog([]);
await waitForAll(['Render: 0', 'Commit: 0', 'Render: 1']);
Scheduler.unstable_advanceTime(10000);
React.startTransition(() => {
setCount(2);
});
flushNextRenderIfExpired();
expect(Scheduler).toHaveYielded([]);
assertLog([]);
});
});
@ -559,7 +559,7 @@ describe('ReactIncrementalUpdates', () => {
ReactNoop.flushSync(() => {
ReactNoop.render(<Text text="A" />);
});
expect(Scheduler).toHaveYielded(['A']);
assertLog(['A']);
Scheduler.unstable_advanceTime(10000);
@ -567,7 +567,7 @@ describe('ReactIncrementalUpdates', () => {
ReactNoop.render(<Text text="B" />);
});
flushNextRenderIfExpired();
expect(Scheduler).toHaveYielded([]);
assertLog([]);
});
it('regression: does not expire soon due to previous expired work', () => {
@ -581,7 +581,7 @@ describe('ReactIncrementalUpdates', () => {
});
Scheduler.unstable_advanceTime(10000);
flushNextRenderIfExpired();
expect(Scheduler).toHaveYielded(['A']);
assertLog(['A']);
Scheduler.unstable_advanceTime(10000);
@ -589,7 +589,7 @@ describe('ReactIncrementalUpdates', () => {
ReactNoop.render(<Text text="B" />);
});
flushNextRenderIfExpired();
expect(Scheduler).toHaveYielded([]);
assertLog([]);
});
it('when rebasing, does not exclude updates that were already committed, regardless of priority', async () => {
@ -620,7 +620,7 @@ describe('ReactIncrementalUpdates', () => {
await act(async () => {
root.render(<App />);
});
expect(Scheduler).toHaveYielded(['Committed: ']);
assertLog(['Committed: ']);
expect(root).toMatchRenderedOutput(null);
await act(async () => {
@ -633,13 +633,9 @@ describe('ReactIncrementalUpdates', () => {
);
});
if (gate(flags => flags.enableUnifiedSyncLane)) {
expect(Scheduler).toHaveYielded([
'Committed: B',
'Committed: BCD',
'Committed: ABCD',
]);
assertLog(['Committed: B', 'Committed: BCD', 'Committed: ABCD']);
} else {
expect(Scheduler).toHaveYielded([
assertLog([
// A and B are pending. B is higher priority, so we'll render that first.
'Committed: B',
// Because A comes first in the queue, we're now in rebase mode. B must
@ -685,7 +681,7 @@ describe('ReactIncrementalUpdates', () => {
await act(async () => {
root.render(<App />);
});
expect(Scheduler).toHaveYielded([]);
assertLog([]);
expect(root).toMatchRenderedOutput(null);
await act(async () => {
@ -697,13 +693,9 @@ describe('ReactIncrementalUpdates', () => {
);
});
if (gate(flags => flags.enableUnifiedSyncLane)) {
expect(Scheduler).toHaveYielded([
'Committed: B',
'Committed: BCD',
'Committed: ABCD',
]);
assertLog(['Committed: B', 'Committed: BCD', 'Committed: ABCD']);
} else {
expect(Scheduler).toHaveYielded([
assertLog([
// A and B are pending. B is higher priority, so we'll render that first.
'Committed: B',
// Because A comes first in the queue, we're now in rebase mode. B must

View File

@ -5,6 +5,9 @@ let startTransition;
let useState;
let useEffect;
let act;
let assertLog;
let waitFor;
let waitForPaint;
describe('ReactInterleavedUpdates', () => {
beforeEach(() => {
@ -17,6 +20,11 @@ describe('ReactInterleavedUpdates', () => {
startTransition = React.startTransition;
useState = React.useState;
useEffect = React.useEffect;
const InternalTestUtils = require('internal-test-utils');
assertLog = InternalTestUtils.assertLog;
waitFor = InternalTestUtils.waitFor;
waitForPaint = InternalTestUtils.waitForPaint;
});
function Text({text}) {
@ -53,7 +61,7 @@ describe('ReactInterleavedUpdates', () => {
</>,
);
});
expect(Scheduler).toHaveYielded([0, 0, 0]);
assertLog([0, 0, 0]);
expect(root).toMatchRenderedOutput('000');
await act(async () => {
@ -61,7 +69,7 @@ describe('ReactInterleavedUpdates', () => {
updateChildren(1);
});
// Partially render the children. Only the first one.
expect(Scheduler).toFlushAndYieldThrough([1]);
await waitFor([1]);
// In an interleaved event, schedule an update on each of the children.
// Including the two that haven't rendered yet.
@ -70,11 +78,11 @@ describe('ReactInterleavedUpdates', () => {
});
// We should continue rendering without including the interleaved updates.
expect(Scheduler).toFlushUntilNextPaint([1, 1]);
await waitForPaint([1, 1]);
expect(root).toMatchRenderedOutput('111');
});
// The interleaved updates flush in a separate render.
expect(Scheduler).toHaveYielded([2, 2, 2]);
assertLog([2, 2, 2]);
expect(root).toMatchRenderedOutput('222');
});
@ -96,7 +104,7 @@ describe('ReactInterleavedUpdates', () => {
await act(async () => {
root.render(<App />);
});
expect(Scheduler).toHaveYielded(['A0', 'B0', 'C0']);
assertLog(['A0', 'B0', 'C0']);
expect(root).toMatchRenderedOutput('A0B0C0');
await act(async () => {
@ -104,7 +112,7 @@ describe('ReactInterleavedUpdates', () => {
startTransition(() => {
setStep(1);
});
expect(Scheduler).toFlushAndYieldThrough(['A1', 'B1']);
await waitFor(['A1', 'B1']);
// Schedule an interleaved update. This gets placed on a special queue.
startTransition(() => {
@ -112,7 +120,7 @@ describe('ReactInterleavedUpdates', () => {
});
// Finish rendering the first update.
expect(Scheduler).toFlushUntilNextPaint(['C1']);
await waitForPaint(['C1']);
// Schedule another update. (In the regression case, this was treated
// as a normal, non-interleaved update and it was inserted into the queue
@ -122,7 +130,7 @@ describe('ReactInterleavedUpdates', () => {
});
});
// The last update should win.
expect(Scheduler).toHaveYielded(['A3', 'B3', 'C3']);
assertLog(['A3', 'B3', 'C3']);
expect(root).toMatchRenderedOutput('A3B3C3');
});
});

View File

@ -1874,7 +1874,7 @@ describe(`onPostCommit`, () => {
loadModules();
});
it('should report time spent in passive effects', () => {
it('should report time spent in passive effects', async () => {
const callback = jest.fn();
const ComponentWithEffects = () => {
@ -1910,7 +1910,7 @@ describe(`onPostCommit`, () => {
</React.Profiler>,
);
});
Scheduler.unstable_flushAll();
await waitForAll([]);
expect(callback).toHaveBeenCalledTimes(1);
@ -1931,7 +1931,7 @@ describe(`onPostCommit`, () => {
</React.Profiler>,
);
});
Scheduler.unstable_flushAll();
await waitForAll([]);
expect(callback).toHaveBeenCalledTimes(2);
@ -1950,7 +1950,7 @@ describe(`onPostCommit`, () => {
<React.Profiler id="unmount-test" onPostCommit={callback} />,
);
});
Scheduler.unstable_flushAll();
await waitForAll([]);
expect(callback).toHaveBeenCalledTimes(3);