From 186ecd21610c57e52945dfd2a32834a250fc6c04 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sat, 14 Feb 2026 17:29:03 +0000 Subject: [PATCH] refactor(test): reuse browser control server harness --- ...-contract-form-layout-act-commands.test.ts | 323 ++---------------- .../server.control-server.test-harness.ts | 8 + 2 files changed, 31 insertions(+), 300 deletions(-) diff --git a/src/browser/server.agent-contract-form-layout-act-commands.test.ts b/src/browser/server.agent-contract-form-layout-act-commands.test.ts index 9df97d3971..6971fce735 100644 --- a/src/browser/server.agent-contract-form-layout-act-commands.test.ts +++ b/src/browser/server.agent-contract-form-layout-act-commands.test.ts @@ -1,302 +1,25 @@ -import fs from "node:fs/promises"; -import { type AddressInfo, createServer } from "node:net"; -import os from "node:os"; import path from "node:path"; import { fetch as realFetch } from "undici"; -import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; +import { describe, expect, it } from "vitest"; import { DEFAULT_UPLOAD_DIR } from "./paths.js"; +import { + getBrowserControlServerBaseUrl, + getBrowserControlServerTestState, + getPwMocks, + installBrowserControlServerHooks, + setBrowserControlServerEvaluateEnabled, + startBrowserControlServerFromConfig, +} from "./server.control-server.test-harness.js"; -let testPort = 0; -let cdpBaseUrl = ""; -let reachable = false; -let cfgAttachOnly = false; -let cfgEvaluateEnabled = true; -let createTargetId: string | null = null; -let prevGatewayPort: string | undefined; - -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 () => {}), - downloadViaPlaywright: vi.fn(async () => ({ - url: "https://example.com/report.pdf", - suggestedFilename: "report.pdf", - path: "/tmp/report.pdf", - })), - dragViaPlaywright: vi.fn(async () => {}), - evaluateViaPlaywright: vi.fn(async () => "ok"), - fillFormViaPlaywright: vi.fn(async () => {}), - getConsoleMessagesViaPlaywright: vi.fn(async () => []), - hoverViaPlaywright: vi.fn(async () => {}), - scrollIntoViewViaPlaywright: vi.fn(async () => {}), - navigateViaPlaywright: vi.fn(async () => ({ url: "https://example.com" })), - pdfViaPlaywright: vi.fn(async () => ({ buffer: Buffer.from("pdf") })), - pressKeyViaPlaywright: vi.fn(async () => {}), - responseBodyViaPlaywright: vi.fn(async () => ({ - url: "https://example.com/api/data", - status: 200, - headers: { "content-type": "application/json" }, - body: '{"ok":true}', - })), - resizeViewportViaPlaywright: vi.fn(async () => {}), - selectOptionViaPlaywright: vi.fn(async () => {}), - setInputFilesViaPlaywright: vi.fn(async () => {}), - snapshotAiViaPlaywright: vi.fn(async () => ({ snapshot: "ok" })), - traceStopViaPlaywright: vi.fn(async () => {}), - takeScreenshotViaPlaywright: vi.fn(async () => ({ - buffer: Buffer.from("png"), - })), - typeViaPlaywright: vi.fn(async () => {}), - waitForDownloadViaPlaywright: vi.fn(async () => ({ - url: "https://example.com/report.pdf", - suggestedFilename: "report.pdf", - path: "/tmp/report.pdf", - })), - waitForViaPlaywright: vi.fn(async () => {}), -})); - -const chromeUserDataDir = vi.hoisted(() => ({ dir: "/tmp/openclaw" })); - -beforeAll(async () => { - chromeUserDataDir.dir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-chrome-user-data-")); -}); - -afterAll(async () => { - await fs.rm(chromeUserDataDir.dir, { recursive: true, force: true }); -}); - -function makeProc(pid = 123) { - const handlers = new Map void>>(); - return { - pid, - killed: false, - exitCode: null as number | null, - on: (event: string, cb: (...args: unknown[]) => void) => { - handlers.set(event, [...(handlers.get(event) ?? []), cb]); - return undefined; - }, - emitExit: () => { - for (const cb of handlers.get("exit") ?? []) { - cb(0); - } - }, - kill: () => { - return true; - }, - }; -} - -const proc = makeProc(); - -vi.mock("../config/config.js", async (importOriginal) => { - const actual = await importOriginal(); - return { - ...actual, - loadConfig: () => ({ - browser: { - enabled: true, - evaluateEnabled: cfgEvaluateEnabled, - color: "#FF4500", - attachOnly: cfgAttachOnly, - headless: true, - defaultProfile: "openclaw", - profiles: { - openclaw: { cdpPort: testPort + 1, color: "#FF4500" }, - }, - }, - }), - writeConfigFile: vi.fn(async () => {}), - }; -}); - -const launchCalls = vi.hoisted(() => [] as Array<{ port: number }>); -vi.mock("./chrome.js", () => ({ - isChromeCdpReady: vi.fn(async () => reachable), - isChromeReachable: vi.fn(async () => reachable), - launchOpenClawChrome: vi.fn(async (_resolved: unknown, profile: { cdpPort: number }) => { - launchCalls.push({ port: profile.cdpPort }); - reachable = true; - return { - pid: 123, - exe: { kind: "chrome", path: "/fake/chrome" }, - userDataDir: chromeUserDataDir.dir, - cdpPort: profile.cdpPort, - startedAt: Date.now(), - proc, - }; - }), - resolveOpenClawUserDataDir: vi.fn(() => chromeUserDataDir.dir), - stopOpenClawChrome: vi.fn(async () => { - reachable = false; - }), -})); - -vi.mock("./cdp.js", () => ({ - createTargetViaCdp: cdpMocks.createTargetViaCdp, - normalizeCdpWsUrl: vi.fn((wsUrl: string) => wsUrl), - snapshotAria: cdpMocks.snapshotAria, - getHeadersWithAuth: vi.fn(() => ({})), - appendCdpPath: vi.fn((cdpUrl: string, path: string) => { - const base = cdpUrl.replace(/\/$/, ""); - const suffix = path.startsWith("/") ? path : `/${path}`; - return `${base}${suffix}`; - }), -})); - -vi.mock("./pw-ai.js", () => pwMocks); - -vi.mock("../media/store.js", () => ({ - ensureMediaDir: vi.fn(async () => {}), - saveMediaBuffer: vi.fn(async () => ({ path: "/tmp/fake.png" })), -})); - -vi.mock("./screenshot.js", () => ({ - DEFAULT_BROWSER_SCREENSHOT_MAX_BYTES: 128, - DEFAULT_BROWSER_SCREENSHOT_MAX_SIDE: 64, - normalizeBrowserScreenshot: vi.fn(async (buf: Buffer) => ({ - buffer: buf, - contentType: "image/png", - })), -})); - -const { startBrowserControlServerFromConfig, stopBrowserControlServer } = - await import("./server.js"); - -async function getFreePort(): Promise { - while (true) { - const port = await new Promise((resolve, reject) => { - const s = createServer(); - s.once("error", reject); - s.listen(0, "127.0.0.1", () => { - const assigned = (s.address() as AddressInfo).port; - s.close((err) => (err ? reject(err) : resolve(assigned))); - }); - }); - if (port < 65535) { - return port; - } - } -} - -function makeResponse( - body: unknown, - init?: { ok?: boolean; status?: number; text?: string }, -): Response { - const ok = init?.ok ?? true; - const status = init?.status ?? 200; - const text = init?.text ?? ""; - return { - ok, - status, - json: async () => body, - text: async () => text, - } as unknown as Response; -} +const state = getBrowserControlServerTestState(); +const pwMocks = getPwMocks(); describe("browser control server", () => { - beforeEach(async () => { - reachable = false; - cfgAttachOnly = false; - cfgEvaluateEnabled = true; - 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(); - cdpBaseUrl = `http://127.0.0.1:${testPort + 1}`; - prevGatewayPort = process.env.OPENCLAW_GATEWAY_PORT; - process.env.OPENCLAW_GATEWAY_PORT = String(testPort - 2); - - // Minimal CDP JSON endpoints used by the server. - let putNewCalls = 0; - vi.stubGlobal( - "fetch", - vi.fn(async (url: string, init?: RequestInit) => { - const u = String(url); - if (u.includes("/json/list")) { - if (!reachable) { - return makeResponse([]); - } - return makeResponse([ - { - id: "abcd1234", - title: "Tab", - url: "https://example.com", - webSocketDebuggerUrl: "ws://127.0.0.1/devtools/page/abcd1234", - type: "page", - }, - { - id: "abce9999", - title: "Other", - url: "https://other", - webSocketDebuggerUrl: "ws://127.0.0.1/devtools/page/abce9999", - type: "page", - }, - ]); - } - if (u.includes("/json/new?")) { - if (init?.method === "PUT") { - putNewCalls += 1; - if (putNewCalls === 1) { - return makeResponse({}, { ok: false, status: 405, text: "" }); - } - } - return makeResponse({ - id: "newtab1", - title: "", - url: "about:blank", - webSocketDebuggerUrl: "ws://127.0.0.1/devtools/page/newtab1", - type: "page", - }); - } - if (u.includes("/json/activate/")) { - return makeResponse("ok"); - } - if (u.includes("/json/close/")) { - return makeResponse("ok"); - } - return makeResponse({}, { ok: false, status: 500, text: "unexpected" }); - }), - ); - }); - - afterEach(async () => { - vi.unstubAllGlobals(); - vi.restoreAllMocks(); - if (prevGatewayPort === undefined) { - delete process.env.OPENCLAW_GATEWAY_PORT; - } else { - process.env.OPENCLAW_GATEWAY_PORT = prevGatewayPort; - } - await stopBrowserControlServer(); - }); + installBrowserControlServerHooks(); const startServerAndBase = async () => { await startBrowserControlServerFromConfig(); - const base = `http://127.0.0.1:${testPort}`; + const base = getBrowserControlServerBaseUrl(); await realFetch(`${base}/start`, { method: "POST" }).then((r) => r.json()); return base; }; @@ -324,7 +47,7 @@ describe("browser control server", () => { }); expect(select.ok).toBe(true); expect(pwMocks.selectOptionViaPlaywright).toHaveBeenCalledWith({ - cdpUrl: cdpBaseUrl, + cdpUrl: state.cdpBaseUrl, targetId: "abcd1234", ref: "5", values: ["a", "b"], @@ -336,7 +59,7 @@ describe("browser control server", () => { }); expect(fill.ok).toBe(true); expect(pwMocks.fillFormViaPlaywright).toHaveBeenCalledWith({ - cdpUrl: cdpBaseUrl, + cdpUrl: state.cdpBaseUrl, targetId: "abcd1234", fields: [{ ref: "6", type: "textbox", value: "hello" }], }); @@ -348,7 +71,7 @@ describe("browser control server", () => { }); expect(resize.ok).toBe(true); expect(pwMocks.resizeViewportViaPlaywright).toHaveBeenCalledWith({ - cdpUrl: cdpBaseUrl, + cdpUrl: state.cdpBaseUrl, targetId: "abcd1234", width: 800, height: 600, @@ -360,7 +83,7 @@ describe("browser control server", () => { }); expect(wait.ok).toBe(true); expect(pwMocks.waitForViaPlaywright).toHaveBeenCalledWith({ - cdpUrl: cdpBaseUrl, + cdpUrl: state.cdpBaseUrl, targetId: "abcd1234", timeMs: 5, text: undefined, @@ -375,7 +98,7 @@ describe("browser control server", () => { expect(evalRes.result).toBe("ok"); expect(pwMocks.evaluateViaPlaywright).toHaveBeenCalledWith( expect.objectContaining({ - cdpUrl: cdpBaseUrl, + cdpUrl: state.cdpBaseUrl, targetId: "abcd1234", fn: "() => 1", ref: undefined, @@ -389,7 +112,7 @@ describe("browser control server", () => { it( "blocks act:evaluate when browser.evaluateEnabled=false", async () => { - cfgEvaluateEnabled = false; + setBrowserControlServerEvaluateEnabled(false); const base = await startServerAndBase(); const waitRes = await postJson(`${base}/act`, { @@ -419,7 +142,7 @@ describe("browser control server", () => { }); expect(upload).toMatchObject({ ok: true }); expect(pwMocks.armFileUploadViaPlaywright).toHaveBeenCalledWith({ - cdpUrl: cdpBaseUrl, + cdpUrl: state.cdpBaseUrl, targetId: "abcd1234", // The server resolves paths (which adds a drive letter on Windows for `\\tmp\\...` style roots). paths: [path.resolve(DEFAULT_UPLOAD_DIR, "a.txt")], @@ -533,7 +256,7 @@ describe("browser control server", () => { expect(res.path).toContain("safe-trace.zip"); expect(pwMocks.traceStopViaPlaywright).toHaveBeenCalledWith( expect.objectContaining({ - cdpUrl: cdpBaseUrl, + cdpUrl: state.cdpBaseUrl, targetId: "abcd1234", path: expect.stringContaining("safe-trace.zip"), }), @@ -570,7 +293,7 @@ describe("browser control server", () => { expect(res.ok).toBe(true); expect(pwMocks.waitForDownloadViaPlaywright).toHaveBeenCalledWith( expect.objectContaining({ - cdpUrl: cdpBaseUrl, + cdpUrl: state.cdpBaseUrl, targetId: "abcd1234", path: expect.stringContaining("safe-wait.pdf"), }), @@ -586,7 +309,7 @@ describe("browser control server", () => { expect(res.ok).toBe(true); expect(pwMocks.downloadViaPlaywright).toHaveBeenCalledWith( expect.objectContaining({ - cdpUrl: cdpBaseUrl, + cdpUrl: state.cdpBaseUrl, targetId: "abcd1234", ref: "e12", path: expect.stringContaining("safe-download.pdf"), diff --git a/src/browser/server.control-server.test-harness.ts b/src/browser/server.control-server.test-harness.ts index 630440806c..f878af3fa3 100644 --- a/src/browser/server.control-server.test-harness.ts +++ b/src/browser/server.control-server.test-harness.ts @@ -10,6 +10,7 @@ type HarnessState = { cdpBaseUrl: string; reachable: boolean; cfgAttachOnly: boolean; + cfgEvaluateEnabled: boolean; createTargetId: string | null; prevGatewayPort: string | undefined; }; @@ -19,6 +20,7 @@ const state: HarnessState = { cdpBaseUrl: "", reachable: false, cfgAttachOnly: false, + cfgEvaluateEnabled: true, createTargetId: null, prevGatewayPort: undefined, }; @@ -39,6 +41,10 @@ export function setBrowserControlServerAttachOnly(attachOnly: boolean): void { state.cfgAttachOnly = attachOnly; } +export function setBrowserControlServerEvaluateEnabled(enabled: boolean): void { + state.cfgEvaluateEnabled = enabled; +} + export function setBrowserControlServerReachable(reachable: boolean): void { state.reachable = reachable; } @@ -86,6 +92,7 @@ const pwMocks = vi.hoisted(() => ({ selectOptionViaPlaywright: vi.fn(async () => {}), setInputFilesViaPlaywright: vi.fn(async () => {}), snapshotAiViaPlaywright: vi.fn(async () => ({ snapshot: "ok" })), + traceStopViaPlaywright: vi.fn(async () => {}), takeScreenshotViaPlaywright: vi.fn(async () => ({ buffer: Buffer.from("png"), })), @@ -142,6 +149,7 @@ vi.mock("../config/config.js", async (importOriginal) => { loadConfig: () => ({ browser: { enabled: true, + evaluateEnabled: state.cfgEvaluateEnabled, color: "#FF4500", attachOnly: state.cfgAttachOnly, headless: true,