From 416843ba68aaab7ae14bbc74c2ac705e877e91a7 Mon Sep 17 00:00:00 2001 From: Mathias Bynens Date: Wed, 16 Aug 2023 13:34:54 +0200 Subject: [PATCH] feat: support chrome-headless-shell (#10739) --- docs/browsers-api/browsers.browser.md | 13 ++- packages/browsers/src/CLI.ts | 12 ++ .../browsers/src/browser-data/browser-data.ts | 32 ++++++ .../src/browser-data/chrome-headless-shell.ts | 79 ++++++++++++++ packages/browsers/src/browser-data/types.ts | 1 + .../chrome-headless-shell-data.spec.ts | 81 ++++++++++++++ .../src/chrome-headless-shell/cli.spec.ts | 91 ++++++++++++++++ .../src/chrome-headless-shell/install.spec.ts | 103 ++++++++++++++++++ packages/browsers/test/src/versions.ts | 3 +- .../browsers/tools/downloadTestBrowsers.mjs | 7 +- 10 files changed, 413 insertions(+), 9 deletions(-) create mode 100644 packages/browsers/src/browser-data/chrome-headless-shell.ts create mode 100644 packages/browsers/test/src/chrome-headless-shell/chrome-headless-shell-data.spec.ts create mode 100644 packages/browsers/test/src/chrome-headless-shell/cli.spec.ts create mode 100644 packages/browsers/test/src/chrome-headless-shell/install.spec.ts diff --git a/docs/browsers-api/browsers.browser.md b/docs/browsers-api/browsers.browser.md index 9020e96331408..a7f7e8fc377ec 100644 --- a/docs/browsers-api/browsers.browser.md +++ b/docs/browsers-api/browsers.browser.md @@ -14,9 +14,10 @@ export declare enum Browser ## Enumeration Members -| Member | Value | Description | -| ------------ | ------------------------------------- | ----------- | -| CHROME | "chrome" | | -| CHROMEDRIVER | "chromedriver" | | -| CHROMIUM | "chromium" | | -| FIREFOX | "firefox" | | +| Member | Value | Description | +| ------------------- | ---------------------------------------------- | ----------- | +| CHROME | "chrome" | | +| CHROMEDRIVER | "chromedriver" | | +| CHROMEHEADLESSSHELL | "chrome-headless-shell" | | +| CHROMIUM | "chromium" | | +| FIREFOX | "firefox" | | diff --git a/packages/browsers/src/CLI.ts b/packages/browsers/src/CLI.ts index 5aa10b21dcbaf..ab72615b3523e 100644 --- a/packages/browsers/src/CLI.ts +++ b/packages/browsers/src/CLI.ts @@ -152,6 +152,18 @@ export class CLI { '$0 install chromedriver@115.0.5790', 'Install the latest available patch (115.0.5790.X) build for ChromeDriver.' ); + yargs.example( + '$0 install chrome-headless-shell', + 'Install the latest available chrome-headless-shell build.' + ); + yargs.example( + '$0 install chrome-headless-shell@beta', + 'Install the latest available chrome-headless-shell build corresponding to the Beta channel.' + ); + yargs.example( + '$0 install chrome-headless-shell@118', + 'Install the latest available chrome-headless-shell 118 build.' + ); yargs.example( '$0 install chromium@1083080', 'Install the revision 1083080 of the Chromium browser.' diff --git a/packages/browsers/src/browser-data/browser-data.ts b/packages/browsers/src/browser-data/browser-data.ts index ff02ab47917ed..3ad8ccbe282e0 100644 --- a/packages/browsers/src/browser-data/browser-data.ts +++ b/packages/browsers/src/browser-data/browser-data.ts @@ -14,6 +14,7 @@ * limitations under the License. */ +import * as chromeHeadlessShell from './chrome-headless-shell.js'; import * as chrome from './chrome.js'; import * as chromedriver from './chromedriver.js'; import * as chromium from './chromium.js'; @@ -30,6 +31,7 @@ export {ProfileOptions}; export const downloadUrls = { [Browser.CHROMEDRIVER]: chromedriver.resolveDownloadUrl, + [Browser.CHROMEHEADLESSSHELL]: chromeHeadlessShell.resolveDownloadUrl, [Browser.CHROME]: chrome.resolveDownloadUrl, [Browser.CHROMIUM]: chromium.resolveDownloadUrl, [Browser.FIREFOX]: firefox.resolveDownloadUrl, @@ -37,6 +39,7 @@ export const downloadUrls = { export const downloadPaths = { [Browser.CHROMEDRIVER]: chromedriver.resolveDownloadPath, + [Browser.CHROMEHEADLESSSHELL]: chromeHeadlessShell.resolveDownloadPath, [Browser.CHROME]: chrome.resolveDownloadPath, [Browser.CHROMIUM]: chromium.resolveDownloadPath, [Browser.FIREFOX]: firefox.resolveDownloadPath, @@ -44,6 +47,7 @@ export const downloadPaths = { export const executablePathByBrowser = { [Browser.CHROMEDRIVER]: chromedriver.relativeExecutablePath, + [Browser.CHROMEHEADLESSSHELL]: chromeHeadlessShell.relativeExecutablePath, [Browser.CHROME]: chrome.relativeExecutablePath, [Browser.CHROMIUM]: chromium.relativeExecutablePath, [Browser.FIREFOX]: firefox.relativeExecutablePath, @@ -111,6 +115,33 @@ export async function resolveBuildId( } return tag; } + case Browser.CHROMEHEADLESSSHELL: { + switch (tag) { + case BrowserTag.LATEST: + case BrowserTag.CANARY: + return await chromeHeadlessShell.resolveBuildId( + ChromeReleaseChannel.CANARY + ); + case BrowserTag.BETA: + return await chromeHeadlessShell.resolveBuildId( + ChromeReleaseChannel.BETA + ); + case BrowserTag.DEV: + return await chromeHeadlessShell.resolveBuildId( + ChromeReleaseChannel.DEV + ); + case BrowserTag.STABLE: + return await chromeHeadlessShell.resolveBuildId( + ChromeReleaseChannel.STABLE + ); + default: + const result = await chromeHeadlessShell.resolveBuildId(tag); + if (result) { + return result; + } + } + return tag; + } case Browser.CHROMIUM: switch (tag as BrowserTag) { case BrowserTag.LATEST: @@ -154,6 +185,7 @@ export function resolveSystemExecutablePath( ): string { switch (browser) { case Browser.CHROMEDRIVER: + case Browser.CHROMEHEADLESSSHELL: case Browser.FIREFOX: case Browser.CHROMIUM: throw new Error( diff --git a/packages/browsers/src/browser-data/chrome-headless-shell.ts b/packages/browsers/src/browser-data/chrome-headless-shell.ts new file mode 100644 index 0000000000000..cb5b48fad88a5 --- /dev/null +++ b/packages/browsers/src/browser-data/chrome-headless-shell.ts @@ -0,0 +1,79 @@ +/** + * Copyright 2023 Google Inc. All rights reserved. + * + * 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 path from 'path'; + +import {BrowserPlatform} from './types.js'; + +function folder(platform: BrowserPlatform): string { + switch (platform) { + case BrowserPlatform.LINUX: + return 'linux64'; + case BrowserPlatform.MAC_ARM: + return 'mac-arm64'; + case BrowserPlatform.MAC: + return 'mac-x64'; + case BrowserPlatform.WIN32: + return 'win32'; + case BrowserPlatform.WIN64: + return 'win64'; + } +} + +export function resolveDownloadUrl( + platform: BrowserPlatform, + buildId: string, + baseUrl = 'https://edgedl.me.gvt1.com/edgedl/chrome/chrome-for-testing' +): string { + return `${baseUrl}/${resolveDownloadPath(platform, buildId).join('/')}`; +} + +export function resolveDownloadPath( + platform: BrowserPlatform, + buildId: string +): string[] { + return [ + buildId, + folder(platform), + `chrome-headless-shell-${folder(platform)}.zip`, + ]; +} + +export function relativeExecutablePath( + platform: BrowserPlatform, + _buildId: string +): string { + switch (platform) { + case BrowserPlatform.MAC: + case BrowserPlatform.MAC_ARM: + return path.join( + 'chrome-headless-shell-' + folder(platform), + 'chrome-headless-shell' + ); + case BrowserPlatform.LINUX: + return path.join( + 'chrome-headless-shell-linux64', + 'chrome-headless-shell' + ); + case BrowserPlatform.WIN32: + case BrowserPlatform.WIN64: + return path.join( + 'chrome-headless-shell-' + folder(platform), + 'chrome-headless-shell.exe' + ); + } +} + +export {resolveBuildId} from './chrome.js'; diff --git a/packages/browsers/src/browser-data/types.ts b/packages/browsers/src/browser-data/types.ts index 48d2ee1c830dd..2f818e095cb5e 100644 --- a/packages/browsers/src/browser-data/types.ts +++ b/packages/browsers/src/browser-data/types.ts @@ -24,6 +24,7 @@ import * as firefox from './firefox.js'; */ export enum Browser { CHROME = 'chrome', + CHROMEHEADLESSSHELL = 'chrome-headless-shell', CHROMIUM = 'chromium', FIREFOX = 'firefox', CHROMEDRIVER = 'chromedriver', diff --git a/packages/browsers/test/src/chrome-headless-shell/chrome-headless-shell-data.spec.ts b/packages/browsers/test/src/chrome-headless-shell/chrome-headless-shell-data.spec.ts new file mode 100644 index 0000000000000..b65ea146cca99 --- /dev/null +++ b/packages/browsers/test/src/chrome-headless-shell/chrome-headless-shell-data.spec.ts @@ -0,0 +1,81 @@ +/** + * Copyright 2023 Google Inc. All rights reserved. + * + * 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 assert from 'assert'; +import path from 'path'; + +import {BrowserPlatform} from '../../../lib/cjs/browser-data/browser-data.js'; +import { + resolveDownloadUrl, + relativeExecutablePath, + resolveBuildId, +} from '../../../lib/cjs/browser-data/chrome-headless-shell.js'; + +describe('chrome-headless-shell', () => { + it('should resolve download URLs', () => { + assert.strictEqual( + resolveDownloadUrl(BrowserPlatform.LINUX, '118.0.5950.0'), + 'https://edgedl.me.gvt1.com/edgedl/chrome/chrome-for-testing/118.0.5950.0/linux64/chrome-headless-shell-linux64.zip' + ); + assert.strictEqual( + resolveDownloadUrl(BrowserPlatform.MAC, '118.0.5950.0'), + 'https://edgedl.me.gvt1.com/edgedl/chrome/chrome-for-testing/118.0.5950.0/mac-x64/chrome-headless-shell-mac-x64.zip' + ); + assert.strictEqual( + resolveDownloadUrl(BrowserPlatform.MAC_ARM, '118.0.5950.0'), + 'https://edgedl.me.gvt1.com/edgedl/chrome/chrome-for-testing/118.0.5950.0/mac-arm64/chrome-headless-shell-mac-arm64.zip' + ); + assert.strictEqual( + resolveDownloadUrl(BrowserPlatform.WIN32, '118.0.5950.0'), + 'https://edgedl.me.gvt1.com/edgedl/chrome/chrome-for-testing/118.0.5950.0/win32/chrome-headless-shell-win32.zip' + ); + assert.strictEqual( + resolveDownloadUrl(BrowserPlatform.WIN64, '118.0.5950.0'), + 'https://edgedl.me.gvt1.com/edgedl/chrome/chrome-for-testing/118.0.5950.0/win64/chrome-headless-shell-win64.zip' + ); + }); + + it('should resolve milestones', async () => { + assert.strictEqual(await resolveBuildId('118'), '118.0.5950.0'); + }); + + it('should resolve build prefix', async () => { + assert.strictEqual(await resolveBuildId('118.0.5950'), '118.0.5950.0'); + }); + + it('should resolve executable paths', () => { + assert.strictEqual( + relativeExecutablePath(BrowserPlatform.LINUX, '12372323'), + path.join('chrome-headless-shell-linux64', 'chrome-headless-shell') + ); + assert.strictEqual( + relativeExecutablePath(BrowserPlatform.MAC, '12372323'), + path.join('chrome-headless-shell-mac-x64/', 'chrome-headless-shell') + ); + assert.strictEqual( + relativeExecutablePath(BrowserPlatform.MAC_ARM, '12372323'), + path.join('chrome-headless-shell-mac-arm64', 'chrome-headless-shell') + ); + assert.strictEqual( + relativeExecutablePath(BrowserPlatform.WIN32, '12372323'), + path.join('chrome-headless-shell-win32', 'chrome-headless-shell.exe') + ); + assert.strictEqual( + relativeExecutablePath(BrowserPlatform.WIN64, '12372323'), + path.join('chrome-headless-shell-win64', 'chrome-headless-shell.exe') + ); + }); +}); diff --git a/packages/browsers/test/src/chrome-headless-shell/cli.spec.ts b/packages/browsers/test/src/chrome-headless-shell/cli.spec.ts new file mode 100644 index 0000000000000..a514628e3a6a2 --- /dev/null +++ b/packages/browsers/test/src/chrome-headless-shell/cli.spec.ts @@ -0,0 +1,91 @@ +/** + * Copyright 2023 Google Inc. All rights reserved. + * + * 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 assert from 'assert'; +import fs from 'fs'; +import os from 'os'; +import path from 'path'; + +import {CLI} from '../../../lib/cjs/CLI.js'; +import { + createMockedReadlineInterface, + setupTestServer, + getServerUrl, +} from '../utils.js'; +import {testChromeHeadlessShellBuildId} from '../versions.js'; + +describe('chrome-headless-shell CLI', function () { + this.timeout(90000); + + setupTestServer(); + + let tmpDir = '/tmp/puppeteer-browsers-test'; + + beforeEach(() => { + tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'puppeteer-browsers-test')); + }); + + afterEach(async () => { + await new CLI(tmpDir, createMockedReadlineInterface('yes')).run([ + 'npx', + '@puppeteer/browsers', + 'clear', + `--path=${tmpDir}`, + `--base-url=${getServerUrl()}`, + ]); + }); + + it('should download chrome-headless-shell binaries', async () => { + await new CLI(tmpDir).run([ + 'npx', + '@puppeteer/browsers', + 'install', + `chrome-headless-shell@${testChromeHeadlessShellBuildId}`, + `--path=${tmpDir}`, + '--platform=linux', + `--base-url=${getServerUrl()}`, + ]); + assert.ok( + fs.existsSync( + path.join( + tmpDir, + 'chrome-headless-shell', + `linux-${testChromeHeadlessShellBuildId}`, + 'chrome-headless-shell-linux64', + 'chrome-headless-shell' + ) + ) + ); + + await new CLI(tmpDir, createMockedReadlineInterface('no')).run([ + 'npx', + '@puppeteer/browsers', + 'clear', + `--path=${tmpDir}`, + ]); + assert.ok( + fs.existsSync( + path.join( + tmpDir, + 'chrome-headless-shell', + `linux-${testChromeHeadlessShellBuildId}`, + 'chrome-headless-shell-linux64', + 'chrome-headless-shell' + ) + ) + ); + }); +}); diff --git a/packages/browsers/test/src/chrome-headless-shell/install.spec.ts b/packages/browsers/test/src/chrome-headless-shell/install.spec.ts new file mode 100644 index 0000000000000..6270887171e1e --- /dev/null +++ b/packages/browsers/test/src/chrome-headless-shell/install.spec.ts @@ -0,0 +1,103 @@ +/** + * Copyright 2023 Google Inc. All rights reserved. + * + * 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 assert from 'assert'; +import fs from 'fs'; +import os from 'os'; +import path from 'path'; + +import { + install, + canDownload, + Browser, + BrowserPlatform, + Cache, +} from '../../../lib/cjs/main.js'; +import {getServerUrl, setupTestServer} from '../utils.js'; +import {testChromeDriverBuildId} from '../versions.js'; + +/** + * Tests in this spec use real download URLs and unpack live browser archives + * so it requires the network access. + */ +describe('ChromeDriver install', () => { + setupTestServer(); + + let tmpDir = '/tmp/puppeteer-browsers-test'; + + beforeEach(() => { + tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'puppeteer-browsers-test')); + }); + + afterEach(() => { + new Cache(tmpDir).clear(); + }); + + it('should check if a buildId can be downloaded', async () => { + assert.ok( + await canDownload({ + cacheDir: tmpDir, + browser: Browser.CHROMEDRIVER, + platform: BrowserPlatform.LINUX, + buildId: testChromeDriverBuildId, + baseUrl: getServerUrl(), + }) + ); + }); + + it('should report if a buildId is not downloadable', async () => { + assert.strictEqual( + await canDownload({ + cacheDir: tmpDir, + browser: Browser.CHROMEDRIVER, + platform: BrowserPlatform.LINUX, + buildId: 'unknown', + baseUrl: getServerUrl(), + }), + false + ); + }); + + it('should download and unpack the binary', async function () { + this.timeout(60000); + const expectedOutputPath = path.join( + tmpDir, + 'chromedriver', + `${BrowserPlatform.LINUX}-${testChromeDriverBuildId}` + ); + assert.strictEqual(fs.existsSync(expectedOutputPath), false); + let browser = await install({ + cacheDir: tmpDir, + browser: Browser.CHROMEDRIVER, + platform: BrowserPlatform.LINUX, + buildId: testChromeDriverBuildId, + baseUrl: getServerUrl(), + }); + assert.strictEqual(browser.path, expectedOutputPath); + assert.ok(fs.existsSync(expectedOutputPath)); + // Second iteration should be no-op. + browser = await install({ + cacheDir: tmpDir, + browser: Browser.CHROMEDRIVER, + platform: BrowserPlatform.LINUX, + buildId: testChromeDriverBuildId, + baseUrl: getServerUrl(), + }); + assert.strictEqual(browser.path, expectedOutputPath); + assert.ok(fs.existsSync(expectedOutputPath)); + assert.ok(fs.existsSync(browser.executablePath)); + }); +}); diff --git a/packages/browsers/test/src/versions.ts b/packages/browsers/test/src/versions.ts index 40ae3567c8bce..311b960a5e224 100644 --- a/packages/browsers/test/src/versions.ts +++ b/packages/browsers/test/src/versions.ts @@ -18,5 +18,6 @@ export const testChromeBuildId = '113.0.5672.0'; export const testChromiumBuildId = '1083080'; // TODO: We can add a Cron job to auto-update on change. // Firefox keeps only `latest` version of Nightly builds. -export const testFirefoxBuildId = '117.0a1'; +export const testFirefoxBuildId = '118.0a1'; export const testChromeDriverBuildId = '115.0.5763.0'; +export const testChromeHeadlessShellBuildId = '118.0.5950.0'; diff --git a/packages/browsers/tools/downloadTestBrowsers.mjs b/packages/browsers/tools/downloadTestBrowsers.mjs index a54d23fbe8800..1d42f158b646d 100644 --- a/packages/browsers/tools/downloadTestBrowsers.mjs +++ b/packages/browsers/tools/downloadTestBrowsers.mjs @@ -32,7 +32,11 @@ function getBrowser(str) { const match = str.match(regex); if (match && match[1]) { - return match[1].toLowerCase(); + const lowercased = match[1].toLowerCase(); + if (lowercased === 'chromeheadlessshell') { + return 'chrome-headless-shell'; + } + return lowercased; } else { return null; } @@ -42,7 +46,6 @@ const cacheDir = normalize(join('.', 'test', 'cache')); for (const version of Object.keys(versions)) { const browser = getBrowser(version); - if (!browser) { continue; }