import { changeField, openSelect } from '@/test-utils/select'
/** @jest-environment jsdom */
import { act, fireEvent, render, screen, waitFor, within } from '@testing-library/react'
import { PerformanceSurveyAdminConfig } from './PerformanceSurveyAdminConfig'
const templateDefinition = {
code: 'visual_review',
name: '视觉案例评分',
shortName: '视觉评分',
description: '视觉案例评分',
surveyType: 'visual_case',
weight: 1,
maxTotal: 60,
questionMaxScore: 20,
requiredWorkItemCount: 3,
workItemImageRequired: true,
questions: [{ id: 'quality', title: '作品质量', maxScore: 20 }],
scoreBands: [],
workflow: {
stages: [{
key: 'review',
label: '终评',
order: 0,
reviewers: [{ source: 'direct_manager', label: '直属主管' }],
}],
scoring: { mode: 'final_reviewer', stageKey: 'review', reviewerSource: 'direct_manager' },
},
} as const
const config = {
month: '2026-07',
groups: [{
id: 'group-1',
code: 'brand_designer',
name: '品牌设计师',
sortOrder: 10,
isActive: true,
status: 'ready',
memberCount: 6,
members: [{
id: 'employee-ruolan',
displayName: '若兰',
departmentName: '品牌创意B部',
positionTitle: '营销策划主管',
}, {
id: 'employee-muyi',
displayName: '木一',
departmentName: '品牌创意A部',
positionTitle: '品牌策划',
}],
rules: [{
id: 'rule-1',
departmentPathPrefix: '集团/品牌中心',
positionTitle: '品牌设计师',
isActive: true,
sortOrder: 0,
}],
effectivePlan: {
id: 'plan-1',
version: 1,
effectiveMonth: '2026-07',
templateVersionIds: ['version-1'],
},
draftPlan: null,
}, {
id: 'group-2',
code: 'ai_designer',
name: 'AI 技术设计师',
sortOrder: 20,
isActive: true,
status: 'pending',
memberCount: 2,
members: [],
rules: [],
effectivePlan: null,
draftPlan: null,
}],
templates: [{
id: 'template-1',
code: 'visual_review',
name: '视觉案例评分',
description: '视觉案例评分',
surveyType: 'visual_case',
isActive: true,
versions: [{
id: 'version-1',
version: 1,
status: 'published',
effectiveMonth: '2026-07',
definition: templateDefinition,
}],
}, {
id: 'template-2',
code: 'course_share',
name: '课程分享',
description: '课程分享',
surveyType: 'item_course',
isActive: true,
versions: [{
id: 'version-2',
version: 1,
status: 'published',
effectiveMonth: '2026-07',
definition: {
...templateDefinition,
code: 'course_share',
name: '课程分享',
shortName: '课程分享',
surveyType: 'item_course',
},
}],
}],
issues: {
unmatched: [{ id: 'employee-1', displayName: '待配置人员', departmentPath: '集团/品牌中心', positionTitle: '设计师' }],
conflicts: [],
},
} as any
function createRequest(configuration = config) {
return jest.fn(async (path: string, options?: RequestInit) => {
if (path.startsWith('/admin/config?')) return configuration
if (path.startsWith('/admin/config-audit-logs')) return { items: [] }
if (path.startsWith('/admin/position-assignments?')) {
return {
items: [{
id: 'employee-1',
employeeId: 'employee-1',
displayName: '张三',
departmentName: '品牌中心',
positionTitle: '设计师',
participation: 'unassigned',
}],
total: 1,
page: 1,
pageSize: 200,
}
}
if (path.startsWith('/employees?purpose=evaluator')) {
return [{ id: 'employee-yuanze', displayName: '元泽', departmentName: '品牌中心' }]
}
if (path.endsWith('/preview?month=2026-07')) {
return {
memberCount: 6,
conflictCount: 0,
draftPlanId: 'plan-1',
draftVersion: 2,
draftUpdatedAt: '2026-07-23T10:00:00.000Z',
}
}
if (path === '/admin/position-groups' && options?.method === 'POST') return { id: 'group-3' }
if (path === '/admin/position-groups/group-1' && options?.method === 'DELETE') {
return { id: 'group-1', action: 'deleted' }
}
if (path === '/admin/templates/template-1' && options?.method === 'DELETE') {
return { id: 'template-1', action: 'deleted' }
}
if (path === '/admin/templates/version-1' && options?.method === 'PATCH') {
return { id: 'version-1' }
}
if (path.endsWith('/copy')) {
return { id: 'copied-version-1', templateId: 'copied-template-1', name: '视觉案例评分(副本)' }
}
return {}
})
}
describe('PerformanceSurveyAdminConfig', () => {
beforeEach(() => {
Object.defineProperty(window, 'matchMedia', {
configurable: true,
value: jest.fn().mockReturnValue({ matches: false, addEventListener: jest.fn(), removeEventListener: jest.fn() }),
})
})
afterEach(() => {
jest.restoreAllMocks()
})
test('概览将岗位设置与发布状态合并展示', async () => {
const request = createRequest()
const onNavigate = jest.fn()
render()
expect(screen.getByText('配置加载中...')).toBeInTheDocument()
await waitFor(() => expect(screen.getByRole('heading', { name: '2026-07 配置概览' })).toBeInTheDocument())
expect(screen.getByRole('heading', { name: '本月上线准备' })).toBeInTheDocument()
expect(screen.getByText(/1 人待确认/)).toBeInTheDocument()
fireEvent.click(screen.getByRole('button', { name: '处理人员' }))
expect(onNavigate).toHaveBeenCalledWith('people')
expect(request).toHaveBeenCalledWith('/admin/config?month=2026-07&scope=overview')
expect(request).toHaveBeenCalledWith(expect.stringContaining('/admin/config-audit-logs'))
})
test('不同管理页面只请求当前页面需要的配置范围', async () => {
const request = createRequest()
const view = render()
await waitFor(() => expect(request).toHaveBeenCalledWith('/admin/config?month=2026-07&scope=templates'))
view.rerender()
await waitFor(() => expect(request).toHaveBeenCalledWith('/admin/config?month=2026-07&scope=people'))
})
test('概览使用汇总数量,不依赖返回全部人员异常明细', async () => {
const request = createRequest({
...config,
issueCounts: { unmatched: 499, conflicts: 0, excluded: 0 },
issues: { unmatched: [], conflicts: [] },
})
render()
expect(await screen.findByText(/499 人待确认/)).toBeInTheDocument()
})
test('受控月份切换通过外层更新,保持管理工作区月份一致', async () => {
const request = createRequest()
const onMonthChange = jest.fn()
render(
,
)
await waitFor(() => expect(screen.getByRole('heading', { name: '2026-08 配置概览' })).toBeInTheDocument())
expect(screen.getByText(/1\/2 个岗位设置已生效/)).toBeInTheDocument()
changeField(screen.getByLabelText('查看月份'), { target: { value: '2026-09' } })
await waitFor(() => expect(onMonthChange).toHaveBeenCalledWith('2026-09'))
})
test.each([
['positions', '岗位管理'],
['people', '人员分配'],
['templates', '模板管理'],
] as const)('%s 管理页面不显示月份查看入口', async (mode, heading) => {
const request = createRequest()
render()
await waitFor(() => expect(screen.getByRole('heading', { name: heading })).toBeInTheDocument())
expect(screen.queryByLabelText('查看月份')).not.toBeInTheDocument()
expect(request).toHaveBeenCalledWith(`/admin/config?month=2026-07&scope=${mode}`)
})
test('岗位编辑产生 dirty 状态并保护岗位切换', async () => {
const request = createRequest()
const onDirtyChange = jest.fn()
render()
await waitFor(() => expect(screen.getByLabelText('岗位名称')).toHaveValue('品牌设计师'))
fireEvent.click(screen.getByRole('button', { name: '修改岗位' }))
changeField(screen.getByLabelText('岗位名称'), { target: { value: '品牌视觉设计师' } })
expect(onDirtyChange).toHaveBeenLastCalledWith(true)
fireEvent.click(screen.getByRole('button', { name: /AI 技术设计师/ }))
expect(await screen.findByRole('alertdialog', { name: '放弃未保存的修改?' })).toBeInTheDocument()
fireEvent.click(screen.getByRole('button', { name: '取消' }))
expect(screen.getByLabelText('岗位名称')).toHaveValue('品牌视觉设计师')
fireEvent.click(screen.getByRole('button', { name: /AI 技术设计师/ }))
fireEvent.click(await screen.findByRole('button', { name: '放弃修改' }))
await waitFor(() => expect(screen.getByLabelText('岗位名称')).toHaveValue('AI 技术设计师'))
})
test('已发布岗位默认只读,并通过修改岗位进入编辑', async () => {
const request = createRequest()
render()
await waitFor(() => expect(screen.getByRole('button', { name: '修改岗位' })).toBeInTheDocument())
expect(screen.getByLabelText('岗位名称')).toBeDisabled()
expect(screen.getByLabelText('排序')).toBeDisabled()
expect(screen.getByLabelText('添加考核项')).toBeDisabled()
expect(screen.queryByRole('button', { name: '移除' })).not.toBeInTheDocument()
expect(screen.queryByRole('button', { name: '保存草稿' })).not.toBeInTheDocument()
expect(screen.queryByRole('button', { name: '发布岗位设置' })).not.toBeInTheDocument()
fireEvent.click(screen.getByRole('button', { name: '修改岗位' }))
expect(screen.getByLabelText('岗位名称')).toBeEnabled()
expect(screen.getByLabelText('排序')).toBeEnabled()
expect(screen.getByLabelText('添加考核项')).toBeEnabled()
expect(screen.getByRole('button', { name: '移除' })).toBeInTheDocument()
expect(screen.getByRole('button', { name: '保存草稿' })).toBeDisabled()
expect(screen.getByRole('button', { name: '发布岗位设置' })).toBeDisabled()
})
test('删除岗位使用统一弹窗收集必填原因并提交', async () => {
const request = createRequest()
render()
await waitFor(() => expect(screen.getByLabelText('岗位名称')).toHaveValue('品牌设计师'))
fireEvent.click(screen.getByRole('button', { name: '删除岗位' }))
const dialog = await screen.findByRole('alertdialog', { name: '删除岗位“品牌设计师”?' })
const dialogQueries = within(dialog)
expect(dialog).toHaveTextContent('6 人将变为待分配')
expect(dialog).not.toHaveTextContent('历史调研记录会保留')
expect(dialog).not.toHaveTextContent('原因将记录在管理操作日志中')
expect(dialog.parentElement).toHaveClass('backdrop-blur-[2px]')
expect(dialogQueries.getByRole('button', { name: '删除岗位' })).toBeDisabled()
changeField(dialogQueries.getByLabelText(/删除原因/), { target: { value: '岗位合并' } })
fireEvent.click(dialogQueries.getByRole('button', { name: '删除岗位' }))
await waitFor(() => expect(request).toHaveBeenCalledWith(
'/admin/position-groups/group-1',
expect.objectContaining({
method: 'DELETE',
body: JSON.stringify({ mode: 'auto', reason: '岗位合并' }),
}),
))
})
test('岗位页以搜索方式维护草稿考核项,并只展示已选择内容', async () => {
const configuration = {
...config,
groups: config.groups.map((group: any) => group.id === 'group-1'
? {
...group,
draftPlan: {
id: 'plan-draft-1',
version: 2,
effectiveMonth: '2026-08',
templateVersionIds: ['version-2'],
},
}
: group),
}
const request = createRequest(configuration)
render()
await waitFor(() => expect(screen.getByRole('heading', { name: '岗位考核项' })).toBeInTheDocument())
expect(screen.getByText('课程分享')).toBeInTheDocument()
expect(screen.queryByText('视觉案例评分')).not.toBeInTheDocument()
changeField(screen.getByLabelText('添加考核项'), { target: { value: '视觉案例' } })
fireEvent.click(screen.getByRole('button', { name: /视觉案例评分/ }))
expect(screen.getByText('视觉案例评分')).toBeInTheDocument()
fireEvent.click(screen.getByRole('button', { name: '保存草稿' }))
await waitFor(() => {
const call = request.mock.calls.find(([path, options]) => (
path === '/admin/position-groups/group-1' && options?.method === 'PATCH'
))
expect(JSON.parse(String(call?.[1]?.body || '{}'))).toEqual({
name: '品牌设计师',
sortOrder: 10,
templateVersionIds: ['version-2', 'version-1'],
})
})
expect(screen.queryByText('人员匹配规则')).not.toBeInTheDocument()
expect(screen.queryByRole('button', { name: '复制岗位' })).not.toBeInTheDocument()
})
test('岗位模板搜索和已选考核项同步显示模板栏的最新名称', async () => {
const configuration = {
...config,
groups: config.groups.map((group: any) => group.id === 'group-1'
? {
...group,
effectivePlan: {
...group.effectivePlan,
templateVersionIds: [],
},
}
: group),
templates: config.templates.map((template: any) => template.id === 'template-1'
? {
...template,
versions: [{
...template.versions[0],
definition: {
...template.versions[0].definition,
name: '品牌创意部门',
},
}],
}
: template),
}
const request = createRequest(configuration)
render()
await waitFor(() => expect(screen.getByRole('heading', { name: '岗位考核项' })).toBeInTheDocument())
fireEvent.click(screen.getByRole('button', { name: '修改岗位' }))
changeField(screen.getByLabelText('添加考核项'), { target: { value: '品牌创意部门' } })
const option = screen.getByRole('button', { name: /品牌创意部门.*已发布/ })
expect(option).toBeInTheDocument()
expect(screen.queryByRole('button', { name: /视觉案例评分.*已发布/ })).not.toBeInTheDocument()
fireEvent.click(option)
expect(screen.getByText('品牌创意部门')).toBeInTheDocument()
})
test('新增岗位使用确定的 collection POST 契约', async () => {
const request = createRequest()
render()
await waitFor(() => expect(screen.getByRole('button', { name: /品牌设计师,6 人,已生效/ })).toBeInTheDocument())
fireEvent.click(screen.getByRole('button', { name: '新增岗位' }))
expect(screen.queryByLabelText('新岗位代码')).not.toBeInTheDocument()
changeField(screen.getByLabelText('新岗位名称'), { target: { value: '新设计岗位' } })
fireEvent.click(screen.getByRole('button', { name: '确认创建' }))
await waitFor(() => expect(request).toHaveBeenCalledWith('/admin/position-groups', expect.objectContaining({
method: 'POST',
body: JSON.stringify({ name: '新设计岗位' }),
})))
})
test('岗位人数可展开查看具体人员并可收起', async () => {
const request = createRequest()
render()
const expandButton = (await screen.findAllByRole('button', { name: '展开人员名单' }))[0]
expect(expandButton).toHaveAttribute('aria-expanded', 'false')
expect(screen.queryByText('若兰')).not.toBeInTheDocument()
fireEvent.click(expandButton)
expect(expandButton).toHaveAttribute('aria-expanded', 'true')
expect(screen.getByRole('list', { name: '品牌设计师人员名单' })).toBeInTheDocument()
expect(screen.getByText('若兰')).toBeInTheDocument()
expect(screen.getByText('品牌创意B部 · 营销策划主管')).toBeInTheDocument()
fireEvent.click(screen.getByRole('button', { name: '收起人员名单' }))
expect(screen.queryByText('若兰')).not.toBeInTheDocument()
})
test('人员批量分配提交最终 REST 请求体', async () => {
const request = createRequest()
render()
await waitFor(() => expect(screen.getAllByText('张三').length).toBeGreaterThan(0))
fireEvent.click(screen.getByLabelText('选择 张三'))
changeField(screen.getByLabelText('批量分配操作'), { target: { value: 'group-1' } })
changeField(screen.getByLabelText('本次调整原因'), { target: { value: '部门异动' } })
fireEvent.click(screen.getByRole('button', { name: '应用分配' }))
await waitFor(() => {
const call = request.mock.calls.find(([path, options]) => (
path === '/admin/position-assignments' && options?.method === 'POST'
))
expect(call).toBeDefined()
expect(JSON.parse(String(call?.[1]?.body || '{}'))).toEqual({
employeeIds: ['employee-1'],
effectiveFromMonth: '2026-07',
effectiveToMonth: null,
assignmentType: 'position',
positionGroupId: 'group-1',
reason: '部门异动',
})
})
})
test('已发布模板通过修改按钮直接更新原版本,并使用统一的删除入口', async () => {
const request = createRequest()
render()
await waitFor(() => expect(screen.getByRole('button', { name: /视觉案例评分.*可用/ })).toBeInTheDocument())
fireEvent.click(screen.getByRole('button', { name: '修改模板' }))
changeField(screen.getByLabelText('模板名称'), { target: { value: '新视觉评分' } })
fireEvent.click(screen.getByRole('button', { name: '保存修改' }))
await waitFor(() => expect(request).toHaveBeenCalledWith(
'/admin/templates/version-1',
expect.objectContaining({ method: 'PATCH' }),
))
expect(request.mock.calls.some(([path]) => path === '/admin/templates/version-1/clone')).toBe(false)
expect(screen.getByRole('button', { name: '删除模板' })).toBeInTheDocument()
expect(screen.queryByText('使用场景')).not.toBeInTheDocument()
})
test('新增模板按钮位于模板列表顶部,与岗位管理入口布局一致', async () => {
const request = createRequest()
render()
await waitFor(() => expect(screen.getByRole('button', { name: /视觉案例评分.*可用/ })).toBeInTheDocument())
const createButton = screen.getByRole('button', { name: '新增模板' })
const deleteButton = screen.getByRole('button', { name: '删除模板' })
const templateSidebar = createButton.closest('aside')
expect(templateSidebar).not.toBeNull()
expect(createButton.parentElement).toBe(templateSidebar?.firstElementChild)
expect(within(templateSidebar as HTMLElement).getByRole('button', { name: '新增模板' })).toBe(createButton)
expect(createButton.nextElementSibling).not.toBe(deleteButton)
})
test('已发布模板可通过修改按钮左侧的复制按钮创建独立模板草稿', async () => {
const request = createRequest()
render()
await waitFor(() => expect(screen.getByRole('button', { name: /视觉案例评分.*可用/ })).toBeInTheDocument())
const copyButton = screen.getByRole('button', { name: '复制模板' })
const modifyButton = screen.getByRole('button', { name: '修改模板' })
expect(copyButton.nextElementSibling).toBe(modifyButton)
fireEvent.click(copyButton)
await waitFor(() => expect(request).toHaveBeenCalledWith(
'/admin/templates/version-1/copy',
expect.objectContaining({ method: 'POST', body: '{}' }),
))
})
test('模板列表只展示单一当前配置状态', async () => {
const configuration = {
...config,
templates: [{
...config.templates[0],
versions: [{
...config.templates[0].versions[0],
id: 'version-draft',
version: 2,
status: 'draft',
effectiveMonth: null,
}, config.templates[0].versions[0]],
}, {
...config.templates[1],
versions: [{
...config.templates[1].versions[0],
id: 'version-future',
effectiveMonth: '2026-09',
}],
}, {
...config.templates[1],
id: 'template-draft-only',
code: 'draft_only',
name: '草稿模板',
versions: [{
...config.templates[1].versions[0],
id: 'version-draft-only',
status: 'draft',
effectiveMonth: null,
definition: {
...config.templates[1].versions[0].definition,
code: 'draft_only',
name: '草稿模板',
},
}],
}, {
...config.templates[0],
id: 'template-current',
code: 'current_only',
name: '当前可用模板',
versions: [{
...config.templates[0].versions[0],
id: 'version-current',
definition: {
...config.templates[0].versions[0].definition,
code: 'current_only',
name: '当前可用模板',
},
}],
}],
}
const request = createRequest(configuration)
render()
const changedTemplate = await screen.findByRole('button', { name: /视觉案例评分.*可用/ })
expect(within(changedTemplate).getByRole('img', { name: '可用' })).toHaveAttribute('data-status-dot', 'success')
const publishedTemplate = screen.getByRole('button', { name: /课程分享.*可用/ })
expect(within(publishedTemplate).getByRole('img', { name: '可用' })).toHaveAttribute('data-status-dot', 'success')
const draftTemplate = screen.getByRole('button', { name: /草稿模板.*待发布/ })
expect(within(draftTemplate).getByRole('img', { name: '待发布' })).toHaveAttribute('data-status-dot', 'warning')
const availableTemplate = screen.getByRole('button', { name: /当前可用模板.*可用/ })
expect(within(availableTemplate).getByRole('img', { name: '可用' })).toHaveAttribute('data-status-dot', 'success')
expect(screen.queryByText('已发布模板会长期使用;只有评分规则变化时才创建新版本')).not.toBeInTheDocument()
})
test('岗位和模板列表复用同一状态点视觉语义', async () => {
const request = createRequest()
const positions = render()
const positionButton = await screen.findByRole('button', { name: /品牌设计师.*6 人.*已生效/ })
expect(within(positionButton).getByRole('img', { name: '已生效' })).toHaveAttribute('data-status-dot', 'success')
positions.unmount()
render()
const templateButton = await screen.findByRole('button', { name: /视觉案例评分.*可用/ })
expect(within(templateButton).getByRole('img', { name: '可用' })).toHaveAttribute('data-status-dot', 'success')
})
test('删除模板使用统一弹窗并保留接口错误以便重试', async () => {
const baseRequest = createRequest()
let deleteAttempts = 0
const request = jest.fn(async (path: string, options?: RequestInit) => {
if (path === '/admin/templates/template-1' && options?.method === 'DELETE') {
deleteAttempts += 1
if (deleteAttempts === 1) throw new Error('模板仍被岗位使用,请先移除岗位考核项')
}
return baseRequest(path, options)
})
render()
await waitFor(() => expect(screen.getByRole('button', { name: /视觉案例评分.*可用/ })).toBeInTheDocument())
fireEvent.click(screen.getByRole('button', { name: '删除模板' }))
const dialog = await screen.findByRole('alertdialog', { name: '删除模板“视觉案例评分”?' })
const dialogQueries = within(dialog)
expect(dialog).not.toHaveAttribute('aria-describedby')
expect(dialog).not.toHaveTextContent('历史调研中的模板和评分数据会继续保留')
expect(dialog).not.toHaveTextContent('如果岗位仍在使用该模板')
expect(dialog).not.toHaveTextContent('原因将记录在管理操作日志中')
expect(dialog.parentElement).toHaveClass('backdrop-blur-[2px]')
changeField(dialogQueries.getByLabelText(/删除原因/), { target: { value: '模板重复' } })
fireEvent.click(dialogQueries.getByRole('button', { name: '删除模板' }))
expect(await dialogQueries.findByRole('alert')).toHaveTextContent('模板仍被岗位使用')
expect(dialogQueries.getByLabelText(/删除原因/)).toHaveValue('模板重复')
fireEvent.click(dialogQueries.getByRole('button', { name: '删除模板' }))
await waitFor(() => expect(request).toHaveBeenCalledWith(
'/admin/templates/template-1',
expect.objectContaining({
method: 'DELETE',
body: JSON.stringify({ mode: 'auto', reason: '模板重复' }),
}),
))
expect(deleteAttempts).toBe(2)
})
test('发布模板不再要求选择生效月份', async () => {
const configuration = {
...config,
templates: config.templates.map((template: any) => template.id === 'template-1'
? {
...template,
versions: template.versions.map((version: any) => ({
...version,
status: 'draft',
effectiveMonth: null,
})),
}
: template),
}
const request = createRequest(configuration)
render()
await waitFor(() => expect(screen.getByRole('button', { name: /视觉案例评分,待发布/ })).toBeInTheDocument())
fireEvent.click(screen.getByRole('button', { name: '发布模板' }))
const dialog = await screen.findByRole('dialog', { name: '发布模板“视觉案例评分”?' })
expect(dialog).toHaveTextContent('模板发布后立即可用')
expect(dialog).not.toHaveTextContent('2026-07')
fireEvent.click(within(dialog).getByRole('button', { name: '确认发布' }))
await waitFor(() => expect(request).toHaveBeenCalledWith(
'/admin/templates/version-1/publish',
expect.objectContaining({
method: 'POST',
body: '{}',
}),
))
})
test('模板编辑按基本信息、评分项、评分人设置分步展示', async () => {
const request = createRequest()
render()
await waitFor(() => expect(screen.getByRole('navigation', { name: '模板编辑步骤' })).toBeInTheDocument())
expect(screen.getByRole('heading', { name: '基本信息' })).toBeInTheDocument()
expect(screen.getByLabelText('考核内容类型')).toHaveValue('visual_case')
expect(screen.queryByText(/决定调研按员工表现、作品案例或一次具体事项来评分/)).not.toBeInTheDocument()
fireEvent.click(screen.getByRole('button', { name: '2. 评分项' }))
expect(screen.getByRole('heading', { name: '评分项' })).toBeInTheDocument()
expect(screen.getByLabelText('评分项名称')).toHaveValue('作品质量')
expect(screen.getByLabelText('总分')).toHaveValue(60)
fireEvent.click(screen.getByRole('button', { name: '3. 评分人设置' }))
expect(screen.getByRole('heading', { name: '评分人设置' })).toBeInTheDocument()
expect(screen.getByLabelText('最终成绩规则')).toBeInTheDocument()
})
test('评分人设置提供直属主管、元泽和发起时选择任何人', async () => {
const configuration = {
...config,
templates: config.templates.map((template: any) => template.id === 'template-1'
? {
...template,
versions: template.versions.map((version: any) => ({ ...version, status: 'draft', effectiveMonth: null })),
}
: template),
}
const request = createRequest(configuration)
render()
await waitFor(() => expect(screen.getByRole('button', { name: '3. 评分人设置' })).toBeInTheDocument())
fireEvent.click(screen.getByRole('button', { name: '3. 评分人设置' }))
const reviewerSelect = screen.getByLabelText('阶段 1 评分人类型 1')
expect(reviewerSelect).toHaveValue('direct_manager')
expect(within(openSelect(reviewerSelect)).getByRole('option', { name: '直属主管' })).toBeInTheDocument()
expect(within(openSelect(reviewerSelect)).getByRole('option', { name: '元泽' })).toBeInTheDocument()
expect(within(openSelect(reviewerSelect)).getByRole('option', { name: '任何人(发起时选择)' })).toBeInTheDocument()
changeField(reviewerSelect, { target: { value: 'fixed_employee' } })
await waitFor(() => expect(screen.getByLabelText('选择元泽人员档案')).toHaveValue('元泽'))
changeField(reviewerSelect, { target: { value: 'manual_selection' } })
await waitFor(() => expect(reviewerSelect).toHaveValue('manual_selection'))
})
test('元泽人员档案异步返回时不会覆盖等待期间的流程修改', async () => {
const configuration = {
...config,
templates: config.templates.map((template: any) => template.id === 'template-1'
? {
...template,
versions: template.versions.map((version: any) => ({ ...version, status: 'draft', effectiveMonth: null })),
}
: template),
}
let resolveEmployees: ((employees: Array<{ id: string; displayName: string }>) => void) | null = null
const employeeRequest = new Promise>((resolve) => {
resolveEmployees = resolve
})
const baseRequest = createRequest(configuration)
const request = jest.fn(async (path: string, options?: RequestInit) => {
if (path.startsWith('/employees?purpose=evaluator')) return employeeRequest
return baseRequest(path, options)
})
render()
await waitFor(() => expect(screen.getByRole('button', { name: '3. 评分人设置' })).toBeInTheDocument())
fireEvent.click(screen.getByRole('button', { name: '3. 评分人设置' }))
changeField(screen.getByLabelText('阶段 1 评分人类型 1'), { target: { value: 'fixed_employee' } })
changeField(screen.getByLabelText('阶段 1 名称'), { target: { value: '终审修改' } })
await act(async () => {
resolveEmployees?.([{ id: 'employee-yuanze', displayName: '元泽' }])
await employeeRequest
})
await waitFor(() => expect(screen.getByLabelText('选择元泽人员档案')).toHaveValue('元泽'))
expect(screen.getByLabelText('阶段 1 名称')).toHaveValue('终审修改')
})
test('模板草稿可自由增删评分项并显示总分分配状态', async () => {
const configuration = {
...config,
templates: config.templates.map((template: any) => template.id === 'template-1'
? {
...template,
versions: template.versions.map((version: any) => ({ ...version, status: 'draft', effectiveMonth: null })),
}
: template),
}
const request = createRequest(configuration)
render()
await waitFor(() => expect(screen.getByRole('button', { name: '2. 评分项' })).toBeInTheDocument())
fireEvent.click(screen.getByRole('button', { name: '2. 评分项' }))
fireEvent.click(screen.getByRole('button', { name: '删除评分项 1' }))
expect(screen.getByText('还没有评分项,请点击“添加评分项”')).toBeInTheDocument()
fireEvent.click(screen.getByRole('button', { name: '添加评分项' }))
expect(screen.getByLabelText('评分项名称')).toHaveValue('评分项 1')
expect(screen.getByText('已分配 20 × 3 个作品 = 60 / 60 分')).toBeInTheDocument()
})
test('移动端提供紧凑模板选择器并在选择后直接更新编辑上下文', async () => {
const request = createRequest()
render()
const templateSelect = await screen.findByLabelText('选择模板')
expect(templateSelect).toHaveValue('template-1')
changeField(templateSelect, { target: { value: 'template-2' } })
await waitFor(() => expect(templateSelect).toHaveValue('template-2'))
expect(screen.getByLabelText('模板名称')).toHaveValue('课程分享')
})
test('评分项支持折叠、复制、排序、删除撤销和底部新增', async () => {
const configuration = {
...config,
templates: config.templates.map((template: any) => template.id === 'template-1'
? {
...template,
versions: template.versions.map((version: any) => ({ ...version, status: 'draft', effectiveMonth: null })),
}
: template),
}
const request = createRequest(configuration)
render()
fireEvent.click(await screen.findByRole('button', { name: '2. 评分项' }))
expect(screen.getByRole('button', { name: /评分项 1.*作品质量.*20 分.*完整/ })).toHaveAttribute('aria-expanded', 'true')
fireEvent.click(screen.getByRole('button', { name: '复制评分项 1' }))
expect(screen.getByRole('button', { name: /评分项 2.*作品质量(副本)/ })).toHaveAttribute('aria-expanded', 'true')
expect(screen.getByRole('button', { name: '上移评分项 2' })).toBeEnabled()
fireEvent.click(screen.getByRole('button', { name: '上移评分项 2' }))
expect(screen.getAllByRole('button', { name: /评分项 \d.*作品质量/ })[0]).toHaveAccessibleName(/评分项 1.*作品质量(副本)/)
fireEvent.click(screen.getByRole('button', { name: '删除评分项 1' }))
expect(screen.getByText('已删除“作品质量(副本)”')).toBeInTheDocument()
fireEvent.click(screen.getByRole('button', { name: '撤销删除' }))
expect(screen.getByRole('button', { name: /评分项 1.*作品质量(副本)/ })).toBeInTheDocument()
expect(screen.getByRole('button', { name: '添加评分项' })).toBeInTheDocument()
})
test('旧模板缺少单项满分时无需编辑即可显示模板默认分值', async () => {
const configuration = {
...config,
templates: config.templates.map((template: any) => template.id === 'template-1'
? {
...template,
versions: template.versions.map((version: any) => ({
...version,
definition: {
...version.definition,
questions: version.definition.questions.map(({ maxScore: _maxScore, ...question }: any) => question),
},
})),
}
: template),
}
const request = createRequest(configuration)
render()
fireEvent.click(await screen.findByRole('button', { name: '2. 评分项' }))
expect(screen.getByRole('button', { name: /评分项 1.*作品质量.*20 分.*完整/ })).toBeInTheDocument()
expect(screen.getByLabelText('满分')).toHaveValue(20)
expect(screen.getByText('已分配 20 × 3 个作品 = 60 / 60 分')).toBeInTheDocument()
})
test('发布前检查会定位问题,并持续反馈草稿保存状态', async () => {
const configuration = {
...config,
templates: config.templates.map((template: any) => template.id === 'template-1'
? {
...template,
versions: template.versions.map((version: any) => ({ ...version, status: 'draft', effectiveMonth: null })),
}
: template),
}
const request = createRequest(configuration)
render()
await waitFor(() => expect(screen.getByLabelText('模板名称')).toHaveValue('视觉案例评分'))
changeField(screen.getByLabelText('模板名称'), { target: { value: '' } })
expect(screen.getByText('请填写模板名称')).toBeInTheDocument()
expect(screen.getByRole('button', { name: '发布模板' })).toBeDisabled()
expect(screen.getByText('未保存')).toBeInTheDocument()
changeField(screen.getByLabelText('模板名称'), { target: { value: '视觉案例评分新版' } })
fireEvent.click(screen.getByRole('button', { name: '保存草稿' }))
await waitFor(() => expect(request).toHaveBeenCalledWith(
'/admin/templates/version-1',
expect.objectContaining({ method: 'PATCH' }),
))
expect(await screen.findByText(/草稿已保存 ·/)).toBeInTheDocument()
})
test('固定评分人搜索提供可读状态与键盘可操作结果列表', async () => {
const configuration = {
...config,
templates: config.templates.map((template: any) => template.id === 'template-1'
? {
...template,
versions: template.versions.map((version: any) => ({ ...version, status: 'draft', effectiveMonth: null })),
}
: template),
}
const request = createRequest(configuration)
render()
fireEvent.click(await screen.findByRole('button', { name: '3. 评分人设置' }))
changeField(screen.getByLabelText('阶段 1 评分人类型 1'), { target: { value: 'fixed_employee' } })
const picker = await screen.findByRole('combobox', { name: '选择元泽人员档案' })
await waitFor(() => expect(picker).toHaveValue('元泽'))
changeField(picker, { target: { value: '元' } })
expect(picker).toHaveAttribute('aria-expanded', 'true')
await waitFor(() => expect(screen.getByRole('listbox', { name: '人员搜索结果' })).toBeInTheDocument())
await waitFor(() => expect(screen.getByText('找到 1 人')).toBeInTheDocument())
fireEvent.keyDown(picker, { key: 'Enter' })
expect(picker).toHaveValue('元泽')
expect(picker).toHaveAttribute('aria-expanded', 'false')
})
test('岗位页保存草稿后可直接经过影响确认并发布', async () => {
const configuration = {
...config,
groups: config.groups.map((group: any) => group.id === 'group-1'
? {
...group,
draftPlan: {
id: 'plan-1',
version: 2,
effectiveMonth: '2026-07',
templateVersionIds: ['version-1', 'version-2'],
},
}
: group),
}
const request = createRequest(configuration)
render()
await waitFor(() => expect(screen.getByRole('heading', { name: '岗位考核项' })).toBeInTheDocument())
const publishButton = screen.getByRole('button', { name: '发布岗位设置' })
expect(publishButton).toBeEnabled()
fireEvent.click(publishButton)
const dialog = await screen.findByRole('dialog', { name: '发布“品牌设计师”的岗位设置?' })
expect(dialog).toHaveTextContent('包含 2 个考核项,预计影响 6 人')
fireEvent.click(within(dialog).getByRole('button', { name: '确认发布' }))
await waitFor(() => expect(request).toHaveBeenCalledWith('/admin/position-groups/group-1/publish', expect.objectContaining({
method: 'POST',
body: JSON.stringify({
draftPlanId: 'plan-1',
draftVersion: 2,
draftUpdatedAt: '2026-07-23T10:00:00.000Z',
}),
})))
})
})