Skip to content

Commit

Permalink
feat(pure): add renderOptions support to render
Browse files Browse the repository at this point in the history
  • Loading branch information
naorpeled committed Jun 1, 2024
1 parent c1f2957 commit faba7c1
Show file tree
Hide file tree
Showing 4 changed files with 136 additions and 27 deletions.
64 changes: 64 additions & 0 deletions src/__tests__/render.js
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,46 @@ describe('render API', () => {
expect(wrapperComponentMountEffect).toHaveBeenCalledTimes(1)
})

test('renderOptions are passed to createRoot', () => {
function Component() {
const id = React.useId()
return <div id={id} />
}

const container = document.createElement('div')
document.body.appendChild(container)

render(<Component />, {
container,
renderOptions: {
identifierPrefix: 'some-identifier-prefix',
},
})

expect(container.firstChild.id).toContain('some-identifier-prefix')
})

test('renderOptions are passed to hydrateRoot', () => {
function Component() {
const id = React.useId()
return <div id={id} />
}

const container = document.createElement('div')
document.body.appendChild(container)
container.innerHTML = ReactDOMServer.renderToString(<Component />)

render(<Component />, {
container,
hydrate: false,
renderOptions: {
identifierPrefix: 'some-identifier-prefix',
},
})

expect(container.firstChild.id).toContain('some-identifier-prefix')
})

testGateReact18('legacyRoot uses legacy ReactDOM.render', () => {
expect(() => {
render(<div />, {legacyRoot: true})
Expand Down Expand Up @@ -262,4 +302,28 @@ describe('render API', () => {
`\`legacyRoot: true\` is not supported in this version of React. If your app runs React 19 or later, you should remove this flag. If your app runs React 18 or earlier, visit https://react.dev/blog/2022/03/08/react-18-upgrade-guide for upgrade instructions.`,
)
})

testGateReact19('renderOptions supports onUncaughtError', () => {
const onUncaughtError = jest.fn()
const error = new Error('uncaught error')
function Component() {
throw error
}

const container = document.createElement('div')
document.body.appendChild(container)

render(<Component />, {
container,
renderOptions: {
onUncaughtError,
},
})

expect(onUncaughtError).toHaveBeenCalledTimes(1)
expect(onUncaughtError).toHaveBeenCalledWith(error)
})

// TODO
// testGateReact19('renderOptions supports onCaughtError', () => {})
})
8 changes: 5 additions & 3 deletions src/pure.js
Original file line number Diff line number Diff line change
Expand Up @@ -91,18 +91,19 @@ function wrapUiIfNeeded(innerElement, wrapperComponent) {

function createConcurrentRoot(
container,
{hydrate, ui, wrapper: WrapperComponent},
{hydrate, ui, wrapper: WrapperComponent, renderOptions},
) {
let root
if (hydrate) {
act(() => {
root = ReactDOMClient.hydrateRoot(
container,
strictModeIfNeeded(wrapUiIfNeeded(ui, WrapperComponent)),
renderOptions,
)
})
} else {
root = ReactDOMClient.createRoot(container)
root = ReactDOMClient.createRoot(container, renderOptions)
}

return {
Expand Down Expand Up @@ -205,6 +206,7 @@ function render(
queries,
hydrate = false,
wrapper,
renderOptions,
} = {},
) {
if (legacyRoot && typeof ReactDOM.render !== 'function') {
Expand All @@ -230,7 +232,7 @@ function render(
// eslint-disable-next-line no-negated-condition -- we want to map the evolution of this over time. The root is created first. Only later is it re-used so we don't want to read the case that happens later first.
if (!mountedContainers.has(container)) {
const createRootImpl = legacyRoot ? createLegacyRoot : createConcurrentRoot
root = createRootImpl(container, {hydrate, ui, wrapper})
root = createRootImpl(container, {hydrate, ui, wrapper, renderOptions})

mountedRootEntries.push({container, root})
// we'll add it to the mounted containers regardless of whether it's actually
Expand Down
54 changes: 30 additions & 24 deletions types/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ export type BaseRenderOptions<
BaseElement extends RendererableContainer | HydrateableContainer,
> = RenderOptions<Q, Container, BaseElement>

type RendererableContainer = ReactDOMClient.Container
type RendererableContainer = Parameters<typeof ReactDOMClient['createRoot']>[0]
type HydrateableContainer = Parameters<typeof ReactDOMClient['hydrateRoot']>[0]
/** @deprecated */
export interface ClientRenderOptions<
Expand All @@ -61,8 +61,8 @@ export interface ClientRenderOptions<
BaseElement extends RendererableContainer = Container,
> extends BaseRenderOptions<Q, Container, BaseElement> {
/**
* If `hydrate` is set to `true`, then it will render with `ReactDOM.hydrate`. This may be useful if you are using server-side
* rendering and use ReactDOM.hydrate to mount your components.
* If `hydrate` is set to `true`, then it will render with `ReactDOM.hydrate`. This may be useful if you are using
* server-side rendering and use ReactDOM.hydrate to mount your components.
*
* @see https://testing-library.com/docs/react-testing-library/api/#hydrate)
*/
Expand All @@ -75,8 +75,8 @@ export interface HydrateOptions<
BaseElement extends HydrateableContainer = Container,
> extends BaseRenderOptions<Q, Container, BaseElement> {
/**
* If `hydrate` is set to `true`, then it will render with `ReactDOM.hydrate`. This may be useful if you are using server-side
* rendering and use ReactDOM.hydrate to mount your components.
* If `hydrate` is set to `true`, then it will render with `ReactDOM.hydrate`. This may be useful if you are using
* server-side rendering and use ReactDOM.hydrate to mount your components.
*
* @see https://testing-library.com/docs/react-testing-library/api/#hydrate)
*/
Expand All @@ -87,10 +87,13 @@ export interface RenderOptions<
Q extends Queries = typeof queries,
Container extends RendererableContainer | HydrateableContainer = HTMLElement,
BaseElement extends RendererableContainer | HydrateableContainer = Container,
LegacyRoot extends boolean = boolean,
Hydrate extends boolean = boolean,
> {
/**
* By default, React Testing Library will create a div and append that div to the document.body. Your React component will be rendered in the created div. If you provide your own HTMLElement container via this option,
* it will not be appended to the document.body automatically.
* By default, React Testing Library will create a div and append that div to the document.body. Your React component
* will be rendered in the created div. If you provide your own HTMLElement container via this option, it will not be
* appended to the document.body automatically.
*
* For example: If you are unit testing a `<tbody>` element, it cannot be a child of a div. In this case, you can
* specify a table as the render container.
Expand All @@ -99,38 +102,43 @@ export interface RenderOptions<
*/
container?: Container
/**
* Defaults to the container if the container is specified. Otherwise `document.body` is used for the default. This is used as
* the base element for the queries as well as what is printed when you use `debug()`.
* Defaults to the container if the container is specified. Otherwise `document.body` is used for the default. This
* is used as the base element for the queries as well as what is printed when you use `debug()`.
*
* @see https://testing-library.com/docs/react-testing-library/api/#baseelement
*/
baseElement?: BaseElement
/**
* If `hydrate` is set to `true`, then it will render with `ReactDOM.hydrate`. This may be useful if you are using server-side
* rendering and use ReactDOM.hydrate to mount your components.
* If `hydrate` is set to `true`, then it will render with `ReactDOM.hydrate`. This may be useful if you are using
* server-side rendering and use ReactDOM.hydrate to mount your components.
*
* @see https://testing-library.com/docs/react-testing-library/api/#hydrate)
*/
hydrate?: boolean
hydrate?: Hydrate
/**
* Only works if used with React 18.
* Set to `true` if you want to force synchronous `ReactDOM.render`.
* Otherwise `render` will default to concurrent React if available.
*/
legacyRoot?: boolean
legacyRoot?: LegacyRoot
/**
* Queries to bind. Overrides the default set from DOM Testing Library unless merged.
*
* @see https://testing-library.com/docs/react-testing-library/api/#queries
*/
queries?: Q
/**
* Pass a React Component as the wrapper option to have it rendered around the inner element. This is most useful for creating
* reusable custom render functions for common data providers. See setup for examples.
* Pass a React Component as the wrapper option to have it rendered around the inner element. This is most useful for
* creating reusable custom render functions for common data providers. See setup for examples.
*
* @see https://testing-library.com/docs/react-testing-library/api/#wrapper
*/
wrapper?: React.JSXElementConstructor<{children: React.ReactNode}>
renderOptions?: LegacyRoot extends true
? never
: Hydrate extends true
? ReactDOMClient.HydrationOptions
: ReactDOMClient.RootOptions
}

type Omit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>
Expand All @@ -142,14 +150,12 @@ export function render<
Q extends Queries = typeof queries,
Container extends RendererableContainer | HydrateableContainer = HTMLElement,
BaseElement extends RendererableContainer | HydrateableContainer = Container,
LegacyRoot extends boolean = boolean,
Hydrate extends boolean = boolean,
>(
ui: React.ReactNode,
options: RenderOptions<Q, Container, BaseElement>,
options?: RenderOptions<Q, Container, BaseElement, LegacyRoot, Hydrate>,
): RenderResult<Q, Container, BaseElement>
export function render(
ui: React.ReactNode,
options?: Omit<RenderOptions, 'queries'>,
): RenderResult

export interface RenderHookResult<Result, Props> {
/**
Expand Down Expand Up @@ -189,8 +195,8 @@ export interface ClientRenderHookOptions<
BaseElement extends Element | DocumentFragment = Container,
> extends BaseRenderHookOptions<Props, Q, Container, BaseElement> {
/**
* If `hydrate` is set to `true`, then it will render with `ReactDOM.hydrate`. This may be useful if you are using server-side
* rendering and use ReactDOM.hydrate to mount your components.
* If `hydrate` is set to `true`, then it will render with `ReactDOM.hydrate`. This may be useful if you are using
* server-side rendering and use ReactDOM.hydrate to mount your components.
*
* @see https://testing-library.com/docs/react-testing-library/api/#hydrate)
*/
Expand All @@ -205,8 +211,8 @@ export interface HydrateHookOptions<
BaseElement extends Element | DocumentFragment = Container,
> extends BaseRenderHookOptions<Props, Q, Container, BaseElement> {
/**
* If `hydrate` is set to `true`, then it will render with `ReactDOM.hydrate`. This may be useful if you are using server-side
* rendering and use ReactDOM.hydrate to mount your components.
* If `hydrate` is set to `true`, then it will render with `ReactDOM.hydrate`. This may be useful if you are using
* server-side rendering and use ReactDOM.hydrate to mount your components.
*
* @see https://testing-library.com/docs/react-testing-library/api/#hydrate)
*/
Expand Down
37 changes: 37 additions & 0 deletions types/test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,43 @@ export function testContainer() {
renderHook(() => null, {container: document, hydrate: true})
}

export function testRootContainerWithOptions() {
render('a', {
container: document.createElement('div'),
legacyRoot: true,
// @ts-expect-error - legacyRoot does not allow additional options
renderOptions: {},
})

render('a', {
container: document.createElement('div'),
legacyRoot: false,
renderOptions: {
identifierPrefix: 'test',
onRecoverableError: (_error, _errorInfo) => {},
// @ts-expect-error - only RootOptions are allowed
nonExistentOption: 'test',
},
})
render('a', {
container: document.createElement('div'),
renderOptions: {
identifierPrefix: 'test',
},
})

render('a', {
container: document.createElement('div'),
hydrate: true,
renderOptions: {
identifierPrefix: 'test',
onRecoverableError: (_error, _errorInfo) => {},
// @ts-expect-error - only HydrationOptions are allowed
nonExistentOption: 'test',
},
})
}

/*
eslint
testing-library/prefer-explicit-assert: "off",
Expand Down

0 comments on commit faba7c1

Please sign in to comment.