feat(swaptrade): 债券三字段计算器交互改为回车触发并加源/AUTO/REV 三态标识

- 约定1:仅在净价/全价/收益率输入框敲回车触发计算器,失焦不触发
- 约定2:计算成功->源字段标"源"、另两字段标"AUTO";失败->保留源值、清空另两字段与标识
- 约定3:编辑某字段未回车->不联动另两字段,清源/AUTO、仅本字段标"REV"
- swapCalc.js 新增 applyBondCalcSuccess/Failure/ManualEdit 纯函数,applyBondCalcResult 第三参由 manualSet 改为 driver(跳过源字段写回)
- fastVue.base.js 新增 enter 事件区分回车/失焦,向后兼容旧的 input 行为
- TradeEdit.cshtml 三字段 input/enter 分离绑定 + 源/AUTO/REV 三态标签(去除旧铅笔图标)
- 重写 bondCalc.test.js / 更新 swapCalc.test.js 覆盖状态机;jest 68 项全过
This commit is contained in:
hjhan
2026-07-29 14:34:25 +08:00
parent 8c4f452482
commit b9f2bd14f6
6 changed files with 271 additions and 197 deletions
+101 -87
View File
@@ -1,126 +1,94 @@
/**
* bondCalc.test.js — 债券净价/全价/收益率三字段互算的"逐字段手动锁定"守卫
* bondCalc.test.js — 债券净价/全价/收益率三字段互算driver 源 + 交互约定2/3 状态机)
* ============================================================================
* 核心保证(用户最担心的,也是第一天讨论的诉求):
* 用户手填过的字段 = 被锁定,回写时**绝不**覆盖它;
* 只有"未手填"的字段才从源反算得出、被回写;计算器未返回的值也不覆盖
* 因此用户可逐个手填全部三个,互算绝不会冲掉其中任何一个
* 模型(交互约定1/2/3):
* - driver(bondDriverType):回车的"源"字段(CP净价 / DP全价 / YD收益率),恒为最后回车者。
* - bondAuto:计算器算出的另两个字段("AUTO"
* - bondRev:编辑未回车的人工输入字段("REV"
* - applyBondCalcResult(state, calc, driver):回写时跳过 driver 源字段(保留用户手输值),
* 另两字段用计算器结果覆盖——即"回车某字段即以它为源反算另两个"。
*
* 运行:cd YLErpWeb/fe-tests && npm i && npm test
* 纯逻辑在 swapCalc.js::applyBondCalcResultUMDnode 可直接 require)。
* 纯逻辑在 swapCalc.jsUMDnode 可直接 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); // 未手填:被反算覆盖
describe('以 driver 为源:回写时源字段保持、另两字段被反算覆盖', () => {
test('回车净价(CP)为源:净价保持,全价/收益率被反算覆盖', () => {
const state = { cleanPrice: 99.5, dirtyPrice: null, ytm: null };
SwapCalc.applyBondCalcResult(state, CALC, 'CP');
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('回车全价(DP)为源:全价保持,净价/收益率被反算覆盖', () => {
const state = { cleanPrice: null, dirtyPrice: 100.5, ytm: null };
SwapCalc.applyBondCalcResult(state, CALC, 'DP');
expect(state.dirtyPrice).toBe(100.5); // :保持
expect(state.cleanPrice).toBe(99);
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):收益率保持,净价/全价被覆盖', () => {
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);
SwapCalc.applyBondCalcResult(state, CALC, 'YD');
expect(state.ytm).toBe(2.6); // 源:保持
expect(state.cleanPrice).toBe(99);
expect(state.dirtyPrice).toBe(100);
});
test('仅手填全价(DP):全价保持,净价/收益率被反算覆盖(最常用场景)', () => {
const state = { cleanPrice: null, dirtyPrice: 100.5, ytm: null }; // 用户手填全价 100.5
SwapCalc.applyBondCalcResult(state, CALC, { CP: false, DP: true, YD: false });
expect(state.dirtyPrice).toBe(100.5); // 手填:绝不回写
expect(state.cleanPrice).toBe(99); // 未手填:被反算覆盖
expect(state.ytm).toBe(2.5);
});
test('手填净价+收益率(CP&YD):两者保持,仅全价被覆盖', () => {
const state = { cleanPrice: 98.5, dirtyPrice: null, ytm: 3.2 };
SwapCalc.applyBondCalcResult(state, CALC, { CP: true, DP: false, YD: true });
expect(state.cleanPrice).toBe(98.5); // 手填:保持
expect(state.ytm).toBe(3.2); // 手填:保持
expect(state.dirtyPrice).toBe(100); // 未手填:被反算覆盖
});
test('手填全价+收益率(DP&YD):两者保持,仅净价被覆盖', () => {
const state = { cleanPrice: null, dirtyPrice: 101, ytm: 2.8 };
SwapCalc.applyBondCalcResult(state, CALC, { CP: false, DP: true, YD: true });
expect(state.dirtyPrice).toBe(101); // 手填:保持
expect(state.ytm).toBe(2.8); // 手填:保持
expect(state.cleanPrice).toBe(99); // 未手填:被反算覆盖
});
});
describe('边界:计算器未返回的值不覆盖、无变化不写', () => {
test('手填净价 且 计算器未返收益率:源与缺失值均不写', () => {
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); // 计算器没返收益率 → 保持用户值
SwapCalc.applyBondCalcResult(state, partial, 'CP');
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 → 不写
SwapCalc.applyBondCalcResult(state, near, 'CP');
expect(state.cleanPrice).toBe(99.5); // :不写
expect(state.dirtyPrice).toBe(100.0); // 差异 1e-5 < EPS → 不写
expect(state.ytm).toBe(2.5);
});
// 模拟 calcBondForItemproxy 统一用【展示态】,模型字段是【存储态】,回写时 bondCalcPriceToStorage。
test('浮动腿 item 写回:仅未手填字段被覆盖,且存储态转换正确', () => {
test('浮动腿 item 写回:仅非源字段被覆盖,且存储态转换正确', () => {
// 用户输入净价 99.5 → vue-number-input(percent:true) 存为存储态 0.995
const item = { PosiNetNoFeePrice: 0.995, PosiGrossPrice: 1.0, InitYtm: 0.026,
bondDriverType: 'CP', bondManual: { CP: true, DP: false, YD: false } };
const item = { PosiNetNoFeePrice: 0.995, PosiGrossPrice: 1.0, InitYtm: 0.026, bondDriverType: 'CP' };
const calc = { cleanPrice: 99, dirtyPrice: 100, ytm: 2.5 }; // 计算器返回展示态
const proxy = {
cleanPrice: SwapCalc.bondPriceToCalc(item.PosiNetNoFeePrice), // 0.995 → 99.5手填
cleanPrice: SwapCalc.bondPriceToCalc(item.PosiNetNoFeePrice), // 0.995 → 99.5
dirtyPrice: SwapCalc.bondPriceToCalc(item.PosiGrossPrice), // 1.0 → 100
ytm: SwapCalc.bondPriceToCalc(item.InitYtm) // 0.026 → 2.6
};
SwapCalc.applyBondCalcResult(proxy, calc, item.bondManual);
SwapCalc.applyBondCalcResult(proxy, calc, 'CP');
item.PosiNetNoFeePrice = SwapCalc.bondCalcPriceToStorage(proxy.cleanPrice);
item.PosiGrossPrice = SwapCalc.bondCalcPriceToStorage(proxy.dirtyPrice);
item.InitYtm = SwapCalc.bondCalcPriceToStorage(proxy.ytm);
expect(item.PosiNetNoFeePrice).toBe(0.995); // 手填(净价):存储态保持 0.995(界面仍显示 99.5)
expect(item.PosiNetNoFeePrice).toBe(0.995); // (净价):存储态保持 0.995(界面仍显示 99.5)
expect(item.PosiGrossPrice).toBe(1.0); // 全价:被反算覆盖,存储态 100/100=1.0
expect(item.InitYtm).toBe(0.025); // 收益率:被反算覆盖,存储态 2.5/100=0.025
});
// 核心回归:用户输入净价 100(存储态 1.0)时,绝不能因 proxy 残留存储态而再 ÷100 变成 0.01
test('核心回归:输入净价 100 不会被二次缩小为 1(存储态 0.01)', () => {
const item = { PosiNetNoFeePrice: 1.0, PosiGrossPrice: null, InitYtm: null,
bondDriverType: 'CP', bondManual: { CP: true, DP: false, YD: false } };
const item = { PosiNetNoFeePrice: 1.0, PosiGrossPrice: null, InitYtm: null, bondDriverType: 'CP' };
const calc = { cleanPrice: 100, dirtyPrice: 102.17928767123287, ytm: 4.860445236 };
const proxy = {
cleanPrice: SwapCalc.bondPriceToCalc(item.PosiNetNoFeePrice), // 1.0 → 100
dirtyPrice: SwapCalc.bondPriceToCalc(item.PosiGrossPrice),
ytm: SwapCalc.bondPriceToCalc(item.InitYtm)
};
SwapCalc.applyBondCalcResult(proxy, calc, item.bondManual);
SwapCalc.applyBondCalcResult(proxy, calc, 'CP');
item.PosiNetNoFeePrice = SwapCalc.bondCalcPriceToStorage(proxy.cleanPrice);
item.PosiGrossPrice = SwapCalc.bondCalcPriceToStorage(proxy.dirtyPrice);
item.InitYtm = SwapCalc.bondCalcPriceToStorage(proxy.ytm);
@@ -135,15 +103,14 @@ describe('边界:计算器未返回的值不覆盖、无变化不写', () => {
[105, 107.3, 4.567890],
[100, 102.17928767123287, 4.860445236]
])('多轮净价校验:输入净价 %s 时,存储态保持 %s/100 且不会被二次缩小', (inputClean, calcDirty, calcYtm) => {
const item = { PosiNetNoFeePrice: inputClean / 100, PosiGrossPrice: null, InitYtm: null,
bondDriverType: 'CP', bondManual: { CP: true, DP: false, YD: false } };
const item = { PosiNetNoFeePrice: inputClean / 100, PosiGrossPrice: null, InitYtm: null, bondDriverType: 'CP' };
const calc = { cleanPrice: inputClean, dirtyPrice: calcDirty, ytm: calcYtm };
const proxy = {
cleanPrice: SwapCalc.bondPriceToCalc(item.PosiNetNoFeePrice),
dirtyPrice: SwapCalc.bondPriceToCalc(item.PosiGrossPrice),
ytm: SwapCalc.bondPriceToCalc(item.InitYtm)
};
SwapCalc.applyBondCalcResult(proxy, calc, item.bondManual);
SwapCalc.applyBondCalcResult(proxy, calc, 'CP');
item.PosiNetNoFeePrice = SwapCalc.bondCalcPriceToStorage(proxy.cleanPrice);
item.PosiGrossPrice = SwapCalc.bondCalcPriceToStorage(proxy.dirtyPrice);
item.InitYtm = SwapCalc.bondCalcPriceToStorage(proxy.ytm);
@@ -212,22 +179,69 @@ describe('错误反馈:getBondCalcErrorMessage(对齐 C# BondCalcHepler 的
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 } };
test('端到端:业务错误时保留源字段、清空另两字段与标识(交互约定2 失败', () => {
const item = { PosiNetNoFeePrice: 99.5, PosiGrossPrice: 100.0, InitYtm: 2.6, bondDriverType: 'CP' };
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);
expect(err).toBe("债券不存在"); // 有错 → 调用方会 main.message(err) 并调 applyBondCalcFailure
if (err) SwapCalc.applyBondCalcFailure(item, 'CP');
expect(item.PosiNetNoFeePrice).toBe(99.5); // 源:保留用户输入
expect(item.PosiGrossPrice).toBeNull(); // 非源:清空
expect(item.InitYtm).toBeNull();
expect(item.bondDriverType).toBeNull(); // 三字段标识全清
expect(item.bondAuto).toEqual({ CP: false, DP: false, YD: false });
expect(item.bondRev).toEqual({ CP: false, DP: false, YD: false });
});
});
// ============================================================================
// 交互约定1/2/3 状态机:applyBondCalcSuccess / applyBondCalcFailure / applyBondManualEdit
// ============================================================================
describe('交互约定2/3 状态机:applyBondCalcSuccess / Failure / ManualEdit', () => {
test('applyBondCalcSuccess(CP):源=CPAUTO={DP,YD}REV 清空', () => {
const s = { bondDriverType: null, bondAuto: { CP: false, DP: false, YD: false }, bondRev: { CP: true, DP: false, YD: false } };
SwapCalc.applyBondCalcSuccess(s, 'CP');
expect(s.bondDriverType).toBe('CP');
expect(s.bondAuto).toEqual({ CP: false, DP: true, YD: true });
expect(s.bondRev).toEqual({ CP: false, DP: false, YD: false });
});
test('applyBondCalcSuccess(DP):源=DPAUTO={CP,YD}', () => {
const s = {};
SwapCalc.applyBondCalcSuccess(s, 'DP');
expect(s.bondDriverType).toBe('DP');
expect(s.bondAuto).toEqual({ CP: true, DP: false, YD: true });
});
test('applyBondCalcFailure(CP):源值保留,另两字段清空,标识全清', () => {
const s = { PosiNetNoFeePrice: 99.5, PosiGrossPrice: 100, InitYtm: 2.6,
bondDriverType: 'CP', bondAuto: { CP: false, DP: true, YD: true }, bondRev: { CP: false, DP: false, YD: false } };
SwapCalc.applyBondCalcFailure(s, 'CP');
expect(s.PosiNetNoFeePrice).toBe(99.5); // 源:保留
expect(s.PosiGrossPrice).toBeNull(); // 非源:清空
expect(s.InitYtm).toBeNull();
expect(s.bondDriverType).toBeNull();
expect(s.bondAuto).toEqual({ CP: false, DP: false, YD: false });
expect(s.bondRev).toEqual({ CP: false, DP: false, YD: false });
});
test('applyBondManualEdit(DP):清 源/AUTO,仅 DP 标 REV(交互约定3', () => {
const s = { bondDriverType: 'CP', bondAuto: { CP: false, DP: true, YD: true }, bondRev: { CP: false, DP: false, YD: false } };
SwapCalc.applyBondManualEdit(s, 'DP');
expect(s.bondDriverType).toBeNull();
expect(s.bondAuto).toEqual({ CP: false, DP: false, YD: false });
expect(s.bondRev).toEqual({ CP: false, DP: true, YD: false }); // 仅本字段 REV
});
test('全价回车后净价再回车:净价成新源、全价降级为 AUTO(最后回车者恒为源)', () => {
const s = { PosiNetNoFeePrice: null, PosiGrossPrice: null, InitYtm: null,
bondDriverType: null, bondAuto: { CP: false, DP: false, YD: false }, bondRev: { CP: false, DP: false, YD: false } };
SwapCalc.applyBondCalcSuccess(s, 'DP'); // 全价回车
expect(s.bondDriverType).toBe('DP');
expect(s.bondAuto).toEqual({ CP: true, DP: false, YD: true });
SwapCalc.applyBondCalcSuccess(s, 'CP'); // 净价再回车
expect(s.bondDriverType).toBe('CP');
expect(s.bondAuto).toEqual({ CP: false, DP: true, YD: true }); // 全价从源降级为 AUTO
});
});
+44 -46
View File
@@ -293,79 +293,77 @@ describe('交叉校验:对齐 C# FrontendCalcCharacterizationTest 金标准',
});
// ============================================================================
// D1 回归守卫:切换债券标的必须清空手动/源标志,避免旧债券手填状态污染新债券
// 旧 bugsetUnderlyingCode 切债券时未重置 bondManual/bondDriverType
// 旧债券标记过的字段在新债券上会被错误跳过/沿用旧态。
// D1 回归守卫:切换债券标的必须清空 源/AUTO/REV 标志,避免旧债券状态污染新债券
// 旧 bugsetUnderlyingCode 切债券时未重置状态,旧债券标记过的字段在新债券上会被错误沿用。
// 修复:setUnderlyingCode 调 SwapCalc.clearBondCalcFlags(item)。
// ============================================================================
describe('D1 切换标的清空债券互算手动/源标志', () => {
test('clearBondCalcFlags 把 bondManual 三字段归 false、bondDriverType 归 null', () => {
describe('D1 切换标的清空债券互算源/AUTO/REV 标志', () => {
test('clearBondCalcFlags 把 bondDriverType/bondAuto/bondRev 全清', () => {
const item = {
bondManual: { CP: true, DP: false, YD: true },
bondDriverType: 'YD'
bondDriverType: 'YD',
bondAuto: { CP: true, DP: true, YD: false },
bondRev: { CP: false, DP: false, YD: true }
};
const out = SwapCalc.clearBondCalcFlags(item);
expect(out.bondManual).toEqual({ CP: false, DP: false, YD: false });
expect(out.bondDriverType).toBeNull();
expect(out.bondAuto).toEqual({ CP: false, DP: false, YD: false });
expect(out.bondRev).toEqual({ CP: false, DP: false, YD: false });
});
test('clearBondCalcFlags 对全新未交互标的(无标志)也安全初始化', () => {
const item = { UnderlyingCode: '200000.IB', isBond: true };
const out = SwapCalc.clearBondCalcFlags(item);
expect(out.bondManual).toEqual({ CP: false, DP: false, YD: false });
expect(out.bondDriverType).toBeNull();
expect(out.bondAuto).toEqual({ CP: false, DP: false, YD: false });
expect(out.bondRev).toEqual({ CP: false, DP: false, YD: false });
expect(out.UnderlyingCode).toBe('200000.IB'); // 其它字段不受影响
});
test('模拟"债券A手填→切债券B":B 不应继承 A 的手动标志', () => {
// 债券 A:用户手填了全价,标记手动 + 设源
test('模拟"债券A回车→切债券B":B 不应继承 A 的源/AUTO/REV', () => {
// 债券 A:用户回车了全价,标记 + AUTO
const item = { UnderlyingCode: '190000.IB', isBond: true,
bondManual: { CP: false, DP: true, YD: false }, bondDriverType: 'DP' };
bondDriverType: 'DP',
bondAuto: { CP: true, DP: false, YD: true },
bondRev: { CP: false, DP: false, YD: false } };
// 切到债券 BsetUnderlyingCode 会调 clearBondCalcFlags
SwapCalc.clearBondCalcFlags(item);
item.UnderlyingCode = '200000.IB';
// 若不清空,applyBondCalcResult 会以旧的 bondManual.DP=true 跳过 B 的全价→错误
const manual = item.bondManual;
expect(manual.CP || manual.DP || manual.YD).toBe(false); // B 上无任何手动标志
expect(item.bondDriverType).toBeNull(); // B 无计算源,下一步反算不会误用 A 的源
expect(item.bondDriverType).toBeNull(); // B 无计算源
expect(item.bondAuto).toEqual({ CP: false, DP: false, YD: false });
expect(item.bondRev).toEqual({ CP: false, DP: false, YD: false });
});
});
// ============================================================================
// D2 回归守卫:重开(审批重开/刷新)已保存的债券成交单时,三字段互算的手动标志
// 随页面重置丢失 → 用户一旦编辑任一价格字段就会以它为源重新反算、覆盖当初保存的其他两格
// 修复:加载路径对"债券且三字段齐全(净价/全价/收益率均有值)"的标的,调 markExistingBondManual
// 把三格一次性锁为手动(true),重开期间计算器不再自动推导;点"重算"才清除重新计算
// D2 回归守卫:重开(审批重开/刷新)已保存的债券成交单时,三字段互算标识随页面重置丢失 →
// 加载路径调 clearBondCalcMarksOnLoad 把 源/AUTO/REV 全部清空,纯展示保存值、无锁定
// 交互约定3规定"编辑未回车不联动",故用户载入后即使改某格(未回车)也不会反算另两格;
// 只有用户主动回车某格时,才以该格为源重新推导(符合交互约定1)
// ============================================================================
describe('D2 重开已保存债券单 → 三字段锁定、编辑不联动另两格', () => {
test('markExistingBondManual 把三字段全锁 true、不置计算源', () => {
const item = { isBond: true, PosiNetNoFeePrice: 0.995, PosiGrossPrice: 1.0, InitYtm: 0.026 };
const out = SwapCalc.markExistingBondManual(item);
expect(out.bondManual).toEqual({ CP: true, DP: true, YD: true });
expect(out.bondDriverType).toBeUndefined(); // 不置源 → calcBondForItem 早返回,重开零自动推导
describe('D2 重开已保存债券单 → 三字段无标识、编辑不联动另两格', () => {
test('clearBondCalcMarksOnLoad 把 源/AUTO/REV 全清、不置计算源、不动价格值', () => {
const item = { isBond: true, PosiNetNoFeePrice: 0.995, PosiGrossPrice: 1.0, InitYtm: 0.026,
bondDriverType: 'CP',
bondAuto: { CP: false, DP: true, YD: true },
bondRev: { CP: false, DP: false, YD: false } };
const out = SwapCalc.clearBondCalcMarksOnLoad(item);
expect(out.bondDriverType).toBeNull();
expect(out.bondAuto).toEqual({ CP: false, DP: false, YD: false });
expect(out.bondRev).toEqual({ CP: false, DP: false, YD: false });
expect(out.PosiNetNoFeePrice).toBe(0.995); // 价格值本身不动,仅清标识
expect(out.PosiGrossPrice).toBe(1.0);
});
test('重开后编辑某一格(如全价):另两格因 manual=true 被 applyBondCalcResult 跳过、不被覆盖', () => {
test('重开后编辑某一格(未回车):交互约定3 使另两格不被反算覆盖', () => {
const item = { isBond: true, PosiNetNoFeePrice: 0.995, PosiGrossPrice: 1.0, InitYtm: 0.026 };
SwapCalc.markExistingBondManual(item);
// 模拟用户编辑全价(DP)onBondPriceInput 会置 bondDriverType='DP' 并重算
item.bondDriverType = 'DP';
const proxy = { cleanPrice: 99.5, dirtyPrice: 100.0, ytm: 6.37 };
const calc = { cleanPrice: 97.82, dirtyPrice: 98.5, ytm: 2.60 };
SwapCalc.applyBondCalcResult(proxy, calc, item.bondManual);
// 三格皆 manual=true → 一个都不回写,保存值(99.5/100.0/6.37)原样保留
expect(proxy.cleanPrice).toBe(99.5);
expect(proxy.dirtyPrice).toBe(100.0);
expect(proxy.ytm).toBe(6.37);
});
test('加载路径不会误锁"全新未填的债券"(三值不全)', () => {
// 全新债券标的,价格字段空 → 不应被标记为手动锁定
const item = { isBond: true, PosiNetNoFeePrice: null, PosiGrossPrice: null, InitYtm: null };
SwapCalc.markExistingBondManual(item);
// 全新标的本不应调用 markExistingBondManual;此处断言:即便误调也不应制造假锁定干扰后续交互
// (实际加载逻辑用 hasV 三值齐全判定,只在已保存单上调用,此用例验证函数本身不副作用其它字段)
expect(item.UnderlyingCode).toBeUndefined();
SwapCalc.clearBondCalcMarksOnLoad(item);
// 模拟用户编辑全价(DP)但未回车onBondPriceEdit 调 applyBondManualEdit → 清源/AUTO、DP 标 REV,不调计算器
SwapCalc.applyBondManualEdit(item, 'DP');
expect(item.bondDriverType).toBeNull(); // 无计算源 → 不会反算
expect(item.bondAuto).toEqual({ CP: false, DP: false, YD: false });
expect(item.bondRev).toEqual({ CP: false, DP: true, YD: false });
expect(item.PosiNetNoFeePrice).toBe(0.995); // 另两格原值保留
expect(item.InitYtm).toBe(0.026);
});
});