From 9fb2dbf8cb6aa48120a6a487337804dc70f4d332 Mon Sep 17 00:00:00 2001 From: Michael Peyper Date: Tue, 12 Mar 2019 22:21:18 +1100 Subject: [PATCH 1/4] Added nextUpdate promise for testing async hooks --- .babelrc | 1 + README.md | 3 ++ package-lock.json | 70 +++++++++--------------------------------- package.json | 5 ++- src/index.js | 12 +++++++- test/asyncHook.test.js | 60 ++++++++++++++++++++++++++++++++++++ 6 files changed, 92 insertions(+), 59 deletions(-) create mode 100644 test/asyncHook.test.js 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..268678a8 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 +- `nextUpdate` (`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..6aec842f 100644 --- a/src/index.js +++ b/src/index.js @@ -7,13 +7,22 @@ function TestHook({ callback, hookProps, children }) { } function renderHook(callback, { initialProps, ...options } = {}) { - const result = { current: null } + const result = { + current: null + } const hookProps = { current: initialProps } + const resolvers = [] + const nextUpdate = () => + new Promise((resolve) => { + resolvers.push(resolve) + }) + const toRender = () => ( {(res) => { result.current = res + resolvers.splice(0, resolvers.length).forEach((resolve) => resolve()) }} ) @@ -22,6 +31,7 @@ function renderHook(callback, { initialProps, ...options } = {}) { return { result, + nextUpdate, 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..2edf977c --- /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, nextUpdate } = renderHook(() => useName()) + + expect(result.current).toBe('nobody') + + await nextUpdate() + + expect(result.current).toBe('Betty') + }) + + test('should wait for multiple updates', async () => { + const { result, nextUpdate, rerender } = renderHook(({ prefix }) => useName(prefix), { + initialProps: { prefix: 'Mrs.' } + }) + + expect(result.current).toBe('nobody') + + await nextUpdate() + + expect(result.current).toBe('Mrs. Betty') + + rerender({ prefix: 'Ms.' }) + + await nextUpdate() + + expect(result.current).toBe('Ms. Betty') + }) + + test('should resolve all when updating', async () => { + const { result, nextUpdate } = renderHook(({ prefix }) => useName(prefix), { + initialProps: { prefix: 'Mrs.' } + }) + + expect(result.current).toBe('nobody') + + await Promise.all([nextUpdate(), nextUpdate(), nextUpdate()]) + + expect(result.current).toBe('Mrs. Betty') + }) +}) From a72a1a5c58c06b892b3b8d9ef66ec6b4aaf48474 Mon Sep 17 00:00:00 2001 From: Michael Peyper Date: Tue, 12 Mar 2019 22:31:13 +1100 Subject: [PATCH 2/4] Updated typings with nextUpdate --- test/typescript/renderHook.ts | 9 +++++++++ typings/index.d.ts | 1 + 2 files changed, 10 insertions(+) diff --git a/test/typescript/renderHook.ts b/test/typescript/renderHook.ts index 78892a53..b1be1183 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 checkTypesForNextUpdate() { + const { nextUpdate } = renderHook(() => {}) + + await nextUpdate() + + // check type + const _nextUpdate: () => Promise = nextUpdate +} diff --git a/typings/index.d.ts b/typings/index.d.ts index b5ba0267..2cfe21de 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -9,6 +9,7 @@ export function renderHook( readonly result: { current: R } + readonly nextUpdate: () => Promise readonly unmount: RenderResult['unmount'] readonly rerender: (hookProps?: P) => void } From fb2a1b25d3f97684deebd0cd03268ba695cff1bc Mon Sep 17 00:00:00 2001 From: Michael Peyper Date: Tue, 12 Mar 2019 22:46:57 +1100 Subject: [PATCH 3/4] Removed whitespace --- src/index.js | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/index.js b/src/index.js index 6aec842f..06c2ee3c 100644 --- a/src/index.js +++ b/src/index.js @@ -7,11 +7,8 @@ function TestHook({ callback, hookProps, children }) { } function renderHook(callback, { initialProps, ...options } = {}) { - const result = { - current: null - } + const result = { current: null } const hookProps = { current: initialProps } - const resolvers = [] const nextUpdate = () => new Promise((resolve) => { From 6e627c8d485c159f3379a4a2f77d2fee81d86eb6 Mon Sep 17 00:00:00 2001 From: Michael Peyper Date: Wed, 13 Mar 2019 12:25:23 +1100 Subject: [PATCH 4/4] Changed nextUpdate to waitForNextUpdate --- README.md | 2 +- src/index.js | 4 ++-- test/asyncHook.test.js | 14 +++++++------- test/typescript/renderHook.ts | 8 ++++---- typings/index.d.ts | 2 +- 5 files changed, 15 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index 268678a8..06b4b5b6 100644 --- a/README.md +++ b/README.md @@ -154,7 +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 -- `nextUpdate` (`function`) - returns a `Promise` that resolves the next time the hook renders, commonly when state is updated as the result of a asynchronous action. +- `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/src/index.js b/src/index.js index 06c2ee3c..59aff861 100644 --- a/src/index.js +++ b/src/index.js @@ -10,7 +10,7 @@ function renderHook(callback, { initialProps, ...options } = {}) { const result = { current: null } const hookProps = { current: initialProps } const resolvers = [] - const nextUpdate = () => + const waitForNextUpdate = () => new Promise((resolve) => { resolvers.push(resolve) }) @@ -28,7 +28,7 @@ function renderHook(callback, { initialProps, ...options } = {}) { return { result, - nextUpdate, + waitForNextUpdate, unmount, rerender: (newProps = hookProps.current) => { hookProps.current = newProps diff --git a/test/asyncHook.test.js b/test/asyncHook.test.js index 2edf977c..ad4b947a 100644 --- a/test/asyncHook.test.js +++ b/test/asyncHook.test.js @@ -19,41 +19,41 @@ describe('async hook tests', () => { afterEach(cleanup) test('should wait for next update', async () => { - const { result, nextUpdate } = renderHook(() => useName()) + const { result, waitForNextUpdate } = renderHook(() => useName()) expect(result.current).toBe('nobody') - await nextUpdate() + await waitForNextUpdate() expect(result.current).toBe('Betty') }) test('should wait for multiple updates', async () => { - const { result, nextUpdate, rerender } = renderHook(({ prefix }) => useName(prefix), { + const { result, waitForNextUpdate, rerender } = renderHook(({ prefix }) => useName(prefix), { initialProps: { prefix: 'Mrs.' } }) expect(result.current).toBe('nobody') - await nextUpdate() + await waitForNextUpdate() expect(result.current).toBe('Mrs. Betty') rerender({ prefix: 'Ms.' }) - await nextUpdate() + await waitForNextUpdate() expect(result.current).toBe('Ms. Betty') }) test('should resolve all when updating', async () => { - const { result, nextUpdate } = renderHook(({ prefix }) => useName(prefix), { + const { result, waitForNextUpdate } = renderHook(({ prefix }) => useName(prefix), { initialProps: { prefix: 'Mrs.' } }) expect(result.current).toBe('nobody') - await Promise.all([nextUpdate(), nextUpdate(), nextUpdate()]) + 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 b1be1183..b5e31e5b 100644 --- a/test/typescript/renderHook.ts +++ b/test/typescript/renderHook.ts @@ -65,11 +65,11 @@ function checkTypesWhenHookReturnsVoid() { const _rerender: () => void = rerender } -async function checkTypesForNextUpdate() { - const { nextUpdate } = renderHook(() => {}) +async function checkTypesForWaitForNextUpdate() { + const { waitForNextUpdate } = renderHook(() => {}) - await nextUpdate() + await waitForNextUpdate() // check type - const _nextUpdate: () => Promise = nextUpdate + const _waitForNextUpdate: () => Promise = waitForNextUpdate } diff --git a/typings/index.d.ts b/typings/index.d.ts index 2cfe21de..2eec5415 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -9,7 +9,7 @@ export function renderHook( readonly result: { current: R } - readonly nextUpdate: () => Promise + readonly waitForNextUpdate: () => Promise readonly unmount: RenderResult['unmount'] readonly rerender: (hookProps?: P) => void }