From a96d6a7dbb62634ff4ad0c2d67df80c09f08dd3e Mon Sep 17 00:00:00 2001 From: Pavel Feldman Date: Sat, 13 Mar 2021 14:02:39 -0800 Subject: [PATCH] feat: allow to pick stable channel (#5817) --- .github/workflows/tests.yml | 2 +- docs/src/api/class-browsertype.md | 13 +++ src/cli/cli.ts | 4 +- src/client/connection.ts | 2 +- src/protocol/channels.ts | 2 + src/protocol/protocol.yml | 1 + src/protocol/validator.ts | 1 + src/server/browserType.ts | 4 +- src/server/chromium/chromium.ts | 7 ++ src/server/chromium/findChromiumChannel.ts | 92 ++++++++++++++++++++++ src/server/types.ts | 1 + test/fixtures.ts | 1 + types/types.d.ts | 13 +++ 13 files changed, 137 insertions(+), 6 deletions(-) create mode 100644 src/server/chromium/findChromiumChannel.ts diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 2e73d086a0..2f9e12ca0c 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -283,7 +283,7 @@ jobs: - run: xvfb-run --auto-servernum --server-args="-screen 0 1280x960x24" -- bash -c "ulimit -c unlimited && npx folio test/ --workers=1 --forbid-only --timeout=60000 --global-timeout=5400000 --retries=3 --reporter=dot,json -p video" env: BROWSER: "chromium" - CRPATH: "/opt/google/chrome/chrome" + PW_CHROMIUM_CHANNEL: "chrome" FOLIO_JSON_OUTPUT_NAME: "test-results/report.json" - uses: actions/upload-artifact@v1 if: ${{ always() }} diff --git a/docs/src/api/class-browsertype.md b/docs/src/api/class-browsertype.md index 8459833dc6..34f0baea9d 100644 --- a/docs/src/api/class-browsertype.md +++ b/docs/src/api/class-browsertype.md @@ -178,6 +178,19 @@ Whether to run browser in headless mode. More details for [Firefox](https://developer.mozilla.org/en-US/docs/Mozilla/Firefox/Headless_mode). Defaults to `true` unless the [`option: devtools`] option is `true`. +### option: BrowserType.launch.channel +- `channel` <[string]> + +Chromium distribution channel, one of +* chrome +* chrome-beta +* chrome-dev +* chrome-canary +* msedge +* msedge-beta +* msedge-dev +* msedge-canary + ### option: BrowserType.launch.executablePath - `executablePath` <[path]> diff --git a/src/cli/cli.ts b/src/cli/cli.ts index 3d99a8da8e..050763ce3a 100755 --- a/src/cli/cli.ts +++ b/src/cli/cli.ts @@ -103,12 +103,12 @@ program program .command('install-deps [browserType...]') - .description('install dependencies necessary to run browser') + .description('install dependencies necessary to run browsers (will ask for sudo permissions)') .action(async function(browserType) { try { await installDeps(browserType); } catch (e) { - console.log(`Failed to install browsers\n${e}`); + console.log(`Failed to install browser dependencies\n${e}`); process.exit(1); } }); diff --git a/src/client/connection.ts b/src/client/connection.ts index c001c60e31..0460f7e2c4 100644 --- a/src/client/connection.ts +++ b/src/client/connection.ts @@ -82,7 +82,7 @@ export class Connection { return await new Promise((resolve, reject) => this._callbacks.set(id, { resolve, reject })); } catch (e) { const innerStack = ((process.env.PWDEBUGIMPL || isUnderTest()) && e.stack) ? e.stack.substring(e.stack.indexOf(e.message) + e.message.length) : ''; - e.stack = e.message + innerStack + stack; + e.stack = e.message + innerStack + '\n' + stack; throw e; } } diff --git a/src/protocol/channels.ts b/src/protocol/channels.ts index fe43f805cd..b13ef6d949 100644 --- a/src/protocol/channels.ts +++ b/src/protocol/channels.ts @@ -217,6 +217,7 @@ export interface BrowserTypeChannel extends Channel { connectOverCDP(params: BrowserTypeConnectOverCDPParams, metadata?: Metadata): Promise; } export type BrowserTypeLaunchParams = { + channel?: string, executablePath?: string, args?: string[], ignoreAllDefaultArgs?: boolean, @@ -240,6 +241,7 @@ export type BrowserTypeLaunchParams = { slowMo?: number, }; export type BrowserTypeLaunchOptions = { + channel?: string, executablePath?: string, args?: string[], ignoreAllDefaultArgs?: boolean, diff --git a/src/protocol/protocol.yml b/src/protocol/protocol.yml index 2bb038548c..15889453b5 100644 --- a/src/protocol/protocol.yml +++ b/src/protocol/protocol.yml @@ -300,6 +300,7 @@ BrowserType: launch: parameters: + channel: string? executablePath: string? args: type: array? diff --git a/src/protocol/validator.ts b/src/protocol/validator.ts index 7c21783fb9..64f7ad61de 100644 --- a/src/protocol/validator.ts +++ b/src/protocol/validator.ts @@ -154,6 +154,7 @@ export function createScheme(tChannel: (name: string) => Validator): Scheme { contentScript: tOptional(tBoolean), }); scheme.BrowserTypeLaunchParams = tObject({ + channel: tOptional(tString), executablePath: tOptional(tString), args: tOptional(tArray(tString)), ignoreAllDefaultArgs: tOptional(tBoolean), diff --git a/src/server/browserType.ts b/src/server/browserType.ts index 752a78c39d..aaa555f31d 100644 --- a/src/server/browserType.ts +++ b/src/server/browserType.ts @@ -51,7 +51,7 @@ export abstract class BrowserType extends SdkObject { this._registry = playwrightOptions.registry; } - executablePath(): string { + executablePath(options?: types.LaunchOptions): string { return this._registry.executablePath(this._name) || ''; } @@ -165,7 +165,7 @@ export abstract class BrowserType extends SdkObject { else browserArguments.push(...this._defaultArgs(options, isPersistent, userDataDir)); - const executable = executablePath || this.executablePath(); + const executable = executablePath || this.executablePath(options); if (!executable) throw new Error(`No executable path is specified. Pass "executablePath" option directly.`); if (!(await existsAsync(executable))) { diff --git a/src/server/chromium/chromium.ts b/src/server/chromium/chromium.ts index 16de00bb24..e9bb4e0671 100644 --- a/src/server/chromium/chromium.ts +++ b/src/server/chromium/chromium.ts @@ -31,6 +31,7 @@ import { ProgressController } from '../progress'; import { TimeoutSettings } from '../../utils/timeoutSettings'; import { helper } from '../helper'; import { CallMetadata } from '../instrumentation'; +import { findChromiumChannel } from './findChromiumChannel'; export class Chromium extends BrowserType { private _devtools: CRDevTools | undefined; @@ -42,6 +43,12 @@ export class Chromium extends BrowserType { this._devtools = this._createDevTools(); } + executablePath(options?: types.LaunchOptions): string { + if (options?.channel) + return findChromiumChannel(options.channel); + return super.executablePath(options); + } + async connectOverCDP(metadata: CallMetadata, wsEndpoint: string, options: { slowMo?: number, sdkLanguage: string }, timeout?: number) { const controller = new ProgressController(metadata, this); controller.setLogName('browser'); diff --git a/src/server/chromium/findChromiumChannel.ts b/src/server/chromium/findChromiumChannel.ts new file mode 100644 index 0000000000..260e148878 --- /dev/null +++ b/src/server/chromium/findChromiumChannel.ts @@ -0,0 +1,92 @@ +/** + * Copyright (c) Microsoft Corporation. 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 fs from 'fs'; +import path from 'path'; + +function darwin(channel: string): string | undefined { + switch (channel) { + case 'chrome': return '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome'; + case 'chrome-canary': return '/Applications/Google Chrome Canary.app/Contents/MacOS/Google Chrome Canary'; + case 'msedge': return '/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge'; + case 'msedge-beta': return '/Applications/Microsoft Edge Beta.app/Contents/MacOS/Microsoft Edge Beta'; + case 'msedge-dev': return '/Applications/Microsoft Edge Dev.app/Contents/MacOS/Microsoft Edge Dev'; + case 'msedge-canary': return '/Applications/Microsoft Edge Canary.app/Contents/MacOS/Microsoft Edge Canary'; + } +} + +function linux(channel: string): string | undefined { + switch (channel) { + case 'chrome': return '/opt/google/chrome/chrome'; + case 'chrome-beta': return '/opt/google/chrome-beta/chrome'; + case 'chrome-dev': return '/opt/google/chrome-unstable/chrome'; + case 'msedge-dev': return '/opt/microsoft/msedge-dev/msedge'; + } +} + +function win32(channel: string): string | undefined { + let suffix: string | undefined; + switch (channel) { + case 'chrome': suffix = `\\Google\\Chrome\\Application\\chrome.exe`; break; + case 'chrome-canary': suffix = `\\Google\\Chrome SxS\\Application\\chrome.exe`; break; + case 'msedge': suffix = `\\Microsoft\\Edge\\Application\\msedge.exe`; break; + case 'msedge-beta': suffix = `\\Microsoft\\Edge Beta\\Application\\msedge.exe`; break; + case 'msedge-dev': suffix = `\\Microsoft\\Edge Dev\\Application\\msedge.exe`; break; + case 'msedge-canary': suffix = `\\Microsoft\\Edge SxS\\Application\\msedge.exe`; break; + } + if (!suffix) + return; + const prefixes = [ + process.env.LOCALAPPDATA, process.env.PROGRAMFILES, process.env['PROGRAMFILES(X86)'] + ].filter(Boolean) as string[]; + + let result: string | undefined; + prefixes.forEach(prefix => { + const chromePath = path.join(prefix, suffix!); + if (canAccess(chromePath)) + result = chromePath; + }); + return result; +} + +function canAccess(file: string) { + if (!file) + return false; + + try { + fs.accessSync(file); + return true; + } catch (e) { + return false; + } +} + +export function findChromiumChannel(channel: string): string { + let result: string | undefined; + if (process.platform === 'linux') + result = linux(channel); + else if (process.platform === 'win32') + result = win32(channel); + else if (process.platform === 'darwin') + result = darwin(channel); + + if (!result) + throw new Error(`Chromium distribution '${channel}' is not supported on ${process.platform}`); + + if (canAccess(result)) + return result; + throw new Error(`Chromium distribution was not found: ${channel}`); +} diff --git a/src/server/types.ts b/src/server/types.ts index e49d8e8005..7e00b8d253 100644 --- a/src/server/types.ts +++ b/src/server/types.ts @@ -250,6 +250,7 @@ export type BrowserContextOptions = { export type EnvArray = { name: string, value: string }[]; type LaunchOptionsBase = { + channel?: string, executablePath?: string, args?: string[], ignoreDefaultArgs?: string[], diff --git a/test/fixtures.ts b/test/fixtures.ts index 0091dfc459..e2d7acb320 100644 --- a/test/fixtures.ts +++ b/test/fixtures.ts @@ -92,6 +92,7 @@ fixtures.browserOptions.override(async ({ browserName, headful, slowMo }, run) = if (executablePath) console.error(`Using executable at ${executablePath}`); await run({ + channel: process.env.PW_CHROMIUM_CHANNEL, executablePath, handleSIGINT: false, slowMo, diff --git a/types/types.d.ts b/types/types.d.ts index 3d1e06ab4f..db58257b77 100644 --- a/types/types.d.ts +++ b/types/types.d.ts @@ -10645,6 +10645,19 @@ export interface LaunchOptions { */ args?: Array; + /** + * Chromium distribution channel, one of + * - chrome + * - chrome-beta + * - chrome-dev + * - chrome-canary + * - msedge + * - msedge-beta + * - msedge-dev + * - msedge-canary + */ + channel?: string; + /** * Enable Chromium sandboxing. Defaults to `false`. */