| import { afterEach, describe, expect, it, vi } from "vitest"; |
|
|
| const browserClientMocks = vi.hoisted(() => ({ |
| browserCloseTab: vi.fn(async () => ({})), |
| browserFocusTab: vi.fn(async () => ({})), |
| browserOpenTab: vi.fn(async () => ({})), |
| browserProfiles: vi.fn(async () => []), |
| browserSnapshot: vi.fn(async () => ({ |
| ok: true, |
| format: "ai", |
| targetId: "t1", |
| url: "https://example.com", |
| snapshot: "ok", |
| })), |
| browserStart: vi.fn(async () => ({})), |
| browserStatus: vi.fn(async () => ({ |
| ok: true, |
| running: true, |
| pid: 1, |
| cdpPort: 18792, |
| cdpUrl: "http://127.0.0.1:18792", |
| })), |
| browserStop: vi.fn(async () => ({})), |
| browserTabs: vi.fn(async () => []), |
| })); |
| vi.mock("../../browser/client.js", () => browserClientMocks); |
|
|
| const browserConfigMocks = vi.hoisted(() => ({ |
| resolveBrowserConfig: vi.fn(() => ({ |
| enabled: true, |
| controlPort: 18791, |
| })), |
| })); |
| vi.mock("../../browser/config.js", () => browserConfigMocks); |
|
|
| const nodesUtilsMocks = vi.hoisted(() => ({ |
| listNodes: vi.fn(async () => []), |
| })); |
| vi.mock("./nodes-utils.js", async () => { |
| const actual = await vi.importActual<typeof import("./nodes-utils.js")>("./nodes-utils.js"); |
| return { |
| ...actual, |
| listNodes: nodesUtilsMocks.listNodes, |
| }; |
| }); |
|
|
| const gatewayMocks = vi.hoisted(() => ({ |
| callGatewayTool: vi.fn(async () => ({ |
| ok: true, |
| payload: { result: { ok: true, running: true } }, |
| })), |
| })); |
| vi.mock("./gateway.js", () => gatewayMocks); |
|
|
| const configMocks = vi.hoisted(() => ({ |
| loadConfig: vi.fn(() => ({ browser: {} })), |
| })); |
| vi.mock("../../config/config.js", () => configMocks); |
|
|
| const toolCommonMocks = vi.hoisted(() => ({ |
| imageResultFromFile: vi.fn(), |
| })); |
| vi.mock("./common.js", async () => { |
| const actual = await vi.importActual<typeof import("./common.js")>("./common.js"); |
| return { |
| ...actual, |
| imageResultFromFile: toolCommonMocks.imageResultFromFile, |
| }; |
| }); |
|
|
| import { DEFAULT_AI_SNAPSHOT_MAX_CHARS } from "../../browser/constants.js"; |
| import { createBrowserTool } from "./browser-tool.js"; |
|
|
| describe("browser tool snapshot maxChars", () => { |
| afterEach(() => { |
| vi.clearAllMocks(); |
| configMocks.loadConfig.mockReturnValue({ browser: {} }); |
| nodesUtilsMocks.listNodes.mockResolvedValue([]); |
| }); |
|
|
| it("applies the default ai snapshot limit", async () => { |
| const tool = createBrowserTool(); |
| await tool.execute?.(null, { action: "snapshot", snapshotFormat: "ai" }); |
|
|
| expect(browserClientMocks.browserSnapshot).toHaveBeenCalledWith( |
| undefined, |
| expect.objectContaining({ |
| format: "ai", |
| maxChars: DEFAULT_AI_SNAPSHOT_MAX_CHARS, |
| }), |
| ); |
| }); |
|
|
| it("respects an explicit maxChars override", async () => { |
| const tool = createBrowserTool(); |
| const override = 2_000; |
| await tool.execute?.(null, { |
| action: "snapshot", |
| snapshotFormat: "ai", |
| maxChars: override, |
| }); |
|
|
| expect(browserClientMocks.browserSnapshot).toHaveBeenCalledWith( |
| undefined, |
| expect.objectContaining({ |
| maxChars: override, |
| }), |
| ); |
| }); |
|
|
| it("skips the default when maxChars is explicitly zero", async () => { |
| const tool = createBrowserTool(); |
| await tool.execute?.(null, { |
| action: "snapshot", |
| snapshotFormat: "ai", |
| maxChars: 0, |
| }); |
|
|
| expect(browserClientMocks.browserSnapshot).toHaveBeenCalled(); |
| const [, opts] = browserClientMocks.browserSnapshot.mock.calls.at(-1) ?? []; |
| expect(Object.hasOwn(opts ?? {}, "maxChars")).toBe(false); |
| }); |
|
|
| it("lists profiles", async () => { |
| const tool = createBrowserTool(); |
| await tool.execute?.(null, { action: "profiles" }); |
|
|
| expect(browserClientMocks.browserProfiles).toHaveBeenCalledWith(undefined); |
| }); |
|
|
| it("passes refs mode through to browser snapshot", async () => { |
| const tool = createBrowserTool(); |
| await tool.execute?.(null, { action: "snapshot", snapshotFormat: "ai", refs: "aria" }); |
|
|
| expect(browserClientMocks.browserSnapshot).toHaveBeenCalledWith( |
| undefined, |
| expect.objectContaining({ |
| format: "ai", |
| refs: "aria", |
| }), |
| ); |
| }); |
|
|
| it("uses config snapshot defaults when mode is not provided", async () => { |
| configMocks.loadConfig.mockReturnValue({ |
| browser: { snapshotDefaults: { mode: "efficient" } }, |
| }); |
| const tool = createBrowserTool(); |
| await tool.execute?.(null, { action: "snapshot", snapshotFormat: "ai" }); |
|
|
| expect(browserClientMocks.browserSnapshot).toHaveBeenCalledWith( |
| undefined, |
| expect.objectContaining({ |
| mode: "efficient", |
| }), |
| ); |
| }); |
|
|
| it("does not apply config snapshot defaults to aria snapshots", async () => { |
| configMocks.loadConfig.mockReturnValue({ |
| browser: { snapshotDefaults: { mode: "efficient" } }, |
| }); |
| const tool = createBrowserTool(); |
| await tool.execute?.(null, { action: "snapshot", snapshotFormat: "aria" }); |
|
|
| expect(browserClientMocks.browserSnapshot).toHaveBeenCalled(); |
| const [, opts] = browserClientMocks.browserSnapshot.mock.calls.at(-1) ?? []; |
| expect(opts?.mode).toBeUndefined(); |
| }); |
|
|
| it("defaults to host when using profile=chrome (even in sandboxed sessions)", async () => { |
| const tool = createBrowserTool({ sandboxBridgeUrl: "http://127.0.0.1:9999" }); |
| await tool.execute?.(null, { action: "snapshot", profile: "chrome", snapshotFormat: "ai" }); |
|
|
| expect(browserClientMocks.browserSnapshot).toHaveBeenCalledWith( |
| undefined, |
| expect.objectContaining({ |
| profile: "chrome", |
| }), |
| ); |
| }); |
|
|
| it("routes to node proxy when target=node", async () => { |
| nodesUtilsMocks.listNodes.mockResolvedValue([ |
| { |
| nodeId: "node-1", |
| displayName: "Browser Node", |
| connected: true, |
| caps: ["browser"], |
| commands: ["browser.proxy"], |
| }, |
| ]); |
| const tool = createBrowserTool(); |
| await tool.execute?.(null, { action: "status", target: "node" }); |
|
|
| expect(gatewayMocks.callGatewayTool).toHaveBeenCalledWith( |
| "node.invoke", |
| { timeoutMs: 20000 }, |
| expect.objectContaining({ |
| nodeId: "node-1", |
| command: "browser.proxy", |
| }), |
| ); |
| expect(browserClientMocks.browserStatus).not.toHaveBeenCalled(); |
| }); |
|
|
| it("keeps sandbox bridge url when node proxy is available", async () => { |
| nodesUtilsMocks.listNodes.mockResolvedValue([ |
| { |
| nodeId: "node-1", |
| displayName: "Browser Node", |
| connected: true, |
| caps: ["browser"], |
| commands: ["browser.proxy"], |
| }, |
| ]); |
| const tool = createBrowserTool({ sandboxBridgeUrl: "http://127.0.0.1:9999" }); |
| await tool.execute?.(null, { action: "status" }); |
|
|
| expect(browserClientMocks.browserStatus).toHaveBeenCalledWith( |
| "http://127.0.0.1:9999", |
| expect.objectContaining({ profile: undefined }), |
| ); |
| expect(gatewayMocks.callGatewayTool).not.toHaveBeenCalled(); |
| }); |
|
|
| it("keeps chrome profile on host when node proxy is available", async () => { |
| nodesUtilsMocks.listNodes.mockResolvedValue([ |
| { |
| nodeId: "node-1", |
| displayName: "Browser Node", |
| connected: true, |
| caps: ["browser"], |
| commands: ["browser.proxy"], |
| }, |
| ]); |
| const tool = createBrowserTool(); |
| await tool.execute?.(null, { action: "status", profile: "chrome" }); |
|
|
| expect(browserClientMocks.browserStatus).toHaveBeenCalledWith( |
| undefined, |
| expect.objectContaining({ profile: "chrome" }), |
| ); |
| expect(gatewayMocks.callGatewayTool).not.toHaveBeenCalled(); |
| }); |
| }); |
|
|
| describe("browser tool snapshot labels", () => { |
| afterEach(() => { |
| vi.clearAllMocks(); |
| configMocks.loadConfig.mockReturnValue({ browser: {} }); |
| }); |
|
|
| it("returns image + text when labels are requested", async () => { |
| const tool = createBrowserTool(); |
| const imageResult = { |
| content: [ |
| { type: "text", text: "label text" }, |
| { type: "image", data: "base64", mimeType: "image/png" }, |
| ], |
| details: { path: "/tmp/snap.png" }, |
| }; |
|
|
| toolCommonMocks.imageResultFromFile.mockResolvedValueOnce(imageResult); |
| browserClientMocks.browserSnapshot.mockResolvedValueOnce({ |
| ok: true, |
| format: "ai", |
| targetId: "t1", |
| url: "https://example.com", |
| snapshot: "label text", |
| imagePath: "/tmp/snap.png", |
| }); |
|
|
| const result = await tool.execute?.(null, { |
| action: "snapshot", |
| snapshotFormat: "ai", |
| labels: true, |
| }); |
|
|
| expect(toolCommonMocks.imageResultFromFile).toHaveBeenCalledWith( |
| expect.objectContaining({ |
| path: "/tmp/snap.png", |
| extraText: "label text", |
| }), |
| ); |
| expect(result).toEqual(imageResult); |
| expect(result?.content).toHaveLength(2); |
| expect(result?.content?.[0]).toMatchObject({ type: "text", text: "label text" }); |
| expect(result?.content?.[1]).toMatchObject({ type: "image" }); |
| }); |
| }); |
|
|