fix(qa): launch control ui flows with runnable chromium

This commit is contained in:
Vincent Koc
2026-06-24 13:57:47 +08:00
parent f9cf00c351
commit 12345e4c9b
4 changed files with 131 additions and 8 deletions

View File

@@ -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);
});
});

View File

@@ -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;
},

View File

@@ -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" });

View File

@@ -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 },