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-server.browser.development.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
|
||||
.DS_Store
|
||||
|
|
|
|||
|
|
@ -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 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",
|
||||
"test": "react-scripts test --env=jsdom",
|
||||
"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 ReactDOMServer;
|
||||
let ReactTestUtils;
|
||||
let act;
|
||||
|
||||
function getTestDocument(markup) {
|
||||
const doc = document.implementation.createHTMLDocument('');
|
||||
|
|
@ -34,7 +33,6 @@ describe('ReactTestUtils', () => {
|
|||
ReactDOM = require('react-dom');
|
||||
ReactDOMServer = require('react-dom/server');
|
||||
ReactTestUtils = require('react-dom/test-utils');
|
||||
act = ReactTestUtils.act;
|
||||
});
|
||||
|
||||
it('Simulate should have locally attached media events', () => {
|
||||
|
|
@ -517,173 +515,4 @@ describe('ReactTestUtils', () => {
|
|||
ReactTestUtils.renderIntoDocument(<Component />);
|
||||
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,
|
||||
findHostInstance,
|
||||
findHostInstanceWithWarning,
|
||||
flushPassiveEffects,
|
||||
} from 'react-reconciler/inline.dom';
|
||||
import {createPortal as createPortalImpl} from 'shared/ReactPortal';
|
||||
import {canUseDOM} from 'shared/ExecutionEnvironment';
|
||||
|
|
@ -807,7 +808,7 @@ const ReactDOM: Object = {
|
|||
|
||||
__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED: {
|
||||
// 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: [
|
||||
getInstanceFromNode,
|
||||
getNodeFromInstance,
|
||||
|
|
@ -820,6 +821,7 @@ const ReactDOM: Object = {
|
|||
restoreStateIfNeeded,
|
||||
dispatchEvent,
|
||||
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,
|
||||
findHostInstance,
|
||||
findHostInstanceWithWarning,
|
||||
flushPassiveEffects,
|
||||
} from 'react-reconciler/inline.fire';
|
||||
import {createPortal as createPortalImpl} from 'shared/ReactPortal';
|
||||
import {canUseDOM} from 'shared/ExecutionEnvironment';
|
||||
|
|
@ -826,6 +827,7 @@ const ReactDOM: Object = {
|
|||
restoreStateIfNeeded,
|
||||
dispatchEvent,
|
||||
runEventsInBatch,
|
||||
flushPassiveEffects,
|
||||
],
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
import type {Thenable} from 'react-reconciler/src/ReactFiberScheduler';
|
||||
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
|
|
@ -22,15 +23,11 @@ import warningWithoutStack from 'shared/warningWithoutStack';
|
|||
import {ELEMENT_NODE} from '../shared/HTMLNodeType';
|
||||
import * as DOMTopLevelEventTypes from '../events/DOMTopLevelEventTypes';
|
||||
import {PLUGIN_EVENT_SYSTEM} from 'events/EventSystemFlags';
|
||||
|
||||
// for .act's return value
|
||||
type Thenable = {
|
||||
then(resolve: () => mixed, reject?: () => mixed): mixed,
|
||||
};
|
||||
import act from './ReactTestUtilsAct';
|
||||
|
||||
const {findDOMNode} = ReactDOM;
|
||||
// Keep in sync with ReactDOMUnstableNativeDependencies.js
|
||||
// and ReactDOM.js:
|
||||
// ReactDOM.js, and ReactTestUtilsAct.js:
|
||||
const [
|
||||
getInstanceFromNode,
|
||||
/* eslint-disable no-unused-vars */
|
||||
|
|
@ -45,6 +42,8 @@ const [
|
|||
restoreStateIfNeeded,
|
||||
dispatchEvent,
|
||||
runEventsInBatch,
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
flushPassiveEffects,
|
||||
] = ReactDOM.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.Events;
|
||||
|
||||
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;
|
||||
|
||||
// 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.
|
||||
*
|
||||
|
|
@ -391,57 +393,24 @@ const ReactTestUtils = {
|
|||
Simulate: null,
|
||||
SimulateNative: {},
|
||||
|
||||
act(callback: () => void): Thenable {
|
||||
act(callback: () => Thenable) {
|
||||
if (actContainerElement === null) {
|
||||
// warn if we can't actually create the stub element
|
||||
if (__DEV__) {
|
||||
// warn if we're trying to use this in something like node (without jsdom)
|
||||
if (didWarnAboutActInNodejs === false) {
|
||||
didWarnAboutActInNodejs = true;
|
||||
warningWithoutStack(
|
||||
typeof document !== 'undefined' &&
|
||||
document !== null &&
|
||||
typeof document.createElement === 'function',
|
||||
'It looks like you called TestUtils.act(...) in a non-browser environment. ' +
|
||||
typeof document !== 'undefined' && document !== null,
|
||||
'It looks like you called ReactTestUtils.act(...) in a non-browser environment. ' +
|
||||
"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');
|
||||
}
|
||||
|
||||
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.',
|
||||
);
|
||||
}
|
||||
},
|
||||
};
|
||||
return act(callback);
|
||||
},
|
||||
};
|
||||
|
||||
|
|
|
|||
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';
|
||||
|
||||
// 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 [
|
||||
getInstanceFromNode,
|
||||
getNodeFromInstance,
|
||||
|
|
|
|||
196
packages/react-noop-renderer/src/createReactNoop.js
vendored
196
packages/react-noop-renderer/src/createReactNoop.js
vendored
|
|
@ -14,6 +14,7 @@
|
|||
* environment.
|
||||
*/
|
||||
|
||||
import type {Thenable} from 'react-reconciler/src/ReactFiberScheduler';
|
||||
import type {Fiber} from 'react-reconciler/src/ReactFiber';
|
||||
import type {UpdateQueue} from 'react-reconciler/src/ReactUpdateQueue';
|
||||
import type {ReactNodeList} from 'shared/ReactTypes';
|
||||
|
|
@ -26,16 +27,12 @@ import {
|
|||
REACT_ELEMENT_TYPE,
|
||||
REACT_EVENT_TARGET_TOUCH_HIT,
|
||||
} from 'shared/ReactSymbols';
|
||||
import warningWithoutStack from 'shared/warningWithoutStack';
|
||||
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';
|
||||
|
||||
// for .act's return value
|
||||
type Thenable = {
|
||||
then(resolve: () => mixed, reject?: () => mixed): mixed,
|
||||
};
|
||||
|
||||
type Container = {
|
||||
rootID: string,
|
||||
children: Array<Instance | TextInstance>,
|
||||
|
|
@ -59,6 +56,8 @@ type TextInstance = {|
|
|||
|};
|
||||
type HostContext = Object;
|
||||
|
||||
const {ReactShouldWarnActingUpdates} = ReactSharedInternals;
|
||||
|
||||
const NO_CONTEXT = {};
|
||||
const UPPERCASE_CONTEXT = {};
|
||||
const EVENT_COMPONENT_CONTEXT = {};
|
||||
|
|
@ -598,6 +597,140 @@ function createReactNoop(reconciler: Function, useMutation: boolean) {
|
|||
const roots = new Map();
|
||||
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) {
|
||||
if (text !== null) {
|
||||
return text;
|
||||
|
|
@ -843,56 +976,13 @@ function createReactNoop(reconciler: Function, useMutation: boolean) {
|
|||
|
||||
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) {
|
||||
NoopRenderer.flushSync(fn);
|
||||
},
|
||||
|
||||
flushPassiveEffects() {
|
||||
// Trick to flush passive effects without exposing an internal API:
|
||||
// Create a throwaway root and schedule a dummy update on it.
|
||||
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);
|
||||
},
|
||||
flushPassiveEffects: NoopRenderer.flushPassiveEffects,
|
||||
|
||||
act,
|
||||
|
||||
// Logs the current state of the tree.
|
||||
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';
|
||||
import {
|
||||
scheduleWork,
|
||||
warnIfNotCurrentlyBatchingInDev,
|
||||
computeExpirationForFiber,
|
||||
flushPassiveEffects,
|
||||
requestCurrentTime,
|
||||
warnIfNotCurrentlyActingUpdatesInDev,
|
||||
} from './ReactFiberScheduler';
|
||||
|
||||
import invariant from 'shared/invariant';
|
||||
|
|
@ -1046,19 +1046,6 @@ function updateMemo<T>(
|
|||
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>(
|
||||
fiber: Fiber,
|
||||
queue: UpdateQueue<S, A>,
|
||||
|
|
@ -1178,8 +1165,11 @@ function dispatchAction<S, A>(
|
|||
}
|
||||
}
|
||||
if (__DEV__) {
|
||||
if (shouldWarnForUnbatchedSetState === true) {
|
||||
warnIfNotCurrentlyBatchingInDev(fiber);
|
||||
// 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) {
|
||||
warnIfNotCurrentlyActingUpdatesInDev(fiber);
|
||||
}
|
||||
}
|
||||
scheduleWork(fiber, expirationTime);
|
||||
|
|
|
|||
|
|
@ -310,6 +310,7 @@ export {
|
|||
flushInteractiveUpdates,
|
||||
flushControlled,
|
||||
flushSync,
|
||||
flushPassiveEffects,
|
||||
};
|
||||
|
||||
export function getPublicRootInstance(
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ import {
|
|||
flushInteractiveUpdates as flushInteractiveUpdates_old,
|
||||
computeUniqueAsyncExpiration as computeUniqueAsyncExpiration_old,
|
||||
flushPassiveEffects as flushPassiveEffects_old,
|
||||
warnIfNotCurrentlyBatchingInDev as warnIfNotCurrentlyBatchingInDev_old,
|
||||
warnIfNotCurrentlyActingUpdatesInDev as warnIfNotCurrentlyActingUpdatesInDev_old,
|
||||
} from './ReactFiberScheduler.old';
|
||||
|
||||
import {
|
||||
|
|
@ -62,7 +62,7 @@ import {
|
|||
flushInteractiveUpdates as flushInteractiveUpdates_new,
|
||||
computeUniqueAsyncExpiration as computeUniqueAsyncExpiration_new,
|
||||
flushPassiveEffects as flushPassiveEffects_new,
|
||||
warnIfNotCurrentlyBatchingInDev as warnIfNotCurrentlyBatchingInDev_new,
|
||||
warnIfNotCurrentlyActingUpdatesInDev as warnIfNotCurrentlyActingUpdatesInDev_new,
|
||||
} from './ReactFiberScheduler.new';
|
||||
|
||||
export let requestCurrentTime = requestCurrentTime_old;
|
||||
|
|
@ -89,7 +89,7 @@ export let interactiveUpdates = interactiveUpdates_old;
|
|||
export let flushInteractiveUpdates = flushInteractiveUpdates_old;
|
||||
export let computeUniqueAsyncExpiration = computeUniqueAsyncExpiration_old;
|
||||
export let flushPassiveEffects = flushPassiveEffects_old;
|
||||
export let warnIfNotCurrentlyBatchingInDev = warnIfNotCurrentlyBatchingInDev_old;
|
||||
export let warnIfNotCurrentlyActingUpdatesInDev = warnIfNotCurrentlyActingUpdatesInDev_old;
|
||||
|
||||
if (enableNewScheduler) {
|
||||
requestCurrentTime = requestCurrentTime_new;
|
||||
|
|
@ -116,9 +116,9 @@ if (enableNewScheduler) {
|
|||
flushInteractiveUpdates = flushInteractiveUpdates_new;
|
||||
computeUniqueAsyncExpiration = computeUniqueAsyncExpiration_new;
|
||||
flushPassiveEffects = flushPassiveEffects_new;
|
||||
warnIfNotCurrentlyBatchingInDev = warnIfNotCurrentlyBatchingInDev_new;
|
||||
warnIfNotCurrentlyActingUpdatesInDev = warnIfNotCurrentlyActingUpdatesInDev_new;
|
||||
}
|
||||
|
||||
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 computeUniqueAsyncExpiration = notYetImplemented;
|
||||
export const flushPassiveEffects = notYetImplemented;
|
||||
export const warnIfNotCurrentlyBatchingInDev = notYetImplemented;
|
||||
export const warnIfNotCurrentlyActingUpdatesInDev = notYetImplemented;
|
||||
|
|
|
|||
|
|
@ -176,10 +176,14 @@ const {
|
|||
} = Scheduler;
|
||||
|
||||
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 didWarnSetStateChildContext;
|
||||
|
|
@ -610,6 +614,7 @@ function markLegacyErrorBoundaryAsFailed(instance: mixed) {
|
|||
}
|
||||
|
||||
function flushPassiveEffects() {
|
||||
const didFlushEffects = passiveEffectCallback !== null;
|
||||
if (passiveEffectCallbackHandle !== null) {
|
||||
cancelCallback(passiveEffectCallbackHandle);
|
||||
}
|
||||
|
|
@ -618,6 +623,7 @@ function flushPassiveEffects() {
|
|||
// to ensure tracing works correctly.
|
||||
passiveEffectCallback();
|
||||
}
|
||||
return didFlushEffects;
|
||||
}
|
||||
|
||||
function commitRoot(root: FiberRoot, finishedWork: Fiber): void {
|
||||
|
|
@ -1836,9 +1842,20 @@ function scheduleWorkToRoot(fiber: Fiber, expirationTime): FiberRoot | null {
|
|||
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 (isRendering === false && isBatchingUpdates === false) {
|
||||
if (
|
||||
isBatchingUpdates === false &&
|
||||
isRendering === false &&
|
||||
ReactShouldWarnActingUpdates.current === false
|
||||
) {
|
||||
warningWithoutStack(
|
||||
false,
|
||||
'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 {
|
||||
render() {
|
||||
act(() =>
|
||||
_setState(() => {
|
||||
ReactCurrentDispatcher.current.readContext(ThemeContext);
|
||||
}),
|
||||
_setState(() =>
|
||||
ReactCurrentDispatcher.current.readContext(ThemeContext),
|
||||
);
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
|
@ -1062,13 +1061,7 @@ describe('ReactHooks', () => {
|
|||
<Cls />
|
||||
</React.Fragment>,
|
||||
),
|
||||
).toWarnDev(
|
||||
[
|
||||
'Context can only be read while React is rendering',
|
||||
'Render methods should be a pure function of props and state',
|
||||
],
|
||||
{withoutStack: 1},
|
||||
);
|
||||
).toWarnDev(['Context can only be read while React is rendering']);
|
||||
});
|
||||
|
||||
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';
|
||||
import invariant from 'shared/invariant';
|
||||
import ReactVersion from 'shared/ReactVersion';
|
||||
import warningWithoutStack from 'shared/warningWithoutStack';
|
||||
import act from './ReactTestRendererAct';
|
||||
|
||||
import {getPublicInstance} from './ReactTestHostConfig';
|
||||
|
||||
|
|
@ -65,11 +65,6 @@ type FindOptions = $Shape<{
|
|||
|
||||
export type Predicate = (node: ReactTestInstance) => ?boolean;
|
||||
|
||||
// for .act's return value
|
||||
type Thenable = {
|
||||
then(resolve: () => mixed, reject?: () => mixed): mixed,
|
||||
};
|
||||
|
||||
const defaultTestOptions = {
|
||||
createNodeMock: function() {
|
||||
return null;
|
||||
|
|
@ -549,64 +544,12 @@ const ReactTestRendererFiber = {
|
|||
return entry;
|
||||
},
|
||||
|
||||
/* eslint-disable camelcase */
|
||||
/* eslint-disable-next-line camelcase */
|
||||
unstable_batchedUpdates: batchedUpdates,
|
||||
/* eslint-enable camelcase */
|
||||
|
||||
act(callback: () => void): Thenable {
|
||||
// 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.',
|
||||
);
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
act,
|
||||
};
|
||||
|
||||
// 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();
|
||||
function wrapFiber(fiber: Fiber): ReactTestInstance {
|
||||
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 />);
|
||||
});
|
||||
|
||||
describe('act', () => {
|
||||
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);
|
||||
});
|
||||
// we run this test here because we need a dom-less scope
|
||||
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'
|
||||
// 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');
|
||||
}).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},
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
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 = {
|
||||
ReactCurrentDispatcher,
|
||||
ReactCurrentOwner,
|
||||
// used by act()
|
||||
ReactShouldWarnActingUpdates: {current: false},
|
||||
// Used by renderers to avoid bundling object-assign twice in UMD bundles:
|
||||
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