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: 19, headerRow: 3, lastHeader: '备注', firstMerge: 'A2:G2' }, { columns: 16, headerRow: 2, lastHeader: '备注', firstMerge: 'A1:P1' }, { columns: 16, headerRow: 3, lastHeader: '备注', firstMerge: 'A2:F2' }, { columns: 17, headerRow: 3, lastHeader: '2026年12月薪资', firstMerge: 'A2:C2' }, { columns: 18, headerRow: 3, lastHeader: '备注', firstMerge: 'A2:G2' }, ] reloaded.worksheets.forEach((sheet, index) => { const target = expected[index] expect(sheet.state).toBe('visible') expect(JSON.stringify(sheet.getRow(target.headerRow).values)).not.toMatch(/加权得分|绩效占比/) 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月合计薪资') 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(/]/) } }) 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)]) expect(JSON.stringify(sheet.getRow(3).values)).not.toMatch(/加权得分/) template.columns.forEach((column, index) => { for (let row = 4; row <= 8; row++) { expect(sheet.getCell(row, index + 1).value).toBeNull() if (column.key.endsWith('Score')) expect(sheet.getCell(row, index + 1).numFmt).toBe('0.00') } }) } }) 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() })