Skip to content

Commit 3d23db2

Browse files
cyfung1031CodFrm
andauthored
♻️ VSCodeConnect 代码重构 (#1170)
* VSCodeConnect 代码重构 VSCodeConnect 代码重构 * AI 再重构 * 🔧 精简 VSCodeConnect 注释和代码整理 - 删除冗余的巨型文档注释和 Features 列表 - isReconnecting 属性移至属性声明区域 - 删除被注释掉的日志代码 - 统一注释为简体中文 * 🔧 移除冗余的 isReconnecting 锁,统一用 reconnectTimer 判断 * ✅ 添加 VSCodeConnect 单元测试和 E2E 测试 --------- Co-authored-by: 王一之 <yz@ggnb.top>
1 parent 283c8d4 commit 3d23db2

5 files changed

Lines changed: 880 additions & 78 deletions

File tree

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+
});

src/app/service/offscreen/client.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { type WindowMessage } from "@Packages/message/window_message";
22
import type { SCRIPT_RUN_STATUS, ScriptRunResource } from "@App/app/repo/scripts";
33
import { Client, sendMessage } from "@Packages/message/client";
44
import type { MessageSend } from "@Packages/message/types";
5-
import { type VSCodeConnect } from "./vscode-connect";
5+
import { type VSCodeConnectParam } from "./vscode-connect";
66

77
export function preparationSandbox(windowMessage: WindowMessage) {
88
return sendMessage(windowMessage, "offscreen/preparationSandbox");
@@ -42,7 +42,7 @@ export class VscodeConnectClient extends Client {
4242
super(msgSender, "offscreen/vscodeConnect");
4343
}
4444

45-
connect(params: Parameters<VSCodeConnect["connect"]>[0]): ReturnType<VSCodeConnect["connect"]> {
45+
connect(params: VSCodeConnectParam): Promise<void> {
4646
return this.do("connect", params);
4747
}
4848
}

0 commit comments

Comments
 (0)