From 3728d45cc40d8863b93068239d46312a299dca85 Mon Sep 17 00:00:00 2001 From: hjhan Date: Fri, 21 Aug 2026 07:09:51 +0800 Subject: [PATCH] =?UTF-8?q?test(web):=20swapPricePrecisionHelper=E8=A1=A51?= =?UTF-8?q?9=E4=BE=8B=E7=89=B9=E5=BE=81=E6=B5=8B=E8=AF=95=E5=86=BB?= =?UTF-8?q?=E7=BB=93=E5=AD=97=E7=AC=A6=E4=B8=B2=E7=B2=BE=E7=A1=AE=E5=8D=81?= =?UTF-8?q?=E8=BF=9B=E5=88=B6=E6=A0=B8=E5=BF=83=E2=80=94=E2=80=94shiftDeci?= =?UTF-8?q?mal/roundDecimal/multiplyDecimal/getRule/roundForSubmit/format?= =?UTF-8?q?=E5=85=A8golden=E5=80=BC=E6=96=AD=E8=A8=80(0.1=C3=970.2?= =?UTF-8?q?=E7=B2=BE=E7=A1=AE0.02=E3=80=811.005=E2=86=921.01=E7=BB=95?= =?UTF-8?q?=E5=BC=80=E6=B5=AE=E7=82=B9=E9=99=B7=E9=98=B1=E3=80=81roundDeci?= =?UTF-8?q?mal=E7=BB=9D=E5=AF=B9=E5=80=BC=E5=9B=9B=E8=88=8D=E4=BA=94?= =?UTF-8?q?=E5=85=A5=E4=B8=8EExitYtm=E7=BA=A6=E5=AE=9A=E5=90=8C=E5=90=91?= =?UTF-8?q?=E3=80=81=E4=B9=98=E6=B3=95=E4=BF=9D=E7=95=99=E5=B0=8F=E6=95=B0?= =?UTF-8?q?=E6=A0=87=E5=BA=A61.5=C3=972=3D3.0=E3=80=81Bond=E5=AD=97?= =?UTF-8?q?=E6=AE=B5=E6=8B=BC=E9=94=99=E2=86=92null=E5=9B=9E=E8=90=BDumpri?= =?UTF-8?q?ce=E8=80=8CStock=E5=9B=9E=E8=90=BD=E5=93=81=E7=A7=8D=E7=BA=A7?= =?UTF-8?q?=E7=9A=84=E4=B8=8D=E5=AF=B9=E7=A7=B0=E5=86=BB=E7=BB=93)?= =?UTF-8?q?=EF=BC=9Brequire=E7=9B=B4=E8=BF=9E=E5=8A=A0=E8=BD=BD=E6=9B=BF?= =?UTF-8?q?=E4=BB=A3readFileSync=E6=B2=99=E7=AE=B1(=E5=8F=AF=E8=A2=AB?= =?UTF-8?q?=E6=8F=92=E6=A1=A9)=EF=BC=9B=E8=A6=86=E7=9B=96=E7=8E=87?= =?UTF-8?q?=E6=B8=85=E5=8D=95=E7=BA=B3=E5=85=A5=E8=AF=A5=E6=96=87=E4=BB=B6?= =?UTF-8?q?=EF=BC=9B=E5=AE=9E=E9=94=A4=E6=B3=A8=E9=87=8A:=E6=BA=90?= =?UTF-8?q?=E7=A0=81=E5=9C=A8rootDir=E5=A4=96=E8=87=B4babel(=E6=97=A0trans?= =?UTF-8?q?former)=E4=B8=8Ev8(=E9=80=83=E9=80=B8=E8=B7=AF=E5=BE=84?= =?UTF-8?q?=E6=97=A0=E6=B3=95=E5=BD=92=E5=9B=A0,=E5=90=ABroots/=E7=BB=9D?= =?UTF-8?q?=E5=AF=B9=E8=B7=AF=E5=BE=84=E5=8F=98=E4=BD=93=E5=AE=9E=E6=B5=8B?= =?UTF-8?q?jest29.7)=E4=B8=A4=E6=8F=90=E4=BE=9B=E5=99=A8=E8=A6=86=E7=9B=96?= =?UTF-8?q?=E7=8E=87=E6=81=920=E4=B8=94=E9=97=A8=E6=A7=9B=E4=B8=8D?= =?UTF-8?q?=E8=A7=A6=E5=8F=91,=E7=9C=9F=E4=BF=AE=E9=9C=80=E9=87=8D?= =?UTF-8?q?=E6=9E=84rootDir=E5=8F=A6=E8=A1=8C=E7=AB=8B=E9=A1=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- YLErpWeb/fe-tests/jest.config.js | 4 + .../fe-tests/swapPricePrecisionHelper.test.js | 178 ++++++++++++++++++ 2 files changed, 182 insertions(+) create mode 100644 YLErpWeb/fe-tests/swapPricePrecisionHelper.test.js diff --git a/YLErpWeb/fe-tests/jest.config.js b/YLErpWeb/fe-tests/jest.config.js index b06bd582..f6877998 100644 --- a/YLErpWeb/fe-tests/jest.config.js +++ b/YLErpWeb/fe-tests/jest.config.js @@ -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', // 逐步加入更多文件 ], diff --git a/YLErpWeb/fe-tests/swapPricePrecisionHelper.test.js b/YLErpWeb/fe-tests/swapPricePrecisionHelper.test.js new file mode 100644 index 00000000..d30ae970 --- /dev/null +++ b/YLErpWeb/fe-tests/swapPricePrecisionHelper.test.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'); + }); +});