Skip to content

Commit 11489b0

Browse files
Esemesekthymikee
authored andcommitted
feat: wrap render with act() for better React Hooks support (#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

Lines changed: 4 additions & 0 deletions
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

Lines changed: 14 additions & 8 deletions
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

Lines changed: 6 additions & 3 deletions
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

Lines changed: 50 additions & 0 deletions
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

Lines changed: 8 additions & 0 deletions
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

Lines changed: 9 additions & 2 deletions
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

Lines changed: 2 additions & 0 deletions
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

Lines changed: 22 additions & 3 deletions
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

Lines changed: 5 additions & 0 deletions
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

Lines changed: 5 additions & 0 deletions
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;

0 commit comments

Comments
 (0)