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() 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() await waitFor(() => expect(request).toHaveBeenCalledWith('/admin/config?month=2026-07&scope=templates')) view.rerender() 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() expect(await screen.findByText(/499 人待确认/)).toBeInTheDocument() }) test('受控月份切换通过外层更新,保持管理工作区月份一致', async () => { const request = createRequest() const onMonthChange = jest.fn() render( , ) 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() 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() 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() 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() 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() 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() 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() 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() 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() 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() 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() 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() 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() 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() const positionButton = await screen.findByRole('button', { name: /品牌设计师.*6 人.*已生效/ }) expect(within(positionButton).getByRole('img', { name: '已生效' })).toHaveAttribute('data-status-dot', 'success') positions.unmount() render() 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() 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() 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() 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() 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>((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() 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() 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() 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() 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() 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() 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() 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() 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', }), }))) }) })