Skip to content

feature: better support for hooks #122

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Feb 28, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions docs/API.md
Original file line number Diff line number Diff line change
Expand Up @@ -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]).
22 changes: 14 additions & 8 deletions flow-typed/npm/react-test-renderer_v16.x.x.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand All @@ -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;
Expand Down
9 changes: 6 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -50,7 +50,10 @@
},
"jest": {
"preset": "react-native",
"moduleFileExtensions": ["js", "json"]
"moduleFileExtensions": [
"js",
"json"
]
},
"greenkeeper": {
"ignore": [
Expand Down
50 changes: 50 additions & 0 deletions src/__tests__/act.test.js
Original file line number Diff line number Diff line change
@@ -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();
});
8 changes: 8 additions & 0 deletions src/act.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
// @flow
import { act } from 'react-test-renderer';

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

export default act || actMock;
11 changes: 9 additions & 2 deletions src/fireEvent.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
// @flow
import act from './act';
import { ErrorWithStack } from './helpers/errors';

const findEventHandler = (element: ReactTestInstance, eventName: string) => {
Expand All @@ -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) =>
Expand Down
2 changes: 2 additions & 0 deletions src/index.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
// @flow
import act from './act';
import render from './render';
import shallow from './shallow';
import flushMicrotasksQueue from './flushMicrotasksQueue';
Expand All @@ -12,3 +13,4 @@ export { flushMicrotasksQueue };
export { debug };
export { fireEvent };
export { waitForElement };
export { act };
25 changes: 22 additions & 3 deletions src/render.js
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -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);
Expand Down
5 changes: 5 additions & 0 deletions typings/__tests__/index.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
flushMicrotasksQueue,
debug,
waitForElement,
act,
} from '../..';

interface HasRequiredProp {
Expand Down Expand Up @@ -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 />);
});
5 changes: 5 additions & 0 deletions typings/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down Expand Up @@ -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;
38 changes: 19 additions & 19 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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"
Expand All @@ -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"
Expand Down Expand Up @@ -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"
Expand Down