diff --git a/client/electron/test/lib/checks.mjs b/client/electron/test/lib/checks.mjs new file mode 100644 index 0000000..7fd492d --- /dev/null +++ b/client/electron/test/lib/checks.mjs @@ -0,0 +1,44 @@ +/* + * 判据的共享 check/finish —— 让"漏打 marker"和"计数写错位置"这两类 + * **在新文件上不可能发生**(pi 2026-09-14 的建议)。 + * + * 背景:`run-all.mjs` 用"自报条数 + 每文件期望条数"当棘轮(只增不减), + * 但之前那是两条靠记性的约定 —— "记得在结尾打 `RESULT pass=N`"、 + * "计数必须写在 `check()` 内部"。这两条各自都踩空过: + * `narrow-layout` 当时只有 `failed` 计数、`markdown-xss` 根本没有计数器。 + * + * 用法(判据文件): + * + * import { check, finish } from './lib/checks.mjs'; + * check('这条判据的名字', cond, '失败时给人看的细节'); + * finish('主题'); // 打完汇总就退出(有失败则退出码 1) + * + * 计数只可能在这个模块里发生,所以只要用了它: + * - marker 一定会有(`finish()` 打); + * - `check()` 的实现不可能被"合并冲突改空"而看不见(空实现 → pass=0 → 棘轮红)。 + * + * ⚠️ 反过来也成立:**判据文件里不要自己再写一份 check/计数**(那正是本模块要消灭的东西)。 + * 已有的 12 条判据不用回改(`run-all.mjs` 的 marker 检查已经覆盖它们)。 + */ +let pass = 0; +let fail = 0; + +/** 一条判据。`ok` 为真记通过,否则记失败并打印细节 */ +export function check(name, ok, detail = '') { + if (ok) { + pass++; + console.log(` 通过 ${name}`); + return true; + } + fail++; + console.error(` 失败 ${name}${detail ? ' — ' + detail : ''}`); + return false; +} + +/** 汇总并退出。`label` 是给人看的名字(例:「主题」) */ +export function finish(label) { + console.log(`\n${label}:${pass} 通过${fail ? `,${fail} 失败` : ''}`); + // 机器可读的契约行:run-all.mjs 只认这一行来判"这条判据到底跑了几条" + console.log(`RESULT pass=${pass} fail=${fail}`); + process.exit(fail ? 1 : 0); +} diff --git a/client/electron/test/markdown-xss.test.mjs b/client/electron/test/markdown-xss.test.mjs index d6bcff1..1a94d37 100644 --- a/client/electron/test/markdown-xss.test.mjs +++ b/client/electron/test/markdown-xss.test.mjs @@ -9,6 +9,7 @@ import { renderToStaticMarkup } from 'react-dom/server'; import React from 'react'; import Markdown from 'react-markdown'; import remarkGfm from 'remark-gfm'; +import { check, finish } from './lib/checks.mjs'; // 只有【真实标签】里的危险内容才算漏。 // 注意不能直接搜 onerror=:raw HTML 被转义成 <img … onerror=" 后, @@ -27,19 +28,14 @@ const payloads = [ '**bold** `code`', ]; -let leaks = 0; +// 每条 payload 就是一条判据(换来的是"跑了几条"可核对,而不是只报一句总括) +console.log('Markdown 注入回归:'); for (const p of payloads) { const html = renderToStaticMarkup( React.createElement(Markdown, { remarkPlugins: [remarkGfm] }, p) ); const bad = dangerous.test(html); - if (bad) leaks++; console.log((bad ? 'LEAK ' : 'safe '), JSON.stringify(p), '->', html.slice(0, 80)); + check(`危险内容被清除:${JSON.stringify(p)}`, !bad, `渲染结果里出现了真实可执行的危险标记:${html.slice(0, 120)}`); } -if (leaks > 0) { - console.error(`\n失败:${leaks} 处 XSS 泄漏`); - process.exit(1); -} -console.log('\n通过:raw HTML 被转义,javascript: URL 被清空'); -// 机器可读汇总:每条 payload 算一条(leaks 就是 fail) -console.log(`RESULT pass=${payloads.length - leaks} fail=${leaks}`); +finish('Markdown 注入'); diff --git a/client/electron/test/narrow-layout.test.mjs b/client/electron/test/narrow-layout.test.mjs index c9c6411..b9a35c2 100644 --- a/client/electron/test/narrow-layout.test.mjs +++ b/client/electron/test/narrow-layout.test.mjs @@ -3,21 +3,9 @@ // 不做视觉快照:那需要 headless 浏览器,且像素级比对在字体差异下极脆。 // 这里守住几条真正会坏掉的不变量。 import { readFileSync } from 'node:fs'; +import { check, finish } from './lib/checks.mjs'; const read = p => readFileSync(new URL(p, import.meta.url), 'utf8'); -let failed = 0; -let passed = 0; // 机器可读汇总用(在 check 内部自增:实现被换空 → 0) -const check = (name, cond, detail = '') => { - if (cond) { - passed++; - } - if (cond) { - console.log(` 通过 ${name}`); - } else { - console.error(` 失败 ${name}${detail ? ' — ' + detail : ''}`); - failed++; - } -}; console.log('窄屏布局回归:'); @@ -238,6 +226,5 @@ check( /\{lead\}\s*