mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-07 20:40:43 +00:00
167 lines
4.7 KiB
TypeScript
167 lines
4.7 KiB
TypeScript
import { randomUUID } from "node:crypto";
|
|
import { chromium, type Browser, type BrowserContext, type Page } from "playwright-core";
|
|
|
|
type QaWebSession = {
|
|
browser: Browser;
|
|
context: BrowserContext;
|
|
page: Page;
|
|
};
|
|
|
|
type QaWebOpenPageParams = {
|
|
url: string;
|
|
headless?: boolean;
|
|
channel?: "chrome";
|
|
timeoutMs?: number;
|
|
viewport?: { width: number; height: number };
|
|
};
|
|
|
|
type QaWebWaitParams = {
|
|
pageId: string;
|
|
selector?: string;
|
|
text?: string;
|
|
timeoutMs?: number;
|
|
};
|
|
|
|
type QaWebTypeParams = {
|
|
pageId: string;
|
|
selector: string;
|
|
text: string;
|
|
submit?: boolean;
|
|
timeoutMs?: number;
|
|
};
|
|
|
|
type QaWebSnapshotParams = {
|
|
pageId: string;
|
|
timeoutMs?: number;
|
|
maxChars?: number;
|
|
};
|
|
|
|
type QaWebEvaluateParams = {
|
|
pageId: string;
|
|
expression: string;
|
|
timeoutMs?: number;
|
|
};
|
|
|
|
const sessions = new Map<string, QaWebSession>();
|
|
const DEFAULT_WEB_TIMEOUT_MS = 20_000;
|
|
|
|
function resolveTimeoutMs(timeoutMs: number | undefined, fallbackMs = DEFAULT_WEB_TIMEOUT_MS) {
|
|
if (typeof timeoutMs !== "number" || !Number.isFinite(timeoutMs)) {
|
|
return fallbackMs;
|
|
}
|
|
return Math.max(1, Math.floor(timeoutMs));
|
|
}
|
|
|
|
function resolveSession(pageId: string): QaWebSession {
|
|
const session = sessions.get(pageId);
|
|
if (!session) {
|
|
throw new Error(`unknown web session: ${pageId}`);
|
|
}
|
|
return session;
|
|
}
|
|
|
|
export async function qaWebOpenPage(params: QaWebOpenPageParams) {
|
|
const timeoutMs = resolveTimeoutMs(params.timeoutMs);
|
|
const browser = await chromium.launch({
|
|
channel: params.channel ?? "chrome",
|
|
headless: params.headless ?? true,
|
|
});
|
|
const context = await browser.newContext({
|
|
ignoreHTTPSErrors: true,
|
|
viewport: params.viewport ?? { width: 1440, height: 1080 },
|
|
});
|
|
const page = await context.newPage();
|
|
await page.goto(params.url, {
|
|
waitUntil: "domcontentloaded",
|
|
timeout: timeoutMs,
|
|
});
|
|
const pageId = randomUUID();
|
|
sessions.set(pageId, { browser, context, page });
|
|
return {
|
|
pageId,
|
|
url: page.url(),
|
|
title: await page.title().catch(() => ""),
|
|
};
|
|
}
|
|
|
|
export async function qaWebWait(params: QaWebWaitParams) {
|
|
const session = resolveSession(params.pageId);
|
|
const timeoutMs = resolveTimeoutMs(params.timeoutMs);
|
|
if (params.selector) {
|
|
await session.page.waitForSelector(params.selector, { timeout: timeoutMs });
|
|
return { ok: true };
|
|
}
|
|
if (params.text) {
|
|
await session.page.waitForFunction(
|
|
(expected) => document.body?.innerText?.toLowerCase().includes(expected.toLowerCase()),
|
|
params.text,
|
|
{ timeout: timeoutMs },
|
|
);
|
|
return { ok: true };
|
|
}
|
|
throw new Error("web wait requires selector or text");
|
|
}
|
|
|
|
export async function qaWebType(params: QaWebTypeParams) {
|
|
const session = resolveSession(params.pageId);
|
|
const timeoutMs = resolveTimeoutMs(params.timeoutMs);
|
|
const locator = session.page.locator(params.selector).first();
|
|
await locator.waitFor({ timeout: timeoutMs });
|
|
await locator.fill(params.text, { timeout: timeoutMs });
|
|
if (params.submit) {
|
|
await locator.press("Enter", { timeout: timeoutMs });
|
|
}
|
|
return { ok: true };
|
|
}
|
|
|
|
export async function qaWebSnapshot(params: QaWebSnapshotParams) {
|
|
const session = resolveSession(params.pageId);
|
|
const timeoutMs = resolveTimeoutMs(params.timeoutMs);
|
|
const body = session.page.locator("body");
|
|
await body.waitFor({ timeout: timeoutMs });
|
|
const text = await body.innerText({ timeout: timeoutMs });
|
|
const maxChars =
|
|
typeof params.maxChars === "number" && Number.isFinite(params.maxChars)
|
|
? Math.max(1, Math.floor(params.maxChars))
|
|
: undefined;
|
|
return {
|
|
url: session.page.url(),
|
|
title: await session.page.title().catch(() => ""),
|
|
text: maxChars ? text.slice(0, maxChars) : text,
|
|
};
|
|
}
|
|
|
|
export async function qaWebEvaluate<T = unknown>(params: QaWebEvaluateParams): Promise<T> {
|
|
const session = resolveSession(params.pageId);
|
|
const timeoutMs = resolveTimeoutMs(params.timeoutMs);
|
|
return (await Promise.race([
|
|
session.page.evaluate(({ expression }) => (0, eval)(expression) as unknown, {
|
|
expression: params.expression,
|
|
}),
|
|
new Promise<never>((_, reject) =>
|
|
setTimeout(() => reject(new Error(`web evaluate timed out after ${timeoutMs}ms`)), timeoutMs),
|
|
),
|
|
])) as T;
|
|
}
|
|
|
|
export async function closeQaWebSessions(pageIds?: Iterable<string>): Promise<void> {
|
|
const active = pageIds
|
|
? [...pageIds].flatMap((pageId) => {
|
|
const session = sessions.get(pageId);
|
|
sessions.delete(pageId);
|
|
return session ? [session] : [];
|
|
})
|
|
: [...sessions.values()];
|
|
if (!pageIds) {
|
|
sessions.clear();
|
|
}
|
|
for (const session of active) {
|
|
await session.context.close().catch(() => {});
|
|
await session.browser.close().catch(() => {});
|
|
}
|
|
}
|
|
|
|
export async function closeAllQaWebSessions(): Promise<void> {
|
|
await closeQaWebSessions();
|
|
}
|