Skip to content

Commit 11489b0

Browse files
Esemesekthymikee
authored andcommitted
feat: wrap render with act() for better React Hooks support (callstack#122)
1 parent 747e864 commit 11489b0

File tree

11 files changed

+144
-35
lines changed

11 files changed

+144
-35
lines changed

docs/API.md

+4
Original file line numberDiff line numberDiff line change
@@ -379,3 +379,7 @@ const { queryAllByText } = render(<Forms />);
379379
const submitButtons = queryAllByText('submit');
380380
expect(submitButtons).toHaveLength(3); // expect 3 elements
381381
```
382+
383+
## `act`
384+
385+
Useful function to help testing components that use hooks API. By default any `render` and `fireEvent` calls are wrapped by this function, so there is no need to wrap it manually. This method is re-exported from [`react-test-renderer`](https://github.com/facebook/react/blob/master/packages/react-test-renderer/src/ReactTestRenderer.js#L567]).

flow-typed/npm/react-test-renderer_v16.x.x.js

+14-8
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,13 @@ type ReactComponentInstance = React$Component<any>;
99
type ReactTestRendererJSON = {
1010
type: string,
1111
props: { [propName: string]: any },
12-
children: null | ReactTestRendererJSON[]
12+
children: null | ReactTestRendererJSON[],
1313
};
1414

1515
type ReactTestRendererTree = ReactTestRendererJSON & {
16-
nodeType: "component" | "host",
16+
nodeType: 'component' | 'host',
1717
instance: ?ReactComponentInstance,
18-
rendered: null | ReactTestRendererTree
18+
rendered: null | ReactTestRendererTree,
1919
};
2020

2121
type ReactTestInstance = {
@@ -40,30 +40,36 @@ type ReactTestInstance = {
4040
findAllByProps(
4141
props: { [propName: string]: any },
4242
options?: { deep: boolean }
43-
): ReactTestInstance[]
43+
): ReactTestInstance[],
4444
};
4545

4646
type TestRendererOptions = {
47-
createNodeMock(element: React$Element<any>): any
47+
createNodeMock(element: React$Element<any>): any,
48+
};
49+
50+
type Thenable = {
51+
then(resolve: () => mixed, reject?: () => mixed): mixed,
4852
};
4953

50-
declare module "react-test-renderer" {
54+
declare module 'react-test-renderer' {
5155
declare export type ReactTestRenderer = {
5256
toJSON(): null | ReactTestRendererJSON,
5357
toTree(): null | ReactTestRendererTree,
5458
unmount(nextElement?: React$Element<any>): void,
5559
update(nextElement: React$Element<any>): void,
5660
getInstance(): ?ReactComponentInstance,
57-
root: ReactTestInstance
61+
root: ReactTestInstance,
5862
};
5963

6064
declare function create(
6165
nextElement: React$Element<any>,
6266
options?: TestRendererOptions
6367
): ReactTestRenderer;
68+
69+
declare function act(callback: () => void): Thenable;
6470
}
6571

66-
declare module "react-test-renderer/shallow" {
72+
declare module 'react-test-renderer/shallow' {
6773
declare export default class ShallowRenderer {
6874
static createRenderer(): ShallowRenderer;
6975
getMountedInstance(): ReactTestInstance;

package.json

+6-3
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,9 @@
2424
"flow-copy-source": "^2.0.2",
2525
"jest": "^24.1.0",
2626
"metro-react-native-babel-preset": "^0.49.1",
27-
"react": "16.6.3",
27+
"react": "^16.8.3",
2828
"react-native": "^0.58.3",
29-
"react-test-renderer": "16.6.3",
29+
"react-test-renderer": "^16.8.3",
3030
"release-it": "^10.0.0",
3131
"strip-ansi": "^5.0.0",
3232
"typescript": "^3.1.1"
@@ -50,7 +50,10 @@
5050
},
5151
"jest": {
5252
"preset": "react-native",
53-
"moduleFileExtensions": ["js", "json"]
53+
"moduleFileExtensions": [
54+
"js",
55+
"json"
56+
]
5457
},
5558
"greenkeeper": {
5659
"ignore": [

src/__tests__/act.test.js

+50
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
// @flow
2+
import React from 'react';
3+
import { Text } from 'react-native';
4+
import ReactTestRenderer from 'react-test-renderer';
5+
import act from '../act';
6+
import render from '../render';
7+
import fireEvent from '../fireEvent';
8+
9+
const UseEffect = ({ callback }: { callback: Function }) => {
10+
React.useEffect(callback);
11+
return null;
12+
};
13+
14+
const Counter = () => {
15+
const [count, setCount] = React.useState(0);
16+
17+
return (
18+
<Text testID="counter" onPress={() => setCount(count + 1)}>
19+
{count}
20+
</Text>
21+
);
22+
};
23+
24+
test('render should trigger useEffect', () => {
25+
const effectCallback = jest.fn();
26+
render(<UseEffect callback={effectCallback} />);
27+
28+
expect(effectCallback).toHaveBeenCalledTimes(1);
29+
});
30+
31+
test('fireEvent should trigger useState', () => {
32+
const { getByTestId } = render(<Counter />);
33+
const counter = getByTestId('counter');
34+
35+
expect(counter.props.children).toEqual(0);
36+
fireEvent.press(counter);
37+
expect(counter.props.children).toEqual(1);
38+
});
39+
40+
test('should act even if there is no act in react-test-renderer', () => {
41+
// $FlowFixMe
42+
ReactTestRenderer.act = undefined;
43+
const callback = jest.fn();
44+
45+
act(() => {
46+
callback();
47+
});
48+
49+
expect(callback).toHaveBeenCalled();
50+
});

src/act.js

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
// @flow
2+
import { act } from 'react-test-renderer';
3+
4+
const actMock = (callback: () => void) => {
5+
callback();
6+
};
7+
8+
export default act || actMock;

src/fireEvent.js

+9-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
// @flow
2+
import act from './act';
23
import { ErrorWithStack } from './helpers/errors';
34

45
const findEventHandler = (element: ReactTestInstance, eventName: string) => {
@@ -23,10 +24,16 @@ const invokeEvent = (
2324
element: ReactTestInstance,
2425
eventName: string,
2526
data?: *
26-
) => {
27+
): any => {
2728
const handler = findEventHandler(element, eventName);
2829

29-
return handler(data);
30+
let returnValue;
31+
32+
act(() => {
33+
returnValue = handler(data);
34+
});
35+
36+
return returnValue;
3037
};
3138

3239
const toEventHandlerName = (eventName: string) =>

src/index.js

+2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
// @flow
2+
import act from './act';
23
import render from './render';
34
import shallow from './shallow';
45
import flushMicrotasksQueue from './flushMicrotasksQueue';
@@ -12,3 +13,4 @@ export { flushMicrotasksQueue };
1213
export { debug };
1314
export { fireEvent };
1415
export { waitForElement };
16+
export { act };

src/render.js

+22-3
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,26 @@
11
// @flow
22
import * as React from 'react';
3-
import TestRenderer from 'react-test-renderer'; // eslint-disable-line import/no-extraneous-dependencies
3+
import TestRenderer, { type ReactTestRenderer } from 'react-test-renderer'; // eslint-disable-line import/no-extraneous-dependencies
4+
import act from './act';
45
import { getByAPI } from './helpers/getByAPI';
56
import { queryByAPI } from './helpers/queryByAPI';
67
import debugShallow from './helpers/debugShallow';
78
import debugDeep from './helpers/debugDeep';
89

10+
type Options = {
11+
createNodeMock: (element: React.Element<any>) => any,
12+
};
13+
914
/**
1015
* Renders test component deeply using react-test-renderer and exposes helpers
1116
* to assert on the output.
1217
*/
1318
export default function render(
1419
component: React.Element<any>,
15-
options?: { createNodeMock: (element: React.Element<any>) => any }
20+
options?: Options
1621
) {
17-
const renderer = TestRenderer.create(component, options);
22+
const renderer = renderWithAct(component, options);
23+
1824
const instance = renderer.root;
1925

2026
return {
@@ -27,6 +33,19 @@ export default function render(
2733
};
2834
}
2935

36+
function renderWithAct(
37+
component: React.Element<any>,
38+
options?: Options
39+
): ReactTestRenderer {
40+
let renderer: ReactTestRenderer;
41+
42+
act(() => {
43+
renderer = TestRenderer.create(component, options);
44+
});
45+
46+
return ((renderer: any): ReactTestRenderer);
47+
}
48+
3049
function debug(instance: ReactTestInstance, renderer) {
3150
function debugImpl(message?: string) {
3251
return debugDeep(renderer.toJSON(), message);

typings/__tests__/index.test.tsx

+5
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
flushMicrotasksQueue,
88
debug,
99
waitForElement,
10+
act,
1011
} from '../..';
1112

1213
interface HasRequiredProp {
@@ -131,3 +132,7 @@ const waitBy: Promise<ReactTestInstance> = waitForElement<ReactTestInstance>(
131132
const waitByAll: Promise<Array<ReactTestInstance>> = waitForElement<
132133
Array<ReactTestInstance>
133134
>(() => tree.getAllByName('View'), 1000, 50);
135+
136+
act(() => {
137+
render(<TestComponent />);
138+
});

typings/index.d.ts

+5
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,10 @@ export interface QueryByAPI {
3333
) => Array<ReactTestInstance> | [];
3434
}
3535

36+
export interface Thenable {
37+
then: (resolve: () => any, reject?: () => any) => any,
38+
}
39+
3640
export interface RenderOptions {
3741
createNodeMock: (element: React.ReactElement<any>) => any;
3842
}
@@ -86,3 +90,4 @@ export declare const flushMicrotasksQueue: () => Promise<any>;
8690
export declare const debug: DebugAPI;
8791
export declare const fireEvent: FireEventAPI;
8892
export declare const waitForElement: WaitForElementFunction;
93+
export declare const act: (callback: () => void) => Thenable;

yarn.lock

+19-19
Original file line numberDiff line numberDiff line change
@@ -6185,10 +6185,10 @@ react-devtools-core@^3.4.2:
61856185
shell-quote "^1.6.1"
61866186
ws "^3.3.1"
61876187

6188-
react-is@^16.6.3:
6189-
version "16.8.0"
6190-
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.8.0.tgz#518db476214f3fb0716af9f82dfd420225ae970f"
6191-
integrity sha512-LOy+3La39aduxaPfuj+lCXC5RQ8ukjVPAAsFJ3yQ+DIOLf4eR9OMKeWKF0IzjRyE95xMj5QELwiXGgfQsIJguA==
6188+
react-is@^16.8.3:
6189+
version "16.8.3"
6190+
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.8.3.tgz#4ad8b029c2a718fc0cfc746c8d4e1b7221e5387d"
6191+
integrity sha512-Y4rC1ZJmsxxkkPuMLwvKvlL1Zfpbcu+Bf4ZigkHup3v9EfdYhAlWAaVyA19olXq2o2mGn0w+dFKvk3pVVlYcIA==
61926192

61936193
react-native@^0.58.3:
61946194
version "0.58.3"
@@ -6258,15 +6258,15 @@ react-proxy@^1.1.7:
62586258
lodash "^4.6.1"
62596259
react-deep-force-update "^1.0.0"
62606260

6261-
react-test-renderer@16.6.3:
6262-
version "16.6.3"
6263-
resolved "https://registry.yarnpkg.com/react-test-renderer/-/react-test-renderer-16.6.3.tgz#5f3a1a7d5c3379d46f7052b848b4b72e47c89f38"
6264-
integrity sha512-B5bCer+qymrQz/wN03lT0LppbZUDRq6AMfzMKrovzkGzfO81a9T+PWQW6MzkWknbwODQH/qpJno/yFQLX5IWrQ==
6261+
react-test-renderer@^16.8.3:
6262+
version "16.8.3"
6263+
resolved "https://registry.yarnpkg.com/react-test-renderer/-/react-test-renderer-16.8.3.tgz#230006af264cc46aeef94392e04747c21839e05e"
6264+
integrity sha512-rjJGYebduKNZH0k1bUivVrRLX04JfIQ0FKJLPK10TAb06XWhfi4gTobooF9K/DEFNW98iGac3OSxkfIJUN9Mdg==
62656265
dependencies:
62666266
object-assign "^4.1.1"
62676267
prop-types "^15.6.2"
6268-
react-is "^16.6.3"
6269-
scheduler "^0.11.2"
6268+
react-is "^16.8.3"
6269+
scheduler "^0.13.3"
62706270

62716271
react-transform-hmr@^1.0.4:
62726272
version "1.0.4"
@@ -6275,15 +6275,15 @@ react-transform-hmr@^1.0.4:
62756275
global "^4.3.0"
62766276
react-proxy "^1.1.7"
62776277

6278-
react@16.6.3:
6279-
version "16.6.3"
6280-
resolved "https://registry.yarnpkg.com/react/-/react-16.6.3.tgz#25d77c91911d6bbdd23db41e70fb094cc1e0871c"
6281-
integrity sha512-zCvmH2vbEolgKxtqXL2wmGCUxUyNheYn/C+PD1YAjfxHC54+MhdruyhO7QieQrYsYeTxrn93PM2y0jRH1zEExw==
6278+
react@^16.8.3:
6279+
version "16.8.3"
6280+
resolved "https://registry.yarnpkg.com/react/-/react-16.8.3.tgz#c6f988a2ce895375de216edcfaedd6b9a76451d9"
6281+
integrity sha512-3UoSIsEq8yTJuSu0luO1QQWYbgGEILm+eJl2QN/VLDi7hL+EN18M3q3oVZwmVzzBJ3DkM7RMdRwBmZZ+b4IzSA==
62826282
dependencies:
62836283
loose-envify "^1.1.0"
62846284
object-assign "^4.1.1"
62856285
prop-types "^15.6.2"
6286-
scheduler "^0.11.2"
6286+
scheduler "^0.13.3"
62876287

62886288
read-pkg-up@^1.0.1:
62896289
version "1.0.1"
@@ -6698,10 +6698,10 @@ sax@~1.1.1:
66986698
version "1.1.6"
66996699
resolved "https://registry.yarnpkg.com/sax/-/sax-1.1.6.tgz#5d616be8a5e607d54e114afae55b7eaf2fcc3240"
67006700

6701-
scheduler@^0.11.2:
6702-
version "0.11.3"
6703-
resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.11.3.tgz#b5769b90cf8b1464f3f3cfcafe8e3cd7555a2d6b"
6704-
integrity sha512-i9X9VRRVZDd3xZw10NY5Z2cVMbdYg6gqFecfj79USv1CFN+YrJ3gIPRKf1qlY+Sxly4djoKdfx1T+m9dnRB8kQ==
6701+
scheduler@^0.13.3:
6702+
version "0.13.3"
6703+
resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.13.3.tgz#bed3c5850f62ea9c716a4d781f9daeb9b2a58896"
6704+
integrity sha512-UxN5QRYWtpR1egNWzJcVLk8jlegxAugswQc984lD3kU7NuobsO37/sRfbpTdBjtnD5TBNFA2Q2oLV5+UmPSmEQ==
67056705
dependencies:
67066706
loose-envify "^1.1.0"
67076707
object-assign "^4.1.1"

0 commit comments

Comments
 (0)