Skip to content

feat(pure): add renderOptions support to render #1333

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

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading