import { describe, expect, it, vi } from "vitest"; import type { BrowserServerState } from "./server-context.js"; vi.mock("./chrome.js", () => ({ isChromeCdpReady: vi.fn(async () => true), isChromeReachable: vi.fn(async () => true), launchClawdChrome: vi.fn(async () => { throw new Error("unexpected launch"); }), resolveClawdUserDataDir: vi.fn(() => "/tmp/clawd"), stopClawdChrome: vi.fn(async () => {}), })); function makeState( profile: "remote" | "clawd", ): BrowserServerState & { profiles: Map } { return { // biome-ignore lint/suspicious/noExplicitAny: test stub server: null as any, port: 0, resolved: { enabled: true, controlUrl: "http://127.0.0.1:18791", controlHost: "127.0.0.1", controlPort: 18791, cdpProtocol: profile === "remote" ? "https" : "http", cdpHost: profile === "remote" ? "browserless.example" : "127.0.0.1", cdpIsLoopback: profile !== "remote", remoteCdpTimeoutMs: 1500, remoteCdpHandshakeTimeoutMs: 3000, color: "#FF4500", headless: true, noSandbox: false, attachOnly: false, defaultProfile: profile, profiles: { remote: { cdpUrl: "https://browserless.example/chrome?token=abc", cdpPort: 443, color: "#00AA00", }, clawd: { cdpPort: 18800, color: "#FF4500" }, }, }, profiles: new Map(), }; } describe("browser server-context remote profile tab operations", () => { it("uses Playwright tab operations when available", async () => { vi.resetModules(); const listPagesViaPlaywright = vi.fn(async () => [ { targetId: "T1", title: "Tab 1", url: "https://a.example", type: "page" }, ]); const createPageViaPlaywright = vi.fn(async () => ({ targetId: "T2", title: "Tab 2", url: "https://b.example", type: "page", })); const closePageByTargetIdViaPlaywright = vi.fn(async () => {}); vi.doMock("./pw-ai.js", () => ({ listPagesViaPlaywright, createPageViaPlaywright, closePageByTargetIdViaPlaywright, })); const fetchMock = vi.fn(async () => { throw new Error("unexpected fetch"); }); // @ts-expect-error test override global.fetch = fetchMock; const { createBrowserRouteContext } = await import("./server-context.js"); const state = makeState("remote"); const ctx = createBrowserRouteContext({ getState: () => state }); const remote = ctx.forProfile("remote"); const tabs = await remote.listTabs(); expect(tabs.map((t) => t.targetId)).toEqual(["T1"]); const opened = await remote.openTab("https://b.example"); expect(opened.targetId).toBe("T2"); expect(state.profiles.get("remote")?.lastTargetId).toBe("T2"); 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 () => { vi.resetModules(); const responses = [ // ensureTabAvailable() calls listTabs twice [ { 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" }, ], // second ensureTabAvailable() calls listTabs twice, order flips [ { 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" }, { targetId: "A", title: "A", url: "https://a.example", type: "page" }, ], ]; const listPagesViaPlaywright = vi.fn(async () => { const next = responses.shift(); if (!next) throw new Error("no more responses"); return next; }); vi.doMock("./pw-ai.js", () => ({ listPagesViaPlaywright, createPageViaPlaywright: vi.fn(async () => { throw new Error("unexpected create"); }), closePageByTargetIdViaPlaywright: vi.fn(async () => { throw new Error("unexpected close"); }), })); const fetchMock = vi.fn(async () => { throw new Error("unexpected fetch"); }); // @ts-expect-error test override global.fetch = fetchMock; const { createBrowserRouteContext } = await import("./server-context.js"); const state = makeState("remote"); const ctx = createBrowserRouteContext({ getState: () => state }); const remote = ctx.forProfile("remote"); const first = await remote.ensureTabAvailable(); expect(first.targetId).toBe("A"); const second = await remote.ensureTabAvailable(); expect(second.targetId).toBe("A"); }); it("uses Playwright focus for remote profiles when available", async () => { vi.resetModules(); const listPagesViaPlaywright = vi.fn(async () => [ { targetId: "T1", title: "Tab 1", url: "https://a.example", type: "page" }, ]); const focusPageByTargetIdViaPlaywright = vi.fn(async () => {}); vi.doMock("./pw-ai.js", () => ({ listPagesViaPlaywright, focusPageByTargetIdViaPlaywright, })); const fetchMock = vi.fn(async () => { throw new Error("unexpected fetch"); }); // @ts-expect-error test override global.fetch = fetchMock; const { createBrowserRouteContext } = await import("./server-context.js"); const state = makeState("remote"); const ctx = createBrowserRouteContext({ getState: () => state }); const remote = ctx.forProfile("remote"); 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.resetModules(); vi.doMock("./pw-ai.js", () => ({ listPagesViaPlaywright: vi.fn(async () => { throw new Error("boom"); }), })); const fetchMock = vi.fn(async () => { throw new Error("unexpected fetch"); }); // @ts-expect-error test override global.fetch = fetchMock; const { createBrowserRouteContext } = await import("./server-context.js"); const state = makeState("remote"); const ctx = createBrowserRouteContext({ getState: () => state }); const remote = ctx.forProfile("remote"); await expect(remote.listTabs()).rejects.toThrow(/boom/); expect(fetchMock).not.toHaveBeenCalled(); }); it("falls back to /json/list when Playwright is not available", async () => { vi.resetModules(); vi.doMock("./pw-ai.js", () => ({ listPagesViaPlaywright: undefined, createPageViaPlaywright: undefined, closePageByTargetIdViaPlaywright: undefined, })); const fetchMock = vi.fn(async (url: unknown) => { const u = String(url); if (!u.includes("/json/list")) throw new Error(`unexpected fetch: ${u}`); return { ok: true, json: async () => [ { id: "T1", title: "Tab 1", url: "https://a.example", webSocketDebuggerUrl: "wss://browserless.example/devtools/page/T1", type: "page", }, ], } as unknown as Response; }); // @ts-expect-error test override global.fetch = fetchMock; const { createBrowserRouteContext } = await import("./server-context.js"); const state = makeState("remote"); const ctx = createBrowserRouteContext({ getState: () => state }); const remote = ctx.forProfile("remote"); const tabs = await remote.listTabs(); expect(tabs.map((t) => t.targetId)).toEqual(["T1"]); expect(fetchMock).toHaveBeenCalledTimes(1); }); }); describe("browser server-context tab selection state", () => { it("updates lastTargetId when openTab is created via CDP", async () => { vi.resetModules(); vi.doUnmock("./pw-ai.js"); vi.doMock("./cdp.js", async () => { const actual = await vi.importActual("./cdp.js"); return { ...actual, createTargetViaCdp: vi.fn(async () => ({ targetId: "CREATED" })), }; }); const fetchMock = vi.fn(async (url: unknown) => { const u = String(url); if (!u.includes("/json/list")) throw new Error(`unexpected fetch: ${u}`); return { ok: true, json: async () => [ { id: "CREATED", title: "New Tab", url: "https://created.example", webSocketDebuggerUrl: "ws://127.0.0.1/devtools/page/CREATED", type: "page", }, ], } as unknown as Response; }); // @ts-expect-error test override global.fetch = fetchMock; const { createBrowserRouteContext } = await import("./server-context.js"); const state = makeState("clawd"); const ctx = createBrowserRouteContext({ getState: () => state }); const clawd = ctx.forProfile("clawd"); const opened = await clawd.openTab("https://created.example"); expect(opened.targetId).toBe("CREATED"); expect(state.profiles.get("clawd")?.lastTargetId).toBe("CREATED"); }); });