test(web): swapPricePrecisionHelper补19例特征测试冻结字符串精确十进制核心——shiftDecimal/roundDecimal/multiplyDecimal/getRule/roundForSubmit/format全golden值断言(0.1×0.2精确0.02、1.005→1.01绕开浮点陷阱、roundDecimal绝对值四舍五入与ExitYtm约定同向、乘法保留小数标度1.5×2=3.0、Bond字段拼错→null回落umprice而Stock回落品种级的不对称冻结);require直连加载替代readFileSync沙箱(可被插桩);覆盖率清单纳入该文件;实锤注释:源码在rootDir外致babel(无transformer)与v8(逃逸路径无法归因,含roots/绝对路径变体实测jest29.7)两提供器覆盖率恒0且门槛不触发,真修需重构rootDir另行立项
This commit is contained in:
@@ -23,9 +23,13 @@ module.exports = {
|
||||
moduleDirectories: ['node_modules', '../wwwroot/Scripts'],
|
||||
|
||||
// 覆盖率配置(--coverage 时生效)
|
||||
// 已知问题:源码在 rootDir(fe-tests) 之外,babel 提供器因 transform:{} 无插桩器、
|
||||
// v8 提供器对 rootDir 逃逸路径无法归因(jest 29.7 实测,含 roots/绝对路径变体),
|
||||
// 覆盖率表恒为 0 且门槛不触发。清单仍保留以固化意图,真实修复需重构 rootDir。
|
||||
collectCoverageFrom: [
|
||||
'../wwwroot/Scripts/app/swaptrade/swapCalc.js',
|
||||
'../wwwroot/Scripts/app/swaptrade/unwindBondCalc.js',
|
||||
'../wwwroot/Scripts/app/swaptrade/swapPricePrecisionHelper.js',
|
||||
'../wwwroot/Scripts/fast/fastVue.base.js',
|
||||
// 逐步加入更多文件
|
||||
],
|
||||
|
||||
@@ -0,0 +1,178 @@
|
||||
/**
|
||||
* swapPricePrecisionHelper.test.js — 字符串精确十进制核心的特征测试
|
||||
* ============================================================================
|
||||
* 目的:swapPricePrecisionHelper.js 的组件事件(swapPriceInput.component.test.js)与
|
||||
* 配置接线(swapPrecisionConfig.test.js)已有测试,但【字符串精确十进制核心】
|
||||
* (shiftDecimal / roundDecimal / multiplyDecimal / getRule / format /
|
||||
* roundForSubmit) 一直只有间接覆盖。精度即资损,这里用 golden 值冻结现状:
|
||||
* 1. 全部走字符串数位运算,绕开 IEEE754 浮点陷阱(0.1*0.2、1.005.toFixed);
|
||||
* 2. roundDecimal 是【绝对值上四舍五入】= AwayFromZero,与 unwindBondCalc
|
||||
* 的 ExitYtm 约定同向;
|
||||
* 3. roundDecimal 不补零(1.2 → '1.2'),补零是 formatCommon/formatFixed 的职责;
|
||||
* 4. roundDecimal 对非法输入/负精度【原样返回不归一】,调用方别拿它当校验器;
|
||||
* 5. getRule 的覆盖优先级:先查 defaults 有无该品种(没有直接 null),再看
|
||||
* main.swapPricePrecision 覆盖,覆盖非法则回落 defaults。
|
||||
*
|
||||
* 做法:直接 require 源文件(jest v8 覆盖率据此插桩,readFileSync+new Function
|
||||
* 的沙箱加载会让覆盖率统计看不见);配置覆盖用 global.main.swapPricePrecision
|
||||
* 注入——模块内 global 绑定到 globalThis,与本测试文件的 global 同源,
|
||||
* beforeEach/afterEach 清理防串扰。
|
||||
*
|
||||
* 运行:cd YLErpWeb/fe-tests && npx jest swapPricePrecisionHelper
|
||||
*/
|
||||
const helper = require('../wwwroot/Scripts/app/swaptrade/swapPricePrecisionHelper.js');
|
||||
|
||||
function withConfig(config, extras) {
|
||||
if (config !== undefined) global.main = { swapPricePrecision: config };
|
||||
if (extras && extras.otcformat) global.otcformat = extras.otcformat;
|
||||
}
|
||||
|
||||
beforeEach(() => { delete global.main; delete global.otcformat; });
|
||||
afterEach(() => { delete global.main; delete global.otcformat; });
|
||||
|
||||
describe('shiftDecimal:纯数位平移,不经浮点', () => {
|
||||
|
||||
test('右移补零 / 左移进小数', () => {
|
||||
expect(helper.shiftDecimal('1.5', 1)).toBe('15');
|
||||
expect(helper.shiftDecimal('1.5', -1)).toBe('0.15');
|
||||
expect(helper.shiftDecimal('12.34', -3)).toBe('0.01234');
|
||||
expect(helper.shiftDecimal('0.001', -2)).toBe('0.00001');
|
||||
expect(helper.shiftDecimal('5', 2)).toBe('500');
|
||||
});
|
||||
|
||||
test('保留符号;places=0 或非整数只做归一化', () => {
|
||||
expect(helper.shiftDecimal('-1.5', 1)).toBe('-15');
|
||||
expect(helper.shiftDecimal('1.50', 0)).toBe('1.50'); // 不动尾巴零
|
||||
expect(helper.shiftDecimal('1.50', 1.5)).toBe('1.50');
|
||||
});
|
||||
|
||||
test('归一化先行:首尾零/符号/科学计数法', () => {
|
||||
expect(helper.shiftDecimal('007.50', 0)).toBe('7.50');
|
||||
expect(helper.shiftDecimal('+7', 0)).toBe('7');
|
||||
expect(helper.shiftDecimal('-0.000', 0)).toBe('0.000'); // 负零坍缩为无符号
|
||||
expect(helper.shiftDecimal('1e-7', 0)).toBe('0.0000001');
|
||||
expect(helper.shiftDecimal('1.23e5', 0)).toBe('123000');
|
||||
});
|
||||
|
||||
test('非法输入返回 null,空白串返回空串', () => {
|
||||
expect(helper.shiftDecimal('abc', 2)).toBeNull();
|
||||
expect(helper.shiftDecimal('1.2.3', 2)).toBeNull();
|
||||
expect(helper.shiftDecimal('', 2)).toBe('');
|
||||
});
|
||||
});
|
||||
|
||||
describe('roundDecimal:绝对值四舍五入(AwayFromZero),字符串路径无浮点陷阱', () => {
|
||||
|
||||
test('经典浮点陷阱对照:1.005 与 0.1×0.2 场景字符串路径给正确答案', () => {
|
||||
expect(Number(1.005).toFixed(2)).toBe('1.00'); // 浮点路径错(冻结对照)
|
||||
expect(helper.roundDecimal('1.005', 2)).toBe('1.01');
|
||||
expect(helper.roundDecimal('2.345', 2)).toBe('2.35');
|
||||
expect(helper.roundDecimal('2.344', 2)).toBe('2.34');
|
||||
});
|
||||
|
||||
test('负数远离零(与 ExitYtm 约定同向)', () => {
|
||||
expect(helper.roundDecimal('-2.345', 2)).toBe('-2.35');
|
||||
expect(helper.roundDecimal('-2.5', 0)).toBe('-3');
|
||||
expect(helper.roundDecimal('-0.00005', 4)).toBe('-0.0001');
|
||||
});
|
||||
|
||||
test('进位链:跨数量级与跨整数位', () => {
|
||||
expect(helper.roundDecimal('9.99', 1)).toBe('10.0');
|
||||
expect(helper.roundDecimal('9.99', 0)).toBe('10');
|
||||
expect(helper.roundDecimal('99.999', 2)).toBe('100.00');
|
||||
expect(helper.roundDecimal('0.00005', 4)).toBe('0.0001');
|
||||
});
|
||||
|
||||
test('不补零也不主动去零:位数不足原样返回,已有位数保留尾巴零', () => {
|
||||
expect(helper.roundDecimal('1.2', 4)).toBe('1.2'); // 不足4位不补零(补零是 formatCommon 的职责)
|
||||
expect(helper.roundDecimal('3.10000', 4)).toBe('3.1000'); // 已到4位:只取整不去零(去零是 format 的职责)
|
||||
});
|
||||
|
||||
test('防御姿态:负精度/非法输入原样返回,不做校验', () => {
|
||||
expect(helper.roundDecimal('2.345', -1)).toBe('2.345');
|
||||
expect(helper.roundDecimal('2.345', 1.5)).toBe('2.345');
|
||||
expect(helper.roundDecimal('abc', 2)).toBe('abc');
|
||||
});
|
||||
});
|
||||
|
||||
describe('multiplyDecimal:字符串精确乘法', () => {
|
||||
|
||||
test('0.1×0.2 精确为 0.02(浮点给 0.020000000000000004)', () => {
|
||||
expect(helper.multiplyDecimal('0.1', '0.2')).toBe('0.02');
|
||||
expect(helper.multiplyDecimal('0.1', '0.1')).toBe('0.01');
|
||||
expect(helper.multiplyDecimal('1.1', '1.1')).toBe('1.21');
|
||||
});
|
||||
|
||||
test('符号组合;保留小数标度(1.5×2=3.0 而非 3)', () => {
|
||||
expect(helper.multiplyDecimal('1.5', '2')).toBe('3.0');
|
||||
expect(helper.multiplyDecimal('-1.5', '2')).toBe('-3.0');
|
||||
expect(helper.multiplyDecimal('-1.5', '-2')).toBe('3.0');
|
||||
expect(helper.multiplyDecimal('-0.03', '-0.02')).toBe('0.0006');
|
||||
});
|
||||
|
||||
test('大整数精确(超出 Number.MAX_SAFE_INTEGER)', () => {
|
||||
expect(helper.multiplyDecimal('123456789', '987654321')).toBe('121932631112635269');
|
||||
});
|
||||
|
||||
test('非法输入 null;空串按 0 处理', () => {
|
||||
expect(helper.multiplyDecimal('abc', '1')).toBeNull();
|
||||
expect(helper.multiplyDecimal('', '1')).toBe('0');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getRule / getInputFormat / roundForSubmit:字段规则解析与覆盖优先级', () => {
|
||||
test('defaults 字段级规则:债券三字段 vs 股票兜底', () => {
|
||||
expect(helper.getRule('Bond', 'yield')).toEqual({ integerDigits: 2, precision: 4 });
|
||||
expect(helper.getRule('Bond', 'grossPrice')).toEqual({ integerDigits: 6, precision: 9 });
|
||||
// 债券顶层没有 integerDigits/precision:字段拼错 → null(后续走 umprice 兜底),
|
||||
// 而 Stock 顶层有 → 字段拼错回落品种级规则。这个不对称冻结于此。
|
||||
expect(helper.getRule('Bond', 'unknownField')).toBeNull();
|
||||
expect(helper.getRule('Stock', 'anything')).toEqual({ integerDigits: 7, precision: 2 });
|
||||
expect(helper.getRule('NoSuchType', 'yield')).toBeNull();
|
||||
});
|
||||
|
||||
test('main.swapPricePrecision 覆盖优先于 defaults,非法覆盖回落 defaults', () => {
|
||||
withConfig({ Bond: { yield: { integerDigits: 3, precision: 5 } } });
|
||||
expect(helper.getRule('Bond', 'yield')).toEqual({ integerDigits: 3, precision: 5 });
|
||||
|
||||
withConfig({ Bond: { yield: { integerDigits: 0 } } });
|
||||
const bad = helper;
|
||||
expect(bad.getRule('Bond', 'yield')).toEqual({ integerDigits: 2, precision: 4 });
|
||||
|
||||
// defaults 里没有的品种,配置了也不认:覆盖只允许白名单内微调
|
||||
withConfig({ BrandNewType: { yield: { integerDigits: 3, precision: 5 } } });
|
||||
const unknown = helper;
|
||||
expect(unknown.getRule('BrandNewType', 'yield')).toBeNull();
|
||||
});
|
||||
|
||||
test('getInputFormat 合并 options', () => {
|
||||
expect(helper.getInputFormat('Bond', 'yield', { negative: true }))
|
||||
.toEqual({ negative: true, integerDigits: 2, precision: 4 });
|
||||
expect(helper.getInputFormat('NoSuchType', 'yield', { negative: true }))
|
||||
.toEqual({ negative: true });
|
||||
});
|
||||
|
||||
test('roundForSubmit:按规则精度+offset 取整;空值/无规则原样返回', () => {
|
||||
expect(helper.roundForSubmit('3.14159265', 'Bond', 'yield')).toBe('3.1416');
|
||||
expect(helper.roundForSubmit('3.14159265', 'Bond', 'yield', 2)).toBe('3.141593');
|
||||
expect(helper.roundForSubmit('', 'Bond', 'yield')).toBe('');
|
||||
expect(helper.roundForSubmit('1.2', 'NoSuchType', 'yield')).toBe('1.2');
|
||||
expect(helper.roundForSubmit('3.14159265', 'Bond', 'yield', 2.5)).toBe('3.1416'); // 非整数offset忽略
|
||||
});
|
||||
});
|
||||
|
||||
describe('format:字段级展示态(取整+去尾巴零)', () => {
|
||||
test('按字段规则取整并去尾零', () => {
|
||||
expect(helper.format('3.14159', 'Bond', 'yield')).toBe('3.1416');
|
||||
expect(helper.format('3.10000', 'Bond', 'yield')).toBe('3.1');
|
||||
expect(helper.format('99.5', 'Stock', 'whatever')).toBe('99.5');
|
||||
expect(helper.format('', 'Bond', 'yield')).toBe('');
|
||||
});
|
||||
|
||||
test('无规则时回落 otcformat.trading.umprice', () => {
|
||||
withConfig(undefined, {
|
||||
otcformat: { trading: { umprice: function (v) { return 'UM:' + v; } } }
|
||||
});
|
||||
expect(helper.format('1.23', 'NoSuchType', 'yield')).toBe('UM:1.23');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user