mirror of
https://github.com/zebrajr/react.git
synced 2025-12-06 12:20:20 +01:00
await act(async () => ...) (#14853)
This took a while, but I'm happy I went through it. Some key moments - recursively flushing effects, flushing microtasks on each async turn, and my team's uncompromising philosophy on code reuse. Really happy with this. I still want to expand test coverage, and I have some more small related todos, but this is good to land. On to the next one. Soundtrack to landing this - https://open.spotify.com/track/0MF8I8OUo8kytiOo8aSHYq?si=gSWqUheKQbiQDXzptCXHTg * hacked up act(async () => {...}) * move stuff around * merge changes * abstract .act warnings and stuff. all renderers. pass all tests. * move testutils.act back into testutils * move into scheduler, rename some bits * smaller bundle * a comment for why we don't do typeof === 'function' * fix test * pass tests - fire, prod * lose actContainerElement * tighter * write a test for TestRenderer it's an odd one, because not only does sync act not flush effects correctly, but the async one does (wut). verified it's fine with the dom version. * lint * rewrote to move flushing logic closer to the renderer the scheduler's `flushPassiveEffects` didn't work as expected for the test renderer, so I decided to go back to the hack (rendering a dumb container) This also makes reactdom not as heavy (by a few bytes, but still). * move it around so the delta isn't too bad * cleanups fix promise chaining propagate errors correctly test for thenable the 'right' way more tests! tidier! ponies! * Stray comment * recursively flush effects * fixed tests * lint, move noop.act into react-reconciler * microtasks when checking if called, s/called/calledLog, cleanup * pass fb lint we could have globally changed our eslint config to assume Promise is available, but that means we expect a promise polyfill on the page, and we don't yet. this code is triggered only in jest anyway, and we're fairly certain Promise will be available there. hence, the once-off disable for the check * shorter timers, fix a test, test for Promise * use global.Promise for existence check * flush microtasks * a version that works in browsers (that support postMessage) I also added a sanity fixture inside fixtures/dom/ mostly for me. * hoist flushEffectsAndMicroTasks * pull out tick logic from ReactFiberScheduler * fix await act (...sync) hanging - fix a hang when awaiting sync logic - a better async/await test for test renderer * feedback changes - use node's setImmediate if available - a warning if MessageChannel isn't available - rename some functions * pass lint/flow checks (without requiring a Promise polyfill/exclusion) * prettier the prettiest, even. * use globalPromise for the missed await warning * __DEV__ check for didWarnAboutMessageChannel * thenables and callbacks instead of promises, pass flow/lint * tinier. better. - pulled most bits out of FiberScheduler - actedUpdates uses callbacks now * pass build validation * augh prettier * golfing 7 more chars * Test that effects are not flushed without also flushing microtasks * export doesHavePendingPassiveEffects, nits * createAct() * dead code * missed in merge? * lose the preflushing bits * ugh prettier * removed `actedUpdates()`, created shared/actingUpdatesScopeDepth * rearrange imports so builds work, remove the hack versions of flushPassiveEffects * represent actingUpdatesScopeDepth as a tuple [number] * use a shared flag on React.__SECRET... * remove createAct, setup act for all relevant renderers * review feedback shared/enqueueTask import ReactSharedInternals from 'shared/ReactSharedInternals'; simpler act() internals ReactSharedInternals.ReactShouldWarnActingUpdates * move act() implementation into createReactNoop * warnIfNotCurrentlyActingUpdatesInDev condition check order
This commit is contained in:
parent
4c75881ee3
commit
aed0e1c30c
2
fixtures/dom/.gitignore
vendored
2
fixtures/dom/.gitignore
vendored
|
|
@ -14,6 +14,8 @@ public/react-dom.development.js
|
||||||
public/react-dom.production.min.js
|
public/react-dom.production.min.js
|
||||||
public/react-dom-server.browser.development.js
|
public/react-dom-server.browser.development.js
|
||||||
public/react-dom-server.browser.production.min.js
|
public/react-dom-server.browser.production.min.js
|
||||||
|
public/react-dom-test-utils.development.js
|
||||||
|
public/react-dom-test-utils.production.min.js
|
||||||
|
|
||||||
# misc
|
# misc
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "react-scripts start",
|
"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 public/",
|
"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/",
|
||||||
"build": "react-scripts build && cp build/index.html build/200.html",
|
"build": "react-scripts build && cp build/index.html build/200.html",
|
||||||
"test": "react-scripts test --env=jsdom",
|
"test": "react-scripts test --env=jsdom",
|
||||||
"eject": "react-scripts eject"
|
"eject": "react-scripts eject"
|
||||||
|
|
|
||||||
41
fixtures/dom/public/act-dom.html
Normal file
41
fixtures/dom/public/act-dom.html
Normal file
|
|
@ -0,0 +1,41 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>sanity test for ReactTestUtils.act</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
this page tests whether act runs properly in a browser.
|
||||||
|
<br/>
|
||||||
|
your console should say "5"
|
||||||
|
<script src='react.development.js'></script>
|
||||||
|
<script src='react-dom.development.js'></script>
|
||||||
|
<script src='react-dom-test-utils.development.js'></script>
|
||||||
|
<script>
|
||||||
|
async function run(){
|
||||||
|
// from ReactTestUtilsAct-test.js
|
||||||
|
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 ReactTestUtils.act(async () => {
|
||||||
|
ReactDOM.render(React.createElement(App), el);
|
||||||
|
});
|
||||||
|
// all 5 ticks present and accounted for
|
||||||
|
console.log(el.innerHTML);
|
||||||
|
}
|
||||||
|
run();
|
||||||
|
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
@ -14,7 +14,6 @@ let React;
|
||||||
let ReactDOM;
|
let ReactDOM;
|
||||||
let ReactDOMServer;
|
let ReactDOMServer;
|
||||||
let ReactTestUtils;
|
let ReactTestUtils;
|
||||||
let act;
|
|
||||||
|
|
||||||
function getTestDocument(markup) {
|
function getTestDocument(markup) {
|
||||||
const doc = document.implementation.createHTMLDocument('');
|
const doc = document.implementation.createHTMLDocument('');
|
||||||
|
|
@ -34,7 +33,6 @@ describe('ReactTestUtils', () => {
|
||||||
ReactDOM = require('react-dom');
|
ReactDOM = require('react-dom');
|
||||||
ReactDOMServer = require('react-dom/server');
|
ReactDOMServer = require('react-dom/server');
|
||||||
ReactTestUtils = require('react-dom/test-utils');
|
ReactTestUtils = require('react-dom/test-utils');
|
||||||
act = ReactTestUtils.act;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Simulate should have locally attached media events', () => {
|
it('Simulate should have locally attached media events', () => {
|
||||||
|
|
@ -517,173 +515,4 @@ describe('ReactTestUtils', () => {
|
||||||
ReactTestUtils.renderIntoDocument(<Component />);
|
ReactTestUtils.renderIntoDocument(<Component />);
|
||||||
expect(mockArgs.length).toEqual(0);
|
expect(mockArgs.length).toEqual(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('can use act to batch effects', () => {
|
|
||||||
function App(props) {
|
|
||||||
React.useEffect(props.callback);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
const container = document.createElement('div');
|
|
||||||
document.body.appendChild(container);
|
|
||||||
|
|
||||||
try {
|
|
||||||
let called = false;
|
|
||||||
act(() => {
|
|
||||||
ReactDOM.render(
|
|
||||||
<App
|
|
||||||
callback={() => {
|
|
||||||
called = true;
|
|
||||||
}}
|
|
||||||
/>,
|
|
||||||
container,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(called).toBe(true);
|
|
||||||
} finally {
|
|
||||||
document.body.removeChild(container);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
it('flushes effects on every call', () => {
|
|
||||||
function App(props) {
|
|
||||||
let [ctr, setCtr] = React.useState(0);
|
|
||||||
React.useEffect(() => {
|
|
||||||
props.callback(ctr);
|
|
||||||
});
|
|
||||||
return (
|
|
||||||
<button id="button" onClick={() => setCtr(x => x + 1)}>
|
|
||||||
click me!
|
|
||||||
</button>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const container = document.createElement('div');
|
|
||||||
document.body.appendChild(container);
|
|
||||||
let calledCtr = 0;
|
|
||||||
act(() => {
|
|
||||||
ReactDOM.render(
|
|
||||||
<App
|
|
||||||
callback={val => {
|
|
||||||
calledCtr = val;
|
|
||||||
}}
|
|
||||||
/>,
|
|
||||||
container,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
const button = document.getElementById('button');
|
|
||||||
function click() {
|
|
||||||
button.dispatchEvent(new MouseEvent('click', {bubbles: true}));
|
|
||||||
}
|
|
||||||
|
|
||||||
act(() => {
|
|
||||||
click();
|
|
||||||
click();
|
|
||||||
click();
|
|
||||||
});
|
|
||||||
expect(calledCtr).toBe(3);
|
|
||||||
act(click);
|
|
||||||
expect(calledCtr).toBe(4);
|
|
||||||
act(click);
|
|
||||||
expect(calledCtr).toBe(5);
|
|
||||||
|
|
||||||
document.body.removeChild(container);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('can use act to batch effects on updates too', () => {
|
|
||||||
function App() {
|
|
||||||
let [ctr, setCtr] = React.useState(0);
|
|
||||||
return (
|
|
||||||
<button id="button" onClick={() => setCtr(x => x + 1)}>
|
|
||||||
{ctr}
|
|
||||||
</button>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
const container = document.createElement('div');
|
|
||||||
document.body.appendChild(container);
|
|
||||||
let button;
|
|
||||||
act(() => {
|
|
||||||
ReactDOM.render(<App />, container);
|
|
||||||
});
|
|
||||||
button = document.getElementById('button');
|
|
||||||
expect(button.innerHTML).toBe('0');
|
|
||||||
act(() => {
|
|
||||||
button.dispatchEvent(new MouseEvent('click', {bubbles: true}));
|
|
||||||
});
|
|
||||||
expect(button.innerHTML).toBe('1');
|
|
||||||
document.body.removeChild(container);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('detects setState being called outside of act(...)', () => {
|
|
||||||
let setValueRef = null;
|
|
||||||
function App() {
|
|
||||||
let [value, setValue] = React.useState(0);
|
|
||||||
setValueRef = setValue;
|
|
||||||
return (
|
|
||||||
<button id="button" onClick={() => setValue(2)}>
|
|
||||||
{value}
|
|
||||||
</button>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
const container = document.createElement('div');
|
|
||||||
document.body.appendChild(container);
|
|
||||||
let button;
|
|
||||||
act(() => {
|
|
||||||
ReactDOM.render(<App />, container);
|
|
||||||
button = container.querySelector('#button');
|
|
||||||
button.dispatchEvent(new MouseEvent('click', {bubbles: true}));
|
|
||||||
});
|
|
||||||
expect(button.innerHTML).toBe('2');
|
|
||||||
expect(() => setValueRef(1)).toWarnDev([
|
|
||||||
'An update to App inside a test was not wrapped in act(...).',
|
|
||||||
]);
|
|
||||||
document.body.removeChild(container);
|
|
||||||
});
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
const container = document.createElement('div');
|
|
||||||
|
|
||||||
act(() => {
|
|
||||||
act(() => {
|
|
||||||
ReactDOM.render(<App />, container);
|
|
||||||
});
|
|
||||||
jest.advanceTimersByTime(250);
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(container.innerHTML).toBe('1');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('warns if you return a value inside act', () => {
|
|
||||||
expect(() => act(() => null)).toWarnDev(
|
|
||||||
[
|
|
||||||
'The callback passed to ReactTestUtils.act(...) function must not return anything.',
|
|
||||||
],
|
|
||||||
{withoutStack: true},
|
|
||||||
);
|
|
||||||
expect(() => act(() => 123)).toWarnDev(
|
|
||||||
[
|
|
||||||
'The callback passed to ReactTestUtils.act(...) function must not return anything.',
|
|
||||||
],
|
|
||||||
{withoutStack: true},
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('warns if you try to await an .act call', () => {
|
|
||||||
expect(act(() => {}).then).toWarnDev(
|
|
||||||
[
|
|
||||||
'Do not await the result of calling ReactTestUtils.act(...), it is not a Promise.',
|
|
||||||
],
|
|
||||||
{withoutStack: true},
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
|
||||||
403
packages/react-dom/src/__tests__/ReactTestUtilsAct-test.js
vendored
Normal file
403
packages/react-dom/src/__tests__/ReactTestUtilsAct-test.js
vendored
Normal file
|
|
@ -0,0 +1,403 @@
|
||||||
|
/**
|
||||||
|
* 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 React;
|
||||||
|
let ReactDOM;
|
||||||
|
let ReactTestUtils;
|
||||||
|
let act;
|
||||||
|
|
||||||
|
jest.useRealTimers();
|
||||||
|
|
||||||
|
function sleep(period) {
|
||||||
|
return new Promise(resolve => {
|
||||||
|
setTimeout(() => {
|
||||||
|
resolve(true);
|
||||||
|
}, period);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('ReactTestUtils.act()', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.resetModules();
|
||||||
|
React = require('react');
|
||||||
|
ReactDOM = require('react-dom');
|
||||||
|
ReactTestUtils = require('react-dom/test-utils');
|
||||||
|
act = ReactTestUtils.act;
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('sync', () => {
|
||||||
|
it('can use act to flush effects', () => {
|
||||||
|
function App(props) {
|
||||||
|
React.useEffect(props.callback);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
let calledLog = [];
|
||||||
|
act(() => {
|
||||||
|
ReactDOM.render(
|
||||||
|
<App
|
||||||
|
callback={() => {
|
||||||
|
calledLog.push(calledLog.length);
|
||||||
|
}}
|
||||||
|
/>,
|
||||||
|
document.createElement('div'),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(calledLog).toEqual([0]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('flushes effects on every call', () => {
|
||||||
|
function App(props) {
|
||||||
|
let [ctr, setCtr] = React.useState(0);
|
||||||
|
React.useEffect(() => {
|
||||||
|
props.callback(ctr);
|
||||||
|
});
|
||||||
|
return (
|
||||||
|
<button id="button" onClick={() => setCtr(x => x + 1)}>
|
||||||
|
{ctr}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const container = document.createElement('div');
|
||||||
|
// attach to body so events works
|
||||||
|
document.body.appendChild(container);
|
||||||
|
let calledCounter = 0;
|
||||||
|
act(() => {
|
||||||
|
ReactDOM.render(
|
||||||
|
<App
|
||||||
|
callback={val => {
|
||||||
|
calledCounter = val;
|
||||||
|
}}
|
||||||
|
/>,
|
||||||
|
container,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
const button = document.getElementById('button');
|
||||||
|
function click() {
|
||||||
|
button.dispatchEvent(new MouseEvent('click', {bubbles: true}));
|
||||||
|
}
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
click();
|
||||||
|
click();
|
||||||
|
click();
|
||||||
|
});
|
||||||
|
expect(calledCounter).toBe(3);
|
||||||
|
act(click);
|
||||||
|
expect(calledCounter).toBe(4);
|
||||||
|
act(click);
|
||||||
|
expect(calledCounter).toBe(5);
|
||||||
|
expect(button.innerHTML).toBe('5');
|
||||||
|
|
||||||
|
document.body.removeChild(container);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should flush effects recursively', () => {
|
||||||
|
function App() {
|
||||||
|
let [ctr, setCtr] = React.useState(0);
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (ctr < 5) {
|
||||||
|
setCtr(x => x + 1);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return ctr;
|
||||||
|
}
|
||||||
|
|
||||||
|
const container = document.createElement('div');
|
||||||
|
act(() => {
|
||||||
|
ReactDOM.render(<App />, container);
|
||||||
|
});
|
||||||
|
|
||||||
|
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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const container = document.createElement('div');
|
||||||
|
document.body.appendChild(container);
|
||||||
|
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(...).',
|
||||||
|
]);
|
||||||
|
document.body.removeChild(container);
|
||||||
|
});
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
const container = document.createElement('div');
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
const container = document.createElement('div');
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
ReactDOM.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;
|
||||||
|
}
|
||||||
|
const el = document.createElement('div');
|
||||||
|
await act(async () => {
|
||||||
|
ReactDOM.render(<App />, el);
|
||||||
|
});
|
||||||
|
|
||||||
|
// all 5 ticks present and accounted for
|
||||||
|
expect(el.innerHTML).toBe('5');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
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 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');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
4
packages/react-dom/src/client/ReactDOM.js
vendored
4
packages/react-dom/src/client/ReactDOM.js
vendored
|
|
@ -35,6 +35,7 @@ import {
|
||||||
getPublicRootInstance,
|
getPublicRootInstance,
|
||||||
findHostInstance,
|
findHostInstance,
|
||||||
findHostInstanceWithWarning,
|
findHostInstanceWithWarning,
|
||||||
|
flushPassiveEffects,
|
||||||
} from 'react-reconciler/inline.dom';
|
} from 'react-reconciler/inline.dom';
|
||||||
import {createPortal as createPortalImpl} from 'shared/ReactPortal';
|
import {createPortal as createPortalImpl} from 'shared/ReactPortal';
|
||||||
import {canUseDOM} from 'shared/ExecutionEnvironment';
|
import {canUseDOM} from 'shared/ExecutionEnvironment';
|
||||||
|
|
@ -807,7 +808,7 @@ const ReactDOM: Object = {
|
||||||
|
|
||||||
__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED: {
|
__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED: {
|
||||||
// Keep in sync with ReactDOMUnstableNativeDependencies.js
|
// Keep in sync with ReactDOMUnstableNativeDependencies.js
|
||||||
// and ReactTestUtils.js. This is an array for better minification.
|
// ReactTestUtils.js, and ReactTestUtilsAct.js. This is an array for better minification.
|
||||||
Events: [
|
Events: [
|
||||||
getInstanceFromNode,
|
getInstanceFromNode,
|
||||||
getNodeFromInstance,
|
getNodeFromInstance,
|
||||||
|
|
@ -820,6 +821,7 @@ const ReactDOM: Object = {
|
||||||
restoreStateIfNeeded,
|
restoreStateIfNeeded,
|
||||||
dispatchEvent,
|
dispatchEvent,
|
||||||
runEventsInBatch,
|
runEventsInBatch,
|
||||||
|
flushPassiveEffects,
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
||||||
2
packages/react-dom/src/fire/ReactFire.js
vendored
2
packages/react-dom/src/fire/ReactFire.js
vendored
|
|
@ -40,6 +40,7 @@ import {
|
||||||
getPublicRootInstance,
|
getPublicRootInstance,
|
||||||
findHostInstance,
|
findHostInstance,
|
||||||
findHostInstanceWithWarning,
|
findHostInstanceWithWarning,
|
||||||
|
flushPassiveEffects,
|
||||||
} from 'react-reconciler/inline.fire';
|
} from 'react-reconciler/inline.fire';
|
||||||
import {createPortal as createPortalImpl} from 'shared/ReactPortal';
|
import {createPortal as createPortalImpl} from 'shared/ReactPortal';
|
||||||
import {canUseDOM} from 'shared/ExecutionEnvironment';
|
import {canUseDOM} from 'shared/ExecutionEnvironment';
|
||||||
|
|
@ -826,6 +827,7 @@ const ReactDOM: Object = {
|
||||||
restoreStateIfNeeded,
|
restoreStateIfNeeded,
|
||||||
dispatchEvent,
|
dispatchEvent,
|
||||||
runEventsInBatch,
|
runEventsInBatch,
|
||||||
|
flushPassiveEffects,
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@
|
||||||
* This source code is licensed under the MIT license found in the
|
* This source code is licensed under the MIT license found in the
|
||||||
* LICENSE file in the root directory of this source tree.
|
* LICENSE file in the root directory of this source tree.
|
||||||
*/
|
*/
|
||||||
|
import type {Thenable} from 'react-reconciler/src/ReactFiberScheduler';
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import ReactDOM from 'react-dom';
|
import ReactDOM from 'react-dom';
|
||||||
|
|
@ -22,15 +23,11 @@ import warningWithoutStack from 'shared/warningWithoutStack';
|
||||||
import {ELEMENT_NODE} from '../shared/HTMLNodeType';
|
import {ELEMENT_NODE} from '../shared/HTMLNodeType';
|
||||||
import * as DOMTopLevelEventTypes from '../events/DOMTopLevelEventTypes';
|
import * as DOMTopLevelEventTypes from '../events/DOMTopLevelEventTypes';
|
||||||
import {PLUGIN_EVENT_SYSTEM} from 'events/EventSystemFlags';
|
import {PLUGIN_EVENT_SYSTEM} from 'events/EventSystemFlags';
|
||||||
|
import act from './ReactTestUtilsAct';
|
||||||
// for .act's return value
|
|
||||||
type Thenable = {
|
|
||||||
then(resolve: () => mixed, reject?: () => mixed): mixed,
|
|
||||||
};
|
|
||||||
|
|
||||||
const {findDOMNode} = ReactDOM;
|
const {findDOMNode} = ReactDOM;
|
||||||
// Keep in sync with ReactDOMUnstableNativeDependencies.js
|
// Keep in sync with ReactDOMUnstableNativeDependencies.js
|
||||||
// and ReactDOM.js:
|
// ReactDOM.js, and ReactTestUtilsAct.js:
|
||||||
const [
|
const [
|
||||||
getInstanceFromNode,
|
getInstanceFromNode,
|
||||||
/* eslint-disable no-unused-vars */
|
/* eslint-disable no-unused-vars */
|
||||||
|
|
@ -45,6 +42,8 @@ const [
|
||||||
restoreStateIfNeeded,
|
restoreStateIfNeeded,
|
||||||
dispatchEvent,
|
dispatchEvent,
|
||||||
runEventsInBatch,
|
runEventsInBatch,
|
||||||
|
// eslint-disable-next-line no-unused-vars
|
||||||
|
flushPassiveEffects,
|
||||||
] = ReactDOM.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.Events;
|
] = ReactDOM.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.Events;
|
||||||
|
|
||||||
function Event(suffix) {}
|
function Event(suffix) {}
|
||||||
|
|
@ -152,9 +151,12 @@ function validateClassInstance(inst, methodName) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// a stub element, lazily initialized, used by act() when flushing effects
|
// a plain dom element, lazily initialized, used by act() when flushing effects
|
||||||
let actContainerElement = null;
|
let actContainerElement = null;
|
||||||
|
|
||||||
|
// a warning for when you try to use TestUtils.act in a non-browser environment
|
||||||
|
let didWarnAboutActInNodejs = false;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Utilities for making it easy to test React components.
|
* Utilities for making it easy to test React components.
|
||||||
*
|
*
|
||||||
|
|
@ -391,57 +393,24 @@ const ReactTestUtils = {
|
||||||
Simulate: null,
|
Simulate: null,
|
||||||
SimulateNative: {},
|
SimulateNative: {},
|
||||||
|
|
||||||
act(callback: () => void): Thenable {
|
act(callback: () => Thenable) {
|
||||||
if (actContainerElement === null) {
|
if (actContainerElement === null) {
|
||||||
// warn if we can't actually create the stub element
|
|
||||||
if (__DEV__) {
|
if (__DEV__) {
|
||||||
|
// warn if we're trying to use this in something like node (without jsdom)
|
||||||
|
if (didWarnAboutActInNodejs === false) {
|
||||||
|
didWarnAboutActInNodejs = true;
|
||||||
warningWithoutStack(
|
warningWithoutStack(
|
||||||
typeof document !== 'undefined' &&
|
typeof document !== 'undefined' && document !== null,
|
||||||
document !== null &&
|
'It looks like you called ReactTestUtils.act(...) in a non-browser environment. ' +
|
||||||
typeof document.createElement === 'function',
|
|
||||||
'It looks like you called TestUtils.act(...) in a non-browser environment. ' +
|
|
||||||
"If you're using TestRenderer for your tests, you should call " +
|
"If you're using TestRenderer for your tests, you should call " +
|
||||||
'TestRenderer.act(...) instead of TestUtils.act(...).',
|
'ReactTestRenderer.act(...) instead of ReactTestUtils.act(...).',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
// then make it
|
}
|
||||||
|
// now make the stub element
|
||||||
actContainerElement = document.createElement('div');
|
actContainerElement = document.createElement('div');
|
||||||
}
|
}
|
||||||
|
return act(callback);
|
||||||
const result = ReactDOM.unstable_batchedUpdates(callback);
|
|
||||||
// note: keep these warning messages in sync with
|
|
||||||
// createReactNoop.js and ReactTestRenderer.js
|
|
||||||
if (__DEV__) {
|
|
||||||
if (result !== undefined) {
|
|
||||||
let addendum;
|
|
||||||
if (result !== null && typeof result.then === 'function') {
|
|
||||||
addendum =
|
|
||||||
'\n\nIt looks like you wrote ReactTestUtils.act(async () => ...), ' +
|
|
||||||
'or returned a Promise from the callback passed to it. ' +
|
|
||||||
'Putting asynchronous logic inside ReactTestUtils.act(...) is not supported.\n';
|
|
||||||
} else {
|
|
||||||
addendum = ' You returned: ' + result;
|
|
||||||
}
|
|
||||||
warningWithoutStack(
|
|
||||||
false,
|
|
||||||
'The callback passed to ReactTestUtils.act(...) function must not return anything.%s',
|
|
||||||
addendum,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ReactDOM.render(<div />, actContainerElement);
|
|
||||||
// we want the user to not expect a return,
|
|
||||||
// but we want to warn if they use it like they can await on it.
|
|
||||||
return {
|
|
||||||
then() {
|
|
||||||
if (__DEV__) {
|
|
||||||
warningWithoutStack(
|
|
||||||
false,
|
|
||||||
'Do not await the result of calling ReactTestUtils.act(...), it is not a Promise.',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
};
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
172
packages/react-dom/src/test-utils/ReactTestUtilsAct.js
vendored
Normal file
172
packages/react-dom/src/test-utils/ReactTestUtilsAct.js
vendored
Normal file
|
|
@ -0,0 +1,172 @@
|
||||||
|
/**
|
||||||
|
* 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.
|
||||||
|
*
|
||||||
|
* @flow
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type {Thenable} from 'react-reconciler/src/ReactFiberScheduler';
|
||||||
|
|
||||||
|
import warningWithoutStack from 'shared/warningWithoutStack';
|
||||||
|
import ReactDOM from 'react-dom';
|
||||||
|
import ReactSharedInternals from 'shared/ReactSharedInternals';
|
||||||
|
import enqueueTask from 'shared/enqueueTask';
|
||||||
|
|
||||||
|
// Keep in sync with ReactDOMUnstableNativeDependencies.js
|
||||||
|
// ReactDOM.js, and ReactTestUtils.js:
|
||||||
|
const [
|
||||||
|
/* eslint-disable no-unused-vars */
|
||||||
|
getInstanceFromNode,
|
||||||
|
getNodeFromInstance,
|
||||||
|
getFiberCurrentPropsFromNode,
|
||||||
|
injectEventPluginsByName,
|
||||||
|
eventNameDispatchConfigs,
|
||||||
|
accumulateTwoPhaseDispatches,
|
||||||
|
accumulateDirectDispatches,
|
||||||
|
enqueueStateRestore,
|
||||||
|
restoreStateIfNeeded,
|
||||||
|
dispatchEvent,
|
||||||
|
runEventsInBatch,
|
||||||
|
/* eslint-enable no-unused-vars */
|
||||||
|
flushPassiveEffects,
|
||||||
|
] = ReactDOM.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.Events;
|
||||||
|
|
||||||
|
const batchedUpdates = ReactDOM.unstable_batchedUpdates;
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
function flushEffectsAndMicroTasks(onDone: (err: ?Error) => void) {
|
||||||
|
try {
|
||||||
|
flushPassiveEffects();
|
||||||
|
enqueueTask(() => {
|
||||||
|
if (flushPassiveEffects()) {
|
||||||
|
flushEffectsAndMicroTasks(onDone);
|
||||||
|
} else {
|
||||||
|
onDone();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
onDone(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function act(callback: () => Thenable) {
|
||||||
|
let previousActingUpdatesScopeDepth;
|
||||||
|
if (__DEV__) {
|
||||||
|
previousActingUpdatesScopeDepth = actingUpdatesScopeDepth;
|
||||||
|
actingUpdatesScopeDepth++;
|
||||||
|
ReactShouldWarnActingUpdates.current = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function onDone() {
|
||||||
|
if (__DEV__) {
|
||||||
|
actingUpdatesScopeDepth--;
|
||||||
|
if (actingUpdatesScopeDepth === 0) {
|
||||||
|
ReactShouldWarnActingUpdates.current = false;
|
||||||
|
}
|
||||||
|
if (actingUpdatesScopeDepth > previousActingUpdatesScopeDepth) {
|
||||||
|
// if it's _less than_ previousActingUpdatesScopeDepth, then we can assume the 'other' one has warned
|
||||||
|
warningWithoutStack(
|
||||||
|
null,
|
||||||
|
'You seem to have overlapping act() calls, this is not supported. ' +
|
||||||
|
'Be sure to await previous act() calls before making a new one. ',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = batchedUpdates(callback);
|
||||||
|
if (
|
||||||
|
result !== null &&
|
||||||
|
typeof result === 'object' &&
|
||||||
|
typeof result.then === 'function'
|
||||||
|
) {
|
||||||
|
// setup a boolean that gets set to true only
|
||||||
|
// once this act() call is await-ed
|
||||||
|
let called = false;
|
||||||
|
if (__DEV__) {
|
||||||
|
if (typeof Promise !== 'undefined') {
|
||||||
|
//eslint-disable-next-line no-undef
|
||||||
|
Promise.resolve()
|
||||||
|
.then(() => {})
|
||||||
|
.then(() => {
|
||||||
|
if (called === false) {
|
||||||
|
warningWithoutStack(
|
||||||
|
null,
|
||||||
|
'You called act(async () => ...) without await. ' +
|
||||||
|
'This could lead to unexpected testing behaviour, interleaving multiple act ' +
|
||||||
|
'calls and mixing their scopes. You should - await act(async () => ...);',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// in the async case, the returned thenable runs the callback, flushes
|
||||||
|
// effects and microtasks in a loop until flushPassiveEffects() === false,
|
||||||
|
// and cleans up
|
||||||
|
return {
|
||||||
|
then(resolve: () => void, reject: (?Error) => void) {
|
||||||
|
called = true;
|
||||||
|
result.then(
|
||||||
|
() => {
|
||||||
|
flushEffectsAndMicroTasks((err: ?Error) => {
|
||||||
|
onDone();
|
||||||
|
if (err) {
|
||||||
|
reject(err);
|
||||||
|
} else {
|
||||||
|
resolve();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
err => {
|
||||||
|
onDone();
|
||||||
|
reject(err);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
if (__DEV__) {
|
||||||
|
warningWithoutStack(
|
||||||
|
result === undefined,
|
||||||
|
'The callback passed to act(...) function ' +
|
||||||
|
'must return undefined, or a Promise. You returned %s',
|
||||||
|
result,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// flush effects until none remain, and cleanup
|
||||||
|
try {
|
||||||
|
while (flushPassiveEffects()) {}
|
||||||
|
onDone();
|
||||||
|
} catch (err) {
|
||||||
|
onDone();
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
|
||||||
|
// in the sync case, the returned thenable only warns *if* await-ed
|
||||||
|
return {
|
||||||
|
then(resolve: () => void) {
|
||||||
|
if (__DEV__) {
|
||||||
|
warningWithoutStack(
|
||||||
|
false,
|
||||||
|
'Do not await the result of calling act(...) with sync logic, it is not a Promise.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
resolve();
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default act;
|
||||||
|
|
@ -11,7 +11,7 @@ import ResponderEventPlugin from 'events/ResponderEventPlugin';
|
||||||
import ResponderTouchHistoryStore from 'events/ResponderTouchHistoryStore';
|
import ResponderTouchHistoryStore from 'events/ResponderTouchHistoryStore';
|
||||||
|
|
||||||
// Inject react-dom's ComponentTree into this module.
|
// Inject react-dom's ComponentTree into this module.
|
||||||
// Keep in sync with ReactDOM.js and ReactTestUtils.js:
|
// Keep in sync with ReactDOM.js, ReactTestUtils.js, and ReactTestUtilsAct.js:
|
||||||
const [
|
const [
|
||||||
getInstanceFromNode,
|
getInstanceFromNode,
|
||||||
getNodeFromInstance,
|
getNodeFromInstance,
|
||||||
|
|
|
||||||
196
packages/react-noop-renderer/src/createReactNoop.js
vendored
196
packages/react-noop-renderer/src/createReactNoop.js
vendored
|
|
@ -14,6 +14,7 @@
|
||||||
* environment.
|
* environment.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import type {Thenable} from 'react-reconciler/src/ReactFiberScheduler';
|
||||||
import type {Fiber} from 'react-reconciler/src/ReactFiber';
|
import type {Fiber} from 'react-reconciler/src/ReactFiber';
|
||||||
import type {UpdateQueue} from 'react-reconciler/src/ReactUpdateQueue';
|
import type {UpdateQueue} from 'react-reconciler/src/ReactUpdateQueue';
|
||||||
import type {ReactNodeList} from 'shared/ReactTypes';
|
import type {ReactNodeList} from 'shared/ReactTypes';
|
||||||
|
|
@ -26,16 +27,12 @@ import {
|
||||||
REACT_ELEMENT_TYPE,
|
REACT_ELEMENT_TYPE,
|
||||||
REACT_EVENT_TARGET_TOUCH_HIT,
|
REACT_EVENT_TARGET_TOUCH_HIT,
|
||||||
} from 'shared/ReactSymbols';
|
} from 'shared/ReactSymbols';
|
||||||
import warningWithoutStack from 'shared/warningWithoutStack';
|
|
||||||
import warning from 'shared/warning';
|
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 {enableEventAPI} from 'shared/ReactFeatureFlags';
|
||||||
|
|
||||||
// for .act's return value
|
|
||||||
type Thenable = {
|
|
||||||
then(resolve: () => mixed, reject?: () => mixed): mixed,
|
|
||||||
};
|
|
||||||
|
|
||||||
type Container = {
|
type Container = {
|
||||||
rootID: string,
|
rootID: string,
|
||||||
children: Array<Instance | TextInstance>,
|
children: Array<Instance | TextInstance>,
|
||||||
|
|
@ -59,6 +56,8 @@ type TextInstance = {|
|
||||||
|};
|
|};
|
||||||
type HostContext = Object;
|
type HostContext = Object;
|
||||||
|
|
||||||
|
const {ReactShouldWarnActingUpdates} = ReactSharedInternals;
|
||||||
|
|
||||||
const NO_CONTEXT = {};
|
const NO_CONTEXT = {};
|
||||||
const UPPERCASE_CONTEXT = {};
|
const UPPERCASE_CONTEXT = {};
|
||||||
const EVENT_COMPONENT_CONTEXT = {};
|
const EVENT_COMPONENT_CONTEXT = {};
|
||||||
|
|
@ -598,6 +597,140 @@ function createReactNoop(reconciler: Function, useMutation: boolean) {
|
||||||
const roots = new Map();
|
const roots = new Map();
|
||||||
const DEFAULT_ROOT_ID = '<default>';
|
const DEFAULT_ROOT_ID = '<default>';
|
||||||
|
|
||||||
|
const {flushPassiveEffects, batchedUpdates} = NoopRenderer;
|
||||||
|
|
||||||
|
// this act() implementation should be exactly the same in
|
||||||
|
// ReactTestUtilsAct.js, ReactTestRendererAct.js, createReactNoop.js
|
||||||
|
|
||||||
|
let actingUpdatesScopeDepth = 0;
|
||||||
|
|
||||||
|
function flushEffectsAndMicroTasks(onDone: (err: ?Error) => void) {
|
||||||
|
try {
|
||||||
|
flushPassiveEffects();
|
||||||
|
enqueueTask(() => {
|
||||||
|
if (flushPassiveEffects()) {
|
||||||
|
flushEffectsAndMicroTasks(onDone);
|
||||||
|
} else {
|
||||||
|
onDone();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
onDone(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function act(callback: () => Thenable) {
|
||||||
|
let previousActingUpdatesScopeDepth;
|
||||||
|
if (__DEV__) {
|
||||||
|
previousActingUpdatesScopeDepth = actingUpdatesScopeDepth;
|
||||||
|
actingUpdatesScopeDepth++;
|
||||||
|
ReactShouldWarnActingUpdates.current = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function onDone() {
|
||||||
|
if (__DEV__) {
|
||||||
|
actingUpdatesScopeDepth--;
|
||||||
|
if (actingUpdatesScopeDepth === 0) {
|
||||||
|
ReactShouldWarnActingUpdates.current = false;
|
||||||
|
}
|
||||||
|
if (actingUpdatesScopeDepth > previousActingUpdatesScopeDepth) {
|
||||||
|
// if it's _less than_ previousActingUpdatesScopeDepth, then we can assume the 'other' one has warned
|
||||||
|
warningWithoutStack(
|
||||||
|
null,
|
||||||
|
'You seem to have overlapping act() calls, this is not supported. ' +
|
||||||
|
'Be sure to await previous act() calls before making a new one. ',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = batchedUpdates(callback);
|
||||||
|
if (
|
||||||
|
result !== null &&
|
||||||
|
typeof result === 'object' &&
|
||||||
|
typeof result.then === 'function'
|
||||||
|
) {
|
||||||
|
// setup a boolean that gets set to true only
|
||||||
|
// once this act() call is await-ed
|
||||||
|
let called = false;
|
||||||
|
if (__DEV__) {
|
||||||
|
if (typeof Promise !== 'undefined') {
|
||||||
|
//eslint-disable-next-line no-undef
|
||||||
|
Promise.resolve()
|
||||||
|
.then(() => {})
|
||||||
|
.then(() => {
|
||||||
|
if (called === false) {
|
||||||
|
warningWithoutStack(
|
||||||
|
null,
|
||||||
|
'You called act(async () => ...) without await. ' +
|
||||||
|
'This could lead to unexpected testing behaviour, interleaving multiple act ' +
|
||||||
|
'calls and mixing their scopes. You should - await act(async () => ...);',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// in the async case, the returned thenable runs the callback, flushes
|
||||||
|
// effects and microtasks in a loop until flushPassiveEffects() === false,
|
||||||
|
// and cleans up
|
||||||
|
return {
|
||||||
|
then(resolve: () => void, reject: (?Error) => void) {
|
||||||
|
called = true;
|
||||||
|
result.then(
|
||||||
|
() => {
|
||||||
|
flushEffectsAndMicroTasks((err: ?Error) => {
|
||||||
|
onDone();
|
||||||
|
if (err) {
|
||||||
|
reject(err);
|
||||||
|
} else {
|
||||||
|
resolve();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
err => {
|
||||||
|
onDone();
|
||||||
|
reject(err);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
if (__DEV__) {
|
||||||
|
warningWithoutStack(
|
||||||
|
result === undefined,
|
||||||
|
'The callback passed to act(...) function ' +
|
||||||
|
'must return undefined, or a Promise. You returned %s',
|
||||||
|
result,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// flush effects until none remain, and cleanup
|
||||||
|
try {
|
||||||
|
while (flushPassiveEffects()) {}
|
||||||
|
onDone();
|
||||||
|
} catch (err) {
|
||||||
|
onDone();
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
|
||||||
|
// in the sync case, the returned thenable only warns *if* await-ed
|
||||||
|
return {
|
||||||
|
then(resolve: () => void) {
|
||||||
|
if (__DEV__) {
|
||||||
|
warningWithoutStack(
|
||||||
|
false,
|
||||||
|
'Do not await the result of calling act(...) with sync logic, it is not a Promise.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
resolve();
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// end act() implementation
|
||||||
|
|
||||||
function childToJSX(child, text) {
|
function childToJSX(child, text) {
|
||||||
if (text !== null) {
|
if (text !== null) {
|
||||||
return text;
|
return text;
|
||||||
|
|
@ -843,56 +976,13 @@ function createReactNoop(reconciler: Function, useMutation: boolean) {
|
||||||
|
|
||||||
interactiveUpdates: NoopRenderer.interactiveUpdates,
|
interactiveUpdates: NoopRenderer.interactiveUpdates,
|
||||||
|
|
||||||
// maybe this should exist only in the test file
|
|
||||||
act(callback: () => void): Thenable {
|
|
||||||
// note: keep these warning messages in sync with
|
|
||||||
// ReactTestRenderer.js and ReactTestUtils.js
|
|
||||||
let result = NoopRenderer.batchedUpdates(callback);
|
|
||||||
if (__DEV__) {
|
|
||||||
if (result !== undefined) {
|
|
||||||
let addendum;
|
|
||||||
if (result !== null && typeof result.then === 'function') {
|
|
||||||
addendum =
|
|
||||||
"\n\nIt looks like you wrote ReactNoop.act(async () => ...) or returned a Promise from it's callback. " +
|
|
||||||
'Putting asynchronous logic inside ReactNoop.act(...) is not supported.\n';
|
|
||||||
} else {
|
|
||||||
addendum = ' You returned: ' + result;
|
|
||||||
}
|
|
||||||
warningWithoutStack(
|
|
||||||
false,
|
|
||||||
'The callback passed to ReactNoop.act(...) function must not return anything.%s',
|
|
||||||
addendum,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ReactNoop.flushPassiveEffects();
|
|
||||||
// we want the user to not expect a return,
|
|
||||||
// but we want to warn if they use it like they can await on it.
|
|
||||||
return {
|
|
||||||
then() {
|
|
||||||
if (__DEV__) {
|
|
||||||
warningWithoutStack(
|
|
||||||
false,
|
|
||||||
'Do not await the result of calling ReactNoop.act(...), it is not a Promise.',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
flushSync(fn: () => mixed) {
|
flushSync(fn: () => mixed) {
|
||||||
NoopRenderer.flushSync(fn);
|
NoopRenderer.flushSync(fn);
|
||||||
},
|
},
|
||||||
|
|
||||||
flushPassiveEffects() {
|
flushPassiveEffects: NoopRenderer.flushPassiveEffects,
|
||||||
// Trick to flush passive effects without exposing an internal API:
|
|
||||||
// Create a throwaway root and schedule a dummy update on it.
|
act,
|
||||||
const rootID = 'bloopandthenmoreletterstoavoidaconflict';
|
|
||||||
const container = {rootID: rootID, pendingChildren: [], children: []};
|
|
||||||
rootContainers.set(rootID, container);
|
|
||||||
const root = NoopRenderer.createContainer(container, true, false);
|
|
||||||
NoopRenderer.updateContainer(null, root, null, null);
|
|
||||||
},
|
|
||||||
|
|
||||||
// Logs the current state of the tree.
|
// Logs the current state of the tree.
|
||||||
dumpTree(rootID: string = DEFAULT_ROOT_ID) {
|
dumpTree(rootID: string = DEFAULT_ROOT_ID) {
|
||||||
|
|
|
||||||
22
packages/react-reconciler/src/ReactFiberHooks.js
vendored
22
packages/react-reconciler/src/ReactFiberHooks.js
vendored
|
|
@ -30,10 +30,10 @@ import {
|
||||||
} from './ReactHookEffectTags';
|
} from './ReactHookEffectTags';
|
||||||
import {
|
import {
|
||||||
scheduleWork,
|
scheduleWork,
|
||||||
warnIfNotCurrentlyBatchingInDev,
|
|
||||||
computeExpirationForFiber,
|
computeExpirationForFiber,
|
||||||
flushPassiveEffects,
|
flushPassiveEffects,
|
||||||
requestCurrentTime,
|
requestCurrentTime,
|
||||||
|
warnIfNotCurrentlyActingUpdatesInDev,
|
||||||
} from './ReactFiberScheduler';
|
} from './ReactFiberScheduler';
|
||||||
|
|
||||||
import invariant from 'shared/invariant';
|
import invariant from 'shared/invariant';
|
||||||
|
|
@ -1046,19 +1046,6 @@ function updateMemo<T>(
|
||||||
return nextValue;
|
return nextValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// in a test-like environment, we want to warn if dispatchAction()
|
|
||||||
// is called outside of a batchedUpdates/TestUtils.act(...) call.
|
|
||||||
let shouldWarnForUnbatchedSetState = false;
|
|
||||||
|
|
||||||
if (__DEV__) {
|
|
||||||
// jest isn't a 'global', it's just exposed to tests via a wrapped function
|
|
||||||
// further, this isn't a test file, so flow doesn't recognize the symbol. So...
|
|
||||||
// $FlowExpectedError - because requirements don't give a damn about your type sigs.
|
|
||||||
if ('undefined' !== typeof jest) {
|
|
||||||
shouldWarnForUnbatchedSetState = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function dispatchAction<S, A>(
|
function dispatchAction<S, A>(
|
||||||
fiber: Fiber,
|
fiber: Fiber,
|
||||||
queue: UpdateQueue<S, A>,
|
queue: UpdateQueue<S, A>,
|
||||||
|
|
@ -1178,8 +1165,11 @@ function dispatchAction<S, A>(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (__DEV__) {
|
if (__DEV__) {
|
||||||
if (shouldWarnForUnbatchedSetState === true) {
|
// jest isn't a 'global', it's just exposed to tests via a wrapped function
|
||||||
warnIfNotCurrentlyBatchingInDev(fiber);
|
// further, this isn't a test file, so flow doesn't recognize the symbol. So...
|
||||||
|
// $FlowExpectedError - because requirements don't give a damn about your type sigs.
|
||||||
|
if ('undefined' !== typeof jest) {
|
||||||
|
warnIfNotCurrentlyActingUpdatesInDev(fiber);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
scheduleWork(fiber, expirationTime);
|
scheduleWork(fiber, expirationTime);
|
||||||
|
|
|
||||||
|
|
@ -310,6 +310,7 @@ export {
|
||||||
flushInteractiveUpdates,
|
flushInteractiveUpdates,
|
||||||
flushControlled,
|
flushControlled,
|
||||||
flushSync,
|
flushSync,
|
||||||
|
flushPassiveEffects,
|
||||||
};
|
};
|
||||||
|
|
||||||
export function getPublicRootInstance(
|
export function getPublicRootInstance(
|
||||||
|
|
|
||||||
|
|
@ -34,7 +34,7 @@ import {
|
||||||
flushInteractiveUpdates as flushInteractiveUpdates_old,
|
flushInteractiveUpdates as flushInteractiveUpdates_old,
|
||||||
computeUniqueAsyncExpiration as computeUniqueAsyncExpiration_old,
|
computeUniqueAsyncExpiration as computeUniqueAsyncExpiration_old,
|
||||||
flushPassiveEffects as flushPassiveEffects_old,
|
flushPassiveEffects as flushPassiveEffects_old,
|
||||||
warnIfNotCurrentlyBatchingInDev as warnIfNotCurrentlyBatchingInDev_old,
|
warnIfNotCurrentlyActingUpdatesInDev as warnIfNotCurrentlyActingUpdatesInDev_old,
|
||||||
} from './ReactFiberScheduler.old';
|
} from './ReactFiberScheduler.old';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
|
@ -62,7 +62,7 @@ import {
|
||||||
flushInteractiveUpdates as flushInteractiveUpdates_new,
|
flushInteractiveUpdates as flushInteractiveUpdates_new,
|
||||||
computeUniqueAsyncExpiration as computeUniqueAsyncExpiration_new,
|
computeUniqueAsyncExpiration as computeUniqueAsyncExpiration_new,
|
||||||
flushPassiveEffects as flushPassiveEffects_new,
|
flushPassiveEffects as flushPassiveEffects_new,
|
||||||
warnIfNotCurrentlyBatchingInDev as warnIfNotCurrentlyBatchingInDev_new,
|
warnIfNotCurrentlyActingUpdatesInDev as warnIfNotCurrentlyActingUpdatesInDev_new,
|
||||||
} from './ReactFiberScheduler.new';
|
} from './ReactFiberScheduler.new';
|
||||||
|
|
||||||
export let requestCurrentTime = requestCurrentTime_old;
|
export let requestCurrentTime = requestCurrentTime_old;
|
||||||
|
|
@ -89,7 +89,7 @@ export let interactiveUpdates = interactiveUpdates_old;
|
||||||
export let flushInteractiveUpdates = flushInteractiveUpdates_old;
|
export let flushInteractiveUpdates = flushInteractiveUpdates_old;
|
||||||
export let computeUniqueAsyncExpiration = computeUniqueAsyncExpiration_old;
|
export let computeUniqueAsyncExpiration = computeUniqueAsyncExpiration_old;
|
||||||
export let flushPassiveEffects = flushPassiveEffects_old;
|
export let flushPassiveEffects = flushPassiveEffects_old;
|
||||||
export let warnIfNotCurrentlyBatchingInDev = warnIfNotCurrentlyBatchingInDev_old;
|
export let warnIfNotCurrentlyActingUpdatesInDev = warnIfNotCurrentlyActingUpdatesInDev_old;
|
||||||
|
|
||||||
if (enableNewScheduler) {
|
if (enableNewScheduler) {
|
||||||
requestCurrentTime = requestCurrentTime_new;
|
requestCurrentTime = requestCurrentTime_new;
|
||||||
|
|
@ -116,9 +116,9 @@ if (enableNewScheduler) {
|
||||||
flushInteractiveUpdates = flushInteractiveUpdates_new;
|
flushInteractiveUpdates = flushInteractiveUpdates_new;
|
||||||
computeUniqueAsyncExpiration = computeUniqueAsyncExpiration_new;
|
computeUniqueAsyncExpiration = computeUniqueAsyncExpiration_new;
|
||||||
flushPassiveEffects = flushPassiveEffects_new;
|
flushPassiveEffects = flushPassiveEffects_new;
|
||||||
warnIfNotCurrentlyBatchingInDev = warnIfNotCurrentlyBatchingInDev_new;
|
warnIfNotCurrentlyActingUpdatesInDev = warnIfNotCurrentlyActingUpdatesInDev_new;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Thenable = {
|
export type Thenable = {
|
||||||
then(resolve: () => mixed, reject?: () => mixed): mixed,
|
then(resolve: () => mixed, reject?: () => mixed): void | Thenable,
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -33,4 +33,4 @@ export const interactiveUpdates = notYetImplemented;
|
||||||
export const flushInteractiveUpdates = notYetImplemented;
|
export const flushInteractiveUpdates = notYetImplemented;
|
||||||
export const computeUniqueAsyncExpiration = notYetImplemented;
|
export const computeUniqueAsyncExpiration = notYetImplemented;
|
||||||
export const flushPassiveEffects = notYetImplemented;
|
export const flushPassiveEffects = notYetImplemented;
|
||||||
export const warnIfNotCurrentlyBatchingInDev = notYetImplemented;
|
export const warnIfNotCurrentlyActingUpdatesInDev = notYetImplemented;
|
||||||
|
|
|
||||||
|
|
@ -176,10 +176,14 @@ const {
|
||||||
} = Scheduler;
|
} = Scheduler;
|
||||||
|
|
||||||
export type Thenable = {
|
export type Thenable = {
|
||||||
then(resolve: () => mixed, reject?: () => mixed): mixed,
|
then(resolve: () => mixed, reject?: () => mixed): void | Thenable,
|
||||||
};
|
};
|
||||||
|
|
||||||
const {ReactCurrentDispatcher, ReactCurrentOwner} = ReactSharedInternals;
|
const {
|
||||||
|
ReactCurrentDispatcher,
|
||||||
|
ReactCurrentOwner,
|
||||||
|
ReactShouldWarnActingUpdates,
|
||||||
|
} = ReactSharedInternals;
|
||||||
|
|
||||||
let didWarnAboutStateTransition;
|
let didWarnAboutStateTransition;
|
||||||
let didWarnSetStateChildContext;
|
let didWarnSetStateChildContext;
|
||||||
|
|
@ -610,6 +614,7 @@ function markLegacyErrorBoundaryAsFailed(instance: mixed) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function flushPassiveEffects() {
|
function flushPassiveEffects() {
|
||||||
|
const didFlushEffects = passiveEffectCallback !== null;
|
||||||
if (passiveEffectCallbackHandle !== null) {
|
if (passiveEffectCallbackHandle !== null) {
|
||||||
cancelCallback(passiveEffectCallbackHandle);
|
cancelCallback(passiveEffectCallbackHandle);
|
||||||
}
|
}
|
||||||
|
|
@ -618,6 +623,7 @@ function flushPassiveEffects() {
|
||||||
// to ensure tracing works correctly.
|
// to ensure tracing works correctly.
|
||||||
passiveEffectCallback();
|
passiveEffectCallback();
|
||||||
}
|
}
|
||||||
|
return didFlushEffects;
|
||||||
}
|
}
|
||||||
|
|
||||||
function commitRoot(root: FiberRoot, finishedWork: Fiber): void {
|
function commitRoot(root: FiberRoot, finishedWork: Fiber): void {
|
||||||
|
|
@ -1836,9 +1842,20 @@ function scheduleWorkToRoot(fiber: Fiber, expirationTime): FiberRoot | null {
|
||||||
return root;
|
return root;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function warnIfNotCurrentlyBatchingInDev(fiber: Fiber): void {
|
// in a test-like environment, we want to warn if dispatchAction() is
|
||||||
|
// called outside of a TestUtils.act(...)/batchedUpdates/render call.
|
||||||
|
// so we have a a step counter for when we descend/ascend from
|
||||||
|
// act() calls, and test on it for when to warn
|
||||||
|
// It's a tuple with a single value. Look for shared/createAct to
|
||||||
|
// see how we change the value inside act() calls
|
||||||
|
|
||||||
|
export function warnIfNotCurrentlyActingUpdatesInDev(fiber: Fiber): void {
|
||||||
if (__DEV__) {
|
if (__DEV__) {
|
||||||
if (isRendering === false && isBatchingUpdates === false) {
|
if (
|
||||||
|
isBatchingUpdates === false &&
|
||||||
|
isRendering === false &&
|
||||||
|
ReactShouldWarnActingUpdates.current === false
|
||||||
|
) {
|
||||||
warningWithoutStack(
|
warningWithoutStack(
|
||||||
false,
|
false,
|
||||||
'An update to %s inside a test was not wrapped in act(...).\n\n' +
|
'An update to %s inside a test was not wrapped in act(...).\n\n' +
|
||||||
|
|
|
||||||
|
|
@ -1046,11 +1046,10 @@ describe('ReactHooks', () => {
|
||||||
|
|
||||||
class Cls extends React.Component {
|
class Cls extends React.Component {
|
||||||
render() {
|
render() {
|
||||||
act(() =>
|
_setState(() =>
|
||||||
_setState(() => {
|
ReactCurrentDispatcher.current.readContext(ThemeContext),
|
||||||
ReactCurrentDispatcher.current.readContext(ThemeContext);
|
|
||||||
}),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1062,13 +1061,7 @@ describe('ReactHooks', () => {
|
||||||
<Cls />
|
<Cls />
|
||||||
</React.Fragment>,
|
</React.Fragment>,
|
||||||
),
|
),
|
||||||
).toWarnDev(
|
).toWarnDev(['Context can only be read while React is rendering']);
|
||||||
[
|
|
||||||
'Context can only be read while React is rendering',
|
|
||||||
'Render methods should be a pure function of props and state',
|
|
||||||
],
|
|
||||||
{withoutStack: 1},
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('warns when calling hooks inside useReducer', () => {
|
it('warns when calling hooks inside useReducer', () => {
|
||||||
|
|
|
||||||
63
packages/react-reconciler/src/__tests__/ReactNoopRendererAct-test.js
vendored
Normal file
63
packages/react-reconciler/src/__tests__/ReactNoopRendererAct-test.js
vendored
Normal file
|
|
@ -0,0 +1,63 @@
|
||||||
|
/**
|
||||||
|
* 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.
|
||||||
|
*
|
||||||
|
* @jest-environment node
|
||||||
|
*/
|
||||||
|
|
||||||
|
// sanity tests for ReactNoop.act()
|
||||||
|
|
||||||
|
jest.useRealTimers();
|
||||||
|
const React = require('react');
|
||||||
|
const ReactNoop = require('react-noop-renderer');
|
||||||
|
const Scheduler = require('scheduler');
|
||||||
|
|
||||||
|
describe('ReactNoop.act()', () => {
|
||||||
|
it('can use act to flush effects', async () => {
|
||||||
|
function App(props) {
|
||||||
|
React.useEffect(props.callback);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
let calledLog = [];
|
||||||
|
ReactNoop.act(() => {
|
||||||
|
ReactNoop.render(
|
||||||
|
<App
|
||||||
|
callback={() => {
|
||||||
|
calledLog.push(calledLog.length);
|
||||||
|
}}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
expect(Scheduler).toFlushWithoutYielding();
|
||||||
|
expect(calledLog).toEqual([0]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should work with async/await', async () => {
|
||||||
|
function App() {
|
||||||
|
let [ctr, setCtr] = React.useState(0);
|
||||||
|
async function someAsyncFunction() {
|
||||||
|
Scheduler.yieldValue('stage 1');
|
||||||
|
await null;
|
||||||
|
Scheduler.yieldValue('stage 2');
|
||||||
|
setCtr(1);
|
||||||
|
}
|
||||||
|
React.useEffect(() => {
|
||||||
|
someAsyncFunction();
|
||||||
|
}, []);
|
||||||
|
return ctr;
|
||||||
|
}
|
||||||
|
await ReactNoop.act(async () => {
|
||||||
|
ReactNoop.act(() => {
|
||||||
|
ReactNoop.render(<App />);
|
||||||
|
});
|
||||||
|
await null;
|
||||||
|
expect(Scheduler).toFlushAndYield(['stage 1']);
|
||||||
|
});
|
||||||
|
expect(Scheduler).toHaveYielded(['stage 2']);
|
||||||
|
expect(Scheduler).toFlushWithoutYielding();
|
||||||
|
expect(ReactNoop.getChildren()).toEqual([{text: '1', hidden: false}]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -40,7 +40,7 @@ import {
|
||||||
} from 'shared/ReactWorkTags';
|
} from 'shared/ReactWorkTags';
|
||||||
import invariant from 'shared/invariant';
|
import invariant from 'shared/invariant';
|
||||||
import ReactVersion from 'shared/ReactVersion';
|
import ReactVersion from 'shared/ReactVersion';
|
||||||
import warningWithoutStack from 'shared/warningWithoutStack';
|
import act from './ReactTestRendererAct';
|
||||||
|
|
||||||
import {getPublicInstance} from './ReactTestHostConfig';
|
import {getPublicInstance} from './ReactTestHostConfig';
|
||||||
|
|
||||||
|
|
@ -65,11 +65,6 @@ type FindOptions = $Shape<{
|
||||||
|
|
||||||
export type Predicate = (node: ReactTestInstance) => ?boolean;
|
export type Predicate = (node: ReactTestInstance) => ?boolean;
|
||||||
|
|
||||||
// for .act's return value
|
|
||||||
type Thenable = {
|
|
||||||
then(resolve: () => mixed, reject?: () => mixed): mixed,
|
|
||||||
};
|
|
||||||
|
|
||||||
const defaultTestOptions = {
|
const defaultTestOptions = {
|
||||||
createNodeMock: function() {
|
createNodeMock: function() {
|
||||||
return null;
|
return null;
|
||||||
|
|
@ -549,64 +544,12 @@ const ReactTestRendererFiber = {
|
||||||
return entry;
|
return entry;
|
||||||
},
|
},
|
||||||
|
|
||||||
/* eslint-disable camelcase */
|
/* eslint-disable-next-line camelcase */
|
||||||
unstable_batchedUpdates: batchedUpdates,
|
unstable_batchedUpdates: batchedUpdates,
|
||||||
/* eslint-enable camelcase */
|
|
||||||
|
|
||||||
act(callback: () => void): Thenable {
|
act,
|
||||||
// note: keep these warning messages in sync with
|
|
||||||
// createNoop.js and ReactTestUtils.js
|
|
||||||
let result = batchedUpdates(callback);
|
|
||||||
if (__DEV__) {
|
|
||||||
if (result !== undefined) {
|
|
||||||
let addendum;
|
|
||||||
if (result !== null && typeof result.then === 'function') {
|
|
||||||
addendum =
|
|
||||||
"\n\nIt looks like you wrote TestRenderer.act(async () => ...) or returned a Promise from it's callback. " +
|
|
||||||
'Putting asynchronous logic inside TestRenderer.act(...) is not supported.\n';
|
|
||||||
} else {
|
|
||||||
addendum = ' You returned: ' + result;
|
|
||||||
}
|
|
||||||
warningWithoutStack(
|
|
||||||
false,
|
|
||||||
'The callback passed to TestRenderer.act(...) function must not return anything.%s',
|
|
||||||
addendum,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
flushPassiveEffects();
|
|
||||||
// we want the user to not expect a return,
|
|
||||||
// but we want to warn if they use it like they can await on it.
|
|
||||||
return {
|
|
||||||
then() {
|
|
||||||
if (__DEV__) {
|
|
||||||
warningWithoutStack(
|
|
||||||
false,
|
|
||||||
'Do not await the result of calling TestRenderer.act(...), it is not a Promise.',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
};
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// root used to flush effects during .act() calls
|
|
||||||
const actRoot = createContainer(
|
|
||||||
{
|
|
||||||
children: [],
|
|
||||||
createNodeMock: defaultTestOptions.createNodeMock,
|
|
||||||
tag: 'CONTAINER',
|
|
||||||
},
|
|
||||||
true,
|
|
||||||
false,
|
|
||||||
);
|
|
||||||
|
|
||||||
function flushPassiveEffects() {
|
|
||||||
// Trick to flush passive effects without exposing an internal API:
|
|
||||||
// Create a throwaway root and schedule a dummy update on it.
|
|
||||||
updateContainer(null, actRoot, null, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
const fiberToWrapper = new WeakMap();
|
const fiberToWrapper = new WeakMap();
|
||||||
function wrapFiber(fiber: Fiber): ReactTestInstance {
|
function wrapFiber(fiber: Fiber): ReactTestInstance {
|
||||||
let wrapper = fiberToWrapper.get(fiber);
|
let wrapper = fiberToWrapper.get(fiber);
|
||||||
|
|
|
||||||
153
packages/react-test-renderer/src/ReactTestRendererAct.js
vendored
Normal file
153
packages/react-test-renderer/src/ReactTestRendererAct.js
vendored
Normal file
|
|
@ -0,0 +1,153 @@
|
||||||
|
/**
|
||||||
|
* 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.
|
||||||
|
*
|
||||||
|
* @flow
|
||||||
|
*/
|
||||||
|
import type {Thenable} from 'react-reconciler/src/ReactFiberScheduler';
|
||||||
|
|
||||||
|
import {
|
||||||
|
batchedUpdates,
|
||||||
|
flushPassiveEffects,
|
||||||
|
} from 'react-reconciler/inline.test';
|
||||||
|
import ReactSharedInternals from 'shared/ReactSharedInternals';
|
||||||
|
import warningWithoutStack from 'shared/warningWithoutStack';
|
||||||
|
import enqueueTask from 'shared/enqueueTask';
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
function flushEffectsAndMicroTasks(onDone: (err: ?Error) => void) {
|
||||||
|
try {
|
||||||
|
flushPassiveEffects();
|
||||||
|
enqueueTask(() => {
|
||||||
|
if (flushPassiveEffects()) {
|
||||||
|
flushEffectsAndMicroTasks(onDone);
|
||||||
|
} else {
|
||||||
|
onDone();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
onDone(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function act(callback: () => Thenable) {
|
||||||
|
let previousActingUpdatesScopeDepth;
|
||||||
|
if (__DEV__) {
|
||||||
|
previousActingUpdatesScopeDepth = actingUpdatesScopeDepth;
|
||||||
|
actingUpdatesScopeDepth++;
|
||||||
|
ReactShouldWarnActingUpdates.current = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function onDone() {
|
||||||
|
if (__DEV__) {
|
||||||
|
actingUpdatesScopeDepth--;
|
||||||
|
if (actingUpdatesScopeDepth === 0) {
|
||||||
|
ReactShouldWarnActingUpdates.current = false;
|
||||||
|
}
|
||||||
|
if (actingUpdatesScopeDepth > previousActingUpdatesScopeDepth) {
|
||||||
|
// if it's _less than_ previousActingUpdatesScopeDepth, then we can assume the 'other' one has warned
|
||||||
|
warningWithoutStack(
|
||||||
|
null,
|
||||||
|
'You seem to have overlapping act() calls, this is not supported. ' +
|
||||||
|
'Be sure to await previous act() calls before making a new one. ',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = batchedUpdates(callback);
|
||||||
|
if (
|
||||||
|
result !== null &&
|
||||||
|
typeof result === 'object' &&
|
||||||
|
typeof result.then === 'function'
|
||||||
|
) {
|
||||||
|
// setup a boolean that gets set to true only
|
||||||
|
// once this act() call is await-ed
|
||||||
|
let called = false;
|
||||||
|
if (__DEV__) {
|
||||||
|
if (typeof Promise !== 'undefined') {
|
||||||
|
//eslint-disable-next-line no-undef
|
||||||
|
Promise.resolve()
|
||||||
|
.then(() => {})
|
||||||
|
.then(() => {
|
||||||
|
if (called === false) {
|
||||||
|
warningWithoutStack(
|
||||||
|
null,
|
||||||
|
'You called act(async () => ...) without await. ' +
|
||||||
|
'This could lead to unexpected testing behaviour, interleaving multiple act ' +
|
||||||
|
'calls and mixing their scopes. You should - await act(async () => ...);',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// in the async case, the returned thenable runs the callback, flushes
|
||||||
|
// effects and microtasks in a loop until flushPassiveEffects() === false,
|
||||||
|
// and cleans up
|
||||||
|
return {
|
||||||
|
then(resolve: () => void, reject: (?Error) => void) {
|
||||||
|
called = true;
|
||||||
|
result.then(
|
||||||
|
() => {
|
||||||
|
flushEffectsAndMicroTasks((err: ?Error) => {
|
||||||
|
onDone();
|
||||||
|
if (err) {
|
||||||
|
reject(err);
|
||||||
|
} else {
|
||||||
|
resolve();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
err => {
|
||||||
|
onDone();
|
||||||
|
reject(err);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
if (__DEV__) {
|
||||||
|
warningWithoutStack(
|
||||||
|
result === undefined,
|
||||||
|
'The callback passed to act(...) function ' +
|
||||||
|
'must return undefined, or a Promise. You returned %s',
|
||||||
|
result,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// flush effects until none remain, and cleanup
|
||||||
|
try {
|
||||||
|
while (flushPassiveEffects()) {}
|
||||||
|
onDone();
|
||||||
|
} catch (err) {
|
||||||
|
onDone();
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
|
||||||
|
// in the sync case, the returned thenable only warns *if* await-ed
|
||||||
|
return {
|
||||||
|
then(resolve: () => void) {
|
||||||
|
if (__DEV__) {
|
||||||
|
warningWithoutStack(
|
||||||
|
false,
|
||||||
|
'Do not await the result of calling act(...) with sync logic, it is not a Promise.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
resolve();
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default act;
|
||||||
|
|
@ -1023,27 +1023,7 @@ describe('ReactTestRenderer', () => {
|
||||||
ReactTestRenderer.create(<App />);
|
ReactTestRenderer.create(<App />);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('act', () => {
|
// we run this test here because we need a dom-less scope
|
||||||
it('can use .act() to batch updates and effects', () => {
|
|
||||||
function App(props) {
|
|
||||||
React.useEffect(() => {
|
|
||||||
props.callback();
|
|
||||||
});
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
let called = false;
|
|
||||||
ReactTestRenderer.act(() => {
|
|
||||||
ReactTestRenderer.create(
|
|
||||||
<App
|
|
||||||
callback={() => {
|
|
||||||
called = true;
|
|
||||||
}}
|
|
||||||
/>,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(called).toBe(true);
|
|
||||||
});
|
|
||||||
it('warns and throws if you use TestUtils.act instead of TestRenderer.act in node', () => {
|
it('warns and throws if you use TestUtils.act instead of TestRenderer.act in node', () => {
|
||||||
// we warn when you try to load 2 renderers in the same 'scope'
|
// we warn when you try to load 2 renderers in the same 'scope'
|
||||||
// so as suggested, we call resetModules() to carry on with the test
|
// so as suggested, we call resetModules() to carry on with the test
|
||||||
|
|
@ -1053,10 +1033,9 @@ describe('ReactTestRenderer', () => {
|
||||||
expect(() => act(() => {})).toThrow('document is not defined');
|
expect(() => act(() => {})).toThrow('document is not defined');
|
||||||
}).toWarnDev(
|
}).toWarnDev(
|
||||||
[
|
[
|
||||||
'It looks like you called TestUtils.act(...) in a non-browser environment',
|
'It looks like you called ReactTestUtils.act(...) in a non-browser environment',
|
||||||
],
|
],
|
||||||
{withoutStack: 1},
|
{withoutStack: 1},
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
|
||||||
122
packages/react-test-renderer/src/__tests__/ReactTestRendererAct-test.js
vendored
Normal file
122
packages/react-test-renderer/src/__tests__/ReactTestRendererAct-test.js
vendored
Normal file
|
|
@ -0,0 +1,122 @@
|
||||||
|
jest.useRealTimers();
|
||||||
|
|
||||||
|
let React;
|
||||||
|
let ReactTestRenderer;
|
||||||
|
let Scheduler;
|
||||||
|
let act;
|
||||||
|
|
||||||
|
describe('ReactTestRenderer.act()', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.resetModules();
|
||||||
|
React = require('react');
|
||||||
|
ReactTestRenderer = require('react-test-renderer');
|
||||||
|
Scheduler = require('scheduler');
|
||||||
|
act = ReactTestRenderer.act;
|
||||||
|
});
|
||||||
|
it('can use .act() to flush effects', () => {
|
||||||
|
function App(props) {
|
||||||
|
let [ctr, setCtr] = React.useState(0);
|
||||||
|
React.useEffect(() => {
|
||||||
|
props.callback();
|
||||||
|
setCtr(1);
|
||||||
|
}, []);
|
||||||
|
return ctr;
|
||||||
|
}
|
||||||
|
let calledLog = [];
|
||||||
|
let root;
|
||||||
|
act(() => {
|
||||||
|
root = ReactTestRenderer.create(
|
||||||
|
<App
|
||||||
|
callback={() => {
|
||||||
|
calledLog.push(calledLog.length);
|
||||||
|
}}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(calledLog).toEqual([0]);
|
||||||
|
expect(root.toJSON()).toEqual('1');
|
||||||
|
});
|
||||||
|
|
||||||
|
it("warns if you don't use .act", () => {
|
||||||
|
let setCtr;
|
||||||
|
function App(props) {
|
||||||
|
let [ctr, _setCtr] = React.useState(0);
|
||||||
|
setCtr = _setCtr;
|
||||||
|
return ctr;
|
||||||
|
}
|
||||||
|
|
||||||
|
ReactTestRenderer.create(<App />);
|
||||||
|
|
||||||
|
expect(() => {
|
||||||
|
setCtr(1);
|
||||||
|
}).toWarnDev([
|
||||||
|
'An update to App inside a test was not wrapped in act(...)',
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('async', () => {
|
||||||
|
it('should work with async/await', async () => {
|
||||||
|
function fetch(url) {
|
||||||
|
return Promise.resolve({
|
||||||
|
details: [1, 2, 3],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
function App() {
|
||||||
|
let [details, setDetails] = React.useState(0);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
async function fetchDetails() {
|
||||||
|
const response = await fetch();
|
||||||
|
setDetails(response.details);
|
||||||
|
}
|
||||||
|
fetchDetails();
|
||||||
|
}, []);
|
||||||
|
return details;
|
||||||
|
}
|
||||||
|
let root;
|
||||||
|
|
||||||
|
await ReactTestRenderer.act(async () => {
|
||||||
|
root = ReactTestRenderer.create(<App />);
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(root.toJSON()).toEqual(['1', '2', '3']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not flush effects without also flushing microtasks', async () => {
|
||||||
|
const {useEffect, useReducer} = React;
|
||||||
|
|
||||||
|
const alreadyResolvedPromise = Promise.resolve();
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
// This component will keep updating itself until step === 3
|
||||||
|
const [step, proceed] = useReducer(s => (s === 3 ? 3 : s + 1), 1);
|
||||||
|
useEffect(() => {
|
||||||
|
Scheduler.yieldValue('Effect');
|
||||||
|
alreadyResolvedPromise.then(() => {
|
||||||
|
Scheduler.yieldValue('Microtask');
|
||||||
|
proceed();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return step;
|
||||||
|
}
|
||||||
|
const root = ReactTestRenderer.create(null);
|
||||||
|
await act(async () => {
|
||||||
|
root.update(<App />);
|
||||||
|
});
|
||||||
|
expect(Scheduler).toHaveYielded([
|
||||||
|
// Should not flush effects without also flushing microtasks
|
||||||
|
// First render:
|
||||||
|
'Effect',
|
||||||
|
'Microtask',
|
||||||
|
// Second render:
|
||||||
|
'Effect',
|
||||||
|
'Microtask',
|
||||||
|
// Final render:
|
||||||
|
'Effect',
|
||||||
|
'Microtask',
|
||||||
|
]);
|
||||||
|
expect(root).toMatchRenderedOutput('3');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -15,6 +15,8 @@ import ReactDebugCurrentFrame from './ReactDebugCurrentFrame';
|
||||||
const ReactSharedInternals = {
|
const ReactSharedInternals = {
|
||||||
ReactCurrentDispatcher,
|
ReactCurrentDispatcher,
|
||||||
ReactCurrentOwner,
|
ReactCurrentOwner,
|
||||||
|
// used by act()
|
||||||
|
ReactShouldWarnActingUpdates: {current: false},
|
||||||
// Used by renderers to avoid bundling object-assign twice in UMD bundles:
|
// Used by renderers to avoid bundling object-assign twice in UMD bundles:
|
||||||
assign,
|
assign,
|
||||||
};
|
};
|
||||||
|
|
|
||||||
42
packages/shared/enqueueTask.js
Normal file
42
packages/shared/enqueueTask.js
Normal file
|
|
@ -0,0 +1,42 @@
|
||||||
|
/**
|
||||||
|
* 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.
|
||||||
|
*
|
||||||
|
* @flow
|
||||||
|
*/
|
||||||
|
|
||||||
|
import warningWithoutStack from './warningWithoutStack';
|
||||||
|
|
||||||
|
let didWarnAboutMessageChannel = false;
|
||||||
|
let enqueueTask;
|
||||||
|
try {
|
||||||
|
// assuming we're in node, let's try to get node's
|
||||||
|
// version of setImmediate, bypassing fake timers if any
|
||||||
|
let r = require; // trick packagers not to bundle this stuff.
|
||||||
|
enqueueTask = r('timers').setImmediate;
|
||||||
|
} catch (_err) {
|
||||||
|
// we're in a browser
|
||||||
|
// we can't use regular timers because they may still be faked
|
||||||
|
// so we try MessageChannel+postMessage instead
|
||||||
|
enqueueTask = function(callback: () => void) {
|
||||||
|
if (__DEV__) {
|
||||||
|
if (didWarnAboutMessageChannel === false) {
|
||||||
|
didWarnAboutMessageChannel = true;
|
||||||
|
warningWithoutStack(
|
||||||
|
typeof MessageChannel !== 'undefined',
|
||||||
|
'This browser does not have a MessageChannel implementation, ' +
|
||||||
|
'so enqueuing tasks via await act(async () => ...) will fail. ' +
|
||||||
|
'Please file an issue at https://github.com/facebook/react/issues ' +
|
||||||
|
'if you encounter this warning.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const channel = new MessageChannel();
|
||||||
|
channel.port1.onmessage = callback;
|
||||||
|
channel.port2.postMessage(undefined);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default enqueueTask;
|
||||||
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user