Skip to content

Commit faba7c1

Browse files
committed
feat(pure): add renderOptions support to render
1 parent c1f2957 commit faba7c1

File tree

4 files changed

+136
-27
lines changed

4 files changed

+136
-27
lines changed

src/__tests__/render.js

+64
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,46 @@ describe('render API', () => {
219219
expect(wrapperComponentMountEffect).toHaveBeenCalledTimes(1)
220220
})
221221

222+
test('renderOptions are passed to createRoot', () => {
223+
function Component() {
224+
const id = React.useId()
225+
return <div id={id} />
226+
}
227+
228+
const container = document.createElement('div')
229+
document.body.appendChild(container)
230+
231+
render(<Component />, {
232+
container,
233+
renderOptions: {
234+
identifierPrefix: 'some-identifier-prefix',
235+
},
236+
})
237+
238+
expect(container.firstChild.id).toContain('some-identifier-prefix')
239+
})
240+
241+
test('renderOptions are passed to hydrateRoot', () => {
242+
function Component() {
243+
const id = React.useId()
244+
return <div id={id} />
245+
}
246+
247+
const container = document.createElement('div')
248+
document.body.appendChild(container)
249+
container.innerHTML = ReactDOMServer.renderToString(<Component />)
250+
251+
render(<Component />, {
252+
container,
253+
hydrate: false,
254+
renderOptions: {
255+
identifierPrefix: 'some-identifier-prefix',
256+
},
257+
})
258+
259+
expect(container.firstChild.id).toContain('some-identifier-prefix')
260+
})
261+
222262
testGateReact18('legacyRoot uses legacy ReactDOM.render', () => {
223263
expect(() => {
224264
render(<div />, {legacyRoot: true})
@@ -262,4 +302,28 @@ describe('render API', () => {
262302
`\`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.`,
263303
)
264304
})
305+
306+
testGateReact19('renderOptions supports onUncaughtError', () => {
307+
const onUncaughtError = jest.fn()
308+
const error = new Error('uncaught error')
309+
function Component() {
310+
throw error
311+
}
312+
313+
const container = document.createElement('div')
314+
document.body.appendChild(container)
315+
316+
render(<Component />, {
317+
container,
318+
renderOptions: {
319+
onUncaughtError,
320+
},
321+
})
322+
323+
expect(onUncaughtError).toHaveBeenCalledTimes(1)
324+
expect(onUncaughtError).toHaveBeenCalledWith(error)
325+
})
326+
327+
// TODO
328+
// testGateReact19('renderOptions supports onCaughtError', () => {})
265329
})

src/pure.js

+5-3
Original file line numberDiff line numberDiff line change
@@ -91,18 +91,19 @@ function wrapUiIfNeeded(innerElement, wrapperComponent) {
9191

9292
function createConcurrentRoot(
9393
container,
94-
{hydrate, ui, wrapper: WrapperComponent},
94+
{hydrate, ui, wrapper: WrapperComponent, renderOptions},
9595
) {
9696
let root
9797
if (hydrate) {
9898
act(() => {
9999
root = ReactDOMClient.hydrateRoot(
100100
container,
101101
strictModeIfNeeded(wrapUiIfNeeded(ui, WrapperComponent)),
102+
renderOptions,
102103
)
103104
})
104105
} else {
105-
root = ReactDOMClient.createRoot(container)
106+
root = ReactDOMClient.createRoot(container, renderOptions)
106107
}
107108

108109
return {
@@ -205,6 +206,7 @@ function render(
205206
queries,
206207
hydrate = false,
207208
wrapper,
209+
renderOptions,
208210
} = {},
209211
) {
210212
if (legacyRoot && typeof ReactDOM.render !== 'function') {
@@ -230,7 +232,7 @@ function render(
230232
// 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.
231233
if (!mountedContainers.has(container)) {
232234
const createRootImpl = legacyRoot ? createLegacyRoot : createConcurrentRoot
233-
root = createRootImpl(container, {hydrate, ui, wrapper})
235+
root = createRootImpl(container, {hydrate, ui, wrapper, renderOptions})
234236

235237
mountedRootEntries.push({container, root})
236238
// we'll add it to the mounted containers regardless of whether it's actually

types/index.d.ts

+30-24
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ export type BaseRenderOptions<
5252
BaseElement extends RendererableContainer | HydrateableContainer,
5353
> = RenderOptions<Q, Container, BaseElement>
5454

55-
type RendererableContainer = ReactDOMClient.Container
55+
type RendererableContainer = Parameters<typeof ReactDOMClient['createRoot']>[0]
5656
type HydrateableContainer = Parameters<typeof ReactDOMClient['hydrateRoot']>[0]
5757
/** @deprecated */
5858
export interface ClientRenderOptions<
@@ -61,8 +61,8 @@ export interface ClientRenderOptions<
6161
BaseElement extends RendererableContainer = Container,
6262
> extends BaseRenderOptions<Q, Container, BaseElement> {
6363
/**
64-
* If `hydrate` is set to `true`, then it will render with `ReactDOM.hydrate`. This may be useful if you are using server-side
65-
* rendering and use ReactDOM.hydrate to mount your components.
64+
* If `hydrate` is set to `true`, then it will render with `ReactDOM.hydrate`. This may be useful if you are using
65+
* server-side rendering and use ReactDOM.hydrate to mount your components.
6666
*
6767
* @see https://testing-library.com/docs/react-testing-library/api/#hydrate)
6868
*/
@@ -75,8 +75,8 @@ export interface HydrateOptions<
7575
BaseElement extends HydrateableContainer = Container,
7676
> extends BaseRenderOptions<Q, Container, BaseElement> {
7777
/**
78-
* If `hydrate` is set to `true`, then it will render with `ReactDOM.hydrate`. This may be useful if you are using server-side
79-
* rendering and use ReactDOM.hydrate to mount your components.
78+
* If `hydrate` is set to `true`, then it will render with `ReactDOM.hydrate`. This may be useful if you are using
79+
* server-side rendering and use ReactDOM.hydrate to mount your components.
8080
*
8181
* @see https://testing-library.com/docs/react-testing-library/api/#hydrate)
8282
*/
@@ -87,10 +87,13 @@ export interface RenderOptions<
8787
Q extends Queries = typeof queries,
8888
Container extends RendererableContainer | HydrateableContainer = HTMLElement,
8989
BaseElement extends RendererableContainer | HydrateableContainer = Container,
90+
LegacyRoot extends boolean = boolean,
91+
Hydrate extends boolean = boolean,
9092
> {
9193
/**
92-
* 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,
93-
* it will not be appended to the document.body automatically.
94+
* By default, React Testing Library will create a div and append that div to the document.body. Your React component
95+
* will be rendered in the created div. If you provide your own HTMLElement container via this option, it will not be
96+
* appended to the document.body automatically.
9497
*
9598
* For example: If you are unit testing a `<tbody>` element, it cannot be a child of a div. In this case, you can
9699
* specify a table as the render container.
@@ -99,38 +102,43 @@ export interface RenderOptions<
99102
*/
100103
container?: Container
101104
/**
102-
* Defaults to the container if the container is specified. Otherwise `document.body` is used for the default. This is used as
103-
* the base element for the queries as well as what is printed when you use `debug()`.
105+
* Defaults to the container if the container is specified. Otherwise `document.body` is used for the default. This
106+
* is used as the base element for the queries as well as what is printed when you use `debug()`.
104107
*
105108
* @see https://testing-library.com/docs/react-testing-library/api/#baseelement
106109
*/
107110
baseElement?: BaseElement
108111
/**
109-
* If `hydrate` is set to `true`, then it will render with `ReactDOM.hydrate`. This may be useful if you are using server-side
110-
* rendering and use ReactDOM.hydrate to mount your components.
112+
* If `hydrate` is set to `true`, then it will render with `ReactDOM.hydrate`. This may be useful if you are using
113+
* server-side rendering and use ReactDOM.hydrate to mount your components.
111114
*
112115
* @see https://testing-library.com/docs/react-testing-library/api/#hydrate)
113116
*/
114-
hydrate?: boolean
117+
hydrate?: Hydrate
115118
/**
116119
* Only works if used with React 18.
117120
* Set to `true` if you want to force synchronous `ReactDOM.render`.
118121
* Otherwise `render` will default to concurrent React if available.
119122
*/
120-
legacyRoot?: boolean
123+
legacyRoot?: LegacyRoot
121124
/**
122125
* Queries to bind. Overrides the default set from DOM Testing Library unless merged.
123126
*
124127
* @see https://testing-library.com/docs/react-testing-library/api/#queries
125128
*/
126129
queries?: Q
127130
/**
128-
* Pass a React Component as the wrapper option to have it rendered around the inner element. This is most useful for creating
129-
* reusable custom render functions for common data providers. See setup for examples.
131+
* Pass a React Component as the wrapper option to have it rendered around the inner element. This is most useful for
132+
* creating reusable custom render functions for common data providers. See setup for examples.
130133
*
131134
* @see https://testing-library.com/docs/react-testing-library/api/#wrapper
132135
*/
133136
wrapper?: React.JSXElementConstructor<{children: React.ReactNode}>
137+
renderOptions?: LegacyRoot extends true
138+
? never
139+
: Hydrate extends true
140+
? ReactDOMClient.HydrationOptions
141+
: ReactDOMClient.RootOptions
134142
}
135143

136144
type Omit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>
@@ -142,14 +150,12 @@ export function render<
142150
Q extends Queries = typeof queries,
143151
Container extends RendererableContainer | HydrateableContainer = HTMLElement,
144152
BaseElement extends RendererableContainer | HydrateableContainer = Container,
153+
LegacyRoot extends boolean = boolean,
154+
Hydrate extends boolean = boolean,
145155
>(
146156
ui: React.ReactNode,
147-
options: RenderOptions<Q, Container, BaseElement>,
157+
options?: RenderOptions<Q, Container, BaseElement, LegacyRoot, Hydrate>,
148158
): RenderResult<Q, Container, BaseElement>
149-
export function render(
150-
ui: React.ReactNode,
151-
options?: Omit<RenderOptions, 'queries'>,
152-
): RenderResult
153159

154160
export interface RenderHookResult<Result, Props> {
155161
/**
@@ -189,8 +195,8 @@ export interface ClientRenderHookOptions<
189195
BaseElement extends Element | DocumentFragment = Container,
190196
> extends BaseRenderHookOptions<Props, Q, Container, BaseElement> {
191197
/**
192-
* If `hydrate` is set to `true`, then it will render with `ReactDOM.hydrate`. This may be useful if you are using server-side
193-
* rendering and use ReactDOM.hydrate to mount your components.
198+
* If `hydrate` is set to `true`, then it will render with `ReactDOM.hydrate`. This may be useful if you are using
199+
* server-side rendering and use ReactDOM.hydrate to mount your components.
194200
*
195201
* @see https://testing-library.com/docs/react-testing-library/api/#hydrate)
196202
*/
@@ -205,8 +211,8 @@ export interface HydrateHookOptions<
205211
BaseElement extends Element | DocumentFragment = Container,
206212
> extends BaseRenderHookOptions<Props, Q, Container, BaseElement> {
207213
/**
208-
* If `hydrate` is set to `true`, then it will render with `ReactDOM.hydrate`. This may be useful if you are using server-side
209-
* rendering and use ReactDOM.hydrate to mount your components.
214+
* If `hydrate` is set to `true`, then it will render with `ReactDOM.hydrate`. This may be useful if you are using
215+
* server-side rendering and use ReactDOM.hydrate to mount your components.
210216
*
211217
* @see https://testing-library.com/docs/react-testing-library/api/#hydrate)
212218
*/

types/test.tsx

+37
Original file line numberDiff line numberDiff line change
@@ -254,6 +254,43 @@ export function testContainer() {
254254
renderHook(() => null, {container: document, hydrate: true})
255255
}
256256

257+
export function testRootContainerWithOptions() {
258+
render('a', {
259+
container: document.createElement('div'),
260+
legacyRoot: true,
261+
// @ts-expect-error - legacyRoot does not allow additional options
262+
renderOptions: {},
263+
})
264+
265+
render('a', {
266+
container: document.createElement('div'),
267+
legacyRoot: false,
268+
renderOptions: {
269+
identifierPrefix: 'test',
270+
onRecoverableError: (_error, _errorInfo) => {},
271+
// @ts-expect-error - only RootOptions are allowed
272+
nonExistentOption: 'test',
273+
},
274+
})
275+
render('a', {
276+
container: document.createElement('div'),
277+
renderOptions: {
278+
identifierPrefix: 'test',
279+
},
280+
})
281+
282+
render('a', {
283+
container: document.createElement('div'),
284+
hydrate: true,
285+
renderOptions: {
286+
identifierPrefix: 'test',
287+
onRecoverableError: (_error, _errorInfo) => {},
288+
// @ts-expect-error - only HydrationOptions are allowed
289+
nonExistentOption: 'test',
290+
},
291+
})
292+
}
293+
257294
/*
258295
eslint
259296
testing-library/prefer-explicit-assert: "off",

0 commit comments

Comments
 (0)