diff --git a/scripts/ensure-playwright-chromium.mjs b/scripts/ensure-playwright-chromium.mjs index f404fe908ab7..a81de0146cd5 100644 --- a/scripts/ensure-playwright-chromium.mjs +++ b/scripts/ensure-playwright-chromium.mjs @@ -9,6 +9,17 @@ import { resolvePnpmRunner } from "./pnpm-runner.mjs"; const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), ".."); const playwrightInstallArgs = ["--dir", "ui", "exec", "playwright", "install", "chromium"]; const executableOverrideEnvKey = "PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH"; +export const systemChromiumExecutableCandidates = [ + "/snap/bin/chromium", + "/usr/bin/chromium-browser", + "/usr/bin/chromium", + "/usr/bin/google-chrome", + "/usr/bin/google-chrome-stable", +]; + +export function resolveSystemChromiumExecutablePath(existsSync = existsSyncImpl) { + return systemChromiumExecutableCandidates.find((candidate) => existsSync(candidate)) ?? ""; +} export function resolvePlaywrightInstallRunner(options = {}) { const env = options.env ?? process.env; @@ -58,6 +69,13 @@ export function ensurePlaywrightChromium(options = {}) { return 0; } + const systemExecutablePath = + options.systemExecutablePath ?? resolveSystemChromiumExecutablePath(existsSync); + if (systemExecutablePath) { + log(`[ui-e2e] Using system Chromium at ${systemExecutablePath}.`); + return 0; + } + if (env.OPENCLAW_UI_E2E_ALLOW_MISSING_CHROMIUM === "1") { log( `[ui-e2e] Playwright Chromium is missing at ${executablePath}; OPENCLAW_UI_E2E_ALLOW_MISSING_CHROMIUM=1 leaves the lane skipped.`, diff --git a/test/scripts/ensure-playwright-chromium.test.ts b/test/scripts/ensure-playwright-chromium.test.ts index d18e6030c432..e8164654ada0 100644 --- a/test/scripts/ensure-playwright-chromium.test.ts +++ b/test/scripts/ensure-playwright-chromium.test.ts @@ -51,6 +51,22 @@ describe("ensurePlaywrightChromium", () => { ); }); + it("uses a system Chromium binary when Playwright Chromium is missing", () => { + const logs: string[] = []; + const spawnSync = vi.fn(); + + expect( + ensurePlaywrightChromium({ + executablePath: "/cache/chromium/chrome", + existsSync: (path: string) => path === "/usr/bin/chromium-browser", + log: (line: string) => logs.push(line), + spawnSync, + }), + ).toBe(0); + expect(spawnSync).not.toHaveBeenCalled(); + expect(logs.join("\n")).toContain("Using system Chromium at /usr/bin/chromium-browser"); + }); + it("preserves the intentional missing-browser skip mode", () => { const logs: string[] = []; const spawnSync = vi.fn(); @@ -81,6 +97,7 @@ describe("ensurePlaywrightChromium", () => { platform: "linux", spawnSync, stdio: "pipe", + systemExecutablePath: "", }), ).toBe(0); expect(spawnSync).toHaveBeenCalledWith( @@ -103,6 +120,7 @@ describe("ensurePlaywrightChromium", () => { existsSync: () => false, spawnSync: vi.fn(() => ({ status: 23 })), stdio: "pipe", + systemExecutablePath: "", }), ).toBe(23); }); diff --git a/ui/src/test-helpers/control-ui-e2e.ts b/ui/src/test-helpers/control-ui-e2e.ts index c75719201f62..34f1ea17fa1e 100644 --- a/ui/src/test-helpers/control-ui-e2e.ts +++ b/ui/src/test-helpers/control-ui-e2e.ts @@ -83,6 +83,13 @@ export type MockGatewayControls = { }; const chromiumExecutableOverrideEnvKey = "PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH"; +const systemChromiumExecutableCandidates = [ + "/snap/bin/chromium", + "/usr/bin/chromium-browser", + "/usr/bin/chromium", + "/usr/bin/google-chrome", + "/usr/bin/google-chrome-stable", +] as const; function resolveRepoRoot(): string { const here = path.dirname(fileURLToPath(import.meta.url)); @@ -94,7 +101,16 @@ export function resolvePlaywrightChromiumExecutablePath( env: NodeJS.ProcessEnv = process.env, ): string { const executableOverride = env[chromiumExecutableOverrideEnvKey]?.trim(); - return executableOverride || defaultExecutablePath; + if (executableOverride) { + return executableOverride; + } + if (existsSync(defaultExecutablePath)) { + return defaultExecutablePath; + } + return ( + systemChromiumExecutableCandidates.find((candidate) => existsSync(candidate)) ?? + defaultExecutablePath + ); } export function canRunPlaywrightChromium(chromiumExecutablePath: string): boolean {