Skip to content

Commit

Permalink
expose TestUtils.act() for batching actions in tests (#14744)
Browse files Browse the repository at this point in the history
* 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)
  • Loading branch information
Sunil Pai committed Feb 5, 2019
1 parent 76dd9f3 commit b35ac30
Show file tree
Hide file tree
Showing 2 changed files with 83 additions and 1 deletion.
61 changes: 60 additions & 1 deletion src/ReactTestRenderer.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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 {
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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);
Expand Down
23 changes: 23 additions & 0 deletions src/__tests__/ReactTestRenderer-test.internal.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
});
});

0 comments on commit b35ac30

Please sign in to comment.