✅ 抽离 example/tests 共用测试框架 (sctest) 并迁移 12 个测试脚本 - #1631
Conversation
toEqual 原先用 stringify(actual) !== stringify(expected) 做深比较,导致 NaN 与 null 被误判相等、显式 undefined 键与缺失键被误判相等、对象键顺序 影响比较结果。改为手写的递归结构比较(Object.is 语义 + hasOwnProperty 探测键存在性 + 数组/对象类型互斥 + 循环引用防护),并补齐 toBeTruthy 的 真值/假值用例。 ConsoleReporter 的 MANUAL 分支此前丢弃了 c.hint,console-only 场景下 用户看不到人工确认需要做什么,现追加 hint 到既有 (待人工确认) 文案后。
typeof GM_log === "function" 的判断已完整覆盖未 @grant GM_log 的降级场景, 外层 try/catch 实际只是把已授权 GM_log 抛出的真实异常静默吞掉。移除 try/catch,让已授权 GM_log 的异常正常抛出。 同时为开始日志(sctest:"run")与跳过/人工用例日志(sctest:"case", status:"skip")补上内容校验断言 —— 此前只有 key 数量断言,不会在 level/label 内容错误时失败。
onCase 的更新分支此前只更新图标/耗时/统计,从不生成 .sc-detail, 而 auto:false 的 suite 每个用例首次真正执行时都会先被预渲染成 skip、 从而永远走这条分支——失败用例因此从不展示期望/实际/错误详情。 新增 renderDetail 统一由两条分支调用,重跑时先移除旧详情再按需重建, 避免重复追加;更新分支同时改用既有的 applyStatus 消掉重复表达式。 强化重跑用例的断言,校验行数不翻倍、跳过数清零,并补充详情展示与 失败转通过后详情清除的覆盖。
GM_addValueChangeListener/GM_addElement 迁移时被误删的三条+两条日志, 均在断言之前/之间无条件执行,超时或挂起时仍会打印,是排查这两个 用例卡住位置的唯一线索,应予保留。
迁移前后计数(全部一致):
- 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 纯净断言无法共存。
e2e gate (e2e/gm-api.spec.ts -g "Sandbox Test"): passed=32, failed=0 before migration; passed=32, failed=0 after migration. N unchanged.
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.
首个 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, 完全对齐。
纯机械改写:删除手写面板与 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 失败, 与迁移前基线一致。
该用例断言空 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 应当被拒绝」是正确的产品行为,改的是实现,本用例随之 翻回原判定即可。
@match token GM_DOWNLOAD_TEST_SC 加进 patchTargetMatchCode;两个 suite 都是 auto:false,页面加载不会自动跑,给 runTestScript 加 beforeCollect 钩子在 page.goto 之后点一次「GM_download 自动套件」的运行按钮,手动用例保持不跑。
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)
迁移前 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 按钮被吃掉点击。改到左上角。
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 按决定不在本次范围,保持原样。
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 无跳过)。
example/tests 下 14 个脚本迁移到 sctest.js 后,in-page self-test 只剩一种汇总格式, 不再有原来三种方言。改写「self-test pattern」小节:统一为框架的三行汇总,说明 background/crontab 走 GM_log、人工用例走 itManual,并标注 gm_value_test.js 是刻意 不迁移的交互式演示(无可判定断言、不打印汇总)。正则示例保留,注释从「三种布局」改为 「框架汇总行」。
终审建议(Minor ①): deepEqual 把 Date/RegExp/Map/Set 当普通对象比较,两个不同 Date 会相等。当前迁移用例的 toEqual 均未触及这些类型,仅补注释说明契约边界,行为不变。
|
應該要先合併 httpbun改 httpbingo那個。。。 |
不要用 |
|
老实说我觉得这样搞不好 |
好吧, 等完成再重新看 |
现在差不多就是完成了
可以接受,可以列到下个月,这个月看看能不能把 外部接入mcp 搞完了 |
|
按 PickInvariant 从“执行语义是否可重建”的角度看,这里有两个和 rebase / 落后 main 无关的行为回归,建议修复后再合并:
用 PickInvariant 的对比来说:当前表示把“人工项显示为可判定”当成了足够信息,但 “实际动作已执行 / 根本未执行”、“检查的是目标中间状态 / 已被后续步骤改写的状态” 都能得到同一个 manual UI 状态,却要求不同的测试结论;缺失的是执行与时序边界(B/χ)信息,所以目前不能重建原测试语义。 |
|
@cyfung1031 关于 gm_download_test 和 gm_menu_test,我之前也发现了,可以先直接回退一下这两个test #1632 能不能 close,抽离共用测试框架这个方向没问题的话,就集中在这里处理 |
|
补充提交
同时新增了对应回归测试,覆盖 falsy 异常值和“失败后重跑通过”的结果清理。定向 sctest 测试 65/65 通过,全量测试 311 个文件、3561 个测试通过。 |
|
已将 这次 CI 日志中的 验证:目标 E2E 同步提交: |
|
已基于最新 相对原 PR 新增的修正 commits:
最终 head 的本地验证:
GitHub CI 已在最终 head |
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 等仍在使用)。
- docs/verification.md:采用 #1674 重写后的“常驻会话驱动”结构, 将 PR 的 in-page self-test 说明作为子节并入 “Driving the session”, 并注明 gm_download/gm_menu(已回退)与 gm_value 不输出统一汇总。 - e2e/gm-api.spec.ts:合并 main 的 headlessArgs 重构(保留 type Page 与 mock server 的 http 类型导入),去除 gm_download E2E 后共享设施不变。 - 回退后的 gm_download_test.js / gm_menu_test.js 与 main 完全一致 (自包含实现 + main 的 httpbingo 更新)。 验证:tsc/lint/build 通过;sctest 单测 66/66;gm-api E2E 12/12(gm_xhr 138、 gm_xhr_redirect 12、gm_api_sync 29、gm_api_async 29、inject_content 11、 window_message 5、sandbox 32、unwrap 3,failed 全 0)。
合并 #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)。

Checklist / 检查清单
背景
example/tests/下 15 个用户脚本形态的测试文件各自内嵌一份手写的测试运行器、结果面板与断言函数,风格不一(三种控制台汇总方言、5 份独立结果 UI),维护成本高;后台/定时脚本也缺乏统一的无界面日志通道。本次改动
example/tests/lib/sctest.js(@require直接加载),提供describe/it/itManual/expect/run与三个展示通道:总测试数/通过/失败,是 e2e 的解析契约)prefers-color-scheme)GM_log输出带sctest/status标签的结构化日志,可在运行日志页按标签过滤)toBe/toEqual/toBeTruthy/toBeTypeOf/toMatch/toThrow,实际值在前);toEqual用真实结构化递归深比较(键顺序不敏感、Object.is语义、区分 NaN/null 与 undefined 键)。SCTest.skip(reason)运行时跳过(用 sentinel 而非消息前缀嗅探)。e2e/gm-api.spec.ts扩展为从本地 mock server 提供框架(@require本地重写),并新接入 gm_xhr_redirect / gm_xhr 两个脚本的 e2e。example/tests/lib/{README.md, sctest.test.js}:用法文档 + 66 个框架单测。docs/references/verification-methods.md的 in-page self-test 小节以匹配统一后的输出(该小节在 ♻️ 重构本地验证流程:常驻会话 + 命令驱动,替代一次性 spec #1674 中已从docs/verification.md拆出,本 PR 合并 main 后归位到拆出后的文件)。实现考虑
@require只接受一个 URL 的硬约束,非疏忽;文件内按上下文检测 / expect / 运行核心 / 三个 reporter 分区。GM_info.scriptMetaStr的@background/@crontab,而非typeof document(后台脚本跑在 offscreen 文档里,document 存在)。onEnd,否则所有auto:false文件的三行汇总会停在加载时的 0/0。gm_xhr_cookie矩阵段的 9 个依赖用例用matrixOk标志 +SCTest.skip复刻原if (matrixRequestPassed)门控(声明式框架无法条件注册用例)。已知限制
gm_download_test.js/gm_menu_test.js已回退到迁移前的自包含实现(a6bad63a):迁移版有两个未修复的行为回归 —— gm_download 的人工用例经itManual注册却从不执行动作(fn:null),面板可点通过/失败造成假阳性;gm_menu 丢失了waitActions的时间屏障,后续用例会改写待检查的菜单状态。相应地e2e/gm-api.spec.ts里的 GM_download e2e 用例一并移除,共享的@require本地重写与 mock server 保留(gm_xhr / gm_xhr_redirect 仍在用)。gm_value_test.js刻意不迁移:它是GM_addValueChangeListener的交互式多框架 dashboard 演示,无可判定断言、不打印汇总;强套 pass/fail 框架会毁掉其跨框架可视化价值。gm_xhr_cookie_test.js依赖外部mockhttp.org,仍无常驻 e2e(补 e2e 需在 mock server 实现整套 cookie 回显语义,属独立工作)。deepEqual把 Date/RegExp/Map/Set 当普通对象比较;当前所有toEqual用例未触及这些类型(已在源码注释)。@connect值导致脚本静默安装失败),已实测复现,超出本 PR 范围,将另开 issue 跟进。建议审查重点
example/tests/lib/sctest.js的deepEqual、运行核心(runCase/runManualSuites的 onEnd 重发)、SkipSignal与三个 reporter。e2e/gm-api.spec.ts的 mock server 路由对既有消费者是否零干扰、@require是否始终命中本地重写。验证
已合并
origin/main(9e31780f,含 #1674 验证流程重构)。以下为合并后复核:pnpm exec tsc --noEmit→ exit 0pnpm exec eslint/prettier --check→ exit 0pnpm test:ci→ 330 files / 3788 tests,3787 通过(含 sctest 66 单测全绿)。唯一失败是LocalBackupSection.test.tsx的负载超时 flake:该文件不在本 PR 改动范围内,单独重跑通过,属vitest.config.ts注释已记录的fast项目 340ms 预算在满载下的既有抖动。pnpm exec playwright test e2e/gm-api.spec.ts→ 12/12 全绿(于合并点66092c79跑过),各脚本计数:gm_xhr 138 / sandbox 32 / gm_api_sync 29 / gm_api_async 29 / gm_xhr_redirect 12 / inject_content 11 / window_message 5 / unwrap_e2e 3,failed 全 0。