From 3841db2f7cb2c727226db16a4ebe5d345f1068fa Mon Sep 17 00:00:00 2001 From: Josh Ellis Date: Sun, 6 Dec 2020 21:55:34 +0000 Subject: [PATCH 01/53] chore: move current config (react-test-renderer) to src/native --- jest.config.js | 2 +- native/index.js | 1 + native/pure.js | 2 + package.json | 9 + src/{ => core}/asyncUtils.js | 4 +- src/{ => core}/cleanup.js | 11 +- .../flushMicrotasks.js} | 2 +- src/core/index.js | 91 ++++++++++ src/index.js | 11 +- src/native/index.js | 5 + src/native/pure.js | 39 ++++ src/pure.js | 111 ++---------- test/cleanup.js | 135 -------------- .../asyncHook.test.js} | 170 ++---------------- .../autoCleanup.disabled.test.js} | 2 +- .../autoCleanup.noAfterEach.test.js} | 2 +- .../autoCleanup.test.js} | 2 +- test/native/cleanup.test.js | 41 +++++ .../customHook.test.js} | 2 +- .../errorHook.test.js} | 25 ++- .../suspenseHook.test.js} | 2 +- .../useContext.test.js} | 2 +- .../useEffect.test.js} | 2 +- test/{useMemo.js => native/useMemo.test.js} | 2 +- .../useReducer.test.js} | 2 +- test/{useRef.js => native/useRef.test.js} | 2 +- test/{useState.js => native/useState.test.js} | 2 +- 27 files changed, 257 insertions(+), 424 deletions(-) create mode 100644 native/index.js create mode 100644 native/pure.js rename src/{ => core}/asyncUtils.js (97%) rename src/{ => core}/cleanup.js (59%) rename src/{flush-microtasks.js => core/flushMicrotasks.js} (96%) create mode 100644 src/core/index.js create mode 100644 src/native/index.js create mode 100644 src/native/pure.js delete mode 100644 test/cleanup.js rename test/{asyncHook.js => native/asyncHook.test.js} (62%) rename test/{autoCleanup.disabled.js => native/autoCleanup.disabled.test.js} (91%) rename test/{autoCleanup.noAfterEach.js => native/autoCleanup.noAfterEach.test.js} (91%) rename test/{autoCleanup.js => native/autoCleanup.test.js} (91%) create mode 100644 test/native/cleanup.test.js rename test/{customHook.js => native/customHook.test.js} (93%) rename test/{errorHook.js => native/errorHook.test.js} (86%) rename test/{suspenseHook.js => native/suspenseHook.test.js} (96%) rename test/{useContext.js => native/useContext.test.js} (97%) rename test/{useEffect.js => native/useEffect.test.js} (96%) rename test/{useMemo.js => native/useMemo.test.js} (96%) rename test/{useReducer.js => native/useReducer.test.js} (90%) rename test/{useRef.js => native/useRef.test.js} (93%) rename test/{useState.js => native/useState.test.js} (91%) diff --git a/jest.config.js b/jest.config.js index c2e6a069..5a389b18 100644 --- a/jest.config.js +++ b/jest.config.js @@ -2,5 +2,5 @@ const { jest: jestConfig } = require('kcd-scripts/config') module.exports = Object.assign(jestConfig, { roots: ['/src', '/test'], - testMatch: ['/test/*.js'] + testMatch: ['/test/**/*.js'] }) diff --git a/native/index.js b/native/index.js new file mode 100644 index 00000000..9ca3ec48 --- /dev/null +++ b/native/index.js @@ -0,0 +1 @@ +module.exports = require('../lib/native') diff --git a/native/pure.js b/native/pure.js new file mode 100644 index 00000000..10dc143f --- /dev/null +++ b/native/pure.js @@ -0,0 +1,2 @@ +// makes it so people can import from '@testing-library/react-hooks/native/pure' +module.exports = require('../lib/native/pure') diff --git a/package.json b/package.json index 5c94d467..56403274 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ "files": [ "lib", "src", + "native", "pure.js", "dont-cleanup-after-each.js" ], @@ -53,5 +54,13 @@ "peerDependencies": { "react": ">=16.9.0", "react-test-renderer": ">=16.9.0" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + }, + "react-test-renderer": { + "optional": true + } } } diff --git a/src/asyncUtils.js b/src/core/asyncUtils.js similarity index 97% rename from src/asyncUtils.js rename to src/core/asyncUtils.js index afb5ac7e..94651b12 100644 --- a/src/asyncUtils.js +++ b/src/core/asyncUtils.js @@ -1,5 +1,3 @@ -import { act } from 'react-test-renderer' - function createTimeoutError(utilName, { timeout }) { const timeoutError = new Error(`Timed out in ${utilName} after ${timeout}ms.`) timeoutError.timeout = true @@ -14,7 +12,7 @@ function resolveAfter(ms) { let hasWarnedDeprecatedWait = false -function asyncUtils(addResolver) { +function asyncUtils(act, addResolver) { let nextUpdatePromise = null const waitForNextUpdate = async (options = {}) => { diff --git a/src/cleanup.js b/src/core/cleanup.js similarity index 59% rename from src/cleanup.js rename to src/core/cleanup.js index 3699180a..b1926709 100644 --- a/src/cleanup.js +++ b/src/core/cleanup.js @@ -1,4 +1,4 @@ -import flushMicroTasks from './flush-microtasks' +import flushMicroTasks from './flushMicrotasks' let cleanupCallbacks = [] @@ -19,4 +19,13 @@ function removeCleanup(callback) { cleanupCallbacks = cleanupCallbacks.filter((cb) => cb !== callback) } +cleanup.autoRegister = function() { + // Automatically registers cleanup in supported testing frameworks + if (typeof afterEach === 'function' && !process.env.RHTL_SKIP_AUTO_CLEANUP) { + afterEach(async () => { + await cleanup() + }) + } +} + export { cleanup, addCleanup, removeCleanup } diff --git a/src/flush-microtasks.js b/src/core/flushMicrotasks.js similarity index 96% rename from src/flush-microtasks.js rename to src/core/flushMicrotasks.js index 0869dfef..c934652d 100644 --- a/src/flush-microtasks.js +++ b/src/core/flushMicrotasks.js @@ -4,7 +4,7 @@ // anyway. So we're just going to ignore coverage for this file /** * copied from React's enqueueTask.js - * copied again from React Testing Library's flush-microtasks.js + * copied again from React Testing Library's flushMicrotasks.js */ let didWarnAboutMessageChannel = false diff --git a/src/core/index.js b/src/core/index.js new file mode 100644 index 00000000..70c000c2 --- /dev/null +++ b/src/core/index.js @@ -0,0 +1,91 @@ +import asyncUtils from './asyncUtils' +import { cleanup, addCleanup, removeCleanup } from './cleanup' + +function TestHook({ callback, setValue, setError, ...props }) { + try { + setValue(callback(props)) + } catch (err) { + if (err.then) { + throw err + } else { + setError(err) + } + } + return null +} + +function resultContainer() { + let value = null + let error = null + const resolvers = [] + + const result = { + get current() { + if (error) { + throw error + } + return value + }, + get error() { + return error + } + } + + const updateResult = (val, err) => { + value = val + error = err + resolvers.splice(0, resolvers.length).forEach((resolve) => resolve()) + } + + return { + result, + addResolver: (resolver) => { + resolvers.push(resolver) + }, + setValue: (val) => updateResult(val), + setError: (err) => updateResult(undefined, err) + } +} + +function defaultWrapper({ children }) { + return children +} + +function createRenderHook(createRenderer) { + return function renderHook(callback, { initialProps, wrapper = defaultWrapper } = {}) { + const { result, setValue, setError, addResolver } = resultContainer() + const hookProps = { current: initialProps } + const props = { callback, setValue, setError } + const options = { initialProps, wrapper } + + const { render, rerender, unmount, act, ...rendererUtils } = createRenderer( + TestHook, + props, + options + ) + + render(hookProps.current) + + function rerenderHook(newProps = hookProps.current) { + hookProps.current = newProps + rerender(hookProps.current) + } + + function unmountHook() { + removeCleanup(unmountHook) + unmount() + } + + addCleanup(unmountHook) + + return { + result, + rerender: rerenderHook, + unmount: unmountHook, + ...asyncUtils(act, addResolver), + ...rendererUtils + } + } +} + +export { createRenderHook, cleanup } diff --git a/src/index.js b/src/index.js index c1abc074..d18e042c 100644 --- a/src/index.js +++ b/src/index.js @@ -1,10 +1,5 @@ -import { cleanup } from './pure' +import { renderHook, act, cleanup } from './pure' -// Automatically registers cleanup in supported testing frameworks -if (typeof afterEach === 'function' && !process.env.RHTL_SKIP_AUTO_CLEANUP) { - afterEach(async () => { - await cleanup() - }) -} +cleanup.autoRegister() -export * from './pure' +export { renderHook, act, cleanup } diff --git a/src/native/index.js b/src/native/index.js new file mode 100644 index 00000000..d18e042c --- /dev/null +++ b/src/native/index.js @@ -0,0 +1,5 @@ +import { renderHook, act, cleanup } from './pure' + +cleanup.autoRegister() + +export { renderHook, act, cleanup } diff --git a/src/native/pure.js b/src/native/pure.js new file mode 100644 index 00000000..325e8ae2 --- /dev/null +++ b/src/native/pure.js @@ -0,0 +1,39 @@ +import React, { Suspense } from 'react' +import { act, create } from 'react-test-renderer' +import { createRenderHook, cleanup } from '../core' + +function FallbackComponent() { + return null +} +function createRenderer(TestHook, testHookProps, { wrapper: Wrapper }) { + let container + + const toRender = (props) => ( + + + + ) + + return { + render(props) { + act(() => { + container = create(}>{toRender(props)}) + }) + }, + rerender(props) { + act(() => { + container.update(}>{toRender(props)}) + }) + }, + unmount() { + act(() => { + container.unmount() + }) + }, + act + } +} + +const renderHook = createRenderHook(createRenderer) + +export { renderHook, act, cleanup } diff --git a/src/pure.js b/src/pure.js index 53ef84c5..8390f7d5 100644 --- a/src/pure.js +++ b/src/pure.js @@ -1,102 +1,23 @@ -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) +const RENDERERS = [{ required: 'react-test-renderer', renderer: './native/pure' }] + +function getRenderer(renderers) { + const hasDependency = (name) => { + try { + require(name) + return true + } catch { + return false } } - return null -} - -function Fallback() { - return null -} - -function resultContainer() { - let value = null - let error = null - const resolvers = [] - - const result = { - get current() { - if (error) { - throw error - } - return value - }, - get error() { - return error - } - } - - const updateResult = (val, err) => { - value = val - error = err - resolvers.splice(0, resolvers.length).forEach((resolve) => resolve()) - } - - return { - result, - addResolver: (resolver) => { - resolvers.push(resolver) - }, - setValue: (val) => updateResult(val), - setError: (err) => updateResult(undefined, err) - } -} - -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) + const [validRenderer] = renderers.filter(({ required }) => hasDependency(required)) - return { - result, - rerender: rerenderHook, - unmount: unmountHook, - ...asyncUtils(addResolver) + if (!validRenderer) { + const options = renderers.map(({ option }) => ` - ${option}`).join('\n') + throw new Error(`Could not auto-detect a React renderer. Options are:\n${options}`) + } else { + return validRenderer.renderer } } -export { renderHook, cleanup, addCleanup, removeCleanup, act } +module.exports = require(getRenderer(RENDERERS)) diff --git a/test/cleanup.js b/test/cleanup.js deleted file mode 100644 index 05dba6dc..00000000 --- a/test/cleanup.js +++ /dev/null @@ -1,135 +0,0 @@ -import { useEffect } from 'react' -import { renderHook, cleanup, addCleanup, removeCleanup } from '../src/pure' - -describe('cleanup tests', () => { - test('should flush effects on cleanup', async () => { - let cleanupCalled = false - - const hookWithCleanup = () => { - useEffect(() => { - return () => { - cleanupCalled = true - } - }) - } - - renderHook(() => hookWithCleanup()) - - await cleanup() - - expect(cleanupCalled).toBe(true) - }) - - test('should cleanup all rendered hooks', async () => { - const cleanupCalled = [] - const hookWithCleanup = (id) => { - useEffect(() => { - return () => { - cleanupCalled[id] = true - } - }) - } - - renderHook(() => hookWithCleanup(1)) - renderHook(() => hookWithCleanup(2)) - - await cleanup() - - expect(cleanupCalled[1]).toBe(true) - expect(cleanupCalled[2]).toBe(true) - }) - - test('should call cleanups in reverse order', async () => { - const callSequence = [] - addCleanup(() => { - callSequence.push('cleanup') - }) - addCleanup(() => { - callSequence.push('another cleanup') - }) - const hookWithCleanup = () => { - useEffect(() => { - return () => { - callSequence.push('unmount') - } - }) - } - renderHook(() => hookWithCleanup()) - - await cleanup() - - expect(callSequence).toEqual(['unmount', 'another cleanup', 'cleanup']) - }) - - test('should wait for async cleanup', async () => { - const callSequence = [] - addCleanup(() => { - callSequence.push('cleanup') - }) - addCleanup(async () => { - await new Promise((resolve) => setTimeout(resolve, 10)) - callSequence.push('another cleanup') - }) - const hookWithCleanup = () => { - useEffect(() => { - return () => { - callSequence.push('unmount') - } - }) - } - renderHook(() => hookWithCleanup()) - - await cleanup() - - expect(callSequence).toEqual(['unmount', 'another cleanup', 'cleanup']) - }) - - test('should remove cleanup using removeCleanup', async () => { - const callSequence = [] - addCleanup(() => { - callSequence.push('cleanup') - }) - const anotherCleanup = () => { - callSequence.push('another cleanup') - } - addCleanup(anotherCleanup) - const hookWithCleanup = () => { - useEffect(() => { - return () => { - callSequence.push('unmount') - } - }) - } - renderHook(() => hookWithCleanup()) - - removeCleanup(anotherCleanup) - - await cleanup() - - expect(callSequence).toEqual(['unmount', 'cleanup']) - }) - - test('should remove cleanup using returned handler', async () => { - const callSequence = [] - addCleanup(() => { - callSequence.push('cleanup') - }) - const remove = addCleanup(() => { - callSequence.push('another cleanup') - }) - const hookWithCleanup = () => { - useEffect(() => { - return () => { - callSequence.push('unmount') - } - }) - } - renderHook(() => hookWithCleanup()) - - remove() - - await cleanup() - - expect(callSequence).toEqual(['unmount', 'cleanup']) - }) -}) diff --git a/test/asyncHook.js b/test/native/asyncHook.test.js similarity index 62% rename from test/asyncHook.js rename to test/native/asyncHook.test.js index 74d321a6..a56d4037 100644 --- a/test/asyncHook.js +++ b/test/native/asyncHook.test.js @@ -1,5 +1,5 @@ import { useState, useRef, useEffect } from 'react' -import { renderHook } from '../src' +import { renderHook } from '../../src/native' describe('async hook tests', () => { const useSequence = (...values) => { @@ -17,7 +17,7 @@ describe('async hook tests', () => { return () => { clearInterval(interval) } - }, [otherValues]) + }, [...values]) return value } @@ -67,47 +67,25 @@ describe('async hook tests', () => { }) test('should wait for expectation to pass', async () => { - const { result, waitFor } = renderHook(() => useSequence('first', 'second', 'third')) + const { result, wait } = renderHook(() => useSequence('first', 'second', 'third')) expect(result.current).toBe('first') let complete = false - await waitFor(() => { + await wait(() => { expect(result.current).toBe('third') complete = true }) expect(complete).toBe(true) }) - test('should wait for arbitrary expectation to pass', async () => { - const { waitFor } = renderHook(() => null) - - let actual = 0 - const expected = 1 - - setTimeout(() => { - actual = expected - }, 200) - - let complete = false - await waitFor( - () => { - expect(actual).toBe(expected) - complete = true - }, - { interval: 100 } - ) - - expect(complete).toBe(true) - }) - test('should not hang if expectation is already passing', async () => { - const { result, waitFor } = renderHook(() => useSequence('first', 'second')) + const { result, wait } = renderHook(() => useSequence('first', 'second')) expect(result.current).toBe('first') let complete = false - await waitFor(() => { + await wait(() => { expect(result.current).toBe('first') complete = true }) @@ -115,12 +93,12 @@ describe('async hook tests', () => { }) test('should reject if callback throws error', async () => { - const { result, waitFor } = renderHook(() => useSequence('first', 'second', 'third')) + const { result, wait } = renderHook(() => useSequence('first', 'second', 'third')) expect(result.current).toBe('first') await expect( - waitFor( + wait( () => { if (result.current === 'second') { throw new Error('Something Unexpected') @@ -135,12 +113,12 @@ describe('async hook tests', () => { }) test('should reject if callback immediately throws error', async () => { - const { result, waitFor } = renderHook(() => useSequence('first', 'second', 'third')) + const { result, wait } = renderHook(() => useSequence('first', 'second', 'third')) expect(result.current).toBe('first') await expect( - waitFor( + wait( () => { throw new Error('Something Unexpected') }, @@ -152,43 +130,28 @@ describe('async hook tests', () => { }) test('should wait for truthy value', async () => { - const { result, waitFor } = renderHook(() => useSequence('first', 'second', 'third')) + const { result, wait } = renderHook(() => useSequence('first', 'second', 'third')) expect(result.current).toBe('first') - await waitFor(() => result.current === 'third') + await wait(() => result.current === 'third') expect(result.current).toBe('third') }) - test('should wait for arbitrary truthy value', async () => { - const { waitFor } = renderHook(() => null) - - let actual = 0 - const expected = 1 - - setTimeout(() => { - actual = expected - }, 200) - - await waitFor(() => actual === 1, { interval: 100 }) - - expect(actual).toBe(expected) - }) - test('should reject if timeout exceeded when waiting for expectation to pass', async () => { - const { result, waitFor } = renderHook(() => useSequence('first', 'second', 'third')) + const { result, wait } = renderHook(() => useSequence('first', 'second', 'third')) expect(result.current).toBe('first') await expect( - waitFor( + wait( () => { expect(result.current).toBe('third') }, { timeout: 75 } ) - ).rejects.toThrow(Error('Timed out in waitFor after 75ms.')) + ).rejects.toThrow(Error('Timed out in wait after 75ms.')) }) test('should wait for value to change', async () => { @@ -203,21 +166,6 @@ describe('async hook tests', () => { expect(result.current).toBe('third') }) - test('should wait for arbitrary value to change', async () => { - const { waitForValueToChange } = renderHook(() => null) - - let actual = 0 - const expected = 1 - - setTimeout(() => { - actual = expected - }, 200) - - await waitForValueToChange(() => actual, { interval: 100 }) - - expect(actual).toBe(expected) - }) - test('should reject if timeout exceeded when waiting for value to change', async () => { const { result, waitForValueToChange } = renderHook(() => useSequence('first', 'second', 'third') @@ -266,92 +214,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/native/autoCleanup.disabled.test.js similarity index 91% rename from test/autoCleanup.disabled.js rename to test/native/autoCleanup.disabled.test.js index d11f9314..e98cc8d3 100644 --- a/test/autoCleanup.disabled.js +++ b/test/native/autoCleanup.disabled.test.js @@ -8,7 +8,7 @@ describe('skip auto cleanup (disabled) tests', () => { beforeAll(() => { process.env.RHTL_SKIP_AUTO_CLEANUP = 'true' - renderHook = require('../src').renderHook + renderHook = require('../../src/native').renderHook }) test('first', () => { diff --git a/test/autoCleanup.noAfterEach.js b/test/native/autoCleanup.noAfterEach.test.js similarity index 91% rename from test/autoCleanup.noAfterEach.js rename to test/native/autoCleanup.noAfterEach.test.js index 9b894e00..d94c0bb0 100644 --- a/test/autoCleanup.noAfterEach.js +++ b/test/native/autoCleanup.noAfterEach.test.js @@ -9,7 +9,7 @@ describe('skip auto cleanup (no afterEach) tests', () => { beforeAll(() => { // eslint-disable-next-line no-global-assign afterEach = false - renderHook = require('../').renderHook + renderHook = require('../../src/native').renderHook }) test('first', () => { diff --git a/test/autoCleanup.js b/test/native/autoCleanup.test.js similarity index 91% rename from test/autoCleanup.js rename to test/native/autoCleanup.test.js index 5dcdc1d1..2d7addf9 100644 --- a/test/autoCleanup.js +++ b/test/native/autoCleanup.test.js @@ -1,5 +1,5 @@ import { useEffect } from 'react' -import { renderHook } from '../src' +import { renderHook } from '../../src/native' // This verifies that by importing RHTL in an // environment which supports afterEach (like Jest) diff --git a/test/native/cleanup.test.js b/test/native/cleanup.test.js new file mode 100644 index 00000000..479a90c8 --- /dev/null +++ b/test/native/cleanup.test.js @@ -0,0 +1,41 @@ +import { useEffect } from 'react' +import { renderHook, cleanup } from '../../src/native' + +describe('cleanup tests', () => { + test('should flush effects on cleanup', async () => { + let cleanupCalled = false + + const hookWithCleanup = () => { + useEffect(() => { + return () => { + cleanupCalled = true + } + }) + } + + renderHook(() => hookWithCleanup()) + + await cleanup() + + expect(cleanupCalled).toBe(true) + }) + + test('should cleanup all rendered hooks', async () => { + let cleanupCalled = [] + const hookWithCleanup = (id) => { + useEffect(() => { + return () => { + cleanupCalled[id] = true + } + }) + } + + renderHook(() => hookWithCleanup(1)) + renderHook(() => hookWithCleanup(2)) + + await cleanup() + + expect(cleanupCalled[1]).toBe(true) + expect(cleanupCalled[2]).toBe(true) + }) +}) diff --git a/test/customHook.js b/test/native/customHook.test.js similarity index 93% rename from test/customHook.js rename to test/native/customHook.test.js index 871c5619..8d699188 100644 --- a/test/customHook.js +++ b/test/native/customHook.test.js @@ -1,5 +1,5 @@ import { useState, useCallback } from 'react' -import { renderHook, act } from '../src' +import { renderHook, act } from '../../src/native' describe('custom hook tests', () => { function useCounter() { diff --git a/test/errorHook.js b/test/native/errorHook.test.js similarity index 86% rename from test/errorHook.js rename to test/native/errorHook.test.js index 55e425e2..4753c8f9 100644 --- a/test/errorHook.js +++ b/test/native/errorHook.test.js @@ -1,5 +1,5 @@ import { useState, useEffect } from 'react' -import { renderHook } from '../src' +import { renderHook } from '../../src/native' describe('error hook tests', () => { function useError(throwError) { @@ -21,7 +21,7 @@ describe('error hook tests', () => { function useEffectError(throwError) { useEffect(() => { useError(throwError) - }, [throwError]) + }, []) return true } @@ -48,13 +48,13 @@ describe('error hook tests', () => { }) test('should reset error', () => { - const { result, rerender } = renderHook((throwError) => useError(throwError), { - initialProps: true + const { result, rerender } = renderHook(({ throwError }) => useError(throwError), { + initialProps: { throwError: true } }) expect(result.error).not.toBe(undefined) - rerender(false) + rerender({ throwError: false }) expect(result.current).not.toBe(undefined) expect(result.error).toBe(undefined) @@ -91,17 +91,15 @@ describe('error hook tests', () => { test('should reset async error', async () => { const { result, waitForNextUpdate, rerender } = renderHook( - (throwError) => useAsyncError(throwError), - { - initialProps: true - } + ({ throwError }) => useAsyncError(throwError), + { initialProps: { throwError: true } } ) await waitForNextUpdate() expect(result.error).not.toBe(undefined) - rerender(false) + rerender({ throwError: false }) await waitForNextUpdate() @@ -115,7 +113,6 @@ describe('error hook tests', () => { Refer to https://github.com/testing-library/react-hooks-testing-library/issues/308 for more details. */ - // eslint-disable-next-line jest/no-disabled-tests describe.skip('effect', () => { test('should raise effect error', () => { const { result } = renderHook(() => useEffectError(true)) @@ -138,13 +135,13 @@ describe('error hook tests', () => { }) test('should reset effect error', () => { - const { result, rerender } = renderHook((throwError) => useEffectError(throwError), { - initialProps: true + const { result, rerender } = renderHook(({ throwError }) => useEffectError(throwError), { + initialProps: { throwError: true } }) expect(result.error).not.toBe(undefined) - rerender(false) + rerender({ throwError: false }) expect(result.current).not.toBe(undefined) expect(result.error).toBe(undefined) diff --git a/test/suspenseHook.js b/test/native/suspenseHook.test.js similarity index 96% rename from test/suspenseHook.js rename to test/native/suspenseHook.test.js index 6dcfdeae..ab473ba5 100644 --- a/test/suspenseHook.js +++ b/test/native/suspenseHook.test.js @@ -1,4 +1,4 @@ -import { renderHook } from '../src' +import { renderHook } from '../../src/native' describe('suspense hook tests', () => { const cache = {} diff --git a/test/useContext.js b/test/native/useContext.test.js similarity index 97% rename from test/useContext.js rename to test/native/useContext.test.js index 4bcbe774..40754e46 100644 --- a/test/useContext.js +++ b/test/native/useContext.test.js @@ -1,5 +1,5 @@ import React, { createContext, useContext } from 'react' -import { renderHook } from '../src' +import { renderHook } from '../../src/native' describe('useContext tests', () => { test('should get default value from context', () => { diff --git a/test/useEffect.js b/test/native/useEffect.test.js similarity index 96% rename from test/useEffect.js rename to test/native/useEffect.test.js index 9e120e07..7bad47ff 100644 --- a/test/useEffect.js +++ b/test/native/useEffect.test.js @@ -1,5 +1,5 @@ import { useEffect, useLayoutEffect } from 'react' -import { renderHook } from '../src' +import { renderHook } from '../../src/native' describe('useEffect tests', () => { test('should handle useEffect hook', () => { diff --git a/test/useMemo.js b/test/native/useMemo.test.js similarity index 96% rename from test/useMemo.js rename to test/native/useMemo.test.js index b2c452ab..465ef591 100644 --- a/test/useMemo.js +++ b/test/native/useMemo.test.js @@ -1,5 +1,5 @@ import { useMemo, useCallback } from 'react' -import { renderHook } from '../src' +import { renderHook } from '../../src/native' describe('useCallback tests', () => { test('should handle useMemo hook', () => { diff --git a/test/useReducer.js b/test/native/useReducer.test.js similarity index 90% rename from test/useReducer.js rename to test/native/useReducer.test.js index 114f579b..92b2ce55 100644 --- a/test/useReducer.js +++ b/test/native/useReducer.test.js @@ -1,5 +1,5 @@ import { useReducer } from 'react' -import { renderHook, act } from '../src' +import { renderHook, act } from '../../src/native' describe('useReducer tests', () => { test('should handle useReducer hook', () => { diff --git a/test/useRef.js b/test/native/useRef.test.js similarity index 93% rename from test/useRef.js rename to test/native/useRef.test.js index b9dbefe3..f1c81c5c 100644 --- a/test/useRef.js +++ b/test/native/useRef.test.js @@ -1,5 +1,5 @@ import { useRef, useImperativeHandle } from 'react' -import { renderHook } from '../src' +import { renderHook } from '../../src/native' describe('useHook tests', () => { test('should handle useRef hook', () => { diff --git a/test/useState.js b/test/native/useState.test.js similarity index 91% rename from test/useState.js rename to test/native/useState.test.js index 42f3f8b0..f384434f 100644 --- a/test/useState.js +++ b/test/native/useState.test.js @@ -1,5 +1,5 @@ import { useState } from 'react' -import { renderHook, act } from '../src' +import { renderHook, act } from '../../src/native' describe('useState tests', () => { test('should use setState value', () => { From 40991cb470f0a8a3f1e7f31037e3809f80365383 Mon Sep 17 00:00:00 2001 From: Josh Ellis Date: Thu, 10 Dec 2020 23:42:54 +0000 Subject: [PATCH 02/53] change: add eslint to avoid conflict & fix lint errors --- package.json | 1 + src/core/cleanup.js | 2 +- src/pure.js | 6 +++--- test/native/cleanup.test.js | 2 +- 4 files changed, 6 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 56403274..f5fa56e6 100644 --- a/package.json +++ b/package.json @@ -46,6 +46,7 @@ "docz": "2.3.1", "docz-theme-default": "1.2.0", "docz-utils": "2.3.0", + "eslint": "^7.15.0", "kcd-scripts": "7.5.1", "react": "17.0.1", "react-test-renderer": "17.0.1", diff --git a/src/core/cleanup.js b/src/core/cleanup.js index b1926709..52a66601 100644 --- a/src/core/cleanup.js +++ b/src/core/cleanup.js @@ -19,7 +19,7 @@ function removeCleanup(callback) { cleanupCallbacks = cleanupCallbacks.filter((cb) => cb !== callback) } -cleanup.autoRegister = function() { +cleanup.autoRegister = () => { // Automatically registers cleanup in supported testing frameworks if (typeof afterEach === 'function' && !process.env.RHTL_SKIP_AUTO_CLEANUP) { afterEach(async () => { diff --git a/src/pure.js b/src/pure.js index 8390f7d5..ccad8705 100644 --- a/src/pure.js +++ b/src/pure.js @@ -12,11 +12,11 @@ function getRenderer(renderers) { const [validRenderer] = renderers.filter(({ required }) => hasDependency(required)) - if (!validRenderer) { + if (validRenderer) { + return validRenderer.renderer + } else { const options = renderers.map(({ option }) => ` - ${option}`).join('\n') throw new Error(`Could not auto-detect a React renderer. Options are:\n${options}`) - } else { - return validRenderer.renderer } } diff --git a/test/native/cleanup.test.js b/test/native/cleanup.test.js index 479a90c8..d86690b1 100644 --- a/test/native/cleanup.test.js +++ b/test/native/cleanup.test.js @@ -21,7 +21,7 @@ describe('cleanup tests', () => { }) test('should cleanup all rendered hooks', async () => { - let cleanupCalled = [] + const cleanupCalled = [] const hookWithCleanup = (id) => { useEffect(() => { return () => { From e3b9104a22d7f2acff51db8363fb89e4223dc807 Mon Sep 17 00:00:00 2001 From: Josh Ellis Date: Fri, 11 Dec 2020 13:28:25 +0000 Subject: [PATCH 03/53] feat: migrate previous server work --- package.json | 6 + server/index.js | 2 + server/pure.js | 2 + src/server/index.js | 5 + src/server/pure.js | 71 +++++ test/server/asyncHook.test.js | 277 ++++++++++++++++++++ test/server/autoCleanup.disabled.test.js | 28 ++ test/server/autoCleanup.noAfterEach.test.js | 29 ++ test/server/autoCleanup.test.js | 32 +++ test/server/cleanup.test.js | 67 +++++ test/server/customHook.test.js | 33 +++ test/server/errorHook.test.js | 170 ++++++++++++ test/server/suspenseHook.test.js | 49 ++++ test/server/useContext.test.js | 45 ++++ test/server/useEffect.test.js | 38 +++ test/server/useMemo.test.js | 87 ++++++ test/server/useReducer.test.js | 21 ++ test/server/useRef.test.js | 29 ++ test/server/useState.test.js | 39 +++ 19 files changed, 1030 insertions(+) create mode 100644 server/index.js create mode 100644 server/pure.js create mode 100644 src/server/index.js create mode 100644 src/server/pure.js create mode 100644 test/server/asyncHook.test.js create mode 100644 test/server/autoCleanup.disabled.test.js create mode 100644 test/server/autoCleanup.noAfterEach.test.js create mode 100644 test/server/autoCleanup.test.js create mode 100644 test/server/cleanup.test.js create mode 100644 test/server/customHook.test.js create mode 100644 test/server/errorHook.test.js create mode 100644 test/server/suspenseHook.test.js create mode 100644 test/server/useContext.test.js create mode 100644 test/server/useEffect.test.js create mode 100644 test/server/useMemo.test.js create mode 100644 test/server/useReducer.test.js create mode 100644 test/server/useRef.test.js create mode 100644 test/server/useState.test.js diff --git a/package.json b/package.json index f5fa56e6..ebdb709b 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,7 @@ "lib", "src", "native", + "server", "pure.js", "dont-cleanup-after-each.js" ], @@ -49,17 +50,22 @@ "eslint": "^7.15.0", "kcd-scripts": "7.5.1", "react": "17.0.1", + "react-dom": "^17.0.1", "react-test-renderer": "17.0.1", "typescript": "4.1.2" }, "peerDependencies": { "react": ">=16.9.0", + "react-dom": ">=16.9.0", "react-test-renderer": ">=16.9.0" }, "peerDependenciesMeta": { "react": { "optional": true }, + "react-dom": { + "optional": true + }, "react-test-renderer": { "optional": true } diff --git a/server/index.js b/server/index.js new file mode 100644 index 00000000..21b80f69 --- /dev/null +++ b/server/index.js @@ -0,0 +1,2 @@ +// makes it so people can import from '@testing-library/react-hooks/server' +module.exports = require('../lib/server') diff --git a/server/pure.js b/server/pure.js new file mode 100644 index 00000000..a64c1790 --- /dev/null +++ b/server/pure.js @@ -0,0 +1,2 @@ +// makes it so people can import from '@testing-library/react-hooks/server/pure' +module.exports = require('../lib/server/pure') diff --git a/src/server/index.js b/src/server/index.js new file mode 100644 index 00000000..d18e042c --- /dev/null +++ b/src/server/index.js @@ -0,0 +1,5 @@ +import { renderHook, act, cleanup } from './pure' + +cleanup.autoRegister() + +export { renderHook, act, cleanup } diff --git a/src/server/pure.js b/src/server/pure.js new file mode 100644 index 00000000..287040fe --- /dev/null +++ b/src/server/pure.js @@ -0,0 +1,71 @@ +import React from 'react' +import ReactDOMServer from 'react-dom/server' +import ReactDOM from 'react-dom' +import { act as baseAct } from 'react-dom/test-utils' +import { createRenderHook, cleanup } from '../core' + +// eslint-disable-next-line import/no-mutable-exports +let act + +function createRenderer(TestHook, testHookProps, { wrapper: Wrapper }) { + const container = document.createElement('div') + + const toRender = (props) => ( + + + + ) + + let renderProps + let hydrated = false + + act = (...args) => { + if (!hydrated) { + throw new Error('You must hydrate the component before you can act') + } + return baseAct(...args) + } + + return { + render(props) { + renderProps = props + baseAct(() => { + const serverOutput = ReactDOMServer.renderToString(toRender(props)) + container.innerHTML = serverOutput + }) + }, + hydrate() { + if (hydrated) { + throw new Error('The component can only be hydrated once') + } + if (!hydrated) { + document.body.appendChild(container) + baseAct(() => { + ReactDOM.hydrate(toRender(renderProps), container) + }) + hydrated = true + } + }, + rerender(props) { + if (!hydrated) { + throw new Error('You must hydrate the component before you can rerender') + } + baseAct(() => { + ReactDOM.render(toRender(props), container) + }) + }, + unmount() { + if (hydrated) { + baseAct(() => { + ReactDOM.unmountComponentAtNode(container) + document.body.removeChild(container) + }) + } + }, + act + } +} + +const renderHook = createRenderHook(createRenderer) + +export { renderHook, act, cleanup } diff --git a/test/server/asyncHook.test.js b/test/server/asyncHook.test.js new file mode 100644 index 00000000..b42fb4ae --- /dev/null +++ b/test/server/asyncHook.test.js @@ -0,0 +1,277 @@ +import { useState, useRef, useEffect } from 'react' +import { renderHook } from '../../src/server' + +describe('async hook tests', () => { + const useSequence = (...values) => { + const [first, ...otherValues] = values + const [value, setValue] = useState(first) + const index = useRef(0) + + useEffect(() => { + const interval = setInterval(() => { + setValue(otherValues[index.current++]) + if (index.current === otherValues.length) { + clearInterval(interval) + } + }, 50) + return () => { + clearInterval(interval) + } + }, [...values]) + + return value + } + + test('should wait for next update', async () => { + const { result, hydrate, waitForNextUpdate } = renderHook(() => useSequence('first', 'second')) + + expect(result.current).toBe('first') + + hydrate() + + expect(result.current).toBe('first') + + await waitForNextUpdate() + + expect(result.current).toBe('second') + }) + + test('should wait for multiple updates', async () => { + const { result, hydrate, waitForNextUpdate } = renderHook(() => + useSequence('first', 'second', 'third') + ) + + expect(result.current).toBe('first') + + hydrate() + + expect(result.current).toBe('first') + + await waitForNextUpdate() + + expect(result.current).toBe('second') + + await waitForNextUpdate() + + expect(result.current).toBe('third') + }) + + test('should resolve all when updating', async () => { + const { result, hydrate, waitForNextUpdate } = renderHook(() => useSequence('first', 'second')) + + expect(result.current).toBe('first') + + hydrate() + + expect(result.current).toBe('first') + + await Promise.all([waitForNextUpdate(), waitForNextUpdate(), waitForNextUpdate()]) + + expect(result.current).toBe('second') + }) + + test('should reject if timeout exceeded when waiting for next update', async () => { + const { result, hydrate, waitForNextUpdate } = renderHook(() => useSequence('first', 'second')) + + expect(result.current).toBe('first') + + hydrate() + + expect(result.current).toBe('first') + + await expect(waitForNextUpdate({ timeout: 10 })).rejects.toThrow( + Error('Timed out in waitForNextUpdate after 10ms.') + ) + }) + + test('should wait for expectation to pass', async () => { + const { result, hydrate, wait } = renderHook(() => useSequence('first', 'second', 'third')) + + expect(result.current).toBe('first') + + hydrate() + + 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', async () => { + const { result, hydrate, wait } = renderHook(() => useSequence('first', 'second')) + + expect(result.current).toBe('first') + + hydrate() + + 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', async () => { + const { result, hydrate, wait } = renderHook(() => useSequence('first', 'second', 'third')) + + expect(result.current).toBe('first') + + hydrate() + + 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', async () => { + const { result, hydrate, wait } = renderHook(() => useSequence('first', 'second', 'third')) + + expect(result.current).toBe('first') + + hydrate() + + 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', async () => { + const { result, hydrate, wait } = renderHook(() => useSequence('first', 'second', 'third')) + + expect(result.current).toBe('first') + + hydrate() + + 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', async () => { + const { result, hydrate, wait } = renderHook(() => useSequence('first', 'second', 'third')) + + expect(result.current).toBe('first') + + hydrate() + + expect(result.current).toBe('first') + + await expect( + wait( + () => { + expect(result.current).toBe('third') + }, + { timeout: 75 } + ) + ).rejects.toThrow(Error('Timed out in wait after 75ms.')) + }) + + test('should wait for value to change', async () => { + const { result, hydrate, waitForValueToChange } = renderHook(() => + useSequence('first', 'second', 'third') + ) + + expect(result.current).toBe('first') + + hydrate() + + expect(result.current).toBe('first') + + await waitForValueToChange(() => result.current === 'third') + + expect(result.current).toBe('third') + }) + + test('should reject if timeout exceeded when waiting for value to change', async () => { + const { result, hydrate, waitForValueToChange } = renderHook(() => + useSequence('first', 'second', 'third') + ) + + expect(result.current).toBe('first') + + hydrate() + + expect(result.current).toBe('first') + + await expect( + waitForValueToChange(() => result.current === 'third', { + timeout: 75 + }) + ).rejects.toThrow(Error('Timed out in waitForValueToChange after 75ms.')) + }) + + test('should reject if selector throws error', async () => { + const { result, hydrate, waitForValueToChange } = renderHook(() => + useSequence('first', 'second') + ) + + expect(result.current).toBe('first') + + hydrate() + + expect(result.current).toBe('first') + + await expect( + waitForValueToChange(() => { + if (result.current === 'second') { + throw new Error('Something Unexpected') + } + return result.current + }) + ).rejects.toThrow(Error('Something Unexpected')) + }) + + test('should not reject if selector throws error and suppress errors option is enabled', async () => { + const { result, hydrate, waitForValueToChange } = renderHook(() => + useSequence('first', 'second', 'third') + ) + + expect(result.current).toBe('first') + + hydrate() + + expect(result.current).toBe('first') + + await waitForValueToChange( + () => { + if (result.current === 'second') { + throw new Error('Something Unexpected') + } + return result.current === 'third' + }, + { suppressErrors: true } + ) + + expect(result.current).toBe('third') + }) +}) diff --git a/test/server/autoCleanup.disabled.test.js b/test/server/autoCleanup.disabled.test.js new file mode 100644 index 00000000..0960f1c1 --- /dev/null +++ b/test/server/autoCleanup.disabled.test.js @@ -0,0 +1,28 @@ +import { useEffect } from 'react' + +// This verifies that if RHTL_SKIP_AUTO_CLEANUP is set +// then we DON'T auto-wire up the afterEach for folks +describe('skip auto cleanup (disabled) tests', () => { + let cleanupCalled = false + let renderHook + + beforeAll(() => { + process.env.RHTL_SKIP_AUTO_CLEANUP = 'true' + renderHook = require('../../src/server').renderHook + }) + + test('first', () => { + const hookWithCleanup = () => { + useEffect(() => { + return () => { + cleanupCalled = true + } + }) + } + renderHook(() => hookWithCleanup()) + }) + + test('second', () => { + expect(cleanupCalled).toBe(false) + }) +}) diff --git a/test/server/autoCleanup.noAfterEach.test.js b/test/server/autoCleanup.noAfterEach.test.js new file mode 100644 index 00000000..141d4ef1 --- /dev/null +++ b/test/server/autoCleanup.noAfterEach.test.js @@ -0,0 +1,29 @@ +import { useEffect } from 'react' + +// This verifies that if RHTL_SKIP_AUTO_CLEANUP is set +// then we DON'T auto-wire up the afterEach for folks +describe('skip auto cleanup (no afterEach) tests', () => { + let cleanupCalled = false + let renderHook + + beforeAll(() => { + // eslint-disable-next-line no-global-assign + afterEach = false + renderHook = require('../../src/server').renderHook + }) + + test('first', () => { + const hookWithCleanup = () => { + useEffect(() => { + return () => { + cleanupCalled = true + } + }) + } + renderHook(() => hookWithCleanup()) + }) + + test('second', () => { + expect(cleanupCalled).toBe(false) + }) +}) diff --git a/test/server/autoCleanup.test.js b/test/server/autoCleanup.test.js new file mode 100644 index 00000000..586b4c66 --- /dev/null +++ b/test/server/autoCleanup.test.js @@ -0,0 +1,32 @@ +import { useEffect } from 'react' +import { renderHook } from '../../src/server' + +// This verifies that by importing RHTL in an +// environment which supports afterEach (like Jest) +// we'll get automatic cleanup between tests. +describe('auto cleanup tests', () => { + const cleanups = { + ssr: false, + hydrated: false + } + + test('first', () => { + const hookWithCleanup = (name) => { + useEffect(() => { + return () => { + cleanups[name] = true + } + }) + } + + renderHook(() => hookWithCleanup('ssr')) + + const { hydrate } = renderHook(() => hookWithCleanup('hydrated')) + hydrate() + }) + + test('second', () => { + expect(cleanups.ssr).toBe(false) + expect(cleanups.hydrated).toBe(true) + }) +}) diff --git a/test/server/cleanup.test.js b/test/server/cleanup.test.js new file mode 100644 index 00000000..1b391809 --- /dev/null +++ b/test/server/cleanup.test.js @@ -0,0 +1,67 @@ +import { useEffect } from 'react' +import { renderHook, cleanup } from '../../src/server' + +describe('cleanup tests', () => { + test('should flush effects on cleanup', async () => { + let cleanupCalled = false + + const hookWithCleanup = () => { + useEffect(() => { + return () => { + cleanupCalled = true + } + }) + } + + const { hydrate } = renderHook(() => hookWithCleanup()) + + hydrate() + + await cleanup() + + expect(cleanupCalled).toBe(true) + }) + + test('should cleanup all rendered hooks', async () => { + let cleanupCalled = [false, false] + const hookWithCleanup = (id) => { + useEffect(() => { + return () => { + cleanupCalled = cleanupCalled.map((_, i) => (i === id ? true : _)) + } + }) + } + + const { hydrate: hydrate1 } = renderHook(() => hookWithCleanup(0)) + const { hydrate: hydrate2 } = renderHook(() => hookWithCleanup(1)) + + hydrate1() + hydrate2() + + await cleanup() + + expect(cleanupCalled[0]).toBe(true) + expect(cleanupCalled[1]).toBe(true) + }) + + test('should only cleanup hydrated hooks', async () => { + let cleanupCalled = [false, false] + const hookWithCleanup = (id) => { + useEffect(() => { + return () => { + cleanupCalled = cleanupCalled.map((_, i) => (i === id ? true : _)) + } + }) + } + + renderHook(() => hookWithCleanup(0)) + const { hydrate } = renderHook(() => hookWithCleanup(1)) + + hydrate() + + await cleanup() + + expect(cleanupCalled[0]).toBe(false) + expect(cleanupCalled[1]).toBe(true) + }) +}) diff --git a/test/server/customHook.test.js b/test/server/customHook.test.js new file mode 100644 index 00000000..2fadd2d6 --- /dev/null +++ b/test/server/customHook.test.js @@ -0,0 +1,33 @@ +import { useState, useCallback } from 'react' +import { renderHook, act } from '../../src/server' + +describe('custom hook tests', () => { + function useCounter() { + const [count, setCount] = useState(0) + + const increment = useCallback(() => setCount(count + 1), [count]) + const decrement = useCallback(() => setCount(count - 1), [count]) + + return { count, increment, decrement } + } + + test('should increment counter', () => { + const { result, hydrate } = renderHook(() => useCounter()) + + hydrate() + + act(() => result.current.increment()) + + expect(result.current.count).toBe(1) + }) + + test('should decrement counter', () => { + const { result, hydrate } = renderHook(() => useCounter()) + + hydrate() + + act(() => result.current.decrement()) + + expect(result.current.count).toBe(-1) + }) +}) diff --git a/test/server/errorHook.test.js b/test/server/errorHook.test.js new file mode 100644 index 00000000..5eb91b89 --- /dev/null +++ b/test/server/errorHook.test.js @@ -0,0 +1,170 @@ +import { useState, useEffect } from 'react' +import { renderHook } from '../../src/server' + +describe('error hook tests', () => { + function useError(throwError) { + if (throwError) { + throw new Error('expected') + } + return true + } + + function useAsyncError(throwError) { + const [value, setValue] = useState() + useEffect(() => { + const timeout = setTimeout(() => setValue(throwError), 100) + return () => clearTimeout(timeout) + }, [throwError]) + return useError(value) + } + + function useEffectError(throwError) { + useEffect(() => { + useError(throwError) + }, []) + return true + } + + describe('synchronous', () => { + test('should raise error', () => { + const { result } = renderHook(() => useError(true)) + + expect(() => { + expect(result.current).not.toBe(undefined) + }).toThrow(Error('expected')) + }) + + test('should capture error', () => { + const { result } = renderHook(() => useError(true)) + + expect(result.error).toEqual(Error('expected')) + }) + + test('should not capture error', () => { + const { result } = renderHook(() => useError(false)) + + expect(result.current).not.toBe(undefined) + expect(result.error).toBe(undefined) + }) + + test('should reset error', () => { + const { result, hydrate, rerender } = renderHook(({ throwError }) => useError(throwError), { + initialProps: { throwError: true } + }) + + expect(result.error).not.toBe(undefined) + + hydrate() + + rerender({ throwError: false }) + + expect(result.current).not.toBe(undefined) + expect(result.error).toBe(undefined) + }) + }) + + describe('asynchronous', () => { + test('should raise async error', async () => { + const { result, hydrate, waitForNextUpdate } = renderHook(() => useAsyncError(true)) + + hydrate() + + await waitForNextUpdate() + + expect(() => { + expect(result.current).not.toBe(undefined) + }).toThrow(Error('expected')) + }) + + test('should capture async error', async () => { + const { result, hydrate, waitForNextUpdate } = renderHook(() => useAsyncError(true)) + + hydrate() + + await waitForNextUpdate() + + expect(result.error).toEqual(Error('expected')) + }) + + test('should not capture async error', async () => { + const { result, hydrate, waitForNextUpdate } = renderHook(() => useAsyncError(false)) + + hydrate() + + await waitForNextUpdate() + + expect(result.current).not.toBe(undefined) + expect(result.error).toBe(undefined) + }) + + test('should reset async error', async () => { + const { result, hydrate, waitForNextUpdate, rerender } = renderHook( + ({ throwError }) => useAsyncError(throwError), + { initialProps: { throwError: true } } + ) + + hydrate() + + await waitForNextUpdate() + + expect(result.error).not.toBe(undefined) + + rerender({ throwError: false }) + + await waitForNextUpdate() + + expect(result.current).not.toBe(undefined) + expect(result.error).toBe(undefined) + }) + }) + + /* + These tests capture error cases that are not currently being caught successfully. + Refer to https://github.com/testing-library/react-hooks-testing-library/issues/308 + for more details. + */ + describe.skip('effect', () => { + test('should raise effect error', () => { + const { result, hydrate } = renderHook(() => useEffectError(true)) + + hydrate() + + expect(() => { + expect(result.current).not.toBe(undefined) + }).toThrow(Error('expected')) + }) + + test('should capture effect error', () => { + const { result, hydrate } = renderHook(() => useEffectError(true)) + + hydrate() + + expect(result.error).toEqual(Error('expected')) + }) + + test('should not capture effect error', () => { + const { result, hydrate } = renderHook(() => useEffectError(false)) + + hydrate() + + expect(result.current).not.toBe(undefined) + expect(result.error).toBe(undefined) + }) + + test('should reset effect error', () => { + const { result, hydrate, rerender } = renderHook( + ({ throwError }) => useEffectError(throwError), + { initialProps: { throwError: true } } + ) + + hydrate() + + expect(result.error).not.toBe(undefined) + + rerender({ throwError: false }) + + expect(result.current).not.toBe(undefined) + expect(result.error).toBe(undefined) + }) + }) +}) diff --git a/test/server/suspenseHook.test.js b/test/server/suspenseHook.test.js new file mode 100644 index 00000000..2ff34647 --- /dev/null +++ b/test/server/suspenseHook.test.js @@ -0,0 +1,49 @@ +import { renderHook } from '../../src/server' + +describe('suspense hook tests', () => { + const cache = {} + const fetchName = (isSuccessful) => { + if (!cache.value) { + cache.value = new Promise((resolve, reject) => { + setTimeout(() => { + if (isSuccessful) { + resolve('Bob') + } else { + reject(new Error('Failed to fetch name')) + } + }, 50) + }) + .then((value) => (cache.value = value)) + .catch((e) => (cache.value = e)) + } + return cache.value + } + + const useFetchName = (isSuccessful = true) => { + const name = fetchName(isSuccessful) + if (typeof name.then === 'function' || name instanceof Error) { + throw name + } + return name + } + + beforeEach(() => { + delete cache.value + }) + + test('should allow rendering to be suspended', async () => { + const { result, waitForNextUpdate } = renderHook(() => useFetchName(true)) + + await waitForNextUpdate() + + expect(result.current).toBe('Bob') + }) + + test('should set error if suspense promise rejects', async () => { + const { result, waitForNextUpdate } = renderHook(() => useFetchName(false)) + + await waitForNextUpdate() + + expect(result.error).toEqual(new Error('Failed to fetch name')) + }) +}) diff --git a/test/server/useContext.test.js b/test/server/useContext.test.js new file mode 100644 index 00000000..28e81b82 --- /dev/null +++ b/test/server/useContext.test.js @@ -0,0 +1,45 @@ +import React, { createContext, useContext } from 'react' +import { renderHook } from '../../src/server' + +describe('useContext tests', () => { + test('should get default value from context', () => { + const TestContext = createContext('foo') + + const { result } = renderHook(() => useContext(TestContext)) + + const value = result.current + + expect(value).toBe('foo') + }) + + test('should get value from context provider', () => { + const TestContext = createContext('foo') + + const wrapper = ({ children }) => ( + {children} + ) + + const { result } = renderHook(() => useContext(TestContext), { wrapper }) + + expect(result.current).toBe('bar') + }) + + test('should update value in context when props are updated', () => { + const TestContext = createContext('foo') + + const wrapper = ({ contextValue, children }) => ( + {children} + ) + + const { result, hydrate, rerender } = renderHook(() => useContext(TestContext), { + wrapper, + initialProps: { contextValue: 'bar' } + }) + + hydrate() + + rerender({ contextValue: 'baz' }) + + expect(result.current).toBe('baz') + }) +}) diff --git a/test/server/useEffect.test.js b/test/server/useEffect.test.js new file mode 100644 index 00000000..35d56e42 --- /dev/null +++ b/test/server/useEffect.test.js @@ -0,0 +1,38 @@ +import { useEffect } from 'react' +import { renderHook } from '../../src/server' + +describe('useEffect tests', () => { + test('should handle useEffect hook', () => { + const sideEffect = { 1: false, 2: false } + + const { hydrate, rerender, unmount } = renderHook( + ({ id }) => { + useEffect(() => { + sideEffect[id] = true + return () => { + sideEffect[id] = false + } + }, [id]) + }, + { initialProps: { id: 1 } } + ) + + expect(sideEffect[1]).toBe(false) + expect(sideEffect[2]).toBe(false) + + hydrate() + + expect(sideEffect[1]).toBe(true) + expect(sideEffect[2]).toBe(false) + + rerender({ id: 2 }) + + expect(sideEffect[1]).toBe(false) + expect(sideEffect[2]).toBe(true) + + unmount() + + expect(sideEffect[1]).toBe(false) + expect(sideEffect[2]).toBe(false) + }) +}) diff --git a/test/server/useMemo.test.js b/test/server/useMemo.test.js new file mode 100644 index 00000000..202db24c --- /dev/null +++ b/test/server/useMemo.test.js @@ -0,0 +1,87 @@ +import { useMemo, useCallback } from 'react' +import { renderHook } from '../../src/server' + +describe('useCallback tests', () => { + test('should handle useMemo hook', () => { + const { result, hydrate, rerender } = renderHook( + ({ value }) => useMemo(() => ({ value }), [value]), + { + initialProps: { value: 1 } + } + ) + + const value1 = result.current + + expect(value1).toEqual({ value: 1 }) + + hydrate() + + const value2 = result.current + + expect(value2).toEqual({ value: 1 }) + + expect(value2).not.toBe(value1) + + rerender() + + const value3 = result.current + + expect(value3).toEqual({ value: 1 }) + + expect(value3).toBe(value2) + + rerender({ value: 2 }) + + const value4 = result.current + + expect(value4).toEqual({ value: 2 }) + + expect(value4).not.toBe(value2) + }) + + test('should handle useCallback hook', () => { + const { result, hydrate, rerender } = renderHook( + ({ value }) => { + const callback = () => ({ value }) + return useCallback(callback, [value]) + }, + { initialProps: { value: 1 } } + ) + + const callback1 = result.current + + const calbackValue1 = callback1() + + expect(calbackValue1).toEqual({ value: 1 }) + + hydrate() + + const callback2 = result.current + + const calbackValue2 = callback2() + + expect(calbackValue2).toEqual({ value: 1 }) + + expect(callback2).not.toBe(callback1) + + rerender() + + const callback3 = result.current + + const calbackValue3 = callback3() + + expect(calbackValue3).toEqual({ value: 1 }) + + expect(callback3).toBe(callback2) + + rerender({ value: 2 }) + + const callback4 = result.current + + const calbackValue4 = callback4() + + expect(calbackValue4).toEqual({ value: 2 }) + + expect(callback4).not.toBe(callback2) + }) +}) diff --git a/test/server/useReducer.test.js b/test/server/useReducer.test.js new file mode 100644 index 00000000..2fb30105 --- /dev/null +++ b/test/server/useReducer.test.js @@ -0,0 +1,21 @@ +import { useReducer } from 'react' +import { renderHook, act } from '../../src/server' + +describe('useReducer tests', () => { + test('should handle useReducer hook', () => { + const reducer = (state, action) => (action.type === 'inc' ? state + 1 : state) + + const { result, hydrate } = renderHook(() => { + const [state, dispatch] = useReducer(reducer, 0) + return { state, dispatch } + }) + + hydrate() + + expect(result.current.state).toBe(0) + + act(() => result.current.dispatch({ type: 'inc' })) + + expect(result.current.state).toBe(1) + }) +}) diff --git a/test/server/useRef.test.js b/test/server/useRef.test.js new file mode 100644 index 00000000..065cd989 --- /dev/null +++ b/test/server/useRef.test.js @@ -0,0 +1,29 @@ +import { useRef, useImperativeHandle } from 'react' +import { renderHook } from '../../src/server' + +describe('useHook tests', () => { + test('should handle useRef hook', () => { + const { result } = renderHook(() => useRef('foo')) + + const refContainer = result.current + + expect(Object.keys(refContainer)).toEqual(['current']) + expect(refContainer.current).toBe('foo') + }) + + test('should handle useImperativeHandle hook', () => { + const { result, hydrate } = renderHook(() => { + const ref = useRef() + useImperativeHandle(ref, () => ({ + fakeImperativeMethod: () => true + })) + return ref + }) + + expect(result.current.current).toBeUndefined() + + hydrate() + + expect(result.current.current.fakeImperativeMethod()).toBe(true) + }) +}) diff --git a/test/server/useState.test.js b/test/server/useState.test.js new file mode 100644 index 00000000..b3546357 --- /dev/null +++ b/test/server/useState.test.js @@ -0,0 +1,39 @@ +import { useState } from 'react' +import { renderHook, act } from '../../src/server' + +describe('useState tests', () => { + test('should use state value', () => { + const { result } = renderHook(() => { + const [value, setValue] = useState('foo') + return { value, setValue } + }) + + expect(result.current.value).toBe('foo') + }) + + test('should retain state value after hydration', () => { + const { result, hydrate } = renderHook(() => { + const [value, setValue] = useState('foo') + return { value, setValue } + }) + + hydrate() + + expect(result.current.value).toBe('foo') + }) + + test('should update state value using setter', () => { + const { result, hydrate } = renderHook(() => { + const [value, setValue] = useState('foo') + return { value, setValue } + }) + + hydrate() + + act(() => { + result.current.setValue('bar') + }) + + expect(result.current.value).toBe('bar') + }) +}) From 2cd41cbd5f042c4c438c208cb63b1076dbd72ff4 Mon Sep 17 00:00:00 2001 From: Josh Ellis Date: Thu, 31 Dec 2020 17:58:33 +0000 Subject: [PATCH 04/53] feat: add global type file --- .gitignore | 3 +- src/types.ts | 103 +++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 105 insertions(+), 1 deletion(-) create mode 100644 src/types.ts diff --git a/.gitignore b/.gitignore index 032db993..a0a75f27 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,5 @@ node_modules coverage lib .docz -site \ No newline at end of file +site +.vscode \ No newline at end of file diff --git a/src/types.ts b/src/types.ts new file mode 100644 index 00000000..38eec2a8 --- /dev/null +++ b/src/types.ts @@ -0,0 +1,103 @@ +import { act as RTRAct } from 'react-test-renderer' +import { act as RDAct } from 'react-dom/test-utils' + +/** + * + * Shared + * + */ + +export type ActTypes = typeof RTRAct | typeof RDAct + +export interface WaitOptions { + interval?: number + timeout?: number + suppressErrors?: boolean +} + +export type WrapperComponent = React.ComponentType + +/** + * + * pure + * + */ + +export interface ReactHooksRenderer { + renderHook: () => RenderHookReturn + act: ActTypes + cleanup: { + autoRegister: () => void + } +} + +export type RenderingEngineArray = Array<{ required: string; renderer: string }> + +/** + * + * core/asyncUtils + * + */ + +export type AsyncUtilsReturn = { + waitFor: ( + callback: () => boolean | void, + { interval, timeout, suppressErrors }?: WaitOptions + ) => Promise + waitForNextUpdate: ({ timeout }?: Pick) => Promise + waitForValueToChange: (selector: () => unknown, options?: WaitOptions) => Promise +} + +/** + * + * core/index + * + */ + +export type RenderResult = { + readonly all: unknown[] + readonly current: unknown + readonly error: Error | undefined +} + +export type ResultContainerReturn = { + result: RenderResult + addResolver: (resolver: () => void) => void + setValue: (val: TValue) => void + setError: (error: Error) => void +} + +export interface RenderHookOptions { + initialProps?: TProps + wrapper?: WrapperComponent +} + +export type RenderHookReturn = { + result: RenderResult + rerender: (props?: TProps) => void + unmount: () => void +} & AsyncUtilsReturn + +/** + * + * native/pure + * + */ + +export type NativeRendererReturn = { + render: (props?: TProps) => void + rerender: (props?: TProps) => void + unmount: () => void + act: typeof RTRAct +} + +export type NativeRendererOptions = { + wrapper: WrapperComponent +} + +export type TestHookProps = { + hookProps: TProps | undefined + callback: (props: TProps) => TResult + setError: (error: Error) => void + setValue: (value: TResult) => void +} From d34245a7f6ffc77b46a7463f6d433d9fff4d1571 Mon Sep 17 00:00:00 2001 From: Josh Ellis Date: Thu, 31 Dec 2020 17:58:53 +0000 Subject: [PATCH 05/53] change: seperate helper funcs --- src/helpers/error.ts | 7 +++++++ src/helpers/promises.ts | 9 +++++++++ 2 files changed, 16 insertions(+) create mode 100644 src/helpers/error.ts create mode 100644 src/helpers/promises.ts diff --git a/src/helpers/error.ts b/src/helpers/error.ts new file mode 100644 index 00000000..5aba68d7 --- /dev/null +++ b/src/helpers/error.ts @@ -0,0 +1,7 @@ +class TimeoutError extends Error { + constructor(util: Function, timeout: number) { + super(`Timed out in ${util.name} after ${timeout}ms.`) + } +} + +export { TimeoutError } diff --git a/src/helpers/promises.ts b/src/helpers/promises.ts new file mode 100644 index 00000000..d7dec9bd --- /dev/null +++ b/src/helpers/promises.ts @@ -0,0 +1,9 @@ +const resolveAfter = (ms: number) => + new Promise((resolve) => { + setTimeout(resolve, ms) + }) + +const isPromise = (value: unknown): boolean => + typeof (value as PromiseLike).then === 'function' + +export { isPromise, resolveAfter } From 85b9fa2a73d0000302f22645ff81a876313e5360 Mon Sep 17 00:00:00 2001 From: Josh Ellis Date: Thu, 31 Dec 2020 17:59:58 +0000 Subject: [PATCH 06/53] feat: add types to createRenderer & createRenderHook --- src/core/asyncUtils.ts | 23 ++------- src/core/cleanup.ts | 6 +-- src/core/index.tsx | 113 +++++++++++++++-------------------------- src/core/testHook.ts | 22 ++++++++ src/native/pure.tsx | 28 +++++----- src/pure.ts | 17 +++---- test/tsconfig.json | 8 +++ tsconfig.json | 7 ++- 8 files changed, 108 insertions(+), 116 deletions(-) create mode 100644 src/core/testHook.ts create mode 100644 test/tsconfig.json diff --git a/src/core/asyncUtils.ts b/src/core/asyncUtils.ts index 2e6e81c8..0904f54c 100644 --- a/src/core/asyncUtils.ts +++ b/src/core/asyncUtils.ts @@ -1,24 +1,9 @@ -import { act } from 'react-test-renderer' +import { ActTypes, WaitOptions, AsyncUtilsReturn } from 'types' -export interface WaitOptions { - interval?: number - timeout?: number - suppressErrors?: boolean -} - -class TimeoutError extends Error { - constructor(util: Function, timeout: number) { - super(`Timed out in ${util.name} after ${timeout}ms.`) - } -} - -function resolveAfter (ms: number) { - return new Promise((resolve) => { - setTimeout(resolve, ms) - }) -} +import { resolveAfter } from 'helpers/promises' +import { TimeoutError } from 'helpers/error' -function asyncUtils (addResolver: (callback: () => void) => void) { +function asyncUtils(act: ActTypes, addResolver: (callback: () => void) => void): AsyncUtilsReturn { let nextUpdatePromise: Promise | null = null const waitForNextUpdate = async ({ timeout }: Pick = {}) => { diff --git a/src/core/cleanup.ts b/src/core/cleanup.ts index 2d133c9c..a74397c0 100644 --- a/src/core/cleanup.ts +++ b/src/core/cleanup.ts @@ -1,18 +1,18 @@ let cleanupCallbacks: (() => Promise | void)[] = [] -async function cleanup () { +async function cleanup() { for (const callback of cleanupCallbacks) { await callback() } cleanupCallbacks = [] } -function addCleanup (callback: () => Promise | void) { +function addCleanup(callback: () => Promise | void) { cleanupCallbacks = [callback, ...cleanupCallbacks] return () => removeCleanup(callback) } -function removeCleanup (callback: () => Promise | void) { +function removeCleanup(callback: () => Promise | void) { cleanupCallbacks = cleanupCallbacks.filter((cb) => cb !== callback) } diff --git a/src/core/index.tsx b/src/core/index.tsx index 28539964..82192087 100644 --- a/src/core/index.tsx +++ b/src/core/index.tsx @@ -1,58 +1,34 @@ -import React, { ReactElement, ReactNode, Suspense } from 'react' -import { act, create, ReactTestRenderer } from 'react-test-renderer' +import React from 'react' + +import { + TestHookProps, + NativeRendererOptions, + NativeRendererReturn, + ResultContainerReturn, + RenderHookOptions, + RenderHookReturn, + RenderResult +} from 'types' + 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) { - if (isPromise(err)) { - throw err - } else { - onError(err as Error) - } - } - return null -} - -function Fallback () { - return null -} - -function resultContainer () { +function resultContainer(): ResultContainerReturn { const results: Array<{ value?: TValue; error?: Error }> = [] const resolvers: Array<() => void> = [] - const result = { - get all () { + const result: RenderResult = { + get all() { return results.map(({ value, error }) => error ?? value) }, - get current () { + get current() { const { value, error } = results[results.length - 1] if (error) { throw error } return value as TValue }, - get error () { + get error() { const { error } = results[results.length - 1] return error } @@ -73,42 +49,37 @@ function resultContainer () { } } -function renderHook ( +// typed this way in relation to this https://github.com/DefinitelyTyped/DefinitelyTyped/issues/44572#issuecomment-625878049 +function defaultWrapper({ children }: { children?: React.ReactNode }) { + return (children as unknown) as JSX.Element +} + +const createRenderHook = ( + createRenderer: ( + testProps: Omit, 'hookProps'>, + opts: NativeRendererOptions + ) => NativeRendererReturn +) => ( callback: (props: TProps) => TResult, - { initialProps, wrapper }: { initialProps?: TProps; wrapper?: React.ComponentType } = {} -) { - const { result, setValue, setError, addResolver } = resultContainer() + { initialProps, wrapper = defaultWrapper }: RenderHookOptions = {} +): RenderHookReturn => { + const { result, setValue, setError, addResolver } = resultContainer() const hookProps = { current: initialProps } + const props = { callback, setValue, setError } + const options = { wrapper } - const wrapUiIfNeeded = (innerElement: ReactNode) => - wrapper ? React.createElement(wrapper, hookProps.current, innerElement) : innerElement - - const toRender = () => - wrapUiIfNeeded( - }> - - {setValue} - - - ) as ReactElement + const { render, rerender, unmount, act } = createRenderer(props, options) - let testRenderer: ReactTestRenderer - act(() => { - testRenderer = create(toRender()) - }) + render(hookProps.current) - function rerenderHook (newProps: typeof initialProps = hookProps.current) { + function rerenderHook(newProps = hookProps.current) { hookProps.current = newProps - act(() => { - testRenderer.update(toRender()) - }) + rerender(hookProps.current) } - function unmountHook () { - act(() => { - removeCleanup(unmountHook) - testRenderer.unmount() - }) + function unmountHook() { + removeCleanup(unmountHook) + unmount() } addCleanup(unmountHook) @@ -117,8 +88,8 @@ function renderHook ( result, rerender: rerenderHook, unmount: unmountHook, - ...asyncUtils(addResolver) + ...asyncUtils(act, addResolver) } } -export { renderHook, cleanup, addCleanup, removeCleanup, act } +export { createRenderHook, cleanup, addCleanup, removeCleanup } diff --git a/src/core/testHook.ts b/src/core/testHook.ts new file mode 100644 index 00000000..08933f97 --- /dev/null +++ b/src/core/testHook.ts @@ -0,0 +1,22 @@ +import { isPromise } from 'helpers/promises' + +import { TestHookProps } from 'types' + +export default function TestHook({ + hookProps, + callback, + setError, + setValue +}: TestHookProps) { + try { + // coerce undefined into TProps, so it maintains the previous behaviour + setValue(callback(hookProps as TProps)) + } catch (err: unknown) { + if (isPromise(err)) { + throw err + } else { + setError(err as Error) + } + } + return null +} diff --git a/src/native/pure.tsx b/src/native/pure.tsx index 325e8ae2..d2d88639 100644 --- a/src/native/pure.tsx +++ b/src/native/pure.tsx @@ -1,28 +1,32 @@ import React, { Suspense } from 'react' -import { act, create } from 'react-test-renderer' -import { createRenderHook, cleanup } from '../core' +import { act, create, ReactTestRenderer } from 'react-test-renderer' -function FallbackComponent() { - return null -} -function createRenderer(TestHook, testHookProps, { wrapper: Wrapper }) { - let container +import { TestHookProps, NativeRendererOptions, NativeRendererReturn } from 'types' + +import { createRenderHook, cleanup, addCleanup, removeCleanup } from 'core/index' +import TestHook from 'core/testHook' + +function createRenderer( + testHookProps: Omit, 'hookProps'>, + { wrapper: Wrapper }: NativeRendererOptions +): NativeRendererReturn { + let container: ReactTestRenderer - const toRender = (props) => ( + const toRender = (props?: TProps): JSX.Element => ( - + ) return { render(props) { act(() => { - container = create(}>{toRender(props)}) + container = create({toRender(props)}) }) }, rerender(props) { act(() => { - container.update(}>{toRender(props)}) + container.update({toRender(props)}) }) }, unmount() { @@ -36,4 +40,4 @@ function createRenderer(TestHook, testHookProps, { wrapper: Wrapper }) { const renderHook = createRenderHook(createRenderer) -export { renderHook, act, cleanup } +export { renderHook, act, cleanup, addCleanup, removeCleanup } diff --git a/src/pure.ts b/src/pure.ts index cd64fb3d..b385f71b 100644 --- a/src/pure.ts +++ b/src/pure.ts @@ -1,8 +1,10 @@ -type RendererArray = Array<{ required: string; renderer: string }> +import { RenderingEngineArray, ReactHooksRenderer } from 'types' -const RENDERERS: RendererArray = [{ required: 'react-test-renderer', renderer: './native/pure' }] +const RENDERERS: RenderingEngineArray = [ + { required: 'react-test-renderer', renderer: './native/pure' } +] -function getRenderer (renderers: RendererArray): string { +function getRenderer(renderers: RenderingEngineArray): string { const hasDependency = (name: string) => { try { require(name) @@ -22,12 +24,7 @@ function getRenderer (renderers: RendererArray): string { } } -interface Renderer { - renderHook: () => void - act: () => void - cleanup: () => void -} - -const { renderHook, act, cleanup } = require(getRenderer(RENDERERS)) as Renderer +// eslint-disable-next-line @typescript-eslint/no-var-requires +const { renderHook, act, cleanup } = require(getRenderer(RENDERERS)) as ReactHooksRenderer export { renderHook, act, cleanup } diff --git a/test/tsconfig.json b/test/tsconfig.json new file mode 100644 index 00000000..7e62d921 --- /dev/null +++ b/test/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "./../tsconfig", + "compilerOptions": { + "declaration": false + }, + "exclude": [], + "include": ["./native/*"] +} diff --git a/tsconfig.json b/tsconfig.json index 1337ac30..c0fb6557 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -4,5 +4,10 @@ "allowJs": true, "target": "ES6" }, - "exclude": ["./test"] + "exclude": ["./test"], + "paths": { + "core/*": ["src/core/*"], + "helpers/*": ["src/helpers/*"], + "types": ["src/types.ts"], + } } From add0639f92aa72853f3ec20b6545f204f3289d9f Mon Sep 17 00:00:00 2001 From: Josh Ellis Date: Thu, 31 Dec 2020 18:00:17 +0000 Subject: [PATCH 07/53] tests: fix tests with type assertions etc. --- test/native/asyncHook.ts | 26 +++++++++++++------------- test/native/autoCleanup.disabled.ts | 5 ++++- test/native/autoCleanup.noAfterEach.ts | 5 ++++- test/native/cleanup.ts | 4 +--- test/native/resultHistory.ts | 2 +- test/native/tsconfig.json | 8 -------- 6 files changed, 23 insertions(+), 27 deletions(-) delete mode 100644 test/native/tsconfig.json diff --git a/test/native/asyncHook.ts b/test/native/asyncHook.ts index 16bc8bc2..94f2cc1c 100644 --- a/test/native/asyncHook.ts +++ b/test/native/asyncHook.ts @@ -67,12 +67,12 @@ describe('async hook tests', () => { }) test('should wait for expectation to pass', async () => { - const { result, wait } = renderHook(() => useSequence('first', 'second', 'third')) + const { result, waitFor } = renderHook(() => useSequence('first', 'second', 'third')) expect(result.current).toBe('first') let complete = false - await wait(() => { + await waitFor(() => { expect(result.current).toBe('third') complete = true }) @@ -80,12 +80,12 @@ describe('async hook tests', () => { }) test('should not hang if expectation is already passing', async () => { - const { result, wait } = renderHook(() => useSequence('first', 'second')) + const { result, waitFor } = renderHook(() => useSequence('first', 'second')) expect(result.current).toBe('first') let complete = false - await wait(() => { + await waitFor(() => { expect(result.current).toBe('first') complete = true }) @@ -93,12 +93,12 @@ describe('async hook tests', () => { }) test('should reject if callback throws error', async () => { - const { result, wait } = renderHook(() => useSequence('first', 'second', 'third')) + const { result, waitFor } = renderHook(() => useSequence('first', 'second', 'third')) expect(result.current).toBe('first') await expect( - wait( + waitFor( () => { if (result.current === 'second') { throw new Error('Something Unexpected') @@ -113,12 +113,12 @@ describe('async hook tests', () => { }) test('should reject if callback immediately throws error', async () => { - const { result, wait } = renderHook(() => useSequence('first', 'second', 'third')) + const { result, waitFor } = renderHook(() => useSequence('first', 'second', 'third')) expect(result.current).toBe('first') await expect( - wait( + waitFor( () => { throw new Error('Something Unexpected') }, @@ -130,28 +130,28 @@ describe('async hook tests', () => { }) test('should wait for truthy value', async () => { - const { result, wait } = renderHook(() => useSequence('first', 'second', 'third')) + const { result, waitFor } = renderHook(() => useSequence('first', 'second', 'third')) expect(result.current).toBe('first') - await wait(() => result.current === 'third') + await waitFor(() => result.current === 'third') expect(result.current).toBe('third') }) test('should reject if timeout exceeded when waiting for expectation to pass', async () => { - const { result, wait } = renderHook(() => useSequence('first', 'second', 'third')) + const { result, waitFor } = renderHook(() => useSequence('first', 'second', 'third')) expect(result.current).toBe('first') await expect( - wait( + waitFor( () => { expect(result.current).toBe('third') }, { timeout: 75 } ) - ).rejects.toThrow(Error('Timed out in wait after 75ms.')) + ).rejects.toThrow(Error('Timed out in waitFor after 75ms.')) }) test('should wait for value to change', async () => { diff --git a/test/native/autoCleanup.disabled.ts b/test/native/autoCleanup.disabled.ts index 0faf4dad..b43794d5 100644 --- a/test/native/autoCleanup.disabled.ts +++ b/test/native/autoCleanup.disabled.ts @@ -1,5 +1,7 @@ import { useEffect } from 'react' +import { ReactHooksRenderer } from 'types' + // This verifies that if RHTL_SKIP_AUTO_CLEANUP is set // then we DON'T auto-wire up the afterEach for folks describe('skip auto cleanup (disabled) tests', () => { @@ -8,7 +10,8 @@ describe('skip auto cleanup (disabled) tests', () => { beforeAll(() => { process.env.RHTL_SKIP_AUTO_CLEANUP = 'true' - renderHook = require('../../src/native').renderHook + // eslint-disable-next-line @typescript-eslint/no-var-requires + renderHook = (require('../../src/native') as ReactHooksRenderer).renderHook }) test('first', () => { diff --git a/test/native/autoCleanup.noAfterEach.ts b/test/native/autoCleanup.noAfterEach.ts index 2a04388e..49b00b3d 100644 --- a/test/native/autoCleanup.noAfterEach.ts +++ b/test/native/autoCleanup.noAfterEach.ts @@ -1,5 +1,7 @@ import { useEffect } from 'react' +import { ReactHooksRenderer } from 'types' + // This verifies that if RHTL_SKIP_AUTO_CLEANUP is set // then we DON'T auto-wire up the afterEach for folks describe('skip auto cleanup (no afterEach) tests', () => { @@ -10,7 +12,8 @@ describe('skip auto cleanup (no afterEach) tests', () => { // @ts-expect-error Turning off AfterEach -- ignore Jest LifeCycle Type // eslint-disable-next-line no-global-assign afterEach = false - renderHook = require('../../src/native').renderHook + // eslint-disable-next-line @typescript-eslint/no-var-requires + renderHook = (require('../../src/native') as ReactHooksRenderer).renderHook }) test('first', () => { diff --git a/test/native/cleanup.ts b/test/native/cleanup.ts index 4fd92df1..9eeed775 100644 --- a/test/native/cleanup.ts +++ b/test/native/cleanup.ts @@ -1,7 +1,5 @@ import { useEffect } from 'react' -import PURE from '../../src/pure' - -const { renderHook, cleanup, addCleanup, removeCleanup } = PURE +import { renderHook, cleanup, addCleanup, removeCleanup } from '../../src/native/pure' describe('cleanup tests', () => { test('should flush effects on cleanup', async () => { diff --git a/test/native/resultHistory.ts b/test/native/resultHistory.ts index 80b9b10b..db01d5b7 100644 --- a/test/native/resultHistory.ts +++ b/test/native/resultHistory.ts @@ -1,4 +1,4 @@ -import { renderHook } from '../src' +import { renderHook } from '../../src/native' describe('result history tests', () => { let count = 0 diff --git a/test/native/tsconfig.json b/test/native/tsconfig.json deleted file mode 100644 index 0a10d5bc..00000000 --- a/test/native/tsconfig.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "extends": "../../tsconfig.json", - "compilerOptions": { - "declaration": false - }, - "exclude": [], - "include": ["."] -} From 785aa5c839def116409b121435dcb4a675e9d206 Mon Sep 17 00:00:00 2001 From: Josh Ellis Date: Thu, 31 Dec 2020 18:05:50 +0000 Subject: [PATCH 08/53] fix: type assertion with RenderResult --- src/core/index.tsx | 6 +++--- src/types.ts | 14 +++++++------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/core/index.tsx b/src/core/index.tsx index 82192087..06a74a12 100644 --- a/src/core/index.tsx +++ b/src/core/index.tsx @@ -17,7 +17,7 @@ function resultContainer(): ResultContainerReturn { const results: Array<{ value?: TValue; error?: Error }> = [] const resolvers: Array<() => void> = [] - const result: RenderResult = { + const result: RenderResult = { get all() { return results.map(({ value, error }) => error ?? value) }, @@ -62,8 +62,8 @@ const createRenderHook = ( ) => ( callback: (props: TProps) => TResult, { initialProps, wrapper = defaultWrapper }: RenderHookOptions = {} -): RenderHookReturn => { - const { result, setValue, setError, addResolver } = resultContainer() +): RenderHookReturn => { + const { result, setValue, setError, addResolver } = resultContainer() const hookProps = { current: initialProps } const props = { callback, setValue, setError } const options = { wrapper } diff --git a/src/types.ts b/src/types.ts index 38eec2a8..933ad400 100644 --- a/src/types.ts +++ b/src/types.ts @@ -24,7 +24,7 @@ export type WrapperComponent = React.ComponentType */ export interface ReactHooksRenderer { - renderHook: () => RenderHookReturn + renderHook: () => RenderHookReturn act: ActTypes cleanup: { autoRegister: () => void @@ -54,14 +54,14 @@ export type AsyncUtilsReturn = { * */ -export type RenderResult = { - readonly all: unknown[] - readonly current: unknown +export type RenderResult = { + readonly all: (TValue | Error | undefined)[] + readonly current: TValue readonly error: Error | undefined } export type ResultContainerReturn = { - result: RenderResult + result: RenderResult addResolver: (resolver: () => void) => void setValue: (val: TValue) => void setError: (error: Error) => void @@ -72,8 +72,8 @@ export interface RenderHookOptions { wrapper?: WrapperComponent } -export type RenderHookReturn = { - result: RenderResult +export type RenderHookReturn = { + result: RenderResult rerender: (props?: TProps) => void unmount: () => void } & AsyncUtilsReturn From b553441af1f4677a681ea8a67614ae54b5456489 Mon Sep 17 00:00:00 2001 From: Josh Ellis Date: Thu, 31 Dec 2020 18:10:49 +0000 Subject: [PATCH 09/53] test: fix errorHook type error. --- test/native/errorHook.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/native/errorHook.ts b/test/native/errorHook.ts index 311fa2b4..adc3aeba 100644 --- a/test/native/errorHook.ts +++ b/test/native/errorHook.ts @@ -2,7 +2,7 @@ import { useState, useEffect } from 'react' import { renderHook } from '../../src/native' describe('error hook tests', () => { - function useError(throwError: boolean) { + function useError(throwError: boolean | undefined) { if (throwError) { throw new Error('expected') } From 7edd62363f815cfe1cda3d02639e029a2f85cd8f Mon Sep 17 00:00:00 2001 From: Josh Ellis Date: Thu, 31 Dec 2020 18:38:27 +0000 Subject: [PATCH 10/53] change: try to solve WrapperComponent PropTypes for useContext test --- src/core/index.tsx | 2 +- src/native/pure.tsx | 2 +- src/types.ts | 8 ++++---- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/core/index.tsx b/src/core/index.tsx index 06a74a12..4d5b49a2 100644 --- a/src/core/index.tsx +++ b/src/core/index.tsx @@ -57,7 +57,7 @@ function defaultWrapper({ children }: { children?: React.ReactNode }) { const createRenderHook = ( createRenderer: ( testProps: Omit, 'hookProps'>, - opts: NativeRendererOptions + opts: NativeRendererOptions ) => NativeRendererReturn ) => ( callback: (props: TProps) => TResult, diff --git a/src/native/pure.tsx b/src/native/pure.tsx index d2d88639..ac7b1818 100644 --- a/src/native/pure.tsx +++ b/src/native/pure.tsx @@ -8,7 +8,7 @@ import TestHook from 'core/testHook' function createRenderer( testHookProps: Omit, 'hookProps'>, - { wrapper: Wrapper }: NativeRendererOptions + { wrapper: Wrapper }: NativeRendererOptions ): NativeRendererReturn { let container: ReactTestRenderer diff --git a/src/types.ts b/src/types.ts index 933ad400..f0770390 100644 --- a/src/types.ts +++ b/src/types.ts @@ -15,7 +15,7 @@ export interface WaitOptions { suppressErrors?: boolean } -export type WrapperComponent = React.ComponentType +export type WrapperComponent = React.ComponentType /** * @@ -69,7 +69,7 @@ export type ResultContainerReturn = { export interface RenderHookOptions { initialProps?: TProps - wrapper?: WrapperComponent + wrapper?: WrapperComponent } export type RenderHookReturn = { @@ -91,8 +91,8 @@ export type NativeRendererReturn = { act: typeof RTRAct } -export type NativeRendererOptions = { - wrapper: WrapperComponent +export type NativeRendererOptions = { + wrapper: WrapperComponent } export type TestHookProps = { From 100c77c3622f01eb23361103e8661ac9fdea75ee Mon Sep 17 00:00:00 2001 From: Josh Ellis Date: Thu, 31 Dec 2020 19:58:48 +0000 Subject: [PATCH 11/53] fix: Wrapper type was breaking with spread operator --- src/native/pure.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/native/pure.tsx b/src/native/pure.tsx index ac7b1818..694468f2 100644 --- a/src/native/pure.tsx +++ b/src/native/pure.tsx @@ -13,7 +13,7 @@ function createRenderer( let container: ReactTestRenderer const toRender = (props?: TProps): JSX.Element => ( - + ) From b5f6ced841795d1612a46d4d42fd33592d383a6b Mon Sep 17 00:00:00 2001 From: Josh Ellis Date: Fri, 1 Jan 2021 16:43:21 +0000 Subject: [PATCH 12/53] feat: add server engine in typescript --- package.json | 1 + src/core/index.tsx | 19 +++-- src/native/pure.tsx | 4 +- src/server/pure.tsx | 39 +++++++--- src/types.ts | 72 ++++++++++++++----- test/server/asyncHook.ts | 29 ++++---- test/server/autoCleanup.disabled.ts | 7 +- test/server/autoCleanup.noAfterEach.ts | 8 ++- test/server/autoCleanup.ts | 4 +- test/server/cleanup.ts | 4 +- test/server/errorHook.ts | 8 +-- test/server/suspenseHook.ts | 49 ------------- test/server/{useContext.ts => useContext.tsx} | 4 +- test/server/useEffect.ts | 2 +- test/server/useReducer.ts | 3 +- test/server/useRef.ts | 4 +- test/tsconfig.json | 2 +- 17 files changed, 140 insertions(+), 119 deletions(-) delete mode 100644 test/server/suspenseHook.ts rename test/server/{useContext.ts => useContext.tsx} (89%) diff --git a/package.json b/package.json index 49d0e5d3..e620a4e9 100644 --- a/package.json +++ b/package.json @@ -42,6 +42,7 @@ "dependencies": { "@babel/runtime": "^7.12.5", "@types/react": ">=16.9.0", + "@types/react-dom": "^17.0.0", "@types/react-test-renderer": ">=16.9.0" }, "devDependencies": { diff --git a/src/core/index.tsx b/src/core/index.tsx index 4d5b49a2..d234b750 100644 --- a/src/core/index.tsx +++ b/src/core/index.tsx @@ -6,8 +6,9 @@ import { NativeRendererReturn, ResultContainerReturn, RenderHookOptions, - RenderHookReturn, - RenderResult + RenderResult, + ServerRendererReturn, + ServerRendererOptions } from 'types' import asyncUtils from './asyncUtils' @@ -57,18 +58,21 @@ function defaultWrapper({ children }: { children?: React.ReactNode }) { const createRenderHook = ( createRenderer: ( testProps: Omit, 'hookProps'>, - opts: NativeRendererOptions - ) => NativeRendererReturn + opts: NativeRendererOptions | ServerRendererOptions + ) => NativeRendererReturn | ServerRendererReturn ) => ( callback: (props: TProps) => TResult, { initialProps, wrapper = defaultWrapper }: RenderHookOptions = {} -): RenderHookReturn => { +) => { const { result, setValue, setError, addResolver } = resultContainer() const hookProps = { current: initialProps } const props = { callback, setValue, setError } const options = { wrapper } - const { render, rerender, unmount, act } = createRenderer(props, options) + const { render, rerender, unmount, act, ...renderUtils } = createRenderer( + props, + options + ) render(hookProps.current) @@ -88,7 +92,8 @@ const createRenderHook = ( result, rerender: rerenderHook, unmount: unmountHook, - ...asyncUtils(act, addResolver) + ...asyncUtils(act, addResolver), + ...renderUtils } } diff --git a/src/native/pure.tsx b/src/native/pure.tsx index 694468f2..72f8084c 100644 --- a/src/native/pure.tsx +++ b/src/native/pure.tsx @@ -6,7 +6,7 @@ import { TestHookProps, NativeRendererOptions, NativeRendererReturn } from 'type import { createRenderHook, cleanup, addCleanup, removeCleanup } from 'core/index' import TestHook from 'core/testHook' -function createRenderer( +function createNativeRenderer( testHookProps: Omit, 'hookProps'>, { wrapper: Wrapper }: NativeRendererOptions ): NativeRendererReturn { @@ -38,6 +38,6 @@ function createRenderer( } } -const renderHook = createRenderHook(createRenderer) +const renderHook = createRenderHook(createNativeRenderer) export { renderHook, act, cleanup, addCleanup, removeCleanup } diff --git a/src/server/pure.tsx b/src/server/pure.tsx index 287040fe..895d221a 100644 --- a/src/server/pure.tsx +++ b/src/server/pure.tsx @@ -2,28 +2,47 @@ import React from 'react' import ReactDOMServer from 'react-dom/server' import ReactDOM from 'react-dom' import { act as baseAct } from 'react-dom/test-utils' -import { createRenderHook, cleanup } from '../core' + +import { + TestHookProps, + ServerRendererOptions, + ServerRendererReturn, + ServerActCallback, + ServerActCallbackAsync, + ServerModifiedAct +} from 'types' + +import { createRenderHook, cleanup } from 'core/index' +import TestHook from 'core/testHook' +import { isPromise } from 'helpers/promises' // eslint-disable-next-line import/no-mutable-exports -let act +let act: ServerModifiedAct -function createRenderer(TestHook, testHookProps, { wrapper: Wrapper }) { +function createServerRenderer( + testHookProps: Omit, 'hookProps'>, + { wrapper: Wrapper }: ServerRendererOptions +): ServerRendererReturn { const container = document.createElement('div') - const toRender = (props) => ( - - + const toRender = (props?: TProps): JSX.Element => ( + + ) - let renderProps + let renderProps: TProps | undefined let hydrated = false - act = (...args) => { + act = (cb: ServerActCallbackAsync | ServerActCallback) => { if (!hydrated) { throw new Error('You must hydrate the component before you can act') } - return baseAct(...args) + if (isPromise(cb)) { + return baseAct(cb as ServerActCallbackAsync) + } else { + return baseAct(cb as ServerActCallback) + } } return { @@ -66,6 +85,6 @@ function createRenderer(TestHook, testHookProps, { wrapper: Wrapper }) { } } -const renderHook = createRenderHook(createRenderer) +const renderHook = createRenderHook(createServerRenderer) export { renderHook, act, cleanup } diff --git a/src/types.ts b/src/types.ts index f0770390..b1e6366a 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,5 +1,4 @@ import { act as RTRAct } from 'react-test-renderer' -import { act as RDAct } from 'react-dom/test-utils' /** * @@ -7,7 +6,7 @@ import { act as RDAct } from 'react-dom/test-utils' * */ -export type ActTypes = typeof RTRAct | typeof RDAct +export type ActTypes = typeof RTRAct | ServerModifiedAct export interface WaitOptions { interval?: number @@ -17,6 +16,16 @@ export interface WaitOptions { export type WrapperComponent = React.ComponentType +export type GenericRendererOptions = { + wrapper: WrapperComponent +} + +export type GenericRendererReturn = { + render: (props?: TProps) => void + rerender: (props?: TProps) => void + unmount: () => void +} + /** * * pure @@ -24,7 +33,9 @@ export type WrapperComponent = React.ComponentType */ export interface ReactHooksRenderer { - renderHook: () => RenderHookReturn + renderHook: () => + | RenderHookReturn + | ServerRenderHook act: ActTypes cleanup: { autoRegister: () => void @@ -74,30 +85,55 @@ export interface RenderHookOptions { export type RenderHookReturn = { result: RenderResult - rerender: (props?: TProps) => void - unmount: () => void -} & AsyncUtilsReturn +} & Omit, 'render'> & + AsyncUtilsReturn + +export type ServerRenderHook = RenderHookReturn & { + hydrate: () => void +} /** * - * native/pure + * core/testHook * */ -export type NativeRendererReturn = { - render: (props?: TProps) => void - rerender: (props?: TProps) => void - unmount: () => void - act: typeof RTRAct -} - -export type NativeRendererOptions = { - wrapper: WrapperComponent -} - export type TestHookProps = { hookProps: TProps | undefined callback: (props: TProps) => TResult setError: (error: Error) => void setValue: (value: TResult) => void } + +/** + * + * native/pure + * + */ + +export interface NativeRendererReturn extends GenericRendererReturn { + act: typeof RTRAct +} + +export type NativeRendererOptions = GenericRendererOptions + +/** + * + * server/pure + * + */ + +export type ServerRendererOptions = GenericRendererOptions + +export type ServerActCallbackAsync = () => Promise + +export type ServerActCallback = () => void | undefined + +export type ServerModifiedAct = ( + cb: ServerActCallbackAsync | ServerActCallback +) => Promise | void + +export interface ServerRendererReturn extends GenericRendererReturn { + act: ServerModifiedAct + hydrate: () => void +} diff --git a/test/server/asyncHook.ts b/test/server/asyncHook.ts index b42fb4ae..39ff8820 100644 --- a/test/server/asyncHook.ts +++ b/test/server/asyncHook.ts @@ -1,8 +1,9 @@ import { useState, useRef, useEffect } from 'react' + import { renderHook } from '../../src/server' describe('async hook tests', () => { - const useSequence = (...values) => { + const useSequence = (...values: string[]) => { const [first, ...otherValues] = values const [value, setValue] = useState(first) const index = useRef(0) @@ -85,7 +86,7 @@ describe('async hook tests', () => { }) test('should wait for expectation to pass', async () => { - const { result, hydrate, wait } = renderHook(() => useSequence('first', 'second', 'third')) + const { result, hydrate, waitFor } = renderHook(() => useSequence('first', 'second', 'third')) expect(result.current).toBe('first') @@ -94,7 +95,7 @@ describe('async hook tests', () => { expect(result.current).toBe('first') let complete = false - await wait(() => { + await waitFor(() => { expect(result.current).toBe('third') complete = true }) @@ -102,7 +103,7 @@ describe('async hook tests', () => { }) test('should not hang if expectation is already passing', async () => { - const { result, hydrate, wait } = renderHook(() => useSequence('first', 'second')) + const { result, hydrate, waitFor } = renderHook(() => useSequence('first', 'second')) expect(result.current).toBe('first') @@ -111,7 +112,7 @@ describe('async hook tests', () => { expect(result.current).toBe('first') let complete = false - await wait(() => { + await waitFor(() => { expect(result.current).toBe('first') complete = true }) @@ -119,7 +120,7 @@ describe('async hook tests', () => { }) test('should reject if callback throws error', async () => { - const { result, hydrate, wait } = renderHook(() => useSequence('first', 'second', 'third')) + const { result, hydrate, waitFor } = renderHook(() => useSequence('first', 'second', 'third')) expect(result.current).toBe('first') @@ -128,7 +129,7 @@ describe('async hook tests', () => { expect(result.current).toBe('first') await expect( - wait( + waitFor( () => { if (result.current === 'second') { throw new Error('Something Unexpected') @@ -143,7 +144,7 @@ describe('async hook tests', () => { }) test('should reject if callback immediately throws error', async () => { - const { result, hydrate, wait } = renderHook(() => useSequence('first', 'second', 'third')) + const { result, hydrate, waitFor } = renderHook(() => useSequence('first', 'second', 'third')) expect(result.current).toBe('first') @@ -152,7 +153,7 @@ describe('async hook tests', () => { expect(result.current).toBe('first') await expect( - wait( + waitFor( () => { throw new Error('Something Unexpected') }, @@ -164,7 +165,7 @@ describe('async hook tests', () => { }) test('should wait for truthy value', async () => { - const { result, hydrate, wait } = renderHook(() => useSequence('first', 'second', 'third')) + const { result, hydrate, waitFor } = renderHook(() => useSequence('first', 'second', 'third')) expect(result.current).toBe('first') @@ -172,13 +173,13 @@ describe('async hook tests', () => { expect(result.current).toBe('first') - await wait(() => result.current === 'third') + await waitFor(() => result.current === 'third') expect(result.current).toBe('third') }) test('should reject if timeout exceeded when waiting for expectation to pass', async () => { - const { result, hydrate, wait } = renderHook(() => useSequence('first', 'second', 'third')) + const { result, hydrate, waitFor } = renderHook(() => useSequence('first', 'second', 'third')) expect(result.current).toBe('first') @@ -187,13 +188,13 @@ describe('async hook tests', () => { expect(result.current).toBe('first') await expect( - wait( + waitFor( () => { expect(result.current).toBe('third') }, { timeout: 75 } ) - ).rejects.toThrow(Error('Timed out in wait after 75ms.')) + ).rejects.toThrow(Error('Timed out in waitFor after 75ms.')) }) test('should wait for value to change', async () => { diff --git a/test/server/autoCleanup.disabled.ts b/test/server/autoCleanup.disabled.ts index 0960f1c1..00853a13 100644 --- a/test/server/autoCleanup.disabled.ts +++ b/test/server/autoCleanup.disabled.ts @@ -1,14 +1,17 @@ import { useEffect } from 'react' +import { ReactHooksRenderer } from 'types' + // This verifies that if RHTL_SKIP_AUTO_CLEANUP is set // 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' - renderHook = require('../../src/server').renderHook + // eslint-disable-next-line @typescript-eslint/no-var-requires + renderHook = (require('../../src/server') as ReactHooksRenderer).renderHook }) test('first', () => { diff --git a/test/server/autoCleanup.noAfterEach.ts b/test/server/autoCleanup.noAfterEach.ts index 141d4ef1..180dbea3 100644 --- a/test/server/autoCleanup.noAfterEach.ts +++ b/test/server/autoCleanup.noAfterEach.ts @@ -1,15 +1,19 @@ import { useEffect } from 'react' +import { ReactHooksRenderer } from 'types' + // This verifies that if RHTL_SKIP_AUTO_CLEANUP is set // 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 - renderHook = require('../../src/server').renderHook + // eslint-disable-next-line @typescript-eslint/no-var-requires + renderHook = (require('../../src/server') as ReactHooksRenderer).renderHook }) test('first', () => { diff --git a/test/server/autoCleanup.ts b/test/server/autoCleanup.ts index 586b4c66..087c2af8 100644 --- a/test/server/autoCleanup.ts +++ b/test/server/autoCleanup.ts @@ -5,13 +5,13 @@ import { renderHook } from '../../src/server' // environment which supports afterEach (like Jest) // we'll get automatic cleanup between tests. describe('auto cleanup tests', () => { - const cleanups = { + const cleanups: Record = { ssr: false, hydrated: false } test('first', () => { - const hookWithCleanup = (name) => { + const hookWithCleanup = (name: string) => { useEffect(() => { return () => { cleanups[name] = true diff --git a/test/server/cleanup.ts b/test/server/cleanup.ts index 1b391809..e8033492 100644 --- a/test/server/cleanup.ts +++ b/test/server/cleanup.ts @@ -24,7 +24,7 @@ describe('cleanup tests', () => { test('should cleanup all rendered hooks', async () => { let cleanupCalled = [false, false] - const hookWithCleanup = (id) => { + const hookWithCleanup = (id: number) => { useEffect(() => { return () => { cleanupCalled = cleanupCalled.map((_, i) => (i === id ? true : _)) @@ -46,7 +46,7 @@ describe('cleanup tests', () => { test('should only cleanup hydrated hooks', async () => { let cleanupCalled = [false, false] - const hookWithCleanup = (id) => { + const hookWithCleanup = (id: number) => { useEffect(() => { return () => { cleanupCalled = cleanupCalled.map((_, i) => (i === id ? true : _)) diff --git a/test/server/errorHook.ts b/test/server/errorHook.ts index 5eb91b89..511d2d77 100644 --- a/test/server/errorHook.ts +++ b/test/server/errorHook.ts @@ -2,15 +2,15 @@ import { useState, useEffect } from 'react' import { renderHook } from '../../src/server' describe('error hook tests', () => { - function useError(throwError) { + function useError(throwError: boolean | undefined) { 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) }, []) diff --git a/test/server/suspenseHook.ts b/test/server/suspenseHook.ts deleted file mode 100644 index 2ff34647..00000000 --- a/test/server/suspenseHook.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { renderHook } from '../../src/server' - -describe('suspense hook tests', () => { - const cache = {} - const fetchName = (isSuccessful) => { - if (!cache.value) { - cache.value = new Promise((resolve, reject) => { - setTimeout(() => { - if (isSuccessful) { - resolve('Bob') - } else { - reject(new Error('Failed to fetch name')) - } - }, 50) - }) - .then((value) => (cache.value = value)) - .catch((e) => (cache.value = e)) - } - return cache.value - } - - const useFetchName = (isSuccessful = true) => { - const name = fetchName(isSuccessful) - if (typeof name.then === 'function' || name instanceof Error) { - throw name - } - return name - } - - beforeEach(() => { - delete cache.value - }) - - test('should allow rendering to be suspended', async () => { - const { result, waitForNextUpdate } = renderHook(() => useFetchName(true)) - - await waitForNextUpdate() - - expect(result.current).toBe('Bob') - }) - - test('should set error if suspense promise rejects', async () => { - const { result, waitForNextUpdate } = renderHook(() => useFetchName(false)) - - await waitForNextUpdate() - - expect(result.error).toEqual(new Error('Failed to fetch name')) - }) -}) diff --git a/test/server/useContext.ts b/test/server/useContext.tsx similarity index 89% rename from test/server/useContext.ts rename to test/server/useContext.tsx index 28e81b82..33c1008b 100644 --- a/test/server/useContext.ts +++ b/test/server/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} ) @@ -27,7 +27,7 @@ describe('useContext tests', () => { test('should update value in context when props are updated', () => { const TestContext = createContext('foo') - const wrapper = ({ contextValue, children }) => ( + const wrapper: React.FC<{ contextValue: string }> = ({ contextValue, children }) => ( {children} ) diff --git a/test/server/useEffect.ts b/test/server/useEffect.ts index 35d56e42..1adf23e4 100644 --- a/test/server/useEffect.ts +++ b/test/server/useEffect.ts @@ -3,7 +3,7 @@ import { renderHook } from '../../src/server' describe('useEffect tests', () => { test('should handle useEffect hook', () => { - const sideEffect = { 1: false, 2: false } + const sideEffect: { [key: number]: boolean } = { 1: false, 2: false } const { hydrate, rerender, unmount } = renderHook( ({ id }) => { diff --git a/test/server/useReducer.ts b/test/server/useReducer.ts index 2fb30105..f11daf50 100644 --- a/test/server/useReducer.ts +++ b/test/server/useReducer.ts @@ -3,7 +3,8 @@ import { renderHook, act } from '../../src/server' 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, hydrate } = renderHook(() => { const [state, dispatch] = useReducer(reducer, 0) diff --git a/test/server/useRef.ts b/test/server/useRef.ts index 065cd989..26cdc323 100644 --- a/test/server/useRef.ts +++ b/test/server/useRef.ts @@ -13,14 +13,14 @@ describe('useHook tests', () => { test('should handle useImperativeHandle hook', () => { const { result, hydrate } = renderHook(() => { - const ref = useRef() + const ref = useRef boolean>>({}) useImperativeHandle(ref, () => ({ fakeImperativeMethod: () => true })) return ref }) - expect(result.current.current).toBeUndefined() + expect(result.current.current).toEqual({}) hydrate() diff --git a/test/tsconfig.json b/test/tsconfig.json index 7e62d921..8336d11a 100644 --- a/test/tsconfig.json +++ b/test/tsconfig.json @@ -4,5 +4,5 @@ "declaration": false }, "exclude": [], - "include": ["./native/*"] + "include": ["./native/*", "./server/*"] } From 56be770460d43407000013cc21809da6f3fb3301 Mon Sep 17 00:00:00 2001 From: Josh Ellis Date: Sat, 2 Jan 2021 22:21:52 +0000 Subject: [PATCH 13/53] test: get coverage to 100 --- src/server/pure.tsx | 11 ++----- src/types.ts | 7 ++--- test/native/asyncHook.ts | 54 +++++++++++++++++++++++++++++++++- test/server/asyncHook.ts | 26 +++++++++++++++- test/server/hydrationErrors.ts | 37 +++++++++++++++++++++++ 5 files changed, 120 insertions(+), 15 deletions(-) create mode 100644 test/server/hydrationErrors.ts diff --git a/src/server/pure.tsx b/src/server/pure.tsx index 895d221a..83a8cbf3 100644 --- a/src/server/pure.tsx +++ b/src/server/pure.tsx @@ -14,7 +14,6 @@ import { import { createRenderHook, cleanup } from 'core/index' import TestHook from 'core/testHook' -import { isPromise } from 'helpers/promises' // eslint-disable-next-line import/no-mutable-exports let act: ServerModifiedAct @@ -38,11 +37,8 @@ function createServerRenderer( if (!hydrated) { throw new Error('You must hydrate the component before you can act') } - if (isPromise(cb)) { - return baseAct(cb as ServerActCallbackAsync) - } else { - return baseAct(cb as ServerActCallback) - } + + return baseAct(cb as ServerActCallback) } return { @@ -56,8 +52,7 @@ function createServerRenderer( hydrate() { if (hydrated) { throw new Error('The component can only be hydrated once') - } - if (!hydrated) { + } else { document.body.appendChild(container) baseAct(() => { ReactDOM.hydrate(toRender(renderProps), container) diff --git a/src/types.ts b/src/types.ts index b1e6366a..7bb67783 100644 --- a/src/types.ts +++ b/src/types.ts @@ -51,11 +51,8 @@ export type RenderingEngineArray = Array<{ required: string; renderer: string }> */ export type AsyncUtilsReturn = { - waitFor: ( - callback: () => boolean | void, - { interval, timeout, suppressErrors }?: WaitOptions - ) => Promise - waitForNextUpdate: ({ timeout }?: Pick) => Promise + waitFor: (callback: () => boolean | void, opts?: WaitOptions) => Promise + waitForNextUpdate: (opts?: Pick) => Promise waitForValueToChange: (selector: () => unknown, options?: WaitOptions) => Promise } diff --git a/test/native/asyncHook.ts b/test/native/asyncHook.ts index 94f2cc1c..18977b19 100644 --- a/test/native/asyncHook.ts +++ b/test/native/asyncHook.ts @@ -17,7 +17,7 @@ describe('async hook tests', () => { return () => { clearInterval(interval) } - }, [...values]) + }, [otherValues]) return value } @@ -79,6 +79,28 @@ describe('async hook tests', () => { expect(complete).toBe(true) }) + test('should wait for arbitrary expectation to pass', async () => { + const { waitFor } = renderHook(() => null) + + let actual = 0 + const expected = 1 + + setTimeout(() => { + actual = expected + }, 200) + + let complete = false + await waitFor( + () => { + expect(actual).toBe(expected) + complete = true + }, + { interval: 100 } + ) + + expect(complete).toBe(true) + }) + test('should not hang if expectation is already passing', async () => { const { result, waitFor } = renderHook(() => useSequence('first', 'second')) @@ -139,6 +161,21 @@ describe('async hook tests', () => { expect(result.current).toBe('third') }) + test('should wait for arbitrary truthy value', async () => { + const { waitFor } = renderHook(() => null) + + let actual = 0 + const expected = 1 + + setTimeout(() => { + actual = expected + }, 200) + + await waitFor(() => actual === 1, { interval: 100 }) + + expect(actual).toBe(expected) + }) + test('should reject if timeout exceeded when waiting for expectation to pass', async () => { const { result, waitFor } = renderHook(() => useSequence('first', 'second', 'third')) @@ -166,6 +203,21 @@ describe('async hook tests', () => { expect(result.current).toBe('third') }) + test('should wait for arbitrary value to change', async () => { + const { waitForValueToChange } = renderHook(() => null) + + let actual = 0 + const expected = 1 + + setTimeout(() => { + actual = expected + }, 200) + + await waitForValueToChange(() => actual, { interval: 100 }) + + expect(actual).toBe(expected) + }) + test('should reject if timeout exceeded when waiting for value to change', async () => { const { result, waitForValueToChange } = renderHook(() => useSequence('first', 'second', 'third') diff --git a/test/server/asyncHook.ts b/test/server/asyncHook.ts index 39ff8820..9c872430 100644 --- a/test/server/asyncHook.ts +++ b/test/server/asyncHook.ts @@ -18,7 +18,7 @@ describe('async hook tests', () => { return () => { clearInterval(interval) } - }, [...values]) + }, [otherValues]) return value } @@ -102,6 +102,30 @@ describe('async hook tests', () => { expect(complete).toBe(true) }) + test('should wait for arbitrary expectation to pass', async () => { + const { waitFor, hydrate } = renderHook(() => null) + + hydrate() + + let actual = 0 + const expected = 1 + + setTimeout(() => { + actual = expected + }, 200) + + let complete = false + await waitFor( + () => { + expect(actual).toBe(expected) + complete = true + }, + { interval: 100 } + ) + + expect(complete).toBe(true) + }) + test('should not hang if expectation is already passing', async () => { const { result, hydrate, waitFor } = renderHook(() => useSequence('first', 'second')) diff --git a/test/server/hydrationErrors.ts b/test/server/hydrationErrors.ts new file mode 100644 index 00000000..bcba11d4 --- /dev/null +++ b/test/server/hydrationErrors.ts @@ -0,0 +1,37 @@ +import { useState, useCallback } from 'react' +import { renderHook, act } from '../../src/server' + +describe('hydration errors tests', () => { + function useCounter() { + const [count, setCount] = useState(0) + + const increment = useCallback(() => setCount(count + 1), [count]) + const decrement = useCallback(() => setCount(count - 1), [count]) + + return { count, increment, decrement } + } + + test('should throw error if component is rehydrated twice in a row', () => { + const { hydrate } = renderHook(() => useCounter()) + + hydrate() + + expect(() => hydrate()).toThrow(Error('The component can only be hydrated once')) + }) + + test('should throw error if component tries to rerender without hydrating', () => { + const { rerender } = renderHook(() => useCounter()) + + expect(() => rerender()).toThrow( + Error('You must hydrate the component before you can rerender') + ) + }) + + test('act should throw if called without hydrating', () => { + const { result } = renderHook(() => useCounter()) + + expect(() => act(() => result.current.decrement())).toThrow( + Error('You must hydrate the component before you can act') + ) + }) +}) From 8d35cac11e1ab043cd9ebd15eb8145ad58407246 Mon Sep 17 00:00:00 2001 From: Josh Ellis Date: Sat, 2 Jan 2021 22:24:33 +0000 Subject: [PATCH 14/53] change: remove rtr act & use own written --- src/types.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/types.ts b/src/types.ts index 7bb67783..dab811a4 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,12 +1,10 @@ -import { act as RTRAct } from 'react-test-renderer' - /** * * Shared * */ -export type ActTypes = typeof RTRAct | ServerModifiedAct +export type ActTypes = NativeModifedAct | ServerModifiedAct export interface WaitOptions { interval?: number @@ -108,8 +106,10 @@ export type TestHookProps = { * */ +export type NativeModifedAct = (callback: () => Promise) => Promise + export interface NativeRendererReturn extends GenericRendererReturn { - act: typeof RTRAct + act: NativeModifedAct } export type NativeRendererOptions = GenericRendererOptions From a4329dd72609bd61cfe9b1461ad15d74d9458751 Mon Sep 17 00:00:00 2001 From: Josh Ellis Date: Sat, 2 Jan 2021 22:52:53 +0000 Subject: [PATCH 15/53] change: use relative paths & add react-dom to renderers --- src/core/asyncUtils.ts | 12 ++++++------ src/core/index.tsx | 2 +- src/core/testHook.ts | 6 +++--- src/native/pure.tsx | 6 +++--- src/pure.ts | 3 ++- src/server/pure.tsx | 6 +++--- tsconfig.json | 7 +------ 7 files changed, 19 insertions(+), 23 deletions(-) diff --git a/src/core/asyncUtils.ts b/src/core/asyncUtils.ts index 0904f54c..e11e908d 100644 --- a/src/core/asyncUtils.ts +++ b/src/core/asyncUtils.ts @@ -1,7 +1,7 @@ -import { ActTypes, WaitOptions, AsyncUtilsReturn } from 'types' +import { ActTypes, WaitOptions, AsyncUtilsReturn } from '../types' -import { resolveAfter } from 'helpers/promises' -import { TimeoutError } from 'helpers/error' +import { resolveAfter } from '../helpers/promises' +import { TimeoutError } from '../helpers/error' function asyncUtils(act: ActTypes, addResolver: (callback: () => void) => void): AsyncUtilsReturn { let nextUpdatePromise: Promise | null = null @@ -35,7 +35,7 @@ function asyncUtils(act: ActTypes, addResolver: (callback: () => void) => void): try { const callbackResult = callback() return callbackResult ?? callbackResult === undefined - } catch (error: unknown) { + } catch (error) { if (!suppressErrors) { throw error as Error } @@ -57,7 +57,7 @@ function asyncUtils(act: ActTypes, addResolver: (callback: () => void) => void): if (checkResult()) { return } - } catch (error: unknown) { + } catch (error) { if (error instanceof TimeoutError && initialTimeout) { throw new TimeoutError(waitFor, initialTimeout) } @@ -79,7 +79,7 @@ function asyncUtils(act: ActTypes, addResolver: (callback: () => void) => void): suppressErrors: false, ...options }) - } catch (error: unknown) { + } catch (error) { if (error instanceof TimeoutError && options.timeout) { throw new TimeoutError(waitForValueToChange, options.timeout) } diff --git a/src/core/index.tsx b/src/core/index.tsx index d234b750..b650ce96 100644 --- a/src/core/index.tsx +++ b/src/core/index.tsx @@ -9,7 +9,7 @@ import { RenderResult, ServerRendererReturn, ServerRendererOptions -} from 'types' +} from '../types' import asyncUtils from './asyncUtils' import { cleanup, addCleanup, removeCleanup } from './cleanup' diff --git a/src/core/testHook.ts b/src/core/testHook.ts index 08933f97..a93fdc2d 100644 --- a/src/core/testHook.ts +++ b/src/core/testHook.ts @@ -1,6 +1,6 @@ -import { isPromise } from 'helpers/promises' +import { isPromise } from '../helpers/promises' -import { TestHookProps } from 'types' +import { TestHookProps } from '../types' export default function TestHook({ hookProps, @@ -11,7 +11,7 @@ export default function TestHook({ try { // coerce undefined into TProps, so it maintains the previous behaviour setValue(callback(hookProps as TProps)) - } catch (err: unknown) { + } catch (err) { if (isPromise(err)) { throw err } else { diff --git a/src/native/pure.tsx b/src/native/pure.tsx index 72f8084c..09884251 100644 --- a/src/native/pure.tsx +++ b/src/native/pure.tsx @@ -1,10 +1,10 @@ import React, { Suspense } from 'react' import { act, create, ReactTestRenderer } from 'react-test-renderer' -import { TestHookProps, NativeRendererOptions, NativeRendererReturn } from 'types' +import { TestHookProps, NativeRendererOptions, NativeRendererReturn } from '../types' -import { createRenderHook, cleanup, addCleanup, removeCleanup } from 'core/index' -import TestHook from 'core/testHook' +import { createRenderHook, cleanup, addCleanup, removeCleanup } from '../core/index' +import TestHook from '../core/testHook' function createNativeRenderer( testHookProps: Omit, 'hookProps'>, diff --git a/src/pure.ts b/src/pure.ts index b385f71b..b3141d82 100644 --- a/src/pure.ts +++ b/src/pure.ts @@ -1,7 +1,8 @@ import { RenderingEngineArray, ReactHooksRenderer } from 'types' const RENDERERS: RenderingEngineArray = [ - { required: 'react-test-renderer', renderer: './native/pure' } + { required: 'react-test-renderer', renderer: './native/pure' }, + { required: 'react-dom', renderer: './server/pure' } ] function getRenderer(renderers: RenderingEngineArray): string { diff --git a/src/server/pure.tsx b/src/server/pure.tsx index 83a8cbf3..cfcc6145 100644 --- a/src/server/pure.tsx +++ b/src/server/pure.tsx @@ -10,10 +10,10 @@ import { ServerActCallback, ServerActCallbackAsync, ServerModifiedAct -} from 'types' +} from '../types' -import { createRenderHook, cleanup } from 'core/index' -import TestHook from 'core/testHook' +import { createRenderHook, cleanup } from '../core/index' +import TestHook from '../core/testHook' // eslint-disable-next-line import/no-mutable-exports let act: ServerModifiedAct diff --git a/tsconfig.json b/tsconfig.json index c0fb6557..1337ac30 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -4,10 +4,5 @@ "allowJs": true, "target": "ES6" }, - "exclude": ["./test"], - "paths": { - "core/*": ["src/core/*"], - "helpers/*": ["src/helpers/*"], - "types": ["src/types.ts"], - } + "exclude": ["./test"] } From 0e296b7c751e976efed5671acaa0e1c7db6f3a32 Mon Sep 17 00:00:00 2001 From: Josh Ellis Date: Sat, 2 Jan 2021 23:04:53 +0000 Subject: [PATCH 16/53] feat: add custom renderer option --- custom/index.js | 1 + custom/pure.js | 2 ++ src/custom/index.ts | 5 +++++ src/custom/pure.ts | 9 +++++++++ 4 files changed, 17 insertions(+) create mode 100644 custom/index.js create mode 100644 custom/pure.js create mode 100644 src/custom/index.ts create mode 100644 src/custom/pure.ts diff --git a/custom/index.js b/custom/index.js new file mode 100644 index 00000000..e22fbb61 --- /dev/null +++ b/custom/index.js @@ -0,0 +1 @@ +module.exports = require('../lib/custom') diff --git a/custom/pure.js b/custom/pure.js new file mode 100644 index 00000000..c98aee61 --- /dev/null +++ b/custom/pure.js @@ -0,0 +1,2 @@ +// makes it so people can import from '@testing-library/react-hooks/custom/pure' +module.exports = require('../lib/custom/pure') diff --git a/src/custom/index.ts b/src/custom/index.ts new file mode 100644 index 00000000..ca4f465a --- /dev/null +++ b/src/custom/index.ts @@ -0,0 +1,5 @@ +import { createCustomRenderer, cleanup } from './pure' + +cleanup.autoRegister() + +export { createCustomRenderer, cleanup } diff --git a/src/custom/pure.ts b/src/custom/pure.ts new file mode 100644 index 00000000..161fe81e --- /dev/null +++ b/src/custom/pure.ts @@ -0,0 +1,9 @@ +import { createRenderHook, cleanup } from '../core/index' + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +function createCustomRenderer(createRenderer: any) { + const renderHook = createRenderHook(createRenderer) + return { renderHook } +} + +export { createCustomRenderer, cleanup } From fdfe432720b55c162898caf958781c0dddc4f855 Mon Sep 17 00:00:00 2001 From: Josh Ellis Date: Sun, 3 Jan 2021 10:23:35 +0000 Subject: [PATCH 17/53] feat: add dom renderer --- dom/index.js | 2 + dom/pure.js | 2 + src/dom/index.ts | 5 + src/dom/pure.ts | 54 ++++++ test/dom/asyncHook.ts | 269 ++++++++++++++++++++++++++++ test/dom/autoCleanup.disabled.ts | 31 ++++ test/dom/autoCleanup.noAfterEach.ts | 33 ++++ test/dom/autoCleanup.ts | 24 +++ test/dom/cleanup.ts | 135 ++++++++++++++ test/dom/customHook.ts | 29 +++ test/dom/errorHook.ts | 150 ++++++++++++++++ test/dom/resultHistory.ts | 34 ++++ test/dom/suspenseHook.ts | 49 +++++ test/dom/useContext.tsx | 63 +++++++ test/dom/useEffect.ts | 62 +++++++ test/dom/useMemo.ts | 64 +++++++ test/dom/useReducer.ts | 20 +++ test/dom/useRef.ts | 27 +++ test/dom/useState.ts | 24 +++ test/tsconfig.json | 2 +- 20 files changed, 1078 insertions(+), 1 deletion(-) create mode 100644 dom/index.js create mode 100644 dom/pure.js create mode 100644 src/dom/index.ts create mode 100644 src/dom/pure.ts create mode 100644 test/dom/asyncHook.ts create mode 100644 test/dom/autoCleanup.disabled.ts create mode 100644 test/dom/autoCleanup.noAfterEach.ts create mode 100644 test/dom/autoCleanup.ts create mode 100644 test/dom/cleanup.ts create mode 100644 test/dom/customHook.ts create mode 100644 test/dom/errorHook.ts create mode 100644 test/dom/resultHistory.ts create mode 100644 test/dom/suspenseHook.ts create mode 100644 test/dom/useContext.tsx create mode 100644 test/dom/useEffect.ts create mode 100644 test/dom/useMemo.ts create mode 100644 test/dom/useReducer.ts create mode 100644 test/dom/useRef.ts create mode 100644 test/dom/useState.ts diff --git a/dom/index.js b/dom/index.js new file mode 100644 index 00000000..5b8693bf --- /dev/null +++ b/dom/index.js @@ -0,0 +1,2 @@ +// makes it so people can import from '@testing-library/react-hooks/dom' +module.exports = require('../lib/dom') diff --git a/dom/pure.js b/dom/pure.js new file mode 100644 index 00000000..c6e171cc --- /dev/null +++ b/dom/pure.js @@ -0,0 +1,2 @@ +// makes it so people can import from '@testing-library/react-hooks/dom/pure' +module.exports = require('../lib/dom/pure') diff --git a/src/dom/index.ts b/src/dom/index.ts new file mode 100644 index 00000000..d18e042c --- /dev/null +++ b/src/dom/index.ts @@ -0,0 +1,5 @@ +import { renderHook, act, cleanup } from './pure' + +cleanup.autoRegister() + +export { renderHook, act, cleanup } diff --git a/src/dom/pure.ts b/src/dom/pure.ts new file mode 100644 index 00000000..c9c789c1 --- /dev/null +++ b/src/dom/pure.ts @@ -0,0 +1,54 @@ +import ReactDOM from 'react-dom' +import { act as baseAct } from 'react-dom/test-utils' + +import { + TestHookProps, + DomRendererOptions, + DomRendererReturn, + ReactDomAct, + ReactDomActCallbackAsync, + ReactDomActCallback +} from '../types' + +import { createRenderHook, cleanup, addCleanup, removeCleanup } from '../core/index' + +import toRender from '../helpers/toRender' + +// eslint-disable-next-line import/no-mutable-exports +let act: ReactDomAct + +function createDomRenderer( + testHookProps: Omit, 'hookProps'>, + { wrapper }: DomRendererOptions +): DomRendererReturn { + const container = document.createElement('div') + + const testHook = toRender(testHookProps, wrapper) + + act = (cb: ReactDomActCallbackAsync | ReactDomActCallback) => baseAct(cb as ReactDomActCallback) + + return { + render(props) { + document.body.appendChild(container) + baseAct(() => { + ReactDOM.render(testHook(props), container) + }) + }, + rerender(props) { + baseAct(() => { + ReactDOM.render(testHook(props), container) + }) + }, + unmount() { + baseAct(() => { + ReactDOM.unmountComponentAtNode(container) + }) + document.body.removeChild(container) + }, + act + } +} + +const renderHook = createRenderHook(createDomRenderer) + +export { renderHook, act, cleanup, addCleanup, removeCleanup } diff --git a/test/dom/asyncHook.ts b/test/dom/asyncHook.ts new file mode 100644 index 00000000..20559e4c --- /dev/null +++ b/test/dom/asyncHook.ts @@ -0,0 +1,269 @@ +import { useState, useRef, useEffect } from 'react' +import { renderHook } from '../../src/dom' + +describe('async hook tests', () => { + const useSequence = (...values: string[]) => { + const [first, ...otherValues] = values + const [value, setValue] = useState(first) + const index = useRef(0) + + useEffect(() => { + const interval = setInterval(() => { + setValue(otherValues[index.current++]) + if (index.current === otherValues.length) { + clearInterval(interval) + } + }, 50) + return () => { + clearInterval(interval) + } + }, [otherValues]) + + return value + } + + test('should wait for next update', async () => { + const { result, waitForNextUpdate } = renderHook(() => useSequence('first', 'second')) + + expect(result.current).toBe('first') + + await waitForNextUpdate() + + expect(result.current).toBe('second') + }) + + test('should wait for multiple updates', async () => { + const { result, waitForNextUpdate } = renderHook(() => useSequence('first', 'second', 'third')) + + expect(result.current).toBe('first') + + await waitForNextUpdate() + + expect(result.current).toBe('second') + + await waitForNextUpdate() + + expect(result.current).toBe('third') + }) + + test('should resolve all when updating', async () => { + const { result, waitForNextUpdate } = renderHook(() => useSequence('first', 'second')) + + expect(result.current).toBe('first') + + await Promise.all([waitForNextUpdate(), waitForNextUpdate(), waitForNextUpdate()]) + + expect(result.current).toBe('second') + }) + + test('should reject if timeout exceeded when waiting for next update', async () => { + const { result, waitForNextUpdate } = renderHook(() => useSequence('first', 'second')) + + expect(result.current).toBe('first') + + await expect(waitForNextUpdate({ timeout: 10 })).rejects.toThrow( + Error('Timed out in waitForNextUpdate after 10ms.') + ) + }) + + test('should wait for expectation to pass', async () => { + const { result, waitFor } = renderHook(() => useSequence('first', 'second', 'third')) + + expect(result.current).toBe('first') + + let complete = false + await waitFor(() => { + expect(result.current).toBe('third') + complete = true + }) + expect(complete).toBe(true) + }) + + test('should wait for arbitrary expectation to pass', async () => { + const { waitFor } = renderHook(() => null) + + let actual = 0 + const expected = 1 + + setTimeout(() => { + actual = expected + }, 200) + + let complete = false + await waitFor( + () => { + expect(actual).toBe(expected) + complete = true + }, + { interval: 100 } + ) + + expect(complete).toBe(true) + }) + + test('should not hang if expectation is already passing', async () => { + const { result, waitFor } = renderHook(() => useSequence('first', 'second')) + + expect(result.current).toBe('first') + + let complete = false + await waitFor(() => { + expect(result.current).toBe('first') + complete = true + }) + expect(complete).toBe(true) + }) + + test('should reject if callback throws error', async () => { + const { result, waitFor } = renderHook(() => useSequence('first', 'second', 'third')) + + expect(result.current).toBe('first') + + await expect( + waitFor( + () => { + 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', async () => { + const { result, waitFor } = renderHook(() => useSequence('first', 'second', 'third')) + + expect(result.current).toBe('first') + + await expect( + waitFor( + () => { + throw new Error('Something Unexpected') + }, + { + suppressErrors: false + } + ) + ).rejects.toThrow(Error('Something Unexpected')) + }) + + test('should wait for truthy value', async () => { + const { result, waitFor } = renderHook(() => useSequence('first', 'second', 'third')) + + expect(result.current).toBe('first') + + await waitFor(() => result.current === 'third') + + expect(result.current).toBe('third') + }) + + test('should wait for arbitrary truthy value', async () => { + const { waitFor } = renderHook(() => null) + + let actual = 0 + const expected = 1 + + setTimeout(() => { + actual = expected + }, 200) + + await waitFor(() => actual === 1, { interval: 100 }) + + expect(actual).toBe(expected) + }) + + test('should reject if timeout exceeded when waiting for expectation to pass', async () => { + const { result, waitFor } = renderHook(() => useSequence('first', 'second', 'third')) + + expect(result.current).toBe('first') + + await expect( + waitFor( + () => { + expect(result.current).toBe('third') + }, + { timeout: 75 } + ) + ).rejects.toThrow(Error('Timed out in waitFor after 75ms.')) + }) + + test('should wait for value to change', async () => { + const { result, waitForValueToChange } = renderHook(() => + useSequence('first', 'second', 'third') + ) + + expect(result.current).toBe('first') + + await waitForValueToChange(() => result.current === 'third') + + expect(result.current).toBe('third') + }) + + test('should wait for arbitrary value to change', async () => { + const { waitForValueToChange } = renderHook(() => null) + + let actual = 0 + const expected = 1 + + setTimeout(() => { + actual = expected + }, 200) + + await waitForValueToChange(() => actual, { interval: 100 }) + + expect(actual).toBe(expected) + }) + + test('should reject if timeout exceeded when waiting for value to change', async () => { + const { result, waitForValueToChange } = renderHook(() => + useSequence('first', 'second', 'third') + ) + + expect(result.current).toBe('first') + + await expect( + waitForValueToChange(() => result.current === 'third', { + timeout: 75 + }) + ).rejects.toThrow(Error('Timed out in waitForValueToChange after 75ms.')) + }) + + test('should reject if selector throws error', async () => { + const { result, waitForValueToChange } = renderHook(() => useSequence('first', 'second')) + + expect(result.current).toBe('first') + + await expect( + waitForValueToChange(() => { + if (result.current === 'second') { + throw new Error('Something Unexpected') + } + return result.current + }) + ).rejects.toThrow(Error('Something Unexpected')) + }) + + test('should not reject if selector throws error and suppress errors option is enabled', async () => { + const { result, waitForValueToChange } = renderHook(() => + useSequence('first', 'second', 'third') + ) + + expect(result.current).toBe('first') + + await waitForValueToChange( + () => { + if (result.current === 'second') { + throw new Error('Something Unexpected') + } + return result.current === 'third' + }, + { suppressErrors: true } + ) + + expect(result.current).toBe('third') + }) +}) diff --git a/test/dom/autoCleanup.disabled.ts b/test/dom/autoCleanup.disabled.ts new file mode 100644 index 00000000..10c8cbdb --- /dev/null +++ b/test/dom/autoCleanup.disabled.ts @@ -0,0 +1,31 @@ +import { useEffect } from 'react' + +import { ReactHooksRenderer } from '../../src/types' + +// This verifies that if RHTL_SKIP_AUTO_CLEANUP is set +// then we DON'T auto-wire up the afterEach for folks +describe('skip auto cleanup (disabled) tests', () => { + let cleanupCalled = false + let renderHook: (arg0: () => void) => void + + beforeAll(() => { + process.env.RHTL_SKIP_AUTO_CLEANUP = 'true' + // eslint-disable-next-line @typescript-eslint/no-var-requires + renderHook = (require('../../src/dom') as ReactHooksRenderer).renderHook + }) + + test('first', () => { + const hookWithCleanup = () => { + useEffect(() => { + return () => { + cleanupCalled = true + } + }) + } + renderHook(() => hookWithCleanup()) + }) + + test('second', () => { + expect(cleanupCalled).toBe(false) + }) +}) diff --git a/test/dom/autoCleanup.noAfterEach.ts b/test/dom/autoCleanup.noAfterEach.ts new file mode 100644 index 00000000..30e567a1 --- /dev/null +++ b/test/dom/autoCleanup.noAfterEach.ts @@ -0,0 +1,33 @@ +import { useEffect } from 'react' + +import { ReactHooksRenderer } from '../../src/types' + +// This verifies that if RHTL_SKIP_AUTO_CLEANUP is set +// then we DON'T auto-wire up the afterEach for folks +describe('skip auto cleanup (no afterEach) tests', () => { + let cleanupCalled = false + 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 @typescript-eslint/no-var-requires + renderHook = (require('../../src/dom') as ReactHooksRenderer).renderHook + }) + + test('first', () => { + const hookWithCleanup = () => { + useEffect(() => { + return () => { + cleanupCalled = true + } + }) + } + renderHook(() => hookWithCleanup()) + }) + + test('second', () => { + expect(cleanupCalled).toBe(false) + }) +}) diff --git a/test/dom/autoCleanup.ts b/test/dom/autoCleanup.ts new file mode 100644 index 00000000..b5585350 --- /dev/null +++ b/test/dom/autoCleanup.ts @@ -0,0 +1,24 @@ +import { useEffect } from 'react' +import { renderHook } from '../../src/dom' + +// This verifies that by importing RHTL in an +// environment which supports afterEach (like Jest) +// we'll get automatic cleanup between tests. +describe('auto cleanup tests', () => { + let cleanupCalled = false + + test('first', () => { + const hookWithCleanup = () => { + useEffect(() => { + return () => { + cleanupCalled = true + } + }) + } + renderHook(() => hookWithCleanup()) + }) + + test('second', () => { + expect(cleanupCalled).toBe(true) + }) +}) diff --git a/test/dom/cleanup.ts b/test/dom/cleanup.ts new file mode 100644 index 00000000..aafa877b --- /dev/null +++ b/test/dom/cleanup.ts @@ -0,0 +1,135 @@ +import { useEffect } from 'react' +import { renderHook, cleanup, addCleanup, removeCleanup } from '../../src/dom/pure' + +describe('cleanup tests', () => { + test('should flush effects on cleanup', async () => { + let cleanupCalled = false + + const hookWithCleanup = () => { + useEffect(() => { + return () => { + cleanupCalled = true + } + }) + } + + renderHook(() => hookWithCleanup()) + + await cleanup() + + expect(cleanupCalled).toBe(true) + }) + + test('should cleanup all rendered hooks', async () => { + const cleanupCalled: boolean[] = [] + const hookWithCleanup = (id: number) => { + useEffect(() => { + return () => { + cleanupCalled[id] = true + } + }) + } + + renderHook(() => hookWithCleanup(1)) + renderHook(() => hookWithCleanup(2)) + + await cleanup() + + expect(cleanupCalled[1]).toBe(true) + expect(cleanupCalled[2]).toBe(true) + }) + + test('should call cleanups in reverse order', async () => { + const callSequence: string[] = [] + addCleanup(() => { + callSequence.push('cleanup') + }) + addCleanup(() => { + callSequence.push('another cleanup') + }) + const hookWithCleanup = () => { + useEffect(() => { + return () => { + callSequence.push('unmount') + } + }) + } + renderHook(() => hookWithCleanup()) + + await cleanup() + + expect(callSequence).toEqual(['unmount', 'another cleanup', 'cleanup']) + }) + + test('should wait for async cleanup', async () => { + const callSequence: string[] = [] + addCleanup(() => { + callSequence.push('cleanup') + }) + addCleanup(async () => { + await new Promise((resolve) => setTimeout(resolve, 10)) + callSequence.push('another cleanup') + }) + const hookWithCleanup = () => { + useEffect(() => { + return () => { + callSequence.push('unmount') + } + }) + } + renderHook(() => hookWithCleanup()) + + await cleanup() + + expect(callSequence).toEqual(['unmount', 'another cleanup', 'cleanup']) + }) + + test('should remove cleanup using removeCleanup', async () => { + const callSequence: string[] = [] + addCleanup(() => { + callSequence.push('cleanup') + }) + const anotherCleanup = () => { + callSequence.push('another cleanup') + } + addCleanup(anotherCleanup) + const hookWithCleanup = () => { + useEffect(() => { + return () => { + callSequence.push('unmount') + } + }) + } + renderHook(() => hookWithCleanup()) + + removeCleanup(anotherCleanup) + + await cleanup() + + expect(callSequence).toEqual(['unmount', 'cleanup']) + }) + + test('should remove cleanup using returned handler', async () => { + const callSequence: string[] = [] + addCleanup(() => { + callSequence.push('cleanup') + }) + const remove = addCleanup(() => { + callSequence.push('another cleanup') + }) + const hookWithCleanup = () => { + useEffect(() => { + return () => { + callSequence.push('unmount') + } + }) + } + renderHook(() => hookWithCleanup()) + + remove() + + await cleanup() + + expect(callSequence).toEqual(['unmount', 'cleanup']) + }) +}) diff --git a/test/dom/customHook.ts b/test/dom/customHook.ts new file mode 100644 index 00000000..ab1b859d --- /dev/null +++ b/test/dom/customHook.ts @@ -0,0 +1,29 @@ +import { useState, useCallback } from 'react' +import { renderHook, act } from '../../src/dom' + +describe('custom hook tests', () => { + function useCounter() { + const [count, setCount] = useState(0) + + const increment = useCallback(() => setCount(count + 1), [count]) + const decrement = useCallback(() => setCount(count - 1), [count]) + + return { count, increment, decrement } + } + + test('should increment counter', () => { + const { result } = renderHook(() => useCounter()) + + act(() => result.current.increment()) + + expect(result.current.count).toBe(1) + }) + + test('should decrement counter', () => { + const { result } = renderHook(() => useCounter()) + + act(() => result.current.decrement()) + + expect(result.current.count).toBe(-1) + }) +}) diff --git a/test/dom/errorHook.ts b/test/dom/errorHook.ts new file mode 100644 index 00000000..481c87b6 --- /dev/null +++ b/test/dom/errorHook.ts @@ -0,0 +1,150 @@ +import { useState, useEffect } from 'react' +import { renderHook } from '../../src/dom' + +describe('error hook tests', () => { + function useError(throwError: boolean | undefined) { + if (throwError) { + throw new Error('expected') + } + return true + } + + function useAsyncError(throwError: boolean) { + const [value, setValue] = useState() + useEffect(() => { + const timeout = setTimeout(() => setValue(throwError), 100) + return () => clearTimeout(timeout) + }, [throwError]) + return useError(value) + } + + function useEffectError(throwError: boolean) { + useEffect(() => { + useError(throwError) + }, []) + return true + } + + describe('synchronous', () => { + test('should raise error', () => { + const { result } = renderHook(() => useError(true)) + + expect(() => { + expect(result.current).not.toBe(undefined) + }).toThrow(Error('expected')) + }) + + test('should capture error', () => { + const { result } = renderHook(() => useError(true)) + + expect(result.error).toEqual(Error('expected')) + }) + + test('should not capture error', () => { + const { result } = renderHook(() => useError(false)) + + expect(result.current).not.toBe(undefined) + expect(result.error).toBe(undefined) + }) + + test('should reset error', () => { + const { result, rerender } = renderHook(({ throwError }) => useError(throwError), { + initialProps: { throwError: true } + }) + + expect(result.error).not.toBe(undefined) + + rerender({ throwError: false }) + + expect(result.current).not.toBe(undefined) + expect(result.error).toBe(undefined) + }) + }) + + describe('asynchronous', () => { + test('should raise async error', async () => { + const { result, waitForNextUpdate } = renderHook(() => useAsyncError(true)) + + await waitForNextUpdate() + + expect(() => { + expect(result.current).not.toBe(undefined) + }).toThrow(Error('expected')) + }) + + test('should capture async error', async () => { + const { result, waitForNextUpdate } = renderHook(() => useAsyncError(true)) + + await waitForNextUpdate() + + expect(result.error).toEqual(Error('expected')) + }) + + test('should not capture async error', async () => { + const { result, waitForNextUpdate } = renderHook(() => useAsyncError(false)) + + await waitForNextUpdate() + + expect(result.current).not.toBe(undefined) + expect(result.error).toBe(undefined) + }) + + test('should reset async error', async () => { + const { result, waitForNextUpdate, rerender } = renderHook( + ({ throwError }) => useAsyncError(throwError), + { initialProps: { throwError: true } } + ) + + await waitForNextUpdate() + + expect(result.error).not.toBe(undefined) + + rerender({ throwError: false }) + + await waitForNextUpdate() + + expect(result.current).not.toBe(undefined) + expect(result.error).toBe(undefined) + }) + }) + + /* + These tests capture error cases that are not currently being caught successfully. + Refer to https://github.com/testing-library/react-hooks-testing-library/issues/308 + for more details. + */ + describe.skip('effect', () => { + test('should raise effect error', () => { + const { result } = renderHook(() => useEffectError(true)) + + expect(() => { + expect(result.current).not.toBe(undefined) + }).toThrow(Error('expected')) + }) + + test('should capture effect error', () => { + const { result } = renderHook(() => useEffectError(true)) + expect(result.error).toEqual(Error('expected')) + }) + + test('should not capture effect error', () => { + const { result } = renderHook(() => useEffectError(false)) + + expect(result.current).not.toBe(undefined) + expect(result.error).toBe(undefined) + }) + + test('should reset effect error', () => { + const { result, rerender } = renderHook(({ throwError }) => useEffectError(throwError), { + initialProps: { throwError: true } + }) + + expect(result.error).not.toBe(undefined) + + rerender({ throwError: false }) + + expect(result.current).not.toBe(undefined) + expect(result.error).toBe(undefined) + }) + }) +}) diff --git a/test/dom/resultHistory.ts b/test/dom/resultHistory.ts new file mode 100644 index 00000000..68c84741 --- /dev/null +++ b/test/dom/resultHistory.ts @@ -0,0 +1,34 @@ +import { renderHook } from '../../src/dom' + +describe('result history tests', () => { + let count = 0 + function useCounter() { + const result = count++ + if (result === 2) { + throw Error('expected') + } + return result + } + + test('should capture all renders states of hook', () => { + const { result, rerender } = renderHook(() => useCounter()) + + expect(result.current).toEqual(0) + expect(result.all).toEqual([0]) + + rerender() + + expect(result.current).toBe(1) + expect(result.all).toEqual([0, 1]) + + rerender() + + expect(result.error).toEqual(Error('expected')) + expect(result.all).toEqual([0, 1, Error('expected')]) + + rerender() + + expect(result.current).toBe(3) + expect(result.all).toEqual([0, 1, Error('expected'), 3]) + }) +}) diff --git a/test/dom/suspenseHook.ts b/test/dom/suspenseHook.ts new file mode 100644 index 00000000..174d70b2 --- /dev/null +++ b/test/dom/suspenseHook.ts @@ -0,0 +1,49 @@ +import { renderHook } from '../../src/dom' + +describe('suspense hook tests', () => { + const cache: { value?: Promise | string | Error } = {} + const fetchName = (isSuccessful: boolean) => { + if (!cache.value) { + cache.value = new Promise((resolve, reject) => { + setTimeout(() => { + if (isSuccessful) { + resolve('Bob') + } else { + reject(new Error('Failed to fetch name')) + } + }, 50) + }) + .then((value) => (cache.value = value)) + .catch((e: Error) => (cache.value = e)) + } + return cache.value + } + + const useFetchName = (isSuccessful = true) => { + const name = fetchName(isSuccessful) + if (name instanceof Promise || name instanceof Error) { + throw name as unknown + } + return name + } + + beforeEach(() => { + delete cache.value + }) + + test('should allow rendering to be suspended', async () => { + const { result, waitForNextUpdate } = renderHook(() => useFetchName(true)) + + await waitForNextUpdate() + + expect(result.current).toBe('Bob') + }) + + test('should set error if suspense promise rejects', async () => { + const { result, waitForNextUpdate } = renderHook(() => useFetchName(false)) + + await waitForNextUpdate() + + expect(result.error).toEqual(new Error('Failed to fetch name')) + }) +}) diff --git a/test/dom/useContext.tsx b/test/dom/useContext.tsx new file mode 100644 index 00000000..0f88c548 --- /dev/null +++ b/test/dom/useContext.tsx @@ -0,0 +1,63 @@ +import React, { createContext, useContext } from 'react' +import { renderHook } from '../../src/dom' + +describe('useContext tests', () => { + test('should get default value from context', () => { + const TestContext = createContext('foo') + + const { result } = renderHook(() => useContext(TestContext)) + + const value = result.current + + expect(value).toBe('foo') + }) + + test('should get value from context provider', () => { + const TestContext = createContext('foo') + + const wrapper: React.FC = ({ children }) => ( + {children} + ) + + const { result } = renderHook(() => useContext(TestContext), { wrapper }) + + expect(result.current).toBe('bar') + }) + + test('should update mutated value in context', () => { + const TestContext = createContext('foo') + + const value = { current: 'bar' } + + const wrapper: React.FC = ({ children }) => ( + {children} + ) + + const { result, rerender } = renderHook(() => useContext(TestContext), { wrapper }) + + value.current = 'baz' + + rerender() + + expect(result.current).toBe('baz') + }) + + test('should update value in context when props are updated', () => { + const TestContext = createContext('foo') + + const wrapper: React.FC<{ current: string }> = ({ current, children }) => ( + {children} + ) + + const { result, rerender } = renderHook(() => useContext(TestContext), { + wrapper, + initialProps: { + current: 'bar' + } + }) + + rerender({ current: 'baz' }) + + expect(result.current).toBe('baz') + }) +}) diff --git a/test/dom/useEffect.ts b/test/dom/useEffect.ts new file mode 100644 index 00000000..b09c2fa6 --- /dev/null +++ b/test/dom/useEffect.ts @@ -0,0 +1,62 @@ +import { useEffect, useLayoutEffect } from 'react' +import { renderHook } from '../../src/dom' + +describe('useEffect tests', () => { + test('should handle useEffect hook', () => { + const sideEffect: { [key: number]: boolean } = { 1: false, 2: false } + + const { rerender, unmount } = renderHook( + ({ id }) => { + useEffect(() => { + sideEffect[id] = true + return () => { + sideEffect[id] = false + } + }, [id]) + }, + { initialProps: { id: 1 } } + ) + + expect(sideEffect[1]).toBe(true) + expect(sideEffect[2]).toBe(false) + + rerender({ id: 2 }) + + expect(sideEffect[1]).toBe(false) + expect(sideEffect[2]).toBe(true) + + unmount() + + expect(sideEffect[1]).toBe(false) + expect(sideEffect[2]).toBe(false) + }) + + test('should handle useLayoutEffect hook', () => { + const sideEffect: { [key: number]: boolean } = { 1: false, 2: false } + + const { rerender, unmount } = renderHook( + ({ id }) => { + useLayoutEffect(() => { + sideEffect[id] = true + return () => { + sideEffect[id] = false + } + }, [id]) + }, + { initialProps: { id: 1 } } + ) + + expect(sideEffect[1]).toBe(true) + expect(sideEffect[2]).toBe(false) + + rerender({ id: 2 }) + + expect(sideEffect[1]).toBe(false) + expect(sideEffect[2]).toBe(true) + + unmount() + + expect(sideEffect[1]).toBe(false) + expect(sideEffect[2]).toBe(false) + }) +}) diff --git a/test/dom/useMemo.ts b/test/dom/useMemo.ts new file mode 100644 index 00000000..f8a7e86a --- /dev/null +++ b/test/dom/useMemo.ts @@ -0,0 +1,64 @@ +import { useMemo, useCallback } from 'react' +import { renderHook } from '../../src/dom' + +describe('useCallback tests', () => { + test('should handle useMemo hook', () => { + const { result, rerender } = renderHook(({ value }) => useMemo(() => ({ value }), [value]), { + initialProps: { value: 1 } + }) + + const value1 = result.current + + expect(value1).toEqual({ value: 1 }) + + rerender() + + const value2 = result.current + + expect(value2).toEqual({ value: 1 }) + + expect(value2).toBe(value1) + + rerender({ value: 2 }) + + const value3 = result.current + + expect(value3).toEqual({ value: 2 }) + + expect(value3).not.toBe(value1) + }) + + test('should handle useCallback hook', () => { + const { result, rerender } = renderHook( + ({ value }) => { + const callback = () => ({ value }) + return useCallback(callback, [value]) + }, + { initialProps: { value: 1 } } + ) + + const callback1 = result.current + + const callbackValue1 = callback1() + + expect(callbackValue1).toEqual({ value: 1 }) + + const callback2 = result.current + + const callbackValue2 = callback2() + + expect(callbackValue2).toEqual({ value: 1 }) + + expect(callback2).toBe(callback1) + + rerender({ value: 2 }) + + const callback3 = result.current + + const callbackValue3 = callback3() + + expect(callbackValue3).toEqual({ value: 2 }) + + expect(callback3).not.toBe(callback1) + }) +}) diff --git a/test/dom/useReducer.ts b/test/dom/useReducer.ts new file mode 100644 index 00000000..0e9ff9e8 --- /dev/null +++ b/test/dom/useReducer.ts @@ -0,0 +1,20 @@ +import { useReducer } from 'react' +import { renderHook, act } from '../../src/dom' + +describe('useReducer tests', () => { + test('should handle useReducer hook', () => { + const reducer = (state: number, action: { type: string }) => + action.type === 'inc' ? state + 1 : state + const { result } = renderHook(() => useReducer(reducer, 0)) + + const [initialState, dispatch] = result.current + + expect(initialState).toBe(0) + + act(() => dispatch({ type: 'inc' })) + + const [state] = result.current + + expect(state).toBe(1) + }) +}) diff --git a/test/dom/useRef.ts b/test/dom/useRef.ts new file mode 100644 index 00000000..baca0ead --- /dev/null +++ b/test/dom/useRef.ts @@ -0,0 +1,27 @@ +import { useRef, useImperativeHandle } from 'react' +import { renderHook } from '../../src/dom' + +describe('useHook tests', () => { + test('should handle useRef hook', () => { + const { result } = renderHook(() => useRef()) + + const refContainer = result.current + + expect(Object.keys(refContainer)).toEqual(['current']) + expect(refContainer.current).toBeUndefined() + }) + + test('should handle useImperativeHandle hook', () => { + const { result } = renderHook(() => { + const ref = useRef boolean>>({}) + useImperativeHandle(ref, () => ({ + fakeImperativeMethod: () => true + })) + return ref + }) + + const refContainer = result.current + + expect(refContainer.current.fakeImperativeMethod()).toBe(true) + }) +}) diff --git a/test/dom/useState.ts b/test/dom/useState.ts new file mode 100644 index 00000000..e25c8bbe --- /dev/null +++ b/test/dom/useState.ts @@ -0,0 +1,24 @@ +import { useState } from 'react' +import { renderHook, act } from '../../src/dom' + +describe('useState tests', () => { + test('should use setState value', () => { + const { result } = renderHook(() => useState('foo')) + + const [value] = result.current + + expect(value).toBe('foo') + }) + + test('should update setState value using setter', () => { + const { result } = renderHook(() => useState('foo')) + + const [ignoredValue, setValue] = result.current + + act(() => setValue('bar')) + + const [value] = result.current + + expect(value).toBe('bar') + }) +}) diff --git a/test/tsconfig.json b/test/tsconfig.json index 8336d11a..3b6b7c4c 100644 --- a/test/tsconfig.json +++ b/test/tsconfig.json @@ -4,5 +4,5 @@ "declaration": false }, "exclude": [], - "include": ["./native/*", "./server/*"] + "include": ["./native/*", "./server/*", "./dom/*"] } From af079a4cfccd4eca3eb6bcaac05a8f0763a46288 Mon Sep 17 00:00:00 2001 From: Josh Ellis Date: Sun, 3 Jan 2021 10:24:44 +0000 Subject: [PATCH 18/53] change: extract toRender to be shared --- src/helpers/toRender.tsx | 30 ++++++++++++++++++++++++++++++ src/native/{pure.tsx => pure.ts} | 16 ++++++---------- src/server/{pure.tsx => pure.ts} | 30 +++++++++++++----------------- 3 files changed, 49 insertions(+), 27 deletions(-) create mode 100644 src/helpers/toRender.tsx rename src/native/{pure.tsx => pure.ts} (62%) rename src/server/{pure.tsx => pure.ts} (69%) diff --git a/src/helpers/toRender.tsx b/src/helpers/toRender.tsx new file mode 100644 index 00000000..ff7e61a8 --- /dev/null +++ b/src/helpers/toRender.tsx @@ -0,0 +1,30 @@ +import React, { Suspense } from 'react' +import TestHook from '../core/testHook' + +import { TestHookProps, WrapperComponent } from '../types' + +const toRender = ( + testHookProps: Omit, 'hookProps'>, + Wrapper: WrapperComponent, + suspense: boolean = true +) => { + return function RenderWrapper(props?: TProps) { + if (suspense) { + return ( + + + + + + ) + } else { + return ( + + + + ) + } + } +} + +export default toRender diff --git a/src/native/pure.tsx b/src/native/pure.ts similarity index 62% rename from src/native/pure.tsx rename to src/native/pure.ts index 09884251..50917edb 100644 --- a/src/native/pure.tsx +++ b/src/native/pure.ts @@ -1,32 +1,28 @@ -import React, { Suspense } from 'react' import { act, create, ReactTestRenderer } from 'react-test-renderer' import { TestHookProps, NativeRendererOptions, NativeRendererReturn } from '../types' import { createRenderHook, cleanup, addCleanup, removeCleanup } from '../core/index' -import TestHook from '../core/testHook' + +import toRender from '../helpers/toRender' function createNativeRenderer( testHookProps: Omit, 'hookProps'>, - { wrapper: Wrapper }: NativeRendererOptions + { wrapper }: NativeRendererOptions ): NativeRendererReturn { let container: ReactTestRenderer - const toRender = (props?: TProps): JSX.Element => ( - - - - ) + const testHook = toRender(testHookProps, wrapper) return { render(props) { act(() => { - container = create({toRender(props)}) + container = create(testHook(props)) }) }, rerender(props) { act(() => { - container.update({toRender(props)}) + container.update(testHook(props)) }) }, unmount() { diff --git a/src/server/pure.tsx b/src/server/pure.ts similarity index 69% rename from src/server/pure.tsx rename to src/server/pure.ts index cfcc6145..6cd16524 100644 --- a/src/server/pure.tsx +++ b/src/server/pure.ts @@ -1,4 +1,3 @@ -import React from 'react' import ReactDOMServer from 'react-dom/server' import ReactDOM from 'react-dom' import { act as baseAct } from 'react-dom/test-utils' @@ -7,45 +6,42 @@ import { TestHookProps, ServerRendererOptions, ServerRendererReturn, - ServerActCallback, - ServerActCallbackAsync, - ServerModifiedAct + ReactDomActCallbackAsync, + ReactDomActCallback, + ReactDomAct } from '../types' import { createRenderHook, cleanup } from '../core/index' -import TestHook from '../core/testHook' + +import toRender from '../helpers/toRender' // eslint-disable-next-line import/no-mutable-exports -let act: ServerModifiedAct +let act: ReactDomAct function createServerRenderer( testHookProps: Omit, 'hookProps'>, - { wrapper: Wrapper }: ServerRendererOptions + { wrapper }: ServerRendererOptions ): ServerRendererReturn { const container = document.createElement('div') - const toRender = (props?: TProps): JSX.Element => ( - - - - ) + const testHook = toRender(testHookProps, wrapper, false) let renderProps: TProps | undefined let hydrated = false - act = (cb: ServerActCallbackAsync | ServerActCallback) => { + act = (cb: ReactDomActCallbackAsync | ReactDomActCallback) => { if (!hydrated) { throw new Error('You must hydrate the component before you can act') } - return baseAct(cb as ServerActCallback) + return baseAct(cb as ReactDomActCallback) } return { render(props) { renderProps = props baseAct(() => { - const serverOutput = ReactDOMServer.renderToString(toRender(props)) + const serverOutput = ReactDOMServer.renderToString(testHook(props)) container.innerHTML = serverOutput }) }, @@ -55,7 +51,7 @@ function createServerRenderer( } else { document.body.appendChild(container) baseAct(() => { - ReactDOM.hydrate(toRender(renderProps), container) + ReactDOM.hydrate(testHook(renderProps), container) }) hydrated = true } @@ -65,7 +61,7 @@ function createServerRenderer( throw new Error('You must hydrate the component before you can rerender') } baseAct(() => { - ReactDOM.render(toRender(props), container) + ReactDOM.render(testHook(props), container) }) }, unmount() { From ec732c5613b56e918f90c78c559dd90de9fe6c5e Mon Sep 17 00:00:00 2001 From: Josh Ellis Date: Sun, 3 Jan 2021 10:25:33 +0000 Subject: [PATCH 19/53] feat: add dom types --- src/core/index.tsx | 5 ++-- src/types.ts | 32 ++++++++++++++++++-------- test/native/autoCleanup.disabled.ts | 2 +- test/native/autoCleanup.noAfterEach.ts | 2 +- test/server/autoCleanup.disabled.ts | 2 +- test/server/autoCleanup.noAfterEach.ts | 2 +- 6 files changed, 29 insertions(+), 16 deletions(-) diff --git a/src/core/index.tsx b/src/core/index.tsx index b650ce96..781efa12 100644 --- a/src/core/index.tsx +++ b/src/core/index.tsx @@ -8,7 +8,8 @@ import { RenderHookOptions, RenderResult, ServerRendererReturn, - ServerRendererOptions + ServerRendererOptions, + DomRendererReturn } from '../types' import asyncUtils from './asyncUtils' @@ -59,7 +60,7 @@ const createRenderHook = ( createRenderer: ( testProps: Omit, 'hookProps'>, opts: NativeRendererOptions | ServerRendererOptions - ) => NativeRendererReturn | ServerRendererReturn + ) => NativeRendererReturn | ServerRendererReturn | DomRendererReturn ) => ( callback: (props: TProps) => TResult, { initialProps, wrapper = defaultWrapper }: RenderHookOptions = {} diff --git a/src/types.ts b/src/types.ts index dab811a4..4cdff79d 100644 --- a/src/types.ts +++ b/src/types.ts @@ -4,7 +4,7 @@ * */ -export type ActTypes = NativeModifedAct | ServerModifiedAct +export type ActTypes = NativeModifedAct | ReactDomAct export interface WaitOptions { interval?: number @@ -24,6 +24,14 @@ export type GenericRendererReturn = { unmount: () => void } +export type ReactDomAct = ( + cb: ReactDomActCallbackAsync | ReactDomActCallback +) => Promise | void + +export type ReactDomActCallbackAsync = () => Promise + +export type ReactDomActCallback = () => void | undefined + /** * * pure @@ -122,15 +130,19 @@ export type NativeRendererOptions = GenericRendererOptions export type ServerRendererOptions = GenericRendererOptions -export type ServerActCallbackAsync = () => Promise - -export type ServerActCallback = () => void | undefined - -export type ServerModifiedAct = ( - cb: ServerActCallbackAsync | ServerActCallback -) => Promise | void - export interface ServerRendererReturn extends GenericRendererReturn { - act: ServerModifiedAct + act: ReactDomAct hydrate: () => void } + +/** + * + * dom/pure + * + */ + +export interface DomRendererReturn extends GenericRendererReturn { + act: ReactDomAct +} + +export type DomRendererOptions = GenericRendererOptions diff --git a/test/native/autoCleanup.disabled.ts b/test/native/autoCleanup.disabled.ts index b43794d5..98b18f6b 100644 --- a/test/native/autoCleanup.disabled.ts +++ b/test/native/autoCleanup.disabled.ts @@ -1,6 +1,6 @@ import { useEffect } from 'react' -import { ReactHooksRenderer } from 'types' +import { ReactHooksRenderer } from '../../src/types' // This verifies that if RHTL_SKIP_AUTO_CLEANUP is set // then we DON'T auto-wire up the afterEach for folks diff --git a/test/native/autoCleanup.noAfterEach.ts b/test/native/autoCleanup.noAfterEach.ts index 49b00b3d..f973cb0d 100644 --- a/test/native/autoCleanup.noAfterEach.ts +++ b/test/native/autoCleanup.noAfterEach.ts @@ -1,6 +1,6 @@ import { useEffect } from 'react' -import { ReactHooksRenderer } from 'types' +import { ReactHooksRenderer } from '../../src/types' // This verifies that if RHTL_SKIP_AUTO_CLEANUP is set // then we DON'T auto-wire up the afterEach for folks diff --git a/test/server/autoCleanup.disabled.ts b/test/server/autoCleanup.disabled.ts index 00853a13..4a54b11f 100644 --- a/test/server/autoCleanup.disabled.ts +++ b/test/server/autoCleanup.disabled.ts @@ -1,6 +1,6 @@ import { useEffect } from 'react' -import { ReactHooksRenderer } from 'types' +import { ReactHooksRenderer } from '../../src/types' // This verifies that if RHTL_SKIP_AUTO_CLEANUP is set // then we DON'T auto-wire up the afterEach for folks diff --git a/test/server/autoCleanup.noAfterEach.ts b/test/server/autoCleanup.noAfterEach.ts index 180dbea3..5bc03ece 100644 --- a/test/server/autoCleanup.noAfterEach.ts +++ b/test/server/autoCleanup.noAfterEach.ts @@ -1,6 +1,6 @@ import { useEffect } from 'react' -import { ReactHooksRenderer } from 'types' +import { ReactHooksRenderer } from '../../src/types' // This verifies that if RHTL_SKIP_AUTO_CLEANUP is set // then we DON'T auto-wire up the afterEach for folks From 53a339e4a6dbb64b4f0008b6d22031fa7cf7a04e Mon Sep 17 00:00:00 2001 From: Josh Ellis Date: Sun, 3 Jan 2021 10:35:20 +0000 Subject: [PATCH 20/53] test: formatting --- src/custom/pure.ts | 7 +++---- test/dom/errorHook.ts | 2 +- test/native/errorHook.ts | 2 +- test/native/useContext.tsx | 5 ++--- test/server/errorHook.ts | 3 ++- 5 files changed, 9 insertions(+), 10 deletions(-) diff --git a/src/custom/pure.ts b/src/custom/pure.ts index 161fe81e..bdbc7068 100644 --- a/src/custom/pure.ts +++ b/src/custom/pure.ts @@ -1,9 +1,8 @@ import { createRenderHook, cleanup } from '../core/index' // eslint-disable-next-line @typescript-eslint/no-explicit-any -function createCustomRenderer(createRenderer: any) { - const renderHook = createRenderHook(createRenderer) - return { renderHook } -} +const createCustomRenderer = (createRenderer: any) => ({ + renderHook: createRenderHook(createRenderer) +}) export { createCustomRenderer, cleanup } diff --git a/test/dom/errorHook.ts b/test/dom/errorHook.ts index 481c87b6..fb293d59 100644 --- a/test/dom/errorHook.ts +++ b/test/dom/errorHook.ts @@ -21,7 +21,7 @@ describe('error hook tests', () => { function useEffectError(throwError: boolean) { useEffect(() => { useError(throwError) - }, []) + }, [throwError]) return true } diff --git a/test/native/errorHook.ts b/test/native/errorHook.ts index adc3aeba..19bdb8d3 100644 --- a/test/native/errorHook.ts +++ b/test/native/errorHook.ts @@ -21,7 +21,7 @@ describe('error hook tests', () => { function useEffectError(throwError: boolean) { useEffect(() => { useError(throwError) - }, []) + }, [throwError]) return true } diff --git a/test/native/useContext.tsx b/test/native/useContext.tsx index a28e013c..c306fb21 100644 --- a/test/native/useContext.tsx +++ b/test/native/useContext.tsx @@ -15,7 +15,7 @@ describe('useContext tests', () => { test('should get value from context provider', () => { const TestContext = createContext('foo') - const wrapper: React.FC = ({ children } ) => ( + const wrapper: React.FC = ({ children }) => ( {children} ) @@ -45,8 +45,7 @@ describe('useContext tests', () => { test('should update value in context when props are updated', () => { const TestContext = createContext('foo') - - const wrapper: React.FC<{current: string}> = ({ current, children }) => ( + const wrapper: React.FC<{ current: string }> = ({ current, children }) => ( {children} ) diff --git a/test/server/errorHook.ts b/test/server/errorHook.ts index 511d2d77..1ba6d82e 100644 --- a/test/server/errorHook.ts +++ b/test/server/errorHook.ts @@ -1,4 +1,5 @@ import { useState, useEffect } from 'react' + import { renderHook } from '../../src/server' describe('error hook tests', () => { @@ -21,7 +22,7 @@ describe('error hook tests', () => { function useEffectError(throwError: boolean) { useEffect(() => { useError(throwError) - }, []) + }, [throwError]) return true } From 7be2c802d439a499807d6d98a44bcf26ae63104d Mon Sep 17 00:00:00 2001 From: Josh Ellis Date: Sun, 3 Jan 2021 10:56:10 +0000 Subject: [PATCH 21/53] change: create overload for createRenderHook this lets us return `hydrate` on a ServerRendererReturn --- src/core/index.tsx | 81 ++++++++++++++++++++++++++++------------------ 1 file changed, 50 insertions(+), 31 deletions(-) diff --git a/src/core/index.tsx b/src/core/index.tsx index 781efa12..c1c46d2a 100644 --- a/src/core/index.tsx +++ b/src/core/index.tsx @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/no-unsafe-call */ import React from 'react' import { @@ -9,7 +10,9 @@ import { RenderResult, ServerRendererReturn, ServerRendererOptions, - DomRendererReturn + DomRendererReturn, + RenderHookReturn, + ServerRenderHook } from '../types' import asyncUtils from './asyncUtils' @@ -56,45 +59,61 @@ function defaultWrapper({ children }: { children?: React.ReactNode }) { return (children as unknown) as JSX.Element } -const createRenderHook = ( +function createRenderHook( createRenderer: ( testProps: Omit, 'hookProps'>, - opts: NativeRendererOptions | ServerRendererOptions - ) => NativeRendererReturn | ServerRendererReturn | DomRendererReturn -) => ( + opts: ServerRendererOptions + ) => ServerRendererReturn +): ( callback: (props: TProps) => TResult, - { initialProps, wrapper = defaultWrapper }: RenderHookOptions = {} -) => { - const { result, setValue, setError, addResolver } = resultContainer() - const hookProps = { current: initialProps } - const props = { callback, setValue, setError } - const options = { wrapper } + opts?: RenderHookOptions +) => ServerRenderHook +function createRenderHook( + createRenderer: ( + testProps: Omit, 'hookProps'>, + opts: NativeRendererOptions + ) => NativeRendererReturn | DomRendererReturn +): ( + callback: (props: TProps) => TResult, + opts?: RenderHookOptions +) => RenderHookReturn +function createRenderHook( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + createRenderer: any +) { + return ( + callback: (props: TProps) => TResult, + { initialProps, wrapper = defaultWrapper }: RenderHookOptions = {} + ) => { + const { result, setValue, setError, addResolver } = resultContainer() + const hookProps = { current: initialProps } + const props = { callback, setValue, setError } + const options = { wrapper } - const { render, rerender, unmount, act, ...renderUtils } = createRenderer( - props, - options - ) + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + const { render, rerender, unmount, act, ...renderUtils } = createRenderer(props, options) - render(hookProps.current) + render(hookProps.current) - function rerenderHook(newProps = hookProps.current) { - hookProps.current = newProps - rerender(hookProps.current) - } + function rerenderHook(newProps = hookProps.current) { + hookProps.current = newProps + rerender(hookProps.current) + } - function unmountHook() { - removeCleanup(unmountHook) - unmount() - } + function unmountHook() { + removeCleanup(unmountHook) + unmount() + } - addCleanup(unmountHook) + addCleanup(unmountHook) - return { - result, - rerender: rerenderHook, - unmount: unmountHook, - ...asyncUtils(act, addResolver), - ...renderUtils + return { + result, + rerender: rerenderHook, + unmount: unmountHook, + ...asyncUtils(act, addResolver), + ...renderUtils + } } } From 9b57e0dca9e9150868a7ca4fd2e5312d3b65f147 Mon Sep 17 00:00:00 2001 From: Josh Ellis Date: Sun, 3 Jan 2021 10:59:16 +0000 Subject: [PATCH 22/53] change: update contributors --- .all-contributorsrc | 7 ++++++- README.md | 3 ++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/.all-contributorsrc b/.all-contributorsrc index d7d137c5..ae0afc76 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -201,7 +201,12 @@ "avatar_url": "https://avatars0.githubusercontent.com/u/37798644?v=4", "profile": "https://github.com/joshuaellis", "contributions": [ - "doc" + "doc", + "code", + "design", + "ideas", + "maintenance", + "test" ] }, { diff --git a/README.md b/README.md index b62c279d..19c6dd5a 100644 --- a/README.md +++ b/README.md @@ -42,6 +42,7 @@ + - [The problem](#the-problem) - [The solution](#the-solution) - [When to use this library](#when-to-use-this-library) @@ -189,7 +190,7 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
Adam Seckel

💻
keiya sasaki

⚠️
Hu Chen

💻 📖 💡 -
Josh

📖 +
Josh

📖 💻 🎨 🤔 🚧 ⚠️
Na'aman Hirschfeld

💻 From cfd3df094213b07f2531750f896af4413e30dc89 Mon Sep 17 00:00:00 2001 From: Josh Ellis Date: Sun, 3 Jan 2021 15:58:13 +0000 Subject: [PATCH 23/53] change: dom should be default renderer for react-dom --- src/pure.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pure.ts b/src/pure.ts index b3141d82..e1f6d755 100644 --- a/src/pure.ts +++ b/src/pure.ts @@ -2,7 +2,7 @@ import { RenderingEngineArray, ReactHooksRenderer } from 'types' const RENDERERS: RenderingEngineArray = [ { required: 'react-test-renderer', renderer: './native/pure' }, - { required: 'react-dom', renderer: './server/pure' } + { required: 'react-dom', renderer: './dom/pure' } ] function getRenderer(renderers: RenderingEngineArray): string { From 125c327f2c33acc7e7b1b90cf4fb62c1cd57c079 Mon Sep 17 00:00:00 2001 From: Josh Ellis Date: Sun, 3 Jan 2021 16:06:22 +0000 Subject: [PATCH 24/53] change: add dom to files --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index e620a4e9..c9252eb5 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,7 @@ "files": [ "lib", "src", + "dom", "native", "server", "pure.js", From ae3f2f706ab054cb7267c7bfb9f0e56457c65019 Mon Sep 17 00:00:00 2001 From: Josh Ellis Date: Sun, 3 Jan 2021 16:09:29 +0000 Subject: [PATCH 25/53] change: @types/react-dom should be >=16.9.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index c9252eb5..2f80cd49 100644 --- a/package.json +++ b/package.json @@ -43,7 +43,7 @@ "dependencies": { "@babel/runtime": "^7.12.5", "@types/react": ">=16.9.0", - "@types/react-dom": "^17.0.0", + "@types/react-dom": ">=16.9.0", "@types/react-test-renderer": ">=16.9.0" }, "devDependencies": { From 0e73e218774754ab49b7159db9b80731ebaafe8c Mon Sep 17 00:00:00 2001 From: Josh Ellis Date: Sun, 3 Jan 2021 16:13:50 +0000 Subject: [PATCH 26/53] change: use generic RendererOptions instead of specific to renderer --- src/core/index.tsx | 7 +++---- src/dom/pure.ts | 4 ++-- src/native/pure.ts | 4 ++-- src/server/pure.ts | 4 ++-- src/types.ts | 8 +------- 5 files changed, 10 insertions(+), 17 deletions(-) diff --git a/src/core/index.tsx b/src/core/index.tsx index c1c46d2a..38a0fbeb 100644 --- a/src/core/index.tsx +++ b/src/core/index.tsx @@ -2,14 +2,13 @@ import React from 'react' import { + RendererOptions, TestHookProps, - NativeRendererOptions, NativeRendererReturn, ResultContainerReturn, RenderHookOptions, RenderResult, ServerRendererReturn, - ServerRendererOptions, DomRendererReturn, RenderHookReturn, ServerRenderHook @@ -62,7 +61,7 @@ function defaultWrapper({ children }: { children?: React.ReactNode }) { function createRenderHook( createRenderer: ( testProps: Omit, 'hookProps'>, - opts: ServerRendererOptions + opts: RendererOptions ) => ServerRendererReturn ): ( callback: (props: TProps) => TResult, @@ -71,7 +70,7 @@ function createRenderHook( function createRenderHook( createRenderer: ( testProps: Omit, 'hookProps'>, - opts: NativeRendererOptions + opts: RendererOptions ) => NativeRendererReturn | DomRendererReturn ): ( callback: (props: TProps) => TResult, diff --git a/src/dom/pure.ts b/src/dom/pure.ts index c9c789c1..0392522a 100644 --- a/src/dom/pure.ts +++ b/src/dom/pure.ts @@ -3,7 +3,7 @@ import { act as baseAct } from 'react-dom/test-utils' import { TestHookProps, - DomRendererOptions, + RendererOptions, DomRendererReturn, ReactDomAct, ReactDomActCallbackAsync, @@ -19,7 +19,7 @@ let act: ReactDomAct function createDomRenderer( testHookProps: Omit, 'hookProps'>, - { wrapper }: DomRendererOptions + { wrapper }: RendererOptions ): DomRendererReturn { const container = document.createElement('div') diff --git a/src/native/pure.ts b/src/native/pure.ts index 50917edb..a44b5f63 100644 --- a/src/native/pure.ts +++ b/src/native/pure.ts @@ -1,6 +1,6 @@ import { act, create, ReactTestRenderer } from 'react-test-renderer' -import { TestHookProps, NativeRendererOptions, NativeRendererReturn } from '../types' +import { TestHookProps, RendererOptions, NativeRendererReturn } from '../types' import { createRenderHook, cleanup, addCleanup, removeCleanup } from '../core/index' @@ -8,7 +8,7 @@ import toRender from '../helpers/toRender' function createNativeRenderer( testHookProps: Omit, 'hookProps'>, - { wrapper }: NativeRendererOptions + { wrapper }: RendererOptions ): NativeRendererReturn { let container: ReactTestRenderer diff --git a/src/server/pure.ts b/src/server/pure.ts index 6cd16524..922a8b8f 100644 --- a/src/server/pure.ts +++ b/src/server/pure.ts @@ -4,7 +4,7 @@ import { act as baseAct } from 'react-dom/test-utils' import { TestHookProps, - ServerRendererOptions, + RendererOptions, ServerRendererReturn, ReactDomActCallbackAsync, ReactDomActCallback, @@ -20,7 +20,7 @@ let act: ReactDomAct function createServerRenderer( testHookProps: Omit, 'hookProps'>, - { wrapper }: ServerRendererOptions + { wrapper }: RendererOptions ): ServerRendererReturn { const container = document.createElement('div') diff --git a/src/types.ts b/src/types.ts index 4cdff79d..90a8336f 100644 --- a/src/types.ts +++ b/src/types.ts @@ -14,7 +14,7 @@ export interface WaitOptions { export type WrapperComponent = React.ComponentType -export type GenericRendererOptions = { +export type RendererOptions = { wrapper: WrapperComponent } @@ -120,16 +120,12 @@ export interface NativeRendererReturn extends GenericRendererReturn = GenericRendererOptions - /** * * server/pure * */ -export type ServerRendererOptions = GenericRendererOptions - export interface ServerRendererReturn extends GenericRendererReturn { act: ReactDomAct hydrate: () => void @@ -144,5 +140,3 @@ export interface ServerRendererReturn extends GenericRendererReturn extends GenericRendererReturn { act: ReactDomAct } - -export type DomRendererOptions = GenericRendererOptions From 48af5ce71d4e86514d7e8c0bc5d372f4d2ec2b80 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 4 Jan 2021 05:22:10 +0000 Subject: [PATCH 27/53] chore(deps-dev): bump eslint from 7.16.0 to 7.17.0 Bumps [eslint](https://github.com/eslint/eslint) from 7.16.0 to 7.17.0. - [Release notes](https://github.com/eslint/eslint/releases) - [Changelog](https://github.com/eslint/eslint/blob/master/CHANGELOG.md) - [Commits](https://github.com/eslint/eslint/compare/v7.16.0...v7.17.0) Signed-off-by: dependabot[bot] --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 93700417..03610292 100644 --- a/package.json +++ b/package.json @@ -49,7 +49,7 @@ "react": "17.0.1", "react-test-renderer": "17.0.1", "typescript": "4.1.3", - "eslint": "7.16.0" + "eslint": "7.17.0" }, "peerDependencies": { "react": ">=16.9.0", From 37739381b873b9ff2221fc10e6e19bd4f411e164 Mon Sep 17 00:00:00 2001 From: Michael Peyper Date: Mon, 4 Jan 2021 20:38:27 +1100 Subject: [PATCH 28/53] chore(refactor): moved require into getRenderer --- src/pure.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/pure.ts b/src/pure.ts index e1f6d755..6eeb2ba8 100644 --- a/src/pure.ts +++ b/src/pure.ts @@ -5,7 +5,7 @@ const RENDERERS: RenderingEngineArray = [ { required: 'react-dom', renderer: './dom/pure' } ] -function getRenderer(renderers: RenderingEngineArray): string { +function getRenderer(renderers: RenderingEngineArray) { const hasDependency = (name: string) => { try { require(name) @@ -18,14 +18,14 @@ function getRenderer(renderers: RenderingEngineArray): string { const [validRenderer] = renderers.filter(({ required }) => hasDependency(required)) if (validRenderer) { - return validRenderer.renderer + // eslint-disable-next-line @typescript-eslint/no-var-requires + return require(validRenderer.renderer) as ReactHooksRenderer } else { const options = renderers.map(({ renderer }) => ` - ${renderer}`).join('\n') throw new Error(`Could not auto-detect a React renderer. Options are:\n${options}`) } } -// eslint-disable-next-line @typescript-eslint/no-var-requires -const { renderHook, act, cleanup } = require(getRenderer(RENDERERS)) as ReactHooksRenderer +const { renderHook, act, cleanup } = getRenderer(RENDERERS) export { renderHook, act, cleanup } From 19d74ce2ea16ee4bc6ea84cc275dda4a4df8df29 Mon Sep 17 00:00:00 2001 From: Michael Peyper Date: Mon, 4 Jan 2021 20:51:22 +1100 Subject: [PATCH 29/53] fix: improve error message when renderer can't be auto-detected --- src/pure.ts | 26 ++++++++++++++++---------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/src/pure.ts b/src/pure.ts index 6eeb2ba8..bf658b6c 100644 --- a/src/pure.ts +++ b/src/pure.ts @@ -5,23 +5,29 @@ const RENDERERS: RenderingEngineArray = [ { required: 'react-dom', renderer: './dom/pure' } ] -function getRenderer(renderers: RenderingEngineArray) { - const hasDependency = (name: string) => { - try { - require(name) - return true - } catch { - return false - } +const KNOWN_RENDERERS = [ + '@testing-library/react-hooks/dom', + '@testing-library/react-hooks/native', + '@testing-library/react-hooks/server' +] + +function hasDependency(name: string) { + try { + require(name) + return true + } catch { + return false } +} - const [validRenderer] = renderers.filter(({ required }) => hasDependency(required)) +function getRenderer(renderers: RenderingEngineArray) { + const validRenderer = renderers.find(({ required }) => hasDependency(required)) if (validRenderer) { // eslint-disable-next-line @typescript-eslint/no-var-requires return require(validRenderer.renderer) as ReactHooksRenderer } else { - const options = renderers.map(({ renderer }) => ` - ${renderer}`).join('\n') + const options = KNOWN_RENDERERS.map((renderer) => ` - ${renderer}`).join('\n') throw new Error(`Could not auto-detect a React renderer. Options are:\n${options}`) } } From bad29f96986bb52c3451339932830659acd51293 Mon Sep 17 00:00:00 2001 From: Michael Peyper Date: Mon, 4 Jan 2021 22:13:53 +1100 Subject: [PATCH 30/53] feat: remove renderer specific types from core renderHook logic --- src/core/asyncUtils.ts | 4 +- src/core/index.tsx | 85 +++++++++++++++--------------------------- src/dom/pure.ts | 25 +++---------- src/native/pure.ts | 5 +-- src/server/pure.ts | 24 ++++++------ src/types.ts | 63 ++++++++++--------------------- 6 files changed, 71 insertions(+), 135 deletions(-) diff --git a/src/core/asyncUtils.ts b/src/core/asyncUtils.ts index e11e908d..2c8cdc02 100644 --- a/src/core/asyncUtils.ts +++ b/src/core/asyncUtils.ts @@ -1,9 +1,9 @@ -import { ActTypes, WaitOptions, AsyncUtilsReturn } from '../types' +import { Act, WaitOptions, AsyncUtilsReturn } from '../types' import { resolveAfter } from '../helpers/promises' import { TimeoutError } from '../helpers/error' -function asyncUtils(act: ActTypes, addResolver: (callback: () => void) => void): AsyncUtilsReturn { +function asyncUtils(act: Act, addResolver: (callback: () => void) => void): AsyncUtilsReturn { let nextUpdatePromise: Promise | null = null const waitForNextUpdate = async ({ timeout }: Pick = {}) => { diff --git a/src/core/index.tsx b/src/core/index.tsx index 38a0fbeb..c4aa47d0 100644 --- a/src/core/index.tsx +++ b/src/core/index.tsx @@ -2,16 +2,12 @@ import React from 'react' import { - RendererOptions, - TestHookProps, - NativeRendererReturn, + CreateRenderer, + RendererUtils, ResultContainerReturn, RenderHookOptions, RenderResult, - ServerRendererReturn, - DomRendererReturn, - RenderHookReturn, - ServerRenderHook + RenderHookReturn } from '../types' import asyncUtils from './asyncUtils' @@ -58,61 +54,40 @@ function defaultWrapper({ children }: { children?: React.ReactNode }) { return (children as unknown) as JSX.Element } -function createRenderHook( - createRenderer: ( - testProps: Omit, 'hookProps'>, - opts: RendererOptions - ) => ServerRendererReturn -): ( +const createRenderHook = ( + createRenderer: TCreateRenderer +) => ( callback: (props: TProps) => TResult, - opts?: RenderHookOptions -) => ServerRenderHook -function createRenderHook( - createRenderer: ( - testProps: Omit, 'hookProps'>, - opts: RendererOptions - ) => NativeRendererReturn | DomRendererReturn -): ( - callback: (props: TProps) => TResult, - opts?: RenderHookOptions -) => RenderHookReturn -function createRenderHook( - // eslint-disable-next-line @typescript-eslint/no-explicit-any - createRenderer: any -) { - return ( - callback: (props: TProps) => TResult, - { initialProps, wrapper = defaultWrapper }: RenderHookOptions = {} - ) => { - const { result, setValue, setError, addResolver } = resultContainer() - const hookProps = { current: initialProps } - const props = { callback, setValue, setError } - const options = { wrapper } + { initialProps, wrapper = defaultWrapper }: RenderHookOptions = {} +): RenderHookReturn & RendererUtils> => { + const { result, setValue, setError, addResolver } = resultContainer() + const hookProps = { current: initialProps } + const props = { callback, setValue, setError } + const options = { wrapper } - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment - const { render, rerender, unmount, act, ...renderUtils } = createRenderer(props, options) + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + const { render, rerender, unmount, act, ...renderUtils } = createRenderer(props, options) - render(hookProps.current) + render(hookProps.current) - function rerenderHook(newProps = hookProps.current) { - hookProps.current = newProps - rerender(hookProps.current) - } + function rerenderHook(newProps = hookProps.current) { + hookProps.current = newProps + rerender(hookProps.current) + } - function unmountHook() { - removeCleanup(unmountHook) - unmount() - } + function unmountHook() { + removeCleanup(unmountHook) + unmount() + } - addCleanup(unmountHook) + addCleanup(unmountHook) - return { - result, - rerender: rerenderHook, - unmount: unmountHook, - ...asyncUtils(act, addResolver), - ...renderUtils - } + return { + result, + rerender: rerenderHook, + unmount: unmountHook, + ...asyncUtils(act, addResolver), + ...(renderUtils as RendererUtils>) } } diff --git a/src/dom/pure.ts b/src/dom/pure.ts index 0392522a..72475067 100644 --- a/src/dom/pure.ts +++ b/src/dom/pure.ts @@ -1,46 +1,33 @@ import ReactDOM from 'react-dom' -import { act as baseAct } from 'react-dom/test-utils' +import { act } from 'react-dom/test-utils' -import { - TestHookProps, - RendererOptions, - DomRendererReturn, - ReactDomAct, - ReactDomActCallbackAsync, - ReactDomActCallback -} from '../types' +import { TestHookProps, RendererOptions, Renderer } from '../types' import { createRenderHook, cleanup, addCleanup, removeCleanup } from '../core/index' - import toRender from '../helpers/toRender' -// eslint-disable-next-line import/no-mutable-exports -let act: ReactDomAct - function createDomRenderer( testHookProps: Omit, 'hookProps'>, { wrapper }: RendererOptions -): DomRendererReturn { +): Renderer { const container = document.createElement('div') const testHook = toRender(testHookProps, wrapper) - act = (cb: ReactDomActCallbackAsync | ReactDomActCallback) => baseAct(cb as ReactDomActCallback) - return { render(props) { document.body.appendChild(container) - baseAct(() => { + act(() => { ReactDOM.render(testHook(props), container) }) }, rerender(props) { - baseAct(() => { + act(() => { ReactDOM.render(testHook(props), container) }) }, unmount() { - baseAct(() => { + act(() => { ReactDOM.unmountComponentAtNode(container) }) document.body.removeChild(container) diff --git a/src/native/pure.ts b/src/native/pure.ts index a44b5f63..5e5cbbe1 100644 --- a/src/native/pure.ts +++ b/src/native/pure.ts @@ -1,15 +1,14 @@ import { act, create, ReactTestRenderer } from 'react-test-renderer' -import { TestHookProps, RendererOptions, NativeRendererReturn } from '../types' +import { TestHookProps, RendererOptions, Renderer } from '../types' import { createRenderHook, cleanup, addCleanup, removeCleanup } from '../core/index' - import toRender from '../helpers/toRender' function createNativeRenderer( testHookProps: Omit, 'hookProps'>, { wrapper }: RendererOptions -): NativeRendererReturn { +): Renderer { let container: ReactTestRenderer const testHook = toRender(testHookProps, wrapper) diff --git a/src/server/pure.ts b/src/server/pure.ts index 922a8b8f..d0e73fcd 100644 --- a/src/server/pure.ts +++ b/src/server/pure.ts @@ -2,26 +2,19 @@ import ReactDOMServer from 'react-dom/server' import ReactDOM from 'react-dom' import { act as baseAct } from 'react-dom/test-utils' -import { - TestHookProps, - RendererOptions, - ServerRendererReturn, - ReactDomActCallbackAsync, - ReactDomActCallback, - ReactDomAct -} from '../types' +import { TestHookProps, RendererOptions, ServerRenderer } from '../types' import { createRenderHook, cleanup } from '../core/index' import toRender from '../helpers/toRender' // eslint-disable-next-line import/no-mutable-exports -let act: ReactDomAct +let act: typeof baseAct function createServerRenderer( testHookProps: Omit, 'hookProps'>, { wrapper }: RendererOptions -): ServerRendererReturn { +): ServerRenderer { const container = document.createElement('div') const testHook = toRender(testHookProps, wrapper, false) @@ -29,16 +22,21 @@ function createServerRenderer( let renderProps: TProps | undefined let hydrated = false - act = (cb: ReactDomActCallbackAsync | ReactDomActCallback) => { + function serverAct(callback: () => void | undefined): void + function serverAct(callback: () => Promise): Promise + // eslint-disable-next-line @typescript-eslint/no-explicit-any + function serverAct(callback: any) { if (!hydrated) { throw new Error('You must hydrate the component before you can act') } - - return baseAct(cb as ReactDomActCallback) + return baseAct(callback) } + act = serverAct + return { render(props) { + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment renderProps = props baseAct(() => { const serverOutput = ReactDOMServer.renderToString(testHook(props)) diff --git a/src/types.ts b/src/types.ts index 90a8336f..d55e51bb 100644 --- a/src/types.ts +++ b/src/types.ts @@ -4,7 +4,10 @@ * */ -export type ActTypes = NativeModifedAct | ReactDomAct +export interface Act { + (callback: () => void | undefined): void + (callback: () => Promise): Promise +} export interface WaitOptions { interval?: number @@ -18,20 +21,13 @@ export type RendererOptions = { wrapper: WrapperComponent } -export type GenericRendererReturn = { +export type Renderer = { render: (props?: TProps) => void rerender: (props?: TProps) => void unmount: () => void + act: Act } -export type ReactDomAct = ( - cb: ReactDomActCallbackAsync | ReactDomActCallback -) => Promise | void - -export type ReactDomActCallbackAsync = () => Promise - -export type ReactDomActCallback = () => void | undefined - /** * * pure @@ -39,10 +35,8 @@ export type ReactDomActCallback = () => void | undefined */ export interface ReactHooksRenderer { - renderHook: () => - | RenderHookReturn - | ServerRenderHook - act: ActTypes + renderHook: () => RenderHookReturn + act: Act cleanup: { autoRegister: () => void } @@ -68,6 +62,16 @@ export type AsyncUtilsReturn = { * */ +export type CreateRenderer = ( + props: Omit, 'hookProps'>, + options: RendererOptions +) => Renderer + +export type RendererUtils> = Omit< + TRenderer, + keyof Renderer +> + export type RenderResult = { readonly all: (TValue | Error | undefined)[] readonly current: TValue @@ -88,13 +92,9 @@ export interface RenderHookOptions { export type RenderHookReturn = { result: RenderResult -} & Omit, 'render'> & +} & Omit, 'render' | 'act'> & AsyncUtilsReturn -export type ServerRenderHook = RenderHookReturn & { - hydrate: () => void -} - /** * * core/testHook @@ -108,35 +108,12 @@ export type TestHookProps = { setValue: (value: TResult) => void } -/** - * - * native/pure - * - */ - -export type NativeModifedAct = (callback: () => Promise) => Promise - -export interface NativeRendererReturn extends GenericRendererReturn { - act: NativeModifedAct -} - /** * * server/pure * */ -export interface ServerRendererReturn extends GenericRendererReturn { - act: ReactDomAct +export interface ServerRenderer extends Renderer { hydrate: () => void } - -/** - * - * dom/pure - * - */ - -export interface DomRendererReturn extends GenericRendererReturn { - act: ReactDomAct -} From 5730f15b52dab4b3f3d986d84173558491832789 Mon Sep 17 00:00:00 2001 From: Michael Peyper Date: Mon, 4 Jan 2021 22:26:04 +1100 Subject: [PATCH 31/53] change: refactor auto cleanup into a separate function --- src/core/cleanup.ts | 4 ++-- src/custom/index.ts | 6 +++--- src/custom/pure.ts | 4 ++-- src/dom/index.ts | 6 +++--- src/dom/pure.ts | 2 +- src/index.ts | 6 +++--- src/native/index.ts | 6 +++--- src/native/pure.ts | 2 +- src/pure.ts | 4 ++-- src/server/index.ts | 6 +++--- src/server/pure.ts | 4 ++-- src/types.ts | 6 +++--- 12 files changed, 28 insertions(+), 28 deletions(-) diff --git a/src/core/cleanup.ts b/src/core/cleanup.ts index a74397c0..2a56d5b1 100644 --- a/src/core/cleanup.ts +++ b/src/core/cleanup.ts @@ -16,7 +16,7 @@ function removeCleanup(callback: () => Promise | void) { cleanupCallbacks = cleanupCallbacks.filter((cb) => cb !== callback) } -cleanup.autoRegister = () => { +function autoRegisterCleanup() { // Automatically registers cleanup in supported testing frameworks if (typeof afterEach === 'function' && !process.env.RHTL_SKIP_AUTO_CLEANUP) { afterEach(async () => { @@ -25,4 +25,4 @@ cleanup.autoRegister = () => { } } -export { cleanup, addCleanup, removeCleanup } +export { cleanup, addCleanup, removeCleanup, autoRegisterCleanup } diff --git a/src/custom/index.ts b/src/custom/index.ts index ca4f465a..7d558c25 100644 --- a/src/custom/index.ts +++ b/src/custom/index.ts @@ -1,5 +1,5 @@ -import { createCustomRenderer, cleanup } from './pure' +import { autoRegisterCleanup } from '../core/cleanup' -cleanup.autoRegister() +autoRegisterCleanup() -export { createCustomRenderer, cleanup } +export * from './pure' diff --git a/src/custom/pure.ts b/src/custom/pure.ts index bdbc7068..c4e45577 100644 --- a/src/custom/pure.ts +++ b/src/custom/pure.ts @@ -1,8 +1,8 @@ -import { createRenderHook, cleanup } from '../core/index' +import { createRenderHook, cleanup, addCleanup, removeCleanup } from '../core' // eslint-disable-next-line @typescript-eslint/no-explicit-any const createCustomRenderer = (createRenderer: any) => ({ renderHook: createRenderHook(createRenderer) }) -export { createCustomRenderer, cleanup } +export { createCustomRenderer, cleanup, addCleanup, removeCleanup } diff --git a/src/dom/index.ts b/src/dom/index.ts index d18e042c..7d558c25 100644 --- a/src/dom/index.ts +++ b/src/dom/index.ts @@ -1,5 +1,5 @@ -import { renderHook, act, cleanup } from './pure' +import { autoRegisterCleanup } from '../core/cleanup' -cleanup.autoRegister() +autoRegisterCleanup() -export { renderHook, act, cleanup } +export * from './pure' diff --git a/src/dom/pure.ts b/src/dom/pure.ts index 72475067..bfd89c3f 100644 --- a/src/dom/pure.ts +++ b/src/dom/pure.ts @@ -3,7 +3,7 @@ import { act } from 'react-dom/test-utils' import { TestHookProps, RendererOptions, Renderer } from '../types' -import { createRenderHook, cleanup, addCleanup, removeCleanup } from '../core/index' +import { createRenderHook, cleanup, addCleanup, removeCleanup } from '../core' import toRender from '../helpers/toRender' function createDomRenderer( diff --git a/src/index.ts b/src/index.ts index d18e042c..10b0b905 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,5 +1,5 @@ -import { renderHook, act, cleanup } from './pure' +import { autoRegisterCleanup } from './core/cleanup' -cleanup.autoRegister() +autoRegisterCleanup() -export { renderHook, act, cleanup } +export * from './pure' diff --git a/src/native/index.ts b/src/native/index.ts index d18e042c..7d558c25 100644 --- a/src/native/index.ts +++ b/src/native/index.ts @@ -1,5 +1,5 @@ -import { renderHook, act, cleanup } from './pure' +import { autoRegisterCleanup } from '../core/cleanup' -cleanup.autoRegister() +autoRegisterCleanup() -export { renderHook, act, cleanup } +export * from './pure' diff --git a/src/native/pure.ts b/src/native/pure.ts index 5e5cbbe1..3833d929 100644 --- a/src/native/pure.ts +++ b/src/native/pure.ts @@ -2,7 +2,7 @@ import { act, create, ReactTestRenderer } from 'react-test-renderer' import { TestHookProps, RendererOptions, Renderer } from '../types' -import { createRenderHook, cleanup, addCleanup, removeCleanup } from '../core/index' +import { createRenderHook, cleanup, addCleanup, removeCleanup } from '../core' import toRender from '../helpers/toRender' function createNativeRenderer( diff --git a/src/pure.ts b/src/pure.ts index bf658b6c..2cf7667b 100644 --- a/src/pure.ts +++ b/src/pure.ts @@ -32,6 +32,6 @@ function getRenderer(renderers: RenderingEngineArray) { } } -const { renderHook, act, cleanup } = getRenderer(RENDERERS) +const { renderHook, act, cleanup, addCleanup, removeCleanup } = getRenderer(RENDERERS) -export { renderHook, act, cleanup } +export { renderHook, act, cleanup, addCleanup, removeCleanup } diff --git a/src/server/index.ts b/src/server/index.ts index d18e042c..7d558c25 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -1,5 +1,5 @@ -import { renderHook, act, cleanup } from './pure' +import { autoRegisterCleanup } from '../core/cleanup' -cleanup.autoRegister() +autoRegisterCleanup() -export { renderHook, act, cleanup } +export * from './pure' diff --git a/src/server/pure.ts b/src/server/pure.ts index d0e73fcd..ff2f70bd 100644 --- a/src/server/pure.ts +++ b/src/server/pure.ts @@ -4,7 +4,7 @@ import { act as baseAct } from 'react-dom/test-utils' import { TestHookProps, RendererOptions, ServerRenderer } from '../types' -import { createRenderHook, cleanup } from '../core/index' +import { createRenderHook, cleanup, addCleanup, removeCleanup } from '../core' import toRender from '../helpers/toRender' @@ -76,4 +76,4 @@ function createServerRenderer( const renderHook = createRenderHook(createServerRenderer) -export { renderHook, act, cleanup } +export { renderHook, act, cleanup, addCleanup, removeCleanup } diff --git a/src/types.ts b/src/types.ts index d55e51bb..e28b6cd1 100644 --- a/src/types.ts +++ b/src/types.ts @@ -37,9 +37,9 @@ export type Renderer = { export interface ReactHooksRenderer { renderHook: () => RenderHookReturn act: Act - cleanup: { - autoRegister: () => void - } + cleanup: () => void + addCleanup: (callback: () => Promise | void) => () => void + removeCleanup: (callback: () => Promise | void) => void } export type RenderingEngineArray = Array<{ required: string; renderer: string }> From 50cdd55a08ed377a47b881d40a1844068a23480b Mon Sep 17 00:00:00 2001 From: Michael Peyper Date: Mon, 4 Jan 2021 22:55:19 +1100 Subject: [PATCH 32/53] feat: added render utility support for custom renderers BREAKING CHANGE: result of createCustomRenderer is no longer an object --- src/custom/pure.ts | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/src/custom/pure.ts b/src/custom/pure.ts index c4e45577..f3c3d5ba 100644 --- a/src/custom/pure.ts +++ b/src/custom/pure.ts @@ -1,8 +1,6 @@ -import { createRenderHook, cleanup, addCleanup, removeCleanup } from '../core' - -// eslint-disable-next-line @typescript-eslint/no-explicit-any -const createCustomRenderer = (createRenderer: any) => ({ - renderHook: createRenderHook(createRenderer) -}) - -export { createCustomRenderer, cleanup, addCleanup, removeCleanup } +export { + createRenderHook as createCustomRenderer, + cleanup, + addCleanup, + removeCleanup +} from '../core' From ba99b1cf10b661149633b82b68cff8c4c31dc5fe Mon Sep 17 00:00:00 2001 From: Michael Peyper Date: Mon, 4 Jan 2021 23:01:56 +1100 Subject: [PATCH 33/53] fix: type catch block errors to fixe lint error --- src/core/asyncUtils.ts | 12 ++++++------ src/core/testHook.ts | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/core/asyncUtils.ts b/src/core/asyncUtils.ts index 2c8cdc02..003a5a38 100644 --- a/src/core/asyncUtils.ts +++ b/src/core/asyncUtils.ts @@ -35,9 +35,9 @@ function asyncUtils(act: Act, addResolver: (callback: () => void) => void): Asyn try { const callbackResult = callback() return callbackResult ?? callbackResult === undefined - } catch (error) { + } catch (error: unknown) { if (!suppressErrors) { - throw error as Error + throw error } return undefined } @@ -57,11 +57,11 @@ function asyncUtils(act: Act, addResolver: (callback: () => void) => void): Asyn if (checkResult()) { return } - } catch (error) { + } catch (error: unknown) { if (error instanceof TimeoutError && initialTimeout) { throw new TimeoutError(waitFor, initialTimeout) } - throw error as Error + throw error } if (timeout) timeout -= Date.now() - startTime } @@ -79,11 +79,11 @@ function asyncUtils(act: Act, addResolver: (callback: () => void) => void): Asyn suppressErrors: false, ...options }) - } catch (error) { + } catch (error: unknown) { if (error instanceof TimeoutError && options.timeout) { throw new TimeoutError(waitForValueToChange, options.timeout) } - throw error as Error + throw error } } diff --git a/src/core/testHook.ts b/src/core/testHook.ts index a93fdc2d..63e91060 100644 --- a/src/core/testHook.ts +++ b/src/core/testHook.ts @@ -11,7 +11,7 @@ export default function TestHook({ try { // coerce undefined into TProps, so it maintains the previous behaviour setValue(callback(hookProps as TProps)) - } catch (err) { + } catch (err: unknown) { if (isPromise(err)) { throw err } else { From fcc8ba3e48cbe6970fe9b40387716a742150be71 Mon Sep 17 00:00:00 2001 From: Michael Peyper Date: Mon, 4 Jan 2021 23:02:45 +1100 Subject: [PATCH 34/53] chore: ignore lint warning for purposely skipped tests --- test/dom/errorHook.ts | 1 + test/native/errorHook.ts | 1 + test/server/errorHook.ts | 1 + 3 files changed, 3 insertions(+) diff --git a/test/dom/errorHook.ts b/test/dom/errorHook.ts index fb293d59..14f5cd64 100644 --- a/test/dom/errorHook.ts +++ b/test/dom/errorHook.ts @@ -113,6 +113,7 @@ describe('error hook tests', () => { Refer to https://github.com/testing-library/react-hooks-testing-library/issues/308 for more details. */ + // eslint-disable-next-line jest/no-disabled-tests describe.skip('effect', () => { test('should raise effect error', () => { const { result } = renderHook(() => useEffectError(true)) diff --git a/test/native/errorHook.ts b/test/native/errorHook.ts index 19bdb8d3..65c0d0eb 100644 --- a/test/native/errorHook.ts +++ b/test/native/errorHook.ts @@ -113,6 +113,7 @@ describe('error hook tests', () => { Refer to https://github.com/testing-library/react-hooks-testing-library/issues/308 for more details. */ + // eslint-disable-next-line jest/no-disabled-tests describe.skip('effect', () => { test('should raise effect error', () => { const { result } = renderHook(() => useEffectError(true)) diff --git a/test/server/errorHook.ts b/test/server/errorHook.ts index 1ba6d82e..bc825795 100644 --- a/test/server/errorHook.ts +++ b/test/server/errorHook.ts @@ -124,6 +124,7 @@ describe('error hook tests', () => { Refer to https://github.com/testing-library/react-hooks-testing-library/issues/308 for more details. */ + // eslint-disable-next-line jest/no-disabled-tests describe.skip('effect', () => { test('should raise effect error', () => { const { result, hydrate } = renderHook(() => useEffectError(true)) From 9ada181f2251d13cd69522d441d7a0b0ec25bfbd Mon Sep 17 00:00:00 2001 From: Michael Peyper Date: Mon, 4 Jan 2021 23:45:14 +1100 Subject: [PATCH 35/53] chore: removed unnecessary lint disable comment --- src/core/index.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/core/index.tsx b/src/core/index.tsx index c4aa47d0..bc1649e0 100644 --- a/src/core/index.tsx +++ b/src/core/index.tsx @@ -1,4 +1,3 @@ -/* eslint-disable @typescript-eslint/no-unsafe-call */ import React from 'react' import { From 785afd2ab0ee700a82354c5fd2b4c00451bb3209 Mon Sep 17 00:00:00 2001 From: Michael Peyper Date: Wed, 6 Jan 2021 12:26:46 +1100 Subject: [PATCH 36/53] feat: renderHook options are now generic based on renderer --- src/core/index.tsx | 31 ++++++++++++++----------------- src/dom/pure.ts | 6 +++--- src/helpers/toRender.tsx | 11 ++++++++--- src/native/pure.ts | 6 +++--- src/server/pure.ts | 24 +++++++++++++----------- src/types.ts | 33 ++++++++++++++++++++------------- 6 files changed, 61 insertions(+), 50 deletions(-) diff --git a/src/core/index.tsx b/src/core/index.tsx index bc1649e0..2b38e016 100644 --- a/src/core/index.tsx +++ b/src/core/index.tsx @@ -1,8 +1,6 @@ -import React from 'react' - import { CreateRenderer, - RendererUtils, + Renderer, ResultContainerReturn, RenderHookOptions, RenderResult, @@ -48,24 +46,23 @@ function resultContainer(): ResultContainerReturn { } } -// typed this way in relation to this https://github.com/DefinitelyTyped/DefinitelyTyped/issues/44572#issuecomment-625878049 -function defaultWrapper({ children }: { children?: React.ReactNode }) { - return (children as unknown) as JSX.Element -} - -const createRenderHook = ( - createRenderer: TCreateRenderer -) => ( +const createRenderHook = >( + createRenderer: CreateRenderer +) => ( callback: (props: TProps) => TResult, - { initialProps, wrapper = defaultWrapper }: RenderHookOptions = {} -): RenderHookReturn & RendererUtils> => { + { initialProps, ...options }: RenderHookOptions = {} as RenderHookOptions< + TProps, + TOptions + > +): RenderHookReturn => { const { result, setValue, setError, addResolver } = resultContainer() const hookProps = { current: initialProps } const props = { callback, setValue, setError } - const options = { wrapper } - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment - const { render, rerender, unmount, act, ...renderUtils } = createRenderer(props, options) + const { render, rerender, unmount, act, ...renderUtils } = createRenderer( + props, + options as TOptions + ) render(hookProps.current) @@ -86,7 +83,7 @@ const createRenderHook = ( rerender: rerenderHook, unmount: unmountHook, ...asyncUtils(act, addResolver), - ...(renderUtils as RendererUtils>) + ...renderUtils } } diff --git a/src/dom/pure.ts b/src/dom/pure.ts index bfd89c3f..c0f30c0b 100644 --- a/src/dom/pure.ts +++ b/src/dom/pure.ts @@ -1,14 +1,14 @@ import ReactDOM from 'react-dom' import { act } from 'react-dom/test-utils' -import { TestHookProps, RendererOptions, Renderer } from '../types' +import { RendererProps, ReactRendererOptions, Renderer } from '../types' import { createRenderHook, cleanup, addCleanup, removeCleanup } from '../core' import toRender from '../helpers/toRender' function createDomRenderer( - testHookProps: Omit, 'hookProps'>, - { wrapper }: RendererOptions + testHookProps: RendererProps, + { wrapper }: ReactRendererOptions ): Renderer { const container = document.createElement('div') diff --git a/src/helpers/toRender.tsx b/src/helpers/toRender.tsx index ff7e61a8..9e589c35 100644 --- a/src/helpers/toRender.tsx +++ b/src/helpers/toRender.tsx @@ -1,11 +1,16 @@ import React, { Suspense } from 'react' import TestHook from '../core/testHook' -import { TestHookProps, WrapperComponent } from '../types' +import { RendererProps, WrapperComponent } from '../types' + +// typed this way in relation to this https://github.com/DefinitelyTyped/DefinitelyTyped/issues/44572#issuecomment-625878049 +function defaultWrapper({ children }: { children?: React.ReactNode }) { + return (children as unknown) as JSX.Element +} const toRender = ( - testHookProps: Omit, 'hookProps'>, - Wrapper: WrapperComponent, + testHookProps: RendererProps, + Wrapper: WrapperComponent = defaultWrapper, suspense: boolean = true ) => { return function RenderWrapper(props?: TProps) { diff --git a/src/native/pure.ts b/src/native/pure.ts index 3833d929..bb33e009 100644 --- a/src/native/pure.ts +++ b/src/native/pure.ts @@ -1,13 +1,13 @@ import { act, create, ReactTestRenderer } from 'react-test-renderer' -import { TestHookProps, RendererOptions, Renderer } from '../types' +import { RendererProps, ReactRendererOptions, Renderer } from '../types' import { createRenderHook, cleanup, addCleanup, removeCleanup } from '../core' import toRender from '../helpers/toRender' function createNativeRenderer( - testHookProps: Omit, 'hookProps'>, - { wrapper }: RendererOptions + testHookProps: RendererProps, + { wrapper }: ReactRendererOptions ): Renderer { let container: ReactTestRenderer diff --git a/src/server/pure.ts b/src/server/pure.ts index ff2f70bd..bb7bbb20 100644 --- a/src/server/pure.ts +++ b/src/server/pure.ts @@ -2,18 +2,25 @@ import ReactDOMServer from 'react-dom/server' import ReactDOM from 'react-dom' import { act as baseAct } from 'react-dom/test-utils' -import { TestHookProps, RendererOptions, ServerRenderer } from '../types' +import { RendererProps, ReactRendererOptions, ServerRenderer } from '../types' import { createRenderHook, cleanup, addCleanup, removeCleanup } from '../core' import toRender from '../helpers/toRender' -// eslint-disable-next-line import/no-mutable-exports -let act: typeof baseAct +let serverAct: typeof baseAct + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const act: typeof serverAct = (callback: any) => { + if (!serverAct) { + return baseAct(callback) + } + return serverAct(callback) +} function createServerRenderer( - testHookProps: Omit, 'hookProps'>, - { wrapper }: RendererOptions + testHookProps: RendererProps, + { wrapper }: ReactRendererOptions ): ServerRenderer { const container = document.createElement('div') @@ -22,21 +29,16 @@ function createServerRenderer( let renderProps: TProps | undefined let hydrated = false - function serverAct(callback: () => void | undefined): void - function serverAct(callback: () => Promise): Promise // eslint-disable-next-line @typescript-eslint/no-explicit-any - function serverAct(callback: any) { + serverAct = (callback: any) => { if (!hydrated) { throw new Error('You must hydrate the component before you can act') } return baseAct(callback) } - act = serverAct - return { render(props) { - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment renderProps = props baseAct(() => { const serverOutput = ReactDOMServer.renderToString(testHook(props)) diff --git a/src/types.ts b/src/types.ts index e28b6cd1..5ceb9e04 100644 --- a/src/types.ts +++ b/src/types.ts @@ -17,8 +17,8 @@ export interface WaitOptions { export type WrapperComponent = React.ComponentType -export type RendererOptions = { - wrapper: WrapperComponent +export type ReactRendererOptions = { + wrapper?: WrapperComponent } export type Renderer = { @@ -62,10 +62,16 @@ export type AsyncUtilsReturn = { * */ -export type CreateRenderer = ( - props: Omit, 'hookProps'>, - options: RendererOptions -) => Renderer +export type RendererProps = { + callback: (props: TProps) => TResult + setError: (error: Error) => void + setValue: (value: TResult) => void +} + +export type CreateRenderer> = ( + props: RendererProps, + options: TOptions +) => TRenderer export type RendererUtils> = Omit< TRenderer, @@ -85,14 +91,18 @@ export type ResultContainerReturn = { setError: (error: Error) => void } -export interface RenderHookOptions { +export type RenderHookOptions = TOptions & { initialProps?: TProps - wrapper?: WrapperComponent } -export type RenderHookReturn = { +export type RenderHookReturn< + TProps, + TValue, + TRenderer extends Renderer = Renderer +> = { result: RenderResult } & Omit, 'render' | 'act'> & + RendererUtils & AsyncUtilsReturn /** @@ -101,11 +111,8 @@ export type RenderHookReturn = { * */ -export type TestHookProps = { +export type TestHookProps = RendererProps & { hookProps: TProps | undefined - callback: (props: TProps) => TResult - setError: (error: Error) => void - setValue: (value: TResult) => void } /** From a1fa12f1e13bf945e0e1d969a7c510e0dcd44d70 Mon Sep 17 00:00:00 2001 From: Michael Peyper Date: Wed, 6 Jan 2021 12:30:30 +1100 Subject: [PATCH 37/53] chore: rename file without tsx exension as it no longer contains any jsx --- src/core/{index.tsx => index.ts} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/core/{index.tsx => index.ts} (100%) diff --git a/src/core/index.tsx b/src/core/index.ts similarity index 100% rename from src/core/index.tsx rename to src/core/index.ts From 511cb33dc4070d11cbb61e41712253e8aa323479 Mon Sep 17 00:00:00 2001 From: Michael Peyper Date: Wed, 6 Jan 2021 13:50:50 +1100 Subject: [PATCH 38/53] fix: refactor type names and removed some unnecessary types --- src/core/asyncUtils.ts | 4 ++-- src/core/index.ts | 8 ++++---- src/dom/pure.ts | 8 ++++---- src/native/pure.ts | 8 ++++---- src/server/pure.ts | 9 ++++----- src/types.ts | 24 +++++------------------- 6 files changed, 23 insertions(+), 38 deletions(-) diff --git a/src/core/asyncUtils.ts b/src/core/asyncUtils.ts index 003a5a38..7dcfa55a 100644 --- a/src/core/asyncUtils.ts +++ b/src/core/asyncUtils.ts @@ -1,9 +1,9 @@ -import { Act, WaitOptions, AsyncUtilsReturn } from '../types' +import { Act, WaitOptions, AsyncUtils } from '../types' import { resolveAfter } from '../helpers/promises' import { TimeoutError } from '../helpers/error' -function asyncUtils(act: Act, addResolver: (callback: () => void) => void): AsyncUtilsReturn { +function asyncUtils(act: Act, addResolver: (callback: () => void) => void): AsyncUtils { let nextUpdatePromise: Promise | null = null const waitForNextUpdate = async ({ timeout }: Pick = {}) => { diff --git a/src/core/index.ts b/src/core/index.ts index 2b38e016..33579c4c 100644 --- a/src/core/index.ts +++ b/src/core/index.ts @@ -1,16 +1,16 @@ import { CreateRenderer, Renderer, - ResultContainerReturn, + ResultContainer, RenderHookOptions, RenderResult, - RenderHookReturn + RenderHook } from '../types' import asyncUtils from './asyncUtils' import { cleanup, addCleanup, removeCleanup } from './cleanup' -function resultContainer(): ResultContainerReturn { +function resultContainer(): ResultContainer { const results: Array<{ value?: TValue; error?: Error }> = [] const resolvers: Array<() => void> = [] @@ -54,7 +54,7 @@ const createRenderHook = -): RenderHookReturn => { +): RenderHook => { const { result, setValue, setError, addResolver } = resultContainer() const hookProps = { current: initialProps } const props = { callback, setValue, setError } diff --git a/src/dom/pure.ts b/src/dom/pure.ts index c0f30c0b..6a8f80ec 100644 --- a/src/dom/pure.ts +++ b/src/dom/pure.ts @@ -1,7 +1,7 @@ import ReactDOM from 'react-dom' import { act } from 'react-dom/test-utils' -import { RendererProps, ReactRendererOptions, Renderer } from '../types' +import { RendererProps, ReactRendererOptions } from '../types' import { createRenderHook, cleanup, addCleanup, removeCleanup } from '../core' import toRender from '../helpers/toRender' @@ -9,19 +9,19 @@ import toRender from '../helpers/toRender' function createDomRenderer( testHookProps: RendererProps, { wrapper }: ReactRendererOptions -): Renderer { +) { const container = document.createElement('div') const testHook = toRender(testHookProps, wrapper) return { - render(props) { + render(props?: TProps) { document.body.appendChild(container) act(() => { ReactDOM.render(testHook(props), container) }) }, - rerender(props) { + rerender(props?: TProps) { act(() => { ReactDOM.render(testHook(props), container) }) diff --git a/src/native/pure.ts b/src/native/pure.ts index bb33e009..6cb841a1 100644 --- a/src/native/pure.ts +++ b/src/native/pure.ts @@ -1,6 +1,6 @@ import { act, create, ReactTestRenderer } from 'react-test-renderer' -import { RendererProps, ReactRendererOptions, Renderer } from '../types' +import { RendererProps, ReactRendererOptions } from '../types' import { createRenderHook, cleanup, addCleanup, removeCleanup } from '../core' import toRender from '../helpers/toRender' @@ -8,18 +8,18 @@ import toRender from '../helpers/toRender' function createNativeRenderer( testHookProps: RendererProps, { wrapper }: ReactRendererOptions -): Renderer { +) { let container: ReactTestRenderer const testHook = toRender(testHookProps, wrapper) return { - render(props) { + render(props?: TProps) { act(() => { container = create(testHook(props)) }) }, - rerender(props) { + rerender(props?: TProps) { act(() => { container.update(testHook(props)) }) diff --git a/src/server/pure.ts b/src/server/pure.ts index bb7bbb20..c3d5a299 100644 --- a/src/server/pure.ts +++ b/src/server/pure.ts @@ -2,10 +2,9 @@ import ReactDOMServer from 'react-dom/server' import ReactDOM from 'react-dom' import { act as baseAct } from 'react-dom/test-utils' -import { RendererProps, ReactRendererOptions, ServerRenderer } from '../types' +import { RendererProps, ReactRendererOptions } from '../types' import { createRenderHook, cleanup, addCleanup, removeCleanup } from '../core' - import toRender from '../helpers/toRender' let serverAct: typeof baseAct @@ -21,7 +20,7 @@ const act: typeof serverAct = (callback: any) => { function createServerRenderer( testHookProps: RendererProps, { wrapper }: ReactRendererOptions -): ServerRenderer { +) { const container = document.createElement('div') const testHook = toRender(testHookProps, wrapper, false) @@ -38,7 +37,7 @@ function createServerRenderer( } return { - render(props) { + render(props?: TProps) { renderProps = props baseAct(() => { const serverOutput = ReactDOMServer.renderToString(testHook(props)) @@ -56,7 +55,7 @@ function createServerRenderer( hydrated = true } }, - rerender(props) { + rerender(props?: TProps) { if (!hydrated) { throw new Error('You must hydrate the component before you can rerender') } diff --git a/src/types.ts b/src/types.ts index 5ceb9e04..a60e0517 100644 --- a/src/types.ts +++ b/src/types.ts @@ -35,7 +35,7 @@ export type Renderer = { */ export interface ReactHooksRenderer { - renderHook: () => RenderHookReturn + renderHook: () => RenderHook act: Act cleanup: () => void addCleanup: (callback: () => Promise | void) => () => void @@ -50,7 +50,7 @@ export type RenderingEngineArray = Array<{ required: string; renderer: string }> * */ -export type AsyncUtilsReturn = { +export type AsyncUtils = { waitFor: (callback: () => boolean | void, opts?: WaitOptions) => Promise waitForNextUpdate: (opts?: Pick) => Promise waitForValueToChange: (selector: () => unknown, options?: WaitOptions) => Promise @@ -84,7 +84,7 @@ export type RenderResult = { readonly error: Error | undefined } -export type ResultContainerReturn = { +export type ResultContainer = { result: RenderResult addResolver: (resolver: () => void) => void setValue: (val: TValue) => void @@ -95,15 +95,11 @@ export type RenderHookOptions = TOptions & { initialProps?: TProps } -export type RenderHookReturn< - TProps, - TValue, - TRenderer extends Renderer = Renderer -> = { +export type RenderHook = Renderer> = { result: RenderResult } & Omit, 'render' | 'act'> & RendererUtils & - AsyncUtilsReturn + AsyncUtils /** * @@ -114,13 +110,3 @@ export type RenderHookReturn< export type TestHookProps = RendererProps & { hookProps: TProps | undefined } - -/** - * - * server/pure - * - */ - -export interface ServerRenderer extends Renderer { - hydrate: () => void -} From b3ccb3213896fe06477209b142b496bae4343cd2 Mon Sep 17 00:00:00 2001 From: Michael Peyper Date: Wed, 6 Jan 2021 15:00:43 +1100 Subject: [PATCH 39/53] fix: remove server act login and just use basic act --- src/server/pure.ts | 28 +++++----------------------- test/server/hydrationErrors.ts | 10 +--------- 2 files changed, 6 insertions(+), 32 deletions(-) diff --git a/src/server/pure.ts b/src/server/pure.ts index c3d5a299..fdb7707a 100644 --- a/src/server/pure.ts +++ b/src/server/pure.ts @@ -1,22 +1,12 @@ import ReactDOMServer from 'react-dom/server' import ReactDOM from 'react-dom' -import { act as baseAct } from 'react-dom/test-utils' +import { act } from 'react-dom/test-utils' import { RendererProps, ReactRendererOptions } from '../types' import { createRenderHook, cleanup, addCleanup, removeCleanup } from '../core' import toRender from '../helpers/toRender' -let serverAct: typeof baseAct - -// eslint-disable-next-line @typescript-eslint/no-explicit-any -const act: typeof serverAct = (callback: any) => { - if (!serverAct) { - return baseAct(callback) - } - return serverAct(callback) -} - function createServerRenderer( testHookProps: RendererProps, { wrapper }: ReactRendererOptions @@ -28,18 +18,10 @@ function createServerRenderer( let renderProps: TProps | undefined let hydrated = false - // eslint-disable-next-line @typescript-eslint/no-explicit-any - serverAct = (callback: any) => { - if (!hydrated) { - throw new Error('You must hydrate the component before you can act') - } - return baseAct(callback) - } - return { render(props?: TProps) { renderProps = props - baseAct(() => { + act(() => { const serverOutput = ReactDOMServer.renderToString(testHook(props)) container.innerHTML = serverOutput }) @@ -49,7 +31,7 @@ function createServerRenderer( throw new Error('The component can only be hydrated once') } else { document.body.appendChild(container) - baseAct(() => { + act(() => { ReactDOM.hydrate(testHook(renderProps), container) }) hydrated = true @@ -59,13 +41,13 @@ function createServerRenderer( if (!hydrated) { throw new Error('You must hydrate the component before you can rerender') } - baseAct(() => { + act(() => { ReactDOM.render(testHook(props), container) }) }, unmount() { if (hydrated) { - baseAct(() => { + act(() => { ReactDOM.unmountComponentAtNode(container) document.body.removeChild(container) }) diff --git a/test/server/hydrationErrors.ts b/test/server/hydrationErrors.ts index bcba11d4..4b14dd0a 100644 --- a/test/server/hydrationErrors.ts +++ b/test/server/hydrationErrors.ts @@ -1,5 +1,5 @@ import { useState, useCallback } from 'react' -import { renderHook, act } from '../../src/server' +import { renderHook } from '../../src/server' describe('hydration errors tests', () => { function useCounter() { @@ -26,12 +26,4 @@ describe('hydration errors tests', () => { Error('You must hydrate the component before you can rerender') ) }) - - test('act should throw if called without hydrating', () => { - const { result } = renderHook(() => useCounter()) - - expect(() => act(() => result.current.decrement())).toThrow( - Error('You must hydrate the component before you can act') - ) - }) }) From 3dfbbb7112e04ccebebb7b9cd0eb42cf187f2193 Mon Sep 17 00:00:00 2001 From: Michael Peyper Date: Wed, 6 Jan 2021 15:02:07 +1100 Subject: [PATCH 40/53] fix: ensure react types are only imported if a react renderer is used --- src/core/asyncUtils.ts | 2 +- src/core/index.ts | 2 +- src/core/testHook.ts | 22 ------------- src/dom/pure.ts | 7 +++-- src/helpers/toRender.tsx | 35 --------------------- src/native/pure.ts | 7 +++-- src/react/createTestHarness.tsx | 43 ++++++++++++++++++++++++++ src/react/types.ts | 11 +++++++ src/server/pure.ts | 7 +++-- src/types.ts | 16 ---------- test/dom/autoCleanup.disabled.ts | 2 +- test/dom/autoCleanup.noAfterEach.ts | 2 +- test/native/autoCleanup.disabled.ts | 2 +- test/native/autoCleanup.noAfterEach.ts | 2 +- test/server/autoCleanup.disabled.ts | 2 +- test/server/autoCleanup.noAfterEach.ts | 2 +- 16 files changed, 74 insertions(+), 90 deletions(-) delete mode 100644 src/core/testHook.ts delete mode 100644 src/helpers/toRender.tsx create mode 100644 src/react/createTestHarness.tsx create mode 100644 src/react/types.ts diff --git a/src/core/asyncUtils.ts b/src/core/asyncUtils.ts index 7dcfa55a..08390d93 100644 --- a/src/core/asyncUtils.ts +++ b/src/core/asyncUtils.ts @@ -1,4 +1,4 @@ -import { Act, WaitOptions, AsyncUtils } from '../types' +import { Act, WaitOptions, AsyncUtils } from 'types' import { resolveAfter } from '../helpers/promises' import { TimeoutError } from '../helpers/error' diff --git a/src/core/index.ts b/src/core/index.ts index 33579c4c..68dfd963 100644 --- a/src/core/index.ts +++ b/src/core/index.ts @@ -5,7 +5,7 @@ import { RenderHookOptions, RenderResult, RenderHook -} from '../types' +} from 'types' import asyncUtils from './asyncUtils' import { cleanup, addCleanup, removeCleanup } from './cleanup' diff --git a/src/core/testHook.ts b/src/core/testHook.ts deleted file mode 100644 index 63e91060..00000000 --- a/src/core/testHook.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { isPromise } from '../helpers/promises' - -import { TestHookProps } from '../types' - -export default function TestHook({ - hookProps, - callback, - setError, - setValue -}: TestHookProps) { - try { - // coerce undefined into TProps, so it maintains the previous behaviour - setValue(callback(hookProps as TProps)) - } catch (err: unknown) { - if (isPromise(err)) { - throw err - } else { - setError(err as Error) - } - } - return null -} diff --git a/src/dom/pure.ts b/src/dom/pure.ts index 6a8f80ec..58a66bf2 100644 --- a/src/dom/pure.ts +++ b/src/dom/pure.ts @@ -1,10 +1,11 @@ import ReactDOM from 'react-dom' import { act } from 'react-dom/test-utils' -import { RendererProps, ReactRendererOptions } from '../types' +import { RendererProps } from 'types' +import { ReactRendererOptions } from '../react/types' import { createRenderHook, cleanup, addCleanup, removeCleanup } from '../core' -import toRender from '../helpers/toRender' +import { createTestHarness } from '../react/createTestHarness' function createDomRenderer( testHookProps: RendererProps, @@ -12,7 +13,7 @@ function createDomRenderer( ) { const container = document.createElement('div') - const testHook = toRender(testHookProps, wrapper) + const testHook = createTestHarness(testHookProps, wrapper) return { render(props?: TProps) { diff --git a/src/helpers/toRender.tsx b/src/helpers/toRender.tsx deleted file mode 100644 index 9e589c35..00000000 --- a/src/helpers/toRender.tsx +++ /dev/null @@ -1,35 +0,0 @@ -import React, { Suspense } from 'react' -import TestHook from '../core/testHook' - -import { RendererProps, WrapperComponent } from '../types' - -// typed this way in relation to this https://github.com/DefinitelyTyped/DefinitelyTyped/issues/44572#issuecomment-625878049 -function defaultWrapper({ children }: { children?: React.ReactNode }) { - return (children as unknown) as JSX.Element -} - -const toRender = ( - testHookProps: RendererProps, - Wrapper: WrapperComponent = defaultWrapper, - suspense: boolean = true -) => { - return function RenderWrapper(props?: TProps) { - if (suspense) { - return ( - - - - - - ) - } else { - return ( - - - - ) - } - } -} - -export default toRender diff --git a/src/native/pure.ts b/src/native/pure.ts index 6cb841a1..ad4e9c47 100644 --- a/src/native/pure.ts +++ b/src/native/pure.ts @@ -1,9 +1,10 @@ import { act, create, ReactTestRenderer } from 'react-test-renderer' -import { RendererProps, ReactRendererOptions } from '../types' +import { RendererProps } from 'types' +import { ReactRendererOptions } from '../react/types' import { createRenderHook, cleanup, addCleanup, removeCleanup } from '../core' -import toRender from '../helpers/toRender' +import { createTestHarness } from '../react/createTestHarness' function createNativeRenderer( testHookProps: RendererProps, @@ -11,7 +12,7 @@ function createNativeRenderer( ) { let container: ReactTestRenderer - const testHook = toRender(testHookProps, wrapper) + const testHook = createTestHarness(testHookProps, wrapper) return { render(props?: TProps) { diff --git a/src/react/createTestHarness.tsx b/src/react/createTestHarness.tsx new file mode 100644 index 00000000..49b5ad65 --- /dev/null +++ b/src/react/createTestHarness.tsx @@ -0,0 +1,43 @@ +import React, { Suspense } from 'react' + +import { RendererProps } from 'types' + +import { isPromise } from '../helpers/promises' + +import { WrapperComponent } from './types' + +function TestHook({ + hookProps, + callback, + setError, + setValue +}: RendererProps & { hookProps: TProps | undefined }) { + try { + // coerce undefined into TProps, so it maintains the previous behaviour + setValue(callback(hookProps as TProps)) + } catch (err: unknown) { + if (isPromise(err)) { + throw err + } else { + setError(err as Error) + } + } + return null +} + +export const createTestHarness = ( + rendererProps: RendererProps, + Wrapper?: WrapperComponent, + suspense: boolean = true +) => { + return (props?: TProps) => { + let component = + if (Wrapper) { + component = {component} + } + if (suspense) { + component = {component} + } + return component + } +} diff --git a/src/react/types.ts b/src/react/types.ts new file mode 100644 index 00000000..b02c1598 --- /dev/null +++ b/src/react/types.ts @@ -0,0 +1,11 @@ +import { RendererProps } from 'types' + +export type WrapperComponent = React.ComponentType + +export type ReactRendererOptions = { + wrapper?: WrapperComponent +} + +export type TestHookProps = RendererProps & { + hookProps: TProps | undefined +} diff --git a/src/server/pure.ts b/src/server/pure.ts index fdb7707a..6fed440f 100644 --- a/src/server/pure.ts +++ b/src/server/pure.ts @@ -2,10 +2,11 @@ import ReactDOMServer from 'react-dom/server' import ReactDOM from 'react-dom' import { act } from 'react-dom/test-utils' -import { RendererProps, ReactRendererOptions } from '../types' +import { RendererProps } from 'types' +import { ReactRendererOptions } from '../react/types' import { createRenderHook, cleanup, addCleanup, removeCleanup } from '../core' -import toRender from '../helpers/toRender' +import { createTestHarness } from '../react/createTestHarness' function createServerRenderer( testHookProps: RendererProps, @@ -13,7 +14,7 @@ function createServerRenderer( ) { const container = document.createElement('div') - const testHook = toRender(testHookProps, wrapper, false) + const testHook = createTestHarness(testHookProps, wrapper, false) let renderProps: TProps | undefined let hydrated = false diff --git a/src/types.ts b/src/types.ts index a60e0517..bcfee71d 100644 --- a/src/types.ts +++ b/src/types.ts @@ -15,12 +15,6 @@ export interface WaitOptions { suppressErrors?: boolean } -export type WrapperComponent = React.ComponentType - -export type ReactRendererOptions = { - wrapper?: WrapperComponent -} - export type Renderer = { render: (props?: TProps) => void rerender: (props?: TProps) => void @@ -100,13 +94,3 @@ export type RenderHook = Rend } & Omit, 'render' | 'act'> & RendererUtils & AsyncUtils - -/** - * - * core/testHook - * - */ - -export type TestHookProps = RendererProps & { - hookProps: TProps | undefined -} diff --git a/test/dom/autoCleanup.disabled.ts b/test/dom/autoCleanup.disabled.ts index 10c8cbdb..2c797345 100644 --- a/test/dom/autoCleanup.disabled.ts +++ b/test/dom/autoCleanup.disabled.ts @@ -1,6 +1,6 @@ import { useEffect } from 'react' -import { ReactHooksRenderer } from '../../src/types' +import { ReactHooksRenderer } from 'types' // This verifies that if RHTL_SKIP_AUTO_CLEANUP is set // then we DON'T auto-wire up the afterEach for folks diff --git a/test/dom/autoCleanup.noAfterEach.ts b/test/dom/autoCleanup.noAfterEach.ts index 30e567a1..1c0821b4 100644 --- a/test/dom/autoCleanup.noAfterEach.ts +++ b/test/dom/autoCleanup.noAfterEach.ts @@ -1,6 +1,6 @@ import { useEffect } from 'react' -import { ReactHooksRenderer } from '../../src/types' +import { ReactHooksRenderer } from 'types' // This verifies that if RHTL_SKIP_AUTO_CLEANUP is set // then we DON'T auto-wire up the afterEach for folks diff --git a/test/native/autoCleanup.disabled.ts b/test/native/autoCleanup.disabled.ts index 98b18f6b..b43794d5 100644 --- a/test/native/autoCleanup.disabled.ts +++ b/test/native/autoCleanup.disabled.ts @@ -1,6 +1,6 @@ import { useEffect } from 'react' -import { ReactHooksRenderer } from '../../src/types' +import { ReactHooksRenderer } from 'types' // This verifies that if RHTL_SKIP_AUTO_CLEANUP is set // then we DON'T auto-wire up the afterEach for folks diff --git a/test/native/autoCleanup.noAfterEach.ts b/test/native/autoCleanup.noAfterEach.ts index f973cb0d..49b00b3d 100644 --- a/test/native/autoCleanup.noAfterEach.ts +++ b/test/native/autoCleanup.noAfterEach.ts @@ -1,6 +1,6 @@ import { useEffect } from 'react' -import { ReactHooksRenderer } from '../../src/types' +import { ReactHooksRenderer } from 'types' // This verifies that if RHTL_SKIP_AUTO_CLEANUP is set // then we DON'T auto-wire up the afterEach for folks diff --git a/test/server/autoCleanup.disabled.ts b/test/server/autoCleanup.disabled.ts index 4a54b11f..00853a13 100644 --- a/test/server/autoCleanup.disabled.ts +++ b/test/server/autoCleanup.disabled.ts @@ -1,6 +1,6 @@ import { useEffect } from 'react' -import { ReactHooksRenderer } from '../../src/types' +import { ReactHooksRenderer } from 'types' // This verifies that if RHTL_SKIP_AUTO_CLEANUP is set // then we DON'T auto-wire up the afterEach for folks diff --git a/test/server/autoCleanup.noAfterEach.ts b/test/server/autoCleanup.noAfterEach.ts index 5bc03ece..180dbea3 100644 --- a/test/server/autoCleanup.noAfterEach.ts +++ b/test/server/autoCleanup.noAfterEach.ts @@ -1,6 +1,6 @@ import { useEffect } from 'react' -import { ReactHooksRenderer } from '../../src/types' +import { ReactHooksRenderer } from 'types' // This verifies that if RHTL_SKIP_AUTO_CLEANUP is set // then we DON'T auto-wire up the afterEach for folks From e1d21534b797f14caa47595ad4d9a2f87751dd4a Mon Sep 17 00:00:00 2001 From: Michael Peyper Date: Wed, 6 Jan 2021 15:32:13 +1100 Subject: [PATCH 41/53] fix: separate types into folders are export types for imports --- src/core/asyncUtils.ts | 2 +- src/core/index.ts | 10 +-- src/custom/pure.ts | 2 + src/dom/pure.ts | 13 +-- src/{react => helpers}/createTestHarness.tsx | 7 +- src/native/pure.ts | 9 +- src/pure.ts | 13 +-- src/server/pure.ts | 9 +- src/{types.ts => types/index.ts} | 91 ++++++-------------- src/types/internal.ts | 17 ++++ src/{react/types.ts => types/react.ts} | 2 +- 11 files changed, 81 insertions(+), 94 deletions(-) rename src/{react => helpers}/createTestHarness.tsx (88%) rename src/{types.ts => types/index.ts} (78%) create mode 100644 src/types/internal.ts rename src/{react/types.ts => types/react.ts} (87%) diff --git a/src/core/asyncUtils.ts b/src/core/asyncUtils.ts index 08390d93..7dcfa55a 100644 --- a/src/core/asyncUtils.ts +++ b/src/core/asyncUtils.ts @@ -1,4 +1,4 @@ -import { Act, WaitOptions, AsyncUtils } from 'types' +import { Act, WaitOptions, AsyncUtils } from '../types' import { resolveAfter } from '../helpers/promises' import { TimeoutError } from '../helpers/error' diff --git a/src/core/index.ts b/src/core/index.ts index 68dfd963..9176b9e3 100644 --- a/src/core/index.ts +++ b/src/core/index.ts @@ -1,11 +1,5 @@ -import { - CreateRenderer, - Renderer, - ResultContainer, - RenderHookOptions, - RenderResult, - RenderHook -} from 'types' +import { CreateRenderer, Renderer, RenderResult, RenderHook } from '../types' +import { ResultContainer, RenderHookOptions } from '../types/internal' import asyncUtils from './asyncUtils' import { cleanup, addCleanup, removeCleanup } from './cleanup' diff --git a/src/custom/pure.ts b/src/custom/pure.ts index f3c3d5ba..532ff171 100644 --- a/src/custom/pure.ts +++ b/src/custom/pure.ts @@ -4,3 +4,5 @@ export { addCleanup, removeCleanup } from '../core' + +export * from '../types' diff --git a/src/dom/pure.ts b/src/dom/pure.ts index 58a66bf2..61af0dd1 100644 --- a/src/dom/pure.ts +++ b/src/dom/pure.ts @@ -1,19 +1,19 @@ import ReactDOM from 'react-dom' import { act } from 'react-dom/test-utils' -import { RendererProps } from 'types' -import { ReactRendererOptions } from '../react/types' +import { RendererProps } from '../types' +import { ReactRendererOptions } from '../types/react' import { createRenderHook, cleanup, addCleanup, removeCleanup } from '../core' -import { createTestHarness } from '../react/createTestHarness' +import { createTestHarness } from '../helpers/createTestHarness' function createDomRenderer( - testHookProps: RendererProps, + rendererProps: RendererProps, { wrapper }: ReactRendererOptions ) { const container = document.createElement('div') - const testHook = createTestHarness(testHookProps, wrapper) + const testHook = createTestHarness(rendererProps, wrapper) return { render(props?: TProps) { @@ -40,3 +40,6 @@ function createDomRenderer( const renderHook = createRenderHook(createDomRenderer) export { renderHook, act, cleanup, addCleanup, removeCleanup } + +export * from '../types' +export * from '../types/react' diff --git a/src/react/createTestHarness.tsx b/src/helpers/createTestHarness.tsx similarity index 88% rename from src/react/createTestHarness.tsx rename to src/helpers/createTestHarness.tsx index 49b5ad65..ce098d2d 100644 --- a/src/react/createTestHarness.tsx +++ b/src/helpers/createTestHarness.tsx @@ -1,10 +1,9 @@ import React, { Suspense } from 'react' -import { RendererProps } from 'types' +import { RendererProps } from '../types' +import { WrapperComponent } from '../types/react' -import { isPromise } from '../helpers/promises' - -import { WrapperComponent } from './types' +import { isPromise } from './promises' function TestHook({ hookProps, diff --git a/src/native/pure.ts b/src/native/pure.ts index ad4e9c47..0368937f 100644 --- a/src/native/pure.ts +++ b/src/native/pure.ts @@ -1,10 +1,10 @@ import { act, create, ReactTestRenderer } from 'react-test-renderer' -import { RendererProps } from 'types' -import { ReactRendererOptions } from '../react/types' +import { RendererProps } from '../types' +import { ReactRendererOptions } from '../types/react' import { createRenderHook, cleanup, addCleanup, removeCleanup } from '../core' -import { createTestHarness } from '../react/createTestHarness' +import { createTestHarness } from '../helpers/createTestHarness' function createNativeRenderer( testHookProps: RendererProps, @@ -37,3 +37,6 @@ function createNativeRenderer( const renderHook = createRenderHook(createNativeRenderer) export { renderHook, act, cleanup, addCleanup, removeCleanup } + +export * from '../types' +export * from '../types/react' diff --git a/src/pure.ts b/src/pure.ts index 2cf7667b..4f2825fd 100644 --- a/src/pure.ts +++ b/src/pure.ts @@ -1,6 +1,6 @@ -import { RenderingEngineArray, ReactHooksRenderer } from 'types' +import { ReactHooksRenderer } from './types' -const RENDERERS: RenderingEngineArray = [ +const RENDERERS = [ { required: 'react-test-renderer', renderer: './native/pure' }, { required: 'react-dom', renderer: './dom/pure' } ] @@ -20,8 +20,8 @@ function hasDependency(name: string) { } } -function getRenderer(renderers: RenderingEngineArray) { - const validRenderer = renderers.find(({ required }) => hasDependency(required)) +function getRenderer() { + const validRenderer = RENDERERS.find(({ required }) => hasDependency(required)) if (validRenderer) { // eslint-disable-next-line @typescript-eslint/no-var-requires @@ -32,6 +32,9 @@ function getRenderer(renderers: RenderingEngineArray) { } } -const { renderHook, act, cleanup, addCleanup, removeCleanup } = getRenderer(RENDERERS) +const { renderHook, act, cleanup, addCleanup, removeCleanup } = getRenderer() export { renderHook, act, cleanup, addCleanup, removeCleanup } + +export * from './types' +export * from './types/react' diff --git a/src/server/pure.ts b/src/server/pure.ts index 6fed440f..91947e07 100644 --- a/src/server/pure.ts +++ b/src/server/pure.ts @@ -2,11 +2,11 @@ import ReactDOMServer from 'react-dom/server' import ReactDOM from 'react-dom' import { act } from 'react-dom/test-utils' -import { RendererProps } from 'types' -import { ReactRendererOptions } from '../react/types' +import { RendererProps } from '../types' +import { ReactRendererOptions } from '../types/react' import { createRenderHook, cleanup, addCleanup, removeCleanup } from '../core' -import { createTestHarness } from '../react/createTestHarness' +import { createTestHarness } from '../helpers/createTestHarness' function createServerRenderer( testHookProps: RendererProps, @@ -61,3 +61,6 @@ function createServerRenderer( const renderHook = createRenderHook(createServerRenderer) export { renderHook, act, cleanup, addCleanup, removeCleanup } + +export * from '../types' +export * from '../types/react' diff --git a/src/types.ts b/src/types/index.ts similarity index 78% rename from src/types.ts rename to src/types/index.ts index bcfee71d..e706e261 100644 --- a/src/types.ts +++ b/src/types/index.ts @@ -1,33 +1,3 @@ -/** - * - * Shared - * - */ - -export interface Act { - (callback: () => void | undefined): void - (callback: () => Promise): Promise -} - -export interface WaitOptions { - interval?: number - timeout?: number - suppressErrors?: boolean -} - -export type Renderer = { - render: (props?: TProps) => void - rerender: (props?: TProps) => void - unmount: () => void - act: Act -} - -/** - * - * pure - * - */ - export interface ReactHooksRenderer { renderHook: () => RenderHook act: Act @@ -36,57 +6,45 @@ export interface ReactHooksRenderer { removeCleanup: (callback: () => Promise | void) => void } -export type RenderingEngineArray = Array<{ required: string; renderer: string }> - -/** - * - * core/asyncUtils - * - */ +export type CreateRenderer> = ( + props: RendererProps, + options: TOptions +) => TRenderer -export type AsyncUtils = { - waitFor: (callback: () => boolean | void, opts?: WaitOptions) => Promise - waitForNextUpdate: (opts?: Pick) => Promise - waitForValueToChange: (selector: () => unknown, options?: WaitOptions) => Promise +export type Renderer = { + render: (props?: TProps) => void + rerender: (props?: TProps) => void + unmount: () => void + act: Act } -/** - * - * core/index - * - */ - export type RendererProps = { callback: (props: TProps) => TResult setError: (error: Error) => void setValue: (value: TResult) => void } -export type CreateRenderer> = ( - props: RendererProps, - options: TOptions -) => TRenderer - -export type RendererUtils> = Omit< - TRenderer, - keyof Renderer -> - export type RenderResult = { readonly all: (TValue | Error | undefined)[] readonly current: TValue readonly error: Error | undefined } -export type ResultContainer = { - result: RenderResult - addResolver: (resolver: () => void) => void - setValue: (val: TValue) => void - setError: (error: Error) => void +export type RendererUtils> = Omit< + TRenderer, + keyof Renderer +> + +export interface WaitOptions { + interval?: number + timeout?: number + suppressErrors?: boolean } -export type RenderHookOptions = TOptions & { - initialProps?: TProps +export type AsyncUtils = { + waitFor: (callback: () => boolean | void, opts?: WaitOptions) => Promise + waitForNextUpdate: (opts?: Pick) => Promise + waitForValueToChange: (selector: () => unknown, options?: WaitOptions) => Promise } export type RenderHook = Renderer> = { @@ -94,3 +52,8 @@ export type RenderHook = Rend } & Omit, 'render' | 'act'> & RendererUtils & AsyncUtils + +export interface Act { + (callback: () => void | undefined): void + (callback: () => Promise): Promise +} diff --git a/src/types/internal.ts b/src/types/internal.ts new file mode 100644 index 00000000..eda7e6bc --- /dev/null +++ b/src/types/internal.ts @@ -0,0 +1,17 @@ +import { Renderer, RendererProps, RenderResult } from '.' + +export type CreateRenderer> = ( + props: RendererProps, + options: TOptions +) => TRenderer + +export type ResultContainer = { + result: RenderResult + addResolver: (resolver: () => void) => void + setValue: (val: TValue) => void + setError: (error: Error) => void +} + +export type RenderHookOptions = TOptions & { + initialProps?: TProps +} diff --git a/src/react/types.ts b/src/types/react.ts similarity index 87% rename from src/react/types.ts rename to src/types/react.ts index b02c1598..2de22df0 100644 --- a/src/react/types.ts +++ b/src/types/react.ts @@ -1,4 +1,4 @@ -import { RendererProps } from 'types' +import { RendererProps } from '.' export type WrapperComponent = React.ComponentType From 137f50c9fd416f4e7a1119ec15b76069cd02a60f Mon Sep 17 00:00:00 2001 From: Michael Peyper Date: Wed, 6 Jan 2021 15:37:24 +1100 Subject: [PATCH 42/53] fix: remove unused types, cleanup types and format all files --- src/dom/pure.ts | 4 ++-- src/helpers/createTestHarness.tsx | 2 +- src/native/pure.ts | 4 ++-- src/server/pure.ts | 4 ++-- src/types/index.ts | 2 +- src/types/react.ts | 10 +++------- test/dom/errorHook.ts | 2 +- test/native/errorHook.ts | 2 +- test/server/errorHook.ts | 2 +- 9 files changed, 14 insertions(+), 18 deletions(-) diff --git a/src/dom/pure.ts b/src/dom/pure.ts index 61af0dd1..c2f90916 100644 --- a/src/dom/pure.ts +++ b/src/dom/pure.ts @@ -2,14 +2,14 @@ import ReactDOM from 'react-dom' import { act } from 'react-dom/test-utils' import { RendererProps } from '../types' -import { ReactRendererOptions } from '../types/react' +import { RendererOptions } from '../types/react' import { createRenderHook, cleanup, addCleanup, removeCleanup } from '../core' import { createTestHarness } from '../helpers/createTestHarness' function createDomRenderer( rendererProps: RendererProps, - { wrapper }: ReactRendererOptions + { wrapper }: RendererOptions ) { const container = document.createElement('div') diff --git a/src/helpers/createTestHarness.tsx b/src/helpers/createTestHarness.tsx index ce098d2d..a09b059b 100644 --- a/src/helpers/createTestHarness.tsx +++ b/src/helpers/createTestHarness.tsx @@ -10,7 +10,7 @@ function TestHook({ callback, setError, setValue -}: RendererProps & { hookProps: TProps | undefined }) { +}: RendererProps & { hookProps?: TProps }) { try { // coerce undefined into TProps, so it maintains the previous behaviour setValue(callback(hookProps as TProps)) diff --git a/src/native/pure.ts b/src/native/pure.ts index 0368937f..e9156bc1 100644 --- a/src/native/pure.ts +++ b/src/native/pure.ts @@ -1,14 +1,14 @@ import { act, create, ReactTestRenderer } from 'react-test-renderer' import { RendererProps } from '../types' -import { ReactRendererOptions } from '../types/react' +import { RendererOptions } from '../types/react' import { createRenderHook, cleanup, addCleanup, removeCleanup } from '../core' import { createTestHarness } from '../helpers/createTestHarness' function createNativeRenderer( testHookProps: RendererProps, - { wrapper }: ReactRendererOptions + { wrapper }: RendererOptions ) { let container: ReactTestRenderer diff --git a/src/server/pure.ts b/src/server/pure.ts index 91947e07..88222239 100644 --- a/src/server/pure.ts +++ b/src/server/pure.ts @@ -3,14 +3,14 @@ import ReactDOM from 'react-dom' import { act } from 'react-dom/test-utils' import { RendererProps } from '../types' -import { ReactRendererOptions } from '../types/react' +import { RendererOptions } from '../types/react' import { createRenderHook, cleanup, addCleanup, removeCleanup } from '../core' import { createTestHarness } from '../helpers/createTestHarness' function createServerRenderer( testHookProps: RendererProps, - { wrapper }: ReactRendererOptions + { wrapper }: RendererOptions ) { const container = document.createElement('div') diff --git a/src/types/index.ts b/src/types/index.ts index e706e261..b50b4beb 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -27,7 +27,7 @@ export type RendererProps = { export type RenderResult = { readonly all: (TValue | Error | undefined)[] readonly current: TValue - readonly error: Error | undefined + readonly error?: Error } export type RendererUtils> = Omit< diff --git a/src/types/react.ts b/src/types/react.ts index 2de22df0..09923286 100644 --- a/src/types/react.ts +++ b/src/types/react.ts @@ -1,11 +1,7 @@ -import { RendererProps } from '.' +import { ComponentType } from 'react' -export type WrapperComponent = React.ComponentType +export type WrapperComponent = ComponentType -export type ReactRendererOptions = { +export type RendererOptions = { wrapper?: WrapperComponent } - -export type TestHookProps = RendererProps & { - hookProps: TProps | undefined -} diff --git a/test/dom/errorHook.ts b/test/dom/errorHook.ts index 14f5cd64..b0a5ba8c 100644 --- a/test/dom/errorHook.ts +++ b/test/dom/errorHook.ts @@ -2,7 +2,7 @@ import { useState, useEffect } from 'react' import { renderHook } from '../../src/dom' describe('error hook tests', () => { - function useError(throwError: boolean | undefined) { + function useError(throwError?: boolean) { if (throwError) { throw new Error('expected') } diff --git a/test/native/errorHook.ts b/test/native/errorHook.ts index 65c0d0eb..078227c7 100644 --- a/test/native/errorHook.ts +++ b/test/native/errorHook.ts @@ -2,7 +2,7 @@ import { useState, useEffect } from 'react' import { renderHook } from '../../src/native' describe('error hook tests', () => { - function useError(throwError: boolean | undefined) { + function useError(throwError?: boolean) { if (throwError) { throw new Error('expected') } diff --git a/test/server/errorHook.ts b/test/server/errorHook.ts index bc825795..1fcbd34b 100644 --- a/test/server/errorHook.ts +++ b/test/server/errorHook.ts @@ -3,7 +3,7 @@ import { useState, useEffect } from 'react' import { renderHook } from '../../src/server' describe('error hook tests', () => { - function useError(throwError: boolean | undefined) { + function useError(throwError?: boolean) { if (throwError) { throw new Error('expected') } From 89e678c65cd628dba12a4fcdf8371696226d93a6 Mon Sep 17 00:00:00 2001 From: Michael Peyper Date: Wed, 6 Jan 2021 15:53:48 +1100 Subject: [PATCH 43/53] fix: update error message when a renderer cannot be auto-detected --- src/pure.ts | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/pure.ts b/src/pure.ts index 4f2825fd..dbf4ae60 100644 --- a/src/pure.ts +++ b/src/pure.ts @@ -1,16 +1,10 @@ import { ReactHooksRenderer } from './types' -const RENDERERS = [ +const renderers = [ { required: 'react-test-renderer', renderer: './native/pure' }, { required: 'react-dom', renderer: './dom/pure' } ] -const KNOWN_RENDERERS = [ - '@testing-library/react-hooks/dom', - '@testing-library/react-hooks/native', - '@testing-library/react-hooks/server' -] - function hasDependency(name: string) { try { require(name) @@ -21,14 +15,20 @@ function hasDependency(name: string) { } function getRenderer() { - const validRenderer = RENDERERS.find(({ required }) => hasDependency(required)) + const validRenderer = renderers.find(({ required }) => hasDependency(required)) if (validRenderer) { // eslint-disable-next-line @typescript-eslint/no-var-requires return require(validRenderer.renderer) as ReactHooksRenderer } else { - const options = KNOWN_RENDERERS.map((renderer) => ` - ${renderer}`).join('\n') - throw new Error(`Could not auto-detect a React renderer. Options are:\n${options}`) + const options = renderers + .map(({ required }) => ` - ${required}`) + .sort((a, b) => a.localeCompare(b)) + throw new Error( + `Could not auto-detect a React renderer. Are you sure you've installed one of the following\n${options.join( + '\n' + )}` + ) } } From 36b81c0c4f2604d1745b355b51502ab61160a14b Mon Sep 17 00:00:00 2001 From: Michael Peyper Date: Wed, 6 Jan 2021 15:58:21 +1100 Subject: [PATCH 44/53] chore: restructure code for better readability after formatting --- src/pure.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/pure.ts b/src/pure.ts index dbf4ae60..30c84181 100644 --- a/src/pure.ts +++ b/src/pure.ts @@ -24,10 +24,10 @@ function getRenderer() { const options = renderers .map(({ required }) => ` - ${required}`) .sort((a, b) => a.localeCompare(b)) + .join('/n') + throw new Error( - `Could not auto-detect a React renderer. Are you sure you've installed one of the following\n${options.join( - '\n' - )}` + `Could not auto-detect a React renderer. Are you sure you've installed one of the following\n${options}` ) } } From 1c6b9e51df363f58a3786d9c4c1c37265946cba4 Mon Sep 17 00:00:00 2001 From: Michael Peyper Date: Wed, 6 Jan 2021 16:06:56 +1100 Subject: [PATCH 45/53] fix: remove unnecessary structures to remove need to cast in renderHook --- src/core/index.ts | 22 ++++++++-------------- 1 file changed, 8 insertions(+), 14 deletions(-) diff --git a/src/core/index.ts b/src/core/index.ts index 9176b9e3..068016f5 100644 --- a/src/core/index.ts +++ b/src/core/index.ts @@ -44,25 +44,19 @@ const createRenderHook = ) => ( callback: (props: TProps) => TResult, - { initialProps, ...options }: RenderHookOptions = {} as RenderHookOptions< - TProps, - TOptions - > + options: RenderHookOptions = {} as RenderHookOptions ): RenderHook => { const { result, setValue, setError, addResolver } = resultContainer() - const hookProps = { current: initialProps } - const props = { callback, setValue, setError } + const renderProps = { callback, setValue, setError } + let hookProps = options.initialProps - const { render, rerender, unmount, act, ...renderUtils } = createRenderer( - props, - options as TOptions - ) + const { render, rerender, unmount, act, ...renderUtils } = createRenderer(renderProps, options) - render(hookProps.current) + render(hookProps) - function rerenderHook(newProps = hookProps.current) { - hookProps.current = newProps - rerender(hookProps.current) + function rerenderHook(newProps = hookProps) { + hookProps = newProps + rerender(hookProps) } function unmountHook() { From b936962bc99c56d6ea346b69d61a0e2d80139052 Mon Sep 17 00:00:00 2001 From: Michael Peyper Date: Wed, 6 Jan 2021 16:28:08 +1100 Subject: [PATCH 46/53] fix: remove duplicate type from internal types --- src/types/index.ts | 26 +++++++++++++------------- src/types/internal.ts | 7 +------ 2 files changed, 14 insertions(+), 19 deletions(-) diff --git a/src/types/index.ts b/src/types/index.ts index b50b4beb..79c65217 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -1,16 +1,3 @@ -export interface ReactHooksRenderer { - renderHook: () => RenderHook - act: Act - cleanup: () => void - addCleanup: (callback: () => Promise | void) => () => void - removeCleanup: (callback: () => Promise | void) => void -} - -export type CreateRenderer> = ( - props: RendererProps, - options: TOptions -) => TRenderer - export type Renderer = { render: (props?: TProps) => void rerender: (props?: TProps) => void @@ -24,6 +11,11 @@ export type RendererProps = { setValue: (value: TResult) => void } +export type CreateRenderer> = ( + props: RendererProps, + options: TOptions +) => TRenderer + export type RenderResult = { readonly all: (TValue | Error | undefined)[] readonly current: TValue @@ -53,6 +45,14 @@ export type RenderHook = Rend RendererUtils & AsyncUtils +export interface ReactHooksRenderer { + renderHook: () => RenderHook + act: Act + cleanup: () => void + addCleanup: (callback: () => Promise | void) => () => void + removeCleanup: (callback: () => Promise | void) => void +} + export interface Act { (callback: () => void | undefined): void (callback: () => Promise): Promise diff --git a/src/types/internal.ts b/src/types/internal.ts index eda7e6bc..3d1a4152 100644 --- a/src/types/internal.ts +++ b/src/types/internal.ts @@ -1,9 +1,4 @@ -import { Renderer, RendererProps, RenderResult } from '.' - -export type CreateRenderer> = ( - props: RendererProps, - options: TOptions -) => TRenderer +import { RenderResult } from '.' export type ResultContainer = { result: RenderResult From 080d98a4cfc0a136bcd9a5ec92411a4992ff8b0e Mon Sep 17 00:00:00 2001 From: Michael Peyper Date: Wed, 6 Jan 2021 16:50:18 +1100 Subject: [PATCH 47/53] fix: simplify type of RenderHook --- src/types/index.ts | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/src/types/index.ts b/src/types/index.ts index 79c65217..cf151e5e 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -22,10 +22,9 @@ export type RenderResult = { readonly error?: Error } -export type RendererUtils> = Omit< - TRenderer, - keyof Renderer -> +export type ResultContainer = { + result: RenderResult +} export interface WaitOptions { interval?: number @@ -39,10 +38,13 @@ export type AsyncUtils = { waitForValueToChange: (selector: () => unknown, options?: WaitOptions) => Promise } -export type RenderHook = Renderer> = { - result: RenderResult -} & Omit, 'render' | 'act'> & - RendererUtils & +export type RenderHook< + TProps, + TValue, + TRenderer extends Renderer = Renderer +> = ResultContainer & + Omit, 'render' | 'act'> & + Omit> & AsyncUtils export interface ReactHooksRenderer { From 209619dbd506f97027fcfca7d2a402a535f29dcb Mon Sep 17 00:00:00 2001 From: Michael Peyper Date: Thu, 7 Jan 2021 10:36:11 +1100 Subject: [PATCH 48/53] feat: generate submodules as part of build step --- .eslintignore | 4 +++ .eslintrc | 3 +- .gitignore | 6 +++- custom/index.js | 1 - custom/pure.js | 2 -- dom/index.js | 2 -- dom/pure.js | 2 -- native/index.js | 1 - native/pure.js | 2 -- package.json | 6 ++-- pure.js | 2 -- scripts/generate-submodules.ts | 54 ++++++++++++++++++++++++++++++++++ scripts/tsconfig.json | 8 +++++ server/index.js | 2 -- server/pure.js | 2 -- test/tsconfig.json | 4 +-- tsconfig.json | 3 +- 17 files changed, 79 insertions(+), 25 deletions(-) delete mode 100644 custom/index.js delete mode 100644 custom/pure.js delete mode 100644 dom/index.js delete mode 100644 dom/pure.js delete mode 100644 native/index.js delete mode 100644 native/pure.js delete mode 100644 pure.js create mode 100644 scripts/generate-submodules.ts create mode 100644 scripts/tsconfig.json delete mode 100644 server/index.js delete mode 100644 server/pure.js diff --git a/.eslintignore b/.eslintignore index 4594ebf4..a9ac17ec 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1,5 +1,9 @@ node_modules coverage lib +dom +native +server +pure .docz site diff --git a/.eslintrc b/.eslintrc index b16789bf..5e90a5cf 100644 --- a/.eslintrc +++ b/.eslintrc @@ -6,12 +6,11 @@ "no-await-in-loop": "off", "no-console": "off", "import/no-unresolved": "off", - "react-hooks/rules-of-hooks": "off", "@typescript-eslint/no-floating-promises": "off", "@typescript-eslint/no-unnecessary-condition": "off", "@typescript-eslint/no-invalid-void-type": "off" }, "parserOptions": { - "project": ["./tsconfig.json", "./test/tsconfig.json"] + "project": ["./tsconfig.json", "./test/tsconfig.json", "./scripts/tsconfig.json"] } } diff --git a/.gitignore b/.gitignore index a0a75f27..2236836e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,10 @@ node_modules coverage lib +dom +native +server +pure .docz site -.vscode \ No newline at end of file +.vscode diff --git a/custom/index.js b/custom/index.js deleted file mode 100644 index e22fbb61..00000000 --- a/custom/index.js +++ /dev/null @@ -1 +0,0 @@ -module.exports = require('../lib/custom') diff --git a/custom/pure.js b/custom/pure.js deleted file mode 100644 index c98aee61..00000000 --- a/custom/pure.js +++ /dev/null @@ -1,2 +0,0 @@ -// makes it so people can import from '@testing-library/react-hooks/custom/pure' -module.exports = require('../lib/custom/pure') diff --git a/dom/index.js b/dom/index.js deleted file mode 100644 index 5b8693bf..00000000 --- a/dom/index.js +++ /dev/null @@ -1,2 +0,0 @@ -// makes it so people can import from '@testing-library/react-hooks/dom' -module.exports = require('../lib/dom') diff --git a/dom/pure.js b/dom/pure.js deleted file mode 100644 index c6e171cc..00000000 --- a/dom/pure.js +++ /dev/null @@ -1,2 +0,0 @@ -// makes it so people can import from '@testing-library/react-hooks/dom/pure' -module.exports = require('../lib/dom/pure') diff --git a/native/index.js b/native/index.js deleted file mode 100644 index 9ca3ec48..00000000 --- a/native/index.js +++ /dev/null @@ -1 +0,0 @@ -module.exports = require('../lib/native') diff --git a/native/pure.js b/native/pure.js deleted file mode 100644 index 10dc143f..00000000 --- a/native/pure.js +++ /dev/null @@ -1,2 +0,0 @@ -// makes it so people can import from '@testing-library/react-hooks/native/pure' -module.exports = require('../lib/native/pure') diff --git a/package.json b/package.json index 2f80cd49..bb0f3d55 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,7 @@ "dom", "native", "server", - "pure.js", + "pure", "dont-cleanup-after-each.js" ], "author": "Michael Peyper ", @@ -30,7 +30,8 @@ "setup": "npm install && npm run validate -s", "validate": "kcd-scripts validate", "prepare": "npm run build", - "build": "kcd-scripts build --out-dir lib", + "build": "kcd-scripts build --out-dir lib && npm run generate:submodules", + "generate:submodules": "ts-node scripts/generate-submodules.ts", "test": "kcd-scripts test", "typecheck": "kcd-scripts typecheck", "lint": "kcd-scripts lint", @@ -60,6 +61,7 @@ "react": "17.0.1", "react-dom": "^17.0.1", "react-test-renderer": "17.0.1", + "ts-node": "^9.1.1", "typescript": "4.1.3" }, "peerDependencies": { diff --git a/pure.js b/pure.js deleted file mode 100644 index 53c16328..00000000 --- a/pure.js +++ /dev/null @@ -1,2 +0,0 @@ -// makes it so people can import from '@testing-library/react-hooks/pure' -module.exports = require('./lib/pure') diff --git a/scripts/generate-submodules.ts b/scripts/generate-submodules.ts new file mode 100644 index 00000000..132e5828 --- /dev/null +++ b/scripts/generate-submodules.ts @@ -0,0 +1,54 @@ +import fs from 'fs' +import path from 'path' + +type Template = (submodule: string) => string + +const templates = { + index: { + '.js': (submodule: string) => `module.exports = require('../lib/${submodule}')`, + '.d.ts': (submodule: string) => `export * from '../lib/${submodule}'` + }, + pure: { + '.js': (submodule: string) => `module.exports = require('../lib/${submodule}/pure')`, + '.d.ts': (submodule: string) => `export * from '../lib/${submodule}/pure'` + } +} + +const submodules = ['dom', 'native', 'server', 'pure'] + +function makeDirectory(submodule: string) { + const submoduleDir = path.join(process.cwd(), submodule) + if (!fs.existsSync(submoduleDir)) { + fs.mkdirSync(submoduleDir) + } + return submoduleDir +} + +function makeFile(directory: string, submodule: string) { + return ([name, extensions]: [string, Record]) => { + Object.entries(extensions).forEach(([extension, template]) => { + const fileName = `${name}${extension}` + console.log(` - ${fileName}`) + const filePath = path.join(directory, fileName) + fs.writeFileSync(filePath, template(submodule)) + }) + } +} + +function requiredFile(submodule: string) { + return ([name]: [string, unknown]) => { + return name !== submodule + } +} + +function makeFiles(directory: string, submodule: string) { + Object.entries(templates).filter(requiredFile(submodule)).forEach(makeFile(directory, submodule)) +} + +function createSubmodule(submodule: string) { + console.log(`Generating submodule: ${submodule}`) + const submoduleDir = makeDirectory(submodule) + makeFiles(submoduleDir, submodule) +} + +submodules.forEach(createSubmodule) diff --git a/scripts/tsconfig.json b/scripts/tsconfig.json new file mode 100644 index 00000000..bbb2c4c6 --- /dev/null +++ b/scripts/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../tsconfig", + "compilerOptions": { + "declaration": false + }, + "exclude": [], + "include": ["./**/*.ts"] +} diff --git a/server/index.js b/server/index.js deleted file mode 100644 index 21b80f69..00000000 --- a/server/index.js +++ /dev/null @@ -1,2 +0,0 @@ -// makes it so people can import from '@testing-library/react-hooks/server' -module.exports = require('../lib/server') diff --git a/server/pure.js b/server/pure.js deleted file mode 100644 index a64c1790..00000000 --- a/server/pure.js +++ /dev/null @@ -1,2 +0,0 @@ -// makes it so people can import from '@testing-library/react-hooks/server/pure' -module.exports = require('../lib/server/pure') diff --git a/test/tsconfig.json b/test/tsconfig.json index 3b6b7c4c..bbb2c4c6 100644 --- a/test/tsconfig.json +++ b/test/tsconfig.json @@ -1,8 +1,8 @@ { - "extends": "./../tsconfig", + "extends": "../tsconfig", "compilerOptions": { "declaration": false }, "exclude": [], - "include": ["./native/*", "./server/*", "./dom/*"] + "include": ["./**/*.ts"] } diff --git a/tsconfig.json b/tsconfig.json index 1337ac30..2b5e3606 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -3,6 +3,5 @@ "compilerOptions": { "allowJs": true, "target": "ES6" - }, - "exclude": ["./test"] + } } From e9fa116d21fe238bace572eca840cd71f0da2978 Mon Sep 17 00:00:00 2001 From: Michael Peyper Date: Thu, 7 Jan 2021 11:31:00 +1100 Subject: [PATCH 49/53] fix: don't wait for already resolved promise in waitForNextUpdate --- src/core/asyncUtils.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/core/asyncUtils.ts b/src/core/asyncUtils.ts index 7dcfa55a..4c2ecf77 100644 --- a/src/core/asyncUtils.ts +++ b/src/core/asyncUtils.ts @@ -7,7 +7,9 @@ function asyncUtils(act: Act, addResolver: (callback: () => void) => void): Asyn let nextUpdatePromise: Promise | null = null const waitForNextUpdate = async ({ timeout }: Pick = {}) => { - if (!nextUpdatePromise) { + if (nextUpdatePromise) { + await nextUpdatePromise + } else { nextUpdatePromise = new Promise((resolve, reject) => { let timeoutId: ReturnType if (timeout && timeout > 0) { @@ -24,7 +26,6 @@ function asyncUtils(act: Act, addResolver: (callback: () => void) => void): Asyn }) await act(() => nextUpdatePromise as Promise) } - await nextUpdatePromise } const waitFor = async ( From fd00043b7c7d70dfdb5d570b925f0385e89b0a0c Mon Sep 17 00:00:00 2001 From: Michael Peyper Date: Thu, 7 Jan 2021 11:40:10 +1100 Subject: [PATCH 50/53] fix: ensure submodule directory is cleaned before generating files --- scripts/generate-submodules.ts | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/scripts/generate-submodules.ts b/scripts/generate-submodules.ts index 132e5828..7946b30f 100644 --- a/scripts/generate-submodules.ts +++ b/scripts/generate-submodules.ts @@ -16,14 +16,29 @@ const templates = { const submodules = ['dom', 'native', 'server', 'pure'] +function cleanDirectory(directory: string) { + const files = fs.readdirSync(directory) + files.forEach((file) => fs.unlinkSync(path.join(directory, file))) +} + function makeDirectory(submodule: string) { const submoduleDir = path.join(process.cwd(), submodule) - if (!fs.existsSync(submoduleDir)) { + + if (fs.existsSync(submoduleDir)) { + cleanDirectory(submoduleDir) + } else { fs.mkdirSync(submoduleDir) } + return submoduleDir } +function requiredFile(submodule: string) { + return ([name]: [string, unknown]) => { + return name !== submodule + } +} + function makeFile(directory: string, submodule: string) { return ([name, extensions]: [string, Record]) => { Object.entries(extensions).forEach(([extension, template]) => { @@ -35,12 +50,6 @@ function makeFile(directory: string, submodule: string) { } } -function requiredFile(submodule: string) { - return ([name]: [string, unknown]) => { - return name !== submodule - } -} - function makeFiles(directory: string, submodule: string) { Object.entries(templates).filter(requiredFile(submodule)).forEach(makeFile(directory, submodule)) } From 16ba7e072fb4a50ecee532c11b45adc5e5d12b10 Mon Sep 17 00:00:00 2001 From: Michael Peyper Date: Thu, 7 Jan 2021 11:43:14 +1100 Subject: [PATCH 51/53] chore: remove unused custom module --- src/custom/index.ts | 5 ----- src/custom/pure.ts | 8 -------- 2 files changed, 13 deletions(-) delete mode 100644 src/custom/index.ts delete mode 100644 src/custom/pure.ts diff --git a/src/custom/index.ts b/src/custom/index.ts deleted file mode 100644 index 7d558c25..00000000 --- a/src/custom/index.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { autoRegisterCleanup } from '../core/cleanup' - -autoRegisterCleanup() - -export * from './pure' diff --git a/src/custom/pure.ts b/src/custom/pure.ts deleted file mode 100644 index 532ff171..00000000 --- a/src/custom/pure.ts +++ /dev/null @@ -1,8 +0,0 @@ -export { - createRenderHook as createCustomRenderer, - cleanup, - addCleanup, - removeCleanup -} from '../core' - -export * from '../types' From 6146121a86638c795b0565203d42da056b7e53a9 Mon Sep 17 00:00:00 2001 From: Michael Peyper Date: Thu, 7 Jan 2021 12:06:32 +1100 Subject: [PATCH 52/53] fix: minor renaming of private members --- src/helpers/createTestHarness.tsx | 4 ++-- src/server/pure.ts | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/helpers/createTestHarness.tsx b/src/helpers/createTestHarness.tsx index a09b059b..b382a080 100644 --- a/src/helpers/createTestHarness.tsx +++ b/src/helpers/createTestHarness.tsx @@ -5,7 +5,7 @@ import { WrapperComponent } from '../types/react' import { isPromise } from './promises' -function TestHook({ +function TestComponent({ hookProps, callback, setError, @@ -30,7 +30,7 @@ export const createTestHarness = ( suspense: boolean = true ) => { return (props?: TProps) => { - let component = + let component = if (Wrapper) { component = {component} } diff --git a/src/server/pure.ts b/src/server/pure.ts index 88222239..ec0737b1 100644 --- a/src/server/pure.ts +++ b/src/server/pure.ts @@ -9,12 +9,12 @@ import { createRenderHook, cleanup, addCleanup, removeCleanup } from '../core' import { createTestHarness } from '../helpers/createTestHarness' function createServerRenderer( - testHookProps: RendererProps, + rendererProps: RendererProps, { wrapper }: RendererOptions ) { const container = document.createElement('div') - const testHook = createTestHarness(testHookProps, wrapper, false) + const testHook = createTestHarness(rendererProps, wrapper, false) let renderProps: TProps | undefined let hydrated = false From e315dce71fb6004a8827aafe65b2ba3a7d648081 Mon Sep 17 00:00:00 2001 From: Michael Peyper Date: Thu, 7 Jan 2021 16:36:12 +1100 Subject: [PATCH 53/53] chore: updated all-contributors to be more accurate --- .all-contributorsrc | 193 ++++++++++++++++++++++++++++++++++++++++++-- README.md | 32 +++++++- 2 files changed, 214 insertions(+), 11 deletions(-) diff --git a/.all-contributorsrc b/.all-contributorsrc index d7d137c5..76a97954 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -8,6 +8,7 @@ ], "imageSize": 100, "commit": false, + "commitConvention": "none", "contributors": [ { "login": "mpeyper", @@ -16,13 +17,9 @@ "profile": "https://github.com/mpeyper", "contributions": [ "code", - "design", "doc", - "ideas", "infra", - "platform", - "test", - "tool" + "test" ] }, { @@ -201,7 +198,8 @@ "avatar_url": "https://avatars0.githubusercontent.com/u/37798644?v=4", "profile": "https://github.com/joshuaellis", "contributions": [ - "doc" + "doc", + "question" ] }, { @@ -270,7 +268,186 @@ "code", "test" ] + }, + { + "login": "marcosvega91", + "name": "Marco Moretti", + "avatar_url": "https://avatars2.githubusercontent.com/u/5365582?v=4", + "profile": "https://github.com/marcosvega91", + "contributions": [ + "infra" + ] + }, + { + "login": "ndresx", + "name": "Martin V.", + "avatar_url": "https://avatars0.githubusercontent.com/u/27507295?v=4", + "profile": "https://www.parkside.at/", + "contributions": [ + "doc" + ] + }, + { + "login": "erozak", + "name": "Erozak", + "avatar_url": "https://avatars3.githubusercontent.com/u/22066282?v=4", + "profile": "https://github.com/erozak", + "contributions": [ + "doc" + ] + }, + { + "login": "nickmccurdy", + "name": "Nick McCurdy", + "avatar_url": "https://avatars0.githubusercontent.com/u/927220?v=4", + "profile": "https://nickmccurdy.com/", + "contributions": [ + "maintenance" + ] + }, + { + "login": "aryyya", + "name": "Arya", + "avatar_url": "https://avatars1.githubusercontent.com/u/29365565?v=4", + "profile": "https://codepen.io/aryyya/", + "contributions": [ + "doc" + ] + }, + { + "login": "numb86", + "name": "numb86", + "avatar_url": "https://avatars1.githubusercontent.com/u/16703337?v=4", + "profile": "https://numb86.net/", + "contributions": [ + "doc" + ] + }, + { + "login": "foray1010", + "name": "Alex Young", + "avatar_url": "https://avatars3.githubusercontent.com/u/3212221?v=4", + "profile": "https://github.com/foray1010", + "contributions": [ + "maintenance" + ] + }, + { + "login": "benjdlambert", + "name": "Ben Lambert", + "avatar_url": "https://avatars1.githubusercontent.com/u/3645856?v=4", + "profile": "https://blam.sh/", + "contributions": [ + "doc" + ] + }, + { + "login": "ElRatonDeFuego", + "name": "David Cho-Lerat", + "avatar_url": "https://avatars1.githubusercontent.com/u/12750934?v=4", + "profile": "https://github.com/ElRatonDeFuego", + "contributions": [ + "doc" + ] + }, + { + "login": "evanharmon", + "name": "Evan Harmon", + "avatar_url": "https://avatars1.githubusercontent.com/u/8229989?v=4", + "profile": "https://github.com/evanharmon", + "contributions": [ + "doc" + ] + }, + { + "login": "browniefed", + "name": "Jason Brown", + "avatar_url": "https://avatars1.githubusercontent.com/u/1714673?v=4", + "profile": "http://codedaily.io/", + "contributions": [ + "doc" + ] + }, + { + "login": "kahwee", + "name": "KahWee Teng", + "avatar_url": "https://avatars1.githubusercontent.com/u/262105?v=4", + "profile": "https://github.com/kahwee", + "contributions": [ + "doc" + ] + }, + { + "login": "shagabutdinov", + "name": "Leonid Shagabutdinov", + "avatar_url": "https://avatars2.githubusercontent.com/u/1635613?v=4", + "profile": "http://shagabutdinov.com/", + "contributions": [ + "doc" + ] + }, + { + "login": "LeviButcher", + "name": "Levi Butcher", + "avatar_url": "https://avatars2.githubusercontent.com/u/31522433?v=4", + "profile": "https://levibutcher.dev/", + "contributions": [ + "doc" + ] + }, + { + "login": "7michele7", + "name": "Michele Settepani", + "avatar_url": "https://avatars2.githubusercontent.com/u/17926167?v=4", + "profile": "https://github.com/7michele7", + "contributions": [ + "doc" + ] + }, + { + "login": "samnoh", + "name": "Sam", + "avatar_url": "https://avatars1.githubusercontent.com/u/14857416?v=4", + "profile": "https://github.com/samnoh", + "contributions": [ + "doc" + ] + }, + { + "login": "tanaypratap", + "name": "Tanay Pratap", + "avatar_url": "https://avatars0.githubusercontent.com/u/10216863?v=4", + "profile": "https://github.com/tanaypratap", + "contributions": [ + "doc" + ] + }, + { + "login": "techanvil", + "name": "Tom Rees-Herdman", + "avatar_url": "https://avatars0.githubusercontent.com/u/18395600?v=4", + "profile": "https://github.com/techanvil", + "contributions": [ + "doc" + ] + }, + { + "login": "iqbal125", + "name": "iqbal125", + "avatar_url": "https://avatars2.githubusercontent.com/u/24860061?v=4", + "profile": "https://github.com/iqbal125", + "contributions": [ + "doc" + ] + }, + { + "login": "cliffzhaobupt", + "name": "cliffzhaobupt", + "avatar_url": "https://avatars3.githubusercontent.com/u/7374506?v=4", + "profile": "https://github.com/cliffzhaobupt", + "contributions": [ + "maintenance" + ] } - ], - "commitConvention": "none" + ] } diff --git a/README.md b/README.md index b62c279d..c5a944f7 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,7 @@ [![downloads](https://img.shields.io/npm/dm/@testing-library/react-hooks.svg?style=flat-square)](http://www.npmtrends.com/@testing-library/react-hooks) [![MIT License](https://img.shields.io/npm/l/@testing-library/react-hooks.svg?style=flat-square)](https://github.com/testing-library/react-hooks-testing-library/blob/master/LICENSE.md) -[![All Contributors](https://img.shields.io/badge/all_contributors-13-orange.svg?style=flat-square)](#contributors) +[![All Contributors](https://img.shields.io/github/all-contributors/testing-library/react-hooks-testing-library?color=orange&style=flat-square)](#contributors) [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square)](http://makeapullrequest.com) [![Code of Conduct](https://img.shields.io/badge/code%20of-conduct-ff69b4.svg?style=flat-square)](https://github.com/testing-library/react-hooks-testing-library/blob/master/CODE_OF_CONDUCT.md) [![Netlify Status](https://api.netlify.com/api/v1/badges/9a8f27a5-df38-4910-a248-4908b1ba29a7/deploy-status)](https://app.netlify.com/sites/react-hooks-testing-library/deploys) @@ -166,7 +166,7 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d - + @@ -189,7 +189,7 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d - + @@ -199,6 +199,32 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d + + + + + + + + + + + + + + + + + + + + + + + + + +

Michael Peyper

💻 🎨 📖 🤔 🚇 📦 ⚠️ 🔧

Michael Peyper

💻 📖 🚇 ⚠️

otofu-square

💻

Patrick P. Henley

🤔 👀

Matheus Marques

💻

Adam Seckel

💻

keiya sasaki

⚠️

Hu Chen

💻 📖 💡

Josh

📖

Josh

📖 💬

Na'aman Hirschfeld

💻

Amr A.Mohammed

💻 ⚠️

Juhana Jauhiainen

💻

Jens Meindertsma

💻 ⚠️

Marco Moretti

🚇

Martin V.

📖

Erozak

📖

Nick McCurdy

🚧

Arya

📖

numb86

📖

Alex Young

🚧

Ben Lambert

📖

David Cho-Lerat

📖

Evan Harmon

📖

Jason Brown

📖

KahWee Teng

📖

Leonid Shagabutdinov

📖

Levi Butcher

📖

Michele Settepani

📖

Sam

📖

Tanay Pratap

📖

Tom Rees-Herdman

📖

iqbal125

📖

cliffzhaobupt

🚧