Skip to content

Commit 2e01683

Browse files
authored
Merge branch 'release/v1.4' into fix/isIncognito-value-in-early-start
2 parents 1ecf37f + 87eac92 commit 2e01683

88 files changed

Lines changed: 5850 additions & 1835 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/build.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ on:
55
branches:
66
- main
77
- release/*
8+
- feature/*
89
- dev
910
paths-ignore:
1011
- ".github/**"

.github/workflows/test.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ on:
55
branches:
66
- main
77
- release/*
8+
- feature/*
89
- dev
910
- develop/*
1011
pull_request:

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,3 +42,4 @@ test-results
4242
playwright-report
4343

4444
superpowers
45+
.omc

.husky/pre-commit

Lines changed: 30 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,35 @@
11
#!/bin/sh
22

3-
pnpm run lint-fix
3+
# Skip checks: SKIP_PRE_PUSH=1 git push or git push --no-verify
4+
if [ "$SKIP_PRE_PUSH" = "1" ]; then
5+
echo "⏭ SKIP_PRE_PUSH=1, skipping pre-push checks"
6+
exit 0
7+
fi
48

5-
# 将已暂存且被 lint-fix 修改过的文件重新加入暂存区
6-
STAGED=$(git diff --cached --name-only --diff-filter=d)
7-
CHANGED=$(git diff --name-only)
9+
# Only run checks when pushing to main or release/* branches
10+
remote="$1"
11+
need_check=0
812

9-
for file in $CHANGED; do
10-
case "$STAGED" in
11-
*"$file"*) git add "$file" ;;
12-
esac
13+
while read local_ref local_sha remote_ref remote_sha; do
14+
branch=$(echo "$remote_ref" | sed 's|refs/heads/||')
15+
16+
if [ "$branch" = "main" ] || echo "$branch" | grep -q "^release/"; then
17+
need_check=1
18+
echo "🔍 Detected push target: $branch"
19+
fi
1320
done
21+
22+
if [ "$need_check" = "0" ]; then
23+
exit 0
24+
fi
25+
26+
echo ""
27+
echo "▶ Running lint..."
28+
pnpm run lint || exit 1
29+
30+
echo ""
31+
echo "▶ Running tests..."
32+
pnpm run test:ci || exit 1
33+
34+
echo ""
35+
echo "✅ All checks passed! (build and e2e tests will run in CI)"

.husky/pre-push

Lines changed: 0 additions & 35 deletions
This file was deleted.

.prettierignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,7 @@ yarn.lock
55

66
# Claude Code
77
.claude
8+
9+
# Docs & examples
10+
*.md
11+
example/

e2e/gm-api.spec.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,4 +196,21 @@ test.describe("GM API", () => {
196196
expect(failed, "Some content inject tests failed").toBe(0);
197197
expect(passed, "No test results found - script may not have run").toBeGreaterThan(0);
198198
});
199+
200+
test("Unwrap scriptlet tests (unwrap_e2e_test.js)", async ({ context, extensionId }) => {
201+
const { passed, failed, logs } = await runTestScript(
202+
context,
203+
extensionId,
204+
"unwrap_e2e_test.js",
205+
TARGET_URL,
206+
60_000
207+
);
208+
209+
console.log(`[unwrap_e2e_test] passed=${passed}, failed=${failed}`);
210+
if (failed !== 0) {
211+
console.log("[unwrap_e2e_test] logs:", logs.join("\n"));
212+
}
213+
expect(failed, "Some unwrap scriptlet tests failed").toBe(0);
214+
expect(passed, "No test results found - script may not have run").toBeGreaterThan(0);
215+
});
199216
});

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 Tool 卡片是页面上第二个 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 tool|/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+
});

0 commit comments

Comments
 (0)