diff --git a/.all-contributorsrc b/.all-contributorsrc index fd2fd14a..d7d137c5 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -212,6 +212,64 @@ "contributions": [ "code" ] + }, + { + "login": "nobrayner", + "name": "Braydon Hall", + "avatar_url": "https://avatars2.githubusercontent.com/u/40751395?v=4", + "profile": "https://github.com/nobrayner", + "contributions": [ + "code" + ] + }, + { + "login": "JacobMGEvans", + "name": "Jacob M-G Evans", + "avatar_url": "https://avatars1.githubusercontent.com/u/27247160?v=4", + "profile": "https://dev.to/jacobmgevans", + "contributions": [ + "code", + "test" + ] + }, + { + "login": "tigerabrodi", + "name": "Tiger Abrodi", + "avatar_url": "https://avatars1.githubusercontent.com/u/49603590?v=4", + "profile": "https://tigerabrodi.dev/", + "contributions": [ + "code", + "test" + ] + }, + { + "login": "merodiro", + "name": "Amr A.Mohammed", + "avatar_url": "https://avatars1.githubusercontent.com/u/17033502?v=4", + "profile": "https://github.com/merodiro", + "contributions": [ + "code", + "test" + ] + }, + { + "login": "juhanakristian", + "name": "Juhana Jauhiainen", + "avatar_url": "https://avatars1.githubusercontent.com/u/544386?v=4", + "profile": "https://github.com/juhanakristian", + "contributions": [ + "code" + ] + }, + { + "login": "jensmeindertsma", + "name": "Jens Meindertsma", + "avatar_url": "https://avatars3.githubusercontent.com/u/64677517?v=4", + "profile": "https://github.com/jensmeindertsma", + "contributions": [ + "code", + "test" + ] } ], "commitConvention": "none" diff --git a/.eslintrc b/.eslintrc index d6c4554f..052c9caa 100644 --- a/.eslintrc +++ b/.eslintrc @@ -1,10 +1,16 @@ { - "extends": "./node_modules/kcd-scripts/eslint.js", + "extends": ["./node_modules/kcd-scripts/eslint.js"], "rules": { "max-lines-per-function": "off", "no-constant-condition": "off", "no-await-in-loop": "off", + "no-console": "off", + "import/no-unresolved": "off", "react-hooks/rules-of-hooks": "off", - "no-console": "off" + "@typescript-eslint/no-floating-promises": "off", + "@typescript-eslint/no-unnecessary-condition": "off" + }, + "parserOptions": { + "project": ["./tsconfig.json", "./test/tsconfig.json"] } } diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 8924985f..24384b04 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -27,11 +27,6 @@ select the added contribution type. Please make sure to run the tests before you commit your changes. You can do so by running `npm test`. -### Update Typings - -The TypeScript type definitions can be found in the -[DefinitelyTyped repo](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/testing-library__react-hooks). - ## Help needed Please check out the diff --git a/README.md b/README.md index 72ef55a9..b62c279d 100644 --- a/README.md +++ b/README.md @@ -192,6 +192,14 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
Josh

📖
Na'aman Hirschfeld

💻 + +
Braydon Hall

💻 +
Jacob M-G Evans

💻 ⚠️ +
Tiger Abrodi

💻 ⚠️ +
Amr A.Mohammed

💻 ⚠️ +
Juhana Jauhiainen

💻 +
Jens Meindertsma

💻 ⚠️ + diff --git a/docs/api-reference.md b/docs/api-reference.md index a3cc05e5..fa7daf4b 100644 --- a/docs/api-reference.md +++ b/docs/api-reference.md @@ -259,28 +259,3 @@ The maximum amount of time in milliseconds (ms) to wait. By default, no timeout If this option is set to `true`, any errors that occur while waiting are treated as a failed check. If this option is set to `false`, any errors that occur while waiting cause the promise to be rejected. By default, errors are not suppressed for this utility. - -### `wait` - -_(DEPRECATED, use [`waitFor`](/reference/api#waitfor) instead)_ - -```js -function wait(callback: function(): boolean|void, options?: { - timeout?: number, - suppressErrors?: boolean -}): Promise -``` - -Returns a `Promise` that resolves if the provided callback executes without exception and returns a -truthy or `undefined` value. It is safe to use the [`result` of `renderHook`](/reference/api#result) -in the callback to perform assertion or to test values. - -#### `timeout` - -The maximum amount of time in milliseconds (ms) to wait. By default, no timeout is applied. - -#### `suppressErrors` - -If this option is set to `true`, any errors that occur while waiting are treated as a failed check. -If this option is set to `false`, any errors that occur while waiting cause the promise to be -rejected. By default, errors are suppressed for this utility. diff --git a/jest.config.js b/jest.config.js index c2e6a069..bb6a1d85 100644 --- a/jest.config.js +++ b/jest.config.js @@ -1,6 +1,7 @@ +// eslint-disable-next-line const { jest: jestConfig } = require('kcd-scripts/config') module.exports = Object.assign(jestConfig, { roots: ['/src', '/test'], - testMatch: ['/test/*.js'] + testMatch: ['/test/*.(ts|tsx|js)'] }) diff --git a/package.json b/package.json index cb1f5f57..5e90940a 100644 --- a/package.json +++ b/package.json @@ -3,6 +3,7 @@ "version": "0.0.0-semantically-released", "description": "Simple and complete React hooks testing utilities that encourage good testing practices.", "main": "lib/index.js", + "types": "lib/index.d.ts", "keywords": [ "testing", "react", @@ -27,29 +28,34 @@ "validate": "kcd-scripts validate", "prepare": "npm run build", "build": "kcd-scripts build --out-dir lib", + "test": "kcd-scripts test", + "typecheck": "kcd-scripts typecheck", "lint": "kcd-scripts lint", "format": "kcd-scripts format", "coverage": "codecov", - "test": "kcd-scripts test", "docs:dev": "docz dev", "docs:build": "docz build", "contributors:add": "all-contributors add" }, "dependencies": { "@babel/runtime": "^7.12.5", - "@types/testing-library__react-hooks": "^3.4.0" + "@types/react": ">=16.9.0", + "@types/react-test-renderer": ">=16.9.0" }, "devDependencies": { + "@typescript-eslint/eslint-plugin": "^4.9.1", + "@typescript-eslint/parser": "^4.9.1", "all-contributors-cli": "6.19.0", "codecov": "3.8.1", "docz": "2.3.1", "docz-theme-default": "1.2.0", "docz-utils": "2.3.0", + "eslint": "7.15.0", "kcd-scripts": "7.5.2", + "prettier": "^2.2.1", "react": "17.0.1", "react-test-renderer": "17.0.1", - "typescript": "4.1.2", - "eslint": "7.15.0" + "typescript": "4.1.2" }, "peerDependencies": { "react": ">=16.9.0", diff --git a/src/asyncUtils.js b/src/asyncUtils.js deleted file mode 100644 index afb5ac7e..00000000 --- a/src/asyncUtils.js +++ /dev/null @@ -1,123 +0,0 @@ -import { act } from 'react-test-renderer' - -function createTimeoutError(utilName, { timeout }) { - const timeoutError = new Error(`Timed out in ${utilName} after ${timeout}ms.`) - timeoutError.timeout = true - return timeoutError -} - -function resolveAfter(ms) { - return new Promise((resolve) => { - setTimeout(resolve, ms) - }) -} - -let hasWarnedDeprecatedWait = false - -function asyncUtils(addResolver) { - let nextUpdatePromise = null - - const waitForNextUpdate = async (options = {}) => { - if (!nextUpdatePromise) { - nextUpdatePromise = new Promise((resolve, reject) => { - let timeoutId - if (options.timeout > 0) { - timeoutId = setTimeout( - () => reject(createTimeoutError('waitForNextUpdate', options)), - options.timeout - ) - } - addResolver(() => { - clearTimeout(timeoutId) - nextUpdatePromise = null - resolve() - }) - }) - await act(() => nextUpdatePromise) - } - await nextUpdatePromise - } - - const waitFor = async (callback, { interval, timeout, suppressErrors = true } = {}) => { - // eslint-disable-next-line consistent-return - const checkResult = () => { - try { - const callbackResult = callback() - return callbackResult || callbackResult === undefined - } catch (e) { - if (!suppressErrors) { - throw e - } - } - } - - const waitForResult = async () => { - const initialTimeout = timeout - while (true) { - const startTime = Date.now() - try { - const nextCheck = interval - ? Promise.race([waitForNextUpdate({ timeout }), resolveAfter(interval)]) - : waitForNextUpdate({ timeout }) - - await nextCheck - - if (checkResult()) { - return - } - } catch (e) { - if (e.timeout) { - throw createTimeoutError('waitFor', { timeout: initialTimeout }) - } - throw e - } - timeout -= Date.now() - startTime - } - } - - if (!checkResult()) { - await waitForResult() - } - } - - const waitForValueToChange = async (selector, options = {}) => { - const initialValue = selector() - try { - await waitFor(() => selector() !== initialValue, { - suppressErrors: false, - ...options - }) - } catch (e) { - if (e.timeout) { - throw createTimeoutError('waitForValueToChange', options) - } - throw e - } - } - - const wait = async (callback, { timeout, suppressErrors } = {}) => { - if (!hasWarnedDeprecatedWait) { - hasWarnedDeprecatedWait = true - console.warn( - '`wait` has been deprecated. Use `waitFor` instead: https://react-hooks-testing-library.com/reference/api#waitfor.' - ) - } - try { - await waitFor(callback, { timeout, suppressErrors }) - } catch (e) { - if (e.timeout) { - throw createTimeoutError('wait', { timeout }) - } - throw e - } - } - - return { - wait, - waitFor, - waitForNextUpdate, - waitForValueToChange - } -} - -export default asyncUtils diff --git a/src/asyncUtils.ts b/src/asyncUtils.ts new file mode 100644 index 00000000..22921721 --- /dev/null +++ b/src/asyncUtils.ts @@ -0,0 +1,112 @@ +import { act } from 'react-test-renderer' + +export interface WaitOptions { + interval?: number + timeout?: number + suppressErrors?: boolean +} + +class TimeoutError extends Error { + constructor(utilName: string, { timeout }: Pick) { + super(`Timed out in ${utilName} after ${timeout as number}ms.`) + } +} + +function resolveAfter(ms: number) { + return new Promise((resolve) => { + setTimeout(resolve, ms) + }) +} + +function asyncUtils(addResolver: (callback: () => void) => void) { + let nextUpdatePromise: Promise | null = null + + const waitForNextUpdate = async (options: Pick = {}) => { + if (!nextUpdatePromise) { + nextUpdatePromise = new Promise((resolve, reject) => { + let timeoutId: ReturnType + if (options.timeout && options.timeout > 0) { + timeoutId = setTimeout( + () => reject(new TimeoutError('waitForNextUpdate', options)), + options.timeout + ) + } + addResolver(() => { + clearTimeout(timeoutId) + nextUpdatePromise = null + resolve() + }) + }) + await act(() => nextUpdatePromise as Promise) + } + await nextUpdatePromise + } + + const waitFor = async ( + callback: () => T | Promise, + { interval, timeout, suppressErrors = true }: WaitOptions = {} + ) => { + const checkResult = () => { + try { + const callbackResult = callback() + return callbackResult || callbackResult === undefined + } catch (error: unknown) { + if (!suppressErrors) { + throw error as Error + } + return undefined + } + } + + const waitForResult = async () => { + const initialTimeout = timeout + while (true) { + const startTime = Date.now() + try { + const nextCheck = interval + ? Promise.race([waitForNextUpdate({ timeout }), resolveAfter(interval)]) + : waitForNextUpdate({ timeout }) + + await nextCheck + + if (checkResult()) { + return + } + } catch (error: unknown) { + if (error instanceof TimeoutError) { + throw new TimeoutError('waitFor', { timeout: initialTimeout }) + } + throw error as Error + } + if (timeout) timeout -= Date.now() - startTime + } + } + + if (!checkResult()) { + await waitForResult() + } + } + + const waitForValueToChange = async (selector: () => unknown, options: WaitOptions = {}) => { + const initialValue = selector() + try { + await waitFor(() => selector() !== initialValue, { + suppressErrors: false, + ...options + }) + } catch (error: unknown) { + if (error instanceof TimeoutError) { + throw new TimeoutError('waitForValueToChange', options) + } + throw error as Error + } + } + + return { + waitFor, + waitForNextUpdate, + waitForValueToChange + } +} + +export default asyncUtils diff --git a/src/cleanup.js b/src/cleanup.ts similarity index 55% rename from src/cleanup.js rename to src/cleanup.ts index d3172c46..8309bd04 100644 --- a/src/cleanup.js +++ b/src/cleanup.ts @@ -1,4 +1,4 @@ -let cleanupCallbacks = [] +let cleanupCallbacks: (() => Promise | void)[] = [] async function cleanup() { for (const callback of cleanupCallbacks) { @@ -7,12 +7,12 @@ async function cleanup() { cleanupCallbacks = [] } -function addCleanup(callback) { - cleanupCallbacks.unshift(callback) +function addCleanup(callback: () => Promise | void) { + cleanupCallbacks = [callback, ...cleanupCallbacks] return () => removeCleanup(callback) } -function removeCleanup(callback) { +function removeCleanup(callback: () => Promise | void) { cleanupCallbacks = cleanupCallbacks.filter((cb) => cb !== callback) } diff --git a/src/index.js b/src/index.ts similarity index 100% rename from src/index.js rename to src/index.ts diff --git a/src/pure.js b/src/pure.js deleted file mode 100644 index 435d0736..00000000 --- a/src/pure.js +++ /dev/null @@ -1,105 +0,0 @@ -import React, { Suspense } from 'react' -import { act, create } from 'react-test-renderer' -import asyncUtils from './asyncUtils' -import { cleanup, addCleanup, removeCleanup } from './cleanup' - -function TestHook({ callback, hookProps, onError, children }) { - try { - children(callback(hookProps)) - } catch (err) { - if (err.then) { - throw err - } else { - onError(err) - } - } - return null -} - -function Fallback() { - return null -} - -function resultContainer() { - const results = [] - const resolvers = [] - - const result = { - get all() { - return results.map(({ value, error }) => error || value) - }, - get current() { - const { value, error } = results[results.length - 1] - if (error) { - throw error - } - return value - }, - get error() { - const { error } = results[results.length - 1] - return error - } - } - - const updateResult = (value, error) => { - results.push({ value, error }) - resolvers.splice(0, resolvers.length).forEach((resolve) => resolve()) - } - - return { - result, - addResolver: (resolver) => { - resolvers.push(resolver) - }, - setValue: (value) => updateResult(value), - setError: (error) => updateResult(undefined, error) - } -} - -function renderHook(callback, { initialProps, wrapper } = {}) { - const { result, setValue, setError, addResolver } = resultContainer() - const hookProps = { current: initialProps } - - const wrapUiIfNeeded = (innerElement) => - wrapper ? React.createElement(wrapper, hookProps.current, innerElement) : innerElement - - const toRender = () => - wrapUiIfNeeded( - }> - - {setValue} - - - ) - - let testRenderer - act(() => { - testRenderer = create(toRender()) - }) - const { unmount, update } = testRenderer - - function rerenderHook(newProps = hookProps.current) { - hookProps.current = newProps - act(() => { - update(toRender()) - }) - } - - function unmountHook() { - act(() => { - removeCleanup(unmountHook) - unmount() - }) - } - - addCleanup(unmountHook) - - return { - result, - rerender: rerenderHook, - unmount: unmountHook, - ...asyncUtils(addResolver) - } -} - -export { renderHook, cleanup, addCleanup, removeCleanup, act } diff --git a/src/pure.tsx b/src/pure.tsx new file mode 100644 index 00000000..a1c14897 --- /dev/null +++ b/src/pure.tsx @@ -0,0 +1,124 @@ +import React, { ReactElement, ReactNode, Suspense } from 'react' +import { act, create, ReactTestRenderer } from 'react-test-renderer' +import asyncUtils from './asyncUtils' +import { cleanup, addCleanup, removeCleanup } from './cleanup' + +function isPromise(value: unknown): boolean { + return typeof (value as PromiseLike).then === 'function' +} + +type TestHookProps = { + callback: (props: TProps) => TResult + hookProps: TProps | undefined + onError: (error: Error) => void + children: (value: TResult) => void +} + +function TestHook({ + callback, + hookProps, + onError, + children +}: TestHookProps) { + try { + // coerce undefined into TProps, so it maintains the previous behaviour + children(callback(hookProps as TProps)) + } catch (err: unknown) { + if (isPromise(err)) { + throw err + } else { + onError(err as Error) + } + } + return null +} + +function Fallback() { + return null +} + +function resultContainer() { + const results: Array<{ value?: TValue; error?: Error }> = [] + const resolvers: Array<() => void> = [] + + const result = { + get all() { + return results.map(({ value, error }) => error ?? value) + }, + get current() { + const { value, error } = results[results.length - 1] + if (error) { + throw error + } + return value as TValue + }, + get error() { + const { error } = results[results.length - 1] + return error + } + } + + const updateResult = (value?: TValue, error?: Error) => { + results.push({ value, error }) + resolvers.splice(0, resolvers.length).forEach((resolve) => resolve()) + } + + return { + result, + addResolver: (resolver: () => void) => { + resolvers.push(resolver) + }, + setValue: (value: TValue) => updateResult(value), + setError: (error: Error) => updateResult(undefined, error) + } +} + +function renderHook( + callback: (props: TProps) => TResult, + { initialProps, wrapper }: { initialProps?: TProps; wrapper?: React.ComponentType } = {} +) { + const { result, setValue, setError, addResolver } = resultContainer() + const hookProps = { current: initialProps } + + const wrapUiIfNeeded = (innerElement: ReactNode) => + wrapper ? React.createElement(wrapper, hookProps.current, innerElement) : innerElement + + const toRender = () => + wrapUiIfNeeded( + }> + + {setValue} + + + ) as ReactElement + + let testRenderer: ReactTestRenderer + act(() => { + testRenderer = create(toRender()) + }) + + function rerenderHook(newProps: typeof initialProps = hookProps.current) { + hookProps.current = newProps + act(() => { + testRenderer.update(toRender()) + }) + } + + function unmountHook() { + act(() => { + removeCleanup(unmountHook) + testRenderer.unmount() + }) + } + + addCleanup(unmountHook) + + return { + result, + rerender: rerenderHook, + unmount: unmountHook, + ...asyncUtils(addResolver) + } +} + +export { renderHook, cleanup, addCleanup, removeCleanup, act } diff --git a/test/asyncHook.js b/test/asyncHook.ts similarity index 74% rename from test/asyncHook.js rename to test/asyncHook.ts index 74d321a6..5479db82 100644 --- a/test/asyncHook.js +++ b/test/asyncHook.ts @@ -2,7 +2,7 @@ import { useState, useRef, useEffect } from 'react' import { renderHook } from '../src' describe('async hook tests', () => { - const useSequence = (...values) => { + const useSequence = (...values: string[]) => { const [first, ...otherValues] = values const [value, setValue] = useState(first) const index = useRef(0) @@ -266,92 +266,4 @@ describe('async hook tests', () => { expect(result.current).toBe('third') }) - - test('should wait for expectation to pass (deprecated)', async () => { - const { result, wait } = renderHook(() => useSequence('first', 'second', 'third')) - - expect(result.current).toBe('first') - - let complete = false - await wait(() => { - expect(result.current).toBe('third') - complete = true - }) - expect(complete).toBe(true) - }) - - test('should not hang if expectation is already passing (deprecated)', async () => { - const { result, wait } = renderHook(() => useSequence('first', 'second')) - - expect(result.current).toBe('first') - - let complete = false - await wait(() => { - expect(result.current).toBe('first') - complete = true - }) - expect(complete).toBe(true) - }) - - test('should reject if callback throws error (deprecated)', async () => { - const { result, wait } = renderHook(() => useSequence('first', 'second', 'third')) - - expect(result.current).toBe('first') - - await expect( - wait( - () => { - if (result.current === 'second') { - throw new Error('Something Unexpected') - } - return result.current === 'third' - }, - { - suppressErrors: false - } - ) - ).rejects.toThrow(Error('Something Unexpected')) - }) - - test('should reject if callback immediately throws error (deprecated)', async () => { - const { result, wait } = renderHook(() => useSequence('first', 'second', 'third')) - - expect(result.current).toBe('first') - - await expect( - wait( - () => { - throw new Error('Something Unexpected') - }, - { - suppressErrors: false - } - ) - ).rejects.toThrow(Error('Something Unexpected')) - }) - - test('should wait for truthy value (deprecated)', async () => { - const { result, wait } = renderHook(() => useSequence('first', 'second', 'third')) - - expect(result.current).toBe('first') - - await wait(() => result.current === 'third') - - expect(result.current).toBe('third') - }) - - test('should reject if timeout exceeded when waiting for expectation to pass (deprecated)', async () => { - const { result, wait } = renderHook(() => useSequence('first', 'second', 'third')) - - expect(result.current).toBe('first') - - await expect( - wait( - () => { - expect(result.current).toBe('third') - }, - { timeout: 75 } - ) - ).rejects.toThrow(Error('Timed out in wait after 75ms.')) - }) }) diff --git a/test/autoCleanup.disabled.js b/test/autoCleanup.disabled.ts similarity index 89% rename from test/autoCleanup.disabled.js rename to test/autoCleanup.disabled.ts index d11f9314..35cbf91a 100644 --- a/test/autoCleanup.disabled.js +++ b/test/autoCleanup.disabled.ts @@ -4,10 +4,11 @@ import { useEffect } from 'react' // then we DON'T auto-wire up the afterEach for folks describe('skip auto cleanup (disabled) tests', () => { let cleanupCalled = false - let renderHook + let renderHook: (arg0: () => void) => void beforeAll(() => { process.env.RHTL_SKIP_AUTO_CLEANUP = 'true' + // eslint-disable-next-line renderHook = require('../src').renderHook }) diff --git a/test/autoCleanup.noAfterEach.js b/test/autoCleanup.noAfterEach.ts similarity index 80% rename from test/autoCleanup.noAfterEach.js rename to test/autoCleanup.noAfterEach.ts index 9b894e00..cd30a841 100644 --- a/test/autoCleanup.noAfterEach.js +++ b/test/autoCleanup.noAfterEach.ts @@ -4,11 +4,13 @@ import { useEffect } from 'react' // then we DON'T auto-wire up the afterEach for folks describe('skip auto cleanup (no afterEach) tests', () => { let cleanupCalled = false - let renderHook + let renderHook: (arg0: () => void) => void beforeAll(() => { + // @ts-expect-error Turning off AfterEach -- ignore Jest LifeCycle Type // eslint-disable-next-line no-global-assign afterEach = false + // eslint-disable-next-line renderHook = require('../').renderHook }) diff --git a/test/autoCleanup.js b/test/autoCleanup.ts similarity index 100% rename from test/autoCleanup.js rename to test/autoCleanup.ts diff --git a/test/cleanup.js b/test/cleanup.ts similarity index 92% rename from test/cleanup.js rename to test/cleanup.ts index 05dba6dc..1eafffbf 100644 --- a/test/cleanup.js +++ b/test/cleanup.ts @@ -21,8 +21,8 @@ describe('cleanup tests', () => { }) test('should cleanup all rendered hooks', async () => { - const cleanupCalled = [] - const hookWithCleanup = (id) => { + const cleanupCalled: boolean[] = [] + const hookWithCleanup = (id: number) => { useEffect(() => { return () => { cleanupCalled[id] = true @@ -40,7 +40,7 @@ describe('cleanup tests', () => { }) test('should call cleanups in reverse order', async () => { - const callSequence = [] + const callSequence: string[] = [] addCleanup(() => { callSequence.push('cleanup') }) @@ -62,7 +62,7 @@ describe('cleanup tests', () => { }) test('should wait for async cleanup', async () => { - const callSequence = [] + const callSequence: string[] = [] addCleanup(() => { callSequence.push('cleanup') }) @@ -85,7 +85,7 @@ describe('cleanup tests', () => { }) test('should remove cleanup using removeCleanup', async () => { - const callSequence = [] + const callSequence: string[] = [] addCleanup(() => { callSequence.push('cleanup') }) @@ -110,7 +110,7 @@ describe('cleanup tests', () => { }) test('should remove cleanup using returned handler', async () => { - const callSequence = [] + const callSequence: string[] = [] addCleanup(() => { callSequence.push('cleanup') }) diff --git a/test/customHook.js b/test/customHook.ts similarity index 100% rename from test/customHook.js rename to test/customHook.ts diff --git a/test/errorHook.js b/test/errorHook.ts similarity index 95% rename from test/errorHook.js rename to test/errorHook.ts index 55e425e2..e507bb92 100644 --- a/test/errorHook.js +++ b/test/errorHook.ts @@ -2,15 +2,15 @@ import { useState, useEffect } from 'react' import { renderHook } from '../src' describe('error hook tests', () => { - function useError(throwError) { + function useError(throwError: boolean) { if (throwError) { throw new Error('expected') } return true } - function useAsyncError(throwError) { - const [value, setValue] = useState() + function useAsyncError(throwError: boolean) { + const [value, setValue] = useState() useEffect(() => { const timeout = setTimeout(() => setValue(throwError), 100) return () => clearTimeout(timeout) @@ -18,7 +18,7 @@ describe('error hook tests', () => { return useError(value) } - function useEffectError(throwError) { + function useEffectError(throwError: boolean) { useEffect(() => { useError(throwError) }, [throwError]) diff --git a/test/resultHistory.js b/test/resultHistory.ts similarity index 100% rename from test/resultHistory.js rename to test/resultHistory.ts diff --git a/test/suspenseHook.js b/test/suspenseHook.ts similarity index 76% rename from test/suspenseHook.js rename to test/suspenseHook.ts index 6dcfdeae..8d696927 100644 --- a/test/suspenseHook.js +++ b/test/suspenseHook.ts @@ -1,10 +1,10 @@ import { renderHook } from '../src' describe('suspense hook tests', () => { - const cache = {} - const fetchName = (isSuccessful) => { + const cache: { value?: Promise | string | Error } = {} + const fetchName = (isSuccessful: boolean) => { if (!cache.value) { - cache.value = new Promise((resolve, reject) => { + cache.value = new Promise((resolve, reject) => { setTimeout(() => { if (isSuccessful) { resolve('Bob') @@ -14,15 +14,15 @@ describe('suspense hook tests', () => { }, 50) }) .then((value) => (cache.value = value)) - .catch((e) => (cache.value = e)) + .catch((e: Error) => (cache.value = e)) } return cache.value } const useFetchName = (isSuccessful = true) => { const name = fetchName(isSuccessful) - if (typeof name.then === 'function' || name instanceof Error) { - throw name + if (name instanceof Promise || name instanceof Error) { + throw name as unknown } return name } diff --git a/test/tsconfig.json b/test/tsconfig.json new file mode 100644 index 00000000..48209b56 --- /dev/null +++ b/test/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../tsconfig.json", + "compilerOptions": { + "declaration": false + }, + "exclude": [], + "include": ["."] +} diff --git a/test/useContext.js b/test/useContext.tsx similarity index 89% rename from test/useContext.js rename to test/useContext.tsx index 4bcbe774..03bc19f4 100644 --- a/test/useContext.js +++ b/test/useContext.tsx @@ -15,7 +15,7 @@ describe('useContext tests', () => { test('should get value from context provider', () => { const TestContext = createContext('foo') - const wrapper = ({ children }) => ( + const wrapper: React.FC = ({ children } ) => ( {children} ) @@ -29,7 +29,7 @@ describe('useContext tests', () => { const value = { current: 'bar' } - const wrapper = ({ children }) => ( + const wrapper: React.FC = ({ children }) => ( {children} ) @@ -45,7 +45,8 @@ describe('useContext tests', () => { test('should update value in context when props are updated', () => { const TestContext = createContext('foo') - const wrapper = ({ current, children }) => ( + + const wrapper: React.FC<{current: string}> = ({ current, children }) => ( {children} ) diff --git a/test/useEffect.js b/test/useEffect.ts similarity index 89% rename from test/useEffect.js rename to test/useEffect.ts index 9e120e07..cad9d0f3 100644 --- a/test/useEffect.js +++ b/test/useEffect.ts @@ -3,7 +3,7 @@ import { renderHook } from '../src' describe('useEffect tests', () => { test('should handle useEffect hook', () => { - const sideEffect = { 1: false, 2: false } + const sideEffect: { [key: number]: boolean } = { 1: false, 2: false } const { rerender, unmount } = renderHook( ({ id }) => { @@ -32,7 +32,7 @@ describe('useEffect tests', () => { }) test('should handle useLayoutEffect hook', () => { - const sideEffect = { 1: false, 2: false } + const sideEffect: { [key: number]: boolean } = { 1: false, 2: false } const { rerender, unmount } = renderHook( ({ id }) => { diff --git a/test/useMemo.js b/test/useMemo.ts similarity index 100% rename from test/useMemo.js rename to test/useMemo.ts diff --git a/test/useReducer.js b/test/useReducer.ts similarity index 79% rename from test/useReducer.js rename to test/useReducer.ts index 114f579b..7b98431a 100644 --- a/test/useReducer.js +++ b/test/useReducer.ts @@ -3,7 +3,8 @@ import { renderHook, act } from '../src' describe('useReducer tests', () => { test('should handle useReducer hook', () => { - const reducer = (state, action) => (action.type === 'inc' ? state + 1 : state) + const reducer = (state: number, action: { type: string }) => + action.type === 'inc' ? state + 1 : state const { result } = renderHook(() => useReducer(reducer, 0)) const [initialState, dispatch] = result.current diff --git a/test/useRef.js b/test/useRef.ts similarity index 92% rename from test/useRef.js rename to test/useRef.ts index b9dbefe3..9d3851ff 100644 --- a/test/useRef.js +++ b/test/useRef.ts @@ -13,7 +13,7 @@ describe('useHook tests', () => { test('should handle useImperativeHandle hook', () => { const { result } = renderHook(() => { - const ref = useRef() + const ref = useRef boolean>>({}) useImperativeHandle(ref, () => ({ fakeImperativeMethod: () => true })) diff --git a/test/useState.js b/test/useState.ts similarity index 100% rename from test/useState.js rename to test/useState.ts diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 00000000..1337ac30 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "./node_modules/kcd-scripts/shared-tsconfig.json", + "compilerOptions": { + "allowJs": true, + "target": "ES6" + }, + "exclude": ["./test"] +}