import {it,expect,beforeEach,afterEach} from 'vitest' import ExcelJS from 'exceljs' import {mkdtemp,rm} from 'node:fs/promises' import {tmpdir} from 'node:os' import path from 'node:path' import {XlsxReader,createWorkbookTemplate} from '../src/main/workbooks' import {Ledger} from '../src/main/ledger' import {defaultSettings} from '../src/shared/settings' let dir:string const headers=['收款日期','学校','年级','姓名','收费项目','金额','出纳','复核'] beforeEach(async()=>{dir=await mkdtemp(path.join(tmpdir(),'receipt-fields-'))}) afterEach(async()=>{await rm(dir,{recursive:true,force:true})}) async function workbook(row:unknown[],columns=headers){ const file=path.join(dir,'data.xlsx'),book=new ExcelJS.Workbook(),sheet=book.addWorksheet('收据数据') sheet.addRow(columns);sheet.addRow(row);await book.xlsx.writeFile(file);return file } it('下载模板包含学校、年级、姓名,金额列和筛选范围随之调整',async()=>{ const file=path.join(dir,'template.xlsx');await createWorkbookTemplate(file) const book=new ExcelJS.Workbook();await book.xlsx.readFile(file);const sheet=book.worksheets[0] expect(sheet.getRow(1).values).toEqual([undefined,...headers]) expect(sheet.getColumn(6).numFmt).toBe('0.00');expect(sheet.autoFilter).toBe('A1:H1') const draft=await new XlsxReader().read(file) expect(draft.rows[0].errors).toEqual([]) expect(draft.rows[0].receipt.customer).toBe('某某学校 初二年级 张三') }) it('三列去除首尾空白后,用单个空格拼接,并保留原字段用于扩展',async()=>{ const file=await workbook(['2026-10-16',' 巴南中学 ',' 高2028届 ',' 张三 ','服务费',3500]) const draft=await new XlsxReader().read(file),receipt=draft.rows[0].receipt expect(receipt.customer).toBe('巴南中学 高2028届 张三') expect(receipt).toMatchObject({school:'巴南中学',grade:'高2028届',studentName:'张三'}) expect(draft.rows[0].errors).toEqual([]) const store=new Ledger(path.join(dir,'db.sqlite'));await store.open() const batch=await store.reserve({name:'data.xlsx',hash:'hash'},[receipt],defaultSettings(),null) expect(store.get(batch.id).receipts[0]).toMatchObject({school:'巴南中学',grade:'高2028届',studentName:'张三',customer:'巴南中学 高2028届 张三'}) }) it.each([[1,'学校'],[2,'年级'],[3,'姓名']] as const)('%s 列缺失时,定位具体必填字段',async(index,label)=>{ const row:unknown[]=['2026-10-16','学校','年级','张三','服务费',3500];row[index]=' ' const draft=await new XlsxReader().read(await workbook(row)) expect(draft.rows[0].errors).toContain(label+'不能为空') }) it('缺少拆分后的表头时提示重新下载模板',async()=>{ const file=await workbook(['2026-10-16','学校 年级 张三','服务费',3500],['收款日期','客户名称','收费项目','金额']) await expect(new XlsxReader().read(file)).rejects.toThrow('第一行缺少表头:学校、年级、姓名') }) it('按表头识别三列,而非依赖固定列序',async()=>{ const file=await workbook(['张三',3500,'高2028届','2026-10-16','服务费','学校'],['姓名','金额','年级','收款日期','收费项目','学校']) const draft=await new XlsxReader().read(file) expect(draft.rows[0].receipt.customer).toBe('学校 高2028届 张三') expect(draft.rows[0].errors).toEqual([]) })