Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
86 changes: 83 additions & 3 deletions e2e/gm-api.spec.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,19 @@
import fs from "fs";
import path from "path";
import os from "os";
import { createServer } from "http";
import type { AddressInfo } from "net";
import { test as base, expect, chromium, type BrowserContext } from "@playwright/test";
import { installScriptByCode } from "./utils";

const HTTPBUN_GET_URL = "https://httpbun.com/get";
const MOCK_CONNECT_HOST = "127.0.0.1";

type GMApiMockServer = {
origin: string;
close: () => Promise<void>;
};

const test = base.extend<{
context: BrowserContext;
extensionId: string;
Expand Down Expand Up @@ -69,6 +79,58 @@ function patchScriptCode(code: string): string {
.replace(/https:\/\/cdn\.jsdelivr\.net\/npm\//g, "https://unpkg.com/");
}

async function startGMApiMockServer(): Promise<GMApiMockServer> {
const server = createServer((req, res) => {
res.setHeader("Access-Control-Allow-Origin", "*");

if (req.method === "OPTIONS") {
res.writeHead(204);
res.end();
return;
}

if (req.url === "/get") {
res.writeHead(200, { "Content-Type": "application/json" });
res.end(
JSON.stringify({
url: `http://${req.headers.host}${req.url}`,
})
);
return;
}

res.writeHead(404, { "Content-Type": "text/plain" });
res.end("not found");
});

await new Promise<void>((resolve, reject) => {
const onError = (error: Error) => reject(error);
server.once("error", onError);
server.listen(0, MOCK_CONNECT_HOST, () => {
server.off("error", onError);
resolve();
});
});

const address = server.address() as AddressInfo;
return {
origin: `http://${MOCK_CONNECT_HOST}:${address.port}`,
close: () =>
new Promise<void>((resolve, reject) => {
server.close((error) => {
if (error) reject(error);
else resolve();
});
}),
};
}

function patchGMApiTestCode(code: string, mockOrigin: string): string {
return code
.replace(/^\/\/\s*@connect\s+httpbun\.com$/gm, `// @connect ${MOCK_CONNECT_HOST}`)
.replace(new RegExp(HTTPBUN_GET_URL.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), "g"), `${mockOrigin}/get`);
}

/**
* Auto-approve permission confirm dialogs opened by the extension.
* Listens for new pages matching confirm.html and clicks the
Expand Down Expand Up @@ -110,10 +172,12 @@ async function runTestScript(
extensionId: string,
scriptFile: string,
targetUrl: string,
timeoutMs: number
timeoutMs: number,
options?: { patchCode?: (code: string) => string }
): Promise<{ passed: number; failed: number; logs: string[] }> {
let code = fs.readFileSync(path.join(__dirname, `../example/tests/${scriptFile}`), "utf-8");
code = patchScriptCode(code);
code = options?.patchCode ? options.patchCode(code) : code;

await installScriptByCode(context, extensionId, code);

Expand Down Expand Up @@ -149,6 +213,20 @@ async function runTestScript(
const TARGET_URL = "https://content-security-policy.com/";

test.describe("GM API", () => {
let gmApiMockServer: GMApiMockServer;

test.beforeAll(async () => {
gmApiMockServer = await startGMApiMockServer();
});

test.afterAll(async () => {
await gmApiMockServer.close();
});

function patchCode(code: string): string {
return patchGMApiTestCode(code, gmApiMockServer.origin);
}

// Two-phase launch + script install + network fetches + permission dialogs
test.setTimeout(300_000);

Expand All @@ -158,7 +236,8 @@ test.describe("GM API", () => {
extensionId,
"gm_api_sync_test.js",
`${TARGET_URL}?gm_api_sync`,
90_000
90_000,
{ patchCode }
);

console.log(`[gm_api_sync_test] passed=${passed}, failed=${failed}`);
Expand All @@ -175,7 +254,8 @@ test.describe("GM API", () => {
extensionId,
"gm_api_async_test.js",
`${TARGET_URL}?gm_api_async`,
90_000
90_000,
{ patchCode }
);

console.log(`[gm_api_async_test] passed=${passed}, failed=${failed}`);
Expand Down
13 changes: 7 additions & 6 deletions example/tests/gm_api_async_test.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// ==UserScript==
// @name GM.* API 完整测试 (异步版本)
// @namespace https://docs.scriptcat.org/
// @version 1.0.0
// @version 1.0.1
// @description 全面测试ScriptCat的所有GM.* (异步Promise版本) API功能
// @author ScriptCat
// @match https://content-security-policy.com/?gm_api_async
Expand All @@ -24,7 +24,7 @@
// @grant unsafeWindow
// @require https://cdn.jsdelivr.net/npm/jquery@3.6.0/dist/jquery.min.js#sha384-vtXRMe3mGCbOeY7l30aIg8H9p3GdeSe4IFlP6G8JMa7o7lXvnz3GFKzPxzJdPfGK
// @resource testCSS https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css#sha256=62f74b1cf824a89f03554c638e719594c309b4d8a627a758928c0516fa7890ab
// @connect api.github.com
// @connect httpbun.com
// @connect example.com
// @run-at document-start
// ==/UserScript==
Expand Down Expand Up @@ -192,15 +192,16 @@
return new Promise((resolve, reject) => {
GM.xmlHttpRequest({
method: "GET",
url: "https://api.github.com/repos/scriptscat/scriptcat",
url: "https://httpbun.com/get",
timeout: 10000,
onload: (response) => {
try {
assert(200, response.status, `请求状态码应该是 200`);
assert(true, !!response.responseText, "响应内容不应为空");
const data = JSON.parse(response.responseText);
assert("scriptcat", data.name, "应该返回 scriptcat 仓库信息");
console.log("GitHub 仓库信息:", data.name, data.description);
assert("object", typeof data, "应该返回有效的 JSON 对象");
assert("https://httpbun.com/get", data.url, "响应应该包含 url 字段");
console.log("httpbun 响应信息:", data.url);
resolve();
} catch (error) {
reject(error);
Expand All @@ -219,7 +220,7 @@
await testAsync("GM.xmlHttpRequest - 返回控制对象", async () => {
const controller = GM.xmlHttpRequest({
method: "GET",
url: "https://api.github.com/repos/scriptscat/scriptcat",
url: "https://httpbun.com/get",
timeout: 10000,
onload: () => {},
onerror: () => {},
Expand Down
11 changes: 6 additions & 5 deletions example/tests/gm_api_sync_test.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// ==UserScript==
// @name GM API 完整测试 (同步版本)
// @namespace https://docs.scriptcat.org/
// @version 1.1.0
// @version 1.1.1
// @description 全面测试ScriptCat的所有GM API功能
// @author ScriptCat
// @match https://content-security-policy.com/?gm_api_sync
Expand All @@ -28,7 +28,7 @@
// @grant unsafeWindow
// @require https://cdn.jsdelivr.net/npm/jquery@3.6.0/dist/jquery.min.js#sha384-vtXRMe3mGCbOeY7l30aIg8H9p3GdeSe4IFlP6G8JMa7o7lXvnz3GFKzPxzJdPfGK
// @resource testCSS https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css#sha256=62f74b1cf824a89f03554c638e719594c309b4d8a627a758928c0516fa7890ab
// @connect api.github.com
// @connect httpbun.com
// @connect example.com
// @run-at document-start
// ==/UserScript==
Expand Down Expand Up @@ -308,15 +308,16 @@
return new Promise((resolve, reject) => {
GM_xmlhttpRequest({
method: "GET",
url: "https://api.github.com/repos/scriptscat/scriptcat",
url: "https://httpbun.com/get",
timeout: 10000,
onload: (response) => {
try {
assert(200, response.status, `请求状态码应该是 200`);
assert(true, !!response.responseText, "响应内容不应为空");
const data = JSON.parse(response.responseText);
assert("scriptcat", data.name, "应该返回 scriptcat 仓库信息");
console.log("GitHub 仓库信息:", data.name, data.description);
assert("object", typeof data, "应该返回有效的 JSON 对象");
assert("https://httpbun.com/get", data.url, "响应应该包含 url 字段");
console.log("httpbun 响应信息:", data.url);
resolve();
} catch (error) {
reject(error);
Expand Down
Loading