916 lines
40 KiB
TypeScript
916 lines
40 KiB
TypeScript
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(<PerformanceSurveyAdminConfig mode="overview" request={request} onNavigate={onNavigate} month="2026-07" />)
|
||
|
||
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(<PerformanceSurveyAdminConfig mode="templates" request={request} month="2026-07" />)
|
||
|
||
await waitFor(() => expect(request).toHaveBeenCalledWith('/admin/config?month=2026-07&scope=templates'))
|
||
|
||
view.rerender(<PerformanceSurveyAdminConfig mode="people" request={request} month="2026-07" />)
|
||
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(<PerformanceSurveyAdminConfig mode="overview" request={request} />)
|
||
|
||
expect(await screen.findByText(/499 人待确认/)).toBeInTheDocument()
|
||
})
|
||
|
||
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 个岗位设置已生效/)).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(<PerformanceSurveyAdminConfig mode={mode} request={request} month="2026-07" />)
|
||
|
||
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(<PerformanceSurveyAdminConfig mode="positions" request={request} onDirtyChange={onDirtyChange} />)
|
||
|
||
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(<PerformanceSurveyAdminConfig mode="positions" request={request} />)
|
||
|
||
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(<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()
|
||
|
||
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(<PerformanceSurveyAdminConfig mode="positions" request={request} />)
|
||
|
||
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(<PerformanceSurveyAdminConfig mode="positions" request={request} />)
|
||
|
||
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(<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()
|
||
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(<PerformanceSurveyAdminConfig mode="positions" request={request} />)
|
||
|
||
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(<PerformanceSurveyAdminConfig mode="people" request={request} month="2026-07" />)
|
||
|
||
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(<PerformanceSurveyAdminConfig mode="templates" request={request} />)
|
||
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(<PerformanceSurveyAdminConfig mode="templates" request={request} />)
|
||
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(<PerformanceSurveyAdminConfig mode="templates" request={request} />)
|
||
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(<PerformanceSurveyAdminConfig mode="templates" request={request} />)
|
||
|
||
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(<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.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(<PerformanceSurveyAdminConfig mode="templates" request={request} />)
|
||
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(<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(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<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. 评分人设置' }))
|
||
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(<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')
|
||
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(<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,
|
||
definition: {
|
||
...version.definition,
|
||
questions: version.definition.questions.map(({ maxScore: _maxScore, ...question }: any) => question),
|
||
},
|
||
})),
|
||
}
|
||
: 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 分.*完整/ })).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(<PerformanceSurveyAdminConfig mode="templates" request={request} />)
|
||
|
||
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(<PerformanceSurveyAdminConfig mode="templates" request={request} />)
|
||
|
||
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(<PerformanceSurveyAdminConfig mode="positions" request={request} month="2026-07" />)
|
||
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',
|
||
}),
|
||
})))
|
||
})
|
||
})
|