Skip to content

Commit a51bec9

Browse files
CodFrmcyfung1031
andauthored
✅ 抽离 example/tests 共用测试框架 (sctest) 并迁移 12 个测试脚本 (#1631)
* ✅ 新增 example/tests 共用测试框架内核与 Console reporter * 🐛 修复 sctest toEqual 的 JSON 序列化误判并补测试 toEqual 原先用 stringify(actual) !== stringify(expected) 做深比较,导致 NaN 与 null 被误判相等、显式 undefined 键与缺失键被误判相等、对象键顺序 影响比较结果。改为手写的递归结构比较(Object.is 语义 + hasOwnProperty 探测键存在性 + 数组/对象类型互斥 + 循环引用防护),并补齐 toBeTruthy 的 真值/假值用例。 ConsoleReporter 的 MANUAL 分支此前丢弃了 c.hint,console-only 场景下 用户看不到人工确认需要做什么,现追加 hint 到既有 (待人工确认) 文案后。 * ✅ 测试框架新增 Shadow DOM 面板 reporter * ✅ 测试框架新增 GM_log reporter 供后台/定时脚本使用 * 🐛 移除 emitLog 中吞异常的 try/catch,补全跳过/开始日志断言 typeof GM_log === "function" 的判断已完整覆盖未 @grant GM_log 的降级场景, 外层 try/catch 实际只是把已授权 GM_log 抛出的真实异常静默吞掉。移除 try/catch,让已授权 GM_log 的异常正常抛出。 同时为开始日志(sctest:"run")与跳过/人工用例日志(sctest:"case", status:"skip")补上内容校验断言 —— 此前只有 key 数量断言,不会在 level/label 内容错误时失败。 * ✅ 测试框架补齐手动 suite 重跑通路与用法文档 * 🐛 修复面板失败详情在手动 suite 重跑时不渲染的问题 onCase 的更新分支此前只更新图标/耗时/统计,从不生成 .sc-detail, 而 auto:false 的 suite 每个用例首次真正执行时都会先被预渲染成 skip、 从而永远走这条分支——失败用例因此从不展示期望/实际/错误详情。 新增 renderDetail 统一由两条分支调用,重跑时先移除旧详情再按需重建, 避免重复追加;更新分支同时改用既有的 applyStatus 消掉重复表达式。 强化重跑用例的断言,校验行数不翻倍、跳过数清零,并补充详情展示与 失败转通过后详情清除的覆盖。 * ✅ e2e 支持把测试框架 @require 重写到本地 mock server * ✅ gm_api_sync_test 迁移到共用测试框架 * 🐛 恢复 gm_api_sync_test 断言前的过程日志 GM_addValueChangeListener/GM_addElement 迁移时被误删的三条+两条日志, 均在断言之前/之间无条件执行,超时或挂起时仍会打印,是排查这两个 用例卡住位置的唯一线索,应予保留。 * ✅ gm_api_async_test 迁移到共用测试框架 * ✅ inject_content 与 early_inject 系列迁移到共用测试框架 迁移前后计数(全部一致): - inject_content_test.js: e2e passed=11 failed=0 → passed=11 failed=0 - early_inject_content_test.js: 总计 14 通过 14 失败 0 → 总测试数 14 通过 14 失败 0 - early_inject_page_test.js: 总计 14 通过 14 失败 0 → 总测试数 14 通过 14 失败 0 后两个文件无 e2e 覆盖,用一次性 Playwright scratch 脚本核对。 两个 early_inject 文件显式指定 reporter: "console":它们断言 document-start 时 DOM 保持原始态,而面板会往 document.documentElement 挂 #sctest-panel-host, 正好破坏 expect(firstElement.innerHTML).toBe("") 这条断言。这两个文件因此 不显示页面面板——注入型 DOM 面板与 DOM 纯净断言无法共存。 * ✅ sandbox_test 迁移到共用测试框架 e2e gate (e2e/gm-api.spec.ts -g "Sandbox Test"): passed=32, failed=0 before migration; passed=32, failed=0 after migration. N unchanged. * ✅ window_message 与 unwrap 系列迁移到共用测试框架 e2e gate (e2e/gm-api.spec.ts -g "WindowMessage Transport Test"): passed=5, failed=0 before migration; passed=5, failed=0 after migration. N unchanged. e2e gate (e2e/gm-api.spec.ts -g "Unwrap scriptlet tests"): passed=3, failed=0 before migration; passed=3, failed=0 after migration. N unchanged. unwrap_test.js has no e2e coverage; verified with a throwaway Playwright scratch script (e2e/scratch/verify-unwrap-test.spec.ts, git-ignored) that installs the migrated script and confirms it injects and reports passed=3/failed=0 on https://example.com/?test_unwrap_123, and does not inject at all (no panel, no console output) on https://example.com/?test_unwrap_excluded per its @exclude. * ✅ gm_xhr_redirect_test 迁移到共用框架并接入 e2e 首个 B 类文件迁移:删除手写面板 + assertEq,接入 SCTest 框架,首次获得 可解析的汇总行与 e2e 覆盖。tests 数组(basicTests + useFetch 变体)保持 数据驱动,映射为 it(),未手动展开。 e2e 新增用例需要: - patchTargetMatchCode 正则备选组加 GM_XHR_REDIRECT_TEST_SC token - patchGMApiTestCode 新增 HB 常量重写规则(该文件及未迁移的 gm_download_test.js/gm_xhr_test.js 都用 `const HB = "https://httpbun.com"` 拼 URL,走模板字符串后原有的字面量 URL 替换规则匹配不到) - mock server 补 /redirect-to 路由(302 + Location) - mock server /get 路由补上查询串回显(该文件断言 response.url 带 query) 用例数:迁移前(原手写面板,真实 httpbun.com,一次性 scratch 脚本量得) passed=12 failed=0;迁移后(新 e2e,走 mock server)passed=12 failed=0, 完全对齐。 * ✅ gm_download_test 迁移到共用测试框架 纯机械改写:删除手写面板与 logLine/setCounts/setStatus/setQueue 及本地 assertEq/assertTrue,26 个用例按 manual 标志拆成「自动套件」与「手动用例」 两个 auto:false suite(沿用 runAuto 原本 filter((t) => !t.manual) 的区分), prefix 从 suite params 读取。 assertEq(a, b) 是实际在前,转成 expect(a).toBe(b) 不换位。断言语义逐条保持 不变——包括 test 18「empty URL」这条迁移前就在失败的用例,本提交不碰它的 判定,只做形式转换。 e2e 接入放在后续提交:本提交后该文件对真实 httpbun 仍是 19 通过 / 2 失败, 与迁移前基线一致。 * 🐛 修正 gm_download empty URL 用例一开始就写错的断言 该用例断言空 url 必须触发 onerror 或抛异常,但 ScriptCat 从未如此表现: src/app/service/content/gm_api/gm_xhr.ts:230 对 url 统一做 new URL(urlResolved, window.location.href),GM_download 在同一函数的 :265-269 分支复用这条解析,空串按 RFC 3986 解析为当前页地址,下载因此正常成功。 不是迁移引入的回归——迁移前打真实 httpbun.com 就是失败的,对 e2e mock server 与解析源码三处一致且确定。属于 docs/references/develop-testing.md 里 「一开始就写错的断言」这条例外,单独提交以便独立复核或回退。 若日后判定「空 url 应当被拒绝」是正确的产品行为,改的是实现,本用例随之 翻回原判定即可。 * ✅ gm_download_test 接入 e2e @match token GM_DOWNLOAD_TEST_SC 加进 patchTargetMatchCode;两个 suite 都是 auto:false,页面加载不会自动跑,给 runTestScript 加 beforeCollect 钩子在 page.goto 之后点一次「GM_download 自动套件」的运行按钮,手动用例保持不跑。 * 🐛 修复手动 suite 跑完后不重新发出 onEnd 的问题 runManualSuites 只逐条调用 onCase,从不调用 onEnd。后果是对**全部 auto:false 的 文件**,ConsoleReporter 的三行汇总永远停在页面加载时打的 "通过: 0 / 失败: 0" (那时这些用例都被预置为 skip),LogReporter 的汇总日志同样永不出现。 面板因为靠 onCase 实时累加,看起来正常,把这个缺陷掩盖了。 三行汇总是 e2e 的解析契约,所以这等于 B 类文件根本无法用标准路径接入 e2e。 Task 12 当时是在 e2e 侧绕过去的——加了个 runSuiteAndCollectFromPanel 去爬面板 Shadow DOM 读结果。现在根因修好,这段绕行代码一并删除,B 类文件回到与其余文件 相同的 console 汇总路径。 - sctest.js: runManualSuites 结束时 buildSummary + 广播 onEnd,并返回 summary - gm-api.spec.ts: beforeCollect 简化为「只点按钮」,返回 void; 轮询改为「先等首次汇总 → 快照计数 → 点击 → 等下一组汇总」, 避免快照取早了被首次的 0/0 立即满足 验证:sctest 单测 45/45(新增 3 条覆盖 Console/Log/返回值三条契约); gm-api.spec.ts 全部 9 个用例通过,计数与各自基线一致 (inject_content 11、sandbox 32、gm_api_sync 29、gm_xhr_redirect 12、 unwrap_e2e 3、window_message 5、gm_api_async 29、gm_download 21,failed 均为 0) * ✅ 测试框架新增用例内主动跳过通道 SCTest.skip 迁移前 gm_download_test 的 runOne 特判错误消息的 "SKIP:" 前缀来区分跳过, 迁移到共用框架后这个通道没了,5 个手动用例超时或人工点 Skip 全部记为失败。 改用独立的 SkipSignal 类型而非消息前缀嗅探:前缀嗅探会把消息碰巧同名的 真实错误一并吞成跳过。STATUS.SKIP、summary.skipped、面板 sc-chip-skip 与 LogReporter 的 ○ 分支本来就在,这里补的是从用例体内产生 skip 的入口。 顺带修一个真实浏览器里复现的遮挡:verdict bar 原本 fixed 在右上角,与 右下角最高 80vh 的 sctest 面板重叠 2px,两者 z-index 同为最大值而面板挂载 更晚,Skip/Pass/Fail 按钮被吃掉点击。改到左上角。 * ✅ gm_xhr_test 迁移到共用框架并接入 e2e * ✅ gm_menu_test 迁移到共用框架,契约检查升级为断言 8 处 GM_registerMenuCommand 返回值契约检查从软打印 console.log(x === y) 升级为真断言 expect(x).toBe(y),放进 it() 用例;scratch 自动核过全部通过(总20 通过11 失败0 跳过9)。菜单点击观察点转 itManual() 按注册顺序交错;保留三个调试开关与全部注册/注销调用及回调内 console.log 面包屑,仅删除 waitActions/myResolve/waitNext 等待机制。gm_value_test.js 按决定不在本次范围,保持原样。 * ✅ gm_xhr_cookie_test 迁移到共用测试框架 12 个 test() → 12 个 it(),归入 3 个 describe。assert(expected,actual) 是期望在前, 转成 expect(actual).toBe(expected) 逐条换位;assertTrue → toBeTruthy。领域 helper assertCookieValues 保签名与 slice().sort() 集合语义不变,内部 assert 改写为 expect(JSON.stringify(actual)).toBe(JSON.stringify(expected))。 矩阵段的 9 个依赖用例:原文用 `if (matrixRequestPassed && lastCookieMap)` 门控 (请求失败则这 9 个 test 不注册)。声明式框架无法条件注册,故引入 matrixOk 标志 + 每个依赖用例开头 `if (!matrixOk) SCTest.skip(...)`,复刻「前置请求失败则跳过依赖用例 而非各自级联失败」的原语义。 对真实 mockhttp.org 各跑 2 次稳定:迁移前基线 12/12/0,迁移后 12/12/0(含 skip 守卫, happy path 无跳过)。 * 📝 更新 verification 文档以匹配统一后的测试框架输出 example/tests 下 14 个脚本迁移到 sctest.js 后,in-page self-test 只剩一种汇总格式, 不再有原来三种方言。改写「self-test pattern」小节:统一为框架的三行汇总,说明 background/crontab 走 GM_log、人工用例走 itManual,并标注 gm_value_test.js 是刻意 不迁移的交互式演示(无可判定断言、不打印汇总)。正则示例保留,注释从「三种布局」改为 「框架汇总行」。 * 📝 记录 sctest deepEqual 对 Date/Map/Set 的已知限制 终审建议(Minor ①): deepEqual 把 Date/RegExp/Map/Set 当普通对象比较,两个不同 Date 会相等。当前迁移用例的 toEqual 均未触及这些类型,仅补注释说明契约边界,行为不变。 * 🐛 修复 SCTest 面板 CSP 注入与设计对齐 * 🐛 修复 SCTest 面板交互功能 * 🐛 固定 SCTest 的提交引用 * fix: 修正 sctest 异常判定与重跑详情清理 * 🐛 修正 GM_download 空 url 测试契约 * 🐛 include manual cases in skip filter * 🐛 install E2E permission watcher before scripts * 🐛 wait for permission confirmation navigation * 📝 clarify E2E dependency scope * 🐛 watch existing permission pages * 🐛 avoid abort race in DNS error tests * 🐛 make blocked download test local * ♻️ 回退 gm_download_test / gm_menu_test 至迁移前 8/8 审查指出的两个行为回归仍未修复: - gm_download 人工用例经 itManual 注册但从不执行动作(fn:null), 面板可点通过/失败造成假阳性; - gm_menu 丢失了 waitActions 的时间屏障,后续用例会改写待检查的菜单状态。 按 PR 讨论的回退方案,恢复这两个脚本迁移前的自包含实现: - 恢复 example/tests/gm_download_test.js / gm_menu_test.js(无 @require、自带 运行器与人工确认流程); - 移除 e2e/gm-api.spec.ts 中依赖 sctest 面板/统一输出的 GM_download E2E 用例 及其 @match 重写条目;共享的 sctest 本地重写与 mock server 保留(gm_xhr / gm_xhr_redirect 等仍在使用)。 * 📝 把 in-page self-test 一节归位到 verification-methods 并修正过期注释 合并 #1674 时只有 docs/verification.md 被 git 标成冲突,但 #1674 已把 in-page self-test 这一节拆去新建的 references/verification-methods.md, 拆出去的那份没跟着改,留下了本 PR 已推翻的描述: - "the line varies by script" —— 12/15 脚本共用 sctest,输出一致; - 代码块里的「总计: N | 通过: N」「Total/Passed」两种格式全树 grep 零命中; - "matches all three layouts" —— 只剩一种; - 缺 跳过 行、itManual、GM_log 后台通道。 docs/README.md 指明这一节归 verification-methods.md 所有,因此把内容 移过去替换过期段落(保留该文件「会话 + spec 两写」的体例), verification.md 只留指向它的链接 —— 顺带补上 #1674 漏写的反向链接。 同时修正 gm-api.spec.ts 两处随本 PR 失效的注释: - beforeCollect 的「B 类文件 / 真实下载副作用」——「B 类」全树无定义, 且 GM_download E2E 已在 a6bad63 回退,现存唯一消费者是 gm_xhr_test.js; 该套件 auto:false 的理由在脚本自身已写明,此处不复述。 - 「三行汇总里的最后一行」—— 汇总现为四行,且末行是「跳过:」。 验证:tsc/eslint/prettier 通过;vitest 78/78(sctest 66 + verification-tools 12)。 --------- Co-authored-by: cyfung1031 <44498510+cyfung1031@users.noreply.github.com>
1 parent 09fac37 commit a51bec9

19 files changed

Lines changed: 4600 additions & 3619 deletions

docs/references/verification-methods.md

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -19,21 +19,26 @@ In a spec, `testWithUserScripts` and `autoApprovePermissions` solve both ([`../.
1919

2020
### The in-page self-test pattern
2121

22-
A userscript runs assertions in the page and prints a summary line the harness parses from the console. The bundled scripts in [`../../example/tests/`](../../example/tests/) do this; the line varies by script, and each emits a `通过`/`Passed` and a `失败`/`Failed` count:
22+
A userscript runs assertions in the page and prints a summary the harness parses from the console. The bundled scripts in [`../../example/tests/`](../../example/tests/) do this. Most share one framework, [`../../example/tests/lib/sctest.js`](../../example/tests/lib/sctest.js) (loaded via `@require` and rewritten to a local mock server under E2E), so all of those emit the same four lines:
2323

2424
```
25-
总计: 12 | 通过: 12 | 失败: 0 # inject_content_test.js / sandbox_test.js (combined line)
26-
总测试数: 12 / 通过: 12 / 失败: 0 # gm_api_sync_test.js / gm_api_async_test.js (counts on separate lines)
27-
Total: 12 | Passed: 12 | Failed: 0 # window_message_test.js (English)
25+
总测试数: 12
26+
通过: 12
27+
失败: 0
28+
跳过: 0 (34ms)
2829
```
2930

30-
In a session there is nothing to wire up — the collector already recorded the line, whichever context printed it (a `@background` script prints from `src/sandbox.html`, not from a page):
31+
A script running in a background / crontab context has no visible page, so the framework additionally emits one `GM_log` entry per case with structured labels (`sctest`, `status`) — filterable chips on the 运行日志 page. Cases that need a human action (e.g. clicking a menu item) are registered with `itManual` and count as skipped until confirmed on the panel. Writing cases against the framework is [`../../example/tests/lib/README.md`](../../example/tests/lib/README.md)'s.
32+
33+
Three scripts print no unified summary and have to be read on their own terms: [`gm_download_test.js`](../../example/tests/gm_download_test.js) and [`gm_menu_test.js`](../../example/tests/gm_menu_test.js) are self-contained runners carrying their own panel and human-confirmation flow (no `@require`), and [`gm_value_test.js`](../../example/tests/gm_value_test.js) is an interactive multi-frame dashboard demo for `GM_addValueChangeListener` with no machine-checkable assertions.
34+
35+
In a session there is nothing to wire up — the collector already recorded the lines, whichever context printed them (a `@background` script prints from `src/sandbox.html`, not from a page):
3136

3237
```bash
3338
node e2e/drive.mjs console 200 | grep -E "(通过|Passed)[::] *[0-9]+"
3439
```
3540

36-
In a spec, collect and assert on itthis regex matches all three layouts:
41+
In a spec, collect and assert on themsame parse as the committed `gm-api.spec.ts` harness:
3742

3843
```ts
3944
const logs: string[] = [];
@@ -52,7 +57,7 @@ expect(failed, logs.join("\n")).toBe(0);
5257
expect(passed).toBeGreaterThan(0);
5358
```
5459

55-
For a new GM API, write a small self-test userscript in the same style. In a session, `node e2e/drive.mjs install <file.user.js>` installs it through the Service Worker and `node e2e/drive.mjs console` shows the summary line the script printed; in a spec, use `installScriptByCode`. Keep the script inside the scenario directory — it is verification scaffolding, not a committed example.
60+
For a new GM API, write a small self-test userscript in the same style. In a session, `node e2e/drive.mjs install <file.user.js>` installs it through the Service Worker and `node e2e/drive.mjs console` shows the summary the script printed; in a spec, use `installScriptByCode`. Keep the script inside the scenario directory — it is verification scaffolding, not a committed example.
5661

5762
## Behaviour fired from extension UI
5863

docs/verification.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,8 +57,10 @@ pnpm exec playwright test --config playwright.scratch.config.ts -g "<test title>
5757

5858
## Driving the session
5959

60-
[`../e2e/README.md`](../e2e/README.md#8-verification-sessions) owns the command reference. What matters for a
61-
verdict:
60+
[`../e2e/README.md`](../e2e/README.md#8-verification-sessions) owns the command reference, and
61+
[`references/verification-methods.md`](references/verification-methods.md) the patterns for behaviour the UI does
62+
not expose directly — the `example/tests/` in-page self-tests, Service Worker messages, themes. What matters for
63+
a verdict:
6264

6365
- **Observe from a path the driven surface does not share.** `drive.mjs storage` reads `chrome.storage.local`
6466
from an extension page, and `drive.mjs sw` evaluates inside the Service Worker — neither goes through the UI

0 commit comments

Comments
 (0)