mirror of
https://github.com/hanzoai/react.git
synced 2026-09-24 17:02:27 +00:00
expose TestUtils.act() for batching actions in tests (#14744)
* expose unstable_interact for batching actions in tests
* move to TestUtils
* move it all into testutils
* s/interact/act
* warn when calling hook-like setState outside batching mode
* pass tests
* merge-temp
* move jsdom test to callsite
* mark failing tests
* pass most tests (except one)
* augh IE
* pass fuzz tests
* better warning, expose the right batchedUpdates on TestRenderer for www
* move it into hooks, test for dom
* expose a flag on the host config, move stuff around
* rename, pass flow
* pass flow... again
* tweak .act() type
* enable for all jest environments/renderers; pass (most) tests.
* pass all tests
* expose just the warning from the scheduler
* don't return values
* a bunch of changes.
can't return values from .act
don't try to await .act calls
pass tests
* fixes and nits
* "fire events that udpates state"
* nit
* 🙄
* my bad
* hi andrew
(prettier fix)
This commit is contained in:
+11
-5
@@ -13,12 +13,14 @@
|
||||
let React;
|
||||
let ReactTestRenderer;
|
||||
let ReactDebugTools;
|
||||
let act;
|
||||
|
||||
describe('ReactHooksInspectionIntergration', () => {
|
||||
describe('ReactHooksInspectionIntegration', () => {
|
||||
beforeEach(() => {
|
||||
jest.resetModules();
|
||||
React = require('react');
|
||||
ReactTestRenderer = require('react-test-renderer');
|
||||
act = ReactTestRenderer.act;
|
||||
ReactDebugTools = require('react-debug-tools');
|
||||
});
|
||||
|
||||
@@ -47,7 +49,7 @@ describe('ReactHooksInspectionIntergration', () => {
|
||||
onMouseUp: setStateB,
|
||||
} = renderer.root.findByType('div').props;
|
||||
|
||||
setStateA('Hi');
|
||||
act(() => setStateA('Hi'));
|
||||
|
||||
childFiber = renderer.root.findByType(Foo)._currentFiber();
|
||||
tree = ReactDebugTools.inspectHooksOfFiber(childFiber);
|
||||
@@ -57,7 +59,7 @@ describe('ReactHooksInspectionIntergration', () => {
|
||||
{name: 'State', value: 'world', subHooks: []},
|
||||
]);
|
||||
|
||||
setStateB('world!');
|
||||
act(() => setStateB('world!'));
|
||||
|
||||
childFiber = renderer.root.findByType(Foo)._currentFiber();
|
||||
tree = ReactDebugTools.inspectHooksOfFiber(childFiber);
|
||||
@@ -91,8 +93,12 @@ describe('ReactHooksInspectionIntergration', () => {
|
||||
React.useMemo(() => state1 + state2, [state1]);
|
||||
|
||||
function update() {
|
||||
setState('A');
|
||||
dispatch({value: 'B'});
|
||||
act(() => {
|
||||
setState('A');
|
||||
});
|
||||
act(() => {
|
||||
dispatch({value: 'B'});
|
||||
});
|
||||
ref.current = 'C';
|
||||
}
|
||||
let memoizedUpdate = React.useCallback(update, []);
|
||||
|
||||
@@ -13,7 +13,9 @@ let React;
|
||||
let ReactDOM;
|
||||
let Suspense;
|
||||
let ReactCache;
|
||||
let ReactTestUtils;
|
||||
let TextResource;
|
||||
let act;
|
||||
|
||||
describe('ReactDOMSuspensePlaceholder', () => {
|
||||
let container;
|
||||
@@ -23,6 +25,8 @@ describe('ReactDOMSuspensePlaceholder', () => {
|
||||
React = require('react');
|
||||
ReactDOM = require('react-dom');
|
||||
ReactCache = require('react-cache');
|
||||
ReactTestUtils = require('react-dom/test-utils');
|
||||
act = ReactTestUtils.act;
|
||||
Suspense = React.Suspense;
|
||||
container = document.createElement('div');
|
||||
document.body.appendChild(container);
|
||||
@@ -142,12 +146,14 @@ describe('ReactDOMSuspensePlaceholder', () => {
|
||||
);
|
||||
}
|
||||
|
||||
ReactDOM.render(<App />, container);
|
||||
act(() => {
|
||||
ReactDOM.render(<App />, container);
|
||||
});
|
||||
expect(container.innerHTML).toEqual(
|
||||
'<span style="display: none;">Sibling</span><span style="display: none;"></span>Loading...',
|
||||
);
|
||||
|
||||
setIsVisible(true);
|
||||
act(() => setIsVisible(true));
|
||||
expect(container.innerHTML).toEqual(
|
||||
'<span style="display: none;">Sibling</span><span style="display: none;"></span>Loading...',
|
||||
);
|
||||
|
||||
@@ -14,6 +14,7 @@ let React;
|
||||
let ReactDOM;
|
||||
let ReactDOMServer;
|
||||
let ReactTestUtils;
|
||||
let act;
|
||||
|
||||
function getTestDocument(markup) {
|
||||
const doc = document.implementation.createHTMLDocument('');
|
||||
@@ -33,6 +34,7 @@ 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', () => {
|
||||
@@ -515,4 +517,168 @@ 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(...).'],
|
||||
{withoutStack: 1},
|
||||
);
|
||||
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(() => 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},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -18,9 +18,15 @@ import {
|
||||
import SyntheticEvent from 'events/SyntheticEvent';
|
||||
import invariant from 'shared/invariant';
|
||||
import lowPriorityWarning from 'shared/lowPriorityWarning';
|
||||
import warningWithoutStack from 'shared/warningWithoutStack';
|
||||
import {ELEMENT_NODE} from '../shared/HTMLNodeType';
|
||||
import * as DOMTopLevelEventTypes from '../events/DOMTopLevelEventTypes';
|
||||
|
||||
// for .act's return value
|
||||
type Thenable = {
|
||||
then(resolve: () => mixed, reject?: () => mixed): mixed,
|
||||
};
|
||||
|
||||
const {findDOMNode} = ReactDOM;
|
||||
// Keep in sync with ReactDOMUnstableNativeDependencies.js
|
||||
// and ReactDOM.js:
|
||||
@@ -145,6 +151,9 @@ function validateClassInstance(inst, methodName) {
|
||||
);
|
||||
}
|
||||
|
||||
// stub element used by act() when flushing effects
|
||||
let actContainerElement = document.createElement('div');
|
||||
|
||||
/**
|
||||
* Utilities for making it easy to test React components.
|
||||
*
|
||||
@@ -380,6 +389,43 @@ const ReactTestUtils = {
|
||||
|
||||
Simulate: null,
|
||||
SimulateNative: {},
|
||||
|
||||
act(callback: () => void): Thenable {
|
||||
// note: keep these warning messages in sync with
|
||||
// createReactNoop.js and ReactTestRenderer.js
|
||||
const result = ReactDOM.unstable_batchedUpdates(callback);
|
||||
if (__DEV__) {
|
||||
if (result !== undefined) {
|
||||
let addendum;
|
||||
if (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.',
|
||||
);
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -21,6 +21,12 @@ import type {ReactNodeList} from 'shared/ReactTypes';
|
||||
import {createPortal} from 'shared/ReactPortal';
|
||||
import expect from 'expect';
|
||||
import {REACT_FRAGMENT_TYPE, REACT_ELEMENT_TYPE} from 'shared/ReactSymbols';
|
||||
import warningWithoutStack from 'shared/warningWithoutStack';
|
||||
|
||||
// for .act's return value
|
||||
type Thenable = {
|
||||
then(resolve: () => mixed, reject?: () => mixed): mixed,
|
||||
};
|
||||
|
||||
type Container = {
|
||||
rootID: string,
|
||||
@@ -864,6 +870,43 @@ 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 (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) {
|
||||
yieldedValues = [];
|
||||
NoopRenderer.flushSync(fn);
|
||||
|
||||
@@ -30,6 +30,7 @@ import {
|
||||
} from './ReactHookEffectTags';
|
||||
import {
|
||||
scheduleWork,
|
||||
warnIfNotCurrentlyBatchingInDev,
|
||||
computeExpirationForFiber,
|
||||
flushPassiveEffects,
|
||||
requestCurrentTime,
|
||||
@@ -1003,6 +1004,19 @@ 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 isnt' 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>,
|
||||
@@ -1121,6 +1135,11 @@ function dispatchAction<S, A>(
|
||||
}
|
||||
}
|
||||
}
|
||||
if (__DEV__) {
|
||||
if (shouldWarnForUnbatchedSetState === true) {
|
||||
warnIfNotCurrentlyBatchingInDev(fiber);
|
||||
}
|
||||
}
|
||||
scheduleWork(fiber, expirationTime);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1790,6 +1790,25 @@ function scheduleWorkToRoot(fiber: Fiber, expirationTime): FiberRoot | null {
|
||||
return root;
|
||||
}
|
||||
|
||||
export function warnIfNotCurrentlyBatchingInDev(fiber: Fiber): void {
|
||||
if (__DEV__) {
|
||||
if (isRendering === false && isBatchingUpdates === false) {
|
||||
warningWithoutStack(
|
||||
false,
|
||||
'An update to %s inside a test was not wrapped in act(...).\n\n' +
|
||||
'When testing, code that causes React state updates should be wrapped into act(...):\n\n' +
|
||||
'act(() => {\n' +
|
||||
' /* fire events that update state */\n' +
|
||||
'});\n' +
|
||||
'/* assert on the output */\n\n' +
|
||||
"This ensures that you're testing the behavior the user would see in the browser." +
|
||||
' Learn more at https://fb.me/react-wrap-tests-with-act',
|
||||
getComponentName(fiber.type),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function scheduleWork(fiber: Fiber, expirationTime: ExpirationTime) {
|
||||
const root = scheduleWorkToRoot(fiber, expirationTime);
|
||||
if (root === null) {
|
||||
|
||||
@@ -16,6 +16,7 @@ let React;
|
||||
let ReactFeatureFlags;
|
||||
let ReactTestRenderer;
|
||||
let ReactDOMServer;
|
||||
let act;
|
||||
|
||||
// Additional tests can be found in ReactHooksWithNoopRenderer. Plan is to
|
||||
// gradually migrate those to this file.
|
||||
@@ -28,6 +29,7 @@ describe('ReactHooks', () => {
|
||||
React = require('react');
|
||||
ReactTestRenderer = require('react-test-renderer');
|
||||
ReactDOMServer = require('react-dom/server');
|
||||
act = ReactTestRenderer.act;
|
||||
});
|
||||
|
||||
if (__DEV__) {
|
||||
@@ -81,8 +83,11 @@ describe('ReactHooks', () => {
|
||||
expect(root).toMatchRenderedOutput('0, 0');
|
||||
|
||||
// Normal update
|
||||
setCounter1(1);
|
||||
setCounter2(1);
|
||||
act(() => {
|
||||
setCounter1(1);
|
||||
setCounter2(1);
|
||||
});
|
||||
|
||||
expect(root).toFlushAndYield([
|
||||
'Parent: 1, 1',
|
||||
'Child: 1, 1',
|
||||
@@ -90,13 +95,16 @@ describe('ReactHooks', () => {
|
||||
]);
|
||||
|
||||
// Update that bails out.
|
||||
setCounter1(1);
|
||||
act(() => setCounter1(1));
|
||||
expect(root).toFlushAndYield(['Parent: 1, 1']);
|
||||
|
||||
// This time, one of the state updates but the other one doesn't. So we
|
||||
// can't bail out.
|
||||
setCounter1(1);
|
||||
setCounter2(2);
|
||||
act(() => {
|
||||
setCounter1(1);
|
||||
setCounter2(2);
|
||||
});
|
||||
|
||||
expect(root).toFlushAndYield([
|
||||
'Parent: 1, 2',
|
||||
'Child: 1, 2',
|
||||
@@ -104,20 +112,24 @@ describe('ReactHooks', () => {
|
||||
]);
|
||||
|
||||
// Lots of updates that eventually resolve to the current values.
|
||||
setCounter1(9);
|
||||
setCounter2(3);
|
||||
setCounter1(4);
|
||||
setCounter2(7);
|
||||
setCounter1(1);
|
||||
setCounter2(2);
|
||||
act(() => {
|
||||
setCounter1(9);
|
||||
setCounter2(3);
|
||||
setCounter1(4);
|
||||
setCounter2(7);
|
||||
setCounter1(1);
|
||||
setCounter2(2);
|
||||
});
|
||||
|
||||
// Because the final values are the same as the current values, the
|
||||
// component bails out.
|
||||
expect(root).toFlushAndYield(['Parent: 1, 2']);
|
||||
|
||||
// prepare to check SameValue
|
||||
setCounter1(0 / -1);
|
||||
setCounter2(NaN);
|
||||
act(() => {
|
||||
setCounter1(0 / -1);
|
||||
setCounter2(NaN);
|
||||
});
|
||||
expect(root).toFlushAndYield([
|
||||
'Parent: 0, NaN',
|
||||
'Child: 0, NaN',
|
||||
@@ -125,14 +137,19 @@ describe('ReactHooks', () => {
|
||||
]);
|
||||
|
||||
// check if re-setting to negative 0 / NaN still bails out
|
||||
setCounter1(0 / -1);
|
||||
setCounter2(NaN);
|
||||
setCounter2(Infinity);
|
||||
setCounter2(NaN);
|
||||
act(() => {
|
||||
setCounter1(0 / -1);
|
||||
setCounter2(NaN);
|
||||
setCounter2(Infinity);
|
||||
setCounter2(NaN);
|
||||
});
|
||||
|
||||
expect(root).toFlushAndYield(['Parent: 0, NaN']);
|
||||
|
||||
// check if changing negative 0 to positive 0 does not bail out
|
||||
setCounter1(0);
|
||||
act(() => {
|
||||
setCounter1(0);
|
||||
});
|
||||
expect(root).toFlushAndYield([
|
||||
'Parent: 0, NaN',
|
||||
'Child: 0, NaN',
|
||||
@@ -172,21 +189,27 @@ describe('ReactHooks', () => {
|
||||
expect(root).toMatchRenderedOutput('0, 0 (light)');
|
||||
|
||||
// Normal update
|
||||
setCounter1(1);
|
||||
setCounter2(1);
|
||||
act(() => {
|
||||
setCounter1(1);
|
||||
setCounter2(1);
|
||||
});
|
||||
|
||||
expect(root).toFlushAndYield([
|
||||
'Parent: 1, 1 (light)',
|
||||
'Child: 1, 1 (light)',
|
||||
]);
|
||||
|
||||
// Update that bails out.
|
||||
setCounter1(1);
|
||||
act(() => setCounter1(1));
|
||||
expect(root).toFlushAndYield(['Parent: 1, 1 (light)']);
|
||||
|
||||
// This time, one of the state updates but the other one doesn't. So we
|
||||
// can't bail out.
|
||||
setCounter1(1);
|
||||
setCounter2(2);
|
||||
act(() => {
|
||||
setCounter1(1);
|
||||
setCounter2(2);
|
||||
});
|
||||
|
||||
expect(root).toFlushAndYield([
|
||||
'Parent: 1, 2 (light)',
|
||||
'Child: 1, 2 (light)',
|
||||
@@ -194,14 +217,20 @@ describe('ReactHooks', () => {
|
||||
|
||||
// Updates bail out, but component still renders because props
|
||||
// have changed
|
||||
setCounter1(1);
|
||||
setCounter2(2);
|
||||
act(() => {
|
||||
setCounter1(1);
|
||||
setCounter2(2);
|
||||
});
|
||||
|
||||
root.update(<Parent theme="dark" />);
|
||||
expect(root).toFlushAndYield(['Parent: 1, 2 (dark)', 'Child: 1, 2 (dark)']);
|
||||
|
||||
// Both props and state bail out
|
||||
setCounter1(1);
|
||||
setCounter2(2);
|
||||
act(() => {
|
||||
setCounter1(1);
|
||||
setCounter2(2);
|
||||
});
|
||||
|
||||
root.update(<Parent theme="dark" />);
|
||||
expect(root).toFlushAndYield(['Parent: 1, 2 (dark)']);
|
||||
});
|
||||
@@ -224,9 +253,11 @@ describe('ReactHooks', () => {
|
||||
expect(root).toMatchRenderedOutput('0');
|
||||
|
||||
expect(() => {
|
||||
setCounter(1, () => {
|
||||
throw new Error('Expected to ignore the callback.');
|
||||
});
|
||||
act(() =>
|
||||
setCounter(1, () => {
|
||||
throw new Error('Expected to ignore the callback.');
|
||||
}),
|
||||
);
|
||||
}).toWarnDev(
|
||||
'State updates from the useState() and useReducer() Hooks ' +
|
||||
"don't support the second callback argument. " +
|
||||
@@ -256,9 +287,11 @@ describe('ReactHooks', () => {
|
||||
expect(root).toMatchRenderedOutput('0');
|
||||
|
||||
expect(() => {
|
||||
dispatch(1, () => {
|
||||
throw new Error('Expected to ignore the callback.');
|
||||
});
|
||||
act(() =>
|
||||
dispatch(1, () => {
|
||||
throw new Error('Expected to ignore the callback.');
|
||||
}),
|
||||
);
|
||||
}).toWarnDev(
|
||||
'State updates from the useState() and useReducer() Hooks ' +
|
||||
"don't support the second callback argument. " +
|
||||
@@ -326,7 +359,7 @@ describe('ReactHooks', () => {
|
||||
expect(root).toMatchRenderedOutput('0 (light)');
|
||||
|
||||
// Normal update
|
||||
setCounter(1);
|
||||
act(() => setCounter(1));
|
||||
expect(root).toFlushAndYield([
|
||||
'Parent: 1 (light)',
|
||||
'Child: 1 (light)',
|
||||
@@ -335,14 +368,17 @@ describe('ReactHooks', () => {
|
||||
expect(root).toMatchRenderedOutput('1 (light)');
|
||||
|
||||
// Update that doesn't change state, so it bails out
|
||||
setCounter(1);
|
||||
act(() => setCounter(1));
|
||||
expect(root).toFlushAndYield(['Parent: 1 (light)']);
|
||||
expect(root).toMatchRenderedOutput('1 (light)');
|
||||
|
||||
// Update that doesn't change state, but the context changes, too, so it
|
||||
// can't bail out
|
||||
setCounter(1);
|
||||
setTheme('dark');
|
||||
act(() => {
|
||||
setCounter(1);
|
||||
setTheme('dark');
|
||||
});
|
||||
|
||||
expect(root).toFlushAndYield([
|
||||
'Theme: dark',
|
||||
'Parent: 1 (dark)',
|
||||
@@ -377,7 +413,7 @@ describe('ReactHooks', () => {
|
||||
expect(root).toMatchRenderedOutput('0');
|
||||
|
||||
// Normal update
|
||||
setCounter(1);
|
||||
act(() => setCounter(1));
|
||||
expect(root).toFlushAndYield(['Parent: 1', 'Child: 1', 'Effect: 1']);
|
||||
expect(root).toMatchRenderedOutput('1');
|
||||
|
||||
@@ -385,38 +421,47 @@ describe('ReactHooks', () => {
|
||||
// because the alterate fiber has pending update priority, so we have to
|
||||
// enter the render phase before we can bail out. But we bail out before
|
||||
// rendering the child, and we don't fire any effects.
|
||||
setCounter(1);
|
||||
act(() => setCounter(1));
|
||||
expect(root).toFlushAndYield(['Parent: 1']);
|
||||
expect(root).toMatchRenderedOutput('1');
|
||||
|
||||
// Update to the same state again. This times, neither fiber has pending
|
||||
// update priority, so we can bail out before even entering the render phase.
|
||||
setCounter(1);
|
||||
act(() => setCounter(1));
|
||||
expect(root).toFlushAndYield([]);
|
||||
expect(root).toMatchRenderedOutput('1');
|
||||
|
||||
// This changes the state to something different so it renders normally.
|
||||
setCounter(2);
|
||||
act(() => setCounter(2));
|
||||
expect(root).toFlushAndYield(['Parent: 2', 'Child: 2', 'Effect: 2']);
|
||||
expect(root).toMatchRenderedOutput('2');
|
||||
|
||||
// prepare to check SameValue
|
||||
setCounter(0);
|
||||
act(() => {
|
||||
setCounter(0);
|
||||
});
|
||||
expect(root).toFlushAndYield(['Parent: 0', 'Child: 0', 'Effect: 0']);
|
||||
expect(root).toMatchRenderedOutput('0');
|
||||
|
||||
// Update to the same state for the first time to flush the queue
|
||||
setCounter(0);
|
||||
act(() => {
|
||||
setCounter(0);
|
||||
});
|
||||
|
||||
expect(root).toFlushAndYield(['Parent: 0']);
|
||||
expect(root).toMatchRenderedOutput('0');
|
||||
|
||||
// Update again to the same state. Should bail out.
|
||||
setCounter(0);
|
||||
act(() => {
|
||||
setCounter(0);
|
||||
});
|
||||
expect(root).toFlushAndYield([]);
|
||||
expect(root).toMatchRenderedOutput('0');
|
||||
|
||||
// Update to a different state (positive 0 to negative 0)
|
||||
setCounter(0 / -1);
|
||||
act(() => {
|
||||
setCounter(0 / -1);
|
||||
});
|
||||
expect(root).toFlushAndYield(['Parent: 0', 'Child: 0', 'Effect: 0']);
|
||||
expect(root).toMatchRenderedOutput('0');
|
||||
});
|
||||
@@ -450,12 +495,14 @@ describe('ReactHooks', () => {
|
||||
return value;
|
||||
});
|
||||
};
|
||||
update(0);
|
||||
update(0);
|
||||
update(0);
|
||||
update(1);
|
||||
update(2);
|
||||
update(3);
|
||||
act(() => {
|
||||
update(0);
|
||||
update(0);
|
||||
update(0);
|
||||
update(1);
|
||||
update(2);
|
||||
update(3);
|
||||
});
|
||||
|
||||
expect(ReactTestRenderer).toHaveYielded([
|
||||
// The first four updates were eagerly computed, because the queue is
|
||||
@@ -511,7 +558,7 @@ describe('ReactHooks', () => {
|
||||
};
|
||||
|
||||
// Update at normal priority
|
||||
update(n => n * 100);
|
||||
act(() => update(n => n * 100));
|
||||
|
||||
// The new state is eagerly computed.
|
||||
expect(ReactTestRenderer).toHaveYielded(['Compute state (1 -> 100)']);
|
||||
@@ -839,9 +886,11 @@ describe('ReactHooks', () => {
|
||||
|
||||
class Cls extends React.Component {
|
||||
render() {
|
||||
_setState(() => {
|
||||
ReactCurrentDispatcher.current.readContext(ThemeContext);
|
||||
});
|
||||
act(() =>
|
||||
_setState(() => {
|
||||
ReactCurrentDispatcher.current.readContext(ThemeContext);
|
||||
}),
|
||||
);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -853,7 +902,13 @@ describe('ReactHooks', () => {
|
||||
<Cls />
|
||||
</React.Fragment>,
|
||||
),
|
||||
).toWarnDev('Context can only be read while React is rendering');
|
||||
).toWarnDev(
|
||||
[
|
||||
'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', () => {
|
||||
@@ -1294,9 +1349,11 @@ describe('ReactHooks', () => {
|
||||
}
|
||||
|
||||
function B() {
|
||||
_setState(() => {
|
||||
throw new Error('Hello');
|
||||
});
|
||||
act(() =>
|
||||
_setState(() => {
|
||||
throw new Error('Hello');
|
||||
}),
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
+66
-41
@@ -26,6 +26,7 @@ let useRef;
|
||||
let useImperativeHandle;
|
||||
let forwardRef;
|
||||
let memo;
|
||||
let act;
|
||||
|
||||
// These tests use React Noop Renderer. All new tests should use React Test
|
||||
// Renderer and go in ReactHooks-test; plan is gradually migrate the noop tests
|
||||
@@ -50,6 +51,7 @@ describe('ReactHooksWithNoopRenderer', () => {
|
||||
useImperativeHandle = React.useImperativeHandle;
|
||||
forwardRef = React.forwardRef;
|
||||
memo = React.memo;
|
||||
act = ReactNoop.act;
|
||||
});
|
||||
|
||||
function span(prop) {
|
||||
@@ -76,8 +78,11 @@ describe('ReactHooksWithNoopRenderer', () => {
|
||||
expect(ReactNoop.getChildren()).toEqual([span('Count: 0')]);
|
||||
|
||||
// Schedule some updates
|
||||
counter.current.updateCount(1);
|
||||
counter.current.updateCount(count => count + 10);
|
||||
act(() => {
|
||||
counter.current.updateCount(1);
|
||||
counter.current.updateCount(count => count + 10);
|
||||
});
|
||||
|
||||
// Partially flush without committing
|
||||
ReactNoop.flushThrough(['Count: 11']);
|
||||
expect(ReactNoop.getChildren()).toEqual([span('Count: 0')]);
|
||||
@@ -157,11 +162,11 @@ describe('ReactHooksWithNoopRenderer', () => {
|
||||
ReactNoop.flush();
|
||||
expect(ReactNoop.getChildren()).toEqual([span('Count: 0')]);
|
||||
|
||||
counter.current.updateCount(1);
|
||||
act(() => counter.current.updateCount(1));
|
||||
ReactNoop.flush();
|
||||
expect(ReactNoop.getChildren()).toEqual([span('Count: 1')]);
|
||||
|
||||
counter.current.updateCount(count => count + 10);
|
||||
act(() => counter.current.updateCount(count => count + 10));
|
||||
ReactNoop.flush();
|
||||
expect(ReactNoop.getChildren()).toEqual([span('Count: 11')]);
|
||||
});
|
||||
@@ -181,7 +186,7 @@ describe('ReactHooksWithNoopRenderer', () => {
|
||||
expect(ReactNoop.flush()).toEqual(['getInitialState', 'Count: 42']);
|
||||
expect(ReactNoop.getChildren()).toEqual([span('Count: 42')]);
|
||||
|
||||
counter.current.updateCount(7);
|
||||
act(() => counter.current.updateCount(7));
|
||||
expect(ReactNoop.flush()).toEqual(['Count: 7']);
|
||||
expect(ReactNoop.getChildren()).toEqual([span('Count: 7')]);
|
||||
});
|
||||
@@ -199,10 +204,10 @@ describe('ReactHooksWithNoopRenderer', () => {
|
||||
ReactNoop.flush();
|
||||
expect(ReactNoop.getChildren()).toEqual([span('Count: 0')]);
|
||||
|
||||
counter.current.updateCount(7);
|
||||
act(() => counter.current.updateCount(7));
|
||||
expect(ReactNoop.flush()).toEqual(['Count: 7']);
|
||||
|
||||
counter.current.updateLabel('Total');
|
||||
act(() => counter.current.updateLabel('Total'));
|
||||
expect(ReactNoop.flush()).toEqual(['Total: 7']);
|
||||
});
|
||||
|
||||
@@ -217,11 +222,11 @@ describe('ReactHooksWithNoopRenderer', () => {
|
||||
ReactNoop.flush();
|
||||
expect(ReactNoop.getChildren()).toEqual([span('Count: 0')]);
|
||||
|
||||
updaters[0](1);
|
||||
act(() => updaters[0](1));
|
||||
ReactNoop.flush();
|
||||
expect(ReactNoop.getChildren()).toEqual([span('Count: 1')]);
|
||||
|
||||
updaters[0](count => count + 10);
|
||||
act(() => updaters[0](count => count + 10));
|
||||
ReactNoop.flush();
|
||||
expect(ReactNoop.getChildren()).toEqual([span('Count: 11')]);
|
||||
|
||||
@@ -240,7 +245,7 @@ describe('ReactHooksWithNoopRenderer', () => {
|
||||
ReactNoop.flush();
|
||||
ReactNoop.render(null);
|
||||
ReactNoop.flush();
|
||||
expect(() => _updateCount(1)).toWarnDev(
|
||||
expect(() => act(() => _updateCount(1))).toWarnDev(
|
||||
"Warning: Can't perform a React state update on an unmounted " +
|
||||
'component. This is a no-op, but it indicates a memory leak in your ' +
|
||||
'application. To fix, cancel all subscriptions and asynchronous ' +
|
||||
@@ -266,7 +271,7 @@ describe('ReactHooksWithNoopRenderer', () => {
|
||||
expect(ReactNoop.flush()).toEqual([]);
|
||||
expect(ReactNoop.getChildren()).toEqual([span('Count: 0')]);
|
||||
|
||||
_updateCount(1);
|
||||
act(() => _updateCount(1));
|
||||
expect(ReactNoop.flush()).toEqual(['Count: 1']);
|
||||
expect(ReactNoop.getChildren()).toEqual([span('Count: 1')]);
|
||||
});
|
||||
@@ -490,13 +495,15 @@ describe('ReactHooksWithNoopRenderer', () => {
|
||||
ReactNoop.flush();
|
||||
expect(ReactNoop.getChildren()).toEqual([span('Count: 0')]);
|
||||
|
||||
counter.current.dispatch(INCREMENT);
|
||||
act(() => counter.current.dispatch(INCREMENT));
|
||||
ReactNoop.flush();
|
||||
expect(ReactNoop.getChildren()).toEqual([span('Count: 1')]);
|
||||
act(() => {
|
||||
counter.current.dispatch(DECREMENT);
|
||||
counter.current.dispatch(DECREMENT);
|
||||
counter.current.dispatch(DECREMENT);
|
||||
});
|
||||
|
||||
counter.current.dispatch(DECREMENT);
|
||||
counter.current.dispatch(DECREMENT);
|
||||
counter.current.dispatch(DECREMENT);
|
||||
ReactNoop.flush();
|
||||
expect(ReactNoop.getChildren()).toEqual([span('Count: -2')]);
|
||||
});
|
||||
@@ -530,13 +537,16 @@ describe('ReactHooksWithNoopRenderer', () => {
|
||||
expect(ReactNoop.flush()).toEqual(['Init', 'Count: 10']);
|
||||
expect(ReactNoop.getChildren()).toEqual([span('Count: 10')]);
|
||||
|
||||
counter.current.dispatch(INCREMENT);
|
||||
act(() => counter.current.dispatch(INCREMENT));
|
||||
expect(ReactNoop.flush()).toEqual(['Count: 11']);
|
||||
expect(ReactNoop.getChildren()).toEqual([span('Count: 11')]);
|
||||
|
||||
counter.current.dispatch(DECREMENT);
|
||||
counter.current.dispatch(DECREMENT);
|
||||
counter.current.dispatch(DECREMENT);
|
||||
act(() => {
|
||||
counter.current.dispatch(DECREMENT);
|
||||
counter.current.dispatch(DECREMENT);
|
||||
counter.current.dispatch(DECREMENT);
|
||||
});
|
||||
|
||||
expect(ReactNoop.flush()).toEqual(['Count: 8']);
|
||||
expect(ReactNoop.getChildren()).toEqual([span('Count: 8')]);
|
||||
});
|
||||
@@ -562,9 +572,12 @@ describe('ReactHooksWithNoopRenderer', () => {
|
||||
ReactNoop.flush();
|
||||
expect(ReactNoop.getChildren()).toEqual([span('Count: 0')]);
|
||||
|
||||
counter.current.dispatch(INCREMENT);
|
||||
counter.current.dispatch(INCREMENT);
|
||||
counter.current.dispatch(INCREMENT);
|
||||
act(() => {
|
||||
counter.current.dispatch(INCREMENT);
|
||||
counter.current.dispatch(INCREMENT);
|
||||
counter.current.dispatch(INCREMENT);
|
||||
});
|
||||
|
||||
ReactNoop.flushSync(() => {
|
||||
counter.current.dispatch(INCREMENT);
|
||||
});
|
||||
@@ -647,15 +660,20 @@ describe('ReactHooksWithNoopRenderer', () => {
|
||||
});
|
||||
return <Text text="Layout" />;
|
||||
}
|
||||
|
||||
ReactNoop.render([<PassiveEffect key="p" />, <LayoutEffect key="l" />]);
|
||||
expect(ReactNoop.flush()).toEqual([
|
||||
'Passive',
|
||||
'Layout',
|
||||
'Layout effect 0',
|
||||
'Passive effect',
|
||||
'Layout',
|
||||
'Layout effect 1',
|
||||
]);
|
||||
|
||||
act(() => {
|
||||
expect(ReactNoop.flush()).toEqual([
|
||||
'Passive',
|
||||
'Layout',
|
||||
'Layout effect 0',
|
||||
'Passive effect',
|
||||
'Layout',
|
||||
'Layout effect 1',
|
||||
]);
|
||||
});
|
||||
|
||||
expect(ReactNoop.getChildren()).toEqual([
|
||||
span('Passive'),
|
||||
span('Layout'),
|
||||
@@ -779,7 +797,10 @@ describe('ReactHooksWithNoopRenderer', () => {
|
||||
ReactNoop.flushThrough(['Schedule update [0]', 'Count: 0']);
|
||||
expect(ReactNoop.getChildren()).toEqual([span('Count: (empty)')]);
|
||||
|
||||
expect(ReactNoop.flush()).toEqual([]);
|
||||
ReactNoop.batchedUpdates(() => {
|
||||
expect(ReactNoop.flush()).toEqual([]);
|
||||
});
|
||||
|
||||
expect(ReactNoop.getChildren()).toEqual([span('Count: 0')]);
|
||||
|
||||
ReactNoop.flushPassiveEffects();
|
||||
@@ -805,7 +826,7 @@ describe('ReactHooksWithNoopRenderer', () => {
|
||||
|
||||
// Enqueuing this update forces the passive effect to be flushed --
|
||||
// updateCount(1) happens first, so 2 wins.
|
||||
_updateCount(2);
|
||||
act(() => _updateCount(2));
|
||||
expect(ReactNoop.flush()).toEqual(['Will set count to 1', 'Count: 2']);
|
||||
expect(ReactNoop.getChildren()).toEqual([span('Count: 2')]);
|
||||
});
|
||||
@@ -851,7 +872,7 @@ describe('ReactHooksWithNoopRenderer', () => {
|
||||
|
||||
// Enqueuing this update forces the passive effect to be flushed --
|
||||
// updateCount(1) happens first, so 2 wins.
|
||||
_updateCount(2);
|
||||
act(() => _updateCount(2));
|
||||
expect(ReactNoop.flush()).toEqual(['Will set count to 1', 'Count: 2']);
|
||||
expect(ReactNoop.getChildren()).toEqual([span('Count: 2')]);
|
||||
|
||||
@@ -1368,7 +1389,7 @@ describe('ReactHooksWithNoopRenderer', () => {
|
||||
span('Count: 0'),
|
||||
]);
|
||||
|
||||
button.current.increment();
|
||||
act(button.current.increment);
|
||||
expect(ReactNoop.flush()).toEqual([
|
||||
// Button should not re-render, because its props haven't changed
|
||||
// 'Increment',
|
||||
@@ -1392,7 +1413,7 @@ describe('ReactHooksWithNoopRenderer', () => {
|
||||
]);
|
||||
|
||||
// Callback should have updated
|
||||
button.current.increment();
|
||||
act(button.current.increment);
|
||||
expect(ReactNoop.flush()).toEqual(['Count: 11']);
|
||||
expect(ReactNoop.getChildren()).toEqual([
|
||||
span('Increment'),
|
||||
@@ -1600,8 +1621,11 @@ describe('ReactHooksWithNoopRenderer', () => {
|
||||
span('A: 0, B: 0, C: [not loaded]'),
|
||||
]);
|
||||
|
||||
updateA(2);
|
||||
updateB(3);
|
||||
act(() => {
|
||||
updateA(2);
|
||||
updateB(3);
|
||||
});
|
||||
|
||||
expect(ReactNoop.flush()).toEqual(['A: 2, B: 3, C: [not loaded]']);
|
||||
expect(ReactNoop.getChildren()).toEqual([
|
||||
span('A: 2, B: 3, C: [not loaded]'),
|
||||
@@ -1646,10 +1670,11 @@ describe('ReactHooksWithNoopRenderer', () => {
|
||||
ReactNoop.render(<App loadC={true} />);
|
||||
expect(ReactNoop.flush()).toEqual(['A: 0, B: 0, C: 0']);
|
||||
expect(ReactNoop.getChildren()).toEqual([span('A: 0, B: 0, C: 0')]);
|
||||
|
||||
updateA(2);
|
||||
updateB(3);
|
||||
updateC(4);
|
||||
act(() => {
|
||||
updateA(2);
|
||||
updateB(3);
|
||||
updateC(4);
|
||||
});
|
||||
expect(ReactNoop.flush()).toEqual(['A: 2, B: 3, C: 4']);
|
||||
expect(ReactNoop.getChildren()).toEqual([span('A: 2, B: 3, C: 4')]);
|
||||
ReactNoop.render(<App loadC={false} />);
|
||||
|
||||
@@ -3,6 +3,7 @@ let ReactTestRenderer;
|
||||
let ReactFeatureFlags;
|
||||
let ReactCache;
|
||||
let Suspense;
|
||||
let act;
|
||||
|
||||
// let JestReact;
|
||||
|
||||
@@ -19,6 +20,7 @@ describe('ReactSuspense', () => {
|
||||
ReactFeatureFlags.replayFailedUnitOfWorkWithInvokeGuardedCallback = false;
|
||||
React = require('react');
|
||||
ReactTestRenderer = require('react-test-renderer');
|
||||
act = ReactTestRenderer.act;
|
||||
// JestReact = require('jest-react');
|
||||
ReactCache = require('react-cache');
|
||||
|
||||
@@ -797,7 +799,7 @@ describe('ReactSuspense', () => {
|
||||
]);
|
||||
expect(root).toMatchRenderedOutput('Tab: 0 + sibling');
|
||||
|
||||
setTab(1);
|
||||
act(() => setTab(1));
|
||||
expect(ReactTestRenderer).toHaveYielded([
|
||||
'Suspend! [Tab: 1]',
|
||||
' + sibling',
|
||||
@@ -811,7 +813,7 @@ describe('ReactSuspense', () => {
|
||||
]);
|
||||
expect(root).toMatchRenderedOutput('Tab: 1 + sibling');
|
||||
|
||||
setTab(2);
|
||||
act(() => setTab(2));
|
||||
expect(ReactTestRenderer).toHaveYielded([
|
||||
'Suspend! [Tab: 2]',
|
||||
' + sibling',
|
||||
@@ -864,7 +866,7 @@ describe('ReactSuspense', () => {
|
||||
]);
|
||||
expect(root).toMatchRenderedOutput('A:0');
|
||||
|
||||
setStep(1);
|
||||
act(() => setStep(1));
|
||||
expect(ReactTestRenderer).toHaveYielded(['Suspend! [A:1]', 'Loading...']);
|
||||
expect(root).toMatchRenderedOutput('Loading...');
|
||||
|
||||
|
||||
@@ -158,7 +158,7 @@ describe('ReactSuspenseFuzz', () => {
|
||||
if ((elapsedTime += 1000) > 1000000) {
|
||||
throw new Error('Something did not resolve properly.');
|
||||
}
|
||||
jest.advanceTimersByTime(1000);
|
||||
ReactTestRenderer.act(() => jest.advanceTimersByTime(1000));
|
||||
root.unstable_flushAll();
|
||||
}
|
||||
|
||||
|
||||
+60
-1
@@ -17,8 +17,8 @@ import {
|
||||
updateContainer,
|
||||
flushSync,
|
||||
injectIntoDevTools,
|
||||
batchedUpdates,
|
||||
} from 'react-reconciler/inline.test';
|
||||
import {batchedUpdates} from 'events/ReactGenericBatching';
|
||||
import {findCurrentFiberUsingSlowPath} from 'react-reconciler/reflection';
|
||||
import {
|
||||
Fragment,
|
||||
@@ -39,6 +39,7 @@ import {
|
||||
} from 'shared/ReactWorkTags';
|
||||
import invariant from 'shared/invariant';
|
||||
import ReactVersion from 'shared/ReactVersion';
|
||||
import warningWithoutStack from 'shared/warningWithoutStack';
|
||||
|
||||
import {getPublicInstance} from './ReactTestHostConfig';
|
||||
import {
|
||||
@@ -70,6 +71,11 @@ 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;
|
||||
@@ -557,8 +563,61 @@ const ReactTestRendererFiber = {
|
||||
/* eslint-enable camelcase */
|
||||
|
||||
unstable_setNowImplementation: setNowImplementation,
|
||||
|
||||
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 (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();
|
||||
function wrapFiber(fiber: Fiber): ReactTestInstance {
|
||||
let wrapper = fiberToWrapper.get(fiber);
|
||||
|
||||
@@ -1021,4 +1021,27 @@ describe('ReactTestRenderer', () => {
|
||||
ReactNoop.flush();
|
||||
ReactTestRenderer.create(<App />);
|
||||
});
|
||||
|
||||
describe('act', () => {
|
||||
it('works', () => {
|
||||
function App(props) {
|
||||
React.useEffect(() => {
|
||||
props.callback();
|
||||
});
|
||||
return null;
|
||||
}
|
||||
let called = false;
|
||||
ReactTestRenderer.act(() => {
|
||||
ReactTestRenderer.create(
|
||||
<App
|
||||
callback={() => {
|
||||
called = true;
|
||||
}}
|
||||
/>,
|
||||
);
|
||||
});
|
||||
|
||||
expect(called).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user