Skip to content
Permalink
Browse files

feat: wrap render with act() for better React Hooks support (#122)

  • Loading branch information...
Esemesek authored and thymikee committed Feb 28, 2019
1 parent 747e864 commit 11489b0e5e65cf279293d5074dba650683de3f0d
Showing with 144 additions and 35 deletions.
  1. +4 −0 docs/API.md
  2. +14 −8 flow-typed/npm/react-test-renderer_v16.x.x.js
  3. +6 −3 package.json
  4. +50 −0 src/__tests__/act.test.js
  5. +8 −0 src/act.js
  6. +9 −2 src/fireEvent.js
  7. +2 −0 src/index.js
  8. +22 −3 src/render.js
  9. +5 −0 typings/__tests__/index.test.tsx
  10. +5 −0 typings/index.d.ts
  11. +19 −19 yarn.lock
@@ -379,3 +379,7 @@ const { queryAllByText } = render(<Forms />);
const submitButtons = queryAllByText('submit');
expect(submitButtons).toHaveLength(3); // expect 3 elements
```

## `act`

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]).
@@ -9,13 +9,13 @@ type ReactComponentInstance = React$Component<any>;
type ReactTestRendererJSON = {
type: string,
props: { [propName: string]: any },
children: null | ReactTestRendererJSON[]
children: null | ReactTestRendererJSON[],
};

type ReactTestRendererTree = ReactTestRendererJSON & {
nodeType: "component" | "host",
nodeType: 'component' | 'host',
instance: ?ReactComponentInstance,
rendered: null | ReactTestRendererTree
rendered: null | ReactTestRendererTree,
};

type ReactTestInstance = {
@@ -40,30 +40,36 @@ type ReactTestInstance = {
findAllByProps(
props: { [propName: string]: any },
options?: { deep: boolean }
): ReactTestInstance[]
): ReactTestInstance[],
};

type TestRendererOptions = {
createNodeMock(element: React$Element<any>): any
createNodeMock(element: React$Element<any>): any,
};

type Thenable = {
then(resolve: () => mixed, reject?: () => mixed): mixed,
};

declare module "react-test-renderer" {
declare module 'react-test-renderer' {
declare export type ReactTestRenderer = {
toJSON(): null | ReactTestRendererJSON,
toTree(): null | ReactTestRendererTree,
unmount(nextElement?: React$Element<any>): void,
update(nextElement: React$Element<any>): void,
getInstance(): ?ReactComponentInstance,
root: ReactTestInstance
root: ReactTestInstance,
};

declare function create(
nextElement: React$Element<any>,
options?: TestRendererOptions
): ReactTestRenderer;

declare function act(callback: () => void): Thenable;
}

declare module "react-test-renderer/shallow" {
declare module 'react-test-renderer/shallow' {
declare export default class ShallowRenderer {
static createRenderer(): ShallowRenderer;
getMountedInstance(): ReactTestInstance;
@@ -24,9 +24,9 @@
"flow-copy-source": "^2.0.2",
"jest": "^24.1.0",
"metro-react-native-babel-preset": "^0.49.1",
"react": "16.6.3",
"react": "^16.8.3",
"react-native": "^0.58.3",
"react-test-renderer": "16.6.3",
"react-test-renderer": "^16.8.3",
"release-it": "^10.0.0",
"strip-ansi": "^5.0.0",
"typescript": "^3.1.1"
@@ -50,7 +50,10 @@
},
"jest": {
"preset": "react-native",
"moduleFileExtensions": ["js", "json"]
"moduleFileExtensions": [
"js",
"json"
]
},
"greenkeeper": {
"ignore": [
@@ -0,0 +1,50 @@
// @flow
import React from 'react';
import { Text } from 'react-native';
import ReactTestRenderer from 'react-test-renderer';
import act from '../act';
import render from '../render';
import fireEvent from '../fireEvent';

const UseEffect = ({ callback }: { callback: Function }) => {
React.useEffect(callback);
return null;
};

const Counter = () => {
const [count, setCount] = React.useState(0);

return (
<Text testID="counter" onPress={() => setCount(count + 1)}>
{count}
</Text>
);
};

test('render should trigger useEffect', () => {
const effectCallback = jest.fn();
render(<UseEffect callback={effectCallback} />);

expect(effectCallback).toHaveBeenCalledTimes(1);
});

test('fireEvent should trigger useState', () => {
const { getByTestId } = render(<Counter />);
const counter = getByTestId('counter');

expect(counter.props.children).toEqual(0);
fireEvent.press(counter);
expect(counter.props.children).toEqual(1);
});

test('should act even if there is no act in react-test-renderer', () => {
// $FlowFixMe
ReactTestRenderer.act = undefined;
const callback = jest.fn();

act(() => {
callback();
});

expect(callback).toHaveBeenCalled();
});
@@ -0,0 +1,8 @@
// @flow
import { act } from 'react-test-renderer';

const actMock = (callback: () => void) => {
callback();
};

export default act || actMock;
@@ -1,4 +1,5 @@
// @flow
import act from './act';
import { ErrorWithStack } from './helpers/errors';

const findEventHandler = (element: ReactTestInstance, eventName: string) => {
@@ -23,10 +24,16 @@ const invokeEvent = (
element: ReactTestInstance,
eventName: string,
data?: *
) => {
): any => {
const handler = findEventHandler(element, eventName);

return handler(data);
let returnValue;

act(() => {
returnValue = handler(data);
});

return returnValue;
};

const toEventHandlerName = (eventName: string) =>
@@ -1,4 +1,5 @@
// @flow
import act from './act';
import render from './render';
import shallow from './shallow';
import flushMicrotasksQueue from './flushMicrotasksQueue';
@@ -12,3 +13,4 @@ export { flushMicrotasksQueue };
export { debug };
export { fireEvent };
export { waitForElement };
export { act };
@@ -1,20 +1,26 @@
// @flow
import * as React from 'react';
import TestRenderer from 'react-test-renderer'; // eslint-disable-line import/no-extraneous-dependencies
import TestRenderer, { type ReactTestRenderer } from 'react-test-renderer'; // eslint-disable-line import/no-extraneous-dependencies
import act from './act';
import { getByAPI } from './helpers/getByAPI';
import { queryByAPI } from './helpers/queryByAPI';
import debugShallow from './helpers/debugShallow';
import debugDeep from './helpers/debugDeep';

type Options = {
createNodeMock: (element: React.Element<any>) => any,
};

/**
* Renders test component deeply using react-test-renderer and exposes helpers
* to assert on the output.
*/
export default function render(
component: React.Element<any>,
options?: { createNodeMock: (element: React.Element<any>) => any }
options?: Options
) {
const renderer = TestRenderer.create(component, options);
const renderer = renderWithAct(component, options);

const instance = renderer.root;

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

function renderWithAct(
component: React.Element<any>,
options?: Options
): ReactTestRenderer {
let renderer: ReactTestRenderer;

act(() => {
renderer = TestRenderer.create(component, options);
});

return ((renderer: any): ReactTestRenderer);
}

function debug(instance: ReactTestInstance, renderer) {
function debugImpl(message?: string) {
return debugDeep(renderer.toJSON(), message);
@@ -7,6 +7,7 @@ import {
flushMicrotasksQueue,
debug,
waitForElement,
act,
} from '../..';

interface HasRequiredProp {
@@ -131,3 +132,7 @@ const waitBy: Promise<ReactTestInstance> = waitForElement<ReactTestInstance>(
const waitByAll: Promise<Array<ReactTestInstance>> = waitForElement<
Array<ReactTestInstance>
>(() => tree.getAllByName('View'), 1000, 50);

act(() => {
render(<TestComponent />);
});
@@ -33,6 +33,10 @@ export interface QueryByAPI {
) => Array<ReactTestInstance> | [];
}

export interface Thenable {
then: (resolve: () => any, reject?: () => any) => any,
}

export interface RenderOptions {
createNodeMock: (element: React.ReactElement<any>) => any;
}
@@ -86,3 +90,4 @@ export declare const flushMicrotasksQueue: () => Promise<any>;
export declare const debug: DebugAPI;
export declare const fireEvent: FireEventAPI;
export declare const waitForElement: WaitForElementFunction;
export declare const act: (callback: () => void) => Thenable;
@@ -6185,10 +6185,10 @@ react-devtools-core@^3.4.2:
shell-quote "^1.6.1"
ws "^3.3.1"

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

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

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

react-transform-hmr@^1.0.4:
version "1.0.4"
@@ -6275,15 +6275,15 @@ react-transform-hmr@^1.0.4:
global "^4.3.0"
react-proxy "^1.1.7"

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

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

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

0 comments on commit 11489b0

Please sign in to comment.
You can’t perform that action at this time.