✅ 抽离 example/tests 共用测试框架 (sctest) 并全量迁移 14 个测试脚本#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那個。。。 |
不要用 |
|
老实说我觉得这样搞不好 |
|
我先想想。 |
那些脚本根本不用维护。就只是用来展示和测试 每一个脚本一眼看到所有代码是不错的 |
|
本身也有 console.assert 那些东西 硬要用一个自家土炮制作的东西来写测试 为什么要这样搞 每个 userscript test 独立,不引用「库」,才是低维护成本的做法。 vanilla javascript 走回头路 @CodFrm 我强烈反对这个PR 能直接关掉这PR吗 对实质测试内容测试对像没帮忙 |
|
之後提交一個pr讓寫法一致一點 |
|
现在每一个test都有assert这些共有的方法,我这个库也只是抽离这些共有的东西进行复用,没有增加任何复杂度,反而是在降低复杂度,也不需要研究这个测试工具,并且这个工具并不复杂(而且现在几乎都是AI了) 现在每一个测试文件都是不同的写法,UI面板也各不相同,如果不复用一套没有一个标准,以后再来test又是另外一套了,或者想增加一个某个特性,又需要每个脚本都修改一下 软件工程都是在说高内聚,低耦合,我反而想不通你为什么会说没有价值 另外我还是想要有UI面板的,一些手动触发的测试可以显示到UI面板上,而且打开页面也可以一眼看清,现在一些没UI面板的测试,我总是要打开开发者工具去看console,而且信息很乱 |
好吧, 等完成再重新看 |

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_download / gm_xhr 三个脚本的 e2e。example/tests/lib/{README.md, sctest.test.js}:用法文档 + 51 个框架单测。docs/verification.md的 in-page self-test 小节以匹配统一后的输出。实现考虑
@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_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是否始终命中本地重写。验证
pnpm lint→ exit 0pnpm exec tsc --noEmit→ exit 0pnpm test:ci→ 311 files / 3503 tests 全绿(含 sctest 51 单测)pnpm build→ exit 0pnpm exec playwright test e2e/gm-api.spec.ts→ 全绿,各脚本计数与基线一致:inject_content 11 / sandbox 32 / gm_api_sync 29 / unwrap_e2e 3 / gm_xhr_redirect 12 / gm_api_async 29 / window_message 5 / gm_download 21 / gm_xhr 138(failed 全 0)