/** * Copyright (c) Microsoft Corporation. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import { fork } from 'node:child_process'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; import { testDebug } from './log.js'; import type { FullConfig } from './config.js'; // Cache installs per-process so repeated launches don't re-run the installer. const installedBrowsers = new Set(); const inflightInstalls = new Map>(); function browserTarget(browserConfig: FullConfig['browser']): string { return browserConfig.launchOptions?.channel ?? browserConfig.browserName ?? 'chrome'; } /** * Runs `playwright install ` in a forked child process. * Safe to call concurrently — in-flight installs are deduplicated per target. */ export async function installBrowser(browserConfig: FullConfig['browser']): Promise { const target = browserTarget(browserConfig); if (installedBrowsers.has(target)) return; const existing = inflightInstalls.get(target); if (existing) return existing; const promise = runInstall(target).then(() => { installedBrowsers.add(target); }).finally(() => { inflightInstalls.delete(target); }); inflightInstalls.set(target, promise); return promise; } async function runInstall(target: string): Promise { testDebug(`auto-installing browser: ${target}`); const cliPath = resolvePlaywrightCli(); await runPlaywrightCli(cliPath, ['install', target]); // Best-effort system-deps install. Only runs when we're already root, // otherwise skipped silently — users will see Playwright's own missing-lib // error on the next launch, which tells them exactly what to apt install. if (process.getuid && process.getuid() === 0) { try { await runPlaywrightCli(cliPath, ['install-deps', target]); } catch (e) { testDebug(`install-deps failed (non-fatal): ${e}`); } } } function resolvePlaywrightCli(): string { try { const cliUrl = import.meta.resolve('playwright/package.json'); return path.join(fileURLToPath(cliUrl), '..', 'cli.js'); } catch (e) { throw new Error( 'Playwright package not found. Install it with: npm install playwright\n' + `Original error: ${e instanceof Error ? e.message : String(e)}` ); } } function runPlaywrightCli(cliPath: string, args: string[]): Promise { const child = fork(cliPath, args, { stdio: 'pipe' }); const output: string[] = []; child.stdout?.on('data', data => output.push(data.toString())); child.stderr?.on('data', data => output.push(data.toString())); return new Promise((resolve, reject) => { child.on('close', code => { if (code === 0) { resolve(); return; } reject(new Error(`playwright ${args.join(' ')} failed (exit ${code}):\n${output.join('')}`)); }); child.on('error', reject); }); } /** * Returns true if the given error indicates the browser executable is missing * and needs to be downloaded via `playwright install`. */ export function isMissingBrowserError(error: unknown): boolean { if (!(error instanceof Error)) return false; const msg = error.message; return msg.includes("Executable doesn't exist") || msg.includes('please run the following command') || msg.includes('npx playwright install'); }