Skip to content

Commit f14c43b

Browse files
committed
Merge remote-tracking branch 'origin/fix/offscreen-sw-postmessage-blob' into feature/agent
# Conflicts: # src/app/service/offscreen/index.ts
2 parents 1b811c8 + 2e60aa6 commit f14c43b

33 files changed

Lines changed: 1774 additions & 160 deletions

e2e/gm-api.spec.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,4 +52,21 @@ testWithUserScripts.describe("GM API", () => {
5252
expect(failed, "Some content inject tests failed").toBe(0);
5353
expect(passed, "No test results found - script may not have run").toBeGreaterThan(0);
5454
});
55+
56+
test("Unwrap scriptlet tests (unwrap_e2e_test.js)", async ({ context, extensionId }) => {
57+
const { passed, failed, logs } = await runTestScript(
58+
context,
59+
extensionId,
60+
"unwrap_e2e_test.js",
61+
TARGET_URL,
62+
60_000
63+
);
64+
65+
console.log(`[unwrap_e2e_test] passed=${passed}, failed=${failed}`);
66+
if (failed !== 0) {
67+
console.log("[unwrap_e2e_test] logs:", logs.join("\n"));
68+
}
69+
expect(failed, "Some unwrap scriptlet tests failed").toBe(0);
70+
expect(passed, "No test results found - script may not have run").toBeGreaterThan(0);
71+
});
5572
});

e2e/vscode-connect.spec.ts

Lines changed: 224 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,224 @@
1+
import { test, expect } from "./fixtures";
2+
import { openOptionsPage } from "./utils";
3+
import type { Page } from "@playwright/test";
4+
import { WebSocketServer, type WebSocket } from "ws";
5+
6+
// ────────────────────────────────────────────────
7+
// 辅助函数
8+
// ────────────────────────────────────────────────
9+
10+
/** 打开 Tools 页面 */
11+
async function openToolsPage(context: Parameters<typeof openOptionsPage>[0], extensionId: string): Promise<Page> {
12+
const page = await openOptionsPage(context, extensionId);
13+
await page.goto(`chrome-extension://${extensionId}/src/options.html#/tools`);
14+
await page.waitForLoadState("domcontentloaded");
15+
return page;
16+
}
17+
18+
/** 获取「开发调试」卡片区域的定位器 */
19+
function getDevCard(page: Page) {
20+
// 开发调试 / Development Debugging 卡片是页面上第二个 Card
21+
return page.locator(".arco-card").nth(1);
22+
}
23+
24+
/** 启动一个临时 WebSocket 服务器,返回 URL 和清理函数 */
25+
function createMockWSServer(): Promise<{
26+
url: string;
27+
connections: WebSocket[];
28+
close: () => Promise<void>;
29+
/** 向所有已连接客户端发送消息 */
30+
broadcast: (data: unknown) => void;
31+
/** 等待收到指定 action 的消息 */
32+
waitForAction: (action: string, timeout?: number) => Promise<unknown>;
33+
}> {
34+
return new Promise((resolve, reject) => {
35+
const connections: WebSocket[] = [];
36+
const messageListeners: Array<(msg: unknown) => void> = [];
37+
38+
const wss = new WebSocketServer({ host: "127.0.0.1", port: 0 }, () => {
39+
const addr = wss.address();
40+
if (typeof addr === "string") {
41+
reject(new Error("Unexpected address type"));
42+
return;
43+
}
44+
const url = `ws://127.0.0.1:${addr.port}`;
45+
46+
wss.on("connection", (ws) => {
47+
connections.push(ws);
48+
ws.on("message", (raw) => {
49+
try {
50+
const msg = JSON.parse(raw.toString());
51+
for (const listener of messageListeners) {
52+
listener(msg);
53+
}
54+
} catch {
55+
// 忽略非 JSON 消息
56+
}
57+
});
58+
});
59+
60+
resolve({
61+
url,
62+
connections,
63+
close: () =>
64+
new Promise<void>((res) => {
65+
for (const ws of connections) ws.close();
66+
wss.close(() => res());
67+
}),
68+
broadcast: (data: unknown) => {
69+
const payload = JSON.stringify(data);
70+
for (const ws of connections) {
71+
if (ws.readyState === ws.OPEN) {
72+
ws.send(payload);
73+
}
74+
}
75+
},
76+
waitForAction: (action: string, timeout = 10_000) =>
77+
new Promise<unknown>((resolve, reject) => {
78+
const timer = setTimeout(() => {
79+
const idx = messageListeners.indexOf(handler);
80+
if (idx >= 0) messageListeners.splice(idx, 1);
81+
reject(new Error(`Timeout waiting for action: ${action}`));
82+
}, timeout);
83+
84+
const handler = (msg: any) => {
85+
if (msg.action === action) {
86+
clearTimeout(timer);
87+
const idx = messageListeners.indexOf(handler);
88+
if (idx >= 0) messageListeners.splice(idx, 1);
89+
resolve(msg);
90+
}
91+
};
92+
messageListeners.push(handler);
93+
}),
94+
});
95+
});
96+
});
97+
}
98+
99+
// ────────────────────────────────────────────────
100+
// 测试
101+
// ────────────────────────────────────────────────
102+
103+
test.describe("VSCode 连接", () => {
104+
test("Tools 页面应显示 VSCode 连接相关 UI 元素", async ({ context, extensionId }) => {
105+
const page = await openToolsPage(context, extensionId);
106+
const card = getDevCard(page);
107+
108+
// 卡片标题
109+
await expect(card.getByText(/development debugging|/i)).toBeVisible();
110+
111+
// VSCode URL 输入框
112+
const urlInput = card.locator(".arco-input");
113+
await expect(urlInput).toBeVisible();
114+
// 默认值应包含 ws://
115+
const value = await urlInput.inputValue();
116+
expect(value).toMatch(/^ws:\/\//);
117+
118+
// 自动连接复选框
119+
const checkbox = card.locator(".arco-checkbox");
120+
await expect(checkbox).toBeVisible();
121+
await expect(card.getByText(/auto connect vscode|vscode/i)).toBeVisible();
122+
123+
// 连接按钮
124+
const connectBtn = card.locator(".arco-btn-primary");
125+
await expect(connectBtn).toBeVisible();
126+
await expect(connectBtn.getByText(/connect|/i)).toBeVisible();
127+
});
128+
129+
test("应能修改 VSCode URL 和切换自动连接", async ({ context, extensionId }) => {
130+
const page = await openToolsPage(context, extensionId);
131+
const card = getDevCard(page);
132+
133+
// 修改 URL
134+
const urlInput = card.locator(".arco-input");
135+
await urlInput.clear();
136+
await urlInput.fill("ws://localhost:9999");
137+
await expect(urlInput).toHaveValue("ws://localhost:9999");
138+
139+
// 切换自动连接复选框
140+
const checkbox = card.locator(".arco-checkbox input");
141+
const initialChecked = await checkbox.isChecked();
142+
await card.locator(".arco-checkbox").click();
143+
const newChecked = await checkbox.isChecked();
144+
expect(newChecked).toBe(!initialChecked);
145+
});
146+
147+
test("点击连接按钮应发送连接命令", async ({ context, extensionId }) => {
148+
const page = await openToolsPage(context, extensionId);
149+
const card = getDevCard(page);
150+
151+
// 连接按钮存在且可点击
152+
const connectBtn = card.locator(".arco-btn-primary");
153+
await connectBtn.click();
154+
155+
// connectVSCode 是消息传递操作,消息投递成功即 resolve,
156+
// 所以即使没有 WebSocket 服务器运行,也应显示「连接成功」提示
157+
const successMsg = page.locator(".arco-message").getByText(/connection successful|/i);
158+
await expect(successMsg).toBeVisible({ timeout: 10_000 });
159+
});
160+
161+
test("应能通过 WebSocket 连接并接收脚本同步", async ({ context, extensionId }) => {
162+
// 启动 Mock WebSocket 服务器
163+
const server = await createMockWSServer();
164+
165+
try {
166+
const page = await openToolsPage(context, extensionId);
167+
const card = getDevCard(page);
168+
169+
// 设置 URL 为 Mock 服务器地址
170+
const urlInput = card.locator(".arco-input");
171+
await urlInput.clear();
172+
await urlInput.fill(server.url);
173+
174+
// 在点击连接之前就开始监听 hello 消息,避免竞态
175+
const helloPromise = server.waitForAction("hello", 30_000);
176+
177+
// 等待 offscreen 文档就绪(service worker 启动后异步创建)
178+
await page.waitForTimeout(2000);
179+
180+
// 点击连接
181+
const connectBtn = card.locator(".arco-btn-primary");
182+
await connectBtn.click();
183+
184+
// 等待「连接成功」消息
185+
const successMsg = page.locator(".arco-message").getByText(/connection successful|/i);
186+
await expect(successMsg).toBeVisible({ timeout: 10_000 });
187+
188+
// 等待收到 hello 握手消息
189+
await helloPromise;
190+
191+
// 验证客户端已连接
192+
expect(server.connections.length).toBeGreaterThanOrEqual(1);
193+
194+
// 发送 onchange 消息,模拟 VSCode 推送脚本
195+
const testScript = `// ==UserScript==
196+
// @name VSCode E2E Test Script
197+
// @namespace https://e2e.test/vscode
198+
// @version 1.0.0
199+
// @description Script synced from VSCode E2E test
200+
// @author E2E
201+
// @match https://example.com/*
202+
// ==/UserScript==
203+
204+
console.log("VSCode synced script");
205+
`;
206+
207+
server.broadcast({
208+
action: "onchange",
209+
data: {
210+
script: testScript,
211+
uri: "file:///e2e-test/vscode-sync-test.user.js",
212+
},
213+
});
214+
215+
// 验证脚本已安装:导航到脚本列表,检查脚本是否出现
216+
const listPage = await openOptionsPage(context, extensionId);
217+
const scriptItem = listPage.getByText("VSCode E2E Test Script");
218+
await expect(scriptItem).toBeVisible({ timeout: 15_000 });
219+
await listPage.close();
220+
} finally {
221+
await server.close();
222+
}
223+
});
224+
});

example/tests/unwrap_e2e_test.js

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
// ==UserScript==
2+
// @name Unwrap E2E Test
3+
// @namespace https://docs.scriptcat.org/
4+
// @version 1.0.0
5+
// @description E2E 测试 @unwrap 功能
6+
// @author ScriptCat
7+
// @match https://content-security-policy.com/*
8+
// @grant GM_setValue
9+
// @unwrap
10+
// ==/UserScript==
11+
12+
var __unwrap_e2e_global_var = "unwrap_success";
13+
14+
(function () {
15+
"use strict";
16+
17+
let testResults = {
18+
passed: 0,
19+
failed: 0,
20+
total: 0,
21+
};
22+
23+
function test(name, fn) {
24+
testResults.total++;
25+
try {
26+
fn();
27+
testResults.passed++;
28+
console.log("%c✓ " + name, "color: green;");
29+
return true;
30+
} catch (error) {
31+
testResults.failed++;
32+
console.error("%c✗ " + name, "color: red;", error);
33+
return false;
34+
}
35+
}
36+
37+
function assert(expected, actual, message) {
38+
if (expected !== actual) {
39+
var valueInfo = "期望 " + JSON.stringify(expected) + ", 实际 " + JSON.stringify(actual);
40+
var error = message ? message + " - " + valueInfo : "断言失败: " + valueInfo;
41+
throw new Error(error);
42+
}
43+
}
44+
45+
// ============ @unwrap 测试 ============
46+
console.log("%c=== @unwrap E2E 测试开始 ===", "color: blue; font-size: 16px; font-weight: bold;");
47+
48+
// 测试1: GM API 在 unwrap 模式下为 undefined
49+
test("GM 对象在 unwrap 模式下为 undefined", function () {
50+
assert("undefined", typeof GM, "GM 应为 undefined");
51+
});
52+
53+
test("GM_setValue 在 unwrap 模式下为 undefined", function () {
54+
assert("undefined", typeof GM_setValue, "GM_setValue 应为 undefined");
55+
});
56+
57+
// 测试2: 脚本代码在页面全局作用域执行
58+
test("全局变量可在页面作用域访问", function () {
59+
assert("unwrap_success", window.__unwrap_e2e_global_var, "全局变量应可访问");
60+
});
61+
62+
// ============ 测试总结 ============
63+
console.log("\n%c=== 测试结果总结 ===", "color: blue; font-size: 16px; font-weight: bold;");
64+
console.log("总测试数: " + testResults.total);
65+
console.log("%c通过: " + testResults.passed, "color: green; font-weight: bold;");
66+
console.log("%c失败: " + testResults.failed, "color: red; font-weight: bold;");
67+
})();

example/tests/unwrap_test.js

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
// ==UserScript==
2+
// @name A Scriptlet for @unwrap test
3+
// @namespace none
4+
// @version 2026-02-07
5+
// @description try to take over the world!
6+
// @author You
7+
// @match https://*/*?test_unwrap*
8+
// @exclude /test_\w+_excluded/
9+
// @grant GM_setValue
10+
// @require https://cdn.jsdelivr.net/npm/jquery@3.6.0/dist/jquery.min.js#sha384-vtXRMe3mGCbOeY7l30aIg8H9p3GdeSe4IFlP6G8JMa7o7lXvnz3GFKzPxzJdPfGK
11+
// @unwrap
12+
// ==/UserScript==
13+
14+
// include: https://example.com/?test_unwrap_123
15+
// exclude: https://example.com/?test_unwrap_excluded
16+
17+
var test_global_injection = "success";
18+
// User can access the variable "test_global_injection" directly in DevTools
19+
20+
(function () {
21+
const results = {
22+
GM: {
23+
expected: "undefined",
24+
actual: typeof GM,
25+
},
26+
GM_setValue: {
27+
expected: "undefined",
28+
actual: typeof GM_setValue,
29+
},
30+
jQuery: {
31+
expected: "function",
32+
actual: typeof jQuery,
33+
},
34+
};
35+
36+
console.group(
37+
"%c@unwrap Test",
38+
"color:#0aa;font-weight:bold"
39+
);
40+
41+
const table = {};
42+
let allPass = true;
43+
44+
for (const key in results) {
45+
const { expected, actual } = results[key];
46+
const pass = expected === actual;
47+
allPass &&= pass;
48+
49+
table[key] = {
50+
Expected: expected,
51+
Actual: actual,
52+
Result: pass ? "✅ PASS" : "❌ FAIL",
53+
};
54+
}
55+
56+
console.table(table);
57+
58+
console.log(
59+
allPass
60+
? "%cAll tests passed ✔"
61+
: "%cSome tests failed ✘",
62+
`font-weight:bold;color:${allPass ? "green" : "red"}`
63+
);
64+
65+
console.groupEnd();
66+
})();

0 commit comments

Comments
 (0)