Files
Agent-System-Frontend/test/rdm-members-gating.test.tsx
T
agent-exec-transport b496c4476d
governance / governance (pull_request) Successful in 22s
ci / test-and-build (pull_request) Successful in 43s
agent-exec transport 5cc70be566de95cb35d2d8bdfb05b392/b0fcba7575e5
2026-10-20 03:54:20 +00:00

244 lines
8.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// RDM 项目设置:成员页门控测试
// 当 canManage=false 时:添加成员入口与行操作按钮(移除/升降级)不出现,不调用 rdm.role.options;
// 当 canManage=true 时:添加成员与行操作按钮出现,请求 rdm.role.options。
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { createElement, act } from 'react'
import { createRoot } from 'react-dom/client'
import { renderToStaticMarkup } from 'react-dom/server'
import { api } from '../src/api'
import { RdmMembersSection } from '../src/components/adminRdmSections'
import type { Project } from '../src/contracts'
import type { RdmMemberRow } from '../src/rdmManagement'
const project: Project = {
id: 'prj_test',
name: '测试项目',
settings: {
'rdm.projectId': 'rdm_prj_test',
'rdm.state': 'ready',
},
createdAt: '2026-10-01T00:00:00Z',
updatedAt: '2026-10-01T00:00:00Z',
}
const mockRows: RdmMemberRow[] = [
{ owner: 'alice', name: 'Alice', role: 'owner', addedBy: 'admin', addedAt: '2026-10-01T10:00:00Z' },
{ owner: 'bob', name: 'Bob', role: 'member', addedBy: 'alice', addedAt: '2026-10-02T10:00:00Z' },
]
describe('RdmMembersSection 静态组件门控呈现', () => {
it('canManage=false 时:添加成员与移除按钮不呈现,显示只读提示,动作列头不渲染', () => {
const html = renderToStaticMarkup(createElement(RdmMembersSection, {
project,
initialCanManage: false,
initialRows: mockRows,
}))
expect(html).toContain('仅项目创建者可管理成员;你可以查看成员列表。')
expect(html).not.toContain('添加成员')
expect(html).not.toContain('移除')
expect(html).not.toContain('降为成员')
expect(html).not.toContain('升为负责人')
// 列表基础内容、搜索框、成员信息仍可见
expect(html).toContain('alice')
expect(html).toContain('bob')
expect(html).toContain('搜索成员')
// 表头只有 5 列(无末尾操作列头 <th></th>)
const thMatches = html.match(/<th\b/g) || []
expect(thMatches).toHaveLength(5)
})
it('canManage=true 时:添加成员与移除按钮呈现,不显示只读提示,动作列头渲染', () => {
const html = renderToStaticMarkup(createElement(RdmMembersSection, {
project,
initialCanManage: true,
initialRows: mockRows,
}))
expect(html).not.toContain('仅项目创建者可管理成员')
expect(html).toContain('添加成员')
expect(html).toContain('移除')
expect(html).toContain('降为成员')
expect(html).toContain('升为负责人')
// 表头共 6 列(包含末尾操作列头 <th></th>)
const thMatches = html.match(/<th\b/g) || []
expect(thMatches).toHaveLength(6)
})
})
describe('RdmMembersSection 动态加载与命令门控交互', () => {
let commandsCalled: { id: string; params?: Record<string, unknown> }[] = []
let mockEnv: { container: any; cleanup: () => void }
function setupMockDom() {
const origDoc = globalThis.document
const origWindow = globalThis.window
const origAct = (globalThis as any).IS_REACT_ACT_ENVIRONMENT
;(globalThis as any).HTMLIFrameElement = class {}
;(globalThis as any).Element = class {}
;(globalThis as any).HTMLElement = class extends (globalThis as any).Element {}
function createMockElement(tag: string): any {
const el: any = {
nodeType: 1,
tagName: tag.toUpperCase(),
children: [],
style: {},
ownerDocument: doc,
get options() {
return el.children.filter((c: any) => c.tagName === 'OPTION')
},
get length() {
return el.children.filter((c: any) => c.tagName === 'OPTION').length
},
setAttribute(k: string, v: string) { el[k] = v },
removeAttribute(k: string) { delete el[k] },
getAttribute(k: string) { return el[k] },
appendChild(c: any) {
el.children.push(c)
c.parentNode = el
if (c.tagName === 'OPTION') {
const idx = el.children.filter((x: any) => x.tagName === 'OPTION').indexOf(c)
el[idx] = c
}
return c
},
removeChild(c: any) {
const i = el.children.indexOf(c)
if (i >= 0) el.children.splice(i, 1)
delete c.parentNode
},
insertBefore(newNode: any, refNode: any) {
const i = el.children.indexOf(refNode)
if (i >= 0) el.children.splice(i, 0, newNode)
else el.children.push(newNode)
newNode.parentNode = el
return newNode
},
addEventListener() {},
removeEventListener() {},
}
return el
}
const doc: any = {
nodeType: 9,
createElement(tag: string) { return createMockElement(tag) },
createElementNS(_ns: string, tag: string) { return createMockElement(tag) },
createTextNode(text: string) {
return { nodeType: 3, nodeValue: text, ownerDocument: doc }
},
createComment(text: string) {
return { nodeType: 8, nodeValue: text, ownerDocument: doc }
},
addEventListener() {},
removeEventListener() {},
defaultView: globalThis,
}
doc.ownerDocument = doc
globalThis.document = doc
globalThis.window = globalThis as any
;(globalThis as any).IS_REACT_ACT_ENVIRONMENT = true
return {
container: createMockElement('div'),
cleanup() {
globalThis.document = origDoc
globalThis.window = origWindow
;(globalThis as any).IS_REACT_ACT_ENVIRONMENT = origAct
},
}
}
function serializeDom(el: any): string {
if (!el) return ''
if (el.nodeType === 3) return el.nodeValue || ''
if (el.nodeType === 8) return ''
let out = `<${el.tagName.toLowerCase()}`
if (el.className) out += ` class="${el.className}"`
out += '>'
if (el.textContent && el.children.length === 0) {
out += el.textContent
} else {
for (const c of el.children) out += serializeDom(c)
}
out += `</${el.tagName.toLowerCase()}>`
return out
}
beforeEach(() => {
commandsCalled = []
mockEnv = setupMockDom()
})
afterEach(() => {
mockEnv.cleanup()
vi.restoreAllMocks()
})
it('当 rdm.project.member.list 返回 canManage=false 时:不请求 rdm.role.options,添加成员与移除按钮缺席', async () => {
vi.spyOn(api, 'rdmProjectCommand').mockImplementation(async (_p, id, params) => {
commandsCalled.push({ id, params })
if (id === 'rdm.project.member.list') {
return { members: mockRows, canManage: false, total: 2 } as any
}
if (id === 'rdm.role.options') {
return { roles: [{ roleId: 'r1', name: '架构师' }] } as any
}
return {} as any
})
const root = createRoot(mockEnv.container)
await act(async () => {
root.render(createElement(RdmMembersSection, { project }))
})
// 等待 useEffect 及微任务完成
await act(async () => {
await new Promise(r => setTimeout(r, 20))
})
const html = serializeDom(mockEnv.container)
// 证明请求了成员列表,但未请求 rdm.role.options
expect(commandsCalled.some(c => c.id === 'rdm.project.member.list')).toBe(true)
expect(commandsCalled.some(c => c.id === 'rdm.role.options')).toBe(false)
// 证明添加成员和移除按钮缺席,显示只读提示
expect(html).not.toContain('添加成员')
expect(html).not.toContain('移除')
expect(html).toContain('仅项目创建者可管理成员;你可以查看成员列表。')
})
it('当 rdm.project.member.list 返回 canManage=true 时:触发 rdm.role.options 请求,呈现添加成员与移除按钮', async () => {
vi.spyOn(api, 'rdmProjectCommand').mockImplementation(async (_p, id, params) => {
commandsCalled.push({ id, params })
if (id === 'rdm.project.member.list') {
return { members: mockRows, canManage: true, total: 2 } as any
}
if (id === 'rdm.role.options') {
return { roles: [{ roleId: 'r1', name: '架构师' }] } as any
}
return {} as any
})
const root = createRoot(mockEnv.container)
await act(async () => {
root.render(createElement(RdmMembersSection, { project }))
})
// 等待 useEffect 及微任务完成
await act(async () => {
await new Promise(r => setTimeout(r, 20))
})
const html = serializeDom(mockEnv.container)
// 证明请求了成员列表,且在 canManage=true 后请求了 rdm.role.options
expect(commandsCalled.some(c => c.id === 'rdm.project.member.list')).toBe(true)
expect(commandsCalled.some(c => c.id === 'rdm.role.options')).toBe(true)
// 证明添加成员和移除按钮存在
expect(html).toContain('添加成员')
expect(html).toContain('移除')
expect(html).not.toContain('仅项目创建者可管理成员')
})
})