/** * OPT-6 行为对齐测试: 对比 ai-panel.js 原 escapeHtml (div.textContent + innerHTML) * 与 utils.js 主实现 (replace 链). * * 说明: 原实现依赖浏览器 DOM, Node 无法直接调用, 这里用最小 DOM stub * 复刻 textContent -> innerHTML 的转义语义 (只转义 & < >, 不转义引号). * utils.js 实现是其严格超集 (额外转义 " '), 因此逐字符对比断言: * 对同一输入, utils 输出 反解实体后 必须与 stub 输出 反解实体后 一致, * 且 utils 输出本身满足 5 个字符的转义期望. * * 运行: node --test docs/escape-html-behavior.test.mjs */ import { test } from 'node:test'; import assert from 'node:assert/strict'; /* utils.js 模块顶层有 document.addEventListener 副作用, Node 下需 stub */ globalThis.document = { addEventListener() {} }; const { escapeHtml } = await import('../static/js/utils.js'); /* 复刻 ai-panel.js 原实现的最小 DOM stub */ function legacyEscapeHtml(text) { const div = { _text: '', set textContent(v) { this._text = String(v); }, get innerHTML() { return this._text .replace(/&/g, '&') .replace(//g, '>'); }, }; div.textContent = text == null ? '' : String(text); return div.innerHTML; } /* 反解 HTML 实体, 用于跨实现等价比较 */ function unescape(s) { return s .replace(/</g, '<') .replace(/>/g, '>') .replace(/"/g, '"') .replace(/'|'/g, "'") .replace(/&/g, '&'); } const cases = [ ['', '<script>alert(1)</script>'], ['a & b', 'a & b'], ['"quote"', '"quote"'], ["'apos'", ''apos''], ['', '<img onerror="x">'], ]; for (const [input, expected] of cases) { test(`utils.js escapeHtml: ${JSON.stringify(input)}`, () => { assert.equal(escapeHtml(input), expected); }); test(`等价性 (utils ⊇ legacy): ${JSON.stringify(input)}`, () => { const legacy = legacyEscapeHtml(input); const unified = escapeHtml(input); assert.equal(unescape(unified), unescape(legacy)); }); } test('null/undefined 输入行为一致', () => { assert.equal(escapeHtml(null), legacyEscapeHtml(null)); assert.equal(escapeHtml(undefined), legacyEscapeHtml(undefined)); });