Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
bf54561
✅ 新增 example/tests 共用测试框架内核与 Console reporter
CodFrm Jul 21, 2026
c589d60
🐛 修复 sctest toEqual 的 JSON 序列化误判并补测试
CodFrm Jul 21, 2026
2a6ee46
✅ 测试框架新增 Shadow DOM 面板 reporter
CodFrm Jul 21, 2026
cc66c5b
✅ 测试框架新增 GM_log reporter 供后台/定时脚本使用
CodFrm Jul 21, 2026
b743444
🐛 移除 emitLog 中吞异常的 try/catch,补全跳过/开始日志断言
CodFrm Jul 21, 2026
ed52b93
✅ 测试框架补齐手动 suite 重跑通路与用法文档
CodFrm Jul 21, 2026
58f7ea5
🐛 修复面板失败详情在手动 suite 重跑时不渲染的问题
CodFrm Jul 21, 2026
bb5bbdc
✅ e2e 支持把测试框架 @require 重写到本地 mock server
CodFrm Jul 21, 2026
79f761e
✅ gm_api_sync_test 迁移到共用测试框架
CodFrm Jul 21, 2026
dbf482e
🐛 恢复 gm_api_sync_test 断言前的过程日志
CodFrm Jul 21, 2026
5263027
✅ gm_api_async_test 迁移到共用测试框架
CodFrm Jul 21, 2026
fa8f0ae
✅ inject_content 与 early_inject 系列迁移到共用测试框架
CodFrm Jul 21, 2026
c6af8c8
✅ sandbox_test 迁移到共用测试框架
CodFrm Jul 21, 2026
ac87145
✅ window_message 与 unwrap 系列迁移到共用测试框架
CodFrm Jul 21, 2026
e985306
✅ gm_xhr_redirect_test 迁移到共用框架并接入 e2e
CodFrm Jul 21, 2026
d1ad1ba
✅ gm_download_test 迁移到共用测试框架
CodFrm Jul 21, 2026
0ee30ea
🐛 修正 gm_download empty URL 用例一开始就写错的断言
CodFrm Jul 21, 2026
033a14c
✅ gm_download_test 接入 e2e
CodFrm Jul 21, 2026
1c1d3ef
🐛 修复手动 suite 跑完后不重新发出 onEnd 的问题
CodFrm Jul 21, 2026
a1c4483
✅ 测试框架新增用例内主动跳过通道 SCTest.skip
CodFrm Jul 21, 2026
01f3250
✅ gm_xhr_test 迁移到共用框架并接入 e2e
CodFrm Jul 21, 2026
dde2ad7
✅ gm_menu_test 迁移到共用框架,契约检查升级为断言
CodFrm Jul 21, 2026
3038fb0
✅ gm_xhr_cookie_test 迁移到共用测试框架
CodFrm Jul 21, 2026
ebf32b1
📝 更新 verification 文档以匹配统一后的测试框架输出
CodFrm Jul 21, 2026
5bd1142
📝 记录 sctest deepEqual 对 Date/Map/Set 的已知限制
CodFrm Jul 21, 2026
2b486f3
Merge branch 'main' into feat/example-tests-framework
CodFrm Jul 21, 2026
f0fd9ef
♻️ 合并 main 并适配 httpbingo 测试
CodFrm Jul 22, 2026
e2fe707
🐛 修复 SCTest 面板 CSP 注入与设计对齐
CodFrm Jul 22, 2026
762f83e
🐛 修复 SCTest 面板交互功能
CodFrm Jul 22, 2026
8382318
🐛 固定 SCTest 的提交引用
CodFrm Jul 27, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 17 additions & 8 deletions docs/verification.md
Original file line number Diff line number Diff line change
Expand Up @@ -201,18 +201,27 @@ copy that file's inline fixture and helpers into your scratch script rather than
### The in-page self-test pattern

The canonical way to verify GM APIs / injection is a userscript that **runs assertions in the page and prints a
summary line**, which the harness parses from the console. The bundled scripts in
[`example/tests/`](../example/tests/) (e.g. `gm_api_sync_test.js`, `gm_api_async_test.js`,
`inject_content_test.js`, `sandbox_test.js`, `window_message_test.js`) do exactly this. The exact line varies by
script — what matters is that each emits a `通过`/`Passed` and a `失败`/`Failed` count the harness can parse:
summary the harness parses from the console**. The bundled scripts in [`example/tests/`](../example/tests/) all
share one framework, [`example/tests/lib/sctest.js`](../example/tests/lib/sctest.js) (loaded via `@require`), and
therefore all emit the same summary lines:

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

Collect and assert on it from your scratch script (the regex below matches all three layouts):
Scripts that run in a background / crontab context have no visible page, so the same framework additionally emits
one `GM_log` entry per case with structured labels (`sctest`, `status`), which show up as 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.

> The one exception is [`gm_value_test.js`](../example/tests/gm_value_test.js) — an interactive multi-frame
> dashboard demo for `GM_addValueChangeListener` with no machine-checkable assertions. It is intentionally left
> off the framework and prints no summary line.

Collect and assert on the summary from your scratch script (the regex below matches the framework's summary lines):

```ts
const logs: string[] = [];
Expand Down
532 changes: 500 additions & 32 deletions e2e/gm-api.spec.ts

Large diffs are not rendered by default.

278 changes: 101 additions & 177 deletions example/tests/early_inject_content_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,215 +15,139 @@
// @grant GM_getValue
// @grant GM_deleteValue
// @grant GM_listValues
// @require https://cdn.jsdelivr.net/gh/scriptscat/scriptcat@762f83e9c1091ab4ebbb605f4efc4709b36f6476/example/tests/lib/sctest.js
// @run-at document-start
// ==/UserScript==

// 测试辅助函数(支持同步和异步)
async function test(name, fn) {
testResults.total++;
try {
await fn();
testResults.passed++;
console.log(`%c✓ ${name}`, "color: green;");
return true;
} catch (error) {
testResults.failed++;
console.error(`%c✗ ${name}`, "color: red;", error);
return false;
}
}

function testSync(name, fn) {
testResults.total++;
try {
fn();
testResults.passed++;
console.log(`%c✓ ${name}`, "color: green;");
return true;
} catch (error) {
testResults.failed++;
console.error(`%c✗ ${name}`, "color: red;", error);
return false;
}
}

// assert(expected, actual, message) - 比较两个值是否相等
function assert(expected, actual, message) {
if (expected !== actual) {
const valueInfo = `期望 ${JSON.stringify(expected)}, 实际 ${JSON.stringify(actual)}`;
const error = message ? `${message} - ${valueInfo}` : `断言失败: ${valueInfo}`;
throw new Error(error);
}
}

// assertTrue(condition, message) - 断言条件为真
function assertTrue(condition, message) {
if (!condition) {
throw new Error(message || "断言失败: 期望条件为真");
}
}

console.log("%c=== Content环境 GM API 测试开始 ===", "color: blue; font-size: 16px; font-weight: bold;");

let testResults = {
passed: 0,
failed: 0,
total: 0,
};
// reporter: "console" — 用例本身在断言 document-start 时 DOM 应保持原始态(head/body 均不存在、
// 唯一节点 innerHTML 为空);Panel reporter 会在 run() 开始时把浮层面板挂到 document.documentElement
// 下,抢在该断言之前弄脏这个待验证的原始态,所以这里必须关闭 Panel,只留 Console 通道。
const { describe, it, expect, run } = SCTest.create({ name: "Early-start 测试(content 环境)", reporter: "console" });

// 同步测试

// ============ GM_addElement/GM_addStyle 测试 ============
console.log("\n%c--- DOM操作 API 测试 ---", "color: orange; font-weight: bold;");

testSync("GM_addElement", () => {
const element = GM_addElement("div", {
textContent: "GM_addElement测试元素",
style: "display:none;",
id: "gm-test-element",
describe("DOM操作 API 测试", () => {
it("GM_addElement", () => {
const element = GM_addElement("div", {
textContent: "GM_addElement测试元素",
style: "display:none;",
id: "gm-test-element",
});
expect(element !== null && element !== undefined).toBeTruthy();
expect(element.id).toBe("gm-test-element");
expect(element.tagName).toBe("DIV");
// 清理测试元素
element.parentNode.removeChild(element);
});
assertTrue(element !== null && element !== undefined, "GM_addElement应该返回元素");
assert("gm-test-element", element.id, "元素ID应该正确");
assert("DIV", element.tagName, "元素标签应该是DIV");
console.log("返回元素:", element);
// 清理测试元素
element.parentNode.removeChild(element);
});

testSync("GM_addStyle", () => {
const styleElement = GM_addStyle(`
it("GM_addStyle", () => {
const styleElement = GM_addStyle(`
.gm-style-test {
color: #10b981 !important;
}
`);
assertTrue(styleElement !== null && styleElement !== undefined, "GM_addStyle应该返回样式元素");
assertTrue(styleElement.tagName === "STYLE" || styleElement.sheet, "应该返回STYLE元素或样式对象");
console.log("返回样式元素:", styleElement);
// 清理测试样式
styleElement.parentNode.removeChild(styleElement);
expect(styleElement !== null && styleElement !== undefined).toBeTruthy();
expect(styleElement.tagName === "STYLE" || styleElement.sheet).toBeTruthy();
// 清理测试样式
styleElement.parentNode.removeChild(styleElement);
});
});

(async function () {
"use strict";

// ============ 早期脚本环境检查 ============
console.log("\n%c--- 早期脚本环境检查 ---", "color: orange; font-weight: bold;");

await test("检查 document.head 不存在", () => {
console.log("document.head 存在:", !!document.head);
console.log("document.head 值:", document.head);
// 早期脚本运行时 document.head 应该不存在
assertTrue(document.head === null || document.head === undefined, "早期脚本运行时 document.head 应该不存在");
});
describe("早期脚本环境检查", () => {
it("检查 document.head 不存在", () => {
console.log("document.head 存在:", !!document.head);
console.log("document.head 值:", document.head);
// 早期脚本运行时 document.head 应该不存在
expect(document.head === null || document.head === undefined).toBeTruthy();
});

await test("检查 document.body 不存在", () => {
console.log("document.body 存在:", !!document.body);
console.log("document.body 值:", document.body);
// 早期脚本运行时 document.body 应该不存在
assertTrue(document.body === null || document.body === undefined, "早期脚本运行时 document.body 应该不存在");
});
it("检查 document.body 不存在", () => {
console.log("document.body 存在:", !!document.body);
console.log("document.body 值:", document.body);
// 早期脚本运行时 document.body 应该不存在
expect(document.body === null || document.body === undefined).toBeTruthy();
});

await test("检查可用的DOM节点应该是HTML元素", () => {
const firstElement = document.querySelector("*");
console.log("querySelector('*') 找到的第一个元素:", firstElement?.tagName);
assertTrue(firstElement !== null, "应该能找到第一个DOM节点");
assert("HTML", firstElement.tagName, "早期脚本运行时,第一个可用节点应该是HTML元素");
assert("", firstElement.innerHTML, "HTML元素内容应该为空");
console.log("节点详情:", {
tagName: firstElement.tagName,
childNodes: firstElement.childNodes.length,
children: firstElement.children.length,
innerHTML: firstElement.innerHTML,
it("检查可用的DOM节点应该是HTML元素", () => {
const firstElement = document.querySelector("*");
console.log("querySelector('*') 找到的第一个元素:", firstElement?.tagName);
expect(firstElement !== null).toBeTruthy();
expect(firstElement.tagName).toBe("HTML");
expect(firstElement.innerHTML).toBe("");
});
});

// ============ CSP绕过测试 ============
console.log("\n%c--- CSP绕过测试 ---", "color: orange; font-weight: bold;");

await test("CSP绕过 - 内联脚本", () => {
const script = document.createElement("script");
script.textContent = 'console.log("Content环境绕过CSP测试");';
// 早期脚本运行时 document.head 和 document.body 不存在
// 使用 querySelector("*") 查找第一个可用的元素(应该是HTML元素)进行注入
let node = document.querySelector("*");
assertTrue(node !== null, "应该能找到可注入的DOM节点");
node.appendChild(script);
assertTrue(script.parentNode === node, "脚本应该成功插入到找到的节点中");
assert("HTML", node.tagName, "注入节点应该是HTML元素");
console.log("脚本注入到:", node.tagName);
describe("CSP绕过测试", () => {
it("CSP绕过 - 内联脚本", () => {
const script = document.createElement("script");
script.textContent = 'console.log("Content环境绕过CSP测试");';
// 早期脚本运行时 document.head 和 document.body 不存在
// 使用 querySelector("*") 查找第一个可用的元素(应该是HTML元素)进行注入
let node = document.querySelector("*");
expect(node !== null).toBeTruthy();
node.appendChild(script);
expect(script.parentNode === node).toBeTruthy();
expect(node.tagName).toBe("HTML");
});
});

// ============ GM_log 测试 ============
console.log("\n%c--- GM_log 测试 ---", "color: orange; font-weight: bold;");

await test("GM_log", () => {
GM_log("测试日志输出", "info", { type: "test", value: 123 });
// GM_log本身不返回值,只要不抛出异常就算成功
assertTrue(true, "GM_log应该能正常输出");
describe("GM_log 测试", () => {
it("GM_log", () => {
GM_log("测试日志输出", "info", { type: "test", value: 123 });
// GM_log本身不返回值,只要不抛出异常就算成功
expect(true).toBeTruthy();
});
});

// ============ GM_info 测试 ============
console.log("\n%c--- GM_info 测试 ---", "color: orange; font-weight: bold;");

await test("GM_info", () => {
assertTrue(typeof GM_info === "object", "GM_info应该是对象");
assertTrue(!!GM_info.script, "GM_info.script应该存在");
assertTrue(!!GM_info.script.name, "GM_info.script.name应该存在");
console.log("脚本信息:", GM_info.script.name);
describe("GM_info 测试", () => {
it("GM_info", () => {
expect(typeof GM_info === "object").toBeTruthy();
expect(!!GM_info.script).toBeTruthy();
expect(!!GM_info.script.name).toBeTruthy();
});
});

// ============ GM 存储 API 测试 ============
console.log("\n%c--- GM 存储 API 测试 ---", "color: orange; font-weight: bold;");

await test("GM_setValue - 字符串", async () => {
await GM.setValue("test_key", "content环境测试值");
const value = GM_getValue("test_key");
assert("content环境测试值", value, "应该正确保存和读取字符串");
});
describe("GM 存储 API 测试", () => {
it("GM_setValue - 字符串", async () => {
await GM.setValue("test_key", "content环境测试值");
const value = GM_getValue("test_key");
expect(value).toBe("content环境测试值");
});

await test("GM_setValue - 数字", () => {
GM_setValue("test_number", 12345);
const value = GM_getValue("test_number");
assert(12345, value, "应该正确保存和读取数字");
});
it("GM_setValue - 数字", () => {
GM_setValue("test_number", 12345);
const value = GM_getValue("test_number");
expect(value).toBe(12345);
});

await test("GM_setValue - 对象", () => {
const obj = { name: "ScriptCat", type: "content" };
GM_setValue("test_object", obj);
const value = GM_getValue("test_object", {});
assert("ScriptCat", value.name, "对象的name属性应该正确");
assert("content", value.type, "对象的type属性应该正确");
});
it("GM_setValue - 对象", () => {
const obj = { name: "ScriptCat", type: "content" };
GM_setValue("test_object", obj);
const value = GM_getValue("test_object", {});
expect(value.name).toBe("ScriptCat");
expect(value.type).toBe("content");
});

await test("GM_getValue - 默认值", () => {
const value = GM_getValue("non_existent_key", "默认值");
assert("默认值", value, "不存在的键应该返回默认值");
});
it("GM_getValue - 默认值", () => {
const value = GM_getValue("non_existent_key", "默认值");
expect(value).toBe("默认值");
});

await test("GM_listValues", () => {
const keys = GM_listValues();
assertTrue(Array.isArray(keys), "GM_listValues应该返回数组");
assertTrue(keys.length >= 3, "应该至少有3个存储键");
console.log("存储的键:", keys);
});
it("GM_listValues", () => {
const keys = GM_listValues();
expect(Array.isArray(keys)).toBeTruthy();
expect(keys.length >= 3).toBeTruthy();
});

await test("GM_deleteValue", () => {
GM_setValue("test_delete", "to_be_deleted");
assert("to_be_deleted", GM_getValue("test_delete"), "值应该存在");
GM_deleteValue("test_delete");
assert(null, GM_getValue("test_delete", null), "值应该被删除");
it("GM_deleteValue", () => {
GM_setValue("test_delete", "to_be_deleted");
expect(GM_getValue("test_delete")).toBe("to_be_deleted");
GM_deleteValue("test_delete");
expect(GM_getValue("test_delete", null)).toBe(null);
});
});

// ============ 输出测试结果 ============
console.log("\n%c=== 测试完成 ===", "color: blue; font-size: 16px; font-weight: bold;");
console.log(
`%c总计: ${testResults.total} | 通过: ${testResults.passed} | 失败: ${testResults.failed}`,
testResults.failed === 0 ? "color: green; font-weight: bold;" : "color: red; font-weight: bold;"
);

if (testResults.failed === 0) {
console.log("%c🎉 所有测试通过!", "color: green; font-size: 14px; font-weight: bold;");
} else {
console.log("%c⚠️ 部分测试失败,请检查上面的错误信息", "color: red; font-size: 14px; font-weight: bold;");
}
await run();
})();
Loading
Loading