diff --git a/.all-contributorsrc b/.all-contributorsrc index 9c318f40..b8a61f52 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -145,7 +145,8 @@ "avatar_url": "https://avatars3.githubusercontent.com/u/3806031?v=4", "profile": "https://jsjoe.io", "contributions": [ - "tutorial" + "tutorial", + "test" ] }, { @@ -566,6 +567,42 @@ "bug", "review" ] + }, + { + "login": "snowystinger", + "name": "Robert Snow", + "avatar_url": "https://avatars.githubusercontent.com/u/698229?v=4", + "profile": "https://github.com/snowystinger", + "contributions": [ + "test" + ] + }, + { + "login": "chris110408", + "name": "Chris Chen", + "avatar_url": "https://avatars.githubusercontent.com/u/10645051?v=4", + "profile": "https://github.com/chris110408", + "contributions": [ + "test" + ] + }, + { + "login": "masious", + "name": "Masious", + "avatar_url": "https://avatars.githubusercontent.com/u/6429009?v=4", + "profile": "https://www.facebook.com/masoud.bonabi", + "contributions": [ + "doc" + ] + }, + { + "login": "Laishuxin", + "name": "Laishuxin", + "avatar_url": "https://avatars.githubusercontent.com/u/56504759?v=4", + "profile": "https://github.com/Laishuxin", + "contributions": [ + "doc" + ] } ], "skipCi": true, diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index 0144c34c..41dde974 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -9,7 +9,12 @@ on: - 'beta' - 'alpha' - '!all-contributors/**' - pull_request: {} + pull_request: + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + jobs: main: # ignore all-contributors PRs @@ -17,11 +22,9 @@ jobs: strategy: matrix: node: [12.13, 12, 14, 16] + react: [16.9.0, ^16, ^17] runs-on: ubuntu-latest steps: - - name: 🛑 Cancel Previous Runs - uses: styfle/cancel-workflow-action@0.9.0 - - name: ⬇️ Checkout repo uses: actions/checkout@v2 @@ -37,11 +40,14 @@ jobs: env: HUSKY_SKIP_INSTALL: true + - name: ⚛️ Use React version + run: REACT_VERSION=${{ matrix.react }} npm run install:react + - name: ▶️ Run validate script run: npm run validate - name: ⬆️ Upload coverage report - uses: codecov/codecov-action@v1 + uses: codecov/codecov-action@v2.1.0 release: needs: main @@ -51,9 +57,6 @@ jobs: contains('refs/heads/main,refs/heads/beta,refs/heads/next,refs/heads/alpha', github.ref) && github.event_name == 'push' }} steps: - - name: 🛑 Cancel Previous Runs - uses: styfle/cancel-workflow-action@0.9.0 - - name: ⬇️ Checkout repo uses: actions/checkout@v2 diff --git a/README.md b/README.md index e3bba708..969e6bc7 100644 --- a/README.md +++ b/README.md @@ -37,6 +37,34 @@ [![Tweet](https://img.shields.io/twitter/url/https/github.com/testing-library/react-hooks-testing-library.svg?style=social)](https://twitter.com/intent/tweet?text=Check%20out%20react-hooks-testing-library%20by%20%40testing-library%20https%3A%2F%2Fgithub.com%2Ftesting-library%2Freact-hooks-testing-library%20%F0%9F%91%8D) +## A Note about React 18 Support + +If you are using the current version of `react-testing-library`, replace + +```js +import { renderHook } from '@testing-library/react-hooks' +``` + +with + +```js +import { renderHook } from '@testing-library/react' +``` + +Once replaced, `@testing-library/react-hooks` can be uninstalled. + +### Details + +As part of the changes for React 18, it has been decided that the `renderHook` API provided by this +library will instead be included as official additions to both `react-testing-library` +([PR](https://github.com/testing-library/react-testing-library/pull/991)) and +`react-native-testing-library` +([PR](https://github.com/callstack/react-native-testing-library/pull/923)) with the intention being +to provide a more cohesive and consistent implementation for our users. + +Please be patient as we finalise these changes in the respective testing libraries. +In the mean time you can install `@testing-library/react@^13.1` + ## Table of Contents @@ -146,9 +174,9 @@ to test against. It also does not come installed with a specific renderer, we cu [`react-test-renderer`](https://www.npmjs.com/package/react-test-renderer) and [`react-dom`](https://www.npmjs.com/package/react-dom). You only need to install one of them, however, if you do have both installed, we will use `react-test-renderer` as the default. For more -information see the [installation docs](https://react-hooks-testing-library.com/#installation). -Generally, the installed versions for `react` and the selected renderer should have matching -versions: +information see the +[installation docs](https://react-hooks-testing-library.com/installation#renderer). Generally, the +installed versions for `react` and the selected renderer should have matching versions: ```sh npm install react@^16.9.0 @@ -186,7 +214,7 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
Vince Malone

💻
Sebastian Weber

📝
Christian Gill

📖 -
JavaScript Joe

+
JavaScript Joe

⚠️
Sarah Dayan

📦 @@ -246,6 +274,10 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
Matan Borenkraout

🚧
andyrooger

💻
Bryan Wain

🐛 👀 +
Robert Snow

⚠️ +
Chris Chen

⚠️ +
Masious

📖 +
Laishuxin

📖 diff --git a/docs/api-reference.md b/docs/api-reference.md index 7416d1f7..6a95b87f 100644 --- a/docs/api-reference.md +++ b/docs/api-reference.md @@ -68,7 +68,7 @@ The `renderHook` function returns an object that has the following properties: } ``` -The `current` value or the `result` will reflect the latest of whatever is returned from the +The `current` value of the `result` will reflect the latest of whatever is returned from the `callback` passed to `renderHook`. Any thrown values from the latest call will be reflected in the `error` value of the `result`. The `all` value is an array containing all the returns (including the most recent) from the callback. These could be `result` or an `error` depending on what the callback @@ -327,7 +327,7 @@ variable to `true` before importing `@testing-library/react-hooks` will also dis If you are using [a pure import](/installation#pure-imports), you are running your tests in an environment that does not support `beforeEach` and `afterEach`, or if the automatic suppression is not available to you for some other reason, then you can use the `suppressErrorOutput` export to -manually start and top suppress the output: +manually start and stop suppressing the output: ```ts import { renderHook, suppressErrorOutput } from '@testing-library/react-hooks/pure' diff --git a/docs/installation.md b/docs/installation.md index d83eed05..13104cfe 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -42,7 +42,7 @@ your hook. We currently support two different renderers: - `react-test-renderer` - `react-dom` -When using standard import for this library (show below), we will attempt to auto-detect which +When using standard import for this library (see below), we will attempt to auto-detect which renderer you have installed and use it without needing any specific wiring up to make it happen. If you have both installed in your project, and you use the standard import (see below) the library will default to using `react-test-renderer`. @@ -63,7 +63,7 @@ import { renderHook } from '@testing-library/react-hooks' ### Act Each render also provides a unique [`act` function](https://reactjs.org/docs/test-utils.html#act) -that cannot be used with other renderers. In order to simplify with `act `function you need to use, +that cannot be used with other renderers. In order to simplify which `act `function you need to use, we also export the correct one alongside the detected renderer for you: ```js @@ -73,8 +73,8 @@ import { renderHook, act } from '@testing-library/react-hooks' ## Being specific Auto-detection is great for simplifying setup and getting out of your way, but sometimes you do need -a little but more control. If a test needs requires a specific type of environment, the import can -be appended to force a specific renderer to be use. The supported environments are: +a little bit more control. If a test needs a specific type of environment, the import can +be appended to force a specific renderer to be used. The supported environments are: - `dom` - `native` diff --git a/docs/introduction.md b/docs/introduction.md index de33732a..34467e3e 100644 --- a/docs/introduction.md +++ b/docs/introduction.md @@ -23,6 +23,17 @@ route: '/'
+## A Note about React 18 Support + +As part of the changes for React 18, it has been decided that the `renderHook` API provided by this +library will instead be included as official additions to both `react-testing-library` +([PR](https://github.com/testing-library/react-testing-library/pull/991)) and +`react-native-testing-library` +([PR](https://github.com/callstack/react-native-testing-library/pull/923)) with the intention being +to provide a more cohesive and consistent implementation for our users. + +Please be patient as we finalise these changes in the respective testing libraries. + ## The problem You're writing an awesome custom hook and you want to test it, but as soon as you call it you see diff --git a/docs/usage/ssr.md b/docs/usage/ssr.md index e3ff6672..88f0c9cc 100644 --- a/docs/usage/ssr.md +++ b/docs/usage/ssr.md @@ -46,7 +46,7 @@ import { renderHook, act } from '@testing-library/react-hooks/server' import useCounter from './useCounter' test('should increment counter', () => { - const { result } = renderHook(() => useCounter(0)) + const { result } = renderHook(() => useCounter()) act(() => { result.current.increment() @@ -64,7 +64,7 @@ import { renderHook, act } from '@testing-library/react-hooks/server' import useCounter from './useCounter' test('should increment counter', () => { - const { result, hydrate } = renderHook(() => useCounter(0)) + const { result, hydrate } = renderHook(() => useCounter()) hydrate() 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/package.json b/package.json index 7d5c3eb0..0b89d6eb 100644 --- a/package.json +++ b/package.json @@ -33,7 +33,6 @@ "scripts": { "setup": "npm install && npm run validate -s", "validate": "kcd-scripts validate", - "prepare": "npm run build", "build": "kcd-scripts build --out-dir lib && npm run generate:submodules", "generate:submodules": "ts-node scripts/generate-submodules.ts", "test": "kcd-scripts test", @@ -43,38 +42,48 @@ "coverage": "codecov", "docs:dev": "docz dev", "docs:build": "docz build", - "contributors:add": "all-contributors add" + "contributors:add": "all-contributors add", + "install:react": "npm install --no-save react@${REACT_VERSION:-latest} react-dom@${REACT_VERSION:-latest} react-test-renderer@${REACT_VERSION:-latest}", + "install:react-16.9.0": "cross-env REACT_VERSION=16.9.0 npm run install:react", + "install:react-16": "cross-env REACT_VERSION=^16 npm run install:react", + "install:react-17": "cross-env REACT_VERSION=^17 npm run install:react" }, "dependencies": { "@babel/runtime": "^7.12.5", - "@types/react": ">=16.9.0", - "@types/react-dom": ">=16.9.0", - "@types/react-test-renderer": ">=16.9.0", "react-error-boundary": "^3.1.0" }, "devDependencies": { - "@typescript-eslint/eslint-plugin": "^4.9.1", - "@typescript-eslint/parser": "^4.9.1", + "@types/react": "17.0.44", + "@types/react-dom": "17.0.15", + "@types/react-test-renderer": "17.0.1", + "@typescript-eslint/eslint-plugin": "5.11.0", + "@typescript-eslint/parser": "5.11.0", "all-contributors-cli": "6.20.0", - "codecov": "3.8.2", + "codecov": "3.8.3", + "cross-env": "7.0.3", "docz": "2.3.1", "docz-theme-default": "1.2.0", "docz-utils": "2.3.0", - "eslint": "7.28.0", - "kcd-scripts": "10.0.0", - "prettier": "^2.2.1", + "eslint": "7.32.0", + "get-pkg-repo": "4.1.1", + "kcd-scripts": "11.2.2", + "prettier": "2.5.1", "react": "17.0.2", - "react-dom": "^17.0.1", + "react-dom": "17.0.2", "react-test-renderer": "17.0.2", - "ts-node": "^10.0.0", - "typescript": "4.3.2" + "ts-node": "10.5.0", + "typescript": "4.5.5" }, "peerDependencies": { - "react": ">=16.9.0", - "react-dom": ">=16.9.0", - "react-test-renderer": ">=16.9.0" + "@types/react": "^16.9.0 || ^17.0.0", + "react": "^16.9.0 || ^17.0.0", + "react-dom": "^16.9.0 || ^17.0.0", + "react-test-renderer": "^16.9.0 || ^17.0.0" }, "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, "react-dom": { "optional": true }, diff --git a/src/__tests__/asyncHook.fakeTimers.test.ts b/src/__tests__/asyncHook.fakeTimers.test.ts new file mode 100644 index 00000000..98d6b2c9 --- /dev/null +++ b/src/__tests__/asyncHook.fakeTimers.test.ts @@ -0,0 +1,58 @@ +describe('async hook (fake timers) tests', () => { + beforeEach(() => { + jest.useFakeTimers() + }) + + afterEach(() => { + jest.useRealTimers() + }) + + runForRenderers(['default', 'dom', 'native', 'server/hydrated'], ({ renderHook }) => { + test('should wait for arbitrary expectation to pass when using advanceTimersByTime()', async () => { + const { waitFor } = renderHook(() => null) + + let actual = 0 + const expected = 1 + + setTimeout(() => { + actual = expected + }, 200) + + let complete = false + + jest.advanceTimersByTime(200) + + await waitFor(() => { + expect(actual).toBe(expected) + complete = true + }) + + expect(complete).toBe(true) + }) + + test('should wait for arbitrary expectation to pass when using runOnlyPendingTimers()', async () => { + const { waitFor } = renderHook(() => null) + + let actual = 0 + const expected = 1 + + setTimeout(() => { + actual = expected + }, 200) + + let complete = false + + jest.runOnlyPendingTimers() + + await waitFor(() => { + expect(actual).toBe(expected) + complete = true + }) + + expect(complete).toBe(true) + }) + }) +}) + +// eslint-disable-next-line jest/no-export +export {} 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.test.ts b/src/__tests__/defaultRenderer.test.ts deleted file mode 100644 index 45d2e7ce..00000000 --- a/src/__tests__/defaultRenderer.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('..') 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('..') 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('..')).toThrowError(new Error(expectedMessage)) - }) -}) 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/__tests__/errorSuppression.disabled.test.ts b/src/__tests__/errorSuppression.disabled.test.ts new file mode 100644 index 00000000..8d496ed0 --- /dev/null +++ b/src/__tests__/errorSuppression.disabled.test.ts @@ -0,0 +1,15 @@ +// 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' + + 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/__tests__/errorSuppression.noAfterEach.test.ts b/src/__tests__/errorSuppression.noAfterEach.test.ts new file mode 100644 index 00000000..952fe043 --- /dev/null +++ b/src/__tests__/errorSuppression.noAfterEach.test.ts @@ -0,0 +1,17 @@ +// 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 + // eslint-disable-next-line no-global-assign + afterEach = false + + 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/__tests__/errorSuppression.noBeforeEach.test.ts b/src/__tests__/errorSuppression.noBeforeEach.test.ts new file mode 100644 index 00000000..f0bc5023 --- /dev/null +++ b/src/__tests__/errorSuppression.noBeforeEach.test.ts @@ -0,0 +1,17 @@ +// 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 + // eslint-disable-next-line no-global-assign + beforeEach = false + + 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/__tests__/errorSuppression.noProcessEnv.test.ts b/src/__tests__/errorSuppression.noProcessEnv.test.ts new file mode 100644 index 00000000..995d0346 --- /dev/null +++ b/src/__tests__/errorSuppression.noProcessEnv.test.ts @@ -0,0 +1,20 @@ +// 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') + } + } + + 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__/ssr.test.ts b/src/__tests__/ssr.test.ts new file mode 100644 index 00000000..1119d1a1 --- /dev/null +++ b/src/__tests__/ssr.test.ts @@ -0,0 +1,18 @@ +/** + * @jest-environment node + */ +import { useState } from 'react' + +// This verifies that renderHook can be called in +// a SSR-like environment. +describe('renderHook', () => { + function useLoading() { + const [loading, setLoading] = useState(false) + return { loading, setLoading } + } + runForRenderers(['server'], ({ renderHook }) => { + test('should not throw in SSR environment', () => { + expect(() => renderHook(() => useLoading())).not.toThrowError('document is not defined') + }) + }) +}) 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..13c9aa92 --- /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('dom/pure'), + 'native/pure': () => requireRenderer('native/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/core/asyncUtils.ts b/src/core/asyncUtils.ts index fe44c715..a7424036 100644 --- a/src/core/asyncUtils.ts +++ b/src/core/asyncUtils.ts @@ -7,7 +7,7 @@ import { AsyncUtils } from '../types' -import { resolveAfter, callAfter } from '../helpers/promises' +import { createTimeoutController } from '../helpers/createTimeoutController' import { TimeoutError } from '../helpers/error' const DEFAULT_INTERVAL = 50 @@ -20,37 +20,26 @@ function asyncUtils(act: Act, addResolver: (callback: () => void) => void): Asyn return callbackResult ?? callbackResult === undefined } + const timeoutSignal = createTimeoutController(timeout) + const waitForResult = async () => { while (true) { - await Promise.race( - [ - new Promise((resolve) => addResolver(resolve)), - interval && resolveAfter(interval) - ].filter(Boolean) - ) - - if (checkResult()) { + const intervalSignal = createTimeoutController(interval) + timeoutSignal.onTimeout(() => intervalSignal.cancel()) + + await intervalSignal.wrap(new Promise(addResolver)) + + if (checkResult() || timeoutSignal.timedOut) { return } } } - let timedOut = false - if (!checkResult()) { - if (timeout) { - const timeoutPromise = () => - callAfter(() => { - timedOut = true - }, timeout) - - await act(() => Promise.race([waitForResult(), timeoutPromise()])) - } else { - await act(waitForResult) - } + await act(() => timeoutSignal.wrap(waitForResult())) } - return !timedOut + return !timeoutSignal.timedOut } const waitFor = async ( 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 2c574b83..00000000 --- a/src/dom/__tests__/autoCleanup.disabled.test.ts +++ /dev/null @@ -1,30 +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 - let renderHook: ReactHooksRenderer['renderHook'] - - beforeAll(() => { - process.env.RHTL_SKIP_AUTO_CLEANUP = 'true' - 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 40b33f16..00000000 --- a/src/dom/__tests__/autoCleanup.noAfterEach.test.ts +++ /dev/null @@ -1,31 +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 - let renderHook: ReactHooksRenderer['renderHook'] - - beforeAll(() => { - // @ts-expect-error Turning off AfterEach -- ignore Jest LifeCycle Type - afterEach = false - 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 f6adc8ad..00000000 --- a/src/dom/__tests__/autoCleanup.noProcessEnv.test.ts +++ /dev/null @@ -1,40 +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('skip auto cleanup (no process.env) tests', () => { - const originalEnv = process.env - let cleanupCalled = false - let renderHook: ReactHooksRenderer['renderHook'] - - beforeAll(() => { - process.env = { - ...process.env, - get RHTL_SKIP_AUTO_CLEANUP(): string | undefined { - throw new Error('expected') - } - } - renderHook = (require('..') as ReactHooksRenderer).renderHook - }) - - afterAll(() => { - process.env = originalEnv - }) - - 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 1f84b87c..00000000 --- a/src/dom/__tests__/autoCleanup.pure.test.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { useEffect } from 'react' - -import { ReactHooksRenderer } from '../../types/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', () => { - let cleanupCalled = false - let renderHook: ReactHooksRenderer['renderHook'] - - beforeAll(() => { - renderHook = (require('../pure') 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.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 6e6c0a38..00000000 --- a/src/dom/__tests__/errorHook.test.ts +++ /dev/null @@ -1,145 +0,0 @@ -import { useState, useEffect } from 'react' -import { renderHook, act } 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 e1921f09..00000000 --- a/src/dom/__tests__/errorSuppression.disabled.test.ts +++ /dev/null @@ -1,16 +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 - - beforeAll(() => { - 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 c736020e..00000000 --- a/src/dom/__tests__/errorSuppression.noAfterEach.test.ts +++ /dev/null @@ -1,17 +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 - - beforeAll(() => { - // @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 c3f2496f..00000000 --- a/src/dom/__tests__/errorSuppression.noBeforeEach.test.ts +++ /dev/null @@ -1,17 +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 - - beforeAll(() => { - // @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.noProcessEnv.test.ts b/src/dom/__tests__/errorSuppression.noProcessEnv.test.ts deleted file mode 100644 index 24a50f21..00000000 --- a/src/dom/__tests__/errorSuppression.noProcessEnv.test.ts +++ /dev/null @@ -1,26 +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 originalEnv = process.env - const originalConsoleError = console.error - - beforeAll(() => { - process.env = { - ...process.env, - get RHTL_DISABLE_ERROR_FILTERING(): string | undefined { - throw new Error('expected') - } - } - require('..') - }) - - afterAll(() => { - process.env = originalEnv - }) - - test('should not patch console.error', () => { - expect(console.error).not.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 e60ec710..00000000 --- a/src/dom/__tests__/errorSuppression.pure.test.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { ReactHooksRenderer } from '../../types/react' - -// 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 - - let suppressErrorOutput!: ReactHooksRenderer['suppressErrorOutput'] - - beforeAll(() => { - suppressErrorOutput = (require('../pure') as ReactHooksRenderer).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) - }) -}) diff --git a/src/dom/__tests__/errorSuppression.test.ts b/src/dom/__tests__/errorSuppression.test.ts deleted file mode 100644 index 69250f47..00000000 --- a/src/dom/__tests__/errorSuppression.test.ts +++ /dev/null @@ -1,75 +0,0 @@ -import { useEffect } from 'react' - -import { ReactHooksRenderer } from '../../types/react' - -describe('error output suppression tests', () => { - test('should not suppress relevant errors', () => { - const consoleError = console.error - console.error = jest.fn() - - const { suppressErrorOutput } = require('..') as ReactHooksRenderer - - 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 () => { - const { renderHook, act } = require('..') as ReactHooksRenderer - const consoleError = console.error - 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/helpers/createTimeoutController.ts b/src/helpers/createTimeoutController.ts new file mode 100644 index 00000000..643d3768 --- /dev/null +++ b/src/helpers/createTimeoutController.ts @@ -0,0 +1,39 @@ +import { WaitOptions } from '../types' + +function createTimeoutController(timeout: WaitOptions['timeout']) { + let timeoutId: NodeJS.Timeout + const timeoutCallbacks: Array<() => void> = [] + + const timeoutController = { + onTimeout(callback: () => void) { + timeoutCallbacks.push(callback) + }, + wrap(promise: Promise) { + return new Promise((resolve, reject) => { + timeoutController.timedOut = false + timeoutController.onTimeout(resolve) + + if (timeout) { + timeoutId = setTimeout(() => { + timeoutController.timedOut = true + timeoutCallbacks.forEach((callback) => callback()) + resolve() + }, timeout) + } + + promise + .then(resolve) + .catch(reject) + .finally(() => timeoutController.cancel()) + }) + }, + cancel() { + clearTimeout(timeoutId) + }, + timedOut: false + } + + return timeoutController +} + +export { createTimeoutController } diff --git a/src/helpers/promises.ts b/src/helpers/promises.ts deleted file mode 100644 index 2fa89e5f..00000000 --- a/src/helpers/promises.ts +++ /dev/null @@ -1,10 +0,0 @@ -function resolveAfter(ms: number) { - return new Promise((resolve) => setTimeout(resolve, ms)) -} - -async function callAfter(callback: () => void, ms: number) { - await resolveAfter(ms) - callback() -} - -export { resolveAfter, callAfter } 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 2c574b83..00000000 --- a/src/native/__tests__/autoCleanup.disabled.test.ts +++ /dev/null @@ -1,30 +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 - let renderHook: ReactHooksRenderer['renderHook'] - - beforeAll(() => { - process.env.RHTL_SKIP_AUTO_CLEANUP = 'true' - 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 40b33f16..00000000 --- a/src/native/__tests__/autoCleanup.noAfterEach.test.ts +++ /dev/null @@ -1,31 +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 - let renderHook: ReactHooksRenderer['renderHook'] - - beforeAll(() => { - // @ts-expect-error Turning off AfterEach -- ignore Jest LifeCycle Type - afterEach = false - 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 f6adc8ad..00000000 --- a/src/native/__tests__/autoCleanup.noProcessEnv.test.ts +++ /dev/null @@ -1,40 +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('skip auto cleanup (no process.env) tests', () => { - const originalEnv = process.env - let cleanupCalled = false - let renderHook: ReactHooksRenderer['renderHook'] - - beforeAll(() => { - process.env = { - ...process.env, - get RHTL_SKIP_AUTO_CLEANUP(): string | undefined { - throw new Error('expected') - } - } - renderHook = (require('..') as ReactHooksRenderer).renderHook - }) - - afterAll(() => { - process.env = originalEnv - }) - - 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 1f84b87c..00000000 --- a/src/native/__tests__/autoCleanup.pure.test.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { useEffect } from 'react' - -import { ReactHooksRenderer } from '../../types/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', () => { - let cleanupCalled = false - let renderHook: ReactHooksRenderer['renderHook'] - - beforeAll(() => { - renderHook = (require('../pure') 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.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 8399a50b..00000000 --- a/src/native/__tests__/errorHook.test.ts +++ /dev/null @@ -1,145 +0,0 @@ -import { useState, useEffect } from 'react' -import { renderHook, act } 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('should raise effect error', () => { - const { result } = renderHook(() => useEffectError(true)) - - expect(() => { - expect(result.current).not.toBe(undefined) - }).toThrow(Error('expected')) - }) - - test('should capture effect error', () => { - const { result } = renderHook(() => useEffectError(true)) - expect(result.error).toEqual(Error('expected')) - }) - - test('should not capture effect error', () => { - const { result } = renderHook(() => useEffectError(false)) - - expect(result.current).not.toBe(undefined) - expect(result.error).toBe(undefined) - }) - - test('should reset effect error', () => { - const { result, rerender } = renderHook(({ throwError }) => useEffectError(throwError), { - initialProps: { throwError: true } - }) - - expect(result.error).not.toBe(undefined) - - rerender({ throwError: false }) - - expect(result.current).not.toBe(undefined) - expect(result.error).toBe(undefined) - }) - }) -}) diff --git a/src/native/__tests__/errorSuppression.disabled.test.ts b/src/native/__tests__/errorSuppression.disabled.test.ts deleted file mode 100644 index e1921f09..00000000 --- a/src/native/__tests__/errorSuppression.disabled.test.ts +++ /dev/null @@ -1,16 +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 - - beforeAll(() => { - 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 c736020e..00000000 --- a/src/native/__tests__/errorSuppression.noAfterEach.test.ts +++ /dev/null @@ -1,17 +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 - - beforeAll(() => { - // @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 c3f2496f..00000000 --- a/src/native/__tests__/errorSuppression.noBeforeEach.test.ts +++ /dev/null @@ -1,17 +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 - - beforeAll(() => { - // @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 24a50f21..00000000 --- a/src/native/__tests__/errorSuppression.noProcessEnv.test.ts +++ /dev/null @@ -1,26 +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 originalEnv = process.env - const originalConsoleError = console.error - - beforeAll(() => { - process.env = { - ...process.env, - get RHTL_DISABLE_ERROR_FILTERING(): string | undefined { - throw new Error('expected') - } - } - require('..') - }) - - afterAll(() => { - process.env = originalEnv - }) - - 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 e60ec710..00000000 --- a/src/native/__tests__/errorSuppression.pure.test.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { ReactHooksRenderer } from '../../types/react' - -// 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 - - let suppressErrorOutput!: ReactHooksRenderer['suppressErrorOutput'] - - beforeAll(() => { - suppressErrorOutput = (require('../pure') as ReactHooksRenderer).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) - }) -}) diff --git a/src/native/__tests__/errorSuppression.test.ts b/src/native/__tests__/errorSuppression.test.ts deleted file mode 100644 index 69250f47..00000000 --- a/src/native/__tests__/errorSuppression.test.ts +++ /dev/null @@ -1,75 +0,0 @@ -import { useEffect } from 'react' - -import { ReactHooksRenderer } from '../../types/react' - -describe('error output suppression tests', () => { - test('should not suppress relevant errors', () => { - const consoleError = console.error - console.error = jest.fn() - - const { suppressErrorOutput } = require('..') as ReactHooksRenderer - - 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 () => { - const { renderHook, act } = require('..') as ReactHooksRenderer - const consoleError = console.error - 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 a39e4e72..00000000 --- a/src/server/__tests__/autoCleanup.disabled.test.ts +++ /dev/null @@ -1,38 +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 - } - let renderHook: ReactHooksServerRenderer['renderHook'] - - beforeAll(() => { - process.env.RHTL_SKIP_AUTO_CLEANUP = 'true' - 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 6468296b..00000000 --- a/src/server/__tests__/autoCleanup.noAfterEach.test.ts +++ /dev/null @@ -1,39 +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 - } - let renderHook: ReactHooksServerRenderer['renderHook'] - - beforeAll(() => { - // @ts-expect-error Turning off AfterEach -- ignore Jest LifeCycle Type - afterEach = false - 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 f734d73e..00000000 --- a/src/server/__tests__/autoCleanup.noProcessEnv.test.ts +++ /dev/null @@ -1,48 +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 originalEnv = process.env - const cleanups: Record = { - ssr: false, - hydrated: false - } - let renderHook: ReactHooksServerRenderer['renderHook'] - - beforeAll(() => { - process.env = { - ...process.env, - get RHTL_SKIP_AUTO_CLEANUP(): string | undefined { - throw new Error('expected') - } - } - renderHook = (require('..') as ReactHooksServerRenderer).renderHook - }) - - afterAll(() => { - process.env = originalEnv - }) - - 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 0044e17f..00000000 --- a/src/server/__tests__/autoCleanup.pure.test.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { useEffect } from 'react' - -import { ReactHooksServerRenderer } from '../../types/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', () => { - const cleanups: Record = { - ssr: false, - hydrated: false - } - let renderHook: ReactHooksServerRenderer['renderHook'] - - beforeAll(() => { - renderHook = (require('../pure') 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.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 75925a98..00000000 --- a/src/server/__tests__/errorHook.test.ts +++ /dev/null @@ -1,166 +0,0 @@ -import { useState, useEffect } from 'react' - -import { renderHook, act } 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.disabled.test.ts b/src/server/__tests__/errorSuppression.disabled.test.ts deleted file mode 100644 index e1921f09..00000000 --- a/src/server/__tests__/errorSuppression.disabled.test.ts +++ /dev/null @@ -1,16 +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 - - beforeAll(() => { - process.env.RHTL_DISABLE_ERROR_FILTERING = 'true' - require('..') - }) - - test('should not patch console.error', () => { - expect(console.error).toBe(originalConsoleError) - }) -}) - -export {} diff --git a/src/server/__tests__/errorSuppression.noAfterEach.test.ts b/src/server/__tests__/errorSuppression.noAfterEach.test.ts deleted file mode 100644 index c736020e..00000000 --- a/src/server/__tests__/errorSuppression.noAfterEach.test.ts +++ /dev/null @@ -1,17 +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 - - beforeAll(() => { - // @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/server/__tests__/errorSuppression.noBeforeEach.test.ts b/src/server/__tests__/errorSuppression.noBeforeEach.test.ts deleted file mode 100644 index c3f2496f..00000000 --- a/src/server/__tests__/errorSuppression.noBeforeEach.test.ts +++ /dev/null @@ -1,17 +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 - - beforeAll(() => { - // @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/server/__tests__/errorSuppression.noProcessEnv.test.ts b/src/server/__tests__/errorSuppression.noProcessEnv.test.ts deleted file mode 100644 index 24a50f21..00000000 --- a/src/server/__tests__/errorSuppression.noProcessEnv.test.ts +++ /dev/null @@ -1,26 +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 originalEnv = process.env - const originalConsoleError = console.error - - beforeAll(() => { - process.env = { - ...process.env, - get RHTL_DISABLE_ERROR_FILTERING(): string | undefined { - throw new Error('expected') - } - } - require('..') - }) - - afterAll(() => { - process.env = originalEnv - }) - - 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 e60ec710..00000000 --- a/src/server/__tests__/errorSuppression.pure.test.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { ReactHooksRenderer } from '../../types/react' - -// 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 - - let suppressErrorOutput!: ReactHooksRenderer['suppressErrorOutput'] - - beforeAll(() => { - suppressErrorOutput = (require('../pure') as ReactHooksRenderer).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) - }) -}) diff --git a/src/server/__tests__/errorSuppression.test.ts b/src/server/__tests__/errorSuppression.test.ts deleted file mode 100644 index e4492756..00000000 --- a/src/server/__tests__/errorSuppression.test.ts +++ /dev/null @@ -1,77 +0,0 @@ -import { useEffect } from 'react' - -import { ReactHooksServerRenderer } from '../../types/react' - -describe('error output suppression tests', () => { - test('should not suppress relevant errors', () => { - const consoleError = console.error - console.error = jest.fn() - - const { suppressErrorOutput } = require('..') as ReactHooksServerRenderer - - 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 () => { - const { renderHook, act } = require('..') as ReactHooksServerRenderer - const consoleError = console.error - 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/server/pure.ts b/src/server/pure.ts index 1978f2d0..aa62a283 100644 --- a/src/server/pure.ts +++ b/src/server/pure.ts @@ -13,19 +13,17 @@ function createServerRenderer( ) { let renderProps: TProps | undefined let container: HTMLDivElement | undefined - let serverOutput: string = '' + let serverOutput = '' const testHarness = createTestHarness(rendererProps, wrapper, false) return { render(props?: TProps) { renderProps = props - act(() => { - try { - serverOutput = ReactDOMServer.renderToString(testHarness(props)) - } catch (e: unknown) { - rendererProps.setError(e as Error) - } - }) + try { + serverOutput = ReactDOMServer.renderToString(testHarness(props)) + } catch (e: unknown) { + rendererProps.setError(e as Error) + } }, hydrate() { if (container) { 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