Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feature/RHTL-68 – server-side-rendering #510

Merged
merged 60 commits into from Jan 7, 2021
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
Show all changes
60 commits
Select commit Hold shift + click to select a range
3841db2
chore: move current config (react-test-renderer) to src/native
joshuaellis Dec 6, 2020
40991cb
change: add eslint to avoid conflict & fix lint errors
joshuaellis Dec 10, 2020
e3b9104
feat: migrate previous server work
joshuaellis Dec 11, 2020
8b11562
merge master to branch
joshuaellis Dec 29, 2020
b34000e
Merge branch 'beta' into feature/RHTL-68
joshuaellis Dec 30, 2020
2cd41cb
feat: add global type file
joshuaellis Dec 31, 2020
d34245a
change: seperate helper funcs
joshuaellis Dec 31, 2020
85b9fa2
feat: add types to createRenderer & createRenderHook
joshuaellis Dec 31, 2020
add0639
tests: fix tests with type assertions etc.
joshuaellis Dec 31, 2020
785aa5c
fix: type assertion with RenderResult
joshuaellis Dec 31, 2020
b553441
test: fix errorHook type error.
joshuaellis Dec 31, 2020
7edd623
change: try to solve WrapperComponent PropTypes for useContext test
joshuaellis Dec 31, 2020
100c77c
fix: Wrapper type was breaking with spread operator
joshuaellis Dec 31, 2020
b5f6ced
feat: add server engine in typescript
joshuaellis Jan 1, 2021
56be770
test: get coverage to 100
joshuaellis Jan 2, 2021
8d35cac
change: remove rtr act & use own written
joshuaellis Jan 2, 2021
a4329dd
change: use relative paths & add react-dom to renderers
joshuaellis Jan 2, 2021
0e296b7
feat: add custom renderer option
joshuaellis Jan 2, 2021
fdfe432
feat: add dom renderer
joshuaellis Jan 3, 2021
af079a4
change: extract toRender to be shared
joshuaellis Jan 3, 2021
ec732c5
feat: add dom types
joshuaellis Jan 3, 2021
53a339e
test: formatting
joshuaellis Jan 3, 2021
7be2c80
change: create overload for createRenderHook
joshuaellis Jan 3, 2021
9b57e0d
change: update contributors
joshuaellis Jan 3, 2021
cfd3df0
change: dom should be default renderer for react-dom
joshuaellis Jan 3, 2021
125c327
change: add dom to files
joshuaellis Jan 3, 2021
ae3f2f7
change: @types/react-dom should be >=16.9.0
joshuaellis Jan 3, 2021
0e73e21
change: use generic RendererOptions instead of specific to renderer
joshuaellis Jan 3, 2021
48af5ce
chore(deps-dev): bump eslint from 7.16.0 to 7.17.0
dependabot[bot] Jan 4, 2021
3773938
chore(refactor): moved require into getRenderer
mpeyper Jan 4, 2021
19d74ce
fix: improve error message when renderer can't be auto-detected
mpeyper Jan 4, 2021
bad29f9
feat: remove renderer specific types from core renderHook logic
mpeyper Jan 4, 2021
5730f15
change: refactor auto cleanup into a separate function
mpeyper Jan 4, 2021
50cdd55
feat: added render utility support for custom renderers
mpeyper Jan 4, 2021
ba99b1c
fix: type catch block errors to fixe lint error
mpeyper Jan 4, 2021
fcc8ba3
chore: ignore lint warning for purposely skipped tests
mpeyper Jan 4, 2021
9ada181
chore: removed unnecessary lint disable comment
mpeyper Jan 4, 2021
81ce711
Merge pull request #522 from testing-library/dependabot/npm_and_yarn/…
joshuaellis Jan 4, 2021
785afd2
feat: renderHook options are now generic based on renderer
mpeyper Jan 6, 2021
a1fa12f
chore: rename file without tsx exension as it no longer contains any jsx
mpeyper Jan 6, 2021
511cb33
fix: refactor type names and removed some unnecessary types
mpeyper Jan 6, 2021
b3ccb32
fix: remove server act login and just use basic act
mpeyper Jan 6, 2021
3dfbbb7
fix: ensure react types are only imported if a react renderer is used
mpeyper Jan 6, 2021
e1d2153
fix: separate types into folders are export types for imports
mpeyper Jan 6, 2021
137f50c
fix: remove unused types, cleanup types and format all files
mpeyper Jan 6, 2021
89e678c
fix: update error message when a renderer cannot be auto-detected
mpeyper Jan 6, 2021
36b81c0
chore: restructure code for better readability after formatting
mpeyper Jan 6, 2021
1c6b9e5
fix: remove unnecessary structures to remove need to cast in renderHook
mpeyper Jan 6, 2021
b936962
fix: remove duplicate type from internal types
mpeyper Jan 6, 2021
080d98a
fix: simplify type of RenderHook
mpeyper Jan 6, 2021
816d312
Merge pull request #1 from testing-library/feature/RHTL-68-suggestions
joshuaellis Jan 6, 2021
209619d
feat: generate submodules as part of build step
mpeyper Jan 6, 2021
e9fa116
fix: don't wait for already resolved promise in waitForNextUpdate
mpeyper Jan 7, 2021
fd00043
fix: ensure submodule directory is cleaned before generating files
mpeyper Jan 7, 2021
16ba7e0
chore: remove unused custom module
mpeyper Jan 7, 2021
6146121
fix: minor renaming of private members
mpeyper Jan 7, 2021
1970833
Merge branch 'beta'
mpeyper Jan 7, 2021
c9460c5
Merge branch 'master' into feature/RHTL-68
mpeyper Jan 7, 2021
e315dce
chore: updated all-contributors to be more accurate
mpeyper Jan 7, 2021
1e2cb37
Merge branch 'master' into feature/RHTL-68
mpeyper Jan 7, 2021
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
3 changes: 2 additions & 1 deletion .gitignore
Expand Up @@ -2,4 +2,5 @@ node_modules
coverage
lib
.docz
site
site
.vscode
2 changes: 1 addition & 1 deletion jest.config.js
Expand Up @@ -3,5 +3,5 @@ const { jest: jestConfig } = require('kcd-scripts/config')

module.exports = Object.assign(jestConfig, {
roots: ['<rootDir>/src', '<rootDir>/test'],
testMatch: ['<rootDir>/test/*.(ts|tsx|js)']
testMatch: ['<rootDir>/test/**/*.(ts|tsx|js)']
})
1 change: 1 addition & 0 deletions native/index.js
@@ -0,0 +1 @@
module.exports = require('../lib/native')
2 changes: 2 additions & 0 deletions native/pure.js
@@ -0,0 +1,2 @@
// makes it so people can import from '@testing-library/react-hooks/native/pure'
module.exports = require('../lib/native/pure')
16 changes: 16 additions & 0 deletions package.json
Expand Up @@ -14,6 +14,8 @@
"files": [
"lib",
"src",
"native",
"server",
joshuaellis marked this conversation as resolved.
Show resolved Hide resolved
"pure.js",
"dont-cleanup-after-each.js"
],
Expand All @@ -40,6 +42,7 @@
"dependencies": {
"@babel/runtime": "^7.12.5",
"@types/react": ">=16.9.0",
"@types/react-dom": "^17.0.0",
joshuaellis marked this conversation as resolved.
Show resolved Hide resolved
"@types/react-test-renderer": ">=16.9.0"
},
"devDependencies": {
Expand All @@ -54,11 +57,24 @@
"kcd-scripts": "7.5.3",
"prettier": "^2.2.1",
"react": "17.0.1",
"react-dom": "^17.0.1",
"react-test-renderer": "17.0.1",
"typescript": "4.1.3"
},
"peerDependencies": {
"react": ">=16.9.0",
"react-dom": ">=16.9.0",
"react-test-renderer": ">=16.9.0"
},
"peerDependenciesMeta": {
"react": {
"optional": true
},
"react-dom": {
"optional": true
},
"react-test-renderer": {
"optional": true
}
}
}
2 changes: 2 additions & 0 deletions server/index.js
@@ -0,0 +1,2 @@
// makes it so people can import from '@testing-library/react-hooks/server'
module.exports = require('../lib/server')
2 changes: 2 additions & 0 deletions server/pure.js
@@ -0,0 +1,2 @@
// makes it so people can import from '@testing-library/react-hooks/server/pure'
module.exports = require('../lib/server/pure')
23 changes: 4 additions & 19 deletions src/asyncUtils.ts → src/core/asyncUtils.ts
@@ -1,24 +1,9 @@
import { act } from 'react-test-renderer'
import { ActTypes, WaitOptions, AsyncUtilsReturn } from 'types'

export interface WaitOptions {
interval?: number
timeout?: number
suppressErrors?: boolean
}

class TimeoutError extends Error {
constructor(util: Function, timeout: number) {
super(`Timed out in ${util.name} after ${timeout}ms.`)
}
}

function resolveAfter(ms: number) {
return new Promise((resolve) => {
setTimeout(resolve, ms)
})
}
import { resolveAfter } from 'helpers/promises'
import { TimeoutError } from 'helpers/error'

function asyncUtils(addResolver: (callback: () => void) => void) {
function asyncUtils(act: ActTypes, addResolver: (callback: () => void) => void): AsyncUtilsReturn {
let nextUpdatePromise: Promise<void> | null = null

const waitForNextUpdate = async ({ timeout }: Pick<WaitOptions, 'timeout'> = {}) => {
Expand Down
9 changes: 9 additions & 0 deletions src/cleanup.ts → src/core/cleanup.ts
Expand Up @@ -16,4 +16,13 @@ function removeCleanup(callback: () => Promise<void> | void) {
cleanupCallbacks = cleanupCallbacks.filter((cb) => cb !== callback)
}

cleanup.autoRegister = () => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm second guessing myself on whether it's a good idea to put this on cleanup or to just have it as another export. I think currently it will be exposed out of the public API, which might be ok, but then we should to document and maintain it.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't change this yet... I'll sleep on it.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Still undecided. I'm leaning towards just another export.

// Automatically registers cleanup in supported testing frameworks
if (typeof afterEach === 'function' && !process.env.RHTL_SKIP_AUTO_CLEANUP) {
afterEach(async () => {
await cleanup()
})
}
}

export { cleanup, addCleanup, removeCleanup }
100 changes: 100 additions & 0 deletions src/core/index.tsx
@@ -0,0 +1,100 @@
import React from 'react'

import {
TestHookProps,
NativeRendererOptions,
NativeRendererReturn,
ResultContainerReturn,
RenderHookOptions,
RenderResult,
ServerRendererReturn,
ServerRendererOptions
} from 'types'

import asyncUtils from './asyncUtils'
import { cleanup, addCleanup, removeCleanup } from './cleanup'

function resultContainer<TValue>(): ResultContainerReturn<TValue> {
const results: Array<{ value?: TValue; error?: Error }> = []
const resolvers: Array<() => void> = []

const result: RenderResult<TValue> = {
get all() {
return results.map(({ value, error }) => error ?? value)
},
get current() {
const { value, error } = results[results.length - 1]
if (error) {
throw error
}
return value as TValue
},
get error() {
const { error } = results[results.length - 1]
return error
}
}

const updateResult = (value?: TValue, error?: Error) => {
results.push({ value, error })
resolvers.splice(0, resolvers.length).forEach((resolve) => resolve())
}

return {
result,
addResolver: (resolver: () => void) => {
resolvers.push(resolver)
},
setValue: (value: TValue) => updateResult(value),
setError: (error: Error) => updateResult(undefined, error)
}
}

// typed this way in relation to this https://github.com/DefinitelyTyped/DefinitelyTyped/issues/44572#issuecomment-625878049
function defaultWrapper({ children }: { children?: React.ReactNode }) {
return (children as unknown) as JSX.Element
}

const createRenderHook = (
createRenderer: <TProps, TResult>(
testProps: Omit<TestHookProps<TProps, TResult>, 'hookProps'>,
opts: NativeRendererOptions<TProps> | ServerRendererOptions<TProps>
) => NativeRendererReturn<TProps> | ServerRendererReturn<TProps>
) => <TProps, TResult>(
callback: (props: TProps) => TResult,
{ initialProps, wrapper = defaultWrapper }: RenderHookOptions<TProps> = {}
) => {
const { result, setValue, setError, addResolver } = resultContainer<TResult>()
const hookProps = { current: initialProps }
const props = { callback, setValue, setError }
const options = { wrapper }

const { render, rerender, unmount, act, ...renderUtils } = createRenderer<TProps, TResult>(
props,
options
)

render(hookProps.current)

function rerenderHook(newProps = hookProps.current) {
hookProps.current = newProps
rerender(hookProps.current)
}

function unmountHook() {
removeCleanup(unmountHook)
unmount()
}

addCleanup(unmountHook)

return {
result,
rerender: rerenderHook,
unmount: unmountHook,
...asyncUtils(act, addResolver),
...renderUtils
}
}

export { createRenderHook, cleanup, addCleanup, removeCleanup }
22 changes: 22 additions & 0 deletions src/core/testHook.ts
@@ -0,0 +1,22 @@
import { isPromise } from 'helpers/promises'

import { TestHookProps } from 'types'

export default function TestHook<TProps, TResult>({
hookProps,
callback,
setError,
setValue
}: TestHookProps<TProps, TResult>) {
try {
// coerce undefined into TProps, so it maintains the previous behaviour
setValue(callback(hookProps as TProps))
} catch (err: unknown) {
if (isPromise(err)) {
throw err
} else {
setError(err as Error)
}
}
return null
}
7 changes: 7 additions & 0 deletions src/helpers/error.ts
@@ -0,0 +1,7 @@
class TimeoutError extends Error {
constructor(util: Function, timeout: number) {
super(`Timed out in ${util.name} after ${timeout}ms.`)
}
}

export { TimeoutError }
9 changes: 9 additions & 0 deletions src/helpers/promises.ts
@@ -0,0 +1,9 @@
const resolveAfter = (ms: number) =>
new Promise((resolve) => {
setTimeout(resolve, ms)
})

const isPromise = <T>(value: unknown): boolean =>
typeof (value as PromiseLike<T>).then === 'function'

export { isPromise, resolveAfter }
11 changes: 3 additions & 8 deletions src/index.ts
@@ -1,10 +1,5 @@
import { cleanup } from './pure'
import { renderHook, act, cleanup } from './pure'

// Automatically registers cleanup in supported testing frameworks
if (typeof afterEach === 'function' && !process.env.RHTL_SKIP_AUTO_CLEANUP) {
afterEach(async () => {
await cleanup()
})
}
cleanup.autoRegister()

export * from './pure'
export { renderHook, act, cleanup }
5 changes: 5 additions & 0 deletions src/native/index.ts
@@ -0,0 +1,5 @@
import { renderHook, act, cleanup } from './pure'

cleanup.autoRegister()

export { renderHook, act, cleanup }
43 changes: 43 additions & 0 deletions src/native/pure.tsx
@@ -0,0 +1,43 @@
import React, { Suspense } from 'react'
import { act, create, ReactTestRenderer } from 'react-test-renderer'

import { TestHookProps, NativeRendererOptions, NativeRendererReturn } from 'types'

import { createRenderHook, cleanup, addCleanup, removeCleanup } from 'core/index'
import TestHook from 'core/testHook'

function createNativeRenderer<TProps, TResult>(
testHookProps: Omit<TestHookProps<TProps, TResult>, 'hookProps'>,
{ wrapper: Wrapper }: NativeRendererOptions<TProps>
): NativeRendererReturn<TProps> {
let container: ReactTestRenderer

const toRender = (props?: TProps): JSX.Element => (
<Wrapper {...(props as TProps)}>
<TestHook hookProps={props} {...testHookProps} />
</Wrapper>
)

return {
render(props) {
act(() => {
container = create(<Suspense fallback={null}>{toRender(props)}</Suspense>)
})
},
rerender(props) {
act(() => {
container.update(<Suspense fallback={null}>{toRender(props)}</Suspense>)
})
},
unmount() {
act(() => {
container.unmount()
})
},
act
}
}

const renderHook = createRenderHook(createNativeRenderer)

export { renderHook, act, cleanup, addCleanup, removeCleanup }
30 changes: 30 additions & 0 deletions src/pure.ts
@@ -0,0 +1,30 @@
import { RenderingEngineArray, ReactHooksRenderer } from 'types'

const RENDERERS: RenderingEngineArray = [
{ required: 'react-test-renderer', renderer: './native/pure' }
]

function getRenderer(renderers: RenderingEngineArray): string {
const hasDependency = (name: string) => {
try {
require(name)
return true
} catch {
return false
}
}

const [validRenderer] = renderers.filter(({ required }) => hasDependency(required))

if (validRenderer) {
return validRenderer.renderer
} else {
const options = renderers.map(({ renderer }) => ` - ${renderer}`).join('\n')
throw new Error(`Could not auto-detect a React renderer. Options are:\n${options}`)
}
}

// eslint-disable-next-line @typescript-eslint/no-var-requires
const { renderHook, act, cleanup } = require(getRenderer(RENDERERS)) as ReactHooksRenderer

export { renderHook, act, cleanup }