Files
zszq-trs/YLErpWeb/fe-tests/diag.test.js
T
hjhan 72b2a4cbf6 fix: 债券三字段调试日志改为 otcdebug gated,出问题时一键可定位
用户指出:上一笔直接删了 onBondPriceKeydown 的逐键 console.log 又没补 gated 版,
等于平时没噪音了、真出问题也看不见——这正是反复改几十遍都满足不了需求时才需要日志兜底的场景。

修复:
- onBondPriceKeydown 逐键 before/after(driver/rev) trace 改为 otcDebug.log(?otcdebug=1 才输出)
- onBondPriceEdit(失焦/粘贴) 补同款 gated trace
- calcBondForItem 补 enter/success/calc-error/post-fail 四条 gated trace
  → 三字段整条链路(逐键→回车→计算器→源/AUTO或失败)开开关即得完整现场
- main.js 第226/669行 catch 内 error console.log 保留(错误日志本就应在生产可见,利于定位)
- otcDebug 机制本身已存在且唯一:?otcdebug=1 或 localStorage.otcdebug=1 开启,
  _MainLayout 注入 jsVersion/git/built,加载时打带版本 banner,平时零噪音

测试:
- diag.test.js 新增源码守卫:swapTradeEdit.js 三字段埋点必须 otcDebug.log、不得退回裸 console.log
- diag 18 例 / bondCalc 81 例全过
2026-07-30 10:15:00 +08:00

190 lines
9.4 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.
/**
* diag.test.js — 运行时诊断机制测试(版本可追溯 + 可控调试日志)
* ============================================================================
* 目的:守卫"可保留调试日志 + 版本可追溯"机制,让排查 EQD-6838 这类"改了不生效"
* 问题时,F12 能一键开启详细日志并看到精确版本(jsVersion + git sha)。
*
* 覆盖:
* 1. 开关解析:URL ?otcdebug=1 / localStorage.otcdebug=1 解析正确
* 2. 静默性:debug=false 时 otcDebug 不输出(生产零噪音)
* 3. banner 格式:debug=true 时输出含 jsVersion + git(版本可追溯)
* 4. 防回归:业务文件不再硬编码 v20260729a 这类易过期的版本串
*
* 建立 console spy 先例:项目此前零 `jest.spyOn(console)` 用法,本测试建立范式。
*
* 运行:cd YLErpWeb/fe-tests && npx jest diag
*/
const { JSDOM } = require('jsdom');
const fs = require('fs');
const path = require('path');
// ---- 与 main.js 顶部一致的 otcDebug 实现(行为黄金标准) ----
// 为什么复制而非 require main.jsmain.js 第 5 行 `main.extend = $.extend` 依赖 jQuery+lodash
// 完整 require 代价过大;otcDebug 工具是纯函数(不依赖 $),提取出来做行为验证最干净。
// main.js 源码一致性由下方"源码守卫"用例保证(断言关键片段存在)。
function createOtcDebug(diag) {
var __otcDiag = diag || { debug: false };
return {
banner: function (name, ver) {
if (!__otcDiag.debug || !console) return;
var git = (__otcDiag.git || '').slice(0, 7);
console.log('%c[' + name + '] v' + ver + ' (bundle=' + __otcDiag.jsVersion + ', git=' + git + ', built=' + __otcDiag.built + ')',
'color:#06c;font-weight:bold');
},
log: function () {
if (!__otcDiag.debug || !console) return;
console.log.apply(console, arguments);
}
};
}
// ---- 与 _MainLayout.cshtml 一致的 diag 开关解析(行为黄金标准) ----
function parseDebugFlag(search, localStorageValue) {
return /[?&]otcdebug=1/.test(search)
|| (localStorageValue === '1');
}
describe('diag: 开关解析(URL / localStorage', () => {
test('?otcdebug=1 在 query 中 → 开启', () => {
expect(parseDebugFlag('?otcdebug=1', null)).toBe(true);
expect(parseDebugFlag('/swap/edit?otcdebug=1&id=5', null)).toBe(true);
});
test('?otcdebug=1 作为唯一参数 → 开启', () => {
expect(parseDebugFlag('?otcdebug=1', null)).toBe(true);
});
test('URL 无参数 → 关闭', () => {
expect(parseDebugFlag('', null)).toBe(false);
expect(parseDebugFlag('/swap/edit', null)).toBe(false);
});
test('localStorage.otcdebug=1 → 持久开启', () => {
expect(parseDebugFlag('', '1')).toBe(true);
});
test('localStorage 其它值 → 关闭', () => {
expect(parseDebugFlag('', null)).toBe(false);
expect(parseDebugFlag('', '0')).toBe(false);
expect(parseDebugFlag('', '')).toBe(false);
});
test('URL 和 localStorage 任一为真即开启(OR 语义)', () => {
expect(parseDebugFlag('?otcdebug=1', '0')).toBe(true);
expect(parseDebugFlag('', '1')).toBe(true);
expect(parseDebugFlag('?foo=bar', '0')).toBe(false);
});
});
describe('diag: otcDebug 静默性(debug=false 生产零输出)', () => {
let logSpy;
beforeEach(() => { logSpy = jest.spyOn(console, 'log').mockImplementation(() => {}); });
afterEach(() => { logSpy.mockRestore(); });
test('debug=false 时 banner 不输出', () => {
const dbg = createOtcDebug({ debug: false, jsVersion: '2507300000', git: 'abc1234', built: '2026-07-30' });
dbg.banner('swapTradeEdit.js', '1.4.2');
expect(logSpy).not.toHaveBeenCalled();
});
test('debug=false 时 log 不输出', () => {
const dbg = createOtcDebug({ debug: false });
dbg.log('排查信息', { a: 1 });
expect(logSpy).not.toHaveBeenCalled();
});
test('无 diag 对象时默认静默', () => {
const dbg = createOtcDebug(undefined);
dbg.banner('x', '1'); dbg.log('y');
expect(logSpy).not.toHaveBeenCalled();
});
});
describe('diag: banner 输出含完整版本信息(可追溯)', () => {
let logSpy;
beforeEach(() => { logSpy = jest.spyOn(console, 'log').mockImplementation(() => {}); });
afterEach(() => { logSpy.mockRestore(); });
test('banner 输出包含 模块名 + jsVersion + git sha 前7位 + built', () => {
const diag = { debug: true, jsVersion: '2507301200', git: 'f5ed65aac5f93028', built: '2026-07-30 10:00:00' };
const dbg = createOtcDebug(diag);
dbg.banner('swapTradeEdit.js', '1.4.2');
expect(logSpy).toHaveBeenCalledTimes(1);
const out = logSpy.mock.calls[0][0];
expect(out).toContain('swapTradeEdit.js');
expect(out).toContain('1.4.2');
expect(out).toContain('2507301200'); // jsVersion
expect(out).toContain('f5ed65a'); // git sha 前7位(被 slice 截断)
expect(out).not.toContain('f5ed65aac5f93028'); // 不含完整 sha(避免 console 过长)
expect(out).toContain('2026-07-30 10:00:00'); // built
});
test('log 透传所有参数', () => {
const dbg = createOtcDebug({ debug: true });
dbg.log('事件', 'keydown', { key: 'Enter' });
expect(logSpy).toHaveBeenCalledTimes(1);
expect(logSpy.mock.calls[0]).toEqual(['事件', 'keydown', { key: 'Enter' }]);
});
test('git 缺失时 banner 不崩(输出 git= 空)', () => {
const dbg = createOtcDebug({ debug: true, jsVersion: '1', git: undefined, built: '' });
expect(() => dbg.banner('m', '1')).not.toThrow();
expect(logSpy.mock.calls[0][0]).toContain('git=');
});
});
describe('diag: 源码守卫(防回归——不许再硬编码易过期版本串)', () => {
const SCRIPTS = path.join(__dirname, '..', 'wwwroot', 'Scripts');
test('swapTradeEdit.js 顶部 banner 不再硬编码 v20260729a', () => {
const src = fs.readFileSync(path.join(SCRIPTS, 'app/swaptrade/swapTradeEdit.js'), 'utf8');
expect(src).not.toContain('v20260729a');
// 必须改为受开关控制的动态 banner
expect(src).toMatch(/otcDebug\.banner|window\.ylotc\.__diag/);
});
test('swapTradeEdit.js 三字段调试埋点必须 gated(otcDebug.log),不得裸 console.log 退回', () => {
// 防回归:历史多次"改了不生效"靠日志兜底,曾出现①把 console.log 直接删了(出问题时看不见)
// ②或解除了开关控制(生产噪音)。正确做法是 otcDebug.log?otcdebug=1 才输出)。
const src = fs.readFileSync(path.join(SCRIPTS, 'app/swaptrade/swapTradeEdit.js'), 'utf8');
// 存在 gated 埋点
expect(src).toMatch(/otcDebug\.log\(/);
// 关键路径不得出现裸 console.log 调试行
expect(src).not.toMatch(/console\.log\('\[onBondPriceKeydown\]/);
expect(src).not.toMatch(/console\.log\('\[calcBondForItem\]/);
expect(src).not.toMatch(/console\.log\('\[onBondPriceEdit\]/);
});
test('fastVue.base.js 顶部 banner 不再硬编码 v20260729a', () => {
const src = fs.readFileSync(path.join(SCRIPTS, 'fast/fastVue.base.js'), 'utf8');
expect(src).not.toContain('v20260729a');
expect(src).toMatch(/window\.ylotc\.__diag/);
});
test('main.js 提供 otcDebug 工具且挂在 window', () => {
const src = fs.readFileSync(path.join(SCRIPTS, 'base/main.js'), 'utf8');
expect(src).toContain('window.otcDebug');
expect(src).toContain('window.ylotc.__diag');
// 工具必须实现 banner + log 两个方法
expect(src).toMatch(/banner\s*[:=]\s*function/);
expect(src).toMatch(/log\s*[:=]\s*function/);
});
test('main.js 不存在第二套调试开关(机制 A 防回归)', () => {
// 历史教训:曾同时存在两套重叠的调试开关——
// 机制A: ?debug=1 / localStorage.__yl_debug__ / main.debugLog/debugBanner
// 机制B: ?otcdebug=1 / localStorage.otcdebug / window.otcDebug(统一方案,保留)
// 两套并存导致排查者记两套参数、两个前缀、fallback 分支。本守卫确保只有一套。
const src = fs.readFileSync(path.join(SCRIPTS, 'base/main.js'), 'utf8');
expect(src).not.toContain('main.debugLog');
expect(src).not.toContain('main.debugWarn');
expect(src).not.toContain('main.debugError');
expect(src).not.toContain('main.debugBanner');
expect(src).not.toContain('main.setDebug');
expect(src).not.toContain('__yl_debug__');
expect(src).not.toContain('?debug=1');
});
test('bundle 产物不再含硬编码 v20260729a(源文件已清除,重建产物自然不含)', () => {
// 版本可追溯不靠 bundle 头部注释(那会随 commit 变化导致 verify 永久失败),
// 而是靠运行时 window.ylotc.__diag(后端 HtmlUtil.GitCommit 注入)+ otcDebug.banner。
// 本守卫确保源文件的硬编码清除后,重建产物不会重新带回它。
const bundle = path.join(__dirname, '..', 'wwwroot', 'Statics', 'bundles', 'bundle.js');
if (!fs.existsSync(bundle)) return; // 产物可能尚未重建,跳过
const src = fs.readFileSync(bundle, 'utf8');
expect(src).not.toContain('v20260729a');
});
});