Files
clawdbot/src/browser/client.test.ts
2026-01-01 22:44:52 +01:00

235 lines
6.8 KiB
TypeScript

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");
});
});