用户指出:上一笔直接删了 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 例全过
190 lines
9.4 KiB
JavaScript
190 lines
9.4 KiB
JavaScript
/**
|
||
* 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.js:main.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');
|
||
});
|
||
});
|