gongxiangp2/lib/__tests__/performance-survey-templates.test.ts

282 lines
11 KiB
TypeScript

/** @jest-environment node */
import {
DEFAULT_PERFORMANCE_SURVEY_TEMPLATES,
calculateAggregateScore,
calculateResponseScores,
getPerformanceSurveyQuestionScoreDetails,
getPerformanceSurveyTemplateVariant,
listPerformanceSurveyTemplateOptions,
} from '@/lib/performance-survey-templates'
describe('performance survey templates', () => {
test('不含文案的视觉评分为四项每项 10 分', () => {
const template = DEFAULT_PERFORMANCE_SURVEY_TEMPLATES.find((item) => item.code === 'visual_brand_tone')!
const customized = getPerformanceSurveyTemplateVariant(template, 'without_copy')
expect(customized.questions).toHaveLength(4)
expect(customized.questions.map((question) => question.id)).not.toContain('copyExpression')
expect(customized.questions.every((question) => question.maxScore === 10)).toBe(true)
expect(customized.questions.reduce((sum, question) => sum + Number(question.maxScore), 0)).toBe(40)
expect(customized.maxTotal).toBe(120)
expect(calculateResponseScores(
customized,
Object.fromEntries(customized.questions.map((question) => [question.id, 10])),
).totalScore).toBe(40)
expect(template.questions).toHaveLength(5)
expect(template.questions.find((question) => question.id === 'copyExpression')?.maxScore).toBe(8)
})
test('含文案的视觉评分为五项每项 8 分', () => {
const template = DEFAULT_PERFORMANCE_SURVEY_TEMPLATES.find((item) => item.code === 'visual_brand_tone')!
const unchanged = getPerformanceSurveyTemplateVariant(template, 'with_copy')
expect(unchanged.questions).toHaveLength(5)
expect(unchanged.questions.find((question) => question.id === 'copyExpression')).toBeDefined()
expect(unchanged.questions.every((question) => question.maxScore === 8)).toBe(true)
expect(unchanged.questions.reduce((sum, question) => sum + Number(question.maxScore), 0)).toBe(40)
})
test('创建选项将视觉评分展开为两个版本但保持同一业务编号', () => {
const options = listPerformanceSurveyTemplateOptions(DEFAULT_PERFORMANCE_SURVEY_TEMPLATES)
const visualOptions = options.filter((item) => item.code === 'visual_brand_tone')
expect(visualOptions).toHaveLength(2)
expect(visualOptions.map((item) => item.templateOptionKey)).toEqual([
'visual_brand_tone:with_copy',
'visual_brand_tone:without_copy',
])
expect(DEFAULT_PERFORMANCE_SURVEY_TEMPLATES.filter((item) => item.code === 'visual_brand_tone')).toHaveLength(1)
})
test('视觉评分未传版本时默认使用含文案版,保持旧链接可用', () => {
const template = DEFAULT_PERFORMANCE_SURVEY_TEMPLATES.find((item) => item.code === 'visual_brand_tone')!
expect(getPerformanceSurveyTemplateVariant(template).templateVariant).toBe('with_copy')
})
test('拒绝未知视觉版本以及非视觉模板携带版本', () => {
const visualTemplate = DEFAULT_PERFORMANCE_SURVEY_TEMPLATES.find((item) => item.code === 'visual_brand_tone')!
const courseTemplate = DEFAULT_PERFORMANCE_SURVEY_TEMPLATES.find((item) => item.code === 'course_share')!
expect(() => getPerformanceSurveyTemplateVariant(visualTemplate, 'unknown')).toThrow('视觉评分模板版本无效')
expect(() => getPerformanceSurveyTemplateVariant(courseTemplate, 'with_copy')).toThrow('非视觉评分模板不支持模板版本')
})
test('课程分享直接保留用户提交的 100 分,不做权重或 KPI 折算', () => {
const template = DEFAULT_PERFORMANCE_SURVEY_TEMPLATES.find((item) => item.code === 'course_share')!
const scores = Object.fromEntries(template.questions.map((question) => [question.id, 20]))
const result = calculateResponseScores(template, scores)
expect(template.questions).toHaveLength(5)
expect(template.maxTotal).toBe(100)
expect(template.weight).toBe(0.3)
expect(template.aggregationStrategy).toBe('trim_extremes')
expect(template.itemName).toBe('课程')
expect(template.respondentNameFallback).toBe('匿名学员')
expect(template.belowBand).toBe('满意度不足 77%,此项考核为 0')
expect(result.totalScore).toBe(100)
expect(result.achievementScore).toBe(100)
expect(result.kpiScore).toBe(100)
expect(result.weightedScore).toBe(100)
})
test('人员月度模板直接保留用户提交的 120 分', () => {
const template = DEFAULT_PERFORMANCE_SURVEY_TEMPLATES.find((item) => item.code === 'cross_department')!
const scores = Object.fromEntries(template.questions.map((question) => [question.id, 20]))
const result = calculateResponseScores(template, scores)
expect(template.questions).toHaveLength(6)
expect(template.maxTotal).toBe(120)
expect(template.weight).toBe(0.2)
expect(result.totalScore).toBe(120)
expect(result.kpiScore).toBe(120)
expect(result.weightedScore).toBe(120)
})
test('question score details keep every item title, score and maximum', () => {
const template = DEFAULT_PERFORMANCE_SURVEY_TEMPLATES.find((item) => item.code === 'course_share')!
const details = getPerformanceSurveyQuestionScoreDetails(template, {
usefulness: 18,
insight: 16.5,
})
expect(details).toHaveLength(template.questions.length)
expect(details[0]).toEqual({
id: 'usefulness',
title: '内容实用度',
score: 18,
maxScore: 20,
})
expect(details[1]).toEqual(expect.objectContaining({
id: 'insight',
score: 16.5,
maxScore: 20,
}))
expect(details[2].score).toBeNull()
})
test('多份评分使用全部实际得分做普通平均,不剔除最高最低分', () => {
const template = DEFAULT_PERFORMANCE_SURVEY_TEMPLATES.find((item) => item.code === 'cross_department')!
const result = calculateAggregateScore(
[
{
totalScore: 60,
createdAt: '2026-07-04T00:00:00.000Z',
scores: Object.fromEntries(template.questions.map((question) => [question.id, 10])),
respondentName: '最低分',
},
{
totalScore: 100,
createdAt: '2026-07-04T00:01:00.000Z',
scores: Object.fromEntries(template.questions.map((question) => [question.id, question.id === 'requirementClarity' ? 18 : 16])),
respondentName: '有效分',
},
{
totalScore: 120,
createdAt: '2026-07-04T00:02:00.000Z',
scores: Object.fromEntries(template.questions.map((question) => [question.id, 20])),
respondentName: '最高分',
},
],
template.scoreBands,
template.weight,
template,
)
expect(result.droppedHighestLowest).toBe(false)
expect(result.effectiveCount).toBe(3)
expect(result.rawAverage).toBe(93.33)
expect(result.trimmedAverage).toBe(93.33)
expect(result.trimNote).toBe('按全部评分直接计算平均分')
expect(result.removedHigh).toBeNull()
expect(result.removedLow).toBeNull()
expect(result.dimensionAverages.requirementClarity).toBe(16)
expect(result.achievementScore).toBe(93.33)
expect(result.kpiScore).toBe(93.33)
expect(result.weightedScore).toBe(93.33)
})
test('低分也按实际得分展示,不再因为低于档位折算为 0', () => {
const template = DEFAULT_PERFORMANCE_SURVEY_TEMPLATES.find((item) => item.code === 'cross_department')!
const result = calculateAggregateScore(
[95, 14].map((totalScore, index) => ({
totalScore,
createdAt: `2026-07-19T00:0${index}:00.000Z`,
})),
template.scoreBands,
template.weight,
template,
)
expect(result.rawAverage).toBe(54.5)
expect(result.trimmedAverage).toBe(54.5)
expect(result.achievementScore).toBe(54.5)
expect(result.kpiScore).toBe(54.5)
expect(result.weightedScore).toBe(54.5)
expect(result.band).toBeNull()
})
test.each([
[[40], 40],
[[40, 80], 60],
[[10, 80, 120], 70],
[[60, 70, 80, 100, 120], 86],
])('对 %p 只做全部评分的普通平均,结果为 %s', (scores, expected) => {
const template = DEFAULT_PERFORMANCE_SURVEY_TEMPLATES.find((item) => item.code === 'cross_department')!
const result = calculateAggregateScore(
scores.map((totalScore, index) => ({
totalScore,
createdAt: `2026-07-19T00:0${index}:00.000Z`,
})),
template.scoreBands,
template.weight,
template,
)
expect(result.achievementScore).toBe(expected)
expect(result.kpiScore).toBe(expected)
expect(result.effectiveCount).toBe(scores.length)
})
test('零分是有效得分,无评分时结果保持为空', () => {
const template = DEFAULT_PERFORMANCE_SURVEY_TEMPLATES.find((item) => item.code === 'cross_department')!
expect(calculateAggregateScore(
[{ totalScore: 0, createdAt: '2026-07-19T00:00:00.000Z' }],
template.scoreBands,
template.weight,
template,
).achievementScore).toBe(0)
expect(calculateAggregateScore([], template.scoreBands, template.weight, template)).toMatchObject({
responseCount: 0,
effectiveCount: 0,
rawAverage: null,
achievementScore: null,
kpiScore: null,
weightedScore: null,
})
})
test('visual scoring keeps a single evaluator score as the direct average', () => {
const template = DEFAULT_PERFORMANCE_SURVEY_TEMPLATES.find((item) => item.code === 'visual_brand_tone')!
const result = calculateAggregateScore(
[{ totalScore: 88, createdAt: '2026-07-04T00:00:00.000Z' }],
template.scoreBands,
template.weight,
template,
)
expect(template.aggregationStrategy).toBe('direct_average')
expect(result.droppedHighestLowest).toBe(false)
expect(result.effectiveCount).toBe(1)
expect(result.achievementScore).toBe(88)
expect(result.removedHigh).toBeNull()
expect(result.removedLow).toBeNull()
})
test('visual scoring directly averages all three evaluator scores without trimming extremes', () => {
const template = DEFAULT_PERFORMANCE_SURVEY_TEMPLATES.find((item) => item.code === 'visual_brand_tone')!
const legacyTemplateSnapshot = { ...template, aggregationStrategy: undefined }
const result = calculateAggregateScore(
[60, 90, 120].map((totalScore, index) => ({
totalScore,
createdAt: `2026-07-04T00:0${index}:00.000Z`,
})),
template.scoreBands,
template.weight,
legacyTemplateSnapshot,
)
expect(result.droppedHighestLowest).toBe(false)
expect(result.effectiveCount).toBe(3)
expect(result.rawAverage).toBe(90)
expect(result.achievementScore).toBe(90)
expect(result.removedHigh).toBeNull()
expect(result.removedLow).toBeNull()
})
test('visual scoring directly averages every evaluator score when there are more than three', () => {
const template = DEFAULT_PERFORMANCE_SURVEY_TEMPLATES.find((item) => item.code === 'visual_brand_tone')!
const result = calculateAggregateScore(
[60, 70, 80, 100, 120].map((totalScore, index) => ({
totalScore,
createdAt: `2026-07-04T00:0${index}:00.000Z`,
})),
template.scoreBands,
template.weight,
template,
)
expect(result.droppedHighestLowest).toBe(false)
expect(result.effectiveCount).toBe(5)
expect(result.rawAverage).toBe(86)
expect(result.achievementScore).toBe(86)
expect(result.removedHigh).toBeNull()
expect(result.removedLow).toBeNull()
})
})