|
| 1 | +import * as ReactDOMClient from 'react-dom/client' |
| 2 | +import { |
| 3 | + getQueriesForElement, |
| 4 | + prettyDOM, |
| 5 | + Queries, |
| 6 | + type RenderOptions, |
| 7 | + type RenderResult, |
| 8 | +} from '@testing-library/react' |
| 9 | +import React from 'react' |
| 10 | +import {SyncQueries} from './syncQueries.js' |
| 11 | + |
| 12 | +// Ideally we'd just use a WeakMap where containers are keys and roots are values. |
| 13 | +// We use two variables so that we can bail out in constant time when we render with a new container (most common use case) |
| 14 | + |
| 15 | +const mountedContainers: Set<import('react-dom').Container> = new Set() |
| 16 | +const mountedRootEntries: Array<{ |
| 17 | + container: import('react-dom').Container |
| 18 | + root: ReturnType<typeof createConcurrentRoot> |
| 19 | +}> = [] |
| 20 | + |
| 21 | +function renderRoot( |
| 22 | + ui: React.ReactNode, |
| 23 | + { |
| 24 | + baseElement, |
| 25 | + container, |
| 26 | + queries, |
| 27 | + wrapper: WrapperComponent, |
| 28 | + root, |
| 29 | + }: Pick<RenderOptions<Queries>, 'queries' | 'wrapper'> & { |
| 30 | + baseElement: ReactDOMClient.Container |
| 31 | + container: ReactDOMClient.Container |
| 32 | + root: ReturnType<typeof createConcurrentRoot> |
| 33 | + }, |
| 34 | +): RenderResult<Queries, any, any> { |
| 35 | + root.render( |
| 36 | + WrapperComponent ? React.createElement(WrapperComponent, null, ui) : ui, |
| 37 | + ) |
| 38 | + |
| 39 | + return { |
| 40 | + container, |
| 41 | + baseElement, |
| 42 | + debug: (el = baseElement, maxLength, options) => |
| 43 | + Array.isArray(el) |
| 44 | + ? // eslint-disable-next-line no-console |
| 45 | + el.forEach(e => |
| 46 | + console.log(prettyDOM(e as Element, maxLength, options)), |
| 47 | + ) |
| 48 | + : // eslint-disable-next-line no-console, |
| 49 | + console.log(prettyDOM(el as Element, maxLength, options)), |
| 50 | + unmount: () => { |
| 51 | + root.unmount() |
| 52 | + }, |
| 53 | + rerender: rerenderUi => { |
| 54 | + renderRoot(rerenderUi, { |
| 55 | + container, |
| 56 | + baseElement, |
| 57 | + root, |
| 58 | + wrapper: WrapperComponent, |
| 59 | + }) |
| 60 | + // Intentionally do not return anything to avoid unnecessarily complicating the API. |
| 61 | + // folks can use all the same utilities we return in the first place that are bound to the container |
| 62 | + }, |
| 63 | + asFragment: () => { |
| 64 | + /* istanbul ignore else (old jsdom limitation) */ |
| 65 | + if (typeof document.createRange === 'function') { |
| 66 | + return document |
| 67 | + .createRange() |
| 68 | + .createContextualFragment((container as HTMLElement).innerHTML) |
| 69 | + } else { |
| 70 | + const template = document.createElement('template') |
| 71 | + template.innerHTML = (container as HTMLElement).innerHTML |
| 72 | + return template.content |
| 73 | + } |
| 74 | + }, |
| 75 | + ...getQueriesForElement<Queries>(baseElement as HTMLElement, queries), |
| 76 | + } as RenderResult<Queries, any, any> // TODO clean up more |
| 77 | +} |
| 78 | + |
| 79 | +export function renderWithoutAct< |
| 80 | + Q extends Queries = SyncQueries, |
| 81 | + Container extends ReactDOMClient.Container = HTMLElement, |
| 82 | + BaseElement extends ReactDOMClient.Container = Container, |
| 83 | +>( |
| 84 | + ui: React.ReactNode, |
| 85 | + options: //Omit< |
| 86 | + RenderOptions<Q, Container, BaseElement>, |
| 87 | + //'hydrate' | 'legacyRoot' >, |
| 88 | +): RenderResult<Q, Container, BaseElement> |
| 89 | +export function renderWithoutAct( |
| 90 | + ui: React.ReactNode, |
| 91 | + options?: |
| 92 | + | Omit<RenderOptions, 'hydrate' | 'legacyRoot' | 'queries'> |
| 93 | + | undefined, |
| 94 | +): RenderResult<Queries, ReactDOMClient.Container, ReactDOMClient.Container> |
| 95 | + |
| 96 | +export function renderWithoutAct( |
| 97 | + ui: React.ReactNode, |
| 98 | + { |
| 99 | + container, |
| 100 | + baseElement = container, |
| 101 | + queries, |
| 102 | + wrapper, |
| 103 | + }: Omit< |
| 104 | + RenderOptions<Queries, ReactDOMClient.Container, ReactDOMClient.Container>, |
| 105 | + 'hydrate' | 'legacyRoot' |
| 106 | + > = {}, |
| 107 | +): RenderResult<any, ReactDOMClient.Container, ReactDOMClient.Container> { |
| 108 | + if (!baseElement) { |
| 109 | + // default to document.body instead of documentElement to avoid output of potentially-large |
| 110 | + // head elements (such as JSS style blocks) in debug output |
| 111 | + baseElement = document.body |
| 112 | + } |
| 113 | + if (!container) { |
| 114 | + container = baseElement.appendChild(document.createElement('div')) |
| 115 | + } |
| 116 | + |
| 117 | + let root: ReturnType<typeof createConcurrentRoot> |
| 118 | + // 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. |
| 119 | + if (!mountedContainers.has(container)) { |
| 120 | + root = createConcurrentRoot(container) |
| 121 | + |
| 122 | + mountedRootEntries.push({container, root}) |
| 123 | + // we'll add it to the mounted containers regardless of whether it's actually |
| 124 | + // added to document.body so the cleanup method works regardless of whether |
| 125 | + // they're passing us a custom container or not. |
| 126 | + mountedContainers.add(container) |
| 127 | + } else { |
| 128 | + mountedRootEntries.forEach(rootEntry => { |
| 129 | + // Else is unreachable since `mountedContainers` has the `container`. |
| 130 | + // Only reachable if one would accidentally add the container to `mountedContainers` but not the root to `mountedRootEntries` |
| 131 | + /* istanbul ignore else */ |
| 132 | + if (rootEntry.container === container) { |
| 133 | + root = rootEntry.root |
| 134 | + } |
| 135 | + }) |
| 136 | + } |
| 137 | + |
| 138 | + return renderRoot(ui, {baseElement, container, queries, wrapper, root: root!}) |
| 139 | +} |
| 140 | + |
| 141 | +function createConcurrentRoot(container: ReactDOMClient.Container) { |
| 142 | + const root = ReactDOMClient.createRoot(container) |
| 143 | + |
| 144 | + return { |
| 145 | + render(element: React.ReactNode) { |
| 146 | + root.render(element) |
| 147 | + }, |
| 148 | + unmount() { |
| 149 | + root.unmount() |
| 150 | + }, |
| 151 | + } |
| 152 | +} |
0 commit comments