refactor(channels): dedupe transport and gateway test scaffolds

This commit is contained in:
Peter Steinberger
2026-02-16 14:52:15 +00:00
parent f717a13039
commit 93ca0ed54f
95 changed files with 4068 additions and 5221 deletions

View File

@@ -8,6 +8,12 @@ describe("cdp", () => {
let httpServer: ReturnType<typeof createServer> | null = null;
let wsServer: WebSocketServer | null = null;
const startWsServer = async () => {
wsServer = new WebSocketServer({ port: 0, host: "127.0.0.1" });
await new Promise<void>((resolve) => wsServer?.once("listening", resolve));
return (wsServer.address() as { port: number }).port;
};
afterEach(async () => {
await new Promise<void>((resolve) => {
if (!httpServer) {
@@ -26,9 +32,7 @@ describe("cdp", () => {
});
it("creates a target via the browser websocket", async () => {
wsServer = new WebSocketServer({ port: 0, host: "127.0.0.1" });
await new Promise<void>((resolve) => wsServer?.once("listening", resolve));
const wsPort = (wsServer.address() as { port: number }).port;
const wsPort = await startWsServer();
wsServer.on("connection", (socket) => {
socket.on("message", (data) => {
@@ -75,9 +79,7 @@ describe("cdp", () => {
});
it("evaluates javascript via CDP", async () => {
wsServer = new WebSocketServer({ port: 0, host: "127.0.0.1" });
await new Promise<void>((resolve) => wsServer?.once("listening", resolve));
const wsPort = (wsServer.address() as { port: number }).port;
const wsPort = await startWsServer();
wsServer.on("connection", (socket) => {
socket.on("message", (data) => {
@@ -112,9 +114,7 @@ describe("cdp", () => {
});
it("captures an aria snapshot via CDP", async () => {
wsServer = new WebSocketServer({ port: 0, host: "127.0.0.1" });
await new Promise<void>((resolve) => wsServer?.once("listening", resolve));
const wsPort = (wsServer.address() as { port: number }).port;
const wsPort = await startWsServer();
wsServer.on("connection", (socket) => {
socket.on("message", (data) => {

View File

@@ -7,21 +7,30 @@ import type {
import { buildProfileQuery, withBaseUrl } from "./client-actions-url.js";
import { fetchBrowserJson } from "./client-fetch.js";
function buildQuerySuffix(params: Array<[string, string | boolean | undefined]>): string {
const query = new URLSearchParams();
for (const [key, value] of params) {
if (typeof value === "boolean") {
query.set(key, String(value));
continue;
}
if (typeof value === "string" && value.length > 0) {
query.set(key, value);
}
}
const encoded = query.toString();
return encoded.length > 0 ? `?${encoded}` : "";
}
export async function browserConsoleMessages(
baseUrl: string | undefined,
opts: { level?: string; targetId?: string; profile?: string } = {},
): Promise<{ ok: true; messages: BrowserConsoleMessage[]; targetId: string }> {
const q = new URLSearchParams();
if (opts.level) {
q.set("level", opts.level);
}
if (opts.targetId) {
q.set("targetId", opts.targetId);
}
if (opts.profile) {
q.set("profile", opts.profile);
}
const suffix = q.toString() ? `?${q.toString()}` : "";
const suffix = buildQuerySuffix([
["level", opts.level],
["targetId", opts.targetId],
["profile", opts.profile],
]);
return await fetchBrowserJson<{
ok: true;
messages: BrowserConsoleMessage[];
@@ -46,17 +55,11 @@ export async function browserPageErrors(
baseUrl: string | undefined,
opts: { targetId?: string; clear?: boolean; profile?: string } = {},
): Promise<{ ok: true; targetId: string; errors: BrowserPageError[] }> {
const q = new URLSearchParams();
if (opts.targetId) {
q.set("targetId", opts.targetId);
}
if (typeof opts.clear === "boolean") {
q.set("clear", String(opts.clear));
}
if (opts.profile) {
q.set("profile", opts.profile);
}
const suffix = q.toString() ? `?${q.toString()}` : "";
const suffix = buildQuerySuffix([
["targetId", opts.targetId],
["clear", typeof opts.clear === "boolean" ? opts.clear : undefined],
["profile", opts.profile],
]);
return await fetchBrowserJson<{
ok: true;
targetId: string;
@@ -73,20 +76,12 @@ export async function browserRequests(
profile?: string;
} = {},
): Promise<{ ok: true; targetId: string; requests: BrowserNetworkRequest[] }> {
const q = new URLSearchParams();
if (opts.targetId) {
q.set("targetId", opts.targetId);
}
if (opts.filter) {
q.set("filter", opts.filter);
}
if (typeof opts.clear === "boolean") {
q.set("clear", String(opts.clear));
}
if (opts.profile) {
q.set("profile", opts.profile);
}
const suffix = q.toString() ? `?${q.toString()}` : "";
const suffix = buildQuerySuffix([
["targetId", opts.targetId],
["filter", opts.filter],
["clear", typeof opts.clear === "boolean" ? opts.clear : undefined],
["profile", opts.profile],
]);
return await fetchBrowserJson<{
ok: true;
targetId: string;

View File

@@ -11,6 +11,25 @@ import {
import { browserOpenTab, browserSnapshot, browserStatus, browserTabs } from "./client.js";
describe("browser client", () => {
function stubSnapshotFetch(calls: string[]) {
vi.stubGlobal(
"fetch",
vi.fn(async (url: string) => {
calls.push(url);
return {
ok: true,
json: async () => ({
ok: true,
format: "ai",
targetId: "t1",
url: "https://x",
snapshot: "ok",
}),
} as unknown as Response;
}),
);
}
afterEach(() => {
vi.unstubAllGlobals();
});
@@ -50,22 +69,7 @@ describe("browser client", () => {
it("adds labels + efficient mode query params to snapshots", async () => {
const calls: string[] = [];
vi.stubGlobal(
"fetch",
vi.fn(async (url: string) => {
calls.push(url);
return {
ok: true,
json: async () => ({
ok: true,
format: "ai",
targetId: "t1",
url: "https://x",
snapshot: "ok",
}),
} as unknown as Response;
}),
);
stubSnapshotFetch(calls);
await expect(
browserSnapshot("http://127.0.0.1:18791", {
@@ -84,22 +88,7 @@ describe("browser client", () => {
it("adds refs=aria to snapshots when requested", async () => {
const calls: string[] = [];
vi.stubGlobal(
"fetch",
vi.fn(async (url: string) => {
calls.push(url);
return {
ok: true,
json: async () => ({
ok: true,
format: "ai",
targetId: "t1",
url: "https://x",
snapshot: "ok",
}),
} as unknown as Response;
}),
);
stubSnapshotFetch(calls);
await browserSnapshot("http://127.0.0.1:18791", {
format: "ai",

View File

@@ -1,5 +1,3 @@
import type { AddressInfo } from "node:net";
import { createServer } from "node:http";
import { afterEach, describe, expect, it } from "vitest";
import WebSocket from "ws";
import {
@@ -7,22 +5,7 @@ import {
getChromeExtensionRelayAuthHeaders,
stopChromeExtensionRelayServer,
} from "./extension-relay.js";
async function getFreePort(): Promise<number> {
while (true) {
const port = await new Promise<number>((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;
}
}
}
import { getFreePort } from "./test-port.js";
function waitForOpen(ws: WebSocket) {
return new Promise<void>((resolve, reject) => {

View File

@@ -23,6 +23,38 @@ describe("pw-tools-core", () => {
tmpDirMocks.resolvePreferredOpenClawTmpDir.mockReturnValue("/tmp/openclaw");
});
async function waitForImplicitDownloadOutput(params: {
downloadUrl: string;
suggestedFilename: string;
}) {
let downloadHandler: ((download: unknown) => void) | undefined;
const on = vi.fn((event: string, handler: (download: unknown) => void) => {
if (event === "download") {
downloadHandler = handler;
}
});
const off = vi.fn();
const saveAs = vi.fn(async () => {});
setPwToolsCoreCurrentPage({ on, off });
const p = mod.waitForDownloadViaPlaywright({
cdpUrl: "http://127.0.0.1:18792",
targetId: "T1",
timeoutMs: 1000,
});
await Promise.resolve();
downloadHandler?.({
url: () => params.downloadUrl,
suggestedFilename: () => params.suggestedFilename,
saveAs,
});
const res = await p;
const outPath = vi.mocked(saveAs).mock.calls[0]?.[0];
return { res, outPath };
}
it("waits for the next download and saves it", async () => {
let downloadHandler: ((download: unknown) => void) | undefined;
const on = vi.fn((event: string, handler: (download: unknown) => void) => {
@@ -98,35 +130,11 @@ describe("pw-tools-core", () => {
expect(res.path).toBe(targetPath);
});
it("uses preferred tmp dir when waiting for download without explicit path", async () => {
let downloadHandler: ((download: unknown) => void) | undefined;
const on = vi.fn((event: string, handler: (download: unknown) => void) => {
if (event === "download") {
downloadHandler = handler;
}
});
const off = vi.fn();
const saveAs = vi.fn(async () => {});
const download = {
url: () => "https://example.com/file.bin",
suggestedFilename: () => "file.bin",
saveAs,
};
tmpDirMocks.resolvePreferredOpenClawTmpDir.mockReturnValue("/tmp/openclaw-preferred");
setPwToolsCoreCurrentPage({ on, off });
const p = mod.waitForDownloadViaPlaywright({
cdpUrl: "http://127.0.0.1:18792",
targetId: "T1",
timeoutMs: 1000,
const { res, outPath } = await waitForImplicitDownloadOutput({
downloadUrl: "https://example.com/file.bin",
suggestedFilename: "file.bin",
});
await Promise.resolve();
downloadHandler?.(download);
const res = await p;
const outPath = vi.mocked(saveAs).mock.calls[0]?.[0];
expect(typeof outPath).toBe("string");
const expectedRootedDownloadsDir = path.join(
path.sep,
@@ -142,35 +150,11 @@ describe("pw-tools-core", () => {
});
it("sanitizes suggested download filenames to prevent traversal escapes", async () => {
let downloadHandler: ((download: unknown) => void) | undefined;
const on = vi.fn((event: string, handler: (download: unknown) => void) => {
if (event === "download") {
downloadHandler = handler;
}
});
const off = vi.fn();
const saveAs = vi.fn(async () => {});
const download = {
url: () => "https://example.com/evil",
suggestedFilename: () => "../../../../etc/passwd",
saveAs,
};
tmpDirMocks.resolvePreferredOpenClawTmpDir.mockReturnValue("/tmp/openclaw-preferred");
setPwToolsCoreCurrentPage({ on, off });
const p = mod.waitForDownloadViaPlaywright({
cdpUrl: "http://127.0.0.1:18792",
targetId: "T1",
timeoutMs: 1000,
const { res, outPath } = await waitForImplicitDownloadOutput({
downloadUrl: "https://example.com/evil",
suggestedFilename: "../../../../etc/passwd",
});
await Promise.resolve();
downloadHandler?.(download);
const res = await p;
const outPath = vi.mocked(saveAs).mock.calls[0]?.[0];
expect(typeof outPath).toBe("string");
expect(path.dirname(String(outPath))).toBe(
path.join(path.sep, "tmp", "openclaw-preferred", "downloads"),

View File

@@ -3,6 +3,23 @@ import type { BrowserRouteRegistrar } from "./types.js";
import { handleRouteError, readBody, requirePwAi, resolveProfileContext } from "./agent.shared.js";
import { jsonError, toBoolean, toNumber, toStringOrEmpty } from "./utils.js";
type StorageKind = "local" | "session";
function resolveBodyTargetId(body: unknown): string | undefined {
if (!body || typeof body !== "object" || Array.isArray(body)) {
return undefined;
}
const targetId = toStringOrEmpty((body as Record<string, unknown>).targetId);
return targetId || undefined;
}
function parseStorageKind(raw: string): StorageKind | null {
if (raw === "local" || raw === "session") {
return raw;
}
return null;
}
export function registerBrowserAgentStorageRoutes(
app: BrowserRouteRegistrar,
ctx: BrowserRouteContext,
@@ -35,7 +52,7 @@ export function registerBrowserAgentStorageRoutes(
return;
}
const body = readBody(req);
const targetId = toStringOrEmpty(body.targetId) || undefined;
const targetId = resolveBodyTargetId(body);
const cookie =
body.cookie && typeof body.cookie === "object" && !Array.isArray(body.cookie)
? (body.cookie as Record<string, unknown>)
@@ -79,7 +96,7 @@ export function registerBrowserAgentStorageRoutes(
return;
}
const body = readBody(req);
const targetId = toStringOrEmpty(body.targetId) || undefined;
const targetId = resolveBodyTargetId(body);
try {
const tab = await profileCtx.ensureTabAvailable(targetId);
const pw = await requirePwAi(res, "cookies clear");
@@ -101,8 +118,8 @@ export function registerBrowserAgentStorageRoutes(
if (!profileCtx) {
return;
}
const kind = toStringOrEmpty(req.params.kind);
if (kind !== "local" && kind !== "session") {
const kind = parseStorageKind(toStringOrEmpty(req.params.kind));
if (!kind) {
return jsonError(res, 400, "kind must be local|session");
}
const targetId = typeof req.query.targetId === "string" ? req.query.targetId.trim() : "";
@@ -130,12 +147,12 @@ export function registerBrowserAgentStorageRoutes(
if (!profileCtx) {
return;
}
const kind = toStringOrEmpty(req.params.kind);
if (kind !== "local" && kind !== "session") {
const kind = parseStorageKind(toStringOrEmpty(req.params.kind));
if (!kind) {
return jsonError(res, 400, "kind must be local|session");
}
const body = readBody(req);
const targetId = toStringOrEmpty(body.targetId) || undefined;
const targetId = resolveBodyTargetId(body);
const key = toStringOrEmpty(body.key);
if (!key) {
return jsonError(res, 400, "key is required");
@@ -165,12 +182,12 @@ export function registerBrowserAgentStorageRoutes(
if (!profileCtx) {
return;
}
const kind = toStringOrEmpty(req.params.kind);
if (kind !== "local" && kind !== "session") {
const kind = parseStorageKind(toStringOrEmpty(req.params.kind));
if (!kind) {
return jsonError(res, 400, "kind must be local|session");
}
const body = readBody(req);
const targetId = toStringOrEmpty(body.targetId) || undefined;
const targetId = resolveBodyTargetId(body);
try {
const tab = await profileCtx.ensureTabAvailable(targetId);
const pw = await requirePwAi(res, "storage clear");
@@ -194,7 +211,7 @@ export function registerBrowserAgentStorageRoutes(
return;
}
const body = readBody(req);
const targetId = toStringOrEmpty(body.targetId) || undefined;
const targetId = resolveBodyTargetId(body);
const offline = toBoolean(body.offline);
if (offline === undefined) {
return jsonError(res, 400, "offline is required");
@@ -222,7 +239,7 @@ export function registerBrowserAgentStorageRoutes(
return;
}
const body = readBody(req);
const targetId = toStringOrEmpty(body.targetId) || undefined;
const targetId = resolveBodyTargetId(body);
const headers =
body.headers && typeof body.headers === "object" && !Array.isArray(body.headers)
? (body.headers as Record<string, unknown>)
@@ -259,7 +276,7 @@ export function registerBrowserAgentStorageRoutes(
return;
}
const body = readBody(req);
const targetId = toStringOrEmpty(body.targetId) || undefined;
const targetId = resolveBodyTargetId(body);
const clear = toBoolean(body.clear) ?? false;
const username = toStringOrEmpty(body.username) || undefined;
const password = typeof body.password === "string" ? body.password : undefined;
@@ -288,7 +305,7 @@ export function registerBrowserAgentStorageRoutes(
return;
}
const body = readBody(req);
const targetId = toStringOrEmpty(body.targetId) || undefined;
const targetId = resolveBodyTargetId(body);
const clear = toBoolean(body.clear) ?? false;
const latitude = toNumber(body.latitude);
const longitude = toNumber(body.longitude);
@@ -321,7 +338,7 @@ export function registerBrowserAgentStorageRoutes(
return;
}
const body = readBody(req);
const targetId = toStringOrEmpty(body.targetId) || undefined;
const targetId = resolveBodyTargetId(body);
const schemeRaw = toStringOrEmpty(body.colorScheme);
const colorScheme =
schemeRaw === "dark" || schemeRaw === "light" || schemeRaw === "no-preference"
@@ -355,7 +372,7 @@ export function registerBrowserAgentStorageRoutes(
return;
}
const body = readBody(req);
const targetId = toStringOrEmpty(body.targetId) || undefined;
const targetId = resolveBodyTargetId(body);
const timezoneId = toStringOrEmpty(body.timezoneId);
if (!timezoneId) {
return jsonError(res, 400, "timezoneId is required");
@@ -383,7 +400,7 @@ export function registerBrowserAgentStorageRoutes(
return;
}
const body = readBody(req);
const targetId = toStringOrEmpty(body.targetId) || undefined;
const targetId = resolveBodyTargetId(body);
const locale = toStringOrEmpty(body.locale);
if (!locale) {
return jsonError(res, 400, "locale is required");
@@ -411,7 +428,7 @@ export function registerBrowserAgentStorageRoutes(
return;
}
const body = readBody(req);
const targetId = toStringOrEmpty(body.targetId) || undefined;
const targetId = resolveBodyTargetId(body);
const name = toStringOrEmpty(body.name);
if (!name) {
return jsonError(res, 400, "name is required");

View File

@@ -0,0 +1,24 @@
import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { afterAll, beforeAll, vi } from "vitest";
const chromeUserDataDir = { 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 });
});
vi.mock("./chrome.js", () => ({
isChromeCdpReady: vi.fn(async () => true),
isChromeReachable: vi.fn(async () => true),
launchOpenClawChrome: vi.fn(async () => {
throw new Error("unexpected launch");
}),
resolveOpenClawUserDataDir: vi.fn(() => chromeUserDataDir.dir),
stopOpenClawChrome: vi.fn(async () => {}),
}));

View File

@@ -1,30 +1,8 @@
import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { afterAll, beforeAll, describe, expect, it, vi } from "vitest";
import { describe, expect, it, vi } from "vitest";
import type { BrowserServerState } from "./server-context.js";
import "./server-context.chrome-test-harness.js";
import { createBrowserRouteContext } from "./server-context.js";
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 });
});
vi.mock("./chrome.js", () => ({
isChromeCdpReady: vi.fn(async () => true),
isChromeReachable: vi.fn(async () => true),
launchOpenClawChrome: vi.fn(async () => {
throw new Error("unexpected launch");
}),
resolveOpenClawUserDataDir: vi.fn(() => chromeUserDataDir.dir),
stopOpenClawChrome: vi.fn(async () => {}),
}));
function makeBrowserState(): BrowserServerState {
return {
// oxlint-disable-next-line typescript/no-explicit-any

View File

@@ -1,32 +1,10 @@
import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { afterAll, afterEach, beforeAll, describe, expect, it, vi } from "vitest";
import { afterEach, describe, expect, it, vi } from "vitest";
import type { BrowserServerState } from "./server-context.js";
import * as cdpModule from "./cdp.js";
import * as pwAiModule from "./pw-ai-module.js";
import "./server-context.chrome-test-harness.js";
import { createBrowserRouteContext } from "./server-context.js";
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 });
});
vi.mock("./chrome.js", () => ({
isChromeCdpReady: vi.fn(async () => true),
isChromeReachable: vi.fn(async () => true),
launchOpenClawChrome: vi.fn(async () => {
throw new Error("unexpected launch");
}),
resolveOpenClawUserDataDir: vi.fn(() => chromeUserDataDir.dir),
stopOpenClawChrome: vi.fn(async () => {}),
}));
const originalFetch = globalThis.fetch;
afterEach(() => {

View File

@@ -3,35 +3,21 @@ import { fetch as realFetch } from "undici";
import { describe, expect, it } from "vitest";
import { DEFAULT_UPLOAD_DIR } from "./paths.js";
import {
getBrowserControlServerBaseUrl,
installAgentContractHooks,
postJson,
startServerAndBase,
} from "./server.agent-contract.test-harness.js";
import {
getBrowserControlServerTestState,
getPwMocks,
installBrowserControlServerHooks,
setBrowserControlServerEvaluateEnabled,
startBrowserControlServerFromConfig,
} from "./server.control-server.test-harness.js";
const state = getBrowserControlServerTestState();
const pwMocks = getPwMocks();
describe("browser control server", () => {
installBrowserControlServerHooks();
const startServerAndBase = async () => {
await startBrowserControlServerFromConfig();
const base = getBrowserControlServerBaseUrl();
await realFetch(`${base}/start`, { method: "POST" }).then((r) => r.json());
return base;
};
const postJson = async <T>(url: string, body?: unknown): Promise<T> => {
const res = await realFetch(url, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: body === undefined ? undefined : JSON.stringify(body),
});
return (await res.json()) as T;
};
installAgentContractHooks();
const slowTimeoutMs = process.platform === "win32" ? 40_000 : 20_000;

View File

@@ -2,12 +2,14 @@ import { fetch as realFetch } from "undici";
import { describe, expect, it } from "vitest";
import { DEFAULT_AI_SNAPSHOT_MAX_CHARS } from "./constants.js";
import {
getBrowserControlServerBaseUrl,
installAgentContractHooks,
postJson,
startServerAndBase,
} from "./server.agent-contract.test-harness.js";
import {
getBrowserControlServerTestState,
getCdpMocks,
getPwMocks,
installBrowserControlServerHooks,
startBrowserControlServerFromConfig,
} from "./server.control-server.test-harness.js";
const state = getBrowserControlServerTestState();
@@ -15,23 +17,7 @@ const cdpMocks = getCdpMocks();
const pwMocks = getPwMocks();
describe("browser control server", () => {
installBrowserControlServerHooks();
const startServerAndBase = async () => {
await startBrowserControlServerFromConfig();
const base = getBrowserControlServerBaseUrl();
await realFetch(`${base}/start`, { method: "POST" }).then((r) => r.json());
return base;
};
const postJson = async <T>(url: string, body?: unknown): Promise<T> => {
const res = await realFetch(url, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: body === undefined ? undefined : JSON.stringify(body),
});
return (await res.json()) as T;
};
installAgentContractHooks();
it("agent contract: snapshot endpoints", async () => {
const base = await startServerAndBase();

View File

@@ -0,0 +1,26 @@
import { fetch as realFetch } from "undici";
import {
getBrowserControlServerBaseUrl,
installBrowserControlServerHooks,
startBrowserControlServerFromConfig,
} from "./server.control-server.test-harness.js";
export function installAgentContractHooks() {
installBrowserControlServerHooks();
}
export async function startServerAndBase(): Promise<string> {
await startBrowserControlServerFromConfig();
const base = getBrowserControlServerBaseUrl();
await realFetch(`${base}/start`, { method: "POST" }).then((r) => r.json());
return base;
}
export async function postJson<T>(url: string, body?: unknown): Promise<T> {
const res = await realFetch(url, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: body === undefined ? undefined : JSON.stringify(body),
});
return (await res.json()) as T;
}

View File

@@ -1,9 +1,11 @@
import fs from "node:fs/promises";
import { type AddressInfo, createServer } from "node:net";
import os from "node:os";
import path from "node:path";
import { afterAll, afterEach, beforeAll, beforeEach, vi } from "vitest";
import type { MockFn } from "../test-utils/vitest-mock-fn.js";
import { getFreePort } from "./test-port.js";
export { getFreePort } from "./test-port.js";
type HarnessState = {
testPort: number;
@@ -226,22 +228,6 @@ const server = await import("./server.js");
export const startBrowserControlServerFromConfig = server.startBrowserControlServerFromConfig;
export const stopBrowserControlServer = server.stopBrowserControlServer;
export async function getFreePort(): Promise<number> {
while (true) {
const port = await new Promise<number>((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;
}
}
}
export function makeResponse(
body: unknown,
init?: { ok?: boolean; status?: number; text?: string },

18
src/browser/test-port.ts Normal file
View File

@@ -0,0 +1,18 @@
import type { AddressInfo } from "node:net";
import { createServer } from "node:http";
export async function getFreePort(): Promise<number> {
while (true) {
const port = await new Promise<number>((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;
}
}
}