gongxiangp2/lib/__tests__/salary-position-contract.test.ts

74 lines
5.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { INITIAL_SALARY_POSITIONS } from '../salary-position-seeds'
import { calculatePositionScore, percentageSchema, positionFieldsSchema, sourceFromKey, sourceKey, sourceLabel } from '../salary-position-contract'
import { getPositionSalaryTemplate } from '../performance-salary-templates'
const copy = () => JSON.parse(JSON.stringify(INITIAL_SALARY_POSITIONS[1].value))
test('经营来源可在岗位配置选择、保存与导出名称解析,主管及技术岗原来源保留', () => {
const source = sourceFromKey('market-intent-top3')
expect(source).toEqual({ kind: 'market-intent-top3' })
expect(sourceKey(source)).toBe('market-intent-top3')
expect(sourceLabel(source, [])).toBe('业绩转化 · 上月最高3条意向率环比')
expect(INITIAL_SALARY_POSITIONS[0].value.dimensions[1].metrics[0].source.kind).toBe('unavailable')
expect(INITIAL_SALARY_POSITIONS[2].value.dimensions[0].metrics[1].source).toEqual({ kind: 'survey', templateCode: 'project_result_validation' })
})
test('三岗初始化完整权重,包括零权重指标', () => {
for (const seed of INITIAL_SALARY_POSITIONS) expect(positionFieldsSchema.safeParse(seed.value).success).toBe(true)
expect(INITIAL_SALARY_POSITIONS.map(seed => seed.value.dimensions.map(d => [d.weight, d.metrics.map(m => m.weight)]))).toEqual([
[['70', ['80', '20']], ['20', ['40', '40', '20']], ['10', ['100']]],
[['25', ['100', '0']], ['70', ['10', '90', '0']], ['5', ['100']]],
[['25', ['50', '50']], ['70', ['0', '60', '40']], ['5', ['100']]],
])
})
test('百分比精确边界、空配置与无自动归一化', () => {
for (const value of ['0', '0.00', '0.01', '99.99', '100.00']) expect(percentageSchema.safeParse(value).success).toBe(true)
for (const value of ['-1', '100.01', '1.001', 'NaN', '', '1e2', 50]) expect(percentageSchema.safeParse(value).success).toBe(false)
const value = copy(); value.dimensions = []; expect(positionFieldsSchema.safeParse(value).success).toBe(true)
const invalid = copy(); invalid.dimensions[0].weight = '24.99'; expect(positionFieldsSchema.safeParse(invalid).success).toBe(false)
invalid.dimensions[0].weight = '25'; invalid.dimensions[0].metrics[0].weight = '99.99'; expect(positionFieldsSchema.safeParse(invalid).success).toBe(false)
})
test('重复稳定ID和名称、非法来源及岗位入口被拒绝', () => {
const value = copy(); value.dimensions[0].metrics[0].id = value.dimensions[1].metrics[0].id
expect(positionFieldsSchema.safeParse(value).success).toBe(false)
value.dimensions = []; value.salaryRole = 'director'; expect(positionFieldsSchema.safeParse(value).success).toBe(false)
const source = copy(); source.dimensions[0].metrics[0].source = { kind: 'manual', score: 100 }
expect(positionFieldsSchema.safeParse(source).success).toBe(false)
})
test('两层权重逐项舍入后求和,正权重缺失阻断,零权重不阻断', () => {
const dimensions = copy().dimensions
const scores: Record<string, number | null> = {}
for (const d of dimensions) for (const m of d.metrics) scores[m.id] = m.weight === '0' ? null : 88.885
expect(calculatePositionScore(dimensions, scores).totalScore).toBe(88.88)
scores[dimensions[0].metrics[0].id] = null
expect(calculatePositionScore(dimensions, scores).totalScore).toBeNull()
dimensions[0].weight = '0'; dimensions[1].weight = '95'
expect(calculatePositionScore(dimensions, scores).totalScore).toBe(88.88)
expect(calculatePositionScore([], {}).totalScore).toBeNull()
})
test.each(['ai-planning', 'ai-technical', 'management', 'photography', 'spatial-visual'] as const)('动态%s模板字段唯一,含基础字段及唯一绩效系数', salaryRole => {
const template = getPositionSalaryTemplate({ ...copy(), salaryRole, id: 'job', revision: 1, updatedAt: '', personnelCount: 0 }, '2027-01')
expect(new Set(template.columns.map(c => c.key)).size).toBe(template.columns.length)
expect(template.columns.map(c => c.key)).toEqual(expect.arrayContaining(['name', 'nickname', 'department', 'baseSalary', 'gradeSalary', 'performanceSalary', 'performanceCoefficient', 'currentTotal']))
expect(template.columns.filter(c => c.key === 'performanceCoefficient')).toHaveLength(1)
expect(template.columns.some(c => c.key === 'performanceScore' || c.label.includes('绩效得分'))).toBe(false)
expect(template.columns.some(c => c.key.endsWith('Weighted') || c.label.includes('加权得分'))).toBe(false)
expect(template.columns.filter(c => c.key.endsWith('Score') && c.key !== 'performanceScore')).toHaveLength(6)
})
test('各项考核得分直接乘综合占比,总分等于显示值相加且无二次加权', () => {
const dimensions = copy().dimensions
const scores: Record<string, number | null> = {}
for (const d of dimensions) for (const m of d.metrics) scores[m.id] = 100
const result = calculatePositionScore(dimensions, scores)
expect(result.contributions[dimensions[1].metrics[1].id]).toBe(63)
expect(result.totalScore).toBe(100)
expect(result).not.toHaveProperty('weighted')
for (const d of dimensions) for (const m of d.metrics) scores[m.id] = 88.885
const rounded = calculatePositionScore(dimensions, scores)
expect(rounded.totalScore).toBe(88.88)
expect(rounded.totalScore).toBe(Object.values(rounded.contributions).reduce<number>((sum, value) => sum + Math.round((value ?? 0) * 100), 0) / 100)
scores[dimensions[0].metrics[1].id] = null
expect(calculatePositionScore(dimensions, scores).contributions[dimensions[0].metrics[1].id]).toBeNull()
scores[dimensions[0].metrics[1].id] = 0
expect(calculatePositionScore(dimensions, scores).contributions[dimensions[0].metrics[1].id]).toBe(0)
})