import { afterEach, describe, expect, it, vi } from "vitest"; import { browserOpenTab, browserSnapshot, browserStatus, browserTabs, } from "./client.js"; import { browserAct, browserArmDialog, browserArmFileChooser, browserConsoleMessages, browserNavigate, browserPdfSave, browserScreenshotAction, } from "./client-actions.js"; describe("browser client", () => { afterEach(() => { vi.unstubAllGlobals(); }); it("wraps connection failures with a gateway hint", async () => { const refused = Object.assign(new Error("connect ECONNREFUSED 127.0.0.1"), { code: "ECONNREFUSED", }); const fetchFailed = Object.assign(new TypeError("fetch failed"), { cause: refused, }); vi.stubGlobal("fetch", vi.fn().mockRejectedValue(fetchFailed)); await expect(browserStatus("http://127.0.0.1:18791")).rejects.toThrow( /Start .*gateway/i, ); }); it("adds useful timeout messaging for abort-like failures", async () => { vi.stubGlobal("fetch", vi.fn().mockRejectedValue(new Error("aborted"))); await expect(browserStatus("http://127.0.0.1:18791")).rejects.toThrow( /timed out/i, ); }); it("surfaces non-2xx responses with body text", async () => { vi.stubGlobal( "fetch", vi.fn().mockResolvedValue({ ok: false, status: 409, text: async () => "conflict", } as unknown as Response), ); await expect( browserSnapshot("http://127.0.0.1:18791", { format: "aria", limit: 1 }), ).rejects.toThrow(/409: conflict/i); }); it("uses the expected endpoints + methods for common calls", async () => { const calls: Array<{ url: string; init?: RequestInit }> = []; vi.stubGlobal( "fetch", vi.fn(async (url: string, init?: RequestInit) => { calls.push({ url, init }); if (url.endsWith("/tabs") && (!init || init.method === undefined)) { return { ok: true, json: async () => ({ running: true, tabs: [{ targetId: "t1", title: "T", url: "https://x" }], }), } as unknown as Response; } if (url.endsWith("/tabs/open")) { return { ok: true, json: async () => ({ targetId: "t2", title: "N", url: "https://y", }), } as unknown as Response; } if (url.endsWith("/navigate")) { return { ok: true, json: async () => ({ ok: true, targetId: "t1", url: "https://y", }), } as unknown as Response; } if (url.endsWith("/act")) { return { ok: true, json: async () => ({ ok: true, targetId: "t1", url: "https://x", result: 1, }), } as unknown as Response; } if (url.endsWith("/hooks/file-chooser")) { return { ok: true, json: async () => ({ ok: true }), } as unknown as Response; } if (url.endsWith("/hooks/dialog")) { return { ok: true, json: async () => ({ ok: true }), } as unknown as Response; } if (url.includes("/console?")) { return { ok: true, json: async () => ({ ok: true, targetId: "t1", messages: [], }), } as unknown as Response; } if (url.endsWith("/pdf")) { return { ok: true, json: async () => ({ ok: true, path: "/tmp/a.pdf", targetId: "t1", url: "https://x", }), } as unknown as Response; } if (url.endsWith("/screenshot")) { return { ok: true, json: async () => ({ ok: true, path: "/tmp/a.png", targetId: "t1", url: "https://x", }), } as unknown as Response; } if (url.includes("/snapshot?")) { return { ok: true, json: async () => ({ ok: true, format: "aria", targetId: "t1", url: "https://x", nodes: [], }), } as unknown as Response; } return { ok: true, json: async () => ({ enabled: true, controlUrl: "http://127.0.0.1:18791", running: true, pid: 1, cdpPort: 18792, cdpUrl: "http://127.0.0.1:18792", chosenBrowser: "chrome", userDataDir: "/tmp", color: "#FF4500", headless: false, noSandbox: false, executablePath: null, attachOnly: false, }), } as unknown as Response; }), ); await expect( browserStatus("http://127.0.0.1:18791"), ).resolves.toMatchObject({ running: true, cdpPort: 18792, }); await expect(browserTabs("http://127.0.0.1:18791")).resolves.toHaveLength( 1, ); await expect( browserOpenTab("http://127.0.0.1:18791", "https://example.com"), ).resolves.toMatchObject({ targetId: "t2" }); await expect( browserSnapshot("http://127.0.0.1:18791", { format: "aria", limit: 1 }), ).resolves.toMatchObject({ ok: true, format: "aria" }); await expect( browserNavigate("http://127.0.0.1:18791", { url: "https://example.com" }), ).resolves.toMatchObject({ ok: true, targetId: "t1" }); await expect( browserAct("http://127.0.0.1:18791", { kind: "click", ref: "1" }), ).resolves.toMatchObject({ ok: true, targetId: "t1" }); await expect( browserArmFileChooser("http://127.0.0.1:18791", { paths: ["/tmp/a.txt"], }), ).resolves.toMatchObject({ ok: true }); await expect( browserArmDialog("http://127.0.0.1:18791", { accept: true }), ).resolves.toMatchObject({ ok: true }); await expect( browserConsoleMessages("http://127.0.0.1:18791", { level: "error" }), ).resolves.toMatchObject({ ok: true, targetId: "t1" }); await expect( browserPdfSave("http://127.0.0.1:18791"), ).resolves.toMatchObject({ ok: true, path: "/tmp/a.pdf" }); await expect( browserScreenshotAction("http://127.0.0.1:18791", { fullPage: true }), ).resolves.toMatchObject({ ok: true, path: "/tmp/a.png" }); expect(calls.some((c) => c.url.endsWith("/tabs"))).toBe(true); const open = calls.find((c) => c.url.endsWith("/tabs/open")); expect(open?.init?.method).toBe("POST"); const screenshot = calls.find((c) => c.url.endsWith("/screenshot")); expect(screenshot?.init?.method).toBe("POST"); }); });