mirror of
https://github.com/getpaseo/paseo.git
synced 2026-07-29 12:01:31 +00:00
refactor(browser-tools): require explicit tab targets
Remove hidden active-browser targeting so automation requests carry workspace in the envelope and browserId in tab command args.
This commit is contained in:
@@ -5,96 +5,215 @@ import {
|
||||
BrowserAutomationExecuteResponseSchema,
|
||||
} from "./rpc-schemas.js";
|
||||
|
||||
describe("browser automation execute RPC schemas", () => {
|
||||
test("rejects navigate and download commands for non-http URLs", () => {
|
||||
for (const command of ["navigate", "download"] as const) {
|
||||
expect(() =>
|
||||
BrowserAutomationExecuteRequestSchema.parse({
|
||||
type: "browser.automation.execute.request",
|
||||
requestId: `req-${command}`,
|
||||
command: {
|
||||
command,
|
||||
args: { url: "file:///tmp/secret.txt" },
|
||||
},
|
||||
}),
|
||||
).toThrow();
|
||||
}
|
||||
});
|
||||
const BROWSER_ID = "11111111-1111-4111-8111-111111111111";
|
||||
const FALLBACK_BROWSER_ID = "1777777777777-abcdef";
|
||||
const BROWSER_ID_MESSAGE =
|
||||
"browserId must be a real id returned by browser_new_tab or browser_list_tabs";
|
||||
const WAIT_CONDITION_MESSAGE = "browser_wait requires exactly one of text or url";
|
||||
|
||||
test("parses list tabs requests with top-level correlation and typed command args", () => {
|
||||
describe("browser automation execute RPC schemas", () => {
|
||||
test("list tabs reads workspace from the request envelope", () => {
|
||||
const parsed = BrowserAutomationExecuteRequestSchema.parse({
|
||||
type: "browser.automation.execute.request",
|
||||
requestId: "req-1",
|
||||
requestId: "req-list-tabs",
|
||||
workspaceId: "workspace-1",
|
||||
command: {
|
||||
command: "list_tabs",
|
||||
args: { workspaceId: "workspace-1" },
|
||||
},
|
||||
command: { command: "list_tabs", args: {} },
|
||||
});
|
||||
|
||||
expect(parsed).toEqual({
|
||||
type: "browser.automation.execute.request",
|
||||
requestId: "req-1",
|
||||
requestId: "req-list-tabs",
|
||||
workspaceId: "workspace-1",
|
||||
command: {
|
||||
command: "list_tabs",
|
||||
args: { workspaceId: "workspace-1" },
|
||||
},
|
||||
command: { command: "list_tabs", args: {} },
|
||||
});
|
||||
});
|
||||
|
||||
test("parses new tab requests and responses", () => {
|
||||
expect(
|
||||
BrowserAutomationExecuteRequestSchema.parse({
|
||||
type: "browser.automation.execute.request",
|
||||
requestId: "req-new-tab",
|
||||
workspaceId: "workspace-1",
|
||||
command: {
|
||||
command: "new_tab",
|
||||
args: { workspaceId: "workspace-1", url: "https://example.com" },
|
||||
},
|
||||
}).command,
|
||||
).toEqual({
|
||||
command: "new_tab",
|
||||
args: { workspaceId: "workspace-1", url: "https://example.com" },
|
||||
test("new tab reads workspace from the request envelope", () => {
|
||||
const parsed = BrowserAutomationExecuteRequestSchema.parse({
|
||||
type: "browser.automation.execute.request",
|
||||
requestId: "req-new-tab",
|
||||
workspaceId: "workspace-1",
|
||||
command: { command: "new_tab", args: { url: "https://example.com" } },
|
||||
});
|
||||
|
||||
expect(
|
||||
BrowserAutomationExecuteResponseSchema.parse({
|
||||
type: "browser.automation.execute.response",
|
||||
payload: {
|
||||
requestId: "req-new-tab",
|
||||
ok: true,
|
||||
result: {
|
||||
command: "new_tab",
|
||||
browserId: "browser-1",
|
||||
workspaceId: "workspace-1",
|
||||
url: "https://example.com",
|
||||
},
|
||||
expect(parsed.command).toEqual({
|
||||
command: "new_tab",
|
||||
args: { url: "https://example.com" },
|
||||
});
|
||||
});
|
||||
|
||||
test("tab commands require a browser id from browser_new_tab or browser_list_tabs", () => {
|
||||
const parsed = BrowserAutomationExecuteRequestSchema.safeParse({
|
||||
type: "browser.automation.execute.request",
|
||||
requestId: "req-snapshot",
|
||||
workspaceId: "workspace-1",
|
||||
command: { command: "snapshot", args: {} },
|
||||
});
|
||||
|
||||
expect(parsed).toMatchObject({
|
||||
success: false,
|
||||
error: { issues: [expect.objectContaining({ message: BROWSER_ID_MESSAGE })] },
|
||||
});
|
||||
});
|
||||
|
||||
test("tab commands reject hallucinated browser ids", () => {
|
||||
const parsed = BrowserAutomationExecuteRequestSchema.safeParse({
|
||||
type: "browser.automation.execute.request",
|
||||
requestId: "req-page-info",
|
||||
workspaceId: "workspace-1",
|
||||
command: { command: "page_info", args: { browserId: "default" } },
|
||||
});
|
||||
|
||||
expect(parsed).toMatchObject({
|
||||
success: false,
|
||||
error: { issues: [expect.objectContaining({ message: BROWSER_ID_MESSAGE })] },
|
||||
});
|
||||
});
|
||||
|
||||
test("tab commands parse browser ids produced by the fallback generator", () => {
|
||||
const parsed = BrowserAutomationExecuteRequestSchema.parse({
|
||||
type: "browser.automation.execute.request",
|
||||
requestId: "req-page-info",
|
||||
workspaceId: "workspace-1",
|
||||
command: { command: "page_info", args: { browserId: FALLBACK_BROWSER_ID } },
|
||||
});
|
||||
|
||||
expect(parsed.command).toEqual({
|
||||
command: "page_info",
|
||||
args: { browserId: FALLBACK_BROWSER_ID },
|
||||
});
|
||||
});
|
||||
|
||||
test("requests reject browser id in the envelope", () => {
|
||||
const parsed = BrowserAutomationExecuteRequestSchema.safeParse({
|
||||
type: "browser.automation.execute.request",
|
||||
requestId: "req-click",
|
||||
workspaceId: "workspace-1",
|
||||
browserId: BROWSER_ID,
|
||||
command: { command: "click", args: { browserId: BROWSER_ID, ref: "@e1" } },
|
||||
});
|
||||
|
||||
expect(parsed).toMatchObject({
|
||||
success: false,
|
||||
error: { issues: [expect.objectContaining({ message: 'Unrecognized key: "browserId"' })] },
|
||||
});
|
||||
});
|
||||
|
||||
test("tab commands reject workspace id in command args", () => {
|
||||
const parsed = BrowserAutomationExecuteRequestSchema.safeParse({
|
||||
type: "browser.automation.execute.request",
|
||||
requestId: "req-click",
|
||||
workspaceId: "workspace-1",
|
||||
command: {
|
||||
command: "click",
|
||||
args: { workspaceId: "workspace-1", browserId: BROWSER_ID, ref: "@e1" },
|
||||
},
|
||||
});
|
||||
|
||||
expect(parsed).toMatchObject({
|
||||
success: false,
|
||||
error: { issues: [expect.objectContaining({ message: 'Unrecognized key: "workspaceId"' })] },
|
||||
});
|
||||
});
|
||||
|
||||
test("wait rejects calls without exactly one condition", () => {
|
||||
const parsed = BrowserAutomationExecuteRequestSchema.safeParse({
|
||||
type: "browser.automation.execute.request",
|
||||
requestId: "req-wait",
|
||||
command: { command: "wait", args: { browserId: BROWSER_ID } },
|
||||
});
|
||||
|
||||
expect(parsed).toMatchObject({
|
||||
success: false,
|
||||
error: { issues: [expect.objectContaining({ message: WAIT_CONDITION_MESSAGE })] },
|
||||
});
|
||||
});
|
||||
|
||||
test("wait rejects calls with both text and url conditions", () => {
|
||||
const parsed = BrowserAutomationExecuteRequestSchema.safeParse({
|
||||
type: "browser.automation.execute.request",
|
||||
requestId: "req-wait",
|
||||
command: {
|
||||
command: "wait",
|
||||
args: { browserId: BROWSER_ID, text: "Ready", url: "/ready" },
|
||||
},
|
||||
});
|
||||
|
||||
expect(parsed).toMatchObject({
|
||||
success: false,
|
||||
error: { issues: [expect.objectContaining({ message: WAIT_CONDITION_MESSAGE })] },
|
||||
});
|
||||
});
|
||||
|
||||
test("wait accepts one text condition", () => {
|
||||
const parsed = BrowserAutomationExecuteRequestSchema.parse({
|
||||
type: "browser.automation.execute.request",
|
||||
requestId: "req-wait",
|
||||
command: {
|
||||
command: "wait",
|
||||
args: { browserId: BROWSER_ID, text: "Ready", timeoutMs: 1000 },
|
||||
},
|
||||
});
|
||||
|
||||
expect(parsed.command).toEqual({
|
||||
command: "wait",
|
||||
args: { browserId: BROWSER_ID, text: "Ready", timeoutMs: 1000 },
|
||||
});
|
||||
});
|
||||
|
||||
test("navigate rejects non-http URLs at the protocol boundary", () => {
|
||||
const parsed = BrowserAutomationExecuteRequestSchema.safeParse({
|
||||
type: "browser.automation.execute.request",
|
||||
requestId: "req-navigate",
|
||||
command: {
|
||||
command: "navigate",
|
||||
args: { browserId: BROWSER_ID, url: "file:///tmp/secret.txt" },
|
||||
},
|
||||
});
|
||||
|
||||
expect(parsed).toMatchObject({
|
||||
success: false,
|
||||
error: { issues: [expect.objectContaining({ message: "URL must use http or https" })] },
|
||||
});
|
||||
});
|
||||
|
||||
test("new tab responses declare the generated browser id shape", () => {
|
||||
const parsed = BrowserAutomationExecuteResponseSchema.parse({
|
||||
type: "browser.automation.execute.response",
|
||||
payload: {
|
||||
requestId: "req-new-tab",
|
||||
ok: true,
|
||||
result: {
|
||||
command: "new_tab",
|
||||
browserId: BROWSER_ID,
|
||||
workspaceId: "workspace-1",
|
||||
url: "https://example.com",
|
||||
},
|
||||
}).payload,
|
||||
).toEqual({
|
||||
},
|
||||
});
|
||||
|
||||
expect(parsed.payload).toEqual({
|
||||
requestId: "req-new-tab",
|
||||
ok: true,
|
||||
result: {
|
||||
command: "new_tab",
|
||||
browserId: "browser-1",
|
||||
browserId: BROWSER_ID,
|
||||
workspaceId: "workspace-1",
|
||||
url: "https://example.com",
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test("parses page info responses with result data under payload", () => {
|
||||
const parsed = BrowserAutomationExecuteResponseSchema.parse({
|
||||
test("responses reject hallucinated browser ids", () => {
|
||||
const parsed = BrowserAutomationExecuteResponseSchema.safeParse({
|
||||
type: "browser.automation.execute.response",
|
||||
payload: {
|
||||
requestId: "req-1",
|
||||
requestId: "req-page-info",
|
||||
ok: true,
|
||||
result: {
|
||||
command: "page_info",
|
||||
tab: {
|
||||
browserId: "browser-1",
|
||||
browserId: "default",
|
||||
workspaceId: "workspace-1",
|
||||
url: "https://example.com",
|
||||
title: "Example",
|
||||
@@ -103,672 +222,9 @@ describe("browser automation execute RPC schemas", () => {
|
||||
},
|
||||
});
|
||||
|
||||
expect(parsed.payload).toEqual({
|
||||
requestId: "req-1",
|
||||
ok: true,
|
||||
result: {
|
||||
command: "page_info",
|
||||
tab: {
|
||||
browserId: "browser-1",
|
||||
workspaceId: "workspace-1",
|
||||
url: "https://example.com",
|
||||
title: "Example",
|
||||
isActive: false,
|
||||
isLoading: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test("parses snapshot requests and ref-bearing snapshot responses", () => {
|
||||
expect(
|
||||
BrowserAutomationExecuteRequestSchema.parse({
|
||||
type: "browser.automation.execute.request",
|
||||
requestId: "req-snapshot",
|
||||
workspaceId: "workspace-1",
|
||||
command: {
|
||||
command: "snapshot",
|
||||
args: { workspaceId: "workspace-1", browserId: "browser-1" },
|
||||
},
|
||||
}),
|
||||
).toEqual({
|
||||
type: "browser.automation.execute.request",
|
||||
requestId: "req-snapshot",
|
||||
workspaceId: "workspace-1",
|
||||
command: {
|
||||
command: "snapshot",
|
||||
args: { workspaceId: "workspace-1", browserId: "browser-1" },
|
||||
},
|
||||
});
|
||||
|
||||
const parsed = BrowserAutomationExecuteResponseSchema.parse({
|
||||
type: "browser.automation.execute.response",
|
||||
payload: {
|
||||
requestId: "req-snapshot",
|
||||
ok: true,
|
||||
result: {
|
||||
command: "snapshot",
|
||||
browserId: "browser-1",
|
||||
workspaceId: "workspace-1",
|
||||
url: "https://example.com/form",
|
||||
title: "Fixture",
|
||||
elements: [
|
||||
{
|
||||
ref: "@e1",
|
||||
role: "textbox",
|
||||
tagName: "input",
|
||||
text: "Name",
|
||||
selector: "#name",
|
||||
attributes: { id: "name", type: "text" },
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(parsed.payload).toEqual({
|
||||
requestId: "req-snapshot",
|
||||
ok: true,
|
||||
result: {
|
||||
command: "snapshot",
|
||||
browserId: "browser-1",
|
||||
workspaceId: "workspace-1",
|
||||
url: "https://example.com/form",
|
||||
title: "Fixture",
|
||||
elements: [
|
||||
{
|
||||
ref: "@e1",
|
||||
role: "textbox",
|
||||
tagName: "input",
|
||||
text: "Name",
|
||||
selector: "#name",
|
||||
attributes: { id: "name", type: "text" },
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test("parses click and fill ref commands", () => {
|
||||
expect(
|
||||
BrowserAutomationExecuteRequestSchema.parse({
|
||||
type: "browser.automation.execute.request",
|
||||
requestId: "req-click",
|
||||
command: {
|
||||
command: "click",
|
||||
args: { workspaceId: "workspace-1", browserId: "browser-1", ref: "@e1" },
|
||||
},
|
||||
}).command,
|
||||
).toEqual({
|
||||
command: "click",
|
||||
args: { workspaceId: "workspace-1", browserId: "browser-1", ref: "@e1" },
|
||||
});
|
||||
|
||||
expect(
|
||||
BrowserAutomationExecuteRequestSchema.parse({
|
||||
type: "browser.automation.execute.request",
|
||||
requestId: "req-fill",
|
||||
command: {
|
||||
command: "fill",
|
||||
args: { workspaceId: "workspace-1", ref: "@e2", value: "Ada" },
|
||||
},
|
||||
}).command,
|
||||
).toEqual({
|
||||
command: "fill",
|
||||
args: { workspaceId: "workspace-1", ref: "@e2", value: "Ada" },
|
||||
});
|
||||
|
||||
expect(
|
||||
BrowserAutomationExecuteResponseSchema.parse({
|
||||
type: "browser.automation.execute.response",
|
||||
payload: {
|
||||
requestId: "req-fill",
|
||||
ok: true,
|
||||
result: { command: "fill", browserId: "browser-1", ref: "@e2" },
|
||||
},
|
||||
}).payload,
|
||||
).toEqual({
|
||||
requestId: "req-fill",
|
||||
ok: true,
|
||||
result: { command: "fill", browserId: "browser-1", ref: "@e2" },
|
||||
});
|
||||
});
|
||||
|
||||
test("parses wait text commands and responses", () => {
|
||||
expect(
|
||||
BrowserAutomationExecuteRequestSchema.parse({
|
||||
type: "browser.automation.execute.request",
|
||||
requestId: "req-wait",
|
||||
command: {
|
||||
command: "wait",
|
||||
args: { workspaceId: "workspace-1", text: "Ready", timeoutMs: 1000 },
|
||||
},
|
||||
}).command,
|
||||
).toEqual({
|
||||
command: "wait",
|
||||
args: { workspaceId: "workspace-1", text: "Ready", timeoutMs: 1000 },
|
||||
});
|
||||
|
||||
expect(
|
||||
BrowserAutomationExecuteResponseSchema.parse({
|
||||
type: "browser.automation.execute.response",
|
||||
payload: {
|
||||
requestId: "req-wait",
|
||||
ok: true,
|
||||
result: { command: "wait", browserId: "browser-1", matched: "text" },
|
||||
},
|
||||
}).payload,
|
||||
).toEqual({
|
||||
requestId: "req-wait",
|
||||
ok: true,
|
||||
result: { command: "wait", browserId: "browser-1", matched: "text" },
|
||||
});
|
||||
});
|
||||
|
||||
test("parses set background commands and responses", () => {
|
||||
expect(
|
||||
BrowserAutomationExecuteRequestSchema.parse({
|
||||
type: "browser.automation.execute.request",
|
||||
requestId: "req-bg",
|
||||
command: {
|
||||
command: "set_background",
|
||||
args: { workspaceId: "workspace-1", color: "red" },
|
||||
},
|
||||
}).command,
|
||||
).toEqual({
|
||||
command: "set_background",
|
||||
args: { workspaceId: "workspace-1", color: "red" },
|
||||
});
|
||||
|
||||
expect(
|
||||
BrowserAutomationExecuteResponseSchema.parse({
|
||||
type: "browser.automation.execute.response",
|
||||
payload: {
|
||||
requestId: "req-bg",
|
||||
ok: true,
|
||||
result: { command: "set_background", browserId: "browser-1", color: "red" },
|
||||
},
|
||||
}).payload,
|
||||
).toEqual({
|
||||
requestId: "req-bg",
|
||||
ok: true,
|
||||
result: { command: "set_background", browserId: "browser-1", color: "red" },
|
||||
});
|
||||
});
|
||||
|
||||
test("parses type and keypress commands", () => {
|
||||
expect(
|
||||
BrowserAutomationExecuteRequestSchema.parse({
|
||||
type: "browser.automation.execute.request",
|
||||
requestId: "req-type",
|
||||
command: { command: "type", args: { browserId: "browser-1", ref: "@e1", text: "Ada" } },
|
||||
}).command,
|
||||
).toEqual({ command: "type", args: { browserId: "browser-1", ref: "@e1", text: "Ada" } });
|
||||
|
||||
expect(
|
||||
BrowserAutomationExecuteRequestSchema.parse({
|
||||
type: "browser.automation.execute.request",
|
||||
requestId: "req-keypress",
|
||||
command: { command: "keypress", args: { browserId: "browser-1", key: "Enter" } },
|
||||
}).command,
|
||||
).toEqual({ command: "keypress", args: { browserId: "browser-1", key: "Enter" } });
|
||||
});
|
||||
|
||||
test("parses navigation commands", () => {
|
||||
expect(
|
||||
BrowserAutomationExecuteRequestSchema.parse({
|
||||
type: "browser.automation.execute.request",
|
||||
requestId: "req-nav",
|
||||
command: {
|
||||
command: "navigate",
|
||||
args: { browserId: "browser-1", url: "https://example.com/next" },
|
||||
},
|
||||
}).command,
|
||||
).toEqual({
|
||||
command: "navigate",
|
||||
args: { browserId: "browser-1", url: "https://example.com/next" },
|
||||
});
|
||||
|
||||
expect(
|
||||
BrowserAutomationExecuteResponseSchema.parse({
|
||||
type: "browser.automation.execute.response",
|
||||
payload: {
|
||||
requestId: "req-nav",
|
||||
ok: true,
|
||||
result: { command: "navigate", browserId: "browser-1", url: "https://example.com/next" },
|
||||
},
|
||||
}).payload,
|
||||
).toEqual({
|
||||
requestId: "req-nav",
|
||||
ok: true,
|
||||
result: { command: "navigate", browserId: "browser-1", url: "https://example.com/next" },
|
||||
});
|
||||
});
|
||||
|
||||
test("parses screenshot responses", () => {
|
||||
expect(
|
||||
BrowserAutomationExecuteResponseSchema.parse({
|
||||
type: "browser.automation.execute.response",
|
||||
payload: {
|
||||
requestId: "req-shot",
|
||||
ok: true,
|
||||
result: {
|
||||
command: "screenshot",
|
||||
browserId: "browser-1",
|
||||
mimeType: "image/png",
|
||||
dataBase64: "iVBORw0KGgo=",
|
||||
width: 100,
|
||||
height: 50,
|
||||
},
|
||||
},
|
||||
}).payload,
|
||||
).toEqual({
|
||||
requestId: "req-shot",
|
||||
ok: true,
|
||||
result: {
|
||||
command: "screenshot",
|
||||
browserId: "browser-1",
|
||||
mimeType: "image/png",
|
||||
dataBase64: "iVBORw0KGgo=",
|
||||
width: 100,
|
||||
height: 50,
|
||||
},
|
||||
});
|
||||
|
||||
expect(
|
||||
BrowserAutomationExecuteResponseSchema.parse({
|
||||
type: "browser.automation.execute.response",
|
||||
payload: {
|
||||
requestId: "req-shot-no-frame",
|
||||
ok: false,
|
||||
error: {
|
||||
code: "screenshot_no_frame",
|
||||
message:
|
||||
"The browser tab has no painted frame. Focus the tab in the app, then try again.",
|
||||
retryable: false,
|
||||
},
|
||||
},
|
||||
}).payload,
|
||||
).toEqual({
|
||||
requestId: "req-shot-no-frame",
|
||||
ok: false,
|
||||
error: {
|
||||
code: "screenshot_no_frame",
|
||||
message: "The browser tab has no painted frame. Focus the tab in the app, then try again.",
|
||||
retryable: false,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test("parses form control commands and responses", () => {
|
||||
expect(
|
||||
BrowserAutomationExecuteRequestSchema.parse({
|
||||
type: "browser.automation.execute.request",
|
||||
requestId: "req-focus",
|
||||
command: { command: "focus", args: { browserId: "browser-1", ref: "@e1" } },
|
||||
}).command,
|
||||
).toEqual({ command: "focus", args: { browserId: "browser-1", ref: "@e1" } });
|
||||
|
||||
expect(
|
||||
BrowserAutomationExecuteRequestSchema.parse({
|
||||
type: "browser.automation.execute.request",
|
||||
requestId: "req-clear",
|
||||
command: { command: "clear", args: { browserId: "browser-1", ref: "@e1" } },
|
||||
}).command,
|
||||
).toEqual({ command: "clear", args: { browserId: "browser-1", ref: "@e1" } });
|
||||
|
||||
expect(
|
||||
BrowserAutomationExecuteRequestSchema.parse({
|
||||
type: "browser.automation.execute.request",
|
||||
requestId: "req-check",
|
||||
command: { command: "check", args: { browserId: "browser-1", ref: "@e2" } },
|
||||
}).command,
|
||||
).toEqual({ command: "check", args: { browserId: "browser-1", ref: "@e2", checked: true } });
|
||||
|
||||
expect(
|
||||
BrowserAutomationExecuteRequestSchema.parse({
|
||||
type: "browser.automation.execute.request",
|
||||
requestId: "req-select",
|
||||
command: { command: "select", args: { browserId: "browser-1", ref: "@e3", value: "us" } },
|
||||
}).command,
|
||||
).toEqual({ command: "select", args: { browserId: "browser-1", ref: "@e3", value: "us" } });
|
||||
|
||||
expect(
|
||||
BrowserAutomationExecuteRequestSchema.parse({
|
||||
type: "browser.automation.execute.request",
|
||||
requestId: "req-hover",
|
||||
command: { command: "hover", args: { browserId: "browser-1", ref: "@e4" } },
|
||||
}).command,
|
||||
).toEqual({ command: "hover", args: { browserId: "browser-1", ref: "@e4" } });
|
||||
|
||||
expect(
|
||||
BrowserAutomationExecuteRequestSchema.parse({
|
||||
type: "browser.automation.execute.request",
|
||||
requestId: "req-drag",
|
||||
command: {
|
||||
command: "drag",
|
||||
args: { browserId: "browser-1", sourceRef: "@e4", targetRef: "@e5" },
|
||||
},
|
||||
}).command,
|
||||
).toEqual({
|
||||
command: "drag",
|
||||
args: { browserId: "browser-1", sourceRef: "@e4", targetRef: "@e5" },
|
||||
});
|
||||
|
||||
expect(
|
||||
BrowserAutomationExecuteResponseSchema.parse({
|
||||
type: "browser.automation.execute.response",
|
||||
payload: {
|
||||
requestId: "req-select",
|
||||
ok: true,
|
||||
result: { command: "select", browserId: "browser-1", ref: "@e3", value: "us" },
|
||||
},
|
||||
}).payload,
|
||||
).toEqual({
|
||||
requestId: "req-select",
|
||||
ok: true,
|
||||
result: { command: "select", browserId: "browser-1", ref: "@e3", value: "us" },
|
||||
});
|
||||
});
|
||||
|
||||
test("parses browser log commands and responses", () => {
|
||||
expect(
|
||||
BrowserAutomationExecuteRequestSchema.parse({
|
||||
type: "browser.automation.execute.request",
|
||||
requestId: "req-logs",
|
||||
command: { command: "logs", args: { browserId: "browser-1" } },
|
||||
}).command,
|
||||
).toEqual({ command: "logs", args: { browserId: "browser-1", maxEntries: 50 } });
|
||||
|
||||
expect(
|
||||
BrowserAutomationExecuteResponseSchema.parse({
|
||||
type: "browser.automation.execute.response",
|
||||
payload: {
|
||||
requestId: "req-logs",
|
||||
ok: true,
|
||||
result: {
|
||||
command: "logs",
|
||||
browserId: "browser-1",
|
||||
console: [{ level: "info", message: "ready", timestamp: 10 }],
|
||||
network: [
|
||||
{
|
||||
url: "https://example.com/app.js",
|
||||
type: "script",
|
||||
startTime: 1,
|
||||
duration: 2,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
}).payload,
|
||||
).toEqual({
|
||||
requestId: "req-logs",
|
||||
ok: true,
|
||||
result: {
|
||||
command: "logs",
|
||||
browserId: "browser-1",
|
||||
console: [{ level: "info", message: "ready", timestamp: 10 }],
|
||||
network: [
|
||||
{
|
||||
url: "https://example.com/app.js",
|
||||
type: "script",
|
||||
startTime: 1,
|
||||
duration: 2,
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test("parses browser storage commands and responses", () => {
|
||||
expect(
|
||||
BrowserAutomationExecuteRequestSchema.parse({
|
||||
type: "browser.automation.execute.request",
|
||||
requestId: "req-storage",
|
||||
command: { command: "storage", args: { browserId: "browser-1" } },
|
||||
}).command,
|
||||
).toEqual({ command: "storage", args: { browserId: "browser-1" } });
|
||||
|
||||
expect(
|
||||
BrowserAutomationExecuteResponseSchema.parse({
|
||||
type: "browser.automation.execute.response",
|
||||
payload: {
|
||||
requestId: "req-storage",
|
||||
ok: true,
|
||||
result: {
|
||||
command: "storage",
|
||||
browserId: "browser-1",
|
||||
url: "https://example.com",
|
||||
cookies: [{ name: "theme", value: "dark", domain: "example.com", httpOnly: true }],
|
||||
localStorage: [{ key: "token", value: "abc" }],
|
||||
sessionStorage: [{ key: "tab", value: "1" }],
|
||||
},
|
||||
},
|
||||
}).payload,
|
||||
).toEqual({
|
||||
requestId: "req-storage",
|
||||
ok: true,
|
||||
result: {
|
||||
command: "storage",
|
||||
browserId: "browser-1",
|
||||
url: "https://example.com",
|
||||
cookies: [{ name: "theme", value: "dark", domain: "example.com", httpOnly: true }],
|
||||
localStorage: [{ key: "token", value: "abc" }],
|
||||
sessionStorage: [{ key: "tab", value: "1" }],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test("parses browser environment commands and responses", () => {
|
||||
expect(
|
||||
BrowserAutomationExecuteRequestSchema.parse({
|
||||
type: "browser.automation.execute.request",
|
||||
requestId: "req-environment",
|
||||
command: {
|
||||
command: "environment",
|
||||
args: {
|
||||
browserId: "browser-1",
|
||||
viewport: { width: 390, height: 844, deviceScaleFactor: 3 },
|
||||
geolocation: { latitude: 37.7749, longitude: -122.4194, accuracy: 5 },
|
||||
},
|
||||
},
|
||||
}).command,
|
||||
).toEqual({
|
||||
command: "environment",
|
||||
args: {
|
||||
browserId: "browser-1",
|
||||
viewport: { width: 390, height: 844, deviceScaleFactor: 3 },
|
||||
geolocation: { latitude: 37.7749, longitude: -122.4194, accuracy: 5 },
|
||||
},
|
||||
});
|
||||
|
||||
expect(
|
||||
BrowserAutomationExecuteResponseSchema.parse({
|
||||
type: "browser.automation.execute.response",
|
||||
payload: {
|
||||
requestId: "req-environment",
|
||||
ok: true,
|
||||
result: {
|
||||
command: "environment",
|
||||
browserId: "browser-1",
|
||||
viewport: { width: 390, height: 844, deviceScaleFactor: 3 },
|
||||
geolocation: { latitude: 37.7749, longitude: -122.4194, accuracy: 5 },
|
||||
},
|
||||
},
|
||||
}).payload,
|
||||
).toEqual({
|
||||
requestId: "req-environment",
|
||||
ok: true,
|
||||
result: {
|
||||
command: "environment",
|
||||
browserId: "browser-1",
|
||||
viewport: { width: 390, height: 844, deviceScaleFactor: 3 },
|
||||
geolocation: { latitude: 37.7749, longitude: -122.4194, accuracy: 5 },
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test("parses browser full-page screenshot and PDF commands and responses", () => {
|
||||
expect(
|
||||
BrowserAutomationExecuteRequestSchema.parse({
|
||||
type: "browser.automation.execute.request",
|
||||
requestId: "req-full-page",
|
||||
command: { command: "full_page_screenshot", args: { browserId: "browser-1" } },
|
||||
}).command,
|
||||
).toEqual({ command: "full_page_screenshot", args: { browserId: "browser-1" } });
|
||||
|
||||
expect(
|
||||
BrowserAutomationExecuteRequestSchema.parse({
|
||||
type: "browser.automation.execute.request",
|
||||
requestId: "req-pdf",
|
||||
command: { command: "pdf", args: { browserId: "browser-1" } },
|
||||
}).command,
|
||||
).toEqual({ command: "pdf", args: { browserId: "browser-1", printBackground: true } });
|
||||
|
||||
expect(
|
||||
BrowserAutomationExecuteResponseSchema.parse({
|
||||
type: "browser.automation.execute.response",
|
||||
payload: {
|
||||
requestId: "req-full-page",
|
||||
ok: true,
|
||||
result: {
|
||||
command: "full_page_screenshot",
|
||||
browserId: "browser-1",
|
||||
mimeType: "image/png",
|
||||
dataBase64: "iVBORw0KGgo=",
|
||||
width: 390,
|
||||
height: 1200,
|
||||
},
|
||||
},
|
||||
}).payload.result,
|
||||
).toEqual({
|
||||
command: "full_page_screenshot",
|
||||
browserId: "browser-1",
|
||||
mimeType: "image/png",
|
||||
dataBase64: "iVBORw0KGgo=",
|
||||
width: 390,
|
||||
height: 1200,
|
||||
});
|
||||
|
||||
expect(
|
||||
BrowserAutomationExecuteResponseSchema.parse({
|
||||
type: "browser.automation.execute.response",
|
||||
payload: {
|
||||
requestId: "req-pdf",
|
||||
ok: true,
|
||||
result: {
|
||||
command: "pdf",
|
||||
browserId: "browser-1",
|
||||
mimeType: "application/pdf",
|
||||
dataBase64: "JVBERi0xLjQ=",
|
||||
},
|
||||
},
|
||||
}).payload.result,
|
||||
).toEqual({
|
||||
command: "pdf",
|
||||
browserId: "browser-1",
|
||||
mimeType: "application/pdf",
|
||||
dataBase64: "JVBERi0xLjQ=",
|
||||
});
|
||||
});
|
||||
|
||||
test("parses browser download and upload commands and responses", () => {
|
||||
expect(
|
||||
BrowserAutomationExecuteRequestSchema.parse({
|
||||
type: "browser.automation.execute.request",
|
||||
requestId: "req-download",
|
||||
command: {
|
||||
command: "download",
|
||||
args: { browserId: "browser-1", url: "https://example.com/file.txt" },
|
||||
},
|
||||
}).command,
|
||||
).toEqual({
|
||||
command: "download",
|
||||
args: { browserId: "browser-1", url: "https://example.com/file.txt" },
|
||||
});
|
||||
|
||||
expect(
|
||||
BrowserAutomationExecuteRequestSchema.parse({
|
||||
type: "browser.automation.execute.request",
|
||||
requestId: "req-upload",
|
||||
command: {
|
||||
command: "upload",
|
||||
args: { browserId: "browser-1", ref: "@e1", filePaths: ["/tmp/file.txt"] },
|
||||
},
|
||||
}).command,
|
||||
).toEqual({
|
||||
command: "upload",
|
||||
args: { browserId: "browser-1", ref: "@e1", filePaths: ["/tmp/file.txt"] },
|
||||
});
|
||||
|
||||
expect(
|
||||
BrowserAutomationExecuteResponseSchema.parse({
|
||||
type: "browser.automation.execute.response",
|
||||
payload: {
|
||||
requestId: "req-download",
|
||||
ok: true,
|
||||
result: {
|
||||
command: "download",
|
||||
browserId: "browser-1",
|
||||
url: "https://example.com/file.txt",
|
||||
filePath: "/tmp/file.txt",
|
||||
totalBytes: 5,
|
||||
state: "completed",
|
||||
},
|
||||
},
|
||||
}).payload.result,
|
||||
).toEqual({
|
||||
command: "download",
|
||||
browserId: "browser-1",
|
||||
url: "https://example.com/file.txt",
|
||||
filePath: "/tmp/file.txt",
|
||||
totalBytes: 5,
|
||||
state: "completed",
|
||||
});
|
||||
|
||||
expect(
|
||||
BrowserAutomationExecuteResponseSchema.parse({
|
||||
type: "browser.automation.execute.response",
|
||||
payload: {
|
||||
requestId: "req-upload",
|
||||
ok: true,
|
||||
result: {
|
||||
command: "upload",
|
||||
browserId: "browser-1",
|
||||
ref: "@e1",
|
||||
filePaths: ["/tmp/file.txt"],
|
||||
},
|
||||
},
|
||||
}).payload.result,
|
||||
).toEqual({
|
||||
command: "upload",
|
||||
browserId: "browser-1",
|
||||
ref: "@e1",
|
||||
filePaths: ["/tmp/file.txt"],
|
||||
});
|
||||
});
|
||||
|
||||
test("parses stable model-actionable error responses", () => {
|
||||
const parsed = BrowserAutomationExecuteResponseSchema.parse({
|
||||
type: "browser.automation.execute.response",
|
||||
payload: {
|
||||
requestId: "req-1",
|
||||
ok: false,
|
||||
error: {
|
||||
code: "browser_no_desktop",
|
||||
message: "No desktop browser automation client is connected.",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(parsed.payload).toEqual({
|
||||
requestId: "req-1",
|
||||
ok: false,
|
||||
error: {
|
||||
code: "browser_no_desktop",
|
||||
message: "No desktop browser automation client is connected.",
|
||||
retryable: false,
|
||||
},
|
||||
expect(parsed).toMatchObject({
|
||||
success: false,
|
||||
error: { issues: [expect.objectContaining({ message: BROWSER_ID_MESSAGE })] },
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -3,7 +3,6 @@ import { z } from "zod";
|
||||
export const BrowserAutomationErrorCodeSchema = z.enum([
|
||||
"browser_disabled",
|
||||
"browser_no_desktop",
|
||||
"browser_no_tab",
|
||||
"browser_tab_not_found",
|
||||
"browser_tab_closed",
|
||||
"browser_timeout",
|
||||
@@ -14,10 +13,23 @@ export const BrowserAutomationErrorCodeSchema = z.enum([
|
||||
"browser_unknown_error",
|
||||
]);
|
||||
|
||||
const BrowserAutomationTabTargetSchema = z.object({
|
||||
workspaceId: z.string().min(1).optional(),
|
||||
browserId: z.string().min(1).optional(),
|
||||
});
|
||||
const BROWSER_AUTOMATION_BROWSER_ID_PATTERN =
|
||||
/^(?:[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}|\d{13,}-[0-9a-f]+)$/i;
|
||||
const BROWSER_AUTOMATION_BROWSER_ID_MESSAGE =
|
||||
"browserId must be a real id returned by browser_new_tab or browser_list_tabs";
|
||||
const BROWSER_AUTOMATION_WAIT_CONDITION_MESSAGE =
|
||||
"browser_wait requires exactly one of text or url";
|
||||
|
||||
export const BrowserAutomationBrowserIdSchema = z
|
||||
.string({ error: () => BROWSER_AUTOMATION_BROWSER_ID_MESSAGE })
|
||||
.min(1, BROWSER_AUTOMATION_BROWSER_ID_MESSAGE)
|
||||
.regex(BROWSER_AUTOMATION_BROWSER_ID_PATTERN, BROWSER_AUTOMATION_BROWSER_ID_MESSAGE);
|
||||
|
||||
const BrowserAutomationTabTargetSchema = z
|
||||
.object({
|
||||
browserId: BrowserAutomationBrowserIdSchema,
|
||||
})
|
||||
.strict();
|
||||
|
||||
const BrowserAutomationRefSchema = z.string().regex(/^@e\d+$/);
|
||||
const BrowserAutomationHttpUrlSchema = z
|
||||
@@ -30,31 +42,27 @@ const BrowserAutomationHttpUrlSchema = z
|
||||
|
||||
export const BrowserAutomationListTabsCommandSchema = z.object({
|
||||
command: z.literal("list_tabs"),
|
||||
args: z
|
||||
.object({
|
||||
workspaceId: z.string().min(1).optional(),
|
||||
})
|
||||
.default({}),
|
||||
args: z.object({}).strict().default({}),
|
||||
});
|
||||
|
||||
export const BrowserAutomationNewTabCommandSchema = z.object({
|
||||
command: z.literal("new_tab"),
|
||||
args: z
|
||||
.object({
|
||||
workspaceId: z.string().min(1).optional(),
|
||||
url: BrowserAutomationHttpUrlSchema.optional(),
|
||||
})
|
||||
.strict()
|
||||
.default({}),
|
||||
});
|
||||
|
||||
export const BrowserAutomationPageInfoCommandSchema = z.object({
|
||||
command: z.literal("page_info"),
|
||||
args: BrowserAutomationTabTargetSchema.default({}),
|
||||
args: BrowserAutomationTabTargetSchema,
|
||||
});
|
||||
|
||||
export const BrowserAutomationSnapshotCommandSchema = z.object({
|
||||
command: z.literal("snapshot"),
|
||||
args: BrowserAutomationTabTargetSchema.default({}),
|
||||
args: BrowserAutomationTabTargetSchema,
|
||||
});
|
||||
|
||||
export const BrowserAutomationClickCommandSchema = z.object({
|
||||
@@ -78,6 +86,8 @@ export const BrowserAutomationWaitCommandSchema = z.object({
|
||||
text: z.string().min(1).optional(),
|
||||
url: z.string().min(1).optional(),
|
||||
timeoutMs: z.number().int().positive().max(30_000).optional(),
|
||||
}).refine((args) => Number(Boolean(args.text)) + Number(Boolean(args.url)) === 1, {
|
||||
message: BROWSER_AUTOMATION_WAIT_CONDITION_MESSAGE,
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -106,27 +116,27 @@ export const BrowserAutomationNavigateCommandSchema = z.object({
|
||||
|
||||
export const BrowserAutomationBackCommandSchema = z.object({
|
||||
command: z.literal("back"),
|
||||
args: BrowserAutomationTabTargetSchema.default({}),
|
||||
args: BrowserAutomationTabTargetSchema,
|
||||
});
|
||||
|
||||
export const BrowserAutomationForwardCommandSchema = z.object({
|
||||
command: z.literal("forward"),
|
||||
args: BrowserAutomationTabTargetSchema.default({}),
|
||||
args: BrowserAutomationTabTargetSchema,
|
||||
});
|
||||
|
||||
export const BrowserAutomationReloadCommandSchema = z.object({
|
||||
command: z.literal("reload"),
|
||||
args: BrowserAutomationTabTargetSchema.default({}),
|
||||
args: BrowserAutomationTabTargetSchema,
|
||||
});
|
||||
|
||||
export const BrowserAutomationScreenshotCommandSchema = z.object({
|
||||
command: z.literal("screenshot"),
|
||||
args: BrowserAutomationTabTargetSchema.default({}),
|
||||
args: BrowserAutomationTabTargetSchema,
|
||||
});
|
||||
|
||||
export const BrowserAutomationFullPageScreenshotCommandSchema = z.object({
|
||||
command: z.literal("full_page_screenshot"),
|
||||
args: BrowserAutomationTabTargetSchema.default({}),
|
||||
args: BrowserAutomationTabTargetSchema,
|
||||
});
|
||||
|
||||
export const BrowserAutomationPdfCommandSchema = z.object({
|
||||
@@ -134,7 +144,7 @@ export const BrowserAutomationPdfCommandSchema = z.object({
|
||||
args: BrowserAutomationTabTargetSchema.extend({
|
||||
landscape: z.boolean().optional(),
|
||||
printBackground: z.boolean().default(true),
|
||||
}).default({ printBackground: true }),
|
||||
}),
|
||||
});
|
||||
|
||||
export const BrowserAutomationDownloadCommandSchema = z.object({
|
||||
@@ -202,12 +212,12 @@ export const BrowserAutomationLogsCommandSchema = z.object({
|
||||
command: z.literal("logs"),
|
||||
args: BrowserAutomationTabTargetSchema.extend({
|
||||
maxEntries: z.number().int().positive().max(200).default(50),
|
||||
}).default({ maxEntries: 50 }),
|
||||
}),
|
||||
});
|
||||
|
||||
export const BrowserAutomationStorageCommandSchema = z.object({
|
||||
command: z.literal("storage"),
|
||||
args: BrowserAutomationTabTargetSchema.default({}),
|
||||
args: BrowserAutomationTabTargetSchema,
|
||||
});
|
||||
|
||||
const BrowserAutomationViewportInputSchema = z.object({
|
||||
@@ -227,7 +237,7 @@ export const BrowserAutomationEnvironmentCommandSchema = z.object({
|
||||
args: BrowserAutomationTabTargetSchema.extend({
|
||||
viewport: BrowserAutomationViewportInputSchema.optional(),
|
||||
geolocation: BrowserAutomationGeolocationInputSchema.optional(),
|
||||
}).default({}),
|
||||
}),
|
||||
});
|
||||
|
||||
export const BrowserAutomationSetBackgroundCommandSchema = z.object({
|
||||
@@ -269,7 +279,7 @@ export const BrowserAutomationCommandSchema = z.discriminatedUnion("command", [
|
||||
]);
|
||||
|
||||
export const BrowserAutomationTabInfoSchema = z.object({
|
||||
browserId: z.string().min(1),
|
||||
browserId: BrowserAutomationBrowserIdSchema,
|
||||
workspaceId: z.string().min(1).optional(),
|
||||
url: z.string(),
|
||||
title: z.string(),
|
||||
@@ -286,7 +296,7 @@ export const BrowserAutomationListTabsResultSchema = z.object({
|
||||
|
||||
export const BrowserAutomationNewTabResultSchema = z.object({
|
||||
command: z.literal("new_tab"),
|
||||
browserId: z.string().min(1),
|
||||
browserId: BrowserAutomationBrowserIdSchema,
|
||||
workspaceId: z.string().min(1),
|
||||
url: z.string().min(1),
|
||||
});
|
||||
@@ -307,7 +317,7 @@ export const BrowserAutomationSnapshotElementSchema = z.object({
|
||||
|
||||
export const BrowserAutomationSnapshotResultSchema = z.object({
|
||||
command: z.literal("snapshot"),
|
||||
browserId: z.string().min(1),
|
||||
browserId: BrowserAutomationBrowserIdSchema,
|
||||
workspaceId: z.string().min(1).optional(),
|
||||
url: z.string(),
|
||||
title: z.string(),
|
||||
@@ -316,59 +326,59 @@ export const BrowserAutomationSnapshotResultSchema = z.object({
|
||||
|
||||
export const BrowserAutomationClickResultSchema = z.object({
|
||||
command: z.literal("click"),
|
||||
browserId: z.string().min(1),
|
||||
browserId: BrowserAutomationBrowserIdSchema,
|
||||
ref: BrowserAutomationRefSchema,
|
||||
});
|
||||
|
||||
export const BrowserAutomationFillResultSchema = z.object({
|
||||
command: z.literal("fill"),
|
||||
browserId: z.string().min(1),
|
||||
browserId: BrowserAutomationBrowserIdSchema,
|
||||
ref: BrowserAutomationRefSchema,
|
||||
});
|
||||
|
||||
export const BrowserAutomationWaitResultSchema = z.object({
|
||||
command: z.literal("wait"),
|
||||
browserId: z.string().min(1),
|
||||
browserId: BrowserAutomationBrowserIdSchema,
|
||||
matched: z.enum(["text", "url"]),
|
||||
});
|
||||
|
||||
export const BrowserAutomationTypeResultSchema = z.object({
|
||||
command: z.literal("type"),
|
||||
browserId: z.string().min(1),
|
||||
browserId: BrowserAutomationBrowserIdSchema,
|
||||
ref: BrowserAutomationRefSchema.optional(),
|
||||
});
|
||||
|
||||
export const BrowserAutomationKeypressResultSchema = z.object({
|
||||
command: z.literal("keypress"),
|
||||
browserId: z.string().min(1),
|
||||
browserId: BrowserAutomationBrowserIdSchema,
|
||||
key: z.string().min(1),
|
||||
ref: BrowserAutomationRefSchema.optional(),
|
||||
});
|
||||
|
||||
export const BrowserAutomationNavigateResultSchema = z.object({
|
||||
command: z.literal("navigate"),
|
||||
browserId: z.string().min(1),
|
||||
browserId: BrowserAutomationBrowserIdSchema,
|
||||
url: z.string().min(1),
|
||||
});
|
||||
|
||||
export const BrowserAutomationBackResultSchema = z.object({
|
||||
command: z.literal("back"),
|
||||
browserId: z.string().min(1),
|
||||
browserId: BrowserAutomationBrowserIdSchema,
|
||||
});
|
||||
|
||||
export const BrowserAutomationForwardResultSchema = z.object({
|
||||
command: z.literal("forward"),
|
||||
browserId: z.string().min(1),
|
||||
browserId: BrowserAutomationBrowserIdSchema,
|
||||
});
|
||||
|
||||
export const BrowserAutomationReloadResultSchema = z.object({
|
||||
command: z.literal("reload"),
|
||||
browserId: z.string().min(1),
|
||||
browserId: BrowserAutomationBrowserIdSchema,
|
||||
});
|
||||
|
||||
export const BrowserAutomationScreenshotResultSchema = z.object({
|
||||
command: z.literal("screenshot"),
|
||||
browserId: z.string().min(1),
|
||||
browserId: BrowserAutomationBrowserIdSchema,
|
||||
mimeType: z.literal("image/png"),
|
||||
dataBase64: z.string().min(1),
|
||||
width: z.number().int().nonnegative(),
|
||||
@@ -377,7 +387,7 @@ export const BrowserAutomationScreenshotResultSchema = z.object({
|
||||
|
||||
export const BrowserAutomationFullPageScreenshotResultSchema = z.object({
|
||||
command: z.literal("full_page_screenshot"),
|
||||
browserId: z.string().min(1),
|
||||
browserId: BrowserAutomationBrowserIdSchema,
|
||||
mimeType: z.literal("image/png"),
|
||||
dataBase64: z.string().min(1),
|
||||
width: z.number().int().nonnegative(),
|
||||
@@ -386,14 +396,14 @@ export const BrowserAutomationFullPageScreenshotResultSchema = z.object({
|
||||
|
||||
export const BrowserAutomationPdfResultSchema = z.object({
|
||||
command: z.literal("pdf"),
|
||||
browserId: z.string().min(1),
|
||||
browserId: BrowserAutomationBrowserIdSchema,
|
||||
mimeType: z.literal("application/pdf"),
|
||||
dataBase64: z.string().min(1),
|
||||
});
|
||||
|
||||
export const BrowserAutomationDownloadResultSchema = z.object({
|
||||
command: z.literal("download"),
|
||||
browserId: z.string().min(1),
|
||||
browserId: BrowserAutomationBrowserIdSchema,
|
||||
url: z.string().min(1),
|
||||
filePath: z.string().min(1),
|
||||
totalBytes: z.number().int().nonnegative().optional(),
|
||||
@@ -402,46 +412,46 @@ export const BrowserAutomationDownloadResultSchema = z.object({
|
||||
|
||||
export const BrowserAutomationUploadResultSchema = z.object({
|
||||
command: z.literal("upload"),
|
||||
browserId: z.string().min(1),
|
||||
browserId: BrowserAutomationBrowserIdSchema,
|
||||
ref: BrowserAutomationRefSchema,
|
||||
filePaths: z.array(z.string().min(1)).min(1),
|
||||
});
|
||||
|
||||
export const BrowserAutomationFocusResultSchema = z.object({
|
||||
command: z.literal("focus"),
|
||||
browserId: z.string().min(1),
|
||||
browserId: BrowserAutomationBrowserIdSchema,
|
||||
ref: BrowserAutomationRefSchema,
|
||||
});
|
||||
|
||||
export const BrowserAutomationClearResultSchema = z.object({
|
||||
command: z.literal("clear"),
|
||||
browserId: z.string().min(1),
|
||||
browserId: BrowserAutomationBrowserIdSchema,
|
||||
ref: BrowserAutomationRefSchema,
|
||||
});
|
||||
|
||||
export const BrowserAutomationCheckResultSchema = z.object({
|
||||
command: z.literal("check"),
|
||||
browserId: z.string().min(1),
|
||||
browserId: BrowserAutomationBrowserIdSchema,
|
||||
ref: BrowserAutomationRefSchema,
|
||||
checked: z.boolean(),
|
||||
});
|
||||
|
||||
export const BrowserAutomationSelectResultSchema = z.object({
|
||||
command: z.literal("select"),
|
||||
browserId: z.string().min(1),
|
||||
browserId: BrowserAutomationBrowserIdSchema,
|
||||
ref: BrowserAutomationRefSchema,
|
||||
value: z.string(),
|
||||
});
|
||||
|
||||
export const BrowserAutomationHoverResultSchema = z.object({
|
||||
command: z.literal("hover"),
|
||||
browserId: z.string().min(1),
|
||||
browserId: BrowserAutomationBrowserIdSchema,
|
||||
ref: BrowserAutomationRefSchema,
|
||||
});
|
||||
|
||||
export const BrowserAutomationDragResultSchema = z.object({
|
||||
command: z.literal("drag"),
|
||||
browserId: z.string().min(1),
|
||||
browserId: BrowserAutomationBrowserIdSchema,
|
||||
sourceRef: BrowserAutomationRefSchema,
|
||||
targetRef: BrowserAutomationRefSchema,
|
||||
});
|
||||
@@ -466,7 +476,7 @@ export const BrowserAutomationNetworkLogEntrySchema = z.object({
|
||||
|
||||
export const BrowserAutomationLogsResultSchema = z.object({
|
||||
command: z.literal("logs"),
|
||||
browserId: z.string().min(1),
|
||||
browserId: BrowserAutomationBrowserIdSchema,
|
||||
console: z.array(BrowserAutomationConsoleLogEntrySchema),
|
||||
network: z.array(BrowserAutomationNetworkLogEntrySchema),
|
||||
});
|
||||
@@ -488,7 +498,7 @@ export const BrowserAutomationStorageEntrySchema = z.object({
|
||||
|
||||
export const BrowserAutomationStorageResultSchema = z.object({
|
||||
command: z.literal("storage"),
|
||||
browserId: z.string().min(1),
|
||||
browserId: BrowserAutomationBrowserIdSchema,
|
||||
url: z.string(),
|
||||
cookies: z.array(BrowserAutomationCookieEntrySchema),
|
||||
localStorage: z.array(BrowserAutomationStorageEntrySchema),
|
||||
@@ -509,14 +519,14 @@ export const BrowserAutomationGeolocationResultSchema = z.object({
|
||||
|
||||
export const BrowserAutomationEnvironmentResultSchema = z.object({
|
||||
command: z.literal("environment"),
|
||||
browserId: z.string().min(1),
|
||||
browserId: BrowserAutomationBrowserIdSchema,
|
||||
viewport: BrowserAutomationViewportResultSchema,
|
||||
geolocation: BrowserAutomationGeolocationResultSchema.optional(),
|
||||
});
|
||||
|
||||
export const BrowserAutomationSetBackgroundResultSchema = z.object({
|
||||
command: z.literal("set_background"),
|
||||
browserId: z.string().min(1),
|
||||
browserId: BrowserAutomationBrowserIdSchema,
|
||||
color: z.string().min(1),
|
||||
});
|
||||
|
||||
@@ -557,15 +567,16 @@ export const BrowserAutomationErrorSchema = z.object({
|
||||
retryable: z.boolean().default(false),
|
||||
});
|
||||
|
||||
export const BrowserAutomationExecuteRequestSchema = z.object({
|
||||
type: z.literal("browser.automation.execute.request"),
|
||||
requestId: z.string().min(1),
|
||||
agentId: z.string().min(1).optional(),
|
||||
cwd: z.string().min(1).optional(),
|
||||
workspaceId: z.string().min(1).optional(),
|
||||
browserId: z.string().min(1).optional(),
|
||||
command: BrowserAutomationCommandSchema,
|
||||
});
|
||||
export const BrowserAutomationExecuteRequestSchema = z
|
||||
.object({
|
||||
type: z.literal("browser.automation.execute.request"),
|
||||
requestId: z.string().min(1),
|
||||
agentId: z.string().min(1).optional(),
|
||||
cwd: z.string().min(1).optional(),
|
||||
workspaceId: z.string().min(1).optional(),
|
||||
command: BrowserAutomationCommandSchema,
|
||||
})
|
||||
.strict();
|
||||
|
||||
export const BrowserAutomationExecuteResponseSchema = z.object({
|
||||
type: z.literal("browser.automation.execute.response"),
|
||||
|
||||
@@ -10,6 +10,8 @@ import {
|
||||
} from "./messages.js";
|
||||
|
||||
describe("browser automation protocol integration", () => {
|
||||
const browserId = "11111111-1111-4111-8111-111111111111";
|
||||
|
||||
test("desktop automation capability parses in hello without narrowing old clients", () => {
|
||||
expect(
|
||||
WSHelloMessageSchema.parse({
|
||||
@@ -39,7 +41,7 @@ describe("browser automation protocol integration", () => {
|
||||
const parsed = SessionOutboundMessageSchema.parse({
|
||||
type: "browser.automation.execute.request",
|
||||
requestId: "req-1",
|
||||
command: { command: "page_info", args: { browserId: "browser-1" } },
|
||||
command: { command: "page_info", args: { browserId } },
|
||||
});
|
||||
|
||||
expect(parsed.type).toBe("browser.automation.execute.request");
|
||||
|
||||
Reference in New Issue
Block a user