|  | 
|  | 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