test(utils): 新建共享 React 测试工具 setupReactAct / renderRoot
收敛组件 spec 两类样板的最小共享面:IS_REACT_ACT_ENVIRONMENT 幂等设置 (vitest 按文件隔离 global,逐 spec 调用与原内联语句语义一致)与 createRoot + act 的分离容器渲染循环。刻意不做查询 DSL / 自动清理 / 新依赖。
This commit is contained in:
@@ -0,0 +1,63 @@
|
||||
/**
|
||||
* Shared React helpers for the component specs (tests/*.spec.tsx): the two
|
||||
* pieces of boilerplate every createRoot-based spec used to repeat inline —
|
||||
* - the React 18 act() environment flag, without which act() warns on every
|
||||
* flush (setupReactAct(), idempotent, called at module scope per spec —
|
||||
* vitest isolates each spec file in its own global, so per-file setup is
|
||||
* required, exactly like the inlined statement it replaces),
|
||||
* - the detached-container render cycle (div → document.body.append →
|
||||
* createRoot → act(render) → act(unmount) + container.remove), collected
|
||||
* as renderRoot().
|
||||
*
|
||||
* Deliberately minimal: no querying DSL, no auto-cleanup registry, no new
|
||||
* dependencies. Specs with bespoke harnesses (domain fixtures around the
|
||||
* render, SSR renders, module-scoped container state) keep their own mounts.
|
||||
*/
|
||||
import type { ReactNode } from 'react'
|
||||
import { createRoot, type Root } from 'react-dom/client'
|
||||
import { act } from 'react-dom/test-utils'
|
||||
|
||||
/**
|
||||
* Declare this file's environment as a React act() environment (React 18.2
|
||||
* reads the flag before flushing effects). Assigning `true` is idempotent,
|
||||
* so every spec may call it unconditionally at module scope.
|
||||
*/
|
||||
export function setupReactAct(): void {
|
||||
;(globalThis as Record<string, unknown>).IS_REACT_ACT_ENVIRONMENT = true
|
||||
}
|
||||
|
||||
/** What renderRoot() hands back: the DOM container plus act()-wrapped controls. */
|
||||
export interface RenderedRoot {
|
||||
/** The div renderRoot() appended to document.body. */
|
||||
container: HTMLDivElement
|
||||
/** The react-dom root, for direct render/unmount control if ever needed. */
|
||||
root: Root
|
||||
/** Re-render a new element tree under act(). */
|
||||
rerender: (next: ReactNode) => void
|
||||
/** Unmount under act() and drop the container from document.body. */
|
||||
unmount: () => void
|
||||
}
|
||||
|
||||
/**
|
||||
* Render `node` into a detached div appended to document.body and flush the
|
||||
* initial commit under act() — the concurrent initial commit is flushed, so
|
||||
* the rendered tree is already in the DOM when renderRoot() returns.
|
||||
* rerender(next) re-renders under act(); unmount() unmounts under act() and
|
||||
* removes the container (call it once — like the per-spec mounts this
|
||||
* replaces, it is not guarded against double unmount).
|
||||
*/
|
||||
export function renderRoot(node: ReactNode): RenderedRoot {
|
||||
const container = document.createElement('div')
|
||||
document.body.append(container)
|
||||
const root: Root = createRoot(container)
|
||||
act(() => { root.render(node) })
|
||||
return {
|
||||
container,
|
||||
root,
|
||||
rerender: (next: ReactNode): void => { act(() => { root.render(next) }) },
|
||||
unmount: (): void => {
|
||||
act(() => { root.unmount() })
|
||||
container.remove()
|
||||
},
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user