diff --git a/index.d.ts b/index.d.ts index 183459231d..858413f594 100644 --- a/index.d.ts +++ b/index.d.ts @@ -19,3 +19,4 @@ export function playwright(browser: 'chromium'): import('./lib/api').ChromiumPla export function playwright(browser: 'firefox'): import('./lib/api').FirefoxPlaywright; export function playwright(browser: 'webkit'): import('./lib/api').WebKitPlaywright; export function connect(browser: 'chromium'): import('./lib/api').ChromiumBrowser.connect; +export function connect(browser: 'firefox'): import('./lib/api').FirefoxBrowser.connect; diff --git a/index.js b/index.js index 571c21677c..8d7e577620 100644 --- a/index.js +++ b/index.js @@ -37,5 +37,7 @@ module.exports.playwright = browser => { module.exports.connect = browser => { if (browser === 'chromium') return api.ChromiumBrowser.connect; + if (browser === 'firefox') + return api.FirefoxBrowser.connect; throw new Error(`Unsupported browser "${browser}"`); }; diff --git a/src/api.ts b/src/api.ts index ff07115f4d..97080c7817 100644 --- a/src/api.ts +++ b/src/api.ts @@ -29,6 +29,7 @@ export { Coverage, FileChooser, Page, Worker } from './page'; export { BrowserFetcher } from './server/browserFetcher'; export { CRPlaywright as ChromiumPlaywright, CRBrowserServer as ChromiumBrowserServer } from './server/crPlaywright'; +export { FFPlaywright as FirefoxPlaywright, FFBrowserServer as FirefoxBrowserServer } from './server/ffPlaywright'; export * from './chromium/crApi'; export * from './firefox/ffApi'; diff --git a/src/chromium/crBrowser.ts b/src/chromium/crBrowser.ts index d99cf4e090..831a50d455 100644 --- a/src/chromium/crBrowser.ts +++ b/src/chromium/crBrowser.ts @@ -305,15 +305,14 @@ export class CRBrowser extends browser.Browser { } export async function createTransport(options: CRConnectOptions): Promise { - assert(Number(!!options.browserWSEndpoint) + Number(!!options.browserURL) + Number(!!options.transport) === 1, 'Exactly one of browserWSEndpoint, browserURL or transport must be passed to playwright.connect'); + assert(Number(!!options.browserWSEndpoint) + Number(!!options.browserURL) + Number(!!options.transport) === 1, 'Exactly one of browserWSEndpoint, browserURL or transport must be passed to connect'); let transport: ConnectionTransport | undefined; - let connectionURL: string = ''; if (options.transport) { transport = options.transport; } else if (options.browserWSEndpoint) { - connectionURL = options.browserWSEndpoint; transport = await platform.createWebSocketTransport(options.browserWSEndpoint); } else if (options.browserURL) { + let connectionURL: string; try { const data = await platform.fetchUrl(new URL('/json/version', options.browserURL).href); connectionURL = JSON.parse(data).webSocketDebuggerUrl; diff --git a/src/firefox/ffApi.ts b/src/firefox/ffApi.ts index ec6fb4d8b4..c4274c95a5 100644 --- a/src/firefox/ffApi.ts +++ b/src/firefox/ffApi.ts @@ -15,4 +15,3 @@ */ export { FFBrowser as FirefoxBrowser } from './ffBrowser'; -export { FFPlaywright as FirefoxPlaywright } from './ffPlaywright'; diff --git a/src/firefox/ffBrowser.ts b/src/firefox/ffBrowser.ts index 6618c2c313..2d76e40644 100644 --- a/src/firefox/ffBrowser.ts +++ b/src/firefox/ffBrowser.ts @@ -22,9 +22,16 @@ import { assert, helper, RegisteredListener } from '../helper'; import * as network from '../network'; import * as types from '../types'; import { Page } from '../page'; -import { ConnectionTransport } from '../transport'; +import { ConnectionTransport, SlowMoTransport } from '../transport'; import { ConnectionEvents, FFConnection, FFSessionEvents } from './ffConnection'; import { FFPage } from './ffPage'; +import * as platform from '../platform'; + +export type FFConnectOptions = { + slowMo?: number, + browserWSEndpoint?: string; + transport?: ConnectionTransport; +}; export class FFBrowser extends browser.Browser { _connection: FFConnection; @@ -33,11 +40,13 @@ export class FFBrowser extends browser.Browser { private _contexts: Map; private _eventListeners: RegisteredListener[]; - static async create(transport: ConnectionTransport) { + static async connect(options: FFConnectOptions): Promise { + const transport = await createTransport(options); const connection = new FFConnection(transport); const {browserContextIds} = await connection.send('Target.getBrowserContexts'); const browser = new FFBrowser(connection, browserContextIds); await connection.send('Target.enable'); + await browser._waitForTarget(t => t.type() === 'page'); return browser; } @@ -270,3 +279,13 @@ export class Target { return this._browser; } } + +export async function createTransport(options: FFConnectOptions): Promise { + assert(Number(!!options.browserWSEndpoint) + Number(!!options.transport) === 1, 'Exactly one of browserWSEndpoint or transport must be passed to connect'); + let transport: ConnectionTransport | undefined; + if (options.transport) + transport = options.transport; + else if (options.browserWSEndpoint) + transport = await platform.createWebSocketTransport(options.browserWSEndpoint); + return SlowMoTransport.wrap(transport, options.slowMo); +} diff --git a/src/firefox/ffPlaywright.ts b/src/firefox/ffPlaywright.ts deleted file mode 100644 index 97d5d6eabc..0000000000 --- a/src/firefox/ffPlaywright.ts +++ /dev/null @@ -1,79 +0,0 @@ -/** - * Copyright 2017 Google Inc. All rights reserved. - * Modifications copyright (c) Microsoft Corporation. - * - * 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 * as browsers from '../browser'; -import { FFBrowser } from './ffBrowser'; -import { BrowserFetcher, BrowserFetcherOptions, OnProgressCallback, BrowserFetcherRevisionInfo } from '../server/browserFetcher'; -import { SlowMoTransport } from '../transport'; -import { DeviceDescriptors } from '../deviceDescriptors'; -import * as Errors from '../errors'; -import * as types from '../types'; -import { FFLauncher, createBrowserFetcher } from './ffLauncher'; -import * as platform from '../platform'; - -export class FFPlaywright { - private _projectRoot: string; - private _launcher: FFLauncher; - readonly _revision: string; - - constructor(projectRoot: string, preferredRevision: string) { - this._projectRoot = projectRoot; - this._launcher = new FFLauncher(projectRoot, preferredRevision); - this._revision = preferredRevision; - } - - async downloadBrowser(options?: BrowserFetcherOptions & { onProgress?: OnProgressCallback }): Promise { - const fetcher = this.createBrowserFetcher(options); - const revisionInfo = fetcher.revisionInfo(this._revision); - await fetcher.download(this._revision, options ? options.onProgress : undefined); - return revisionInfo; - } - - async launch(options: any): Promise { - const server = await this._launcher.launch(options); - return server.connect(); - } - - async launchServer(options: any): Promise> { - return this._launcher.launch(options); - } - - async connect(options: { slowMo?: number, browserWSEndpoint: string }): Promise { - const transport = await platform.createWebSocketTransport(options.browserWSEndpoint); - return FFBrowser.create(SlowMoTransport.wrap(transport, options.slowMo || 0)); - } - - executablePath(): string { - return this._launcher.executablePath(); - } - - get devices(): types.Devices { - return DeviceDescriptors; - } - - get errors(): any { - return Errors; - } - - defaultArgs(options: any | undefined): string[] { - return this._launcher.defaultArgs(options); - } - - createBrowserFetcher(options?: BrowserFetcherOptions): BrowserFetcher { - return createBrowserFetcher(this._projectRoot, options); - } -} diff --git a/src/firefox/ffLauncher.ts b/src/server/ffPlaywright.ts similarity index 73% rename from src/firefox/ffLauncher.ts rename to src/server/ffPlaywright.ts index 619f7f49a4..8a7da196a0 100644 --- a/src/firefox/ffLauncher.ts +++ b/src/server/ffPlaywright.ts @@ -15,52 +15,77 @@ * limitations under the License. */ +import { FFBrowser, FFConnectOptions, createTransport } from '../firefox/ffBrowser'; +import { BrowserFetcher, BrowserFetcherOptions, OnProgressCallback, BrowserFetcherRevisionInfo } from '../server/browserFetcher'; +import { DeviceDescriptors } from '../deviceDescriptors'; +import { launchProcess, waitForLine } from '../server/processLauncher'; +import * as Errors from '../errors'; +import * as types from '../types'; +import * as platform from '../platform'; +import { FFConnection } from '../firefox/ffConnection'; +import { ChildProcess } from 'child_process'; +import * as fs from 'fs'; import * as os from 'os'; import * as path from 'path'; -import { FFBrowser } from './ffBrowser'; -import { BrowserFetcher, BrowserFetcherOptions } from '../server/browserFetcher'; -import * as fs from 'fs'; import * as util from 'util'; -import { assert } from '../helper'; import { TimeoutError } from '../errors'; -import { SlowMoTransport } from '../transport'; -import { launchProcess, waitForLine } from '../server/processLauncher'; -import { BrowserServer } from '../browser'; -import * as platform from '../platform'; +import { assert } from '../helper'; -const mkdtempAsync = util.promisify(fs.mkdtemp); -const writeFileAsync = util.promisify(fs.writeFile); +export class FFBrowserServer { + private _process: ChildProcess; + private _connectOptions: FFConnectOptions; -const DEFAULT_ARGS = [ - '-no-remote', -]; + constructor(process: ChildProcess, connectOptions: FFConnectOptions) { + this._process = process; + this._connectOptions = connectOptions; + } -export class FFLauncher { + async connect(): Promise { + return FFBrowser.connect(this._connectOptions); + } + + process(): ChildProcess { + return this._process; + } + + wsEndpoint(): string | null { + return this._connectOptions.browserWSEndpoint || null; + } + + connectOptions(): FFConnectOptions { + return this._connectOptions; + } + + async close(): Promise { + const transport = await createTransport(this._connectOptions); + const connection = new FFConnection(transport); + await connection.send('Browser.close'); + connection.dispose(); + } +} + +export class FFPlaywright { private _projectRoot: string; - private _preferredRevision: string; - constructor(projectRoot, preferredRevision) { + readonly _revision: string; + + constructor(projectRoot: string, preferredRevision: string) { this._projectRoot = projectRoot; - this._preferredRevision = preferredRevision; + this._revision = preferredRevision; } - defaultArgs(options: any = {}) { - const { - headless = true, - args = [], - userDataDir = null, - } = options; - const firefoxArguments = [...DEFAULT_ARGS]; - if (userDataDir) - firefoxArguments.push('-profile', userDataDir); - if (headless) - firefoxArguments.push('-headless'); - firefoxArguments.push(...args); - if (args.every(arg => arg.startsWith('-'))) - firefoxArguments.push('about:blank'); - return firefoxArguments; + async downloadBrowser(options?: BrowserFetcherOptions & { onProgress?: OnProgressCallback }): Promise { + const fetcher = this.createBrowserFetcher(options); + const revisionInfo = fetcher.revisionInfo(this._revision); + await fetcher.download(this._revision, options ? options.onProgress : undefined); + return revisionInfo; } - async launch(options: any = {}): Promise> { + async launch(options: any): Promise { + const server = await this.launchServer(options); + return server.connect(); + } + + async launchServer(options: any = {}): Promise { const { ignoreDefaultArgs = false, args = [], @@ -113,81 +138,115 @@ export class FFLauncher { pipe: false, tempDir: temporaryProfileDir }, () => { - if (temporaryProfileDir || !browser) + if (temporaryProfileDir || !server) return Promise.reject(); - browser.close(); + server.close(); }); - let browser: FFBrowser | undefined; + let server: FFBrowserServer | undefined; try { const timeoutError = new TimeoutError(`Timed out after ${timeout} ms while trying to connect to Firefox!`); const match = await waitForLine(launchedProcess, launchedProcess.stdout, /^Juggler listening on (ws:\/\/.*)$/, timeout, timeoutError); const url = match[1]; - const transport = await platform.createWebSocketTransport(url); - browser = await FFBrowser.create(SlowMoTransport.wrap(transport, slowMo)); - await browser._waitForTarget(t => t.type() === 'page'); - return new BrowserServer(browser, launchedProcess, url); + server = new FFBrowserServer(launchedProcess, { browserWSEndpoint: url, slowMo }); + return server; } catch (e) { - if (browser) - await browser.close(); + if (server) + await server.close(); throw e; } } + async connect(options: FFConnectOptions): Promise { + return FFBrowser.connect(options); + } + executablePath(): string { return this._resolveExecutablePath().executablePath; } + get devices(): types.Devices { + return DeviceDescriptors; + } + + get errors(): any { + return Errors; + } + + defaultArgs(options: any = {}): string[] { + const { + headless = true, + args = [], + userDataDir = null, + } = options; + const firefoxArguments = [...DEFAULT_ARGS]; + if (userDataDir) + firefoxArguments.push('-profile', userDataDir); + if (headless) + firefoxArguments.push('-headless'); + firefoxArguments.push(...args); + if (args.every(arg => arg.startsWith('-'))) + firefoxArguments.push('about:blank'); + return firefoxArguments; + } + + createBrowserFetcher(options: BrowserFetcherOptions = {}): BrowserFetcher { + const downloadURLs = { + linux: '%s/builds/firefox/%s/firefox-linux.zip', + mac: '%s/builds/firefox/%s/firefox-mac.zip', + win32: '%s/builds/firefox/%s/firefox-win32.zip', + win64: '%s/builds/firefox/%s/firefox-win64.zip', + }; + + const defaultOptions = { + path: path.join(this._projectRoot, '.local-firefox'), + host: 'https://playwrightaccount.blob.core.windows.net', + platform: (() => { + const platform = os.platform(); + if (platform === 'darwin') + return 'mac'; + if (platform === 'linux') + return 'linux'; + if (platform === 'win32') + return os.arch() === 'x64' ? 'win64' : 'win32'; + return platform; + })() + }; + options = { + ...defaultOptions, + ...options, + }; + assert(!!downloadURLs[options.platform], 'Unsupported platform: ' + options.platform); + + return new BrowserFetcher(options.path, options.platform, (platform: string, revision: string) => { + let executablePath = ''; + if (platform === 'linux') + executablePath = path.join('firefox', 'firefox'); + else if (platform === 'mac') + executablePath = path.join('firefox', 'Nightly.app', 'Contents', 'MacOS', 'firefox'); + else if (platform === 'win32' || platform === 'win64') + executablePath = path.join('firefox', 'firefox.exe'); + return { + downloadUrl: util.format(downloadURLs[platform], options.host, revision), + executablePath + }; + }); + } + _resolveExecutablePath() { - const browserFetcher = createBrowserFetcher(this._projectRoot); - const revisionInfo = browserFetcher.revisionInfo(this._preferredRevision); + const browserFetcher = this.createBrowserFetcher(); + const revisionInfo = browserFetcher.revisionInfo(this._revision); const missingText = !revisionInfo.local ? `Firefox revision is not downloaded. Run "npm install" or "yarn install"` : null; - return {executablePath: revisionInfo.executablePath, missingText}; + return { executablePath: revisionInfo.executablePath, missingText }; } } -export function createBrowserFetcher(projectRoot: string, options: BrowserFetcherOptions = {}): BrowserFetcher { - const downloadURLs = { - linux: '%s/builds/firefox/%s/firefox-linux.zip', - mac: '%s/builds/firefox/%s/firefox-mac.zip', - win32: '%s/builds/firefox/%s/firefox-win32.zip', - win64: '%s/builds/firefox/%s/firefox-win64.zip', - }; +const mkdtempAsync = platform.promisify(fs.mkdtemp); +const writeFileAsync = platform.promisify(fs.writeFile); - const defaultOptions = { - path: path.join(projectRoot, '.local-firefox'), - host: 'https://playwrightaccount.blob.core.windows.net', - platform: (() => { - const platform = os.platform(); - if (platform === 'darwin') - return 'mac'; - if (platform === 'linux') - return 'linux'; - if (platform === 'win32') - return os.arch() === 'x64' ? 'win64' : 'win32'; - return platform; - })() - }; - options = { - ...defaultOptions, - ...options, - }; - assert(!!downloadURLs[options.platform], 'Unsupported platform: ' + options.platform); - - return new BrowserFetcher(options.path, options.platform, (platform: string, revision: string) => { - let executablePath = ''; - if (platform === 'linux') - executablePath = path.join('firefox', 'firefox'); - else if (platform === 'mac') - executablePath = path.join('firefox', 'Nightly.app', 'Contents', 'MacOS', 'firefox'); - else if (platform === 'win32' || platform === 'win64') - executablePath = path.join('firefox', 'firefox.exe'); - return { - downloadUrl: util.format(downloadURLs[platform], options.host, revision), - executablePath - }; - }); -} +const DEFAULT_ARGS = [ + '-no-remote', +]; const DUMMY_UMA_SERVER = 'dummy.test'; const DEFAULT_PREFERENCES = {