// RdmMembersSection rdm.role.options 授权门控:rdm.perm.check 给出 member.manage // 未允许时,不得提前发 rdm.role.options(普通成员被 403);允许后必须发。 // // 测试在 node 环境下用最小化的 DOM mock 驱动 React 19 createRoot,让 useEffect 正常落槽; // 不去依赖 happy-dom/jsdom(受网络/包锁约束,allowed_paths 也限制仅 src + test)。 import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { createElement, type ReactNode } from 'react' import { act } from 'react' import { createRoot, type Root } from 'react-dom/client' import { RdmMembersSection } from '../src/components/adminRdmSections' import { api } from '../src/api' import type { Project } from '../src/contracts' // 用 vi.mock 隔离 useStore:组件只用到 select(state).me,其它 store 字段触发不到, // 不会影响此处关注点(避免 host Store 启动监听器、送 SSE 等副作用污染测试)。 vi.mock('../src/store', () => { const state = { me: { owner: 'bob', name: 'bob', auth: true }, } return { useStore: (select: (value: typeof state) => unknown) => select(state), eventTime: (_e: { ts?: string }, now: number) => now, } }) // ---- 自定义 React DOM 桩:让 useEffect 在 node 环境落到 commit 阶段 ---- // 服务端 React 不运行 useEffect;createRoot 需要一个 DOM-shaped container。 // 这里只覆盖 react-dom-client 用到的几个关键入口(addEventListener、tagName、namespaceURI、 // ownerDocument.defaultView.HTMLIFrameElement、activeElement、documentElement)。任何 // 新字段缺失都会在第一处 React 调用时显式爆出,便于修复而不是静默吞下。 class HTMLIFrameElement {} class MockDocument { nodeType = 9 defaultView: any HTMLIFrameElement = HTMLIFrameElement activeElement: any = null documentElement: any constructor() { this.defaultView = makeWindow() this.defaultView.document = this this.activeElement = null this.documentElement = this } addEventListener() {} removeEventListener() {} dispatchEvent() {} contains() { return false } createElement(tag: string) { const el = new MockElement() el.tagName = tag el.nodeName = tag el.ownerDocument = this if (renderObserver) renderObserver.append(tag) return el } createElementNS(_ns: string, tag: string) { return this.createElement(tag) } createTextNode() { return new MockElement() } } /** 创建 DOM 节点时把它们的 tag 累积下来,替代 innerHTML 读取(Mock 上 innerHTML 为空)。 */ let renderObserver: { tags: string[]; reset(): void; append(t: string): void } | null = null class MockElement { nodeType = 1 style: Record = {} attributes: Record = {} tagName = 'DIV' nodeName = 'DIV' namespaceURI = 'http://www.w3.org/1999/xhtml' ownerDocument: any childNodes: any[] = [] parentNode: any = null appendChild(c: any) { this.childNodes.push(c); c.parentNode = this; return c } removeChild(c: any) { const i = this.childNodes.indexOf(c); if (i >= 0) this.childNodes.splice(i, 1); c.parentNode = null; return c } insertBefore(c: any, ref: any) { const i = ref ? this.childNodes.indexOf(ref) : this.childNodes.length this.childNodes.splice(i < 0 ? this.childNodes.length : i, 0, c) c.parentNode = this return c } contains(c: any) { for (let n: any = c; n; n = n.parentNode) if (n === this) return true return false } addEventListener() {} removeEventListener() {} dispatchEvent() {} focus() {} setAttribute(k: string, v: string) { this.attributes[k] = v } removeAttribute(k: string) { delete this.attributes[k] } cloneNode() { return new MockElement() } /** React 写 `