♻️ 重构本地验证流程:常驻会话 + 命令驱动,替代一次性 spec - #1674
Merged
Merged
Conversation
启动一次浏览器常驻,之后逐条命令附着驱动,验证一件事不再需要 先写一个 spec 再整体重跑。 - session.mjs:start/status/stop,默认无头,--headed 供人工旁观 - drive.mjs:open/goto/click/fill/snapshot/shot/eval/sw/storage/install 等 - 端口向内核申请(listen(0))、profile 走 mkdtemp、证据落各自的 scenario 目录,多个 worktree 可并发验证 - 会话全程记录 console/pageerror 与操作,报告直接引用
原先 9 处把 "--headless=new" 写死在 args 里,而启动参数会覆盖 Playwright 的 headless 选项,所以 headless: false(含 --debug/PWDEBUG) 其实一直开不出窗口。统一走 headlessArgs(),E2E_HEADED=1 才可见。
verification.md 以驱动会话为默认形式,写 spec 收敛为「顺序/时序即契约」 时的例外;驱动方法拆到 references/verification-methods.md;e2e/README.md 新增 §8 会话手册(命令表、证据目录、并发、能力边界)。
Playwright 只暴露页面的 console 事件,但脚本自测与后台脚本的断言打在 Service Worker、Offscreen 文档和 Sandbox iframe 里 —— 这些各自是独立 的 CDP target。改为连浏览器级 CDP 端点并递归 setAutoAttach。 - waitForDebuggerOnStart 保证 document-start 的输出不漏 - 每个 target 只 Runtime.enable 一次:重复 enable 会重放 console 历史 - 开 setDiscoverTargets,否则导航后 URL 停在附着那一刻 - 对象参数按 preview 渲染,不再是无信息的 Object
Collaborator
|
针对原 PR「♻️ 重构本地验证流程:常驻会话 + 命令驱动,替代一次性 spec」,我在原分支追加了 3 个修正 commits(未 force-push,未修改 PR 标题和描述):
本地验证证据:
|
CodFrm
added a commit
that referenced
this pull request
Aug 17, 2026
- 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)。
CodFrm
added a commit
that referenced
this pull request
Aug 17, 2026
合并 #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)。
3 tasks
CodFrm
added a commit
that referenced
this pull request
Aug 18, 2026
* ✅ 新增 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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Checklist / 检查清单
N/A — 无关联 issue,本次是本地验证流程的自发重构。
Description / 描述
背景
本地验证一直是「写一个 spec → 整体跑一遍 → 看输出 → 想换个角度看 → 再改再跑」。
代价在仓库里是可量化的:
e2e/scratch/里累计了 20 个一次性 spec、6871 行,是 committed 套件(3251 行)的 2.1 倍,而它们几乎都只被跑过一次。
根因是没有可附着的浏览器:全仓
grep -rn "remote-debugging\|connectOverCDP"零命中,每次观察都必须从头启动一个 spec。
本次改动
e2e/session.mjs:启动一个常驻的、加载dist/ext的浏览器并保持存活。start/status/stop --all。e2e/drive.mjs:每次调用是一个短命进程,附着到会话做一件事。open/goto/click/fill/press/wait/snapshot/text/shot/eval/sw/storage/install/pages/use/close/console。Offscreen 文档、Sandbox iframe 里,它们各自是独立的 CDP target,Playwright 只暴露
页面的 console 事件。改为连浏览器级 CDP 端点并递归
setAutoAttach,四类上下文统一落到一个
console.log并标注来源。E2E_HEADED:原先 9 处把"--headless=new"写死在args里,覆盖了Playwright 的
headless选项 —— 也就是说headless: false和--debug一直开不出窗口。现在统一走
headlessArgs()。docs/verification.md改以驱动会话为默认形式;e2e/README.md新增§8 会话手册;驱动方法拆到
docs/references/verification-methods.md。实现考虑
--headed只为人工旁观保留。listen(0))而不写死;profile 走
mkdtemp,证据落各自的 scenario 目录。多个 worktree 可同时验证。已实测两个会话互不串扰(一个装了脚本读到 1,另一个读到 0)。
updateExtensionConfiguration授权userScripts会重载扩展,重载期间扩展自己的页面返回
ERR_BLOCKED_BY_CLIENT。所以授权放在一次性启动里,再从同一 profile 重启接管 —— 与既有 fixture 已验证过的两阶段模式同源。
Runtime.enable一次,重复 enable 会重放该上下文的 console 历史造成成倍重复;不开
setDiscoverTargets就收不到targetInfoChanged,页面导航后来源会一直停在附着那一刻的
about:blank;对象参数不带value,必须按preview渲染,否则断言里的
{passed, failed}只会记成Object。snapshot的选择器优先级:testid > 稳定 id > 可见文本。Radix 生成的radix-_r_0_每次渲染都会变,明确排除。已知限制
会话不适合验证 Service Worker 的驱逐 / 空闲终止行为,那条线仍走
keep-alive.spec.ts。.test域名映射,需要 mock server 的验证仍走 scratch spec。snapshot不进 iframe。e2e/scratch/下清掉了 21 个一次性文件;发版配图工具(
mobile-shots.spec.ts/promo-render.spec.ts/_assets/)保留,它们仍是平铺的,未纳入
<scenario>/约定。建议审查重点
e2e/fixtures.ts的headlessArgs()被另外三个 fixture 引入,确认 CI 仍为无头。attachConsoleCollector():waitForDebuggerOnStart要求每个 target 都必须收到runIfWaitingForDebugger,否则新 target 会被永久挂起 —— 这条路径值得重点看。session.mjs的 SIGTERM/context.on("close")清理路径,是否会残留 profile 或进程。drive.mjs的wrapEvalSource():带return当函数体、否则当表达式。验证
覆盖了全部 4 个被改动的 fixture 文件(
fixtures/server-fixtures/agent-fixtures/gm-api内联 fixture)。会话本身的实测(非推断):
(src/service_worker.js)、(src/sandbox.html)(装一个
@background脚本并启用后打出的行)、(src/options.html);对象渲染为{passed: 29, failed: 0}而非Objectstop --all后无残留 profile 与进程E2E_HEADED=1与session.mjs --headed均确认开出真实窗口(UA 无Headless)dist/ext/manifest.json缺失时start拒绝启动,退出码 1snapshot→click [data-testid="theme-toggle"]→documentElement.className变为
dark、lightMode=dark文档检查:6 个文档的 69 条相对链接与锚点全部解析通过;文中每条命令均已实跑。