import { computeSalaryResult, fetchSalaryRecords, saveSalaryRecord, deleteSalaryRecord } from '../api/api'; import type { SalaryCalcInput } from '../types'; // 内存版 storage mock,使记录类接口可在单测中验证(storage 为唯一真相源) jest.mock('../lib/storage', () => { const store: Record = {}; return { storageGet: jest.fn(async (k: string) => (k in store ? store[k] : null)), storageSet: jest.fn(async (k: string, v: string) => { store[k] = v; }), storageRemove: jest.fn(async (k: string) => { delete store[k]; }), }; }); describe('computeSalaryResult(薪资计算器业务规则)', () => { it('低薪且五险一金+起征点覆盖时个税为 0', () => { const input = { monthlyGross: 5000, city: '北京', bonusMonths: 0, socialBase: 10000, providentFundRate: 0.12, specialDeduction: 0, }; const r = computeSalaryResult(input as any); // 社保基数 10000 在北京市上下限(6821-33891)内,不触发 clamp // monthlySocial = 10000*(0.08+0.02+0.005+0.12) = 2250 expect(r.monthlySocial).toBe(2250); // taxable = max(0, 5000-2250-5000) = 0 → 个税 0 expect(r.monthlyTax).toBe(0); // monthlyNet = 5000 - 2250 = 2750 expect(r.monthlyNet).toBe(2750); expect(r.annualGross).toBe(60000); expect(r.annualNet).toBe(33000); }); it('含年终奖时月度税前合计摊入', () => { const input = { monthlyGross: 10000, bonusMonths: 2, socialBase: 10000, providentFundRate: 0.12, specialDeduction: 0, }; const r = computeSalaryResult(input as any); // monthlyTotal = 10000 * (1 + 2/12) = 11666.67 → 四舍五入 11667 expect(r.monthlyTotal).toBe(11667); }); }); describe('薪资计算记录(本地持久化)', () => { const baseInput: SalaryCalcInput = { monthlyGross: 30000, city: '北京', socialBase: 25000, providentFundRate: 0.12, specialDeduction: 2000, bonusMonths: 0, }; it('保存后可在记录列表中读取,并按时间倒序', async () => { const r1 = computeSalaryResult(baseInput); const rec1 = await saveSalaryRecord(baseInput, r1, '深圳 offer'); expect(rec1.id).toBeTruthy(); expect(rec1.note).toBe('深圳 offer'); const later = await new Promise>((resolve) => { setTimeout(async () => { const r2 = computeSalaryResult({ ...baseInput, monthlyGross: 40000 }); resolve(await saveSalaryRecord({ ...baseInput, monthlyGross: 40000 }, r2)); }, 5); }); const list = await fetchSalaryRecords(); expect(list).toHaveLength(2); // 后保存的排在最前 expect(list[0].id).toBe(later.id); expect(list[1].id).toBe(rec1.id); }); it('删除记录后不再出现在列表中', async () => { const listBefore = await fetchSalaryRecords(); const target = listBefore[0]; await deleteSalaryRecord(target.id); const listAfter = await fetchSalaryRecords(); expect(listAfter.find((r) => r.id === target.id)).toBeUndefined(); expect(listAfter).toHaveLength(listBefore.length - 1); }); });