import {expect,it} from 'vitest' import path from 'node:path' import {groupReceipts} from '../src/main/export-layout' import type {Receipt} from '../src/shared/types' function receipt(school:string|undefined,grade='高一',item='服务费'):Receipt { return {number:'SJ00000001',date:'2026-10-19',school,grade,customer:'学生',item,amountCents:100,cashier:'',reviewer:''} } it('旧记录没有学校年级时保留原客户名称,并落入未分类目录',()=>{ const original={...receipt(undefined),grade:undefined,customer:'以前的客户名称'} expect(groupReceipts([original])[0].directory).toBe(path.join('未分类学校','未分类年级','服务费')) expect(original.customer).toBe('以前的客户名称') }) it('Windows 非法字符、保留名、路径跳转及超长名称生成安全且不混组的目录',()=>{ const schools=['学校/A','学校?A','学校_A','CON','..','ABC','abc','甲'.repeat(80)+'一','甲'.repeat(80)+'二'] const rows=schools.map((school,i)=>({...receipt(school,'../高一','资料:费'),number:String(i)})) const groups=groupReceipts(rows),root=path.resolve('export-root') expect(groups).toHaveLength(rows.length) expect(new Set(groups.map(g=>g.directory.toLowerCase())).size).toBe(rows.length) for(const group of groups){ expect(path.resolve(root,group.directory).startsWith(root+path.sep)).toBe(true) const parts=group.directory.split(path.sep) expect(parts).toHaveLength(3) for(const part of parts){ expect(part).not.toMatch(/[<>:"/\\|?*\u0000-\u001f]/) expect(part).not.toMatch(/[. ]$/) expect(part).not.toMatch(/^(con|prn|aux|nul|com[1-9]|lpt[1-9])(?:\.|$)/i) expect(part.length).toBeLessThanOrEqual(80) } } expect(groupReceipts(rows).map(g=>g.directory)).toEqual(groups.map(g=>g.directory)) })