782 lines
27 KiB
TypeScript
782 lines
27 KiB
TypeScript
/** @jest-environment jsdom */
|
||
|
||
import { fireEvent, render, screen, waitFor, within } from '@testing-library/react'
|
||
import PerformanceSurveyPage from './[[...path]]/page'
|
||
|
||
const mockRouterReplace = jest.fn()
|
||
const mockRouterPush = jest.fn()
|
||
const mockChoosePerformanceSurveyExportDestination = jest.fn()
|
||
const mockSavePerformanceSurveyDashboardWorkbook = jest.fn()
|
||
let mockSearchParams = new URLSearchParams()
|
||
|
||
jest.mock('next/navigation', () => ({
|
||
useParams: () => ({ path: [] }),
|
||
useRouter: () => ({
|
||
push: mockRouterPush,
|
||
replace: mockRouterReplace,
|
||
}),
|
||
useSearchParams: () => mockSearchParams,
|
||
}))
|
||
|
||
jest.mock('next-auth/react', () => ({
|
||
useSession: () => ({
|
||
data: {
|
||
user: {
|
||
id: 'user-admin',
|
||
name: '管理员',
|
||
email: 'admin@example.com',
|
||
},
|
||
expires: '2099-01-01T00:00:00.000Z',
|
||
},
|
||
status: 'authenticated',
|
||
}),
|
||
}))
|
||
|
||
jest.mock('@/lib/performance-survey-dashboard-export', () => ({
|
||
choosePerformanceSurveyExportDestination: (...args: unknown[]) => (
|
||
mockChoosePerformanceSurveyExportDestination(...args)
|
||
),
|
||
sanitizePerformanceSurveyExportFileName: (value: string) => `${value}.xlsx`,
|
||
savePerformanceSurveyDashboardWorkbook: (...args: unknown[]) => (
|
||
mockSavePerformanceSurveyDashboardWorkbook(...args)
|
||
),
|
||
}))
|
||
|
||
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' },
|
||
},
|
||
}
|
||
|
||
const adminConfig = {
|
||
month: '2026-07',
|
||
groups: [{
|
||
id: 'group-1',
|
||
code: 'brand_designer',
|
||
name: '品牌设计师',
|
||
sortOrder: 10,
|
||
isActive: true,
|
||
status: 'ready',
|
||
memberCount: 2,
|
||
rules: [],
|
||
effectivePlan: {
|
||
id: 'plan-1',
|
||
version: 1,
|
||
effectiveMonth: '2026-07',
|
||
templateVersionIds: ['version-1'],
|
||
},
|
||
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,
|
||
}],
|
||
}],
|
||
issues: {
|
||
unmatched: [],
|
||
conflicts: [],
|
||
},
|
||
}
|
||
|
||
function jsonResponse(data: unknown) {
|
||
return {
|
||
ok: true,
|
||
status: 200,
|
||
headers: { get: () => 'application/json' },
|
||
json: async () => ({ success: true, data }),
|
||
text: async () => '',
|
||
} as unknown as Response
|
||
}
|
||
|
||
function jsonErrorResponse(error: string, status = 409) {
|
||
return {
|
||
ok: false,
|
||
status,
|
||
headers: { get: () => 'application/json' },
|
||
json: async () => ({ success: false, error }),
|
||
text: async () => '',
|
||
} as unknown as Response
|
||
}
|
||
|
||
describe('考核调研顶层导航保护', () => {
|
||
const originalFetch = global.fetch
|
||
|
||
beforeEach(() => {
|
||
mockRouterReplace.mockReset()
|
||
mockRouterPush.mockReset()
|
||
mockChoosePerformanceSurveyExportDestination.mockReset()
|
||
mockChoosePerformanceSurveyExportDestination.mockResolvedValue({ kind: 'download' })
|
||
mockSavePerformanceSurveyDashboardWorkbook.mockReset()
|
||
mockSavePerformanceSurveyDashboardWorkbook.mockResolvedValue(undefined)
|
||
mockSearchParams = new URLSearchParams('tab=manage&adminWorkspace=positions&adminMonth=2026-07')
|
||
global.fetch = jest.fn(async (input) => {
|
||
const url = String(input)
|
||
if (url.includes('/admin/config-audit-logs')) return jsonResponse({ items: [] })
|
||
if (url.includes('/admin/config?')) return jsonResponse(adminConfig)
|
||
if (url.endsWith('/me')) {
|
||
return jsonResponse({
|
||
user: { id: 'user-admin', name: '管理员', email: 'admin@example.com' },
|
||
employee: { id: 'employee-admin', displayName: '管理员' },
|
||
permissions: {
|
||
canCreate: true,
|
||
isSystemAdmin: true,
|
||
isDepartmentAdmin: false,
|
||
isLeadership: false,
|
||
},
|
||
})
|
||
}
|
||
if (url.includes('/departments?purpose=evaluator')) return jsonResponse([])
|
||
if (url.endsWith('/surveys')) return jsonResponse([])
|
||
return jsonResponse([])
|
||
}) as jest.Mock
|
||
Object.defineProperty(window, 'matchMedia', {
|
||
configurable: true,
|
||
value: jest.fn().mockReturnValue({ matches: false }),
|
||
})
|
||
})
|
||
|
||
afterEach(() => {
|
||
global.fetch = originalFetch
|
||
jest.restoreAllMocks()
|
||
})
|
||
|
||
test('岗位存在未保存修改时切换个人看板会使用统一弹窗保护', async () => {
|
||
render(<PerformanceSurveyPage />)
|
||
|
||
const nameInput = await screen.findByLabelText('岗位名称')
|
||
const requestedUrls = (global.fetch as jest.Mock).mock.calls.map(([input]) => String(input))
|
||
expect(requestedUrls).toContain(
|
||
'/api/performance-survey/admin/config?month=2026-07&scope=positions',
|
||
)
|
||
expect(requestedUrls.some((url) => url.includes('/departments?purpose=evaluator'))).toBe(false)
|
||
expect(requestedUrls.some((url) => url.endsWith('/surveys'))).toBe(false)
|
||
|
||
fireEvent.change(nameInput, { target: { value: '品牌设计师(修改中)' } })
|
||
expect(await screen.findByText('未保存')).toBeInTheDocument()
|
||
|
||
fireEvent.click(screen.getAllByRole('button', { name: '个人看板' })[0])
|
||
const firstDialog = await screen.findByRole('alertdialog', { name: '放弃尚未保存的修改?' })
|
||
fireEvent.click(within(firstDialog).getByRole('button', { name: '取消' }))
|
||
await waitFor(() => expect(screen.queryByRole('alertdialog', { name: '放弃尚未保存的修改?' })).not.toBeInTheDocument())
|
||
expect(mockRouterReplace).not.toHaveBeenCalledWith(
|
||
expect.stringContaining('tab=personal'),
|
||
expect.anything(),
|
||
)
|
||
|
||
fireEvent.click(screen.getAllByRole('button', { name: '个人看板' })[0])
|
||
const secondDialog = await screen.findByRole('alertdialog', { name: '放弃尚未保存的修改?' })
|
||
fireEvent.click(within(secondDialog).getByRole('button', { name: '放弃并离开' }))
|
||
|
||
await waitFor(() => expect(mockRouterReplace).toHaveBeenCalledWith(
|
||
'/performance-survey?tab=personal&adminWorkspace=positions',
|
||
{ scroll: false },
|
||
))
|
||
})
|
||
|
||
test('发起调研按当前被考核人的人员分配自动关联岗位', async () => {
|
||
mockSearchParams = new URLSearchParams('tab=manage&adminWorkspace=launch&adminMonth=2026-07')
|
||
global.fetch = jest.fn(async (input) => {
|
||
const url = String(input)
|
||
if (url.endsWith('/me')) {
|
||
return jsonResponse({
|
||
user: { id: 'user-admin', name: '管理员', email: 'admin@example.com' },
|
||
employee: {
|
||
id: 'employee-admin',
|
||
displayName: '管理员',
|
||
departmentName: '品牌创意部',
|
||
positionLevel: 'M1',
|
||
},
|
||
permissions: {
|
||
canCreate: true,
|
||
isSystemAdmin: true,
|
||
isDepartmentAdmin: true,
|
||
isLeadership: true,
|
||
},
|
||
})
|
||
}
|
||
if (url.includes('/position-groups?month=2026-07')) {
|
||
return jsonResponse({
|
||
month: '2026-07',
|
||
groups: [{
|
||
id: 'group-1',
|
||
code: 'brand_designer',
|
||
name: '品牌设计师',
|
||
status: 'ready',
|
||
templateCount: 1,
|
||
}],
|
||
membership: {
|
||
status: 'matched',
|
||
groupCode: 'brand_designer',
|
||
groupName: '品牌设计师',
|
||
conflictGroupNames: [],
|
||
},
|
||
})
|
||
}
|
||
if (url.includes('/templates?month=2026-07&positionGroupCode=brand_designer')) {
|
||
return jsonResponse([{
|
||
...templateDefinition,
|
||
name: '品牌调性视觉评分(综合主管)',
|
||
shortName: '视觉评分',
|
||
templateVersionId: 'version-1',
|
||
templateOptionKey: 'visual_review:version-1',
|
||
templateVariantName: '视觉评分(综合主管)',
|
||
}])
|
||
}
|
||
if (url.includes('/departments?purpose=evaluator')) return jsonResponse([])
|
||
if (url.endsWith('/surveys')) return jsonResponse([])
|
||
return jsonResponse([])
|
||
}) as jest.Mock
|
||
|
||
render(<PerformanceSurveyPage />)
|
||
|
||
expect(await screen.findByText('品牌设计师')).toBeInTheDocument()
|
||
expect(screen.queryByRole('combobox', { name: '考核岗位' })).not.toBeInTheDocument()
|
||
expect(screen.getByText('已按被考核人的人员分配自动关联,发起时无需选择。')).toBeInTheDocument()
|
||
expect(await screen.findByRole('option', { name: '品牌调性视觉评分(综合主管)' })).toBeInTheDocument()
|
||
expect(screen.queryByRole('option', { name: '视觉评分(综合主管)' })).not.toBeInTheDocument()
|
||
await waitFor(() => expect(global.fetch).toHaveBeenCalledWith(
|
||
'/api/performance-survey/templates?month=2026-07&positionGroupCode=brand_designer',
|
||
expect.anything(),
|
||
))
|
||
})
|
||
|
||
test('公共看板可选择全部岗位并请求跨岗位数据', async () => {
|
||
mockSearchParams = new URLSearchParams('tab=public')
|
||
const positionGroups = [
|
||
{
|
||
id: 'group-brand',
|
||
code: 'brand_designer',
|
||
name: '品牌设计主管',
|
||
status: 'ready',
|
||
templateCount: 1,
|
||
},
|
||
{
|
||
id: 'group-general',
|
||
code: 'ai_planning_general',
|
||
name: 'AI策划-综合部',
|
||
status: 'ready',
|
||
templateCount: 1,
|
||
},
|
||
]
|
||
global.fetch = jest.fn(async (input) => {
|
||
const url = String(input)
|
||
if (url.endsWith('/me')) {
|
||
return jsonResponse({
|
||
user: { id: 'user-admin', name: '管理员', email: 'admin@example.com' },
|
||
employee: {
|
||
id: 'employee-admin',
|
||
displayName: '管理员',
|
||
departmentName: '品牌设计部',
|
||
positionLevel: 'M1',
|
||
},
|
||
permissions: {
|
||
canCreate: true,
|
||
isSystemAdmin: true,
|
||
isDepartmentAdmin: true,
|
||
isLeadership: true,
|
||
},
|
||
})
|
||
}
|
||
if (url.includes('/departments?purpose=dashboard')) return jsonResponse([])
|
||
if (url.includes('/dashboard?')) {
|
||
const allSelected = url.includes('positionGroupCode=all')
|
||
return jsonResponse({
|
||
month: '2026-07',
|
||
filters: {
|
||
view: 'public',
|
||
month: '2026-07',
|
||
positionGroupCode: allSelected ? 'all' : 'brand_designer',
|
||
itemPage: 1,
|
||
},
|
||
permissions: { canOpenPublicDetail: true },
|
||
positionGroups,
|
||
positionMembership: {
|
||
status: 'matched',
|
||
groupCode: 'brand_designer',
|
||
groupName: '品牌设计主管',
|
||
conflictGroupNames: [],
|
||
},
|
||
selectedPositionGroup: allSelected ? null : positionGroups[0],
|
||
templatePager: {
|
||
page: 1,
|
||
totalPages: 1,
|
||
totalItems: allSelected ? 2 : 1,
|
||
start: 1,
|
||
end: allSelected ? 2 : 1,
|
||
},
|
||
templates: allSelected
|
||
? [
|
||
{ code: 'brand_review', name: '品牌调性视觉评分', maxTotal: 120 },
|
||
{ code: 'general_review', name: '品牌调性视觉评分(无文案)', maxTotal: 120 },
|
||
]
|
||
: [{ code: 'brand_review', name: '品牌调性视觉评分', maxTotal: 120 }],
|
||
personal: { rows: [] },
|
||
public: { rows: [] },
|
||
})
|
||
}
|
||
return jsonResponse([])
|
||
}) as jest.Mock
|
||
|
||
render(<PerformanceSurveyPage />)
|
||
|
||
const positionSelect = await screen.findByRole('combobox', { name: '考核岗位' })
|
||
expect(within(positionSelect).getByRole('option', { name: '全部岗位' })).toBeInTheDocument()
|
||
|
||
fireEvent.change(positionSelect, { target: { value: 'all' } })
|
||
|
||
expect(await screen.findByText('2 个已生效岗位 · 2 个考核项')).toBeInTheDocument()
|
||
await waitFor(() => expect(
|
||
(global.fetch as jest.Mock).mock.calls
|
||
.map(([input]) => String(input))
|
||
.some((url) => url.includes('/dashboard?') && url.includes('positionGroupCode=all')),
|
||
).toBe(true))
|
||
expect(
|
||
(global.fetch as jest.Mock).mock.calls
|
||
.map(([input]) => String(input))
|
||
.some((url) => url.includes('/departments?purpose=dashboard') && url.includes('positionGroupCode=all')),
|
||
).toBe(false)
|
||
})
|
||
|
||
test('公共看板按当前岗位和月份选择保存位置并导出全部考核项', async () => {
|
||
mockSearchParams = new URLSearchParams('tab=public&month=2026-07')
|
||
const dashboardData = {
|
||
month: '2026-07',
|
||
filters: {
|
||
view: 'public',
|
||
month: '2026-07',
|
||
positionGroupCode: 'brand_creative_manager',
|
||
itemPage: 1,
|
||
},
|
||
permissions: { canOpenPublicDetail: true },
|
||
positionGroups: [{
|
||
id: 'group-creative-manager',
|
||
code: 'brand_creative_manager',
|
||
name: '品牌创意主管',
|
||
status: 'ready',
|
||
templateCount: 4,
|
||
}],
|
||
positionMembership: {
|
||
status: 'matched',
|
||
groupCode: 'brand_creative_manager',
|
||
groupName: '品牌创意主管',
|
||
conflictGroupNames: [],
|
||
},
|
||
selectedPositionGroup: {
|
||
id: 'group-creative-manager',
|
||
code: 'brand_creative_manager',
|
||
name: '品牌创意主管',
|
||
status: 'ready',
|
||
},
|
||
templatePager: {
|
||
page: 1,
|
||
totalPages: 1,
|
||
totalItems: 4,
|
||
start: 1,
|
||
end: 4,
|
||
},
|
||
templates: [{ code: 'coordination', name: '跨部门协同', maxTotal: 120 }],
|
||
personal: { rows: [] },
|
||
public: {
|
||
rows: [{
|
||
month: '2026-07',
|
||
employee: {
|
||
id: 'employee-boyan',
|
||
displayName: '伯言',
|
||
jobNumber: 'JS15040602',
|
||
departmentName: '品牌创意A部',
|
||
positionLevel: 'M2',
|
||
},
|
||
cells: {
|
||
coordination: {
|
||
score: 114.5,
|
||
surveyCount: 1,
|
||
positionGroupCode: 'brand_creative_manager',
|
||
},
|
||
},
|
||
}],
|
||
},
|
||
}
|
||
|
||
global.fetch = jest.fn(async (input) => {
|
||
const url = String(input)
|
||
if (url.endsWith('/me')) {
|
||
return jsonResponse({
|
||
user: { id: 'user-admin', name: '管理员', email: 'admin@example.com' },
|
||
employee: { id: 'employee-admin', displayName: '管理员' },
|
||
permissions: { canCreate: true, isSystemAdmin: true, isDepartmentAdmin: true, isLeadership: true },
|
||
})
|
||
}
|
||
if (url.includes('/departments?purpose=dashboard')) return jsonResponse([])
|
||
if (url.includes('/dashboard/export?')) return jsonResponse(dashboardData)
|
||
if (url.includes('/dashboard?')) return jsonResponse(dashboardData)
|
||
return jsonResponse([])
|
||
}) as jest.Mock
|
||
|
||
render(<PerformanceSurveyPage />)
|
||
|
||
fireEvent.click(await screen.findByRole('button', { name: '导出' }))
|
||
const dialog = await screen.findByRole('dialog', { name: '导出公共看板' })
|
||
expect(within(dialog).getByText('品牌创意主管')).toBeInTheDocument()
|
||
expect(within(dialog).getByText('2026年7月')).toBeInTheDocument()
|
||
expect(within(dialog).getByText('1 人')).toBeInTheDocument()
|
||
expect(within(dialog).getByText('4 项')).toBeInTheDocument()
|
||
|
||
fireEvent.click(within(dialog).getByRole('button', { name: '选择保存位置并导出' }))
|
||
|
||
await waitFor(() => expect(mockChoosePerformanceSurveyExportDestination).toHaveBeenCalledWith(
|
||
'考核调研_品牌创意主管_2026-07.xlsx',
|
||
))
|
||
await waitFor(() => expect(global.fetch).toHaveBeenCalledWith(
|
||
'/api/performance-survey/dashboard/export?month=2026-07&positionGroupCode=brand_creative_manager',
|
||
expect.anything(),
|
||
))
|
||
await waitFor(() => expect(mockSavePerformanceSurveyDashboardWorkbook).toHaveBeenCalledWith(
|
||
dashboardData,
|
||
'考核调研_品牌创意主管_2026-07.xlsx',
|
||
{ kind: 'download' },
|
||
))
|
||
expect(await screen.findByRole('status')).toHaveTextContent('请在浏览器下载记录中查看')
|
||
})
|
||
|
||
test('我发起的草稿发布失败时展示发布进度和后端原因', async () => {
|
||
mockSearchParams = new URLSearchParams('tab=personal&workspace=created')
|
||
let resolvePublish: (response: Response) => void = () => {}
|
||
const publishResponse = new Promise<Response>((resolve) => {
|
||
resolvePublish = resolve
|
||
})
|
||
global.fetch = jest.fn(async (input, init) => {
|
||
const url = String(input)
|
||
if (url.endsWith('/me')) {
|
||
return jsonResponse({
|
||
user: { id: 'user-admin', name: '管理员', email: 'admin@example.com' },
|
||
employee: { id: 'employee-admin', displayName: '管理员' },
|
||
permissions: {
|
||
canCreate: true,
|
||
isSystemAdmin: true,
|
||
isDepartmentAdmin: true,
|
||
isLeadership: true,
|
||
},
|
||
})
|
||
}
|
||
if (url.endsWith('/surveys') && (!init?.method || init.method === 'GET')) {
|
||
return jsonResponse({
|
||
summary: {
|
||
pendingCount: 0,
|
||
submittedCount: 0,
|
||
createdCount: 1,
|
||
openCreatedCount: 0,
|
||
},
|
||
pending: [],
|
||
submitted: [],
|
||
created: [{
|
||
id: 'survey-draft',
|
||
title: '2026年7月 · AI作品评审',
|
||
status: 'draft',
|
||
departmentName: '综合支持部',
|
||
dueAt: null,
|
||
counts: { targets: 1, assignments: 3, responses: 0 },
|
||
}],
|
||
})
|
||
}
|
||
if (url.endsWith('/surveys/survey-draft') && init?.method === 'PATCH') {
|
||
return publishResponse
|
||
}
|
||
return jsonResponse([])
|
||
}) as jest.Mock
|
||
|
||
render(<PerformanceSurveyPage />)
|
||
|
||
const publishButtons = await screen.findAllByRole('button', { name: '发布调研:2026年7月 · AI作品评审' })
|
||
expect(screen.getAllByRole('link', { name: '继续编辑草稿:2026年7月 · AI作品评审' })[0]).toHaveAttribute(
|
||
'href',
|
||
'/performance-survey?tab=manage&adminWorkspace=launch&draftId=survey-draft',
|
||
)
|
||
fireEvent.click(publishButtons[0])
|
||
|
||
expect(await screen.findAllByText('发布中...')).not.toHaveLength(0)
|
||
resolvePublish(jsonErrorResponse('视觉评分作品尚未上传完整:无邪(0/3)'))
|
||
|
||
expect(await screen.findAllByRole('alert')).not.toHaveLength(0)
|
||
expect(await screen.findAllByText('发布失败:视觉评分作品尚未上传完整:无邪(0/3)')).not.toHaveLength(0)
|
||
expect(screen.getAllByRole('link', { name: '继续上传作品' })[0]).toHaveAttribute(
|
||
'href',
|
||
'/performance-survey/survey/survey-draft',
|
||
)
|
||
expect(screen.getAllByRole('button', { name: '发布调研:2026年7月 · AI作品评审' })[0]).toBeEnabled()
|
||
})
|
||
|
||
test('继续编辑草稿时恢复原模板和已上传作品', async () => {
|
||
mockSearchParams = new URLSearchParams('tab=manage&adminWorkspace=launch&adminMonth=2026-07&draftId=survey-draft')
|
||
global.fetch = jest.fn(async (input) => {
|
||
const url = String(input)
|
||
if (url.endsWith('/me')) {
|
||
return jsonResponse({
|
||
user: { id: 'user-admin', name: '管理员', email: 'admin@example.com' },
|
||
employee: {
|
||
id: 'employee-admin',
|
||
displayName: '管理员',
|
||
departmentName: '品牌创意部',
|
||
positionLevel: 'M1',
|
||
},
|
||
permissions: {
|
||
canCreate: true,
|
||
isSystemAdmin: true,
|
||
isDepartmentAdmin: true,
|
||
isLeadership: true,
|
||
},
|
||
})
|
||
}
|
||
if (url.endsWith('/surveys/survey-draft')) {
|
||
return jsonResponse({
|
||
id: 'survey-draft',
|
||
title: '2026年7月 · AI作品评审',
|
||
description: '草稿说明',
|
||
templateCode: 'visual_review',
|
||
template: {
|
||
...templateDefinition,
|
||
name: 'AI作品评审',
|
||
shortName: 'AI作品评审',
|
||
requiredWorkItemCount: 3,
|
||
},
|
||
status: 'draft',
|
||
canManage: true,
|
||
assessmentMonth: '2026-07',
|
||
dueAt: null,
|
||
targets: [{
|
||
id: 'target-1',
|
||
targetType: 'employee',
|
||
targetName: '管理员',
|
||
targetSnapshot: {
|
||
id: 'employee-admin',
|
||
displayName: '管理员',
|
||
departmentName: '品牌创意部',
|
||
positionLevel: 'M1',
|
||
},
|
||
canCreateWorkItem: true,
|
||
canUploadWorkItems: true,
|
||
workItemUploadRemaining: 2,
|
||
workItems: [{
|
||
id: 'work-item-1',
|
||
title: '已上传作品',
|
||
description: '第一版',
|
||
imageUrls: ['https://s3.jiansun.vip/example.webp'],
|
||
}],
|
||
assignments: [{
|
||
id: 'assignment-1',
|
||
evaluatorEmployeeId: 'evaluator-1',
|
||
evaluatorName: '元泽',
|
||
evaluatorSnapshot: { id: 'evaluator-1', displayName: '元泽' },
|
||
reviewerSource: 'fixed_employee',
|
||
status: 'pending',
|
||
}],
|
||
}],
|
||
})
|
||
}
|
||
if (url.includes('/position-groups?month=2026-07')) {
|
||
return jsonResponse({
|
||
month: '2026-07',
|
||
groups: [{
|
||
id: 'group-1',
|
||
code: 'brand_designer',
|
||
name: '品牌设计师',
|
||
status: 'ready',
|
||
templateCount: 1,
|
||
}],
|
||
membership: {
|
||
status: 'matched',
|
||
groupCode: 'brand_designer',
|
||
groupName: '品牌设计师',
|
||
conflictGroupNames: [],
|
||
},
|
||
})
|
||
}
|
||
if (url.includes('/templates?month=2026-07&positionGroupCode=brand_designer')) {
|
||
return jsonResponse([{
|
||
...templateDefinition,
|
||
name: 'AI作品评审',
|
||
shortName: 'AI作品评审',
|
||
templateVersionId: 'version-1',
|
||
templateOptionKey: 'visual_review:version-1',
|
||
requiredWorkItemCount: 3,
|
||
}])
|
||
}
|
||
if (url.includes('/departments?purpose=evaluator')) return jsonResponse([])
|
||
if (url.endsWith('/surveys')) {
|
||
return jsonResponse({
|
||
summary: {},
|
||
pending: [],
|
||
submitted: [],
|
||
created: [],
|
||
})
|
||
}
|
||
return jsonResponse([])
|
||
}) as jest.Mock
|
||
|
||
render(<PerformanceSurveyPage />)
|
||
|
||
expect(await screen.findByText('继续编辑未发布草稿')).toBeInTheDocument()
|
||
expect(screen.getByText('已恢复 1/3 个作品;补齐内容后可直接发布。被考核人与评分流程沿用原草稿。')).toBeInTheDocument()
|
||
expect(screen.getByText('已上传作品')).toBeInTheDocument()
|
||
expect(screen.getByLabelText('考评模板')).toBeDisabled()
|
||
expect(screen.getByLabelText('案例 2 名称')).toBeInTheDocument()
|
||
expect(screen.getByLabelText('案例 3 名称')).toBeInTheDocument()
|
||
expect(screen.getByRole('button', { name: '上传剩余作品并发布' })).toBeDisabled()
|
||
})
|
||
|
||
test('公共看板仅按后端权限在分数下显示删除按钮并彻底删除单项调研', async () => {
|
||
mockSearchParams = new URLSearchParams('tab=public&workspace=pending&month=2026-07')
|
||
let dashboardRequestCount = 0
|
||
const deleteRequests: Array<{ url: string; init?: RequestInit }> = []
|
||
global.fetch = jest.fn(async (input, init) => {
|
||
const url = String(input)
|
||
if (url.endsWith('/me')) {
|
||
return jsonResponse({
|
||
user: { id: 'user-wuxie', name: '无邪', email: 'wuxie@example.com' },
|
||
employee: { id: 'employee-wuxie', displayName: '无邪' },
|
||
permissions: {
|
||
canCreate: true,
|
||
isSystemAdmin: true,
|
||
isDepartmentAdmin: true,
|
||
isLeadership: true,
|
||
},
|
||
})
|
||
}
|
||
if (url.includes('/departments?purpose=dashboard')) return jsonResponse([])
|
||
if (url.includes('/dashboard/public/assessments/employee-ruolan') && init?.method === 'DELETE') {
|
||
deleteRequests.push({ url, init })
|
||
return jsonResponse({
|
||
employeeProfileId: 'employee-ruolan',
|
||
employeeName: '若兰',
|
||
templateCode: 'work_coordination',
|
||
deletedTargetCount: 1,
|
||
})
|
||
}
|
||
if (url.includes('/dashboard?')) {
|
||
dashboardRequestCount += 1
|
||
return jsonResponse({
|
||
month: '2026-07',
|
||
filters: {
|
||
view: 'public',
|
||
month: '2026-07',
|
||
positionGroupCode: 'ai_planning_creative',
|
||
itemPage: 1,
|
||
},
|
||
permissions: {
|
||
canOpenPublicDetail: true,
|
||
canDeletePublicDashboardAssessment: true,
|
||
},
|
||
positionGroups: [{
|
||
id: 'group-creative',
|
||
code: 'ai_planning_creative',
|
||
name: 'AI策划-创意部',
|
||
status: 'ready',
|
||
templateCount: 1,
|
||
}],
|
||
positionMembership: {
|
||
status: 'matched',
|
||
groupCode: 'ai_planning_creative',
|
||
groupName: 'AI策划-创意部',
|
||
conflictGroupNames: [],
|
||
},
|
||
selectedPositionGroup: {
|
||
id: 'group-creative',
|
||
code: 'ai_planning_creative',
|
||
name: 'AI策划-创意部',
|
||
status: 'ready',
|
||
},
|
||
templatePager: {
|
||
page: 1,
|
||
totalPages: 1,
|
||
totalItems: 1,
|
||
start: 1,
|
||
end: 1,
|
||
},
|
||
templates: [{
|
||
code: 'work_coordination',
|
||
name: '工作配合度',
|
||
maxTotal: 120,
|
||
}],
|
||
personal: { rows: [] },
|
||
public: {
|
||
rows: dashboardRequestCount > 1 ? [] : [{
|
||
month: '2026-07',
|
||
employee: {
|
||
id: 'employee-ruolan',
|
||
displayName: '若兰',
|
||
jobNumber: 'JS18102201',
|
||
departmentName: '品牌创意部',
|
||
positionLevel: 'M0',
|
||
},
|
||
cells: {
|
||
work_coordination: {
|
||
surveyCount: 1,
|
||
score: 113,
|
||
maxTotal: 120,
|
||
positionGroupCode: 'ai_planning_creative',
|
||
},
|
||
},
|
||
}],
|
||
},
|
||
})
|
||
}
|
||
return jsonResponse([])
|
||
}) as jest.Mock
|
||
|
||
render(<PerformanceSurveyPage />)
|
||
|
||
const removeButtons = await screen.findAllByRole('button', { name: '删除若兰的工作配合度调研' })
|
||
expect(removeButtons).toHaveLength(2)
|
||
fireEvent.click(removeButtons[0])
|
||
|
||
expect(await screen.findByRole('alertdialog')).toHaveTextContent('彻底删除“若兰”的工作配合度?')
|
||
expect(screen.getByRole('alertdialog')).toHaveTextContent('删除后不可恢复,但可以重新发起该月该项调研')
|
||
const confirmButton = screen.getByRole('button', { name: '确认彻底删除' })
|
||
expect(confirmButton).toBeDisabled()
|
||
fireEvent.change(screen.getByRole('textbox', { name: /删除原因/ }), {
|
||
target: { value: '岗位归属错误' },
|
||
})
|
||
expect(confirmButton).toBeEnabled()
|
||
fireEvent.click(confirmButton)
|
||
|
||
expect(await screen.findByRole('status')).toHaveTextContent(
|
||
'已彻底删除若兰在2026年7月的“工作配合度”调研,现在可以重新发起。',
|
||
)
|
||
expect(deleteRequests).toHaveLength(1)
|
||
expect(JSON.parse(String(deleteRequests[0].init?.body))).toEqual({
|
||
month: '2026-07',
|
||
positionGroupCode: 'ai_planning_creative',
|
||
templateCode: 'work_coordination',
|
||
reason: '岗位归属错误',
|
||
})
|
||
expect(dashboardRequestCount).toBeGreaterThan(1)
|
||
})
|
||
})
|