diff --git a/example/tests/early_inject_content_test.js b/example/tests/early_inject_content_test.js
index ad28dcfed..f2e39d851 100644
--- a/example/tests/early_inject_content_test.js
+++ b/example/tests/early_inject_content_test.js
@@ -18,93 +18,40 @@
// @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 || "断言失败: 期望条件为真");
- }
-}
+(async ({ test, testSync, assert, assertTrue, printSummary }) => {
+ console.log("%c=== Content环境 GM API 测试开始 ===", "color: blue; font-size: 16px; font-weight: bold;");
-console.log("%c=== Content环境 GM API 测试开始 ===", "color: blue; font-size: 16px; font-weight: bold;");
+ // 同步测试
-let testResults = {
- passed: 0,
- failed: 0,
- total: 0,
-};
+ // ============ GM_addElement/GM_addStyle 测试 ============
+ console.log("\n%c--- DOM操作 API 测试 ---", "color: orange; font-weight: bold;");
-// 同步测试
-
-// ============ 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",
+ testSync("GM_addElement", () => {
+ const element = GM_addElement("div", {
+ textContent: "GM_addElement测试元素",
+ style: "display:none;",
+ id: "gm-test-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);
});
- 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(`
+
+ testSync("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);
-});
-
-(async function () {
- "use strict";
+ assertTrue(styleElement !== null && styleElement !== undefined, "GM_addStyle应该返回样式元素");
+ assertTrue(styleElement.tagName === "STYLE" || styleElement.sheet, "应该返回STYLE元素或样式对象");
+ console.log("返回样式元素:", styleElement);
+ // 清理测试样式
+ styleElement.parentNode.removeChild(styleElement);
+ });
// ============ 早期脚本环境检查 ============
console.log("\n%c--- 早期脚本环境检查 ---", "color: orange; font-weight: bold;");
@@ -215,15 +162,73 @@ testSync("GM_addStyle", () => {
});
// ============ 输出测试结果 ============
- 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;");
+ printSummary();
+})((() => {
+ let testResults = {
+ passed: 0,
+ failed: 0,
+ total: 0,
+ };
+
+ // 测试辅助函数(支持同步和异步)
+ 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 || "断言失败: 期望条件为真");
+ }
+ }
+
+ // printSummary() - 输出测试结果汇总
+ function printSummary() {
+ 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;");
+ }
+ }
+
+ return { test, testSync, assert, assertTrue, printSummary };
+})());
diff --git a/example/tests/early_inject_page_test.js b/example/tests/early_inject_page_test.js
index 9d6e9d0bb..d58c02157 100644
--- a/example/tests/early_inject_page_test.js
+++ b/example/tests/early_inject_page_test.js
@@ -17,91 +17,38 @@
// @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=== 早期脚本 GM API 测试开始 ===", "color: blue; font-size: 16px; font-weight: bold;");
-
-let testResults = {
- passed: 0,
- failed: 0,
- total: 0,
-};
+(async ({ test, testSync, assert, assertTrue, printSummary }) => {
+ console.log("%c=== 早期脚本 GM API 测试开始 ===", "color: blue; font-size: 16px; font-weight: bold;");
-// ============ GM_addElement/GM_addStyle 测试 ============
-console.log("\n%c--- DOM操作 API 测试 ---", "color: orange; font-weight: bold;");
+ // ============ 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",
+ testSync("GM_addElement", () => {
+ const element = GM_addElement("div", {
+ textContent: "GM_addElement测试元素",
+ style: "display:none;",
+ id: "gm-test-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);
});
- 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(`
+
+ testSync("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);
-});
-
-(async function () {
- "use strict";
+ assertTrue(styleElement !== null && styleElement !== undefined, "GM_addStyle应该返回样式元素");
+ assertTrue(styleElement.tagName === "STYLE" || styleElement.sheet, "应该返回STYLE元素或样式对象");
+ console.log("返回样式元素:", styleElement);
+ // 清理测试样式
+ styleElement.parentNode.removeChild(styleElement);
+ });
// ============ 早期脚本环境检查 ============
console.log("\n%c--- 早期脚本环境检查 ---", "color: orange; font-weight: bold;");
@@ -241,15 +188,73 @@ testSync("GM_addStyle", () => {
});
// ============ 输出测试结果 ============
- 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;");
+ printSummary();
+})((() => {
+ let testResults = {
+ passed: 0,
+ failed: 0,
+ total: 0,
+ };
+
+ // 测试辅助函数(支持同步和异步)
+ 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 || "断言失败: 期望条件为真");
+ }
+ }
+
+ // printSummary() - 输出测试结果汇总
+ function printSummary() {
+ 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;");
+ }
+ }
+
+ return { test, testSync, assert, assertTrue, printSummary };
+})());
diff --git a/example/tests/gm_api_async_test.js b/example/tests/gm_api_async_test.js
index c8535935c..a6f022b9e 100644
--- a/example/tests/gm_api_async_test.js
+++ b/example/tests/gm_api_async_test.js
@@ -29,44 +29,14 @@
// @run-at document-start
// ==/UserScript==
-(async function () {
- "use strict";
+"use strict";
+(async ({ test, assert, printSummary, showResultPanel }) => {
console.log("%c=== ScriptCat GM.* API 测试开始 ===", "color: blue; font-size: 16px; font-weight: bold;");
- let testResults = {
- passed: 0,
- failed: 0,
- total: 0,
- };
-
- // 测试辅助函数
- async function testAsync(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;
- }
- }
-
- // assert 函数
- 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);
- }
- }
-
// ============ GM.info 测试 ============
console.log("\n%c--- GM.info 测试 ---", "color: orange; font-weight: bold;");
- await testAsync("GM.info 存在", async () => {
+ await test("GM.info 存在", async () => {
assert("object", typeof GM.info, "GM.info 应该是一个对象");
assert(true, !!GM.info.script, "GM.info.script 应该存在");
assert(true, !!GM.info.scriptMetaStr, "GM.info.scriptMetaStr 应该存在");
@@ -76,25 +46,25 @@
// ============ GM 存储 API 测试 ============
console.log("\n%c--- GM 存储 API 测试 ---", "color: orange; font-weight: bold;");
- await testAsync("GM.setValue - 字符串", async () => {
+ await test("GM.setValue - 字符串", async () => {
await GM.setValue("test_string", "Hello ScriptCat Async");
const value = await GM.getValue("test_string");
assert("Hello ScriptCat Async", value, "GM.getValue 应该返回正确的字符串值");
});
- await testAsync("GM.setValue - 数字", async () => {
+ await test("GM.setValue - 数字", async () => {
await GM.setValue("test_number", 42);
const value = await GM.getValue("test_number");
assert(42, value, "GM.getValue 应该返回正确的数字值");
});
- await testAsync("GM.setValue - 布尔值", async () => {
+ await test("GM.setValue - 布尔值", async () => {
await GM.setValue("test_boolean", true);
const value = await GM.getValue("test_boolean");
assert(true, value, "GM.getValue 应该返回正确的布尔值");
});
- await testAsync("GM.setValue - 对象", async () => {
+ await test("GM.setValue - 对象", async () => {
const obj = { name: "ScriptCat", version: "1.3.0", features: ["GM API", "Async"] };
await GM.setValue("test_object", obj);
const value = await GM.getValue("test_object");
@@ -104,7 +74,7 @@
assert(JSON.stringify(obj.features), JSON.stringify(value.features), "features 数组应该相等");
});
- await testAsync("GM.setValue - 数组", async () => {
+ await test("GM.setValue - 数组", async () => {
const arr = [1, 2, 3, "test", { key: "value" }];
await GM.setValue("test_array", arr);
const value = await GM.getValue("test_array");
@@ -115,19 +85,19 @@
assert(arr[4].key, value[4].key, "对象元素的属性应该相等");
});
- await testAsync("GM.getValue - 默认值", async () => {
+ await test("GM.getValue - 默认值", async () => {
const value = await GM.getValue("non_existent_key", "default_value");
assert("default_value", value, "不存在的键应该返回默认值");
});
- await testAsync("GM.listValues", async () => {
+ await test("GM.listValues", async () => {
const values = await GM.listValues();
assert(true, Array.isArray(values), "GM.listValues 应该返回数组");
assert(true, values.includes("test_string"), "应该包含已存储的键");
console.log("存储的键:", values);
});
- await testAsync("GM.deleteValue", async () => {
+ await test("GM.deleteValue", async () => {
await GM.setValue("test_delete", "to be deleted");
assert("to be deleted", await GM.getValue("test_delete"), "值应该存在");
await GM.deleteValue("test_delete");
@@ -137,7 +107,7 @@
// ============ GM.addStyle 测试 ============
console.log("\n%c--- GM 样式 API 测试 ---", "color: orange; font-weight: bold;");
- await testAsync("GM.addStyle - CSS字符串", async () => {
+ await test("GM.addStyle - CSS字符串", async () => {
const css = `
.scriptcat-test-async {
color: blue;
@@ -150,7 +120,7 @@
});
// ============ GM.addElement 测试 ============
- await testAsync("GM.addElement - 创建元素", async () => {
+ await test("GM.addElement - 创建元素", async () => {
assert("function", typeof GM.addElement, "GM.addElement 应该是函数");
const div = await GM.addElement("div", {
@@ -167,7 +137,7 @@
// ============ GM.getResourceText/Url 测试 ============
console.log("\n%c--- GM 资源 API 测试 ---", "color: orange; font-weight: bold;");
- await testAsync("GM.getResourceText", async () => {
+ await test("GM.getResourceText", async () => {
assert("function", typeof GM.getResourceText, "GM.getResourceText 应该是函数");
const css = await GM.getResourceText("testCSS");
@@ -176,7 +146,7 @@
console.log("资源文本长度:", css.length);
});
- await testAsync("GM.getResourceUrl", async () => {
+ await test("GM.getResourceUrl", async () => {
assert("function", typeof GM.getResourceUrl, "GM.getResourceUrl 应该是函数");
const url = await GM.getResourceUrl("testCSS");
@@ -188,7 +158,7 @@
// ============ GM.xmlHttpRequest 测试 ============
console.log("\n%c--- GM 网络请求 API 测试 ---", "color: orange; font-weight: bold;");
- await testAsync("GM.xmlHttpRequest - GET 请求", async () => {
+ await test("GM.xmlHttpRequest - GET 请求", async () => {
return new Promise((resolve, reject) => {
GM.xmlHttpRequest({
method: "GET",
@@ -217,7 +187,7 @@
});
});
- await testAsync("GM.xmlHttpRequest - 返回控制对象", async () => {
+ await test("GM.xmlHttpRequest - 返回控制对象", async () => {
const controller = GM.xmlHttpRequest({
method: "GET",
url: "https://httpbun.com/get",
@@ -234,7 +204,7 @@
// ============ GM.notification 测试 ============
console.log("\n%c--- GM 通知 API 测试 ---", "color: orange; font-weight: bold;");
- await testAsync("GM.notification - Promise 版本", async () => {
+ await test("GM.notification - Promise 版本", async () => {
assert("function", typeof GM.notification, "GM.notification 应该是函数");
const notificationPromise = GM.notification({
@@ -258,7 +228,7 @@
// ============ GM.setClipboard 测试 ============
console.log("\n%c--- GM 剪贴板 API 测试 ---", "color: orange; font-weight: bold;");
- await testAsync("GM.setClipboard", async () => {
+ await test("GM.setClipboard", async () => {
assert("function", typeof GM.setClipboard, "GM.setClipboard 应该是函数");
await GM.setClipboard("ScriptCat GM.* API 测试文本 - " + new Date().toLocaleString());
@@ -268,7 +238,7 @@
// ============ GM.openInTab 测试 ============
console.log("\n%c--- GM 标签页 API 测试 ---", "color: orange; font-weight: bold;");
- await testAsync("GM.openInTab (不执行)", async () => {
+ await test("GM.openInTab (不执行)", async () => {
// 不实际打开标签页,只测试函数是否存在
assert("function", typeof GM.openInTab, "GM.openInTab 应该是函数");
console.log("GM.openInTab 可用 (未实际打开标签页)");
@@ -277,7 +247,7 @@
// ============ GM.registerMenuCommand 测试 ============
console.log("\n%c--- GM 菜单 API 测试 ---", "color: orange; font-weight: bold;");
- await testAsync("GM.registerMenuCommand", async () => {
+ await test("GM.registerMenuCommand", async () => {
const menuId = await GM.registerMenuCommand("ScriptCat 异步测试菜单", () => {
alert("异步测试菜单被点击!");
});
@@ -288,12 +258,12 @@
// ============ GM.cookie 测试 ============
console.log("\n%c--- GM.cookie API 测试 ---", "color: orange; font-weight: bold;");
- await testAsync("GM.cookie 函数存在", async () => {
+ await test("GM.cookie 函数存在", async () => {
assert("function", typeof GM.cookie, "GM.cookie 应该是一个函数");
console.log("GM.cookie API 可用");
});
- await testAsync("GM.cookie.set", async () => {
+ await test("GM.cookie.set", async () => {
await GM.cookie.set({
url: "http://example.com/cookie",
name: "scriptcat_async_test1",
@@ -302,7 +272,7 @@
console.log("Cookie 已设置: scriptcat_async_test1 @ example.com");
});
- await testAsync("GM.cookie.set (带 domain 和 path)", async () => {
+ await test("GM.cookie.set (带 domain 和 path)", async () => {
await GM.cookie.set({
url: "http://www.example.com/",
domain: ".example.com",
@@ -313,7 +283,7 @@
console.log("Cookie 已设置: scriptcat_async_test2 @ .example.com/path");
});
- await testAsync("GM.cookie.list (by domain)", async () => {
+ await test("GM.cookie.list (by domain)", async () => {
const cookies = await GM.cookie.list({
domain: "example.com",
});
@@ -323,7 +293,7 @@
console.log("示例 Cookie:", cookies[0]);
});
- await testAsync("GM.cookie.list (by url)", async () => {
+ await test("GM.cookie.list (by url)", async () => {
const cookies = await GM.cookie.list({
url: "http://example.com/cookie",
});
@@ -331,7 +301,7 @@
console.log("通过 URL 列出的 cookies:", cookies.length, "个");
});
- await testAsync("GM.cookie.delete", async () => {
+ await test("GM.cookie.delete", async () => {
await GM.cookie.delete({
url: "http://www.example.com/path",
name: "scriptcat_async_test2",
@@ -339,7 +309,7 @@
console.log("Cookie 已删除: scriptcat_async_test2");
});
- await testAsync("GM.cookie - 验证删除后", async () => {
+ await test("GM.cookie - 验证删除后", async () => {
const cookies = await GM.cookie.list({
domain: "example.com",
});
@@ -349,7 +319,7 @@
});
// 清理所有测试 cookies
- await testAsync("清理测试 cookies", async () => {
+ await test("清理测试 cookies", async () => {
const cookies = await GM.cookie.list({ domain: "example.com" });
const testCookies = cookies.filter((c) => c.name.startsWith("scriptcat_async_test"));
@@ -372,7 +342,7 @@
// ============ unsafeWindow 测试 ============
console.log("\n%c--- unsafeWindow 测试 ---", "color: orange; font-weight: bold;");
- await testAsync("unsafeWindow", async () => {
+ await test("unsafeWindow", async () => {
assert("object", typeof unsafeWindow, "unsafeWindow 应该存在");
assert(document, unsafeWindow.document, "unsafeWindow.document 应该等于 document");
console.log("unsafeWindow 可用");
@@ -381,28 +351,67 @@
// ============ @require 测试 ============
console.log("\n%c--- @require 测试 ---", "color: orange; font-weight: bold;");
- await testAsync("jQuery 加载 (@require)", async () => {
+ await test("jQuery 加载 (@require)", async () => {
assert("function", typeof jQuery, "jQuery 应该已加载");
assert("function", typeof $, "$ 应该已加载");
console.log("jQuery 版本:", jQuery.fn.jquery);
});
// ============ 测试总结 ============
- console.log("\n%c=== 测试结果总结 ===", "color: blue; font-size: 16px; font-weight: bold;");
- console.log(`总测试数: ${testResults.total}`);
- console.log(`%c通过: ${testResults.passed}`, "color: green; font-weight: bold;");
- console.log(`%c失败: ${testResults.failed}`, "color: red; font-weight: bold;");
- console.log(`成功率: ${((testResults.passed / testResults.total) * 100).toFixed(2)}%`);
+ printSummary();
// 使用 GM.addElement 在页面上显示结果
- const successRate = ((testResults.passed / testResults.total) * 100).toFixed(2);
- const bgColor =
- testResults.failed === 0 ? "#e8f5e9" : testResults.failed < testResults.total / 2 ? "#fff9c4" : "#ffebee";
- const borderColor =
- testResults.failed === 0 ? "#4caf50" : testResults.failed < testResults.total / 2 ? "#ffc107" : "#f44336";
-
- const resultContainer = await GM.addElement(document.body, "div", {
- style: `
+ await showResultPanel();
+
+ console.log("%c=== ScriptCat GM.* API 测试完成 ===", "color: blue; font-size: 16px; font-weight: bold;");
+})((() => {
+ const testResults = {
+ passed: 0,
+ failed: 0,
+ total: 0,
+ };
+
+ // 测试辅助函数
+ 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;
+ }
+ }
+
+ // assert 函数
+ 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);
+ }
+ }
+
+ function printSummary() {
+ console.log("\n%c=== 测试结果总结 ===", "color: blue; font-size: 16px; font-weight: bold;");
+ console.log(`总测试数: ${testResults.total}`);
+ console.log(`%c通过: ${testResults.passed}`, "color: green; font-weight: bold;");
+ console.log(`%c失败: ${testResults.failed}`, "color: red; font-weight: bold;");
+ console.log(`成功率: ${((testResults.passed / testResults.total) * 100).toFixed(2)}%`);
+ }
+
+ async function showResultPanel() {
+ const successRate = ((testResults.passed / testResults.total) * 100).toFixed(2);
+ const bgColor =
+ testResults.failed === 0 ? "#e8f5e9" : testResults.failed < testResults.total / 2 ? "#fff9c4" : "#ffebee";
+ const borderColor =
+ testResults.failed === 0 ? "#4caf50" : testResults.failed < testResults.total / 2 ? "#ffc107" : "#f44336";
+
+ const resultContainer = await GM.addElement(document.body, "div", {
+ style: `
position: fixed;
bottom: 20px;
right: 20px;
@@ -416,10 +425,10 @@
min-width: 350px;
animation: slideIn 0.5s ease-out;
`,
- });
+ });
- // 添加动画样式
- await GM.addStyle(`
+ // 添加动画样式
+ await GM.addStyle(`
@keyframes slideIn {
from {
transform: translateX(400px);
@@ -432,67 +441,67 @@
}
`);
- // 标题
- await GM.addElement(resultContainer, "h3", {
- textContent: "🐱 ScriptCat GM.* API 测试结果 (异步版本)",
- style:
- "margin: 0 0 15px 0; color: #333; font-size: 18px; font-weight: bold; border-bottom: 2px solid " +
- borderColor +
- "; padding-bottom: 10px;",
- });
+ // 标题
+ await GM.addElement(resultContainer, "h3", {
+ textContent: "🐱 ScriptCat GM.* API 测试结果 (异步版本)",
+ style:
+ "margin: 0 0 15px 0; color: #333; font-size: 18px; font-weight: bold; border-bottom: 2px solid " +
+ borderColor +
+ "; padding-bottom: 10px;",
+ });
- // 测试统计容器
- const statsContainer = await GM.addElement(resultContainer, "div", {
- style: "margin-bottom: 15px;",
- });
+ // 测试统计容器
+ const statsContainer = await GM.addElement(resultContainer, "div", {
+ style: "margin-bottom: 15px;",
+ });
- // 总测试数
- const totalLine = await GM.addElement(statsContainer, "div", {
- style: "margin: 8px 0; font-size: 14px; display: flex; justify-content: space-between;",
- });
- await GM.addElement(totalLine, "span", { textContent: "📊 总测试数:" });
- await GM.addElement(totalLine, "strong", {
- textContent: testResults.total,
- style: "font-size: 16px;",
- });
+ // 总测试数
+ const totalLine = await GM.addElement(statsContainer, "div", {
+ style: "margin: 8px 0; font-size: 14px; display: flex; justify-content: space-between;",
+ });
+ await GM.addElement(totalLine, "span", { textContent: "📊 总测试数:" });
+ await GM.addElement(totalLine, "strong", {
+ textContent: testResults.total,
+ style: "font-size: 16px;",
+ });
- // 通过数
- const passedLine = await GM.addElement(statsContainer, "div", {
- style: "margin: 8px 0; font-size: 14px; display: flex; justify-content: space-between;",
- });
- await GM.addElement(passedLine, "span", { textContent: "✅ 通过:" });
- await GM.addElement(passedLine, "strong", {
- textContent: testResults.passed,
- style: "color: #4caf50; font-size: 16px;",
- });
+ // 通过数
+ const passedLine = await GM.addElement(statsContainer, "div", {
+ style: "margin: 8px 0; font-size: 14px; display: flex; justify-content: space-between;",
+ });
+ await GM.addElement(passedLine, "span", { textContent: "✅ 通过:" });
+ await GM.addElement(passedLine, "strong", {
+ textContent: testResults.passed,
+ style: "color: #4caf50; font-size: 16px;",
+ });
- // 失败数
- const failedLine = await GM.addElement(statsContainer, "div", {
- style: "margin: 8px 0; font-size: 14px; display: flex; justify-content: space-between;",
- });
- await GM.addElement(failedLine, "span", { textContent: "❌ 失败:" });
- await GM.addElement(failedLine, "strong", {
- textContent: testResults.failed,
- style: "color: #f44336; font-size: 16px;",
- });
+ // 失败数
+ const failedLine = await GM.addElement(statsContainer, "div", {
+ style: "margin: 8px 0; font-size: 14px; display: flex; justify-content: space-between;",
+ });
+ await GM.addElement(failedLine, "span", { textContent: "❌ 失败:" });
+ await GM.addElement(failedLine, "strong", {
+ textContent: testResults.failed,
+ style: "color: #f44336; font-size: 16px;",
+ });
- // 成功率
- const rateLine = await GM.addElement(statsContainer, "div", {
- style: "margin: 8px 0; font-size: 14px; display: flex; justify-content: space-between;",
- });
- await GM.addElement(rateLine, "span", { textContent: "📈 成功率:" });
- await GM.addElement(rateLine, "strong", {
- textContent: successRate + "%",
- style:
- "color: " + (successRate >= 90 ? "#4caf50" : successRate >= 70 ? "#ffc107" : "#f44336") + "; font-size: 16px;",
- });
+ // 成功率
+ const rateLine = await GM.addElement(statsContainer, "div", {
+ style: "margin: 8px 0; font-size: 14px; display: flex; justify-content: space-between;",
+ });
+ await GM.addElement(rateLine, "span", { textContent: "📈 成功率:" });
+ await GM.addElement(rateLine, "strong", {
+ textContent: successRate + "%",
+ style:
+ "color: " + (successRate >= 90 ? "#4caf50" : successRate >= 70 ? "#ffc107" : "#f44336") + "; font-size: 16px;",
+ });
- // 进度条
- const progressBar = await GM.addElement(resultContainer, "div", {
- style: "background: #e0e0e0; height: 20px; border-radius: 10px; overflow: hidden; margin: 15px 0;",
- });
- await GM.addElement(progressBar, "div", {
- style: `
+ // 进度条
+ const progressBar = await GM.addElement(resultContainer, "div", {
+ style: "background: #e0e0e0; height: 20px; border-radius: 10px; overflow: hidden; margin: 15px 0;",
+ });
+ await GM.addElement(progressBar, "div", {
+ style: `
background: linear-gradient(90deg, #4caf50, #81c784);
height: 100%;
width: ${successRate}%;
@@ -504,18 +513,18 @@
font-size: 12px;
font-weight: bold;
`,
- textContent: successRate + "%",
- });
+ textContent: successRate + "%",
+ });
- // 按钮容器
- const buttonContainer = await GM.addElement(resultContainer, "div", {
- style: "display: flex; gap: 10px; margin-top: 15px;",
- });
+ // 按钮容器
+ const buttonContainer = await GM.addElement(resultContainer, "div", {
+ style: "display: flex; gap: 10px; margin-top: 15px;",
+ });
- // 关闭按钮
- const closeBtn = await GM.addElement(buttonContainer, "button", {
- textContent: "关闭",
- style: `
+ // 关闭按钮
+ const closeBtn = await GM.addElement(buttonContainer, "button", {
+ textContent: "关闭",
+ style: `
flex: 1;
padding: 8px 15px;
cursor: pointer;
@@ -527,15 +536,15 @@
font-weight: bold;
transition: background 0.3s;
`,
- });
- closeBtn.onmouseover = () => (closeBtn.style.background = "#616161");
- closeBtn.onmouseout = () => (closeBtn.style.background = "#757575");
- closeBtn.onclick = () => resultContainer.remove();
-
- // 查看日志按钮
- const logBtn = await GM.addElement(buttonContainer, "button", {
- textContent: "查看详细日志",
- style: `
+ });
+ closeBtn.onmouseover = () => (closeBtn.style.background = "#616161");
+ closeBtn.onmouseout = () => (closeBtn.style.background = "#757575");
+ closeBtn.onclick = () => resultContainer.remove();
+
+ // 查看日志按钮
+ const logBtn = await GM.addElement(buttonContainer, "button", {
+ textContent: "查看详细日志",
+ style: `
flex: 1;
padding: 8px 15px;
cursor: pointer;
@@ -547,13 +556,14 @@
font-weight: bold;
transition: background 0.3s;
`,
- });
- logBtn.onmouseover = () => (logBtn.style.background = "#1976d2");
- logBtn.onmouseout = () => (logBtn.style.background = "#2196f3");
- logBtn.onclick = () => {
- console.log("%c=== 完整测试报告 ===", "color: blue; font-size: 16px; font-weight: bold;");
- alert("请查看控制台中的详细测试日志");
- };
+ });
+ logBtn.onmouseover = () => (logBtn.style.background = "#1976d2");
+ logBtn.onmouseout = () => (logBtn.style.background = "#2196f3");
+ logBtn.onclick = () => {
+ console.log("%c=== 完整测试报告 ===", "color: blue; font-size: 16px; font-weight: bold;");
+ alert("请查看控制台中的详细测试日志");
+ };
+ }
- console.log("%c=== ScriptCat GM.* API 测试完成 ===", "color: blue; font-size: 16px; font-weight: bold;");
-})();
+ return { test, assert, printSummary, showResultPanel };
+})());
diff --git a/example/tests/gm_api_sync_test.js b/example/tests/gm_api_sync_test.js
index 7b6008ce2..4cfeb4ddb 100644
--- a/example/tests/gm_api_sync_test.js
+++ b/example/tests/gm_api_sync_test.js
@@ -33,55 +33,11 @@
// @run-at document-start
// ==/UserScript==
-(async function () {
- "use strict";
+"use strict";
+(async ({ test, testAsync, assert, printSummary, showResultPanel }) => {
console.log("%c=== ScriptCat GM API 测试开始 ===", "color: blue; font-size: 16px; font-weight: bold;");
- let testResults = {
- passed: 0,
- failed: 0,
- total: 0,
- };
-
- // 测试辅助函数
- function test(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;
- }
- }
-
- async function testAsync(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;
- }
- }
-
- // 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);
- }
- }
-
// ============ GM_info 测试 ============
console.log("\n%c--- GM_info 测试 ---", "color: orange; font-weight: bold;");
test("GM_info 存在", () => {
@@ -588,13 +544,67 @@
});
// ============ 测试总结 ============
+ printSummary();
+
+ // 使用 GM_addElement 在页面上显示结果
+ showResultPanel();
+
+ console.log("%c=== ScriptCat GM API 测试完成 ===", "color: blue; font-size: 16px; font-weight: bold;");
+ })();
+})((() => {
+ const testResults = {
+ passed: 0,
+ failed: 0,
+ total: 0,
+ };
+
+ // 测试辅助函数
+ function test(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;
+ }
+ }
+
+ async function testAsync(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;
+ }
+ }
+
+ // 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);
+ }
+ }
+
+ function printSummary() {
console.log("\n%c=== 测试结果总结 ===", "color: blue; font-size: 16px; font-weight: bold;");
console.log(`总测试数: ${testResults.total}`);
console.log(`%c通过: ${testResults.passed}`, "color: green; font-weight: bold;");
console.log(`%c失败: ${testResults.failed}`, "color: red; font-weight: bold;");
console.log(`成功率: ${((testResults.passed / testResults.total) * 100).toFixed(2)}%`);
+ }
- // 使用 GM_addElement 在页面上显示结果
+ function showResultPanel() {
const successRate = ((testResults.passed / testResults.total) * 100).toFixed(2);
const bgColor =
testResults.failed === 0 ? "#d4edda" : testResults.failed < testResults.total / 2 ? "#fff3cd" : "#f8d7da";
@@ -754,7 +764,7 @@
console.log("%c=== 完整测试报告 ===", "color: blue; font-size: 16px; font-weight: bold;");
alert("请查看控制台中的详细测试日志");
};
+ }
- console.log("%c=== ScriptCat GM API 测试完成 ===", "color: blue; font-size: 16px; font-weight: bold;");
- })();
-})();
+ return { test, testAsync, assert, printSummary, showResultPanel };
+})());
diff --git a/example/tests/gm_download_test.js b/example/tests/gm_download_test.js
index 8814de5ca..d08d34973 100644
--- a/example/tests/gm_download_test.js
+++ b/example/tests/gm_download_test.js
@@ -69,33 +69,13 @@
Click "Set prefix" to change it. Click "Clear log" to reset counts.
*/
+"use strict";
+
const enableTool = true;
-(function () {
- "use strict";
+(async ({ h, escapeHtml, fmtMs, btnStyle, logLine, pass, fail, skip, setStatus, setQueue, showProgress, updateProgress, hideProgress, assert, assertTrue, withTimeout, awaitVerdict, showAwaitingAction, hideAwaiting, printSummary, showResultPanel }) => {
if (!enableTool) return;
- // ---------- Tiny DOM helper ----------
- function h(tag, props = {}, ...children) {
- const el = document.createElement(tag);
- Object.entries(props).forEach(([k, v]) => {
- if (k === "style" && typeof v === "object") Object.assign(el.style, v);
- else if (k.startsWith("on") && typeof v === "function") el.addEventListener(k.slice(2), v);
- else el[k] = v;
- });
- for (const c of children) el.append(c && c.nodeType ? c : document.createTextNode(String(c)));
- return el;
- }
-
- function escapeHtml(s) {
- return String(s).replace(
- /[&<>"']/g,
- (m) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[m]
- );
- }
-
- function fmtMs(ms) {
- return ms < 1000 ? `${ms | 0}ms` : `${(ms / 1000).toFixed(2)}s`;
- }
+ const { panel, $manualButtons, $prefix, $runTagLabel } = showResultPanel();
// ---------- Settings (persisted) ----------
// Prefix is the sub-folder under the user's Downloads dir. Trailing slash auto-appended.
@@ -139,141 +119,9 @@ const enableTool = true;
// httpbun deterministic endpoint — returns N random bytes.
const HB = "https://httpbun.com";
- // ---------- Panel ----------
- const panel = h(
- "div",
- {
- id: "gmdl-test-panel",
- style: {
- position: "fixed", bottom: "12px", right: "12px",
- width: "520px", maxHeight: "78vh", overflow: "auto",
- zIndex: 2147483647,
- background: "#111", color: "#f5f5f5",
- font: "13px/1.4 system-ui, -apple-system, Segoe UI, Roboto, sans-serif",
- borderRadius: "10px", boxShadow: "0 12px 30px rgba(0,0,0,.4)",
- border: "1px solid #333",
- },
- },
- h(
- "div",
- {
- style: {
- position: "sticky", top: 0, background: "#181818",
- padding: "10px 12px", borderBottom: "1px solid #333",
- display: "flex", alignItems: "center", gap: "8px",
- },
- },
- h("div", { style: { flex: "1 1 auto" } },
- h("div", { style: { fontWeight: "600" } },
- `GM_download Test Harness ${(typeof GM_info === "object" && GM_info.script && GM_info.script.version) || ""}`
- ),
- h("div", { style: { display: "flex", flexDirection: "row", gap: "10px", marginTop: "2px", opacity: .85, flexWrap: "wrap" } },
- h("div", { style: { fontWeight: "400" } },
- `${(typeof GM_info === "object" && GM_info.scriptHandler) || "?"} ${(typeof GM_info === "object" && GM_info.version) || ""}`),
- h("div", { id: "counts", style: { marginLeft: "auto" } }, "…")
- )
- ),
- h("button", { id: "start", style: btnStyle() }, "Run Auto"),
- h("button", { id: "clear", style: btnStyle("#444") }, "Clear log")
- ),
-
- h("div", { id: "status", style: { padding: "6px 12px", borderBottom: "1px solid #222", opacity: .9 } }, "Status: idle"),
-
- // Settings strip.
- h("div", { style: { padding: "6px 12px", borderBottom: "1px solid #222", display: "flex", gap: "8px", alignItems: "center", flexWrap: "wrap" } },
- h("span", { style: { opacity: .8 } }, "Download prefix:"),
- h("code", { id: "prefix", style: { background: "#222", padding: "2px 6px", borderRadius: "4px" } }, getPrefix()),
- h("button", { id: "setPrefix", style: btnStyle("#444") }, "Set prefix"),
- h("span", { style: { opacity: .6, marginLeft: "auto", fontSize: "11.5px" } }, `RunTag: ${RUN_TAG}`)
- ),
-
- // Manual section.
- h("details",
- { id: "manualWrap", open: false, style: { padding: "0 12px 8px", borderBottom: "1px solid #222" } },
- h("summary", { style: { padding: "6px 0", cursor: "pointer", userSelect: "none" } }, "Manual tests (require human)"),
- h("div", { id: "manualHint", style: { fontSize: "12px", opacity: .75, margin: "4px 0 6px" } },
- "Each manual test waits for your verdict. Read the instructions in the log, perform the action, then click Mark Pass or Mark Fail. Skip ends the test without a verdict."
- ),
- h("div", { id: "manualButtons", style: { display: "flex", flexWrap: "wrap", gap: "6px", marginTop: "4px" } })
- ),
-
- // Awaiting bar — shown only while a manual test is in flight.
- h("div", { id: "awaitingWrap", style: { padding: "8px 12px", borderBottom: "1px solid #222", display: "none", background: "#1a1408" } },
- h("div", { style: { display: "flex", alignItems: "center", gap: "8px", flexWrap: "wrap" } },
- h("div", { style: { flex: "1 1 auto" } },
- h("div", { style: { fontWeight: "600", color: "#fbbf24" } }, "⏳ Awaiting your action"),
- h("div", { id: "awaitingLabel", style: { fontSize: "12px", opacity: .85, marginTop: "2px" } }, "")
- ),
- h("div", { id: "awaitingTimer", style: { fontSize: "12px", opacity: .85, fontFamily: "ui-monospace, monospace" } }, ""),
- // Optional in-flight action button (e.g. "🛑 Abort download"); tests register a handler via showAwaitingAction().
- h("button", { id: "awaitingAction", style: { ...btnStyle("#0ea5e9"), display: "none" } }, ""),
- h("button", { id: "awaitingPass", style: btnStyle("#16a34a") }, "✓ Mark Pass"),
- h("button", { id: "awaitingFail", style: btnStyle("#dc2626") }, "✗ Mark Fail"),
- h("button", { id: "awaitingSkip", style: btnStyle("#475569") }, "Skip")
- )
- ),
-
- // Queue.
- h("details", { id: "queueWrap", open: false, style: { padding: "0 12px 6px", borderBottom: "1px solid #222" } },
- h("summary", { style: { padding: "6px 0", cursor: "pointer", userSelect: "none" } }, "Pending auto tests"),
- h("div", {
- id: "queue",
- style: {
- fontFamily: "ui-monospace, SFMono-Regular, Consolas, monospace",
- whiteSpace: "pre-wrap", opacity: .8,
- },
- }, "(none)")
- ),
-
- // Live progress for currently running test.
- h("div", { id: "progressWrap", style: { padding: "6px 12px", borderBottom: "1px solid #222", display: "none" } },
- h("div", { id: "progressLabel", style: { fontSize: "12px", opacity: .8, marginBottom: "4px" } }, ""),
- h("div", { style: { background: "#222", height: "6px", borderRadius: "3px", overflow: "hidden" } },
- h("div", { id: "progressBar", style: { background: "#2a6df1", height: "100%", width: "0%", transition: "width .15s" } })
- )
- ),
-
- h("div", { id: "log", style: { padding: "10px 12px" } })
- );
- document.documentElement.appendChild(panel);
-
- function btnStyle(bg) {
- return {
- background: bg || "#2a6df1",
- color: "white",
- border: "0",
- padding: "6px 10px",
- borderRadius: "6px",
- cursor: "pointer",
- font: "inherit",
- };
- }
-
- const $log = panel.querySelector("#log");
- const $counts = panel.querySelector("#counts");
- const $status = panel.querySelector("#status");
- const $queue = panel.querySelector("#queue");
- const $prefix = panel.querySelector("#prefix");
- const $progressWrap = panel.querySelector("#progressWrap");
- const $progressLabel = panel.querySelector("#progressLabel");
- const $progressBar = panel.querySelector("#progressBar");
- const $manualButtons = panel.querySelector("#manualButtons");
- const $awaitingWrap = panel.querySelector("#awaitingWrap");
- const $awaitingLabel = panel.querySelector("#awaitingLabel");
- const $awaitingTimer = panel.querySelector("#awaitingTimer");
- const $awaitingPass = panel.querySelector("#awaitingPass");
- const $awaitingFail = panel.querySelector("#awaitingFail");
- const $awaitingSkip = panel.querySelector("#awaitingSkip");
- const $awaitingAction = panel.querySelector("#awaitingAction");
-
- panel.querySelector("#clear").addEventListener("click", () => {
- $log.textContent = "";
- state.pass = state.fail = state.skip = 0;
- setCounts();
- setStatus("idle");
- setQueue([]);
- hideProgress();
- });
+ // ---------- Panel wiring that depends on this suite's own settings/runner ----------
+ $prefix.textContent = getPrefix();
+ $runTagLabel.textContent = `RunTag: ${RUN_TAG}`;
panel.querySelector("#start").addEventListener("click", () => runAuto());
panel.querySelector("#setPrefix").addEventListener("click", () => {
const cur = getPrefix();
@@ -284,150 +132,6 @@ const enableTool = true;
logLine(`Prefix set to ${escapeHtml(getPrefix())}`);
});
- function logLine(html, cls = "") {
- const line = h("div", { style: { padding: "6px 0", borderBottom: "1px dashed #2a2a2a" } });
- line.innerHTML = html;
- if (cls) line.className = cls;
- $log.prepend(line);
- }
-
- // ---------- Counters & status ----------
- const state = { pass: 0, fail: 0, skip: 0 };
- function setCounts() {
- $counts.textContent = `✅ ${state.pass} ❌ ${state.fail} ⏭️ ${state.skip}`;
- }
- setCounts();
- function setStatus(text) { $status.textContent = `Status: ${text}`; }
- function setQueue(items) {
- $queue.textContent = items.length ? items.map((t, i) => `${i + 1}. ${t}`).join("\n") : "(none)";
- }
- function pass(msg) { state.pass++; setCounts(); logLine(`✅ ${escapeHtml(msg)}`); }
- function fail(msg, extra) {
- state.fail++; setCounts();
- logLine(
- `❌ ${escapeHtml(msg)}${extra ? `
${escapeHtml(extra)}` : ""}`,
- "fail"
- );
- }
- function skip(msg) { state.skip++; setCounts(); logLine(`⏭️ ${escapeHtml(msg)}`); }
-
- function showProgress(label) {
- $progressWrap.style.display = "";
- $progressLabel.textContent = label;
- $progressBar.style.width = "0%";
- }
- function updateProgress(loaded, total) {
- if (total > 0) {
- $progressBar.style.width = Math.min(100, Math.round((loaded / total) * 100)) + "%";
- } else {
- // Unknown total — fake an indeterminate bar that creeps up.
- const cur = parseFloat($progressBar.style.width) || 0;
- $progressBar.style.width = Math.min(95, cur + 5) + "%";
- }
- }
- function hideProgress() {
- $progressWrap.style.display = "none";
- $progressBar.style.width = "0%";
- }
-
- // ---------- Assertion helpers ----------
- function assertEq(a, b, msg) {
- if (a !== b) throw new Error(msg ? `${msg}: expected ${JSON.stringify(b)}, got ${JSON.stringify(a)}` : `expected ${b}, got ${a}`);
- }
- function assertTrue(cond, msg) { if (!cond) throw new Error(msg || "assertTrue failed"); }
- function withTimeout(p, ms, label) {
- return new Promise((resolve, reject) => {
- let done = false;
- const t = setTimeout(() => {
- if (done) return;
- done = true;
- reject(new Error(`timed out after ${ms}ms: ${label || ""}`));
- }, ms);
- p.then((v) => { if (done) return; done = true; clearTimeout(t); resolve(v); },
- (e) => { if (done) return; done = true; clearTimeout(t); reject(e); });
- });
- }
-
- // ---------- Awaiting bar (manual-test verdict UI) ----------
- // The manual tests can't be "asserted" purely from JS — the contract often is
- // "user sees a dialog, picks Cancel, the script doesn't crash". So we hand the
- // verdict back to the human via Pass/Fail/Skip buttons. To avoid the runner
- // hanging forever if the human disappears, every manual test runs under a
- // countdown that auto-skips when it hits zero.
- let _verdictResolve = null;
- let _verdictTimerId = null;
- let _verdictDeadline = 0;
-
- function showAwaiting(label, deadlineSecs) {
- $awaitingLabel.innerHTML = label; // caller controls HTML, we trust it
- $awaitingWrap.style.display = "";
- _verdictDeadline = performance.now() + deadlineSecs * 1000;
- tickAwaitingTimer();
- if (_verdictTimerId) clearInterval(_verdictTimerId);
- _verdictTimerId = setInterval(tickAwaitingTimer, 250);
- }
- function tickAwaitingTimer() {
- const remaining = Math.max(0, Math.ceil((_verdictDeadline - performance.now()) / 1000));
- $awaitingTimer.textContent = `auto-skip in ${remaining}s`;
- if (remaining === 0) {
- // Time's up — auto-skip so the runner doesn't hang.
- resolveVerdict({ verdict: "skip", reason: "timed out waiting for verdict" });
- }
- }
- function hideAwaiting() {
- $awaitingWrap.style.display = "none";
- $awaitingLabel.innerHTML = "";
- $awaitingTimer.textContent = "";
- if (_verdictTimerId) { clearInterval(_verdictTimerId); _verdictTimerId = null; }
- // Tear down any registered action button so it doesn't leak into the next test.
- $awaitingAction.style.display = "none";
- $awaitingAction.textContent = "";
- $awaitingAction.onclick = null;
- }
- function resolveVerdict(v) {
- if (!_verdictResolve) return;
- const r = _verdictResolve;
- _verdictResolve = null;
- hideAwaiting();
- r(v);
- }
- $awaitingPass.addEventListener("click", () => resolveVerdict({ verdict: "pass" }));
- $awaitingFail.addEventListener("click", () => {
- const reason = prompt("Why did this fail? (optional)", "") || "marked failed by user";
- resolveVerdict({ verdict: "fail", reason });
- });
- $awaitingSkip.addEventListener("click", () => resolveVerdict({ verdict: "skip", reason: "skipped by user" }));
-
- /**
- * Wait for the human to give a verdict via the awaiting bar.
- * @param {string} promptHtml HTML shown in the awaiting bar (be careful — trusted source).
- * @param {number} [deadlineSecs=120] Auto-skip after this many seconds of no input.
- * @returns {Promise<{verdict: "pass"|"fail"|"skip", reason?: string}>}
- */
- function awaitVerdict(promptHtml, deadlineSecs = 120) {
- return new Promise((resolve) => {
- _verdictResolve = resolve;
- showAwaiting(promptHtml, deadlineSecs);
- });
- }
-
- /**
- * Register an in-flight action button on the awaiting bar.
- * Use to expose things like "🛑 Abort download" while we wait for a verdict.
- * The button auto-hides when the verdict resolves (or the next showAwaiting() is called).
- * @param {string} label Button text.
- * @param {() => void} onClick Click handler. Stays attached until the bar hides.
- */
- function showAwaitingAction(label, onClick) {
- $awaitingAction.textContent = label;
- $awaitingAction.style.display = "";
- $awaitingAction.onclick = (ev) => {
- ev.preventDefault();
- try { onClick(); } catch (e) { console.error("awaiting action handler threw:", e); }
- };
- }
-
-
// ---------- GM_download wrappers ----------
/**
@@ -497,7 +201,7 @@ const enableTool = true;
// 1) sanity: APIs exist
autoTest("APIs exist (GM_download / GM.download)", async () => {
- assertEq(typeof GM_download, "function", "GM_download must be a function");
+ assert(typeof GM_download, "function", "GM_download must be a function");
assertTrue(typeof GM !== "undefined" && typeof GM.download === "function", "GM.download must exist");
});
@@ -535,7 +239,7 @@ const enableTool = true;
conflictAction: "uniquify",
});
const r = await withTimeout(promise, 10000, "blob: URL download");
- assertEq(r.kind, "load", "onload should fire");
+ assert(r.kind, "load", "onload should fire");
} finally {
URL.revokeObjectURL(blobUrl);
}
@@ -549,7 +253,7 @@ const enableTool = true;
name,
});
const r = await withTimeout(promise, 10000, "Blob object download");
- assertEq(r.kind, "load", "onload should fire");
+ assert(r.kind, "load", "onload should fire");
});
// 5) File object as url — File extends Blob, should also work
@@ -560,7 +264,7 @@ const enableTool = true;
name,
});
const r = await withTimeout(promise, 10000, "File object download");
- assertEq(r.kind, "load", "onload should fire");
+ assert(r.kind, "load", "onload should fire");
});
// 6) data: URL
@@ -572,7 +276,7 @@ const enableTool = true;
name,
});
const r = await withTimeout(promise, 10000, "data: URL download");
- assertEq(r.kind, "load", "onload should fire");
+ assert(r.kind, "load", "onload should fire");
});
// 7) GM.download promise form
@@ -733,7 +437,7 @@ const enableTool = true;
h.abort();
// Give the system 1.5s to (not) call any callbacks.
await new Promise((r) => setTimeout(r, 1500));
- assertEq(onloadCalled, false, "onload should not fire after immediate abort");
+ assert(onloadCalled, false, "onload should not fire after immediate abort");
// Note: onerror may still fire in some impls — we accept either no-call or a
// generic error. The important contract is: no successful onload.
if (onerrorCalled) {
@@ -776,7 +480,7 @@ const enableTool = true;
// Safety timeout
setTimeout(resolve, 8000);
});
- assertEq(onloadCalled, false, "onload must NOT fire");
+ assert(onloadCalled, false, "onload must NOT fire");
assertTrue(!!errSeen, "onerror should fire");
});
@@ -795,7 +499,7 @@ const enableTool = true;
setTimeout(resolve, 4000);
});
} catch (e) { threw = e; }
- assertEq(onloadCalled, false, "onload must NOT fire on bad URL");
+ assert(onloadCalled, false, "onload must NOT fire on bad URL");
assertTrue(errSeen != null || threw != null, "either onerror fires or it throws");
});
@@ -814,7 +518,7 @@ const enableTool = true;
setTimeout(resolve, 3000);
});
} catch (e) { threw = e; }
- assertEq(onloadCalled, false, "onload must NOT fire on empty URL");
+ assert(onloadCalled, false, "onload must NOT fire on empty URL");
assertTrue(errSeen != null || threw != null, "either onerror fires or it throws");
});
@@ -1090,7 +794,7 @@ const enableTool = true;
setQueue(names.slice(i + 1));
}
setStatus("done");
- logLine(`Done. Summary — ✅ ${state.pass} ❌ ${state.fail} ⏭️ ${state.skip}`);
+ printSummary();
} finally {
running = false;
setAllButtonsDisabled(false);
@@ -1123,4 +827,361 @@ const enableTool = true;
setStatus("idle");
// No auto-start: GM_download writes to disk, so we wait for explicit user action.
-})();
+})((() => {
+ // 跟测试对象无关的基础设施:DOM 构建、日志面板、计数器、断言函数、人工验收(awaiting bar)控制。
+
+ // ---------- Tiny DOM helper ----------
+ function h(tag, props = {}, ...children) {
+ const el = document.createElement(tag);
+ Object.entries(props).forEach(([k, v]) => {
+ if (k === "style" && typeof v === "object") Object.assign(el.style, v);
+ else if (k.startsWith("on") && typeof v === "function") el.addEventListener(k.slice(2), v);
+ else el[k] = v;
+ });
+ for (const c of children) el.append(c && c.nodeType ? c : document.createTextNode(String(c)));
+ return el;
+ }
+
+ function escapeHtml(s) {
+ return String(s).replace(
+ /[&<>"']/g,
+ (m) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[m]
+ );
+ }
+
+ function fmtMs(ms) {
+ return ms < 1000 ? `${ms | 0}ms` : `${(ms / 1000).toFixed(2)}s`;
+ }
+
+ function btnStyle(bg) {
+ return {
+ background: bg || "#2a6df1",
+ color: "white",
+ border: "0",
+ padding: "6px 10px",
+ borderRadius: "6px",
+ cursor: "pointer",
+ font: "inherit",
+ };
+ }
+
+ // Renders the small, fixed vocabulary of inline markup this harness's log
+ // lines use (, , , , entities) into real DOM
+ // nodes — no innerHTML/insertAdjacentHTML/DOMParser, so no HTML-string
+ // parsing ever touches the page (CSP-safe in the injected-script context).
+ const ENTITY_MAP = { amp: "&", lt: "<", gt: ">", quot: '"', "#39": "'", nbsp: " " };
+ function decodeEntities(s) {
+ return s.replace(/&(#39|amp|lt|gt|quot|nbsp);/g, (m, name) => ENTITY_MAP[name]);
+ }
+ function renderMarkup(container, markup) {
+ container.textContent = "";
+ const re = /<(\/?)(b|i|code|pre)(?:\s+style="([^"]*)")?>/g;
+ let last = 0, m;
+ const stack = [container];
+ while ((m = re.exec(markup))) {
+ const text = markup.slice(last, m.index);
+ if (text) stack[stack.length - 1].appendChild(document.createTextNode(decodeEntities(text)));
+ const [, closing, tagName, style] = m;
+ if (closing) {
+ if (stack.length > 1) stack.pop();
+ } else {
+ const el = document.createElement(tagName);
+ if (style) el.style.cssText = style;
+ stack[stack.length - 1].appendChild(el);
+ stack.push(el);
+ }
+ last = re.lastIndex;
+ }
+ const rest = markup.slice(last);
+ if (rest) stack[stack.length - 1].appendChild(document.createTextNode(decodeEntities(rest)));
+ }
+
+ // ---------- Panel DOM refs (populated by showResultPanel()) ----------
+ let panel, $log, $counts, $status, $queue, $prefix, $runTagLabel, $progressWrap, $progressLabel, $progressBar,
+ $manualButtons, $awaitingWrap, $awaitingLabel, $awaitingTimer, $awaitingPass, $awaitingFail,
+ $awaitingSkip, $awaitingAction;
+
+ function logLine(html, cls = "") {
+ const line = document.createElement("div");
+ line.style.cssText = "padding:6px 0;border-bottom:1px dashed #2a2a2a";
+ renderMarkup(line, html);
+ if (cls) line.className = cls;
+ $log.prepend(line);
+ }
+
+ // ---------- Counters & status ----------
+ const state = { pass: 0, fail: 0, skip: 0 };
+ function setCounts() {
+ $counts.textContent = `✅ ${state.pass} ❌ ${state.fail} ⏭️ ${state.skip}`;
+ }
+ function setStatus(text) { $status.textContent = `Status: ${text}`; }
+ function setQueue(items) {
+ $queue.textContent = items.length ? items.map((t, i) => `${i + 1}. ${t}`).join("\n") : "(none)";
+ }
+ function pass(msg) { state.pass++; setCounts(); logLine(`✅ ${escapeHtml(msg)}`); }
+ function fail(msg, extra) {
+ state.fail++; setCounts();
+ logLine(
+ `❌ ${escapeHtml(msg)}${extra ? `${escapeHtml(extra)}` : ""}`,
+ "fail"
+ );
+ }
+ function skip(msg) { state.skip++; setCounts(); logLine(`⏭️ ${escapeHtml(msg)}`); }
+ function printSummary() {
+ logLine(`Done. Summary — ✅ ${state.pass} ❌ ${state.fail} ⏭️ ${state.skip}`);
+ }
+
+ function showProgress(label) {
+ $progressWrap.style.display = "";
+ $progressLabel.textContent = label;
+ $progressBar.style.width = "0%";
+ }
+ function updateProgress(loaded, total) {
+ if (total > 0) {
+ $progressBar.style.width = Math.min(100, Math.round((loaded / total) * 100)) + "%";
+ } else {
+ // Unknown total — fake an indeterminate bar that creeps up.
+ const cur = parseFloat($progressBar.style.width) || 0;
+ $progressBar.style.width = Math.min(95, cur + 5) + "%";
+ }
+ }
+ function hideProgress() {
+ $progressWrap.style.display = "none";
+ $progressBar.style.width = "0%";
+ }
+
+ // ---------- Assertion helpers ----------
+ function assert(a, b, msg) {
+ if (a !== b) throw new Error(msg ? `${msg}: expected ${JSON.stringify(b)}, got ${JSON.stringify(a)}` : `expected ${b}, got ${a}`);
+ }
+ function assertTrue(cond, msg) { if (!cond) throw new Error(msg || "assertTrue failed"); }
+ function withTimeout(p, ms, label) {
+ return new Promise((resolve, reject) => {
+ let done = false;
+ const t = setTimeout(() => {
+ if (done) return;
+ done = true;
+ reject(new Error(`timed out after ${ms}ms: ${label || ""}`));
+ }, ms);
+ p.then((v) => { if (done) return; done = true; clearTimeout(t); resolve(v); },
+ (e) => { if (done) return; done = true; clearTimeout(t); reject(e); });
+ });
+ }
+
+ // ---------- Awaiting bar (manual-test verdict UI) ----------
+ // The manual tests can't be "asserted" purely from JS — the contract often is
+ // "user sees a dialog, picks Cancel, the script doesn't crash". So we hand the
+ // verdict back to the human via Pass/Fail/Skip buttons. To avoid the runner
+ // hanging forever if the human disappears, every manual test runs under a
+ // countdown that auto-skips when it hits zero.
+ let _verdictResolve = null;
+ let _verdictTimerId = null;
+ let _verdictDeadline = 0;
+
+ function showAwaiting(label, deadlineSecs) {
+ renderMarkup($awaitingLabel, label);
+ $awaitingWrap.style.display = "";
+ _verdictDeadline = performance.now() + deadlineSecs * 1000;
+ tickAwaitingTimer();
+ if (_verdictTimerId) clearInterval(_verdictTimerId);
+ _verdictTimerId = setInterval(tickAwaitingTimer, 250);
+ }
+ function tickAwaitingTimer() {
+ const remaining = Math.max(0, Math.ceil((_verdictDeadline - performance.now()) / 1000));
+ $awaitingTimer.textContent = `auto-skip in ${remaining}s`;
+ if (remaining === 0) {
+ // Time's up — auto-skip so the runner doesn't hang.
+ resolveVerdict({ verdict: "skip", reason: "timed out waiting for verdict" });
+ }
+ }
+ function hideAwaiting() {
+ $awaitingWrap.style.display = "none";
+ $awaitingLabel.textContent = "";
+ $awaitingTimer.textContent = "";
+ if (_verdictTimerId) { clearInterval(_verdictTimerId); _verdictTimerId = null; }
+ // Tear down any registered action button so it doesn't leak into the next test.
+ $awaitingAction.style.display = "none";
+ $awaitingAction.textContent = "";
+ $awaitingAction.onclick = null;
+ }
+ function resolveVerdict(v) {
+ if (!_verdictResolve) return;
+ const r = _verdictResolve;
+ _verdictResolve = null;
+ hideAwaiting();
+ r(v);
+ }
+
+ /**
+ * Wait for the human to give a verdict via the awaiting bar.
+ * @param {string} promptHtml HTML shown in the awaiting bar (be careful — trusted source).
+ * @param {number} [deadlineSecs=120] Auto-skip after this many seconds of no input.
+ * @returns {Promise<{verdict: "pass"|"fail"|"skip", reason?: string}>}
+ */
+ function awaitVerdict(promptHtml, deadlineSecs = 120) {
+ return new Promise((resolve) => {
+ _verdictResolve = resolve;
+ showAwaiting(promptHtml, deadlineSecs);
+ });
+ }
+
+ /**
+ * Register an in-flight action button on the awaiting bar.
+ * Use to expose things like "🛑 Abort download" while we wait for a verdict.
+ * The button auto-hides when the verdict resolves (or the next showAwaiting() is called).
+ * @param {string} label Button text.
+ * @param {() => void} onClick Click handler. Stays attached until the bar hides.
+ */
+ function showAwaitingAction(label, onClick) {
+ $awaitingAction.textContent = label;
+ $awaitingAction.style.display = "";
+ $awaitingAction.onclick = (ev) => {
+ ev.preventDefault();
+ try { onClick(); } catch (e) { console.error("awaiting action handler threw:", e); }
+ };
+ }
+
+ // ---------- Panel ----------
+ // Builds the whole test-runner panel DOM tree, injects it into the page,
+ // wires up the parts of the UI that don't depend on this file's specific
+ // test suite (clear log, verdict buttons), and returns the handles the
+ // suite-specific code needs (start/setPrefix button wiring lives with the
+ // suite, since it depends on suite-specific settings and the runner).
+ function showResultPanel() {
+ panel = h(
+ "div",
+ {
+ id: "gmdl-test-panel",
+ style: {
+ position: "fixed", bottom: "12px", right: "12px",
+ width: "520px", maxHeight: "78vh", overflow: "auto",
+ zIndex: 2147483647,
+ background: "#111", color: "#f5f5f5",
+ font: "13px/1.4 system-ui, -apple-system, Segoe UI, Roboto, sans-serif",
+ borderRadius: "10px", boxShadow: "0 12px 30px rgba(0,0,0,.4)",
+ border: "1px solid #333",
+ },
+ },
+ h(
+ "div",
+ {
+ style: {
+ position: "sticky", top: 0, background: "#181818",
+ padding: "10px 12px", borderBottom: "1px solid #333",
+ display: "flex", alignItems: "center", gap: "8px",
+ },
+ },
+ h("div", { style: { flex: "1 1 auto" } },
+ h("div", { style: { fontWeight: "600" } },
+ `GM_download Test Harness ${(typeof GM_info === "object" && GM_info.script && GM_info.script.version) || ""}`
+ ),
+ h("div", { style: { display: "flex", flexDirection: "row", gap: "10px", marginTop: "2px", opacity: .85, flexWrap: "wrap" } },
+ h("div", { style: { fontWeight: "400" } },
+ `${(typeof GM_info === "object" && GM_info.scriptHandler) || "?"} ${(typeof GM_info === "object" && GM_info.version) || ""}`),
+ h("div", { id: "counts", style: { marginLeft: "auto" } }, "…")
+ )
+ ),
+ h("button", { id: "start", style: btnStyle() }, "Run Auto"),
+ h("button", { id: "clear", style: btnStyle("#444") }, "Clear log")
+ ),
+
+ h("div", { id: "status", style: { padding: "6px 12px", borderBottom: "1px solid #222", opacity: .9 } }, "Status: idle"),
+
+ // Settings strip.
+ h("div", { style: { padding: "6px 12px", borderBottom: "1px solid #222", display: "flex", gap: "8px", alignItems: "center", flexWrap: "wrap" } },
+ h("span", { style: { opacity: .8 } }, "Download prefix:"),
+ h("code", { id: "prefix", style: { background: "#222", padding: "2px 6px", borderRadius: "4px" } }, ""),
+ h("button", { id: "setPrefix", style: btnStyle("#444") }, "Set prefix"),
+ h("span", { id: "runTagLabel", style: { opacity: .6, marginLeft: "auto", fontSize: "11.5px" } }, "")
+ ),
+
+ // Manual section.
+ h("details",
+ { id: "manualWrap", open: false, style: { padding: "0 12px 8px", borderBottom: "1px solid #222" } },
+ h("summary", { style: { padding: "6px 0", cursor: "pointer", userSelect: "none" } }, "Manual tests (require human)"),
+ h("div", { id: "manualHint", style: { fontSize: "12px", opacity: .75, margin: "4px 0 6px" } },
+ "Each manual test waits for your verdict. Read the instructions in the log, perform the action, then click Mark Pass or Mark Fail. Skip ends the test without a verdict."
+ ),
+ h("div", { id: "manualButtons", style: { display: "flex", flexWrap: "wrap", gap: "6px", marginTop: "4px" } })
+ ),
+
+ // Awaiting bar — shown only while a manual test is in flight.
+ h("div", { id: "awaitingWrap", style: { padding: "8px 12px", borderBottom: "1px solid #222", display: "none", background: "#1a1408" } },
+ h("div", { style: { display: "flex", alignItems: "center", gap: "8px", flexWrap: "wrap" } },
+ h("div", { style: { flex: "1 1 auto" } },
+ h("div", { style: { fontWeight: "600", color: "#fbbf24" } }, "⏳ Awaiting your action"),
+ h("div", { id: "awaitingLabel", style: { fontSize: "12px", opacity: .85, marginTop: "2px" } }, "")
+ ),
+ h("div", { id: "awaitingTimer", style: { fontSize: "12px", opacity: .85, fontFamily: "ui-monospace, monospace" } }, ""),
+ // Optional in-flight action button (e.g. "🛑 Abort download"); tests register a handler via showAwaitingAction().
+ h("button", { id: "awaitingAction", style: { ...btnStyle("#0ea5e9"), display: "none" } }, ""),
+ h("button", { id: "awaitingPass", style: btnStyle("#16a34a") }, "✓ Mark Pass"),
+ h("button", { id: "awaitingFail", style: btnStyle("#dc2626") }, "✗ Mark Fail"),
+ h("button", { id: "awaitingSkip", style: btnStyle("#475569") }, "Skip")
+ )
+ ),
+
+ // Queue.
+ h("details", { id: "queueWrap", open: false, style: { padding: "0 12px 6px", borderBottom: "1px solid #222" } },
+ h("summary", { style: { padding: "6px 0", cursor: "pointer", userSelect: "none" } }, "Pending auto tests"),
+ h("div", {
+ id: "queue",
+ style: {
+ fontFamily: "ui-monospace, SFMono-Regular, Consolas, monospace",
+ whiteSpace: "pre-wrap", opacity: .8,
+ },
+ }, "(none)")
+ ),
+
+ // Live progress for currently running test.
+ h("div", { id: "progressWrap", style: { padding: "6px 12px", borderBottom: "1px solid #222", display: "none" } },
+ h("div", { id: "progressLabel", style: { fontSize: "12px", opacity: .8, marginBottom: "4px" } }, ""),
+ h("div", { style: { background: "#222", height: "6px", borderRadius: "3px", overflow: "hidden" } },
+ h("div", { id: "progressBar", style: { background: "#2a6df1", height: "100%", width: "0%", transition: "width .15s" } })
+ )
+ ),
+
+ h("div", { id: "log", style: { padding: "10px 12px" } })
+ );
+ document.documentElement.appendChild(panel);
+
+ $log = panel.querySelector("#log");
+ $counts = panel.querySelector("#counts");
+ $status = panel.querySelector("#status");
+ $queue = panel.querySelector("#queue");
+ $prefix = panel.querySelector("#prefix");
+ $runTagLabel = panel.querySelector("#runTagLabel");
+ $progressWrap = panel.querySelector("#progressWrap");
+ $progressLabel = panel.querySelector("#progressLabel");
+ $progressBar = panel.querySelector("#progressBar");
+ $manualButtons = panel.querySelector("#manualButtons");
+ $awaitingWrap = panel.querySelector("#awaitingWrap");
+ $awaitingLabel = panel.querySelector("#awaitingLabel");
+ $awaitingTimer = panel.querySelector("#awaitingTimer");
+ $awaitingPass = panel.querySelector("#awaitingPass");
+ $awaitingFail = panel.querySelector("#awaitingFail");
+ $awaitingSkip = panel.querySelector("#awaitingSkip");
+ $awaitingAction = panel.querySelector("#awaitingAction");
+
+ panel.querySelector("#clear").addEventListener("click", () => {
+ $log.textContent = "";
+ state.pass = state.fail = state.skip = 0;
+ setCounts();
+ setStatus("idle");
+ setQueue([]);
+ hideProgress();
+ });
+ $awaitingPass.addEventListener("click", () => resolveVerdict({ verdict: "pass" }));
+ $awaitingFail.addEventListener("click", () => {
+ const reason = prompt("Why did this fail? (optional)", "") || "marked failed by user";
+ resolveVerdict({ verdict: "fail", reason });
+ });
+ $awaitingSkip.addEventListener("click", () => resolveVerdict({ verdict: "skip", reason: "skipped by user" }));
+
+ setCounts();
+
+ return { panel, $manualButtons, $prefix, $runTagLabel };
+ }
+
+ return { h, escapeHtml, fmtMs, btnStyle, logLine, pass, fail, skip, setStatus, setQueue, showProgress, updateProgress, hideProgress, assert, assertTrue, withTimeout, awaitVerdict, showAwaitingAction, hideAwaiting, printSummary, showResultPanel };
+})());
diff --git a/example/tests/gm_menu_test.js b/example/tests/gm_menu_test.js
index 216015dfb..2a1b08e31 100644
--- a/example/tests/gm_menu_test.js
+++ b/example/tests/gm_menu_test.js
@@ -8,40 +8,11 @@
// @grant GM_unregisterMenuCommand
// ==/UserScript==
-(async function () {
- 'use strict';
-
+(async ({ waitActions, isInSubFrame, resolveNext }) => {
const checkSubFrameIdSequence = false;
const intervalChanging = false;
- const skipClickCheck = false;
-
- let myResolve = () => { };
- const waitNext = async () => {
- await new Promise((resolve) => {
- myResolve = () => { setTimeout(resolve, 50) };
- });
- };
-
- const waitActions = async (...messages) => {
- if (skipClickCheck) return;
- messages = messages.flat();
- for (const message of messages) {
- console.log(message);
- await waitNext();
- }
- };
-
- const isInSubFrame = () => {
-
- try {
- return window.top !== window;
- } catch {
- return true;
- }
- }
-
if (intervalChanging) {
// TM: 在打开菜单时,显示会不断改变
let i = 1000;
@@ -78,13 +49,13 @@
const r01 = GM_registerMenuCommand("MenuReg abc-1", () => {
console.log("abc-1");
- myResolve();
+ resolveNext();
}, obj1);
const r02 = GM_registerMenuCommand("MenuReg abc-2", () => {
console.log("abc-2");
- myResolve();
+ resolveNext();
}, obj1);
console.log("abc-1 id === abc", r01 === "abc");
@@ -96,13 +67,13 @@
GM_registerMenuCommand("MenuReg abc-1", () => {
console.log("abc-1.abd");
- myResolve();
+ resolveNext();
}, { id: "abd" });
GM_registerMenuCommand("MenuReg abc-2", () => {
console.log("abc-2.abe");
- myResolve();
+ resolveNext();
}, { id: "abe" });
@@ -113,7 +84,7 @@
GM_registerMenuCommand("MenuReg abc-2", () => {
console.log("abc-2.abf");
- myResolve();
+ resolveNext();
}, { id: "abf", accessKey: "h" });
// there shall be only "MenuReg abc-1" and "MenuReg abc-2" in the menu.
@@ -129,14 +100,14 @@
const p10 = GM_registerMenuCommand("MenuReg D-23", () => {
console.log(110);
- myResolve();
+ resolveNext();
}, "b");
const p20 = GM_registerMenuCommand("MenuReg D-23", () => {
console.log(120);
- myResolve();
+ resolveNext();
}, "b");
console.log("p10 === 1", p10 === 1);
@@ -149,7 +120,7 @@
const p30 = GM_registerMenuCommand("MenuReg D-26", () => {
console.log(130);
- myResolve();
+ resolveNext();
}, { id: "2" });
console.log("p30 === '2'", p30 === "2");
@@ -162,7 +133,7 @@
const p32 = GM_registerMenuCommand("MenuReg D-26", () => {
console.log(210);
- myResolve();
+ resolveNext();
}, { id: 2 });
console.log("p32 === 2", p32 === 2);
@@ -175,7 +146,7 @@
const p33 = GM_registerMenuCommand("MenuReg D-26", () => {
console.log(220);
- myResolve();
+ resolveNext();
}, { id: 3 });
console.log("p33 === 3", p33 === 3);
@@ -189,7 +160,7 @@
const p34 = GM_registerMenuCommand("MenuReg D-26", () => {
console.log(230);
- myResolve();
+ resolveNext();
}, { id: "4" });
console.log("p34 === '4'", p34 === "4");
@@ -219,7 +190,40 @@
-})().finally(() => {
+})((() => {
+ // 跟测试对象无关的基础设施:交互式等待(点击菜单以继续)相关的辅助函数。
+ 'use strict';
+
+ const skipClickCheck = false;
+
+ let myResolve = () => { };
+ const waitNext = async () => {
+ await new Promise((resolve) => {
+ myResolve = () => { setTimeout(resolve, 50) };
+ });
+ };
+
+ const waitActions = async (...messages) => {
+ if (skipClickCheck) return;
+ messages = messages.flat();
+ for (const message of messages) {
+ console.log(message);
+ await waitNext();
+ }
+ };
+
+ const isInSubFrame = () => {
+
+ try {
+ return window.top !== window;
+ } catch {
+ return true;
+ }
+ }
+
+ const resolveNext = () => { myResolve(); };
+
+ return { waitActions, isInSubFrame, resolveNext };
+})()).finally(() => {
console.log("finish");
});
-
diff --git a/example/tests/gm_value_test.js b/example/tests/gm_value_test.js
index d0fee5b33..3c3571bd6 100644
--- a/example/tests/gm_value_test.js
+++ b/example/tests/gm_value_test.js
@@ -12,9 +12,9 @@
// @run-at document-idle
// ==/UserScript==
-(function () {
- 'use strict';
+'use strict';
+(async ({ escHtml, nowTime, buildLogLine, buildKvCard }) => {
if (!location.search.includes('testGMAddValueChangeListener')) return;
document.documentElement.appendChild(document.createElement("style")).textContent=`@import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;700,800&display=swap');`;
@@ -53,29 +53,6 @@
const frameId = new URLSearchParams(location.search).get('frameId')
|| (isMain ? 'main' : 'unknown');
- /* ══════════════════════════════════════════════════════════
- HELPERS
- ══════════════════════════════════════════════════════════ */
- function escHtml(s) {
- return String(s).replace(/[&<>"']/g, ch => ({
- '&': '&',
- '<': '<',
- '>': '>',
- '"': '"',
- "'": ''',
- }[ch]));
- }
-
- function fmtVal(v) {
- return v === undefined
- ? 'not set'
- : escHtml(JSON.stringify(v));
- }
-
- function nowTime() {
- return new Date().toLocaleTimeString('en-GB', { hour12: false });
- }
-
/* ══════════════════════════════════════════════════════════
MSG BUS
══════════════════════════════════════════════════════════ */
@@ -205,14 +182,7 @@
const logBox = iframeShadow.getElementById('iframe-log');
if (!logBox) return;
- const line = document.createElement('div');
- line.className = 'log-line';
- line.innerHTML = `
- ${escHtml(nowTime())}
- ${msg}
- `;
-
- logBox.appendChild(line);
+ logBox.appendChild(buildLogLine(nowTime(), msg, type));
logBox.scrollTop = logBox.scrollHeight;
}
@@ -364,11 +334,22 @@
const shell = document.createElement('div');
shell.id = 'iframe-shell';
- shell.innerHTML = `
- ${escHtml(LABEL[frameId])}
- Controlled by main frame dashboard ↑
-
- `;
+
+ const titleDiv = document.createElement('div');
+ titleDiv.className = 'iframe-title';
+ titleDiv.textContent = LABEL[frameId];
+
+ const subtitleDiv = document.createElement('div');
+ subtitleDiv.className = 'iframe-subtitle';
+ subtitleDiv.textContent = 'Controlled by main frame dashboard ↑';
+
+ const logDiv = document.createElement('div');
+ logDiv.id = 'iframe-log';
+ logDiv.className = 'log-box';
+
+ shell.appendChild(titleDiv);
+ shell.appendChild(subtitleDiv);
+ shell.appendChild(logDiv);
iframeShadow.appendChild(shell);
}
@@ -720,7 +701,10 @@
const topbar = document.createElement('div');
topbar.id = 'topbar';
- topbar.innerHTML = `⚙ GM_addValueChangeListener Test`;
+ const topbarTitle = document.createElement('span');
+ topbarTitle.id = 'topbar-title';
+ topbarTitle.textContent = '⚙ GM_addValueChangeListener Test';
+ topbar.appendChild(topbarTitle);
const closeBtn = document.createElement('button');
closeBtn.className = 'danger';
@@ -866,7 +850,7 @@
const clrB = makeBtn('✕ clear log', true);
clrB.style.marginTop = '5px';
clrB.onclick = () => {
- logBox.innerHTML = '';
+ logBox.textContent = '';
state.logs[id] = [];
};
card.appendChild(clrB);
@@ -917,39 +901,20 @@
function mLog(msg, type = '') {
const { logBox } = refs();
- const line = document.createElement('div');
- line.className = 'log-line';
- line.innerHTML = `
- ${nowTime()}
- ${msg}
- `;
-
- logBox.appendChild(line);
+ logBox.appendChild(buildLogLine(nowTime(), msg, type));
logBox.scrollTop = logBox.scrollHeight;
}
async function mRefreshKV() {
const { kvTable, myKey: mk, accent } = refs();
- kvTable.innerHTML = '';
+ kvTable.textContent = '';
for (const k of ALL_KEYS) {
const v = await GM_getValue(k, undefined); // main frame
const own = k === mk;
- const card = document.createElement('div');
- card.className = 'kv-card';
-
- if (own) card.style.borderColor = accent + '88';
-
- card.innerHTML = `
- ${escHtml(k)}${own ? ' (mine)' : ''}
-
- ${fmtVal(v)}
-
- `;
-
- kvTable.appendChild(card);
+ kvTable.appendChild(buildKvCard(k, v, own, accent));
}
}
@@ -1096,14 +1061,7 @@
const { logBox } = refs;
- const line = document.createElement('div');
- line.className = 'log-line';
- line.innerHTML = `
- ${escHtml(entry.t || nowTime())}
- ${entry.msg || ''}
- `;
-
- logBox.appendChild(line);
+ logBox.appendChild(buildLogLine(entry.t || nowTime(), entry.msg || '', entry.type || ''));
logBox.scrollTop = logBox.scrollHeight;
}
@@ -1113,25 +1071,13 @@
const { kvTable, myKey, accent } = refs;
- kvTable.innerHTML = '';
+ kvTable.textContent = '';
for (const k of ALL_KEYS) {
const v = kvMap[k];
const own = k === myKey;
- const card = document.createElement('div');
- card.className = 'kv-card';
-
- if (own) card.style.borderColor = accent + '88';
-
- card.innerHTML = `
- ${escHtml(k)}${own ? ' (mine)' : ''}
-
- ${fmtVal(v)}
-
- `;
-
- kvTable.appendChild(card);
+ kvTable.appendChild(buildKvCard(k, v, own, accent));
}
}
@@ -1146,4 +1092,99 @@
dot.className = 'dot' + (activeKeys.has(k) ? ' on' : '');
}
}
-})();
+})((() => {
+ // 跟测试对象无关的基础设施:HTML 转义、值格式化、日志行/KV 卡片的 DOM 构建。
+ // 用手写的极简标签渲染器(只认识 //,可带 style 属性)代替
+ // innerHTML,避免任何 HTML 字符串解析进入页面(注入脚本上下文下的 CSP 要求)。
+
+ function escHtml(s) {
+ return String(s).replace(/[&<>"']/g, ch => ({
+ '&': '&',
+ '<': '<',
+ '>': '>',
+ '"': '"',
+ "'": ''',
+ }[ch]));
+ }
+
+ const ENTITY_MAP = { amp: '&', lt: '<', gt: '>', quot: '"', '#39': "'" };
+ function decodeEntities(s) {
+ return s.replace(/&(#39|amp|lt|gt|quot);/g, (m, name) => ENTITY_MAP[name]);
+ }
+
+ function appendInline(container, markup) {
+ const re = /<(\/?)(b|i|small)(?:\s+style="([^"]*)")?>/g;
+ let last = 0, m;
+ const stack = [container];
+ while ((m = re.exec(markup))) {
+ const text = markup.slice(last, m.index);
+ if (text) stack[stack.length - 1].appendChild(document.createTextNode(decodeEntities(text)));
+ const [, closing, tagName, style] = m;
+ if (closing) {
+ if (stack.length > 1) stack.pop();
+ } else {
+ const el = document.createElement(tagName);
+ if (style) el.style.cssText = style;
+ stack[stack.length - 1].appendChild(el);
+ stack.push(el);
+ }
+ last = re.lastIndex;
+ }
+ const rest = markup.slice(last);
+ if (rest) stack[stack.length - 1].appendChild(document.createTextNode(decodeEntities(rest)));
+ }
+
+ function fmtVal(v) {
+ return v === undefined
+ ? 'not set'
+ : escHtml(JSON.stringify(v));
+ }
+
+ function nowTime() {
+ return new Date().toLocaleTimeString('en-GB', { hour12: false });
+ }
+
+ function buildLogLine(time, msg, type) {
+ const line = document.createElement('div');
+ line.className = 'log-line';
+
+ const timeSpan = document.createElement('span');
+ timeSpan.className = 'log-time';
+ timeSpan.textContent = time;
+
+ const msgSpan = document.createElement('span');
+ msgSpan.className = `log-msg ${type || ''}`.trim();
+ appendInline(msgSpan, msg);
+
+ line.appendChild(timeSpan);
+ line.appendChild(msgSpan);
+ return line;
+ }
+
+ function buildKvCard(k, v, own, accent) {
+ const card = document.createElement('div');
+ card.className = 'kv-card';
+ if (own) card.style.borderColor = accent + '88';
+
+ const keyDiv = document.createElement('div');
+ keyDiv.className = 'kv-key';
+ keyDiv.appendChild(document.createTextNode(k));
+ if (own) {
+ keyDiv.appendChild(document.createTextNode(' '));
+ const mineTag = document.createElement('i');
+ mineTag.textContent = '(mine)';
+ keyDiv.appendChild(mineTag);
+ }
+
+ const valDiv = document.createElement('div');
+ valDiv.className = 'kv-val';
+ if (own) valDiv.style.cssText = `color:${accent};font-weight:700`;
+ appendInline(valDiv, fmtVal(v));
+
+ card.appendChild(keyDiv);
+ card.appendChild(valDiv);
+ return card;
+ }
+
+ return { escHtml, appendInline, nowTime, fmtVal, buildLogLine, buildKvCard };
+})());
diff --git a/example/tests/gm_xhr_cookie_test.js b/example/tests/gm_xhr_cookie_test.js
index f473c66b0..b42b85369 100644
--- a/example/tests/gm_xhr_cookie_test.js
+++ b/example/tests/gm_xhr_cookie_test.js
@@ -9,9 +9,9 @@
// @noframes
// ==/UserScript==
-(async function () {
- "use strict";
+"use strict";
+(async ({ test, assert, assertTrue, printSummary }) => {
console.log(
"%c=== GM_xmlhttpRequest cookie 覆盖测试开始 ===",
"color: blue; font-size: 16px; font-weight: bold;"
@@ -19,47 +19,6 @@
const MOCKHTTP = "https://mockhttp.org";
- const testResults = {
- passed: 0,
- failed: 0,
- total: 0,
- };
-
- 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 assert(expected, actual, message) {
- if (expected !== actual) {
- const valueInfo =
- `期望 ${JSON.stringify(expected)}, ` +
- `实际 ${JSON.stringify(actual)}`;
-
- throw new Error(
- message
- ? `${message} - ${valueInfo}`
- : `断言失败: ${valueInfo}`
- );
- }
- }
-
- function assertTrue(condition, message) {
- if (!condition) {
- throw new Error(message || "断言失败: 期望条件为真");
- }
- }
-
function gmRequest(details) {
return new Promise((resolve, reject) => {
GM_xmlhttpRequest({
@@ -517,29 +476,76 @@
}
// ============ 输出测试结果 ============
- console.log(
- "\n%c=== 测试完成 ===",
- "color: blue; font-size: 16px; font-weight: bold;"
- );
+ printSummary();
+})((() => {
+ const testResults = {
+ passed: 0,
+ failed: 0,
+ total: 0,
+ };
- console.log(
- `%c总计: ${testResults.total} | ` +
- `通过: ${testResults.passed} | ` +
- `失败: ${testResults.failed}`,
- testResults.failed === 0
- ? "color: green; font-weight: bold;"
- : "color: red; font-weight: bold;"
- );
+ 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 assert(expected, actual, message) {
+ if (expected !== actual) {
+ const valueInfo =
+ `期望 ${JSON.stringify(expected)}, ` +
+ `实际 ${JSON.stringify(actual)}`;
+
+ throw new Error(
+ message
+ ? `${message} - ${valueInfo}`
+ : `断言失败: ${valueInfo}`
+ );
+ }
+ }
- if (testResults.failed === 0) {
+ function assertTrue(condition, message) {
+ if (!condition) {
+ throw new Error(message || "断言失败: 期望条件为真");
+ }
+ }
+
+ function printSummary() {
console.log(
- "%c🎉 所有测试通过!",
- "color: green; font-size: 14px; font-weight: bold;"
+ "\n%c=== 测试完成 ===",
+ "color: blue; font-size: 16px; font-weight: bold;"
);
- } else {
+
console.log(
- "%c⚠️ 部分测试失败,请检查上面的错误信息",
- "color: red; font-size: 14px; font-weight: bold;"
+ `%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;"
+ );
+ }
}
-})();
+
+ return { test, assert, assertTrue, printSummary };
+})());
diff --git a/example/tests/gm_xhr_redirect_test.js b/example/tests/gm_xhr_redirect_test.js
index c54682c6f..4bae00f0b 100644
--- a/example/tests/gm_xhr_redirect_test.js
+++ b/example/tests/gm_xhr_redirect_test.js
@@ -11,82 +11,19 @@
// ==/UserScript==
const enableTool = true;
-(function () {
- "use strict";
- if (!enableTool) return;
-
- // ---------- Panel ----------
- const panel = document.createElement("div");
- panel.id = "gmxhr-test-panel";
- panel.innerHTML = `
-
-
-
- GM_xmlhttpRequest Test Harness
- …
-
-
-
-
- Status: idle
-
- `;
- document.documentElement.append(panel);
-
- panel.querySelector("#ver").textContent = GM.info?.script?.version ?? "";
- panel.querySelector("#handler").textContent = `${GM.info?.scriptHandler} ${GM.info?.version}`;
-
- const $log = panel.querySelector("#log");
- const $counts = panel.querySelector("#counts");
- const $status = panel.querySelector("#status");
+(async ({ assert, pass, fail, skip, state, setCounts, logLine, fmtMs, $status, verSpan, handlerSpan, startBtn, clearBtn }) => {
+ if (!enableTool) return;
- panel.querySelector("#clear").addEventListener("click", () => {
- $log.textContent = "";
+ startBtn.addEventListener("click", runAll);
+ clearBtn.addEventListener("click", () => {
+ logLine.clear();
setCounts(0, 0, 0);
$status.textContent = "Status: idle";
});
- panel.querySelector("#start").addEventListener("click", runAll);
- function logLine(html) {
- const el = document.createElement("div");
- el.innerHTML = html;
- $log.prepend(el);
- }
-
- function escapeHtml(s) {
- return String(s).replace(/[&<>"']/g, m =>
- ({ "&":"&","<":"<",">":">",'"':""","'":"'" })[m]);
- }
-
- const state = { pass: 0, fail: 0, skip: 0 };
- function setCounts(p, f, s) { $counts.textContent = `✅ ${p} ❌ ${f} ⏳ ${s}`; }
- function pass(msg) { state.pass++; setCounts(state.pass, state.fail, state.skip); logLine(`✅ ${escapeHtml(msg)}`); }
- function fail(msg, extra) {
- state.fail++; setCounts(state.pass, state.fail, state.skip);
- logLine(`❌ ${escapeHtml(msg)}${extra ? `${escapeHtml(extra)}` : ""}`);
- }
- function skip(msg) { state.skip++; setCounts(state.pass, state.fail, state.skip); logLine(`⏭️ ${escapeHtml(msg)}`); }
+ verSpan.textContent = GM.info?.script?.version ?? "";
+ handlerSpan.textContent = `${GM.info?.scriptHandler} ${GM.info?.version}`;
// ---------- Request helper ----------
function gmRequest(details, { abortAfterMs } = {}) {
@@ -106,11 +43,6 @@ const enableTool = true;
const HB = "https://httpbun.com";
- // ---------- Assertion utils ----------
- function assertEq(a, b, msg) {
- if (a !== b) throw new Error(msg ? `${msg}: expected ${b}, got ${a}` : `expected ${b}, got ${a}`);
- }
-
function objectProps(o) {
if (!o || typeof o !== "object") return "not an object";
let z, oD, zD;
@@ -130,22 +62,22 @@ const enableTool = true;
name: 'GET basic with search params 1',
async run(fetch) {
const { res } = await gmRequest({ method: "GET", url: `${HB}/get?testing=234&abc=567`, responseType: "json", fetch });
- assertEq(res.status, 200, "status 200");
- assertEq(res.response?.args?.testing, "234", "response ok");
- assertEq(res.response?.args?.abc, "567", "response ok");
- assertEq(res.response?.url, `${HB}/get?testing=234&abc=567`, "response ok");
- assertEq(objectProps(res), "ok", "Object Props OK");
+ assert(200, res.status, "status 200");
+ assert("234", res.response?.args?.testing, "response ok");
+ assert("567", res.response?.args?.abc, "response ok");
+ assert(`${HB}/get?testing=234&abc=567`, res.response?.url, "response ok");
+ assert("ok", objectProps(res), "Object Props OK");
},
},
{
name: 'GET basic with search params 2',
async run(fetch) {
const { res } = await gmRequest({ method: "GET", url: `${HB}/get?abc=567&testing=234`, responseType: "json", fetch });
- assertEq(res.status, 200, "status 200");
- assertEq(res.response?.args?.testing, "234", "response ok");
- assertEq(res.response?.args?.abc, "567", "response ok");
- assertEq(res.response?.url, `${HB}/get?abc=567&testing=234`, "response ok");
- assertEq(objectProps(res), "ok", "Object Props OK");
+ assert(200, res.status, "status 200");
+ assert("234", res.response?.args?.testing, "response ok");
+ assert("567", res.response?.args?.abc, "response ok");
+ assert(`${HB}/get?abc=567&testing=234`, res.response?.url, "response ok");
+ assert("ok", objectProps(res), "Object Props OK");
},
},
{
@@ -153,9 +85,9 @@ const enableTool = true;
async run(fetch) {
const target = `${HB}/get?z=92`;
const { res } = await gmRequest({ method: "GET", url: `${HB}/redirect-to?url=${encodeURIComponent(target)}`, fetch });
- assertEq(res.status, 200, "status after redirect is 200");
- assertEq(res.finalUrl, target, "finalUrl is redirected target");
- assertEq(objectProps(res), "ok", "Object Props OK");
+ assert(200, res.status, "status after redirect is 200");
+ assert(target, res.finalUrl, "finalUrl is redirected target");
+ assert("ok", objectProps(res), "Object Props OK");
},
},
{
@@ -163,9 +95,9 @@ const enableTool = true;
async run(fetch) {
const target = `${HB}/get?z=94`;
const { res } = await gmRequest({ method: "GET", url: `${HB}/redirect-to?url=${encodeURIComponent(target)}`, redirect: "follow", fetch });
- assertEq(res.status, 200, "status after redirect is 200");
- assertEq(res.finalUrl, target, "finalUrl is redirected target");
- assertEq(objectProps(res), "ok", "Object Props OK");
+ assert(200, res.status, "status after redirect is 200");
+ assert(target, res.finalUrl, "finalUrl is redirected target");
+ assert("ok", objectProps(res), "Object Props OK");
},
},
{
@@ -178,11 +110,11 @@ const enableTool = true;
]);
throw new Error("Expected error, got load");
} catch (e) {
- assertEq(e?.kind, "error", "error ok");
- assertEq(e?.res?.status, 408, "statusCode ok");
- assertEq(!e?.res?.finalUrl, true, "!finalUrl ok");
- assertEq(e?.res?.responseHeaders, "", "responseHeaders ok");
- assertEq(objectProps(e?.res), "ok", "Object Props OK");
+ assert("error", e?.kind, "error ok");
+ assert(408, e?.res?.status, "statusCode ok");
+ assert(true, !e?.res?.finalUrl, "!finalUrl ok");
+ assert("", e?.res?.responseHeaders, "responseHeaders ok");
+ assert("ok", objectProps(e?.res), "Object Props OK");
}
},
},
@@ -194,10 +126,10 @@ const enableTool = true;
gmRequest({ method: "GET", url, redirect: "manual", fetch }),
new Promise(resolve => setTimeout(resolve, 4000)),
]);
- assertEq(res?.status, 301, "status is 301");
- assertEq(res?.finalUrl, url, "finalUrl is original url");
- assertEq(typeof res?.responseHeaders === "string" && res?.responseHeaders !== "", true, "responseHeaders ok");
- assertEq(objectProps(res), "ok", "Object Props OK");
+ assert(301, res?.status, "status is 301");
+ assert(url, res?.finalUrl, "finalUrl is original url");
+ assert(true, typeof res?.responseHeaders === "string" && res?.responseHeaders !== "", "responseHeaders ok");
+ assert("ok", objectProps(res), "Object Props OK");
},
},
];
@@ -208,18 +140,16 @@ const enableTool = true;
];
// ---------- Runner ----------
- function fmtMs(ms) { return ms < 1000 ? `${ms | 0}ms` : `${(ms / 1000).toFixed(2)}s`; }
-
async function runAll() {
state.pass = state.fail = state.skip = 0;
setCounts(0, 0, 0);
- logLine(`Starting GM_xmlhttpRequest test suite — ${new Date().toLocaleString()}`);
+ logLine([{ text: "Starting GM_xmlhttpRequest test suite", bold: true }, { text: ` — ${new Date().toLocaleString()}` }]);
for (let i = 0; i < tests.length; i++) {
const t = tests[i];
const tName = `${t.useFetch ? "[fetch]" : "[xhr]"} ${t.name}`;
$status.textContent = `Status: running (${i + 1}/${tests.length}): ${tName}`;
- logLine(`▶️ ${escapeHtml(tName)}`);
+ logLine([{ text: "▶️ " }, { text: tName, bold: true }]);
const t0 = performance.now();
try {
await t.run(t.useFetch ? true : false);
@@ -232,7 +162,7 @@ const enableTool = true;
}
$status.textContent = "Status: done";
- logLine(`Done. ✅ ${state.pass} ❌ ${state.fail} ⏳ ${state.skip}`);
+ logLine([{ text: "Done.", bold: true }, { text: ` ✅ ${state.pass} ❌ ${state.fail} ⏳ ${state.skip}` }]);
}
setTimeout(() => {
@@ -241,4 +171,123 @@ const enableTool = true;
runAll();
}
}, 600);
-})();
\ No newline at end of file
+})((() => {
+ // ---------- Panel (DOM构建,避免innerHTML以兼容CSP) ----------
+ const panel = document.createElement("div");
+ panel.id = "gmxhr-test-panel";
+
+ const style = document.createElement("style");
+ style.textContent = `
+ #gmxhr-test-panel {
+ position:fixed; bottom:12px; right:12px; width:460px; max-height:70vh;
+ overflow:auto; z-index:2147483647; background:#111; color:#f5f5f5;
+ font:13px/1.4 system-ui,-apple-system,Segoe UI,Roboto,sans-serif;
+ border-radius:10px; box-shadow:0 12px 30px rgba(0,0,0,.4); border:1px solid #333;
+ }
+ #gmxhr-test-panel .hdr {
+ position:sticky; top:0; background:#181818; padding:10px 12px;
+ border-bottom:1px solid #333; display:flex; align-items:center; gap:8px;
+ }
+ #gmxhr-test-panel .hdr-info { flex:1 }
+ #gmxhr-test-panel button {
+ background:#2a6df1; color:#fff; border:0; padding:6px 10px;
+ border-radius:6px; cursor:pointer;
+ }
+ #gmxhr-test-panel #status { padding:6px 12px; border-bottom:1px solid #222; opacity:.9 }
+ #gmxhr-test-panel #log { padding:10px 12px }
+ #gmxhr-test-panel #log > div { padding:6px 0; border-bottom:1px dashed #2a2a2a }
+ #gmxhr-test-panel pre { white-space:pre-wrap; color:#bbb; margin:.5em 0 0 }
+ `;
+ panel.appendChild(style);
+
+ const hdr = document.createElement("div");
+ hdr.className = "hdr";
+
+ const hdrInfo = document.createElement("div");
+ hdrInfo.className = "hdr-info";
+
+ const titleDiv = document.createElement("div");
+ titleDiv.style.fontWeight = "500";
+ titleDiv.appendChild(document.createTextNode("GM_xmlhttpRequest Test Harness "));
+ const verSpan = document.createElement("span");
+ verSpan.id = "ver";
+ titleDiv.appendChild(verSpan);
+ hdrInfo.appendChild(titleDiv);
+
+ const subRow = document.createElement("div");
+ subRow.style.display = "flex";
+ const handlerSpan = document.createElement("span");
+ handlerSpan.id = "handler";
+ const countsSpan = document.createElement("span");
+ countsSpan.id = "counts";
+ countsSpan.style.marginLeft = "auto";
+ countsSpan.style.opacity = ".8";
+ countsSpan.textContent = "…";
+ subRow.appendChild(handlerSpan);
+ subRow.appendChild(countsSpan);
+ hdrInfo.appendChild(subRow);
+
+ hdr.appendChild(hdrInfo);
+
+ const startBtn = document.createElement("button");
+ startBtn.id = "start";
+ startBtn.textContent = "Run";
+ const clearBtn = document.createElement("button");
+ clearBtn.id = "clear";
+ clearBtn.textContent = "Clear";
+ hdr.appendChild(startBtn);
+ hdr.appendChild(clearBtn);
+
+ panel.appendChild(hdr);
+
+ const $status = document.createElement("div");
+ $status.id = "status";
+ $status.textContent = "Status: idle";
+ panel.appendChild($status);
+
+ const $log = document.createElement("div");
+ $log.id = "log";
+ panel.appendChild($log);
+
+ document.documentElement.append(panel);
+
+ // assert(expected, actual, message) - 比较两个值是否相等
+ function assert(expected, actual, message) {
+ if (expected !== actual) {
+ throw new Error(message ? `${message}: expected ${expected}, got ${actual}` : `expected ${expected}, got ${actual}`);
+ }
+ }
+
+ const state = { pass: 0, fail: 0, skip: 0 };
+ function setCounts(p, f, s) { countsSpan.textContent = `✅ ${p} ❌ ${f} ⏳ ${s}`; }
+
+ // logLine(parts, extra) - parts 为字符串或 { text, bold } 片段数组;extra 为附加的 文本
+ function logLine(parts, extra) {
+ const el = document.createElement("div");
+ const segments = Array.isArray(parts) ? parts : [{ text: parts }];
+ segments.forEach(seg => {
+ if (seg.bold) {
+ const b = document.createElement("b");
+ b.textContent = seg.text;
+ el.appendChild(b);
+ } else {
+ el.appendChild(document.createTextNode(seg.text));
+ }
+ });
+ if (extra) {
+ const pre = document.createElement("pre");
+ pre.textContent = extra;
+ el.appendChild(pre);
+ }
+ $log.prepend(el);
+ }
+ logLine.clear = () => { $log.textContent = ""; };
+
+ function pass(msg) { state.pass++; setCounts(state.pass, state.fail, state.skip); logLine(`✅ ${msg}`); }
+ function fail(msg, extra) { state.fail++; setCounts(state.pass, state.fail, state.skip); logLine(`❌ ${msg}`, extra); }
+ function skip(msg) { state.skip++; setCounts(state.pass, state.fail, state.skip); logLine(`⏭️ ${msg}`); }
+
+ function fmtMs(ms) { return ms < 1000 ? `${ms | 0}ms` : `${(ms / 1000).toFixed(2)}s`; }
+
+ return { assert, pass, fail, skip, state, setCounts, logLine, fmtMs, $status, verSpan, handlerSpan, startBtn, clearBtn };
+})());
diff --git a/example/tests/gm_xhr_test.js b/example/tests/gm_xhr_test.js
index 2cccdf56a..b9e7c73d9 100644
--- a/example/tests/gm_xhr_test.js
+++ b/example/tests/gm_xhr_test.js
@@ -44,378 +44,15 @@
✓ edge cases: huge headers trimmed? invalid method; invalid URL; missing @connect domain triggers onerror
*/
+"use strict";
+
const enableTool = true;
-(function () {
- "use strict";
+(async ({ escapeHtml, fmtMs, logLine, pass, fail, skip, setCounts, setStatus, setQueue, prettyStack, state, assert, assertTrue, assertDeepEq, resPrint, isFirefox, printSummary, showResultPanel }) => {
if (!enableTool) return;
- // ---------- Small DOM helper ----------
- function h(tag, props = {}, ...children) {
- const el = document.createElement(tag);
- Object.entries(props).forEach(([k, v]) => {
- if (k === "style" && typeof v === "object") Object.assign(el.style, v);
- else if (k.startsWith("on") && typeof v === "function") el.addEventListener(k.slice(2), v);
- else el[k] = v;
- });
- for (const c of children) el.append(c && c.nodeType ? c : document.createTextNode(String(c)));
- return el;
- }
-
- // value type helper
- const typing = (x) => {
- let t = x === null ? "null" : typeof x;
- if (!x) t = `<${t}>`;
- if (t === "object") {
- try {
- return x[Symbol.toStringTag] || "object";
- } catch (e) {}
- }
- return t;
- };
-
- const statusCode = (response) => {
- return (+response.readyState + +response.status / 1000).toFixed(3);
- };
-
- const resPrint = (r) => {
- const a = statusCode(r);
- const b1 = "response" in r ? typing(r.response) : "missing";
- const b2 = "responseText" in r ? typing(r.responseText) : "missing";
- const b3 = "responseXML" in r ? typing(r.responseXML) : "missing";
- return `${a};r=${b1};t=${b2};x=${b3}`;
- };
-
- const isFirefox = typeof mozInnerScreenX === "number";
-
- // ---------- Test Panel ----------
- const panel = h(
- "div",
- {
- id: "gmxhr-test-panel",
- style: {
- position: "fixed",
- bottom: "12px",
- right: "12px",
- width: "460px",
- maxHeight: "70vh",
- overflow: "auto",
- zIndex: 2147483647,
- background: "#111",
- color: "#f5f5f5",
- font: "13px/1.4 system-ui, -apple-system, Segoe UI, Roboto, sans-serif",
- borderRadius: "10px",
- boxShadow: "0 12px 30px rgba(0,0,0,.4)",
- border: "1px solid #333",
- },
- },
- h(
- "div",
- {
- style: {
- position: "sticky",
- top: 0,
- background: "#181818",
- padding: "10px 12px",
- borderBottom: "1px solid #333",
- display: "flex",
- alignItems: "center",
- gap: "8px",
- },
- },
- h("div", {}, h("div", { style: { fontWeight: "500" } }, `GM_xmlhttpRequest Test Harness ${GM.info?.script?.version}`), h("div", { style: { display: "flex", flexDirection: "row" } }, h("div", { style: { fontWeight: "400" } }, `${GM.info?.scriptHandler} ${GM.info?.version}`), h("div", { id: "counts", style: { marginLeft: "auto", opacity: 0.8 } }, "…"))),
- h("button", { id: "start", style: btn() }, "Run"),
- h("button", { id: "clear", style: btn() }, "Clear")
- ),
- // Added: live status + pending queue (minimal UI)
- h(
- "div",
- { id: "status", style: { padding: "6px 12px", borderBottom: "1px solid #222", opacity: 0.9 } },
- "Status: idle"
- ),
- h(
- "details",
- { id: "queueWrap", open: false, style: { padding: "0 12px 6px", borderBottom: "1px solid #222" } },
- h("summary", {}, "Pending tests"),
- h(
- "div",
- {
- id: "queue",
- style: {
- fontFamily: "ui-monospace, SFMono-Regular, Consolas, monospace",
- whiteSpace: "pre-wrap",
- opacity: 0.8,
- },
- },
- "(none)"
- )
- ),
- h("div", { id: "log", style: { padding: "10px 12px" } })
- );
- document.documentElement.append(panel);
-
- function btn() {
- return {
- background: "#2a6df1",
- color: "white",
- border: "0",
- padding: "6px 10px",
- borderRadius: "6px",
- cursor: "pointer",
- };
- }
-
- const $log = panel.querySelector("#log");
- const $counts = panel.querySelector("#counts");
- const $status = panel.querySelector("#status");
- const $queue = panel.querySelector("#queue");
- panel.querySelector("#clear").addEventListener("click", () => {
- $log.textContent = "";
- setCounts(0, 0, 0);
- setStatus("idle");
- setQueue([]);
- });
+ const { panel } = showResultPanel();
panel.querySelector("#start").addEventListener("click", runAll);
- function logLine(html, cls = "") {
- const line = h("div", { style: { padding: "6px 0", borderBottom: "1px dashed #2a2a2a" } });
- line.innerHTML = html;
- if (cls) line.className = cls;
- $log.prepend(line);
- }
-
- function setCounts(p, f, s) {
- $counts.textContent = `✅ ${p} ❌ ${f} ⏳ ${s}`;
- }
- function setStatus(text) {
- $status.textContent = `Status: ${text}`;
- }
- function setQueue(items) {
- $queue.textContent = items.length ? items.map((t, i) => `${i + 1}. ${t}`).join("\n") : "(none)";
- }
-
- // ---------- Pretty Stack -----------
- function prettyStack(errorOrStack, options = {}) {
- const {
- stripQuery = true,
- decode = true,
- maxUrlLength = 90,
- dropExtensionUuid = true,
- pathSegments = 2,
- indent = " ",
- minFnWidth = 8,
- maxFnWidth = 48,
- minLocWidth = 5,
- maxLocWidth = 12,
- maxLines = -1,
- } = options;
-
- const rawStack =
- typeof errorOrStack === "string"
- ? errorOrStack
- : errorOrStack && errorOrStack.stack
- ? errorOrStack.stack
- : String(errorOrStack);
-
- const lines = rawStack.split(/\r?\n/).filter(Boolean);
-
- const frames = lines
- .filter((line, j) => maxLines > 0 ? j < maxLines : true)
- .map(parseStackLine)
- .filter(Boolean)
- .map(frame => ({
- ...frame,
- fn: cleanFunctionName(frame.fn),
- file: cleanFileName(frame.file, {
- stripQuery,
- decode,
- maxUrlLength,
- dropExtensionUuid,
- pathSegments,
- }),
- }));
-
- if (!frames.length) return rawStack;
-
- const fnWidth = clamp(
- frames.reduce((max, f) => Math.max(max, f.fn.length), minFnWidth),
- minFnWidth,
- maxFnWidth
- );
-
- const locWidth = clamp(
- frames.reduce((max, f) => {
- return Math.max(max, `${f.line}:${f.col}`.length);
- }, minLocWidth),
- minLocWidth,
- maxLocWidth
- );
-
- return frames
- .map(f => {
- const fn = padRight(truncateMiddle(f.fn, fnWidth), fnWidth);
- const loc = padLeft(`${f.line}:${f.col}`, locWidth);
- return `${indent}${fn} ${loc} ${f.file}`;
- })
- .join("\n");
- }
-
- function parseStackLine(line) {
- const s = line.trim();
-
- // Chrome / V8:
- // at fn (file:line:col)
- //
- // Examples:
- // at run (https://example.com/app.js:10:5)
- // at async runAll (https://example.com/app.js:20:9)
- // at new Foo (https://example.com/app.js:30:11)
- let m = s.match(/^at\s+(.+?)\s+\((.+):(\d+):(\d+)\)$/);
- if (m) {
- return {
- fn: m[1],
- file: m[2],
- line: Number(m[3]),
- col: Number(m[4]),
- };
- }
-
- // Chrome / V8 anonymous:
- // at file:line:col
- m = s.match(/^at\s+(.+):(\d+):(\d+)$/);
- if (m) {
- return {
- fn: "",
- file: m[1],
- line: Number(m[2]),
- col: Number(m[3]),
- };
- }
-
- // Firefox:
- // fn@file:line:col
- // async*fn@file:line:col
- // setTimeout handler*fn@file:line:col
- //
- // Use a greedy file capture so the last two numeric groups win.
- m = s.match(/^(.*?)@(.+):(\d+):(\d+)$/);
- if (m) {
- return {
- fn: m[1] || "",
- file: m[2],
- line: Number(m[3]),
- col: Number(m[4]),
- };
- }
-
- return null;
- }
-
- function cleanFunctionName(fn) {
- let s = String(fn).trim();
-
- if (!s) return "";
- if (s === "") return s;
-
- return s
- .replace(/^async\*/, "async ")
- .replace(/^setTimeout handler\*/, "timer ")
- .replace(/^promise callback\*/, "promise ")
- .replace(/\["#-[^"]+"\]/g, "[userscript]")
- .replace(/\/+/g, "")
- .replace(/\s+/g, " ")
- .trim() || "";
- }
-
- function cleanFileName(file, options) {
- let s = String(file);
-
- if (options.stripQuery) {
- s = s.replace(/[?#].*$/, "");
- }
-
- if (options.dropExtensionUuid) {
- s = s.replace(
- /^(?:moz|chrome)-extension:\/\/[^/]+\//,
- "extension://"
- );
- }
-
- let parts = s.split("/");
-
- if (options.pathSegments > 0) {
- parts = parts.slice(-options.pathSegments);
- }
-
- if (options.decode) {
- parts = parts.map(part => {
- try {
- return decodeURIComponent(part);
- } catch {
- return part;
- }
- });
- }
-
- s = parts.join("/");
-
- return truncateMiddle(s, options.maxUrlLength);
- }
-
- function truncateMiddle(value, max) {
- const str = String(value);
-
- if (!Number.isFinite(max) || max <= 0) return "";
- if (str.length <= max) return str;
- if (max <= 3) return str.slice(0, max);
-
- const available = max - 1;
- const left = Math.ceil(available / 2);
- const right = Math.floor(available / 2);
-
- return `${str.slice(0, left)}…${str.slice(-right)}`;
- }
-
- function padRight(value, width) {
- return String(value).padEnd(width, " ");
- }
-
- function padLeft(value, width) {
- return String(value).padStart(width, " ");
- }
-
- function clamp(value, min, max) {
- return Math.min(Math.max(value, min), max);
- }
-
- // ---------- Assertion & request helpers ----------
- const state = { pass: 0, fail: 0, skip: 0 };
- function pass(msg) {
- state.pass++;
- setCounts(state.pass, state.fail, state.skip);
- logLine(`✅ ${escapeHtml(msg)}`);
- }
- function fail(msg, extra) {
- state.fail++;
- setCounts(state.pass, state.fail, state.skip);
- logLine(
- `❌ ${escapeHtml(msg)}${extra ? `${escapeHtml(extra)}` : ""}`,
- "fail"
- );
- }
- function skip(msg) {
- state.skip++;
- setCounts(state.pass, state.fail, state.skip);
- logLine(`⏭️ ${escapeHtml(msg)}`, "skip");
- }
-
- function escapeHtml(s) {
- return String(s).replace(
- /[&<>"']/g,
- (m) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[m]
- );
- }
-
function gmRequest(details, { abortAfterMs } = {}) {
return new Promise((resolve, reject) => {
const t0 = performance.now();
@@ -465,11 +102,11 @@ const enableTool = true;
url,
fetch,
});
- assertEq(res.status, 200, "status 200");
- assertEq(res.responseText, decodedBase64, "responseText ok");
- assertEq(res.response, decodedBase64, "response ok");
- assertEq(res.responseXML instanceof XMLDocument, true, "responseXML ok");
- assertEq(objectProps(res), "ok", "Object Props OK");
+ assert(res.status, 200, "status 200");
+ assert(res.responseText, decodedBase64, "responseText ok");
+ assert(res.response, decodedBase64, "response ok");
+ assert(res.responseXML instanceof XMLDocument, true, "responseXML ok");
+ assert(objectProps(res), "ok", "Object Props OK");
},
},
{
@@ -482,11 +119,11 @@ const enableTool = true;
responseType: "",
fetch,
});
- assertEq(res.status, 200, "status 200");
- assertEq(res.responseText, decodedBase64, "responseText ok");
- assertEq(res.response, decodedBase64, "response ok");
- assertEq(res.responseXML instanceof XMLDocument, true, "responseXML ok");
- assertEq(objectProps(res), "ok", "Object Props OK");
+ assert(res.status, 200, "status 200");
+ assert(res.responseText, decodedBase64, "responseText ok");
+ assert(res.response, decodedBase64, "response ok");
+ assert(res.responseXML instanceof XMLDocument, true, "responseXML ok");
+ assert(objectProps(res), "ok", "Object Props OK");
},
},
{
@@ -499,11 +136,11 @@ const enableTool = true;
responseType: "text",
fetch,
});
- assertEq(res.status, 200, "status 200");
- assertEq(res.responseText, decodedBase64, "responseText ok");
- assertEq(res.response, decodedBase64, "response ok");
- assertEq(res.responseXML instanceof XMLDocument, true, "responseXML ok");
- assertEq(objectProps(res), "ok", "Object Props OK");
+ assert(res.status, 200, "status 200");
+ assert(res.responseText, decodedBase64, "responseText ok");
+ assert(res.response, decodedBase64, "response ok");
+ assert(res.responseXML instanceof XMLDocument, true, "responseXML ok");
+ assert(objectProps(res), "ok", "Object Props OK");
},
},
@@ -517,11 +154,11 @@ const enableTool = true;
responseType: "json",
fetch,
});
- assertEq(res.status, 200, "status 200");
- assertEq(res.responseText, decodedBase64, "responseText ok");
- assertEq(res.response, undefined, "response ok");
- assertEq(res.responseXML instanceof XMLDocument, true, "responseXML ok");
- assertEq(objectProps(res), "ok", "Object Props OK");
+ assert(res.status, 200, "status 200");
+ assert(res.responseText, decodedBase64, "responseText ok");
+ assert(res.response, undefined, "response ok");
+ assert(res.responseXML instanceof XMLDocument, true, "responseXML ok");
+ assert(objectProps(res), "ok", "Object Props OK");
},
},
{
@@ -534,11 +171,11 @@ const enableTool = true;
responseType: "document",
fetch,
});
- assertEq(res.status, 200, "status 200");
- assertEq(res.responseText, decodedBase64, "responseText ok");
- assertEq(res.response instanceof XMLDocument, true, "response ok");
- assertEq(res.responseXML instanceof XMLDocument, true, "responseXML ok");
- assertEq(objectProps(res), "ok", "Object Props OK");
+ assert(res.status, 200, "status 200");
+ assert(res.responseText, decodedBase64, "responseText ok");
+ assert(res.response instanceof XMLDocument, true, "response ok");
+ assert(res.responseXML instanceof XMLDocument, true, "responseXML ok");
+ assert(objectProps(res), "ok", "Object Props OK");
},
},
{
@@ -551,11 +188,11 @@ const enableTool = true;
responseType: "stream",
fetch,
});
- assertEq(res.status, 200, "status 200");
- assertEq(res.responseText, undefined, "responseText ok");
- assertEq(res.response instanceof ReadableStream, true, "response ok");
- assertEq(res.responseXML, undefined, "responseXML ok");
- assertEq(objectProps(res), "ok", "Object Props OK");
+ assert(res.status, 200, "status 200");
+ assert(res.responseText, undefined, "responseText ok");
+ assert(res.response instanceof ReadableStream, true, "response ok");
+ assert(res.responseXML, undefined, "responseXML ok");
+ assert(objectProps(res), "ok", "Object Props OK");
},
},
{
@@ -568,11 +205,11 @@ const enableTool = true;
responseType: "arraybuffer",
fetch,
});
- assertEq(res.status, 200, "status 200");
- assertEq(res.responseText, decodedBase64, "responseText ok");
- assertEq(res.response instanceof ArrayBuffer, true, "response ok");
- assertEq(res.responseXML instanceof XMLDocument, true, "responseXML ok");
- assertEq(objectProps(res), "ok", "Object Props OK");
+ assert(res.status, 200, "status 200");
+ assert(res.responseText, decodedBase64, "responseText ok");
+ assert(res.response instanceof ArrayBuffer, true, "response ok");
+ assert(res.responseXML instanceof XMLDocument, true, "responseXML ok");
+ assert(objectProps(res), "ok", "Object Props OK");
},
},
{
@@ -585,11 +222,11 @@ const enableTool = true;
responseType: "blob",
fetch,
});
- assertEq(res.status, 200, "status 200");
- assertEq(res.responseText, decodedBase64, "responseText ok");
- assertEq(res.response instanceof Blob, true, "response ok");
- assertEq(res.responseXML instanceof XMLDocument, true, "responseXML ok");
- assertEq(objectProps(res), "ok", "Object Props OK");
+ assert(res.status, 200, "status 200");
+ assert(res.responseText, decodedBase64, "responseText ok");
+ assert(res.response instanceof Blob, true, "response ok");
+ assert(res.responseXML instanceof XMLDocument, true, "responseXML ok");
+ assert(objectProps(res), "ok", "Object Props OK");
},
},
{
@@ -601,11 +238,11 @@ const enableTool = true;
url,
fetch,
});
- assertEq(res.status, 200, "status 200");
- assertEq(`${res.responseText}`.includes('"code": 200'), true, "responseText ok");
- assertEq(`${res.response}`.includes('"code": 200'), true, "response ok");
- assertEq(res.responseXML instanceof XMLDocument, true, "responseXML ok");
- assertEq(objectProps(res), "ok", "Object Props OK");
+ assert(res.status, 200, "status 200");
+ assert(`${res.responseText}`.includes('"code": 200'), true, "responseText ok");
+ assert(`${res.response}`.includes('"code": 200'), true, "response ok");
+ assert(res.responseXML instanceof XMLDocument, true, "responseXML ok");
+ assert(objectProps(res), "ok", "Object Props OK");
},
},
{
@@ -618,11 +255,11 @@ const enableTool = true;
responseType: "",
fetch,
});
- assertEq(res.status, 200, "status 200");
- assertEq(`${res.responseText}`.includes('"code": 200'), true, "responseText ok");
- assertEq(`${res.response}`.includes('"code": 200'), true, "response ok");
- assertEq(res.responseXML instanceof XMLDocument, true, "responseXML ok");
- assertEq(objectProps(res), "ok", "Object Props OK");
+ assert(res.status, 200, "status 200");
+ assert(`${res.responseText}`.includes('"code": 200'), true, "responseText ok");
+ assert(`${res.response}`.includes('"code": 200'), true, "response ok");
+ assert(res.responseXML instanceof XMLDocument, true, "responseXML ok");
+ assert(objectProps(res), "ok", "Object Props OK");
},
},
{
@@ -635,11 +272,11 @@ const enableTool = true;
responseType: "text",
fetch,
});
- assertEq(res.status, 200, "status 200");
- assertEq(`${res.responseText}`.includes('"code": 200'), true, "responseText ok");
- assertEq(`${res.response}`.includes('"code": 200'), true, "response ok");
- assertEq(res.responseXML instanceof XMLDocument, true, "responseXML ok");
- assertEq(objectProps(res), "ok", "Object Props OK");
+ assert(res.status, 200, "status 200");
+ assert(`${res.responseText}`.includes('"code": 200'), true, "responseText ok");
+ assert(`${res.response}`.includes('"code": 200'), true, "response ok");
+ assert(res.responseXML instanceof XMLDocument, true, "responseXML ok");
+ assert(objectProps(res), "ok", "Object Props OK");
},
},
{
@@ -652,11 +289,11 @@ const enableTool = true;
responseType: "json",
fetch,
});
- assertEq(res.status, 200, "status 200");
- assertEq(`${res.responseText}`.includes('"code": 200'), true, "responseText ok");
- assertEq(typeof res.response === "object" && res.response?.code === 200, true, "response ok");
- assertEq(res.responseXML instanceof XMLDocument, true, "responseXML ok");
- assertEq(objectProps(res), "ok", "Object Props OK");
+ assert(res.status, 200, "status 200");
+ assert(`${res.responseText}`.includes('"code": 200'), true, "responseText ok");
+ assert(typeof res.response === "object" && res.response?.code === 200, true, "response ok");
+ assert(res.responseXML instanceof XMLDocument, true, "responseXML ok");
+ assert(objectProps(res), "ok", "Object Props OK");
},
},
{
@@ -669,11 +306,11 @@ const enableTool = true;
responseType: "document",
fetch,
});
- assertEq(res.status, 200, "status 200");
- assertEq(`${res.responseText}`.includes('"code": 200'), true, "responseText ok");
- assertEq(res.response instanceof XMLDocument, true, "response ok");
- assertEq(res.responseXML instanceof XMLDocument, true, "responseXML ok");
- assertEq(objectProps(res), "ok", "Object Props OK");
+ assert(res.status, 200, "status 200");
+ assert(`${res.responseText}`.includes('"code": 200'), true, "responseText ok");
+ assert(res.response instanceof XMLDocument, true, "response ok");
+ assert(res.responseXML instanceof XMLDocument, true, "responseXML ok");
+ assert(objectProps(res), "ok", "Object Props OK");
},
},
{
@@ -686,11 +323,11 @@ const enableTool = true;
responseType: "stream",
fetch,
});
- assertEq(res.status, 200, "status 200");
- assertEq(res.responseText, undefined, "responseText ok");
- assertEq(res.response instanceof ReadableStream, true, "response ok");
- assertEq(res.responseXML, undefined, "responseXML ok");
- assertEq(objectProps(res), "ok", "Object Props OK");
+ assert(res.status, 200, "status 200");
+ assert(res.responseText, undefined, "responseText ok");
+ assert(res.response instanceof ReadableStream, true, "response ok");
+ assert(res.responseXML, undefined, "responseXML ok");
+ assert(objectProps(res), "ok", "Object Props OK");
},
},
{
@@ -703,11 +340,11 @@ const enableTool = true;
responseType: "arraybuffer",
fetch,
});
- assertEq(res.status, 200, "status 200");
- assertEq(`${res.responseText}`.includes('"code": 200'), true, "responseText ok");
- assertEq(res.response instanceof ArrayBuffer, true, "response ok");
- assertEq(res.responseXML instanceof XMLDocument, true, "responseXML ok");
- assertEq(objectProps(res), "ok", "Object Props OK");
+ assert(res.status, 200, "status 200");
+ assert(`${res.responseText}`.includes('"code": 200'), true, "responseText ok");
+ assert(res.response instanceof ArrayBuffer, true, "response ok");
+ assert(res.responseXML instanceof XMLDocument, true, "responseXML ok");
+ assert(objectProps(res), "ok", "Object Props OK");
},
},
{
@@ -720,11 +357,11 @@ const enableTool = true;
responseType: "blob",
fetch,
});
- assertEq(res.status, 200, "status 200");
- assertEq(`${res.responseText}`.includes('"code": 200'), true, "responseText ok");
- assertEq(res.response instanceof Blob, true, "response ok");
- assertEq(res.responseXML instanceof XMLDocument, true, "responseXML ok");
- assertEq(objectProps(res), "ok", "Object Props OK");
+ assert(res.status, 200, "status 200");
+ assert(`${res.responseText}`.includes('"code": 200'), true, "responseText ok");
+ assert(res.response instanceof Blob, true, "response ok");
+ assert(res.responseXML instanceof XMLDocument, true, "responseXML ok");
+ assert(objectProps(res), "ok", "Object Props OK");
},
},
{
@@ -736,11 +373,11 @@ const enableTool = true;
url,
fetch,
});
- assertEq(res.status, 200, "status 200");
- assertEq(res.responseText?.length >= 8 && res.responseText?.length <= 32, true, "responseText ok");
- assertEq(res.response, res.responseText, "response ok");
- assertEq(res.responseXML instanceof XMLDocument, true, "responseXML ok");
- assertEq(objectProps(res), "ok", "Object Props OK");
+ assert(res.status, 200, "status 200");
+ assert(res.responseText?.length >= 8 && res.responseText?.length <= 32, true, "responseText ok");
+ assert(res.response, res.responseText, "response ok");
+ assert(res.responseXML instanceof XMLDocument, true, "responseXML ok");
+ assert(objectProps(res), "ok", "Object Props OK");
},
},
{
@@ -753,11 +390,11 @@ const enableTool = true;
responseType: "",
fetch,
});
- assertEq(res.status, 200, "status 200");
- assertEq(res.responseText?.length >= 8 && res.responseText?.length <= 32, true, "responseText ok");
- assertEq(res.response, res.responseText, "response ok");
- assertEq(res.responseXML instanceof XMLDocument, true, "responseXML ok");
- assertEq(objectProps(res), "ok", "Object Props OK");
+ assert(res.status, 200, "status 200");
+ assert(res.responseText?.length >= 8 && res.responseText?.length <= 32, true, "responseText ok");
+ assert(res.response, res.responseText, "response ok");
+ assert(res.responseXML instanceof XMLDocument, true, "responseXML ok");
+ assert(objectProps(res), "ok", "Object Props OK");
},
},
{
@@ -770,11 +407,11 @@ const enableTool = true;
responseType: "text",
fetch,
});
- assertEq(res.status, 200, "status 200");
- assertEq(res.responseText?.length >= 8 && res.responseText?.length <= 32, true, "responseText ok");
- assertEq(res.response, res.responseText, "response ok");
- assertEq(res.responseXML instanceof XMLDocument, true, "responseXML ok");
- assertEq(objectProps(res), "ok", "Object Props OK");
+ assert(res.status, 200, "status 200");
+ assert(res.responseText?.length >= 8 && res.responseText?.length <= 32, true, "responseText ok");
+ assert(res.response, res.responseText, "response ok");
+ assert(res.responseXML instanceof XMLDocument, true, "responseXML ok");
+ assert(objectProps(res), "ok", "Object Props OK");
},
},
{
@@ -787,11 +424,11 @@ const enableTool = true;
responseType: "json",
fetch,
});
- assertEq(res.status, 200, "status 200");
- assertEq(res.responseText?.length >= 8 && res.responseText?.length <= 32, true, "responseText ok");
- assertEq(res.response, undefined, "response ok");
- assertEq(res.responseXML instanceof XMLDocument, true, "responseXML ok");
- assertEq(objectProps(res), "ok", "Object Props OK");
+ assert(res.status, 200, "status 200");
+ assert(res.responseText?.length >= 8 && res.responseText?.length <= 32, true, "responseText ok");
+ assert(res.response, undefined, "response ok");
+ assert(res.responseXML instanceof XMLDocument, true, "responseXML ok");
+ assert(objectProps(res), "ok", "Object Props OK");
},
},
{
@@ -804,11 +441,11 @@ const enableTool = true;
responseType: "document",
fetch,
});
- assertEq(res.status, 200, "status 200");
- assertEq(res.responseText?.length >= 8 && res.responseText?.length <= 32, true, "responseText ok");
- assertEq(res.response instanceof XMLDocument, true, "response ok");
- assertEq(res.responseXML instanceof XMLDocument, true, "responseXML ok");
- assertEq(objectProps(res), "ok", "Object Props OK");
+ assert(res.status, 200, "status 200");
+ assert(res.responseText?.length >= 8 && res.responseText?.length <= 32, true, "responseText ok");
+ assert(res.response instanceof XMLDocument, true, "response ok");
+ assert(res.responseXML instanceof XMLDocument, true, "responseXML ok");
+ assert(objectProps(res), "ok", "Object Props OK");
},
},
{
@@ -821,11 +458,11 @@ const enableTool = true;
responseType: "stream",
fetch,
});
- assertEq(res.status, 200, "status 200");
- assertEq(res.responseText, undefined, "responseText ok");
- assertEq(res.response instanceof ReadableStream, true, "response ok");
- assertEq(res.responseXML, undefined, "responseXML ok");
- assertEq(objectProps(res), "ok", "Object Props OK");
+ assert(res.status, 200, "status 200");
+ assert(res.responseText, undefined, "responseText ok");
+ assert(res.response instanceof ReadableStream, true, "response ok");
+ assert(res.responseXML, undefined, "responseXML ok");
+ assert(objectProps(res), "ok", "Object Props OK");
},
},
{
@@ -838,11 +475,11 @@ const enableTool = true;
responseType: "arraybuffer",
fetch,
});
- assertEq(res.status, 200, "status 200");
- assertEq(res.responseText?.length >= 8 && res.responseText?.length <= 32, true, "responseText ok");
- assertEq(res.response instanceof ArrayBuffer, true, "response ok");
- assertEq(res.responseXML instanceof XMLDocument, true, "responseXML ok");
- assertEq(objectProps(res), "ok", "Object Props OK");
+ assert(res.status, 200, "status 200");
+ assert(res.responseText?.length >= 8 && res.responseText?.length <= 32, true, "responseText ok");
+ assert(res.response instanceof ArrayBuffer, true, "response ok");
+ assert(res.responseXML instanceof XMLDocument, true, "responseXML ok");
+ assert(objectProps(res), "ok", "Object Props OK");
},
},
{
@@ -855,11 +492,11 @@ const enableTool = true;
responseType: "blob",
fetch,
});
- assertEq(res.status, 200, "status 200");
- assertEq(res.responseText?.length >= 8 && res.responseText?.length <= 32, true, "responseText ok");
- assertEq(res.response instanceof Blob, true, "response ok");
- assertEq(res.responseXML instanceof XMLDocument, true, "responseXML ok");
- assertEq(objectProps(res), "ok", "Object Props OK");
+ assert(res.status, 200, "status 200");
+ assert(res.responseText?.length >= 8 && res.responseText?.length <= 32, true, "responseText ok");
+ assert(res.response instanceof Blob, true, "response ok");
+ assert(res.responseXML instanceof XMLDocument, true, "responseXML ok");
+ assert(objectProps(res), "ok", "Object Props OK");
},
},
{
@@ -873,13 +510,13 @@ const enableTool = true;
fetch,
});
const body = JSON.parse(res.responseText);
- assertEq(res.status, 200, "status 200");
+ assert(res.status, 200, "status 200");
const q = getQueryObj(body);
- assertEq(q.x, "1", "query args");
+ assert(q.x, "1", "query args");
const hdrs = body.headers || {};
- assertEq(hdrs["X-Custom"] || hdrs["x-custom"], "Hello", "custom header echo");
- assertEq(res.finalUrl, url, "finalUrl matches");
- assertEq(objectProps(res), "ok", "Object Props OK");
+ assert(hdrs["X-Custom"] || hdrs["x-custom"], "Hello", "custom header echo");
+ assert(res.finalUrl, url, "finalUrl matches");
+ assert(objectProps(res), "ok", "Object Props OK");
},
},
{
@@ -892,9 +529,9 @@ const enableTool = true;
url,
fetch,
});
- assertEq(res.status, 200, "status after redirect is 200");
- assertEq(res.finalUrl, target, "finalUrl is redirected target");
- assertEq(objectProps(res), "ok", "Object Props OK");
+ assert(res.status, 200, "status after redirect is 200");
+ assert(res.finalUrl, target, "finalUrl is redirected target");
+ assert(objectProps(res), "ok", "Object Props OK");
},
},
{
@@ -908,9 +545,9 @@ const enableTool = true;
redirect: "follow",
fetch,
});
- assertEq(res.status, 200, "status after redirect is 200");
- assertEq(res.finalUrl, target, "finalUrl is redirected target");
- assertEq(objectProps(res), "ok", "Object Props OK");
+ assert(res.status, 200, "status after redirect is 200");
+ assert(res.finalUrl, target, "finalUrl is redirected target");
+ assert(objectProps(res), "ok", "Object Props OK");
},
},
{
@@ -932,11 +569,11 @@ const enableTool = true;
]);
throw new Error("Expected error, got load");
} catch (e) {
- assertEq(e?.kind, "error", "error ok");
- assertEq(e?.res?.status, 408, "statusCode ok");
- assertEq(!e?.res?.finalUrl, true, "!finalUrl ok");
- assertEq(e?.res?.responseHeaders, "", "responseHeaders ok");
- assertEq(objectProps(e?.res), "ok", "Object Props OK");
+ assert(e?.kind, "error", "error ok");
+ assert(e?.res?.status, 408, "statusCode ok");
+ assert(!e?.res?.finalUrl, true, "!finalUrl ok");
+ assert(e?.res?.responseHeaders, "", "responseHeaders ok");
+ assert(objectProps(e?.res), "ok", "Object Props OK");
}
},
},
@@ -955,10 +592,10 @@ const enableTool = true;
}),
new Promise((resolve) => setTimeout(resolve, 4000)),
]);
- assertEq(res?.status, 301, "status is 301");
- assertEq(res?.finalUrl, url, "finalUrl is original url");
- assertEq(typeof res?.responseHeaders === "string" && res?.responseHeaders !== "", true, "responseHeaders ok");
- assertEq(objectProps(res), "ok", "Object Props OK");
+ assert(res?.status, 301, "status is 301");
+ assert(res?.finalUrl, url, "finalUrl is original url");
+ assert(typeof res?.responseHeaders === "string" && res?.responseHeaders !== "", true, "responseHeaders ok");
+ assert(objectProps(res), "ok", "Object Props OK");
},
},
{
@@ -973,10 +610,10 @@ const enableTool = true;
fetch,
});
const body = JSON.parse(res.responseText);
- assertEq(res.status, 200);
- assertEq((body.form || {}).a, "1", "form a");
- assertEq((body.form || {}).b, "two", "form b");
- assertEq(objectProps(res), "ok", "Object Props OK");
+ assert(res.status, 200);
+ assert((body.form || {}).a, "1", "form a");
+ assert((body.form || {}).b, "two", "form b");
+ assert(objectProps(res), "ok", "Object Props OK");
},
},
{
@@ -991,9 +628,9 @@ const enableTool = true;
fetch,
});
const body = JSON.parse(res.responseText);
- assertEq(res.status, 200);
+ assert(res.status, 200);
assertDeepEq(body.json, payload, "JSON echo matches");
- assertEq(objectProps(res), "ok", "Object Props OK");
+ assert(objectProps(res), "ok", "Object Props OK");
},
},
{
@@ -1008,9 +645,9 @@ const enableTool = true;
fetch,
});
const body = JSON.parse(res.responseText);
- assertEq(res.status, 200);
- assert(body.data && body.data.length > 0, "server received some data");
- assertEq(objectProps(res), "ok", "Object Props OK");
+ assert(res.status, 200);
+ assertTrue(body.data && body.data.length > 0, "server received some data");
+ assert(objectProps(res), "ok", "Object Props OK");
},
},
{
@@ -1027,11 +664,11 @@ const enableTool = true;
},
fetch,
});
- assertEq(res.status, 200);
- assert(res.response instanceof ArrayBuffer, "arraybuffer present");
- assertEq(res.response.byteLength, size, "byte length matches");
- assert(progressCounter >= 1, "progressCounter >= 1");
- assertEq(objectProps(res), "ok", "Object Props OK");
+ assert(res.status, 200);
+ assertTrue(res.response instanceof ArrayBuffer, "arraybuffer present");
+ assert(res.response.byteLength, size, "byte length matches");
+ assertTrue(progressCounter >= 1, "progressCounter >= 1");
+ assert(objectProps(res), "ok", "Object Props OK");
},
},
{
@@ -1049,12 +686,12 @@ const enableTool = true;
},
fetch,
});
- assertEq(res.status, 200);
- assert(res.response instanceof Blob, "blob present");
+ assert(res.status, 200);
+ assertTrue(res.response instanceof Blob, "blob present");
const buf = await res.response.arrayBuffer();
- assertEq(buf.byteLength, size, "byte length matches");
- assert(progressCounter >= 1, "progressCounter >= 1");
- assertEq(objectProps(res), "ok", "Object Props OK");
+ assert(buf.byteLength, size, "byte length matches");
+ assertTrue(progressCounter >= 1, "progressCounter >= 1");
+ assert(objectProps(res), "ok", "Object Props OK");
// Do not assert image MIME; httpbun returns octet-stream here.
},
},
@@ -1068,10 +705,10 @@ const enableTool = true;
responseType: "json",
fetch,
});
- assertEq(res.status, 200);
- assert(res.response && typeof res.response === "object", "parsed JSON object");
- assert(res.response.origin, "has JSON fields");
- assertEq(objectProps(res), "ok", "Object Props OK");
+ assert(res.status, 200);
+ assertTrue(res.response && typeof res.response === "object", "parsed JSON object");
+ assertTrue(res.response.origin, "has JSON fields");
+ assert(objectProps(res), "ok", "Object Props OK");
},
},
{
@@ -1083,10 +720,10 @@ const enableTool = true;
responseType: "document",
fetch,
});
- assertEq(res.status, 200);
- assert(res.response instanceof Document, "xml present");
- assert(res.responseXML !== null, "xml OK");
- assert(!!res.responseXML.querySelector("test-123"), "xml content ok");
+ assert(res.status, 200);
+ assertTrue(res.response instanceof Document, "xml present");
+ assertTrue(res.responseXML !== null, "xml OK");
+ assertTrue(!!res.responseXML.querySelector("test-123"), "xml content ok");
},
},
{
@@ -1098,10 +735,10 @@ const enableTool = true;
responseType: "document",
fetch,
});
- assertEq(res.status, 200);
- assert(res.response instanceof Document, "xml present");
- assert(res.responseXML !== null, "xml OK");
- assert(!!res.responseXML.querySelector("parsererror"), "xml content ok");
+ assert(res.status, 200);
+ assertTrue(res.response instanceof Document, "xml present");
+ assertTrue(res.responseXML !== null, "xml OK");
+ assertTrue(!!res.responseXML.querySelector("parsererror"), "xml content ok");
},
},
{
@@ -1113,9 +750,9 @@ const enableTool = true;
overrideMimeType: "text/plain;charset=utf-8",
fetch,
});
- assertEq(res.status, 200);
- assert(typeof res.responseText === "string" && res.responseText.length > 0, "responseText available");
- assertEq(objectProps(res), "ok", "Object Props OK");
+ assert(res.status, 200);
+ assertTrue(typeof res.responseText === "string" && res.responseText.length > 0, "responseText available");
+ assert(objectProps(res), "ok", "Object Props OK");
},
},
{
@@ -1130,7 +767,7 @@ const enableTool = true;
});
throw new Error("Expected timeout, got load");
} catch (e) {
- assertEq(e.kind, "timeout", "timeout path taken");
+ assert(e.kind, "timeout", "timeout path taken");
}
},
},
@@ -1177,7 +814,7 @@ const enableTool = true;
url: `${HB}/get`,
});
assertDeepEq(normal.events, ["onload", "onloadend"], "normal fires onload then onloadend");
- assertEq(normal.response.status, 200, "normal onloadend status 200");
+ assert(normal.response.status, 200, "normal onloadend status 200");
const timeout = await runCase({
url: `${HB}/delay/5`,
@@ -1235,12 +872,12 @@ const enableTool = true;
fetch,
});
});
- assertEq(res.status, 200);
- assert(progressEvents >= 4, "received at least 4 progress events");
+ assert(res.status, 200);
+ assertTrue(progressEvents >= 4, "received at least 4 progress events");
// `progress` is guaranteed to fire only in the Fetch API.
- assert(fetch ? lastLoaded > 0 : lastLoaded >= 0, "progress loaded captured");
- assert(!response, "no response");
- assertEq(objectProps(res), "ok", "Object Props OK");
+ assertTrue(fetch ? lastLoaded > 0 : lastLoaded >= 0, "progress loaded captured");
+ assertTrue(!response, "no response");
+ assert(objectProps(res), "ok", "Object Props OK");
},
},
{
@@ -1279,12 +916,12 @@ const enableTool = true;
fetch,
});
});
- assertEq(res.status, 200);
- assert(progressEvents >= 4, "received at least 4 progress events");
+ assert(res.status, 200);
+ assertTrue(progressEvents >= 4, "received at least 4 progress events");
// `progress` is guaranteed to fire only in the Fetch API.
- assert(fetch ? lastLoaded > 0 : lastLoaded >= 0, "progress loaded captured");
- assert(response instanceof ReadableStream && typeof response.getReader === "function", "response");
- assertEq(objectProps(res), "ok", "Object Props OK");
+ assertTrue(fetch ? lastLoaded > 0 : lastLoaded >= 0, "progress loaded captured");
+ assertTrue(response instanceof ReadableStream && typeof response.getReader === "function", "response");
+ assert(objectProps(res), "ok", "Object Props OK");
},
},
{
@@ -1295,10 +932,10 @@ const enableTool = true;
url: `${HB}/response-headers`,
fetch,
});
- assertEq(res.status, 200);
- assert((res.responseText || "")?.length > 0, "body for HEAD");
- assert(typeof res.responseHeaders === "string", "response headers present");
- assertEq(objectProps(res), "ok", "Object Props OK");
+ assert(res.status, 200);
+ assertTrue((res.responseText || "")?.length > 0, "body for HEAD");
+ assertTrue(typeof res.responseHeaders === "string", "response headers present");
+ assert(objectProps(res), "ok", "Object Props OK");
},
},
{
@@ -1309,10 +946,10 @@ const enableTool = true;
url: `${HB}/response-headers`,
fetch,
});
- assertEq(res.status, 200);
- assertEq(res.responseText || "", "", "no body for HEAD");
- assert(typeof res.responseHeaders === "string", "response headers present");
- assertEq(objectProps(res), "ok", "Object Props OK");
+ assert(res.status, 200);
+ assert(res.responseText || "", "", "no body for HEAD");
+ assertTrue(typeof res.responseHeaders === "string", "response headers present");
+ assert(objectProps(res), "ok", "Object Props OK");
},
},
{
@@ -1324,8 +961,8 @@ const enableTool = true;
fetch,
});
// httpbun commonly returns 200 for OPTIONS
- assert(res.status === 200 || res.status === 204, "200/204 on OPTIONS");
- assertEq(objectProps(res), "ok", "Object Props OK");
+ assertTrue(res.status === 200 || res.status === 204, "200/204 on OPTIONS");
+ assert(objectProps(res), "ok", "Object Props OK");
},
},
{
@@ -1336,10 +973,10 @@ const enableTool = true;
url: `${HB}/delete`,
fetch,
});
- assertEq(res.status, 200);
+ assert(res.status, 200);
const body = JSON.parse(res.responseText);
- assertEq(body.method, "DELETE", "server saw DELETE");
- assertEq(objectProps(res), "ok", "Object Props OK");
+ assert(body.method, "DELETE", "server saw DELETE");
+ assert(objectProps(res), "ok", "Object Props OK");
},
},
{
@@ -1362,11 +999,11 @@ const enableTool = true;
url: `${HB}/cookies`,
fetch,
});
- assertEq(res.status, 200);
+ assert(res.status, 200);
const body = JSON.parse(res.responseText);
const cookieABC = body.cookies.abc;
- assertEq(cookieABC, "123", "cookie abc=123");
- assertEq(objectProps(res), "ok", "Object Props OK");
+ assert(cookieABC, "123", "cookie abc=123");
+ assert(objectProps(res), "ok", "Object Props OK");
},
},
{
@@ -1381,8 +1018,8 @@ const enableTool = true;
});
const body = JSON.parse(res.responseText);
const cookies = body.headers.Cookie || body.headers.cookie;
- assert(!`${cookies}`.includes("abc=123"), "no Cookie header when anonymous");
- assertEq(objectProps(res), "ok", "Object Props OK");
+ assertTrue(!`${cookies}`.includes("abc=123"), "no Cookie header when anonymous");
+ assert(objectProps(res), "ok", "Object Props OK");
},
},
{
@@ -1396,8 +1033,8 @@ const enableTool = true;
});
const body = JSON.parse(res.responseText);
const cookies = body.headers.Cookie || body.headers.cookie;
- assert(`${cookies}`.includes("abc=123"), "Cookie header");
- assertEq(objectProps(res), "ok", "Object Props OK");
+ assertTrue(`${cookies}`.includes("abc=123"), "Cookie header");
+ assert(objectProps(res), "ok", "Object Props OK");
},
},
{
@@ -1436,8 +1073,8 @@ const enableTool = true;
});
const body = JSON.parse(res.responseText);
const cookies = body.headers.Cookie || body.headers.cookie;
- assert(!cookies, "no Cookie header when anonymous");
- assertEq(objectProps(res), "ok", "Object Props OK");
+ assertTrue(!cookies, "no Cookie header when anonymous");
+ assert(objectProps(res), "ok", "Object Props OK");
},
},
{
@@ -1464,11 +1101,11 @@ const enableTool = true;
password: pass,
fetch,
});
- assertEq(res.status, 200);
+ assert(res.status, 200);
const body = JSON.parse(res.responseText);
- assertEq(body.authenticated, true, "authenticated true");
- assertEq(body.user, "user", "user echoed");
- assertEq(objectProps(res), "ok", "Object Props OK");
+ assert(body.authenticated, true, "authenticated true");
+ assert(body.user, "user", "user echoed");
+ assert(objectProps(res), "ok", "Object Props OK");
},
},
{
@@ -1479,8 +1116,8 @@ const enableTool = true;
url: `${HB}/status/418`,
fetch,
});
- assertEq(res.status, 418, "418 I'm a teapot");
- assertEq(objectProps(res), "ok", "Object Props OK");
+ assert(res.status, 418, "418 I'm a teapot");
+ assert(objectProps(res), "ok", "Object Props OK");
// Still triggers onload, not onerror
},
},
@@ -1493,8 +1130,8 @@ const enableTool = true;
url: `${HB}/headers`,
fetch,
});
- assert([200, 405].includes(res.status), "200 or 405 depending on server handling");
- assertEq(objectProps(res), "ok", "Object Props OK");
+ assertTrue([200, 405].includes(res.status), "200 or 405 depending on server handling");
+ assert(objectProps(res), "ok", "Object Props OK");
},
},
{
@@ -1509,22 +1146,22 @@ const enableTool = true;
});
throw new Error("Expected onerror due to @connect, but got onload");
} catch (e) {
- assertEq(e.kind, "error", "onerror path taken");
- assert(e.res, "e.res exists");
- assertEq(e.res.status, 0, "status 0");
- assertEq(e.res.statusText, "", 'statusText ""');
- assertEq(e.res.finalUrl, undefined, "finalUrl undefined");
- assertEq(e.res.readyState, 4, "readyState DONE");
- assertEq(!e.res.response, true, "!response ok");
- assertEq(e.res.responseText, "", 'responseText ""');
- assertEq(e.res.responseXML, undefined, "responseXML undefined");
- assertEq(typeof (e.res.error || undefined), "string", "error set");
- assertEq(
+ assert(e.kind, "error", "onerror path taken");
+ assertTrue(e.res, "e.res exists");
+ assert(e.res.status, 0, "status 0");
+ assert(e.res.statusText, "", 'statusText ""');
+ assert(e.res.finalUrl, undefined, "finalUrl undefined");
+ assert(e.res.readyState, 4, "readyState DONE");
+ assert(!e.res.response, true, "!response ok");
+ assert(e.res.responseText, "", 'responseText ""');
+ assert(e.res.responseXML, undefined, "responseXML undefined");
+ assert(typeof (e.res.error || undefined), "string", "error set");
+ assert(
`${e.res.error}`.includes(`Refused to connect to "https://example.org/": `),
true,
"Refused to connect to ..."
);
- assertEq(objectProps(e.res), "ok", "Object Props OK");
+ assert(objectProps(e.res), "ok", "Object Props OK");
}
},
},
@@ -1539,22 +1176,22 @@ const enableTool = true;
});
throw new Error("Expected error, got load");
} catch (e) {
- assertEq(e.kind, "error", "onerror path taken");
- assert(e.res, "e.res exists");
- assertEq(e.res.status, 0, "status 0");
- assertEq(e.res.statusText, "", 'statusText ""');
- assertEq(e.res.finalUrl, undefined, "finalUrl undefined");
- assertEq(e.res.readyState, 4, "readyState DONE");
- assertEq(!e.res.response, true, "!response ok");
- assertEq(e.res.responseText, "", 'responseText ""');
- assertEq(e.res.responseXML, undefined, "responseXML undefined");
- assertEq(typeof (e.res.error || undefined), "string", "error set");
- assertEq(
+ assert(e.kind, "error", "onerror path taken");
+ assertTrue(e.res, "e.res exists");
+ assert(e.res.status, 0, "status 0");
+ assert(e.res.statusText, "", 'statusText ""');
+ assert(e.res.finalUrl, undefined, "finalUrl undefined");
+ assert(e.res.readyState, 4, "readyState DONE");
+ assert(!e.res.response, true, "!response ok");
+ assert(e.res.responseText, "", 'responseText ""');
+ assert(e.res.responseXML, undefined, "responseXML undefined");
+ assert(typeof (e.res.error || undefined), "string", "error set");
+ assert(
`${e.res.error}`.includes(`Refused to connect to "http://domain-abcxyz.test/": `),
true,
"Refused to connect to ..."
);
- assertEq(objectProps(e.res), "ok", "Object Props OK");
+ assert(objectProps(e.res), "ok", "Object Props OK");
}
},
},
@@ -1569,13 +1206,13 @@ const enableTool = true;
});
throw new Error("Expected error, got load");
} catch (e) {
- assertEq(e.kind, "error", "onerror path taken");
- assert(e.res, "e.res exists");
- assertEq(!e.res.response, true, "!response ok");
- assertEq(e.res.responseXML, undefined, "responseXML undefined");
- assertEq(e.res.responseHeaders, "", 'responseHeaders ""');
- assertEq(e.res.readyState, 4, "readyState 4");
- assertEq(objectProps(e.res), "ok", "Object Props OK");
+ assert(e.kind, "error", "onerror path taken");
+ assertTrue(e.res, "e.res exists");
+ assert(!e.res.response, true, "!response ok");
+ assert(e.res.responseXML, undefined, "responseXML undefined");
+ assert(e.res.responseHeaders, "", 'responseHeaders ""');
+ assert(e.res.readyState, 4, "readyState 4");
+ assert(objectProps(e.res), "ok", "Object Props OK");
}
},
},
@@ -1596,7 +1233,7 @@ const enableTool = true;
]);
throw new Error("Expected abort, got load");
} catch (e) {
- assertEq(e.kind, "abort", "abort path taken");
+ assert(e.kind, "abort", "abort path taken");
}
},
},
@@ -1611,11 +1248,11 @@ const enableTool = true;
fetch,
onprogress() {},
});
- assertEq(res.status, 200, "status 200");
- assertEq(`${res.responseText}`.includes('"code": 200'), true, "responseText ok");
- assertEq(typeof res.response === "object" && res.response?.code === 200, true, "response ok");
- assertEq(res.responseXML instanceof XMLDocument, true, "responseXML ok");
- assertEq(objectProps(res), "ok", "Object Props OK");
+ assert(res.status, 200, "status 200");
+ assert(`${res.responseText}`.includes('"code": 200'), true, "responseText ok");
+ assert(typeof res.response === "object" && res.response?.code === 200, true, "response ok");
+ assert(res.responseXML instanceof XMLDocument, true, "responseXML ok");
+ assert(objectProps(res), "ok", "Object Props OK");
},
},
{
@@ -1632,12 +1269,12 @@ const enableTool = true;
readyStateList.push(resp.readyState);
},
});
- assertEq(res.status, 200, "status 200");
- assertEq(`${res.responseText}`.includes('"code": 200'), true, "responseText ok");
- assertEq(typeof res.response === "object" && res.response?.code === 200, true, "response ok");
- assertEq(res.responseXML instanceof XMLDocument, true, "responseXML ok");
+ assert(res.status, 200, "status 200");
+ assert(`${res.responseText}`.includes('"code": 200'), true, "responseText ok");
+ assert(typeof res.response === "object" && res.response?.code === 200, true, "response ok");
+ assert(res.responseXML instanceof XMLDocument, true, "responseXML ok");
assertDeepEq(readyStateList, fetch ? [2, 4] : [1, 2, 3, 4], "status 200");
- assertEq(objectProps(res), "ok", "Object Props OK");
+ assert(objectProps(res), "ok", "Object Props OK");
},
},
{
@@ -1725,7 +1362,7 @@ const enableTool = true;
);
const resultList = [...resultSet];
if (!fetch) {
- assertEq(progressCount >= 2, true, "progressCount ok");
+ assert(progressCount >= 2, true, "progressCount ok");
assertDeepEq(
resultList,
[
@@ -1741,7 +1378,7 @@ const enableTool = true;
"standard-type GMXhr OK"
);
} else {
- assertEq(progressCount >= 2, true, "progressCount ok");
+ assert(progressCount >= 2, true, "progressCount ok");
assertDeepEq(
resultList,
[
@@ -1789,7 +1426,7 @@ const enableTool = true;
);
const resultList = [...resultSet];
if (!fetch) {
- assertEq(progressCount >= 2, true, "progressCount ok");
+ assert(progressCount >= 2, true, "progressCount ok");
assertDeepEq(
resultList,
[
@@ -1805,7 +1442,7 @@ const enableTool = true;
"standard-type GMXhr OK"
);
} else {
- assertEq(progressCount >= 2, true, "progressCount ok");
+ assert(progressCount >= 2, true, "progressCount ok");
assertDeepEq(
resultList,
[
@@ -1851,14 +1488,14 @@ const enableTool = true;
})
);
const headers = resultHeaders;
- assertEq(headers.get("content-type"), "application/json; charset=utf-8", "content-type ok");
- assertEq(
+ assert(headers.get("content-type"), "application/json; charset=utf-8", "content-type ok");
+ assert(
headers.get("reporting-endpoints").replace(/context=[-+\w]+/, "context=eJzj4tD"),
'default="/_/TranslateApiHttp/web-reports?context=eJzj4tD"',
"reporting-endpoints ok"
);
- assertEq(headers.get("cross-origin-opener-policy"), "same-origin", "cross-origin-opener-policy ok");
- assertEq(headers.get("content-encoding") !== "deflate", true, "content-encoding ok");
+ assert(headers.get("cross-origin-opener-policy"), "same-origin", "cross-origin-opener-policy ok");
+ assert(headers.get("content-encoding") !== "deflate", true, "content-encoding ok");
},
},
{
@@ -1894,14 +1531,14 @@ const enableTool = true;
})
);
const headers = resultHeaders;
- assertEq(headers.get("content-type"), "application/json; charset=utf-8", "content-type ok");
- assertEq(
+ assert(headers.get("content-type"), "application/json; charset=utf-8", "content-type ok");
+ assert(
headers.get("reporting-endpoints").replace(/context=[-+\w]+/, "context=eJzj4tD"),
'default="/_/TranslateApiHttp/web-reports?context=eJzj4tD"',
"reporting-endpoints ok"
);
- assertEq(headers.get("cross-origin-opener-policy"), "same-origin", "cross-origin-opener-policy ok");
- assertEq(
+ assert(headers.get("cross-origin-opener-policy"), "same-origin", "cross-origin-opener-policy ok");
+ assert(
headers.get("content-encoding") === "deflate" || headers.get("content-encoding") === null,
true,
"content-encoding ok"
@@ -1917,31 +1554,424 @@ const enableTool = true;
url,
fetch,
});
- assertEq(res.status, 200, "status 200");
- assertEq(typeof res.responseHeaders, "string", "responseHeaders is string");
- assertEq(res.responseHeaders.trim() === res.responseHeaders, true, "no extra space");
+ assert(res.status, 200, "status 200");
+ assert(typeof res.responseHeaders, "string", "responseHeaders is string");
+ assert(res.responseHeaders.trim() === res.responseHeaders, true, "no extra space");
// Each line should end with \r\n
const lines = res.responseHeaders.split("\r\n");
for (let i = 0; i < lines.length - 1; i++) {
- assert(lines[i].length > 0, `header line ${i} present`);
+ assertTrue(lines[i].length > 0, `header line ${i} present`);
+ }
+ assert(objectProps(res), "ok", "Object Props OK");
+ },
+ },
+ ];
+
+ const tests = [
+ ...basicTests,
+ ...basicTests.map((item) => {
+ return { ...item, useFetch: true };
+ }),
+ ];
+
+
+ function getHeader(headersStr, key) {
+ const lines = (headersStr || "").split(/\r?\n/);
+ const line = lines.find((l) => l.toLowerCase().startsWith(key.toLowerCase() + ":"));
+ return line ? line.split(":").slice(1).join(":").trim() : "";
+ }
+ function objectProps(o) {
+ if (!o || typeof o !== "object") return "not an object";
+ let z, oD, zD;
+ try {
+ z = Object.assign({}, o);
+ } catch {
+ return "Object.assign failed";
+ }
+ // accept null / "" / undefined for normal/failed/fetch_normal/fetch_failed XHR
+ // non-empty text (still primitive) can be also accepted. (common in xhr error case)
+ if (typeof (z.response ?? "") !== "string") return "non-primitive response value exposed";
+ if (typeof (z.responseText ?? "") !== "string") return "non-primitive responseText value exposed";
+ if (typeof (z.responseXML ?? "") !== "string") return "non-primitive responseXML value exposed";
+ try {
+ oD = JSON.stringify(o);
+ } catch {
+ return "JSON.stringify failed";
+ }
+ try {
+ zD = JSON.stringify(z);
+ } catch {
+ return "JSON.stringify failed";
+ }
+ if (oD !== zD) return "Object Props Failed";
+ return "ok";
+ }
+
+ // ---------- Runner ----------
+ async function runAll() {
+ // reset counts
+ state.pass = state.fail = state.skip = 0;
+ setCounts(0, 0, 0);
+ const names = tests.map((t) => t.name);
+ setQueue(names.slice());
+ logLine(`Starting GM_xmlhttpRequest test suite — ${new Date().toLocaleString()}`);
+
+ for (let i = 0; i < tests.length; i++) {
+ const t = tests[i];
+ const tName = `${t.useFetch ? "[fetch]" : "[xhr]"} ${t.name}`;
+ const title = `• ${tName}`;
+ const t0 = performance.now();
+ setStatus(`running (${i + 1}/${tests.length}): ${tName}`);
+ try {
+ logLine(`▶️ ${escapeHtml(tName)} (queued: ${tests.length - i - 1} remaining)`);
+ await t.run(t.useFetch ? true : false);
+ pass(`${title} (${fmtMs(performance.now() - t0)})`);
+ } catch (e) {
+ console.error(e);
+ const extra = e && e.stack ? prettyStack(e, { maxLines: 4 }) : null;
+ fail(`${title} (${fmtMs(performance.now() - t0)})`, [e?.message, extra].filter(Boolean).join("\n"));
+ } finally {
+ // update pending list
+ setQueue(names.slice(i + 1));
+ }
+ }
+
+ setStatus("done");
+ printSummary();
+ }
+
+ // Auto-run once after a short delay to let the page settle
+ setTimeout(() => {
+ // Only auto-run if not already run in this page session
+ if (!window.__gmxhr_test_autorun__) {
+ window.__gmxhr_test_autorun__ = true;
+ runAll();
+ }
+ }, 600);
+})((() => {
+ // 跟测试对象无关的基础设施:DOM 构建、日志面板、计数器、断言函数、堆栈美化打印。
+
+ // ---------- Small DOM helper ----------
+ // ---------- Small DOM helper ----------
+ function h(tag, props = {}, ...children) {
+ const el = document.createElement(tag);
+ Object.entries(props).forEach(([k, v]) => {
+ if (k === "style" && typeof v === "object") Object.assign(el.style, v);
+ else if (k.startsWith("on") && typeof v === "function") el.addEventListener(k.slice(2), v);
+ else el[k] = v;
+ });
+ for (const c of children) el.append(c && c.nodeType ? c : document.createTextNode(String(c)));
+ return el;
+ }
+
+ // value type helper
+ const typing = (x) => {
+ let t = x === null ? "null" : typeof x;
+ if (!x) t = `<${t}>`;
+ if (t === "object") {
+ try {
+ return x[Symbol.toStringTag] || "object";
+ } catch (e) {}
+ }
+ return t;
+ };
+
+ const statusCode = (response) => {
+ return (+response.readyState + +response.status / 1000).toFixed(3);
+ };
+
+ const resPrint = (r) => {
+ const a = statusCode(r);
+ const b1 = "response" in r ? typing(r.response) : "missing";
+ const b2 = "responseText" in r ? typing(r.responseText) : "missing";
+ const b3 = "responseXML" in r ? typing(r.responseXML) : "missing";
+ return `${a};r=${b1};t=${b2};x=${b3}`;
+ };
+
+ const isFirefox = typeof mozInnerScreenX === "number";
+
+ // ---------- Test Panel ----------
+
+ // Renders the small, fixed vocabulary of inline markup this harness's log
+ // lines use (, , entities) into real DOM nodes — no
+ // innerHTML/insertAdjacentHTML/DOMParser, so no HTML-string parsing ever
+ // touches the page (CSP-safe in the injected-script context).
+ const ENTITY_MAP = { amp: "&", lt: "<", gt: ">", quot: '"', "#39": "'" };
+ function decodeEntities(s) {
+ return s.replace(/&(#39|amp|lt|gt|quot);/g, (m, name) => ENTITY_MAP[name]);
+ }
+ function renderMarkup(container, markup) {
+ container.textContent = "";
+ const re = /<(\/?)(b|pre)(?:\s+style="([^"]*)")?>/g;
+ let last = 0, m;
+ const stack = [container];
+ while ((m = re.exec(markup))) {
+ const text = markup.slice(last, m.index);
+ if (text) stack[stack.length - 1].appendChild(document.createTextNode(decodeEntities(text)));
+ const [, closing, tagName, style] = m;
+ if (closing) {
+ if (stack.length > 1) stack.pop();
+ } else {
+ const el = document.createElement(tagName);
+ if (style) el.style.cssText = style;
+ stack[stack.length - 1].appendChild(el);
+ stack.push(el);
+ }
+ last = re.lastIndex;
+ }
+ const rest = markup.slice(last);
+ if (rest) stack[stack.length - 1].appendChild(document.createTextNode(decodeEntities(rest)));
+ }
+
+ // ---------- Panel DOM refs (populated by showResultPanel()) ----------
+ let panel, $log, $counts, $status, $queue;
+
+
+ function logLine(html, cls = "") {
+ const line = h("div", { style: { padding: "6px 0", borderBottom: "1px dashed #2a2a2a" } });
+ renderMarkup(line, html);
+ if (cls) line.className = cls;
+ $log.prepend(line);
+ }
+
+ function setCounts(p, f, s) {
+ $counts.textContent = `✅ ${p} ❌ ${f} ⏳ ${s}`;
+ }
+ function setStatus(text) {
+ $status.textContent = `Status: ${text}`;
+ }
+ function setQueue(items) {
+ $queue.textContent = items.length ? items.map((t, i) => `${i + 1}. ${t}`).join("\n") : "(none)";
+ }
+
+ // ---------- Pretty Stack -----------
+ function prettyStack(errorOrStack, options = {}) {
+ const {
+ stripQuery = true,
+ decode = true,
+ maxUrlLength = 90,
+ dropExtensionUuid = true,
+ pathSegments = 2,
+ indent = " ",
+ minFnWidth = 8,
+ maxFnWidth = 48,
+ minLocWidth = 5,
+ maxLocWidth = 12,
+ maxLines = -1,
+ } = options;
+
+ const rawStack =
+ typeof errorOrStack === "string"
+ ? errorOrStack
+ : errorOrStack && errorOrStack.stack
+ ? errorOrStack.stack
+ : String(errorOrStack);
+
+ const lines = rawStack.split(/\r?\n/).filter(Boolean);
+
+ const frames = lines
+ .filter((line, j) => maxLines > 0 ? j < maxLines : true)
+ .map(parseStackLine)
+ .filter(Boolean)
+ .map(frame => ({
+ ...frame,
+ fn: cleanFunctionName(frame.fn),
+ file: cleanFileName(frame.file, {
+ stripQuery,
+ decode,
+ maxUrlLength,
+ dropExtensionUuid,
+ pathSegments,
+ }),
+ }));
+
+ if (!frames.length) return rawStack;
+
+ const fnWidth = clamp(
+ frames.reduce((max, f) => Math.max(max, f.fn.length), minFnWidth),
+ minFnWidth,
+ maxFnWidth
+ );
+
+ const locWidth = clamp(
+ frames.reduce((max, f) => {
+ return Math.max(max, `${f.line}:${f.col}`.length);
+ }, minLocWidth),
+ minLocWidth,
+ maxLocWidth
+ );
+
+ return frames
+ .map(f => {
+ const fn = padRight(truncateMiddle(f.fn, fnWidth), fnWidth);
+ const loc = padLeft(`${f.line}:${f.col}`, locWidth);
+ return `${indent}${fn} ${loc} ${f.file}`;
+ })
+ .join("\n");
+ }
+
+ function parseStackLine(line) {
+ const s = line.trim();
+
+ // Chrome / V8:
+ // at fn (file:line:col)
+ //
+ // Examples:
+ // at run (https://example.com/app.js:10:5)
+ // at async runAll (https://example.com/app.js:20:9)
+ // at new Foo (https://example.com/app.js:30:11)
+ let m = s.match(/^at\s+(.+?)\s+\((.+):(\d+):(\d+)\)$/);
+ if (m) {
+ return {
+ fn: m[1],
+ file: m[2],
+ line: Number(m[3]),
+ col: Number(m[4]),
+ };
+ }
+
+ // Chrome / V8 anonymous:
+ // at file:line:col
+ m = s.match(/^at\s+(.+):(\d+):(\d+)$/);
+ if (m) {
+ return {
+ fn: "",
+ file: m[1],
+ line: Number(m[2]),
+ col: Number(m[3]),
+ };
+ }
+
+ // Firefox:
+ // fn@file:line:col
+ // async*fn@file:line:col
+ // setTimeout handler*fn@file:line:col
+ //
+ // Use a greedy file capture so the last two numeric groups win.
+ m = s.match(/^(.*?)@(.+):(\d+):(\d+)$/);
+ if (m) {
+ return {
+ fn: m[1] || "",
+ file: m[2],
+ line: Number(m[3]),
+ col: Number(m[4]),
+ };
+ }
+
+ return null;
+ }
+
+ function cleanFunctionName(fn) {
+ let s = String(fn).trim();
+
+ if (!s) return "";
+ if (s === "") return s;
+
+ return s
+ .replace(/^async\*/, "async ")
+ .replace(/^setTimeout handler\*/, "timer ")
+ .replace(/^promise callback\*/, "promise ")
+ .replace(/\["#-[^"]+"\]/g, "[userscript]")
+ .replace(/\/+/g, "")
+ .replace(/\s+/g, " ")
+ .trim() || "";
+ }
+
+ function cleanFileName(file, options) {
+ let s = String(file);
+
+ if (options.stripQuery) {
+ s = s.replace(/[?#].*$/, "");
+ }
+
+ if (options.dropExtensionUuid) {
+ s = s.replace(
+ /^(?:moz|chrome)-extension:\/\/[^/]+\//,
+ "extension://"
+ );
+ }
+
+ let parts = s.split("/");
+
+ if (options.pathSegments > 0) {
+ parts = parts.slice(-options.pathSegments);
+ }
+
+ if (options.decode) {
+ parts = parts.map(part => {
+ try {
+ return decodeURIComponent(part);
+ } catch {
+ return part;
}
- assertEq(objectProps(res), "ok", "Object Props OK");
- },
- },
- ];
+ });
+ }
- const tests = [
- ...basicTests,
- ...basicTests.map((item) => {
- return { ...item, useFetch: true };
- }),
- ];
+ s = parts.join("/");
+
+ return truncateMiddle(s, options.maxUrlLength);
+ }
+
+ function truncateMiddle(value, max) {
+ const str = String(value);
+
+ if (!Number.isFinite(max) || max <= 0) return "";
+ if (str.length <= max) return str;
+ if (max <= 3) return str.slice(0, max);
+
+ const available = max - 1;
+ const left = Math.ceil(available / 2);
+ const right = Math.floor(available / 2);
+
+ return `${str.slice(0, left)}…${str.slice(-right)}`;
+ }
+
+ function padRight(value, width) {
+ return String(value).padEnd(width, " ");
+ }
+
+ function padLeft(value, width) {
+ return String(value).padStart(width, " ");
+ }
+
+ function clamp(value, min, max) {
+ return Math.min(Math.max(value, min), max);
+ }
+
+ // ---------- Assertion & request helpers ----------
+ const state = { pass: 0, fail: 0, skip: 0 };
+ function pass(msg) {
+ state.pass++;
+ setCounts(state.pass, state.fail, state.skip);
+ logLine(`✅ ${escapeHtml(msg)}`);
+ }
+ function fail(msg, extra) {
+ state.fail++;
+ setCounts(state.pass, state.fail, state.skip);
+ logLine(
+ `❌ ${escapeHtml(msg)}${extra ? `${escapeHtml(extra)}` : ""}`,
+ "fail"
+ );
+ }
+ function skip(msg) {
+ state.skip++;
+ setCounts(state.pass, state.fail, state.skip);
+ logLine(`⏭️ ${escapeHtml(msg)}`, "skip");
+ }
+
+ function escapeHtml(s) {
+ return String(s).replace(
+ /[&<>"']/g,
+ (m) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[m]
+ );
+ }
// ---------- Assertion utils ----------
- function assert(condition, msg) {
+ function assertTrue(condition, msg) {
if (!condition) throw new Error(msg || "assertion failed");
}
- function assertEq(a, b, msg) {
+ function assert(a, b, msg) {
if (a !== b) throw new Error(msg ? `${msg}: expected ${b}, got ${a}` : `expected ${b}, got ${a}`);
}
function assertDeepEq(a, b, msg) {
@@ -1949,81 +1979,110 @@ const enableTool = true;
const bj = JSON.stringify(b);
if (aj !== bj) throw new Error(msg ? `${msg}: expected ${bj}, got ${aj}` : `deep equal failed`);
}
- function getHeader(headersStr, key) {
- const lines = (headersStr || "").split(/\r?\n/);
- const line = lines.find((l) => l.toLowerCase().startsWith(key.toLowerCase() + ":"));
- return line ? line.split(":").slice(1).join(":").trim() : "";
+
+ function fmtMs(ms) {
+ return ms < 1000 ? `${ms | 0}ms` : `${(ms / 1000).toFixed(2)}s`;
}
- function objectProps(o) {
- if (!o || typeof o !== "object") return "not an object";
- let z, oD, zD;
- try {
- z = Object.assign({}, o);
- } catch {
- return "Object.assign failed";
- }
- // accept null / "" / undefined for normal/failed/fetch_normal/fetch_failed XHR
- // non-empty text (still primitive) can be also accepted. (common in xhr error case)
- if (typeof (z.response ?? "") !== "string") return "non-primitive response value exposed";
- if (typeof (z.responseText ?? "") !== "string") return "non-primitive responseText value exposed";
- if (typeof (z.responseXML ?? "") !== "string") return "non-primitive responseXML value exposed";
- try {
- oD = JSON.stringify(o);
- } catch {
- return "JSON.stringify failed";
- }
- try {
- zD = JSON.stringify(z);
- } catch {
- return "JSON.stringify failed";
- }
- if (oD !== zD) return "Object Props Failed";
- return "ok";
+
+ function printSummary() {
+ logLine(`Done. Summary — ✅ ${state.pass} ❌ ${state.fail} ⏳ ${state.skip}`);
}
- // ---------- Runner ----------
- async function runAll() {
- // reset counts
- state.pass = state.fail = state.skip = 0;
- setCounts(0, 0, 0);
- const names = tests.map((t) => t.name);
- setQueue(names.slice());
- logLine(`Starting GM_xmlhttpRequest test suite — ${new Date().toLocaleString()}`);
+ // ---------- Panel ----------
+ // Builds the whole test-runner panel DOM tree and injects it into the page.
+ // Wiring the #start button (which needs runAll, a suite-specific function)
+ // is left to the caller — only the parts that don't depend on this file's
+ // specific test suite live here.
+ function showResultPanel() {
+ panel = h(
+ "div",
+ {
+ id: "gmxhr-test-panel",
+ style: {
+ position: "fixed",
+ bottom: "12px",
+ right: "12px",
+ width: "460px",
+ maxHeight: "70vh",
+ overflow: "auto",
+ zIndex: 2147483647,
+ background: "#111",
+ color: "#f5f5f5",
+ font: "13px/1.4 system-ui, -apple-system, Segoe UI, Roboto, sans-serif",
+ borderRadius: "10px",
+ boxShadow: "0 12px 30px rgba(0,0,0,.4)",
+ border: "1px solid #333",
+ },
+ },
+ h(
+ "div",
+ {
+ style: {
+ position: "sticky",
+ top: 0,
+ background: "#181818",
+ padding: "10px 12px",
+ borderBottom: "1px solid #333",
+ display: "flex",
+ alignItems: "center",
+ gap: "8px",
+ },
+ },
+ h("div", {}, h("div", { style: { fontWeight: "500" } }, `GM_xmlhttpRequest Test Harness ${GM.info?.script?.version}`), h("div", { style: { display: "flex", flexDirection: "row" } }, h("div", { style: { fontWeight: "400" } }, `${GM.info?.scriptHandler} ${GM.info?.version}`), h("div", { id: "counts", style: { marginLeft: "auto", opacity: 0.8 } }, "…"))),
+ h("button", { id: "start", style: btn() }, "Run"),
+ h("button", { id: "clear", style: btn() }, "Clear")
+ ),
+ // Added: live status + pending queue (minimal UI)
+ h(
+ "div",
+ { id: "status", style: { padding: "6px 12px", borderBottom: "1px solid #222", opacity: 0.9 } },
+ "Status: idle"
+ ),
+ h(
+ "details",
+ { id: "queueWrap", open: false, style: { padding: "0 12px 6px", borderBottom: "1px solid #222" } },
+ h("summary", {}, "Pending tests"),
+ h(
+ "div",
+ {
+ id: "queue",
+ style: {
+ fontFamily: "ui-monospace, SFMono-Regular, Consolas, monospace",
+ whiteSpace: "pre-wrap",
+ opacity: 0.8,
+ },
+ },
+ "(none)"
+ )
+ ),
+ h("div", { id: "log", style: { padding: "10px 12px" } })
+ );
+ document.documentElement.append(panel);
- for (let i = 0; i < tests.length; i++) {
- const t = tests[i];
- const tName = `${t.useFetch ? "[fetch]" : "[xhr]"} ${t.name}`;
- const title = `• ${tName}`;
- const t0 = performance.now();
- setStatus(`running (${i + 1}/${tests.length}): ${tName}`);
- try {
- logLine(`▶️ ${escapeHtml(tName)} (queued: ${tests.length - i - 1} remaining)`);
- await t.run(t.useFetch ? true : false);
- pass(`${title} (${fmtMs(performance.now() - t0)})`);
- } catch (e) {
- console.error(e);
- const extra = e && e.stack ? prettyStack(e, { maxLines: 4 }) : null;
- fail(`${title} (${fmtMs(performance.now() - t0)})`, [e?.message, extra].filter(Boolean).join("\n"));
- } finally {
- // update pending list
- setQueue(names.slice(i + 1));
- }
+ function btn() {
+ return {
+ background: "#2a6df1",
+ color: "white",
+ border: "0",
+ padding: "6px 10px",
+ borderRadius: "6px",
+ cursor: "pointer",
+ };
}
- setStatus("done");
- logLine(`Done. Summary — ✅ ${state.pass} ❌ ${state.fail} ⏳ ${state.skip}`);
- }
+ $log = panel.querySelector("#log");
+ $counts = panel.querySelector("#counts");
+ $status = panel.querySelector("#status");
+ $queue = panel.querySelector("#queue");
+ panel.querySelector("#clear").addEventListener("click", () => {
+ $log.textContent = "";
+ setCounts(0, 0, 0);
+ setStatus("idle");
+ setQueue([]);
+ });
- function fmtMs(ms) {
- return ms < 1000 ? `${ms | 0}ms` : `${(ms / 1000).toFixed(2)}s`;
+ return { panel };
}
- // Auto-run once after a short delay to let the page settle
- setTimeout(() => {
- // Only auto-run if not already run in this page session
- if (!window.__gmxhr_test_autorun__) {
- window.__gmxhr_test_autorun__ = true;
- runAll();
- }
- }, 600);
-})();
+ return { escapeHtml, fmtMs, logLine, pass, fail, skip, setCounts, setStatus, setQueue, prettyStack, state, assert, assertTrue, assertDeepEq, resPrint, isFirefox, printSummary, showResultPanel };
+})());
diff --git a/example/tests/inject_content_test.js b/example/tests/inject_content_test.js
index e34dd5805..16ef13bc7 100644
--- a/example/tests/inject_content_test.js
+++ b/example/tests/inject_content_test.js
@@ -17,48 +17,9 @@
// @run-at document-start
// ==/UserScript==
-(async function () {
- "use strict";
-
+(async ({ test, assert, assertTrue, printSummary }) => {
console.log("%c=== Content环境 GM API 测试开始 ===", "color: blue; font-size: 16px; font-weight: bold;");
- let testResults = {
- passed: 0,
- failed: 0,
- total: 0,
- };
-
- // 测试辅助函数
- 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;
- }
- }
-
- // 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 || "断言失败: 期望条件为真");
- }
- }
-
// ============ CSP绕过测试 ============
console.log("\n%c--- CSP绕过测试 ---", "color: orange; font-weight: bold;");
@@ -157,15 +118,59 @@
});
// ============ 输出测试结果 ============
- 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;");
+ printSummary();
+})((() => {
+ let testResults = {
+ passed: 0,
+ failed: 0,
+ total: 0,
+ };
+
+ // 测试辅助函数
+ 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;
+ }
+ }
+
+ // 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 || "断言失败: 期望条件为真");
+ }
+ }
+
+ // printSummary() - 输出测试结果汇总
+ function printSummary() {
+ 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;");
+ }
+ }
+
+ return { test, assert, assertTrue, printSummary };
+})());
diff --git a/example/tests/sandbox_test.js b/example/tests/sandbox_test.js
index 405aae470..43822a565 100644
--- a/example/tests/sandbox_test.js
+++ b/example/tests/sandbox_test.js
@@ -27,54 +27,18 @@ const mpt = document.body.appendChild(document.createElement("test-element-1002"
mpt.id = "test-element-1002";
mpt.name = "test-element-1002";
-(async function () {
+(async (helpers) => {
"use strict";
+ const { test, assert, assertNotSame, assertTrue, section, printSummary } = helpers;
+
const markerPrefix = `__scriptcat_sandbox_${Date.now()}_${Math.random().toString(36).slice(2)}`;
- const testResults = {
- passed: 0,
- failed: 0,
- total: 0,
- };
console.log(
"%c=== 半沙盒环境测试开始 ===",
"color: blue; font-size: 16px; font-weight: bold;",
);
- function formatValue(value) {
- if (value === window) return "[sandbox window]";
- if (value === unsafeWindow) return "[unsafeWindow]";
- if (value === document) return "[document]";
- if (value && value.nodeType) return `[node ${value.nodeName}]`;
- if (typeof value === "function")
- return `[function ${value.name || "anonymous"}]`;
- if (typeof value === "symbol") return value.toString();
- try {
- return JSON.stringify(value);
- } catch {
- return String(value);
- }
- }
-
- function assertSame(expected, actual, message) {
- if (!Object.is(expected, actual)) {
- throw new Error(
- `${message} - 期望 ${formatValue(expected)}, 实际 ${formatValue(actual)}`,
- );
- }
- }
-
- function assertNotSame(unexpected, actual, message) {
- if (Object.is(unexpected, actual)) {
- throw new Error(`${message} - 不应等于 ${formatValue(unexpected)}`);
- }
- }
-
- function assertTrue(condition, message) {
- if (!condition) throw new Error(message || "断言失败: 条件不为真");
- }
-
function assertThrowsOrKeepsValue(assign, read, expected, message) {
let threw = false;
try {
@@ -82,31 +46,13 @@ mpt.name = "test-element-1002";
} catch {
threw = true;
}
- assertSame(
+ assert(
expected,
read(),
`${message}${threw ? "(赋值抛出,值保持不变)" : ""}`,
);
}
- async function test(name, fn) {
- testResults.total++;
- try {
- await fn();
- testResults.passed++;
- console.log(`%cPASS ${name}`, "color: green;");
- return true;
- } catch (error) {
- testResults.failed++;
- console.error(`%cFAIL ${name}`, "color: red;", error);
- return false;
- }
- }
-
- function section(name) {
- console.log(`\n%c--- ${name} ---`, "color: orange; font-weight: bold;");
- }
-
function waitForEventLoop() {
return new Promise((resolve) => setTimeout(resolve, 0));
}
@@ -151,28 +97,28 @@ mpt.name = "test-element-1002";
section("沙盒全局身份");
await test("检测全局 testVar1001 会否跳出沙盒", () => {
- assertSame(undefined, window["testVar1001"], "全局 testVar1001 不应跳出沙盒");
- assertSame(undefined, unsafeWindow["testVar1001"], "全局 testVar1001 不应跳出沙盒");
+ assert(undefined, window["testVar1001"], "全局 testVar1001 不应跳出沙盒");
+ assert(undefined, unsafeWindow["testVar1001"], "全局 testVar1001 不应跳出沙盒");
});
await test("检测全局 test-element-1002", () => {
- assertSame("test-element-1002", unsafeWindow["test-element-1002"]?.id, "全局 test-element-1002");
- assertSame(undefined, window["test-element-1002"]?.id, "半沙盒无法检测 test-element-1002");
+ assert("test-element-1002", unsafeWindow["test-element-1002"]?.id, "全局 test-element-1002");
+ assert(undefined, window["test-element-1002"]?.id, "半沙盒无法检测 test-element-1002");
});
await test("window/self/globalThis/top/parent/frames 均指向沙盒对象", () => {
- assertSame("object", typeof unsafeWindow, "unsafeWindow 应存在");
+ assert("object", typeof unsafeWindow, "unsafeWindow 应存在");
assertNotSame(
unsafeWindow,
window,
"默认 grant 环境下 window 不应是页面 window",
);
- assertSame(window, self, "self 应指向沙盒 window");
- assertSame(window, globalThis, "globalThis 应指向沙盒 window");
- assertSame(window, top, "top 应指向沙盒 window");
- assertSame(window, parent, "parent 应指向沙盒 window");
- assertSame(window, frames, "frames 应指向沙盒 window");
- assertSame(
+ assert(window, self, "self 应指向沙盒 window");
+ assert(window, globalThis, "globalThis 应指向沙盒 window");
+ assert(window, top, "top 应指向沙盒 window");
+ assert(window, parent, "parent 应指向沙盒 window");
+ assert(window, frames, "frames 应指向沙盒 window");
+ assert(
"[object Window]",
Object.prototype.toString.call(window),
"沙盒 window 应保持 Window 标记",
@@ -180,22 +126,22 @@ mpt.name = "test-element-1002";
});
await test("沙盒 window 使用空原型,但保留页面 Window 外观 (Issue #962)", () => {
- assertSame(
+ assert(
null,
Object.getPrototypeOf(window),
"沙盒 window 的原型应为空,避免 Object.prototype 污染穿透",
);
- assertSame(
+ assert(
unsafeWindow.constructor,
window.constructor,
"constructor 应与页面 Window 构造器一致",
);
- assertSame(
+ assert(
unsafeWindow.__proto__,
window.__proto__,
"__proto__ 应暴露页面 Window 原型用于兼容",
);
- assertSame(
+ assert(
false,
window instanceof unsafeWindow.constructor,
"沙盒 window 不应是真实 Window 实例",
@@ -203,13 +149,13 @@ mpt.name = "test-element-1002";
});
await test("页面 DOM getter 返回真实页面对象", () => {
- assertSame(unsafeWindow.document, document, "document 应是页面 document");
- assertSame(
+ assert(unsafeWindow.document, document, "document 应是页面 document");
+ assert(
unsafeWindow.location.href,
location.href,
"location 应读取页面地址",
);
- assertSame(
+ assert(
unsafeWindow.document.documentElement,
document.documentElement,
"DOM 节点身份应与页面一致",
@@ -221,16 +167,16 @@ mpt.name = "test-element-1002";
() => {
const key = `${markerPrefix}_page_global`;
unsafeWindow[key] = "page-value";
- assertSame(
+ assert(
"page-value",
unsafeWindow[key],
"页面变量应写入 unsafeWindow",
);
- assertSame(undefined, window[key], "页面变量不应出现在沙盒 window");
+ assert(undefined, window[key], "页面变量不应出现在沙盒 window");
window[key] = "sandbox-value";
- assertSame("sandbox-value", window[key], "沙盒变量应写入沙盒 window");
- assertSame("page-value", unsafeWindow[key], "沙盒变量不应覆盖页面变量");
+ assert("sandbox-value", window[key], "沙盒变量应写入沙盒 window");
+ assert("page-value", unsafeWindow[key], "沙盒变量不应覆盖页面变量");
},
() => {
delete window[`${markerPrefix}_page_global`];
@@ -246,12 +192,12 @@ mpt.name = "test-element-1002";
div.id = id;
document.body.appendChild(div);
- assertSame(
+ assert(
div,
unsafeWindow[id],
"页面 window 应可通过 named property 访问元素",
);
- assertSame(
+ assert(
undefined,
window[id],
"沙盒 window 不应通过 named property 访问页面元素",
@@ -268,16 +214,16 @@ mpt.name = "test-element-1002";
const key = `${markerPrefix}_delete_page_global`;
unsafeWindow[key] = "page-value";
- assertSame(undefined, window[key], "页面变量不应自动出现在沙盒 window");
+ assert(undefined, window[key], "页面变量不应自动出现在沙盒 window");
window[key] = "sandbox-value";
- assertSame("sandbox-value", window[key], "沙盒变量应存在");
- assertSame("page-value", unsafeWindow[key], "页面变量应保持存在");
+ assert("sandbox-value", window[key], "沙盒变量应存在");
+ assert("page-value", unsafeWindow[key], "页面变量应保持存在");
delete window[key];
- assertSame(undefined, window[key], "删除后沙盒变量应消失");
- assertSame(
+ assert(undefined, window[key], "删除后沙盒变量应消失");
+ assert(
"page-value",
unsafeWindow[key],
"删除沙盒变量不应删除页面变量",
@@ -297,8 +243,8 @@ mpt.name = "test-element-1002";
unsafeWindow[key] = "page-value";
window[key] = "sandbox-value";
- assertSame("sandbox-value", window[key], "裸变量应读取沙盒值");
- assertSame("page-value", unsafeWindow[key], "页面变量应保持存在");
+ assert("sandbox-value", window[key], "裸变量应读取沙盒值");
+ assert("page-value", unsafeWindow[key], "页面变量应保持存在");
try {
Function(`return delete ${key};`)(); // 半沙盒在页面执行
@@ -307,8 +253,8 @@ mpt.name = "test-element-1002";
delete unsafeWindow[key]; // fallback
}
- assertSame(undefined, unsafeWindow[key], "裸 delete 后页面变量应消失");
- assertSame("sandbox-value", window[key], "裸 delete 后沙盒变量不应消失");
+ assert(undefined, unsafeWindow[key], "裸 delete 后页面变量应消失");
+ assert("sandbox-value", window[key], "裸 delete 后沙盒变量不应消失");
},
() => {
window[`${markerPrefix}_delete_bare_page_global`] = undefined;
@@ -322,13 +268,13 @@ mpt.name = "test-element-1002";
() => {
const key = `${markerPrefix}_polluted`;
Object.prototype[key] = "polluted-value";
- assertSame("polluted-value", {}[key], "测试前应确认原型污染已生效");
- assertSame(
+ assert("polluted-value", {}[key], "测试前应确认原型污染已生效");
+ assert(
undefined,
window[key],
"沙盒 window 不应读取 Object.prototype 上的污染字段",
);
- assertSame(
+ assert(
false,
key in window,
"污染字段不应出现在沙盒 window 的原型链",
@@ -349,9 +295,9 @@ mpt.name = "test-element-1002";
}
}
- assertSame(undefined, window.define, "define 应被沙盒置为 undefined");
- assertSame(undefined, window.module, "module 应被沙盒置为 undefined");
- assertSame(undefined, window.exports, "exports 应被沙盒置为 undefined");
+ assert(undefined, window.define, "define 应被沙盒置为 undefined");
+ assert(undefined, window.module, "module 应被沙盒置为 undefined");
+ assert(undefined, window.exports, "exports 应被沙盒置为 undefined");
for (const key of [
"runFlag",
@@ -369,7 +315,7 @@ mpt.name = "test-element-1002";
"setInvalidContext",
"isInvalidContext",
]) {
- assertSame(undefined, window[key], `${key} 不应暴露到沙盒 window`);
+ assert(undefined, window[key], `${key} 不应暴露到沙盒 window`);
}
},
(() => {
@@ -384,8 +330,8 @@ mpt.name = "test-element-1002";
console,
"沙盒 console 应与页面 console 不是同一个对象",
);
- assertSame("function", typeof console.log, "console.log 应可调用");
- assertSame("function", typeof console.error, "console.error 应可调用");
+ assert("function", typeof console.log, "console.log 应可调用");
+ assert("function", typeof console.error, "console.error 应可调用");
});
section("原生函数与事件代理");
@@ -401,7 +347,7 @@ mpt.name = "test-element-1002";
resolve();
}, 0);
});
- assertSame(true, called, "裸调用 setTimeout 应正常执行");
+ assert(true, called, "裸调用 setTimeout 应正常执行");
let intervalCount = 0;
await new Promise((resolve) => {
@@ -411,7 +357,7 @@ mpt.name = "test-element-1002";
resolve();
}, 0);
});
- assertSame(1, intervalCount, "裸调用 setInterval 应正常执行");
+ assert(1, intervalCount, "裸调用 setInterval 应正常执行");
const rawAddEventListener = addEventListener;
const rawRemoveEventListener = removeEventListener;
@@ -423,11 +369,11 @@ mpt.name = "test-element-1002";
rawAddEventListener(eventName, handler);
unsafeWindow.dispatchEvent(new Event(eventName));
rawRemoveEventListener(eventName, handler);
- assertSame(1, count, "裸调用 addEventListener 应绑定到页面 window");
+ assert(1, count, "裸调用 addEventListener 应绑定到页面 window");
if (typeof fetch === "function") {
const rawFetch = fetch;
- assertSame("function", typeof rawFetch, "fetch 应可读取为裸函数");
+ assert("function", typeof rawFetch, "fetch 应可读取为裸函数");
}
});
@@ -446,7 +392,7 @@ mpt.name = "test-element-1002";
unsafeWindow.dispatchEvent(new Event(eventName));
rawRemoveEventListener(eventName, handler);
- assertSame(
+ assert(
1,
count,
"window.addEventListener 取出后调用应绑定到页面 window",
@@ -465,14 +411,14 @@ mpt.name = "test-element-1002";
}, 0);
});
- assertSame(true, called, "Proxy 包装后的 setTimeout 应正常执行");
+ assert(true, called, "Proxy 包装后的 setTimeout 应正常执行");
});
await test("getter 返回页面 window 时会替换为沙盒 window (Issue #1427)", () => {
- assertSame(window, self, "self getter 应返回沙盒 window");
- assertSame(window, parent, "parent getter 应返回沙盒 window");
- assertSame(window, top, "top getter 应返回沙盒 window");
- assertSame(window, frames, "frames getter 应返回沙盒 window");
+ assert(window, self, "self getter 应返回沙盒 window");
+ assert(window, parent, "parent getter 应返回沙盒 window");
+ assert(window, top, "top getter 应返回沙盒 window");
+ assert(window, frames, "frames getter 应返回沙盒 window");
});
await test("onxxx 函数赋值由页面事件触发,event.target 为 unsafeWindow", () =>
@@ -487,18 +433,18 @@ mpt.name = "test-element-1002";
count++;
thisIsNotWindow = this !== unsafeWindow;
eventTargetIsUnsafeWindow = event.target === unsafeWindow;
- assertSame("resize", event.type, "事件对象应正常传入");
+ assert("resize", event.type, "事件对象应正常传入");
};
unsafeWindow.dispatchEvent(new Event("resize"));
- assertSame(1, count, "页面 resize 应触发沙盒 onresize");
- assertSame(true, thisIsNotWindow, "onresize 回调 this 不应为 unsafeWindow");
- assertSame(true, eventTargetIsUnsafeWindow, "onresize 回调 event.target 应为 unsafeWindow");
+ assert(1, count, "页面 resize 应触发沙盒 onresize");
+ assert(true, thisIsNotWindow, "onresize 回调 this 不应为 unsafeWindow");
+ assert(true, eventTargetIsUnsafeWindow, "onresize 回调 event.target 应为 unsafeWindow");
window.onresize = null;
unsafeWindow.dispatchEvent(new Event("resize"));
unsafeWindow.dispatchEvent(new Event(eventName));
- assertSame(1, count, "清空 onresize 后不应继续触发");
+ assert(1, count, "清空 onresize 后不应继续触发");
},
() => {
window.onresize = null;
@@ -515,10 +461,10 @@ mpt.name = "test-element-1002";
},
};
window.onfocus = listenerObject;
- assertSame(listenerObject, window.onfocus, "非 primitive 对象应被保存");
+ assert(listenerObject, window.onfocus, "非 primitive 对象应被保存");
unsafeWindow.dispatchEvent(new Event("focus"));
await waitForEventLoop();
- assertSame(
+ assert(
false,
handled,
"EventListenerObject 形式不应被 onxxx 代理注册",
@@ -526,10 +472,10 @@ mpt.name = "test-element-1002";
handled = false;
const func = function () { handled = true };
window.onfocus = func;
- assertSame(func, window.onfocus, "function 对象应被保存");
+ assert(func, window.onfocus, "function 对象应被保存");
unsafeWindow.dispatchEvent(new Event("focus"));
await waitForEventLoop();
- assertSame(
+ assert(
true,
handled,
"EventListener 形式应被 onxxx 代理注册",
@@ -539,7 +485,7 @@ mpt.name = "test-element-1002";
assertNotSame(func, window.onfocus, "primitive 对象时注册能被移除 (1)");
unsafeWindow.dispatchEvent(new Event("focus"));
await waitForEventLoop();
- assertSame(
+ assert(
false,
handled,
"primitive 对象时注册能被移除 (2)",
@@ -564,8 +510,8 @@ mpt.name = "test-element-1002";
};
unsafeWindow.dispatchEvent(new Event("hashchange"));
- assertSame(0, oldCount, "旧 onhashchange 不应再被调用");
- assertSame(1, newCount, "新 onhashchange 应被调用一次");
+ assert(0, oldCount, "旧 onhashchange 不应再被调用");
+ assert(1, newCount, "新 onhashchange 应被调用一次");
},
() => {
window.onhashchange = null;
@@ -595,43 +541,43 @@ mpt.name = "test-element-1002";
await test("TM半沙盒:把祖先类别继承直接写在半沙盒上 (Issue #1462 PR #1463)", async () => {
const trueWindow = unsafeWindow;
const sandboxWindow = window;
- assertSame(false, Object.hasOwn(trueWindow, "addEventListener"), "unsafeWindow 继承 Object.hasOwn");
- assertSame(true, Reflect.has(trueWindow, "addEventListener"), "unsafeWindow 继承 Reflect.has");
- assertSame(true, Object.hasOwn(sandboxWindow, "addEventListener"), "window 属性 Object.hasOwn");
- assertSame(true, Reflect.has(sandboxWindow, "addEventListener"), "window 属性 Reflect.has");
+ assert(false, Object.hasOwn(trueWindow, "addEventListener"), "unsafeWindow 继承 Object.hasOwn");
+ assert(true, Reflect.has(trueWindow, "addEventListener"), "unsafeWindow 继承 Reflect.has");
+ assert(true, Object.hasOwn(sandboxWindow, "addEventListener"), "window 属性 Object.hasOwn");
+ assert(true, Reflect.has(sandboxWindow, "addEventListener"), "window 属性 Reflect.has");
});
section("GM API 注入与命名空间");
await test("GM_info、GM.info 与 unsafeWindow 正确暴露", () => {
- assertSame("object", typeof GM_info, "GM_info 应可用");
- assertSame("object", typeof GM.info, "GM.info 应可用");
- assertSame(JSON.stringify(GM_info), JSON.stringify(GM.info), "GM.info 应与 GM_info 一致 (JSON.stringify)");
- assertSame(
+ assert("object", typeof GM_info, "GM_info 应可用");
+ assert("object", typeof GM.info, "GM.info 应可用");
+ assert(JSON.stringify(GM_info), JSON.stringify(GM.info), "GM.info 应与 GM_info 一致 (JSON.stringify)");
+ assert(
unsafeWindow,
window.unsafeWindow,
"unsafeWindow 应指向页面 window",
);
- assertSame("object", typeof GM_info.script, "GM_info.script 应存在");
+ assert("object", typeof GM_info.script, "GM_info.script 应存在");
});
await test("GM_ 与 GM.* 双命名空间由 grant 自动补齐", () => {
- assertSame("function", typeof GM_getValue, "GM_getValue 应可用");
- assertSame(
+ assert("function", typeof GM_getValue, "GM_getValue 应可用");
+ assert(
"function",
typeof GM.getValue,
"GM.getValue 应由 GM_getValue grant 补齐",
);
- assertSame("function", typeof GM_setValue, "GM_setValue 应可用");
- assertSame("function", typeof GM.setValue, "GM.setValue 应可用");
- assertSame("function", typeof GM_deleteValue, "GM_deleteValue 应可用");
- assertSame(
+ assert("function", typeof GM_setValue, "GM_setValue 应可用");
+ assert("function", typeof GM.setValue, "GM.setValue 应可用");
+ assert("function", typeof GM_deleteValue, "GM_deleteValue 应可用");
+ assert(
"function",
typeof GM.deleteValue,
"GM.deleteValue 应由 GM_deleteValue grant 补齐",
);
- assertSame("function", typeof GM_listValues, "GM_listValues 应可用");
- assertSame(
+ assert("function", typeof GM_listValues, "GM_listValues 应可用");
+ assert(
"function",
typeof GM.listValues,
"GM.listValues 应由 GM_listValues grant 补齐",
@@ -644,14 +590,14 @@ mpt.name = "test-element-1002";
const key = `${markerPrefix}_value`;
GM_setValue(key, { env: "sandbox", ok: true });
const stored = GM_getValue(key);
- assertSame("sandbox", stored.env, "GM_getValue 应读取同步写入对象");
- assertSame(true, stored.ok, "对象值应保持属性");
+ assert("sandbox", stored.env, "GM_getValue 应读取同步写入对象");
+ assert(true, stored.ok, "对象值应保持属性");
assertTrue(
GM_listValues().includes(key),
"GM_listValues 应包含写入的键",
);
GM_deleteValue(key);
- assertSame(
+ assert(
"fallback",
GM_getValue(key, "fallback"),
"GM_deleteValue 应删除值",
@@ -667,7 +613,7 @@ mpt.name = "test-element-1002";
async () => {
const key = `${markerPrefix}_async_value`;
await withTimeout(GM.setValue(key, "async-value"), "GM.setValue");
- assertSame(
+ assert(
"async-value",
await withTimeout(GM.getValue(key), "GM.getValue"),
"GM.getValue 应读取 GM.setValue 写入值",
@@ -677,7 +623,7 @@ mpt.name = "test-element-1002";
"GM.listValues 应包含异步写入的键",
);
await withTimeout(GM.deleteValue(key), "GM.deleteValue");
- assertSame(
+ assert(
"fallback",
await withTimeout(
GM.getValue(key, "fallback"),
@@ -700,9 +646,9 @@ mpt.name = "test-element-1002";
GM_setValues({ [keyA]: "A", [keyB]: { deep: 1 } });
const picked = GM_getValues([keyA, keyB, keyMissing]);
- assertSame("A", picked[keyA], "GM_getValues 数组模式应返回已存在键");
- assertSame(1, picked[keyB].deep, "GM_getValues 应返回对象值");
- assertSame(
+ assert("A", picked[keyA], "GM_getValues 数组模式应返回已存在键");
+ assert(1, picked[keyB].deep, "GM_getValues 应返回对象值");
+ assert(
false,
Object.prototype.hasOwnProperty.call(picked, keyMissing),
"数组模式不应包含缺失键",
@@ -712,8 +658,8 @@ mpt.name = "test-element-1002";
[keyA]: "default-a",
[keyMissing]: "default-missing",
});
- assertSame("A", defaults[keyA], "对象模式应优先返回已存在值");
- assertSame(
+ assert("A", defaults[keyA], "对象模式应优先返回已存在值");
+ assert(
"default-missing",
defaults[keyMissing],
"对象模式应为缺失键返回默认值",
@@ -723,7 +669,7 @@ mpt.name = "test-element-1002";
GM.getValues({ [keyB]: null }),
"GM.getValues",
);
- assertSame(
+ assert(
1,
asyncPicked[keyB].deep,
"GM.getValues 应由 GM_getValues grant 依赖注入",
@@ -736,33 +682,33 @@ mpt.name = "test-element-1002";
));
await test("GM_cookie grant 构建函数对象与多级命名空间", () => {
- assertSame("function", typeof GM_cookie, "GM_cookie 应可用");
- assertSame(
+ assert("function", typeof GM_cookie, "GM_cookie 应可用");
+ assert(
"function",
typeof GM_cookie.set,
"GM_cookie.set 应由兼容命名空间注入",
);
- assertSame(
+ assert(
"function",
typeof GM_cookie.list,
"GM_cookie.list 应由兼容命名空间注入",
);
- assertSame(
+ assert(
"function",
typeof GM_cookie.delete,
"GM_cookie.delete 应由兼容命名空间注入",
);
- assertSame(
+ assert(
"function",
typeof GM.cookie.set,
"GM.cookie.set 应由 GM.cookie 依赖注入",
);
- assertSame(
+ assert(
"function",
typeof GM.cookie.list,
"GM.cookie.list 应由 GM.cookie 依赖注入",
);
- assertSame(
+ assert(
"function",
typeof GM.cookie.delete,
"GM.cookie.delete 应由 GM.cookie 依赖注入",
@@ -776,8 +722,8 @@ mpt.name = "test-element-1002";
const style = GM_addStyle(
`.${className} { color: rgb(1, 2, 3) !important; }`,
);
- assertSame("STYLE", style.tagName, "GM_addStyle 应创建 style 标签");
- assertSame(
+ assert("STYLE", style.tagName, "GM_addStyle 应创建 style 标签");
+ assert(
document,
style.ownerDocument,
"GM_addStyle 返回元素应属于页面 document",
@@ -789,12 +735,12 @@ mpt.name = "test-element-1002";
),
"GM.addStyle",
);
- assertSame(
+ assert(
"STYLE",
asyncStyle.tagName,
"GM.addStyle 应 resolve style 标签",
);
- assertSame(
+ assert(
document,
asyncStyle.ownerDocument,
"GM.addStyle 返回元素应属于页面 document",
@@ -819,13 +765,13 @@ mpt.name = "test-element-1002";
textContent: "ScriptCat sandbox test",
hidden: true,
});
- assertSame("DIV", div.tagName, "GM_addElement(tag, attrs) 应创建元素");
- assertSame(
+ assert("DIV", div.tagName, "GM_addElement(tag, attrs) 应创建元素");
+ assert(
document,
div.ownerDocument,
"默认创建元素应属于页面 document",
);
- assertSame(true, div.hidden, "boolean 应通过 property setter 设置");
+ assert(true, div.hidden, "boolean 应通过 property setter 设置");
const child = await withTimeout(
GM.addElement(div, "span", {
@@ -833,22 +779,22 @@ mpt.name = "test-element-1002";
}),
"GM.addElement",
);
- assertSame(
+ assert(
"SPAN",
child.tagName,
"GM.addElement(parent, tag, attrs) 应创建子元素",
);
- assertSame(div, child.parentNode, "显式 parent 应生效");
+ assert(div, child.parentNode, "显式 parent 应生效");
const script = GM_addElement("script", {
textContent: `window["${key}"] = "from-gm-add-element";`,
});
- assertSame(
+ assert(
"from-gm-add-element",
unsafeWindow[key],
"GM_addElement 插入 script 应在页面 window 执行",
);
- assertSame(
+ assert(
undefined,
window[key],
"页面执行结果不应自动写回沙盒 window",
@@ -862,12 +808,12 @@ mpt.name = "test-element-1002";
));
await test("window.close/window.focus grant 暴露为沙盒 window 方法", () => {
- assertSame(
+ assert(
"function",
typeof window.close,
"window.close grant 应暴露 close",
);
- assertSame(
+ assert(
"function",
typeof window.focus,
"window.focus grant 应暴露 focus",
@@ -887,15 +833,15 @@ mpt.name = "test-element-1002";
section("兼容行为");
await test("Object 静态方法与 RegExp 静态状态保持可用", () => {
- assertSame(
+ assert(
true,
Object.isFrozen(Object.freeze({})),
"Object.freeze 应可裸用",
);
const match = "abc123".match(/(\d+)/);
- assertSame("123", match && match[1], "RegExp match 应正常返回捕获组");
- assertSame("123", RegExp.$1, "RegExp.$1 应保留页面原生静态状态行为");
+ assert("123", match && match[1], "RegExp match 应正常返回捕获组");
+ assert("123", RegExp.$1, "RegExp.$1 应保留页面原生静态状态行为");
});
await test("Symbol 属性只写入当前沙盒,不影响页面 window", () =>
@@ -903,12 +849,12 @@ mpt.name = "test-element-1002";
() => {
const symbolKey = Symbol(`${markerPrefix}_symbol`);
window[symbolKey] = "sandbox-symbol";
- assertSame(
+ assert(
"sandbox-symbol",
window[symbolKey],
"沙盒应允许 Symbol 属性",
);
- assertSame(
+ assert(
undefined,
unsafeWindow[symbolKey],
"Symbol 属性不应写入页面 window",
@@ -925,32 +871,95 @@ mpt.name = "test-element-1002";
await test("eval 保持可用,并在当前沙盒内解析全局", () => {
const key = `${markerPrefix}_eval`;
eval(`window["${key}"] = "from-eval";`);
- assertSame("from-eval", window[key], "eval 应能写入沙盒 window");
- assertSame(undefined, unsafeWindow[key], "eval 写入不应穿透页面 window");
+ assert("from-eval", window[key], "eval 应能写入沙盒 window");
+ assert(undefined, unsafeWindow[key], "eval 写入不应穿透页面 window");
delete window[key];
});
}
- 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;",
- );
+ printSummary();
+})((() => {
+ const testResults = {
+ passed: 0,
+ failed: 0,
+ total: 0,
+ };
- if (testResults.failed === 0) {
+ function formatValue(value) {
+ if (value === window) return "[sandbox window]";
+ if (value === unsafeWindow) return "[unsafeWindow]";
+ if (value === document) return "[document]";
+ if (value && value.nodeType) return `[node ${value.nodeName}]`;
+ if (typeof value === "function")
+ return `[function ${value.name || "anonymous"}]`;
+ if (typeof value === "symbol") return value.toString();
+ try {
+ return JSON.stringify(value);
+ } catch {
+ return String(value);
+ }
+ }
+
+ function assert(expected, actual, message) {
+ if (!Object.is(expected, actual)) {
+ throw new Error(
+ `${message} - 期望 ${formatValue(expected)}, 实际 ${formatValue(actual)}`,
+ );
+ }
+ }
+
+ function assertNotSame(unexpected, actual, message) {
+ if (Object.is(unexpected, actual)) {
+ throw new Error(`${message} - 不应等于 ${formatValue(unexpected)}`);
+ }
+ }
+
+ function assertTrue(condition, message) {
+ if (!condition) throw new Error(message || "断言失败: 条件不为真");
+ }
+
+ function section(name) {
+ console.log(`\n%c--- ${name} ---`, "color: orange; font-weight: bold;");
+ }
+
+ async function test(name, fn) {
+ testResults.total++;
+ try {
+ await fn();
+ testResults.passed++;
+ console.log(`%cPASS ${name}`, "color: green;");
+ return true;
+ } catch (error) {
+ testResults.failed++;
+ console.error(`%cFAIL ${name}`, "color: red;", error);
+ return false;
+ }
+ }
+
+ function printSummary() {
console.log(
- "%c所有测试通过",
- "color: green; font-size: 14px; font-weight: bold;",
+ "\n%c=== 测试完成 ===",
+ "color: blue; font-size: 16px; font-weight: bold;",
);
- } else {
console.log(
- "%c部分测试失败,请检查上面的错误信息",
- "color: red; font-size: 14px; font-weight: bold;",
+ `%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;",
+ );
+ }
}
-})();
+
+ return { test, assert, assertNotSame, assertTrue, section, printSummary };
+})());
diff --git a/example/tests/unwrap_e2e_test.js b/example/tests/unwrap_e2e_test.js
index 680bce1b8..66cff7a15 100644
--- a/example/tests/unwrap_e2e_test.js
+++ b/example/tests/unwrap_e2e_test.js
@@ -11,7 +11,28 @@
var __unwrap_e2e_global_var = "unwrap_success";
-(function () {
+(async ({ test, assert, printSummary }) => {
+ // ============ @unwrap 测试 ============
+ console.log("%c=== @unwrap E2E 测试开始 ===", "color: blue; font-size: 16px; font-weight: bold;");
+
+ // 测试1: GM API 在 unwrap 模式下为 undefined
+ test("GM 对象在 unwrap 模式下为 undefined", function () {
+ assert("undefined", typeof GM, "GM 应为 undefined");
+ });
+
+ test("GM_setValue 在 unwrap 模式下为 undefined", function () {
+ assert("undefined", typeof GM_setValue, "GM_setValue 应为 undefined");
+ });
+
+ // 测试2: 脚本代码在页面全局作用域执行
+ test("全局变量可在页面作用域访问", function () {
+ assert("unwrap_success", window.__unwrap_e2e_global_var, "全局变量应可访问");
+ });
+
+ // ============ 测试总结 ============
+ printSummary();
+})((() => {
+ // 跟测试对象无关的基础设施:计数器、test/assert 断言函数、结果汇总打印。
"use strict";
let testResults = {
@@ -42,26 +63,12 @@ var __unwrap_e2e_global_var = "unwrap_success";
}
}
- // ============ @unwrap 测试 ============
- console.log("%c=== @unwrap E2E 测试开始 ===", "color: blue; font-size: 16px; font-weight: bold;");
-
- // 测试1: GM API 在 unwrap 模式下为 undefined
- test("GM 对象在 unwrap 模式下为 undefined", function () {
- assert("undefined", typeof GM, "GM 应为 undefined");
- });
-
- test("GM_setValue 在 unwrap 模式下为 undefined", function () {
- assert("undefined", typeof GM_setValue, "GM_setValue 应为 undefined");
- });
-
- // 测试2: 脚本代码在页面全局作用域执行
- test("全局变量可在页面作用域访问", function () {
- assert("unwrap_success", window.__unwrap_e2e_global_var, "全局变量应可访问");
- });
+ function printSummary() {
+ console.log("\n%c=== 测试结果总结 ===", "color: blue; font-size: 16px; font-weight: bold;");
+ console.log("总测试数: " + testResults.total);
+ console.log("%c通过: " + testResults.passed, "color: green; font-weight: bold;");
+ console.log("%c失败: " + testResults.failed, "color: red; font-weight: bold;");
+ }
- // ============ 测试总结 ============
- console.log("\n%c=== 测试结果总结 ===", "color: blue; font-size: 16px; font-weight: bold;");
- console.log("总测试数: " + testResults.total);
- console.log("%c通过: " + testResults.passed, "color: green; font-weight: bold;");
- console.log("%c失败: " + testResults.failed, "color: red; font-weight: bold;");
-})();
+ return { test, assert, printSummary };
+})());
diff --git a/example/tests/unwrap_test.js b/example/tests/unwrap_test.js
index 87f82e676..4c9337a82 100644
--- a/example/tests/unwrap_test.js
+++ b/example/tests/unwrap_test.js
@@ -17,7 +17,7 @@
var test_global_injection = "success";
// User can access the variable "test_global_injection" directly in DevTools
-(function () {
+(async ({ printSummary }) => {
const results = {
GM: {
expected: "undefined",
@@ -33,34 +33,41 @@ var test_global_injection = "success";
},
};
- console.group(
- "%c@unwrap Test",
- "color:#0aa;font-weight:bold"
- );
+ printSummary(results);
+})((() => {
+ // 跟测试对象无关的基础设施:控制台分组/表格输出与汇总打印。
+ function printSummary(results) {
+ console.group(
+ "%c@unwrap Test",
+ "color:#0aa;font-weight:bold"
+ );
- const table = {};
- let allPass = true;
+ const table = {};
+ let allPass = true;
- for (const key in results) {
- const { expected, actual } = results[key];
- const pass = expected === actual;
- allPass &&= pass;
+ for (const key in results) {
+ const { expected, actual } = results[key];
+ const pass = expected === actual;
+ allPass &&= pass;
- table[key] = {
- Expected: expected,
- Actual: actual,
- Result: pass ? "✅ PASS" : "❌ FAIL",
- };
- }
+ table[key] = {
+ Expected: expected,
+ Actual: actual,
+ Result: pass ? "✅ PASS" : "❌ FAIL",
+ };
+ }
+
+ console.table(table);
- console.table(table);
+ console.log(
+ allPass
+ ? "%cAll tests passed ✔"
+ : "%cSome tests failed ✘",
+ `font-weight:bold;color:${allPass ? "green" : "red"}`
+ );
- console.log(
- allPass
- ? "%cAll tests passed ✔"
- : "%cSome tests failed ✘",
- `font-weight:bold;color:${allPass ? "green" : "red"}`
- );
+ console.groupEnd();
+ }
- console.groupEnd();
-})();
+ return { printSummary };
+})());
diff --git a/example/tests/window_message_test.js b/example/tests/window_message_test.js
index 444e21179..00c0577f1 100644
--- a/example/tests/window_message_test.js
+++ b/example/tests/window_message_test.js
@@ -14,15 +14,7 @@
// @noframes
// ==/UserScript==
-(async function () {
- "use strict";
-
- const results = {
- passed: 0,
- failed: 0,
- total: 0,
- };
-
+(async ({ section, assert, assertTrue, withTimeout, test, printSummary }) => {
console.log(
"%c=== WindowMessage transport test start ===",
"color: blue; font-size: 16px; font-weight: bold;",
@@ -31,53 +23,13 @@
"This userscript exercises the production WindowMessage route used by the sandbox/offscreen document. Run it on a URL ending with ?WINDOW_MESSAGE_TEST_SC.",
);
- function section(name) {
- console.log(`\n%c--- ${name} ---`, "color: orange; font-weight: bold;");
- }
-
- function assertSame(expected, actual, message) {
- if (!Object.is(expected, actual)) {
- throw new Error(
- `${message} - expected ${JSON.stringify(expected)}, got ${JSON.stringify(actual)}`,
- );
- }
- }
-
- function assertTrue(condition, message) {
- if (!condition) {
- throw new Error(message || "Assertion failed");
- }
- }
-
- function withTimeout(promise, label, ms = 10000) {
- let timer = null;
- const timeout = new Promise((_, reject) => {
- timer = setTimeout(() => reject(new Error(`${label} timed out after ${ms}ms`)), ms);
- });
- return Promise.race([promise, timeout]).finally(() => clearTimeout(timer));
- }
-
- async function test(name, fn) {
- results.total++;
- try {
- await fn();
- results.passed++;
- console.log(`%cPASS ${name}`, "color: green;");
- return true;
- } catch (error) {
- results.failed++;
- console.error(`%cFAIL ${name}`, "color: red;", error);
- return false;
- }
- }
-
section("Sandbox endpoint");
await test("default userscript runs in the sandbox window", () => {
- assertSame("object", typeof unsafeWindow, "unsafeWindow should be available");
+ assert("object", typeof unsafeWindow, "unsafeWindow should be available");
assertTrue(window !== unsafeWindow, "window should be the sandbox window, not the page window");
- assertSame(window, self, "self should point at the sandbox window");
- assertSame(window, globalThis, "globalThis should point at the sandbox window");
+ assert(window, self, "self should point at the sandbox window");
+ assert(window, globalThis, "globalThis should point at the sandbox window");
});
section("One-shot sendMessage path");
@@ -100,7 +52,7 @@
"GM.xmlHttpRequest",
);
- assertSame(200, response.status, "status should be 200");
+ assert(200, response.status, "status should be 200");
assertTrue(response.finalUrl.includes("httpbun.com/get"), "finalUrl should be populated");
assertTrue(typeof response.responseHeaders === "string", "responseHeaders should be a string");
assertTrue(response.response && typeof response.response === "object", "JSON response should be parsed");
@@ -110,7 +62,7 @@
response.response.query ||
response.response.params ||
{};
- assertSame(marker, args.marker, "query marker should round-trip through the response");
+ assert(marker, args.marker, "query marker should round-trip through the response");
});
await test("GM_xmlhttpRequest forwards readyState events over the connect channel", async () => {
@@ -132,7 +84,7 @@
"GM_xmlhttpRequest readyState",
);
- assertSame(200, response.status, "status should be 200");
+ assert(200, response.status, "status should be 200");
assertTrue(states.includes(4), "readyState DONE should be observed");
assertTrue(response.responseText.length > 0, "responseText should contain the payload");
});
@@ -148,8 +100,8 @@
ontimeout: reject,
onabort: (res) => {
try {
- assertSame(0, res.readyState, "aborted readyState should be UNSENT");
- assertSame(0, res.status, "aborted status should be 0");
+ assert(0, res.readyState, "aborted readyState should be UNSENT");
+ assert(0, res.status, "aborted status should be 0");
resolve();
} catch (error) {
reject(error);
@@ -163,14 +115,69 @@
);
});
- console.log(
- "\n%c=== WindowMessage transport test complete ===",
- "color: blue; font-size: 16px; font-weight: bold;",
- );
- console.log(
- `%cTotal: ${results.total} | Passed: ${results.passed} | Failed: ${results.failed}`,
- results.failed === 0
- ? "color: green; font-weight: bold;"
- : "color: red; font-weight: bold;",
- );
-})();
+ printSummary();
+})((() => {
+ // 跟测试对象无关的基础设施:计数器、分节/断言/超时包装函数、结果汇总打印。
+ "use strict";
+
+ const results = {
+ passed: 0,
+ failed: 0,
+ total: 0,
+ };
+
+ function section(name) {
+ console.log(`\n%c--- ${name} ---`, "color: orange; font-weight: bold;");
+ }
+
+ function assert(expected, actual, message) {
+ if (!Object.is(expected, actual)) {
+ throw new Error(
+ `${message} - expected ${JSON.stringify(expected)}, got ${JSON.stringify(actual)}`,
+ );
+ }
+ }
+
+ function assertTrue(condition, message) {
+ if (!condition) {
+ throw new Error(message || "Assertion failed");
+ }
+ }
+
+ function withTimeout(promise, label, ms = 10000) {
+ let timer = null;
+ const timeout = new Promise((_, reject) => {
+ timer = setTimeout(() => reject(new Error(`${label} timed out after ${ms}ms`)), ms);
+ });
+ return Promise.race([promise, timeout]).finally(() => clearTimeout(timer));
+ }
+
+ async function test(name, fn) {
+ results.total++;
+ try {
+ await fn();
+ results.passed++;
+ console.log(`%cPASS ${name}`, "color: green;");
+ return true;
+ } catch (error) {
+ results.failed++;
+ console.error(`%cFAIL ${name}`, "color: red;", error);
+ return false;
+ }
+ }
+
+ function printSummary() {
+ console.log(
+ "\n%c=== WindowMessage transport test complete ===",
+ "color: blue; font-size: 16px; font-weight: bold;",
+ );
+ console.log(
+ `%cTotal: ${results.total} | Passed: ${results.passed} | Failed: ${results.failed}`,
+ results.failed === 0
+ ? "color: green; font-weight: bold;"
+ : "color: red; font-weight: bold;",
+ );
+ }
+
+ return { section, assert, assertTrue, withTimeout, test, printSummary };
+})());