Path: blob/main/src/tools/impl/chrome-for-testing.ts
12921 views
/*1* chrome-for-testing.ts2*3* Utilities for downloading binaries from the Chrome for Testing (CfT) API.4* https://github.com/GoogleChromeLabs/chrome-for-testing5* https://googlechromelabs.github.io/chrome-for-testing/6*7* Copyright (C) 2026 Posit Software, PBC8*/910import { basename, join } from "../../deno_ral/path.ts";11import { existsSync, safeChmodSync, safeRemoveSync, walkSync } from "../../deno_ral/fs.ts";12import { debug } from "../../deno_ral/log.ts";13import { arch, isWindows, os } from "../../deno_ral/platform.ts";14import { unzip } from "../../core/zip.ts";15import { InstallContext } from "../types.ts";1617/** CfT platform identifiers matching the Google Chrome for Testing API. */18export type CftPlatform =19| "linux64"20| "mac-arm64"21| "mac-x64"22| "win32"23| "win64";2425/** Platform detection result. */26export interface PlatformInfo {27platform: CftPlatform;28os: string;29arch: string;30}3132/**33* Map os + arch to a CfT platform string.34* Throws on unsupported platforms (e.g., linux aarch64 — to be handled by Playwright CDN).35*/36export function detectCftPlatform(): PlatformInfo {37const platformMap: Record<string, CftPlatform> = {38"linux-x86_64": "linux64",39"darwin-aarch64": "mac-arm64",40"darwin-x86_64": "mac-x64",41"windows-x86_64": "win64",42"windows-x86": "win32",43};4445const key = `${os}-${arch}`;46const platform = platformMap[key];4748if (!platform) {49if (os === "linux" && arch === "aarch64") {50throw new Error(51"linux-arm64 is not supported by Chrome for Testing. " +52"Use 'quarto install chromium' for arm64 support.",53);54}55throw new Error(56`Unsupported platform for Chrome for Testing: ${os} ${arch}`,57);58}5960return { platform, os, arch };61}6263/** A single download entry from the CfT API. */64export interface CftDownload {65platform: CftPlatform;66url: string;67}6869/** Parsed stable release from the CfT last-known-good-versions API. */70export interface CftStableRelease {71version: string;72downloads: {73chrome?: CftDownload[];74"chrome-headless-shell"?: CftDownload[];75chromedriver?: CftDownload[];76};77}7879const kCftVersionsUrl =80"https://googlechromelabs.github.io/chrome-for-testing/last-known-good-versions-with-downloads.json";8182/**83* Fetch the latest stable Chrome version and download URLs from the CfT API.84*/85export async function fetchLatestCftRelease(): Promise<CftStableRelease> {86let response: Response;87try {88response = await fetch(kCftVersionsUrl);89} catch (e) {90throw new Error(91`Failed to fetch Chrome for Testing API: ${92e instanceof Error ? e.message : String(e)93}`,94);95}9697if (!response.ok) {98throw new Error(99`Chrome for Testing API returned ${response.status}: ${response.statusText}`,100);101}102103// deno-lint-ignore no-explicit-any104let data: any;105try {106data = await response.json();107} catch {108throw new Error("Chrome for Testing API returned invalid JSON");109}110111const stable = data?.channels?.Stable;112if (!stable || !stable.version || !stable.downloads) {113throw new Error(114"Chrome for Testing API response missing expected 'channels.Stable' structure",115);116}117118return {119version: stable.version,120downloads: stable.downloads,121};122}123124/**125* Find a named executable inside an extracted CfT directory.126* Handles platform-specific naming (.exe on Windows) and nested directory structures.127* Returns absolute path to the executable, or undefined if not found.128*/129export function findCftExecutable(130extractedDir: string,131binaryName: string,132): string | undefined {133const target = isWindows ? `${binaryName}.exe` : binaryName;134135// CfT zips extract to {binaryName}-{platform}/{target}136try {137const { platform } = detectCftPlatform();138const knownPath = join(extractedDir, `${binaryName}-${platform}`, target);139if (existsSync(knownPath)) {140return knownPath;141}142} catch (e) {143debug(`findCftExecutable: platform detection failed, falling back to walk: ${e}`);144}145146// Fallback: bounded walk for unexpected directory structures147for (const entry of walkSync(extractedDir, { includeDirs: false, maxDepth: 3 })) {148if (basename(entry.path) === target) {149return entry.path;150}151}152153return undefined;154}155156/**157* Download a CfT zip from URL, extract to targetDir, set executable permissions.158* Uses InstallContext.download() for progress reporting with the given label.159* When binaryName is provided, sets executable permission only on that binary.160* Returns the target directory path.161*/162export async function downloadAndExtractCft(163label: string,164url: string,165targetDir: string,166context: InstallContext,167binaryName?: string,168): Promise<string> {169const tempZipPath = Deno.makeTempFileSync({ suffix: ".zip" });170171try {172await context.download(label, url, tempZipPath);173await unzip(tempZipPath, targetDir);174} finally {175safeRemoveSync(tempZipPath);176}177178if (binaryName) {179const executable = findCftExecutable(targetDir, binaryName);180if (executable) {181safeChmodSync(executable, 0o755);182} else {183debug(`downloadAndExtractCft: expected binary '${binaryName}' not found in ${targetDir}`);184}185}186187return targetDir;188}189190191