1) otcdebug 埋点(unwindSwapTrade.js, 仅 ?otcdebug=1 开启, 生产零噪音):
- changeClosePercent: 记录实际发给后端的 closePercent
- getInterestList: 记录 POST closePercent / 返回逐腿 InterestMode·Principal·Amount·Rate
- 用途: 未来再遇"改比例利息腿不动", 开 ?otcdebug=1 即可定位前端没传对还是后端没缩放
2) 修复分支上既有3个红测试(均为配置/生产已改、测试未跟上或契约过时):
- swapPrecisionConfig.test.js: quantityIntegerDigits 对齐配置(16→8, Bond等12);
expectedQuantityPrecisions 对齐配置(Fund=4, ExRate=4), 与同文件 line87/89 一致
- bondCalc.integration.test.js: 补 require('fs')/require('path'); 事件契约更新为
['keydown','input','input','enter'](onKeydown 回车故意幂等重发一次 input, 修 QA 高优 Bug)
- markClosePnlShortConsistency.test.js: 空头一致性测试由 RED 转 GREEN(income 已补 longRatio);
CASE deliveryPrice 105→1.05 与 GOLD 的 105*scale(0.01) 单位对齐
引入溯源: 固定值腿缩放缺陷见 d1badfe4; 精度配置 16→8 来自 0672dbbc(张名锐);
红测试为已知缺口记录(用户 hjhan)。
918 lines
40 KiB
JavaScript
918 lines
40 KiB
JavaScript
/**
|
||
* bondCalc.integration.test.js — 债券三字段互算集成层测试
|
||
* ============================================================================
|
||
* 目的:覆盖 swapCalc.js 纯函数与宿主框架(Vue/jQuery/main.post)之间的"胶水代码",
|
||
* 守卫纯函数测试(bondCalc.test.js)无法触及的集成层断裂点。
|
||
*
|
||
* 覆盖的根因(见 项目文档/互换债券三字段互算踩坑总结与测试指南.md):
|
||
* 根因1:vue-number-input .native 修饰符被 jQuery 拦截 → keydown 事件不触发
|
||
* 根因2:Vue 2 非 data() 属性不可响应 → 直接赋值不触发视图更新
|
||
* 根因3:$set 设置相同对象引用不触发更新 → 需创建新对象引用
|
||
* 根因4:main.post 业务错误走 reject → .done() 不执行失败分支
|
||
*
|
||
* 运行:cd YLErpWeb/fe-tests && npx jest bondCalc.integration
|
||
*/
|
||
|
||
const fs = require('fs');
|
||
const path = require('path');
|
||
const { JSDOM } = require('jsdom');
|
||
const dom = new JSDOM('<!DOCTYPE html><html><body></body></html>');
|
||
global.window = dom.window;
|
||
global.document = dom.window.document;
|
||
global.navigator = dom.window.navigator;
|
||
|
||
// 桥接 FastVue 全局
|
||
Object.defineProperty(global, 'FastVue', {
|
||
get() { return global.window.FastVue; },
|
||
set(value) { global.window.FastVue = value; },
|
||
configurable: true
|
||
});
|
||
|
||
const $ = require('jquery');
|
||
global.$ = $;
|
||
global.jQuery = $;
|
||
|
||
// main mock(含 formatNumber + post 的 Deferred 模拟)
|
||
global.main = {
|
||
formatNumber: function (number, precision, options) {
|
||
const n = Number(number);
|
||
if (Number.isNaN(n)) return '0';
|
||
const useGrouping = options && options.grouping;
|
||
if (useGrouping) {
|
||
return n.toLocaleString('en-US', { minimumFractionDigits: precision, maximumFractionDigits: precision });
|
||
}
|
||
return n.toFixed(precision);
|
||
},
|
||
message: jest.fn(),
|
||
post: null // 各测试块自行设置
|
||
};
|
||
|
||
const SwapCalc = require('../wwwroot/Scripts/app/swaptrade/swapCalc.js');
|
||
|
||
describe('TradeEdit bond price input bindings', () => {
|
||
const view = fs.readFileSync(path.join(__dirname, '..', 'Views', 'SwapTrade2', 'TradeEdit.cshtml'), 'utf8');
|
||
const methods = fs.readFileSync(path.join(__dirname, '..', 'wwwroot', 'Scripts', 'app', 'swaptrade', 'swapTradeEdit.js'), 'utf8');
|
||
const precisionHelper = fs.readFileSync(path.join(__dirname, '..', 'wwwroot', 'Scripts', 'app', 'swaptrade', 'swapPricePrecisionHelper.js'), 'utf8');
|
||
|
||
test('binds the three bond price fields to current Vue methods', () => {
|
||
expect(view).toMatch(/v-model="item\.PosiGrossPrice"[^>]*v-on:input="onDpPriceInput\(item\)"/);
|
||
expect(view).toMatch(/v-model="item\.PosiNetNoFeePrice"[^>]*v-on:input="onBondPriceEdit\(item,'CP'\)"/);
|
||
expect(view).toMatch(/v-model="item\.InitYtm"[^>]*v-on:input="onBondPriceEdit\(item,'YD'\)"/);
|
||
expect(view).not.toContain('v-on:input="onBondPriceInput(');
|
||
expect(methods).toMatch(/^\s*onBondPriceEdit\s*\(/m);
|
||
});
|
||
|
||
test('precision input preserves the keydown, input, enter event contract', () => {
|
||
const helper = new Function('window', precisionHelper + '\nreturn swapPricePrecision;')({});
|
||
const component = helper.createVueInputComponent();
|
||
expect(component.template).toContain('@blur="onChange"');
|
||
expect(component.template).not.toContain('@change="onChange"');
|
||
const emitted = [];
|
||
const vm = {
|
||
text: '99.5',
|
||
enterPressed: false,
|
||
format: { precision: 4, percent: true },
|
||
$emit: (event, value) => emitted.push({ event, value })
|
||
};
|
||
Object.keys(component.methods).forEach(name => {
|
||
vm[name] = component.methods[name].bind(vm);
|
||
});
|
||
const target = {
|
||
value: vm.text,
|
||
blur: () => vm.onChange({ target })
|
||
};
|
||
|
||
vm.onKeydown({ keyCode: 13, target });
|
||
|
||
// onKeydown 回车时故意再派发一次 input:event.target.blur() 触发 onChange 派发一次,
|
||
// onKeydown 又直接派发一次(见 swapPricePrecisionHelper.js,注释标明"幂等无害"),
|
||
// 用于修复「重输/重贴原值 + 回车」静默不计算的 QA 高优 Bug。
|
||
expect(emitted.map(x => x.event)).toEqual(['keydown', 'input', 'input', 'enter']);
|
||
expect(emitted[1].value).toBe('0.995');
|
||
expect(emitted[2].value).toBe('0.995');
|
||
expect(emitted[3].value).toBe('0.995');
|
||
});
|
||
|
||
test('equal format refresh does not overwrite uncommitted input', () => {
|
||
const helper = new Function('window', precisionHelper + '\nreturn swapPricePrecision;')({});
|
||
const component = helper.createVueInputComponent();
|
||
const format = { precision: 4, percent: true };
|
||
const vm = {
|
||
value: '0.995',
|
||
text: '99.5',
|
||
format,
|
||
formatSnapshot: JSON.stringify(format),
|
||
$emit: () => {}
|
||
};
|
||
Object.keys(component.methods).forEach(name => {
|
||
vm[name] = component.methods[name].bind(vm);
|
||
});
|
||
|
||
vm.onInput({ target: { value: '99.51' } });
|
||
component.watch.format.handler.call(vm);
|
||
|
||
expect(vm.text).toBe('99.51');
|
||
});
|
||
});
|
||
|
||
// ============================================================================
|
||
// 根因 1:vue-number-input keydown 事件 emit 链路
|
||
// ============================================================================
|
||
// 背景:vue-number-input 在 mounted() 中用 jQuery $(_el).on('keydown', __keyHandle)
|
||
// 重新绑定了 keydown。.native 修饰符依赖 Vue 的根元素事件代理,
|
||
// 但 jQuery 绑定后原生事件不再冒泡到 Vue 的事件代理层,
|
||
// 导致 v-on:keydown.native 收不到事件。
|
||
// 修复:在 mounted() 内部 this.$el.addEventListener('keydown', fn) + self.$emit('keydown', e),
|
||
// 绕过 jQuery 事件系统直接 emit。
|
||
// 此测试验证修复后的 emit 链路是否正常。
|
||
describe('根因1:vue-number-input keydown 事件 emit 链路', () => {
|
||
|
||
beforeEach(() => {
|
||
jest.resetModules();
|
||
// 重新加载 fastVue.base.js 获取最新组件定义
|
||
delete global.window.FastVue;
|
||
// 需要重新 require 以触发 IIFE 注册 FastVue
|
||
require('../wwwroot/Scripts/fast/fastVue.base.js');
|
||
});
|
||
|
||
/**
|
||
* 模拟 vueNumberInput 组件的 mounted() 钩子行为。
|
||
* 由于 jest 环境没有完整 Vue 运行时,这里手动执行 mounted 中的关键逻辑:
|
||
* addEventListener('keydown') + $emit('keydown')
|
||
*/
|
||
function mountComponent(el) {
|
||
const emitted = [];
|
||
const self = {
|
||
$el: el,
|
||
$emit(event, payload) { emitted.push({ event, payload }); },
|
||
onchange: function () {},
|
||
format: { precision: 2, percent: false, append: '' }
|
||
};
|
||
|
||
// 复刻 mounted() 中的 keydown 监听(修复后的代码)
|
||
el.addEventListener('keydown', function (e) {
|
||
self.$emit('keydown', e);
|
||
});
|
||
|
||
return { self, emitted };
|
||
}
|
||
|
||
test('按键(数字2)时组件应 $emit("keydown") —— 验证修复后事件链路正常', () => {
|
||
const el = document.createElement('input');
|
||
document.body.appendChild(el);
|
||
const { emitted } = mountComponent(el);
|
||
|
||
const keydown = new window.KeyboardEvent('keydown', { keyCode: 50, bubbles: true });
|
||
el.dispatchEvent(keydown);
|
||
|
||
expect(emitted.length).toBeGreaterThanOrEqual(1);
|
||
expect(emitted[0].event).toBe('keydown');
|
||
});
|
||
|
||
test('回车键(keyCode=13)时也 $emit("keydown") —— 回车除外逻辑在父组件而非组件内部', () => {
|
||
const el = document.createElement('input');
|
||
document.body.appendChild(el);
|
||
const { emitted } = mountComponent(el);
|
||
|
||
const keydown = new window.KeyboardEvent('keydown', { keyCode: 13, bubbles: true });
|
||
el.dispatchEvent(keydown);
|
||
|
||
// 组件内部不区分键码,一律 emit;回车除外逻辑由父组件 onBondPriceKeydown 处理
|
||
expect(emitted.length).toBeGreaterThanOrEqual(1);
|
||
expect(emitted[0].event).toBe('keydown');
|
||
});
|
||
|
||
test('修饰键(Ctrl+V)也 $emit("keydown") —— 修饰键除外逻辑在父组件', () => {
|
||
const el = document.createElement('input');
|
||
document.body.appendChild(el);
|
||
const { emitted } = mountComponent(el);
|
||
|
||
const keydown = new window.KeyboardEvent('keydown', {
|
||
keyCode: 86, ctrlKey: true, bubbles: true
|
||
});
|
||
el.dispatchEvent(keydown);
|
||
|
||
expect(emitted.length).toBeGreaterThanOrEqual(1);
|
||
});
|
||
|
||
test('模拟 .native 修饰符失效场景:jQuery 重绑后原生 addEventListener 仍能收到事件', () => {
|
||
const el = document.createElement('input');
|
||
document.body.appendChild(el);
|
||
|
||
// 模拟 jQuery $(_el).on('keydown', __keyHandle) 重绑 keydown
|
||
// 这会覆盖 Vue .native 的事件代理,但不影响直接 addEventListener
|
||
$(el).on('keydown', function () { /* jQuery handler */ });
|
||
|
||
const { emitted } = mountComponent(el);
|
||
|
||
// 即使 jQuery 重绑了 keydown,addEventListener 仍能收到事件
|
||
const keydown = new window.KeyboardEvent('keydown', { keyCode: 50, bubbles: true });
|
||
el.dispatchEvent(keydown);
|
||
|
||
expect(emitted.length).toBeGreaterThanOrEqual(1);
|
||
expect(emitted[0].event).toBe('keydown');
|
||
});
|
||
});
|
||
|
||
// ============================================================================
|
||
// 根因 2+3:Vue 2 响应式——$set 新引用 vs 旧引用 vs 直接赋值
|
||
// ============================================================================
|
||
// 背景:swapCalc.js 的纯函数直接对 state 赋值(state.bondDriverType = null 等),
|
||
// 但 bondDriverType/bondAuto/bondRev 不在 Vue data() 中声明,是非响应式属性。
|
||
// Vue 2 基于 Object.defineProperty,只能追踪初始化时已存在的属性。
|
||
// 直接赋值不触发视图更新;$set 可注册响应式,但传入相同引用时 Vue 跳过更新。
|
||
// 修复:syncBondFlags 每次创建全新对象引用 + $forceUpdate 兜底。
|
||
describe('根因2+3:Vue 2 响应式——$set 新引用 vs 旧引用 vs 直接赋值', () => {
|
||
|
||
/**
|
||
* 模拟 Vue 2 的响应式系统行为。
|
||
* Vue 2 用 Object.defineProperty,只能追踪已存在的属性。
|
||
* $set 可以添加新响应式属性,但对相同引用的对象会跳过更新。
|
||
*/
|
||
function createReactiveMock() {
|
||
// trackKeys: 已被 $set 注册为响应式的 (target, key) 对
|
||
const tracked = new WeakMap(); // target -> Set of keys
|
||
const updateLog = [];
|
||
|
||
function isTracked(target, key) {
|
||
const keys = tracked.get(target);
|
||
return !!(keys && keys.has(key));
|
||
}
|
||
function markTracked(target, key) {
|
||
if (!tracked.has(target)) tracked.set(target, new Set());
|
||
tracked.get(target).add(key);
|
||
}
|
||
|
||
const vm = {
|
||
$set(target, key, value) {
|
||
const oldVal = target[key];
|
||
const alreadyTracked = isTracked(target, key);
|
||
|
||
// Vue 2 $set 的核心行为:
|
||
// 1. 如果属性尚未被注册为响应式 → 注册 + 赋值 + 触发更新(即使值相同)
|
||
// 2. 如果属性已是响应式 → 按相同引用/值跳过优化
|
||
if (alreadyTracked && oldVal === value) {
|
||
updateLog.push({ key, skipped: true, reason: 'same_val' });
|
||
return;
|
||
}
|
||
|
||
if (!alreadyTracked) {
|
||
markTracked(target, key);
|
||
}
|
||
target[key] = value;
|
||
updateLog.push({ key, skipped: false, oldVal: oldVal, newVal: value });
|
||
},
|
||
$forceUpdate() {
|
||
updateLog.push({ key: '__forceUpdate', skipped: false });
|
||
},
|
||
getUpdateLog() { return updateLog; },
|
||
isReactive(target, key) {
|
||
// 支持两种调用方式:isReactive(key) 或 isReactive(target, key)
|
||
if (arguments.length >= 2) return isTracked(target, key);
|
||
// 兼容旧签名:遍历 tracked 查找(仅用于无 target 的场景)
|
||
return false;
|
||
}
|
||
};
|
||
|
||
return vm;
|
||
}
|
||
|
||
function makeBondItem() {
|
||
return {
|
||
PosiNetNoFeePrice: 0.995,
|
||
PosiGrossPrice: 1.0,
|
||
InitYtm: 0.026,
|
||
isBond: true,
|
||
UnderlyingCode: '190000.IB',
|
||
// bondDriverType/bondAuto/bondRev 初始不存在(模拟后端数据无这些字段)
|
||
};
|
||
}
|
||
|
||
test('根因2:直接赋值 bondDriverType 不注册响应式 → 视图不更新', () => {
|
||
const item = makeBondItem();
|
||
const vm = createReactiveMock();
|
||
|
||
// 模拟 swapCalc 纯函数直接赋值(无 $set)
|
||
SwapCalc.applyBondManualEdit(item, 'CP');
|
||
|
||
// item 上有了属性,但 Vue 不知道它是响应式的
|
||
expect(item.bondDriverType).toBeNull();
|
||
expect(item.bondRev).toEqual({ CP: true, DP: false, YD: false });
|
||
// 未通过 $set 注册 → 不响应式
|
||
expect(vm.isReactive(item, 'bondDriverType')).toBe(false);
|
||
});
|
||
|
||
test('根因2修复:$set 注册 bondDriverType 为响应式', () => {
|
||
const item = makeBondItem();
|
||
const vm = createReactiveMock();
|
||
|
||
SwapCalc.applyBondManualEdit(item, 'CP');
|
||
|
||
// applyBondManualEdit 已直接赋值 bondDriverType=null(非响应式)
|
||
expect(item.bondDriverType).toBeNull();
|
||
|
||
// 模拟 syncBondFlags 的 $set 调用:首次 $set 注册为响应式
|
||
vm.$set(item, 'bondDriverType', item.bondDriverType === undefined ? null : item.bondDriverType);
|
||
|
||
expect(vm.isReactive(item, 'bondDriverType')).toBe(true);
|
||
const log = vm.getUpdateLog();
|
||
expect(log[0].skipped).toBe(false); // 首次注册,即使值相同也触发
|
||
});
|
||
|
||
test('根因3:$set 传入相同引用 → Vue 跳过更新(bondAuto 对象)', () => {
|
||
const item = makeBondItem();
|
||
// 先初始化 bondAuto
|
||
item.bondAuto = { CP: false, DP: true, YD: true };
|
||
const vm = createReactiveMock();
|
||
// 第一次 $set 注册
|
||
vm.$set(item, 'bondAuto', item.bondAuto);
|
||
|
||
// 纯函数原地修改 bondAuto(创建新对象)
|
||
SwapCalc.applyBondManualEdit(item, 'CP');
|
||
// 现在 item.bondAuto 是新对象 { CP: true, DP: false, YD: false }
|
||
|
||
// 如果错误地传入相同引用(模拟旧版 syncBondFlags bug)
|
||
const sameRef = item.bondAuto;
|
||
vm.$set(item, 'bondAuto', sameRef);
|
||
|
||
const log = vm.getUpdateLog();
|
||
const lastSet = log[log.length - 1];
|
||
expect(lastSet.skipped).toBe(true); // 相同引用 → 跳过
|
||
expect(lastSet.reason).toBe('same_val');
|
||
});
|
||
|
||
test('根因3修复:$set 传入新对象引用 → Vue 触发更新', () => {
|
||
const item = makeBondItem();
|
||
item.bondAuto = { CP: false, DP: true, YD: true };
|
||
const vm = createReactiveMock();
|
||
// 第一次 $set 注册为响应式
|
||
vm.$set(item, 'bondAuto', item.bondAuto);
|
||
|
||
// 纯函数原地修改(applyBondManualEdit 创建新对象赋给 state.bondAuto)
|
||
SwapCalc.applyBondManualEdit(item, 'CP');
|
||
// item.bondAuto 现在是新对象 { CP: true, DP: false, YD: false }
|
||
const afterEdit = item.bondAuto;
|
||
|
||
// 修复后的 syncBondFlags:每次创建全新对象字面量
|
||
const newRef = { CP: !!item.bondAuto.CP, DP: !!item.bondAuto.DP, YD: !!item.bondAuto.YD };
|
||
expect(newRef).not.toBe(afterEdit); // 新引用确实与旧引用不同
|
||
|
||
vm.$set(item, 'bondAuto', newRef);
|
||
|
||
const log = vm.getUpdateLog();
|
||
const lastSet = log[log.length - 1];
|
||
expect(lastSet.skipped).toBe(false); // 不同引用 → 触发更新
|
||
});
|
||
|
||
test('完整 syncBondFlags 模拟:$set + $forceUpdate 全链路', () => {
|
||
const item = makeBondItem();
|
||
const vm = createReactiveMock();
|
||
|
||
// 模拟 swapCalc.applyBondManualEdit(直接赋值,非响应式)
|
||
SwapCalc.applyBondManualEdit(item, 'DP');
|
||
|
||
// 模拟修复后的 syncBondFlags:$set 首次注册(都应触发更新)+ $forceUpdate
|
||
vm.$set(item, 'bondDriverType', item.bondDriverType === undefined ? null : item.bondDriverType);
|
||
vm.$set(item, 'bondAuto', { CP: !!item.bondAuto.CP, DP: !!item.bondAuto.DP, YD: !!item.bondAuto.YD });
|
||
vm.$set(item, 'bondRev', { CP: !!item.bondRev.CP, DP: !!item.bondRev.DP, YD: !!item.bondRev.YD });
|
||
vm.$forceUpdate();
|
||
|
||
const log = vm.getUpdateLog();
|
||
// 3 个 $set 都是首次注册(属性之前不是响应式)→ 都不跳过 + 1 个 forceUpdate
|
||
const sets = log.filter(e => e.key !== '__forceUpdate');
|
||
expect(sets.length).toBe(3);
|
||
sets.forEach(s => expect(s.skipped).toBe(false));
|
||
expect(log.some(e => e.key === '__forceUpdate')).toBe(true);
|
||
});
|
||
|
||
test('连续两次 applyBondManualEdit + syncBondFlags:第二次也触发更新(新引用)', () => {
|
||
const item = makeBondItem();
|
||
const vm = createReactiveMock();
|
||
|
||
// 第一次:CP 标 REV
|
||
SwapCalc.applyBondManualEdit(item, 'CP');
|
||
vm.$set(item, 'bondRev', { CP: !!item.bondRev.CP, DP: !!item.bondRev.DP, YD: !!item.bondRev.YD });
|
||
const log1 = vm.getUpdateLog().filter(e => !e.skipped);
|
||
|
||
// 第二次:DP 标 REV
|
||
SwapCalc.applyBondManualEdit(item, 'DP');
|
||
// applyBondManualEdit 把 state.bondRev 重新赋为新对象 { CP:false, DP:true, YD:false }
|
||
// syncBondFlags 再创建新引用 → $set 时新旧引用不同 → 触发更新
|
||
vm.$set(item, 'bondRev', { CP: !!item.bondRev.CP, DP: !!item.bondRev.DP, YD: !!item.bondRev.YD });
|
||
const log2 = vm.getUpdateLog().filter(e => !e.skipped);
|
||
|
||
// 第二次也应触发更新(新引用保证)
|
||
expect(log2.length).toBeGreaterThan(log1.length);
|
||
const lastSet = vm.getUpdateLog()[vm.getUpdateLog().length - 1];
|
||
expect(lastSet.skipped).toBe(false);
|
||
});
|
||
});
|
||
|
||
// ============================================================================
|
||
// 根因 4:main.post Promise 链——reject 走 .fail 不走 .done
|
||
// ============================================================================
|
||
// 背景:main.post 包装了 jQuery $.ajax,在 resp.success===false(业务错误)时
|
||
// 调用 deferred.reject(resp)。calcBondForItem 只挂了 .done(),reject 时不执行。
|
||
// 修复:补 .fail() 处理 reject 路径。
|
||
describe('根因4:main.post Promise 链——reject 不走 .done', () => {
|
||
|
||
/**
|
||
* 模拟 main.__post 的 resolve/reject 行为。
|
||
* 返回 jQuery Deferred promise,可链式 .done()/.fail()。
|
||
*/
|
||
function mockPost(response, shouldReject) {
|
||
const d = $.Deferred();
|
||
if (shouldReject) {
|
||
d.reject(response);
|
||
} else {
|
||
d.resolve(response);
|
||
}
|
||
return d.promise();
|
||
}
|
||
|
||
test('resolve 时 .done 被调用、.fail 不被调用', () => {
|
||
const doneFn = jest.fn();
|
||
const failFn = jest.fn();
|
||
|
||
mockPost({ success: true, obj: { cleanPrice: 99 } }, false)
|
||
.done(doneFn)
|
||
.fail(failFn);
|
||
|
||
expect(doneFn).toHaveBeenCalled();
|
||
expect(failFn).not.toHaveBeenCalled();
|
||
});
|
||
|
||
test('reject 时 .fail 被调用、.done 不被调用', () => {
|
||
const doneFn = jest.fn();
|
||
const failFn = jest.fn();
|
||
|
||
mockPost({ success: false, msg: '债券不存在' }, true)
|
||
.done(doneFn)
|
||
.fail(failFn);
|
||
|
||
expect(doneFn).not.toHaveBeenCalled();
|
||
expect(failFn).toHaveBeenCalled();
|
||
});
|
||
|
||
test('仅挂 .done(旧 bug):reject 时失败分支静默跳过', () => {
|
||
const doneFn = jest.fn();
|
||
const failHandlerCalled = { value: false };
|
||
|
||
// 模拟旧代码:只有 .done,没有 .fail
|
||
mockPost({ success: false, msg: '债券不存在' }, true)
|
||
.done(function () {
|
||
// 这段代码永远不会执行
|
||
failHandlerCalled.value = true;
|
||
});
|
||
|
||
expect(doneFn).not.toHaveBeenCalled();
|
||
expect(failHandlerCalled.value).toBe(false); // 失败分支被跳过!
|
||
});
|
||
|
||
test('修复后:.done + .fail 都挂 → reject 时 .fail 中的 applyBondCalcFailure 被执行', () => {
|
||
const item = {
|
||
PosiNetNoFeePrice: 0.995,
|
||
PosiGrossPrice: 1.0,
|
||
InitYtm: 0.026,
|
||
isBond: true,
|
||
bondDriverType: 'CP',
|
||
bondAuto: { CP: false, DP: true, YD: true },
|
||
bondRev: { CP: false, DP: false, YD: false }
|
||
};
|
||
|
||
// 模拟修复后的 calcBondForItem Promise 链
|
||
mockPost({ success: false, msg: '债券不存在' }, true)
|
||
.done(function () {
|
||
// 成功分支(不会执行)
|
||
SwapCalc.applyBondCalcSuccess(item, 'CP');
|
||
})
|
||
.fail(function () {
|
||
// 修复:失败分支
|
||
SwapCalc.applyBondCalcFailure(item, 'CP');
|
||
});
|
||
|
||
// 验证失败分支正确执行了 applyBondCalcFailure
|
||
expect(item.PosiGrossPrice).toBeNull(); // 非源字段被清空
|
||
expect(item.InitYtm).toBeNull(); // 非源字段被清空
|
||
expect(item.PosiNetNoFeePrice).toBe(0.995); // 源字段保留
|
||
expect(item.bondDriverType).toBeNull(); // 标识全清
|
||
expect(item.bondAuto).toEqual({ CP: false, DP: false, YD: false });
|
||
expect(item.bondRev).toEqual({ CP: false, DP: false, YD: false });
|
||
});
|
||
|
||
test('网络异常也走 reject → .fail 被调用', () => {
|
||
const failFn = jest.fn();
|
||
const doneFn = jest.fn();
|
||
|
||
// 网络异常时 jQuery $.ajax().fail(deferred.reject) 也会触发 reject
|
||
mockPost(null, true)
|
||
.done(doneFn)
|
||
.fail(failFn);
|
||
|
||
expect(doneFn).not.toHaveBeenCalled();
|
||
expect(failFn).toHaveBeenCalled();
|
||
});
|
||
});
|
||
|
||
// ============================================================================
|
||
// 约定2补充:calcBondForItem 所有失败分支都调 applyBondCalcFailure + syncBondFlags
|
||
// ============================================================================
|
||
// 验证 calcBondForItem 的每个退出路径都正确清空了非源字段和标识。
|
||
// 这些测试模拟 calcBondForItem 内部的条件判断,验证纯函数层面的正确性。
|
||
describe('约定2补充:所有失败分支都清空非源字段+清标识', () => {
|
||
|
||
function makeBondItem(driver) {
|
||
return {
|
||
PosiNetNoFeePrice: 0.995,
|
||
PosiGrossPrice: 1.0,
|
||
InitYtm: 0.026,
|
||
isBond: true,
|
||
UnderlyingCode: '190000.IB',
|
||
bondDriverType: driver,
|
||
bondAuto: { CP: driver !== 'CP', DP: driver !== 'DP', YD: driver !== 'YD' },
|
||
bondRev: { CP: false, DP: false, YD: false }
|
||
};
|
||
}
|
||
|
||
test('分支1:源字段为空/非数 → applyBondCalcFailure', () => {
|
||
const item = makeBondItem('CP');
|
||
item.PosiNetNoFeePrice = null; // 源字段为空
|
||
|
||
SwapCalc.applyBondCalcFailure(item, 'CP');
|
||
|
||
expect(item.PosiNetNoFeePrice).toBeNull(); // 源字段保留 null
|
||
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 });
|
||
});
|
||
|
||
test('分支1b:源字段为 NaN → applyBondCalcFailure', () => {
|
||
const item = makeBondItem('DP');
|
||
item.PosiGrossPrice = NaN;
|
||
|
||
SwapCalc.applyBondCalcFailure(item, 'DP');
|
||
|
||
expect(item.PosiNetNoFeePrice).toBeNull(); // 非源清空
|
||
// PosiGrossPrice 是 NaN,applyBondCalcFailure 会把它设为 null
|
||
expect(item.InitYtm).toBeNull();
|
||
expect(item.bondDriverType).toBeNull();
|
||
});
|
||
|
||
test('分支2:估值日缺失 → applyBondCalcFailure', () => {
|
||
const item = makeBondItem('CP');
|
||
|
||
// 模拟 calcBondForItem 中估值日缺失的处理
|
||
const sdMsg = SwapCalc.getBondStartDateMissingMsg(null);
|
||
expect(sdMsg).not.toBeNull();
|
||
|
||
SwapCalc.applyBondCalcFailure(item, 'CP');
|
||
|
||
expect(item.PosiNetNoFeePrice).toBe(0.995); // 源字段保留
|
||
expect(item.PosiGrossPrice).toBeNull(); // 非源清空
|
||
expect(item.InitYtm).toBeNull();
|
||
expect(item.bondDriverType).toBeNull();
|
||
});
|
||
|
||
test('分支3:计算器返回业务错误(reject) → applyBondCalcFailure(经 .fail 路径)', () => {
|
||
const item = makeBondItem('YD');
|
||
|
||
// 模拟计算器返回业务错误
|
||
const respObj = { errCode: 1, errMsg: '债券不存在', cleanPrice: 0, dirtyPrice: 0, ytm: 0 };
|
||
const err = SwapCalc.getBondCalcErrorMessage(respObj);
|
||
expect(err).toBe('债券不存在');
|
||
|
||
// .fail 中调 applyBondCalcFailure
|
||
SwapCalc.applyBondCalcFailure(item, 'YD');
|
||
|
||
expect(item.InitYtm).toBe(0.026); // 源字段保留
|
||
expect(item.PosiNetNoFeePrice).toBeNull(); // 非源清空
|
||
expect(item.PosiGrossPrice).toBeNull(); // 非源清空
|
||
expect(item.bondDriverType).toBeNull();
|
||
});
|
||
|
||
test('分支4:计算器返回成功但值域异常 → applyBondCalcFailure(经 .done 内拦截)', () => {
|
||
const item = makeBondItem('CP');
|
||
|
||
// 模拟计算器返回 errCode=0 但净价为负
|
||
const respObj = { errCode: 0, dirtyPrice: 100, cleanPrice: -117.93, ytm: 6.37 };
|
||
const err = SwapCalc.getBondCalcErrorMessage(respObj);
|
||
expect(err).not.toBeNull();
|
||
expect(err).toContain('净价');
|
||
|
||
// .done 内检测到错误 → 调 applyBondCalcFailure
|
||
SwapCalc.applyBondCalcFailure(item, 'CP');
|
||
|
||
expect(item.PosiNetNoFeePrice).toBe(0.995); // 源字段保留
|
||
expect(item.PosiGrossPrice).toBeNull(); // 非源清空
|
||
expect(item.InitYtm).toBeNull();
|
||
});
|
||
|
||
test('分支5:计算器无响应(resp.obj 为 null) → applyBondCalcFailure', () => {
|
||
const item = makeBondItem('DP');
|
||
|
||
// 模拟 .done 内 !resp || !resp.obj 分支
|
||
const resp = null;
|
||
const err = SwapCalc.getBondCalcErrorMessage(resp);
|
||
expect(err).toBe('债券计算器无响应,已保留手工输入');
|
||
|
||
SwapCalc.applyBondCalcFailure(item, 'DP');
|
||
|
||
expect(item.PosiGrossPrice).toBe(1.0); // 源字段保留
|
||
expect(item.PosiNetNoFeePrice).toBeNull(); // 非源清空
|
||
expect(item.InitYtm).toBeNull();
|
||
});
|
||
|
||
test('分支6:计算器返回哨兵值(-999999) → applyBondCalcFailure', () => {
|
||
const item = makeBondItem('CP');
|
||
|
||
const respObj = { errCode: 0, dirtyPrice: 100, cleanPrice: 99, ytm: -999999 };
|
||
const err = SwapCalc.getBondCalcErrorMessage(respObj);
|
||
expect(err).not.toBeNull();
|
||
expect(err).toContain('哨兵值');
|
||
|
||
SwapCalc.applyBondCalcFailure(item, 'CP');
|
||
|
||
expect(item.PosiNetNoFeePrice).toBe(0.995);
|
||
expect(item.PosiGrossPrice).toBeNull();
|
||
expect(item.InitYtm).toBeNull();
|
||
});
|
||
});
|
||
|
||
// ============================================================================
|
||
// 约定3完整状态机:回车→源/AUTO,编辑→REV,回车→源/AUTO(覆盖REV)
|
||
// ============================================================================
|
||
// 端到端验证约定1/2/3 的状态机转换顺序:
|
||
// 初始(无标识) → 回车CP(源CP+AUTO DP/YD) → 编辑DP(REV DP, 清源/AUTO) → 回车DP(源DP+AUTO CP/YD)
|
||
describe('约定1/2/3 状态机:端到端转换', () => {
|
||
|
||
function makeBondItem() {
|
||
return {
|
||
PosiNetNoFeePrice: 0.995,
|
||
PosiGrossPrice: 1.0,
|
||
InitYtm: 0.026,
|
||
bondDriverType: null,
|
||
bondAuto: { CP: false, DP: false, YD: false },
|
||
bondRev: { CP: false, DP: false, YD: false }
|
||
};
|
||
}
|
||
|
||
test('Step1 回车净价(CP) → 源=CP, AUTO={DP,YD}, REV 全清', () => {
|
||
const item = makeBondItem();
|
||
SwapCalc.applyBondCalcSuccess(item, 'CP');
|
||
|
||
expect(item.bondDriverType).toBe('CP');
|
||
expect(item.bondAuto).toEqual({ CP: false, DP: true, YD: true });
|
||
expect(item.bondRev).toEqual({ CP: false, DP: false, YD: false });
|
||
});
|
||
|
||
test('Step2 编辑全价(DP)未回车 → REV=DP, 清源/AUTO', () => {
|
||
const item = makeBondItem();
|
||
// 先回车 CP
|
||
SwapCalc.applyBondCalcSuccess(item, 'CP');
|
||
// 再编辑 DP(未回车)
|
||
SwapCalc.applyBondManualEdit(item, 'DP');
|
||
|
||
expect(item.bondDriverType).toBeNull(); // 源被清
|
||
expect(item.bondAuto).toEqual({ CP: false, DP: false, YD: false }); // AUTO 被清
|
||
expect(item.bondRev).toEqual({ CP: false, DP: true, YD: false }); // 仅 DP 标 REV
|
||
});
|
||
|
||
test('Step3 回车全价(DP) → 源=DP, AUTO={CP,YD}, REV 全清(REV 被覆盖)', () => {
|
||
const item = makeBondItem();
|
||
// 回车 CP
|
||
SwapCalc.applyBondCalcSuccess(item, 'CP');
|
||
// 编辑 DP(未回车)
|
||
SwapCalc.applyBondManualEdit(item, 'DP');
|
||
// 回车 DP
|
||
SwapCalc.applyBondCalcSuccess(item, 'DP');
|
||
|
||
expect(item.bondDriverType).toBe('DP'); // 新源
|
||
expect(item.bondAuto).toEqual({ CP: true, DP: false, YD: true }); // CP 降级为 AUTO
|
||
expect(item.bondRev).toEqual({ CP: false, DP: false, YD: false }); // REV 被清
|
||
});
|
||
|
||
test('Step4 回车收益率(YD) → 源=YD, AUTO={CP,DP}(最后回车者恒为源)', () => {
|
||
const item = makeBondItem();
|
||
SwapCalc.applyBondCalcSuccess(item, 'CP');
|
||
SwapCalc.applyBondCalcSuccess(item, 'DP');
|
||
SwapCalc.applyBondCalcSuccess(item, 'YD');
|
||
|
||
expect(item.bondDriverType).toBe('YD');
|
||
expect(item.bondAuto).toEqual({ CP: true, DP: true, YD: false });
|
||
});
|
||
|
||
test('Step5 计算器失败 → 保留源值、另两清空、标识全清', () => {
|
||
const item = makeBondItem();
|
||
SwapCalc.applyBondCalcSuccess(item, 'CP');
|
||
// 模拟计算器失败
|
||
SwapCalc.applyBondCalcFailure(item, 'CP');
|
||
|
||
expect(item.bondDriverType).toBeNull();
|
||
expect(item.bondAuto).toEqual({ CP: false, DP: false, YD: false });
|
||
expect(item.bondRev).toEqual({ CP: false, DP: false, YD: false });
|
||
// PosiNetNoFeePrice(源) 保留,另两被清空
|
||
});
|
||
|
||
test('Step6 加载已保存单 → 标识全清(clearBondCalcMarksOnLoad)', () => {
|
||
const item = makeBondItem();
|
||
SwapCalc.applyBondCalcSuccess(item, 'DP');
|
||
// 模拟重开/刷新
|
||
SwapCalc.clearBondCalcMarksOnLoad(item);
|
||
|
||
expect(item.bondDriverType).toBeNull();
|
||
expect(item.bondAuto).toEqual({ CP: false, DP: false, YD: false });
|
||
expect(item.bondRev).toEqual({ CP: false, DP: false, YD: false });
|
||
// 价格值保留
|
||
expect(item.PosiNetNoFeePrice).toBe(0.995);
|
||
});
|
||
|
||
test('Step7 切换标的 → 标识全清(clearBondCalcFlags)', () => {
|
||
const item = makeBondItem();
|
||
SwapCalc.applyBondCalcSuccess(item, 'CP');
|
||
SwapCalc.applyBondManualEdit(item, 'DP');
|
||
// 模拟切换标的
|
||
SwapCalc.clearBondCalcFlags(item);
|
||
|
||
expect(item.bondDriverType).toBeNull();
|
||
expect(item.bondAuto).toEqual({ CP: false, DP: false, YD: false });
|
||
expect(item.bondRev).toEqual({ CP: false, DP: false, YD: false });
|
||
});
|
||
|
||
// ---- 以下为步骤3-b 及多字段组合场景(文档明确列出但此前缺失) ----
|
||
|
||
test('Step3-b 回车CP→改DP→改YD(均不回车) → DP与YD均累计REV(修复后行为)', () => {
|
||
const item = makeBondItem();
|
||
// 步骤1:回车净价(CP)
|
||
SwapCalc.applyBondCalcSuccess(item, 'CP');
|
||
expect(item.bondDriverType).toBe('CP');
|
||
expect(item.bondRev).toEqual({ CP: false, DP: false, YD: false });
|
||
|
||
// 步骤2:编辑全价(DP)未回车 → REV={DP:true}
|
||
SwapCalc.applyBondManualEdit(item, 'DP');
|
||
expect(item.bondRev).toEqual({ CP: false, DP: true, YD: false });
|
||
|
||
// ★ 步骤3-b:不回车,又改收益率(YD) → DP的REV保留、YD也标REV(累计,不再被覆盖)
|
||
SwapCalc.applyBondManualEdit(item, 'YD');
|
||
expect(item.bondDriverType).toBeNull(); // 源已被清
|
||
expect(item.bondAuto).toEqual({ CP: false, DP: false, YD: false }); // AUTO 已清
|
||
// 核心断言:applyBondManualEdit 改为累计 REV,多字段连续手动编辑时每个改过的字段都留 REV
|
||
expect(item.bondRev).toEqual({ CP: false, DP: true, YD: true });
|
||
// 数值保持不变(约定3:编辑不联动)
|
||
expect(item.PosiNetNoFeePrice).toBe(0.995); // 净价保留
|
||
expect(item.PosiGrossPrice).toBe(1.0); // 全价保留
|
||
expect(item.InitYtm).toBe(0.026); // 收益率保留
|
||
});
|
||
|
||
test('Step3-b 变体:回车CP后连续编辑三字段(CP→DP→YD) → 三个字段均累计REV', () => {
|
||
const item = makeBondItem();
|
||
SwapCalc.applyBondCalcSuccess(item, 'CP');
|
||
|
||
// 连续编辑三个字段,都不回车
|
||
SwapCalc.applyBondManualEdit(item, 'CP'); // 先改源字段本身
|
||
expect(item.bondRev).toEqual({ CP: true, DP: false, YD: false });
|
||
|
||
SwapCalc.applyBondManualEdit(item, 'DP'); // 再改第二个 → CP的REV保留、DP也标REV
|
||
expect(item.bondRev).toEqual({ CP: true, DP: true, YD: false });
|
||
|
||
SwapCalc.applyBondManualEdit(item, 'YD'); // 最后改第三个 → 三者均累计
|
||
expect(item.bondRev).toEqual({ CP: true, DP: true, YD: true }); // 全部累计,不再"最后赢"
|
||
});
|
||
|
||
test('Step3-b 三字段轮换编辑(CP→DP→YD→CP) → 三个字段均累计REV', () => {
|
||
const item = makeBondItem();
|
||
SwapCalc.applyBondCalcSuccess(item, 'CP');
|
||
|
||
// 四次连续手工编辑,模拟用户在三个输入框之间来回跳转
|
||
SwapCalc.applyBondManualEdit(item, 'CP');
|
||
SwapCalc.applyBondManualEdit(item, 'DP');
|
||
SwapCalc.applyBondManualEdit(item, 'YD');
|
||
SwapCalc.applyBondManualEdit(item, 'CP'); // 回到第一个
|
||
|
||
// 无论轮换多少次,每次编辑的字段都累计REV(不再"只有最后一次留REV")
|
||
expect(item.bondRev).toEqual({ CP: true, DP: true, YD: true });
|
||
expect(item.bondDriverType).toBeNull();
|
||
expect(item.bondAuto).toEqual({ CP: false, DP: false, YD: false });
|
||
});
|
||
|
||
test('步骤4-1 回车CP→回车DP→改YD(不回车) → 从多源状态降级为单REV(YD)', () => {
|
||
const item = makeBondItem();
|
||
// 连续回车两个字段 → 多源状态
|
||
SwapCalc.applyBondCalcSuccess(item, 'CP');
|
||
SwapCalc.applyBondCalcSuccess(item, 'DP');
|
||
expect(item.bondDriverType).toBe('DP'); // 最后回车者为源
|
||
expect(item.bondAuto).toEqual({ CP: true, DP: false, YD: true }); // CP降级AUTO
|
||
|
||
// 编辑第三个字段但不回车 → 降级为单字段REV
|
||
SwapCalc.applyBondManualEdit(item, 'YD');
|
||
expect(item.bondDriverType).toBeNull(); // 源被清
|
||
expect(item.bondAuto).toEqual({ CP: false, DP: false, YD: false }); // AUTO全清
|
||
expect(item.bondRev).toEqual({ CP: false, DP: false, YD: true }); // 仅YD留REV
|
||
});
|
||
|
||
test('步骤4-1 变体:回车CP→回车DP→回车YD→改CP(不回车) → 最终仅CP留REV', () => {
|
||
const item = makeBondItem();
|
||
// 三字段都回车过 → YD是最终源
|
||
SwapCalc.applyBondCalcSuccess(item, 'CP');
|
||
SwapCalc.applyBondCalcSuccess(item, 'DP');
|
||
SwapCalc.applyBondCalcSuccess(item, 'YD');
|
||
expect(item.bondDriverType).toBe('YD');
|
||
|
||
// 手工编辑CP(不回车)→ 全部标识降级,仅CP留REV
|
||
SwapCalc.applyBondManualEdit(item, 'CP');
|
||
expect(item.bondDriverType).toBeNull();
|
||
expect(item.bondAuto).toEqual({ CP: false, DP: false, YD: false });
|
||
expect(item.bondRev).toEqual({ CP: true, DP: false, YD: false });
|
||
});
|
||
|
||
test('多字段连续编辑时数值始终保留、不联动(约定3核心语义)', () => {
|
||
const item = makeBondItem();
|
||
// 设定初始值
|
||
item.PosiNetNoFeePrice = 0.995; // 净价 99.5
|
||
item.PosiGrossPrice = 1.0; // 全价 100
|
||
item.InitYtm = 0.03; // 收益率 3%
|
||
|
||
// 回车CP触发计算(模拟成功——数值会被覆盖,但这里只看标识)
|
||
SwapCalc.applyBondCalcSuccess(item, 'CP');
|
||
|
||
// 连续编辑另两字段(不回车)→ 数值不应被联动修改
|
||
SwapCalc.applyBondManualEdit(item, 'DP');
|
||
expect(item.PosiNetNoFeePrice).toBe(0.995);
|
||
expect(item.PosiGrossPrice).toBe(1.0);
|
||
expect(item.InitYtm).toBe(0.03);
|
||
|
||
SwapCalc.applyBondManualEdit(item, 'YD');
|
||
expect(item.PosiNetNoFeePrice).toBe(0.995); // 净价不变
|
||
expect(item.PosiGrossPrice).toBe(1.0); // 全价不变
|
||
expect(item.InitYtm).toBe(0.03); // 收益率不变
|
||
|
||
// 再编辑回CP本身
|
||
SwapCalc.applyBondManualEdit(item, 'CP');
|
||
expect(item.PosiNetNoFeePrice).toBe(0.995); // 仍然不变
|
||
expect(item.PosiGrossPrice).toBe(1.0);
|
||
expect(item.InitYtm).toBe(0.03);
|
||
});
|
||
});
|
||
|
||
// ============================================================================
|
||
// 回归守卫:onBondPriceKeydown 的键码过滤逻辑
|
||
// ============================================================================
|
||
// 验证 onBondPriceKeydown 中"哪些键触发清标识、哪些不触发"的判断逻辑。
|
||
// 回车(13/108)除外——由 v-on:enter 驱动;修饰键(Ctrl/Alt/Meta)除外;
|
||
// Tab(9)/Home(35)/End(36)/左(37)/右(39)/F5(116)除外——不改变数值。
|
||
describe('onBondPriceKeydown 键码过滤逻辑回归', () => {
|
||
|
||
// 复刻 onBondPriceKeydown 的过滤判断(不含 Vue 调用)
|
||
function shouldClearFlags(event) {
|
||
var kc = event.which || event.keyCode;
|
||
if (kc === 13 || kc === 108) return false; // 回车
|
||
if (event.ctrlKey || event.altKey || event.metaKey) return false; // 修饰键
|
||
if (kc === 9 || kc === 35 || kc === 36 || kc === 37 || kc === 39 || kc === 116) return false; // 导航/功能键
|
||
return true; // 其余按键(数字/小数点/Backspace/Delete/减号)改变数值 → 清标识
|
||
}
|
||
|
||
test('回车键(13/108)不触发清标识', () => {
|
||
expect(shouldClearFlags({ keyCode: 13 })).toBe(false);
|
||
expect(shouldClearFlags({ keyCode: 108 })).toBe(false);
|
||
});
|
||
|
||
test('修饰键不触发清标识', () => {
|
||
expect(shouldClearFlags({ keyCode: 67, ctrlKey: true })).toBe(false); // Ctrl+C
|
||
expect(shouldClearFlags({ keyCode: 86, ctrlKey: true })).toBe(false); // Ctrl+V
|
||
expect(shouldClearFlags({ keyCode: 65, altKey: true })).toBe(false); // Alt+A
|
||
expect(shouldClearFlags({ keyCode: 82, metaKey: true })).toBe(false); // Cmd+R
|
||
});
|
||
|
||
test('导航/功能键不触发清标识', () => {
|
||
expect(shouldClearFlags({ keyCode: 9 })).toBe(false); // Tab
|
||
expect(shouldClearFlags({ keyCode: 35 })).toBe(false); // End
|
||
expect(shouldClearFlags({ keyCode: 36 })).toBe(false); // Home
|
||
expect(shouldClearFlags({ keyCode: 37 })).toBe(false); // Left
|
||
expect(shouldClearFlags({ keyCode: 39 })).toBe(false); // Right
|
||
expect(shouldClearFlags({ keyCode: 116 })).toBe(false); // F5
|
||
});
|
||
|
||
test('数字键触发清标识', () => {
|
||
expect(shouldClearFlags({ keyCode: 48 })).toBe(true); // 0
|
||
expect(shouldClearFlags({ keyCode: 57 })).toBe(true); // 9
|
||
expect(shouldClearFlags({ keyCode: 96 })).toBe(true); // Numpad 0
|
||
expect(shouldClearFlags({ keyCode: 105 })).toBe(true); // Numpad 9
|
||
});
|
||
|
||
test('Backspace(8)/Delete(46)触发清标识', () => {
|
||
expect(shouldClearFlags({ keyCode: 8 })).toBe(true); // Backspace
|
||
expect(shouldClearFlags({ keyCode: 46 })).toBe(true); // Delete
|
||
});
|
||
|
||
test('小数点(110/190)触发清标识', () => {
|
||
expect(shouldClearFlags({ keyCode: 110 })).toBe(true); // Numpad dot
|
||
expect(shouldClearFlags({ keyCode: 190 })).toBe(true); // Dot
|
||
});
|
||
|
||
test('减号(109/189)触发清标识', () => {
|
||
expect(shouldClearFlags({ keyCode: 109 })).toBe(true); // Numpad minus
|
||
expect(shouldClearFlags({ keyCode: 189 })).toBe(true); // Minus
|
||
});
|
||
});
|