From faba7c10171b1966c145d39a6208c55721d8349d Mon Sep 17 00:00:00 2001 From: Naor Peled Date: Sun, 2 Jun 2024 00:28:36 +0300 Subject: [PATCH] feat(pure): add renderOptions support to render --- src/__tests__/render.js | 64 +++++++++++++++++++++++++++++++++++++++++ src/pure.js | 8 ++++-- types/index.d.ts | 54 ++++++++++++++++++---------------- types/test.tsx | 37 ++++++++++++++++++++++++ 4 files changed, 136 insertions(+), 27 deletions(-) diff --git a/src/__tests__/render.js b/src/__tests__/render.js index f00410b4..24646728 100644 --- a/src/__tests__/render.js +++ b/src/__tests__/render.js @@ -219,6 +219,46 @@ describe('render API', () => { expect(wrapperComponentMountEffect).toHaveBeenCalledTimes(1) }) + test('renderOptions are passed to createRoot', () => { + function Component() { + const id = React.useId() + return
+ } + + const container = document.createElement('div') + document.body.appendChild(container) + + render(, { + 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
+ } + + const container = document.createElement('div') + document.body.appendChild(container) + container.innerHTML = ReactDOMServer.renderToString() + + render(, { + container, + hydrate: false, + renderOptions: { + identifierPrefix: 'some-identifier-prefix', + }, + }) + + expect(container.firstChild.id).toContain('some-identifier-prefix') + }) + testGateReact18('legacyRoot uses legacy ReactDOM.render', () => { expect(() => { render(
, {legacyRoot: true}) @@ -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(, { + container, + renderOptions: { + onUncaughtError, + }, + }) + + expect(onUncaughtError).toHaveBeenCalledTimes(1) + expect(onUncaughtError).toHaveBeenCalledWith(error) + }) + + // TODO + // testGateReact19('renderOptions supports onCaughtError', () => {}) }) diff --git a/src/pure.js b/src/pure.js index f546af98..c71152e6 100644 --- a/src/pure.js +++ b/src/pure.js @@ -91,7 +91,7 @@ function wrapUiIfNeeded(innerElement, wrapperComponent) { function createConcurrentRoot( container, - {hydrate, ui, wrapper: WrapperComponent}, + {hydrate, ui, wrapper: WrapperComponent, renderOptions}, ) { let root if (hydrate) { @@ -99,10 +99,11 @@ function createConcurrentRoot( root = ReactDOMClient.hydrateRoot( container, strictModeIfNeeded(wrapUiIfNeeded(ui, WrapperComponent)), + renderOptions, ) }) } else { - root = ReactDOMClient.createRoot(container) + root = ReactDOMClient.createRoot(container, renderOptions) } return { @@ -205,6 +206,7 @@ function render( queries, hydrate = false, wrapper, + renderOptions, } = {}, ) { if (legacyRoot && typeof ReactDOM.render !== 'function') { @@ -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 diff --git a/types/index.d.ts b/types/index.d.ts index 37c8392a..dc41db15 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -52,7 +52,7 @@ export type BaseRenderOptions< BaseElement extends RendererableContainer | HydrateableContainer, > = RenderOptions -type RendererableContainer = ReactDOMClient.Container +type RendererableContainer = Parameters[0] type HydrateableContainer = Parameters[0] /** @deprecated */ export interface ClientRenderOptions< @@ -61,8 +61,8 @@ export interface ClientRenderOptions< BaseElement extends RendererableContainer = Container, > extends BaseRenderOptions { /** - * 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) */ @@ -75,8 +75,8 @@ export interface HydrateOptions< BaseElement extends HydrateableContainer = Container, > extends BaseRenderOptions { /** - * 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) */ @@ -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 `` element, it cannot be a child of a div. In this case, you can * specify a table as the render container. @@ -99,25 +102,25 @@ 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. * @@ -125,12 +128,17 @@ export interface RenderOptions< */ 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 = Pick> @@ -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, + options?: RenderOptions, ): RenderResult -export function render( - ui: React.ReactNode, - options?: Omit, -): RenderResult export interface RenderHookResult { /** @@ -189,8 +195,8 @@ export interface ClientRenderHookOptions< BaseElement extends Element | DocumentFragment = Container, > extends BaseRenderHookOptions { /** - * 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) */ @@ -205,8 +211,8 @@ export interface HydrateHookOptions< BaseElement extends Element | DocumentFragment = Container, > extends BaseRenderHookOptions { /** - * 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) */ diff --git a/types/test.tsx b/types/test.tsx index f8cf4aad..037a6cf2 100644 --- a/types/test.tsx +++ b/types/test.tsx @@ -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",