Files

30 lines
4.1 KiB
TypeScript

import { describe,it,expect,beforeEach,afterEach } from 'vitest'
import { mkdtemp,rm,writeFile } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import path from 'node:path'
import { parseMoney,chineseMoney,moneyCells,MAX_CENTS } from '../src/core/money'
import { parseReceiptDate } from '../src/core/date'
import { buildDocument,TemplateRegistry } from '../src/core/templates'
import { defaultSettings } from '../src/shared/settings'
import { Ledger } from '../src/main/ledger'
import { XlsxReader,createWorkbookTemplate } from '../src/main/workbooks'
import ExcelJS from 'exceljs'
describe('金额与日期',()=>{
it('以整数分保存,金额格不截断',()=>{expect(parseMoney('3,500.01')).toBe(350001);expect(parseMoney('0.01')).toBe(1);expect(parseMoney('9999999.99')).toBe(MAX_CENTS);expect(moneyCells(MAX_CENTS).join('')).toBe('999999999')})
it.each(['-1','0','1.001','1e3','12,34','10000000','abc'])('拒绝无效金额 %s',v=>{expect(()=>parseMoney(v)).toThrow()})
it.each([[350000,'叁仟伍佰元整'],[100101,'壹仟零壹元零壹分'],[100000001,'壹佰万元零壹分'],[1010000,'壹万零壹佰元整'],[1234,'壹拾贰元叁角肆分']])('正确转换金额大写', (n,text)=>expect(chineseMoney(n as number)).toBe(text))
it('验证真实日期与 Excel 的 1900/1904 日期制',()=>{expect(parseReceiptDate('2024-2-29')).toBe('2024-02-29');expect(()=>parseReceiptDate('2026-2-29')).toThrow();expect(parseReceiptDate(61)).toBe('1900-03-01');expect(parseReceiptDate(0,true)).toBe('1904-01-01');expect(()=>parseReceiptDate(60)).toThrow()})
})
let dir:string
beforeEach(async()=>{dir=await mkdtemp(path.join(tmpdir(),'receipt-studio-test-'))})
afterEach(async()=>{await rm(dir,{recursive:true,force:true})})
const input={date:'2026-10-16',customer:'测试客户',item:'服务费',amountCents:350000,cashier:'',reviewer:''}
describe('持久编号',()=>{
it('预览不推进,保留批次快照并在重启后继续编号',async()=>{const store=new Ledger(path.join(dir,'data.sqlite'));await store.open();expect(store.nextNumber('SJ')).toBe('SJ00000001');expect(store.nextNumber('SJ')).toBe('SJ00000001');const settings={...defaultSettings(),defaultCashier:'张三'};const batch=await store.reserve({name:'a.xlsx',hash:'hash'},[input,input],settings,null);expect(batch.receipts.map(r=>r.number)).toEqual(['SJ00000001','SJ00000002']);expect(batch.receipts[0].cashier).toBe('张三');const again=new Ledger(path.join(dir,'data.sqlite'));await again.open();expect(again.nextNumber('SJ')).toBe('SJ00000003');expect(again.get(batch.id).receipts).toEqual(batch.receipts)})
it('数据损坏时停止分配编号,保留原文件',async()=>{const file=path.join(dir,'bad.sqlite');await writeFile(file,'broken');await expect(new Ledger(file).open()).rejects.toThrow('停止分配编号')})
})
describe('模板和工作簿',()=>{
it('模板转义用户文字,奇数张 A4 保留空白半页',()=>{const registry=new TemplateRegistry(),r={...input,number:'SJ00000001',customer:'<script>alert(1)</script>'};const html=buildDocument(registry.get('classic-v1'),[r,r,r],{settings:defaultSettings(),seal:null,preview:false},'print',2,true);expect(html).not.toContain('<script>');expect(html).toContain('&lt;script&gt;');expect(html.match(/<section class="page">/g)).toHaveLength(2);expect(html.match(/class="slot"/g)).toHaveLength(4);expect(html).not.toContain('预览样张')})
it('导入标准模板并定位错误行,支持千分位与重复行提醒',async()=>{const file=path.join(dir,'test.xlsx');await createWorkbookTemplate(file);const book=new ExcelJS.Workbook();await book.xlsx.readFile(file);book.worksheets[0].addRow(['2026-10-16','学校','年级','客户','项目','12.345']);book.worksheets[0].addRow(['2026-10-16','学校','年级','客户','项目','1,234.50']);book.worksheets[0].addRow(['2026-10-16','学校','年级','客户','项目','1,234.50']);await book.xlsx.writeFile(file);const draft=await new XlsxReader().read(file);expect(draft.rows).toHaveLength(4);expect(draft.rows[1].excelRow).toBe(3);expect(draft.rows[1].errors[0]).toContain('两位小数');expect(draft.rows[2].receipt.amountCents).toBe(123450);expect(draft.rows[3].warnings).toHaveLength(1)})
})