Files
oh-my-pi/packages/coding-agent/test/tools/browser-attach.test.ts
T
Nik Divjak b8b0d342da fix(tools): stop browser automation from stealing focus in an attached browser
Attaching over app.cdp_url points automation at a browser the user is driving,
so two behaviors that are correct for a browser we own are wrong there.

pickElectronTarget enumerated CDP targets and took the first usable one, which
is not necessarily the tab in front of the user, and #captureScreenshot always
called page.bringToFront(), which switches the user's visible tab and pulls
window focus on every screenshot.

Connected browsers now prefer a tab that reports document.visibilityState
"visible" and skip the pre-capture activation, accepting the compositor stall
risk that activation avoids. Headless and spawned browsers are unchanged.
2026-07-29 11:26:13 +02:00

95 lines
3.5 KiB
TypeScript

import { describe, expect, test } from "bun:test";
import { pickElectronTarget } from "@oh-my-pi/pi-coding-agent/tools/browser/attach";
import { normalizeConnectedCdpUrl } from "@oh-my-pi/pi-coding-agent/tools/browser/registry";
import type { Browser, Page, Target } from "puppeteer-core";
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", () => {
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, "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, "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, undefined, 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, undefined, true)).resolves.toBe(first);
});
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");
});
});