Files
zszq-trs/YLErpWeb/fe-tests/bondCalc.test.js
T
hjhan d3b47f6fa1 feat(swap): 债券净价/全价/收益率互算核心(逐字段手动锁定+错误不覆盖纯函数+测试)
- swapCalc.applyBondCalcResult(state, calc, manualSet):回写时跳过"用户手填过的字段"(逐字段手动锁定,永不覆盖),只反向填充未手填字段;EPS 去抖避免光标跳动。

- 纯函数 getBondCalcErrorMessage(resp):空响应/errCode!=0 返回文案、否则 null,与 C# errCode 守卫一一对应,保证"坏结果"不静默回写。

- 新增 jest 用例覆盖:三字段互算、逐字段锁定(手填全部三个无一被回写)、业务错误不覆盖手工输入、getBondCalcErrorMessage。85→86 全绿。
2026-07-22 11:35:53 +08:00

132 lines
7.2 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* bondCalc.test.js — 债券净价/全价/收益率三字段互算的"逐字段手动锁定"守卫
* ============================================================================
* 核心保证(用户最担心的,也是第一天讨论的诉求):
* 用户手填过的字段 = 被锁定,回写时**绝不**覆盖它;
* 只有"未手填"的字段才从源反算得出、被回写;计算器未返回的值也不覆盖。
* 因此用户可逐个手填全部三个,互算绝不会冲掉其中任何一个。
*
* 运行:cd YLErpWeb/fe-tests && npm i && npm test
* 纯逻辑在 swapCalc.js::applyBondCalcResultUMDnode 可直接 require)。
*/
const SwapCalc = require('../wwwroot/Scripts/app/swaptrade/swapCalc.js');
const CALC = { cleanPrice: 99, dirtyPrice: 100, ytm: 2.5 };
describe('逐字段手动锁定:手填过的字段永不回写', () => {
test('仅手填净价(CP):净价保持,全价/收益率被反算覆盖', () => {
const state = { cleanPrice: 99.5, dirtyPrice: null, ytm: null }; // 用户手填净价 99.5
SwapCalc.applyBondCalcResult(state, CALC, { CP: true, DP: false, YD: false });
expect(state.cleanPrice).toBe(99.5); // 手填:绝不回写
expect(state.dirtyPrice).toBe(100); // 未手填:被反算覆盖
expect(state.ytm).toBe(2.5);
});
test('手填净价+全价(CP&DP):两者保持,仅收益率被覆盖', () => {
const state = { cleanPrice: 99.5, dirtyPrice: 100.5, ytm: null };
SwapCalc.applyBondCalcResult(state, CALC, { CP: true, DP: true, YD: false });
expect(state.cleanPrice).toBe(99.5); // 手填:保持
expect(state.dirtyPrice).toBe(100.5); // 手填:保持
expect(state.ytm).toBe(2.5); // 未手填:被反算覆盖
});
test('用户不认可计算结果、手填全部三个:全部保留,无一被回写(核心场景)', () => {
const state = { cleanPrice: 98, dirtyPrice: 101, ytm: 3.0 }; // 用户全手填
SwapCalc.applyBondCalcResult(state, CALC, { CP: true, DP: true, YD: true });
expect(state.cleanPrice).toBe(98); // 全部手填 → 全部保留
expect(state.dirtyPrice).toBe(101);
expect(state.ytm).toBe(3.0);
});
test('手填收益率(YD):收益率保持,净价/全价被覆盖', () => {
const state = { cleanPrice: null, dirtyPrice: null, ytm: 2.6 };
SwapCalc.applyBondCalcResult(state, CALC, { CP: false, DP: false, YD: true });
expect(state.ytm).toBe(2.6);
expect(state.cleanPrice).toBe(99);
expect(state.dirtyPrice).toBe(100);
});
});
describe('边界:计算器未返回的值不覆盖、无变化不写', () => {
test('手填净价 且 计算器未返收益率:源与缺失值均不写', () => {
const state = { cleanPrice: null, dirtyPrice: null, ytm: 2.6 };
const partial = { cleanPrice: 99, dirtyPrice: 100, ytm: undefined };
SwapCalc.applyBondCalcResult(state, partial, { CP: true, DP: false, YD: false });
expect(state.cleanPrice).toBe(null); // CP 手填:即便 calc 返 99 也不写
expect(state.dirtyPrice).toBe(100); // 未手填:被覆盖
expect(state.ytm).toBe(2.6); // 计算器没返收益率 → 保持用户值
});
test('派生值与当前值差异<EPS:不写(避免光标跳动)', () => {
const state = { cleanPrice: 99.5, dirtyPrice: 100.0, ytm: 2.5 };
const near = { cleanPrice: 99.5, dirtyPrice: 100.00001, ytm: 2.5 };
SwapCalc.applyBondCalcResult(state, near, { CP: true, DP: false, YD: false });
expect(state.cleanPrice).toBe(99.5); // 手填:不写
expect(state.dirtyPrice).toBe(100.0); // 差异 1e-5 < EPS → 不写
expect(state.ytm).toBe(2.5);
});
// 模拟 calcBondForItem:以既有三字段为代理调纯函数,再写回 item
test('浮动腿 item 写回:仅未手填字段被覆盖', () => {
const item = { PosiNetNoFeePrice: 99.5, PosiGrossPrice: 100.0, InitYtm: 2.6,
bondDriverType: 'CP', bondManual: { CP: true, DP: false, YD: false } };
const calc = { cleanPrice: 99, dirtyPrice: 100, ytm: 2.5 };
const proxy = {
cleanPrice: item.PosiNetNoFeePrice, // 净价 = 手填
dirtyPrice: item.PosiGrossPrice,
ytm: item.InitYtm
};
SwapCalc.applyBondCalcResult(proxy, calc, item.bondManual);
item.PosiNetNoFeePrice = proxy.cleanPrice;
item.PosiGrossPrice = proxy.dirtyPrice;
item.InitYtm = proxy.ytm;
expect(item.PosiNetNoFeePrice).toBe(99.5); // 手填(净价):保持
expect(item.PosiGrossPrice).toBe(100); // 全价:被反算覆盖
expect(item.InitYtm).toBe(2.5); // 收益率:被反算覆盖
});
});
describe('错误反馈:getBondCalcErrorMessage(对齐 C# BondCalcHepler 的 errCode 守卫)', () => {
test('空响应 → 提示"无响应",且绝不回写', () => {
expect(SwapCalc.getBondCalcErrorMessage(null)).toBe("债券计算器无响应,已保留手工输入");
});
test('业务错误码 errCode!=0(债券不存在/信息不全/参数非法)→ 返回真实 errMsg', () => {
const resp = { errCode: 1, errMsg: "债券不存在或信息不全", dirtyPrice: 0, cleanPrice: 0, ytm: 0 };
expect(SwapCalc.getBondCalcErrorMessage(resp)).toBe("债券不存在或信息不全");
});
test('业务错误码 errCode!=0 但缺 errMsg → 返回兜底文案', () => {
const resp = { errCode: 2, dirtyPrice: 0, cleanPrice: 0, ytm: 0 };
expect(SwapCalc.getBondCalcErrorMessage(resp)).toBe("债券计算失败,请检查标的或参数");
});
test('正常成功响应(errCode=0) → 返回 null(应继续回写)', () => {
const resp = { errCode: 0, errMsg: null, dirtyPrice: 100, cleanPrice: 99, ytm: 2.5 };
expect(SwapCalc.getBondCalcErrorMessage(resp)).toBeNull();
});
test('无 errCode 字段的正常响应 → 返回 null(应继续回写)', () => {
const resp = { dirtyPrice: 100, cleanPrice: 99, ytm: 2.5 };
expect(SwapCalc.getBondCalcErrorMessage(resp)).toBeNull();
});
test('端到端:业务错误时不覆盖手工输入(仅提示)', () => {
const item = { PosiNetNoFeePrice: 99.5, PosiGrossPrice: 100.0, InitYtm: 2.6,
bondDriverType: 'CP', bondManual: { CP: true, DP: false, YD: false } };
const respObj = { errCode: 1, errMsg: "债券不存在", dirtyPrice: 0, cleanPrice: 0, ytm: 0 };
const err = SwapCalc.getBondCalcErrorMessage(respObj);
expect(err).toBe("债券不存在"); // 有错 → 调用方会 main.message(err) 并 return
if (!err) {
const proxy = { cleanPrice: item.PosiNetNoFeePrice, dirtyPrice: item.PosiGrossPrice, ytm: item.InitYtm };
SwapCalc.applyBondCalcResult(proxy, respObj, item.bondManual);
item.PosiNetNoFeePrice = proxy.cleanPrice;
item.PosiGrossPrice = proxy.dirtyPrice;
item.InitYtm = proxy.ytm;
}
expect(item.PosiNetNoFeePrice).toBe(99.5);
expect(item.PosiGrossPrice).toBe(100.0);
expect(item.InitYtm).toBe(2.6);
});
});