Skip to content

✅ 抽离 example/tests 共用测试框架 (sctest) 并全量迁移 14 个测试脚本#1631

Draft
CodFrm wants to merge 29 commits into
mainfrom
feat/example-tests-framework
Draft

✅ 抽离 example/tests 共用测试框架 (sctest) 并全量迁移 14 个测试脚本#1631
CodFrm wants to merge 29 commits into
mainfrom
feat/example-tests-framework

Conversation

@CodFrm

@CodFrm CodFrm commented Jul 21, 2026

Copy link
Copy Markdown
Member

Checklist / 检查清单

  • Changes tested / 已完成测试
  • Code reviewed by human / 代码通过人工检查
  • Fixes mentioned issues / 修复已提及的问题(无关联 issue)

背景

example/tests/ 下 15 个用户脚本形态的测试文件各自内嵌一份手写的测试运行器、结果面板与断言函数,风格不一(三种控制台汇总方言、5 份独立结果 UI),维护成本高;后台/定时脚本也缺乏统一的无界面日志通道。

本次改动

  • 新增单文件、零依赖、零构建的测试框架 example/tests/lib/sctest.js@require 直接加载),提供 describe/it/itManual/expect/run 与三个展示通道:
    • ConsoleReporter(恒开,三行汇总 总测试数/通过/失败,是 e2e 的解析契约)
    • PanelReporter(Shadow DOM 面板,跟随系统 prefers-color-scheme
    • LogReporter(后台/定时脚本经 GM_log 输出带 sctest/status 标签的结构化日志,可在运行日志页按标签过滤)
  • 断言统一为 6 个 matcher(toBe/toEqual/toBeTruthy/toBeTypeOf/toMatch/toThrow,实际值在前);toEqual 用真实结构化递归深比较(键顺序不敏感、Object.is 语义、区分 NaN/null 与 undefined 键)。
  • 用例内可 SCTest.skip(reason) 运行时跳过(用 sentinel 而非消息前缀嗅探)。
  • 14 个测试脚本全量迁移到该框架,删除各自的手写基建;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 分区。
  • 运行上下文(page/background/crontab)解析 GM_info.scriptMetaStr@background/@crontab,而非 typeof document(后台脚本跑在 offscreen 文档里,document 存在)。
  • 手动 suite 跑完后重新广播 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 用例未触及这些类型(已在源码注释)。
  • 迁移中发现一个产品 bug(重复 @connect 值导致脚本静默安装失败),已实测复现,超出本 PR 范围,将另开 issue 跟进。

建议审查重点

  • example/tests/lib/sctest.jsdeepEqual、运行核心(runCase/runManualSuites 的 onEnd 重发)、SkipSignal 与三个 reporter。
  • e2e/gm-api.spec.ts 的 mock server 路由对既有消费者是否零干扰、@require 是否始终命中本地重写。
  • 迁移是否行为等价:各脚本用例数与通过/失败是否与迁移前基线一致(见验证)。

验证

  • pnpm lint → exit 0
  • pnpm exec tsc --noEmit → exit 0
  • pnpm test:ci → 311 files / 3503 tests 全绿(含 sctest 51 单测)
  • pnpm build → exit 0
  • pnpm 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)
  • 无常驻 e2e 的脚本(gm_menu / gm_xhr_cookie / early_inject 等)用一次性 scratch 脚本对真实环境核过计数一致。

分支基线 c7543bb0origin/main 已前进到 0d45b0b7,合并前可能需 rebase。

CodFrm added 26 commits July 21, 2026 13:21
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 均未触及这些类型,仅补注释说明契约边界,行为不变。
@cyfung1031

Copy link
Copy Markdown
Collaborator

應該要先合併 httpbun改 httpbingo那個。。。
否則合併這個後那個 conflict resolve很麻煩吧

@cyfung1031

Copy link
Copy Markdown
Collaborator
https://cdn.jsdelivr.net/gh/scriptscat/scriptcat@main/example/tests/lib/sctest.js

不要用 @main. 要指定 commit

@cyfung1031

Copy link
Copy Markdown
Collaborator

老实说我觉得这样搞不好
userscript 应该能独立跑
硬要套一个外部引用很奇怪

@cyfung1031
cyfung1031 marked this pull request as draft July 21, 2026 22:18
@cyfung1031

Copy link
Copy Markdown
Collaborator

我先想想。

@cyfung1031

Copy link
Copy Markdown
Collaborator

example/tests/ 下 15 个用户脚本形态的测试文件各自内嵌一份手写的测试运行器、结果面板与断言函数,风格不一(三种控制台汇总方言、5 份独立结果 UI),维护成本高;后台/定时脚本也缺乏统一的无界面日志通道。

@CodFrm

那些脚本根本不用维护。就只是用来展示和测试
为什么一定要统一?
每个测试的内容都不一样,很难统一的
而且本身的写法也很简单
为什么要搞到这么复杂?

每一个脚本一眼看到所有代码是不错的
引用的话反而看不到测试套件的逻辑

@cyfung1031

cyfung1031 commented Jul 21, 2026

Copy link
Copy Markdown
Collaborator

本身也有 console.assert 那些东西
不喜欢自定一堆 UI的话可以直接用 console.assert
不用搞一个 SCTest

硬要用一个自家土炮制作的东西来写测试
维护成本更大
以后要写一个简单测试脚本,也要去「配合」这个SCTest
想到这里就觉得很辛苦了

为什么要这样搞
完全没有价值


每个 userscript test 独立,不引用「库」,才是低维护成本的做法。
一个普通用户想看看某个 GM API 怎样用
直接看 userscript test 就行
结果原来要先研究一下一个不知名的测试工具是怎么设计
这才是本末倒置

vanilla javascript 走回头路


@CodFrm 我强烈反对这个PR

能直接关掉这PR吗
不要搞形式主义

对实质测试内容测试对像没帮忙

@cyfung1031

Copy link
Copy Markdown
Collaborator

之後提交一個pr讓寫法一致一點

@CodFrm

CodFrm commented Jul 22, 2026

Copy link
Copy Markdown
Member Author

现在每一个test都有assert这些共有的方法,我这个库也只是抽离这些共有的东西进行复用,没有增加任何复杂度,反而是在降低复杂度,也不需要研究这个测试工具,并且这个工具并不复杂(而且现在几乎都是AI了)

现在每一个测试文件都是不同的写法,UI面板也各不相同,如果不复用一套没有一个标准,以后再来test又是另外一套了,或者想增加一个某个特性,又需要每个脚本都修改一下

软件工程都是在说高内聚,低耦合,我反而想不通你为什么会说没有价值

另外我还是想要有UI面板的,一些手动触发的测试可以显示到UI面板上,而且打开页面也可以一眼看清,现在一些没UI面板的测试,我总是要打开开发者工具去看console,而且信息很乱

@CodFrm

CodFrm commented Jul 22, 2026

Copy link
Copy Markdown
Member Author

一个普通用户想看看某个 GM API 怎样用

直接看 userscript test 就行

结果原来要先研究一下一个不知名的测试工具是怎么设计

根本不需要研究测试工具,代码里面只有describe、it、expect这些通用的方法/断言和vitest框架保持一致,相比原来,反而结构更加清晰,就像脚本猫代码一样,你也不会去研究那些单元测试框架怎么跑的吧

而且当想添加一些特性的时候,不用修改每一个测试,只需要修改库就好了,我的观点和你完全相反

每个测试的内容都不一样,很难统一的

只统一 describe、it、expect 这些测试断言的,现在好几个脚本都是如此的了,不用这些的不引用就是

整体和方向我认为没问题,只是可能有些点不合适,可以进行修改和调整


QQ_1784690733824

这种面板来显示测试不清晰得多么,还可以再优化一下,现在的面板的UI/UX都不统一,也非常的难用

另外这个PR还不算完成,还有些问题需要调整,昨天交给AI后就睡觉了

@cyfung1031

cyfung1031 commented Jul 22, 2026

Copy link
Copy Markdown
Collaborator

另外这个PR还不算完成,还有些问题需要调整,昨天交给AI后就睡觉了

好吧, 等完成再重新看

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants