fix(browser): wait for extension tabs after relay drop (#32331)

This commit is contained in:
AaronWander
2026-03-03 11:03:49 +08:00
committed by Peter Steinberger
parent dcdce83da7
commit bcb0d1b8b4
2 changed files with 48 additions and 6 deletions

View File

@@ -122,4 +122,33 @@ describe("browser server-context ensureTabAvailable", () => {
const chrome = ctx.forProfile("chrome"); const chrome = ctx.forProfile("chrome");
await expect(chrome.ensureTabAvailable()).rejects.toThrow(/no attached Chrome tabs/i); await expect(chrome.ensureTabAvailable()).rejects.toThrow(/no attached Chrome tabs/i);
}); });
it("waits briefly for extension tabs to reappear when a previous target exists", async () => {
vi.useFakeTimers();
try {
const responses = [
// First call: select tab A and store lastTargetId.
[{ id: "A", type: "page", url: "https://a.example", webSocketDebuggerUrl: "ws://x/a" }],
[{ id: "A", type: "page", url: "https://a.example", webSocketDebuggerUrl: "ws://x/a" }],
// Second call: transient drop, then the extension re-announces attached tab A.
[],
[{ id: "A", type: "page", url: "https://a.example", webSocketDebuggerUrl: "ws://x/a" }],
[{ id: "A", type: "page", url: "https://a.example", webSocketDebuggerUrl: "ws://x/a" }],
];
stubChromeJsonList(responses);
const state = makeBrowserState();
const ctx = createBrowserRouteContext({ getState: () => state });
const chrome = ctx.forProfile("chrome");
const first = await chrome.ensureTabAvailable();
expect(first.targetId).toBe("A");
const secondPromise = chrome.ensureTabAvailable();
await vi.advanceTimersByTimeAsync(250);
const second = await secondPromise;
expect(second.targetId).toBe("A");
} finally {
vi.useRealTimers();
}
});
}); });

View File

@@ -32,16 +32,29 @@ export function createProfileSelectionOps({
const ensureTabAvailable = async (targetId?: string): Promise<BrowserTab> => { const ensureTabAvailable = async (targetId?: string): Promise<BrowserTab> => {
await ensureBrowserAvailable(); await ensureBrowserAvailable();
const profileState = getProfileState(); const profileState = getProfileState();
const tabs1 = await listTabs(); let tabs1 = await listTabs();
if (tabs1.length === 0) { if (tabs1.length === 0) {
if (profile.driver === "extension") { if (profile.driver === "extension") {
// Chrome extension relay can briefly drop its WebSocket connection (MV3 service worker
// lifecycle, relay restart). If we previously had a target selected, wait briefly for
// the extension to reconnect and re-announce its attached tabs before failing.
if (profileState.lastTargetId?.trim()) {
const deadlineAt = Date.now() + 3_000;
while (tabs1.length === 0 && Date.now() < deadlineAt) {
await new Promise((resolve) => setTimeout(resolve, 200));
tabs1 = await listTabs();
}
}
if (tabs1.length === 0) {
throw new Error( throw new Error(
`tab not found (no attached Chrome tabs for profile "${profile.name}"). ` + `tab not found (no attached Chrome tabs for profile "${profile.name}"). ` +
"Click the OpenClaw Browser Relay toolbar icon on the tab you want to control (badge ON).", "Click the OpenClaw Browser Relay toolbar icon on the tab you want to control (badge ON).",
); );
} }
} else {
await openTab("about:blank"); await openTab("about:blank");
} }
}
const tabs = await listTabs(); const tabs = await listTabs();
// For remote profiles using Playwright's persistent connection, we don't need wsUrl // For remote profiles using Playwright's persistent connection, we don't need wsUrl