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
20 changes: 1 addition & 19 deletions e2e/agent-navigation.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { test, expect } from "./fixtures";
import { openAgentChatPage, openAgentProviderPage, openOptionsPage } from "./utils";
import { openOptionsPage } from "./utils";

// new-ui 侧边栏 Agent 子菜单(shadcn):折叠态切换按钮 data-testid="nav-agent",
// 展开后子项为 NavLink(a[href="#/agent/..."]),容器 data-testid="sidebar-agent-submenu"。
Expand Down Expand Up @@ -27,22 +27,4 @@ test.describe("Agent 导航", () => {
await expect(page).toHaveURL(/#\/agent\/provider/);
await page.close();
});

test("应能直接加载 Agent 会话页", async ({ context, extensionId }) => {
const page = await openAgentChatPage(context, extensionId);
await expect(page.getByTestId("conv-new")).toBeVisible({ timeout: 10_000 });
await page.close();
});

test("应能直接加载 Agent 模型服务页", async ({ context, extensionId }) => {
const page = await openAgentProviderPage(context, extensionId);
await expect(page.getByTestId("model-add")).toBeVisible({ timeout: 10_000 });
await page.close();
});

test("未配置模型时模型服务页应显示空状态", async ({ context, extensionId }) => {
const page = await openAgentProviderPage(context, extensionId);
await expect(page.getByTestId("empty-state")).toBeVisible({ timeout: 10_000 });
await page.close();
});
});
57 changes: 52 additions & 5 deletions e2e/backup-zip.spec.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import fs from "fs";
import type { BrowserContext, Page } from "@playwright/test";
import { test, expect } from "./fixtures";
import { testWithUserScripts as test, expect } from "./fixtures";
import { installScriptByCode, openOptionsPage } from "./utils";

/**
Expand All @@ -18,18 +18,43 @@ import { installScriptByCode, openOptionsPage } from "./utils";
*/

const SCRIPT_NAME = "Backup RoundTrip E2E";
const TARGET_ORIGIN = "http://backup-roundtrip.test";
const script = `// ==UserScript==
// @name ${SCRIPT_NAME}
// @namespace https://e2e.test
// @version 1.0.0
// @description backup zip e2e
// @author E2E
// @match http://example.com/*
// @grant none
// @match ${TARGET_ORIGIN}/*
// @grant GM_getValue
// @grant GM_setValue
// ==/UserScript==
(function(){})();

document.documentElement.setAttribute("data-restored-value", GM_getValue("backup-key", "missing"));
GM_setValue("backup-key", "value-from-backup");
`;

async function purgeInstalledScript(page: Page): Promise<void> {
const result = await page.evaluate(async (name) => {
const all = await chrome.runtime.sendMessage({ action: "serviceWorker/script/getAllScripts" });
const script = all.data.find((item: { name: string }) => item.name === name) as { uuid: string } | undefined;
if (!script) throw new Error("导出后未找到待清理脚本");
const deleted = await chrome.runtime.sendMessage({
action: "serviceWorker/script/deletes",
data: [script.uuid],
});
const purged = await chrome.runtime.sendMessage({
action: "serviceWorker/script/purges",
data: [script.uuid],
});
return { deleted, purged };
}, SCRIPT_NAME);
expect(result.deleted.code || 0, result.deleted.message).toBe(0);
expect(result.deleted.data).toBe(true);
expect(result.purged.code || 0, result.purged.message).toBe(0);
expect(result.purged.data).toBe(true);
}

/** 轮询 SW 的 chrome.downloads,拿到刚导出的 blob 备份文件在磁盘上的路径 */
async function waitForExportedZipPath(context: BrowserContext, timeoutMs = 15_000): Promise<string> {
const sw = context.serviceWorkers()[0];
Expand Down Expand Up @@ -57,9 +82,17 @@ async function waitForExportedZipPath(context: BrowserContext, timeoutMs = 15_00
}

test.describe("Backup zip export/import round-trip (#1479)", () => {
test("导出备份生成合法 zip,再导入能被 loadAsyncJSZip 解析并列出脚本", async ({ context, extensionId }) => {
test("导出后彻底删除原脚本,真正导入应恢复脚本与 GM Value", async ({ context, extensionId }) => {
await context.route(`${TARGET_ORIGIN}/**`, (route) =>
route.fulfill({ status: 200, contentType: "text/html", body: "<!doctype html><html><body></body></html>" })
);
await installScriptByCode(context, extensionId, script);

const seedPage = await context.newPage();
await seedPage.goto(`${TARGET_ORIGIN}/seed`, { waitUntil: "domcontentloaded" });
await expect(seedPage.locator("html")).toHaveAttribute("data-restored-value", "missing", { timeout: 20_000 });
await seedPage.close();

const page = await openOptionsPage(context, extensionId);
await page.goto(`chrome-extension://${extensionId}/src/options.html#/tools`);
await page.waitForLoadState("domcontentloaded");
Expand All @@ -76,6 +109,9 @@ test.describe("Backup zip export/import round-trip (#1479)", () => {
expect(buf[0]).toBe(0x50); // 'P'
expect(buf[1]).toBe(0x4b); // 'K'

// 导入前彻底删除原脚本和共享值,避免导入页只显示预览也造成假阳性。
await purgeInstalledScript(page);

// 3) 导入:点击“导入文件”→ 触发隐藏 file input 的 click(filechooser),选中刚导出的 zip,
// openImportWindow 会把文件写入 cache 并经扩展 API 新开导入页标签,从该标签 URL 取 uuid。
// 注意:扩展首启且 userScripts 未开启时会异步用 chrome.tabs.create 打开「开启开发者模式」
Expand Down Expand Up @@ -109,6 +145,17 @@ test.describe("Backup zip export/import round-trip (#1479)", () => {
await importPage.waitForLoadState("domcontentloaded");
await expect(importPage.locator("body")).toContainText(SCRIPT_NAME, { timeout: 15_000 });

await expect(importPage.getByTestId("import-btn")).toBeEnabled({ timeout: 10_000 });
await importPage.getByTestId("import-btn").click();
await expect(importPage.getByTestId("import-complete")).toBeVisible({ timeout: 30_000 });

const restoredPage = await context.newPage();
await restoredPage.goto(`${TARGET_ORIGIN}/restored`, { waitUntil: "domcontentloaded" });
await expect(restoredPage.locator("html")).toHaveAttribute("data-restored-value", "value-from-backup", {
timeout: 20_000,
});

await restoredPage.close();
await importPage.close();
await page.close();
});
Expand Down
30 changes: 29 additions & 1 deletion e2e/gm-api.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -212,7 +212,7 @@ function patchTargetMatchCode(code: string, targetUrl: string): string {
const url = new URL(targetUrl);
const targetPattern = `${url.protocol}//${url.hostname}/*${url.search}`;
return code.replace(
/^\/\/\s*@match\s+.*\?(gm_api_sync|gm_api_async|inject_content|WINDOW_MESSAGE_TEST_SC|SANDBOX_TEST_SC|unwrap_e2e_test)$/gm,
/^\/\/\s*@match\s+.*\?(gm_api_sync|gm_api_async|inject_content|early_inject_content|early_inject_page|WINDOW_MESSAGE_TEST_SC|SANDBOX_TEST_SC|unwrap_e2e_test)$/gm,
`// @match ${targetPattern}`
);
}
Expand Down Expand Up @@ -358,6 +358,34 @@ test.describe("GM API", () => {
expect(passed, "No test results found - script may not have run").toBeGreaterThan(0);
});

test("@early-start page world 脚本应在 CSP 页面的解析早期执行", async ({ context, extensionId }) => {
const { passed, failed, logs } = await runTestScript(
context,
extensionId,
"early_inject_page_test.js",
`${gmApiMockServer.cspOrigin}/?early_inject_page`,
60_000
);

if (failed !== 0) console.log("[early_inject_page_test] logs:", logs.join("\n"));
expect(failed, "Some early page-world injection tests failed").toBe(0);
expect(passed, "No early page-world results found - script may not have run").toBeGreaterThan(0);
});

test("@early-start content world 脚本应在 CSP 页面的解析早期执行", async ({ context, extensionId }) => {
const { passed, failed, logs } = await runTestScript(
context,
extensionId,
"early_inject_content_test.js",
`${gmApiMockServer.cspOrigin}/?early_inject_content`,
60_000
);

if (failed !== 0) console.log("[early_inject_content_test] logs:", logs.join("\n"));
expect(failed, "Some early content-world injection tests failed").toBe(0);
expect(passed, "No early content-world results found - script may not have run").toBeGreaterThan(0);
});

test("Unwrap scriptlet tests (unwrap_e2e_test.js)", async ({ context, extensionId }) => {
const { passed, failed, logs } = await runTestScript(
context,
Expand Down
19 changes: 2 additions & 17 deletions e2e/gm-xhr-site-access.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,23 +116,6 @@ test.describe("GM_xmlhttpRequest site-access & DNR marker (#1477)", () => {
await server.close();
});

test("声明 @connect 的跨域 GET 请求成功", async ({ context, extensionId }) => {
const code = xhrScript({
name: "XHR connect ok",
matchHost: "sitea.test",
connect: ["xhrtarget.test"],
url: server.url("xhrtarget.test", "/xhr"),
port: server.port,
});
await installScriptByCode(context, extensionId, code);

const { data, logs } = await runXhr(context, server.url("sitea.test", "/page"));
expect(data.phase, `期望 load,实际: ${JSON.stringify(data)}\n${logs.join("\n")}`).toBe("load");
expect(data.status).toBe(200);
expect(data.ok).toBe(true);
expect(data.method).toBe("GET");
});

test("自定义请求头透传到服务器,x-sc-request-marker 标记头被 DNR 剥离", async ({ context, extensionId }) => {
const code = xhrScript({
name: "XHR header marker",
Expand All @@ -147,6 +130,8 @@ test.describe("GM_xmlhttpRequest site-access & DNR marker (#1477)", () => {
const { data } = await runXhr(context, server.url("sitea.test", "/page"));
expect(data.phase).toBe("load");
expect(data.status).toBe(200);
expect(data.ok).toBe(true);
expect(data.method).toBe("GET");

// 以服务器实际收到的请求为准(DNR 在网络层剥离标记头)
const xhrReq = server.requestLog.find((r) => r.pathname === "/xhr");
Expand Down
94 changes: 56 additions & 38 deletions e2e/install.spec.ts
Original file line number Diff line number Diff line change
@@ -1,54 +1,72 @@
import type { BrowserContext } from "@playwright/test";
import { test, expect } from "./fixtures";

// new-ui 安装页(shadcn,data-testid 丰富):?url= 触发页面级 fetch 拉取脚本,
// 用 page.route 拦截避免真实网络抖动;脚本元信息渲染后 content-area 可见、
// 主操作按钮(install-primary)可用、脚本名出现在 h1。
const SCRIPT_URL = "https://e2e.test/install-test.user.js";
const SCRIPT_NAME = "E2E Install Test";
const SCRIPT_BODY = `// ==UserScript==
import type { BrowserContext, Page } from "@playwright/test";
import { testWithUserScripts as test, expect } from "./fixtures";

const SCRIPT_URL = "https://e2e.test/install-update.user.js";
const TARGET_ORIGIN = "http://install-update.test";
const SCRIPT_NAME = "E2E Install Update";

function scriptBody(version: string): string {
return `// ==UserScript==
// @name ${SCRIPT_NAME}
// @namespace https://e2e.test
// @version 1.0.0
// @description install page e2e
// @author E2E
// @match https://example.com/*
// @namespace https://e2e.test/install-update
// @version ${version}
// @description install and update e2e
// @match ${TARGET_ORIGIN}/*
// @updateURL ${SCRIPT_URL}
// @downloadURL ${SCRIPT_URL}
// @grant none
// ==/UserScript==

console.log("install e2e");
document.documentElement.setAttribute("data-install-update-version", ${JSON.stringify(version)});
`;
}

async function openMockedInstallPage(context: BrowserContext, extensionId: string) {
async function openInstallPage(context: BrowserContext, extensionId: string): Promise<Page> {
const page = await context.newPage();
// 必须在 goto 之前注册路由,安装页挂载即发起 fetch
await page.route("**/install-test.user.js", (route) =>
route.fulfill({
status: 200,
headers: { "Content-Type": "application/javascript; charset=utf-8" },
body: SCRIPT_BODY,
})
);
// 安装页按原始子串读取 url=(location.search.slice),不做 decode,故此处不能 encodeURIComponent
await page.goto(`chrome-extension://${extensionId}/src/install.html?url=${SCRIPT_URL}`, {
waitUntil: "domcontentloaded",
});
await expect(page.getByText(SCRIPT_NAME).first()).toBeVisible({ timeout: 15_000 });
await expect(page.getByTestId("install-primary")).toBeEnabled({ timeout: 10_000 });
return page;
}

test.describe("Install 安装页", () => {
test("应通过 URL 参数打开安装页且标题为 ScriptCat", async ({ context, extensionId }) => {
const page = await openMockedInstallPage(context, extensionId);
await expect(page).toHaveTitle(/ScriptCat/i);
});
async function installFromPage(page: Page): Promise<void> {
await Promise.all([page.waitForEvent("close", { timeout: 10_000 }), page.getByTestId("install-primary").click()]);
}

async function expectExecutedVersion(context: BrowserContext, version: string): Promise<void> {
const page = await context.newPage();
try {
await page.goto(`${TARGET_ORIGIN}/?version=${version}`, { waitUntil: "domcontentloaded" });
await expect(page.locator("html")).toHaveAttribute("data-install-update-version", version, { timeout: 20_000 });
} finally {
await page.close();
}
}

test.describe("安装与更新真实执行链路", () => {
test("从安装页安装 v1 后应执行,并可从同一来源更新到 v2", async ({ context, extensionId }) => {
let version = "1.0.0";
await context.route(SCRIPT_URL, (route) =>
route.fulfill({
status: 200,
contentType: "application/javascript; charset=utf-8",
body: scriptBody(version),
})
);
await context.route(`${TARGET_ORIGIN}/**`, (route) =>
route.fulfill({ status: 200, contentType: "text/html", body: "<!doctype html><html><body></body></html>" })
);

test("加载脚本后应展示脚本元信息并可安装", async ({ context, extensionId }) => {
const page = await openMockedInstallPage(context, extensionId);
await installFromPage(await openInstallPage(context, extensionId));
await expectExecutedVersion(context, "1.0.0");

// 脚本名渲染(元信息加载完成的可见信号)
await expect(page.getByText(SCRIPT_NAME).first()).toBeVisible({ timeout: 15_000 });
// 内容区已填充
await expect(page.getByTestId("content-area")).toBeVisible({ timeout: 10_000 });
// 主操作按钮可用(非 disabled)
await expect(page.getByTestId("install-primary")).toBeEnabled({ timeout: 10_000 });
version = "2.0.0";
const updatePage = await openInstallPage(context, extensionId);
await expect(updatePage.getByTestId("version-old")).toHaveText("v1.0.0");
await expect(updatePage.getByTestId("version-new")).toHaveText("v2.0.0");
await installFromPage(updatePage);
await expectExecutedVersion(context, "2.0.0");
});
});
19 changes: 0 additions & 19 deletions e2e/script-editor.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,25 +4,6 @@ import { openEditorPage, openOptionsPage, saveCurrentEditor } from "./utils";
// new-ui 脚本编辑器:路由 #/script/editor 加载空白模板(normal.tpl,含 ==UserScript==);
// Monaco 选择器(.monaco-editor/.view-lines) 为框架级不变;保存成功为 sonner toast。
test.describe("Script 编辑器", () => {
test("应加载编辑器页并渲染 Monaco", async ({ context, extensionId }) => {
const page = await openEditorPage(context, extensionId);
await expect(page.locator(".monaco-editor")).toBeVisible({ timeout: 10_000 });
});

test("应载入新建脚本模板", async ({ context, extensionId }) => {
const page = await openEditorPage(context, extensionId);
await expect(page.locator(".monaco-editor")).toBeVisible({ timeout: 10_000 });
await expect(page.locator(".view-lines")).toContainText("==UserScript==", { timeout: 10_000 });
});

test("应能成功保存脚本", async ({ context, extensionId }) => {
const page = await openEditorPage(context, extensionId);
await expect(page.locator(".monaco-editor")).toBeVisible({ timeout: 10_000 });
await expect(page.locator(".view-lines")).toContainText("==UserScript==", { timeout: 10_000 });

await saveCurrentEditor(context, extensionId, page);
});

test("保存后脚本应出现在列表中", async ({ context, extensionId }) => {
const editorPage = await openEditorPage(context, extensionId);
await expect(editorPage.locator(".monaco-editor")).toBeVisible({ timeout: 10_000 });
Expand Down
34 changes: 0 additions & 34 deletions e2e/settings.spec.ts

This file was deleted.

Loading
Loading