轻松聘
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

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);
});
});