diff --git a/e2e/gm-api.spec.ts b/e2e/gm-api.spec.ts index 6eaad1a7f..908038167 100644 --- a/e2e/gm-api.spec.ts +++ b/e2e/gm-api.spec.ts @@ -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; +}; + const test = base.extend<{ context: BrowserContext; extensionId: string; @@ -69,6 +79,58 @@ function patchScriptCode(code: string): string { .replace(/https:\/\/cdn\.jsdelivr\.net\/npm\//g, "https://unpkg.com/"); } +async function startGMApiMockServer(): Promise { + 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((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((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 @@ -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); @@ -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); @@ -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}`); @@ -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}`); diff --git a/example/tests/gm_api_async_test.js b/example/tests/gm_api_async_test.js index 749d7b1fb..c8535935c 100644 --- a/example/tests/gm_api_async_test.js +++ b/example/tests/gm_api_async_test.js @@ -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 @@ -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== @@ -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); @@ -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: () => {}, diff --git a/example/tests/gm_api_sync_test.js b/example/tests/gm_api_sync_test.js index 0b268974c..7b6008ce2 100644 --- a/example/tests/gm_api_sync_test.js +++ b/example/tests/gm_api_sync_test.js @@ -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 @@ -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== @@ -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);