/** * 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'); }); });