diff --git a/.babelrc b/.babelrc index 80653af3..4d899c70 100644 --- a/.babelrc +++ b/.babelrc @@ -4,6 +4,7 @@ "@babel/react" ], "plugins": [ + "@babel/plugin-transform-runtime", "@babel/proposal-object-rest-spread", ["module-resolver", { "alias": { "src": "./src" } }], "@babel/transform-modules-commonjs" diff --git a/README.md b/README.md index fa3d9057..06b4b5b6 100644 --- a/README.md +++ b/README.md @@ -77,7 +77,9 @@ const useTheme = (initialTheme) => { } return useMemo(() => ({ ...themes[theme], toggleTheme }), [theme]) } +``` +```js // useTheme.test.js import { renderHook, cleanup, act } from 'react-hooks-testing-library' @@ -152,6 +154,7 @@ Renders a test component that will call the provided `callback`, including any h - `result` (`object`) - `current` (`any`) - the return value of the `callback` function +- `waitForNextUpdate` (`function`) - returns a `Promise` that resolves the next time the hook renders, commonly when state is updated as the result of a asynchronous action. - `rerender` (`function([newProps])`) - function to rerender the test component including any hooks called in the `callback` function. If `newProps` are passed, the will replace the `initialProps` passed the the `callback` function for future renders. - `unmount` (`function()`) - function to unmount the test component, commonly used to trigger cleanup effects for `useEffect` hooks. diff --git a/package-lock.json b/package-lock.json index 24acaf06..90c6b3d8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -781,6 +781,18 @@ "regenerator-transform": "^0.13.4" } }, + "@babel/plugin-transform-runtime": { + "version": "7.3.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.3.4.tgz", + "integrity": "sha512-PaoARuztAdd5MgeVjAxnIDAIUet5KpogqaefQvPOmPYCxYoaPhautxDh3aO8a4xHsKgT/b9gSxR0BKK1MIewPA==", + "dev": true, + "requires": { + "@babel/helper-module-imports": "^7.0.0", + "@babel/helper-plugin-utils": "^7.0.0", + "resolve": "^1.8.1", + "semver": "^5.5.1" + } + }, "@babel/plugin-transform-shorthand-properties": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.2.0.tgz", @@ -904,9 +916,9 @@ } }, "@babel/runtime": { - "version": "7.3.1", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.3.1.tgz", - "integrity": "sha512-7jGW8ppV0ant637pIqAcFfQDDH1orEPGJb8aXfUozuCU3QqX7rX4DA8iwrbPrR1hcH0FTTHz47yQnk+bl5xHQA==", + "version": "7.3.4", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.3.4.tgz", + "integrity": "sha512-IvfvnMdSaLBateu0jfsYIpZTxAc2cKEXEMiezGGN75QcBcecDUKd3PgLAncT0oOgxKy8dd8hrJKj9MfzgfZd6g==", "requires": { "regenerator-runtime": "^0.12.0" } @@ -1529,25 +1541,6 @@ "resolve": "^1.4.0" } }, - "babel-polyfill": { - "version": "6.26.0", - "resolved": "https://registry.npmjs.org/babel-polyfill/-/babel-polyfill-6.26.0.tgz", - "integrity": "sha1-N5k3q8Z9eJWXCtxiHyhM2WbPIVM=", - "dev": true, - "requires": { - "babel-runtime": "^6.26.0", - "core-js": "^2.5.0", - "regenerator-runtime": "^0.10.5" - }, - "dependencies": { - "regenerator-runtime": { - "version": "0.10.5", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.10.5.tgz", - "integrity": "sha1-M2w+/BIgrc7dosn6tntaeVWjNlg=", - "dev": true - } - } - }, "babel-preset-jest": { "version": "24.1.0", "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-24.1.0.tgz", @@ -2888,18 +2881,6 @@ "bser": "^2.0.0" } }, - "fetch-mock": { - "version": "7.3.1", - "resolved": "https://registry.npmjs.org/fetch-mock/-/fetch-mock-7.3.1.tgz", - "integrity": "sha512-euKqWnxeApj0toZ5MSavZJ7IIxbMaHpgteV2GNuz6/slAY0JUbRe95U/ueaz2spT/4nR75H4wpEmy2MMEsCoRg==", - "dev": true, - "requires": { - "babel-polyfill": "^6.26.0", - "glob-to-regexp": "^0.4.0", - "path-to-regexp": "^2.2.1", - "whatwg-url": "^6.5.0" - } - }, "figures": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/figures/-/figures-2.0.0.tgz", @@ -3701,12 +3682,6 @@ } } }, - "glob-to-regexp": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.0.tgz", - "integrity": "sha512-fyPCII4vn9Gvjq2U/oDAfP433aiE64cyP/CJjRJcpVGjqqNdioUYn9+r0cSzT1XPwmGAHuTT7iv+rQT8u/YHKQ==", - "dev": true - }, "globals": { "version": "11.11.0", "resolved": "https://registry.npmjs.org/globals/-/globals-11.11.0.tgz", @@ -6067,12 +6042,6 @@ "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==", "dev": true }, - "path-to-regexp": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-2.4.0.tgz", - "integrity": "sha512-G6zHoVqC6GGTQkZwF4lkuEyMbVOjoBKAEybQUypI1WTkqinCOrq2x6U2+phkJ1XsEMTy4LjtwPI7HW+NVrRR2w==", - "dev": true - }, "path-type": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/path-type/-/path-type-3.0.0.tgz", @@ -7137,15 +7106,6 @@ "scheduler": "^0.13.3" } }, - "react-async": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/react-async/-/react-async-5.1.0.tgz", - "integrity": "sha512-NrsmtBrIMcqdfTzDAhJ4wcySGTB8dHgSPfMeZWVk4NZSimjQ176tGHgjySvhijS5BjT8RykxX9hCUdIh4xhEQg==", - "dev": true, - "requires": { - "prop-types": ">=15.5.7" - } - }, "react-dom": { "version": "16.8.3", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-16.8.3.tgz", diff --git a/package.json b/package.json index 97d027ea..27d136c5 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,7 @@ "contributors:add": "all-contributors add" }, "dependencies": { + "@babel/runtime": "^7.3.4", "react-testing-library": "^6.0.0" }, "devDependencies": { @@ -34,6 +35,7 @@ "@babel/core": "^7.3.4", "@babel/plugin-proposal-object-rest-spread": "^7.3.4", "@babel/plugin-transform-modules-commonjs": "^7.2.0", + "@babel/plugin-transform-runtime": "^7.3.4", "@babel/preset-env": "^7.3.4", "@babel/preset-react": "^7.0.0", "@types/react": "^16.8.5", @@ -44,16 +46,13 @@ "eslint": "^5.14.1", "eslint-config-prettier": "^4.0.0", "eslint-plugin-prettier": "^3.0.1", - "fetch-mock": "^7.3.1", "husky": "^1.3.1", "jest": "^24.1.0", "lint-staged": "^8.1.4", - "node-fetch": "^2.3.0", "prettier": "^1.16.4", "prettier-eslint": "^8.8.2", "prettier-eslint-cli": "^4.7.1", "react": "^16.8.3", - "react-async": "^5.1.0", "react-dom": "^16.8.3", "typescript": "^3.3.3333", "typings-tester": "^0.3.2" diff --git a/src/index.js b/src/index.js index de7de5a9..59aff861 100644 --- a/src/index.js +++ b/src/index.js @@ -9,11 +9,17 @@ function TestHook({ callback, hookProps, children }) { function renderHook(callback, { initialProps, ...options } = {}) { const result = { current: null } const hookProps = { current: initialProps } + const resolvers = [] + const waitForNextUpdate = () => + new Promise((resolve) => { + resolvers.push(resolve) + }) const toRender = () => ( {(res) => { result.current = res + resolvers.splice(0, resolvers.length).forEach((resolve) => resolve()) }} ) @@ -22,6 +28,7 @@ function renderHook(callback, { initialProps, ...options } = {}) { return { result, + waitForNextUpdate, unmount, rerender: (newProps = hookProps.current) => { hookProps.current = newProps diff --git a/test/asyncHook.test.js b/test/asyncHook.test.js new file mode 100644 index 00000000..ad4b947a --- /dev/null +++ b/test/asyncHook.test.js @@ -0,0 +1,60 @@ +import { useState, useEffect } from 'react' +import { renderHook, cleanup } from 'src' + +describe('async hook tests', () => { + const getSomeName = () => Promise.resolve('Betty') + + const useName = (prefix) => { + const [name, setName] = useState('nobody') + + useEffect(() => { + getSomeName().then((theName) => { + setName(prefix ? `${prefix} ${theName}` : theName) + }) + }, [prefix]) + + return name + } + + afterEach(cleanup) + + test('should wait for next update', async () => { + const { result, waitForNextUpdate } = renderHook(() => useName()) + + expect(result.current).toBe('nobody') + + await waitForNextUpdate() + + expect(result.current).toBe('Betty') + }) + + test('should wait for multiple updates', async () => { + const { result, waitForNextUpdate, rerender } = renderHook(({ prefix }) => useName(prefix), { + initialProps: { prefix: 'Mrs.' } + }) + + expect(result.current).toBe('nobody') + + await waitForNextUpdate() + + expect(result.current).toBe('Mrs. Betty') + + rerender({ prefix: 'Ms.' }) + + await waitForNextUpdate() + + expect(result.current).toBe('Ms. Betty') + }) + + test('should resolve all when updating', async () => { + const { result, waitForNextUpdate } = renderHook(({ prefix }) => useName(prefix), { + initialProps: { prefix: 'Mrs.' } + }) + + expect(result.current).toBe('nobody') + + await Promise.all([waitForNextUpdate(), waitForNextUpdate(), waitForNextUpdate()]) + + expect(result.current).toBe('Mrs. Betty') + }) +}) diff --git a/test/typescript/renderHook.ts b/test/typescript/renderHook.ts index 78892a53..b5e31e5b 100644 --- a/test/typescript/renderHook.ts +++ b/test/typescript/renderHook.ts @@ -64,3 +64,12 @@ function checkTypesWhenHookReturnsVoid() { const _unmount: () => boolean = unmount const _rerender: () => void = rerender } + +async function checkTypesForWaitForNextUpdate() { + const { waitForNextUpdate } = renderHook(() => {}) + + await waitForNextUpdate() + + // check type + const _waitForNextUpdate: () => Promise = waitForNextUpdate +} diff --git a/typings/index.d.ts b/typings/index.d.ts index b5ba0267..2eec5415 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -9,6 +9,7 @@ export function renderHook( readonly result: { current: R } + readonly waitForNextUpdate: () => Promise readonly unmount: RenderResult['unmount'] readonly rerender: (hookProps?: P) => void }