`act()` - s / flushPassiveEffects / Scheduler.unstable_flushWithoutYielding (#15591)
* s/flushPassiveEffects/unstable_flushWithoutYielding a first crack at flushing the scheduler manually from inside act(). uses unstable_flushWithoutYielding(). The tests that changed, mostly replaced toFlushAndYield(...) with toHaveYielded(). For some tests that tested the state of the tree before flushing effects (but still after updates), I replaced act() with bacthedUpdates(). * ugh lint * pass build, flushPassiveEffects returns nothing now * pass test-fire * flush all work (not just effects), add a compatibility mode of note, unstable_flushWithoutYielding now returns a boolean much like flushPassiveEffects * umd build for scheduler/unstable_mock, pass the fixture with it * add a comment to Shcduler.umd.js for why we're exporting unstable_flushWithoutYielding * run testsutilsact tests in both sync/concurrent modes * augh lint * use a feature flag for the missing mock scheduler warning I also tried writing a test for it, but couldn't get the scheduler to unmock. included the failing test. * Update ReactTestUtilsAct-test.js - pass the mock scheduler warning test, - rewrite some tests to use Scheduler.yieldValue - structure concurrent/legacy suites neatly * pass failing tests in batchedmode-test * fix pretty/lint/import errors * pass test-build * nit: pull .create(null) out of the act() call
This commit is contained in:
parent
aad5a264d2
commit
d278a3ff8b
|
@ -8,6 +8,8 @@ coverage
|
|||
|
||||
# production
|
||||
build
|
||||
public/scheduler-unstable_mock.development.js
|
||||
public/scheduler-unstable_mock.production.min.js
|
||||
public/react.development.js
|
||||
public/react.production.min.js
|
||||
public/react-dom.development.js
|
||||
|
|
|
@ -18,7 +18,7 @@
|
|||
},
|
||||
"scripts": {
|
||||
"start": "react-scripts start",
|
||||
"prestart": "cp ../../build/node_modules/react/umd/react.development.js ../../build/node_modules/react-dom/umd/react-dom.development.js ../../build/node_modules/react/umd/react.production.min.js ../../build/node_modules/react-dom/umd/react-dom.production.min.js ../../build/node_modules/react-dom/umd/react-dom-server.browser.development.js ../../build/node_modules/react-dom/umd/react-dom-server.browser.production.min.js ../../build/node_modules/react-dom/umd/react-dom-test-utils.development.js ../../build/node_modules/react-dom/umd/react-dom-test-utils.production.min.js public/",
|
||||
"prestart": "cp ../../build/node_modules/scheduler/umd/scheduler-unstable_mock.development.js ../../build/node_modules/scheduler/umd/scheduler-unstable_mock.production.min.js ../../build/node_modules/react/umd/react.development.js ../../build/node_modules/react-dom/umd/react-dom.development.js ../../build/node_modules/react/umd/react.production.min.js ../../build/node_modules/react-dom/umd/react-dom.production.min.js ../../build/node_modules/react-dom/umd/react-dom-server.browser.development.js ../../build/node_modules/react-dom/umd/react-dom-server.browser.production.min.js ../../build/node_modules/react-dom/umd/react-dom-test-utils.development.js ../../build/node_modules/react-dom/umd/react-dom-test-utils.production.min.js public/",
|
||||
"build": "react-scripts build && cp build/index.html build/200.html",
|
||||
"test": "react-scripts test --env=jsdom",
|
||||
"eject": "react-scripts eject"
|
||||
|
|
|
@ -7,7 +7,11 @@
|
|||
this page tests whether act runs properly in a browser.
|
||||
<br/>
|
||||
your console should say "5"
|
||||
<script src='scheduler-unstable_mock.development.js'></script>
|
||||
<script src='react.development.js'></script>
|
||||
<script type="text/javascript">
|
||||
window.React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.Scheduler = window.SchedulerMock
|
||||
</script>
|
||||
<script src='react-dom.development.js'></script>
|
||||
<script src='react-dom-test-utils.development.js'></script>
|
||||
<script>
|
||||
|
|
|
@ -0,0 +1,28 @@
|
|||
/**
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*
|
||||
* @emails react-core
|
||||
*/
|
||||
|
||||
let ReactFeatureFlags;
|
||||
let act;
|
||||
describe('mocked scheduler', () => {
|
||||
beforeEach(() => {
|
||||
jest.resetModules();
|
||||
ReactFeatureFlags = require('shared/ReactFeatureFlags');
|
||||
ReactFeatureFlags.warnAboutMissingMockScheduler = true;
|
||||
jest.unmock('scheduler');
|
||||
act = require('react-dom/test-utils').act;
|
||||
});
|
||||
it("should warn when the scheduler isn't mocked", () => {
|
||||
expect(() => act(() => {})).toWarnDev(
|
||||
[
|
||||
'Starting from React v17, the "scheduler" module will need to be mocked',
|
||||
],
|
||||
{withoutStack: true},
|
||||
);
|
||||
});
|
||||
});
|
|
@ -11,6 +11,7 @@ let React;
|
|||
let ReactDOM;
|
||||
let ReactTestUtils;
|
||||
let SchedulerTracing;
|
||||
let Scheduler;
|
||||
let act;
|
||||
let container;
|
||||
|
||||
|
@ -25,175 +26,386 @@ function sleep(period) {
|
|||
}
|
||||
|
||||
describe('ReactTestUtils.act()', () => {
|
||||
beforeEach(() => {
|
||||
jest.resetModules();
|
||||
React = require('react');
|
||||
ReactDOM = require('react-dom');
|
||||
ReactTestUtils = require('react-dom/test-utils');
|
||||
SchedulerTracing = require('scheduler/tracing');
|
||||
act = ReactTestUtils.act;
|
||||
container = document.createElement('div');
|
||||
document.body.appendChild(container);
|
||||
});
|
||||
afterEach(() => {
|
||||
ReactDOM.unmountComponentAtNode(container);
|
||||
document.body.removeChild(container);
|
||||
});
|
||||
// first we run all the tests with concurrent mode
|
||||
let concurrentRoot;
|
||||
function renderConcurrent(el, dom) {
|
||||
concurrentRoot = ReactDOM.unstable_createRoot(dom);
|
||||
concurrentRoot.render(el);
|
||||
}
|
||||
function unmountConcurrent(_dom) {
|
||||
if (concurrentRoot !== null) {
|
||||
concurrentRoot.unmount();
|
||||
concurrentRoot = null;
|
||||
}
|
||||
}
|
||||
runActTests('concurrent mode', renderConcurrent, unmountConcurrent);
|
||||
|
||||
describe('sync', () => {
|
||||
it('can use act to flush effects', () => {
|
||||
function App(props) {
|
||||
React.useEffect(props.callback);
|
||||
return null;
|
||||
}
|
||||
// and then in sync mode
|
||||
function renderSync(el, dom) {
|
||||
ReactDOM.render(el, dom);
|
||||
}
|
||||
function unmountSync(dom) {
|
||||
ReactDOM.unmountComponentAtNode(dom);
|
||||
}
|
||||
runActTests('legacy sync mode', renderSync, unmountSync);
|
||||
});
|
||||
|
||||
let calledLog = [];
|
||||
act(() => {
|
||||
ReactDOM.render(
|
||||
<App
|
||||
callback={() => {
|
||||
calledLog.push(calledLog.length);
|
||||
}}
|
||||
/>,
|
||||
document.createElement('div'),
|
||||
);
|
||||
});
|
||||
|
||||
expect(calledLog).toEqual([0]);
|
||||
function runActTests(label, render, unmount) {
|
||||
describe(label, () => {
|
||||
beforeEach(() => {
|
||||
jest.resetModules();
|
||||
React = require('react');
|
||||
ReactDOM = require('react-dom');
|
||||
ReactTestUtils = require('react-dom/test-utils');
|
||||
SchedulerTracing = require('scheduler/tracing');
|
||||
Scheduler = require('scheduler');
|
||||
act = ReactTestUtils.act;
|
||||
container = document.createElement('div');
|
||||
document.body.appendChild(container);
|
||||
});
|
||||
afterEach(() => {
|
||||
unmount(container);
|
||||
document.body.removeChild(container);
|
||||
});
|
||||
describe('sync', () => {
|
||||
it('can use act to flush effects', () => {
|
||||
function App() {
|
||||
React.useEffect(() => {
|
||||
Scheduler.yieldValue(100);
|
||||
});
|
||||
return null;
|
||||
}
|
||||
|
||||
it('flushes effects on every call', () => {
|
||||
function App(props) {
|
||||
let [ctr, setCtr] = React.useState(0);
|
||||
React.useEffect(() => {
|
||||
props.callback(ctr);
|
||||
act(() => {
|
||||
render(<App />, container);
|
||||
});
|
||||
return (
|
||||
<button id="button" onClick={() => setCtr(x => x + 1)}>
|
||||
{ctr}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
let calledCounter = 0;
|
||||
act(() => {
|
||||
ReactDOM.render(
|
||||
<App
|
||||
callback={val => {
|
||||
calledCounter = val;
|
||||
}}
|
||||
/>,
|
||||
container,
|
||||
);
|
||||
expect(Scheduler).toHaveYielded([100]);
|
||||
});
|
||||
const button = document.getElementById('button');
|
||||
function click() {
|
||||
button.dispatchEvent(new MouseEvent('click', {bubbles: true}));
|
||||
}
|
||||
|
||||
act(() => {
|
||||
click();
|
||||
click();
|
||||
click();
|
||||
it('flushes effects on every call', () => {
|
||||
function App() {
|
||||
let [ctr, setCtr] = React.useState(0);
|
||||
React.useEffect(() => {
|
||||
Scheduler.yieldValue(ctr);
|
||||
});
|
||||
return (
|
||||
<button id="button" onClick={() => setCtr(x => x + 1)}>
|
||||
{ctr}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
act(() => {
|
||||
render(<App />, container);
|
||||
});
|
||||
expect(Scheduler).toHaveYielded([0]);
|
||||
const button = container.querySelector('#button');
|
||||
function click() {
|
||||
button.dispatchEvent(new MouseEvent('click', {bubbles: true}));
|
||||
}
|
||||
|
||||
act(() => {
|
||||
click();
|
||||
click();
|
||||
click();
|
||||
});
|
||||
// it consolidates the 3 updates, then fires the effect
|
||||
expect(Scheduler).toHaveYielded([3]);
|
||||
act(click);
|
||||
expect(Scheduler).toHaveYielded([4]);
|
||||
act(click);
|
||||
expect(Scheduler).toHaveYielded([5]);
|
||||
expect(button.innerHTML).toBe('5');
|
||||
});
|
||||
expect(calledCounter).toBe(3);
|
||||
act(click);
|
||||
expect(calledCounter).toBe(4);
|
||||
act(click);
|
||||
expect(calledCounter).toBe(5);
|
||||
expect(button.innerHTML).toBe('5');
|
||||
});
|
||||
|
||||
it('should flush effects recursively', () => {
|
||||
function App() {
|
||||
let [ctr, setCtr] = React.useState(0);
|
||||
React.useEffect(() => {
|
||||
if (ctr < 5) {
|
||||
setCtr(x => x + 1);
|
||||
it("should keep flushing effects until the're done", () => {
|
||||
function App() {
|
||||
let [ctr, setCtr] = React.useState(0);
|
||||
React.useEffect(() => {
|
||||
if (ctr < 5) {
|
||||
setCtr(x => x + 1);
|
||||
}
|
||||
});
|
||||
return ctr;
|
||||
}
|
||||
|
||||
act(() => {
|
||||
render(<App />, container);
|
||||
});
|
||||
|
||||
expect(container.innerHTML).toBe('5');
|
||||
});
|
||||
|
||||
it('warns if a setState is called outside of act(...)', () => {
|
||||
let setValue = null;
|
||||
function App() {
|
||||
let [value, _setValue] = React.useState(0);
|
||||
setValue = _setValue;
|
||||
return value;
|
||||
}
|
||||
|
||||
act(() => {
|
||||
render(<App />, container);
|
||||
});
|
||||
|
||||
expect(() => setValue(1)).toWarnDev([
|
||||
'An update to App inside a test was not wrapped in act(...).',
|
||||
]);
|
||||
});
|
||||
describe('fake timers', () => {
|
||||
beforeEach(() => {
|
||||
jest.useFakeTimers();
|
||||
});
|
||||
afterEach(() => {
|
||||
jest.useRealTimers();
|
||||
});
|
||||
it('lets a ticker update', () => {
|
||||
function App() {
|
||||
let [toggle, setToggle] = React.useState(0);
|
||||
React.useEffect(() => {
|
||||
let timeout = setTimeout(() => {
|
||||
setToggle(1);
|
||||
}, 200);
|
||||
return () => clearTimeout(timeout);
|
||||
}, []);
|
||||
return toggle;
|
||||
}
|
||||
});
|
||||
return ctr;
|
||||
}
|
||||
|
||||
act(() => {
|
||||
ReactDOM.render(<App />, container);
|
||||
act(() => {
|
||||
render(<App />, container);
|
||||
});
|
||||
act(() => {
|
||||
jest.runAllTimers();
|
||||
});
|
||||
|
||||
expect(container.innerHTML).toBe('1');
|
||||
});
|
||||
it('can use the async version to catch microtasks', async () => {
|
||||
function App() {
|
||||
let [toggle, setToggle] = React.useState(0);
|
||||
React.useEffect(() => {
|
||||
// just like the previous test, except we
|
||||
// use a promise and schedule the update
|
||||
// after it resolves
|
||||
sleep(200).then(() => setToggle(1));
|
||||
}, []);
|
||||
return toggle;
|
||||
}
|
||||
|
||||
act(() => {
|
||||
render(<App />, container);
|
||||
});
|
||||
await act(async () => {
|
||||
jest.runAllTimers();
|
||||
});
|
||||
|
||||
expect(container.innerHTML).toBe('1');
|
||||
});
|
||||
it('can handle cascading promises with fake timers', async () => {
|
||||
// this component triggers an effect, that waits a tick,
|
||||
// then sets state. repeats this 5 times.
|
||||
function App() {
|
||||
let [state, setState] = React.useState(0);
|
||||
async function ticker() {
|
||||
await null;
|
||||
setState(x => x + 1);
|
||||
}
|
||||
React.useEffect(
|
||||
() => {
|
||||
ticker();
|
||||
},
|
||||
[Math.min(state, 4)],
|
||||
);
|
||||
return state;
|
||||
}
|
||||
|
||||
await act(async () => {
|
||||
render(<App />, container);
|
||||
});
|
||||
|
||||
// all 5 ticks present and accounted for
|
||||
expect(container.innerHTML).toBe('5');
|
||||
});
|
||||
it('flushes immediate re-renders with act', () => {
|
||||
function App() {
|
||||
let [ctr, setCtr] = React.useState(0);
|
||||
React.useEffect(() => {
|
||||
if (ctr === 0) {
|
||||
setCtr(1);
|
||||
}
|
||||
const timeout = setTimeout(() => setCtr(2), 1000);
|
||||
return () => clearTimeout(timeout);
|
||||
});
|
||||
return ctr;
|
||||
}
|
||||
|
||||
act(() => {
|
||||
render(<App />, container);
|
||||
// Since effects haven't been flushed yet, this does not advance the timer
|
||||
jest.runAllTimers();
|
||||
});
|
||||
|
||||
expect(container.innerHTML).toBe('1');
|
||||
|
||||
act(() => {
|
||||
jest.runAllTimers();
|
||||
});
|
||||
|
||||
expect(container.innerHTML).toBe('2');
|
||||
});
|
||||
});
|
||||
|
||||
expect(container.innerHTML).toBe('5');
|
||||
});
|
||||
|
||||
it('detects setState being called outside of act(...)', () => {
|
||||
let setValue = null;
|
||||
function App() {
|
||||
let [value, _setValue] = React.useState(0);
|
||||
setValue = _setValue;
|
||||
return (
|
||||
<button id="button" onClick={() => setValue(2)}>
|
||||
{value}
|
||||
</button>
|
||||
it('warns if you return a value inside act', () => {
|
||||
expect(() => act(() => null)).toWarnDev(
|
||||
[
|
||||
'The callback passed to act(...) function must return undefined, or a Promise.',
|
||||
],
|
||||
{withoutStack: true},
|
||||
);
|
||||
expect(() => act(() => 123)).toWarnDev(
|
||||
[
|
||||
'The callback passed to act(...) function must return undefined, or a Promise.',
|
||||
],
|
||||
{withoutStack: true},
|
||||
);
|
||||
});
|
||||
|
||||
it('warns if you try to await a sync .act call', () => {
|
||||
expect(() => act(() => {}).then(() => {})).toWarnDev(
|
||||
[
|
||||
'Do not await the result of calling act(...) with sync logic, it is not a Promise.',
|
||||
],
|
||||
{withoutStack: true},
|
||||
);
|
||||
}
|
||||
let button;
|
||||
act(() => {
|
||||
ReactDOM.render(<App />, container);
|
||||
button = container.querySelector('#button');
|
||||
button.dispatchEvent(new MouseEvent('click', {bubbles: true}));
|
||||
});
|
||||
expect(button.innerHTML).toBe('2');
|
||||
expect(() => setValue(1)).toWarnDev([
|
||||
'An update to App inside a test was not wrapped in act(...).',
|
||||
]);
|
||||
});
|
||||
describe('fake timers', () => {
|
||||
beforeEach(() => {
|
||||
jest.useFakeTimers();
|
||||
});
|
||||
afterEach(() => {
|
||||
jest.useRealTimers();
|
||||
});
|
||||
it('lets a ticker update', () => {
|
||||
describe('asynchronous tests', () => {
|
||||
it('can handle timers', async () => {
|
||||
function App() {
|
||||
let [toggle, setToggle] = React.useState(0);
|
||||
let [ctr, setCtr] = React.useState(0);
|
||||
function doSomething() {
|
||||
setTimeout(() => {
|
||||
setCtr(1);
|
||||
}, 50);
|
||||
}
|
||||
|
||||
React.useEffect(() => {
|
||||
let timeout = setTimeout(() => {
|
||||
setToggle(1);
|
||||
}, 200);
|
||||
return () => clearTimeout(timeout);
|
||||
doSomething();
|
||||
}, []);
|
||||
return toggle;
|
||||
return ctr;
|
||||
}
|
||||
|
||||
act(() => {
|
||||
ReactDOM.render(<App />, container);
|
||||
});
|
||||
act(() => {
|
||||
jest.runAllTimers();
|
||||
});
|
||||
|
||||
expect(container.innerHTML).toBe('1');
|
||||
});
|
||||
it('can use the async version to catch microtasks', async () => {
|
||||
function App() {
|
||||
let [toggle, setToggle] = React.useState(0);
|
||||
React.useEffect(() => {
|
||||
// just like the previous test, except we
|
||||
// use a promise and schedule the update
|
||||
// after it resolves
|
||||
sleep(200).then(() => setToggle(1));
|
||||
}, []);
|
||||
return toggle;
|
||||
}
|
||||
|
||||
act(() => {
|
||||
ReactDOM.render(<App />, container);
|
||||
render(<App />, container);
|
||||
});
|
||||
await act(async () => {
|
||||
jest.runAllTimers();
|
||||
await sleep(100);
|
||||
});
|
||||
|
||||
expect(container.innerHTML).toBe('1');
|
||||
});
|
||||
it('can handle cascading promises with fake timers', async () => {
|
||||
|
||||
it('can handle async/await', async () => {
|
||||
function App() {
|
||||
let [ctr, setCtr] = React.useState(0);
|
||||
async function someAsyncFunction() {
|
||||
// queue a bunch of promises to be sure they all flush
|
||||
await null;
|
||||
await null;
|
||||
await null;
|
||||
setCtr(1);
|
||||
}
|
||||
React.useEffect(() => {
|
||||
someAsyncFunction();
|
||||
}, []);
|
||||
return ctr;
|
||||
}
|
||||
|
||||
await act(async () => {
|
||||
act(() => {
|
||||
render(<App />, container);
|
||||
});
|
||||
// pending promises will close before this ends
|
||||
});
|
||||
expect(container.innerHTML).toEqual('1');
|
||||
});
|
||||
|
||||
it('warns if you do not await an act call', async () => {
|
||||
spyOnDevAndProd(console, 'error');
|
||||
act(async () => {});
|
||||
// it's annoying that we have to wait a tick before this warning comes in
|
||||
await sleep(0);
|
||||
if (__DEV__) {
|
||||
expect(console.error.calls.count()).toEqual(1);
|
||||
expect(console.error.calls.argsFor(0)[0]).toMatch(
|
||||
'You called act(async () => ...) without await.',
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
it('warns if you try to interleave multiple act calls', async () => {
|
||||
spyOnDevAndProd(console, 'error');
|
||||
// let's try to cheat and spin off a 'thread' with an act call
|
||||
(async () => {
|
||||
await act(async () => {
|
||||
await sleep(50);
|
||||
});
|
||||
})();
|
||||
|
||||
await act(async () => {
|
||||
await sleep(100);
|
||||
});
|
||||
|
||||
await sleep(150);
|
||||
if (__DEV__) {
|
||||
expect(console.error).toHaveBeenCalledTimes(1);
|
||||
}
|
||||
});
|
||||
|
||||
it('commits and effects are guaranteed to be flushed', async () => {
|
||||
function App() {
|
||||
let [state, setState] = React.useState(0);
|
||||
async function something() {
|
||||
await null;
|
||||
setState(1);
|
||||
}
|
||||
React.useEffect(() => {
|
||||
something();
|
||||
}, []);
|
||||
React.useEffect(() => {
|
||||
Scheduler.yieldValue(state);
|
||||
});
|
||||
return state;
|
||||
}
|
||||
|
||||
await act(async () => {
|
||||
act(() => {
|
||||
render(<App />, container);
|
||||
});
|
||||
expect(container.innerHTML).toBe('0');
|
||||
expect(Scheduler).toHaveYielded([0]);
|
||||
});
|
||||
// this may seem odd, but it matches user behaviour -
|
||||
// a flash of "0" followed by "1"
|
||||
|
||||
expect(container.innerHTML).toBe('1');
|
||||
expect(Scheduler).toHaveYielded([1]);
|
||||
});
|
||||
|
||||
it('propagates errors', async () => {
|
||||
let err;
|
||||
try {
|
||||
await act(async () => {
|
||||
await sleep(100);
|
||||
throw new Error('some error');
|
||||
});
|
||||
} catch (_err) {
|
||||
err = _err;
|
||||
} finally {
|
||||
expect(err instanceof Error).toBe(true);
|
||||
expect(err.message).toBe('some error');
|
||||
}
|
||||
});
|
||||
it('can handle cascading promises', async () => {
|
||||
// this component triggers an effect, that waits a tick,
|
||||
// then sets state. repeats this 5 times.
|
||||
function App() {
|
||||
|
@ -204,303 +416,100 @@ describe('ReactTestUtils.act()', () => {
|
|||
}
|
||||
React.useEffect(
|
||||
() => {
|
||||
Scheduler.yieldValue(state);
|
||||
ticker();
|
||||
},
|
||||
[Math.min(state, 4)],
|
||||
);
|
||||
return state;
|
||||
}
|
||||
const el = document.createElement('div');
|
||||
await act(async () => {
|
||||
ReactDOM.render(<App />, el);
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
render(<App />, container);
|
||||
});
|
||||
// all 5 ticks present and accounted for
|
||||
expect(el.innerHTML).toBe('5');
|
||||
expect(Scheduler).toHaveYielded([0, 1, 2, 3, 4]);
|
||||
expect(container.innerHTML).toBe('5');
|
||||
});
|
||||
it('flushes immediate re-renders with act', () => {
|
||||
function App() {
|
||||
let [ctr, setCtr] = React.useState(0);
|
||||
React.useEffect(() => {
|
||||
if (ctr === 0) {
|
||||
setCtr(1);
|
||||
}
|
||||
const timeout = setTimeout(() => setCtr(2), 1000);
|
||||
return () => clearTimeout(timeout);
|
||||
});
|
||||
|
||||
describe('interaction tracing', () => {
|
||||
if (__DEV__) {
|
||||
it('should correctly trace interactions for sync roots', () => {
|
||||
let expectedInteraction;
|
||||
|
||||
const Component = jest.fn(() => {
|
||||
expect(expectedInteraction).toBeDefined();
|
||||
|
||||
const interactions = SchedulerTracing.unstable_getCurrent();
|
||||
expect(interactions.size).toBe(1);
|
||||
expect(interactions).toContain(expectedInteraction);
|
||||
|
||||
return null;
|
||||
});
|
||||
return ctr;
|
||||
}
|
||||
|
||||
act(() => {
|
||||
ReactDOM.render(<App />, container);
|
||||
// Since the effects won't be flushed yet, this does not advance the timer
|
||||
jest.runAllTimers();
|
||||
});
|
||||
act(() => {
|
||||
SchedulerTracing.unstable_trace(
|
||||
'mount traced inside act',
|
||||
performance.now(),
|
||||
() => {
|
||||
const interactions = SchedulerTracing.unstable_getCurrent();
|
||||
expect(interactions.size).toBe(1);
|
||||
expectedInteraction = Array.from(interactions)[0];
|
||||
|
||||
expect(container.innerHTML).toBe('1');
|
||||
render(<Component />, container);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
act(() => {
|
||||
jest.runAllTimers();
|
||||
});
|
||||
act(() => {
|
||||
SchedulerTracing.unstable_trace(
|
||||
'update traced inside act',
|
||||
performance.now(),
|
||||
() => {
|
||||
const interactions = SchedulerTracing.unstable_getCurrent();
|
||||
expect(interactions.size).toBe(1);
|
||||
expectedInteraction = Array.from(interactions)[0];
|
||||
|
||||
expect(container.innerHTML).toBe('2');
|
||||
});
|
||||
});
|
||||
render(<Component />, container);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
it('warns if you return a value inside act', () => {
|
||||
expect(() => act(() => null)).toWarnDev(
|
||||
[
|
||||
'The callback passed to act(...) function must return undefined, or a Promise.',
|
||||
],
|
||||
{withoutStack: true},
|
||||
);
|
||||
expect(() => act(() => 123)).toWarnDev(
|
||||
[
|
||||
'The callback passed to act(...) function must return undefined, or a Promise.',
|
||||
],
|
||||
{withoutStack: true},
|
||||
);
|
||||
});
|
||||
const secondContainer = document.createElement('div');
|
||||
|
||||
it('warns if you try to await an .act call', () => {
|
||||
expect(() => act(() => {}).then(() => {})).toWarnDev(
|
||||
[
|
||||
'Do not await the result of calling act(...) with sync logic, it is not a Promise.',
|
||||
],
|
||||
{withoutStack: true},
|
||||
);
|
||||
});
|
||||
});
|
||||
describe('asynchronous tests', () => {
|
||||
it('can handle timers', async () => {
|
||||
function App() {
|
||||
let [ctr, setCtr] = React.useState(0);
|
||||
function doSomething() {
|
||||
setTimeout(() => {
|
||||
setCtr(1);
|
||||
}, 50);
|
||||
}
|
||||
|
||||
React.useEffect(() => {
|
||||
doSomething();
|
||||
}, []);
|
||||
return ctr;
|
||||
}
|
||||
const el = document.createElement('div');
|
||||
await act(async () => {
|
||||
act(() => {
|
||||
ReactDOM.render(<App />, el);
|
||||
});
|
||||
|
||||
await sleep(100);
|
||||
expect(el.innerHTML).toBe('1');
|
||||
});
|
||||
});
|
||||
|
||||
it('can handle async/await', async () => {
|
||||
function App() {
|
||||
let [ctr, setCtr] = React.useState(0);
|
||||
async function someAsyncFunction() {
|
||||
// queue a bunch of promises to be sure they all flush
|
||||
await null;
|
||||
await null;
|
||||
await null;
|
||||
setCtr(1);
|
||||
}
|
||||
React.useEffect(() => {
|
||||
someAsyncFunction();
|
||||
}, []);
|
||||
return ctr;
|
||||
}
|
||||
const el = document.createElement('div');
|
||||
|
||||
await act(async () => {
|
||||
act(() => {
|
||||
ReactDOM.render(<App />, el);
|
||||
});
|
||||
// pending promises will close before this ends
|
||||
});
|
||||
expect(el.innerHTML).toEqual('1');
|
||||
});
|
||||
|
||||
it('warns if you do not await an act call', async () => {
|
||||
spyOnDevAndProd(console, 'error');
|
||||
act(async () => {});
|
||||
// it's annoying that we have to wait a tick before this warning comes in
|
||||
await sleep(0);
|
||||
if (__DEV__) {
|
||||
expect(console.error.calls.count()).toEqual(1);
|
||||
expect(console.error.calls.argsFor(0)[0]).toMatch(
|
||||
'You called act(async () => ...) without await.',
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
it('warns if you try to interleave multiple act calls', async () => {
|
||||
spyOnDevAndProd(console, 'error');
|
||||
// let's try to cheat and spin off a 'thread' with an act call
|
||||
(async () => {
|
||||
await act(async () => {
|
||||
await sleep(50);
|
||||
});
|
||||
})();
|
||||
|
||||
await act(async () => {
|
||||
await sleep(100);
|
||||
});
|
||||
|
||||
await sleep(150);
|
||||
if (__DEV__) {
|
||||
expect(console.error).toHaveBeenCalledTimes(1);
|
||||
}
|
||||
});
|
||||
|
||||
it('commits and effects are guaranteed to be flushed', async () => {
|
||||
function App(props) {
|
||||
let [state, setState] = React.useState(0);
|
||||
async function something() {
|
||||
await null;
|
||||
setState(1);
|
||||
}
|
||||
React.useEffect(() => {
|
||||
something();
|
||||
}, []);
|
||||
React.useEffect(() => {
|
||||
props.callback();
|
||||
});
|
||||
return state;
|
||||
}
|
||||
let ctr = 0;
|
||||
const div = document.createElement('div');
|
||||
|
||||
await act(async () => {
|
||||
act(() => {
|
||||
ReactDOM.render(<App callback={() => ctr++} />, div);
|
||||
});
|
||||
expect(div.innerHTML).toBe('0');
|
||||
expect(ctr).toBe(1);
|
||||
});
|
||||
// this may seem odd, but it matches user behaviour -
|
||||
// a flash of "0" followed by "1"
|
||||
|
||||
expect(div.innerHTML).toBe('1');
|
||||
expect(ctr).toBe(2);
|
||||
});
|
||||
|
||||
it('propagates errors', async () => {
|
||||
let err;
|
||||
try {
|
||||
await act(async () => {
|
||||
throw new Error('some error');
|
||||
});
|
||||
} catch (_err) {
|
||||
err = _err;
|
||||
} finally {
|
||||
expect(err instanceof Error).toBe(true);
|
||||
expect(err.message).toBe('some error');
|
||||
}
|
||||
});
|
||||
it('can handle cascading promises', async () => {
|
||||
// this component triggers an effect, that waits a tick,
|
||||
// then sets state. repeats this 5 times.
|
||||
function App() {
|
||||
let [state, setState] = React.useState(0);
|
||||
async function ticker() {
|
||||
await null;
|
||||
setState(x => x + 1);
|
||||
}
|
||||
React.useEffect(
|
||||
() => {
|
||||
ticker();
|
||||
},
|
||||
[Math.min(state, 4)],
|
||||
);
|
||||
return state;
|
||||
}
|
||||
const el = document.createElement('div');
|
||||
await act(async () => {
|
||||
ReactDOM.render(<App />, el);
|
||||
});
|
||||
// all 5 ticks present and accounted for
|
||||
expect(el.innerHTML).toBe('5');
|
||||
});
|
||||
});
|
||||
|
||||
describe('interaction tracing', () => {
|
||||
if (__DEV__) {
|
||||
it('should correctly trace interactions for sync roots', () => {
|
||||
let expectedInteraction;
|
||||
|
||||
const Component = jest.fn(() => {
|
||||
expect(expectedInteraction).toBeDefined();
|
||||
|
||||
const interactions = SchedulerTracing.unstable_getCurrent();
|
||||
expect(interactions.size).toBe(1);
|
||||
expect(interactions).toContain(expectedInteraction);
|
||||
|
||||
return null;
|
||||
});
|
||||
|
||||
act(() => {
|
||||
SchedulerTracing.unstable_trace(
|
||||
'mount traced inside act',
|
||||
'mount traced outside act',
|
||||
performance.now(),
|
||||
() => {
|
||||
const interactions = SchedulerTracing.unstable_getCurrent();
|
||||
expect(interactions.size).toBe(1);
|
||||
expectedInteraction = Array.from(interactions)[0];
|
||||
act(() => {
|
||||
const interactions = SchedulerTracing.unstable_getCurrent();
|
||||
expect(interactions.size).toBe(1);
|
||||
expectedInteraction = Array.from(interactions)[0];
|
||||
|
||||
ReactDOM.render(<Component />, container);
|
||||
render(<Component />, secondContainer);
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
act(() => {
|
||||
SchedulerTracing.unstable_trace(
|
||||
'update traced inside act',
|
||||
'update traced outside act',
|
||||
performance.now(),
|
||||
() => {
|
||||
const interactions = SchedulerTracing.unstable_getCurrent();
|
||||
expect(interactions.size).toBe(1);
|
||||
expectedInteraction = Array.from(interactions)[0];
|
||||
act(() => {
|
||||
const interactions = SchedulerTracing.unstable_getCurrent();
|
||||
expect(interactions.size).toBe(1);
|
||||
expectedInteraction = Array.from(interactions)[0];
|
||||
|
||||
ReactDOM.render(<Component />, container);
|
||||
render(<Component />, secondContainer);
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
expect(Component).toHaveBeenCalledTimes(4);
|
||||
unmount(secondContainer);
|
||||
});
|
||||
|
||||
const secondContainer = document.createElement('div');
|
||||
|
||||
SchedulerTracing.unstable_trace(
|
||||
'mount traced outside act',
|
||||
performance.now(),
|
||||
() => {
|
||||
act(() => {
|
||||
const interactions = SchedulerTracing.unstable_getCurrent();
|
||||
expect(interactions.size).toBe(1);
|
||||
expectedInteraction = Array.from(interactions)[0];
|
||||
|
||||
ReactDOM.render(<Component />, secondContainer);
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
SchedulerTracing.unstable_trace(
|
||||
'update traced outside act',
|
||||
performance.now(),
|
||||
() => {
|
||||
act(() => {
|
||||
const interactions = SchedulerTracing.unstable_getCurrent();
|
||||
expect(interactions.size).toBe(1);
|
||||
expectedInteraction = Array.from(interactions)[0];
|
||||
|
||||
ReactDOM.render(<Component />, secondContainer);
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
expect(Component).toHaveBeenCalledTimes(4);
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
@ -12,7 +12,9 @@ import type {Thenable} from 'react-reconciler/src/ReactFiberScheduler';
|
|||
import warningWithoutStack from 'shared/warningWithoutStack';
|
||||
import ReactDOM from 'react-dom';
|
||||
import ReactSharedInternals from 'shared/ReactSharedInternals';
|
||||
import {warnAboutMissingMockScheduler} from 'shared/ReactFeatureFlags';
|
||||
import enqueueTask from 'shared/enqueueTask';
|
||||
import * as Scheduler from 'scheduler';
|
||||
|
||||
// Keep in sync with ReactDOMUnstableNativeDependencies.js
|
||||
// ReactDOM.js, and ReactTestUtils.js:
|
||||
|
@ -40,16 +42,33 @@ const {ReactShouldWarnActingUpdates} = ReactSharedInternals;
|
|||
// this implementation should be exactly the same in
|
||||
// ReactTestUtilsAct.js, ReactTestRendererAct.js, createReactNoop.js
|
||||
|
||||
// we track the 'depth' of the act() calls with this counter,
|
||||
// so we can tell if any async act() calls try to run in parallel.
|
||||
let actingUpdatesScopeDepth = 0;
|
||||
let hasWarnedAboutMissingMockScheduler = false;
|
||||
const flushWork =
|
||||
Scheduler.unstable_flushWithoutYielding ||
|
||||
function() {
|
||||
if (warnAboutMissingMockScheduler === true) {
|
||||
if (hasWarnedAboutMissingMockScheduler === false) {
|
||||
warningWithoutStack(
|
||||
null,
|
||||
'Starting from React v17, the "scheduler" module will need to be mocked ' +
|
||||
'to guarantee consistent behaviour across tests and browsers. To fix this, add the following ' +
|
||||
"to the top of your tests, or in your framework's global config file -\n\n" +
|
||||
'As an example, for jest - \n' +
|
||||
"jest.mock('scheduler', () => require.requireActual('scheduler/unstable_mock'));\n\n" +
|
||||
'For more info, visit https://fb.me/react-mock-scheduler',
|
||||
);
|
||||
hasWarnedAboutMissingMockScheduler = true;
|
||||
}
|
||||
}
|
||||
while (flushPassiveEffects()) {}
|
||||
};
|
||||
|
||||
function flushEffectsAndMicroTasks(onDone: (err: ?Error) => void) {
|
||||
function flushWorkAndMicroTasks(onDone: (err: ?Error) => void) {
|
||||
try {
|
||||
flushPassiveEffects();
|
||||
flushWork();
|
||||
enqueueTask(() => {
|
||||
if (flushPassiveEffects()) {
|
||||
flushEffectsAndMicroTasks(onDone);
|
||||
if (flushWork()) {
|
||||
flushWorkAndMicroTasks(onDone);
|
||||
} else {
|
||||
onDone();
|
||||
}
|
||||
|
@ -59,6 +78,11 @@ function flushEffectsAndMicroTasks(onDone: (err: ?Error) => void) {
|
|||
}
|
||||
}
|
||||
|
||||
// we track the 'depth' of the act() calls with this counter,
|
||||
// so we can tell if any async act() calls try to run in parallel.
|
||||
|
||||
let actingUpdatesScopeDepth = 0;
|
||||
|
||||
function act(callback: () => Thenable) {
|
||||
let previousActingUpdatesScopeDepth;
|
||||
if (__DEV__) {
|
||||
|
@ -119,7 +143,7 @@ function act(callback: () => Thenable) {
|
|||
called = true;
|
||||
result.then(
|
||||
() => {
|
||||
flushEffectsAndMicroTasks((err: ?Error) => {
|
||||
flushWorkAndMicroTasks((err: ?Error) => {
|
||||
onDone();
|
||||
if (err) {
|
||||
reject(err);
|
||||
|
@ -147,7 +171,7 @@ function act(callback: () => Thenable) {
|
|||
|
||||
// flush effects until none remain, and cleanup
|
||||
try {
|
||||
while (flushPassiveEffects()) {}
|
||||
flushWork();
|
||||
onDone();
|
||||
} catch (err) {
|
||||
onDone();
|
||||
|
|
|
@ -32,7 +32,10 @@ import warning from 'shared/warning';
|
|||
import enqueueTask from 'shared/enqueueTask';
|
||||
import ReactSharedInternals from 'shared/ReactSharedInternals';
|
||||
import warningWithoutStack from 'shared/warningWithoutStack';
|
||||
import {enableEventAPI} from 'shared/ReactFeatureFlags';
|
||||
import {
|
||||
warnAboutMissingMockScheduler,
|
||||
enableEventAPI,
|
||||
} from 'shared/ReactFeatureFlags';
|
||||
import {ConcurrentRoot, BatchedRoot, LegacyRoot} from 'shared/ReactRootTags';
|
||||
|
||||
type EventTargetChildElement = {
|
||||
|
@ -652,14 +655,33 @@ function createReactNoop(reconciler: Function, useMutation: boolean) {
|
|||
// this act() implementation should be exactly the same in
|
||||
// ReactTestUtilsAct.js, ReactTestRendererAct.js, createReactNoop.js
|
||||
|
||||
let actingUpdatesScopeDepth = 0;
|
||||
let hasWarnedAboutMissingMockScheduler = false;
|
||||
const flushWork =
|
||||
Scheduler.unstable_flushWithoutYielding ||
|
||||
function() {
|
||||
if (warnAboutMissingMockScheduler === true) {
|
||||
if (hasWarnedAboutMissingMockScheduler === false) {
|
||||
warningWithoutStack(
|
||||
null,
|
||||
'Starting from React v17, the "scheduler" module will need to be mocked ' +
|
||||
'to guarantee consistent behaviour across tests and browsers. To fix this, add the following ' +
|
||||
"to the top of your tests, or in your framework's global config file -\n\n" +
|
||||
'As an example, for jest - \n' +
|
||||
"jest.mock('scheduler', () => require.requireActual('scheduler/unstable_mock'));\n\n" +
|
||||
'For more info, visit https://fb.me/react-mock-scheduler',
|
||||
);
|
||||
hasWarnedAboutMissingMockScheduler = true;
|
||||
}
|
||||
}
|
||||
while (flushPassiveEffects()) {}
|
||||
};
|
||||
|
||||
function flushEffectsAndMicroTasks(onDone: (err: ?Error) => void) {
|
||||
function flushWorkAndMicroTasks(onDone: (err: ?Error) => void) {
|
||||
try {
|
||||
flushPassiveEffects();
|
||||
flushWork();
|
||||
enqueueTask(() => {
|
||||
if (flushPassiveEffects()) {
|
||||
flushEffectsAndMicroTasks(onDone);
|
||||
if (flushWork()) {
|
||||
flushWorkAndMicroTasks(onDone);
|
||||
} else {
|
||||
onDone();
|
||||
}
|
||||
|
@ -669,6 +691,11 @@ function createReactNoop(reconciler: Function, useMutation: boolean) {
|
|||
}
|
||||
}
|
||||
|
||||
// we track the 'depth' of the act() calls with this counter,
|
||||
// so we can tell if any async act() calls try to run in parallel.
|
||||
|
||||
let actingUpdatesScopeDepth = 0;
|
||||
|
||||
function act(callback: () => Thenable) {
|
||||
let previousActingUpdatesScopeDepth;
|
||||
if (__DEV__) {
|
||||
|
@ -729,7 +756,7 @@ function createReactNoop(reconciler: Function, useMutation: boolean) {
|
|||
called = true;
|
||||
result.then(
|
||||
() => {
|
||||
flushEffectsAndMicroTasks((err: ?Error) => {
|
||||
flushWorkAndMicroTasks((err: ?Error) => {
|
||||
onDone();
|
||||
if (err) {
|
||||
reject(err);
|
||||
|
@ -757,7 +784,7 @@ function createReactNoop(reconciler: Function, useMutation: boolean) {
|
|||
|
||||
// flush effects until none remain, and cleanup
|
||||
try {
|
||||
while (flushPassiveEffects()) {}
|
||||
flushWork();
|
||||
onDone();
|
||||
} catch (err) {
|
||||
onDone();
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
let React;
|
||||
let ReactFeatureFlags;
|
||||
let ReactNoop;
|
||||
let act;
|
||||
let Scheduler;
|
||||
let ReactCache;
|
||||
let Suspense;
|
||||
|
@ -15,7 +14,6 @@ describe('ReactBatchedMode', () => {
|
|||
ReactFeatureFlags.replayFailedUnitOfWorkWithInvokeGuardedCallback = false;
|
||||
React = require('react');
|
||||
ReactNoop = require('react-noop-renderer');
|
||||
act = ReactNoop.act;
|
||||
Scheduler = require('scheduler');
|
||||
ReactCache = require('react-cache');
|
||||
Suspense = React.Suspense;
|
||||
|
@ -146,10 +144,10 @@ describe('ReactBatchedMode', () => {
|
|||
expect(root).toMatchRenderedOutput('A0B0');
|
||||
|
||||
// Schedule a batched update to the first sibling
|
||||
act(() => foo1.current.setStep(1));
|
||||
ReactNoop.batchedUpdates(() => foo1.current.setStep(1));
|
||||
|
||||
// Before it flushes, update the second sibling inside flushSync
|
||||
act(() =>
|
||||
ReactNoop.batchedUpdates(() =>
|
||||
ReactNoop.flushSync(() => {
|
||||
foo2.current.setStep(1);
|
||||
}),
|
||||
|
|
|
@ -95,7 +95,7 @@ describe('ReactHooks', () => {
|
|||
setCounter2(1);
|
||||
});
|
||||
|
||||
expect(Scheduler).toFlushAndYield([
|
||||
expect(Scheduler).toHaveYielded([
|
||||
'Parent: 1, 1',
|
||||
'Child: 1, 1',
|
||||
'Effect: 1, 1',
|
||||
|
@ -103,7 +103,7 @@ describe('ReactHooks', () => {
|
|||
|
||||
// Update that bails out.
|
||||
act(() => setCounter1(1));
|
||||
expect(Scheduler).toFlushAndYield(['Parent: 1, 1']);
|
||||
expect(Scheduler).toHaveYielded(['Parent: 1, 1']);
|
||||
|
||||
// This time, one of the state updates but the other one doesn't. So we
|
||||
// can't bail out.
|
||||
|
@ -112,7 +112,7 @@ describe('ReactHooks', () => {
|
|||
setCounter2(2);
|
||||
});
|
||||
|
||||
expect(Scheduler).toFlushAndYield([
|
||||
expect(Scheduler).toHaveYielded([
|
||||
'Parent: 1, 2',
|
||||
'Child: 1, 2',
|
||||
'Effect: 1, 2',
|
||||
|
@ -130,14 +130,15 @@ describe('ReactHooks', () => {
|
|||
|
||||
// Because the final values are the same as the current values, the
|
||||
// component bails out.
|
||||
expect(Scheduler).toFlushAndYield(['Parent: 1, 2']);
|
||||
expect(Scheduler).toHaveYielded(['Parent: 1, 2']);
|
||||
|
||||
// prepare to check SameValue
|
||||
act(() => {
|
||||
setCounter1(0 / -1);
|
||||
setCounter2(NaN);
|
||||
});
|
||||
expect(Scheduler).toFlushAndYield([
|
||||
|
||||
expect(Scheduler).toHaveYielded([
|
||||
'Parent: 0, NaN',
|
||||
'Child: 0, NaN',
|
||||
'Effect: 0, NaN',
|
||||
|
@ -151,13 +152,13 @@ describe('ReactHooks', () => {
|
|||
setCounter2(NaN);
|
||||
});
|
||||
|
||||
expect(Scheduler).toFlushAndYield(['Parent: 0, NaN']);
|
||||
expect(Scheduler).toHaveYielded(['Parent: 0, NaN']);
|
||||
|
||||
// check if changing negative 0 to positive 0 does not bail out
|
||||
act(() => {
|
||||
setCounter1(0);
|
||||
});
|
||||
expect(Scheduler).toFlushAndYield([
|
||||
expect(Scheduler).toHaveYielded([
|
||||
'Parent: 0, NaN',
|
||||
'Child: 0, NaN',
|
||||
'Effect: 0, NaN',
|
||||
|
@ -201,14 +202,14 @@ describe('ReactHooks', () => {
|
|||
setCounter2(1);
|
||||
});
|
||||
|
||||
expect(Scheduler).toFlushAndYield([
|
||||
expect(Scheduler).toHaveYielded([
|
||||
'Parent: 1, 1 (light)',
|
||||
'Child: 1, 1 (light)',
|
||||
]);
|
||||
|
||||
// Update that bails out.
|
||||
act(() => setCounter1(1));
|
||||
expect(Scheduler).toFlushAndYield(['Parent: 1, 1 (light)']);
|
||||
expect(Scheduler).toHaveYielded(['Parent: 1, 1 (light)']);
|
||||
|
||||
// This time, one of the state updates but the other one doesn't. So we
|
||||
// can't bail out.
|
||||
|
@ -217,7 +218,7 @@ describe('ReactHooks', () => {
|
|||
setCounter2(2);
|
||||
});
|
||||
|
||||
expect(Scheduler).toFlushAndYield([
|
||||
expect(Scheduler).toHaveYielded([
|
||||
'Parent: 1, 2 (light)',
|
||||
'Child: 1, 2 (light)',
|
||||
]);
|
||||
|
@ -227,10 +228,10 @@ describe('ReactHooks', () => {
|
|||
act(() => {
|
||||
setCounter1(1);
|
||||
setCounter2(2);
|
||||
root.update(<Parent theme="dark" />);
|
||||
});
|
||||
|
||||
root.update(<Parent theme="dark" />);
|
||||
expect(Scheduler).toFlushAndYield([
|
||||
expect(Scheduler).toHaveYielded([
|
||||
'Parent: 1, 2 (dark)',
|
||||
'Child: 1, 2 (dark)',
|
||||
]);
|
||||
|
@ -239,10 +240,10 @@ describe('ReactHooks', () => {
|
|||
act(() => {
|
||||
setCounter1(1);
|
||||
setCounter2(2);
|
||||
root.update(<Parent theme="dark" />);
|
||||
});
|
||||
|
||||
root.update(<Parent theme="dark" />);
|
||||
expect(Scheduler).toFlushAndYield(['Parent: 1, 2 (dark)']);
|
||||
expect(Scheduler).toHaveYielded(['Parent: 1, 2 (dark)']);
|
||||
});
|
||||
|
||||
it('warns about setState second argument', () => {
|
||||
|
@ -275,7 +276,7 @@ describe('ReactHooks', () => {
|
|||
'declare it in the component body with useEffect().',
|
||||
{withoutStack: true},
|
||||
);
|
||||
expect(Scheduler).toFlushAndYield(['Count: 1']);
|
||||
expect(Scheduler).toHaveYielded(['Count: 1']);
|
||||
expect(root).toMatchRenderedOutput('1');
|
||||
});
|
||||
|
||||
|
@ -309,7 +310,7 @@ describe('ReactHooks', () => {
|
|||
'declare it in the component body with useEffect().',
|
||||
{withoutStack: true},
|
||||
);
|
||||
expect(Scheduler).toFlushAndYield(['Count: 1']);
|
||||
expect(Scheduler).toHaveYielded(['Count: 1']);
|
||||
expect(root).toMatchRenderedOutput('1');
|
||||
});
|
||||
|
||||
|
@ -347,14 +348,16 @@ describe('ReactHooks', () => {
|
|||
});
|
||||
return <Child text={text} />;
|
||||
}
|
||||
|
||||
const root = ReactTestRenderer.create(null, {unstable_isConcurrent: true});
|
||||
root.update(
|
||||
<ThemeProvider>
|
||||
<Parent />
|
||||
</ThemeProvider>,
|
||||
);
|
||||
expect(Scheduler).toFlushAndYield([
|
||||
act(() => {
|
||||
root.update(
|
||||
<ThemeProvider>
|
||||
<Parent />
|
||||
</ThemeProvider>,
|
||||
);
|
||||
});
|
||||
|
||||
expect(Scheduler).toHaveYielded([
|
||||
'Theme: light',
|
||||
'Parent: 0 (light)',
|
||||
'Child: 0 (light)',
|
||||
|
@ -370,7 +373,7 @@ describe('ReactHooks', () => {
|
|||
|
||||
// Normal update
|
||||
act(() => setCounter(1));
|
||||
expect(Scheduler).toFlushAndYield([
|
||||
expect(Scheduler).toHaveYielded([
|
||||
'Parent: 1 (light)',
|
||||
'Child: 1 (light)',
|
||||
'Effect: 1 (light)',
|
||||
|
@ -379,7 +382,7 @@ describe('ReactHooks', () => {
|
|||
|
||||
// Update that doesn't change state, so it bails out
|
||||
act(() => setCounter(1));
|
||||
expect(Scheduler).toFlushAndYield(['Parent: 1 (light)']);
|
||||
expect(Scheduler).toHaveYielded(['Parent: 1 (light)']);
|
||||
expect(root).toMatchRenderedOutput('1 (light)');
|
||||
|
||||
// Update that doesn't change state, but the context changes, too, so it
|
||||
|
@ -389,7 +392,7 @@ describe('ReactHooks', () => {
|
|||
setTheme('dark');
|
||||
});
|
||||
|
||||
expect(Scheduler).toFlushAndYield([
|
||||
expect(Scheduler).toHaveYielded([
|
||||
'Theme: dark',
|
||||
'Parent: 1 (dark)',
|
||||
'Child: 1 (dark)',
|
||||
|
@ -424,7 +427,7 @@ describe('ReactHooks', () => {
|
|||
|
||||
// Normal update
|
||||
act(() => setCounter(1));
|
||||
expect(Scheduler).toFlushAndYield(['Parent: 1', 'Child: 1', 'Effect: 1']);
|
||||
expect(Scheduler).toHaveYielded(['Parent: 1', 'Child: 1', 'Effect: 1']);
|
||||
expect(root).toMatchRenderedOutput('1');
|
||||
|
||||
// Update to the same state. React doesn't know if the queue is empty
|
||||
|
@ -432,7 +435,7 @@ describe('ReactHooks', () => {
|
|||
// enter the render phase before we can bail out. But we bail out before
|
||||
// rendering the child, and we don't fire any effects.
|
||||
act(() => setCounter(1));
|
||||
expect(Scheduler).toFlushAndYield(['Parent: 1']);
|
||||
expect(Scheduler).toHaveYielded(['Parent: 1']);
|
||||
expect(root).toMatchRenderedOutput('1');
|
||||
|
||||
// Update to the same state again. This times, neither fiber has pending
|
||||
|
@ -443,14 +446,14 @@ describe('ReactHooks', () => {
|
|||
|
||||
// This changes the state to something different so it renders normally.
|
||||
act(() => setCounter(2));
|
||||
expect(Scheduler).toFlushAndYield(['Parent: 2', 'Child: 2', 'Effect: 2']);
|
||||
expect(Scheduler).toHaveYielded(['Parent: 2', 'Child: 2', 'Effect: 2']);
|
||||
expect(root).toMatchRenderedOutput('2');
|
||||
|
||||
// prepare to check SameValue
|
||||
act(() => {
|
||||
setCounter(0);
|
||||
});
|
||||
expect(Scheduler).toFlushAndYield(['Parent: 0', 'Child: 0', 'Effect: 0']);
|
||||
expect(Scheduler).toHaveYielded(['Parent: 0', 'Child: 0', 'Effect: 0']);
|
||||
expect(root).toMatchRenderedOutput('0');
|
||||
|
||||
// Update to the same state for the first time to flush the queue
|
||||
|
@ -458,7 +461,7 @@ describe('ReactHooks', () => {
|
|||
setCounter(0);
|
||||
});
|
||||
|
||||
expect(Scheduler).toFlushAndYield(['Parent: 0']);
|
||||
expect(Scheduler).toHaveYielded(['Parent: 0']);
|
||||
expect(root).toMatchRenderedOutput('0');
|
||||
|
||||
// Update again to the same state. Should bail out.
|
||||
|
@ -472,7 +475,7 @@ describe('ReactHooks', () => {
|
|||
act(() => {
|
||||
setCounter(0 / -1);
|
||||
});
|
||||
expect(Scheduler).toFlushAndYield(['Parent: 0', 'Child: 0', 'Effect: 0']);
|
||||
expect(Scheduler).toHaveYielded(['Parent: 0', 'Child: 0', 'Effect: 0']);
|
||||
expect(root).toMatchRenderedOutput('0');
|
||||
});
|
||||
|
||||
|
@ -503,7 +506,7 @@ describe('ReactHooks', () => {
|
|||
return value;
|
||||
});
|
||||
};
|
||||
act(() => {
|
||||
ReactTestRenderer.unstable_batchedUpdates(() => {
|
||||
update(0);
|
||||
update(0);
|
||||
update(0);
|
||||
|
@ -564,7 +567,7 @@ describe('ReactHooks', () => {
|
|||
};
|
||||
|
||||
// Update at normal priority
|
||||
act(() => update(n => n * 100));
|
||||
ReactTestRenderer.unstable_batchedUpdates(() => update(n => n * 100));
|
||||
|
||||
// The new state is eagerly computed.
|
||||
expect(Scheduler).toHaveYielded(['Compute state (1 -> 100)']);
|
||||
|
|
|
@ -80,7 +80,7 @@ describe('ReactHooksWithNoopRenderer', () => {
|
|||
expect(ReactNoop.getChildren()).toEqual([span('Count: 0')]);
|
||||
|
||||
// Schedule some updates
|
||||
act(() => {
|
||||
ReactNoop.batchedUpdates(() => {
|
||||
counter.current.updateCount(1);
|
||||
counter.current.updateCount(count => count + 10);
|
||||
});
|
||||
|
@ -189,11 +189,11 @@ describe('ReactHooksWithNoopRenderer', () => {
|
|||
expect(ReactNoop.getChildren()).toEqual([span('Count: 0')]);
|
||||
|
||||
act(() => counter.current.updateCount(1));
|
||||
expect(Scheduler).toFlushAndYield(['Count: 1']);
|
||||
expect(Scheduler).toHaveYielded(['Count: 1']);
|
||||
expect(ReactNoop.getChildren()).toEqual([span('Count: 1')]);
|
||||
|
||||
act(() => counter.current.updateCount(count => count + 10));
|
||||
expect(Scheduler).toFlushAndYield(['Count: 11']);
|
||||
expect(Scheduler).toHaveYielded(['Count: 11']);
|
||||
expect(ReactNoop.getChildren()).toEqual([span('Count: 11')]);
|
||||
});
|
||||
|
||||
|
@ -213,7 +213,7 @@ describe('ReactHooksWithNoopRenderer', () => {
|
|||
expect(ReactNoop.getChildren()).toEqual([span('Count: 42')]);
|
||||
|
||||
act(() => counter.current.updateCount(7));
|
||||
expect(Scheduler).toFlushAndYield(['Count: 7']);
|
||||
expect(Scheduler).toHaveYielded(['Count: 7']);
|
||||
expect(ReactNoop.getChildren()).toEqual([span('Count: 7')]);
|
||||
});
|
||||
|
||||
|
@ -231,10 +231,10 @@ describe('ReactHooksWithNoopRenderer', () => {
|
|||
expect(ReactNoop.getChildren()).toEqual([span('Count: 0')]);
|
||||
|
||||
act(() => counter.current.updateCount(7));
|
||||
expect(Scheduler).toFlushAndYield(['Count: 7']);
|
||||
expect(Scheduler).toHaveYielded(['Count: 7']);
|
||||
|
||||
act(() => counter.current.updateLabel('Total'));
|
||||
expect(Scheduler).toFlushAndYield(['Total: 7']);
|
||||
expect(Scheduler).toHaveYielded(['Total: 7']);
|
||||
});
|
||||
|
||||
it('returns the same updater function every time', () => {
|
||||
|
@ -249,11 +249,11 @@ describe('ReactHooksWithNoopRenderer', () => {
|
|||
expect(ReactNoop.getChildren()).toEqual([span('Count: 0')]);
|
||||
|
||||
act(() => updaters[0](1));
|
||||
expect(Scheduler).toFlushAndYield(['Count: 1']);
|
||||
expect(Scheduler).toHaveYielded(['Count: 1']);
|
||||
expect(ReactNoop.getChildren()).toEqual([span('Count: 1')]);
|
||||
|
||||
act(() => updaters[0](count => count + 10));
|
||||
expect(Scheduler).toFlushAndYield(['Count: 11']);
|
||||
expect(Scheduler).toHaveYielded(['Count: 11']);
|
||||
expect(ReactNoop.getChildren()).toEqual([span('Count: 11')]);
|
||||
|
||||
expect(updaters).toEqual([updaters[0], updaters[0], updaters[0]]);
|
||||
|
@ -298,7 +298,7 @@ describe('ReactHooksWithNoopRenderer', () => {
|
|||
expect(ReactNoop.getChildren()).toEqual([span('Count: 0')]);
|
||||
|
||||
act(() => _updateCount(1));
|
||||
expect(Scheduler).toFlushAndYield(['Count: 1']);
|
||||
expect(Scheduler).toHaveYielded(['Count: 1']);
|
||||
expect(ReactNoop.getChildren()).toEqual([span('Count: 1')]);
|
||||
});
|
||||
});
|
||||
|
@ -484,7 +484,7 @@ describe('ReactHooksWithNoopRenderer', () => {
|
|||
counter.current.dispatch('reset');
|
||||
});
|
||||
ReactNoop.render(<Counter ref={counter} />);
|
||||
expect(Scheduler).toFlushAndYield([
|
||||
expect(Scheduler).toHaveYielded([
|
||||
'Render: 0',
|
||||
'Render: 1',
|
||||
'Render: 11',
|
||||
|
@ -524,7 +524,7 @@ describe('ReactHooksWithNoopRenderer', () => {
|
|||
expect(ReactNoop.getChildren()).toEqual([span('Count: 0')]);
|
||||
|
||||
act(() => counter.current.dispatch(INCREMENT));
|
||||
expect(Scheduler).toFlushAndYield(['Count: 1']);
|
||||
expect(Scheduler).toHaveYielded(['Count: 1']);
|
||||
expect(ReactNoop.getChildren()).toEqual([span('Count: 1')]);
|
||||
act(() => {
|
||||
counter.current.dispatch(DECREMENT);
|
||||
|
@ -532,7 +532,7 @@ describe('ReactHooksWithNoopRenderer', () => {
|
|||
counter.current.dispatch(DECREMENT);
|
||||
});
|
||||
|
||||
expect(Scheduler).toFlushAndYield(['Count: -2']);
|
||||
expect(Scheduler).toHaveYielded(['Count: -2']);
|
||||
expect(ReactNoop.getChildren()).toEqual([span('Count: -2')]);
|
||||
});
|
||||
|
||||
|
@ -566,7 +566,7 @@ describe('ReactHooksWithNoopRenderer', () => {
|
|||
expect(ReactNoop.getChildren()).toEqual([span('Count: 10')]);
|
||||
|
||||
act(() => counter.current.dispatch(INCREMENT));
|
||||
expect(Scheduler).toFlushAndYield(['Count: 11']);
|
||||
expect(Scheduler).toHaveYielded(['Count: 11']);
|
||||
expect(ReactNoop.getChildren()).toEqual([span('Count: 11')]);
|
||||
|
||||
act(() => {
|
||||
|
@ -575,7 +575,7 @@ describe('ReactHooksWithNoopRenderer', () => {
|
|||
counter.current.dispatch(DECREMENT);
|
||||
});
|
||||
|
||||
expect(Scheduler).toFlushAndYield(['Count: 8']);
|
||||
expect(Scheduler).toHaveYielded(['Count: 8']);
|
||||
expect(ReactNoop.getChildren()).toEqual([span('Count: 8')]);
|
||||
});
|
||||
|
||||
|
@ -600,7 +600,7 @@ describe('ReactHooksWithNoopRenderer', () => {
|
|||
expect(Scheduler).toFlushAndYield(['Count: 0']);
|
||||
expect(ReactNoop.getChildren()).toEqual([span('Count: 0')]);
|
||||
|
||||
act(() => {
|
||||
ReactNoop.batchedUpdates(() => {
|
||||
counter.current.dispatch(INCREMENT);
|
||||
counter.current.dispatch(INCREMENT);
|
||||
counter.current.dispatch(INCREMENT);
|
||||
|
@ -884,8 +884,12 @@ describe('ReactHooksWithNoopRenderer', () => {
|
|||
|
||||
// A discrete event forces the passive effect to be flushed --
|
||||
// updateCount(1) happens first, so 2 wins.
|
||||
|
||||
ReactNoop.interactiveUpdates(() => {
|
||||
act(() => _updateCount(2));
|
||||
// (use batchedUpdates to silence the act() warning)
|
||||
ReactNoop.batchedUpdates(() => {
|
||||
_updateCount(2);
|
||||
});
|
||||
});
|
||||
expect(Scheduler).toHaveYielded(['Will set count to 1']);
|
||||
expect(Scheduler).toFlushAndYield(['Count: 2']);
|
||||
|
@ -936,7 +940,8 @@ describe('ReactHooksWithNoopRenderer', () => {
|
|||
// A discrete event forces the passive effect to be flushed --
|
||||
// updateCount(1) happens first, so 2 wins.
|
||||
ReactNoop.interactiveUpdates(() => {
|
||||
act(() => _updateCount(2));
|
||||
// use batchedUpdates to silence the act warning
|
||||
ReactNoop.batchedUpdates(() => _updateCount(2));
|
||||
});
|
||||
expect(Scheduler).toHaveYielded(['Will set count to 1']);
|
||||
expect(Scheduler).toFlushAndYield(['Count: 2']);
|
||||
|
@ -1527,7 +1532,7 @@ describe('ReactHooksWithNoopRenderer', () => {
|
|||
]);
|
||||
|
||||
act(button.current.increment);
|
||||
expect(Scheduler).toFlushAndYield([
|
||||
expect(Scheduler).toHaveYielded([
|
||||
// Button should not re-render, because its props haven't changed
|
||||
// 'Increment',
|
||||
'Count: 1',
|
||||
|
@ -1551,7 +1556,7 @@ describe('ReactHooksWithNoopRenderer', () => {
|
|||
|
||||
// Callback should have updated
|
||||
act(button.current.increment);
|
||||
expect(Scheduler).toFlushAndYield(['Count: 11']);
|
||||
expect(Scheduler).toHaveYielded(['Count: 11']);
|
||||
expect(ReactNoop.getChildren()).toEqual([
|
||||
span('Increment'),
|
||||
span('Count: 11'),
|
||||
|
@ -1754,7 +1759,7 @@ describe('ReactHooksWithNoopRenderer', () => {
|
|||
act(() => {
|
||||
counter.current.dispatch(INCREMENT);
|
||||
});
|
||||
expect(Scheduler).toFlushAndYield(['Count: 1']);
|
||||
expect(Scheduler).toHaveYielded(['Count: 1']);
|
||||
expect(ReactNoop.getChildren()).toEqual([span('Count: 1')]);
|
||||
// Intentionally not updated because of [] deps:
|
||||
expect(counter.current.count).toBe(0);
|
||||
|
@ -1784,7 +1789,7 @@ describe('ReactHooksWithNoopRenderer', () => {
|
|||
act(() => {
|
||||
counter.current.dispatch(INCREMENT);
|
||||
});
|
||||
expect(Scheduler).toFlushAndYield(['Count: 1']);
|
||||
expect(Scheduler).toHaveYielded(['Count: 1']);
|
||||
expect(ReactNoop.getChildren()).toEqual([span('Count: 1')]);
|
||||
expect(counter.current.count).toBe(1);
|
||||
});
|
||||
|
@ -1821,7 +1826,7 @@ describe('ReactHooksWithNoopRenderer', () => {
|
|||
act(() => {
|
||||
counter.current.dispatch(INCREMENT);
|
||||
});
|
||||
expect(Scheduler).toFlushAndYield(['Count: 1']);
|
||||
expect(Scheduler).toHaveYielded(['Count: 1']);
|
||||
expect(ReactNoop.getChildren()).toEqual([span('Count: 1')]);
|
||||
expect(counter.current.count).toBe(1);
|
||||
expect(totalRefUpdates).toBe(2);
|
||||
|
@ -1868,7 +1873,7 @@ describe('ReactHooksWithNoopRenderer', () => {
|
|||
updateB(3);
|
||||
});
|
||||
|
||||
expect(Scheduler).toFlushAndYield(['A: 2, B: 3, C: [not loaded]']);
|
||||
expect(Scheduler).toHaveYielded(['A: 2, B: 3, C: [not loaded]']);
|
||||
expect(ReactNoop.getChildren()).toEqual([
|
||||
span('A: 2, B: 3, C: [not loaded]'),
|
||||
]);
|
||||
|
@ -1929,7 +1934,7 @@ describe('ReactHooksWithNoopRenderer', () => {
|
|||
updateB(3);
|
||||
updateC(4);
|
||||
});
|
||||
expect(Scheduler).toFlushAndYield(['A: 2, B: 3, C: 4']);
|
||||
expect(Scheduler).toHaveYielded(['A: 2, B: 3, C: 4']);
|
||||
expect(ReactNoop.getChildren()).toEqual([span('A: 2, B: 3, C: 4')]);
|
||||
ReactNoop.render(<App loadC={false} />);
|
||||
expect(Scheduler).toFlushAndThrow(
|
||||
|
@ -2035,7 +2040,7 @@ describe('ReactHooksWithNoopRenderer', () => {
|
|||
act(() => {
|
||||
setCounter(2);
|
||||
});
|
||||
expect(Scheduler).toFlushAndYield([
|
||||
expect(Scheduler).toHaveYielded([
|
||||
'Render: 1',
|
||||
'Effect: 2',
|
||||
'Reducer: 2',
|
||||
|
@ -2074,7 +2079,7 @@ describe('ReactHooksWithNoopRenderer', () => {
|
|||
expect(ReactNoop).toMatchRenderedOutput('0');
|
||||
|
||||
act(() => dispatch());
|
||||
expect(Scheduler).toFlushAndYield(['Step: 5, Shadow: 5']);
|
||||
expect(Scheduler).toHaveYielded(['Step: 5, Shadow: 5']);
|
||||
expect(ReactNoop).toMatchRenderedOutput('5');
|
||||
});
|
||||
|
||||
|
@ -2113,7 +2118,8 @@ describe('ReactHooksWithNoopRenderer', () => {
|
|||
|
||||
// Enqueuing this update forces the passive effect to be flushed --
|
||||
// updateCount(1) happens first, so 2 wins.
|
||||
act(() => _updateCount(2));
|
||||
// (use batchedUpdates to silence the act() warning)
|
||||
ReactNoop.batchedUpdates(() => _updateCount(2));
|
||||
expect(Scheduler).toHaveYielded(['Will set count to 1']);
|
||||
expect(Scheduler).toFlushAndYield(['Count: 2']);
|
||||
expect(ReactNoop.getChildren()).toEqual([span('Count: 2')]);
|
||||
|
|
|
@ -104,14 +104,14 @@ describe('ReactIncrementalScheduling', () => {
|
|||
ReactNoop.renderToRootWithID(<Text text="b:1" />, 'b');
|
||||
ReactNoop.renderToRootWithID(<Text text="c:1" />, 'c');
|
||||
});
|
||||
expect(Scheduler).toFlushAndYield(['a:1', 'b:1', 'c:1']);
|
||||
expect(Scheduler).toHaveYielded(['a:1', 'b:1', 'c:1']);
|
||||
|
||||
expect(ReactNoop.getChildrenAsJSX('a')).toEqual('a:1');
|
||||
expect(ReactNoop.getChildrenAsJSX('b')).toEqual('b:1');
|
||||
expect(ReactNoop.getChildrenAsJSX('c')).toEqual('c:1');
|
||||
|
||||
// Schedule deferred work in the reverse order
|
||||
ReactNoop.act(() => {
|
||||
ReactNoop.batchedUpdates(() => {
|
||||
ReactNoop.renderToRootWithID(<Text text="c:2" />, 'c');
|
||||
ReactNoop.renderToRootWithID(<Text text="b:2" />, 'b');
|
||||
});
|
||||
|
@ -122,7 +122,7 @@ describe('ReactIncrementalScheduling', () => {
|
|||
expect(ReactNoop.getChildrenAsJSX('b')).toEqual('b:1');
|
||||
expect(ReactNoop.getChildrenAsJSX('c')).toEqual('c:2');
|
||||
// Schedule last bit of work, it will get processed the last
|
||||
ReactNoop.act(() => {
|
||||
ReactNoop.batchedUpdates(() => {
|
||||
ReactNoop.renderToRootWithID(<Text text="a:2" />, 'a');
|
||||
});
|
||||
// Keep performing work in the order it was scheduled
|
||||
|
|
|
@ -42,6 +42,7 @@ describe('ReactNoop.act()', () => {
|
|||
Scheduler.yieldValue('stage 1');
|
||||
await null;
|
||||
Scheduler.yieldValue('stage 2');
|
||||
await null;
|
||||
setCtr(1);
|
||||
}
|
||||
React.useEffect(() => {
|
||||
|
@ -50,13 +51,9 @@ describe('ReactNoop.act()', () => {
|
|||
return ctr;
|
||||
}
|
||||
await ReactNoop.act(async () => {
|
||||
ReactNoop.act(() => {
|
||||
ReactNoop.render(<App />);
|
||||
});
|
||||
await null;
|
||||
expect(Scheduler).toFlushAndYield(['stage 1']);
|
||||
ReactNoop.render(<App />);
|
||||
});
|
||||
expect(Scheduler).toHaveYielded(['stage 2']);
|
||||
expect(Scheduler).toHaveYielded(['stage 1', 'stage 2']);
|
||||
expect(Scheduler).toFlushWithoutYielding();
|
||||
expect(ReactNoop.getChildren()).toEqual([{text: '1', hidden: false}]);
|
||||
});
|
||||
|
|
|
@ -14,23 +14,42 @@ import {
|
|||
} from 'react-reconciler/inline.test';
|
||||
import ReactSharedInternals from 'shared/ReactSharedInternals';
|
||||
import warningWithoutStack from 'shared/warningWithoutStack';
|
||||
import {warnAboutMissingMockScheduler} from 'shared/ReactFeatureFlags';
|
||||
import enqueueTask from 'shared/enqueueTask';
|
||||
import * as Scheduler from 'scheduler';
|
||||
|
||||
const {ReactShouldWarnActingUpdates} = ReactSharedInternals;
|
||||
|
||||
// this implementation should be exactly the same in
|
||||
// ReactTestUtilsAct.js, ReactTestRendererAct.js, createReactNoop.js
|
||||
|
||||
// we track the 'depth' of the act() calls with this counter,
|
||||
// so we can tell if any async act() calls try to run in parallel.
|
||||
let actingUpdatesScopeDepth = 0;
|
||||
let hasWarnedAboutMissingMockScheduler = false;
|
||||
const flushWork =
|
||||
Scheduler.unstable_flushWithoutYielding ||
|
||||
function() {
|
||||
if (warnAboutMissingMockScheduler === true) {
|
||||
if (hasWarnedAboutMissingMockScheduler === false) {
|
||||
warningWithoutStack(
|
||||
null,
|
||||
'Starting from React v17, the "scheduler" module will need to be mocked ' +
|
||||
'to guarantee consistent behaviour across tests and browsers. To fix this, add the following ' +
|
||||
"to the top of your tests, or in your framework's global config file -\n\n" +
|
||||
'As an example, for jest - \n' +
|
||||
"jest.mock('scheduler', () => require.requireActual('scheduler/unstable_mock'));\n\n" +
|
||||
'For more info, visit https://fb.me/react-mock-scheduler',
|
||||
);
|
||||
hasWarnedAboutMissingMockScheduler = true;
|
||||
}
|
||||
}
|
||||
while (flushPassiveEffects()) {}
|
||||
};
|
||||
|
||||
function flushEffectsAndMicroTasks(onDone: (err: ?Error) => void) {
|
||||
function flushWorkAndMicroTasks(onDone: (err: ?Error) => void) {
|
||||
try {
|
||||
flushPassiveEffects();
|
||||
flushWork();
|
||||
enqueueTask(() => {
|
||||
if (flushPassiveEffects()) {
|
||||
flushEffectsAndMicroTasks(onDone);
|
||||
if (flushWork()) {
|
||||
flushWorkAndMicroTasks(onDone);
|
||||
} else {
|
||||
onDone();
|
||||
}
|
||||
|
@ -40,6 +59,11 @@ function flushEffectsAndMicroTasks(onDone: (err: ?Error) => void) {
|
|||
}
|
||||
}
|
||||
|
||||
// we track the 'depth' of the act() calls with this counter,
|
||||
// so we can tell if any async act() calls try to run in parallel.
|
||||
|
||||
let actingUpdatesScopeDepth = 0;
|
||||
|
||||
function act(callback: () => Thenable) {
|
||||
let previousActingUpdatesScopeDepth;
|
||||
if (__DEV__) {
|
||||
|
@ -100,7 +124,7 @@ function act(callback: () => Thenable) {
|
|||
called = true;
|
||||
result.then(
|
||||
() => {
|
||||
flushEffectsAndMicroTasks((err: ?Error) => {
|
||||
flushWorkAndMicroTasks((err: ?Error) => {
|
||||
onDone();
|
||||
if (err) {
|
||||
reject(err);
|
||||
|
@ -128,7 +152,7 @@ function act(callback: () => Thenable) {
|
|||
|
||||
// flush effects until none remain, and cleanup
|
||||
try {
|
||||
while (flushPassiveEffects()) {}
|
||||
flushWork();
|
||||
onDone();
|
||||
} catch (err) {
|
||||
onDone();
|
||||
|
|
|
@ -103,12 +103,15 @@ export function unstable_flushExpired() {
|
|||
}
|
||||
}
|
||||
|
||||
export function unstable_flushWithoutYielding(): void {
|
||||
export function unstable_flushWithoutYielding(): boolean {
|
||||
if (isFlushing) {
|
||||
throw new Error('Already flushing work.');
|
||||
}
|
||||
isFlushing = true;
|
||||
try {
|
||||
if (scheduledCallback === null) {
|
||||
return false;
|
||||
}
|
||||
while (scheduledCallback !== null) {
|
||||
const cb = scheduledCallback;
|
||||
scheduledCallback = null;
|
||||
|
@ -117,6 +120,7 @@ export function unstable_flushWithoutYielding(): void {
|
|||
scheduledCallbackExpiration <= currentTime;
|
||||
cb(didTimeout);
|
||||
}
|
||||
return true;
|
||||
} finally {
|
||||
expectedNumberOfYields = -1;
|
||||
didStop = false;
|
||||
|
|
|
@ -68,5 +68,8 @@ export const enableEventAPI = false;
|
|||
// New API for JSX transforms to target - https://github.com/reactjs/rfcs/pull/107
|
||||
export const enableJSXTransformAPI = false;
|
||||
|
||||
// We will enforce mocking scheduler with scheduler/unstable_mock at some point. (v17?)
|
||||
// Till then, we warn about the missing mock, but still fallback to a sync mode compatible version
|
||||
export const warnAboutMissingMockScheduler = false;
|
||||
// Temporary flag to revert the fix in #15650
|
||||
export const revertPassiveEffectsChange = false;
|
||||
|
|
|
@ -32,6 +32,7 @@ export const warnAboutDeprecatedLifecycles = true;
|
|||
export const warnAboutDeprecatedSetNativeProps = true;
|
||||
export const enableEventAPI = false;
|
||||
export const enableJSXTransformAPI = false;
|
||||
export const warnAboutMissingMockScheduler = true;
|
||||
export const revertPassiveEffectsChange = false;
|
||||
|
||||
// Only used in www builds.
|
||||
|
|
|
@ -29,6 +29,7 @@ export const enableSchedulerDebugging = false;
|
|||
export const warnAboutDeprecatedSetNativeProps = false;
|
||||
export const enableEventAPI = false;
|
||||
export const enableJSXTransformAPI = false;
|
||||
export const warnAboutMissingMockScheduler = false;
|
||||
export const revertPassiveEffectsChange = false;
|
||||
|
||||
// Only used in www builds.
|
||||
|
|
|
@ -29,6 +29,7 @@ export const enableSchedulerDebugging = false;
|
|||
export const warnAboutDeprecatedSetNativeProps = false;
|
||||
export const enableEventAPI = false;
|
||||
export const enableJSXTransformAPI = false;
|
||||
export const warnAboutMissingMockScheduler = true;
|
||||
export const revertPassiveEffectsChange = false;
|
||||
|
||||
// Only used in www builds.
|
||||
|
|
|
@ -29,6 +29,7 @@ export const enableSchedulerDebugging = false;
|
|||
export const warnAboutDeprecatedSetNativeProps = false;
|
||||
export const enableEventAPI = false;
|
||||
export const enableJSXTransformAPI = false;
|
||||
export const warnAboutMissingMockScheduler = false;
|
||||
export const revertPassiveEffectsChange = false;
|
||||
|
||||
// Only used in www builds.
|
||||
|
|
|
@ -30,6 +30,7 @@ export const disableJavaScriptURLs = false;
|
|||
export const disableYielding = false;
|
||||
export const enableEventAPI = true;
|
||||
export const enableJSXTransformAPI = true;
|
||||
export const warnAboutMissingMockScheduler = true;
|
||||
|
||||
// Only used in www builds.
|
||||
export function addUserTimingListener() {
|
||||
|
|
|
@ -72,6 +72,8 @@ export const enableEventAPI = true;
|
|||
|
||||
export const enableJSXTransformAPI = true;
|
||||
|
||||
export const warnAboutMissingMockScheduler = true;
|
||||
|
||||
// Flow magic to verify the exports of this file match the original version.
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
type Check<_X, Y: _X, X: Y = _X> = null;
|
||||
|
|
|
@ -28,6 +28,10 @@ const {
|
|||
unstable_LowPriority,
|
||||
unstable_IdlePriority,
|
||||
unstable_forceFrameRate,
|
||||
|
||||
// this doesn't actually exist on the scheduler, but it *does*
|
||||
// on scheduler/unstable_mock, which we'll need inside act().
|
||||
unstable_flushWithoutYielding,
|
||||
} = ReactInternals.Scheduler;
|
||||
|
||||
export {
|
||||
|
@ -47,4 +51,5 @@ export {
|
|||
unstable_LowPriority,
|
||||
unstable_IdlePriority,
|
||||
unstable_forceFrameRate,
|
||||
unstable_flushWithoutYielding,
|
||||
};
|
||||
|
|
|
@ -418,7 +418,14 @@ const bundles = [
|
|||
|
||||
/******* React Scheduler Mock (experimental) *******/
|
||||
{
|
||||
bundleTypes: [NODE_DEV, NODE_PROD, FB_WWW_DEV, FB_WWW_PROD],
|
||||
bundleTypes: [
|
||||
UMD_DEV,
|
||||
UMD_PROD,
|
||||
NODE_DEV,
|
||||
NODE_PROD,
|
||||
FB_WWW_DEV,
|
||||
FB_WWW_PROD,
|
||||
],
|
||||
moduleType: ISOMORPHIC,
|
||||
entry: 'scheduler/unstable_mock',
|
||||
global: 'SchedulerMock',
|
||||
|
|
Loading…
Reference in New Issue