mirror of
https://github.com/openclaw/openclaw.git
synced 2026-06-30 19:59:35 +00:00
fix(qa): launch control ui flows with runnable chromium
This commit is contained in:
@@ -303,7 +303,7 @@ describe("qa suite runtime flow", () => {
|
||||
});
|
||||
|
||||
await call.deps.webOpenPage({ url: "https://openclaw.ai" });
|
||||
expect(webOpenPage).toHaveBeenCalledWith({ url: "https://openclaw.ai" });
|
||||
expect(webOpenPage).toHaveBeenCalledWith({ url: "https://openclaw.ai", repoRoot: "/repo" });
|
||||
expect(env.webSessionIds.has("page-1")).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -186,7 +186,7 @@ function createQaSuiteScenarioDeps(params: QaSuiteScenarioDepsParams) {
|
||||
browserSnapshot: qaBrowserSnapshot,
|
||||
browserAct: qaBrowserAct,
|
||||
webOpenPage: async (webParams: Parameters<typeof qaWebOpenPage>[0]) => {
|
||||
const opened = await qaWebOpenPage(webParams);
|
||||
const opened = await qaWebOpenPage({ ...webParams, repoRoot: params.env.repoRoot });
|
||||
params.env.webSessionIds.add(opened.pageId);
|
||||
return opened;
|
||||
},
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
// Qa Lab tests cover web runtime plugin behavior.
|
||||
import { MAX_TIMER_TIMEOUT_MS } from "openclaw/plugin-sdk/number-runtime";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const {
|
||||
bodyLocator,
|
||||
browserClose,
|
||||
contextClose,
|
||||
contextNewPage,
|
||||
existsSync,
|
||||
goto,
|
||||
launch,
|
||||
locatorFill,
|
||||
@@ -17,6 +18,7 @@ const {
|
||||
pageUrl,
|
||||
pageWaitForFunction,
|
||||
pageWaitForSelector,
|
||||
spawnSync,
|
||||
} = vi.hoisted(() => ({
|
||||
bodyLocator: {
|
||||
waitFor: vi.fn(async () => undefined),
|
||||
@@ -25,6 +27,7 @@ const {
|
||||
browserClose: vi.fn(async () => undefined),
|
||||
contextClose: vi.fn(async () => undefined),
|
||||
contextNewPage: vi.fn(),
|
||||
existsSync: vi.fn((_candidate: unknown) => false),
|
||||
goto: vi.fn(async () => undefined),
|
||||
launch: vi.fn(),
|
||||
locatorFill: vi.fn(async () => undefined),
|
||||
@@ -35,6 +38,16 @@ const {
|
||||
pageUrl: vi.fn(() => "http://127.0.0.1:3000/chat"),
|
||||
pageWaitForFunction: vi.fn(async () => undefined),
|
||||
pageWaitForSelector: vi.fn(async () => undefined),
|
||||
spawnSync: vi.fn(() => ({ status: 0 })),
|
||||
}));
|
||||
|
||||
vi.mock("node:child_process", () => ({
|
||||
spawnSync,
|
||||
}));
|
||||
|
||||
vi.mock("node:fs", async (importOriginal) => ({
|
||||
...(await importOriginal<typeof import("node:fs")>()),
|
||||
existsSync,
|
||||
}));
|
||||
|
||||
vi.mock("playwright-core", () => ({
|
||||
@@ -85,6 +98,12 @@ beforeEach(async () => {
|
||||
contextNewPage.mockResolvedValue(page);
|
||||
launch.mockResolvedValue(browser);
|
||||
vi.clearAllMocks();
|
||||
existsSync.mockReturnValue(false);
|
||||
spawnSync.mockReturnValue({ status: 0 });
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.unstubAllEnvs();
|
||||
});
|
||||
|
||||
function requireLaunchOptions() {
|
||||
@@ -116,7 +135,13 @@ describe("qa web runtime", () => {
|
||||
await closeQaWebSessions();
|
||||
|
||||
const launchOptions = requireLaunchOptions();
|
||||
expect(launchOptions?.channel).toBe("chrome");
|
||||
expect(spawnSync).toHaveBeenCalledWith(
|
||||
process.execPath,
|
||||
["scripts/ensure-playwright-chromium.mjs", "--skip-ffmpeg"],
|
||||
expect.objectContaining({ cwd: process.cwd(), stdio: "inherit" }),
|
||||
);
|
||||
expect(launchOptions?.channel).toBeUndefined();
|
||||
expect(launchOptions?.executablePath).toBeUndefined();
|
||||
expect(launchOptions?.headless).toBe(true);
|
||||
expect(goto).toHaveBeenCalledWith("http://127.0.0.1:3000/chat", {
|
||||
waitUntil: "domcontentloaded",
|
||||
@@ -132,6 +157,44 @@ describe("qa web runtime", () => {
|
||||
expect(browserClose).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("launches an explicit Chromium executable override when configured", async () => {
|
||||
vi.stubEnv("PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH", "/custom/chromium");
|
||||
existsSync.mockImplementation((candidate) => candidate === "/custom/chromium");
|
||||
|
||||
await qaWebOpenPage({ url: "http://127.0.0.1:3000/chat" });
|
||||
|
||||
const launchOptions = requireLaunchOptions();
|
||||
expect(spawnSync).toHaveBeenCalledWith("/custom/chromium", ["--version"], {
|
||||
stdio: "ignore",
|
||||
});
|
||||
expect(launchOptions?.channel).toBeUndefined();
|
||||
expect(launchOptions?.executablePath).toBe("/custom/chromium");
|
||||
await closeQaWebSessions();
|
||||
});
|
||||
|
||||
it("launches detected system Chromium without requiring branded Chrome", async () => {
|
||||
existsSync.mockImplementation((candidate) => candidate === "/usr/bin/chromium");
|
||||
|
||||
await qaWebOpenPage({ url: "http://127.0.0.1:3000/chat" });
|
||||
|
||||
const launchOptions = requireLaunchOptions();
|
||||
expect(spawnSync).toHaveBeenCalledWith("/usr/bin/chromium", ["--version"], {
|
||||
stdio: "ignore",
|
||||
});
|
||||
expect(launchOptions?.channel).toBeUndefined();
|
||||
expect(launchOptions?.executablePath).toBe("/usr/bin/chromium");
|
||||
await closeQaWebSessions();
|
||||
});
|
||||
|
||||
it("keeps an explicit browser channel request explicit", async () => {
|
||||
await qaWebOpenPage({ url: "http://127.0.0.1:3000/chat", channel: "chrome" });
|
||||
|
||||
const launchOptions = requireLaunchOptions();
|
||||
expect(launchOptions?.channel).toBe("chrome");
|
||||
expect(launchOptions?.executablePath).toBeUndefined();
|
||||
await closeQaWebSessions();
|
||||
});
|
||||
|
||||
it("can close only selected page sessions", async () => {
|
||||
const first = await qaWebOpenPage({ url: "http://127.0.0.1:3000/one" });
|
||||
const second = await qaWebOpenPage({ url: "http://127.0.0.1:3000/two" });
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { spawnSync } from "node:child_process";
|
||||
// Qa Lab plugin module implements web runtime behavior.
|
||||
import { randomUUID } from "node:crypto";
|
||||
import { existsSync } from "node:fs";
|
||||
import { resolvePositiveTimerTimeoutMs } from "openclaw/plugin-sdk/number-runtime";
|
||||
import { chromium, type Browser, type BrowserContext, type Page } from "playwright-core";
|
||||
|
||||
@@ -19,6 +21,7 @@ type QaWebOpenPageParams = {
|
||||
url: string;
|
||||
headless?: boolean;
|
||||
channel?: "chrome";
|
||||
repoRoot?: string;
|
||||
timeoutMs?: number;
|
||||
viewport?: { width: number; height: number };
|
||||
};
|
||||
@@ -54,6 +57,14 @@ const sessions = new Map<string, QaWebSession>();
|
||||
const DEFAULT_WEB_TIMEOUT_MS = 20_000;
|
||||
const MAX_DIAGNOSTIC_ENTRIES = 50;
|
||||
const MAX_DIAGNOSTIC_TEXT_CHARS = 2_000;
|
||||
const PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH_ENV = "PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH";
|
||||
const SYSTEM_CHROMIUM_EXECUTABLE_CANDIDATES = [
|
||||
"/snap/bin/chromium",
|
||||
"/usr/bin/chromium-browser",
|
||||
"/usr/bin/chromium",
|
||||
"/usr/bin/google-chrome",
|
||||
"/usr/bin/google-chrome-stable",
|
||||
] as const;
|
||||
|
||||
function appendDiagnostic(diagnostics: QaWebDiagnosticEntry[], entry: QaWebDiagnosticEntry): void {
|
||||
diagnostics.push({
|
||||
@@ -77,12 +88,61 @@ function resolveSession(pageId: string): QaWebSession {
|
||||
return session;
|
||||
}
|
||||
|
||||
function canRunChromiumExecutable(executablePath: string): boolean {
|
||||
const result = spawnSync(executablePath, ["--version"], { stdio: "ignore" });
|
||||
return result.status === 0;
|
||||
}
|
||||
|
||||
function resolveRunnableChromiumExecutablePath(): string | undefined {
|
||||
const executableOverride = process.env[PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH_ENV]?.trim();
|
||||
if (executableOverride) {
|
||||
return existsSync(executableOverride) && canRunChromiumExecutable(executableOverride)
|
||||
? executableOverride
|
||||
: undefined;
|
||||
}
|
||||
return SYSTEM_CHROMIUM_EXECUTABLE_CANDIDATES.find(
|
||||
(candidate) => existsSync(candidate) && canRunChromiumExecutable(candidate),
|
||||
);
|
||||
}
|
||||
|
||||
function ensureChromiumAvailable(repoRoot: string) {
|
||||
const result = spawnSync(
|
||||
process.execPath,
|
||||
["scripts/ensure-playwright-chromium.mjs", "--skip-ffmpeg"],
|
||||
{
|
||||
cwd: repoRoot,
|
||||
env: process.env,
|
||||
stdio: "inherit",
|
||||
},
|
||||
);
|
||||
if ((result.status ?? 1) !== 0) {
|
||||
throw new Error(`failed to ensure Playwright Chromium; status=${result.status ?? "unknown"}`);
|
||||
}
|
||||
}
|
||||
|
||||
function buildChromiumLaunchOptions(params: QaWebOpenPageParams) {
|
||||
const baseOptions = {
|
||||
headless: params.headless ?? true,
|
||||
};
|
||||
if (params.channel) {
|
||||
return {
|
||||
...baseOptions,
|
||||
channel: params.channel,
|
||||
};
|
||||
}
|
||||
const executablePath = resolveRunnableChromiumExecutablePath();
|
||||
return executablePath
|
||||
? {
|
||||
...baseOptions,
|
||||
executablePath,
|
||||
}
|
||||
: baseOptions;
|
||||
}
|
||||
|
||||
export async function qaWebOpenPage(params: QaWebOpenPageParams) {
|
||||
const timeoutMs = resolveTimeoutMs(params.timeoutMs);
|
||||
const browser = await chromium.launch({
|
||||
channel: params.channel ?? "chrome",
|
||||
headless: params.headless ?? true,
|
||||
});
|
||||
ensureChromiumAvailable(params.repoRoot ?? process.cwd());
|
||||
const browser = await chromium.launch(buildChromiumLaunchOptions(params));
|
||||
const context = await browser.newContext({
|
||||
ignoreHTTPSErrors: true,
|
||||
viewport: params.viewport ?? { width: 1440, height: 1080 },
|
||||
|
||||
Reference in New Issue
Block a user