From fac89d403bc9b6bebd35b09ded2569d3c724013b Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Fri, 3 Apr 2026 20:02:38 +0900 Subject: [PATCH] perf(browser): split remote profile tab op tests --- ...xt.remote-profile-tab-ops.fallback.test.ts | 143 +++++++++ ....remote-profile-tab-ops.playwright.test.ts | 175 ++++++++++ ...r-context.remote-profile-tab-ops.shared.ts | 48 +++ ...ver-context.remote-profile-tab-ops.test.ts | 302 ------------------ 4 files changed, 366 insertions(+), 302 deletions(-) create mode 100644 extensions/browser/src/browser/server-context.remote-profile-tab-ops.fallback.test.ts create mode 100644 extensions/browser/src/browser/server-context.remote-profile-tab-ops.playwright.test.ts create mode 100644 extensions/browser/src/browser/server-context.remote-profile-tab-ops.shared.ts delete mode 100644 extensions/browser/src/browser/server-context.remote-profile-tab-ops.test.ts diff --git a/extensions/browser/src/browser/server-context.remote-profile-tab-ops.fallback.test.ts b/extensions/browser/src/browser/server-context.remote-profile-tab-ops.fallback.test.ts new file mode 100644 index 00000000000..d1094b45b57 --- /dev/null +++ b/extensions/browser/src/browser/server-context.remote-profile-tab-ops.fallback.test.ts @@ -0,0 +1,143 @@ +import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; +import { + loadRemoteProfileTestDeps, + type RemoteProfileTestDeps, +} from "./server-context.remote-profile-tab-ops.shared.js"; + +let deps: RemoteProfileTestDeps; + +beforeAll(async () => { + deps = await loadRemoteProfileTestDeps(); +}); + +beforeEach(() => { + vi.clearAllMocks(); + globalThis.fetch = deps.originalFetch; +}); + +afterEach(async () => { + await deps.closePlaywrightBrowserConnection().catch(() => {}); + globalThis.fetch = deps.originalFetch; + vi.restoreAllMocks(); +}); + +describe("browser remote profile fallback and attachOnly behavior", () => { + it("uses profile-level attachOnly when global attachOnly is false", async () => { + const state = deps.makeState("openclaw"); + state.resolved.attachOnly = false; + state.resolved.profiles.openclaw = { + cdpPort: 18800, + attachOnly: true, + color: "#FF4500", + }; + + const reachableMock = vi + .mocked(deps.chromeModule.isChromeReachable) + .mockResolvedValueOnce(false); + const launchMock = vi.mocked(deps.chromeModule.launchOpenClawChrome); + const ctx = deps.createBrowserRouteContext({ getState: () => state }); + + await expect(ctx.forProfile("openclaw").ensureBrowserAvailable()).rejects.toThrow( + /attachOnly is enabled/i, + ); + expect(reachableMock).toHaveBeenCalled(); + expect(launchMock).not.toHaveBeenCalled(); + }); + + it("keeps attachOnly websocket failures off the loopback ownership error path", async () => { + const state = deps.makeState("openclaw"); + state.resolved.attachOnly = false; + state.resolved.profiles.openclaw = { + cdpPort: 18800, + attachOnly: true, + color: "#FF4500", + }; + + const httpReachableMock = vi + .mocked(deps.chromeModule.isChromeReachable) + .mockResolvedValueOnce(true); + const wsReachableMock = vi + .mocked(deps.chromeModule.isChromeCdpReady) + .mockResolvedValueOnce(false); + const launchMock = vi.mocked(deps.chromeModule.launchOpenClawChrome); + const ctx = deps.createBrowserRouteContext({ getState: () => state }); + + await expect(ctx.forProfile("openclaw").ensureBrowserAvailable()).rejects.toThrow( + /attachOnly is enabled and CDP websocket/i, + ); + expect(httpReachableMock).toHaveBeenCalled(); + expect(wsReachableMock).toHaveBeenCalled(); + expect(launchMock).not.toHaveBeenCalled(); + }); + + it("falls back to /json/list when Playwright is not available", async () => { + vi.spyOn(deps.pwAiModule, "getPwAiModule").mockResolvedValue(null); + const { remote } = deps.createRemoteRouteHarness( + vi.fn( + deps.createJsonListFetchMock([ + { + id: "T1", + title: "Tab 1", + url: "https://example.com", + webSocketDebuggerUrl: "wss://browserless.example/devtools/page/T1", + type: "page", + }, + ]), + ), + ); + + const tabs = await remote.listTabs(); + expect(tabs.map((t) => t.targetId)).toEqual(["T1"]); + }); + + it("fails closed for remote tab opens in strict mode without Playwright", async () => { + vi.spyOn(deps.pwAiModule, "getPwAiModule").mockResolvedValue(null); + const { state, remote, fetchMock } = deps.createRemoteRouteHarness(); + state.resolved.ssrfPolicy = {}; + + await expect(remote.openTab("https://example.com")).rejects.toBeInstanceOf( + deps.InvalidBrowserNavigationUrlError, + ); + expect(fetchMock).not.toHaveBeenCalled(); + }); + + it("does not enforce managed tab cap for remote openclaw profiles", async () => { + const listPagesViaPlaywright = vi + .fn() + .mockResolvedValueOnce([ + { targetId: "T1", title: "1", url: "https://1.example", type: "page" }, + ]) + .mockResolvedValueOnce([ + { targetId: "T1", title: "1", url: "https://1.example", type: "page" }, + { targetId: "T2", title: "2", url: "https://2.example", type: "page" }, + { targetId: "T3", title: "3", url: "https://3.example", type: "page" }, + { targetId: "T4", title: "4", url: "https://4.example", type: "page" }, + { targetId: "T5", title: "5", url: "https://5.example", type: "page" }, + { targetId: "T6", title: "6", url: "https://6.example", type: "page" }, + { targetId: "T7", title: "7", url: "https://7.example", type: "page" }, + { targetId: "T8", title: "8", url: "https://8.example", type: "page" }, + { targetId: "T9", title: "9", url: "https://9.example", type: "page" }, + ]); + + const createPageViaPlaywright = vi.fn(async () => ({ + targetId: "T1", + title: "Tab 1", + url: "https://1.example", + type: "page", + })); + + vi.spyOn(deps.pwAiModule, "getPwAiModule").mockResolvedValue({ + listPagesViaPlaywright, + createPageViaPlaywright, + } as unknown as Awaited>); + + const fetchMock = vi.fn(async (url: unknown) => { + throw new Error(`unexpected fetch: ${String(url)}`); + }); + + const { remote } = deps.createRemoteRouteHarness(fetchMock); + const opened = await remote.openTab("https://1.example"); + expect(opened.targetId).toBe("T1"); + expect(fetchMock).not.toHaveBeenCalled(); + }); +}); diff --git a/extensions/browser/src/browser/server-context.remote-profile-tab-ops.playwright.test.ts b/extensions/browser/src/browser/server-context.remote-profile-tab-ops.playwright.test.ts new file mode 100644 index 00000000000..98090648d7d --- /dev/null +++ b/extensions/browser/src/browser/server-context.remote-profile-tab-ops.playwright.test.ts @@ -0,0 +1,175 @@ +import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; +import { + loadRemoteProfileTestDeps, + type RemoteProfileTestDeps, +} from "./server-context.remote-profile-tab-ops.shared.js"; + +let deps: RemoteProfileTestDeps; + +beforeAll(async () => { + deps = await loadRemoteProfileTestDeps(); +}); + +beforeEach(() => { + vi.clearAllMocks(); + globalThis.fetch = deps.originalFetch; +}); + +afterEach(async () => { + await deps.closePlaywrightBrowserConnection().catch(() => {}); + globalThis.fetch = deps.originalFetch; + vi.restoreAllMocks(); +}); + +describe("browser remote profile tab ops via Playwright", () => { + it("uses Playwright tab operations when available", async () => { + const listPagesViaPlaywright = vi.fn(async () => [ + { targetId: "T1", title: "Tab 1", url: "https://example.com", type: "page" }, + ]); + const createPageViaPlaywright = vi.fn(async () => ({ + targetId: "T2", + title: "Tab 2", + url: "http://127.0.0.1:3000", + type: "page", + })); + const closePageByTargetIdViaPlaywright = vi.fn(async () => {}); + + vi.spyOn(deps.pwAiModule, "getPwAiModule").mockResolvedValue({ + listPagesViaPlaywright, + createPageViaPlaywright, + closePageByTargetIdViaPlaywright, + } as unknown as Awaited>); + + const { state, remote, fetchMock } = deps.createRemoteRouteHarness(); + + const tabs = await remote.listTabs(); + expect(tabs.map((t) => t.targetId)).toEqual(["T1"]); + + const opened = await remote.openTab("http://127.0.0.1:3000"); + expect(opened.targetId).toBe("T2"); + expect(state.profiles.get("remote")?.lastTargetId).toBe("T2"); + expect(createPageViaPlaywright).toHaveBeenCalledWith({ + cdpUrl: "https://browserless.example/chrome?token=abc", + url: "http://127.0.0.1:3000", + ssrfPolicy: { allowPrivateNetwork: true }, + }); + + await remote.closeTab("T1"); + expect(closePageByTargetIdViaPlaywright).toHaveBeenCalledWith({ + cdpUrl: "https://browserless.example/chrome?token=abc", + targetId: "T1", + }); + expect(fetchMock).not.toHaveBeenCalled(); + }); + + it("prefers lastTargetId for remote profiles when targetId is omitted", async () => { + const responses = [ + [ + { targetId: "A", title: "A", url: "https://example.com", type: "page" }, + { targetId: "B", title: "B", url: "https://www.example.com", type: "page" }, + ], + [ + { targetId: "A", title: "A", url: "https://example.com", type: "page" }, + { targetId: "B", title: "B", url: "https://www.example.com", type: "page" }, + ], + [ + { targetId: "B", title: "B", url: "https://www.example.com", type: "page" }, + { targetId: "A", title: "A", url: "https://example.com", type: "page" }, + ], + [ + { targetId: "B", title: "B", url: "https://www.example.com", type: "page" }, + { targetId: "A", title: "A", url: "https://example.com", type: "page" }, + ], + ]; + + const listPagesViaPlaywright = vi.fn(deps.createSequentialPageLister(responses)); + + vi.spyOn(deps.pwAiModule, "getPwAiModule").mockResolvedValue({ + listPagesViaPlaywright, + createPageViaPlaywright: vi.fn(async () => { + throw new Error("unexpected create"); + }), + closePageByTargetIdViaPlaywright: vi.fn(async () => { + throw new Error("unexpected close"); + }), + } as unknown as Awaited>); + + const { remote } = deps.createRemoteRouteHarness(); + + const first = await remote.ensureTabAvailable(); + expect(first.targetId).toBe("A"); + const second = await remote.ensureTabAvailable(); + expect(second.targetId).toBe("A"); + }); + + it("rejects stale targetId for remote profiles even when only one tab remains", async () => { + const responses = [ + [{ targetId: "T1", title: "Tab 1", url: "https://example.com", type: "page" }], + [{ targetId: "T1", title: "Tab 1", url: "https://example.com", type: "page" }], + ]; + const listPagesViaPlaywright = vi.fn(deps.createSequentialPageLister(responses)); + + vi.spyOn(deps.pwAiModule, "getPwAiModule").mockResolvedValue({ + listPagesViaPlaywright, + } as unknown as Awaited>); + + const { remote } = deps.createRemoteRouteHarness(); + await expect(remote.ensureTabAvailable("STALE_TARGET")).rejects.toThrow(/tab not found/i); + }); + + it("keeps rejecting stale targetId for remote profiles when multiple tabs exist", async () => { + const responses = [ + [ + { targetId: "A", title: "A", url: "https://a.example", type: "page" }, + { targetId: "B", title: "B", url: "https://b.example", type: "page" }, + ], + [ + { targetId: "A", title: "A", url: "https://a.example", type: "page" }, + { targetId: "B", title: "B", url: "https://b.example", type: "page" }, + ], + ]; + const listPagesViaPlaywright = vi.fn(deps.createSequentialPageLister(responses)); + + vi.spyOn(deps.pwAiModule, "getPwAiModule").mockResolvedValue({ + listPagesViaPlaywright, + } as unknown as Awaited>); + + const { remote } = deps.createRemoteRouteHarness(); + await expect(remote.ensureTabAvailable("STALE_TARGET")).rejects.toThrow(/tab not found/i); + }); + + it("uses Playwright focus for remote profiles when available", async () => { + const listPagesViaPlaywright = vi.fn(async () => [ + { targetId: "T1", title: "Tab 1", url: "https://example.com", type: "page" }, + ]); + const focusPageByTargetIdViaPlaywright = vi.fn(async () => {}); + + vi.spyOn(deps.pwAiModule, "getPwAiModule").mockResolvedValue({ + listPagesViaPlaywright, + focusPageByTargetIdViaPlaywright, + } as unknown as Awaited>); + + const { state, remote, fetchMock } = deps.createRemoteRouteHarness(); + + await remote.focusTab("T1"); + expect(focusPageByTargetIdViaPlaywright).toHaveBeenCalledWith({ + cdpUrl: "https://browserless.example/chrome?token=abc", + targetId: "T1", + }); + expect(fetchMock).not.toHaveBeenCalled(); + expect(state.profiles.get("remote")?.lastTargetId).toBe("T1"); + }); + + it("does not swallow Playwright runtime errors for remote profiles", async () => { + vi.spyOn(deps.pwAiModule, "getPwAiModule").mockResolvedValue({ + listPagesViaPlaywright: vi.fn(async () => { + throw new Error("boom"); + }), + } as unknown as Awaited>); + + const { remote, fetchMock } = deps.createRemoteRouteHarness(); + + await expect(remote.listTabs()).rejects.toThrow(/boom/); + expect(fetchMock).not.toHaveBeenCalled(); + }); +}); diff --git a/extensions/browser/src/browser/server-context.remote-profile-tab-ops.shared.ts b/extensions/browser/src/browser/server-context.remote-profile-tab-ops.shared.ts new file mode 100644 index 00000000000..0421c9c3c08 --- /dev/null +++ b/extensions/browser/src/browser/server-context.remote-profile-tab-ops.shared.ts @@ -0,0 +1,48 @@ +import { vi } from "vitest"; + +export type RemoteProfileTestDeps = { + chromeModule: typeof import("./chrome.js"); + InvalidBrowserNavigationUrlError: typeof import("./navigation-guard.js").InvalidBrowserNavigationUrlError; + pwAiModule: typeof import("./pw-ai-module.js"); + closePlaywrightBrowserConnection: typeof import("./pw-session.js").closePlaywrightBrowserConnection; + createBrowserRouteContext: typeof import("./server-context.js").createBrowserRouteContext; + createJsonListFetchMock: typeof import("./server-context.remote-tab-ops.harness.js").createJsonListFetchMock; + createRemoteRouteHarness: typeof import("./server-context.remote-tab-ops.harness.js").createRemoteRouteHarness; + createSequentialPageLister: typeof import("./server-context.remote-tab-ops.harness.js").createSequentialPageLister; + makeState: typeof import("./server-context.remote-tab-ops.harness.js").makeState; + originalFetch: typeof import("./server-context.remote-tab-ops.harness.js").originalFetch; +}; + +let depsPromise: Promise | undefined; + +export async function loadRemoteProfileTestDeps(): Promise { + depsPromise ??= (async () => { + vi.resetModules(); + await import("./server-context.chrome-test-harness.js"); + const chromeModule = await import("./chrome.js"); + const { InvalidBrowserNavigationUrlError } = await import("./navigation-guard.js"); + const pwAiModule = await import("./pw-ai-module.js"); + const { closePlaywrightBrowserConnection } = await import("./pw-session.js"); + const { createBrowserRouteContext } = await import("./server-context.js"); + const { + createJsonListFetchMock, + createRemoteRouteHarness, + createSequentialPageLister, + makeState, + originalFetch, + } = await import("./server-context.remote-tab-ops.harness.js"); + return { + chromeModule, + InvalidBrowserNavigationUrlError, + pwAiModule, + closePlaywrightBrowserConnection, + createBrowserRouteContext, + createJsonListFetchMock, + createRemoteRouteHarness, + createSequentialPageLister, + makeState, + originalFetch, + }; + })(); + return await depsPromise; +} diff --git a/extensions/browser/src/browser/server-context.remote-profile-tab-ops.test.ts b/extensions/browser/src/browser/server-context.remote-profile-tab-ops.test.ts deleted file mode 100644 index c9a7fa34216..00000000000 --- a/extensions/browser/src/browser/server-context.remote-profile-tab-ops.test.ts +++ /dev/null @@ -1,302 +0,0 @@ -import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; - -const originalFetch = globalThis.fetch; - -let chromeModule: typeof import("./chrome.js"); -let InvalidBrowserNavigationUrlError: typeof import("./navigation-guard.js").InvalidBrowserNavigationUrlError; -let pwAiModule: typeof import("./pw-ai-module.js"); -let closePlaywrightBrowserConnection: typeof import("./pw-session.js").closePlaywrightBrowserConnection; -let createBrowserRouteContext: typeof import("./server-context.js").createBrowserRouteContext; -let createJsonListFetchMock: typeof import("./server-context.remote-tab-ops.harness.js").createJsonListFetchMock; -let createRemoteRouteHarness: typeof import("./server-context.remote-tab-ops.harness.js").createRemoteRouteHarness; -let createSequentialPageLister: typeof import("./server-context.remote-tab-ops.harness.js").createSequentialPageLister; -let makeState: typeof import("./server-context.remote-tab-ops.harness.js").makeState; - -beforeAll(async () => { - vi.resetModules(); - await import("./server-context.chrome-test-harness.js"); - chromeModule = await import("./chrome.js"); - ({ InvalidBrowserNavigationUrlError } = await import("./navigation-guard.js")); - pwAiModule = await import("./pw-ai-module.js"); - ({ closePlaywrightBrowserConnection } = await import("./pw-session.js")); - ({ createBrowserRouteContext } = await import("./server-context.js")); - ({ createJsonListFetchMock, createRemoteRouteHarness, createSequentialPageLister, makeState } = - await import("./server-context.remote-tab-ops.harness.js")); -}); - -beforeEach(() => { - vi.clearAllMocks(); - globalThis.fetch = originalFetch; -}); - -afterEach(async () => { - await closePlaywrightBrowserConnection().catch(() => {}); - globalThis.fetch = originalFetch; - vi.restoreAllMocks(); -}); - -describe("browser server-context remote profile tab operations", () => { - it("uses profile-level attachOnly when global attachOnly is false", async () => { - const state = makeState("openclaw"); - state.resolved.attachOnly = false; - state.resolved.profiles.openclaw = { - cdpPort: 18800, - attachOnly: true, - color: "#FF4500", - }; - - const reachableMock = vi.mocked(chromeModule.isChromeReachable).mockResolvedValueOnce(false); - const launchMock = vi.mocked(chromeModule.launchOpenClawChrome); - const ctx = createBrowserRouteContext({ getState: () => state }); - - await expect(ctx.forProfile("openclaw").ensureBrowserAvailable()).rejects.toThrow( - /attachOnly is enabled/i, - ); - expect(reachableMock).toHaveBeenCalled(); - expect(launchMock).not.toHaveBeenCalled(); - }); - - it("keeps attachOnly websocket failures off the loopback ownership error path", async () => { - const state = makeState("openclaw"); - state.resolved.attachOnly = false; - state.resolved.profiles.openclaw = { - cdpPort: 18800, - attachOnly: true, - color: "#FF4500", - }; - - const httpReachableMock = vi.mocked(chromeModule.isChromeReachable).mockResolvedValueOnce(true); - const wsReachableMock = vi.mocked(chromeModule.isChromeCdpReady).mockResolvedValueOnce(false); - const launchMock = vi.mocked(chromeModule.launchOpenClawChrome); - const ctx = createBrowserRouteContext({ getState: () => state }); - - await expect(ctx.forProfile("openclaw").ensureBrowserAvailable()).rejects.toThrow( - /attachOnly is enabled and CDP websocket/i, - ); - expect(httpReachableMock).toHaveBeenCalled(); - expect(wsReachableMock).toHaveBeenCalled(); - expect(launchMock).not.toHaveBeenCalled(); - }); - - it("uses Playwright tab operations when available", async () => { - const listPagesViaPlaywright = vi.fn(async () => [ - { targetId: "T1", title: "Tab 1", url: "https://example.com", type: "page" }, - ]); - const createPageViaPlaywright = vi.fn(async () => ({ - targetId: "T2", - title: "Tab 2", - url: "http://127.0.0.1:3000", - type: "page", - })); - const closePageByTargetIdViaPlaywright = vi.fn(async () => {}); - - vi.spyOn(pwAiModule, "getPwAiModule").mockResolvedValue({ - listPagesViaPlaywright, - createPageViaPlaywright, - closePageByTargetIdViaPlaywright, - } as unknown as Awaited>); - - const { state, remote, fetchMock } = createRemoteRouteHarness(); - - const tabs = await remote.listTabs(); - expect(tabs.map((t) => t.targetId)).toEqual(["T1"]); - - const opened = await remote.openTab("http://127.0.0.1:3000"); - expect(opened.targetId).toBe("T2"); - expect(state.profiles.get("remote")?.lastTargetId).toBe("T2"); - expect(createPageViaPlaywright).toHaveBeenCalledWith({ - cdpUrl: "https://browserless.example/chrome?token=abc", - url: "http://127.0.0.1:3000", - ssrfPolicy: { allowPrivateNetwork: true }, - }); - - await remote.closeTab("T1"); - expect(closePageByTargetIdViaPlaywright).toHaveBeenCalledWith({ - cdpUrl: "https://browserless.example/chrome?token=abc", - targetId: "T1", - }); - expect(fetchMock).not.toHaveBeenCalled(); - }); - - it("prefers lastTargetId for remote profiles when targetId is omitted", async () => { - const responses = [ - [ - { targetId: "A", title: "A", url: "https://example.com", type: "page" }, - { targetId: "B", title: "B", url: "https://www.example.com", type: "page" }, - ], - [ - { targetId: "A", title: "A", url: "https://example.com", type: "page" }, - { targetId: "B", title: "B", url: "https://www.example.com", type: "page" }, - ], - [ - { targetId: "B", title: "B", url: "https://www.example.com", type: "page" }, - { targetId: "A", title: "A", url: "https://example.com", type: "page" }, - ], - [ - { targetId: "B", title: "B", url: "https://www.example.com", type: "page" }, - { targetId: "A", title: "A", url: "https://example.com", type: "page" }, - ], - ]; - - const listPagesViaPlaywright = vi.fn(createSequentialPageLister(responses)); - - vi.spyOn(pwAiModule, "getPwAiModule").mockResolvedValue({ - listPagesViaPlaywright, - createPageViaPlaywright: vi.fn(async () => { - throw new Error("unexpected create"); - }), - closePageByTargetIdViaPlaywright: vi.fn(async () => { - throw new Error("unexpected close"); - }), - } as unknown as Awaited>); - - const { remote } = createRemoteRouteHarness(); - - const first = await remote.ensureTabAvailable(); - expect(first.targetId).toBe("A"); - const second = await remote.ensureTabAvailable(); - expect(second.targetId).toBe("A"); - }); - - it("rejects stale targetId for remote profiles even when only one tab remains", async () => { - const responses = [ - [{ targetId: "T1", title: "Tab 1", url: "https://example.com", type: "page" }], - [{ targetId: "T1", title: "Tab 1", url: "https://example.com", type: "page" }], - ]; - const listPagesViaPlaywright = vi.fn(createSequentialPageLister(responses)); - - vi.spyOn(pwAiModule, "getPwAiModule").mockResolvedValue({ - listPagesViaPlaywright, - } as unknown as Awaited>); - - const { remote } = createRemoteRouteHarness(); - await expect(remote.ensureTabAvailable("STALE_TARGET")).rejects.toThrow(/tab not found/i); - }); - - it("keeps rejecting stale targetId for remote profiles when multiple tabs exist", async () => { - const responses = [ - [ - { targetId: "A", title: "A", url: "https://a.example", type: "page" }, - { targetId: "B", title: "B", url: "https://b.example", type: "page" }, - ], - [ - { targetId: "A", title: "A", url: "https://a.example", type: "page" }, - { targetId: "B", title: "B", url: "https://b.example", type: "page" }, - ], - ]; - const listPagesViaPlaywright = vi.fn(createSequentialPageLister(responses)); - - vi.spyOn(pwAiModule, "getPwAiModule").mockResolvedValue({ - listPagesViaPlaywright, - } as unknown as Awaited>); - - const { remote } = createRemoteRouteHarness(); - await expect(remote.ensureTabAvailable("STALE_TARGET")).rejects.toThrow(/tab not found/i); - }); - - it("uses Playwright focus for remote profiles when available", async () => { - const listPagesViaPlaywright = vi.fn(async () => [ - { targetId: "T1", title: "Tab 1", url: "https://example.com", type: "page" }, - ]); - const focusPageByTargetIdViaPlaywright = vi.fn(async () => {}); - - vi.spyOn(pwAiModule, "getPwAiModule").mockResolvedValue({ - listPagesViaPlaywright, - focusPageByTargetIdViaPlaywright, - } as unknown as Awaited>); - - const { state, remote, fetchMock } = createRemoteRouteHarness(); - - await remote.focusTab("T1"); - expect(focusPageByTargetIdViaPlaywright).toHaveBeenCalledWith({ - cdpUrl: "https://browserless.example/chrome?token=abc", - targetId: "T1", - }); - expect(fetchMock).not.toHaveBeenCalled(); - expect(state.profiles.get("remote")?.lastTargetId).toBe("T1"); - }); - - it("does not swallow Playwright runtime errors for remote profiles", async () => { - vi.spyOn(pwAiModule, "getPwAiModule").mockResolvedValue({ - listPagesViaPlaywright: vi.fn(async () => { - throw new Error("boom"); - }), - } as unknown as Awaited>); - - const { remote, fetchMock } = createRemoteRouteHarness(); - - await expect(remote.listTabs()).rejects.toThrow(/boom/); - expect(fetchMock).not.toHaveBeenCalled(); - }); - - it("falls back to /json/list when Playwright is not available", async () => { - vi.spyOn(pwAiModule, "getPwAiModule").mockResolvedValue(null); - const { remote } = createRemoteRouteHarness( - vi.fn( - createJsonListFetchMock([ - { - id: "T1", - title: "Tab 1", - url: "https://example.com", - webSocketDebuggerUrl: "wss://browserless.example/devtools/page/T1", - type: "page", - }, - ]), - ), - ); - - const tabs = await remote.listTabs(); - expect(tabs.map((t) => t.targetId)).toEqual(["T1"]); - }); - - it("fails closed for remote tab opens in strict mode without Playwright", async () => { - vi.spyOn(pwAiModule, "getPwAiModule").mockResolvedValue(null); - const { state, remote, fetchMock } = createRemoteRouteHarness(); - state.resolved.ssrfPolicy = {}; - - await expect(remote.openTab("https://example.com")).rejects.toBeInstanceOf( - InvalidBrowserNavigationUrlError, - ); - expect(fetchMock).not.toHaveBeenCalled(); - }); - - it("does not enforce managed tab cap for remote openclaw profiles", async () => { - const listPagesViaPlaywright = vi - .fn() - .mockResolvedValueOnce([ - { targetId: "T1", title: "1", url: "https://1.example", type: "page" }, - ]) - .mockResolvedValueOnce([ - { targetId: "T1", title: "1", url: "https://1.example", type: "page" }, - { targetId: "T2", title: "2", url: "https://2.example", type: "page" }, - { targetId: "T3", title: "3", url: "https://3.example", type: "page" }, - { targetId: "T4", title: "4", url: "https://4.example", type: "page" }, - { targetId: "T5", title: "5", url: "https://5.example", type: "page" }, - { targetId: "T6", title: "6", url: "https://6.example", type: "page" }, - { targetId: "T7", title: "7", url: "https://7.example", type: "page" }, - { targetId: "T8", title: "8", url: "https://8.example", type: "page" }, - { targetId: "T9", title: "9", url: "https://9.example", type: "page" }, - ]); - - const createPageViaPlaywright = vi.fn(async () => ({ - targetId: "T1", - title: "Tab 1", - url: "https://1.example", - type: "page", - })); - - vi.spyOn(pwAiModule, "getPwAiModule").mockResolvedValue({ - listPagesViaPlaywright, - createPageViaPlaywright, - } as unknown as Awaited>); - - const fetchMock = vi.fn(async (url: unknown) => { - throw new Error(`unexpected fetch: ${String(url)}`); - }); - - const { remote } = createRemoteRouteHarness(fetchMock); - const opened = await remote.openTab("https://1.example"); - expect(opened.targetId).toBe("T1"); - expect(fetchMock).not.toHaveBeenCalled(); - }); -});