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

263 lines
9.5 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()
render(<PerformanceSurveyAdminConfig mode="overview" request={request} />)
expect(screen.getByText('配置加载中...')).toBeInTheDocument()
await waitFor(() => expect(screen.getByRole('heading', { name: '2026-07 配置概览' })).toBeInTheDocument())
expect(screen.getByRole('rowheader', { name: '岗位' })).toBeInTheDocument()
expect(screen.getByText('未匹配 1 人')).toBeInTheDocument()
expect(request).toHaveBeenCalledWith(expect.stringContaining('/admin/config-audit-logs'))
})
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.getByText('张三')).toBeInTheDocument())
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="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',
}),
})))
})
})