Skip to content

Commit 222d2f1

Browse files
committed
🐛 修复 CI 测试失败:navigator mock 污染和 e2e fixture 两阶段启动不可靠
- agent_chat.test.ts: 改用 Object.defineProperty 只 mock navigator.storage, 避免 spread 操作丢失 getter 属性(userAgent)导致 react-dom 初始化崩溃 - agent-fixtures.ts: 重构为单 context 方案,去掉关闭→重启浏览器的两阶段流程, 避免 CI 上 Linux headless Chrome profile 持久化不可靠导致扩展加载失败
1 parent 3ae34cb commit 222d2f1

2 files changed

Lines changed: 50 additions & 66 deletions

File tree

e2e/agent-fixtures.ts

Lines changed: 44 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,4 @@
1-
import fs from "fs";
21
import path from "path";
3-
import os from "os";
42
import { test as base, chromium, type BrowserContext, type Route } from "@playwright/test";
53
import { installScriptByCode } from "./utils";
64

@@ -76,27 +74,52 @@ export type AgentFixtures = {
7674

7775
export { makeTextSSE, makeToolCallSSE };
7876

77+
/**
78+
* Agent test fixtures — 单 context 方案
79+
*
80+
* 与旧版两阶段方案(启动→关闭→重启)不同,这里在同一个 context 内完成
81+
* userScripts 启用和 model 配置写入,避免了 CI 上 profile 持久化不可靠的问题。
82+
*/
7983
export const test = base.extend<AgentFixtures>({
8084
// eslint-disable-next-line no-empty-pattern
8185
context: async ({}, use) => {
8286
const pathToExtension = path.resolve(__dirname, "../dist/ext");
83-
const userDataDir = fs.mkdtempSync(path.join(os.tmpdir(), "pw-agent-"));
84-
const chromeArgs = [`--disable-extensions-except=${pathToExtension}`, `--load-extension=${pathToExtension}`];
85-
86-
// Phase 1: Enable user scripts permission
87-
const ctx1 = await chromium.launchPersistentContext(userDataDir, {
87+
const context = await chromium.launchPersistentContext("", {
8888
headless: false,
89-
args: ["--headless=new", ...chromeArgs],
89+
args: ["--headless=new", `--disable-extensions-except=${pathToExtension}`, `--load-extension=${pathToExtension}`],
9090
});
91-
let [bg] = ctx1.serviceWorkers();
92-
if (!bg) bg = await ctx1.waitForEvent("serviceworker");
91+
92+
await use(context);
93+
await context.close();
94+
},
95+
96+
extensionId: async ({ context }, use) => {
97+
// 等待 service worker 启动
98+
let [bg] = context.serviceWorkers();
99+
if (!bg) bg = await context.waitForEvent("serviceworker");
93100
const extensionId = bg.url().split("/")[2];
94-
const extPage = await ctx1.newPage();
95-
await extPage.goto("chrome://extensions/");
96-
await extPage.waitForLoadState("domcontentloaded");
97-
await extPage.waitForTimeout(1_000);
98-
// 预写入 mock model 配置到 storage(在 userScripts 启用之前,SW 仍可用)
99-
await bg.evaluate(() => {
101+
102+
// 在同一 context 内启用 userScripts 权限
103+
const setupPage = await context.newPage();
104+
await setupPage.goto("chrome://extensions/");
105+
await setupPage.waitForLoadState("domcontentloaded");
106+
await setupPage.waitForTimeout(1_000);
107+
await setupPage.evaluate(async (id) => {
108+
await (chrome as any).developerPrivate.updateExtensionConfiguration({
109+
extensionId: id,
110+
userScriptsAccess: true,
111+
});
112+
}, extensionId);
113+
await setupPage.close();
114+
115+
// 启用 userScripts 后 SW 可能会重启,重新获取
116+
let currentBg = context.serviceWorkers().find((w) => w.url().includes(extensionId));
117+
if (!currentBg) {
118+
currentBg = await context.waitForEvent("serviceworker", { timeout: 15_000 });
119+
}
120+
121+
// 写入 mock model 配置到 storage
122+
await currentBg.evaluate(() => {
100123
const modelConfig = {
101124
id: "mock-model",
102125
name: "Mock LLM",
@@ -116,57 +139,15 @@ export const test = base.extend<AgentFixtures>({
116139
});
117140
});
118141

119-
// 启用 userScripts 权限(会触发扩展重载,SW 可能被终止)
120-
await extPage.evaluate(async (id) => {
121-
await (chrome as any).developerPrivate.updateExtensionConfiguration({
122-
extensionId: id,
123-
userScriptsAccess: true,
124-
});
125-
}, extensionId);
126-
await extPage.close();
127-
await ctx1.close();
128-
129-
// Phase 2: Relaunch with user scripts enabled
130-
const context = await chromium.launchPersistentContext(userDataDir, {
131-
headless: false,
132-
args: ["--headless=new", ...chromeArgs],
133-
});
134-
135-
// 先注册监听再检查,避免事件在 check 和 listen 之间丢失
136-
// 如果 SW 已存在则忽略 promise(防止 dangling promise 在 context 关闭时报错)
137-
const swPromise = context.waitForEvent("serviceworker", { timeout: 30_000 }).catch(() => null);
138-
let [bg2] = context.serviceWorkers();
139-
if (!bg2) bg2 = (await swPromise)!;
140-
141-
(context as any).__extensionId = extensionId;
142-
143-
await use(context);
144-
await context.close();
145-
fs.rmSync(userDataDir, { recursive: true, force: true });
146-
},
147-
148-
extensionId: async ({ context }, use) => {
149-
const extensionId: string = (context as any).__extensionId;
150-
151-
// Dismiss first-use dialog(导航也会唤醒 SW)
152-
// 扩展可能还在初始化中,重试导航以应对 ERR_BLOCKED_BY_CLIENT
142+
// 关闭首次使用引导
153143
const initPage = await context.newPage();
154-
for (let attempt = 0; attempt < 5; attempt++) {
155-
try {
156-
await initPage.goto(`chrome-extension://${extensionId}/src/options.html`, {
157-
waitUntil: "domcontentloaded",
158-
timeout: 15_000,
159-
});
160-
break;
161-
} catch {
162-
if (attempt === 4) throw new Error("Failed to load options page after 5 attempts");
163-
await initPage.waitForTimeout(3_000);
164-
}
165-
}
144+
await initPage.goto(`chrome-extension://${extensionId}/src/options.html`, {
145+
waitUntil: "domcontentloaded",
146+
timeout: 30_000,
147+
});
166148
await initPage.evaluate(() => localStorage.setItem("firstUse", "false"));
167149
await initPage.close();
168150

169-
// mock model 已在 Phase 1 预写入 storage,Phase 2 SW 启动时缓存自动包含
170151
await use(extensionId);
171152
},
172153

src/app/repo/agent_chat.test.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -82,11 +82,14 @@ function createMockOPFS() {
8282
const rootStore = new Map<string, any>();
8383
const mockRoot = createMockDirHandle(rootStore);
8484

85-
vi.stubGlobal("navigator", {
86-
...navigator,
87-
storage: {
85+
// 只 mock navigator.storage,避免展开 navigator 丢失 getter 属性(如 userAgent)
86+
// 在 isolate=false 下破坏全局 navigator 会导致后续测试 react-dom 初始化失败
87+
Object.defineProperty(navigator, "storage", {
88+
value: {
8889
getDirectory: vi.fn(async () => mockRoot),
8990
},
91+
configurable: true,
92+
writable: true,
9093
});
9194

9295
return { rootStore, mockRoot };

0 commit comments

Comments
 (0)