244 lines
8.8 KiB
TypeScript
244 lines
8.8 KiB
TypeScript
// 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('仅项目创建者可管理成员')
|
||
})
|
||
})
|