173 lines
5.3 KiB
JavaScript
173 lines
5.3 KiB
JavaScript
import fs from "node:fs";
|
|
import { execFileSync } from "node:child_process";
|
|
import path from "node:path";
|
|
import { pathToFileURL } from "node:url";
|
|
|
|
export const playwrightDependencyCommand = "npm install";
|
|
export const playwrightDependencySummary =
|
|
"Install repository dependencies before running browser smokes: npm install provides the declared playwright package; the Code Queue image may also provide the same package globally.";
|
|
|
|
const chromiumExecutableEnvKeys = Object.freeze([
|
|
"HWLAB_PLAYWRIGHT_CHROMIUM",
|
|
"PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH",
|
|
"CHROMIUM_PATH"
|
|
]);
|
|
|
|
const chromiumExecutableCandidates = Object.freeze([
|
|
"/snap/bin/chromium",
|
|
"/usr/bin/chromium-browser",
|
|
"/usr/bin/chromium",
|
|
"/usr/bin/google-chrome",
|
|
"/usr/bin/google-chrome-stable"
|
|
]);
|
|
|
|
export async function importPlaywright() {
|
|
try {
|
|
return normalizePlaywrightModule(await import("playwright"));
|
|
} catch (localError) {
|
|
const globalRoot = npmGlobalRoot();
|
|
if (globalRoot) {
|
|
try {
|
|
return normalizePlaywrightModule(await import(pathToFileURL(path.join(globalRoot, "playwright/index.js")).href));
|
|
} catch {
|
|
// Keep the local resolution error so remediation points at this repository's package.json.
|
|
}
|
|
}
|
|
throw localError;
|
|
}
|
|
}
|
|
|
|
export async function launchChromium(chromium, options = {}) {
|
|
const plan = chromiumLaunchPlan();
|
|
const launchOptions = {
|
|
...options,
|
|
headless: options.headless ?? true,
|
|
args: ["--no-sandbox", ...(options.args ?? [])],
|
|
...(plan.executablePath ? { executablePath: plan.executablePath } : {})
|
|
};
|
|
try {
|
|
const browser = await chromium.launch(launchOptions);
|
|
browser.hwlabLaunchPlan = { ...plan, failureCode: null };
|
|
return browser;
|
|
} catch (error) {
|
|
error.hwlabBrowserLaunch = {
|
|
...plan,
|
|
failureCode: classifyBrowserLaunchFailure(error),
|
|
remediation: browserLaunchRemediation(plan)
|
|
};
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
export async function browserDoctor() {
|
|
let playwright;
|
|
try {
|
|
playwright = await importPlaywright();
|
|
} catch (error) {
|
|
return {
|
|
status: "fail",
|
|
failureCode: "playwright-import-failed",
|
|
playwrightPackageSource: "missing",
|
|
browser: chromiumLaunchPlan(),
|
|
remediation: [playwrightDependencyCommand],
|
|
error: error instanceof Error ? error.message : String(error)
|
|
};
|
|
}
|
|
|
|
const browser = await launchChromium(playwright.chromium).catch((error) => ({ error }));
|
|
if (browser?.error) {
|
|
const plan = browser.error.hwlabBrowserLaunch ?? chromiumLaunchPlan();
|
|
return {
|
|
status: "fail",
|
|
failureCode: plan.failureCode ?? classifyBrowserLaunchFailure(browser.error),
|
|
playwrightPackageSource: "playwright",
|
|
browser: plan,
|
|
remediation: browserLaunchRemediation(plan),
|
|
error: browser.error instanceof Error ? browser.error.message : String(browser.error)
|
|
};
|
|
}
|
|
|
|
try {
|
|
const page = await browser.newPage();
|
|
await page.goto("data:text/html,<title>hwlab-browser-doctor</title>", { waitUntil: "domcontentloaded", timeout: 5000 });
|
|
await page.close();
|
|
} finally {
|
|
await browser.close();
|
|
}
|
|
|
|
return {
|
|
status: "pass",
|
|
failureCode: null,
|
|
playwrightPackageSource: "playwright",
|
|
browser: browser.hwlabLaunchPlan ?? chromiumLaunchPlan(),
|
|
remediation: []
|
|
};
|
|
}
|
|
|
|
export function chromiumLaunchPlan(env = process.env) {
|
|
const candidatePaths = chromiumExecutableCandidates.filter((candidate) => fs.existsSync(candidate));
|
|
for (const key of chromiumExecutableEnvKeys) {
|
|
const value = env[key];
|
|
if (value && fs.existsSync(value)) {
|
|
return {
|
|
browserSource: "env",
|
|
executablePath: value,
|
|
executablePathEnvKey: key,
|
|
fallbackUsed: key !== "HWLAB_PLAYWRIGHT_CHROMIUM",
|
|
candidatePaths
|
|
};
|
|
}
|
|
}
|
|
for (const candidate of chromiumExecutableCandidates) {
|
|
if (fs.existsSync(candidate)) {
|
|
return {
|
|
browserSource: "system-chromium",
|
|
executablePath: candidate,
|
|
executablePathEnvKey: null,
|
|
fallbackUsed: true,
|
|
candidatePaths
|
|
};
|
|
}
|
|
}
|
|
return {
|
|
browserSource: "playwright-cache",
|
|
executablePath: "",
|
|
executablePathEnvKey: null,
|
|
fallbackUsed: false,
|
|
candidatePaths
|
|
};
|
|
}
|
|
|
|
export function classifyBrowserLaunchFailure(error) {
|
|
const message = error instanceof Error ? error.message : String(error);
|
|
if (/Executable doesn't exist|download new browsers|playwright install/iu.test(message)) return "browser-cache-missing";
|
|
if (/spawn|ENOENT|permission denied|EACCES/iu.test(message)) return "browser-executable-unavailable";
|
|
return "browser-launch-failed";
|
|
}
|
|
|
|
export function browserLaunchRemediation(plan = chromiumLaunchPlan()) {
|
|
if (plan.executablePath) return [`using ${plan.executablePath}`];
|
|
return [
|
|
"Set HWLAB_PLAYWRIGHT_CHROMIUM=/snap/bin/chromium when system Chromium exists.",
|
|
"Install Playwright browsers with npx playwright install when no system Chromium is available."
|
|
];
|
|
}
|
|
|
|
function normalizePlaywrightModule(module) {
|
|
if (module?.chromium) return module;
|
|
if (module?.default?.chromium) return module.default;
|
|
return module;
|
|
}
|
|
|
|
function npmGlobalRoot() {
|
|
try {
|
|
return execFileSync("npm", ["root", "-g"], {
|
|
encoding: "utf8",
|
|
stdio: ["ignore", "pipe", "ignore"],
|
|
timeout: 5000
|
|
}).trim();
|
|
} catch {
|
|
return "";
|
|
}
|
|
}
|