import type { AdapterContext, ComputerRef } from "@rakazo/adapter-kit"; import { describe, expect, it } from "vitest"; import { browserActFromTool, browserNavigateFromTool, browserSnapshotFromTool, parseBrowserActions, } from "./browser-tools.js"; import { filterPageBrowserTools, PAGE_BROWSER_TOOL_NAMES } from "./executor.js"; import { FakeBrowserProvider } from "./fake-browser.js"; const context: AdapterContext = { operationId: "1", traceId: "1", spaceId: "w", userId: "u", signal: new AbortController().signal, }; const computer: ComputerRef = { id: "c1", botId: "b1", kind: "fake", providerRef: "c1", }; describe("browser tools", () => { it("parses click/fill/type actions and rejects bad batches", () => { expect( parseBrowserActions([ { kind: "fill", ref: "e1", text: "hi" }, { kind: "click", ref: "e2" }, ]), ).toEqual([ { kind: "fill", ref: "e1", text: "hi" }, { kind: "click", ref: "e2" }, ]); expect(() => parseBrowserActions([])).toThrow(/at least one/i); expect(() => parseBrowserActions([{ kind: "click" }])).toThrow(/ref/i); expect(() => parseBrowserActions([{ kind: "fill", ref: "e1" }])).toThrow(/text/i); }); it("rejects URL credentials before invoking the browser", async () => { const browser = new FakeBrowserProvider(); browser.navigateError = new Error("must not invoke browser"); for (const scheme of ["http", "https"]) { expect( await browserNavigateFromTool(browser, computer, context, { url: `${scheme}://example:fake-password@example.test`, }), ).toMatchObject({ error: expect.stringContaining("credentials") }); } }); it("navigates, snapshots, and acts through the tool helpers", async () => { const browser = new FakeBrowserProvider({ pages: { "https://example.test/x": { title: "Demo", html: `Demo `, }, }, }); const navigated = await browserNavigateFromTool(browser, computer, context, { url: "https://example.test/x", }); expect(navigated).toMatchObject({ title: "Demo" }); expect("fallback" in navigated && navigated.fallback).toBeFalsy(); const snap = await browserSnapshotFromTool(browser, computer, context, {}); expect(snap).toMatchObject({ title: "Demo" }); expect(String((snap as { tree?: string }).tree)).toMatch(/\[e\d+\]/); const nameRef = (snap as { elements: Array<{ ref: string; name: string }> }).elements.find( (el) => el.name.includes("Name"), )!.ref; const acted = await browserActFromTool(browser, computer, context, { actions: [{ kind: "fill", ref: nameRef, text: "Ada" }], }); expect(acted).toMatchObject({ ok: true, completed: 1 }); }); it("toggles checkboxes via click without undoing activation", async () => { const browser = new FakeBrowserProvider({ pages: { "https://example.test/form": { title: "Form", html: `Form `, }, }, }); await browserNavigateFromTool(browser, computer, context, { url: "https://example.test/form" }); const before = await browserSnapshotFromTool(browser, computer, context, {}); const agree = ( before as { elements: Array<{ ref: string; name: string; value?: string }> } ).elements.find((el) => el.name.includes("Agree"))!; expect(agree.value).toMatch(/unchecked|false|^$/i); const acted = await browserActFromTool(browser, computer, context, { actions: [{ kind: "click", ref: agree.ref }], }); expect(acted).toMatchObject({ ok: true }); const after = await browserSnapshotFromTool(browser, computer, context, {}); const toggled = (after as { elements: Array<{ name: string; value?: string }> }).elements.find( (el) => el.name.includes("Agree"), )!; expect(toggled.value).toMatch(/checked|true/i); }); it("falls back to computer_act for link navigation the in-process page cannot load", async () => { const browser = new FakeBrowserProvider({ pages: { "https://example.test/start": { title: "Start", html: `Start Next `, }, }, }); await browserNavigateFromTool(browser, computer, context, { url: "https://example.test/start", }); const snap = await browserSnapshotFromTool(browser, computer, context, {}); const link = (snap as { elements: Array<{ ref: string; name: string }> }).elements.find((el) => el.name.includes("Next"), )!; const acted = await browserActFromTool(browser, computer, context, { actions: [{ kind: "click", ref: link.ref }], }); expect(acted).toMatchObject({ ok: false, fallback: "computer_act", error: expect.stringMatching(/cannot navigate links|computer_act/i), }); }); it("surfaces computer_act fallback from the tool layer", async () => { const browser = new FakeBrowserProvider(); browser.forceFallback = "cannot attach to page"; const result = await browserNavigateFromTool(browser, computer, context, { url: "https://example.test", }); expect(result).toMatchObject({ fallback: "computer_act", note: expect.stringMatching(/computer_act/i), }); }); it("hides page browser tools when the computer is not graphical", () => { const tools = [...PAGE_BROWSER_TOOL_NAMES].map((name) => ({ name })); expect(filterPageBrowserTools(tools, true)).toHaveLength(3); expect(filterPageBrowserTools(tools, false)).toEqual([]); expect(filterPageBrowserTools([{ name: "computer_act" }, ...tools], false)).toEqual([ { name: "computer_act" }, ]); }); });