diff --git a/docs/API.md b/docs/API.md
index 915834ec6..d59702ad3 100644
--- a/docs/API.md
+++ b/docs/API.md
@@ -379,3 +379,7 @@ const { queryAllByText } = render();
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]).
diff --git a/flow-typed/npm/react-test-renderer_v16.x.x.js b/flow-typed/npm/react-test-renderer_v16.x.x.js
index 4e1d1ffe5..28ca6bade 100644
--- a/flow-typed/npm/react-test-renderer_v16.x.x.js
+++ b/flow-typed/npm/react-test-renderer_v16.x.x.js
@@ -9,13 +9,13 @@ type ReactComponentInstance = React$Component;
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
+ createNodeMock(element: React$Element): 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): void,
update(nextElement: React$Element): void,
getInstance(): ?ReactComponentInstance,
- root: ReactTestInstance
+ root: ReactTestInstance,
};
declare function create(
nextElement: React$Element,
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;
diff --git a/package.json b/package.json
index 2efdb4b5c..386dd8dcf 100644
--- a/package.json
+++ b/package.json
@@ -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": [
diff --git a/src/__tests__/act.test.js b/src/__tests__/act.test.js
new file mode 100644
index 000000000..db29ea4f3
--- /dev/null
+++ b/src/__tests__/act.test.js
@@ -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 (
+ setCount(count + 1)}>
+ {count}
+
+ );
+};
+
+test('render should trigger useEffect', () => {
+ const effectCallback = jest.fn();
+ render();
+
+ expect(effectCallback).toHaveBeenCalledTimes(1);
+});
+
+test('fireEvent should trigger useState', () => {
+ const { getByTestId } = render();
+ 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();
+});
diff --git a/src/act.js b/src/act.js
new file mode 100644
index 000000000..ac1dcb90e
--- /dev/null
+++ b/src/act.js
@@ -0,0 +1,8 @@
+// @flow
+import { act } from 'react-test-renderer';
+
+const actMock = (callback: () => void) => {
+ callback();
+};
+
+export default act || actMock;
diff --git a/src/fireEvent.js b/src/fireEvent.js
index 8386ecd6a..b2c49af78 100644
--- a/src/fireEvent.js
+++ b/src/fireEvent.js
@@ -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) =>
diff --git a/src/index.js b/src/index.js
index f8e6548fe..42547250c 100644
--- a/src/index.js
+++ b/src/index.js
@@ -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 };
diff --git a/src/render.js b/src/render.js
index 3262645ee..147175427 100644
--- a/src/render.js
+++ b/src/render.js
@@ -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,
+};
+
/**
* Renders test component deeply using react-test-renderer and exposes helpers
* to assert on the output.
*/
export default function render(
component: React.Element,
- options?: { createNodeMock: (element: React.Element) => 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,
+ 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);
diff --git a/typings/__tests__/index.test.tsx b/typings/__tests__/index.test.tsx
index b9312587f..37d084cbc 100644
--- a/typings/__tests__/index.test.tsx
+++ b/typings/__tests__/index.test.tsx
@@ -7,6 +7,7 @@ import {
flushMicrotasksQueue,
debug,
waitForElement,
+ act,
} from '../..';
interface HasRequiredProp {
@@ -131,3 +132,7 @@ const waitBy: Promise = waitForElement(
const waitByAll: Promise> = waitForElement<
Array
>(() => tree.getAllByName('View'), 1000, 50);
+
+act(() => {
+ render();
+});
diff --git a/typings/index.d.ts b/typings/index.d.ts
index 0b974e597..88a2b29e6 100644
--- a/typings/index.d.ts
+++ b/typings/index.d.ts
@@ -33,6 +33,10 @@ export interface QueryByAPI {
) => Array | [];
}
+export interface Thenable {
+ then: (resolve: () => any, reject?: () => any) => any,
+}
+
export interface RenderOptions {
createNodeMock: (element: React.ReactElement) => any;
}
@@ -86,3 +90,4 @@ export declare const flushMicrotasksQueue: () => Promise;
export declare const debug: DebugAPI;
export declare const fireEvent: FireEventAPI;
export declare const waitForElement: WaitForElementFunction;
+export declare const act: (callback: () => void) => Thenable;
diff --git a/yarn.lock b/yarn.lock
index 84cecb50e..1d9f2773f 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -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"