From fd66b44f5e2caa3f802b072f646e08e68ab587b0 Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Wed, 24 Jun 2026 13:24:43 +0800 Subject: [PATCH] fix(qa): recover Playwright Chromium on Ubuntu 26 --- .../src/test-file-scenario-runner.test.ts | 2 +- .../qa-lab/src/test-file-scenario-runner.ts | 2 +- scripts/ensure-playwright-chromium.mjs | 103 +++++++++++++++++- .../ensure-playwright-chromium.test.ts | 93 ++++++++++++++++ 4 files changed, 194 insertions(+), 6 deletions(-) diff --git a/extensions/qa-lab/src/test-file-scenario-runner.test.ts b/extensions/qa-lab/src/test-file-scenario-runner.test.ts index f0387d83794..deed317bb5e 100644 --- a/extensions/qa-lab/src/test-file-scenario-runner.test.ts +++ b/extensions/qa-lab/src/test-file-scenario-runner.test.ts @@ -179,7 +179,7 @@ describe("qa test file scenario runner", () => { expect(result.executionKind).toBe("playwright"); expect(commands.map((command) => command.args)).toEqual([ - ["scripts/ensure-playwright-chromium.mjs"], + ["scripts/ensure-playwright-chromium.mjs", "--skip-ffmpeg"], [ "scripts/run-vitest.mjs", "run", diff --git a/extensions/qa-lab/src/test-file-scenario-runner.ts b/extensions/qa-lab/src/test-file-scenario-runner.ts index 6232432a462..5273562badd 100644 --- a/extensions/qa-lab/src/test-file-scenario-runner.ts +++ b/extensions/qa-lab/src/test-file-scenario-runner.ts @@ -121,7 +121,7 @@ function playwrightSteps(scenario: QaTestFileScenario): QaScenarioCommandStep[] return [ { command: process.execPath, - args: ["scripts/ensure-playwright-chromium.mjs"], + args: ["scripts/ensure-playwright-chromium.mjs", "--skip-ffmpeg"], }, { command: process.execPath, diff --git a/scripts/ensure-playwright-chromium.mjs b/scripts/ensure-playwright-chromium.mjs index e398476248d..68e75702be3 100644 --- a/scripts/ensure-playwright-chromium.mjs +++ b/scripts/ensure-playwright-chromium.mjs @@ -10,6 +10,7 @@ import { resolvePnpmRunner } from "./pnpm-runner.mjs"; const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), ".."); const playwrightInstallBaseArgs = ["--dir", "ui", "exec", "playwright", "install"]; const executableOverrideEnvKey = "PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH"; +const chromiumPackageNames = ["chromium-browser", "chromium"]; /** * System Chromium executable paths used before downloading Playwright browsers. */ @@ -89,6 +90,68 @@ export function shouldInstallPlaywrightSystemDependencies(options = {}) { ); } +function resolveLinuxPrivilegePrefix(options = {}) { + const getuid = options.getuid ?? process.getuid; + const spawnSync = options.spawnSync ?? spawnSyncImpl; + if (typeof getuid === "function" && getuid() === 0) { + return []; + } + const result = spawnSync("sudo", ["-n", "true"], { stdio: "ignore" }); + if (result.status === 0) { + return ["sudo", "-n"]; + } + return undefined; +} + +/** + * Installs a distro Chromium package for CI images newer than Playwright's + * bundled browser support matrix. + */ +export function installLinuxSystemChromiumPackage(options = {}) { + const platform = options.platform ?? process.platform; + if (platform !== "linux") { + return 1; + } + const spawnSync = options.spawnSync ?? spawnSyncImpl; + const privilegePrefix = resolveLinuxPrivilegePrefix({ + getuid: options.getuid, + spawnSync, + }); + if (!privilegePrefix) { + return 1; + } + const env = { + ...(options.env ?? process.env), + DEBIAN_FRONTEND: "noninteractive", + }; + const cwd = options.cwd ?? repoRoot; + const stdio = options.stdio ?? "inherit"; + const runAptGet = (args) => { + const command = privilegePrefix[0] ?? "apt-get"; + const commandArgs = + privilegePrefix.length === 0 ? args : [...privilegePrefix.slice(1), "apt-get", ...args]; + return ( + spawnSync(command, commandArgs, { + cwd, + env, + stdio, + }).status ?? 1 + ); + }; + + const updateStatus = runAptGet(["update", "-qq"]); + if (updateStatus !== 0) { + return updateStatus; + } + for (const packageName of chromiumPackageNames) { + const installStatus = runAptGet(["install", "-y", packageName]); + if (installStatus === 0) { + return 0; + } + } + return 1; +} + /** * Checks whether this module is the direct script entrypoint. */ @@ -135,6 +198,31 @@ export function ensurePlaywrightChromium(options = {}) { }); return result.status ?? 1; }; + const useLinuxSystemChromiumPackage = () => { + log(`[ui-e2e] Playwright install is unavailable; installing a system Chromium package.`); + const installStatus = installLinuxSystemChromiumPackage({ + cwd: options.cwd, + env, + getuid: options.getuid, + platform: options.platform, + spawnSync, + stdio: options.stdio, + }); + if (installStatus !== 0) { + log(`[ui-e2e] System Chromium package install failed with status ${installStatus}.`); + return installStatus; + } + const installedSystemExecutablePath = resolveSystemChromiumExecutablePath( + existsSync, + spawnSync, + ); + if (installedSystemExecutablePath) { + log(`[ui-e2e] Using system Chromium at ${installedSystemExecutablePath}.`); + return ensureFfmpeg(); + } + log(`[ui-e2e] System Chromium package install completed but no runnable Chromium was found.`); + return 1; + }; const ensureFfmpeg = () => { if (!options.ensureFfmpeg) { return 0; @@ -188,7 +276,7 @@ export function ensurePlaywrightChromium(options = {}) { ); const depsStatus = runPlaywrightInstall(["chromium"], true); if (depsStatus !== 0) { - return depsStatus; + return useLinuxSystemChromiumPackage(); } if (existsSync(executablePath) && canRunChromiumExecutable(executablePath, spawnSync)) { return ensureFfmpeg(); @@ -208,11 +296,12 @@ export function ensurePlaywrightChromium(options = {}) { ); const depsStatus = runPlaywrightInstall(["chromium"], true); if (depsStatus !== 0) { - return depsStatus; + return useLinuxSystemChromiumPackage(); } if (existsSync(executablePath) && canRunChromiumExecutable(executablePath, spawnSync)) { return ensureFfmpeg(); } + return useLinuxSystemChromiumPackage(); } log( `[ui-e2e] Playwright install completed but Chromium is still not runnable at ${executablePath}.`, @@ -222,6 +311,12 @@ export function ensurePlaywrightChromium(options = {}) { return ensureFfmpeg(); } -if (isDirectScriptExecution()) { - process.exitCode = ensurePlaywrightChromium({ ensureFfmpeg: true }); +export function shouldEnsureFfmpegFromArgv(argv = process.argv) { + return !argv.includes("--skip-ffmpeg"); +} + +if (isDirectScriptExecution()) { + process.exitCode = ensurePlaywrightChromium({ + ensureFfmpeg: shouldEnsureFfmpegFromArgv(), + }); } diff --git a/test/scripts/ensure-playwright-chromium.test.ts b/test/scripts/ensure-playwright-chromium.test.ts index 964289d5020..9d11e3b96d4 100644 --- a/test/scripts/ensure-playwright-chromium.test.ts +++ b/test/scripts/ensure-playwright-chromium.test.ts @@ -2,7 +2,9 @@ import { describe, expect, it, vi } from "vitest"; import { ensurePlaywrightChromium, + installLinuxSystemChromiumPackage, resolvePlaywrightInstallRunner, + shouldEnsureFfmpegFromArgv, shouldInstallPlaywrightSystemDependencies, } from "../../scripts/ensure-playwright-chromium.mjs"; @@ -276,6 +278,55 @@ describe("ensurePlaywrightChromium", () => { expect(logs.join("\n")).toContain("installing Linux system dependencies"); }); + it("falls back to distro Chromium when Playwright does not support the Linux runner image", () => { + const logs: string[] = []; + let installedSystemChromium = false; + const spawnSync = vi.fn((command: string, args: string[]) => { + if (command === "pnpm" && args.includes("chromium")) { + return { status: 1 }; + } + if (command === "apt-get" && args.includes("update")) { + return { status: 0 }; + } + if (command === "apt-get" && args.includes("chromium-browser")) { + installedSystemChromium = true; + return { status: 0 }; + } + if (command === "/usr/bin/chromium-browser") { + return { status: installedSystemChromium ? 0 : 127 }; + } + return { status: 1 }; + }); + + expect( + ensurePlaywrightChromium({ + cwd: "/repo", + env: { CI: "1", PATH: "/bin" }, + executablePath: "/cache/chromium/chrome", + existsSync: (path: string) => + installedSystemChromium && path === "/usr/bin/chromium-browser", + getuid: () => 0, + log: (line: string) => logs.push(line), + platform: "linux", + spawnSync, + stdio: "pipe", + systemExecutablePath: "", + }), + ).toBe(0); + expect(spawnSync).toHaveBeenCalledWith( + "apt-get", + ["update", "-qq"], + expect.objectContaining({ cwd: "/repo", stdio: "pipe" }), + ); + expect(spawnSync).toHaveBeenCalledWith( + "apt-get", + ["install", "-y", "chromium-browser"], + expect.objectContaining({ cwd: "/repo", stdio: "pipe" }), + ); + expect(logs.join("\n")).toContain("installing a system Chromium package"); + expect(logs.join("\n")).toContain("Using system Chromium at /usr/bin/chromium-browser"); + }); + it("does not install Linux system dependencies for an unprivileged local lane", () => { const spawnSync = vi .fn() @@ -343,6 +394,7 @@ describe("ensurePlaywrightChromium", () => { ensurePlaywrightChromium({ executablePath: "/cache/chromium/chrome", existsSync: () => false, + platform: "darwin", spawnSync: vi.fn(() => ({ status: 23 })), stdio: "pipe", systemExecutablePath: "", @@ -404,4 +456,45 @@ describe("ensurePlaywrightChromium", () => { }), ).toBe(true); }); + + it("installs Linux system Chromium packages with sudo for non-root lanes", () => { + const spawnSync = vi.fn(() => ({ status: 0 })); + + expect( + installLinuxSystemChromiumPackage({ + cwd: "/repo", + env: { PATH: "/bin" }, + getuid: () => 501, + platform: "linux", + spawnSync, + stdio: "pipe", + }), + ).toBe(0); + expect(spawnSync).toHaveBeenNthCalledWith(1, "sudo", ["-n", "true"], { stdio: "ignore" }); + expect(spawnSync).toHaveBeenNthCalledWith( + 2, + "sudo", + ["-n", "apt-get", "update", "-qq"], + expect.objectContaining({ cwd: "/repo", stdio: "pipe" }), + ); + expect(spawnSync).toHaveBeenNthCalledWith( + 3, + "sudo", + ["-n", "apt-get", "install", "-y", "chromium-browser"], + expect.objectContaining({ cwd: "/repo", stdio: "pipe" }), + ); + }); + + it("allows QA scenario runners to skip optional Playwright ffmpeg", () => { + expect(shouldEnsureFfmpegFromArgv(["node", "scripts/ensure-playwright-chromium.mjs"])).toBe( + true, + ); + expect( + shouldEnsureFfmpegFromArgv([ + "node", + "scripts/ensure-playwright-chromium.mjs", + "--skip-ffmpeg", + ]), + ).toBe(false); + }); });