diff --git a/docs/browsers-api/browsers.browser.md b/docs/browsers-api/browsers.browser.md index 53b649aac38..9020e963314 100644 --- a/docs/browsers-api/browsers.browser.md +++ b/docs/browsers-api/browsers.browser.md @@ -14,8 +14,9 @@ export declare enum Browser ## Enumeration Members -| Member | Value | Description | -| -------- | --------------------------------- | ----------- | -| CHROME | "chrome" | | -| CHROMIUM | "chromium" | | -| FIREFOX | "firefox" | | +| Member | Value | Description | +| ------------ | ------------------------------------- | ----------- | +| CHROME | "chrome" | | +| CHROMEDRIVER | "chromedriver" | | +| CHROMIUM | "chromium" | | +| FIREFOX | "firefox" | | diff --git a/packages/browsers/src/browser-data/browser-data.ts b/packages/browsers/src/browser-data/browser-data.ts index 03f95f5979e..413435453a8 100644 --- a/packages/browsers/src/browser-data/browser-data.ts +++ b/packages/browsers/src/browser-data/browser-data.ts @@ -15,6 +15,7 @@ */ import * as chrome from './chrome.js'; +import * as chromedriver from './chromedriver.js'; import * as chromium from './chromium.js'; import * as firefox from './firefox.js'; import { @@ -28,18 +29,21 @@ import { export {ProfileOptions}; export const downloadUrls = { + [Browser.CHROMEDRIVER]: chromedriver.resolveDownloadUrl, [Browser.CHROME]: chrome.resolveDownloadUrl, [Browser.CHROMIUM]: chromium.resolveDownloadUrl, [Browser.FIREFOX]: firefox.resolveDownloadUrl, }; export const downloadPaths = { + [Browser.CHROMEDRIVER]: chromedriver.resolveDownloadPath, [Browser.CHROME]: chrome.resolveDownloadPath, [Browser.CHROMIUM]: chromium.resolveDownloadPath, [Browser.FIREFOX]: firefox.resolveDownloadPath, }; export const executablePathByBrowser = { + [Browser.CHROMEDRIVER]: chromedriver.relativeExecutablePath, [Browser.CHROME]: chrome.relativeExecutablePath, [Browser.CHROMIUM]: chromium.relativeExecutablePath, [Browser.FIREFOX]: firefox.relativeExecutablePath, @@ -67,6 +71,11 @@ export async function resolveBuildId( // In CfT beta is the latest version. return await chrome.resolveBuildId(platform, 'beta'); } + case Browser.CHROMEDRIVER: + switch (tag as BrowserTag) { + case BrowserTag.LATEST: + return await chromedriver.resolveBuildId('latest'); + } case Browser.CHROMIUM: switch (tag as BrowserTag) { case BrowserTag.LATEST: @@ -102,9 +111,10 @@ export function resolveSystemExecutablePath( channel: ChromeReleaseChannel ): string { switch (browser) { + case Browser.CHROMEDRIVER: case Browser.FIREFOX: throw new Error( - 'System browser detection is not supported for Firefox yet.' + `System browser detection is not supported for ${browser} yet.` ); case Browser.CHROME: return chromium.resolveSystemExecutablePath(platform, channel); diff --git a/packages/browsers/src/browser-data/chromedriver.ts b/packages/browsers/src/browser-data/chromedriver.ts new file mode 100644 index 00000000000..39894d2e866 --- /dev/null +++ b/packages/browsers/src/browser-data/chromedriver.ts @@ -0,0 +1,93 @@ +/** + * 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 {httpRequest} from '../httpUtil.js'; + +import {BrowserPlatform} from './types.js'; + +function archive(platform: BrowserPlatform): string { + switch (platform) { + case BrowserPlatform.LINUX: + return 'chromedriver_linux64'; + case BrowserPlatform.MAC_ARM: + return 'chromedriver_mac_arm64'; + case BrowserPlatform.MAC: + return 'chromedriver_mac64'; + case BrowserPlatform.WIN32: + case BrowserPlatform.WIN64: + return 'chromedriver_win32'; + } +} + +export function resolveDownloadUrl( + platform: BrowserPlatform, + buildId: string, + baseUrl = 'https://chromedriver.storage.googleapis.com' +): string { + return `${baseUrl}/${resolveDownloadPath(platform, buildId).join('/')}`; +} + +export function resolveDownloadPath( + platform: BrowserPlatform, + buildId: string +): string[] { + return [buildId, `${archive(platform)}.zip`]; +} + +export function relativeExecutablePath( + platform: BrowserPlatform, + _buildId: string +): string { + switch (platform) { + case BrowserPlatform.MAC: + case BrowserPlatform.MAC_ARM: + case BrowserPlatform.LINUX: + return 'chromedriver'; + case BrowserPlatform.WIN32: + case BrowserPlatform.WIN64: + return 'chromedriver.exe'; + } +} +export async function resolveBuildId( + _channel: 'latest' = 'latest' +): Promise { + return new Promise((resolve, reject) => { + const request = httpRequest( + new URL(`https://chromedriver.storage.googleapis.com/LATEST_RELEASE`), + 'GET', + response => { + let data = ''; + if (response.statusCode && response.statusCode >= 400) { + return reject(new Error(`Got status code ${response.statusCode}`)); + } + response.on('data', chunk => { + data += chunk; + }); + response.on('end', () => { + try { + return resolve(String(data)); + } catch { + return reject(new Error('Chrome version not found')); + } + }); + }, + false + ); + request.on('error', err => { + reject(err); + }); + }); +} diff --git a/packages/browsers/src/browser-data/types.ts b/packages/browsers/src/browser-data/types.ts index 5b2f84d8ab9..f88d2ca0982 100644 --- a/packages/browsers/src/browser-data/types.ts +++ b/packages/browsers/src/browser-data/types.ts @@ -26,6 +26,7 @@ export enum Browser { CHROME = 'chrome', CHROMIUM = 'chromium', FIREFOX = 'firefox', + CHROMEDRIVER = 'chromedriver', } /** diff --git a/packages/browsers/test/src/chromedriver/chromedriver-data.spec.ts b/packages/browsers/test/src/chromedriver/chromedriver-data.spec.ts new file mode 100644 index 00000000000..fb4134a6631 --- /dev/null +++ b/packages/browsers/test/src/chromedriver/chromedriver-data.spec.ts @@ -0,0 +1,71 @@ +/** + * 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 {BrowserPlatform} from '../../../lib/cjs/browser-data/browser-data.js'; +import { + resolveDownloadUrl, + relativeExecutablePath, +} from '../../../lib/cjs/browser-data/chromedriver.js'; + +describe('ChromeDriver', () => { + it('should resolve download URLs', () => { + assert.strictEqual( + resolveDownloadUrl(BrowserPlatform.LINUX, '112.0.5615.49'), + 'https://chromedriver.storage.googleapis.com/112.0.5615.49/chromedriver_linux64.zip' + ); + assert.strictEqual( + resolveDownloadUrl(BrowserPlatform.MAC, '112.0.5615.49'), + 'https://chromedriver.storage.googleapis.com/112.0.5615.49/chromedriver_mac64.zip' + ); + assert.strictEqual( + resolveDownloadUrl(BrowserPlatform.MAC_ARM, '112.0.5615.49'), + 'https://chromedriver.storage.googleapis.com/112.0.5615.49/chromedriver_mac_arm64.zip' + ); + assert.strictEqual( + resolveDownloadUrl(BrowserPlatform.WIN32, '112.0.5615.49'), + 'https://chromedriver.storage.googleapis.com/112.0.5615.49/chromedriver_win32.zip' + ); + assert.strictEqual( + resolveDownloadUrl(BrowserPlatform.WIN64, '112.0.5615.49'), + 'https://chromedriver.storage.googleapis.com/112.0.5615.49/chromedriver_win32.zip' + ); + }); + + it('should resolve executable paths', () => { + assert.strictEqual( + relativeExecutablePath(BrowserPlatform.LINUX, '12372323'), + 'chromedriver' + ); + assert.strictEqual( + relativeExecutablePath(BrowserPlatform.MAC, '12372323'), + 'chromedriver' + ); + assert.strictEqual( + relativeExecutablePath(BrowserPlatform.MAC_ARM, '12372323'), + 'chromedriver' + ); + assert.strictEqual( + relativeExecutablePath(BrowserPlatform.WIN32, '12372323'), + 'chromedriver.exe' + ); + assert.strictEqual( + relativeExecutablePath(BrowserPlatform.WIN64, '12372323'), + 'chromedriver.exe' + ); + }); +}); diff --git a/packages/browsers/test/src/chromedriver/cli.spec.ts b/packages/browsers/test/src/chromedriver/cli.spec.ts new file mode 100644 index 00000000000..52c23d22c23 --- /dev/null +++ b/packages/browsers/test/src/chromedriver/cli.spec.ts @@ -0,0 +1,89 @@ +/** + * 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 {testChromeDriverBuildId} from '../versions.js'; + +describe('ChromeDriver 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 ChromeDriver binaries', async () => { + await new CLI(tmpDir).run([ + 'npx', + '@puppeteer/browsers', + 'install', + `chromedriver@${testChromeDriverBuildId}`, + `--path=${tmpDir}`, + '--platform=linux', + `--base-url=${getServerUrl()}`, + ]); + assert.ok( + fs.existsSync( + path.join( + tmpDir, + 'chromedriver', + `linux-${testChromeDriverBuildId}`, + 'chromedriver' + ) + ) + ); + + await new CLI(tmpDir, createMockedReadlineInterface('no')).run([ + 'npx', + '@puppeteer/browsers', + 'clear', + `--path=${tmpDir}`, + ]); + assert.ok( + fs.existsSync( + path.join( + tmpDir, + 'chromedriver', + `linux-${testChromeDriverBuildId}`, + 'chromedriver' + ) + ) + ); + }); +}); diff --git a/packages/browsers/test/src/chromedriver/install.spec.ts b/packages/browsers/test/src/chromedriver/install.spec.ts new file mode 100644 index 00000000000..fb725de010e --- /dev/null +++ b/packages/browsers/test/src/chromedriver/install.spec.ts @@ -0,0 +1,102 @@ +/** + * 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)); + }); +}); diff --git a/packages/browsers/test/src/versions.ts b/packages/browsers/test/src/versions.ts index b09b8a6de5a..56440c8b187 100644 --- a/packages/browsers/test/src/versions.ts +++ b/packages/browsers/test/src/versions.ts @@ -19,3 +19,4 @@ 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 = '113.0a1'; +export const testChromeDriverBuildId = '112.0.5615.49';