gongxiangp2/app/performance-survey/PerformanceSurveyAdminConfig.test.tsx

722 lines
32 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.

/** @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,
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,
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.endsWith('/clone')) return { id: 'draft-version-2' }
return {}
})
}
describe('PerformanceSurveyAdminConfig', () => {
beforeEach(() => {
Object.defineProperty(window, 'matchMedia', {
configurable: true,
value: jest.fn().mockReturnValue({ matches: false }),
})
})
afterEach(() => {
jest.restoreAllMocks()
})
test('概览将岗位设置与发布状态合并展示', async () => {
const request = createRequest()
const onNavigate = jest.fn()
render(<PerformanceSurveyAdminConfig mode="overview" request={request} onNavigate={onNavigate} />)
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(expect.stringContaining('/admin/config-audit-logs'))
})
test('受控月份切换通过外层更新,保持管理工作区月份一致', async () => {
const request = createRequest()
const onMonthChange = jest.fn()
render(
<PerformanceSurveyAdminConfig
mode="overview"
request={request}
month="2026-08"
onMonthChange={onMonthChange}
/>,
)
await waitFor(() => expect(screen.getByRole('heading', { name: '2026-08 配置概览' })).toBeInTheDocument())
expect(screen.getByText(/1\/2 个岗位设置已生效,.*1 个沿用较早设置/)).toBeInTheDocument()
fireEvent.change(screen.getByLabelText('查看月份'), { target: { value: '2026-09' } })
await waitFor(() => expect(onMonthChange).toHaveBeenCalledWith('2026-09'))
})
test('岗位编辑产生 dirty 状态并保护岗位切换', async () => {
const request = createRequest()
const onDirtyChange = jest.fn()
render(<PerformanceSurveyAdminConfig mode="positions" request={request} onDirtyChange={onDirtyChange} />)
await waitFor(() => expect(screen.getByLabelText('岗位名称')).toHaveValue('品牌设计师'))
fireEvent.change(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(<PerformanceSurveyAdminConfig mode="positions" request={request} />)
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()
fireEvent.change(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(<PerformanceSurveyAdminConfig mode="positions" request={request} />)
await waitFor(() => expect(screen.getByRole('heading', { name: '岗位考核项' })).toBeInTheDocument())
expect(screen.getByText('课程分享')).toBeInTheDocument()
expect(screen.queryByText('视觉案例评分')).not.toBeInTheDocument()
fireEvent.change(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'],
effectiveMonth: '2026-07',
})
})
expect(screen.queryByText('人员匹配规则')).not.toBeInTheDocument()
expect(screen.queryByRole('button', { name: '复制岗位' })).not.toBeInTheDocument()
})
test('新增岗位使用确定的 collection POST 契约', async () => {
const request = createRequest()
render(<PerformanceSurveyAdminConfig mode="positions" request={request} />)
await waitFor(() => expect(screen.getByRole('button', { name: /品牌设计师,6 人,已生效/ })).toBeInTheDocument())
fireEvent.click(screen.getByRole('button', { name: '新增岗位' }))
expect(screen.queryByLabelText('新岗位代码')).not.toBeInTheDocument()
fireEvent.change(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: '新设计岗位', effectiveMonth: '2026-07' }),
})))
})
test('人员批量分配提交最终 REST 请求体', async () => {
const request = createRequest()
render(<PerformanceSurveyAdminConfig mode="people" request={request} />)
await waitFor(() => expect(screen.getAllByText('张三').length).toBeGreaterThan(0))
fireEvent.click(screen.getByLabelText('选择 张三'))
fireEvent.change(screen.getByLabelText('批量分配操作'), { target: { value: 'group-1' } })
fireEvent.change(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(<PerformanceSurveyAdminConfig mode="templates" request={request} />)
await waitFor(() => expect(screen.getByText(/v1 · 已发布/)).toBeInTheDocument())
fireEvent.click(screen.getByRole('button', { name: '修改模板' }))
await waitFor(() => expect(request).toHaveBeenCalledWith('/admin/templates/version-1/clone', expect.objectContaining({ method: 'POST' })))
fireEvent.change(screen.getByLabelText('模板名称'), { target: { value: '新视觉评分' } })
fireEvent.click(screen.getByRole('button', { name: /发布至/ }))
expect(await screen.findByText('请先保存模板草稿,再执行发布')).toBeInTheDocument()
expect(screen.getByRole('button', { name: '删除模板' })).toBeInTheDocument()
expect(screen.queryByText('使用场景')).not.toBeInTheDocument()
})
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(<PerformanceSurveyAdminConfig mode="templates" request={request} />)
const changedTemplate = await screen.findByRole('button', { name: /视觉案例评分.*有待发布修改/ })
expect(within(changedTemplate).getByRole('img', { name: '有待发布修改' })).toHaveAttribute('data-status-dot', 'warning')
const futureTemplate = screen.getByRole('button', { name: /课程分享.*待生效/ })
expect(within(futureTemplate).getByRole('img', { name: '待生效' })).toHaveAttribute('data-status-dot', 'warning')
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(<PerformanceSurveyAdminConfig mode="positions" request={request} />)
const positionButton = await screen.findByRole('button', { name: /品牌设计师.*6 人.*已生效/ })
expect(within(positionButton).getByRole('img', { name: '已生效' })).toHaveAttribute('data-status-dot', 'success')
positions.unmount()
render(<PerformanceSurveyAdminConfig mode="templates" request={request} />)
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(<PerformanceSurveyAdminConfig mode="templates" request={request} />)
await waitFor(() => expect(screen.getByText(/v1 · 已发布/)).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]')
fireEvent.change(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(<PerformanceSurveyAdminConfig mode="templates" request={request} />)
await waitFor(() => expect(screen.getByRole('button', { name: /视觉案例评分,待发布/ })).toBeInTheDocument())
fireEvent.click(screen.getByRole('button', { name: /发布至 2026-07/ }))
const dialog = await screen.findByRole('dialog', { name: '发布模板“视觉案例评分”?' })
expect(dialog).toHaveTextContent('模板将从 2026-07 起成为可用模板')
fireEvent.click(within(dialog).getByRole('button', { name: '发布至 2026-07' }))
await waitFor(() => expect(request).toHaveBeenCalledWith(
'/admin/templates/version-1/publish',
expect.objectContaining({
method: 'POST',
body: JSON.stringify({ effectiveMonth: '2026-07' }),
}),
))
})
test('模板编辑按基本信息、评分项、评分人设置分步展示', async () => {
const request = createRequest()
render(<PerformanceSurveyAdminConfig mode="templates" request={request} />)
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(<PerformanceSurveyAdminConfig mode="templates" request={request} />)
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(reviewerSelect).getByRole('option', { name: '直属主管' })).toBeInTheDocument()
expect(within(reviewerSelect).getByRole('option', { name: '元泽' })).toBeInTheDocument()
expect(within(reviewerSelect).getByRole('option', { name: '任何人(发起时选择)' })).toBeInTheDocument()
fireEvent.change(reviewerSelect, { target: { value: 'fixed_employee' } })
await waitFor(() => expect(screen.getByLabelText('选择元泽人员档案')).toHaveValue('元泽'))
fireEvent.change(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<Array<{ id: string; displayName: string }>>((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(<PerformanceSurveyAdminConfig mode="templates" request={request} />)
await waitFor(() => expect(screen.getByRole('button', { name: '3. 评分人设置' })).toBeInTheDocument())
fireEvent.click(screen.getByRole('button', { name: '3. 评分人设置' }))
fireEvent.change(screen.getByLabelText('阶段 1 评分人类型 1'), { target: { value: 'fixed_employee' } })
fireEvent.change(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(<PerformanceSurveyAdminConfig mode="templates" request={request} />)
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(<PerformanceSurveyAdminConfig mode="templates" request={request} />)
const templateSelect = await screen.findByLabelText('选择模板')
expect(templateSelect).toHaveValue('template-1')
fireEvent.change(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(<PerformanceSurveyAdminConfig mode="templates" request={request} />)
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, status: 'draft', effectiveMonth: null })),
}
: template),
}
const request = createRequest(configuration)
render(<PerformanceSurveyAdminConfig mode="templates" request={request} />)
await waitFor(() => expect(screen.getByLabelText('模板名称')).toHaveValue('视觉案例评分'))
fireEvent.change(screen.getByLabelText('模板名称'), { target: { value: '' } })
expect(screen.getByText('请填写模板名称')).toBeInTheDocument()
expect(screen.getByRole('button', { name: '发布至 2026-07' })).toBeDisabled()
expect(screen.getByText('未保存')).toBeInTheDocument()
fireEvent.change(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(<PerformanceSurveyAdminConfig mode="templates" request={request} />)
fireEvent.click(await screen.findByRole('button', { name: '3. 评分人设置' }))
fireEvent.change(screen.getByLabelText('阶段 1 评分人类型 1'), { target: { value: 'fixed_employee' } })
const picker = await screen.findByRole('combobox', { name: '选择元泽人员档案' })
await waitFor(() => expect(picker).toHaveValue('元泽'))
fireEvent.change(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(<PerformanceSurveyAdminConfig mode="positions" request={request} />)
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({
effectiveMonth: '2026-07',
draftPlanId: 'plan-1',
draftVersion: 2,
draftUpdatedAt: '2026-07-23T10:00:00.000Z',
}),
})))
})
})