298 lines
11 KiB
TypeScript
298 lines
11 KiB
TypeScript
/** @jest-environment jsdom */
|
|
|
|
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
|
|
import { PerformanceSurveyAdminConfig } from './PerformanceSurveyAdminConfig'
|
|
|
|
const templateDefinition = {
|
|
code: 'visual_review',
|
|
name: '视觉案例评分',
|
|
shortName: '视觉评分',
|
|
description: '视觉案例评分',
|
|
surveyType: 'visual_case',
|
|
weight: 1,
|
|
maxTotal: 120,
|
|
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() {
|
|
return jest.fn(async (path: string, options?: RequestInit) => {
|
|
if (path.startsWith('/admin/config?')) return config
|
|
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.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.endsWith('/clone')) return { id: 'draft-version-2' }
|
|
return {}
|
|
})
|
|
}
|
|
|
|
describe('PerformanceSurveyAdminConfig', () => {
|
|
beforeEach(() => {
|
|
jest.spyOn(window, 'confirm').mockReturnValue(true)
|
|
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())
|
|
fireEvent.change(screen.getByLabelText('查看月份'), { target: { value: '2026-09' } })
|
|
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)
|
|
|
|
;(window.confirm as jest.Mock).mockReturnValueOnce(false)
|
|
fireEvent.click(screen.getByRole('button', { name: /AI 技术设计师/ }))
|
|
expect(screen.getByLabelText('岗位名称')).toHaveValue('品牌视觉设计师')
|
|
|
|
;(window.confirm as jest.Mock).mockReturnValueOnce(true)
|
|
fireEvent.click(screen.getByRole('button', { name: /AI 技术设计师/ }))
|
|
await waitFor(() => expect(screen.getByLabelText('岗位名称')).toHaveValue('AI 技术设计师'))
|
|
})
|
|
|
|
test('新增岗位使用确定的 collection POST 契约', async () => {
|
|
const request = createRequest()
|
|
render(<PerformanceSurveyAdminConfig mode="positions" request={request} />)
|
|
await waitFor(() => expect(screen.getByText('品牌设计师')).toBeInTheDocument())
|
|
|
|
fireEvent.click(screen.getByRole('button', { name: '新增岗位' }))
|
|
fireEvent.change(screen.getByLabelText('新岗位代码'), { target: { value: 'new_designer' } })
|
|
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({ code: 'new_designer', 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('模板发布前要求先保存 dirty 草稿,并提供复制与归档入口', 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()
|
|
})
|
|
|
|
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()
|
|
fireEvent.click(screen.getByRole('button', { name: '2. 评分项' }))
|
|
expect(screen.getByRole('heading', { name: '评分项' })).toBeInTheDocument()
|
|
expect(screen.getByLabelText('评分项名称')).toHaveValue('作品质量')
|
|
|
|
fireEvent.click(screen.getByRole('button', { name: '3. 评审与计分' }))
|
|
expect(screen.getByRole('heading', { name: '评审与计分' })).toBeInTheDocument()
|
|
expect(screen.getByLabelText('最终成绩规则')).toBeInTheDocument()
|
|
})
|
|
|
|
test('月度方案可添加排序模板并经过影响确认后发布', async () => {
|
|
const request = createRequest()
|
|
render(<PerformanceSurveyAdminConfig mode="plans" request={request} />)
|
|
await waitFor(() => expect(screen.getByRole('heading', { name: '2026-07 月度方案' })).toBeInTheDocument())
|
|
|
|
const courseLabel = screen.getByText('课程分享').closest('label')
|
|
expect(courseLabel).not.toBeNull()
|
|
fireEvent.click(courseLabel!.querySelector('input')!)
|
|
fireEvent.click(screen.getByRole('button', { name: '发布前检查' }))
|
|
|
|
await waitFor(() => expect(screen.getByRole('region', { name: '发布影响确认' })).toBeInTheDocument())
|
|
fireEvent.click(screen.getByRole('button', { name: '确认发布至 2026-07' }))
|
|
|
|
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',
|
|
}),
|
|
})))
|
|
})
|
|
})
|