126 lines
5.3 KiB
TypeScript
126 lines
5.3 KiB
TypeScript
// 项目成员页检索(R2 契约):服务端先过滤后分页,前端只拼参数、映射候选与追加分页。
|
|
// node 环境无 DOM——决策逻辑全部落在 src/rdmManagement.ts 的纯函数上逐项断言。
|
|
import { describe, expect, it } from 'vitest'
|
|
import type { Project } from '../src/contracts'
|
|
import {
|
|
JOINED_REASON, MATCH_MODE_LABELS, MEMBER_PAGE_LIMIT, RDM_MEMBER_COMMANDS,
|
|
accountListParams, accountsToPickItems, appendMemberPage, memberListParams,
|
|
personLabel, rdmMembersReady, rdmProjectCommandError, roleOptionsPayload,
|
|
type RdmMemberRow,
|
|
} from '../src/rdmManagement'
|
|
|
|
const row = (owner: string, name?: string): RdmMemberRow =>
|
|
({ owner, name, role: 'member', addedBy: 'admin', addedAt: '' })
|
|
|
|
describe('accountListParams', () => {
|
|
it('passes roleId, match and cursor to rdm.account.list', () => {
|
|
expect(accountListParams('p1', { query: ' zhang ', cursor: 'c2', limit: 20, roleId: 'r-eng', match: 'exact' }))
|
|
.toEqual({ projectId: 'p1', query: 'zhang', cursor: 'c2', limit: 20, roleId: 'r-eng', match: 'exact' })
|
|
})
|
|
|
|
it('omits empty filters and defaults to fuzzy matching', () => {
|
|
expect(accountListParams('p1', { query: ' ', cursor: '', limit: 20, roleId: '' }))
|
|
.toEqual({ projectId: 'p1', limit: 20, match: 'fuzzy' })
|
|
})
|
|
|
|
it('labels both match modes in Chinese', () => {
|
|
expect(MATCH_MODE_LABELS).toEqual({ fuzzy: '模糊匹配', exact: '精确账号或姓名' })
|
|
})
|
|
})
|
|
|
|
describe('memberListParams', () => {
|
|
it('sends query, roleId and cursor with a page size of 50', () => {
|
|
expect(memberListParams('p1', { query: 'li', roleId: 'r-qa', cursor: 'n1' }))
|
|
.toEqual({ projectId: 'p1', query: 'li', roleId: 'r-qa', cursor: 'n1', limit: MEMBER_PAGE_LIMIT })
|
|
expect(MEMBER_PAGE_LIMIT).toBe(50)
|
|
})
|
|
|
|
it('loads the unfiltered first page when no filter is set', () => {
|
|
expect(memberListParams('p1', { query: '', roleId: '', cursor: '' })).toEqual({ projectId: 'p1', limit: 50 })
|
|
})
|
|
|
|
it('forwards the optional match and projectRole filters', () => {
|
|
expect(memberListParams('p1', { match: 'exact', projectRole: 'owner', limit: 10 }))
|
|
.toEqual({ projectId: 'p1', limit: 10, match: 'exact', projectRole: 'owner' })
|
|
})
|
|
})
|
|
|
|
describe('roleOptionsPayload', () => {
|
|
it('asks for member-purpose role options', () => {
|
|
expect(roleOptionsPayload('p1')).toEqual({ projectId: 'p1', purpose: 'members' })
|
|
})
|
|
})
|
|
|
|
describe('accountsToPickItems', () => {
|
|
it('drops invalid accounts', () => {
|
|
const items = accountsToPickItems([
|
|
{ owner: 'a', name: '甲', valid: true, joined: false },
|
|
{ owner: 'gone', name: '离职', valid: false, joined: false },
|
|
])
|
|
expect(items.map(it => it.id)).toEqual(['a'])
|
|
})
|
|
|
|
it('disables joined candidates with the 已加入项目 reason instead of a badge', () => {
|
|
const items = accountsToPickItems([
|
|
{ owner: 'a', name: '甲', valid: true, joined: true },
|
|
{ owner: 'b', valid: true, joined: false },
|
|
{ owner: 'c', name: '丙', valid: true },
|
|
], ['b'])
|
|
expect(items).toEqual([
|
|
{ id: 'a', title: '甲', subtitle: 'a', disabled: true, disabledReason: JOINED_REASON },
|
|
{ id: 'b', title: 'b', subtitle: 'b', disabled: true, disabledReason: '已加入项目' },
|
|
{ id: 'c', title: '丙', subtitle: 'c' },
|
|
])
|
|
expect(items.some(it => 'badge' in it)).toBe(false)
|
|
})
|
|
|
|
it('keeps name as title and account as subtitle so the selection shows both', () => {
|
|
const [it] = accountsToPickItems([{ owner: 'zhangsan', name: '张三', valid: true, joined: false }])
|
|
expect(it.title).toBe('张三')
|
|
expect(it.subtitle).toBe('zhangsan')
|
|
})
|
|
|
|
it('tolerates a missing account list', () => {
|
|
expect(accountsToPickItems(undefined)).toEqual([])
|
|
})
|
|
})
|
|
|
|
describe('personLabel', () => {
|
|
it('renders name · account', () => {
|
|
expect(personLabel({ owner: 'zhangsan', name: '张三' })).toBe('张三 · zhangsan')
|
|
})
|
|
|
|
it('falls back to the account when there is no name', () => {
|
|
expect(personLabel({ owner: 'zhangsan' })).toBe('zhangsan')
|
|
expect(personLabel({ owner: 'zhangsan', name: ' ' })).toBe('zhangsan')
|
|
expect(personLabel({ owner: 'zhangsan', name: 'zhangsan' })).toBe('zhangsan')
|
|
})
|
|
})
|
|
|
|
describe('appendMemberPage', () => {
|
|
it('appends the next page in order without duplicating accounts', () => {
|
|
const first = [row('a', '甲'), row('b')]
|
|
const got = appendMemberPage(first, [row('b', '乙-重复'), row('c', '丙')])
|
|
expect(got.map(m => m.owner)).toEqual(['a', 'b', 'c'])
|
|
expect(got[1].name).toBeUndefined()
|
|
expect(first.map(m => m.owner)).toEqual(['a', 'b'])
|
|
})
|
|
|
|
it('starts a fresh list from null and dedupes within a page', () => {
|
|
expect(appendMemberPage(null, [row('a'), row('a')]).map(m => m.owner)).toEqual(['a'])
|
|
expect(appendMemberPage(null, undefined)).toEqual([])
|
|
})
|
|
})
|
|
|
|
describe('rdm.role.options command gating', () => {
|
|
it('is a project read command, so archived projects can still load role filters', () => {
|
|
const archived = { id: 'p1', settings: { 'rdm.state': 'archived', 'rdm.projectId': 'rdm-1' } } as unknown as Project
|
|
expect(rdmProjectCommandError(archived, 'rdm.role.options')).toBeNull()
|
|
})
|
|
|
|
it('does not gate member page readiness', () => {
|
|
expect(RDM_MEMBER_COMMANDS).not.toContain('rdm.role.options')
|
|
expect(rdmMembersReady(RDM_MEMBER_COMMANDS.map(id => ({ id, enabled: true })))).toBe(true)
|
|
})
|
|
})
|