gongxiangp2/lib/__tests__/performance-salary-export.test.ts

101 lines
6.2 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 ExcelJS from 'exceljs'
import JSZip from 'jszip'
import { buildSalaryTemplateWorkbook, salaryExportFileName, saveSalaryTemplateWorkbook } from '@/lib/performance-salary-export'
import { INITIAL_SALARY_POSITIONS } from '@/lib/salary-position-seeds'
import { getPositionSalaryTemplate } from '@/lib/performance-salary-templates'
test('导出后重新读取:五岗模板字段、空行、冻结及月份均正确且无隐私数据或公式', async () => {
const workbook = await buildSalaryTemplateWorkbook('2027-01')
const buffer = await workbook.xlsx.writeBuffer()
const reloaded = new ExcelJS.Workbook()
await reloaded.xlsx.load(buffer)
expect(reloaded.worksheets.map(sheet => sheet.name)).toEqual(['AI全案策划岗', '摄影中心岗', '空间视觉岗', '管理岗', 'AI技术设计师岗'])
const expected = [
{ columns: 26, headerRow: 3, lastHeader: '备注', firstMerge: 'A2:H2' },
{ columns: 16, headerRow: 2, lastHeader: '备注', firstMerge: 'A1:P1' },
{ columns: 22, headerRow: 3, lastHeader: '备注', firstMerge: 'A2:G2' },
{ columns: 23, headerRow: 3, lastHeader: '2026年12月薪资', firstMerge: 'A2:D2' },
{ columns: 25, headerRow: 3, lastHeader: '备注', firstMerge: 'A2:H2' },
]
reloaded.worksheets.forEach((sheet, index) => {
const target = expected[index]
expect(sheet.state).toBe('visible')
expect(JSON.stringify(sheet.getRow(target.headerRow).values)).not.toMatch(/加权得分|绩效占比|绩效得分/)
const headers = sheet.getRow(target.headerRow).values as string[]
expect(sheet.getCell(target.headerRow, 1).value).toBe('花名')
if (index !== 1) expect(sheet.getCell(target.headerRow, 2).value).toBe('姓名')
expect(headers.filter(value => value === '绩效系数')).toHaveLength(1)
expect(sheet.getCell(target.headerRow + 1, headers.indexOf('绩效系数')).numFmt).toBe('0.00')
expect(sheet.columnCount).toBe(target.columns)
expect(sheet.rowCount).toBe(target.headerRow + 5)
expect(sheet.getCell(1, 1).value).toBe(`2027-01 ${sheet.name}薪资明细`)
expect(sheet.getCell(target.headerRow, target.columns).value).toBe(target.lastHeader)
expect(sheet.model.merges).toContain(target.firstMerge)
expect(sheet.views[0]).toEqual(expect.objectContaining({ state: 'frozen', xSplit: 1, ySplit: target.headerRow }))
for (let row = target.headerRow + 1; row <= sheet.rowCount; row += 1) {
for (let col = 1; col <= target.columns; col += 1) expect(sheet.getCell(row, col).value).toBeNull()
}
})
expect(reloaded.worksheets[1].getCell('N2').value).toBe('2026年11月合计薪资')
expect(JSON.stringify(reloaded.worksheets[0].getRow(3).values)).toContain('创意成果市场验证 · 市场验证得分(上月)')
const zip = await JSZip.loadAsync(buffer)
expect(Object.keys(zip.files).some(path => /externalLinks|hidden/i.test(path))).toBe(false)
for (const path of Object.keys(zip.files).filter(path => /^xl\/worksheets\/sheet\d+\.xml$/.test(path))) {
const xml = await zip.file(path)!.async('string')
expect(xml).not.toMatch(/<f[\s>]/)
}
})
test('校验导出月份与文件名', async () => {
expect(salaryExportFileName('2026-09')).toBe('绩效薪资模板_2026-09.xlsx')
await expect(buildSalaryTemplateWorkbook('../../secret')).rejects.toThrow('有效月份')
})
test('按保存岗位导出使用相邻原始值与考核得分,百分比不缩放,空行不含业务数据', async () => {
const positions = INITIAL_SALARY_POSITIONS.map((seed, index) => ({ ...seed.value, id: `position-${index}`, revision: 1, updatedAt: '', personnelCount: 7 }))
const buffer = await (await buildSalaryTemplateWorkbook('2026-10', positions)).xlsx.writeBuffer()
const restored = new ExcelJS.Workbook()
await restored.xlsx.load(buffer)
for (const position of positions) {
const sheet = restored.getWorksheet(position.name)!
const template = getPositionSalaryTemplate(position, '2026-10')
expect(sheet.getCell(3, 1).value).toBe('花名')
expect(sheet.getCell(3, 2).value).toBe('姓名')
expect(sheet.views[0]).toEqual(expect.objectContaining({ state: 'frozen', xSplit: 1, ySplit: 3 }))
expect(sheet.getRow(3).values).toEqual([undefined, ...template.columns.map(column => column.label)])
if (position.salaryRole === 'ai-planning') expect(JSON.stringify(sheet.getRow(3).values)).toContain('创意成果市场验证 · 市场验证得分(上月)')
expect(JSON.stringify(sheet.getRow(3).values)).not.toMatch(/加权得分/)
if (position.salaryRole === 'ai-planning') expect(sheet.getRow(3).values).toContain('创意成果市场验证 · 考核得分(上月,综合占比7%)')
template.columns.forEach((column, index) => {
for (let row = 4; row <= 8; row++) {
expect(sheet.getCell(row, index + 1).value).toBeNull()
if (column.key === 'performanceCoefficient') expect(sheet.getCell(row, index + 1).numFmt).toBe('0.00')
if (column.key === 'currentTotal') expect(sheet.getCell(row, index + 1).numFmt).toBe('#,##0.00')
if (column.key.endsWith('Score') || column.key.endsWith('RawValue')) {
expect(sheet.getCell(row, index + 1).numFmt).toBe(column.kind === 'percent' ? '0.00"%"' : '0.00')
}
}
if (column.key.endsWith('RawValue')) expect(template.columns[index + 1].key).toBe(column.key.replace(/RawValue$/, 'Score'))
})
}
})
test('标准下载文件并回收对象URL', async () => {
const create = jest.fn(() => 'blob:salary-template')
const revoke = jest.fn()
Object.defineProperty(URL, 'createObjectURL', { configurable: true, value: create })
Object.defineProperty(URL, 'revokeObjectURL', { configurable: true, value: revoke })
const click = jest.spyOn(HTMLAnchorElement.prototype, 'click').mockImplementation(function(this: HTMLAnchorElement) {
expect(this.download).toBe('绩效薪资模板_2026-09.xlsx')
})
const timer = jest.spyOn(window, 'setTimeout')
await saveSalaryTemplateWorkbook('2026-09')
expect(create).toHaveBeenCalledWith(expect.any(Blob))
expect(click).toHaveBeenCalledTimes(1)
const cleanup = timer.mock.calls.find(call => call[1] === 1000)?.[0] as (() => void)
cleanup()
expect(revoke).toHaveBeenCalledWith('blob:salary-template')
click.mockRestore()
timer.mockRestore()
})