From a627a4c84d8b2bb50360923471c4d57e700ac518 Mon Sep 17 00:00:00 2001 From: Menghuan1918 Date: Thu, 3 Sep 2026 12:04:24 +0800 Subject: [PATCH] =?UTF-8?q?test(utils):=20=E6=96=B0=E5=BB=BA=E5=85=B1?= =?UTF-8?q?=E4=BA=AB=20React=20=E6=B5=8B=E8=AF=95=E5=B7=A5=E5=85=B7=20setu?= =?UTF-8?q?pReactAct=20/=20renderRoot?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 收敛组件 spec 两类样板的最小共享面:IS_REACT_ACT_ENVIRONMENT 幂等设置 (vitest 按文件隔离 global,逐 spec 调用与原内联语句语义一致)与 createRoot + act 的分离容器渲染循环。刻意不做查询 DSL / 自动清理 / 新依赖。 --- tests/test-utils.ts | 63 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 63 insertions(+) create mode 100644 tests/test-utils.ts diff --git a/tests/test-utils.ts b/tests/test-utils.ts new file mode 100644 index 0000000..cb38baf --- /dev/null +++ b/tests/test-utils.ts @@ -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).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() + }, + } +}