Skip to content

Commit a25993f

Browse files
joshuaellismpeyper
authored andcommitted
feat: react-dom and SSR compatible rendering
- Abstracted rendering out of library core to allow different types of renderers - Auto-detection of `react-test-renderer` or `react-dom` renderers. Submodules for: - `dom` (`react-dom`) - `native` (`react-test-renderer`) - `server` (`react-dom/server`) Co-authored-by: Michael Peyper <[email protected]> BREAKING CHANGE: Importing from `renderHook` and `act` from `@testing-library/react-hooks` will now auto-detect which renderer to used based on the project's dependencies - `peerDependencies` are now optional to support different dependencies being required - This means there will be no warning if the dependency is not installed at all, but it will still warn if an incompatible version is installed
1 parent b35b152 commit a25993f

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

74 files changed

+2568
-201
lines changed

.all-contributorsrc

+8-2
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@
1919
"code",
2020
"doc",
2121
"infra",
22+
"maintenance",
23+
"question",
2224
"test"
2325
]
2426
},
@@ -199,7 +201,11 @@
199201
"profile": "https://github.com/joshuaellis",
200202
"contributions": [
201203
"doc",
202-
"question"
204+
"question",
205+
"code",
206+
"ideas",
207+
"maintenance",
208+
"test"
203209
]
204210
},
205211
{
@@ -450,4 +456,4 @@
450456
]
451457
}
452458
]
453-
}
459+
}

.eslintignore

+4
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
node_modules
22
coverage
33
lib
4+
dom
5+
native
6+
server
7+
pure
48
.docz
59
site

.eslintrc

+1-2
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,11 @@
66
"no-await-in-loop": "off",
77
"no-console": "off",
88
"import/no-unresolved": "off",
9-
"react-hooks/rules-of-hooks": "off",
109
"@typescript-eslint/no-floating-promises": "off",
1110
"@typescript-eslint/no-unnecessary-condition": "off",
1211
"@typescript-eslint/no-invalid-void-type": "off"
1312
},
1413
"parserOptions": {
15-
"project": ["./tsconfig.json", "./test/tsconfig.json"]
14+
"project": ["./tsconfig.json", "./test/tsconfig.json", "./scripts/tsconfig.json"]
1615
}
1716
}

.gitignore

+6-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
node_modules
22
coverage
33
lib
4+
dom
5+
native
6+
server
7+
pure
48
.docz
5-
site
9+
site
10+
.vscode

README.md

+2-2
Original file line numberDiff line numberDiff line change
@@ -166,7 +166,7 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
166166
<!-- markdownlint-disable -->
167167
<table>
168168
<tr>
169-
<td align="center"><a href="https://github.com/mpeyper"><img src="https://avatars0.githubusercontent.com/u/23029903?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Michael Peyper</b></sub></a><br /><a href="https://github.com/testing-library/react-hooks-testing-library/commits?author=mpeyper" title="Code">💻</a> <a href="https://github.com/testing-library/react-hooks-testing-library/commits?author=mpeyper" title="Documentation">📖</a> <a href="#infra-mpeyper" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a> <a href="https://github.com/testing-library/react-hooks-testing-library/commits?author=mpeyper" title="Tests">⚠️</a></td>
169+
<td align="center"><a href="https://github.com/mpeyper"><img src="https://avatars0.githubusercontent.com/u/23029903?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Michael Peyper</b></sub></a><br /><a href="https://github.com/testing-library/react-hooks-testing-library/commits?author=mpeyper" title="Code">💻</a> <a href="https://github.com/testing-library/react-hooks-testing-library/commits?author=mpeyper" title="Documentation">📖</a> <a href="#ideas-mpeyper" title="Ideas, Planning, & Feedback">🤔</a> <a href="#infra-mpeyper" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a> <a href="#maintenance-mpeyper" title="Maintenance">🚧</a> <a href="#question-mpeyper" title="Answering Questions">💬</a> <a href="https://github.com/testing-library/react-hooks-testing-library/commits?author=mpeyper" title="Tests">⚠️</a></td>
170170
<td align="center"><a href="https://github.com/otofu-square"><img src="https://avatars0.githubusercontent.com/u/10118235?v=4?s=100" width="100px;" alt=""/><br /><sub><b>otofu-square</b></sub></a><br /><a href="https://github.com/testing-library/react-hooks-testing-library/commits?author=otofu-square" title="Code">💻</a></td>
171171
<td align="center"><a href="https://github.com/ab18556"><img src="https://avatars2.githubusercontent.com/u/988696?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Patrick P. Henley</b></sub></a><br /><a href="#ideas-ab18556" title="Ideas, Planning, & Feedback">🤔</a> <a href="https://github.com/testing-library/react-hooks-testing-library/pulls?q=is%3Apr+reviewed-by%3Aab18556" title="Reviewed Pull Requests">👀</a></td>
172172
<td align="center"><a href="https://twitter.com/matiosfm"><img src="https://avatars3.githubusercontent.com/u/7120471?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Matheus Marques</b></sub></a><br /><a href="https://github.com/testing-library/react-hooks-testing-library/commits?author=marquesm91" title="Code">💻</a></td>
@@ -189,7 +189,7 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
189189
<td align="center"><a href="https://github.com/hemlok"><img src="https://avatars2.githubusercontent.com/u/9043345?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Adam Seckel</b></sub></a><br /><a href="https://github.com/testing-library/react-hooks-testing-library/commits?author=hemlok" title="Code">💻</a></td>
190190
<td align="center"><a href="https://keiya01.github.io/portfolio"><img src="https://avatars1.githubusercontent.com/u/34934510?v=4?s=100" width="100px;" alt=""/><br /><sub><b>keiya sasaki</b></sub></a><br /><a href="https://github.com/testing-library/react-hooks-testing-library/commits?author=keiya01" title="Tests">⚠️</a></td>
191191
<td align="center"><a href="https://huchen.dev/"><img src="https://avatars3.githubusercontent.com/u/2078389?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Hu Chen</b></sub></a><br /><a href="https://github.com/testing-library/react-hooks-testing-library/commits?author=huchenme" title="Code">💻</a> <a href="https://github.com/testing-library/react-hooks-testing-library/commits?author=huchenme" title="Documentation">📖</a> <a href="#example-huchenme" title="Examples">💡</a></td>
192-
<td align="center"><a href="https://github.com/joshuaellis"><img src="https://avatars0.githubusercontent.com/u/37798644?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Josh</b></sub></a><br /><a href="https://github.com/testing-library/react-hooks-testing-library/commits?author=joshuaellis" title="Documentation">📖</a> <a href="#question-joshuaellis" title="Answering Questions">💬</a></td>
192+
<td align="center"><a href="https://github.com/joshuaellis"><img src="https://avatars0.githubusercontent.com/u/37798644?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Josh</b></sub></a><br /><a href="https://github.com/testing-library/react-hooks-testing-library/commits?author=joshuaellis" title="Documentation">📖</a> <a href="#question-joshuaellis" title="Answering Questions">💬</a> <a href="https://github.com/testing-library/react-hooks-testing-library/commits?author=joshuaellis" title="Code">💻</a> <a href="#ideas-joshuaellis" title="Ideas, Planning, & Feedback">🤔</a> <a href="#maintenance-joshuaellis" title="Maintenance">🚧</a> <a href="https://github.com/testing-library/react-hooks-testing-library/commits?author=joshuaellis" title="Tests">⚠️</a></td>
193193
<td align="center"><a href="https://github.com/Goldziher"><img src="https://avatars1.githubusercontent.com/u/30733348?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Na'aman Hirschfeld</b></sub></a><br /><a href="https://github.com/testing-library/react-hooks-testing-library/commits?author=Goldziher" title="Code">💻</a></td>
194194
</tr>
195195
<tr>

jest.config.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,5 @@ const { jest: jestConfig } = require('kcd-scripts/config')
33

44
module.exports = Object.assign(jestConfig, {
55
roots: ['<rootDir>/src', '<rootDir>/test'],
6-
testMatch: ['<rootDir>/test/*.(ts|tsx|js)']
6+
testMatch: ['<rootDir>/test/**/*.(ts|tsx|js)']
77
})

package.json

+21-2
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,10 @@
1414
"files": [
1515
"lib",
1616
"src",
17-
"pure.js",
17+
"dom",
18+
"native",
19+
"server",
20+
"pure",
1821
"dont-cleanup-after-each.js"
1922
],
2023
"author": "Michael Peyper <[email protected]>",
@@ -27,7 +30,8 @@
2730
"setup": "npm install && npm run validate -s",
2831
"validate": "kcd-scripts validate",
2932
"prepare": "npm run build",
30-
"build": "kcd-scripts build --out-dir lib",
33+
"build": "kcd-scripts build --out-dir lib && npm run generate:submodules",
34+
"generate:submodules": "ts-node scripts/generate-submodules.ts",
3135
"test": "kcd-scripts test",
3236
"typecheck": "kcd-scripts typecheck",
3337
"lint": "kcd-scripts lint",
@@ -40,6 +44,7 @@
4044
"dependencies": {
4145
"@babel/runtime": "^7.12.5",
4246
"@types/react": ">=16.9.0",
47+
"@types/react-dom": ">=16.9.0",
4348
"@types/react-test-renderer": ">=16.9.0"
4449
},
4550
"devDependencies": {
@@ -54,11 +59,25 @@
5459
"kcd-scripts": "7.5.3",
5560
"prettier": "^2.2.1",
5661
"react": "17.0.1",
62+
"react-dom": "^17.0.1",
5763
"react-test-renderer": "17.0.1",
64+
"ts-node": "^9.1.1",
5865
"typescript": "4.1.3"
5966
},
6067
"peerDependencies": {
6168
"react": ">=16.9.0",
69+
"react-dom": ">=16.9.0",
6270
"react-test-renderer": ">=16.9.0"
71+
},
72+
"peerDependenciesMeta": {
73+
"react": {
74+
"optional": true
75+
},
76+
"react-dom": {
77+
"optional": true
78+
},
79+
"react-test-renderer": {
80+
"optional": true
81+
}
6382
}
6483
}

pure.js

-2
This file was deleted.

scripts/generate-submodules.ts

+63
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import fs from 'fs'
2+
import path from 'path'
3+
4+
type Template = (submodule: string) => string
5+
6+
const templates = {
7+
index: {
8+
'.js': (submodule: string) => `module.exports = require('../lib/${submodule}')`,
9+
'.d.ts': (submodule: string) => `export * from '../lib/${submodule}'`
10+
},
11+
pure: {
12+
'.js': (submodule: string) => `module.exports = require('../lib/${submodule}/pure')`,
13+
'.d.ts': (submodule: string) => `export * from '../lib/${submodule}/pure'`
14+
}
15+
}
16+
17+
const submodules = ['dom', 'native', 'server', 'pure']
18+
19+
function cleanDirectory(directory: string) {
20+
const files = fs.readdirSync(directory)
21+
files.forEach((file) => fs.unlinkSync(path.join(directory, file)))
22+
}
23+
24+
function makeDirectory(submodule: string) {
25+
const submoduleDir = path.join(process.cwd(), submodule)
26+
27+
if (fs.existsSync(submoduleDir)) {
28+
cleanDirectory(submoduleDir)
29+
} else {
30+
fs.mkdirSync(submoduleDir)
31+
}
32+
33+
return submoduleDir
34+
}
35+
36+
function requiredFile(submodule: string) {
37+
return ([name]: [string, unknown]) => {
38+
return name !== submodule
39+
}
40+
}
41+
42+
function makeFile(directory: string, submodule: string) {
43+
return ([name, extensions]: [string, Record<string, Template>]) => {
44+
Object.entries(extensions).forEach(([extension, template]) => {
45+
const fileName = `${name}${extension}`
46+
console.log(` - ${fileName}`)
47+
const filePath = path.join(directory, fileName)
48+
fs.writeFileSync(filePath, template(submodule))
49+
})
50+
}
51+
}
52+
53+
function makeFiles(directory: string, submodule: string) {
54+
Object.entries(templates).filter(requiredFile(submodule)).forEach(makeFile(directory, submodule))
55+
}
56+
57+
function createSubmodule(submodule: string) {
58+
console.log(`Generating submodule: ${submodule}`)
59+
const submoduleDir = makeDirectory(submodule)
60+
makeFiles(submoduleDir, submodule)
61+
}
62+
63+
submodules.forEach(createSubmodule)

scripts/tsconfig.json

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"extends": "../tsconfig",
3+
"compilerOptions": {
4+
"declaration": false
5+
},
6+
"exclude": [],
7+
"include": ["./**/*.ts"]
8+
}

src/asyncUtils.ts renamed to src/core/asyncUtils.ts

+10-24
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,15 @@
1-
import { act } from 'react-test-renderer'
1+
import { Act, WaitOptions, AsyncUtils } from '../types'
22

3-
export interface WaitOptions {
4-
interval?: number
5-
timeout?: number
6-
suppressErrors?: boolean
7-
}
8-
9-
class TimeoutError extends Error {
10-
constructor(util: Function, timeout: number) {
11-
super(`Timed out in ${util.name} after ${timeout}ms.`)
12-
}
13-
}
14-
15-
function resolveAfter(ms: number) {
16-
return new Promise((resolve) => {
17-
setTimeout(resolve, ms)
18-
})
19-
}
3+
import { resolveAfter } from '../helpers/promises'
4+
import { TimeoutError } from '../helpers/error'
205

21-
function asyncUtils(addResolver: (callback: () => void) => void) {
6+
function asyncUtils(act: Act, addResolver: (callback: () => void) => void): AsyncUtils {
227
let nextUpdatePromise: Promise<void> | null = null
238

249
const waitForNextUpdate = async ({ timeout }: Pick<WaitOptions, 'timeout'> = {}) => {
25-
if (!nextUpdatePromise) {
10+
if (nextUpdatePromise) {
11+
await nextUpdatePromise
12+
} else {
2613
nextUpdatePromise = new Promise((resolve, reject) => {
2714
let timeoutId: ReturnType<typeof setTimeout>
2815
if (timeout && timeout > 0) {
@@ -39,7 +26,6 @@ function asyncUtils(addResolver: (callback: () => void) => void) {
3926
})
4027
await act(() => nextUpdatePromise as Promise<void>)
4128
}
42-
await nextUpdatePromise
4329
}
4430

4531
const waitFor = async (
@@ -52,7 +38,7 @@ function asyncUtils(addResolver: (callback: () => void) => void) {
5238
return callbackResult ?? callbackResult === undefined
5339
} catch (error: unknown) {
5440
if (!suppressErrors) {
55-
throw error as Error
41+
throw error
5642
}
5743
return undefined
5844
}
@@ -76,7 +62,7 @@ function asyncUtils(addResolver: (callback: () => void) => void) {
7662
if (error instanceof TimeoutError && initialTimeout) {
7763
throw new TimeoutError(waitFor, initialTimeout)
7864
}
79-
throw error as Error
65+
throw error
8066
}
8167
if (timeout) timeout -= Date.now() - startTime
8268
}
@@ -98,7 +84,7 @@ function asyncUtils(addResolver: (callback: () => void) => void) {
9884
if (error instanceof TimeoutError && options.timeout) {
9985
throw new TimeoutError(waitForValueToChange, options.timeout)
10086
}
101-
throw error as Error
87+
throw error
10288
}
10389
}
10490

src/cleanup.ts renamed to src/core/cleanup.ts

+10-1
Original file line numberDiff line numberDiff line change
@@ -16,4 +16,13 @@ function removeCleanup(callback: () => Promise<void> | void) {
1616
cleanupCallbacks = cleanupCallbacks.filter((cb) => cb !== callback)
1717
}
1818

19-
export { cleanup, addCleanup, removeCleanup }
19+
function autoRegisterCleanup() {
20+
// Automatically registers cleanup in supported testing frameworks
21+
if (typeof afterEach === 'function' && !process.env.RHTL_SKIP_AUTO_CLEANUP) {
22+
afterEach(async () => {
23+
await cleanup()
24+
})
25+
}
26+
}
27+
28+
export { cleanup, addCleanup, removeCleanup, autoRegisterCleanup }

src/core/index.ts

+78
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import { CreateRenderer, Renderer, RenderResult, RenderHook } from '../types'
2+
import { ResultContainer, RenderHookOptions } from '../types/internal'
3+
4+
import asyncUtils from './asyncUtils'
5+
import { cleanup, addCleanup, removeCleanup } from './cleanup'
6+
7+
function resultContainer<TValue>(): ResultContainer<TValue> {
8+
const results: Array<{ value?: TValue; error?: Error }> = []
9+
const resolvers: Array<() => void> = []
10+
11+
const result: RenderResult<TValue> = {
12+
get all() {
13+
return results.map(({ value, error }) => error ?? value)
14+
},
15+
get current() {
16+
const { value, error } = results[results.length - 1]
17+
if (error) {
18+
throw error
19+
}
20+
return value as TValue
21+
},
22+
get error() {
23+
const { error } = results[results.length - 1]
24+
return error
25+
}
26+
}
27+
28+
const updateResult = (value?: TValue, error?: Error) => {
29+
results.push({ value, error })
30+
resolvers.splice(0, resolvers.length).forEach((resolve) => resolve())
31+
}
32+
33+
return {
34+
result,
35+
addResolver: (resolver: () => void) => {
36+
resolvers.push(resolver)
37+
},
38+
setValue: (value: TValue) => updateResult(value),
39+
setError: (error: Error) => updateResult(undefined, error)
40+
}
41+
}
42+
43+
const createRenderHook = <TProps, TResult, TOptions extends {}, TRenderer extends Renderer<TProps>>(
44+
createRenderer: CreateRenderer<TProps, TResult, TOptions, TRenderer>
45+
) => (
46+
callback: (props: TProps) => TResult,
47+
options: RenderHookOptions<TProps, TOptions> = {} as RenderHookOptions<TProps, TOptions>
48+
): RenderHook<TProps, TResult, TRenderer> => {
49+
const { result, setValue, setError, addResolver } = resultContainer<TResult>()
50+
const renderProps = { callback, setValue, setError }
51+
let hookProps = options.initialProps
52+
53+
const { render, rerender, unmount, act, ...renderUtils } = createRenderer(renderProps, options)
54+
55+
render(hookProps)
56+
57+
function rerenderHook(newProps = hookProps) {
58+
hookProps = newProps
59+
rerender(hookProps)
60+
}
61+
62+
function unmountHook() {
63+
removeCleanup(unmountHook)
64+
unmount()
65+
}
66+
67+
addCleanup(unmountHook)
68+
69+
return {
70+
result,
71+
rerender: rerenderHook,
72+
unmount: unmountHook,
73+
...asyncUtils(act, addResolver),
74+
...renderUtils
75+
}
76+
}
77+
78+
export { createRenderHook, cleanup, addCleanup, removeCleanup }

src/dom/index.ts

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import { autoRegisterCleanup } from '../core/cleanup'
2+
3+
autoRegisterCleanup()
4+
5+
export * from './pure'

0 commit comments

Comments
 (0)