You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
93 lines
3.1 KiB
93 lines
3.1 KiB
import { computeSalaryResult, fetchSalaryRecords, saveSalaryRecord, deleteSalaryRecord } from '../api/api';
|
|
import type { SalaryCalcInput } from '../types';
|
|
|
|
// 内存版 storage mock,使记录类接口可在单测中验证(storage 为唯一真相源)
|
|
jest.mock('../lib/storage', () => {
|
|
const store: Record<string, string> = {};
|
|
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<ReturnType<typeof saveSalaryRecord>>((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);
|
|
});
|
|
});
|
|
|
|
|