Skip to content

Commit fd66b44

Browse files
committed
fix(qa): recover Playwright Chromium on Ubuntu 26
1 parent 2ab3b22 commit fd66b44

4 files changed

Lines changed: 193 additions & 5 deletions

File tree

extensions/qa-lab/src/test-file-scenario-runner.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -179,7 +179,7 @@ describe("qa test file scenario runner", () => {
179179

180180
expect(result.executionKind).toBe("playwright");
181181
expect(commands.map((command) => command.args)).toEqual([
182-
["scripts/ensure-playwright-chromium.mjs"],
182+
["scripts/ensure-playwright-chromium.mjs", "--skip-ffmpeg"],
183183
[
184184
"scripts/run-vitest.mjs",
185185
"run",

extensions/qa-lab/src/test-file-scenario-runner.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@ function playwrightSteps(scenario: QaTestFileScenario): QaScenarioCommandStep[]
121121
return [
122122
{
123123
command: process.execPath,
124-
args: ["scripts/ensure-playwright-chromium.mjs"],
124+
args: ["scripts/ensure-playwright-chromium.mjs", "--skip-ffmpeg"],
125125
},
126126
{
127127
command: process.execPath,

scripts/ensure-playwright-chromium.mjs

Lines changed: 98 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { resolvePnpmRunner } from "./pnpm-runner.mjs";
1010
const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");
1111
const playwrightInstallBaseArgs = ["--dir", "ui", "exec", "playwright", "install"];
1212
const executableOverrideEnvKey = "PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH";
13+
const chromiumPackageNames = ["chromium-browser", "chromium"];
1314
/**
1415
* System Chromium executable paths used before downloading Playwright browsers.
1516
*/
@@ -89,6 +90,68 @@ export function shouldInstallPlaywrightSystemDependencies(options = {}) {
8990
);
9091
}
9192

93+
function resolveLinuxPrivilegePrefix(options = {}) {
94+
const getuid = options.getuid ?? process.getuid;
95+
const spawnSync = options.spawnSync ?? spawnSyncImpl;
96+
if (typeof getuid === "function" && getuid() === 0) {
97+
return [];
98+
}
99+
const result = spawnSync("sudo", ["-n", "true"], { stdio: "ignore" });
100+
if (result.status === 0) {
101+
return ["sudo", "-n"];
102+
}
103+
return undefined;
104+
}
105+
106+
/**
107+
* Installs a distro Chromium package for CI images newer than Playwright's
108+
* bundled browser support matrix.
109+
*/
110+
export function installLinuxSystemChromiumPackage(options = {}) {
111+
const platform = options.platform ?? process.platform;
112+
if (platform !== "linux") {
113+
return 1;
114+
}
115+
const spawnSync = options.spawnSync ?? spawnSyncImpl;
116+
const privilegePrefix = resolveLinuxPrivilegePrefix({
117+
getuid: options.getuid,
118+
spawnSync,
119+
});
120+
if (!privilegePrefix) {
121+
return 1;
122+
}
123+
const env = {
124+
...(options.env ?? process.env),
125+
DEBIAN_FRONTEND: "noninteractive",
126+
};
127+
const cwd = options.cwd ?? repoRoot;
128+
const stdio = options.stdio ?? "inherit";
129+
const runAptGet = (args) => {
130+
const command = privilegePrefix[0] ?? "apt-get";
131+
const commandArgs =
132+
privilegePrefix.length === 0 ? args : [...privilegePrefix.slice(1), "apt-get", ...args];
133+
return (
134+
spawnSync(command, commandArgs, {
135+
cwd,
136+
env,
137+
stdio,
138+
}).status ?? 1
139+
);
140+
};
141+
142+
const updateStatus = runAptGet(["update", "-qq"]);
143+
if (updateStatus !== 0) {
144+
return updateStatus;
145+
}
146+
for (const packageName of chromiumPackageNames) {
147+
const installStatus = runAptGet(["install", "-y", packageName]);
148+
if (installStatus === 0) {
149+
return 0;
150+
}
151+
}
152+
return 1;
153+
}
154+
92155
/**
93156
* Checks whether this module is the direct script entrypoint.
94157
*/
@@ -135,6 +198,31 @@ export function ensurePlaywrightChromium(options = {}) {
135198
});
136199
return result.status ?? 1;
137200
};
201+
const useLinuxSystemChromiumPackage = () => {
202+
log(`[ui-e2e] Playwright install is unavailable; installing a system Chromium package.`);
203+
const installStatus = installLinuxSystemChromiumPackage({
204+
cwd: options.cwd,
205+
env,
206+
getuid: options.getuid,
207+
platform: options.platform,
208+
spawnSync,
209+
stdio: options.stdio,
210+
});
211+
if (installStatus !== 0) {
212+
log(`[ui-e2e] System Chromium package install failed with status ${installStatus}.`);
213+
return installStatus;
214+
}
215+
const installedSystemExecutablePath = resolveSystemChromiumExecutablePath(
216+
existsSync,
217+
spawnSync,
218+
);
219+
if (installedSystemExecutablePath) {
220+
log(`[ui-e2e] Using system Chromium at ${installedSystemExecutablePath}.`);
221+
return ensureFfmpeg();
222+
}
223+
log(`[ui-e2e] System Chromium package install completed but no runnable Chromium was found.`);
224+
return 1;
225+
};
138226
const ensureFfmpeg = () => {
139227
if (!options.ensureFfmpeg) {
140228
return 0;
@@ -188,7 +276,7 @@ export function ensurePlaywrightChromium(options = {}) {
188276
);
189277
const depsStatus = runPlaywrightInstall(["chromium"], true);
190278
if (depsStatus !== 0) {
191-
return depsStatus;
279+
return useLinuxSystemChromiumPackage();
192280
}
193281
if (existsSync(executablePath) && canRunChromiumExecutable(executablePath, spawnSync)) {
194282
return ensureFfmpeg();
@@ -208,11 +296,12 @@ export function ensurePlaywrightChromium(options = {}) {
208296
);
209297
const depsStatus = runPlaywrightInstall(["chromium"], true);
210298
if (depsStatus !== 0) {
211-
return depsStatus;
299+
return useLinuxSystemChromiumPackage();
212300
}
213301
if (existsSync(executablePath) && canRunChromiumExecutable(executablePath, spawnSync)) {
214302
return ensureFfmpeg();
215303
}
304+
return useLinuxSystemChromiumPackage();
216305
}
217306
log(
218307
`[ui-e2e] Playwright install completed but Chromium is still not runnable at ${executablePath}.`,
@@ -222,6 +311,12 @@ export function ensurePlaywrightChromium(options = {}) {
222311
return ensureFfmpeg();
223312
}
224313

314+
export function shouldEnsureFfmpegFromArgv(argv = process.argv) {
315+
return !argv.includes("--skip-ffmpeg");
316+
}
317+
225318
if (isDirectScriptExecution()) {
226-
process.exitCode = ensurePlaywrightChromium({ ensureFfmpeg: true });
319+
process.exitCode = ensurePlaywrightChromium({
320+
ensureFfmpeg: shouldEnsureFfmpegFromArgv(),
321+
});
227322
}

test/scripts/ensure-playwright-chromium.test.ts

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@
22
import { describe, expect, it, vi } from "vitest";
33
import {
44
ensurePlaywrightChromium,
5+
installLinuxSystemChromiumPackage,
56
resolvePlaywrightInstallRunner,
7+
shouldEnsureFfmpegFromArgv,
68
shouldInstallPlaywrightSystemDependencies,
79
} from "../../scripts/ensure-playwright-chromium.mjs";
810

@@ -276,6 +278,55 @@ describe("ensurePlaywrightChromium", () => {
276278
expect(logs.join("\n")).toContain("installing Linux system dependencies");
277279
});
278280

281+
it("falls back to distro Chromium when Playwright does not support the Linux runner image", () => {
282+
const logs: string[] = [];
283+
let installedSystemChromium = false;
284+
const spawnSync = vi.fn((command: string, args: string[]) => {
285+
if (command === "pnpm" && args.includes("chromium")) {
286+
return { status: 1 };
287+
}
288+
if (command === "apt-get" && args.includes("update")) {
289+
return { status: 0 };
290+
}
291+
if (command === "apt-get" && args.includes("chromium-browser")) {
292+
installedSystemChromium = true;
293+
return { status: 0 };
294+
}
295+
if (command === "/usr/bin/chromium-browser") {
296+
return { status: installedSystemChromium ? 0 : 127 };
297+
}
298+
return { status: 1 };
299+
});
300+
301+
expect(
302+
ensurePlaywrightChromium({
303+
cwd: "/repo",
304+
env: { CI: "1", PATH: "/bin" },
305+
executablePath: "/cache/chromium/chrome",
306+
existsSync: (path: string) =>
307+
installedSystemChromium && path === "/usr/bin/chromium-browser",
308+
getuid: () => 0,
309+
log: (line: string) => logs.push(line),
310+
platform: "linux",
311+
spawnSync,
312+
stdio: "pipe",
313+
systemExecutablePath: "",
314+
}),
315+
).toBe(0);
316+
expect(spawnSync).toHaveBeenCalledWith(
317+
"apt-get",
318+
["update", "-qq"],
319+
expect.objectContaining({ cwd: "/repo", stdio: "pipe" }),
320+
);
321+
expect(spawnSync).toHaveBeenCalledWith(
322+
"apt-get",
323+
["install", "-y", "chromium-browser"],
324+
expect.objectContaining({ cwd: "/repo", stdio: "pipe" }),
325+
);
326+
expect(logs.join("\n")).toContain("installing a system Chromium package");
327+
expect(logs.join("\n")).toContain("Using system Chromium at /usr/bin/chromium-browser");
328+
});
329+
279330
it("does not install Linux system dependencies for an unprivileged local lane", () => {
280331
const spawnSync = vi
281332
.fn()
@@ -343,6 +394,7 @@ describe("ensurePlaywrightChromium", () => {
343394
ensurePlaywrightChromium({
344395
executablePath: "/cache/chromium/chrome",
345396
existsSync: () => false,
397+
platform: "darwin",
346398
spawnSync: vi.fn(() => ({ status: 23 })),
347399
stdio: "pipe",
348400
systemExecutablePath: "",
@@ -404,4 +456,45 @@ describe("ensurePlaywrightChromium", () => {
404456
}),
405457
).toBe(true);
406458
});
459+
460+
it("installs Linux system Chromium packages with sudo for non-root lanes", () => {
461+
const spawnSync = vi.fn(() => ({ status: 0 }));
462+
463+
expect(
464+
installLinuxSystemChromiumPackage({
465+
cwd: "/repo",
466+
env: { PATH: "/bin" },
467+
getuid: () => 501,
468+
platform: "linux",
469+
spawnSync,
470+
stdio: "pipe",
471+
}),
472+
).toBe(0);
473+
expect(spawnSync).toHaveBeenNthCalledWith(1, "sudo", ["-n", "true"], { stdio: "ignore" });
474+
expect(spawnSync).toHaveBeenNthCalledWith(
475+
2,
476+
"sudo",
477+
["-n", "apt-get", "update", "-qq"],
478+
expect.objectContaining({ cwd: "/repo", stdio: "pipe" }),
479+
);
480+
expect(spawnSync).toHaveBeenNthCalledWith(
481+
3,
482+
"sudo",
483+
["-n", "apt-get", "install", "-y", "chromium-browser"],
484+
expect.objectContaining({ cwd: "/repo", stdio: "pipe" }),
485+
);
486+
});
487+
488+
it("allows QA scenario runners to skip optional Playwright ffmpeg", () => {
489+
expect(shouldEnsureFfmpegFromArgv(["node", "scripts/ensure-playwright-chromium.mjs"])).toBe(
490+
true,
491+
);
492+
expect(
493+
shouldEnsureFfmpegFromArgv([
494+
"node",
495+
"scripts/ensure-playwright-chromium.mjs",
496+
"--skip-ffmpeg",
497+
]),
498+
).toBe(false);
499+
});
407500
});

0 commit comments

Comments
 (0)