diff options
Diffstat (limited to 'remote/test/puppeteer/packages/browsers/src')
10 files changed, 233 insertions, 27 deletions
diff --git a/remote/test/puppeteer/packages/browsers/src/CLI.ts b/remote/test/puppeteer/packages/browsers/src/CLI.ts index 255f5545b4..281f22c9f5 100644 --- a/remote/test/puppeteer/packages/browsers/src/CLI.ts +++ b/remote/test/puppeteer/packages/browsers/src/CLI.ts @@ -173,6 +173,18 @@ export class CLI { 'Install the latest available build for the Chrome browser.' ); yargs.example( + '$0 install chrome@stable', + 'Install the latest available build for the Chrome browser from the stable channel.' + ); + yargs.example( + '$0 install chrome@beta', + 'Install the latest available build for the Chrome browser from the beta channel.' + ); + yargs.example( + '$0 install chrome@dev', + 'Install the latest available build for the Chrome browser from the dev channel.' + ); + yargs.example( '$0 install chrome@canary', 'Install the latest available build for the Chrome Canary browser.' ); @@ -238,6 +250,7 @@ export class CLI { } args.browser.buildId = pinnedVersion; } + const originalBuildId = args.browser.buildId; args.browser.buildId = await resolveBuildId( args.browser.name, args.platform, @@ -253,6 +266,10 @@ export class CLI { args.browser.buildId ), baseUrl: args.baseUrl, + buildIdAlias: + originalBuildId !== args.browser.buildId + ? originalBuildId + : undefined, }); console.log( `${args.browser.name}@${ diff --git a/remote/test/puppeteer/packages/browsers/src/Cache.ts b/remote/test/puppeteer/packages/browsers/src/Cache.ts index 13b465835a..e6b574d9dd 100644 --- a/remote/test/puppeteer/packages/browsers/src/Cache.ts +++ b/remote/test/puppeteer/packages/browsers/src/Cache.ts @@ -8,13 +8,18 @@ import fs from 'fs'; import os from 'os'; import path from 'path'; +import debug from 'debug'; + import { Browser, type BrowserPlatform, executablePathByBrowser, + getVersionComparator, } from './browser-data/browser-data.js'; import {detectBrowserPlatform} from './detectPlatform.js'; +const debugCache = debug('puppeteer:browsers:cache'); + /** * @public */ @@ -57,6 +62,14 @@ export class InstalledBrowser { this.buildId ); } + + readMetadata(): Metadata { + return this.#cache.readMetadata(this.browser); + } + + writeMetadata(metadata: Metadata): void { + this.#cache.writeMetadata(this.browser, metadata); + } } /** @@ -80,6 +93,11 @@ export interface ComputeExecutablePathOptions { buildId: string; } +export interface Metadata { + // Maps an alias (canary/latest/dev/etc.) to a buildId. + aliases: Record<string, string>; +} + /** * The cache used by Puppeteer relies on the following structure: * @@ -112,6 +130,39 @@ export class Cache { return path.join(this.#rootDir, browser); } + metadataFile(browser: Browser): string { + return path.join(this.browserRoot(browser), '.metadata'); + } + + readMetadata(browser: Browser): Metadata { + const metatadaPath = this.metadataFile(browser); + if (!fs.existsSync(metatadaPath)) { + return {aliases: {}}; + } + // TODO: add type-safe parsing. + const data = JSON.parse(fs.readFileSync(metatadaPath, 'utf8')); + if (typeof data !== 'object') { + throw new Error('.metadata is not an object'); + } + return data; + } + + writeMetadata(browser: Browser, metadata: Metadata): void { + const metatadaPath = this.metadataFile(browser); + fs.mkdirSync(path.dirname(metatadaPath), {recursive: true}); + fs.writeFileSync(metatadaPath, JSON.stringify(metadata, null, 2)); + } + + resolveAlias(browser: Browser, alias: string): string | undefined { + const metadata = this.readMetadata(browser); + if (alias === 'latest') { + return Object.values(metadata.aliases || {}) + .sort(getVersionComparator(browser)) + .at(-1); + } + return metadata.aliases[alias]; + } + installationDir( browser: Browser, platform: BrowserPlatform, @@ -134,6 +185,12 @@ export class Cache { platform: BrowserPlatform, buildId: string ): void { + const metadata = this.readMetadata(browser); + for (const alias of Object.keys(metadata.aliases)) { + if (metadata.aliases[alias] === buildId) { + delete metadata.aliases[alias]; + } + } fs.rmSync(this.installationDir(browser, platform, buildId), { force: true, recursive: true, @@ -180,6 +237,12 @@ export class Cache { `Cannot download a binary for the provided platform: ${os.platform()} (${os.arch()})` ); } + try { + options.buildId = + this.resolveAlias(options.browser, options.buildId) ?? options.buildId; + } catch { + debugCache('could not read .metadata file for the browser'); + } const installationDir = this.installationDir( options.browser, options.platform, diff --git a/remote/test/puppeteer/packages/browsers/src/browser-data/browser-data.ts b/remote/test/puppeteer/packages/browsers/src/browser-data/browser-data.ts index 67bb4990b2..3e78030aa7 100644 --- a/remote/test/puppeteer/packages/browsers/src/browser-data/browser-data.ts +++ b/remote/test/puppeteer/packages/browsers/src/browser-data/browser-data.ts @@ -43,6 +43,14 @@ export const executablePathByBrowser = { [Browser.FIREFOX]: firefox.relativeExecutablePath, }; +export const versionComparators = { + [Browser.CHROMEDRIVER]: chromedriver.compareVersions, + [Browser.CHROMEHEADLESSSHELL]: chromeHeadlessShell.compareVersions, + [Browser.CHROME]: chrome.compareVersions, + [Browser.CHROMIUM]: chromium.compareVersions, + [Browser.FIREFOX]: firefox.compareVersions, +}; + export {Browser, BrowserPlatform, ChromeReleaseChannel}; /** @@ -185,3 +193,15 @@ export function resolveSystemExecutablePath( return chrome.resolveSystemExecutablePath(platform, channel); } } + +/** + * Returns a version comparator for the given browser that can be used to sort + * browser versions. + * + * @public + */ +export function getVersionComparator( + browser: Browser +): (a: string, b: string) => number { + return versionComparators[browser]; +} diff --git a/remote/test/puppeteer/packages/browsers/src/browser-data/chrome-headless-shell.ts b/remote/test/puppeteer/packages/browsers/src/browser-data/chrome-headless-shell.ts index b1c6178de8..f5f65cdbd4 100644 --- a/remote/test/puppeteer/packages/browsers/src/browser-data/chrome-headless-shell.ts +++ b/remote/test/puppeteer/packages/browsers/src/browser-data/chrome-headless-shell.ts @@ -25,7 +25,7 @@ function folder(platform: BrowserPlatform): string { export function resolveDownloadUrl( platform: BrowserPlatform, buildId: string, - baseUrl = 'https://edgedl.me.gvt1.com/edgedl/chrome/chrome-for-testing' + baseUrl = 'https://storage.googleapis.com/chrome-for-testing-public' ): string { return `${baseUrl}/${resolveDownloadPath(platform, buildId).join('/')}`; } @@ -66,4 +66,4 @@ export function relativeExecutablePath( } } -export {resolveBuildId} from './chrome.js'; +export {resolveBuildId, compareVersions} from './chrome.js'; diff --git a/remote/test/puppeteer/packages/browsers/src/browser-data/chrome.ts b/remote/test/puppeteer/packages/browsers/src/browser-data/chrome.ts index c6329255c3..c7975163fc 100644 --- a/remote/test/puppeteer/packages/browsers/src/browser-data/chrome.ts +++ b/remote/test/puppeteer/packages/browsers/src/browser-data/chrome.ts @@ -6,6 +6,8 @@ import path from 'path'; +import semver from 'semver'; + import {getJSON} from '../httpUtil.js'; import {BrowserPlatform, ChromeReleaseChannel} from './types.js'; @@ -28,7 +30,7 @@ function folder(platform: BrowserPlatform): string { export function resolveDownloadUrl( platform: BrowserPlatform, buildId: string, - baseUrl = 'https://edgedl.me.gvt1.com/edgedl/chrome/chrome-for-testing' + baseUrl = 'https://storage.googleapis.com/chrome-for-testing-public' ): string { return `${baseUrl}/${resolveDownloadPath(platform, buildId).join('/')}`; } @@ -193,3 +195,19 @@ export function resolveSystemExecutablePath( `Unable to detect browser executable path for '${channel}' on ${platform}.` ); } + +export function compareVersions(a: string, b: string): number { + if (!semver.valid(a)) { + throw new Error(`Version ${a} is not a valid semver version`); + } + if (!semver.valid(b)) { + throw new Error(`Version ${b} is not a valid semver version`); + } + if (semver.gt(a, b)) { + return 1; + } else if (semver.lt(a, b)) { + return -1; + } else { + return 0; + } +} diff --git a/remote/test/puppeteer/packages/browsers/src/browser-data/chromedriver.ts b/remote/test/puppeteer/packages/browsers/src/browser-data/chromedriver.ts index 290598d0d7..2f1242b249 100644 --- a/remote/test/puppeteer/packages/browsers/src/browser-data/chromedriver.ts +++ b/remote/test/puppeteer/packages/browsers/src/browser-data/chromedriver.ts @@ -25,7 +25,7 @@ function folder(platform: BrowserPlatform): string { export function resolveDownloadUrl( platform: BrowserPlatform, buildId: string, - baseUrl = 'https://edgedl.me.gvt1.com/edgedl/chrome/chrome-for-testing' + baseUrl = 'https://storage.googleapis.com/chrome-for-testing-public' ): string { return `${baseUrl}/${resolveDownloadPath(platform, buildId).join('/')}`; } @@ -53,4 +53,4 @@ export function relativeExecutablePath( } } -export {resolveBuildId} from './chrome.js'; +export {resolveBuildId, compareVersions} from './chrome.js'; diff --git a/remote/test/puppeteer/packages/browsers/src/browser-data/chromium.ts b/remote/test/puppeteer/packages/browsers/src/browser-data/chromium.ts index 09cfc987a8..820e76d018 100644 --- a/remote/test/puppeteer/packages/browsers/src/browser-data/chromium.ts +++ b/remote/test/puppeteer/packages/browsers/src/browser-data/chromium.ts @@ -86,3 +86,7 @@ export async function resolveBuildId( ) ); } + +export function compareVersions(a: string, b: string): number { + return Number(a) - Number(b); +} diff --git a/remote/test/puppeteer/packages/browsers/src/browser-data/firefox.ts b/remote/test/puppeteer/packages/browsers/src/browser-data/firefox.ts index ccc30fa1b5..87e3804c8f 100644 --- a/remote/test/puppeteer/packages/browsers/src/browser-data/firefox.ts +++ b/remote/test/puppeteer/packages/browsers/src/browser-data/firefox.ts @@ -235,10 +235,6 @@ function defaultProfilePreferences( // Disable the GFX sanity window 'media.sanity-test.disabled': true, - // Prevent various error message on the console - // jest-puppeteer asserts that no error message is emitted by the console - 'network.cookie.cookieBehavior': 0, - // Disable experimental feature that is only available in Nightly 'network.cookie.sameSite.laxByDefault': false, @@ -328,3 +324,8 @@ async function writePreferences(options: ProfileOptions): Promise<void> { await fs.promises.copyFile(prefsPath, prefsBackupPath); } } + +export function compareVersions(a: string, b: string): number { + // TODO: this is a not very reliable check. + return parseInt(a.replace('.', ''), 16) - parseInt(b.replace('.', ''), 16); +} diff --git a/remote/test/puppeteer/packages/browsers/src/install.ts b/remote/test/puppeteer/packages/browsers/src/install.ts index 375c75babc..e78b2c3461 100644 --- a/remote/test/puppeteer/packages/browsers/src/install.ts +++ b/remote/test/puppeteer/packages/browsers/src/install.ts @@ -11,15 +11,15 @@ import os from 'os'; import path from 'path'; import { - type Browser, - type BrowserPlatform, + Browser, + BrowserPlatform, downloadUrls, } from './browser-data/browser-data.js'; import {Cache, InstalledBrowser} from './Cache.js'; import {debug} from './debug.js'; import {detectBrowserPlatform} from './detectPlatform.js'; import {unpackArchive} from './fileUtil.js'; -import {downloadFile, headHttpRequest} from './httpUtil.js'; +import {downloadFile, getJSON, headHttpRequest} from './httpUtil.js'; const debugInstall = debug('puppeteer:browsers:install'); @@ -63,6 +63,13 @@ export interface InstallOptions { */ buildId: string; /** + * An alias for the provided `buildId`. It will be used to maintain local + * metadata to support aliases in the `launch` command. + * + * @example 'canary' + */ + buildIdAlias?: string; + /** * Provides information about the progress of the download. */ downloadProgressCallback?: ( @@ -74,7 +81,7 @@ export interface InstallOptions { * * @defaultValue Either * - * - https://edgedl.me.gvt1.com/edgedl/chrome/chrome-for-testing or + * - https://storage.googleapis.com/chrome-for-testing-public or * - https://archive.mozilla.org/pub/firefox/nightly/latest-mozilla-central * */ @@ -115,6 +122,68 @@ export async function install( options.buildId, options.baseUrl ); + try { + return await installUrl(url, options); + } catch (err) { + debugInstall(`Error downloading from ${url}.`); + switch (options.browser) { + case Browser.CHROME: + case Browser.CHROMEDRIVER: + case Browser.CHROMEHEADLESSSHELL: { + debugInstall( + `Trying to find download URL via https://googlechromelabs.github.io/chrome-for-testing.` + ); + interface Version { + downloads: Record<string, Array<{platform: string; url: string}>>; + } + const version = (await getJSON( + new URL( + `https://googlechromelabs.github.io/chrome-for-testing/${options.buildId}.json` + ) + )) as Version; + let platform = ''; + switch (options.platform) { + case BrowserPlatform.LINUX: + platform = 'linux64'; + break; + case BrowserPlatform.MAC_ARM: + platform = 'mac-arm64'; + break; + case BrowserPlatform.MAC: + platform = 'mac-x64'; + break; + case BrowserPlatform.WIN32: + platform = 'win32'; + break; + case BrowserPlatform.WIN64: + platform = 'win64'; + break; + } + const url = version.downloads[options.browser]?.find(link => { + return link['platform'] === platform; + })?.url; + if (url) { + debugInstall(`Falling back to downloading from ${url}.`); + return await installUrl(new URL(url), options); + } + throw err; + } + default: + throw err; + } + } +} + +async function installUrl( + url: URL, + options: InstallOptions +): Promise<InstalledBrowser | string> { + options.platform ??= detectBrowserPlatform(); + if (!options.platform) { + throw new Error( + `Cannot download a binary for the provided platform: ${os.platform()} (${os.arch()})` + ); + } const fileName = url.toString().split('/').pop(); assert(fileName, `A malformed download URL was found: ${url}.`); const cache = new Cache(options.cacheDir); @@ -140,15 +209,22 @@ export async function install( options.platform, options.buildId ); - if (existsSync(outputPath)) { - return new InstalledBrowser( - cache, - options.browser, - options.buildId, - options.platform - ); - } + try { + if (existsSync(outputPath)) { + const installedBrowser = new InstalledBrowser( + cache, + options.browser, + options.buildId, + options.platform + ); + if (!existsSync(installedBrowser.executablePath)) { + throw new Error( + `The browser folder (${outputPath}) exists but the executable (${installedBrowser.executablePath}) is missing` + ); + } + return installedBrowser; + } debugInstall(`Downloading binary from ${url}`); try { debugTime('download'); @@ -164,17 +240,23 @@ export async function install( } finally { debugTimeEnd('extract'); } + const installedBrowser = new InstalledBrowser( + cache, + options.browser, + options.buildId, + options.platform + ); + if (options.buildIdAlias) { + const metadata = installedBrowser.readMetadata(); + metadata.aliases[options.buildIdAlias] = options.buildId; + installedBrowser.writeMetadata(metadata); + } + return installedBrowser; } finally { if (existsSync(archivePath)) { await unlink(archivePath); } } - return new InstalledBrowser( - cache, - options.browser, - options.buildId, - options.platform - ); } /** diff --git a/remote/test/puppeteer/packages/browsers/src/main.ts b/remote/test/puppeteer/packages/browsers/src/main.ts index df93de530d..c03c0c0090 100644 --- a/remote/test/puppeteer/packages/browsers/src/main.ts +++ b/remote/test/puppeteer/packages/browsers/src/main.ts @@ -37,6 +37,7 @@ export { BrowserPlatform, ChromeReleaseChannel, createProfile, + getVersionComparator, } from './browser-data/browser-data.js'; export {CLI, makeProgressCallback} from './CLI.js'; export {Cache, InstalledBrowser} from './Cache.js'; |