test(browser): cover agent contract
parent
9b8a4d0c76
commit
429972b5c5
|
|
@ -0,0 +1,110 @@
|
||||||
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
|
|
||||||
|
let currentPage: Record<string, unknown> | null = null;
|
||||||
|
let currentRefLocator: Record<string, unknown> | null = null;
|
||||||
|
|
||||||
|
const sessionMocks = vi.hoisted(() => ({
|
||||||
|
getPageForTargetId: vi.fn(async () => {
|
||||||
|
if (!currentPage) throw new Error("missing page");
|
||||||
|
return currentPage;
|
||||||
|
}),
|
||||||
|
ensurePageState: vi.fn(() => ({
|
||||||
|
console: [],
|
||||||
|
armIdUpload: 0,
|
||||||
|
armIdDialog: 0,
|
||||||
|
})),
|
||||||
|
refLocator: vi.fn(() => {
|
||||||
|
if (!currentRefLocator) throw new Error("missing locator");
|
||||||
|
return currentRefLocator;
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("./pw-session.js", () => sessionMocks);
|
||||||
|
|
||||||
|
async function importModule() {
|
||||||
|
return await import("./pw-tools-core.js");
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("pw-tools-core", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
currentPage = null;
|
||||||
|
currentRefLocator = null;
|
||||||
|
for (const fn of Object.values(sessionMocks)) fn.mockClear();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("screenshots an element selector", async () => {
|
||||||
|
const elementScreenshot = vi.fn(async () => Buffer.from("E"));
|
||||||
|
currentPage = {
|
||||||
|
locator: vi.fn(() => ({
|
||||||
|
first: () => ({ screenshot: elementScreenshot }),
|
||||||
|
})),
|
||||||
|
screenshot: vi.fn(async () => Buffer.from("P")),
|
||||||
|
};
|
||||||
|
|
||||||
|
const mod = await importModule();
|
||||||
|
const res = await mod.takeScreenshotViaPlaywright({
|
||||||
|
cdpPort: 18792,
|
||||||
|
targetId: "T1",
|
||||||
|
element: "#main",
|
||||||
|
type: "png",
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(res.buffer.toString()).toBe("E");
|
||||||
|
expect(sessionMocks.getPageForTargetId).toHaveBeenCalled();
|
||||||
|
expect(
|
||||||
|
currentPage.locator as ReturnType<typeof vi.fn>,
|
||||||
|
).toHaveBeenCalledWith("#main");
|
||||||
|
expect(elementScreenshot).toHaveBeenCalledWith({ type: "png" });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("screenshots a ref locator", async () => {
|
||||||
|
const refScreenshot = vi.fn(async () => Buffer.from("R"));
|
||||||
|
currentRefLocator = { screenshot: refScreenshot };
|
||||||
|
currentPage = {
|
||||||
|
locator: vi.fn(),
|
||||||
|
screenshot: vi.fn(async () => Buffer.from("P")),
|
||||||
|
};
|
||||||
|
|
||||||
|
const mod = await importModule();
|
||||||
|
const res = await mod.takeScreenshotViaPlaywright({
|
||||||
|
cdpPort: 18792,
|
||||||
|
targetId: "T1",
|
||||||
|
ref: "76",
|
||||||
|
type: "jpeg",
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(res.buffer.toString()).toBe("R");
|
||||||
|
expect(sessionMocks.refLocator).toHaveBeenCalledWith(currentPage, "76");
|
||||||
|
expect(refScreenshot).toHaveBeenCalledWith({ type: "jpeg" });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("rejects fullPage for element or ref screenshots", async () => {
|
||||||
|
currentRefLocator = { screenshot: vi.fn(async () => Buffer.from("R")) };
|
||||||
|
currentPage = {
|
||||||
|
locator: vi.fn(() => ({
|
||||||
|
first: () => ({ screenshot: vi.fn(async () => Buffer.from("E")) }),
|
||||||
|
})),
|
||||||
|
screenshot: vi.fn(async () => Buffer.from("P")),
|
||||||
|
};
|
||||||
|
|
||||||
|
const mod = await importModule();
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
mod.takeScreenshotViaPlaywright({
|
||||||
|
cdpPort: 18792,
|
||||||
|
targetId: "T1",
|
||||||
|
element: "#x",
|
||||||
|
fullPage: true,
|
||||||
|
}),
|
||||||
|
).rejects.toThrow(/fullPage is not supported/i);
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
mod.takeScreenshotViaPlaywright({
|
||||||
|
cdpPort: 18792,
|
||||||
|
targetId: "T1",
|
||||||
|
ref: "1",
|
||||||
|
fullPage: true,
|
||||||
|
}),
|
||||||
|
).rejects.toThrow(/fullPage is not supported/i);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -382,6 +382,14 @@ export function registerBrowserAgentRoutes(
|
||||||
const element = toStringOrEmpty(body.element) || undefined;
|
const element = toStringOrEmpty(body.element) || undefined;
|
||||||
const type = body.type === "jpeg" ? "jpeg" : "png";
|
const type = body.type === "jpeg" ? "jpeg" : "png";
|
||||||
|
|
||||||
|
if (fullPage && (ref || element)) {
|
||||||
|
return jsonError(
|
||||||
|
res,
|
||||||
|
400,
|
||||||
|
"fullPage is not supported for element screenshots",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const tab = await ctx.ensureTabAvailable(targetId);
|
const tab = await ctx.ensureTabAvailable(targetId);
|
||||||
const snap = await takeScreenshotViaPlaywright({
|
const snap = await takeScreenshotViaPlaywright({
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,39 @@ let reachable = false;
|
||||||
let cfgAttachOnly = false;
|
let cfgAttachOnly = false;
|
||||||
let createTargetId: string | null = null;
|
let createTargetId: string | null = null;
|
||||||
|
|
||||||
|
const cdpMocks = vi.hoisted(() => ({
|
||||||
|
createTargetViaCdp: vi.fn(async () => {
|
||||||
|
throw new Error("cdp disabled");
|
||||||
|
}),
|
||||||
|
snapshotAria: vi.fn(async () => ({
|
||||||
|
nodes: [{ ref: "1", role: "link", name: "x", depth: 0 }],
|
||||||
|
})),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const pwMocks = vi.hoisted(() => ({
|
||||||
|
armDialogViaPlaywright: vi.fn(async () => {}),
|
||||||
|
armFileUploadViaPlaywright: vi.fn(async () => {}),
|
||||||
|
clickViaPlaywright: vi.fn(async () => {}),
|
||||||
|
closePageViaPlaywright: vi.fn(async () => {}),
|
||||||
|
closePlaywrightBrowserConnection: vi.fn(async () => {}),
|
||||||
|
dragViaPlaywright: vi.fn(async () => {}),
|
||||||
|
evaluateViaPlaywright: vi.fn(async () => "ok"),
|
||||||
|
fillFormViaPlaywright: vi.fn(async () => {}),
|
||||||
|
getConsoleMessagesViaPlaywright: vi.fn(async () => []),
|
||||||
|
hoverViaPlaywright: vi.fn(async () => {}),
|
||||||
|
navigateViaPlaywright: vi.fn(async () => ({ url: "https://example.com" })),
|
||||||
|
pdfViaPlaywright: vi.fn(async () => ({ buffer: Buffer.from("pdf") })),
|
||||||
|
pressKeyViaPlaywright: vi.fn(async () => {}),
|
||||||
|
resizeViewportViaPlaywright: vi.fn(async () => {}),
|
||||||
|
selectOptionViaPlaywright: vi.fn(async () => {}),
|
||||||
|
snapshotAiViaPlaywright: vi.fn(async () => ({ snapshot: "ok" })),
|
||||||
|
takeScreenshotViaPlaywright: vi.fn(async () => ({
|
||||||
|
buffer: Buffer.from("png"),
|
||||||
|
})),
|
||||||
|
typeViaPlaywright: vi.fn(async () => {}),
|
||||||
|
waitForViaPlaywright: vi.fn(async () => {}),
|
||||||
|
}));
|
||||||
|
|
||||||
function makeProc(pid = 123) {
|
function makeProc(pid = 123) {
|
||||||
const handlers = new Map<string, Array<(...args: unknown[]) => void>>();
|
const handlers = new Map<string, Array<(...args: unknown[]) => void>>();
|
||||||
return {
|
return {
|
||||||
|
|
@ -62,38 +95,11 @@ vi.mock("./chrome.js", () => ({
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock("./cdp.js", () => ({
|
vi.mock("./cdp.js", () => ({
|
||||||
createTargetViaCdp: vi.fn(async () => {
|
createTargetViaCdp: cdpMocks.createTargetViaCdp,
|
||||||
if (createTargetId) return { targetId: createTargetId };
|
snapshotAria: cdpMocks.snapshotAria,
|
||||||
throw new Error("cdp disabled");
|
|
||||||
}),
|
|
||||||
snapshotAria: vi.fn(async () => ({
|
|
||||||
nodes: [{ ref: "1", role: "link", name: "x", depth: 0 }],
|
|
||||||
})),
|
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock("./pw-ai.js", () => ({
|
vi.mock("./pw-ai.js", () => pwMocks);
|
||||||
armDialogViaPlaywright: vi.fn(async () => {}),
|
|
||||||
armFileUploadViaPlaywright: vi.fn(async () => {}),
|
|
||||||
clickViaPlaywright: vi.fn(async () => {}),
|
|
||||||
closePageViaPlaywright: vi.fn(async () => {}),
|
|
||||||
closePlaywrightBrowserConnection: vi.fn(async () => {}),
|
|
||||||
evaluateViaPlaywright: vi.fn(async () => "ok"),
|
|
||||||
fillFormViaPlaywright: vi.fn(async () => {}),
|
|
||||||
getConsoleMessagesViaPlaywright: vi.fn(async () => []),
|
|
||||||
hoverViaPlaywright: vi.fn(async () => {}),
|
|
||||||
navigateViaPlaywright: vi.fn(async () => ({ url: "https://example.com" })),
|
|
||||||
pdfViaPlaywright: vi.fn(async () => ({ buffer: Buffer.from("pdf") })),
|
|
||||||
pressKeyViaPlaywright: vi.fn(async () => {}),
|
|
||||||
resizeViewportViaPlaywright: vi.fn(async () => {}),
|
|
||||||
selectOptionViaPlaywright: vi.fn(async () => {}),
|
|
||||||
snapshotAiViaPlaywright: vi.fn(async () => ({ snapshot: "ok" })),
|
|
||||||
takeScreenshotViaPlaywright: vi.fn(async () => ({
|
|
||||||
buffer: Buffer.from("png"),
|
|
||||||
})),
|
|
||||||
typeViaPlaywright: vi.fn(async () => {}),
|
|
||||||
waitForViaPlaywright: vi.fn(async () => {}),
|
|
||||||
dragViaPlaywright: vi.fn(async () => {}),
|
|
||||||
}));
|
|
||||||
|
|
||||||
vi.mock("../media/store.js", () => ({
|
vi.mock("../media/store.js", () => ({
|
||||||
ensureMediaDir: vi.fn(async () => {}),
|
ensureMediaDir: vi.fn(async () => {}),
|
||||||
|
|
@ -141,6 +147,14 @@ describe("browser control server", () => {
|
||||||
cfgAttachOnly = false;
|
cfgAttachOnly = false;
|
||||||
createTargetId = null;
|
createTargetId = null;
|
||||||
|
|
||||||
|
cdpMocks.createTargetViaCdp.mockImplementation(async () => {
|
||||||
|
if (createTargetId) return { targetId: createTargetId };
|
||||||
|
throw new Error("cdp disabled");
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const fn of Object.values(pwMocks)) fn.mockClear();
|
||||||
|
for (const fn of Object.values(cdpMocks)) fn.mockClear();
|
||||||
|
|
||||||
testPort = await getFreePort();
|
testPort = await getFreePort();
|
||||||
|
|
||||||
// Minimal CDP JSON endpoints used by the server.
|
// Minimal CDP JSON endpoints used by the server.
|
||||||
|
|
@ -265,12 +279,20 @@ describe("browser control server", () => {
|
||||||
};
|
};
|
||||||
expect(snapAria.ok).toBe(true);
|
expect(snapAria.ok).toBe(true);
|
||||||
expect(snapAria.format).toBe("aria");
|
expect(snapAria.format).toBe("aria");
|
||||||
|
expect(cdpMocks.snapshotAria).toHaveBeenCalledWith({
|
||||||
|
wsUrl: "ws://127.0.0.1/devtools/page/abcd1234",
|
||||||
|
limit: 1,
|
||||||
|
});
|
||||||
|
|
||||||
const snapAi = (await realFetch(`${base}/snapshot?format=ai`).then((r) =>
|
const snapAi = (await realFetch(`${base}/snapshot?format=ai`).then((r) =>
|
||||||
r.json(),
|
r.json(),
|
||||||
)) as { ok: boolean; format?: string; snapshot?: string };
|
)) as { ok: boolean; format?: string; snapshot?: string };
|
||||||
expect(snapAi.ok).toBe(true);
|
expect(snapAi.ok).toBe(true);
|
||||||
expect(snapAi.format).toBe("ai");
|
expect(snapAi.format).toBe("ai");
|
||||||
|
expect(pwMocks.snapshotAiViaPlaywright).toHaveBeenCalledWith({
|
||||||
|
cdpPort: testPort + 1,
|
||||||
|
targetId: "abcd1234",
|
||||||
|
});
|
||||||
|
|
||||||
const nav = (await realFetch(`${base}/navigate`, {
|
const nav = (await realFetch(`${base}/navigate`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
|
|
@ -279,13 +301,138 @@ describe("browser control server", () => {
|
||||||
}).then((r) => r.json())) as { ok: boolean; targetId?: string };
|
}).then((r) => r.json())) as { ok: boolean; targetId?: string };
|
||||||
expect(nav.ok).toBe(true);
|
expect(nav.ok).toBe(true);
|
||||||
expect(typeof nav.targetId).toBe("string");
|
expect(typeof nav.targetId).toBe("string");
|
||||||
|
expect(pwMocks.navigateViaPlaywright).toHaveBeenCalledWith({
|
||||||
|
cdpPort: testPort + 1,
|
||||||
|
targetId: "abcd1234",
|
||||||
|
url: "https://example.com",
|
||||||
|
});
|
||||||
|
|
||||||
const click = (await realFetch(`${base}/act`, {
|
const click = (await realFetch(`${base}/act`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: { "Content-Type": "application/json" },
|
headers: { "Content-Type": "application/json" },
|
||||||
body: JSON.stringify({ kind: "click", ref: "1" }),
|
body: JSON.stringify({
|
||||||
|
kind: "click",
|
||||||
|
ref: "1",
|
||||||
|
button: "left",
|
||||||
|
modifiers: ["Shift"],
|
||||||
|
}),
|
||||||
}).then((r) => r.json())) as { ok: boolean };
|
}).then((r) => r.json())) as { ok: boolean };
|
||||||
expect(click.ok).toBe(true);
|
expect(click.ok).toBe(true);
|
||||||
|
expect(pwMocks.clickViaPlaywright).toHaveBeenCalledWith({
|
||||||
|
cdpPort: testPort + 1,
|
||||||
|
targetId: "abcd1234",
|
||||||
|
ref: "1",
|
||||||
|
doubleClick: false,
|
||||||
|
button: "left",
|
||||||
|
modifiers: ["Shift"],
|
||||||
|
});
|
||||||
|
|
||||||
|
const type = (await realFetch(`${base}/act`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({ kind: "type", ref: "1", text: "" }),
|
||||||
|
}).then((r) => r.json())) as { ok: boolean };
|
||||||
|
expect(type.ok).toBe(true);
|
||||||
|
expect(pwMocks.typeViaPlaywright).toHaveBeenCalledWith({
|
||||||
|
cdpPort: testPort + 1,
|
||||||
|
targetId: "abcd1234",
|
||||||
|
ref: "1",
|
||||||
|
text: "",
|
||||||
|
submit: false,
|
||||||
|
slowly: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const press = (await realFetch(`${base}/act`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({ kind: "press", key: "Enter" }),
|
||||||
|
}).then((r) => r.json())) as { ok: boolean };
|
||||||
|
expect(press.ok).toBe(true);
|
||||||
|
expect(pwMocks.pressKeyViaPlaywright).toHaveBeenCalledWith({
|
||||||
|
cdpPort: testPort + 1,
|
||||||
|
targetId: "abcd1234",
|
||||||
|
key: "Enter",
|
||||||
|
});
|
||||||
|
|
||||||
|
const hover = (await realFetch(`${base}/act`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({ kind: "hover", ref: "2" }),
|
||||||
|
}).then((r) => r.json())) as { ok: boolean };
|
||||||
|
expect(hover.ok).toBe(true);
|
||||||
|
expect(pwMocks.hoverViaPlaywright).toHaveBeenCalledWith({
|
||||||
|
cdpPort: testPort + 1,
|
||||||
|
targetId: "abcd1234",
|
||||||
|
ref: "2",
|
||||||
|
});
|
||||||
|
|
||||||
|
const drag = (await realFetch(`${base}/act`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({ kind: "drag", startRef: "3", endRef: "4" }),
|
||||||
|
}).then((r) => r.json())) as { ok: boolean };
|
||||||
|
expect(drag.ok).toBe(true);
|
||||||
|
expect(pwMocks.dragViaPlaywright).toHaveBeenCalledWith({
|
||||||
|
cdpPort: testPort + 1,
|
||||||
|
targetId: "abcd1234",
|
||||||
|
startRef: "3",
|
||||||
|
endRef: "4",
|
||||||
|
});
|
||||||
|
|
||||||
|
const select = (await realFetch(`${base}/act`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({ kind: "select", ref: "5", values: ["a", "b"] }),
|
||||||
|
}).then((r) => r.json())) as { ok: boolean };
|
||||||
|
expect(select.ok).toBe(true);
|
||||||
|
expect(pwMocks.selectOptionViaPlaywright).toHaveBeenCalledWith({
|
||||||
|
cdpPort: testPort + 1,
|
||||||
|
targetId: "abcd1234",
|
||||||
|
ref: "5",
|
||||||
|
values: ["a", "b"],
|
||||||
|
});
|
||||||
|
|
||||||
|
const fill = (await realFetch(`${base}/act`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({
|
||||||
|
kind: "fill",
|
||||||
|
fields: [{ ref: "6", type: "textbox", value: "hello" }],
|
||||||
|
}),
|
||||||
|
}).then((r) => r.json())) as { ok: boolean };
|
||||||
|
expect(fill.ok).toBe(true);
|
||||||
|
expect(pwMocks.fillFormViaPlaywright).toHaveBeenCalledWith({
|
||||||
|
cdpPort: testPort + 1,
|
||||||
|
targetId: "abcd1234",
|
||||||
|
fields: [{ ref: "6", type: "textbox", value: "hello" }],
|
||||||
|
});
|
||||||
|
|
||||||
|
const resize = (await realFetch(`${base}/act`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({ kind: "resize", width: 800, height: 600 }),
|
||||||
|
}).then((r) => r.json())) as { ok: boolean };
|
||||||
|
expect(resize.ok).toBe(true);
|
||||||
|
expect(pwMocks.resizeViewportViaPlaywright).toHaveBeenCalledWith({
|
||||||
|
cdpPort: testPort + 1,
|
||||||
|
targetId: "abcd1234",
|
||||||
|
width: 800,
|
||||||
|
height: 600,
|
||||||
|
});
|
||||||
|
|
||||||
|
const wait = (await realFetch(`${base}/act`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({ kind: "wait", timeMs: 5 }),
|
||||||
|
}).then((r) => r.json())) as { ok: boolean };
|
||||||
|
expect(wait.ok).toBe(true);
|
||||||
|
expect(pwMocks.waitForViaPlaywright).toHaveBeenCalledWith({
|
||||||
|
cdpPort: testPort + 1,
|
||||||
|
targetId: "abcd1234",
|
||||||
|
timeMs: 5,
|
||||||
|
text: undefined,
|
||||||
|
textGone: undefined,
|
||||||
|
});
|
||||||
|
|
||||||
const evalRes = (await realFetch(`${base}/act`, {
|
const evalRes = (await realFetch(`${base}/act`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
|
|
@ -293,26 +440,51 @@ describe("browser control server", () => {
|
||||||
body: JSON.stringify({ kind: "evaluate", fn: "() => 1" }),
|
body: JSON.stringify({ kind: "evaluate", fn: "() => 1" }),
|
||||||
}).then((r) => r.json())) as { ok: boolean; result?: unknown };
|
}).then((r) => r.json())) as { ok: boolean; result?: unknown };
|
||||||
expect(evalRes.ok).toBe(true);
|
expect(evalRes.ok).toBe(true);
|
||||||
|
expect(evalRes.result).toBe("ok");
|
||||||
|
expect(pwMocks.evaluateViaPlaywright).toHaveBeenCalledWith({
|
||||||
|
cdpPort: testPort + 1,
|
||||||
|
targetId: "abcd1234",
|
||||||
|
fn: "() => 1",
|
||||||
|
ref: undefined,
|
||||||
|
});
|
||||||
|
|
||||||
const upload = await realFetch(`${base}/hooks/file-chooser`, {
|
const upload = await realFetch(`${base}/hooks/file-chooser`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: { "Content-Type": "application/json" },
|
headers: { "Content-Type": "application/json" },
|
||||||
body: JSON.stringify({ paths: ["/tmp/a.txt"] }),
|
body: JSON.stringify({ paths: ["/tmp/a.txt"], timeoutMs: 1234 }),
|
||||||
}).then((r) => r.json());
|
}).then((r) => r.json());
|
||||||
expect(upload).toMatchObject({ ok: true });
|
expect(upload).toMatchObject({ ok: true });
|
||||||
|
expect(pwMocks.armFileUploadViaPlaywright).toHaveBeenCalledWith({
|
||||||
|
cdpPort: testPort + 1,
|
||||||
|
targetId: "abcd1234",
|
||||||
|
paths: ["/tmp/a.txt"],
|
||||||
|
timeoutMs: 1234,
|
||||||
|
});
|
||||||
|
|
||||||
const dialog = await realFetch(`${base}/hooks/dialog`, {
|
const dialog = await realFetch(`${base}/hooks/dialog`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: { "Content-Type": "application/json" },
|
headers: { "Content-Type": "application/json" },
|
||||||
body: JSON.stringify({ accept: true }),
|
body: JSON.stringify({ accept: true, timeoutMs: 5678 }),
|
||||||
}).then((r) => r.json());
|
}).then((r) => r.json());
|
||||||
expect(dialog).toMatchObject({ ok: true });
|
expect(dialog).toMatchObject({ ok: true });
|
||||||
|
expect(pwMocks.armDialogViaPlaywright).toHaveBeenCalledWith({
|
||||||
|
cdpPort: testPort + 1,
|
||||||
|
targetId: "abcd1234",
|
||||||
|
accept: true,
|
||||||
|
promptText: undefined,
|
||||||
|
timeoutMs: 5678,
|
||||||
|
});
|
||||||
|
|
||||||
const consoleRes = (await realFetch(`${base}/console`).then((r) =>
|
const consoleRes = (await realFetch(`${base}/console?level=error`).then(
|
||||||
r.json(),
|
(r) => r.json(),
|
||||||
)) as { ok: boolean; messages?: unknown[] };
|
)) as { ok: boolean; messages?: unknown[] };
|
||||||
expect(consoleRes.ok).toBe(true);
|
expect(consoleRes.ok).toBe(true);
|
||||||
expect(Array.isArray(consoleRes.messages)).toBe(true);
|
expect(Array.isArray(consoleRes.messages)).toBe(true);
|
||||||
|
expect(pwMocks.getConsoleMessagesViaPlaywright).toHaveBeenCalledWith({
|
||||||
|
cdpPort: testPort + 1,
|
||||||
|
targetId: "abcd1234",
|
||||||
|
level: "error",
|
||||||
|
});
|
||||||
|
|
||||||
const pdf = (await realFetch(`${base}/pdf`, {
|
const pdf = (await realFetch(`${base}/pdf`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
|
|
@ -325,10 +497,29 @@ describe("browser control server", () => {
|
||||||
const shot = (await realFetch(`${base}/screenshot`, {
|
const shot = (await realFetch(`${base}/screenshot`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: { "Content-Type": "application/json" },
|
headers: { "Content-Type": "application/json" },
|
||||||
body: JSON.stringify({ fullPage: true }),
|
body: JSON.stringify({ element: "body", type: "jpeg" }),
|
||||||
}).then((r) => r.json())) as { ok: boolean; path?: string };
|
}).then((r) => r.json())) as { ok: boolean; path?: string };
|
||||||
expect(shot.ok).toBe(true);
|
expect(shot.ok).toBe(true);
|
||||||
expect(typeof shot.path).toBe("string");
|
expect(typeof shot.path).toBe("string");
|
||||||
|
expect(pwMocks.takeScreenshotViaPlaywright).toHaveBeenCalledWith({
|
||||||
|
cdpPort: testPort + 1,
|
||||||
|
targetId: "abcd1234",
|
||||||
|
ref: undefined,
|
||||||
|
element: "body",
|
||||||
|
fullPage: false,
|
||||||
|
type: "jpeg",
|
||||||
|
});
|
||||||
|
|
||||||
|
const close = (await realFetch(`${base}/act`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({ kind: "close" }),
|
||||||
|
}).then((r) => r.json())) as { ok: boolean };
|
||||||
|
expect(close.ok).toBe(true);
|
||||||
|
expect(pwMocks.closePageViaPlaywright).toHaveBeenCalledWith({
|
||||||
|
cdpPort: testPort + 1,
|
||||||
|
targetId: "abcd1234",
|
||||||
|
});
|
||||||
|
|
||||||
const stopped = (await realFetch(`${base}/stop`, {
|
const stopped = (await realFetch(`${base}/stop`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
|
|
@ -337,6 +528,82 @@ describe("browser control server", () => {
|
||||||
expect(stopped.stopped).toBe(true);
|
expect(stopped.stopped).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("validates agent inputs (agent routes)", async () => {
|
||||||
|
const { startBrowserControlServerFromConfig } = await import("./server.js");
|
||||||
|
await startBrowserControlServerFromConfig();
|
||||||
|
const base = `http://127.0.0.1:${testPort}`;
|
||||||
|
await realFetch(`${base}/start`, { method: "POST" }).then((r) => r.json());
|
||||||
|
|
||||||
|
const navMissing = await realFetch(`${base}/navigate`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({}),
|
||||||
|
});
|
||||||
|
expect(navMissing.status).toBe(400);
|
||||||
|
|
||||||
|
const actMissing = await realFetch(`${base}/act`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({}),
|
||||||
|
});
|
||||||
|
expect(actMissing.status).toBe(400);
|
||||||
|
|
||||||
|
const clickMissingRef = await realFetch(`${base}/act`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({ kind: "click" }),
|
||||||
|
});
|
||||||
|
expect(clickMissingRef.status).toBe(400);
|
||||||
|
|
||||||
|
const clickBadButton = await realFetch(`${base}/act`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({ kind: "click", ref: "1", button: "nope" }),
|
||||||
|
});
|
||||||
|
expect(clickBadButton.status).toBe(400);
|
||||||
|
|
||||||
|
const clickBadModifiers = await realFetch(`${base}/act`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({ kind: "click", ref: "1", modifiers: ["Nope"] }),
|
||||||
|
});
|
||||||
|
expect(clickBadModifiers.status).toBe(400);
|
||||||
|
|
||||||
|
const typeBadText = await realFetch(`${base}/act`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({ kind: "type", ref: "1", text: 123 }),
|
||||||
|
});
|
||||||
|
expect(typeBadText.status).toBe(400);
|
||||||
|
|
||||||
|
const uploadMissingPaths = await realFetch(`${base}/hooks/file-chooser`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({}),
|
||||||
|
});
|
||||||
|
expect(uploadMissingPaths.status).toBe(400);
|
||||||
|
|
||||||
|
const dialogMissingAccept = await realFetch(`${base}/hooks/dialog`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({}),
|
||||||
|
});
|
||||||
|
expect(dialogMissingAccept.status).toBe(400);
|
||||||
|
|
||||||
|
const snapDefault = (await realFetch(`${base}/snapshot?format=wat`).then(
|
||||||
|
(r) => r.json(),
|
||||||
|
)) as { ok: boolean; format?: string };
|
||||||
|
expect(snapDefault.ok).toBe(true);
|
||||||
|
expect(snapDefault.format).toBe("ai");
|
||||||
|
|
||||||
|
const screenshotBadCombo = await realFetch(`${base}/screenshot`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({ fullPage: true, element: "body" }),
|
||||||
|
});
|
||||||
|
expect(screenshotBadCombo.status).toBe(400);
|
||||||
|
});
|
||||||
|
|
||||||
it("covers common error branches", async () => {
|
it("covers common error branches", async () => {
|
||||||
cfgAttachOnly = true;
|
cfgAttachOnly = true;
|
||||||
const { startBrowserControlServerFromConfig } = await import("./server.js");
|
const { startBrowserControlServerFromConfig } = await import("./server.js");
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue