96 lines
5.8 KiB
TypeScript
96 lines
5.8 KiB
TypeScript
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(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.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()
|
||
})
|