Files
zszq-trs/YLErpWeb/fe-tests/closePercentInterestRefresh.test.js
T

319 lines
13 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.
/**
* closePercentInterestRefresh.test.js — 部分平仓比例变更触发利息腿刷新守卫
* ============================================================================
* 目的:验证用户从「全部平仓」切换到「部分平仓」并修改比例时,
* getInterestList() 被调用且传递的 closePercent 值是正确的(口径A)。
*
* 测试策略:
* 1. 源码守卫(wiring guard):读 unwindSwapTrade.js 源码文本,断言关键方法
* 在比例变更后确实调用 getInterestList()。
* 2. 行为模拟(behavior mock):在 vm sandbox 中加载 unwindSwapTrade.js
* mock main.post 捕获 postData,验证不同 closePercent 值被正确传递。
* ============================================================================
*/
const fs = require('fs');
const path = require('path');
const vm = require('vm');
const unwindSrcPath = path.join(__dirname, '..', 'wwwroot', 'Scripts', 'app', 'swaptrade', 'unwindSwapTrade.js');
const unwindSrc = fs.readFileSync(unwindSrcPath, 'utf8');
// ====================================================================
// 第1部分:源码守卫 — getInterestList 调用链完整性
// ====================================================================
describe('部分平仓比例变更 → getInterestList 调用链守卫', () => {
describe('changeClosePercent 必须调用 getInterestList', () => {
test('changeClosePercent 方法体中调用 this.getInterestList()', () => {
const section = unwindSrc.match(/changeClosePercent\(\)[\s\S]*?\n\s*\},/);
expect(section).toBeTruthy();
expect(section[0]).toMatch(/this\.getInterestList\(\)/);
});
});
describe('changeCloseMethod 必须调用 getInterestList', () => {
test('changeCloseMethod 方法体中调用 this.getInterestList()', () => {
const section = unwindSrc.match(/changeCloseMethod\(\)[\s\S]*?\n\s*\},/);
expect(section).toBeTruthy();
expect(section[0]).toMatch(/this\.getInterestList\(\)/);
});
});
describe('changeCloseNotionalValue 必须调用 getInterestList', () => {
test('changeCloseNotionalValue 方法体中调用 this.getInterestList()', () => {
const section = unwindSrc.match(/changeCloseNotionalValue\(\)[\s\S]*?\n\s*\},/);
expect(section).toBeTruthy();
expect(section[0]).toMatch(/this\.getInterestList\(\)/);
});
});
describe('changeCloseQty 必须调用 getInterestList', () => {
test('changeCloseQty 方法体中调用 this.getInterestList()', () => {
const section = unwindSrc.match(/changeCloseQty\(\)[\s\S]*?\n\s*\},/);
expect(section).toBeTruthy();
expect(section[0]).toMatch(/this\.getInterestList\(\)/);
});
});
});
// ====================================================================
// 第2部分:行为模拟 — getInterestList 传递的 closePercent 正确性
// ============================================================================
function loadVueApp(model, mockPost) {
const code = fs.readFileSync(unwindSrcPath, 'utf8');
const stockEqvNotional = (v) => Number(Number(v || 0).toFixed(2));
const sandbox = {
module: { exports: {} },
exports: {},
console,
require,
window: { otcformat: { options: {} } },
otcformat: {
options: {},
trading: {
premiumRateP: { precision: 4 },
tradePrice: { precision: 4 },
notional: { precision: 6 },
StockEqvNotional: stockEqvNotional,
marginRateP: { precision: 4 },
umpriceP: { precision: 4 }
},
fixed6: (v) => Number(Number(v || 0).toFixed(6))
},
model: model,
isUseApproval: false,
Vue: function (options) {
// 返回一个模拟的 Vue 实例,暴露 methods 供测试调用
const instance = Object.assign({}, options);
instance.deal = JSON.parse(JSON.stringify(model));
instance.interestList = [];
instance.marginList = [];
instance.floatPosition = null;
instance.initPosiNetPrice = 0;
instance.oriClosePercent = 1;
instance.ratio = 1;
instance.shortRatio = 1;
instance.multiplier = model.StructureType === '普通债券类收益互换' ? 100 : 1;
// 绑定 methods 的 this
const boundMethods = {};
for (const key in options.methods) {
boundMethods[key] = (...args) => options.methods[key].apply(instance, args);
}
instance.methods = boundMethods;
// 供 methods 内部调用 this.xxx
for (const key in options.methods) {
instance[key] = boundMethods[key];
}
// 执行 created/mounted(会调 initDeal → getInterestList
if (options.created) options.created.call(instance);
// 暴露到 sandbox 供测试访问
sandbox.__vueInstance = instance;
return instance;
},
FastVue: {
vueDatePicker() { return {}; },
vueNumberInput() { return {}; }
},
swapPricePrecision: {
createVueInputComponent() { return {}; },
getCommonInputFormat() { return {}; },
normalizeCommon(type, value) { return value; },
formatCommon(type, value) { return value; },
getInputFormat(type, field, fallback) { return fallback; },
roundForSubmit(value) { return value; },
shiftDecimal(value) { return value; },
format(value) { return value; }
},
tradeHelper: { IsBond() { return false; } },
main: {
post(url, postData, opts) {
mockPost(url, postData);
return {
done(cb) {
// 模拟后端返回空利息列表
if (cb) cb({ obj: [] });
return this;
}
};
},
message() { }
},
SwapCalc: {
roundHalfAwayFromZero(value) { return value; },
calcCloseQtyByOriginalPercent(closePercent, oriClosePercent, positionQty) {
var ori = Number(oriClosePercent);
if (ori === 0) return 0;
if (Number(closePercent) >= ori) return Number(positionQty);
return Number(Number(positionQty) * (Number(closePercent) / ori).toFixed(6));
}
},
_: {
round(value, precision) {
return Number(Number(value || 0).toFixed(precision || 0));
}
}
};
sandbox.window.otcformat = sandbox.otcformat;
vm.runInNewContext(code, sandbox, { filename: unwindSrcPath });
return sandbox;
}
describe('getInterestList 传递的 closePercent 正确性', () => {
// 模拟一笔未平仓的交易:NotionalValue=100M, PosiNotionalValue=100M
const baseModel = {
ValueDate: '2026-07-27',
UnwindDate: '2026-07-27',
SwapTradeId: 1,
NotionalValue: 100000000,
PosiNotionalValue: 100000000,
NotionalQty: 1000000,
PositionQty: 1000000,
CloseType: 2,
CloseMethod: 1,
ClosePercent: 1,
CloseNotionalValue: 100000000,
CloseQty: 1000000,
StartDate: '2026-04-21',
StructureType: '',
TradeStartDate: '',
FlowEvents: [{
UnderlyingCode: '000001',
PosiGrossPrice: 10.0,
PosiNetPrice: 9.5,
PayDirection: 1,
PositionType: 1,
TradingAmountAvg: 10.0,
PosiQuantity: 1000000,
PosiNotionalValue: 100000000,
UnderlyingInstrumentType: 'stock',
PosiFeeType: 0,
PosiTradingFeeUnit: 0
}]
};
test('全部平仓 → 部分平仓(50%):getInterestList 传递 closePercent=0.5', () => {
const postCalls = [];
const model = JSON.parse(JSON.stringify(baseModel));
const sandbox = loadVueApp(model, (url, postData) => {
if (url === '/swaptrade2/GetUnwindInterestList') {
postCalls.push(JSON.parse(JSON.stringify(postData)));
}
});
const vue = sandbox.__vueInstance;
// 1. 初始化时 created() → setValueDate() → getInterestList() (closePercent=1)
expect(postCalls.length).toBeGreaterThanOrEqual(1);
expect(postCalls[0].closePercent).toBe(1);
// 2. 切换为部分平仓
vue.deal.CloseMethod = 2;
vue.changeCloseMethod();
// 切换后 closePercent 仍为 1= oriClosePercent),getInterestList 传 closePercent=1
expect(postCalls.length).toBeGreaterThanOrEqual(2);
expect(postCalls[postCalls.length - 1].closePercent).toBe(1);
// 3. 用户输入 50% → changeClosePercent
vue.deal.ClosePercent = 0.5;
vue.changeClosePercent();
// getInterestList 应传 closePercent=0.5
expect(postCalls.length).toBeGreaterThanOrEqual(3);
expect(postCalls[postCalls.length - 1].closePercent).toBe(0.5);
});
test('多次部分平仓后(已平30%):全部→部分(50%)closePercent=0.5 传递正确', () => {
const postCalls = [];
// 已平30%NotionalValue=100M, PosiNotionalValue=70M, oriClosePercent=0.7
const model = JSON.parse(JSON.stringify(baseModel));
model.PosiNotionalValue = 70000000;
model.PositionQty = 700000;
model.ClosePercent = 0.7;
model.CloseNotionalValue = 70000000;
model.CloseQty = 700000;
const sandbox = loadVueApp(model, (url, postData) => {
if (url === '/swaptrade2/GetUnwindInterestList') {
postCalls.push(JSON.parse(JSON.stringify(postData)));
}
});
const vue = sandbox.__vueInstance;
// oriClosePercent = 70M / 100M = 0.7
expect(vue.oriClosePercent).toBeCloseTo(0.7, 6);
// 切换为部分平仓
vue.deal.CloseMethod = 2;
vue.changeCloseMethod();
// closePercent 仍为 0.7= oriClosePercent),后端转为 B = 0.7*100M/70M = 1.0
const lastCall = postCalls[postCalls.length - 1];
expect(lastCall.closePercent).toBeCloseTo(0.7, 6);
expect(lastCall.notionalValue).toBe(100000000);
expect(lastCall.posiNotionalValue).toBe(70000000);
// 用户输入 50%(占期初口径A=0.5)
vue.deal.ClosePercent = 0.5;
vue.changeClosePercent();
// 后端收到 closePercent=0.5(A), notionalValue=100M, posiNotionalValue=70M
// 后端 ToRemainingClosePercent: B = 0.5 * 100M / 70M = 0.714...
const finalCall = postCalls[postCalls.length - 1];
expect(finalCall.closePercent).toBeCloseTo(0.5, 6);
expect(finalCall.notionalValue).toBe(100000000);
expect(finalCall.posiNotionalValue).toBe(70000000);
});
test('getInterestList postData 必须同时传 notionalValue 和 posiNotionalValue', () => {
const postCalls = [];
const model = JSON.parse(JSON.stringify(baseModel));
const sandbox = loadVueApp(model, (url, postData) => {
if (url === '/swaptrade2/GetUnwindInterestList') {
postCalls.push(postData);
}
});
expect(postCalls.length).toBeGreaterThan(0);
const postData = postCalls[0];
expect(postData).toHaveProperty('notionalValue');
expect(postData).toHaveProperty('posiNotionalValue');
expect(postData).toHaveProperty('closePercent');
expect(postData).toHaveProperty('tradeId');
expect(postData).toHaveProperty('valueDate');
});
});
// ====================================================================
// 第3部分:vue-number-input change 事件触发验证
// ============================================================================
describe('vue-number-input change 事件触发 onchange 机制', () => {
const baseJsPath = path.join(__dirname, '..', 'wwwroot', 'Scripts', 'fast', 'fastVue.base.js');
const baseSrc = fs.readFileSync(baseJsPath, 'utf8');
test('__change 函数调用 onchange 回调', () => {
// 验证 __change 函数确实调用 _options.onchange
expect(baseSrc).toMatch(/function\s+__change/);
expect(baseSrc).toMatch(/_options\.onchange\s*&&\s*_options\.onchange\(/);
});
test('__onInput 不直接调用 onchange(只在格式化/追加后缀)', () => {
// __onInput 只做格式化,不触发 onchange
const onInputMatch = baseSrc.match(/function\s+__onInput\(\)\s*\{[\s\S]*?\n\s*\}/);
expect(onInputMatch).toBeTruthy();
// __onInput 方法体不应调用 onchange
expect(onInputMatch[0]).not.toMatch(/onchange/);
});
test('change 事件绑定在元素上(jQuery .on("change", __change)', () => {
expect(baseSrc).toMatch(/\$\(.*\)\.on\(.*change.*__change/);
});
test('vueNumberInput 组件的 onchange 方法 emit input 事件', () => {
// 组件 onchange → $emit('input', result)
expect(baseSrc).toMatch(/onchange\s*\(value,\s*text,\s*isEnter\)/);
expect(baseSrc).toMatch(/\$emit\('input'/);
});
});