diff --git a/jest.config.js b/jest.config.js new file mode 100644 index 00000000..66b8064e --- /dev/null +++ b/jest.config.js @@ -0,0 +1,4 @@ +const { jest: jestConfig } = require('kcd-scripts/config') +module.exports = Object.assign(jestConfig, { + setupFiles: ['/src/__tests__/utils/runForRenderers.ts'] +}) diff --git a/src/__tests__/asyncHook.test.ts b/src/__tests__/asyncHook.test.ts new file mode 100644 index 00000000..17979ae2 --- /dev/null +++ b/src/__tests__/asyncHook.test.ts @@ -0,0 +1,258 @@ +import { useState, useRef, useEffect } from 'react' + +describe('async hook tests', () => { + const useSequence = (values: string[], intervalMs = 50) => { + 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) + } + }, intervalMs) + return () => { + clearInterval(interval) + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, otherValues) + + return value + } + + runForRenderers(['default', 'dom', 'native', 'server/hydrated'], ({ renderHook }) => { + 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 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 not reject when waiting for next update if timeout has been disabled', async () => { + const { result, waitForNextUpdate } = renderHook(() => useSequence(['first', 'second'], 1100)) + + expect(result.current).toBe('first') + + await waitForNextUpdate({ timeout: false }) + + expect(result.current).toBe('second') + }) + + 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 + }) + + 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 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) + + 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 not reject when waiting for expectation to pass if timeout has been disabled', async () => { + const { result, waitFor } = renderHook(() => useSequence(['first', 'second', 'third'], 550)) + + expect(result.current).toBe('first') + + await waitFor( + () => { + expect(result.current).toBe('third') + }, + { timeout: false } + ) + + expect(result.current).toBe('third') + }) + + test('should check on interval when waiting for expectation to pass', async () => { + const { result, waitFor } = renderHook(() => useSequence(['first', 'second', 'third'])) + + let checks = 0 + + await waitFor( + () => { + checks++ + return result.current === 'third' + }, + { interval: 100 } + ) + + expect(checks).toBe(3) + }) + + 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) + + 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 not reject when waiting for value to change if timeout is disabled', async () => { + const { result, waitForValueToChange } = renderHook(() => + useSequence(['first', 'second', 'third'], 550) + ) + + expect(result.current).toBe('first') + + await waitForValueToChange(() => result.current === 'third', { + timeout: false + }) + + expect(result.current).toBe('third') + }) + + 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')) + }) + }) +}) diff --git a/src/__tests__/autoCleanup.disabled.test.ts b/src/__tests__/autoCleanup.disabled.test.ts new file mode 100644 index 00000000..d3b1f31b --- /dev/null +++ b/src/__tests__/autoCleanup.disabled.test.ts @@ -0,0 +1,26 @@ +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', () => { + process.env.RHTL_SKIP_AUTO_CLEANUP = 'true' + + runForRenderers(['default', 'dom', 'native', 'server/hydrated'], ({ renderHook }) => { + let cleanupCalled = false + + test('first', () => { + const useHookWithCleanup = () => { + useEffect(() => { + return () => { + cleanupCalled = true + } + }) + } + renderHook(() => useHookWithCleanup()) + }) + + test('second', () => { + expect(cleanupCalled).toBe(false) + }) + }) +}) diff --git a/src/__tests__/autoCleanup.noAfterEach.test.ts b/src/__tests__/autoCleanup.noAfterEach.test.ts new file mode 100644 index 00000000..dad26492 --- /dev/null +++ b/src/__tests__/autoCleanup.noAfterEach.test.ts @@ -0,0 +1,28 @@ +import { useEffect } from 'react' + +// This verifies that if afterEach is unavailable +// then we DON'T auto-wire up the afterEach for folks +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 + + runForRenderers(['default', 'dom', 'native', 'server/hydrated'], ({ renderHook }) => { + let cleanupCalled = false + + test('first', () => { + const useHookWithCleanup = () => { + useEffect(() => { + return () => { + cleanupCalled = true + } + }) + } + renderHook(() => useHookWithCleanup()) + }) + + test('second', () => { + expect(cleanupCalled).toBe(false) + }) + }) +}) diff --git a/src/__tests__/autoCleanup.noProcessEnv.test.ts b/src/__tests__/autoCleanup.noProcessEnv.test.ts new file mode 100644 index 00000000..18a72827 --- /dev/null +++ b/src/__tests__/autoCleanup.noProcessEnv.test.ts @@ -0,0 +1,31 @@ +import { useEffect } from 'react' + +// This verifies that if process.env is unavailable +// then we still auto-wire up the afterEach for folks +describe('auto cleanup (no process.env) tests', () => { + process.env = { + ...process.env, + get RHTL_SKIP_AUTO_CLEANUP(): string | undefined { + throw new Error('expected') + } + } + + runForRenderers(['default', 'dom', 'native', 'server/hydrated'], ({ renderHook }) => { + let cleanupCalled = false + + test('first', () => { + const useHookWithCleanup = () => { + useEffect(() => { + return () => { + cleanupCalled = true + } + }) + } + renderHook(() => useHookWithCleanup()) + }) + + test('second', () => { + expect(cleanupCalled).toBe(true) + }) + }) +}) diff --git a/src/__tests__/autoCleanup.pure.test.ts b/src/__tests__/autoCleanup.pure.test.ts new file mode 100644 index 00000000..1ad8c317 --- /dev/null +++ b/src/__tests__/autoCleanup.pure.test.ts @@ -0,0 +1,27 @@ +import { useEffect } from 'react' + +// This verifies that if pure imports are used +// then we DON'T auto-wire up the afterEach for folks +describe('skip auto cleanup (pure) tests', () => { + runForRenderers( + ['default/pure', 'dom/pure', 'native/pure', 'server/hydrated/pure'], + ({ renderHook }) => { + let cleanupCalled = false + + test('first', () => { + const useHookWithCleanup = () => { + useEffect(() => { + return () => { + cleanupCalled = true + } + }) + } + renderHook(() => useHookWithCleanup()) + }) + + test('second', () => { + expect(cleanupCalled).toBe(false) + }) + } + ) +}) diff --git a/src/__tests__/autoCleanup.test.ts b/src/__tests__/autoCleanup.test.ts new file mode 100644 index 00000000..250ef5ee --- /dev/null +++ b/src/__tests__/autoCleanup.test.ts @@ -0,0 +1,52 @@ +import { useEffect } from 'react' + +// 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', () => { + runForRenderers(['default', 'dom', 'native'], ({ renderHook }) => { + let cleanupCalled = false + + test('first', () => { + const useHookWithCleanup = () => { + useEffect(() => { + return () => { + cleanupCalled = true + } + }) + } + renderHook(() => useHookWithCleanup()) + }) + + test('second', () => { + expect(cleanupCalled).toBe(true) + }) + }) + + runForRenderers(['server'], ({ renderHook }) => { + const cleanups: Record = { + ssr: false, + hydrated: false + } + + test('first (with hydration)', () => { + const useHookWithCleanup = (name: string) => { + useEffect(() => { + return () => { + cleanups[name] = true + } + }) + } + + renderHook(() => useHookWithCleanup('ssr')) + const { hydrate } = renderHook(() => useHookWithCleanup('hydrated')) + + hydrate() + }) + + test('second (with hydration)', () => { + expect(cleanups.ssr).toBe(false) + expect(cleanups.hydrated).toBe(true) + }) + }) +}) diff --git a/src/__tests__/autoDetectRenderer.test.ts b/src/__tests__/autoDetectRenderer.test.ts new file mode 100644 index 00000000..2e87d47e --- /dev/null +++ b/src/__tests__/autoDetectRenderer.test.ts @@ -0,0 +1,70 @@ +/* eslint-disable @typescript-eslint/no-var-requires */ +import { ReactHooksRenderer } from '../types/react' + +describe('auto-detect renderer', () => { + function setUpDependencies({ + reactTestRenderer, + reactDom + }: { + reactTestRenderer?: boolean + reactDom?: boolean + }) { + jest.resetModules() + jest.unmock('react-test-renderer') + jest.unmock('react-dom') + + if (!reactTestRenderer) { + jest.doMock('react-test-renderer', () => require('missing-dependency')) + } + + if (!reactDom) { + jest.doMock('react-dom', () => require('missing-dependency')) + } + } + + runForLazyRenderers(['default', 'default/pure'], (getRenderer, rendererName) => { + describe('react-test-renderer available', () => { + setUpDependencies({ reactTestRenderer: true, reactDom: true }) + + const actualRenderer = getRenderer() + const expectedRenderer = require(rendererName.includes('pure') + ? '../native/pure' + : '../native') as ReactHooksRenderer + + test('should resolve native renderer as default renderer', () => { + expect(actualRenderer).toEqual(expectedRenderer) + }) + }) + + describe('react-dom available', () => { + setUpDependencies({ reactTestRenderer: false, reactDom: true }) + + const actualRenderer = getRenderer() + const expectedRenderer = require(rendererName.includes('pure') + ? '../dom/pure' + : '../dom') as ReactHooksRenderer + + test('should resolve dom renderer as default renderer', () => { + expect(actualRenderer).toEqual(expectedRenderer) + }) + }) + + describe('no renderers available', () => { + setUpDependencies({ reactTestRenderer: false, reactDom: false }) + + test('should throw error if a default renderer cannot be resolved', () => { + jest.doMock('react-test-renderer', () => { + throw new Error('missing dependency') + }) + jest.doMock('react-dom', () => { + throw new Error('missing dependency') + }) + + const expectedMessage = + "Could not auto-detect a React renderer. Are you sure you've installed one of the following\n - react-dom\n - react-test-renderer\nIf you are using a bundler, please update your imports to use a specific renderer.\nFor instructions see: https://react-hooks-testing-library.com/installation#being-specific" + + expect(() => getRenderer()).toThrowError(new Error(expectedMessage)) + }) + }) + }) +}) diff --git a/src/__tests__/cleanup.test.ts b/src/__tests__/cleanup.test.ts new file mode 100644 index 00000000..8cadddab --- /dev/null +++ b/src/__tests__/cleanup.test.ts @@ -0,0 +1,166 @@ +import { useEffect } from 'react' + +describe('cleanup tests', () => { + runForRenderers( + ['default/pure', 'dom/pure', 'native/pure', 'server/hydrated/pure'], + ({ renderHook, cleanup, addCleanup, removeCleanup }) => { + test('should flush effects on cleanup', async () => { + let cleanupCalled = false + + const useHookWithCleanup = () => { + useEffect(() => { + return () => { + cleanupCalled = true + } + }) + } + + renderHook(() => useHookWithCleanup()) + + await cleanup() + + expect(cleanupCalled).toBe(true) + }) + + test('should cleanup all rendered hooks', async () => { + const cleanupCalled: boolean[] = [] + const useHookWithCleanup = (id: number) => { + useEffect(() => { + return () => { + cleanupCalled[id] = true + } + }) + } + + renderHook(() => useHookWithCleanup(1)) + renderHook(() => useHookWithCleanup(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 useHookWithCleanup = () => { + useEffect(() => { + return () => { + callSequence.push('unmount') + } + }) + } + renderHook(() => useHookWithCleanup()) + + 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 useHookWithCleanup = () => { + useEffect(() => { + return () => { + callSequence.push('unmount') + } + }) + } + renderHook(() => useHookWithCleanup()) + + 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 useHookWithCleanup = () => { + useEffect(() => { + return () => { + callSequence.push('unmount') + } + }) + } + renderHook(() => useHookWithCleanup()) + + 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 useHookWithCleanup = () => { + useEffect(() => { + return () => { + callSequence.push('unmount') + } + }) + } + renderHook(() => useHookWithCleanup()) + + remove() + + await cleanup() + + expect(callSequence).toEqual(['unmount', 'cleanup']) + }) + } + ) + + runForRenderers(['server/pure'], ({ renderHook, cleanup }) => { + test('should only cleanup hydrated hooks', async () => { + const cleanups: Record = { + ssr: false, + hydrated: false + } + + const useHookWithCleanup = (name: string) => { + useEffect(() => { + return () => { + cleanups[name] = true + } + }) + } + + renderHook(() => useHookWithCleanup('ssr')) + const { hydrate } = renderHook(() => useHookWithCleanup('hydrated')) + + hydrate() + + await cleanup() + + expect(cleanups.ssr).toBe(false) + expect(cleanups.hydrated).toBe(true) + }) + }) +}) diff --git a/src/__tests__/customHook.test.ts b/src/__tests__/customHook.test.ts new file mode 100644 index 00000000..a9eb0dff --- /dev/null +++ b/src/__tests__/customHook.test.ts @@ -0,0 +1,30 @@ +import { useState, useCallback } from 'react' + +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 } + } + + runForRenderers(['default', 'dom', 'native', 'server/hydrated'], ({ renderHook, act }) => { + 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/src/__tests__/defaultRenderer.pure.test.ts b/src/__tests__/defaultRenderer.pure.test.ts deleted file mode 100644 index d0fe14ff..00000000 --- a/src/__tests__/defaultRenderer.pure.test.ts +++ /dev/null @@ -1,41 +0,0 @@ -/* eslint-disable @typescript-eslint/no-var-requires */ -import { ReactHooksRenderer } from '../types/react' - -describe('default renderer', () => { - beforeEach(() => { - jest.resetModules() - }) - - test('should resolve native renderer as default renderer', () => { - const expectedRenderer = require('../native/pure') as ReactHooksRenderer - const actualRenderer = require('../pure') as ReactHooksRenderer - - expect(actualRenderer).toEqual(expectedRenderer) - }) - - test('should resolve dom renderer as default renderer', () => { - jest.doMock('react-test-renderer', () => { - throw new Error('missing dependency') - }) - - const expectedRenderer = require('../dom/pure') as ReactHooksRenderer - const actualRenderer = require('../pure') as ReactHooksRenderer - - expect(actualRenderer).toEqual(expectedRenderer) - }) - - test('should throw error if a default renderer cannot be resolved', () => { - jest.doMock('react-test-renderer', () => { - throw new Error('missing dependency') - }) - - jest.doMock('react-dom', () => { - throw new Error('missing dependency') - }) - - const expectedMessage = - "Could not auto-detect a React renderer. Are you sure you've installed one of the following\n - react-dom\n - react-test-renderer\nIf you are using a bundler, please update your imports to use a specific renderer.\nFor instructions see: https://react-hooks-testing-library.com/installation#being-specific" - - expect(() => require('../pure')).toThrowError(new Error(expectedMessage)) - }) -}) diff --git a/src/__tests__/defaultRenderer.test.ts b/src/__tests__/defaultRenderer.test.ts deleted file mode 100644 index de38354a..00000000 --- a/src/__tests__/defaultRenderer.test.ts +++ /dev/null @@ -1,8 +0,0 @@ -import * as actualRenderer from '..' -import * as expectedRenderer from '../native' - -describe('default renderer', () => { - test('should resolve native renderer as default renderer', () => { - expect(actualRenderer).toEqual(expectedRenderer) - }) -}) diff --git a/src/__tests__/errorHook.test.ts b/src/__tests__/errorHook.test.ts new file mode 100644 index 00000000..d93971ba --- /dev/null +++ b/src/__tests__/errorHook.test.ts @@ -0,0 +1,151 @@ +import { useState, useEffect } from 'react' + +describe('error hook tests', () => { + function throwError(shouldThrow?: boolean) { + if (shouldThrow) { + throw new Error('expected') + } + } + + runForRenderers(['default', 'dom', 'native', 'server/hydrated'], ({ renderHook }) => { + describe('synchronous', () => { + function useError(shouldThrow?: boolean) { + throwError(shouldThrow) + return true + } + + 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(({ shouldThrow }) => useError(shouldThrow), { + initialProps: { shouldThrow: true } + }) + + expect(result.error).not.toBe(undefined) + + rerender({ shouldThrow: false }) + + expect(result.current).not.toBe(undefined) + expect(result.error).toBe(undefined) + }) + }) + + describe('asynchronous', () => { + function useAsyncError(shouldThrow: boolean) { + const [value, setValue] = useState() + useEffect(() => { + const timeout = setTimeout(() => setValue(shouldThrow), 100) + return () => clearTimeout(timeout) + }, [shouldThrow]) + throwError(value) + return true + } + + 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( + ({ shouldThrow }) => useAsyncError(shouldThrow), + { initialProps: { shouldThrow: true } } + ) + + await waitForNextUpdate() + + expect(result.error).not.toBe(undefined) + + rerender({ shouldThrow: false }) + + await waitForNextUpdate() + + expect(result.current).not.toBe(undefined) + expect(result.error).toBe(undefined) + }) + }) + + describe('effect', () => { + function useEffectError(shouldThrow: boolean) { + useEffect(() => { + throwError(shouldThrow) + }, [shouldThrow]) + return true + } + + test('this one - should raise effect error', () => { + const { result } = renderHook(() => useEffectError(true)) + + expect(() => { + expect(result.current).not.toBe(undefined) + }).toThrow(Error('expected')) + }) + + test('this one - 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(({ shouldThrow }) => useEffectError(shouldThrow), { + initialProps: { shouldThrow: true } + }) + + expect(result.error).not.toBe(undefined) + + rerender({ shouldThrow: false }) + + expect(result.current).not.toBe(undefined) + expect(result.error).toBe(undefined) + }) + }) + }) +}) diff --git a/src/server/__tests__/errorSuppression.disabled.test.ts b/src/__tests__/errorSuppression.disabled.test.ts similarity index 56% rename from src/server/__tests__/errorSuppression.disabled.test.ts rename to src/__tests__/errorSuppression.disabled.test.ts index 8cf200ab..8d496ed0 100644 --- a/src/server/__tests__/errorSuppression.disabled.test.ts +++ b/src/__tests__/errorSuppression.disabled.test.ts @@ -3,11 +3,13 @@ describe('error output suppression (disabled) tests', () => { const originalConsoleError = console.error process.env.RHTL_DISABLE_ERROR_FILTERING = 'true' - require('..') - test('should not patch console.error', () => { - expect(console.error).toBe(originalConsoleError) + runForRenderers(['default', 'dom', 'native', 'server'], () => { + test('should not patch console.error', () => { + expect(console.error).toBe(originalConsoleError) + }) }) }) +// eslint-disable-next-line jest/no-export export {} diff --git a/src/server/__tests__/errorSuppression.noAfterEach.test.ts b/src/__tests__/errorSuppression.noAfterEach.test.ts similarity index 54% rename from src/server/__tests__/errorSuppression.noAfterEach.test.ts rename to src/__tests__/errorSuppression.noAfterEach.test.ts index f83d068f..952fe043 100644 --- a/src/server/__tests__/errorSuppression.noAfterEach.test.ts +++ b/src/__tests__/errorSuppression.noAfterEach.test.ts @@ -3,12 +3,15 @@ describe('error output suppression (noAfterEach) tests', () => { const originalConsoleError = console.error // @ts-expect-error Turning off AfterEach -- ignore Jest LifeCycle Type + // eslint-disable-next-line no-global-assign afterEach = false - require('..') - test('should not patch console.error', () => { - expect(console.error).toBe(originalConsoleError) + runForRenderers(['default', 'dom', 'native', 'server'], () => { + test('should not patch console.error', () => { + expect(console.error).toBe(originalConsoleError) + }) }) }) +// eslint-disable-next-line jest/no-export export {} diff --git a/src/server/__tests__/errorSuppression.noBeforeEach.test.ts b/src/__tests__/errorSuppression.noBeforeEach.test.ts similarity index 55% rename from src/server/__tests__/errorSuppression.noBeforeEach.test.ts rename to src/__tests__/errorSuppression.noBeforeEach.test.ts index 609cab95..f0bc5023 100644 --- a/src/server/__tests__/errorSuppression.noBeforeEach.test.ts +++ b/src/__tests__/errorSuppression.noBeforeEach.test.ts @@ -3,12 +3,15 @@ describe('error output suppression (noBeforeEach) tests', () => { const originalConsoleError = console.error // @ts-expect-error Turning off BeforeEach -- ignore Jest LifeCycle Type + // eslint-disable-next-line no-global-assign beforeEach = false - require('..') - test('should not patch console.error', () => { - expect(console.error).toBe(originalConsoleError) + runForRenderers(['default', 'dom', 'native', 'server'], () => { + test('should not patch console.error', () => { + expect(console.error).toBe(originalConsoleError) + }) }) }) +// eslint-disable-next-line jest/no-export export {} diff --git a/src/dom/__tests__/errorSuppression.noProcessEnv.test.ts b/src/__tests__/errorSuppression.noProcessEnv.test.ts similarity index 63% rename from src/dom/__tests__/errorSuppression.noProcessEnv.test.ts rename to src/__tests__/errorSuppression.noProcessEnv.test.ts index 414aea26..995d0346 100644 --- a/src/dom/__tests__/errorSuppression.noProcessEnv.test.ts +++ b/src/__tests__/errorSuppression.noProcessEnv.test.ts @@ -8,11 +8,13 @@ describe('error output suppression (no process.env) tests', () => { throw new Error('expected') } } - require('..') - test('should not patch console.error', () => { - expect(console.error).not.toBe(originalConsoleError) + runForRenderers(['default', 'dom', 'native', 'server'], () => { + test('should patch console.error', () => { + expect(console.error).not.toBe(originalConsoleError) + }) }) }) +// eslint-disable-next-line jest/no-export export {} diff --git a/src/__tests__/errorSuppression.pure.test.ts b/src/__tests__/errorSuppression.pure.test.ts new file mode 100644 index 00000000..22b31ac7 --- /dev/null +++ b/src/__tests__/errorSuppression.pure.test.ts @@ -0,0 +1,29 @@ +// This verifies that if pure imports are used +// then we DON'T auto-wire up the afterEach for folks +describe('error output suppression (pure) tests', () => { + const originalConsoleError = console.error + + runForRenderers( + ['default/pure', 'dom/pure', 'native/pure', 'server/pure'], + ({ suppressErrorOutput }) => { + test('should not patch console.error', () => { + expect(console.error).toBe(originalConsoleError) + }) + + test('should manually patch console.error', () => { + const restore = suppressErrorOutput() + + try { + expect(console.error).not.toBe(originalConsoleError) + } finally { + restore() + } + + expect(console.error).toBe(originalConsoleError) + }) + } + ) +}) + +// eslint-disable-next-line jest/no-export +export {} diff --git a/src/__tests__/errorSuppression.test.ts b/src/__tests__/errorSuppression.test.ts new file mode 100644 index 00000000..848d5e97 --- /dev/null +++ b/src/__tests__/errorSuppression.test.ts @@ -0,0 +1,74 @@ +import { useEffect } from 'react' + +describe('error output suppression tests', () => { + const consoleError = console.error + + runForRenderers( + ['default', 'dom', 'native', 'server/hydrated'], + ({ renderHook, act, suppressErrorOutput }, rendererName) => { + test('should not suppress relevant errors', () => { + console.error = jest.fn() + try { + const restoreConsole = suppressErrorOutput() + + console.error('expected') + console.error(new Error('expected')) + console.error('expected with args', new Error('expected')) + + restoreConsole() + + expect(console.error).toBeCalledWith('expected') + expect(console.error).toBeCalledWith(new Error('expected')) + expect(console.error).toBeCalledWith('expected with args', new Error('expected')) + expect(console.error).toBeCalledTimes(3) + } finally { + console.error = consoleError + } + }) + + test('should allow console.error to be mocked', async () => { + console.error = jest.fn() + + try { + const { rerender, unmount } = renderHook( + (stage) => { + useEffect(() => { + console.error(`expected in effect`) + return () => { + console.error(`expected in unmount`) + } + }, []) + console.error(`expected in ${stage}`) + }, + { + initialProps: 'render' + } + ) + + act(() => { + console.error('expected in act') + }) + + await act(async () => { + await new Promise((resolve) => setTimeout(resolve, 100)) + console.error('expected in async act') + }) + + rerender('rerender') + + unmount() + + expect(console.error).toBeCalledWith('expected in render') + expect(console.error).toBeCalledWith('expected in effect') + expect(console.error).toBeCalledWith('expected in act') + expect(console.error).toBeCalledWith('expected in async act') + expect(console.error).toBeCalledWith('expected in rerender') + expect(console.error).toBeCalledWith('expected in unmount') + expect(console.error).toBeCalledTimes(rendererName.includes('hydrated') ? 7 : 6) + } finally { + console.error = consoleError + } + }) + } + ) +}) diff --git a/src/__tests__/hydrationErrors.test.ts b/src/__tests__/hydrationErrors.test.ts new file mode 100644 index 00000000..b82ba96e --- /dev/null +++ b/src/__tests__/hydrationErrors.test.ts @@ -0,0 +1,30 @@ +import { useState, useCallback } from 'react' + +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 } + } + + runForRenderers(['server', 'server/pure'], ({ renderHook }) => { + 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') + ) + }) + }) +}) diff --git a/src/__tests__/resultHistory.test.ts b/src/__tests__/resultHistory.test.ts new file mode 100644 index 00000000..edb8837f --- /dev/null +++ b/src/__tests__/resultHistory.test.ts @@ -0,0 +1,78 @@ +describe('result history tests', () => { + function useValue(value: number) { + if (value === 2) { + throw Error('expected') + } + return value + } + + runForRenderers(['default', 'dom', 'native'], ({ renderHook }) => { + test('should capture all renders states of hook', () => { + const { result, rerender } = renderHook((value) => useValue(value), { + initialProps: 0 + }) + + expect(result.current).toEqual(0) + expect(result.all).toEqual([0]) + + rerender(1) + + expect(result.current).toBe(1) + expect(result.all).toEqual([0, 1]) + + rerender(2) + + expect(result.error).toEqual(Error('expected')) + expect(result.all).toEqual([0, 1, Error('expected')]) + + rerender(3) + + expect(result.current).toBe(3) + expect(result.all).toEqual([0, 1, Error('expected'), 3]) + + rerender() + + expect(result.current).toBe(3) + expect(result.all).toEqual([0, 1, Error('expected'), 3, 3]) + }) + }) + + runForRenderers(['server'], ({ renderHook }) => { + test('should capture all renders states of hook with hydration', () => { + const { result, hydrate, rerender } = renderHook((value) => useValue(value), { + initialProps: 0 + }) + + expect(result.current).toEqual(0) + expect(result.all).toEqual([0]) + + hydrate() + + expect(result.current).toEqual(0) + expect(result.all).toEqual([0, 0]) + + rerender(1) + + expect(result.current).toBe(1) + expect(result.all).toEqual([0, 0, 1]) + + rerender(2) + + expect(result.error).toEqual(Error('expected')) + expect(result.all).toEqual([0, 0, 1, Error('expected')]) + + rerender(3) + + expect(result.current).toBe(3) + expect(result.all).toEqual([0, 0, 1, Error('expected'), 3]) + + rerender() + + expect(result.current).toBe(3) + expect(result.all).toEqual([0, 0, 1, Error('expected'), 3, 3]) + }) + }) +}) + +// eslint-disable-next-line jest/no-export +export {} diff --git a/src/__tests__/suspenseHook.test.ts b/src/__tests__/suspenseHook.test.ts new file mode 100644 index 00000000..864c81e7 --- /dev/null +++ b/src/__tests__/suspenseHook.test.ts @@ -0,0 +1,64 @@ +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 + }) + + runForRenderers(['default', 'dom', 'native'], ({ renderHook }) => { + 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')) + }) + + test('should return undefined if current value is requested before suspension has resolved', async () => { + const { result } = renderHook(() => useFetchName(true)) + + expect(result.current).toBe(undefined) + }) + + test('should return undefined if error is requested before suspension has resolved', async () => { + const { result } = renderHook(() => useFetchName(true)) + + expect(result.error).toBe(undefined) + }) + }) +}) + +// eslint-disable-next-line jest/no-export +export {} diff --git a/src/__tests__/useContext.test.tsx b/src/__tests__/useContext.test.tsx new file mode 100644 index 00000000..841cbde7 --- /dev/null +++ b/src/__tests__/useContext.test.tsx @@ -0,0 +1,64 @@ +import React, { createContext, useContext } from 'react' + +describe('useContext tests', () => { + runForRenderers(['default', 'dom', 'native', 'server/hydrated'], ({ renderHook }) => { + 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/src/__tests__/useEffect.test.ts b/src/__tests__/useEffect.test.ts new file mode 100644 index 00000000..cc2cdd6a --- /dev/null +++ b/src/__tests__/useEffect.test.ts @@ -0,0 +1,99 @@ +import { useEffect, useLayoutEffect } from 'react' + +describe('useEffect tests', () => { + runForRenderers(['default', 'dom', 'native'], ({ renderHook }) => { + 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) + }) + }) + + runForRenderers(['server'], ({ renderHook }) => { + test('should handle useEffect hook when hydrated', () => { + const sideEffect: { [key: number]: boolean } = { 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/src/__tests__/useMemo.test.ts b/src/__tests__/useMemo.test.ts new file mode 100644 index 00000000..466546fe --- /dev/null +++ b/src/__tests__/useMemo.test.ts @@ -0,0 +1,65 @@ +import { useMemo, useCallback } from 'react' + +describe('useCallback tests', () => { + runForRenderers(['default', 'dom', 'native', 'server/hydrated'], ({ renderHook }) => { + 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/src/__tests__/useReducer.test.ts b/src/__tests__/useReducer.test.ts new file mode 100644 index 00000000..097831e4 --- /dev/null +++ b/src/__tests__/useReducer.test.ts @@ -0,0 +1,21 @@ +import { useReducer } from 'react' + +describe('useReducer tests', () => { + runForRenderers(['default', 'dom', 'native', 'server/hydrated'], ({ renderHook, act }) => { + 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/src/__tests__/useRef.test.ts b/src/__tests__/useRef.test.ts new file mode 100644 index 00000000..06cbc563 --- /dev/null +++ b/src/__tests__/useRef.test.ts @@ -0,0 +1,23 @@ +import { useRef, useImperativeHandle } from 'react' + +describe('useHook tests', () => { + runForRenderers(['default', 'dom', 'native', 'server/hydrated'], ({ renderHook }) => { + test('should handle useRef hook', () => { + const { result } = renderHook(() => useRef('value')) + + expect(result.current.current).toBe('value') + }) + + test('should handle useImperativeHandle hook', () => { + const { result } = renderHook(() => { + const ref = useRef boolean>>({}) + useImperativeHandle(ref, () => ({ + fakeImperativeMethod: () => true + })) + return ref + }) + + expect(result.current.current.fakeImperativeMethod()).toBe(true) + }) + }) +}) diff --git a/src/__tests__/useState.test.ts b/src/__tests__/useState.test.ts new file mode 100644 index 00000000..aff33ff4 --- /dev/null +++ b/src/__tests__/useState.test.ts @@ -0,0 +1,25 @@ +import { useState } from 'react' + +describe('useState tests', () => { + runForRenderers(['default', 'dom', 'native', 'server/hydrated'], ({ renderHook, act }) => { + test('should use setState value', () => { + const { result } = renderHook(() => { + const [value, setValue] = useState('foo') + return { value, setValue } + }) + + expect(result.current.value).toBe('foo') + }) + + test('should update setState value using setter', () => { + const { result } = renderHook(() => { + const [value, setValue] = useState('foo') + return { value, setValue } + }) + + act(() => result.current.setValue('bar')) + + expect(result.current.value).toBe('bar') + }) + }) +}) diff --git a/src/__tests__/utils/runForRenderers.ts b/src/__tests__/utils/runForRenderers.ts new file mode 100644 index 00000000..c0375f4d --- /dev/null +++ b/src/__tests__/utils/runForRenderers.ts @@ -0,0 +1,87 @@ +import { + ReactHooksRenderer, + ReactHooksServerRenderer, + RenderHookOptions, + RenderHookResult +} from '../../types/react' + +type RendererResolvers = typeof rendererResolvers +type Renderer = keyof RendererResolvers +type InferredRenderer = ReturnType + +declare global { + function runForRenderers( + renderers: TRenderers[], + fn: (renderer: InferredRenderer, rendererName: Renderer) => void + ): void + + function runForLazyRenderers( + renderers: TRenderer[], + fn: (getRenderer: () => InferredRenderer, rendererName: Renderer) => void + ): void +} + +function requireRenderer( + rendererName: Renderer +) { + let requirePath = `../../${rendererName}` + if (rendererName.startsWith('default')) { + requirePath = requirePath.replace('/default', '') + } + /* eslint-disable @typescript-eslint/no-var-requires */ + return require(requirePath) as TRendererType +} + +// This render turns the `server` renderer into a client renderer as many of the tests only +// require hydration after the hook is renderer to be able to be reused for all the renderers +function hydratedServerRenderer(baseRenderer: 'server' | 'server/pure'): ReactHooksRenderer { + const { renderHook, ...otherImports } = requireRenderer(baseRenderer) + + return { + renderHook( + callback: (props: TProps) => TResult, + options?: RenderHookOptions + ): RenderHookResult { + const { hydrate, ...otherUtils } = renderHook(callback, options) + hydrate() + return { + ...otherUtils + } + }, + ...otherImports + } +} + +const rendererResolvers = { + default: () => requireRenderer('default'), + dom: () => requireRenderer('dom'), + native: () => requireRenderer('native'), + server: () => requireRenderer('server'), + 'default/pure': () => requireRenderer('default/pure'), + 'dom/pure': () => requireRenderer('default/pure'), + 'native/pure': () => requireRenderer('default/pure'), + 'server/pure': () => requireRenderer('server/pure'), + 'server/hydrated': () => hydratedServerRenderer('server'), + 'server/hydrated/pure': () => hydratedServerRenderer('server/pure') +} + +global.runForRenderers = function runForRenderers( + renderers: TRenderer[], + fn: (renderer: InferredRenderer, rendererName: Renderer) => void +): void { + runForLazyRenderers(renderers, (getRenderer, rendererName) => fn(getRenderer(), rendererName)) +} + +global.runForLazyRenderers = function runForLazyRenderers( + renderers: TRenderer[], + fn: (getRenderer: () => InferredRenderer, rendererName: Renderer) => void +): void { + renderers.forEach((renderer) => { + // eslint-disable-next-line jest/valid-title + describe(renderer, () => { + fn(() => rendererResolvers[renderer]() as InferredRenderer, renderer) + }) + }) +} + +export {} diff --git a/src/dom/__tests__/asyncHook.test.ts b/src/dom/__tests__/asyncHook.test.ts deleted file mode 100644 index d460d35f..00000000 --- a/src/dom/__tests__/asyncHook.test.ts +++ /dev/null @@ -1,259 +0,0 @@ -import { useState, useRef, useEffect } from 'react' -import { renderHook } from '..' - -describe('async hook tests', () => { - const useSequence = (values: string[], intervalMs = 50) => { - 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) - } - }, intervalMs) - return () => { - clearInterval(interval) - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, 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 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 not reject when waiting for next update if timeout has been disabled', async () => { - const { result, waitForNextUpdate } = renderHook(() => useSequence(['first', 'second'], 1100)) - - expect(result.current).toBe('first') - - await waitForNextUpdate({ timeout: false }) - - expect(result.current).toBe('second') - }) - - 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 - }) - - 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 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) - - 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 not reject when waiting for expectation to pass if timeout has been disabled', async () => { - const { result, waitFor } = renderHook(() => useSequence(['first', 'second', 'third'], 550)) - - expect(result.current).toBe('first') - - await waitFor( - () => { - expect(result.current).toBe('third') - }, - { timeout: false } - ) - - expect(result.current).toBe('third') - }) - - test('should check on interval when waiting for expectation to pass', async () => { - const { result, waitFor } = renderHook(() => useSequence(['first', 'second', 'third'])) - - let checks = 0 - - try { - await waitFor( - () => { - checks++ - return result.current === 'third' - }, - { interval: 100 } - ) - } catch {} - - expect(checks).toBe(3) - }) - - 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) - - 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 not reject when waiting for value to change if timeout is disabled', async () => { - const { result, waitForValueToChange } = renderHook(() => - useSequence(['first', 'second', 'third'], 550) - ) - - expect(result.current).toBe('first') - - await waitForValueToChange(() => result.current === 'third', { - timeout: false - }) - - expect(result.current).toBe('third') - }) - - 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')) - }) -}) diff --git a/src/dom/__tests__/autoCleanup.disabled.test.ts b/src/dom/__tests__/autoCleanup.disabled.test.ts deleted file mode 100644 index cd32a7ee..00000000 --- a/src/dom/__tests__/autoCleanup.disabled.test.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { useEffect } from 'react' - -import { ReactHooksRenderer } from '../../types/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 - process.env.RHTL_SKIP_AUTO_CLEANUP = 'true' - const renderHook = (require('..') as ReactHooksRenderer).renderHook - - test('first', () => { - const hookWithCleanup = () => { - useEffect(() => { - return () => { - cleanupCalled = true - } - }) - } - renderHook(() => hookWithCleanup()) - }) - - test('second', () => { - expect(cleanupCalled).toBe(false) - }) -}) diff --git a/src/dom/__tests__/autoCleanup.noAfterEach.test.ts b/src/dom/__tests__/autoCleanup.noAfterEach.test.ts deleted file mode 100644 index 5f773d93..00000000 --- a/src/dom/__tests__/autoCleanup.noAfterEach.test.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { useEffect } from 'react' - -import { ReactHooksRenderer } from '../../types/react' - -// This verifies that if afterEach is unavailable -// then we DON'T auto-wire up the afterEach for folks -describe('skip auto cleanup (no afterEach) tests', () => { - let cleanupCalled = false - // @ts-expect-error Turning off AfterEach -- ignore Jest LifeCycle Type - afterEach = false - const renderHook = (require('..') as ReactHooksRenderer).renderHook - - test('first', () => { - const hookWithCleanup = () => { - useEffect(() => { - return () => { - cleanupCalled = true - } - }) - } - renderHook(() => hookWithCleanup()) - }) - - test('second', () => { - expect(cleanupCalled).toBe(false) - }) -}) diff --git a/src/dom/__tests__/autoCleanup.noProcessEnv.test.ts b/src/dom/__tests__/autoCleanup.noProcessEnv.test.ts deleted file mode 100644 index 35febc66..00000000 --- a/src/dom/__tests__/autoCleanup.noProcessEnv.test.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { useEffect } from 'react' - -import { ReactHooksRenderer } from '../../types/react' - -// This verifies that if process.env is unavailable -// then we still auto-wire up the afterEach for folks -describe('auto cleanup (no process.env) tests', () => { - let cleanupCalled = false - process.env = { - ...process.env, - get RHTL_SKIP_AUTO_CLEANUP(): string | undefined { - throw new Error('expected') - } - } - const renderHook = (require('..') as ReactHooksRenderer).renderHook - - test('first', () => { - const hookWithCleanup = () => { - useEffect(() => { - return () => { - cleanupCalled = true - } - }) - } - renderHook(() => hookWithCleanup()) - }) - - test('second', () => { - expect(cleanupCalled).toBe(true) - }) -}) diff --git a/src/dom/__tests__/autoCleanup.pure.test.ts b/src/dom/__tests__/autoCleanup.pure.test.ts deleted file mode 100644 index 66c27a07..00000000 --- a/src/dom/__tests__/autoCleanup.pure.test.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { useEffect } from 'react' -import { renderHook } from '../pure' - -// This verifies that if pure imports are used -// then we DON'T auto-wire up the afterEach for folks -describe('skip auto cleanup (pure) tests', () => { - let cleanupCalled = false - - test('first', () => { - const hookWithCleanup = () => { - useEffect(() => { - return () => { - cleanupCalled = true - } - }) - } - renderHook(() => hookWithCleanup()) - }) - - test('second', () => { - expect(cleanupCalled).toBe(false) - }) -}) diff --git a/src/dom/__tests__/autoCleanup.test.ts b/src/dom/__tests__/autoCleanup.test.ts deleted file mode 100644 index f783f1c2..00000000 --- a/src/dom/__tests__/autoCleanup.test.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { useEffect } from 'react' -import { renderHook } from '..' - -// 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/src/dom/__tests__/cleanup.test.ts b/src/dom/__tests__/cleanup.test.ts deleted file mode 100644 index 20a0f267..00000000 --- a/src/dom/__tests__/cleanup.test.ts +++ /dev/null @@ -1,135 +0,0 @@ -import { useEffect } from 'react' -import { renderHook, cleanup, addCleanup, removeCleanup } from '../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/src/dom/__tests__/customHook.test.ts b/src/dom/__tests__/customHook.test.ts deleted file mode 100644 index 5a1e83ab..00000000 --- a/src/dom/__tests__/customHook.test.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { useState, useCallback } from 'react' -import { renderHook, act } from '..' - -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/src/dom/__tests__/errorHook.test.ts b/src/dom/__tests__/errorHook.test.ts deleted file mode 100644 index 8e97e5de..00000000 --- a/src/dom/__tests__/errorHook.test.ts +++ /dev/null @@ -1,145 +0,0 @@ -import { useState, useEffect } from 'react' -import { renderHook } from '..' - -describe('error hook tests', () => { - function useError(throwError?: boolean) { - 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) - }, [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) - }) - }) - - describe('effect', () => { - test('this one - should raise effect error', () => { - const { result } = renderHook(() => useEffectError(true)) - - expect(() => { - expect(result.current).not.toBe(undefined) - }).toThrow(Error('expected')) - }) - - test('this one - 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/src/dom/__tests__/errorSuppression.disabled.test.ts b/src/dom/__tests__/errorSuppression.disabled.test.ts deleted file mode 100644 index 8cf200ab..00000000 --- a/src/dom/__tests__/errorSuppression.disabled.test.ts +++ /dev/null @@ -1,13 +0,0 @@ -// This verifies that if RHTL_DISABLE_ERROR_FILTERING is set -// then we DON'T auto-wire up the afterEach for folks -describe('error output suppression (disabled) tests', () => { - const originalConsoleError = console.error - process.env.RHTL_DISABLE_ERROR_FILTERING = 'true' - require('..') - - test('should not patch console.error', () => { - expect(console.error).toBe(originalConsoleError) - }) -}) - -export {} diff --git a/src/dom/__tests__/errorSuppression.noAfterEach.test.ts b/src/dom/__tests__/errorSuppression.noAfterEach.test.ts deleted file mode 100644 index f83d068f..00000000 --- a/src/dom/__tests__/errorSuppression.noAfterEach.test.ts +++ /dev/null @@ -1,14 +0,0 @@ -// This verifies that if afterEach is unavailable -// then we DON'T auto-wire up the afterEach for folks -describe('error output suppression (noAfterEach) tests', () => { - const originalConsoleError = console.error - // @ts-expect-error Turning off AfterEach -- ignore Jest LifeCycle Type - afterEach = false - require('..') - - test('should not patch console.error', () => { - expect(console.error).toBe(originalConsoleError) - }) -}) - -export {} diff --git a/src/dom/__tests__/errorSuppression.noBeforeEach.test.ts b/src/dom/__tests__/errorSuppression.noBeforeEach.test.ts deleted file mode 100644 index 609cab95..00000000 --- a/src/dom/__tests__/errorSuppression.noBeforeEach.test.ts +++ /dev/null @@ -1,14 +0,0 @@ -// This verifies that if afterEach is unavailable -// then we DON'T auto-wire up the afterEach for folks -describe('error output suppression (noBeforeEach) tests', () => { - const originalConsoleError = console.error - // @ts-expect-error Turning off BeforeEach -- ignore Jest LifeCycle Type - beforeEach = false - require('..') - - test('should not patch console.error', () => { - expect(console.error).toBe(originalConsoleError) - }) -}) - -export {} diff --git a/src/dom/__tests__/errorSuppression.pure.test.ts b/src/dom/__tests__/errorSuppression.pure.test.ts deleted file mode 100644 index 6e356cb6..00000000 --- a/src/dom/__tests__/errorSuppression.pure.test.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { suppressErrorOutput } from '../pure' - -// This verifies that if pure imports are used -// then we DON'T auto-wire up the afterEach for folks -describe('error output suppression (pure) tests', () => { - const originalConsoleError = console.error - - test('should not patch console.error', () => { - expect(console.error).toBe(originalConsoleError) - }) - - test('should manually patch console.error', () => { - const restore = suppressErrorOutput() - - try { - expect(console.error).not.toBe(originalConsoleError) - } finally { - restore() - } - - expect(console.error).toBe(originalConsoleError) - }) -}) diff --git a/src/dom/__tests__/errorSuppression.test.ts b/src/dom/__tests__/errorSuppression.test.ts deleted file mode 100644 index 8a4b72ad..00000000 --- a/src/dom/__tests__/errorSuppression.test.ts +++ /dev/null @@ -1,70 +0,0 @@ -import { useEffect } from 'react' -import { renderHook, act, suppressErrorOutput } from '..' - -describe('error output suppression tests', () => { - const consoleError = console.error - - test('should not suppress relevant errors', () => { - console.error = jest.fn() - try { - const restoreConsole = suppressErrorOutput() - - console.error('expected') - console.error(new Error('expected')) - console.error('expected with args', new Error('expected')) - - restoreConsole() - - expect(console.error).toBeCalledWith('expected') - expect(console.error).toBeCalledWith(new Error('expected')) - expect(console.error).toBeCalledWith('expected with args', new Error('expected')) - expect(console.error).toBeCalledTimes(3) - } finally { - console.error = consoleError - } - }) - - test('should allow console.error to be mocked', async () => { - console.error = jest.fn() - - try { - const { rerender, unmount } = renderHook( - (stage) => { - useEffect(() => { - console.error(`expected in effect`) - return () => { - console.error(`expected in unmount`) - } - }, []) - console.error(`expected in ${stage}`) - }, - { - initialProps: 'render' - } - ) - - act(() => { - console.error('expected in act') - }) - - await act(async () => { - await new Promise((resolve) => setTimeout(resolve, 100)) - console.error('expected in async act') - }) - - rerender('rerender') - - unmount() - - expect(console.error).toBeCalledWith('expected in render') - expect(console.error).toBeCalledWith('expected in effect') - expect(console.error).toBeCalledWith('expected in act') - expect(console.error).toBeCalledWith('expected in async act') - expect(console.error).toBeCalledWith('expected in rerender') - expect(console.error).toBeCalledWith('expected in unmount') - expect(console.error).toBeCalledTimes(6) - } finally { - console.error = consoleError - } - }) -}) diff --git a/src/dom/__tests__/resultHistory.test.ts b/src/dom/__tests__/resultHistory.test.ts deleted file mode 100644 index 69ce2408..00000000 --- a/src/dom/__tests__/resultHistory.test.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { renderHook } from '..' - -describe('result history tests', () => { - function useValue(value: number) { - if (value === 2) { - throw Error('expected') - } - return value - } - - test('should capture all renders states of hook', () => { - const { result, rerender } = renderHook((value) => useValue(value), { - initialProps: 0 - }) - - expect(result.current).toEqual(0) - expect(result.all).toEqual([0]) - - rerender(1) - - expect(result.current).toBe(1) - expect(result.all).toEqual([0, 1]) - - rerender(2) - - expect(result.error).toEqual(Error('expected')) - expect(result.all).toEqual([0, 1, Error('expected')]) - - rerender(3) - - expect(result.current).toBe(3) - expect(result.all).toEqual([0, 1, Error('expected'), 3]) - - rerender() - - expect(result.current).toBe(3) - expect(result.all).toEqual([0, 1, Error('expected'), 3, 3]) - }) -}) diff --git a/src/dom/__tests__/suspenseHook.test.ts b/src/dom/__tests__/suspenseHook.test.ts deleted file mode 100644 index 41e9f99a..00000000 --- a/src/dom/__tests__/suspenseHook.test.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { renderHook } from '..' - -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')) - }) - - test('should return undefined if current value is requested before suspension has resolved', async () => { - const { result } = renderHook(() => useFetchName(true)) - - expect(result.current).toBe(undefined) - }) - - test('should return undefined if error is requested before suspension has resolved', async () => { - const { result } = renderHook(() => useFetchName(true)) - - expect(result.error).toBe(undefined) - }) -}) diff --git a/src/dom/__tests__/useContext.test.tsx b/src/dom/__tests__/useContext.test.tsx deleted file mode 100644 index 84046e30..00000000 --- a/src/dom/__tests__/useContext.test.tsx +++ /dev/null @@ -1,63 +0,0 @@ -import React, { createContext, useContext } from 'react' -import { renderHook } from '..' - -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/src/dom/__tests__/useEffect.test.ts b/src/dom/__tests__/useEffect.test.ts deleted file mode 100644 index 0091b7a8..00000000 --- a/src/dom/__tests__/useEffect.test.ts +++ /dev/null @@ -1,62 +0,0 @@ -import { useEffect, useLayoutEffect } from 'react' -import { renderHook } from '..' - -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/src/dom/__tests__/useMemo.test.ts b/src/dom/__tests__/useMemo.test.ts deleted file mode 100644 index dcf0de7d..00000000 --- a/src/dom/__tests__/useMemo.test.ts +++ /dev/null @@ -1,64 +0,0 @@ -import { useMemo, useCallback } from 'react' -import { renderHook } from '..' - -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/src/dom/__tests__/useReducer.test.ts b/src/dom/__tests__/useReducer.test.ts deleted file mode 100644 index fab39201..00000000 --- a/src/dom/__tests__/useReducer.test.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { useReducer } from 'react' -import { renderHook, act } from '..' - -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/src/dom/__tests__/useRef.test.ts b/src/dom/__tests__/useRef.test.ts deleted file mode 100644 index a8663e16..00000000 --- a/src/dom/__tests__/useRef.test.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { useRef, useImperativeHandle } from 'react' -import { renderHook } from '..' - -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/src/dom/__tests__/useState.test.ts b/src/dom/__tests__/useState.test.ts deleted file mode 100644 index 78cbaa6f..00000000 --- a/src/dom/__tests__/useState.test.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { useState } from 'react' -import { renderHook, act } from '..' - -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/src/native/__tests__/asyncHook.test.ts b/src/native/__tests__/asyncHook.test.ts deleted file mode 100644 index d460d35f..00000000 --- a/src/native/__tests__/asyncHook.test.ts +++ /dev/null @@ -1,259 +0,0 @@ -import { useState, useRef, useEffect } from 'react' -import { renderHook } from '..' - -describe('async hook tests', () => { - const useSequence = (values: string[], intervalMs = 50) => { - 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) - } - }, intervalMs) - return () => { - clearInterval(interval) - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, 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 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 not reject when waiting for next update if timeout has been disabled', async () => { - const { result, waitForNextUpdate } = renderHook(() => useSequence(['first', 'second'], 1100)) - - expect(result.current).toBe('first') - - await waitForNextUpdate({ timeout: false }) - - expect(result.current).toBe('second') - }) - - 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 - }) - - 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 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) - - 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 not reject when waiting for expectation to pass if timeout has been disabled', async () => { - const { result, waitFor } = renderHook(() => useSequence(['first', 'second', 'third'], 550)) - - expect(result.current).toBe('first') - - await waitFor( - () => { - expect(result.current).toBe('third') - }, - { timeout: false } - ) - - expect(result.current).toBe('third') - }) - - test('should check on interval when waiting for expectation to pass', async () => { - const { result, waitFor } = renderHook(() => useSequence(['first', 'second', 'third'])) - - let checks = 0 - - try { - await waitFor( - () => { - checks++ - return result.current === 'third' - }, - { interval: 100 } - ) - } catch {} - - expect(checks).toBe(3) - }) - - 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) - - 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 not reject when waiting for value to change if timeout is disabled', async () => { - const { result, waitForValueToChange } = renderHook(() => - useSequence(['first', 'second', 'third'], 550) - ) - - expect(result.current).toBe('first') - - await waitForValueToChange(() => result.current === 'third', { - timeout: false - }) - - expect(result.current).toBe('third') - }) - - 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')) - }) -}) diff --git a/src/native/__tests__/autoCleanup.disabled.test.ts b/src/native/__tests__/autoCleanup.disabled.test.ts deleted file mode 100644 index cd32a7ee..00000000 --- a/src/native/__tests__/autoCleanup.disabled.test.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { useEffect } from 'react' - -import { ReactHooksRenderer } from '../../types/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 - process.env.RHTL_SKIP_AUTO_CLEANUP = 'true' - const renderHook = (require('..') as ReactHooksRenderer).renderHook - - test('first', () => { - const hookWithCleanup = () => { - useEffect(() => { - return () => { - cleanupCalled = true - } - }) - } - renderHook(() => hookWithCleanup()) - }) - - test('second', () => { - expect(cleanupCalled).toBe(false) - }) -}) diff --git a/src/native/__tests__/autoCleanup.noAfterEach.test.ts b/src/native/__tests__/autoCleanup.noAfterEach.test.ts deleted file mode 100644 index 5f773d93..00000000 --- a/src/native/__tests__/autoCleanup.noAfterEach.test.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { useEffect } from 'react' - -import { ReactHooksRenderer } from '../../types/react' - -// This verifies that if afterEach is unavailable -// then we DON'T auto-wire up the afterEach for folks -describe('skip auto cleanup (no afterEach) tests', () => { - let cleanupCalled = false - // @ts-expect-error Turning off AfterEach -- ignore Jest LifeCycle Type - afterEach = false - const renderHook = (require('..') as ReactHooksRenderer).renderHook - - test('first', () => { - const hookWithCleanup = () => { - useEffect(() => { - return () => { - cleanupCalled = true - } - }) - } - renderHook(() => hookWithCleanup()) - }) - - test('second', () => { - expect(cleanupCalled).toBe(false) - }) -}) diff --git a/src/native/__tests__/autoCleanup.noProcessEnv.test.ts b/src/native/__tests__/autoCleanup.noProcessEnv.test.ts deleted file mode 100644 index 35febc66..00000000 --- a/src/native/__tests__/autoCleanup.noProcessEnv.test.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { useEffect } from 'react' - -import { ReactHooksRenderer } from '../../types/react' - -// This verifies that if process.env is unavailable -// then we still auto-wire up the afterEach for folks -describe('auto cleanup (no process.env) tests', () => { - let cleanupCalled = false - process.env = { - ...process.env, - get RHTL_SKIP_AUTO_CLEANUP(): string | undefined { - throw new Error('expected') - } - } - const renderHook = (require('..') as ReactHooksRenderer).renderHook - - test('first', () => { - const hookWithCleanup = () => { - useEffect(() => { - return () => { - cleanupCalled = true - } - }) - } - renderHook(() => hookWithCleanup()) - }) - - test('second', () => { - expect(cleanupCalled).toBe(true) - }) -}) diff --git a/src/native/__tests__/autoCleanup.pure.test.ts b/src/native/__tests__/autoCleanup.pure.test.ts deleted file mode 100644 index 66c27a07..00000000 --- a/src/native/__tests__/autoCleanup.pure.test.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { useEffect } from 'react' -import { renderHook } from '../pure' - -// This verifies that if pure imports are used -// then we DON'T auto-wire up the afterEach for folks -describe('skip auto cleanup (pure) tests', () => { - let cleanupCalled = false - - test('first', () => { - const hookWithCleanup = () => { - useEffect(() => { - return () => { - cleanupCalled = true - } - }) - } - renderHook(() => hookWithCleanup()) - }) - - test('second', () => { - expect(cleanupCalled).toBe(false) - }) -}) diff --git a/src/native/__tests__/autoCleanup.test.ts b/src/native/__tests__/autoCleanup.test.ts deleted file mode 100644 index f783f1c2..00000000 --- a/src/native/__tests__/autoCleanup.test.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { useEffect } from 'react' -import { renderHook } from '..' - -// 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/src/native/__tests__/cleanup.test.ts b/src/native/__tests__/cleanup.test.ts deleted file mode 100644 index 20a0f267..00000000 --- a/src/native/__tests__/cleanup.test.ts +++ /dev/null @@ -1,135 +0,0 @@ -import { useEffect } from 'react' -import { renderHook, cleanup, addCleanup, removeCleanup } from '../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/src/native/__tests__/customHook.test.ts b/src/native/__tests__/customHook.test.ts deleted file mode 100644 index 5a1e83ab..00000000 --- a/src/native/__tests__/customHook.test.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { useState, useCallback } from 'react' -import { renderHook, act } from '..' - -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/src/native/__tests__/errorHook.test.ts b/src/native/__tests__/errorHook.test.ts deleted file mode 100644 index 8e97e5de..00000000 --- a/src/native/__tests__/errorHook.test.ts +++ /dev/null @@ -1,145 +0,0 @@ -import { useState, useEffect } from 'react' -import { renderHook } from '..' - -describe('error hook tests', () => { - function useError(throwError?: boolean) { - 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) - }, [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) - }) - }) - - describe('effect', () => { - test('this one - should raise effect error', () => { - const { result } = renderHook(() => useEffectError(true)) - - expect(() => { - expect(result.current).not.toBe(undefined) - }).toThrow(Error('expected')) - }) - - test('this one - 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/src/native/__tests__/errorSuppression.disabled.test.ts b/src/native/__tests__/errorSuppression.disabled.test.ts deleted file mode 100644 index 8cf200ab..00000000 --- a/src/native/__tests__/errorSuppression.disabled.test.ts +++ /dev/null @@ -1,13 +0,0 @@ -// This verifies that if RHTL_DISABLE_ERROR_FILTERING is set -// then we DON'T auto-wire up the afterEach for folks -describe('error output suppression (disabled) tests', () => { - const originalConsoleError = console.error - process.env.RHTL_DISABLE_ERROR_FILTERING = 'true' - require('..') - - test('should not patch console.error', () => { - expect(console.error).toBe(originalConsoleError) - }) -}) - -export {} diff --git a/src/native/__tests__/errorSuppression.noAfterEach.test.ts b/src/native/__tests__/errorSuppression.noAfterEach.test.ts deleted file mode 100644 index f83d068f..00000000 --- a/src/native/__tests__/errorSuppression.noAfterEach.test.ts +++ /dev/null @@ -1,14 +0,0 @@ -// This verifies that if afterEach is unavailable -// then we DON'T auto-wire up the afterEach for folks -describe('error output suppression (noAfterEach) tests', () => { - const originalConsoleError = console.error - // @ts-expect-error Turning off AfterEach -- ignore Jest LifeCycle Type - afterEach = false - require('..') - - test('should not patch console.error', () => { - expect(console.error).toBe(originalConsoleError) - }) -}) - -export {} diff --git a/src/native/__tests__/errorSuppression.noBeforeEach.test.ts b/src/native/__tests__/errorSuppression.noBeforeEach.test.ts deleted file mode 100644 index 609cab95..00000000 --- a/src/native/__tests__/errorSuppression.noBeforeEach.test.ts +++ /dev/null @@ -1,14 +0,0 @@ -// This verifies that if afterEach is unavailable -// then we DON'T auto-wire up the afterEach for folks -describe('error output suppression (noBeforeEach) tests', () => { - const originalConsoleError = console.error - // @ts-expect-error Turning off BeforeEach -- ignore Jest LifeCycle Type - beforeEach = false - require('..') - - test('should not patch console.error', () => { - expect(console.error).toBe(originalConsoleError) - }) -}) - -export {} diff --git a/src/native/__tests__/errorSuppression.noProcessEnv.test.ts b/src/native/__tests__/errorSuppression.noProcessEnv.test.ts deleted file mode 100644 index 414aea26..00000000 --- a/src/native/__tests__/errorSuppression.noProcessEnv.test.ts +++ /dev/null @@ -1,18 +0,0 @@ -// This verifies that if process.env is unavailable -// then we still auto-wire up the afterEach for folks -describe('error output suppression (no process.env) tests', () => { - const originalConsoleError = console.error - process.env = { - ...process.env, - get RHTL_DISABLE_ERROR_FILTERING(): string | undefined { - throw new Error('expected') - } - } - require('..') - - test('should not patch console.error', () => { - expect(console.error).not.toBe(originalConsoleError) - }) -}) - -export {} diff --git a/src/native/__tests__/errorSuppression.pure.test.ts b/src/native/__tests__/errorSuppression.pure.test.ts deleted file mode 100644 index 6e356cb6..00000000 --- a/src/native/__tests__/errorSuppression.pure.test.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { suppressErrorOutput } from '../pure' - -// This verifies that if pure imports are used -// then we DON'T auto-wire up the afterEach for folks -describe('error output suppression (pure) tests', () => { - const originalConsoleError = console.error - - test('should not patch console.error', () => { - expect(console.error).toBe(originalConsoleError) - }) - - test('should manually patch console.error', () => { - const restore = suppressErrorOutput() - - try { - expect(console.error).not.toBe(originalConsoleError) - } finally { - restore() - } - - expect(console.error).toBe(originalConsoleError) - }) -}) diff --git a/src/native/__tests__/errorSuppression.test.ts b/src/native/__tests__/errorSuppression.test.ts deleted file mode 100644 index a5cb4b79..00000000 --- a/src/native/__tests__/errorSuppression.test.ts +++ /dev/null @@ -1,70 +0,0 @@ -import { useEffect } from 'react' -import { renderHook, act, suppressErrorOutput } from '..' - -describe('error output suppression tests', () => { - const consoleError = console.error - - test('should not suppress relevant errors', () => { - console.error = jest.fn() - - try { - const restoreConsole = suppressErrorOutput() - - console.error('expected') - console.error(new Error('expected')) - console.error('expected with args', new Error('expected')) - - restoreConsole() - - expect(console.error).toBeCalledWith('expected') - expect(console.error).toBeCalledWith(new Error('expected')) - expect(console.error).toBeCalledWith('expected with args', new Error('expected')) - expect(console.error).toBeCalledTimes(3) - } finally { - console.error = consoleError - } - }) - - test('should allow console.error to be mocked', async () => { - console.error = jest.fn() - try { - const { rerender, unmount } = renderHook( - (stage) => { - useEffect(() => { - console.error(`expected in effect`) - return () => { - console.error(`expected in unmount`) - } - }, []) - console.error(`expected in ${stage}`) - }, - { - initialProps: 'render' - } - ) - - act(() => { - console.error('expected in act') - }) - - await act(async () => { - await new Promise((resolve) => setTimeout(resolve, 100)) - console.error('expected in async act') - }) - - rerender('rerender') - - unmount() - - expect(console.error).toBeCalledWith('expected in render') - expect(console.error).toBeCalledWith('expected in effect') - expect(console.error).toBeCalledWith('expected in act') - expect(console.error).toBeCalledWith('expected in async act') - expect(console.error).toBeCalledWith('expected in rerender') - expect(console.error).toBeCalledWith('expected in unmount') - expect(console.error).toBeCalledTimes(6) - } finally { - console.error = consoleError - } - }) -}) diff --git a/src/native/__tests__/resultHistory.test.ts b/src/native/__tests__/resultHistory.test.ts deleted file mode 100644 index 69ce2408..00000000 --- a/src/native/__tests__/resultHistory.test.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { renderHook } from '..' - -describe('result history tests', () => { - function useValue(value: number) { - if (value === 2) { - throw Error('expected') - } - return value - } - - test('should capture all renders states of hook', () => { - const { result, rerender } = renderHook((value) => useValue(value), { - initialProps: 0 - }) - - expect(result.current).toEqual(0) - expect(result.all).toEqual([0]) - - rerender(1) - - expect(result.current).toBe(1) - expect(result.all).toEqual([0, 1]) - - rerender(2) - - expect(result.error).toEqual(Error('expected')) - expect(result.all).toEqual([0, 1, Error('expected')]) - - rerender(3) - - expect(result.current).toBe(3) - expect(result.all).toEqual([0, 1, Error('expected'), 3]) - - rerender() - - expect(result.current).toBe(3) - expect(result.all).toEqual([0, 1, Error('expected'), 3, 3]) - }) -}) diff --git a/src/native/__tests__/suspenseHook.test.ts b/src/native/__tests__/suspenseHook.test.ts deleted file mode 100644 index 41e9f99a..00000000 --- a/src/native/__tests__/suspenseHook.test.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { renderHook } from '..' - -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')) - }) - - test('should return undefined if current value is requested before suspension has resolved', async () => { - const { result } = renderHook(() => useFetchName(true)) - - expect(result.current).toBe(undefined) - }) - - test('should return undefined if error is requested before suspension has resolved', async () => { - const { result } = renderHook(() => useFetchName(true)) - - expect(result.error).toBe(undefined) - }) -}) diff --git a/src/native/__tests__/useContext.test.tsx b/src/native/__tests__/useContext.test.tsx deleted file mode 100644 index 84046e30..00000000 --- a/src/native/__tests__/useContext.test.tsx +++ /dev/null @@ -1,63 +0,0 @@ -import React, { createContext, useContext } from 'react' -import { renderHook } from '..' - -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/src/native/__tests__/useEffect.test.ts b/src/native/__tests__/useEffect.test.ts deleted file mode 100644 index 0091b7a8..00000000 --- a/src/native/__tests__/useEffect.test.ts +++ /dev/null @@ -1,62 +0,0 @@ -import { useEffect, useLayoutEffect } from 'react' -import { renderHook } from '..' - -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/src/native/__tests__/useMemo.test.ts b/src/native/__tests__/useMemo.test.ts deleted file mode 100644 index dcf0de7d..00000000 --- a/src/native/__tests__/useMemo.test.ts +++ /dev/null @@ -1,64 +0,0 @@ -import { useMemo, useCallback } from 'react' -import { renderHook } from '..' - -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/src/native/__tests__/useReducer.test.ts b/src/native/__tests__/useReducer.test.ts deleted file mode 100644 index fab39201..00000000 --- a/src/native/__tests__/useReducer.test.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { useReducer } from 'react' -import { renderHook, act } from '..' - -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/src/native/__tests__/useRef.test.ts b/src/native/__tests__/useRef.test.ts deleted file mode 100644 index a8663e16..00000000 --- a/src/native/__tests__/useRef.test.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { useRef, useImperativeHandle } from 'react' -import { renderHook } from '..' - -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/src/native/__tests__/useState.test.ts b/src/native/__tests__/useState.test.ts deleted file mode 100644 index 78cbaa6f..00000000 --- a/src/native/__tests__/useState.test.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { useState } from 'react' -import { renderHook, act } from '..' - -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/src/server/__tests__/asyncHook.test.ts b/src/server/__tests__/asyncHook.test.ts deleted file mode 100644 index 7d23a981..00000000 --- a/src/server/__tests__/asyncHook.test.ts +++ /dev/null @@ -1,325 +0,0 @@ -import { useState, useRef, useEffect } from 'react' -import { renderHook } from '..' - -describe('async hook tests', () => { - const useSequence = (values: string[], intervalMs = 50) => { - 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) - } - }, intervalMs) - return () => { - clearInterval(interval) - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, otherValues) - - 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 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 not reject when waiting for next update if timeout has been disabled', async () => { - const { result, hydrate, waitForNextUpdate } = renderHook(() => - useSequence(['first', 'second'], 1100) - ) - - expect(result.current).toBe('first') - - hydrate() - - expect(result.current).toBe('first') - - await waitForNextUpdate({ timeout: false }) - - expect(result.current).toBe('second') - }) - - test('should wait for expectation to pass', async () => { - const { result, hydrate, waitFor } = renderHook(() => useSequence(['first', 'second', 'third'])) - - expect(result.current).toBe('first') - - hydrate() - - 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, 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 - }) - - expect(complete).toBe(true) - }) - - test('should not hang if expectation is already passing', async () => { - const { result, hydrate, waitFor } = renderHook(() => useSequence(['first', 'second'])) - - expect(result.current).toBe('first') - - hydrate() - - expect(result.current).toBe('first') - - let complete = false - await waitFor(() => { - expect(result.current).toBe('first') - complete = true - }) - expect(complete).toBe(true) - }) - - test('should wait for truthy value', async () => { - const { result, hydrate, waitFor } = renderHook(() => useSequence(['first', 'second', 'third'])) - - expect(result.current).toBe('first') - - hydrate() - - 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) - - expect(actual).toBe(expected) - }) - - test('should reject if timeout exceeded when waiting for expectation to pass', async () => { - const { result, hydrate, waitFor } = renderHook(() => useSequence(['first', 'second', 'third'])) - - expect(result.current).toBe('first') - - hydrate() - - 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 not reject when waiting for expectation to pass if timeout has been disabled', async () => { - const { result, hydrate, waitFor } = renderHook(() => - useSequence(['first', 'second', 'third'], 550) - ) - - expect(result.current).toBe('first') - - hydrate() - - expect(result.current).toBe('first') - - await waitFor( - () => { - expect(result.current).toBe('third') - }, - { timeout: false } - ) - - expect(result.current).toBe('third') - }) - - test('should check on interval when waiting for expectation to pass', async () => { - const { result, waitFor, hydrate } = renderHook(() => useSequence(['first', 'second', 'third'])) - - hydrate() - - let checks = 0 - - try { - await waitFor( - () => { - checks++ - return result.current === 'third' - }, - { interval: 100 } - ) - } catch {} - - expect(checks).toBe(3) - }) - - 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 wait for arbitrary value to change', async () => { - const { waitForValueToChange } = renderHook(() => null) - - let actual = 0 - const expected = 1 - - setTimeout(() => { - actual = expected - }, 200) - - await waitForValueToChange(() => actual) - - expect(actual).toBe(expected) - }) - - 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 not reject when waiting for value to change if timeout is disabled', async () => { - const { result, hydrate, waitForValueToChange } = renderHook(() => - useSequence(['first', 'second', 'third'], 550) - ) - - expect(result.current).toBe('first') - - hydrate() - - expect(result.current).toBe('first') - - await waitForValueToChange(() => result.current === 'third', { - timeout: false - }) - - expect(result.current).toBe('third') - }) - - 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')) - }) -}) diff --git a/src/server/__tests__/autoCleanup.disabled.test.ts b/src/server/__tests__/autoCleanup.disabled.test.ts deleted file mode 100644 index fd597168..00000000 --- a/src/server/__tests__/autoCleanup.disabled.test.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { useEffect } from 'react' - -import { ReactHooksServerRenderer } from '../../types/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', () => { - const cleanups: Record = { - ssr: false, - hydrated: false - } - process.env.RHTL_SKIP_AUTO_CLEANUP = 'true' - const renderHook = (require('..') as ReactHooksServerRenderer).renderHook - - test('first', () => { - const hookWithCleanup = (name: string) => { - 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(false) - }) -}) diff --git a/src/server/__tests__/autoCleanup.noAfterEach.test.ts b/src/server/__tests__/autoCleanup.noAfterEach.test.ts deleted file mode 100644 index d7ea75ac..00000000 --- a/src/server/__tests__/autoCleanup.noAfterEach.test.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { useEffect } from 'react' - -import { ReactHooksServerRenderer } from '../../types/react' - -// This verifies that if afterEach is unavailable -// then we DON'T auto-wire up the afterEach for folks -describe('skip auto cleanup (no afterEach) tests', () => { - const cleanups: Record = { - ssr: false, - hydrated: false - } - // @ts-expect-error Turning off AfterEach -- ignore Jest LifeCycle Type - afterEach = false - const renderHook = (require('..') as ReactHooksServerRenderer).renderHook - - test('first', () => { - const hookWithCleanup = (name: string) => { - 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(false) - }) -}) diff --git a/src/server/__tests__/autoCleanup.noProcessEnv.test.ts b/src/server/__tests__/autoCleanup.noProcessEnv.test.ts deleted file mode 100644 index de8bf795..00000000 --- a/src/server/__tests__/autoCleanup.noProcessEnv.test.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { useEffect } from 'react' - -import { ReactHooksServerRenderer } from '../../types/react' - -// This verifies that if process.env is unavailable -// then we still auto-wire up the afterEach for folks -describe('skip auto cleanup (no process.env) tests', () => { - const cleanups: Record = { - ssr: false, - hydrated: false - } - process.env = { - ...process.env, - get RHTL_SKIP_AUTO_CLEANUP(): string | undefined { - throw new Error('expected') - } - } - const renderHook = (require('..') as ReactHooksServerRenderer).renderHook - - test('first', () => { - const hookWithCleanup = (name: string) => { - 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/src/server/__tests__/autoCleanup.pure.test.ts b/src/server/__tests__/autoCleanup.pure.test.ts deleted file mode 100644 index ecf8a72d..00000000 --- a/src/server/__tests__/autoCleanup.pure.test.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { useEffect } from 'react' -import { renderHook } from '../pure' - -// This verifies that if pure imports are used -// then we DON'T auto-wire up the afterEach for folks -describe('skip auto cleanup (pure) tests', () => { - const cleanups: Record = { - ssr: false, - hydrated: false - } - - test('first', () => { - const hookWithCleanup = (name: string) => { - 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(false) - }) -}) diff --git a/src/server/__tests__/autoCleanup.test.ts b/src/server/__tests__/autoCleanup.test.ts deleted file mode 100644 index 87e473c1..00000000 --- a/src/server/__tests__/autoCleanup.test.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { useEffect } from 'react' -import { renderHook } from '..' - -// 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: Record = { - ssr: false, - hydrated: false - } - - test('first', () => { - const hookWithCleanup = (name: string) => { - 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/src/server/__tests__/cleanup.test.ts b/src/server/__tests__/cleanup.test.ts deleted file mode 100644 index c12815ac..00000000 --- a/src/server/__tests__/cleanup.test.ts +++ /dev/null @@ -1,67 +0,0 @@ -import { useEffect } from 'react' -import { renderHook, cleanup } from '..' - -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: number) => { - 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: number) => { - 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/src/server/__tests__/customHook.test.ts b/src/server/__tests__/customHook.test.ts deleted file mode 100644 index cb512682..00000000 --- a/src/server/__tests__/customHook.test.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { useState, useCallback } from 'react' -import { renderHook, act } from '..' - -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/src/server/__tests__/errorHook.test.ts b/src/server/__tests__/errorHook.test.ts deleted file mode 100644 index f7977465..00000000 --- a/src/server/__tests__/errorHook.test.ts +++ /dev/null @@ -1,166 +0,0 @@ -import { useState, useEffect } from 'react' - -import { renderHook } from '..' - -describe('error hook tests', () => { - function useError(throwError?: boolean) { - 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) - }, [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) - }) - }) - - describe('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/src/server/__tests__/errorSuppression.noProcessEnv.test.ts b/src/server/__tests__/errorSuppression.noProcessEnv.test.ts deleted file mode 100644 index 414aea26..00000000 --- a/src/server/__tests__/errorSuppression.noProcessEnv.test.ts +++ /dev/null @@ -1,18 +0,0 @@ -// This verifies that if process.env is unavailable -// then we still auto-wire up the afterEach for folks -describe('error output suppression (no process.env) tests', () => { - const originalConsoleError = console.error - process.env = { - ...process.env, - get RHTL_DISABLE_ERROR_FILTERING(): string | undefined { - throw new Error('expected') - } - } - require('..') - - test('should not patch console.error', () => { - expect(console.error).not.toBe(originalConsoleError) - }) -}) - -export {} diff --git a/src/server/__tests__/errorSuppression.pure.test.ts b/src/server/__tests__/errorSuppression.pure.test.ts deleted file mode 100644 index 6e356cb6..00000000 --- a/src/server/__tests__/errorSuppression.pure.test.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { suppressErrorOutput } from '../pure' - -// This verifies that if pure imports are used -// then we DON'T auto-wire up the afterEach for folks -describe('error output suppression (pure) tests', () => { - const originalConsoleError = console.error - - test('should not patch console.error', () => { - expect(console.error).toBe(originalConsoleError) - }) - - test('should manually patch console.error', () => { - const restore = suppressErrorOutput() - - try { - expect(console.error).not.toBe(originalConsoleError) - } finally { - restore() - } - - expect(console.error).toBe(originalConsoleError) - }) -}) diff --git a/src/server/__tests__/errorSuppression.test.ts b/src/server/__tests__/errorSuppression.test.ts deleted file mode 100644 index d97f9735..00000000 --- a/src/server/__tests__/errorSuppression.test.ts +++ /dev/null @@ -1,72 +0,0 @@ -import { useEffect } from 'react' -import { renderHook, act, suppressErrorOutput } from '..' - -describe('error output suppression tests', () => { - const consoleError = console.error - - test('should not suppress relevant errors', () => { - console.error = jest.fn() - - try { - const restoreConsole = suppressErrorOutput() - - console.error('expected') - console.error(new Error('expected')) - console.error('expected with args', new Error('expected')) - - restoreConsole() - - expect(console.error).toBeCalledWith('expected') - expect(console.error).toBeCalledWith(new Error('expected')) - expect(console.error).toBeCalledWith('expected with args', new Error('expected')) - expect(console.error).toBeCalledTimes(3) - } finally { - console.error = consoleError - } - }) - - test('should allow console.error to be mocked', async () => { - console.error = jest.fn() - try { - const { hydrate, rerender, unmount } = renderHook( - (stage) => { - useEffect(() => { - console.error(`expected in effect`) - return () => { - console.error(`expected in unmount`) - } - }, []) - console.error(`expected in ${stage}`) - }, - { - initialProps: 'render' - } - ) - - hydrate() - - act(() => { - console.error('expected in act') - }) - - await act(async () => { - await new Promise((resolve) => setTimeout(resolve, 100)) - console.error('expected in async act') - }) - - rerender('rerender') - - unmount() - - expect(console.error).toBeCalledWith('expected in render') // twice render/hydrate - expect(console.error).toBeCalledWith('expected in effect') - expect(console.error).toBeCalledWith('expected in act') - expect(console.error).toBeCalledWith('expected in async act') - expect(console.error).toBeCalledWith('expected in rerender') - expect(console.error).toBeCalledWith('expected in unmount') - expect(console.error).toBeCalledTimes(7) - } finally { - console.error = consoleError - } - }) -}) diff --git a/src/server/__tests__/hydrationErrors.test.ts b/src/server/__tests__/hydrationErrors.test.ts deleted file mode 100644 index 56a11aea..00000000 --- a/src/server/__tests__/hydrationErrors.test.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { useState, useCallback } from 'react' -import { renderHook } from '..' - -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') - ) - }) -}) diff --git a/src/server/__tests__/resultHistory.test.ts b/src/server/__tests__/resultHistory.test.ts deleted file mode 100644 index 5f2f8b9c..00000000 --- a/src/server/__tests__/resultHistory.test.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { renderHook } from '..' - -describe('result history tests', () => { - function useValue(value: number) { - if (value === 2) { - throw Error('expected') - } - return value - } - - test('should capture all renders states of hook', () => { - const { result, hydrate, rerender } = renderHook((value) => useValue(value), { - initialProps: 0 - }) - - expect(result.current).toEqual(0) - expect(result.all).toEqual([0]) - - hydrate() - - expect(result.current).toEqual(0) - expect(result.all).toEqual([0, 0]) - - rerender(1) - - expect(result.current).toBe(1) - expect(result.all).toEqual([0, 0, 1]) - - rerender(2) - - expect(result.error).toEqual(Error('expected')) - expect(result.all).toEqual([0, 0, 1, Error('expected')]) - - rerender(3) - - expect(result.current).toBe(3) - expect(result.all).toEqual([0, 0, 1, Error('expected'), 3]) - - rerender() - - expect(result.current).toBe(3) - expect(result.all).toEqual([0, 0, 1, Error('expected'), 3, 3]) - }) -}) diff --git a/src/server/__tests__/useContext.test.tsx b/src/server/__tests__/useContext.test.tsx deleted file mode 100644 index cf92aab4..00000000 --- a/src/server/__tests__/useContext.test.tsx +++ /dev/null @@ -1,45 +0,0 @@ -import React, { createContext, useContext } from 'react' -import { renderHook } from '..' - -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 value in context when props are updated', () => { - const TestContext = createContext('foo') - - const wrapper: React.FC<{ contextValue: string }> = ({ 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/src/server/__tests__/useEffect.test.ts b/src/server/__tests__/useEffect.test.ts deleted file mode 100644 index 782b7a03..00000000 --- a/src/server/__tests__/useEffect.test.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { useEffect } from 'react' -import { renderHook } from '..' - -describe('useEffect tests', () => { - test('should handle useEffect hook', () => { - const sideEffect: { [key: number]: boolean } = { 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/src/server/__tests__/useMemo.test.ts b/src/server/__tests__/useMemo.test.ts deleted file mode 100644 index d762cf6a..00000000 --- a/src/server/__tests__/useMemo.test.ts +++ /dev/null @@ -1,87 +0,0 @@ -import { useMemo, useCallback } from 'react' -import { renderHook } from '..' - -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/src/server/__tests__/useReducer.test.ts b/src/server/__tests__/useReducer.test.ts deleted file mode 100644 index 6184094a..00000000 --- a/src/server/__tests__/useReducer.test.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { useReducer } from 'react' -import { renderHook, act } from '..' - -describe('useReducer tests', () => { - test('should handle useReducer hook', () => { - const reducer = (state: number, action: { type: string }) => - 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/src/server/__tests__/useRef.test.ts b/src/server/__tests__/useRef.test.ts deleted file mode 100644 index f30d0bd7..00000000 --- a/src/server/__tests__/useRef.test.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { useRef, useImperativeHandle } from 'react' -import { renderHook } from '..' - -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 boolean>>({}) - useImperativeHandle(ref, () => ({ - fakeImperativeMethod: () => true - })) - return ref - }) - - expect(result.current.current).toEqual({}) - - hydrate() - - expect(result.current.current.fakeImperativeMethod()).toBe(true) - }) -}) diff --git a/src/server/__tests__/useState.test.ts b/src/server/__tests__/useState.test.ts deleted file mode 100644 index 27925863..00000000 --- a/src/server/__tests__/useState.test.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { useState } from 'react' -import { renderHook, act } from '..' - -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') - }) -}) diff --git a/src/types/react.ts b/src/types/react.ts index d7091776..c03ad33d 100644 --- a/src/types/react.ts +++ b/src/types/react.ts @@ -24,7 +24,7 @@ export type ReactHooksRenderer = { options?: RenderHookOptions ) => RenderHookResult act: Act - cleanup: () => void + cleanup: () => Promise addCleanup: (callback: CleanupCallback) => () => void removeCleanup: (callback: CleanupCallback) => void suppressErrorOutput: () => () => void