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

View File

@ -18,6 +18,8 @@ let ReactTestRenderer;
let Scheduler; let Scheduler;
let ReactDOMServer; let ReactDOMServer;
let act; let act;
let assertLog;
let waitForAll;
describe('ReactHooks', () => { describe('ReactHooks', () => {
beforeEach(() => { beforeEach(() => {
@ -30,6 +32,10 @@ describe('ReactHooks', () => {
Scheduler = require('scheduler'); Scheduler = require('scheduler');
ReactDOMServer = require('react-dom/server'); ReactDOMServer = require('react-dom/server');
act = require('jest-react').act; act = require('jest-react').act;
const InternalTestUtils = require('internal-test-utils');
assertLog = InternalTestUtils.assertLog;
waitForAll = InternalTestUtils.waitForAll;
}); });
if (__DEV__) { 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; const {useState, useLayoutEffect} = React;
function Child({text}) { function Child({text}) {
@ -80,11 +86,7 @@ describe('ReactHooks', () => {
const root = ReactTestRenderer.create(null, {unstable_isConcurrent: true}); const root = ReactTestRenderer.create(null, {unstable_isConcurrent: true});
root.update(<Parent />); root.update(<Parent />);
expect(Scheduler).toFlushAndYield([ await waitForAll(['Parent: 0, 0', 'Child: 0, 0', 'Effect: 0, 0']);
'Parent: 0, 0',
'Child: 0, 0',
'Effect: 0, 0',
]);
expect(root).toMatchRenderedOutput('0, 0'); expect(root).toMatchRenderedOutput('0, 0');
// Normal update // Normal update
@ -93,15 +95,11 @@ describe('ReactHooks', () => {
setCounter2(1); setCounter2(1);
}); });
expect(Scheduler).toHaveYielded([ assertLog(['Parent: 1, 1', 'Child: 1, 1', 'Effect: 1, 1']);
'Parent: 1, 1',
'Child: 1, 1',
'Effect: 1, 1',
]);
// Update that bails out. // Update that bails out.
act(() => setCounter1(1)); 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 // This time, one of the state updates but the other one doesn't. So we
// can't bail out. // can't bail out.
@ -110,11 +108,7 @@ describe('ReactHooks', () => {
setCounter2(2); setCounter2(2);
}); });
expect(Scheduler).toHaveYielded([ assertLog(['Parent: 1, 2', 'Child: 1, 2', 'Effect: 1, 2']);
'Parent: 1, 2',
'Child: 1, 2',
'Effect: 1, 2',
]);
// Lots of updates that eventually resolve to the current values. // Lots of updates that eventually resolve to the current values.
act(() => { act(() => {
@ -128,7 +122,7 @@ describe('ReactHooks', () => {
// Because the final values are the same as the current values, the // Because the final values are the same as the current values, the
// component bails out. // component bails out.
expect(Scheduler).toHaveYielded(['Parent: 1, 2']); assertLog(['Parent: 1, 2']);
// prepare to check SameValue // prepare to check SameValue
act(() => { act(() => {
@ -136,11 +130,7 @@ describe('ReactHooks', () => {
setCounter2(NaN); setCounter2(NaN);
}); });
expect(Scheduler).toHaveYielded([ assertLog(['Parent: 0, NaN', 'Child: 0, NaN', 'Effect: 0, NaN']);
'Parent: 0, NaN',
'Child: 0, NaN',
'Effect: 0, NaN',
]);
// check if re-setting to negative 0 / NaN still bails out // check if re-setting to negative 0 / NaN still bails out
act(() => { act(() => {
@ -150,20 +140,16 @@ describe('ReactHooks', () => {
setCounter2(NaN); setCounter2(NaN);
}); });
expect(Scheduler).toHaveYielded(['Parent: 0, NaN']); assertLog(['Parent: 0, NaN']);
// check if changing negative 0 to positive 0 does not bail out // check if changing negative 0 to positive 0 does not bail out
act(() => { act(() => {
setCounter1(0); setCounter1(0);
}); });
expect(Scheduler).toHaveYielded([ assertLog(['Parent: 0, NaN', 'Child: 0, NaN', 'Effect: 0, NaN']);
'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; const {useState, memo} = React;
function Child({text}) { function Child({text}) {
@ -188,10 +174,7 @@ describe('ReactHooks', () => {
const root = ReactTestRenderer.create(null, {unstable_isConcurrent: true}); const root = ReactTestRenderer.create(null, {unstable_isConcurrent: true});
root.update(<Parent theme="light" />); root.update(<Parent theme="light" />);
expect(Scheduler).toFlushAndYield([ await waitForAll(['Parent: 0, 0 (light)', 'Child: 0, 0 (light)']);
'Parent: 0, 0 (light)',
'Child: 0, 0 (light)',
]);
expect(root).toMatchRenderedOutput('0, 0 (light)'); expect(root).toMatchRenderedOutput('0, 0 (light)');
// Normal update // Normal update
@ -200,14 +183,11 @@ describe('ReactHooks', () => {
setCounter2(1); setCounter2(1);
}); });
expect(Scheduler).toHaveYielded([ assertLog(['Parent: 1, 1 (light)', 'Child: 1, 1 (light)']);
'Parent: 1, 1 (light)',
'Child: 1, 1 (light)',
]);
// Update that bails out. // Update that bails out.
act(() => setCounter1(1)); 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 // This time, one of the state updates but the other one doesn't. So we
// can't bail out. // can't bail out.
@ -216,10 +196,7 @@ describe('ReactHooks', () => {
setCounter2(2); setCounter2(2);
}); });
expect(Scheduler).toHaveYielded([ assertLog(['Parent: 1, 2 (light)', 'Child: 1, 2 (light)']);
'Parent: 1, 2 (light)',
'Child: 1, 2 (light)',
]);
// Updates bail out, but component still renders because props // Updates bail out, but component still renders because props
// have changed // have changed
@ -229,10 +206,7 @@ describe('ReactHooks', () => {
root.update(<Parent theme="dark" />); root.update(<Parent theme="dark" />);
}); });
expect(Scheduler).toHaveYielded([ assertLog(['Parent: 1, 2 (dark)', 'Child: 1, 2 (dark)']);
'Parent: 1, 2 (dark)',
'Child: 1, 2 (dark)',
]);
// Both props and state bail out // Both props and state bail out
act(() => { act(() => {
@ -241,10 +215,10 @@ describe('ReactHooks', () => {
root.update(<Parent theme="dark" />); 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; const {useState} = React;
let setCounter; let setCounter;
@ -258,7 +232,7 @@ describe('ReactHooks', () => {
const root = ReactTestRenderer.create(null, {unstable_isConcurrent: true}); const root = ReactTestRenderer.create(null, {unstable_isConcurrent: true});
root.update(<Counter />); root.update(<Counter />);
expect(Scheduler).toFlushAndYield(['Count: 0']); await waitForAll(['Count: 0']);
expect(root).toMatchRenderedOutput('0'); expect(root).toMatchRenderedOutput('0');
expect(() => { expect(() => {
@ -274,11 +248,11 @@ describe('ReactHooks', () => {
'declare it in the component body with useEffect().', 'declare it in the component body with useEffect().',
{withoutStack: true}, {withoutStack: true},
); );
expect(Scheduler).toHaveYielded(['Count: 1']); assertLog(['Count: 1']);
expect(root).toMatchRenderedOutput('1'); expect(root).toMatchRenderedOutput('1');
}); });
it('warns about dispatch second argument', () => { it('warns about dispatch second argument', async () => {
const {useReducer} = React; const {useReducer} = React;
let dispatch; let dispatch;
@ -292,7 +266,7 @@ describe('ReactHooks', () => {
const root = ReactTestRenderer.create(null, {unstable_isConcurrent: true}); const root = ReactTestRenderer.create(null, {unstable_isConcurrent: true});
root.update(<Counter />); root.update(<Counter />);
expect(Scheduler).toFlushAndYield(['Count: 0']); await waitForAll(['Count: 0']);
expect(root).toMatchRenderedOutput('0'); expect(root).toMatchRenderedOutput('0');
expect(() => { expect(() => {
@ -308,11 +282,11 @@ describe('ReactHooks', () => {
'declare it in the component body with useEffect().', 'declare it in the component body with useEffect().',
{withoutStack: true}, {withoutStack: true},
); );
expect(Scheduler).toHaveYielded(['Count: 1']); assertLog(['Count: 1']);
expect(root).toMatchRenderedOutput('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 {useState, useLayoutEffect, useContext} = React;
const ThemeContext = React.createContext('light'); const ThemeContext = React.createContext('light');
@ -355,7 +329,7 @@ describe('ReactHooks', () => {
); );
}); });
expect(Scheduler).toHaveYielded([ assertLog([
'Theme: light', 'Theme: light',
'Parent: 0 (light)', 'Parent: 0 (light)',
'Child: 0 (light)', 'Child: 0 (light)',
@ -366,21 +340,17 @@ describe('ReactHooks', () => {
// Updating the theme to the same value doesn't cause the consumers // Updating the theme to the same value doesn't cause the consumers
// to re-render. // to re-render.
setTheme('light'); setTheme('light');
expect(Scheduler).toFlushAndYield([]); await waitForAll([]);
expect(root).toMatchRenderedOutput('0 (light)'); expect(root).toMatchRenderedOutput('0 (light)');
// Normal update // Normal update
act(() => setCounter(1)); act(() => setCounter(1));
expect(Scheduler).toHaveYielded([ assertLog(['Parent: 1 (light)', 'Child: 1 (light)', 'Effect: 1 (light)']);
'Parent: 1 (light)',
'Child: 1 (light)',
'Effect: 1 (light)',
]);
expect(root).toMatchRenderedOutput('1 (light)'); expect(root).toMatchRenderedOutput('1 (light)');
// Update that doesn't change state, so it bails out // Update that doesn't change state, so it bails out
act(() => setCounter(1)); act(() => setCounter(1));
expect(Scheduler).toHaveYielded(['Parent: 1 (light)']); assertLog(['Parent: 1 (light)']);
expect(root).toMatchRenderedOutput('1 (light)'); expect(root).toMatchRenderedOutput('1 (light)');
// Update that doesn't change state, but the context changes, too, so it // Update that doesn't change state, but the context changes, too, so it
@ -390,7 +360,7 @@ describe('ReactHooks', () => {
setTheme('dark'); setTheme('dark');
}); });
expect(Scheduler).toHaveYielded([ assertLog([
'Theme: dark', 'Theme: dark',
'Parent: 1 (dark)', 'Parent: 1 (dark)',
'Child: 1 (dark)', 'Child: 1 (dark)',
@ -399,7 +369,7 @@ describe('ReactHooks', () => {
expect(root).toMatchRenderedOutput('1 (dark)'); 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; const {useState, useLayoutEffect} = React;
function Child({text}) { function Child({text}) {
@ -420,12 +390,12 @@ describe('ReactHooks', () => {
const root = ReactTestRenderer.create(null, {unstable_isConcurrent: true}); const root = ReactTestRenderer.create(null, {unstable_isConcurrent: true});
root.update(<Parent />); root.update(<Parent />);
expect(Scheduler).toFlushAndYield(['Parent: 0', 'Child: 0', 'Effect: 0']); await waitForAll(['Parent: 0', 'Child: 0', 'Effect: 0']);
expect(root).toMatchRenderedOutput('0'); expect(root).toMatchRenderedOutput('0');
// Normal update // Normal update
act(() => setCounter(1)); act(() => setCounter(1));
expect(Scheduler).toHaveYielded(['Parent: 1', 'Child: 1', 'Effect: 1']); assertLog(['Parent: 1', 'Child: 1', 'Effect: 1']);
expect(root).toMatchRenderedOutput('1'); expect(root).toMatchRenderedOutput('1');
// Update to the same state. React doesn't know if the queue is empty // 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 // enter the render phase before we can bail out. But we bail out before
// rendering the child, and we don't fire any effects. // rendering the child, and we don't fire any effects.
act(() => setCounter(1)); act(() => setCounter(1));
expect(Scheduler).toHaveYielded(['Parent: 1']); assertLog(['Parent: 1']);
expect(root).toMatchRenderedOutput('1'); expect(root).toMatchRenderedOutput('1');
// Update to the same state again. This times, neither fiber has pending // 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. // update priority, so we can bail out before even entering the render phase.
act(() => setCounter(1)); act(() => setCounter(1));
expect(Scheduler).toFlushAndYield([]); await waitForAll([]);
expect(root).toMatchRenderedOutput('1'); expect(root).toMatchRenderedOutput('1');
// This changes the state to something different so it renders normally. // This changes the state to something different so it renders normally.
act(() => setCounter(2)); act(() => setCounter(2));
expect(Scheduler).toHaveYielded(['Parent: 2', 'Child: 2', 'Effect: 2']); assertLog(['Parent: 2', 'Child: 2', 'Effect: 2']);
expect(root).toMatchRenderedOutput('2'); expect(root).toMatchRenderedOutput('2');
// prepare to check SameValue // prepare to check SameValue
act(() => { act(() => {
setCounter(0); setCounter(0);
}); });
expect(Scheduler).toHaveYielded(['Parent: 0', 'Child: 0', 'Effect: 0']); assertLog(['Parent: 0', 'Child: 0', 'Effect: 0']);
expect(root).toMatchRenderedOutput('0'); expect(root).toMatchRenderedOutput('0');
// Update to the same state for the first time to flush the queue // Update to the same state for the first time to flush the queue
@ -459,25 +429,25 @@ describe('ReactHooks', () => {
setCounter(0); setCounter(0);
}); });
expect(Scheduler).toHaveYielded(['Parent: 0']); assertLog(['Parent: 0']);
expect(root).toMatchRenderedOutput('0'); expect(root).toMatchRenderedOutput('0');
// Update again to the same state. Should bail out. // Update again to the same state. Should bail out.
act(() => { act(() => {
setCounter(0); setCounter(0);
}); });
expect(Scheduler).toFlushAndYield([]); await waitForAll([]);
expect(root).toMatchRenderedOutput('0'); expect(root).toMatchRenderedOutput('0');
// Update to a different state (positive 0 to negative 0) // Update to a different state (positive 0 to negative 0)
act(() => { act(() => {
setCounter(0 / -1); setCounter(0 / -1);
}); });
expect(Scheduler).toHaveYielded(['Parent: 0', 'Child: 0', 'Effect: 0']); assertLog(['Parent: 0', 'Child: 0', 'Effect: 0']);
expect(root).toMatchRenderedOutput('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; const {useState} = React;
function Child({text}) { function Child({text}) {
@ -495,7 +465,7 @@ describe('ReactHooks', () => {
const root = ReactTestRenderer.create(null, {unstable_isConcurrent: true}); const root = ReactTestRenderer.create(null, {unstable_isConcurrent: true});
root.update(<Parent />); root.update(<Parent />);
expect(Scheduler).toFlushAndYield(['Parent: 0', 'Child: 0']); await waitForAll(['Parent: 0', 'Child: 0']);
expect(root).toMatchRenderedOutput('0'); expect(root).toMatchRenderedOutput('0');
const update = value => { const update = value => {
@ -515,7 +485,7 @@ describe('ReactHooks', () => {
update(3); update(3);
}); });
expect(Scheduler).toHaveYielded([ assertLog([
// The first four updates were eagerly computed, because the queue is // The first four updates were eagerly computed, because the queue is
// empty before each one. // empty before each one.
'Compute state (0 -> 0)', 'Compute state (0 -> 0)',
@ -527,7 +497,7 @@ describe('ReactHooks', () => {
]); ]);
// Now let's enter the render phase // 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. // We don't need to re-compute the first four updates. Only the final two.
'Compute state (1 -> 2)', 'Compute state (1 -> 2)',
'Compute state (2 -> 3)', 'Compute state (2 -> 3)',
@ -537,7 +507,7 @@ describe('ReactHooks', () => {
expect(root).toMatchRenderedOutput('3'); 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; const {useState} = React;
function Child({text}) { function Child({text}) {
@ -555,7 +525,7 @@ describe('ReactHooks', () => {
const root = ReactTestRenderer.create(null, {unstable_isConcurrent: true}); const root = ReactTestRenderer.create(null, {unstable_isConcurrent: true});
root.update(<Parent />); root.update(<Parent />);
expect(Scheduler).toFlushAndYield(['Parent: 1', 'Child: 1']); await waitForAll(['Parent: 1', 'Child: 1']);
expect(root).toMatchRenderedOutput('1'); expect(root).toMatchRenderedOutput('1');
const update = compute => { const update = compute => {
@ -576,13 +546,13 @@ describe('ReactHooks', () => {
ReactTestRenderer.unstable_batchedUpdates(() => update(n => n * 100)); ReactTestRenderer.unstable_batchedUpdates(() => update(n => n * 100));
} }
// The new state is eagerly computed. // 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. // but before it's flushed, a higher priority update interrupts it.
root.unstable_flushSync(() => { root.unstable_flushSync(() => {
update(n => n + 5); update(n => n + 5);
}); });
expect(Scheduler).toHaveYielded([ assertLog([
// The eagerly computed state was completely skipped // The eagerly computed state was completely skipped
'Compute state (1 -> 6)', 'Compute state (1 -> 6)',
'Parent: 6', 'Parent: 6',
@ -593,7 +563,7 @@ describe('ReactHooks', () => {
// Now when we finish the first update, the second update is rebased on top. // 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 // Notice we didn't have to recompute the first update even though it was
// skipped in the previous render. // skipped in the previous render.
expect(Scheduler).toFlushAndYield([ await waitForAll([
'Compute state (100 -> 105)', 'Compute state (100 -> 105)',
'Parent: 105', 'Parent: 105',
'Child: 105', 'Child: 105',
@ -612,7 +582,7 @@ describe('ReactHooks', () => {
return props.dependencies; return props.dependencies;
} }
const root = ReactTestRenderer.create(<App dependencies={['A']} />); const root = ReactTestRenderer.create(<App dependencies={['A']} />);
expect(Scheduler).toHaveYielded(['Did commit: A']); assertLog(['Did commit: A']);
expect(() => { expect(() => {
root.update(<App dependencies={['A', 'B']} />); root.update(<App dependencies={['A', 'B']} />);
}).toErrorDev([ }).toErrorDev([
@ -639,7 +609,7 @@ describe('ReactHooks', () => {
const root = ReactTestRenderer.create(null); const root = ReactTestRenderer.create(null);
root.update(<App text="Hello" hasDeps={true} />); root.update(<App text="Hello" hasDeps={true} />);
expect(Scheduler).toHaveYielded(['Compute']); assertLog(['Compute']);
expect(root).toMatchRenderedOutput('HELLO'); expect(root).toMatchRenderedOutput('HELLO');
expect(() => { expect(() => {
@ -1841,7 +1811,7 @@ describe('ReactHooks', () => {
); );
expect(root).toMatchRenderedOutput('loading'); expect(root).toMatchRenderedOutput('loading');
await Promise.resolve(); await Promise.resolve();
Scheduler.unstable_flushAll(); await waitForAll([]);
expect(root).toMatchRenderedOutput('hello'); expect(root).toMatchRenderedOutput('hello');
}); });
@ -1873,7 +1843,7 @@ describe('ReactHooks', () => {
); );
expect(root).toMatchRenderedOutput('loading'); expect(root).toMatchRenderedOutput('loading');
await Promise.resolve(); await Promise.resolve();
Scheduler.unstable_flushAll(); await waitForAll([]);
expect(root).toMatchRenderedOutput('hello'); expect(root).toMatchRenderedOutput('hello');
}); });
@ -1905,7 +1875,7 @@ describe('ReactHooks', () => {
); );
expect(root).toMatchRenderedOutput('loading'); expect(root).toMatchRenderedOutput('loading');
await Promise.resolve(); await Promise.resolve();
Scheduler.unstable_flushAll(); await waitForAll([]);
expect(root).toMatchRenderedOutput('hello'); 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 ReactNoop;
let Scheduler; let Scheduler;
let act; let act;
let assertLog;
let waitForAll;
let waitFor;
let waitForThrow;
describe('ReactIncrementalErrorHandling', () => { describe('ReactIncrementalErrorHandling', () => {
beforeEach(() => { beforeEach(() => {
@ -27,6 +31,12 @@ describe('ReactIncrementalErrorHandling', () => {
ReactNoop = require('react-noop-renderer'); ReactNoop = require('react-noop-renderer');
Scheduler = require('scheduler'); Scheduler = require('scheduler');
act = require('jest-react').act; act = require('jest-react').act;
const InternalTestUtils = require('internal-test-utils');
assertLog = InternalTestUtils.assertLog;
waitForAll = InternalTestUtils.waitForAll;
waitFor = InternalTestUtils.waitFor;
waitForThrow = InternalTestUtils.waitForThrow;
}); });
afterEach(() => { afterEach(() => {
@ -55,7 +65,7 @@ describe('ReactIncrementalErrorHandling', () => {
); );
} }
it('recovers from errors asynchronously', () => { it('recovers from errors asynchronously', async () => {
class ErrorBoundary extends React.Component { class ErrorBoundary extends React.Component {
state = {error: null}; state = {error: null};
static getDerivedStateFromError(error) { static getDerivedStateFromError(error) {
@ -104,7 +114,7 @@ describe('ReactIncrementalErrorHandling', () => {
}); });
// Start rendering asynchronously // Start rendering asynchronously
expect(Scheduler).toFlushAndYieldThrough([ await waitFor([
'ErrorBoundary (try)', 'ErrorBoundary (try)',
'Indirection', 'Indirection',
'Indirection', 'Indirection',
@ -114,9 +124,9 @@ describe('ReactIncrementalErrorHandling', () => {
]); ]);
// Still rendering async... // Still rendering async...
expect(Scheduler).toFlushAndYieldThrough(['Indirection']); await waitFor(['Indirection']);
expect(Scheduler).toFlushAndYieldThrough([ await waitFor([
'Indirection', 'Indirection',
// Call getDerivedStateFromError and re-render the error boundary, this // 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 { class ErrorBoundary extends React.Component {
state = {error: null}; state = {error: null};
componentDidCatch(error) { componentDidCatch(error) {
@ -202,7 +212,7 @@ describe('ReactIncrementalErrorHandling', () => {
}); });
// Start rendering asynchronously // Start rendering asynchronously
expect(Scheduler).toFlushAndYieldThrough([ await waitFor([
'ErrorBoundary (try)', 'ErrorBoundary (try)',
'Indirection', 'Indirection',
'Indirection', 'Indirection',
@ -212,9 +222,9 @@ describe('ReactIncrementalErrorHandling', () => {
]); ]);
// Still rendering async... // Still rendering async...
expect(Scheduler).toFlushAndYieldThrough(['Indirection']); await waitFor(['Indirection']);
expect(Scheduler).toFlushAndYieldThrough([ await waitFor([
'Indirection', 'Indirection',
// Now that the tree is complete, and there's no remaining work, React // 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. // reverts to legacy mode to retry one more time before handling the error.
@ -254,7 +264,7 @@ describe('ReactIncrementalErrorHandling', () => {
React.startTransition(() => { React.startTransition(() => {
ReactNoop.render(<App isBroken={true} />, onCommit); ReactNoop.render(<App isBroken={true} />, onCommit);
}); });
expect(Scheduler).toFlushAndYieldThrough(['error']); await waitFor(['error']);
React.startTransition(() => { React.startTransition(() => {
// This update is in a separate batch // 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 // to recover from the error is synchronous, this should be enough to
// finish the rest of the work. // finish the rest of the work.
Scheduler.unstable_flushNumberOfYields(1); Scheduler.unstable_flushNumberOfYields(1);
expect(Scheduler).toHaveYielded([ assertLog([
'success', 'success',
// Nothing commits until the second update completes. // Nothing commits until the second update completes.
'commit', 'commit',
@ -280,7 +290,7 @@ describe('ReactIncrementalErrorHandling', () => {
}); });
// @gate www // @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) { function App(props) {
if (props.isBroken) { if (props.isBroken) {
Scheduler.unstable_yieldValue('error'); Scheduler.unstable_yieldValue('error');
@ -304,7 +314,7 @@ describe('ReactIncrementalErrorHandling', () => {
React.startTransition(() => { React.startTransition(() => {
ReactNoop.render(<App isBroken={true} />, onCommit); ReactNoop.render(<App isBroken={true} />, onCommit);
}); });
expect(Scheduler).toFlushAndYieldThrough(['error']); await waitFor(['error']);
expect(ReactNoop).toMatchRenderedOutput(null); expect(ReactNoop).toMatchRenderedOutput(null);
@ -320,7 +330,7 @@ describe('ReactIncrementalErrorHandling', () => {
// to recover from the error is synchronous, this should be enough to // to recover from the error is synchronous, this should be enough to
// finish the rest of the work. // finish the rest of the work.
Scheduler.unstable_flushNumberOfYields(1); Scheduler.unstable_flushNumberOfYields(1);
expect(Scheduler).toHaveYielded([ assertLog([
'success', 'success',
// Nothing commits until the second update completes. // Nothing commits until the second update completes.
'commit', 'commit',
@ -335,7 +345,7 @@ describe('ReactIncrementalErrorHandling', () => {
); );
// The offscreen content finishes in a subsequent render // The offscreen content finishes in a subsequent render
expect(Scheduler).toFlushAndYield([]); await waitForAll([]);
expect(ReactNoop).toMatchRenderedOutput( expect(ReactNoop).toMatchRenderedOutput(
<> <>
Everything is fine 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}) { function BadRender({unused}) {
Scheduler.unstable_yieldValue('BadRender'); Scheduler.unstable_yieldValue('BadRender');
throw new Error('oops'); throw new Error('oops');
@ -374,19 +384,14 @@ describe('ReactIncrementalErrorHandling', () => {
}); });
// Render the bad component asynchronously // Render the bad component asynchronously
expect(Scheduler).toFlushAndYieldThrough(['Parent', 'BadRender']); await waitFor(['Parent', 'BadRender']);
// Finish the rest of the async work // Finish the rest of the async work
expect(Scheduler).toFlushAndYieldThrough(['Sibling']); await waitFor(['Sibling']);
// Old scheduler renders, commits, and throws synchronously // Old scheduler renders, commits, and throws synchronously
expect(() => Scheduler.unstable_flushNumberOfYields(1)).toThrow('oops'); expect(() => Scheduler.unstable_flushNumberOfYields(1)).toThrow('oops');
expect(Scheduler).toHaveYielded([ assertLog(['Parent', 'BadRender', 'Sibling', 'commit']);
'Parent',
'BadRender',
'Sibling',
'commit',
]);
expect(ReactNoop).toMatchRenderedOutput(null); expect(ReactNoop).toMatchRenderedOutput(null);
}); });
@ -418,7 +423,7 @@ describe('ReactIncrementalErrorHandling', () => {
}); });
// Render part of the tree // Render part of the tree
expect(Scheduler).toFlushAndYieldThrough(['A', 'B']); await waitFor(['A', 'B']);
// Expire the render midway through // Expire the render midway through
Scheduler.unstable_advanceTime(10000); Scheduler.unstable_advanceTime(10000);
@ -428,7 +433,7 @@ describe('ReactIncrementalErrorHandling', () => {
ReactNoop.flushSync(); ReactNoop.flushSync();
}).toThrow('Oops'); }).toThrow('Oops');
expect(Scheduler).toHaveYielded([ assertLog([
// The render expired, but we shouldn't throw out the partial work. // The render expired, but we shouldn't throw out the partial work.
// Finish the current level. // Finish the current level.
'Oops', 'Oops',
@ -446,7 +451,7 @@ describe('ReactIncrementalErrorHandling', () => {
expect(ReactNoop).toMatchRenderedOutput(null); expect(ReactNoop).toMatchRenderedOutput(null);
}); });
it('calls componentDidCatch multiple times for multiple errors', () => { it('calls componentDidCatch multiple times for multiple errors', async () => {
let id = 0; let id = 0;
class BadMount extends React.Component { class BadMount extends React.Component {
componentDidMount() { componentDidMount() {
@ -481,7 +486,7 @@ describe('ReactIncrementalErrorHandling', () => {
</ErrorBoundary>, </ErrorBoundary>,
); );
expect(Scheduler).toFlushAndYield([ await waitForAll([
'ErrorBoundary', 'ErrorBoundary',
'BadMount', 'BadMount',
'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 { class ErrorBoundary extends React.Component {
state = {error: null}; state = {error: null};
componentDidCatch(error) { componentDidCatch(error) {
@ -520,13 +525,13 @@ describe('ReactIncrementalErrorHandling', () => {
<BrokenRender /> <BrokenRender />
</ErrorBoundary>, </ErrorBoundary>,
); );
expect(Scheduler).toFlushWithoutYielding(); await waitForAll([]);
expect(ReactNoop).toMatchRenderedOutput( expect(ReactNoop).toMatchRenderedOutput(
<span prop="Caught an error: Hello." />, <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 { class ErrorBoundary extends React.Component {
state = {error: null}; state = {error: null};
componentDidCatch(error) { componentDidCatch(error) {
@ -558,10 +563,10 @@ describe('ReactIncrementalErrorHandling', () => {
); );
}); });
expect(Scheduler).toFlushAndYieldThrough(['ErrorBoundary render success']); await waitFor(['ErrorBoundary render success']);
expect(ReactNoop).toMatchRenderedOutput(null); expect(ReactNoop).toMatchRenderedOutput(null);
expect(Scheduler).toFlushAndYield([ await waitForAll([
'BrokenRender', 'BrokenRender',
// React retries one more time // React retries one more time
'ErrorBoundary render success', 'ErrorBoundary render success',
@ -608,7 +613,7 @@ describe('ReactIncrementalErrorHandling', () => {
); );
}); });
expect(Scheduler).toHaveYielded([ assertLog([
'ErrorBoundary render success', 'ErrorBoundary render success',
'BrokenRender', 'BrokenRender',
@ -658,7 +663,7 @@ describe('ReactIncrementalErrorHandling', () => {
); );
}); });
expect(Scheduler).toHaveYielded([ assertLog([
'ErrorBoundary render success', 'ErrorBoundary render success',
'BrokenRender', '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 { class RethrowErrorBoundary extends React.Component {
componentDidCatch(error) { componentDidCatch(error) {
Scheduler.unstable_yieldValue('RethrowErrorBoundary componentDidCatch'); Scheduler.unstable_yieldValue('RethrowErrorBoundary componentDidCatch');
@ -698,23 +703,22 @@ describe('ReactIncrementalErrorHandling', () => {
</RethrowErrorBoundary>, </RethrowErrorBoundary>,
); );
expect(() => { await waitForThrow('Hello');
expect(Scheduler).toFlushAndYield([ assertLog([
'RethrowErrorBoundary render', 'RethrowErrorBoundary render',
'BrokenRender', 'BrokenRender',
// React retries one more time // React retries one more time
'RethrowErrorBoundary render', 'RethrowErrorBoundary render',
'BrokenRender', 'BrokenRender',
// Errored again on retry. Now handle it. // Errored again on retry. Now handle it.
'RethrowErrorBoundary componentDidCatch', 'RethrowErrorBoundary componentDidCatch',
]); ]);
}).toThrow('Hello');
expect(ReactNoop.getChildrenAsJSX()).toEqual(null); 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 { class RethrowErrorBoundary extends React.Component {
componentDidCatch(error) { componentDidCatch(error) {
Scheduler.unstable_yieldValue('RethrowErrorBoundary componentDidCatch'); Scheduler.unstable_yieldValue('RethrowErrorBoundary componentDidCatch');
@ -739,12 +743,10 @@ describe('ReactIncrementalErrorHandling', () => {
); );
}); });
expect(Scheduler).toFlushAndYieldThrough(['RethrowErrorBoundary render']); await waitFor(['RethrowErrorBoundary render']);
expect(() => { await waitForThrow('Hello');
expect(Scheduler).toFlushWithoutYielding(); assertLog([
}).toThrow('Hello');
expect(Scheduler).toHaveYielded([
'BrokenRender', 'BrokenRender',
// React retries one more time // React retries one more time
@ -783,7 +785,7 @@ describe('ReactIncrementalErrorHandling', () => {
); );
}); });
}).toThrow('Hello'); }).toThrow('Hello');
expect(Scheduler).toHaveYielded([ assertLog([
'RethrowErrorBoundary render', 'RethrowErrorBoundary render',
'BrokenRender', 'BrokenRender',
@ -826,7 +828,7 @@ describe('ReactIncrementalErrorHandling', () => {
); );
}); });
}).toThrow('Hello'); }).toThrow('Hello');
expect(Scheduler).toHaveYielded([ assertLog([
'RethrowErrorBoundary render', 'RethrowErrorBoundary render',
'BrokenRender', 'BrokenRender',
@ -840,7 +842,7 @@ describe('ReactIncrementalErrorHandling', () => {
expect(ReactNoop).toMatchRenderedOutput(null); 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" />); ReactNoop.render(<span prop="a:1" />);
expect(() => { expect(() => {
ReactNoop.batchedUpdates(() => { ReactNoop.batchedUpdates(() => {
@ -849,11 +851,11 @@ describe('ReactIncrementalErrorHandling', () => {
throw new Error('Hello'); throw new Error('Hello');
}); });
}).toThrow('Hello'); }).toThrow('Hello');
expect(Scheduler).toFlushWithoutYielding(); await waitForAll([]);
expect(ReactNoop).toMatchRenderedOutput(<span prop="a:3" />); 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" />); ReactNoop.render(<span prop="a:1" />);
expect(() => { expect(() => {
ReactNoop.batchedUpdates(() => { ReactNoop.batchedUpdates(() => {
@ -866,12 +868,12 @@ describe('ReactIncrementalErrorHandling', () => {
}); });
}); });
}).toThrow('Hello'); }).toThrow('Hello');
expect(Scheduler).toFlushWithoutYielding(); await waitForAll([]);
expect(ReactNoop).toMatchRenderedOutput(<span prop="a:5" />); expect(ReactNoop).toMatchRenderedOutput(<span prop="a:5" />);
}); });
// TODO: Is this a breaking change? // 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" />); ReactNoop.render(<span prop="a:1" />);
expect(() => { expect(() => {
ReactNoop.flushSync(() => { ReactNoop.flushSync(() => {
@ -882,11 +884,11 @@ describe('ReactIncrementalErrorHandling', () => {
}); });
}); });
}).toThrow('Hello'); }).toThrow('Hello');
Scheduler.unstable_flushAll(); await waitForAll([]);
expect(ReactNoop).toMatchRenderedOutput(<span prop="a:3" />); 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}) { function BrokenRender({unused}) {
Scheduler.unstable_yieldValue('BrokenRender'); Scheduler.unstable_yieldValue('BrokenRender');
throw new Error('Hello'); throw new Error('Hello');
@ -898,20 +900,18 @@ describe('ReactIncrementalErrorHandling', () => {
} }
ReactNoop.render(<BrokenRender />); ReactNoop.render(<BrokenRender />);
expect(() => { await waitForThrow('Hello');
expect(Scheduler).toFlushWithoutYielding();
}).toThrow('Hello');
ReactNoop.render(<Foo />); ReactNoop.render(<Foo />);
expect(Scheduler).toHaveYielded([ assertLog([
'BrokenRender', 'BrokenRender',
// React retries one more time // React retries one more time
'BrokenRender', 'BrokenRender',
// Errored again on retry // 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}) { function BrokenRender({shouldThrow}) {
Scheduler.unstable_yieldValue('BrokenRender'); Scheduler.unstable_yieldValue('BrokenRender');
if (shouldThrow) { if (shouldThrow) {
@ -926,13 +926,11 @@ describe('ReactIncrementalErrorHandling', () => {
} }
ReactNoop.render(<BrokenRender shouldThrow={false} />); ReactNoop.render(<BrokenRender shouldThrow={false} />);
expect(Scheduler).toFlushAndYield(['BrokenRender']); await waitForAll(['BrokenRender']);
expect(() => { ReactNoop.render(<BrokenRender shouldThrow={true} />);
ReactNoop.render(<BrokenRender shouldThrow={true} />); await waitForThrow('Hello');
expect(Scheduler).toFlushWithoutYielding(); assertLog([
}).toThrow('Hello');
expect(Scheduler).toHaveYielded([
'BrokenRender', 'BrokenRender',
// React retries one more time // React retries one more time
'BrokenRender', 'BrokenRender',
@ -940,10 +938,10 @@ describe('ReactIncrementalErrorHandling', () => {
]); ]);
ReactNoop.render(<Foo />); 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 { class BrokenComponentWillUnmount extends React.Component {
render() { render() {
return <div />; return <div />;
@ -959,19 +957,17 @@ describe('ReactIncrementalErrorHandling', () => {
} }
ReactNoop.render(<BrokenComponentWillUnmount />); ReactNoop.render(<BrokenComponentWillUnmount />);
expect(Scheduler).toFlushWithoutYielding(); await waitForAll([]);
expect(() => { ReactNoop.render(<div />);
ReactNoop.render(<div />); await waitForThrow('Hello');
expect(Scheduler).toFlushWithoutYielding();
}).toThrow('Hello');
ReactNoop.render(<Foo />); ReactNoop.render(<Foo />);
expect(Scheduler).toFlushAndYield(['Foo']); await waitForAll(['Foo']);
}); });
// @gate skipUnmountedBoundaries // @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 { class Parent extends React.Component {
componentWillUnmount() { componentWillUnmount() {
Scheduler.unstable_yieldValue('Parent componentWillUnmount'); Scheduler.unstable_yieldValue('Parent componentWillUnmount');
@ -1001,22 +997,21 @@ describe('ReactIncrementalErrorHandling', () => {
} }
ReactNoop.render(<Parent />); ReactNoop.render(<Parent />);
expect(Scheduler).toFlushWithoutYielding(); await waitForAll([]);
// Because the error boundary is also unmounting, // Because the error boundary is also unmounting,
// an error in ThrowsOnUnmount should be rethrown. // an error in ThrowsOnUnmount should be rethrown.
expect(() => { ReactNoop.render(null);
ReactNoop.render(null); await waitForThrow('unmount error');
expect(Scheduler).toFlushAndYield([ await assertLog([
'Parent componentWillUnmount', 'Parent componentWillUnmount',
'ThrowsOnUnmount componentWillUnmount', 'ThrowsOnUnmount componentWillUnmount',
]); ]);
}).toThrow('unmount error');
ReactNoop.render(<Parent />); 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; let parent;
class Parent extends React.Component { class Parent extends React.Component {
@ -1045,14 +1040,14 @@ describe('ReactIncrementalErrorHandling', () => {
} }
ReactNoop.render(<Parent />); ReactNoop.render(<Parent />);
expect(Scheduler).toFlushWithoutYielding(); await waitForAll([]);
ReactNoop.flushSync(() => { ReactNoop.flushSync(() => {
ReactNoop.render(<Parent />); 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 { class ErrorBoundary extends React.Component {
state = {error: null}; state = {error: null};
componentDidCatch(error) { componentDidCatch(error) {
@ -1079,50 +1074,42 @@ describe('ReactIncrementalErrorHandling', () => {
'a', 'a',
); );
ReactNoop.renderToRootWithID(<span prop="b:1" />, 'b'); ReactNoop.renderToRootWithID(<span prop="b:1" />, 'b');
expect(Scheduler).toFlushWithoutYielding(); await waitForAll([]);
expect(ReactNoop.getChildrenAsJSX('a')).toEqual( expect(ReactNoop.getChildrenAsJSX('a')).toEqual(
<span prop="Caught an error: Hello." />, <span prop="Caught an error: Hello." />,
); );
expect(Scheduler).toFlushWithoutYielding(); await waitForAll([]);
expect(ReactNoop.getChildrenAsJSX('b')).toEqual(<span prop="b:1" />); 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) { function BrokenRender(props) {
throw new Error(props.label); throw new Error(props.label);
} }
ReactNoop.renderToRootWithID(<BrokenRender label="a" />, 'a'); ReactNoop.renderToRootWithID(<BrokenRender label="a" />, 'a');
expect(() => { await waitForThrow('a');
expect(Scheduler).toFlushWithoutYielding();
}).toThrow('a');
expect(ReactNoop.getChildrenAsJSX('a')).toEqual(null); expect(ReactNoop.getChildrenAsJSX('a')).toEqual(null);
ReactNoop.renderToRootWithID(<BrokenRender label="a" />, 'a'); ReactNoop.renderToRootWithID(<BrokenRender label="a" />, 'a');
ReactNoop.renderToRootWithID(<span prop="b:2" />, 'b'); ReactNoop.renderToRootWithID(<span prop="b:2" />, 'b');
expect(() => { await waitForThrow('a');
expect(Scheduler).toFlushWithoutYielding();
}).toThrow('a');
expect(Scheduler).toFlushWithoutYielding(); await waitForAll([]);
expect(ReactNoop.getChildrenAsJSX('a')).toEqual(null); expect(ReactNoop.getChildrenAsJSX('a')).toEqual(null);
expect(ReactNoop.getChildrenAsJSX('b')).toEqual(<span prop="b:2" />); expect(ReactNoop.getChildrenAsJSX('b')).toEqual(<span prop="b:2" />);
ReactNoop.renderToRootWithID(<span prop="a:3" />, 'a'); ReactNoop.renderToRootWithID(<span prop="a:3" />, 'a');
ReactNoop.renderToRootWithID(<BrokenRender label="b" />, 'b'); ReactNoop.renderToRootWithID(<BrokenRender label="b" />, 'b');
expect(() => { await waitForThrow('b');
expect(Scheduler).toFlushWithoutYielding();
}).toThrow('b');
expect(ReactNoop.getChildrenAsJSX('a')).toEqual(<span prop="a:3" />); expect(ReactNoop.getChildrenAsJSX('a')).toEqual(<span prop="a:3" />);
expect(ReactNoop.getChildrenAsJSX('b')).toEqual(null); expect(ReactNoop.getChildrenAsJSX('b')).toEqual(null);
ReactNoop.renderToRootWithID(<span prop="a:4" />, 'a'); ReactNoop.renderToRootWithID(<span prop="a:4" />, 'a');
ReactNoop.renderToRootWithID(<BrokenRender label="b" />, 'b'); ReactNoop.renderToRootWithID(<BrokenRender label="b" />, 'b');
ReactNoop.renderToRootWithID(<span prop="c:4" />, 'c'); ReactNoop.renderToRootWithID(<span prop="c:4" />, 'c');
expect(() => { await waitForThrow('b');
expect(Scheduler).toFlushWithoutYielding(); await waitForAll([]);
}).toThrow('b');
expect(Scheduler).toFlushWithoutYielding();
expect(ReactNoop.getChildrenAsJSX('a')).toEqual(<span prop="a:4" />); expect(ReactNoop.getChildrenAsJSX('a')).toEqual(<span prop="a:4" />);
expect(ReactNoop.getChildrenAsJSX('b')).toEqual(null); expect(ReactNoop.getChildrenAsJSX('b')).toEqual(null);
expect(ReactNoop.getChildrenAsJSX('c')).toEqual(<span prop="c:4" />); 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="c:5" />, 'c');
ReactNoop.renderToRootWithID(<span prop="d:5" />, 'd'); ReactNoop.renderToRootWithID(<span prop="d:5" />, 'd');
ReactNoop.renderToRootWithID(<BrokenRender label="e" />, 'e'); ReactNoop.renderToRootWithID(<BrokenRender label="e" />, 'e');
expect(() => { await waitForThrow('e');
expect(Scheduler).toFlushWithoutYielding(); await waitForAll([]);
}).toThrow('e');
expect(Scheduler).toFlushWithoutYielding();
expect(ReactNoop.getChildrenAsJSX('a')).toEqual(<span prop="a:5" />); expect(ReactNoop.getChildrenAsJSX('a')).toEqual(<span prop="a:5" />);
expect(ReactNoop.getChildrenAsJSX('b')).toEqual(<span prop="b:5" />); expect(ReactNoop.getChildrenAsJSX('b')).toEqual(<span prop="b:5" />);
expect(ReactNoop.getChildrenAsJSX('c')).toEqual(<span prop="c:5" />); expect(ReactNoop.getChildrenAsJSX('c')).toEqual(<span prop="c:5" />);
@ -1149,17 +1134,11 @@ describe('ReactIncrementalErrorHandling', () => {
ReactNoop.renderToRootWithID(<BrokenRender label="e" />, 'e'); ReactNoop.renderToRootWithID(<BrokenRender label="e" />, 'e');
ReactNoop.renderToRootWithID(<span prop="f:6" />, 'f'); ReactNoop.renderToRootWithID(<span prop="f:6" />, 'f');
expect(() => { await waitForThrow('a');
expect(Scheduler).toFlushWithoutYielding(); await waitForThrow('c');
}).toThrow('a'); await waitForThrow('e');
expect(() => {
expect(Scheduler).toFlushWithoutYielding();
}).toThrow('c');
expect(() => {
expect(Scheduler).toFlushWithoutYielding();
}).toThrow('e');
expect(Scheduler).toFlushWithoutYielding(); await waitForAll([]);
expect(ReactNoop.getChildrenAsJSX('a')).toEqual(null); expect(ReactNoop.getChildrenAsJSX('a')).toEqual(null);
expect(ReactNoop.getChildrenAsJSX('b')).toEqual(<span prop="b:6" />); expect(ReactNoop.getChildrenAsJSX('b')).toEqual(<span prop="b:6" />);
expect(ReactNoop.getChildrenAsJSX('c')).toEqual(null); expect(ReactNoop.getChildrenAsJSX('c')).toEqual(null);
@ -1173,7 +1152,7 @@ describe('ReactIncrementalErrorHandling', () => {
ReactNoop.unmountRootWithID('d'); ReactNoop.unmountRootWithID('d');
ReactNoop.unmountRootWithID('e'); ReactNoop.unmountRootWithID('e');
ReactNoop.unmountRootWithID('f'); ReactNoop.unmountRootWithID('f');
expect(Scheduler).toFlushWithoutYielding(); await waitForAll([]);
expect(ReactNoop.getChildrenAsJSX('a')).toEqual(null); expect(ReactNoop.getChildrenAsJSX('a')).toEqual(null);
expect(ReactNoop.getChildrenAsJSX('b')).toEqual(null); expect(ReactNoop.getChildrenAsJSX('b')).toEqual(null);
expect(ReactNoop.getChildrenAsJSX('c')).toEqual(null); expect(ReactNoop.getChildrenAsJSX('c')).toEqual(null);
@ -1182,7 +1161,7 @@ describe('ReactIncrementalErrorHandling', () => {
expect(ReactNoop.getChildrenAsJSX('f')).toEqual(null); 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 { class Provider extends React.Component {
static childContextTypes = {message: PropTypes.string}; static childContextTypes = {message: PropTypes.string};
static contextTypes = {message: PropTypes.string}; static contextTypes = {message: PropTypes.string};
@ -1234,7 +1213,7 @@ describe('ReactIncrementalErrorHandling', () => {
<Connector /> <Connector />
</Provider>, </Provider>,
); );
expect(Scheduler).toFlushWithoutYielding(); await waitForAll([]);
// If the context stack does not unwind, span will get 'abcde' // If the context stack does not unwind, span will get 'abcde'
expect(ReactNoop).toMatchRenderedOutput(<span prop="a" />); 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 { class ErrorBoundary extends React.Component {
state = {error: null}; state = {error: null};
componentDidCatch(error) { componentDidCatch(error) {
@ -1307,7 +1286,7 @@ describe('ReactIncrementalErrorHandling', () => {
<BrokenRender fail={false} /> <BrokenRender fail={false} />
</ErrorBoundary>, </ErrorBoundary>,
); );
expect(Scheduler).toFlushWithoutYielding(); await waitForAll([]);
ReactNoop.render( ReactNoop.render(
<ErrorBoundary> <ErrorBoundary>
@ -1334,7 +1313,7 @@ describe('ReactIncrementalErrorHandling', () => {
); );
}); });
it('recovers from uncaught reconciler errors', () => { it('recovers from uncaught reconciler errors', async () => {
const InvalidType = undefined; const InvalidType = undefined;
expect(() => ReactNoop.render(<InvalidType />)).toErrorDev( expect(() => ReactNoop.render(<InvalidType />)).toErrorDev(
'Warning: React.createElement: type is invalid -- expected a string', 'Warning: React.createElement: type is invalid -- expected a string',
@ -1350,11 +1329,11 @@ describe('ReactIncrementalErrorHandling', () => {
); );
ReactNoop.render(<span prop="hi" />); ReactNoop.render(<span prop="hi" />);
expect(Scheduler).toFlushWithoutYielding(); await waitForAll([]);
expect(ReactNoop).toMatchRenderedOutput(<span prop="hi" />); expect(ReactNoop).toMatchRenderedOutput(<span prop="hi" />);
}); });
it('unmounts components with uncaught errors', () => { it('unmounts components with uncaught errors', async () => {
let inst; let inst;
class BrokenRenderAndUnmount extends React.Component { class BrokenRenderAndUnmount extends React.Component {
@ -1390,14 +1369,21 @@ describe('ReactIncrementalErrorHandling', () => {
</Parent> </Parent>
</Parent>, </Parent>,
); );
expect(Scheduler).toFlushWithoutYielding(); await waitForAll([]);
inst.setState({fail: true});
expect(() => { expect(() => {
expect(Scheduler).toFlushWithoutYielding(); expect(() => {
}).toThrowError('Hello.'); 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. // Attempt to clean up.
// Errors in parents shouldn't stop children from unmounting. // Errors in parents shouldn't stop children from unmounting.
'Parent componentWillUnmount [!]', 'Parent componentWillUnmount [!]',
@ -1405,13 +1391,9 @@ describe('ReactIncrementalErrorHandling', () => {
'BrokenRenderAndUnmount componentWillUnmount', 'BrokenRenderAndUnmount componentWillUnmount',
]); ]);
expect(ReactNoop).toMatchRenderedOutput(null); 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 { class Bar extends React.Component {
componentWillUnmount() { componentWillUnmount() {
Scheduler.unstable_yieldValue('Bar unmount'); Scheduler.unstable_yieldValue('Bar unmount');
@ -1434,7 +1416,7 @@ describe('ReactIncrementalErrorHandling', () => {
} }
ReactNoop.render(<Foo />); ReactNoop.render(<Foo />);
expect(Scheduler).toFlushAndYield(['barRef attach']); await waitForAll(['barRef attach']);
expect(ReactNoop).toMatchRenderedOutput( expect(ReactNoop).toMatchRenderedOutput(
<div> <div>
<span prop="Bar" /> <span prop="Bar" />
@ -1444,7 +1426,7 @@ describe('ReactIncrementalErrorHandling', () => {
// Unmount // Unmount
ReactNoop.render(<Foo hide={true} />); ReactNoop.render(<Foo hide={true} />);
expect(Scheduler).toFlushAndThrow('Detach error'); expect(Scheduler).toFlushAndThrow('Detach error');
expect(Scheduler).toHaveYielded([ assertLog([
'barRef detach', 'barRef detach',
// Bar should unmount even though its ref threw an error while detaching // Bar should unmount even though its ref threw an error while detaching
'Bar unmount', 'Bar unmount',
@ -1465,7 +1447,7 @@ describe('ReactIncrementalErrorHandling', () => {
expect(Scheduler).toFlushAndThrow('Error!'); expect(Scheduler).toFlushAndThrow('Error!');
}); });
it('error boundaries capture non-errors', () => { it('error boundaries capture non-errors', async () => {
spyOnProd(console, 'error').mockImplementation(() => {}); spyOnProd(console, 'error').mockImplementation(() => {});
spyOnDev(console, 'error').mockImplementation(() => {}); spyOnDev(console, 'error').mockImplementation(() => {});
@ -1509,7 +1491,7 @@ describe('ReactIncrementalErrorHandling', () => {
</ErrorBoundary>, </ErrorBoundary>,
); );
expect(Scheduler).toFlushAndYield([ await waitForAll([
'ErrorBoundary (try)', 'ErrorBoundary (try)',
'Indirection', 'Indirection',
'BadRender', 'BadRender',
@ -1540,7 +1522,7 @@ describe('ReactIncrementalErrorHandling', () => {
// TODO: Error boundary does not catch promises // 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 { class ErrorBoundary extends React.Component {
state = {error: null}; state = {error: null};
componentDidCatch(error) { componentDidCatch(error) {
@ -1580,7 +1562,7 @@ describe('ReactIncrementalErrorHandling', () => {
</ErrorBoundary>, </ErrorBoundary>,
); );
expect(Scheduler).toFlushAndYield([ await waitForAll([
'ErrorBoundary (try)', 'ErrorBoundary (try)',
'throw', 'throw',
// Continue rendering siblings after BadRender throws // 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 // 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 // where we checked for the existence of didUpdate instead of didMount, and
// didMount was not defined. // didMount was not defined.
@ -1632,7 +1614,7 @@ describe('ReactIncrementalErrorHandling', () => {
} }
ReactNoop.render(<Parent step={1} />); ReactNoop.render(<Parent step={1} />);
expect(Scheduler).toFlushAndYieldThrough([ await waitFor([
'render', 'render',
'throw', 'throw',
'render', '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 { class ErrorBoundary extends React.Component {
state = {error: null, errorInfo: null}; state = {error: null, errorInfo: null};
componentDidCatch(error, errorInfo) { componentDidCatch(error, errorInfo) {
@ -1676,7 +1658,7 @@ describe('ReactIncrementalErrorHandling', () => {
<BrokenRender /> <BrokenRender />
</ErrorBoundary>, </ErrorBoundary>,
); );
expect(Scheduler).toFlushAndYield(['render error message']); await waitForAll(['render error message']);
expect(ReactNoop).toMatchRenderedOutput( expect(ReactNoop).toMatchRenderedOutput(
<span <span
prop={ 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 { class ErrorBoundary extends React.Component {
state = {error: null}; state = {error: null};
static getDerivedStateFromError(error, errorInfo) { static getDerivedStateFromError(error, errorInfo) {
@ -1712,13 +1694,13 @@ describe('ReactIncrementalErrorHandling', () => {
<BrokenRender /> <BrokenRender />
</ErrorBoundary>, </ErrorBoundary>,
); );
expect(Scheduler).toFlushWithoutYielding(); await waitForAll([]);
expect(ReactNoop).toMatchRenderedOutput( expect(ReactNoop).toMatchRenderedOutput(
<span prop="Caught an error: Hello" />, <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) { Error.prepareStackTrace = function (error, callsites) {
const stack = ['An error occurred:', error.message]; const stack = ['An error occurred:', error.message];
for (let i = 0; i < callsites.length; i++) { for (let i = 0; i < callsites.length; i++) {
@ -1762,7 +1744,7 @@ describe('ReactIncrementalErrorHandling', () => {
<BrokenRender /> <BrokenRender />
</ErrorBoundary>, </ErrorBoundary>,
); );
expect(Scheduler).toFlushAndYield(['render error message']); await waitForAll(['render error message']);
Error.prepareStackTrace = undefined; Error.prepareStackTrace = undefined;
expect(ReactNoop).toMatchRenderedOutput( expect(ReactNoop).toMatchRenderedOutput(
@ -1822,7 +1804,7 @@ describe('ReactIncrementalErrorHandling', () => {
}); });
// Render past the component that throws, then yield. // Render past the component that throws, then yield.
expect(Scheduler).toFlushAndYieldThrough(['Oops']); await waitFor(['Oops']);
expect(root).toMatchRenderedOutput(null); expect(root).toMatchRenderedOutput(null);
// Interleaved update. When the root completes, instead of throwing the // Interleaved update. When the root completes, instead of throwing the
// error, it should try rendering again. This update will cause it to // 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 // Render through just the default pri update. The low pri update remains on
// the queue. // the queue.
expect(Scheduler).toFlushAndYieldThrough(['Everything is fine.']); await waitFor(['Everything is fine.']);
// Schedule a discrete update on a child that triggers an error. // Schedule a discrete update on a child that triggers an error.
// The root should capture this error. But since there's still a pending // 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. // 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.'); expect(root).toMatchRenderedOutput('Everything is fine.');
}); });

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -15,6 +15,9 @@ let ReactNoop;
let Scheduler; let Scheduler;
let ContinuousEventPriority; let ContinuousEventPriority;
let act; let act;
let waitForAll;
let waitFor;
let assertLog;
describe('ReactIncrementalUpdates', () => { describe('ReactIncrementalUpdates', () => {
beforeEach(() => { beforeEach(() => {
@ -26,6 +29,11 @@ describe('ReactIncrementalUpdates', () => {
act = require('jest-react').act; act = require('jest-react').act;
ContinuousEventPriority = ContinuousEventPriority =
require('react-reconciler/constants').ContinuousEventPriority; require('react-reconciler/constants').ContinuousEventPriority;
const InternalTestUtils = require('internal-test-utils');
waitForAll = InternalTestUtils.waitForAll;
waitFor = InternalTestUtils.waitFor;
assertLog = InternalTestUtils.assertLog;
}); });
function flushNextRenderIfExpired() { function flushNextRenderIfExpired() {
@ -37,7 +45,7 @@ describe('ReactIncrementalUpdates', () => {
ReactNoop.flushSync(); ReactNoop.flushSync();
} }
it('applies updates in order of priority', () => { it('applies updates in order of priority', async () => {
let state; let state;
class Foo extends React.Component { class Foo extends React.Component {
state = {}; state = {};
@ -58,14 +66,14 @@ describe('ReactIncrementalUpdates', () => {
} }
ReactNoop.render(<Foo />); ReactNoop.render(<Foo />);
expect(Scheduler).toFlushAndYieldThrough(['commit']); await waitFor(['commit']);
expect(state).toEqual({a: 'a'}); expect(state).toEqual({a: 'a'});
expect(Scheduler).toFlushWithoutYielding(); await waitForAll([]);
expect(state).toEqual({a: 'a', b: 'b', c: 'c'}); 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; let state;
class Foo extends React.Component { class Foo extends React.Component {
state = {}; state = {};
@ -82,11 +90,11 @@ describe('ReactIncrementalUpdates', () => {
} }
ReactNoop.render(<Foo />); ReactNoop.render(<Foo />);
expect(Scheduler).toFlushWithoutYielding(); await waitForAll([]);
expect(state).toEqual({a: 'a', b: 'b', c: 'c'}); 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; let instance;
class Foo extends React.Component { class Foo extends React.Component {
state = {}; state = {};
@ -104,7 +112,7 @@ describe('ReactIncrementalUpdates', () => {
} }
ReactNoop.render(<Foo />); ReactNoop.render(<Foo />);
expect(Scheduler).toFlushAndYield(['render', 'componentDidMount']); await waitForAll(['render', 'componentDidMount']);
ReactNoop.flushSync(() => { ReactNoop.flushSync(() => {
React.startTransition(() => { React.startTransition(() => {
@ -122,14 +130,14 @@ describe('ReactIncrementalUpdates', () => {
// Even though a replaceState has been already scheduled, it hasn't been // Even though a replaceState has been already scheduled, it hasn't been
// flushed yet because it has async priority. // flushed yet because it has async priority.
expect(instance.state).toEqual({a: 'a', b: 'b'}); 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. // Now the rest of the updates are flushed, including the replaceState.
expect(instance.state).toEqual({c: 'c', d: 'd'}); 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; let instance;
class Foo extends React.Component { class Foo extends React.Component {
state = {}; state = {};
@ -140,7 +148,7 @@ describe('ReactIncrementalUpdates', () => {
} }
ReactNoop.render(<Foo />); ReactNoop.render(<Foo />);
expect(Scheduler).toFlushWithoutYielding(); await waitForAll([]);
function createUpdate(letter) { function createUpdate(letter) {
return () => { return () => {
@ -159,7 +167,7 @@ describe('ReactIncrementalUpdates', () => {
}); });
// Begin the updates but don't flush them yet // 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="" />); expect(ReactNoop).toMatchRenderedOutput(<span prop="" />);
// Schedule some more updates at different priorities // Schedule some more updates at different priorities
@ -174,11 +182,11 @@ describe('ReactIncrementalUpdates', () => {
// The sync updates should have flushed, but not the async ones. // The sync updates should have flushed, but not the async ones.
if (gate(flags => flags.enableUnifiedSyncLane)) { if (gate(flags => flags.enableUnifiedSyncLane)) {
expect(Scheduler).toHaveYielded(['d', 'e', 'f']); assertLog(['d', 'e', 'f']);
expect(ReactNoop).toMatchRenderedOutput(<span prop="def" />); expect(ReactNoop).toMatchRenderedOutput(<span prop="def" />);
} else { } else {
// Update d was dropped and replaced by e. // Update d was dropped and replaced by e.
expect(Scheduler).toHaveYielded(['e', 'f']); assertLog(['e', 'f']);
expect(ReactNoop).toMatchRenderedOutput(<span prop="ef" />); expect(ReactNoop).toMatchRenderedOutput(<span prop="ef" />);
} }
@ -186,7 +194,7 @@ describe('ReactIncrementalUpdates', () => {
// they should be processed again, to ensure that the terminal state // they should be processed again, to ensure that the terminal state
// is deterministic. // is deterministic.
if (gate(flags => !flags.enableUnifiedSyncLane)) { if (gate(flags => !flags.enableUnifiedSyncLane)) {
expect(Scheduler).toFlushAndYield([ await waitForAll([
// Since 'g' is in a transition, we'll process 'd' separately first. // Since 'g' is in a transition, we'll process 'd' separately first.
// That causes us to process 'd' with 'e' and 'f' rebased. // That causes us to process 'd' with 'e' and 'f' rebased.
'd', 'd',
@ -202,7 +210,7 @@ describe('ReactIncrementalUpdates', () => {
'g', 'g',
]); ]);
} else { } else {
expect(Scheduler).toFlushAndYield([ await waitForAll([
// Then we'll re-process everything for 'g'. // Then we'll re-process everything for 'g'.
'a', 'a',
'b', 'b',
@ -216,7 +224,7 @@ describe('ReactIncrementalUpdates', () => {
expect(ReactNoop).toMatchRenderedOutput(<span prop="abcdefg" />); 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; let instance;
class Foo extends React.Component { class Foo extends React.Component {
state = {}; state = {};
@ -227,7 +235,7 @@ describe('ReactIncrementalUpdates', () => {
} }
ReactNoop.render(<Foo />); ReactNoop.render(<Foo />);
expect(Scheduler).toFlushWithoutYielding(); await waitForAll([]);
function createUpdate(letter) { function createUpdate(letter) {
return () => { return () => {
@ -246,7 +254,7 @@ describe('ReactIncrementalUpdates', () => {
}); });
// Begin the updates but don't flush them yet // 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="" />); expect(ReactNoop).toMatchRenderedOutput(<span prop="" />);
// Schedule some more updates at different priorities // Schedule some more updates at different priorities
@ -264,10 +272,10 @@ describe('ReactIncrementalUpdates', () => {
// The sync updates should have flushed, but not the async ones. // The sync updates should have flushed, but not the async ones.
if (gate(flags => flags.enableUnifiedSyncLane)) { if (gate(flags => flags.enableUnifiedSyncLane)) {
expect(Scheduler).toHaveYielded(['d', 'e', 'f']); assertLog(['d', 'e', 'f']);
} else { } else {
// Update d was dropped and replaced by e. // Update d was dropped and replaced by e.
expect(Scheduler).toHaveYielded(['e', 'f']); assertLog(['e', 'f']);
} }
expect(ReactNoop).toMatchRenderedOutput(<span prop="f" />); expect(ReactNoop).toMatchRenderedOutput(<span prop="f" />);
@ -275,7 +283,7 @@ describe('ReactIncrementalUpdates', () => {
// they should be processed again, to ensure that the terminal state // they should be processed again, to ensure that the terminal state
// is deterministic. // is deterministic.
if (gate(flags => !flags.enableUnifiedSyncLane)) { if (gate(flags => !flags.enableUnifiedSyncLane)) {
expect(Scheduler).toFlushAndYield([ await waitForAll([
// Since 'g' is in a transition, we'll process 'd' separately first. // Since 'g' is in a transition, we'll process 'd' separately first.
// That causes us to process 'd' with 'e' and 'f' rebased. // That causes us to process 'd' with 'e' and 'f' rebased.
'd', 'd',
@ -291,7 +299,7 @@ describe('ReactIncrementalUpdates', () => {
'g', 'g',
]); ]);
} else { } else {
expect(Scheduler).toFlushAndYield([ await waitForAll([
// Then we'll re-process everything for 'g'. // Then we'll re-process everything for 'g'.
'a', 'a',
'b', 'b',
@ -305,7 +313,7 @@ describe('ReactIncrementalUpdates', () => {
expect(ReactNoop).toMatchRenderedOutput(<span prop="fg" />); 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; let instance;
class Foo extends React.Component { class Foo extends React.Component {
state = {}; state = {};
@ -315,7 +323,7 @@ describe('ReactIncrementalUpdates', () => {
} }
} }
ReactNoop.render(<Foo />); ReactNoop.render(<Foo />);
expect(Scheduler).toFlushWithoutYielding(); await waitForAll([]);
instance.setState({a: 'a'}); instance.setState({a: 'a'});
instance.setState({b: 'b'}); instance.setState({b: 'b'});
@ -324,11 +332,11 @@ describe('ReactIncrementalUpdates', () => {
instance.updater.enqueueReplaceState(instance, previousState => ({ instance.updater.enqueueReplaceState(instance, previousState => ({
previousState, previousState,
})); }));
expect(Scheduler).toFlushWithoutYielding(); await waitForAll([]);
expect(instance.state).toEqual({previousState: {a: 'a', b: 'b'}}); 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 { class Foo extends React.Component {
state = {}; state = {};
componentDidMount() { componentDidMount() {
@ -347,7 +355,7 @@ describe('ReactIncrementalUpdates', () => {
} }
ReactNoop.render(<Foo />); ReactNoop.render(<Foo />);
expect(Scheduler).toFlushAndYield([ await waitForAll([
'render', 'render',
'did mount', 'did mount',
'render', '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; let instance;
class Foo extends React.Component { class Foo extends React.Component {
@ -373,7 +381,7 @@ describe('ReactIncrementalUpdates', () => {
} }
} }
ReactNoop.render(<Foo />); ReactNoop.render(<Foo />);
expect(Scheduler).toFlushAndYield(['render']); await waitForAll(['render']);
ReactNoop.flushSync(() => { ReactNoop.flushSync(() => {
instance.setState({a: 'a'}); instance.setState({a: 'a'});
@ -384,17 +392,13 @@ describe('ReactIncrementalUpdates', () => {
expect(instance.state).toEqual({a: 'a', b: 'b'}); expect(instance.state).toEqual({a: 'a', b: 'b'});
if (gate(flags => flags.deferRenderPhaseUpdateToNextBatch)) { if (gate(flags => flags.deferRenderPhaseUpdateToNextBatch)) {
expect(Scheduler).toHaveYielded([ assertLog(['componentWillReceiveProps', 'render', 'render']);
'componentWillReceiveProps',
'render',
'render',
]);
} else { } 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; let instance;
class Foo extends React.Component { class Foo extends React.Component {
state = {}; state = {};
@ -406,7 +410,7 @@ describe('ReactIncrementalUpdates', () => {
} }
ReactNoop.render(<Foo />); ReactNoop.render(<Foo />);
expect(Scheduler).toFlushAndYield([ await waitForAll([
// Initial render // Initial render
'render', 'render',
]); ]);
@ -451,7 +455,7 @@ describe('ReactIncrementalUpdates', () => {
this.setState({a: 'a'}); this.setState({a: 'a'});
return {b: 'b'}; return {b: 'b'};
}); });
expect(Scheduler).toFlushAndYield( await waitForAll(
gate(flags => gate(flags =>
flags.deferRenderPhaseUpdateToNextBatch flags.deferRenderPhaseUpdateToNextBatch
? // In the new reconciler, updates inside the render phase are ? // In the new reconciler, updates inside the render phase are
@ -529,24 +533,20 @@ describe('ReactIncrementalUpdates', () => {
return null; return null;
} }
act(() => { act(async () => {
React.startTransition(() => { React.startTransition(() => {
ReactNoop.render(<App />); ReactNoop.render(<App />);
}); });
flushNextRenderIfExpired(); flushNextRenderIfExpired();
expect(Scheduler).toHaveYielded([]); assertLog([]);
expect(Scheduler).toFlushAndYield([ await waitForAll(['Render: 0', 'Commit: 0', 'Render: 1']);
'Render: 0',
'Commit: 0',
'Render: 1',
]);
Scheduler.unstable_advanceTime(10000); Scheduler.unstable_advanceTime(10000);
React.startTransition(() => { React.startTransition(() => {
setCount(2); setCount(2);
}); });
flushNextRenderIfExpired(); flushNextRenderIfExpired();
expect(Scheduler).toHaveYielded([]); assertLog([]);
}); });
}); });
@ -559,7 +559,7 @@ describe('ReactIncrementalUpdates', () => {
ReactNoop.flushSync(() => { ReactNoop.flushSync(() => {
ReactNoop.render(<Text text="A" />); ReactNoop.render(<Text text="A" />);
}); });
expect(Scheduler).toHaveYielded(['A']); assertLog(['A']);
Scheduler.unstable_advanceTime(10000); Scheduler.unstable_advanceTime(10000);
@ -567,7 +567,7 @@ describe('ReactIncrementalUpdates', () => {
ReactNoop.render(<Text text="B" />); ReactNoop.render(<Text text="B" />);
}); });
flushNextRenderIfExpired(); flushNextRenderIfExpired();
expect(Scheduler).toHaveYielded([]); assertLog([]);
}); });
it('regression: does not expire soon due to previous expired work', () => { it('regression: does not expire soon due to previous expired work', () => {
@ -581,7 +581,7 @@ describe('ReactIncrementalUpdates', () => {
}); });
Scheduler.unstable_advanceTime(10000); Scheduler.unstable_advanceTime(10000);
flushNextRenderIfExpired(); flushNextRenderIfExpired();
expect(Scheduler).toHaveYielded(['A']); assertLog(['A']);
Scheduler.unstable_advanceTime(10000); Scheduler.unstable_advanceTime(10000);
@ -589,7 +589,7 @@ describe('ReactIncrementalUpdates', () => {
ReactNoop.render(<Text text="B" />); ReactNoop.render(<Text text="B" />);
}); });
flushNextRenderIfExpired(); flushNextRenderIfExpired();
expect(Scheduler).toHaveYielded([]); assertLog([]);
}); });
it('when rebasing, does not exclude updates that were already committed, regardless of priority', async () => { it('when rebasing, does not exclude updates that were already committed, regardless of priority', async () => {
@ -620,7 +620,7 @@ describe('ReactIncrementalUpdates', () => {
await act(async () => { await act(async () => {
root.render(<App />); root.render(<App />);
}); });
expect(Scheduler).toHaveYielded(['Committed: ']); assertLog(['Committed: ']);
expect(root).toMatchRenderedOutput(null); expect(root).toMatchRenderedOutput(null);
await act(async () => { await act(async () => {
@ -633,13 +633,9 @@ describe('ReactIncrementalUpdates', () => {
); );
}); });
if (gate(flags => flags.enableUnifiedSyncLane)) { if (gate(flags => flags.enableUnifiedSyncLane)) {
expect(Scheduler).toHaveYielded([ assertLog(['Committed: B', 'Committed: BCD', 'Committed: ABCD']);
'Committed: B',
'Committed: BCD',
'Committed: ABCD',
]);
} else { } else {
expect(Scheduler).toHaveYielded([ assertLog([
// A and B are pending. B is higher priority, so we'll render that first. // A and B are pending. B is higher priority, so we'll render that first.
'Committed: B', 'Committed: B',
// Because A comes first in the queue, we're now in rebase mode. B must // Because A comes first in the queue, we're now in rebase mode. B must
@ -685,7 +681,7 @@ describe('ReactIncrementalUpdates', () => {
await act(async () => { await act(async () => {
root.render(<App />); root.render(<App />);
}); });
expect(Scheduler).toHaveYielded([]); assertLog([]);
expect(root).toMatchRenderedOutput(null); expect(root).toMatchRenderedOutput(null);
await act(async () => { await act(async () => {
@ -697,13 +693,9 @@ describe('ReactIncrementalUpdates', () => {
); );
}); });
if (gate(flags => flags.enableUnifiedSyncLane)) { if (gate(flags => flags.enableUnifiedSyncLane)) {
expect(Scheduler).toHaveYielded([ assertLog(['Committed: B', 'Committed: BCD', 'Committed: ABCD']);
'Committed: B',
'Committed: BCD',
'Committed: ABCD',
]);
} else { } else {
expect(Scheduler).toHaveYielded([ assertLog([
// A and B are pending. B is higher priority, so we'll render that first. // A and B are pending. B is higher priority, so we'll render that first.
'Committed: B', 'Committed: B',
// Because A comes first in the queue, we're now in rebase mode. B must // 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 useState;
let useEffect; let useEffect;
let act; let act;
let assertLog;
let waitFor;
let waitForPaint;
describe('ReactInterleavedUpdates', () => { describe('ReactInterleavedUpdates', () => {
beforeEach(() => { beforeEach(() => {
@ -17,6 +20,11 @@ describe('ReactInterleavedUpdates', () => {
startTransition = React.startTransition; startTransition = React.startTransition;
useState = React.useState; useState = React.useState;
useEffect = React.useEffect; useEffect = React.useEffect;
const InternalTestUtils = require('internal-test-utils');
assertLog = InternalTestUtils.assertLog;
waitFor = InternalTestUtils.waitFor;
waitForPaint = InternalTestUtils.waitForPaint;
}); });
function Text({text}) { function Text({text}) {
@ -53,7 +61,7 @@ describe('ReactInterleavedUpdates', () => {
</>, </>,
); );
}); });
expect(Scheduler).toHaveYielded([0, 0, 0]); assertLog([0, 0, 0]);
expect(root).toMatchRenderedOutput('000'); expect(root).toMatchRenderedOutput('000');
await act(async () => { await act(async () => {
@ -61,7 +69,7 @@ describe('ReactInterleavedUpdates', () => {
updateChildren(1); updateChildren(1);
}); });
// Partially render the children. Only the first one. // 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. // In an interleaved event, schedule an update on each of the children.
// Including the two that haven't rendered yet. // Including the two that haven't rendered yet.
@ -70,11 +78,11 @@ describe('ReactInterleavedUpdates', () => {
}); });
// We should continue rendering without including the interleaved updates. // We should continue rendering without including the interleaved updates.
expect(Scheduler).toFlushUntilNextPaint([1, 1]); await waitForPaint([1, 1]);
expect(root).toMatchRenderedOutput('111'); expect(root).toMatchRenderedOutput('111');
}); });
// The interleaved updates flush in a separate render. // The interleaved updates flush in a separate render.
expect(Scheduler).toHaveYielded([2, 2, 2]); assertLog([2, 2, 2]);
expect(root).toMatchRenderedOutput('222'); expect(root).toMatchRenderedOutput('222');
}); });
@ -96,7 +104,7 @@ describe('ReactInterleavedUpdates', () => {
await act(async () => { await act(async () => {
root.render(<App />); root.render(<App />);
}); });
expect(Scheduler).toHaveYielded(['A0', 'B0', 'C0']); assertLog(['A0', 'B0', 'C0']);
expect(root).toMatchRenderedOutput('A0B0C0'); expect(root).toMatchRenderedOutput('A0B0C0');
await act(async () => { await act(async () => {
@ -104,7 +112,7 @@ describe('ReactInterleavedUpdates', () => {
startTransition(() => { startTransition(() => {
setStep(1); setStep(1);
}); });
expect(Scheduler).toFlushAndYieldThrough(['A1', 'B1']); await waitFor(['A1', 'B1']);
// Schedule an interleaved update. This gets placed on a special queue. // Schedule an interleaved update. This gets placed on a special queue.
startTransition(() => { startTransition(() => {
@ -112,7 +120,7 @@ describe('ReactInterleavedUpdates', () => {
}); });
// Finish rendering the first update. // Finish rendering the first update.
expect(Scheduler).toFlushUntilNextPaint(['C1']); await waitForPaint(['C1']);
// Schedule another update. (In the regression case, this was treated // Schedule another update. (In the regression case, this was treated
// as a normal, non-interleaved update and it was inserted into the queue // as a normal, non-interleaved update and it was inserted into the queue
@ -122,7 +130,7 @@ describe('ReactInterleavedUpdates', () => {
}); });
}); });
// The last update should win. // The last update should win.
expect(Scheduler).toHaveYielded(['A3', 'B3', 'C3']); assertLog(['A3', 'B3', 'C3']);
expect(root).toMatchRenderedOutput('A3B3C3'); expect(root).toMatchRenderedOutput('A3B3C3');
}); });
}); });

View File

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