diff --git a/docs/browsers-api/browsers.browser.md b/docs/browsers-api/browsers.browser.md
index 9020e963314..a7f7e8fc377 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 5aa10b21dcb..ab72615b352 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 ff02ab47917..3ad8ccbe282 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 00000000000..cb5b48fad88
--- /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 48d2ee1c830..2f818e095cb 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 00000000000..b65ea146cca
--- /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 00000000000..a514628e3a6
--- /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 00000000000..6270887171e
--- /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 40ae3567c8b..311b960a5e2 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 a54d23fbe88..1d42f158b64 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;
}