import { afterAll, beforeAll, describe, expect, test } from "bun:test"; import { Settings } from "@oh-my-pi/pi-coding-agent/config/settings"; import type { ToolSession } from "@oh-my-pi/pi-coding-agent/sdk"; import { BrowserTool } from "@oh-my-pi/pi-coding-agent/tools/browser"; import { findFreeCdpPort, pickElectronTarget, probeCdpStatus, shouldPreserveConnectedBrowserFocus, } from "@oh-my-pi/pi-coding-agent/tools/browser/attach"; import { acquireBrowser, type BrowserHandle, normalizeConnectedCdpUrl, releaseBrowser, } from "@oh-my-pi/pi-coding-agent/tools/browser/registry"; import { acquireTab } from "@oh-my-pi/pi-coding-agent/tools/browser/tab-supervisor"; import type { Browser, Page, Target } from "puppeteer-core"; import { chromiumAvailable } from "./chromium-probe"; const CHROMIUM_AVAILABLE = await chromiumAvailable(); let sharedHeadless: BrowserHandle | undefined; function makeSession(): ToolSession { return { cwd: process.cwd(), hasUI: false, getSessionFile: () => null, getSessionSpawns: () => "*", settings: Settings.isolated({ "browser.headless": true }), }; } interface FakePageOptions { url: string; title: string; visible?: boolean; } function fakePage(options: FakePageOptions): Page { return { url: () => options.url, title: async () => options.title, evaluate: async () => options.visible === true, } as unknown as Page; } function fakeTarget(type: string, page: Page | null): Target { return { type: () => type, page: async () => page, } as unknown as Target; } describe("pickElectronTarget", () => { beforeAll(async () => { if (!CHROMIUM_AVAILABLE) return; sharedHeadless = await acquireBrowser({ kind: "headless", headless: true }, { cwd: process.cwd() }); }); afterAll(async () => { if (sharedHeadless) await releaseBrowser(sharedHeadless, { kill: true }); }); test("uses discovered CDP page targets when browser.pages is empty", async () => { const page = fakePage({ url: "https://www.google.com/", title: "Google" }); let pagesCalled = false; const browser = { targets: () => [fakeTarget("browser", null), fakeTarget("page", page)], pages: async () => { pagesCalled = true; return []; }, } as unknown as Browser; await expect(pickElectronTarget(browser, { matcher: "google" })).resolves.toBe(page); expect(pagesCalled).toBe(false); }); test("falls back to browser.pages when discovered targets have no usable page", async () => { const page = fakePage({ url: "https://example.com/", title: "Example" }); const browser = { targets: () => [fakeTarget("browser", null), fakeTarget("service_worker", null)], pages: async () => [page], } as unknown as Browser; await expect(pickElectronTarget(browser)).resolves.toBe(page); }); test("reports available pages when the matcher misses", async () => { const page = fakePage({ url: "https://example.com/", title: "Example" }); const browser = { targets: () => [fakeTarget("page", page)], pages: async () => [], } as unknown as Browser; await expect(pickElectronTarget(browser, { matcher: "missing" })).rejects.toThrow( 'No page target matched "missing". Available pages:\n- Example https://example.com/', ); }); test("prefers the foreground tab when asked to, without disturbing default order", async () => { const background = fakePage({ url: "https://example.com/", title: "Example" }); const foreground = fakePage({ url: "https://example.org/", title: "Example Org", visible: true }); const browser = { targets: () => [fakeTarget("page", background), fakeTarget("page", foreground)], pages: async () => [], } as unknown as Browser; await expect(pickElectronTarget(browser, { preferVisible: true })).resolves.toBe(foreground); await expect(pickElectronTarget(browser)).resolves.toBe(background); }); test("falls back to the first usable tab when no tab reports itself visible", async () => { const first = fakePage({ url: "https://example.com/", title: "Example" }); const second = fakePage({ url: "https://example.org/", title: "Example Org" }); const browser = { targets: () => [fakeTarget("page", first), fakeTarget("page", second)], pages: async () => [], } as unknown as Browser; await expect(pickElectronTarget(browser, { preferVisible: true })).resolves.toBe(first); }); test("preserves connected-browser focus only for automatic target selection", () => { expect(shouldPreserveConnectedBrowserFocus()).toBe(true); expect(shouldPreserveConnectedBrowserFocus("example.com")).toBe(false); }); test("rejects websocket cdp_url values with an actionable diagnostic", () => { expect(() => normalizeConnectedCdpUrl("ws://127.0.0.1:9222/devtools/browser/id")).toThrow( "browser app.cdp_url must be the HTTP CDP discovery endpoint", ); expect(normalizeConnectedCdpUrl("http://127.0.0.1:9222/")).toBe("http://127.0.0.1:9222"); }); // Launches real headless Chromium; skipped where Chrome's system libraries are absent. test.skipIf(!CHROMIUM_AVAILABLE)( "navigates a fresh attached tab and releases its handle without closing the target", async () => { const launched = sharedHeadless; if (!launched || !("browser" in launched)) throw new Error("Expected a shared Puppeteer browser"); const endpoint = new URL(launched.browser.wsEndpoint()); const tool = new BrowserTool(makeSession()); let opened = false; const tabName = `attach-navigation-${process.pid}-${Math.random().toString(36).slice(2)}`; const requested = "data:text/html,