Files
apes-sidebar/tests/test-utils.ts
T
Menghuan1918 a627a4c84d test(utils): 新建共享 React 测试工具 setupReactAct / renderRoot
收敛组件 spec 两类样板的最小共享面:IS_REACT_ACT_ENVIRONMENT 幂等设置
(vitest 按文件隔离 global,逐 spec 调用与原内联语句语义一致)与
createRoot + act 的分离容器渲染循环。刻意不做查询 DSL / 自动清理 /
新依赖。
2026-09-03 12:04:24 +08:00

64 lines
2.6 KiB
TypeScript

/**
* 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()
},
}
}