initial: blog-app snapshot
This commit is contained in:
70
docs/escape-html-behavior.test.mjs
Normal file
70
docs/escape-html-behavior.test.mjs
Normal file
@@ -0,0 +1,70 @@
|
||||
/**
|
||||
* 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, '<')
|
||||
.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>', '<script>alert(1)</script>'],
|
||||
['a & b', 'a & b'],
|
||||
['"quote"', '"quote"'],
|
||||
["'apos'", ''apos''],
|
||||
['<img onerror="x">', '<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));
|
||||
});
|
||||
39
docs/escapeHtml-parity-test.md
Normal file
39
docs/escapeHtml-parity-test.md
Normal file
@@ -0,0 +1,39 @@
|
||||
# escapeHtml parity test (OPT-6 收编验证)
|
||||
|
||||
utils.js:38 是 blog-app 唯一一份 escapeHtml 实现.
|
||||
ai-panel.js 已通过 `import { escapeHtml } from './utils.js'` (line 9) 复用.
|
||||
无任何本地副本, 无 div.textContent-based 实现.
|
||||
|
||||
## 字符集
|
||||
|
||||
utils.js:38 escape 5 个字符: `& < > " '`
|
||||
- `&` → `&`
|
||||
- `<` → `<`
|
||||
- `>` → `>`
|
||||
- `"` → `"`
|
||||
- `'` → `'` (注: 任务 spec 写的是 `'`, 但 utils.js 用十进制 `'`, 语义等价 — 都是单引号的 HTML entity)
|
||||
|
||||
## 测试用例 (行为对齐)
|
||||
|
||||
| 输入 | 期望输出 | utils.js 输出 | 一致? |
|
||||
|---|---|---|---|
|
||||
| `<script>alert(1)</script>` | `<script>alert(1)</script>` | 同 | ✓ |
|
||||
| `a & b` | `a & b` | 同 | ✓ |
|
||||
| `"quote"` | `"quote"` | 同 | ✓ |
|
||||
| `'apos'` | `'apos'` | 同 (等价于 `'`) | ✓ |
|
||||
| `<img onerror="x">` | `<img onerror="x">` | 同 | ✓ |
|
||||
|
||||
## 验证命令
|
||||
|
||||
grep -nE "function escapeHtml|const escapeHtml" static/js/ai-panel.js
|
||||
# 预期: 无输出 (本地实现已不存在)
|
||||
|
||||
grep -nE "^import.*escapeHtml" static/js/ai-panel.js
|
||||
# 预期: 9:import { escapeHtml } from './utils.js';
|
||||
|
||||
grep -rnE "function escapeHtml|const escapeHtml" static/js/
|
||||
# 预期: 仅 utils.js:38 一处
|
||||
|
||||
## 状态
|
||||
|
||||
OPT-6 收编完成 — ai-panel.js 第三份本地实现已不存在.
|
||||
Reference in New Issue
Block a user