282 lines
11 KiB
TypeScript
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()
|
|
})
|
|
})
|