diff --git a/chromium.js b/chromium.js index e6be4d2daa..12432c0807 100644 --- a/chromium.js +++ b/chromium.js @@ -22,7 +22,7 @@ for (const className in api.Chromium) { helper.installAsyncStackHooks(api.Chromium[className]); } -const {Playwright} = require('./lib/chromium/Playwright'); +const { CRPlaywright } = require('./lib/chromium/crPlaywright'); const packageJson = require('./package.json'); -module.exports = new Playwright(__dirname, packageJson.playwright.chromium_revision); +module.exports = new CRPlaywright(__dirname, packageJson.playwright.chromium_revision); diff --git a/firefox.js b/firefox.js index f39d4d51bb..58b92dcf90 100644 --- a/firefox.js +++ b/firefox.js @@ -22,7 +22,7 @@ for (const className in api.Firefox) { helper.installAsyncStackHooks(api.Firefox[className]); } -const {Playwright} = require('./lib/firefox/Playwright'); +const { FFPlaywright } = require('./lib/firefox/ffPlaywright'); const packageJson = require('./package.json'); -module.exports = new Playwright(__dirname, packageJson.playwright.firefox_revision); +module.exports = new FFPlaywright(__dirname, packageJson.playwright.firefox_revision); diff --git a/src/chromium/Browser.ts b/src/chromium/Browser.ts deleted file mode 100644 index f83e07e9ba..0000000000 --- a/src/chromium/Browser.ts +++ /dev/null @@ -1,301 +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 { EventEmitter } from 'events'; -import { Events } from './events'; -import { assert, helper } from '../helper'; -import { BrowserContext, BrowserContextOptions } from '../browserContext'; -import { Connection, ConnectionEvents, CDPSession } from './Connection'; -import { Page } from '../page'; -import { Target } from './Target'; -import { Protocol } from './protocol'; -import { FrameManager } from './FrameManager'; -import * as browser from '../browser'; -import * as network from '../network'; -import { Permissions } from './features/permissions'; -import { Overrides } from './features/overrides'; -import { Worker } from './features/workers'; -import { ConnectionTransport } from '../transport'; -import { readProtocolStream } from './protocolHelper'; - -export class Browser extends EventEmitter implements browser.Browser { - _connection: Connection; - _client: CDPSession; - private _defaultContext: BrowserContext; - private _contexts = new Map(); - _targets = new Map(); - - private _tracingRecording = false; - private _tracingPath = ''; - private _tracingClient: CDPSession | undefined; - - static async create( - transport: ConnectionTransport) { - const connection = new Connection(transport); - - const { browserContextIds } = await connection.rootSession.send('Target.getBrowserContexts'); - const browser = new Browser(connection, browserContextIds); - await connection.rootSession.send('Target.setDiscoverTargets', { discover: true }); - await browser.waitForTarget(t => t.type() === 'page'); - return browser; - } - - constructor(connection: Connection, contextIds: string[]) { - super(); - this._connection = connection; - this._client = connection.rootSession; - - this._defaultContext = this._createBrowserContext(null, {}); - for (const contextId of contextIds) - this._contexts.set(contextId, this._createBrowserContext(contextId, {})); - - this._connection.on(ConnectionEvents.Disconnected, () => this.emit(Events.Browser.Disconnected)); - this._client.on('Target.targetCreated', this._targetCreated.bind(this)); - this._client.on('Target.targetDestroyed', this._targetDestroyed.bind(this)); - this._client.on('Target.targetInfoChanged', this._targetInfoChanged.bind(this)); - } - - _createBrowserContext(contextId: string | null, options: BrowserContextOptions): BrowserContext { - let overrides: Overrides | null = null; - const context = new BrowserContext({ - pages: async (): Promise => { - const targets = this._allTargets().filter(target => target.browserContext() === context && target.type() === 'page'); - const pages = await Promise.all(targets.map(target => target.page())); - return pages.filter(page => !!page); - }, - - newPage: async (): Promise => { - const { targetId } = await this._client.send('Target.createTarget', { url: 'about:blank', browserContextId: contextId || undefined }); - const target = this._targets.get(targetId); - assert(await target._initializedPromise, 'Failed to create target for page'); - const page = await target.page(); - const session = (page._delegate as FrameManager)._client; - const promises: Promise[] = [ overrides._applyOverrides(page) ]; - if (options.bypassCSP) - promises.push(session.send('Page.setBypassCSP', { enabled: true })); - if (options.ignoreHTTPSErrors) - promises.push(session.send('Security.setIgnoreCertificateErrors', { ignore: true })); - if (options.viewport) - promises.push(page._delegate.setViewport(options.viewport)); - if (options.javaScriptEnabled === false) - promises.push(session.send('Emulation.setScriptExecutionDisabled', { value: true })); - if (options.userAgent) - (page._delegate as FrameManager)._networkManager.setUserAgent(options.userAgent); - if (options.mediaType || options.colorScheme) { - const features = options.colorScheme ? [{ name: 'prefers-color-scheme', value: options.colorScheme }] : []; - promises.push(session.send('Emulation.setEmulatedMedia', { media: options.mediaType || '', features })); - } - if (options.timezoneId) - promises.push(emulateTimezone(session, options.timezoneId)); - await Promise.all(promises); - return page; - }, - - close: async (): Promise => { - assert(contextId, 'Non-incognito profiles cannot be closed!'); - await this._client.send('Target.disposeBrowserContext', {browserContextId: contextId || undefined}); - this._contexts.delete(contextId); - }, - - cookies: async (): Promise => { - const { cookies } = await this._client.send('Storage.getCookies', { browserContextId: contextId || undefined }); - return cookies.map(c => { - const copy: any = { sameSite: 'None', ...c }; - delete copy.size; - delete copy.priority; - return copy as network.NetworkCookie; - }); - }, - - clearCookies: async (): Promise => { - await this._client.send('Storage.clearCookies', { browserContextId: contextId || undefined }); - }, - - setCookies: async (cookies: network.SetNetworkCookieParam[]): Promise => { - await this._client.send('Storage.setCookies', { cookies, browserContextId: contextId || undefined }); - }, - }, options); - overrides = new Overrides(context); - (context as any).permissions = new Permissions(this._client, contextId); - (context as any).overrides = overrides; - return context; - } - - async newContext(options: BrowserContextOptions = {}): Promise { - const { browserContextId } = await this._client.send('Target.createBrowserContext'); - const context = this._createBrowserContext(browserContextId, options); - this._contexts.set(browserContextId, context); - return context; - } - - browserContexts(): BrowserContext[] { - return [this._defaultContext, ...Array.from(this._contexts.values())]; - } - - defaultContext(): BrowserContext { - return this._defaultContext; - } - - async _targetCreated(event: Protocol.Target.targetCreatedPayload) { - const targetInfo = event.targetInfo; - const {browserContextId} = targetInfo; - const context = (browserContextId && this._contexts.has(browserContextId)) ? this._contexts.get(browserContextId) : this._defaultContext; - - const target = new Target(this, targetInfo, context, () => this._connection.createSession(targetInfo)); - assert(!this._targets.has(event.targetInfo.targetId), 'Target should not exist before targetCreated'); - this._targets.set(event.targetInfo.targetId, target); - - if (target._isInitialized || await target._initializedPromise) - this.emit(Events.Browser.TargetCreated, target); - } - - async _targetDestroyed(event: { targetId: string; }) { - const target = this._targets.get(event.targetId); - target._initializedCallback(false); - this._targets.delete(event.targetId); - target._didClose(); - if (await target._initializedPromise) - this.emit(Events.Browser.TargetDestroyed, target); - } - - _targetInfoChanged(event: Protocol.Target.targetInfoChangedPayload) { - const target = this._targets.get(event.targetInfo.targetId); - assert(target, 'target should exist before targetInfoChanged'); - const previousURL = target.url(); - const wasInitialized = target._isInitialized; - target._targetInfoChanged(event.targetInfo); - if (wasInitialized && previousURL !== target.url()) - this.emit(Events.Browser.TargetChanged, target); - } - - async _closePage(page: Page) { - await this._client.send('Target.closeTarget', { targetId: Target.fromPage(page)._targetId }); - } - - _allTargets(): Target[] { - return Array.from(this._targets.values()).filter(target => target._isInitialized); - } - - async _activatePage(page: Page) { - await (page._delegate as FrameManager)._client.send('Target.activateTarget', {targetId: Target.fromPage(page)._targetId}); - } - - async waitForTarget(predicate: (arg0: Target) => boolean, options: { timeout?: number; } | undefined = {}): Promise { - const { - timeout = 30000 - } = options; - const existingTarget = this._allTargets().find(predicate); - if (existingTarget) - return existingTarget; - let resolve: (target: Target) => void; - const targetPromise = new Promise(x => resolve = x); - this.on(Events.Browser.TargetCreated, check); - this.on(Events.Browser.TargetChanged, check); - try { - if (!timeout) - return await targetPromise; - return await helper.waitWithTimeout(targetPromise, 'target', timeout); - } finally { - this.removeListener(Events.Browser.TargetCreated, check); - this.removeListener(Events.Browser.TargetChanged, check); - } - - function check(target: Target) { - if (predicate(target)) - resolve(target); - } - } - - async close() { - await this._connection.rootSession.send('Browser.close'); - this.disconnect(); - } - - browserTarget(): Target { - return [...this._targets.values()].find(t => t.type() === 'browser'); - } - - serviceWorker(target: Target): Promise { - return target._worker(); - } - - async startTracing(page: Page | undefined, options: { path?: string; screenshots?: boolean; categories?: string[]; } = {}) { - assert(!this._tracingRecording, 'Cannot start recording trace while already recording trace.'); - this._tracingClient = page ? (page._delegate as FrameManager)._client : this._client; - - const defaultCategories = [ - '-*', 'devtools.timeline', 'v8.execute', 'disabled-by-default-devtools.timeline', - 'disabled-by-default-devtools.timeline.frame', 'toplevel', - 'blink.console', 'blink.user_timing', 'latencyInfo', 'disabled-by-default-devtools.timeline.stack', - 'disabled-by-default-v8.cpu_profiler', 'disabled-by-default-v8.cpu_profiler.hires' - ]; - const { - path = null, - screenshots = false, - categories = defaultCategories, - } = options; - - if (screenshots) - categories.push('disabled-by-default-devtools.screenshot'); - - this._tracingPath = path; - this._tracingRecording = true; - await this._tracingClient.send('Tracing.start', { - transferMode: 'ReturnAsStream', - categories: categories.join(',') - }); - } - - async stopTracing(): Promise { - assert(this._tracingClient, 'Tracing was not started.'); - let fulfill: (buffer: Buffer) => void; - const contentPromise = new Promise(x => fulfill = x); - this._tracingClient.once('Tracing.tracingComplete', event => { - readProtocolStream(this._tracingClient, event.stream, this._tracingPath).then(fulfill); - }); - await this._tracingClient.send('Tracing.end'); - this._tracingRecording = false; - return contentPromise; - } - - targets(context?: BrowserContext): Target[] { - const targets = this._allTargets(); - return context ? targets.filter(t => t.browserContext() === context) : targets; - } - - pageTarget(page: Page): Target { - return Target.fromPage(page); - } - - disconnect() { - this._connection.dispose(); - } - - isConnected(): boolean { - return !this._connection._closed; - } -} - -async function emulateTimezone(session: CDPSession, timezoneId: string) { - try { - await session.send('Emulation.setTimezoneOverride', { timezoneId: timezoneId }); - } catch (exception) { - if (exception.message.includes('Invalid timezone')) - throw new Error(`Invalid timezone ID: ${timezoneId}`); - throw exception; - } -} diff --git a/src/chromium/Connection.ts b/src/chromium/Connection.ts deleted file mode 100644 index 87403c2a7b..0000000000 --- a/src/chromium/Connection.ts +++ /dev/null @@ -1,185 +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 debug from 'debug'; -import { EventEmitter } from 'events'; -import { ConnectionTransport } from '../transport'; -import { assert } from '../helper'; -import { Protocol } from './protocol'; - -const debugProtocol = debug('playwright:protocol'); - -export const ConnectionEvents = { - Disconnected: Symbol('ConnectionEvents.Disconnected') -}; - -export class Connection extends EventEmitter { - private _lastId = 0; - private _transport: ConnectionTransport; - private _sessions = new Map(); - readonly rootSession: CDPSession; - _closed = false; - - constructor(transport: ConnectionTransport) { - super(); - this._transport = transport; - this._transport.onmessage = this._onMessage.bind(this); - this._transport.onclose = this._onClose.bind(this); - this.rootSession = new CDPSession(this, 'browser', ''); - this._sessions.set('', this.rootSession); - } - - static fromSession(session: CDPSession): Connection { - return session._connection; - } - - session(sessionId: string): CDPSession | null { - return this._sessions.get(sessionId) || null; - } - - _rawSend(sessionId: string, message: any): number { - const id = ++this._lastId; - message.id = id; - if (sessionId) - message.sessionId = sessionId; - const data = JSON.stringify(message); - debugProtocol('SEND ► ' + data); - this._transport.send(data); - return id; - } - - async _onMessage(message: string) { - debugProtocol('◀ RECV ' + message); - const object = JSON.parse(message); - if (object.method === 'Target.attachedToTarget') { - const sessionId = object.params.sessionId; - const session = new CDPSession(this, object.params.targetInfo.type, sessionId); - this._sessions.set(sessionId, session); - } else if (object.method === 'Target.detachedFromTarget') { - const session = this._sessions.get(object.params.sessionId); - if (session) { - session._onClosed(); - this._sessions.delete(object.params.sessionId); - } - } - const session = this._sessions.get(object.sessionId || ''); - if (session) - session._onMessage(object); - } - - _onClose() { - if (this._closed) - return; - this._closed = true; - this._transport.onmessage = null; - this._transport.onclose = null; - for (const session of this._sessions.values()) - session._onClosed(); - this._sessions.clear(); - Promise.resolve().then(() => this.emit(ConnectionEvents.Disconnected)); - } - - dispose() { - this._onClose(); - this._transport.close(); - } - - async createSession(targetInfo: Protocol.Target.TargetInfo): Promise { - const { sessionId } = await this.rootSession.send('Target.attachToTarget', { targetId: targetInfo.targetId, flatten: true }); - return this._sessions.get(sessionId); - } - - async createBrowserSession(): Promise { - const { sessionId } = await this.rootSession.send('Target.attachToBrowserTarget'); - return this._sessions.get(sessionId); - } -} - -export const CDPSessionEvents = { - Disconnected: Symbol('Events.CDPSession.Disconnected') -}; - -export class CDPSession extends EventEmitter { - _connection: Connection; - private _callbacks = new Map void, reject: (e: Error) => void, error: Error, method: string}>(); - private _targetType: string; - private _sessionId: string; - on: (event: T, listener: (payload: T extends symbol ? any : Protocol.Events[T extends keyof Protocol.Events ? T : never]) => void) => this; - addListener: (event: T, listener: (payload: T extends symbol ? any : Protocol.Events[T extends keyof Protocol.Events ? T : never]) => void) => this; - off: (event: T, listener: (payload: T extends symbol ? any : Protocol.Events[T extends keyof Protocol.Events ? T : never]) => void) => this; - removeListener: (event: T, listener: (payload: T extends symbol ? any : Protocol.Events[T extends keyof Protocol.Events ? T : never]) => void) => this; - once: (event: T, listener: (payload: T extends symbol ? any : Protocol.Events[T extends keyof Protocol.Events ? T : never]) => void) => this; - - constructor(connection: Connection, targetType: string, sessionId: string) { - super(); - this._connection = connection; - this._targetType = targetType; - this._sessionId = sessionId; - } - - send( - method: T, - params?: Protocol.CommandParameters[T] - ): Promise { - if (!this._connection) - return Promise.reject(new Error(`Protocol error (${method}): Session closed. Most likely the ${this._targetType} has been closed.`)); - const id = this._connection._rawSend(this._sessionId, { method, params }); - return new Promise((resolve, reject) => { - this._callbacks.set(id, {resolve, reject, error: new Error(), method}); - }); - } - - _onMessage(object: { id?: number; method: string; params: any; error: { message: string; data: any; }; result?: any; }) { - if (object.id && this._callbacks.has(object.id)) { - const callback = this._callbacks.get(object.id); - this._callbacks.delete(object.id); - if (object.error) - callback.reject(createProtocolError(callback.error, callback.method, object)); - else - callback.resolve(object.result); - } else { - assert(!object.id); - Promise.resolve().then(() => this.emit(object.method, object.params)); - } - } - - async detach() { - if (!this._connection) - throw new Error(`Session already detached. Most likely the ${this._targetType} has been closed.`); - await this._connection.rootSession.send('Target.detachFromTarget', { sessionId: this._sessionId }); - } - - _onClosed() { - for (const callback of this._callbacks.values()) - callback.reject(rewriteError(callback.error, `Protocol error (${callback.method}): Target closed.`)); - this._callbacks.clear(); - this._connection = null; - Promise.resolve().then(() => this.emit(CDPSessionEvents.Disconnected)); - } -} - -function createProtocolError(error: Error, method: string, object: { error: { message: string; data: any; }; }): Error { - let message = `Protocol error (${method}): ${object.error.message}`; - if ('data' in object.error) - message += ` ${object.error.data}`; - return rewriteError(error, message); -} - -function rewriteError(error: Error, message: string): Error { - error.message = message; - return error; -} diff --git a/src/chromium/ExecutionContext.ts b/src/chromium/ExecutionContext.ts deleted file mode 100644 index 44b0fde665..0000000000 --- a/src/chromium/ExecutionContext.ts +++ /dev/null @@ -1,178 +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 { CDPSession } from './Connection'; -import { helper } from '../helper'; -import { valueFromRemoteObject, getExceptionMessage, releaseObject } from './protocolHelper'; -import { Protocol } from './protocol'; -import * as js from '../javascript'; - -export const EVALUATION_SCRIPT_URL = '__playwright_evaluation_script__'; -const SOURCE_URL_REGEX = /^[\040\t]*\/\/[@#] sourceURL=\s*(\S*?)\s*$/m; - -export class ExecutionContextDelegate implements js.ExecutionContextDelegate { - _client: CDPSession; - _contextId: number; - - constructor(client: CDPSession, contextPayload: Protocol.Runtime.ExecutionContextDescription) { - this._client = client; - this._contextId = contextPayload.id; - } - - async evaluate(context: js.ExecutionContext, returnByValue: boolean, pageFunction: Function | string, ...args: any[]): Promise { - const suffix = `//# sourceURL=${EVALUATION_SCRIPT_URL}`; - - if (helper.isString(pageFunction)) { - const contextId = this._contextId; - const expression: string = pageFunction as string; - const expressionWithSourceUrl = SOURCE_URL_REGEX.test(expression) ? expression : expression + '\n' + suffix; - const {exceptionDetails, result: remoteObject} = await this._client.send('Runtime.evaluate', { - expression: expressionWithSourceUrl, - contextId, - returnByValue, - awaitPromise: true, - userGesture: true - }).catch(rewriteError); - if (exceptionDetails) - throw new Error('Evaluation failed: ' + getExceptionMessage(exceptionDetails)); - return returnByValue ? valueFromRemoteObject(remoteObject) : context._createHandle(remoteObject); - } - - if (typeof pageFunction !== 'function') - throw new Error(`Expected to get |string| or |function| as the first argument, but got "${pageFunction}" instead.`); - - let functionText = pageFunction.toString(); - try { - new Function('(' + functionText + ')'); - } catch (e1) { - // This means we might have a function shorthand. Try another - // time prefixing 'function '. - if (functionText.startsWith('async ')) - functionText = 'async function ' + functionText.substring('async '.length); - else - functionText = 'function ' + functionText; - try { - new Function('(' + functionText + ')'); - } catch (e2) { - // We tried hard to serialize, but there's a weird beast here. - throw new Error('Passed function is not well-serializable!'); - } - } - let callFunctionOnPromise; - try { - callFunctionOnPromise = this._client.send('Runtime.callFunctionOn', { - functionDeclaration: functionText + '\n' + suffix + '\n', - executionContextId: this._contextId, - arguments: args.map(convertArgument.bind(this)), - returnByValue, - awaitPromise: true, - userGesture: true - }); - } catch (err) { - if (err instanceof TypeError && err.message.startsWith('Converting circular structure to JSON')) - err.message += ' Are you passing a nested JSHandle?'; - throw err; - } - const { exceptionDetails, result: remoteObject } = await callFunctionOnPromise.catch(rewriteError); - if (exceptionDetails) - throw new Error('Evaluation failed: ' + getExceptionMessage(exceptionDetails)); - return returnByValue ? valueFromRemoteObject(remoteObject) : context._createHandle(remoteObject); - - function convertArgument(arg: any): any { - if (typeof arg === 'bigint') // eslint-disable-line valid-typeof - return { unserializableValue: `${arg.toString()}n` }; - if (Object.is(arg, -0)) - return { unserializableValue: '-0' }; - if (Object.is(arg, Infinity)) - return { unserializableValue: 'Infinity' }; - if (Object.is(arg, -Infinity)) - return { unserializableValue: '-Infinity' }; - if (Object.is(arg, NaN)) - return { unserializableValue: 'NaN' }; - const objectHandle = arg && (arg instanceof js.JSHandle) ? arg : null; - if (objectHandle) { - if (objectHandle._context !== context) - throw new Error('JSHandles can be evaluated only in the context they were created!'); - if (objectHandle._disposed) - throw new Error('JSHandle is disposed!'); - const remoteObject = toRemoteObject(objectHandle); - if (remoteObject.unserializableValue) - return { unserializableValue: remoteObject.unserializableValue }; - if (!remoteObject.objectId) - return { value: remoteObject.value }; - return { objectId: remoteObject.objectId }; - } - return { value: arg }; - } - - function rewriteError(error: Error): Protocol.Runtime.evaluateReturnValue { - if (error.message.includes('Object reference chain is too long')) - return {result: {type: 'undefined'}}; - if (error.message.includes('Object couldn\'t be returned by value')) - return {result: {type: 'undefined'}}; - - if (error.message.endsWith('Cannot find context with specified id') || error.message.endsWith('Inspected target navigated or closed') || error.message.endsWith('Execution context was destroyed.')) - throw new Error('Execution context was destroyed, most likely because of a navigation.'); - throw error; - } - } - - async getProperties(handle: js.JSHandle): Promise> { - const response = await this._client.send('Runtime.getProperties', { - objectId: toRemoteObject(handle).objectId, - ownProperties: true - }); - const result = new Map(); - for (const property of response.result) { - if (!property.enumerable) - continue; - result.set(property.name, handle._context._createHandle(property.value)); - } - return result; - } - - async releaseHandle(handle: js.JSHandle): Promise { - await releaseObject(this._client, toRemoteObject(handle)); - } - - async handleJSONValue(handle: js.JSHandle): Promise { - const remoteObject = toRemoteObject(handle); - if (remoteObject.objectId) { - const response = await this._client.send('Runtime.callFunctionOn', { - functionDeclaration: 'function() { return this; }', - objectId: remoteObject.objectId, - returnByValue: true, - awaitPromise: true, - }); - return valueFromRemoteObject(response.result); - } - return valueFromRemoteObject(remoteObject); - } - - handleToString(handle: js.JSHandle, includeType: boolean): string { - const object = toRemoteObject(handle); - if (object.objectId) { - const type = object.subtype || object.type; - return 'JSHandle@' + type; - } - return (includeType ? 'JSHandle:' : '') + valueFromRemoteObject(object); - } -} - -function toRemoteObject(handle: js.JSHandle): Protocol.Runtime.RemoteObject { - return handle._remoteObject as Protocol.Runtime.RemoteObject; -} diff --git a/src/chromium/FrameManager.ts b/src/chromium/FrameManager.ts deleted file mode 100644 index 00b57dc113..0000000000 --- a/src/chromium/FrameManager.ts +++ /dev/null @@ -1,459 +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 dom from '../dom'; -import * as frames from '../frames'; -import { debugError, helper, RegisteredListener } from '../helper'; -import * as network from '../network'; -import { CDPSession } from './Connection'; -import { EVALUATION_SCRIPT_URL, ExecutionContextDelegate } from './ExecutionContext'; -import { NetworkManager } from './NetworkManager'; -import { Page } from '../page'; -import { Protocol } from './protocol'; -import { Events } from '../events'; -import { toConsoleMessageLocation, exceptionToError, releaseObject } from './protocolHelper'; -import * as dialog from '../dialog'; -import { PageDelegate } from '../page'; -import { RawMouseImpl, RawKeyboardImpl } from './Input'; -import { Accessibility } from './features/accessibility'; -import { Coverage } from './features/coverage'; -import { PDF } from './features/pdf'; -import { Workers } from './features/workers'; -import { Interception } from './features/interception'; -import { Browser } from './Browser'; -import { BrowserContext } from '../browserContext'; -import * as types from '../types'; -import * as input from '../input'; -import { ConsoleMessage } from '../console'; - -const UTILITY_WORLD_NAME = '__playwright_utility_world__'; - -export class FrameManager implements PageDelegate { - _client: CDPSession; - private _page: Page; - readonly _networkManager: NetworkManager; - private _contextIdToContext = new Map(); - private _isolatedWorlds = new Set(); - private _eventListeners: RegisteredListener[]; - rawMouse: RawMouseImpl; - rawKeyboard: RawKeyboardImpl; - private _browser: Browser; - - constructor(client: CDPSession, browser: Browser, browserContext: BrowserContext) { - this._client = client; - this._browser = browser; - this.rawKeyboard = new RawKeyboardImpl(client); - this.rawMouse = new RawMouseImpl(client); - this._page = new Page(this, browserContext); - this._networkManager = new NetworkManager(client, this._page); - (this._page as any).accessibility = new Accessibility(client); - (this._page as any).coverage = new Coverage(client); - (this._page as any).pdf = new PDF(client); - (this._page as any).workers = new Workers(client, this._page._addConsoleMessage.bind(this._page), error => this._page.emit(Events.Page.PageError, error)); - (this._page as any).interception = new Interception(this._networkManager); - - this._eventListeners = [ - helper.addEventListener(client, 'Inspector.targetCrashed', event => this._onTargetCrashed()), - helper.addEventListener(client, 'Log.entryAdded', event => this._onLogEntryAdded(event)), - helper.addEventListener(client, 'Page.fileChooserOpened', event => this._onFileChooserOpened(event)), - helper.addEventListener(client, 'Page.frameAttached', event => this._onFrameAttached(event.frameId, event.parentFrameId)), - helper.addEventListener(client, 'Page.frameDetached', event => this._onFrameDetached(event.frameId)), - helper.addEventListener(client, 'Page.frameNavigated', event => this._onFrameNavigated(event.frame, false)), - helper.addEventListener(client, 'Page.frameStoppedLoading', event => this._onFrameStoppedLoading(event.frameId)), - helper.addEventListener(client, 'Page.javascriptDialogOpening', event => this._onDialog(event)), - helper.addEventListener(client, 'Page.lifecycleEvent', event => this._onLifecycleEvent(event)), - helper.addEventListener(client, 'Page.navigatedWithinDocument', event => this._onFrameNavigatedWithinDocument(event.frameId, event.url)), - helper.addEventListener(client, 'Runtime.bindingCalled', event => this._onBindingCalled(event)), - helper.addEventListener(client, 'Runtime.consoleAPICalled', event => this._onConsoleAPI(event)), - helper.addEventListener(client, 'Runtime.exceptionThrown', exception => this._handleException(exception.exceptionDetails)), - helper.addEventListener(client, 'Runtime.executionContextCreated', event => this._onExecutionContextCreated(event.context)), - helper.addEventListener(client, 'Runtime.executionContextDestroyed', event => this._onExecutionContextDestroyed(event.executionContextId)), - helper.addEventListener(client, 'Runtime.executionContextsCleared', event => this._onExecutionContextsCleared()), - ]; - } - - async initialize() { - const [,{frameTree}] = await Promise.all([ - this._client.send('Page.enable'), - this._client.send('Page.getFrameTree'), - ]); - this._handleFrameTree(frameTree); - await Promise.all([ - this._client.send('Log.enable', {}), - this._client.send('Page.setInterceptFileChooserDialog', {enabled: true}), - this._client.send('Page.setLifecycleEventsEnabled', { enabled: true }), - this._client.send('Runtime.enable', {}).then(() => this._ensureIsolatedWorld(UTILITY_WORLD_NAME)), - this._networkManager.initialize(), - ]); - } - - didClose() { - helper.removeEventListeners(this._eventListeners); - this._networkManager.dispose(); - this._page._didClose(); - } - - async navigateFrame(frame: frames.Frame, url: string, referrer: string | undefined): Promise { - const response = await this._client.send('Page.navigate', { url, referrer, frameId: frame._id }); - if (response.errorText) - throw new Error(`${response.errorText} at ${url}`); - return { newDocumentId: response.loaderId, isSameDocument: !response.loaderId }; - } - - needsLifecycleResetOnSetContent(): boolean { - // We rely upon the fact that document.open() will reset frame lifecycle with "init" - // lifecycle event. @see https://crrev.com/608658 - return false; - } - - _onLifecycleEvent(event: Protocol.Page.lifecycleEventPayload) { - if (event.name === 'init') - this._page._frameManager.frameLifecycleEvent(event.frameId, 'clear'); - else if (event.name === 'load') - this._page._frameManager.frameLifecycleEvent(event.frameId, 'load'); - else if (event.name === 'DOMContentLoaded') - this._page._frameManager.frameLifecycleEvent(event.frameId, 'domcontentloaded'); - } - - _onFrameStoppedLoading(frameId: string) { - this._page._frameManager.frameStoppedLoading(frameId); - } - - _handleFrameTree(frameTree: Protocol.Page.FrameTree) { - this._onFrameAttached(frameTree.frame.id, frameTree.frame.parentId); - this._onFrameNavigated(frameTree.frame, true); - if (!frameTree.childFrames) - return; - - for (const child of frameTree.childFrames) - this._handleFrameTree(child); - } - - page(): Page { - return this._page; - } - - _onFrameAttached(frameId: string, parentFrameId: string | null) { - this._page._frameManager.frameAttached(frameId, parentFrameId); - } - - _onFrameNavigated(framePayload: Protocol.Page.Frame, initial: boolean) { - this._page._frameManager.frameCommittedNewDocumentNavigation(framePayload.id, framePayload.url, framePayload.name || '', framePayload.loaderId, initial); - } - - async _ensureIsolatedWorld(name: string) { - if (this._isolatedWorlds.has(name)) - return; - this._isolatedWorlds.add(name); - await this._client.send('Page.addScriptToEvaluateOnNewDocument', { - source: `//# sourceURL=${EVALUATION_SCRIPT_URL}`, - worldName: name, - }); - await Promise.all(this._page.frames().map(frame => this._client.send('Page.createIsolatedWorld', { - frameId: frame._id, - grantUniveralAccess: true, - worldName: name, - }).catch(debugError))); // frames might be removed before we send this - } - - _onFrameNavigatedWithinDocument(frameId: string, url: string) { - this._page._frameManager.frameCommittedSameDocumentNavigation(frameId, url); - } - - _onFrameDetached(frameId: string) { - this._page._frameManager.frameDetached(frameId); - } - - _onExecutionContextCreated(contextPayload: Protocol.Runtime.ExecutionContextDescription) { - const frame = this._page._frameManager.frame(contextPayload.auxData ? contextPayload.auxData.frameId : null); - if (!frame) - return; - if (contextPayload.auxData && contextPayload.auxData.type === 'isolated') - this._isolatedWorlds.add(contextPayload.name); - const delegate = new ExecutionContextDelegate(this._client, contextPayload); - const context = new dom.FrameExecutionContext(delegate, frame); - if (contextPayload.auxData && !!contextPayload.auxData.isDefault) - frame._contextCreated('main', context); - else if (contextPayload.name === UTILITY_WORLD_NAME) - frame._contextCreated('utility', context); - this._contextIdToContext.set(contextPayload.id, context); - } - - _onExecutionContextDestroyed(executionContextId: number) { - const context = this._contextIdToContext.get(executionContextId); - if (!context) - return; - this._contextIdToContext.delete(executionContextId); - context.frame._contextDestroyed(context); - } - - _onExecutionContextsCleared() { - for (const contextId of Array.from(this._contextIdToContext.keys())) - this._onExecutionContextDestroyed(contextId); - } - - async _onConsoleAPI(event: Protocol.Runtime.consoleAPICalledPayload) { - if (event.executionContextId === 0) { - // DevTools protocol stores the last 1000 console messages. These - // messages are always reported even for removed execution contexts. In - // this case, they are marked with executionContextId = 0 and are - // reported upon enabling Runtime agent. - // - // Ignore these messages since: - // - there's no execution context we can use to operate with message - // arguments - // - these messages are reported before Playwright clients can subscribe - // to the 'console' - // page event. - // - // @see https://github.com/GoogleChrome/puppeteer/issues/3865 - return; - } - const context = this._contextIdToContext.get(event.executionContextId); - const values = event.args.map(arg => context._createHandle(arg)); - this._page._addConsoleMessage(event.type, values, toConsoleMessageLocation(event.stackTrace)); - } - - async exposeBinding(name: string, bindingFunction: string) { - await this._client.send('Runtime.addBinding', {name: name}); - await this._client.send('Page.addScriptToEvaluateOnNewDocument', {source: bindingFunction}); - await Promise.all(this._page.frames().map(frame => frame.evaluate(bindingFunction).catch(debugError))); - } - - _onBindingCalled(event: Protocol.Runtime.bindingCalledPayload) { - const context = this._contextIdToContext.get(event.executionContextId); - this._page._onBindingCalled(event.payload, context); - } - - _onDialog(event : Protocol.Page.javascriptDialogOpeningPayload) { - this._page.emit(Events.Page.Dialog, new dialog.Dialog( - event.type as dialog.DialogType, - event.message, - async (accept: boolean, promptText?: string) => { - await this._client.send('Page.handleJavaScriptDialog', { accept, promptText }); - }, - event.defaultPrompt)); - } - - _handleException(exceptionDetails: Protocol.Runtime.ExceptionDetails) { - this._page.emit(Events.Page.PageError, exceptionToError(exceptionDetails)); - } - - _onTargetCrashed() { - this._page.emit('error', new Error('Page crashed!')); - } - - _onLogEntryAdded(event: Protocol.Log.entryAddedPayload) { - const {level, text, args, source, url, lineNumber} = event.entry; - if (args) - args.map(arg => releaseObject(this._client, arg)); - if (source !== 'worker') - this._page.emit(Events.Page.Console, new ConsoleMessage(level, text, [], {url, lineNumber})); - } - - async _onFileChooserOpened(event: Protocol.Page.fileChooserOpenedPayload) { - const frame = this._page._frameManager.frame(event.frameId); - const utilityContext = await frame._utilityContext(); - const handle = await this.adoptBackendNodeId(event.backendNodeId, utilityContext); - this._page._onFileChooserOpened(handle); - } - - async setExtraHTTPHeaders(headers: network.Headers): Promise { - await this._client.send('Network.setExtraHTTPHeaders', { headers }); - } - - async setViewport(viewport: types.Viewport): Promise { - const { - width, - height, - isMobile = false, - deviceScaleFactor = 1, - hasTouch = false, - isLandscape = false, - } = viewport; - const screenOrientation: Protocol.Emulation.ScreenOrientation = isLandscape ? { angle: 90, type: 'landscapePrimary' } : { angle: 0, type: 'portraitPrimary' }; - await Promise.all([ - this._client.send('Emulation.setDeviceMetricsOverride', { mobile: isMobile, width, height, deviceScaleFactor, screenOrientation }), - this._client.send('Emulation.setTouchEmulationEnabled', { - enabled: hasTouch - }) - ]); - } - - async setEmulateMedia(mediaType: input.MediaType | null, mediaColorScheme: input.ColorScheme | null): Promise { - const features = mediaColorScheme ? [{ name: 'prefers-color-scheme', value: mediaColorScheme }] : []; - await this._client.send('Emulation.setEmulatedMedia', { media: mediaType || '', features }); - } - - setCacheEnabled(enabled: boolean): Promise { - return this._networkManager.setCacheEnabled(enabled); - } - - async reload(): Promise { - await this._client.send('Page.reload'); - } - - private async _go(delta: number): Promise { - const history = await this._client.send('Page.getNavigationHistory'); - const entry = history.entries[history.currentIndex + delta]; - if (!entry) - return false; - await this._client.send('Page.navigateToHistoryEntry', { entryId: entry.id }); - return true; - } - - goBack(): Promise { - return this._go(-1); - } - - goForward(): Promise { - return this._go(+1); - } - - async evaluateOnNewDocument(source: string): Promise { - await this._client.send('Page.addScriptToEvaluateOnNewDocument', { source }); - } - - async closePage(runBeforeUnload: boolean): Promise { - if (runBeforeUnload) - await this._client.send('Page.close'); - else - await this._browser._closePage(this._page); - } - - async getBoundingBoxForScreenshot(handle: dom.ElementHandle): Promise { - const rect = await handle.boundingBox(); - if (!rect) - return rect; - const { layoutViewport: { pageX, pageY } } = await this._client.send('Page.getLayoutMetrics'); - rect.x += pageX; - rect.y += pageY; - return rect; - } - - canScreenshotOutsideViewport(): boolean { - return false; - } - - async setBackgroundColor(color?: { r: number; g: number; b: number; a: number; }): Promise { - await this._client.send('Emulation.setDefaultBackgroundColorOverride', { color }); - } - - async takeScreenshot(format: 'png' | 'jpeg', options: types.ScreenshotOptions): Promise { - await this._client.send('Page.bringToFront', {}); - const clip = options.clip ? { ...options.clip, scale: 1 } : undefined; - const result = await this._client.send('Page.captureScreenshot', { format, quality: options.quality, clip }); - return Buffer.from(result.data, 'base64'); - } - - async resetViewport(): Promise { - await this._client.send('Emulation.setDeviceMetricsOverride', { mobile: false, width: 0, height: 0, deviceScaleFactor: 0 }); - } - - async getContentFrame(handle: dom.ElementHandle): Promise { - const nodeInfo = await this._client.send('DOM.describeNode', { - objectId: toRemoteObject(handle).objectId - }); - if (!nodeInfo || typeof nodeInfo.node.frameId !== 'string') - return null; - return this._page._frameManager.frame(nodeInfo.node.frameId); - } - - async getOwnerFrame(handle: dom.ElementHandle): Promise { - // document.documentElement has frameId of the owner frame. - const documentElement = await handle.evaluateHandle(node => { - const doc = node as Document; - if (doc.documentElement && doc.documentElement.ownerDocument === doc) - return doc.documentElement; - return node.ownerDocument ? node.ownerDocument.documentElement : null; - }); - if (!documentElement) - return null; - const remoteObject = toRemoteObject(documentElement); - if (!remoteObject.objectId) - return null; - const nodeInfo = await this._client.send('DOM.describeNode', { - objectId: remoteObject.objectId - }); - const frame = nodeInfo && typeof nodeInfo.node.frameId === 'string' ? - this._page._frameManager.frame(nodeInfo.node.frameId) : null; - await documentElement.dispose(); - return frame; - } - - isElementHandle(remoteObject: any): boolean { - return (remoteObject as Protocol.Runtime.RemoteObject).subtype === 'node'; - } - - async getBoundingBox(handle: dom.ElementHandle): Promise { - const result = await this._client.send('DOM.getBoxModel', { - objectId: toRemoteObject(handle).objectId - }).catch(debugError); - if (!result) - return null; - const quad = result.model.border; - const x = Math.min(quad[0], quad[2], quad[4], quad[6]); - const y = Math.min(quad[1], quad[3], quad[5], quad[7]); - const width = Math.max(quad[0], quad[2], quad[4], quad[6]) - x; - const height = Math.max(quad[1], quad[3], quad[5], quad[7]) - y; - return {x, y, width, height}; - } - - async getContentQuads(handle: dom.ElementHandle): Promise { - const result = await this._client.send('DOM.getContentQuads', { - objectId: toRemoteObject(handle).objectId - }).catch(debugError); - if (!result) - return null; - return result.quads.map(quad => [ - { x: quad[0], y: quad[1] }, - { x: quad[2], y: quad[3] }, - { x: quad[4], y: quad[5] }, - { x: quad[6], y: quad[7] } - ]); - } - - async layoutViewport(): Promise<{ width: number, height: number }> { - const layoutMetrics = await this._client.send('Page.getLayoutMetrics'); - return { width: layoutMetrics.layoutViewport.clientWidth, height: layoutMetrics.layoutViewport.clientHeight }; - } - - async setInputFiles(handle: dom.ElementHandle, files: input.FilePayload[]): Promise { - await handle.evaluate(input.setFileInputFunction, files); - } - - async adoptElementHandle(handle: dom.ElementHandle, to: dom.FrameExecutionContext): Promise> { - const nodeInfo = await this._client.send('DOM.describeNode', { - objectId: toRemoteObject(handle).objectId, - }); - return this.adoptBackendNodeId(nodeInfo.node.backendNodeId, to) as Promise>; - } - - async adoptBackendNodeId(backendNodeId: Protocol.DOM.BackendNodeId, to: dom.FrameExecutionContext): Promise { - const result = await this._client.send('DOM.resolveNode', { - backendNodeId, - executionContextId: (to._delegate as ExecutionContextDelegate)._contextId, - }).catch(debugError); - if (!result || result.object.subtype === 'null') - throw new Error('Unable to adopt element handle from a different document'); - return to._createHandle(result.object).asElement()!; - } -} - -function toRemoteObject(handle: dom.ElementHandle): Protocol.Runtime.RemoteObject { - return handle._remoteObject as Protocol.Runtime.RemoteObject; -} diff --git a/src/chromium/Input.ts b/src/chromium/Input.ts deleted file mode 100644 index 6cffc4d109..0000000000 --- a/src/chromium/Input.ts +++ /dev/null @@ -1,110 +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 input from '../input'; -import { CDPSession } from './Connection'; - -function toModifiersMask(modifiers: Set): number { - let mask = 0; - if (modifiers.has('Alt')) - mask |= 1; - if (modifiers.has('Control')) - mask |= 2; - if (modifiers.has('Meta')) - mask |= 4; - if (modifiers.has('Shift')) - mask |= 8; - return mask; -} - -export class RawKeyboardImpl implements input.RawKeyboard { - private _client: CDPSession; - - constructor(client: CDPSession) { - this._client = client; - } - - async keydown(modifiers: Set, code: string, keyCode: number, keyCodeWithoutLocation: number, key: string, location: number, autoRepeat: boolean, text: string | undefined): Promise { - await this._client.send('Input.dispatchKeyEvent', { - type: text ? 'keyDown' : 'rawKeyDown', - modifiers: toModifiersMask(modifiers), - windowsVirtualKeyCode: keyCodeWithoutLocation, - code, - key, - text, - unmodifiedText: text, - autoRepeat, - location, - isKeypad: location === input.keypadLocation - }); - } - - async keyup(modifiers: Set, code: string, keyCode: number, keyCodeWithoutLocation: number, key: string, location: number): Promise { - await this._client.send('Input.dispatchKeyEvent', { - type: 'keyUp', - modifiers: toModifiersMask(modifiers), - key, - windowsVirtualKeyCode: keyCodeWithoutLocation, - code, - location - }); - } - - async sendText(text: string): Promise { - await this._client.send('Input.insertText', { text }); - } -} - -export class RawMouseImpl implements input.RawMouse { - private _client: CDPSession; - - constructor(client: CDPSession) { - this._client = client; - } - - async move(x: number, y: number, button: input.Button | 'none', buttons: Set, modifiers: Set): Promise { - await this._client.send('Input.dispatchMouseEvent', { - type: 'mouseMoved', - button, - x, - y, - modifiers: toModifiersMask(modifiers) - }); - } - - async down(x: number, y: number, button: input.Button, buttons: Set, modifiers: Set, clickCount: number): Promise { - await this._client.send('Input.dispatchMouseEvent', { - type: 'mousePressed', - button, - x, - y, - modifiers: toModifiersMask(modifiers), - clickCount - }); - } - - async up(x: number, y: number, button: input.Button, buttons: Set, modifiers: Set, clickCount: number): Promise { - await this._client.send('Input.dispatchMouseEvent', { - type: 'mouseReleased', - button, - x, - y, - modifiers: toModifiersMask(modifiers), - clickCount - }); - } -} diff --git a/src/chromium/Launcher.ts b/src/chromium/Launcher.ts deleted file mode 100644 index efd5238485..0000000000 --- a/src/chromium/Launcher.ts +++ /dev/null @@ -1,258 +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 fs from 'fs'; -import * as os from 'os'; -import * as path from 'path'; -import * as util from 'util'; -import { BrowserFetcher, BrowserFetcherOptions } from '../browserFetcher'; -import { TimeoutError } from '../errors'; -import { assert, helper } from '../helper'; -import { launchProcess, waitForLine } from '../processLauncher'; -import { ConnectionTransport, PipeTransport, SlowMoTransport, WebSocketTransport } from '../transport'; -import { Browser } from './Browser'; -import { BrowserServer } from '../browser'; - -const mkdtempAsync = helper.promisify(fs.mkdtemp); - -const CHROME_PROFILE_PATH = path.join(os.tmpdir(), 'playwright_dev_profile-'); - -const DEFAULT_ARGS = [ - '--disable-background-networking', - '--enable-features=NetworkService,NetworkServiceInProcess', - '--disable-background-timer-throttling', - '--disable-backgrounding-occluded-windows', - '--disable-breakpad', - '--disable-client-side-phishing-detection', - '--disable-component-extensions-with-background-pages', - '--disable-default-apps', - '--disable-dev-shm-usage', - '--disable-extensions', - // BlinkGenPropertyTrees disabled due to crbug.com/937609 - '--disable-features=TranslateUI,BlinkGenPropertyTrees', - '--disable-hang-monitor', - '--disable-ipc-flooding-protection', - '--disable-popup-blocking', - '--disable-prompt-on-repost', - '--disable-renderer-backgrounding', - '--disable-sync', - '--force-color-profile=srgb', - '--metrics-recording-only', - '--no-first-run', - '--enable-automation', - '--password-store=basic', - '--use-mock-keychain', -]; - -export class Launcher { - private _projectRoot: string; - private _preferredRevision: string; - - constructor(projectRoot: string, preferredRevision: string) { - this._projectRoot = projectRoot; - this._preferredRevision = preferredRevision; - } - - async launch(options: (LauncherLaunchOptions & LauncherChromeArgOptions & ConnectionOptions) = {}): Promise> { - const { - ignoreDefaultArgs = false, - args = [], - dumpio = false, - executablePath = null, - pipe = false, - env = process.env, - handleSIGINT = true, - handleSIGTERM = true, - handleSIGHUP = true, - slowMo = 0, - timeout = 30000 - } = options; - - const chromeArguments = []; - if (!ignoreDefaultArgs) - chromeArguments.push(...this.defaultArgs(options)); - else if (Array.isArray(ignoreDefaultArgs)) - chromeArguments.push(...this.defaultArgs(options).filter(arg => ignoreDefaultArgs.indexOf(arg) === -1)); - else - chromeArguments.push(...args); - - let temporaryUserDataDir: string | null = null; - - if (!chromeArguments.some(argument => argument.startsWith('--remote-debugging-'))) - chromeArguments.push(pipe ? '--remote-debugging-pipe' : '--remote-debugging-port=0'); - if (!chromeArguments.some(arg => arg.startsWith('--user-data-dir'))) { - temporaryUserDataDir = await mkdtempAsync(CHROME_PROFILE_PATH); - chromeArguments.push(`--user-data-dir=${temporaryUserDataDir}`); - } - - let chromeExecutable = executablePath; - if (!executablePath) { - const {missingText, executablePath} = this._resolveExecutablePath(); - if (missingText) - throw new Error(missingText); - chromeExecutable = executablePath; - } - - const usePipe = chromeArguments.includes('--remote-debugging-pipe'); - - const launchedProcess = await launchProcess({ - executablePath: chromeExecutable, - args: chromeArguments, - env, - handleSIGINT, - handleSIGTERM, - handleSIGHUP, - dumpio, - pipe: usePipe, - tempDir: temporaryUserDataDir - }, () => { - if (temporaryUserDataDir || !browser) - return Promise.reject(); - return browser.close(); - }); - - let browser: Browser | undefined; - try { - let transport: ConnectionTransport | null = null; - let browserWSEndpoint: string = ''; - if (!usePipe) { - const timeoutError = new TimeoutError(`Timed out after ${timeout} ms while trying to connect to Chrome! The only Chrome revision guaranteed to work is r${this._preferredRevision}`); - const match = await waitForLine(launchedProcess, launchedProcess.stderr, /^DevTools listening on (ws:\/\/.*)$/, timeout, timeoutError); - browserWSEndpoint = match[1]; - transport = await WebSocketTransport.create(browserWSEndpoint); - } else { - transport = new PipeTransport(launchedProcess.stdio[3] as NodeJS.WritableStream, launchedProcess.stdio[4] as NodeJS.ReadableStream); - } - - browser = await Browser.create(SlowMoTransport.wrap(transport, slowMo)); - return new BrowserServer(browser, launchedProcess, browserWSEndpoint); - } catch (e) { - if (browser) - await browser.close(); - throw e; - } - } - - defaultArgs(options: LauncherChromeArgOptions = {}): string[] { - const { - devtools = false, - headless = !devtools, - args = [], - userDataDir = null - } = options; - const chromeArguments = [...DEFAULT_ARGS]; - if (userDataDir) - chromeArguments.push(`--user-data-dir=${userDataDir}`); - if (devtools) - chromeArguments.push('--auto-open-devtools-for-tabs'); - if (headless) { - chromeArguments.push( - '--headless', - '--hide-scrollbars', - '--mute-audio' - ); - } - if (args.every(arg => arg.startsWith('-'))) - chromeArguments.push('about:blank'); - chromeArguments.push(...args); - return chromeArguments; - } - - executablePath(): string { - return this._resolveExecutablePath().executablePath; - } - - _resolveExecutablePath(): { executablePath: string; missingText: string | null; } { - const browserFetcher = createBrowserFetcher(this._projectRoot); - const revisionInfo = browserFetcher.revisionInfo(this._preferredRevision); - const missingText = !revisionInfo.local ? `Chromium revision is not downloaded. Run "npm install" or "yarn install"` : null; - return {executablePath: revisionInfo.executablePath, missingText}; - } - -} - -export type LauncherChromeArgOptions = { - headless?: boolean, - args?: string[], - userDataDir?: string, - devtools?: boolean, -}; - -export type LauncherLaunchOptions = { - executablePath?: string, - ignoreDefaultArgs?: boolean|string[], - handleSIGINT?: boolean, - handleSIGTERM?: boolean, - handleSIGHUP?: boolean, - timeout?: number, - dumpio?: boolean, - env?: {[key: string]: string} | undefined, - pipe?: boolean, -}; - -export type ConnectionOptions = { - slowMo?: number, -}; - -export function createBrowserFetcher(projectRoot: string, options: BrowserFetcherOptions = {}): BrowserFetcher { - const downloadURLs = { - linux: '%s/chromium-browser-snapshots/Linux_x64/%d/%s.zip', - mac: '%s/chromium-browser-snapshots/Mac/%d/%s.zip', - win32: '%s/chromium-browser-snapshots/Win/%d/%s.zip', - win64: '%s/chromium-browser-snapshots/Win_x64/%d/%s.zip', - }; - - const defaultOptions = { - path: path.join(projectRoot, '.local-chromium'), - host: 'https://storage.googleapis.com', - 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 as any)[options.platform], 'Unsupported platform: ' + options.platform); - - return new BrowserFetcher(options.path, options.platform, (platform: string, revision: string) => { - let archiveName = ''; - let executablePath = ''; - if (platform === 'linux') { - archiveName = 'chrome-linux'; - executablePath = path.join(archiveName, 'chrome'); - } else if (platform === 'mac') { - archiveName = 'chrome-mac'; - executablePath = path.join(archiveName, 'Chromium.app', 'Contents', 'MacOS', 'Chromium'); - } else if (platform === 'win32' || platform === 'win64') { - // Windows archive name changed at r591479. - archiveName = parseInt(revision, 10) > 591479 ? 'chrome-win' : 'chrome-win32'; - executablePath = path.join(archiveName, 'chrome.exe'); - } - return { - downloadUrl: util.format((downloadURLs as any)[platform], options.host, revision, archiveName), - executablePath - }; - }); -} diff --git a/src/chromium/NetworkManager.ts b/src/chromium/NetworkManager.ts deleted file mode 100644 index 58fe1f2eec..0000000000 --- a/src/chromium/NetworkManager.ts +++ /dev/null @@ -1,452 +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 { CDPSession } from './Connection'; -import { Page } from '../page'; -import { assert, debugError, helper, RegisteredListener } from '../helper'; -import { Protocol } from './protocol'; -import * as network from '../network'; -import * as frames from '../frames'; - -export class NetworkManager { - private _client: CDPSession; - private _page: Page; - private _requestIdToRequest = new Map(); - private _requestIdToRequestWillBeSentEvent = new Map(); - private _offline = false; - private _credentials: {username: string, password: string} | null = null; - private _attemptedAuthentications = new Set(); - private _userRequestInterceptionEnabled = false; - private _protocolRequestInterceptionEnabled = false; - private _userCacheDisabled = false; - private _requestIdToInterceptionId = new Map(); - private _eventListeners: RegisteredListener[]; - - constructor(client: CDPSession, page: Page) { - this._client = client; - this._page = page; - - this._eventListeners = [ - helper.addEventListener(client, 'Fetch.requestPaused', this._onRequestPaused.bind(this)), - helper.addEventListener(client, 'Fetch.authRequired', this._onAuthRequired.bind(this)), - helper.addEventListener(client, 'Network.requestWillBeSent', this._onRequestWillBeSent.bind(this)), - helper.addEventListener(client, 'Network.responseReceived', this._onResponseReceived.bind(this)), - helper.addEventListener(client, 'Network.loadingFinished', this._onLoadingFinished.bind(this)), - helper.addEventListener(client, 'Network.loadingFailed', this._onLoadingFailed.bind(this)), - ]; - } - - async initialize() { - await this._client.send('Network.enable'); - } - - dispose() { - helper.removeEventListeners(this._eventListeners); - } - - async authenticate(credentials: { username: string; password: string; } | null) { - this._credentials = credentials; - await this._updateProtocolRequestInterception(); - } - - async setOfflineMode(value: boolean) { - if (this._offline === value) - return; - this._offline = value; - await this._client.send('Network.emulateNetworkConditions', { - offline: this._offline, - // values of 0 remove any active throttling. crbug.com/456324#c9 - latency: 0, - downloadThroughput: -1, - uploadThroughput: -1 - }); - } - - async setUserAgent(userAgent: string) { - await this._client.send('Network.setUserAgentOverride', { userAgent }); - } - - async setCacheEnabled(enabled: boolean) { - this._userCacheDisabled = !enabled; - await this._updateProtocolCacheDisabled(); - } - - async setRequestInterception(value: boolean) { - this._userRequestInterceptionEnabled = value; - await this._updateProtocolRequestInterception(); - } - - async _updateProtocolRequestInterception() { - const enabled = this._userRequestInterceptionEnabled || !!this._credentials; - if (enabled === this._protocolRequestInterceptionEnabled) - return; - this._protocolRequestInterceptionEnabled = enabled; - if (enabled) { - await Promise.all([ - this._updateProtocolCacheDisabled(), - this._client.send('Fetch.enable', { - handleAuthRequests: true, - patterns: [{urlPattern: '*'}], - }), - ]); - } else { - await Promise.all([ - this._updateProtocolCacheDisabled(), - this._client.send('Fetch.disable') - ]); - } - } - - async _updateProtocolCacheDisabled() { - await this._client.send('Network.setCacheDisabled', { - cacheDisabled: this._userCacheDisabled || this._protocolRequestInterceptionEnabled - }); - } - - _onRequestWillBeSent(event: Protocol.Network.requestWillBeSentPayload) { - // Request interception doesn't happen for data URLs with Network Service. - if (this._protocolRequestInterceptionEnabled && !event.request.url.startsWith('data:')) { - const requestId = event.requestId; - const interceptionId = this._requestIdToInterceptionId.get(requestId); - if (interceptionId) { - this._onRequest(event, interceptionId); - this._requestIdToInterceptionId.delete(requestId); - } else { - this._requestIdToRequestWillBeSentEvent.set(event.requestId, event); - } - return; - } - this._onRequest(event, null); - } - - _onAuthRequired(event: Protocol.Fetch.authRequiredPayload) { - let response: 'Default' | 'CancelAuth' | 'ProvideCredentials' = 'Default'; - if (this._attemptedAuthentications.has(event.requestId)) { - response = 'CancelAuth'; - } else if (this._credentials) { - response = 'ProvideCredentials'; - this._attemptedAuthentications.add(event.requestId); - } - const {username, password} = this._credentials || {username: undefined, password: undefined}; - this._client.send('Fetch.continueWithAuth', { - requestId: event.requestId, - authChallengeResponse: { response, username, password }, - }).catch(debugError); - } - - _onRequestPaused(event: Protocol.Fetch.requestPausedPayload) { - if (!this._userRequestInterceptionEnabled && this._protocolRequestInterceptionEnabled) { - this._client.send('Fetch.continueRequest', { - requestId: event.requestId - }).catch(debugError); - } - - const requestId = event.networkId; - const interceptionId = event.requestId; - if (requestId && this._requestIdToRequestWillBeSentEvent.has(requestId)) { - const requestWillBeSentEvent = this._requestIdToRequestWillBeSentEvent.get(requestId); - this._onRequest(requestWillBeSentEvent, interceptionId); - this._requestIdToRequestWillBeSentEvent.delete(requestId); - } else { - this._requestIdToInterceptionId.set(requestId, interceptionId); - } - } - - _onRequest(event: Protocol.Network.requestWillBeSentPayload, interceptionId: string | null) { - let redirectChain: network.Request[] = []; - if (event.redirectResponse) { - const request = this._requestIdToRequest.get(event.requestId); - // If we connect late to the target, we could have missed the requestWillBeSent event. - if (request) { - this._handleRequestRedirect(request, event.redirectResponse); - redirectChain = request.request._redirectChain; - } - } - // TODO: how can frame be null here? - const frame = event.frameId ? this._page._frameManager.frame(event.frameId) : null; - const isNavigationRequest = event.requestId === event.loaderId && event.type === 'Document'; - const documentId = isNavigationRequest ? event.loaderId : undefined; - const request = new InterceptableRequest(this._client, frame, interceptionId, documentId, this._userRequestInterceptionEnabled, event, redirectChain); - this._requestIdToRequest.set(event.requestId, request); - this._page._frameManager.requestStarted(request.request); - } - - _createResponse(request: InterceptableRequest, responsePayload: Protocol.Network.Response): network.Response { - const remoteAddress: network.RemoteAddress = { ip: responsePayload.remoteIPAddress, port: responsePayload.remotePort }; - const getResponseBody = async () => { - const response = await this._client.send('Network.getResponseBody', { requestId: request._requestId }); - return Buffer.from(response.body, response.base64Encoded ? 'base64' : 'utf8'); - }; - return new network.Response(request.request, responsePayload.status, responsePayload.statusText, headersObject(responsePayload.headers), remoteAddress, getResponseBody); - } - - _handleRequestRedirect(request: InterceptableRequest, responsePayload: Protocol.Network.Response) { - const response = this._createResponse(request, responsePayload); - request.request._redirectChain.push(request.request); - response._requestFinished(new Error('Response body is unavailable for redirect responses')); - this._requestIdToRequest.delete(request._requestId); - this._attemptedAuthentications.delete(request._interceptionId); - this._page._frameManager.requestReceivedResponse(response); - this._page._frameManager.requestFinished(request.request); - } - - _onResponseReceived(event: Protocol.Network.responseReceivedPayload) { - const request = this._requestIdToRequest.get(event.requestId); - // FileUpload sends a response without a matching request. - if (!request) - return; - const response = this._createResponse(request, event.response); - this._page._frameManager.requestReceivedResponse(response); - } - - _onLoadingFinished(event: Protocol.Network.loadingFinishedPayload) { - const request = this._requestIdToRequest.get(event.requestId); - // For certain requestIds we never receive requestWillBeSent event. - // @see https://crbug.com/750469 - if (!request) - return; - - // Under certain conditions we never get the Network.responseReceived - // event from protocol. @see https://crbug.com/883475 - if (request.request.response()) - request.request.response()._requestFinished(); - this._requestIdToRequest.delete(request._requestId); - this._attemptedAuthentications.delete(request._interceptionId); - this._page._frameManager.requestFinished(request.request); - } - - _onLoadingFailed(event: Protocol.Network.loadingFailedPayload) { - const request = this._requestIdToRequest.get(event.requestId); - // For certain requestIds we never receive requestWillBeSent event. - // @see https://crbug.com/750469 - if (!request) - return; - const response = request.request.response(); - if (response) - response._requestFinished(); - this._requestIdToRequest.delete(request._requestId); - this._attemptedAuthentications.delete(request._interceptionId); - request.request._setFailureText(event.errorText); - this._page._frameManager.requestFailed(request.request, event.canceled); - } -} - -const interceptableRequestSymbol = Symbol('interceptableRequest'); - -export function toInterceptableRequest(request: network.Request): InterceptableRequest { - return (request as any)[interceptableRequestSymbol]; -} - -class InterceptableRequest { - readonly request: network.Request; - _requestId: string; - _interceptionId: string; - _documentId: string; - private _client: CDPSession; - private _allowInterception: boolean; - private _interceptionHandled = false; - - constructor(client: CDPSession, frame: frames.Frame | null, interceptionId: string, documentId: string | undefined, allowInterception: boolean, event: Protocol.Network.requestWillBeSentPayload, redirectChain: network.Request[]) { - this._client = client; - this._requestId = event.requestId; - this._interceptionId = interceptionId; - this._documentId = documentId; - this._allowInterception = allowInterception; - - this.request = new network.Request(frame, redirectChain, documentId, - event.request.url, event.type.toLowerCase(), event.request.method, event.request.postData, headersObject(event.request.headers)); - (this.request as any)[interceptableRequestSymbol] = this; - } - - async continue(overrides: { url?: string; method?: string; postData?: string; headers?: {[key: string]: string}; } = {}) { - // Request interception is not supported for data: urls. - if (this.request.url().startsWith('data:')) - return; - assert(this._allowInterception, 'Request Interception is not enabled!'); - assert(!this._interceptionHandled, 'Request is already handled!'); - const { - url, - method, - postData, - headers - } = overrides; - this._interceptionHandled = true; - await this._client.send('Fetch.continueRequest', { - requestId: this._interceptionId, - url, - method, - postData, - headers: headers ? headersArray(headers) : undefined, - }).catch(error => { - // In certain cases, protocol will return error if the request was already canceled - // or the page was closed. We should tolerate these errors. - debugError(error); - }); - } - - async fulfill(response: { status: number; headers: {[key: string]: string}; contentType: string; body: (string | Buffer); }) { - // Mocking responses for dataURL requests is not currently supported. - if (this.request.url().startsWith('data:')) - return; - assert(this._allowInterception, 'Request Interception is not enabled!'); - assert(!this._interceptionHandled, 'Request is already handled!'); - this._interceptionHandled = true; - - const responseBody = response.body && helper.isString(response.body) ? Buffer.from(/** @type {string} */(response.body)) : /** @type {?Buffer} */(response.body || null); - - const responseHeaders: { [s: string]: string; } = {}; - if (response.headers) { - for (const header of Object.keys(response.headers)) - responseHeaders[header.toLowerCase()] = response.headers[header]; - } - if (response.contentType) - responseHeaders['content-type'] = response.contentType; - if (responseBody && !('content-length' in responseHeaders)) - responseHeaders['content-length'] = String(Buffer.byteLength(responseBody)); - - await this._client.send('Fetch.fulfillRequest', { - requestId: this._interceptionId, - responseCode: response.status || 200, - responsePhrase: STATUS_TEXTS[String(response.status || 200)], - responseHeaders: headersArray(responseHeaders), - body: responseBody ? responseBody.toString('base64') : undefined, - }).catch(error => { - // In certain cases, protocol will return error if the request was already canceled - // or the page was closed. We should tolerate these errors. - debugError(error); - }); - } - - async abort(errorCode: string = 'failed') { - // Request interception is not supported for data: urls. - if (this.request.url().startsWith('data:')) - return; - const errorReason = errorReasons[errorCode]; - assert(errorReason, 'Unknown error code: ' + errorCode); - assert(this._allowInterception, 'Request Interception is not enabled!'); - assert(!this._interceptionHandled, 'Request is already handled!'); - this._interceptionHandled = true; - await this._client.send('Fetch.failRequest', { - requestId: this._interceptionId, - errorReason - }).catch(error => { - // In certain cases, protocol will return error if the request was already canceled - // or the page was closed. We should tolerate these errors. - debugError(error); - }); - } -} - -const errorReasons: { [reason: string]: Protocol.Network.ErrorReason } = { - 'aborted': 'Aborted', - 'accessdenied': 'AccessDenied', - 'addressunreachable': 'AddressUnreachable', - 'blockedbyclient': 'BlockedByClient', - 'blockedbyresponse': 'BlockedByResponse', - 'connectionaborted': 'ConnectionAborted', - 'connectionclosed': 'ConnectionClosed', - 'connectionfailed': 'ConnectionFailed', - 'connectionrefused': 'ConnectionRefused', - 'connectionreset': 'ConnectionReset', - 'internetdisconnected': 'InternetDisconnected', - 'namenotresolved': 'NameNotResolved', - 'timedout': 'TimedOut', - 'failed': 'Failed', -}; - -function headersArray(headers: { [s: string]: string; }): { name: string; value: string; }[] { - const result = []; - for (const name in headers) { - if (!Object.is(headers[name], undefined)) - result.push({name, value: headers[name] + ''}); - } - return result; -} - -function headersObject(headers: Protocol.Network.Headers): network.Headers { - const result: network.Headers = {}; - for (const key of Object.keys(headers)) - result[key.toLowerCase()] = headers[key]; - return result; -} - -// List taken from https://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml with extra 306 and 418 codes. -const STATUS_TEXTS: { [status: string]: string } = { - '100': 'Continue', - '101': 'Switching Protocols', - '102': 'Processing', - '103': 'Early Hints', - '200': 'OK', - '201': 'Created', - '202': 'Accepted', - '203': 'Non-Authoritative Information', - '204': 'No Content', - '205': 'Reset Content', - '206': 'Partial Content', - '207': 'Multi-Status', - '208': 'Already Reported', - '226': 'IM Used', - '300': 'Multiple Choices', - '301': 'Moved Permanently', - '302': 'Found', - '303': 'See Other', - '304': 'Not Modified', - '305': 'Use Proxy', - '306': 'Switch Proxy', - '307': 'Temporary Redirect', - '308': 'Permanent Redirect', - '400': 'Bad Request', - '401': 'Unauthorized', - '402': 'Payment Required', - '403': 'Forbidden', - '404': 'Not Found', - '405': 'Method Not Allowed', - '406': 'Not Acceptable', - '407': 'Proxy Authentication Required', - '408': 'Request Timeout', - '409': 'Conflict', - '410': 'Gone', - '411': 'Length Required', - '412': 'Precondition Failed', - '413': 'Payload Too Large', - '414': 'URI Too Long', - '415': 'Unsupported Media Type', - '416': 'Range Not Satisfiable', - '417': 'Expectation Failed', - '418': 'I\'m a teapot', - '421': 'Misdirected Request', - '422': 'Unprocessable Entity', - '423': 'Locked', - '424': 'Failed Dependency', - '425': 'Too Early', - '426': 'Upgrade Required', - '428': 'Precondition Required', - '429': 'Too Many Requests', - '431': 'Request Header Fields Too Large', - '451': 'Unavailable For Legal Reasons', - '500': 'Internal Server Error', - '501': 'Not Implemented', - '502': 'Bad Gateway', - '503': 'Service Unavailable', - '504': 'Gateway Timeout', - '505': 'HTTP Version Not Supported', - '506': 'Variant Also Negotiates', - '507': 'Insufficient Storage', - '508': 'Loop Detected', - '510': 'Not Extended', - '511': 'Network Authentication Required', -}; diff --git a/src/chromium/Playwright.ts b/src/chromium/Playwright.ts deleted file mode 100644 index ae1a2e3831..0000000000 --- a/src/chromium/Playwright.ts +++ /dev/null @@ -1,131 +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 http from 'http'; -import * as https from 'https'; -import * as URL from 'url'; -import * as browsers from '../browser'; -import { BrowserFetcher, BrowserFetcherOptions, BrowserFetcherRevisionInfo, OnProgressCallback } from '../browserFetcher'; -import { DeviceDescriptor, DeviceDescriptors } from '../deviceDescriptors'; -import * as Errors from '../errors'; -import { assert } from '../helper'; -import { ConnectionTransport, WebSocketTransport, SlowMoTransport } from '../transport'; -import { ConnectionOptions, createBrowserFetcher, Launcher, LauncherChromeArgOptions, LauncherLaunchOptions } from './Launcher'; -import { Browser } from './Browser'; - -type Devices = { [name: string]: DeviceDescriptor } & DeviceDescriptor[]; - -export class Playwright { - private _projectRoot: string; - private _launcher: Launcher; - readonly _revision: string; - - constructor(projectRoot: string, preferredRevision: string) { - this._projectRoot = projectRoot; - this._launcher = new Launcher(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?: (LauncherLaunchOptions & LauncherChromeArgOptions & ConnectionOptions) | undefined): Promise { - const server = await this._launcher.launch(options); - return server.connect(); - } - - async launchServer(options: (LauncherLaunchOptions & LauncherChromeArgOptions & ConnectionOptions) = {}): Promise> { - return this._launcher.launch(options); - } - - async connect(options: (ConnectionOptions & { - browserWSEndpoint?: string; - browserURL?: string; - transport?: ConnectionTransport; })): 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'); - - let transport: ConnectionTransport | undefined; - let connectionURL: string = ''; - if (options.transport) { - transport = options.transport; - } else if (options.browserWSEndpoint) { - connectionURL = options.browserWSEndpoint; - transport = await WebSocketTransport.create(options.browserWSEndpoint); - } else if (options.browserURL) { - connectionURL = await getWSEndpoint(options.browserURL); - transport = await WebSocketTransport.create(connectionURL); - } - return Browser.create(SlowMoTransport.wrap(transport, options.slowMo)); - } - - executablePath(): string { - return this._launcher.executablePath(); - } - - get devices(): Devices { - const result = DeviceDescriptors.slice() as Devices; - for (const device of DeviceDescriptors) - result[device.name] = device; - return result; - } - - get errors(): any { - return Errors; - } - - defaultArgs(options: LauncherChromeArgOptions | undefined): string[] { - return this._launcher.defaultArgs(options); - } - - createBrowserFetcher(options?: BrowserFetcherOptions): BrowserFetcher { - return createBrowserFetcher(this._projectRoot, options); - } -} - -function getWSEndpoint(browserURL: string): Promise { - let resolve: (url: string) => void; - let reject: (e: Error) => void; - const promise = new Promise((res, rej) => { resolve = res; reject = rej; }); - - const endpointURL = URL.resolve(browserURL, '/json/version'); - const protocol = endpointURL.startsWith('https') ? https : http; - const requestOptions = Object.assign(URL.parse(endpointURL), { method: 'GET' }); - const request = protocol.request(requestOptions, res => { - let data = ''; - if (res.statusCode !== 200) { - // Consume response data to free up memory. - res.resume(); - reject(new Error('HTTP ' + res.statusCode)); - return; - } - res.setEncoding('utf8'); - res.on('data', chunk => data += chunk); - res.on('end', () => resolve(JSON.parse(data).webSocketDebuggerUrl)); - }); - - request.on('error', reject); - request.end(); - - return promise.catch(e => { - e.message = `Failed to fetch browser webSocket url from ${endpointURL}: ` + e.message; - throw e; - }); -} \ No newline at end of file diff --git a/src/chromium/Target.ts b/src/chromium/Target.ts deleted file mode 100644 index 6d0beda65a..0000000000 --- a/src/chromium/Target.ts +++ /dev/null @@ -1,147 +0,0 @@ -/** - * Copyright 2019 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 { Browser } from './Browser'; -import { BrowserContext } from '../browserContext'; -import { CDPSession, CDPSessionEvents } from './Connection'; -import { Events } from '../events'; -import { Worker } from './features/workers'; -import { Page } from '../page'; -import { Protocol } from './protocol'; -import { debugError } from '../helper'; -import { FrameManager } from './FrameManager'; - -const targetSymbol = Symbol('target'); - -export class Target { - private _targetInfo: Protocol.Target.TargetInfo; - private _browser: Browser; - private _browserContext: BrowserContext; - _targetId: string; - private _sessionFactory: () => Promise; - private _pagePromise: Promise | null = null; - private _frameManager: FrameManager | null = null; - private _workerPromise: Promise | null = null; - _initializedPromise: Promise; - _initializedCallback: (value?: unknown) => void; - _isInitialized: boolean; - - static fromPage(page: Page): Target { - return (page as any)[targetSymbol]; - } - - constructor( - browser: Browser, - targetInfo: Protocol.Target.TargetInfo, - browserContext: BrowserContext, - sessionFactory: () => Promise) { - this._targetInfo = targetInfo; - this._browser = browser; - this._browserContext = browserContext; - this._targetId = targetInfo.targetId; - this._sessionFactory = sessionFactory; - this._initializedPromise = new Promise(fulfill => this._initializedCallback = fulfill).then(async success => { - if (!success) - return false; - const opener = this.opener(); - if (!opener || !opener._pagePromise || this.type() !== 'page') - return true; - const openerPage = await opener._pagePromise; - if (!openerPage.listenerCount(Events.Page.Popup)) - return true; - const popupPage = await this.page(); - openerPage.emit(Events.Page.Popup, popupPage); - return true; - }); - this._isInitialized = this._targetInfo.type !== 'page' || this._targetInfo.url !== ''; - if (this._isInitialized) - this._initializedCallback(true); - } - - _didClose() { - if (this._frameManager) - this._frameManager.didClose(); - } - - async page(): Promise { - if ((this._targetInfo.type === 'page' || this._targetInfo.type === 'background_page') && !this._pagePromise) { - this._pagePromise = this._sessionFactory().then(async client => { - this._frameManager = new FrameManager(client, this._browser, this._browserContext); - const page = this._frameManager.page(); - (page as any)[targetSymbol] = this; - client.once(CDPSessionEvents.Disconnected, () => page._didDisconnect()); - client.on('Target.attachedToTarget', event => { - if (event.targetInfo.type !== 'worker') { - // If we don't detach from service workers, they will never die. - client.send('Target.detachFromTarget', { sessionId: event.sessionId }).catch(debugError); - } - }); - await this._frameManager.initialize(); - await client.send('Target.setAutoAttach', {autoAttach: true, waitForDebuggerOnStart: false, flatten: true}); - return page; - }); - } - return this._pagePromise; - } - - async _worker(): Promise { - if (this._targetInfo.type !== 'service_worker' && this._targetInfo.type !== 'shared_worker') - return null; - if (!this._workerPromise) { - // TODO(einbinder): Make workers send their console logs. - this._workerPromise = this._sessionFactory() - .then(client => new Worker(client, this._targetInfo.url, () => { } /* consoleAPICalled */, () => { } /* exceptionThrown */)); - } - return this._workerPromise; - } - - url(): string { - return this._targetInfo.url; - } - - type(): 'page' | 'background_page' | 'service_worker' | 'shared_worker' | 'other' | 'browser' { - const type = this._targetInfo.type; - if (type === 'page' || type === 'background_page' || type === 'service_worker' || type === 'shared_worker' || type === 'browser') - return type; - return 'other'; - } - - browserContext(): BrowserContext { - return this._browserContext; - } - - opener(): Target | null { - const { openerId } = this._targetInfo; - if (!openerId) - return null; - return this._browser._targets.get(openerId); - } - - createCDPSession(): Promise { - return this._sessionFactory(); - } - - _targetInfoChanged(targetInfo: Protocol.Target.TargetInfo) { - this._targetInfo = targetInfo; - - if (!this._isInitialized && (this._targetInfo.type !== 'page' || this._targetInfo.url !== '')) { - this._isInitialized = true; - this._initializedCallback(true); - return; - } - } -} diff --git a/src/chromium/api.ts b/src/chromium/api.ts index f3aa2dfc1e..d0beab85a8 100644 --- a/src/chromium/api.ts +++ b/src/chromium/api.ts @@ -10,13 +10,13 @@ export { Keyboard, Mouse } from '../input'; export { JSHandle } from '../javascript'; export { Request, Response } from '../network'; export { BrowserContext } from '../browserContext'; -export { CDPSession } from './Connection'; -export { Accessibility } from './features/accessibility'; -export { Coverage } from './features/coverage'; -export { Interception } from './features/interception'; -export { Overrides } from './features/overrides'; -export { PDF } from './features/pdf'; -export { Permissions } from './features/permissions'; -export { Worker, Workers } from './features/workers'; +export { CRSession as CDPSession } from './crConnection'; +export { CRAccessibility as Accessibility } from './features/crAccessibility'; +export { CRCoverage as Coverage } from './features/crCoverage'; +export { CRInterception as Interception } from './features/crInterception'; +export { CROverrides as Overrides } from './features/crOverrides'; +export { CRPdf as PDF } from './features/crPdf'; +export { CRPermissions as Permissions } from './features/crPermissions'; +export { Worker, CRWorkers as Workers } from './features/crWorkers'; export { Page } from '../page'; -export { Target } from './Target'; +export { CRTarget as Target } from './crTarget'; diff --git a/src/chromium/events.ts b/src/chromium/events.ts index bd402c6ac5..ebb4f96255 100644 --- a/src/chromium/events.ts +++ b/src/chromium/events.ts @@ -17,7 +17,6 @@ export const Events = { Browser: { - Disconnected: 'disconnected', TargetCreated: 'targetcreated', TargetDestroyed: 'targetdestroyed', TargetChanged: 'targetchanged', diff --git a/src/chromium/features/accessibility.ts b/src/chromium/features/accessibility.ts deleted file mode 100644 index e823e3bb5a..0000000000 --- a/src/chromium/features/accessibility.ts +++ /dev/null @@ -1,374 +0,0 @@ -/** - * Copyright 2018 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 { CDPSession } from '../Connection'; -import { Protocol } from '../protocol'; -import * as dom from '../../dom'; - -type SerializedAXNode = { - role: string, - name?: string, - value?: string|number, - description?: string, - - keyshortcuts?: string, - roledescription?: string, - valuetext?: string, - - disabled?: boolean, - expanded?: boolean, - focused?: boolean, - modal?: boolean, - multiline?: boolean, - multiselectable?: boolean, - readonly?: boolean, - required?: boolean, - selected?: boolean, - - checked?: boolean|'mixed', - pressed?: boolean|'mixed', - - level?: number, - valuemin?: number, - valuemax?: number, - - autocomplete?: string, - haspopup?: string, - invalid?: string, - orientation?: string, - - children?: SerializedAXNode[] -}; - -export class Accessibility { - private _client: CDPSession; - - constructor(client: CDPSession) { - this._client = client; - } - - async snapshot(options: { - interestingOnly?: boolean; - root?: dom.ElementHandle | null; - } = {}): Promise { - const { - interestingOnly = true, - root = null, - } = options; - const {nodes} = await this._client.send('Accessibility.getFullAXTree'); - let backendNodeId = null; - if (root) { - const remoteObject = root._remoteObject as Protocol.Runtime.RemoteObject; - const {node} = await this._client.send('DOM.describeNode', {objectId: remoteObject.objectId}); - backendNodeId = node.backendNodeId; - } - const defaultRoot = AXNode.createTree(nodes); - let needle = defaultRoot; - if (backendNodeId) { - needle = defaultRoot.find(node => node._payload.backendDOMNodeId === backendNodeId); - if (!needle) - return null; - } - if (!interestingOnly) - return serializeTree(needle)[0]; - - const interestingNodes: Set = new Set(); - collectInterestingNodes(interestingNodes, defaultRoot, false); - if (!interestingNodes.has(needle)) - return null; - return serializeTree(needle, interestingNodes)[0]; - } -} - -function collectInterestingNodes(collection: Set, node: AXNode, insideControl: boolean) { - if (node.isInteresting(insideControl)) - collection.add(node); - if (node.isLeafNode()) - return; - insideControl = insideControl || node.isControl(); - for (const child of node._children) - collectInterestingNodes(collection, child, insideControl); -} - -function serializeTree(node: AXNode, whitelistedNodes?: Set): SerializedAXNode[] { - const children: SerializedAXNode[] = []; - for (const child of node._children) - children.push(...serializeTree(child, whitelistedNodes)); - - if (whitelistedNodes && !whitelistedNodes.has(node)) - return children; - - const serializedNode = node.serialize(); - if (children.length) - serializedNode.children = children; - return [serializedNode]; -} - - -class AXNode { - _payload: Protocol.Accessibility.AXNode; - _children: AXNode[] = []; - private _richlyEditable = false; - private _editable = false; - private _focusable = false; - private _expanded = false; - private _hidden = false; - private _name: string; - private _role: string; - private _cachedHasFocusableChild: boolean | undefined; - - constructor(payload: Protocol.Accessibility.AXNode) { - this._payload = payload; - - this._name = this._payload.name ? this._payload.name.value : ''; - this._role = this._payload.role ? this._payload.role.value : 'Unknown'; - - for (const property of this._payload.properties || []) { - if (property.name === 'editable') { - this._richlyEditable = property.value.value === 'richtext'; - this._editable = true; - } - if (property.name === 'focusable') - this._focusable = property.value.value; - if (property.name === 'expanded') - this._expanded = property.value.value; - if (property.name === 'hidden') - this._hidden = property.value.value; - } - } - - private _isPlainTextField(): boolean { - if (this._richlyEditable) - return false; - if (this._editable) - return true; - return this._role === 'textbox' || this._role === 'ComboBox' || this._role === 'searchbox'; - } - - private _isTextOnlyObject(): boolean { - const role = this._role; - return (role === 'LineBreak' || role === 'text' || - role === 'InlineTextBox'); - } - - private _hasFocusableChild(): boolean { - if (this._cachedHasFocusableChild === undefined) { - this._cachedHasFocusableChild = false; - for (const child of this._children) { - if (child._focusable || child._hasFocusableChild()) { - this._cachedHasFocusableChild = true; - break; - } - } - } - return this._cachedHasFocusableChild; - } - - find(predicate: (arg0: AXNode) => boolean): AXNode | null { - if (predicate(this)) - return this; - for (const child of this._children) { - const result = child.find(predicate); - if (result) - return result; - } - return null; - } - - isLeafNode(): boolean { - if (!this._children.length) - return true; - - // These types of objects may have children that we use as internal - // implementation details, but we want to expose them as leaves to platform - // accessibility APIs because screen readers might be confused if they find - // any children. - if (this._isPlainTextField() || this._isTextOnlyObject()) - return true; - - // Roles whose children are only presentational according to the ARIA and - // HTML5 Specs should be hidden from screen readers. - // (Note that whilst ARIA buttons can have only presentational children, HTML5 - // buttons are allowed to have content.) - switch (this._role) { - case 'doc-cover': - case 'graphics-symbol': - case 'img': - case 'Meter': - case 'scrollbar': - case 'slider': - case 'separator': - case 'progressbar': - return true; - default: - break; - } - - // Here and below: Android heuristics - if (this._hasFocusableChild()) - return false; - if (this._focusable && this._name) - return true; - if (this._role === 'heading' && this._name) - return true; - return false; - } - - isControl(): boolean { - switch (this._role) { - case 'button': - case 'checkbox': - case 'ColorWell': - case 'combobox': - case 'DisclosureTriangle': - case 'listbox': - case 'menu': - case 'menubar': - case 'menuitem': - case 'menuitemcheckbox': - case 'menuitemradio': - case 'radio': - case 'scrollbar': - case 'searchbox': - case 'slider': - case 'spinbutton': - case 'switch': - case 'tab': - case 'textbox': - case 'tree': - return true; - default: - return false; - } - } - - isInteresting(insideControl: boolean): boolean { - const role = this._role; - if (role === 'Ignored' || this._hidden) - return false; - - if (this._focusable || this._richlyEditable) - return true; - - // If it's not focusable but has a control role, then it's interesting. - if (this.isControl()) - return true; - - // A non focusable child of a control is not interesting - if (insideControl) - return false; - - return this.isLeafNode() && !!this._name; - } - - serialize(): SerializedAXNode { - const properties: Map = new Map(); - for (const property of this._payload.properties || []) - properties.set(property.name.toLowerCase(), property.value.value); - if (this._payload.name) - properties.set('name', this._payload.name.value); - if (this._payload.value) - properties.set('value', this._payload.value.value); - if (this._payload.description) - properties.set('description', this._payload.description.value); - - const node: {[x in keyof SerializedAXNode]: any} = { - role: this._role - }; - - const userStringProperties: Array = [ - 'name', - 'value', - 'description', - 'keyshortcuts', - 'roledescription', - 'valuetext', - ]; - for (const userStringProperty of userStringProperties) { - if (!properties.has(userStringProperty)) - continue; - node[userStringProperty] = properties.get(userStringProperty); - } - - const booleanProperties: Array = [ - 'disabled', - 'expanded', - 'focused', - 'modal', - 'multiline', - 'multiselectable', - 'readonly', - 'required', - 'selected', - ]; - for (const booleanProperty of booleanProperties) { - // WebArea's treat focus differently than other nodes. They report whether their frame has focus, - // not whether focus is specifically on the root node. - if (booleanProperty === 'focused' && this._role === 'WebArea') - continue; - const value = properties.get(booleanProperty); - if (!value) - continue; - node[booleanProperty] = value; - } - - const tristateProperties: Array = [ - 'checked', - 'pressed', - ]; - for (const tristateProperty of tristateProperties) { - if (!properties.has(tristateProperty)) - continue; - const value = properties.get(tristateProperty); - node[tristateProperty] = value === 'mixed' ? 'mixed' : value === 'true' ? true : false; - } - const numericalProperties: Array = [ - 'level', - 'valuemax', - 'valuemin', - ]; - for (const numericalProperty of numericalProperties) { - if (!properties.has(numericalProperty)) - continue; - node[numericalProperty] = properties.get(numericalProperty); - } - const tokenProperties: Array = [ - 'autocomplete', - 'haspopup', - 'invalid', - 'orientation', - ]; - for (const tokenProperty of tokenProperties) { - const value = properties.get(tokenProperty); - if (!value || value === 'false') - continue; - node[tokenProperty] = value; - } - return node as SerializedAXNode; - } - - static createTree(payloads: Protocol.Accessibility.AXNode[]): AXNode { - const nodeById: Map = new Map(); - for (const payload of payloads) - nodeById.set(payload.nodeId, new AXNode(payload)); - for (const node of nodeById.values()) { - for (const childId of node._payload.childIds || []) - node._children.push(nodeById.get(childId)); - } - return nodeById.values().next().value; - } -} diff --git a/src/chromium/features/coverage.ts b/src/chromium/features/coverage.ts deleted file mode 100644 index b089e21e76..0000000000 --- a/src/chromium/features/coverage.ts +++ /dev/null @@ -1,297 +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 { CDPSession } from '../Connection'; -import { assert, debugError, helper, RegisteredListener } from '../../helper'; -import { Protocol } from '../protocol'; - -import { EVALUATION_SCRIPT_URL } from '../ExecutionContext'; - -type CoverageEntry = { - url: string, - text: string, - ranges : {start: number, end: number}[] -}; - -export class Coverage { - private _jsCoverage: JSCoverage; - private _cssCoverage: CSSCoverage; - - constructor(client: CDPSession) { - this._jsCoverage = new JSCoverage(client); - this._cssCoverage = new CSSCoverage(client); - } - - async startJSCoverage(options: { - resetOnNavigation?: boolean; - reportAnonymousScripts?: boolean; - }) { - return await this._jsCoverage.start(options); - } - - async stopJSCoverage(): Promise { - return await this._jsCoverage.stop(); - } - - async startCSSCoverage(options: { resetOnNavigation?: boolean; } = {}) { - return await this._cssCoverage.start(options); - } - - async stopCSSCoverage(): Promise { - return await this._cssCoverage.stop(); - } -} - -class JSCoverage { - _client: CDPSession; - _enabled: boolean; - _scriptURLs: Map; - _scriptSources: Map; - _eventListeners: RegisteredListener[]; - _resetOnNavigation: boolean; - _reportAnonymousScripts: boolean; - - constructor(client: CDPSession) { - this._client = client; - this._enabled = false; - this._scriptURLs = new Map(); - this._scriptSources = new Map(); - this._eventListeners = []; - this._resetOnNavigation = false; - } - - async start(options: { - resetOnNavigation?: boolean; - reportAnonymousScripts?: boolean; - } = {}) { - assert(!this._enabled, 'JSCoverage is already enabled'); - const { - resetOnNavigation = true, - reportAnonymousScripts = false - } = options; - this._resetOnNavigation = resetOnNavigation; - this._reportAnonymousScripts = reportAnonymousScripts; - this._enabled = true; - this._scriptURLs.clear(); - this._scriptSources.clear(); - this._eventListeners = [ - helper.addEventListener(this._client, 'Debugger.scriptParsed', this._onScriptParsed.bind(this)), - helper.addEventListener(this._client, 'Runtime.executionContextsCleared', this._onExecutionContextsCleared.bind(this)), - ]; - this._client.on('Debugger.paused', () => this._client.send('Debugger.resume')); - await Promise.all([ - this._client.send('Profiler.enable'), - this._client.send('Profiler.startPreciseCoverage', {callCount: false, detailed: true}), - this._client.send('Debugger.enable'), - this._client.send('Debugger.setSkipAllPauses', {skip: true}) - ]); - } - - _onExecutionContextsCleared() { - if (!this._resetOnNavigation) - return; - this._scriptURLs.clear(); - this._scriptSources.clear(); - } - - async _onScriptParsed(event: Protocol.Debugger.scriptParsedPayload) { - // Ignore playwright-injected scripts - if (event.url === EVALUATION_SCRIPT_URL) - return; - // Ignore other anonymous scripts unless the reportAnonymousScripts option is true. - if (!event.url && !this._reportAnonymousScripts) - return; - try { - const response = await this._client.send('Debugger.getScriptSource', {scriptId: event.scriptId}); - this._scriptURLs.set(event.scriptId, event.url); - this._scriptSources.set(event.scriptId, response.scriptSource); - } catch (e) { - // This might happen if the page has already navigated away. - debugError(e); - } - } - - async stop(): Promise { - assert(this._enabled, 'JSCoverage is not enabled'); - this._enabled = false; - const [profileResponse] = await Promise.all([ - this._client.send('Profiler.takePreciseCoverage'), - this._client.send('Profiler.stopPreciseCoverage'), - this._client.send('Profiler.disable'), - this._client.send('Debugger.disable'), - ]); - helper.removeEventListeners(this._eventListeners); - - const coverage = []; - for (const entry of profileResponse.result) { - let url = this._scriptURLs.get(entry.scriptId); - if (!url && this._reportAnonymousScripts) - url = 'debugger://VM' + entry.scriptId; - const text = this._scriptSources.get(entry.scriptId); - if (text === undefined || url === undefined) - continue; - const flattenRanges = []; - for (const func of entry.functions) - flattenRanges.push(...func.ranges); - const ranges = convertToDisjointRanges(flattenRanges); - coverage.push({url, ranges, text}); - } - return coverage; - } -} - -class CSSCoverage { - _client: CDPSession; - _enabled: boolean; - _stylesheetURLs: Map; - _stylesheetSources: Map; - _eventListeners: RegisteredListener[]; - _resetOnNavigation: boolean; - - constructor(client: CDPSession) { - this._client = client; - this._enabled = false; - this._stylesheetURLs = new Map(); - this._stylesheetSources = new Map(); - this._eventListeners = []; - this._resetOnNavigation = false; - } - - async start(options: { resetOnNavigation?: boolean; } = {}) { - assert(!this._enabled, 'CSSCoverage is already enabled'); - const {resetOnNavigation = true} = options; - this._resetOnNavigation = resetOnNavigation; - this._enabled = true; - this._stylesheetURLs.clear(); - this._stylesheetSources.clear(); - this._eventListeners = [ - helper.addEventListener(this._client, 'CSS.styleSheetAdded', this._onStyleSheet.bind(this)), - helper.addEventListener(this._client, 'Runtime.executionContextsCleared', this._onExecutionContextsCleared.bind(this)), - ]; - await Promise.all([ - this._client.send('DOM.enable'), - this._client.send('CSS.enable'), - this._client.send('CSS.startRuleUsageTracking'), - ]); - } - - _onExecutionContextsCleared() { - if (!this._resetOnNavigation) - return; - this._stylesheetURLs.clear(); - this._stylesheetSources.clear(); - } - - async _onStyleSheet(event: Protocol.CSS.styleSheetAddedPayload) { - const header = event.header; - // Ignore anonymous scripts - if (!header.sourceURL) - return; - try { - const response = await this._client.send('CSS.getStyleSheetText', {styleSheetId: header.styleSheetId}); - this._stylesheetURLs.set(header.styleSheetId, header.sourceURL); - this._stylesheetSources.set(header.styleSheetId, response.text); - } catch (e) { - // This might happen if the page has already navigated away. - debugError(e); - } - } - - async stop(): Promise { - assert(this._enabled, 'CSSCoverage is not enabled'); - this._enabled = false; - const ruleTrackingResponse = await this._client.send('CSS.stopRuleUsageTracking'); - await Promise.all([ - this._client.send('CSS.disable'), - this._client.send('DOM.disable'), - ]); - helper.removeEventListeners(this._eventListeners); - - // aggregate by styleSheetId - const styleSheetIdToCoverage = new Map(); - for (const entry of ruleTrackingResponse.ruleUsage) { - let ranges = styleSheetIdToCoverage.get(entry.styleSheetId); - if (!ranges) { - ranges = []; - styleSheetIdToCoverage.set(entry.styleSheetId, ranges); - } - ranges.push({ - startOffset: entry.startOffset, - endOffset: entry.endOffset, - count: entry.used ? 1 : 0, - }); - } - - const coverage = []; - for (const styleSheetId of this._stylesheetURLs.keys()) { - const url = this._stylesheetURLs.get(styleSheetId); - const text = this._stylesheetSources.get(styleSheetId); - const ranges = convertToDisjointRanges(styleSheetIdToCoverage.get(styleSheetId) || []); - coverage.push({url, ranges, text}); - } - - return coverage; - } -} - -function convertToDisjointRanges(nestedRanges: { - startOffset: number; - endOffset: number; - count: number; }[]): { start: number; end: number; }[] { - const points = []; - for (const range of nestedRanges) { - points.push({ offset: range.startOffset, type: 0, range }); - points.push({ offset: range.endOffset, type: 1, range }); - } - // Sort points to form a valid parenthesis sequence. - points.sort((a, b) => { - // Sort with increasing offsets. - if (a.offset !== b.offset) - return a.offset - b.offset; - // All "end" points should go before "start" points. - if (a.type !== b.type) - return b.type - a.type; - const aLength = a.range.endOffset - a.range.startOffset; - const bLength = b.range.endOffset - b.range.startOffset; - // For two "start" points, the one with longer range goes first. - if (a.type === 0) - return bLength - aLength; - // For two "end" points, the one with shorter range goes first. - return aLength - bLength; - }); - - const hitCountStack = []; - const results = []; - let lastOffset = 0; - // Run scanning line to intersect all ranges. - for (const point of points) { - if (hitCountStack.length && lastOffset < point.offset && hitCountStack[hitCountStack.length - 1] > 0) { - const lastResult = results.length ? results[results.length - 1] : null; - if (lastResult && lastResult.end === lastOffset) - lastResult.end = point.offset; - else - results.push({start: lastOffset, end: point.offset}); - } - lastOffset = point.offset; - if (point.type === 0) - hitCountStack.push(point.range.count); - else - hitCountStack.pop(); - } - // Filter out empty ranges. - return results.filter(range => range.end - range.start > 1); -} diff --git a/src/chromium/features/interception.ts b/src/chromium/features/interception.ts deleted file mode 100644 index 8a78dffdb9..0000000000 --- a/src/chromium/features/interception.ts +++ /dev/null @@ -1,41 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. - -import { NetworkManager, toInterceptableRequest } from '../NetworkManager'; -import * as network from '../../network'; - -export class Interception { - private _networkManager: NetworkManager; - - constructor(networkManager: NetworkManager) { - this._networkManager = networkManager; - } - - async enable() { - await this._networkManager.setRequestInterception(true); - } - - async disable() { - await this._networkManager.setRequestInterception(false); - } - - async continue(request: network.Request, overrides: { url?: string; method?: string; postData?: string; headers?: {[key: string]: string}; } = {}) { - return toInterceptableRequest(request).continue(overrides); - } - - async fulfill(request: network.Request, response: { status: number; headers: {[key: string]: string}; contentType: string; body: (string | Buffer); }) { - return toInterceptableRequest(request).fulfill(response); - } - - async abort(request: network.Request, errorCode: string = 'failed') { - return toInterceptableRequest(request).abort(errorCode); - } - - setOfflineMode(enabled: boolean) { - return this._networkManager.setOfflineMode(enabled); - } - - async authenticate(credentials: { username: string; password: string; } | null) { - return this._networkManager.authenticate(credentials); - } -} diff --git a/src/chromium/features/overrides.ts b/src/chromium/features/overrides.ts deleted file mode 100644 index 8086ea781d..0000000000 --- a/src/chromium/features/overrides.ts +++ /dev/null @@ -1,54 +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 { BrowserContext } from '../../browserContext'; -import { FrameManager } from '../FrameManager'; -import { Page } from '../api'; - -export class Overrides { - private _context: BrowserContext; - private _geolocation: { longitude?: number; latitude?: number; accuracy?: number; } | null = null; - - constructor(context: BrowserContext) { - this._context = context; - } - - async setGeolocation(options: { longitude?: number; latitude?: number; accuracy?: (number | undefined); } | null) { - if (!options) { - for (const page of await this._context.pages()) - await (page._delegate as FrameManager)._client.send('Emulation.clearGeolocationOverride', {}); - this._geolocation = null; - return; - } - - const { longitude, latitude, accuracy = 0} = options; - if (longitude < -180 || longitude > 180) - throw new Error(`Invalid longitude "${longitude}": precondition -180 <= LONGITUDE <= 180 failed.`); - if (latitude < -90 || latitude > 90) - throw new Error(`Invalid latitude "${latitude}": precondition -90 <= LATITUDE <= 90 failed.`); - if (accuracy < 0) - throw new Error(`Invalid accuracy "${accuracy}": precondition 0 <= ACCURACY failed.`); - this._geolocation = { longitude, latitude, accuracy }; - for (const page of await this._context.pages()) - await (page._delegate as FrameManager)._client.send('Emulation.setGeolocationOverride', this._geolocation); - } - - async _applyOverrides(page: Page): Promise { - if (this._geolocation) - await (page._delegate as FrameManager)._client.send('Emulation.setGeolocationOverride', this._geolocation); - } -} diff --git a/src/chromium/features/pdf.ts b/src/chromium/features/pdf.ts deleted file mode 100644 index 0fc72972fe..0000000000 --- a/src/chromium/features/pdf.ts +++ /dev/null @@ -1,144 +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 { assert, helper } from '../../helper'; -import { CDPSession } from '../Connection'; -import { readProtocolStream } from '../protocolHelper'; - -type PDFOptions = { - scale?: number, - displayHeaderFooter?: boolean, - headerTemplate?: string, - footerTemplate?: string, - printBackground?: boolean, - landscape?: boolean, - pageRanges?: string, - format?: string, - width?: string|number, - height?: string|number, - preferCSSPageSize?: boolean, - margin?: {top?: string|number, bottom?: string|number, left?: string|number, right?: string|number}, - path?: string, -} - -const PagePaperFormats = { - letter: {width: 8.5, height: 11}, - legal: {width: 8.5, height: 14}, - tabloid: {width: 11, height: 17}, - ledger: {width: 17, height: 11}, - a0: {width: 33.1, height: 46.8 }, - a1: {width: 23.4, height: 33.1 }, - a2: {width: 16.54, height: 23.4 }, - a3: {width: 11.7, height: 16.54 }, - a4: {width: 8.27, height: 11.7 }, - a5: {width: 5.83, height: 8.27 }, - a6: {width: 4.13, height: 5.83 }, -}; - -const unitToPixels = { - 'px': 1, - 'in': 96, - 'cm': 37.8, - 'mm': 3.78 -}; - -function convertPrintParameterToInches(parameter: (string | number | undefined)): (number | undefined) { - if (typeof parameter === 'undefined') - return undefined; - let pixels: number; - if (helper.isNumber(parameter)) { - // Treat numbers as pixel values to be aligned with phantom's paperSize. - pixels = parameter as number; - } else if (helper.isString(parameter)) { - const text: string = parameter as string; - let unit = text.substring(text.length - 2).toLowerCase(); - let valueText = ''; - if (unitToPixels.hasOwnProperty(unit)) { - valueText = text.substring(0, text.length - 2); - } else { - // In case of unknown unit try to parse the whole parameter as number of pixels. - // This is consistent with phantom's paperSize behavior. - unit = 'px'; - valueText = text; - } - const value = Number(valueText); - assert(!isNaN(value), 'Failed to parse parameter value: ' + text); - pixels = value * unitToPixels[unit]; - } else { - throw new Error('page.pdf() Cannot handle parameter type: ' + (typeof parameter)); - } - return pixels / 96; -} - -export class PDF { - private _client: CDPSession; - - constructor(client: CDPSession) { - this._client = client; - } - - async generate(options: PDFOptions = {}): Promise { - const { - scale = 1, - displayHeaderFooter = false, - headerTemplate = '', - footerTemplate = '', - printBackground = false, - landscape = false, - pageRanges = '', - preferCSSPageSize = false, - margin = {}, - path = null - } = options; - - let paperWidth = 8.5; - let paperHeight = 11; - if (options.format) { - const format = PagePaperFormats[options.format.toLowerCase()]; - assert(format, 'Unknown paper format: ' + options.format); - paperWidth = format.width; - paperHeight = format.height; - } else { - paperWidth = convertPrintParameterToInches(options.width) || paperWidth; - paperHeight = convertPrintParameterToInches(options.height) || paperHeight; - } - - const marginTop = convertPrintParameterToInches(margin.top) || 0; - const marginLeft = convertPrintParameterToInches(margin.left) || 0; - const marginBottom = convertPrintParameterToInches(margin.bottom) || 0; - const marginRight = convertPrintParameterToInches(margin.right) || 0; - - const result = await this._client.send('Page.printToPDF', { - transferMode: 'ReturnAsStream', - landscape, - displayHeaderFooter, - headerTemplate, - footerTemplate, - printBackground, - scale, - paperWidth, - paperHeight, - marginTop, - marginBottom, - marginLeft, - marginRight, - pageRanges, - preferCSSPageSize - }); - return await readProtocolStream(this._client, result.stream, path); - } -} diff --git a/src/chromium/features/permissions.ts b/src/chromium/features/permissions.ts deleted file mode 100644 index 831cfb7617..0000000000 --- a/src/chromium/features/permissions.ts +++ /dev/null @@ -1,61 +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 { Protocol } from '../protocol'; -import { CDPSession } from './../Connection'; - -export class Permissions { - private _client: CDPSession; - private _browserContextId: string; - - constructor(client: CDPSession, browserContextId: string | null) { - this._client = client; - this._browserContextId = browserContextId; - } - - async override(origin: string, permissions: string[]) { - const webPermissionToProtocol = new Map([ - ['geolocation', 'geolocation'], - ['midi', 'midi'], - ['notifications', 'notifications'], - ['camera', 'videoCapture'], - ['microphone', 'audioCapture'], - ['background-sync', 'backgroundSync'], - ['ambient-light-sensor', 'sensors'], - ['accelerometer', 'sensors'], - ['gyroscope', 'sensors'], - ['magnetometer', 'sensors'], - ['accessibility-events', 'accessibilityEvents'], - ['clipboard-read', 'clipboardReadWrite'], - ['clipboard-write', 'clipboardSanitizedWrite'], - ['payment-handler', 'paymentHandler'], - // chrome-specific permissions we have. - ['midi-sysex', 'midiSysex'], - ]); - const filtered = permissions.map(permission => { - const protocolPermission = webPermissionToProtocol.get(permission); - if (!protocolPermission) - throw new Error('Unknown permission: ' + permission); - return protocolPermission; - }); - await this._client.send('Browser.grantPermissions', {origin, browserContextId: this._browserContextId || undefined, permissions: filtered}); - } - - async clearOverrides() { - await this._client.send('Browser.resetPermissions', {browserContextId: this._browserContextId || undefined}); - } -} diff --git a/src/chromium/features/workers.ts b/src/chromium/features/workers.ts deleted file mode 100644 index aa13f58da7..0000000000 --- a/src/chromium/features/workers.ts +++ /dev/null @@ -1,95 +0,0 @@ -/** - * Copyright 2018 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 { EventEmitter } from 'events'; -import { CDPSession, Connection } from '../Connection'; -import { debugError } from '../../helper'; -import { Protocol } from '../protocol'; -import { Events } from '../events'; -import * as types from '../../types'; -import * as js from '../../javascript'; -import * as console from '../../console'; -import { ExecutionContextDelegate } from '../ExecutionContext'; -import { toConsoleMessageLocation, exceptionToError } from '../protocolHelper'; - -type AddToConsoleCallback = (type: string, args: js.JSHandle[], location: console.ConsoleMessageLocation) => void; -type HandleExceptionCallback = (error: Error) => void; - -export class Workers extends EventEmitter { - private _workers = new Map(); - - constructor(client: CDPSession, addToConsole: AddToConsoleCallback, handleException: HandleExceptionCallback) { - super(); - - client.on('Target.attachedToTarget', event => { - if (event.targetInfo.type !== 'worker') - return; - const session = Connection.fromSession(client).session(event.sessionId); - const worker = new Worker(session, event.targetInfo.url, addToConsole, handleException); - this._workers.set(event.sessionId, worker); - this.emit(Events.Workers.WorkerCreated, worker); - }); - client.on('Target.detachedFromTarget', event => { - const worker = this._workers.get(event.sessionId); - if (!worker) - return; - this.emit(Events.Workers.WorkerDestroyed, worker); - this._workers.delete(event.sessionId); - }); - } - - list(): Worker[] { - return Array.from(this._workers.values()); - } -} - -export class Worker extends EventEmitter { - private _client: CDPSession; - private _url: string; - private _executionContextPromise: Promise; - private _executionContextCallback: (value?: js.ExecutionContext) => void; - - constructor(client: CDPSession, url: string, addToConsole: AddToConsoleCallback, handleException: HandleExceptionCallback) { - super(); - this._client = client; - this._url = url; - this._executionContextPromise = new Promise(x => this._executionContextCallback = x); - let jsHandleFactory: (o: Protocol.Runtime.RemoteObject) => js.JSHandle; - this._client.once('Runtime.executionContextCreated', async event => { - jsHandleFactory = remoteObject => executionContext._createHandle(remoteObject); - const executionContext = new js.ExecutionContext(new ExecutionContextDelegate(client, event.context)); - this._executionContextCallback(executionContext); - }); - // This might fail if the target is closed before we recieve all execution contexts. - this._client.send('Runtime.enable', {}).catch(debugError); - - this._client.on('Runtime.consoleAPICalled', event => addToConsole(event.type, event.args.map(jsHandleFactory), toConsoleMessageLocation(event.stackTrace))); - this._client.on('Runtime.exceptionThrown', exception => handleException(exceptionToError(exception.exceptionDetails))); - } - - url(): string { - return this._url; - } - - evaluate: types.Evaluate = async (pageFunction, ...args) => { - return (await this._executionContextPromise).evaluate(pageFunction, ...args as any); - } - - evaluateHandle: types.EvaluateHandle = async (pageFunction, ...args) => { - return (await this._executionContextPromise).evaluateHandle(pageFunction, ...args as any); - } -} diff --git a/src/chromium/protocolHelper.ts b/src/chromium/protocolHelper.ts index 32c2e71490..a5fcc9b137 100644 --- a/src/chromium/protocolHelper.ts +++ b/src/chromium/protocolHelper.ts @@ -16,7 +16,7 @@ */ import * as fs from 'fs'; import {helper, assert, debugError} from '../helper'; -import { CDPSession } from './Connection'; +import { CRSession } from './crConnection'; import { Protocol } from './protocol'; const openAsync = helper.promisify(fs.open); @@ -59,7 +59,7 @@ export function valueFromRemoteObject(remoteObject: Protocol.Runtime.RemoteObjec return remoteObject.value; } -export async function releaseObject(client: CDPSession, remoteObject: Protocol.Runtime.RemoteObject) { +export async function releaseObject(client: CRSession, remoteObject: Protocol.Runtime.RemoteObject) { if (!remoteObject.objectId) return; await client.send('Runtime.releaseObject', {objectId: remoteObject.objectId}).catch(error => { @@ -69,7 +69,7 @@ export async function releaseObject(client: CDPSession, remoteObject: Protocol.R }); } -export async function readProtocolStream(client: CDPSession, handle: string, path: string | null): Promise { +export async function readProtocolStream(client: CRSession, handle: string, path: string | null): Promise { let eof = false; let file; if (path) diff --git a/src/events.ts b/src/events.ts index 5e0f2292f0..2e4980677b 100644 --- a/src/events.ts +++ b/src/events.ts @@ -16,6 +16,10 @@ */ export const Events = { + Browser: { + Disconnected: 'disconnected' + }, + Page: { Close: 'close', Console: 'console', diff --git a/src/firefox/Browser.ts b/src/firefox/Browser.ts deleted file mode 100644 index 4119985b14..0000000000 --- a/src/firefox/Browser.ts +++ /dev/null @@ -1,265 +0,0 @@ -/** - * Copyright 2018 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 { EventEmitter } from 'events'; -import { helper, RegisteredListener, assert } from '../helper'; -import { Connection, ConnectionEvents, JugglerSessionEvents } from './Connection'; -import { Events } from './events'; -import { Events as CommonEvents } from '../events'; -import { Permissions } from './features/permissions'; -import { Page } from '../page'; -import { FrameManager } from './FrameManager'; -import * as browser from '../browser'; -import * as network from '../network'; -import { BrowserContext, BrowserContextOptions } from '../browserContext'; -import { ConnectionTransport } from '../transport'; - -export class Browser extends EventEmitter implements browser.Browser { - _connection: Connection; - _targets: Map; - private _defaultContext: BrowserContext; - private _contexts: Map; - private _eventListeners: RegisteredListener[]; - - static async create(transport: ConnectionTransport) { - const connection = new Connection(transport); - const {browserContextIds} = await connection.send('Target.getBrowserContexts'); - const browser = new Browser(connection, browserContextIds); - await connection.send('Target.enable'); - return browser; - } - - constructor(connection: Connection, browserContextIds: Array) { - super(); - this._connection = connection; - this._targets = new Map(); - - this._defaultContext = this._createBrowserContext(null, {}); - this._contexts = new Map(); - for (const browserContextId of browserContextIds) - this._contexts.set(browserContextId, this._createBrowserContext(browserContextId, {})); - - this._connection.on(ConnectionEvents.Disconnected, () => this.emit(Events.Browser.Disconnected)); - - this._eventListeners = [ - helper.addEventListener(this._connection, 'Target.targetCreated', this._onTargetCreated.bind(this)), - helper.addEventListener(this._connection, 'Target.targetDestroyed', this._onTargetDestroyed.bind(this)), - helper.addEventListener(this._connection, 'Target.targetInfoChanged', this._onTargetInfoChanged.bind(this)), - ]; - } - - disconnect() { - this._connection.dispose(); - } - - isConnected(): boolean { - return !this._connection._closed; - } - - async newContext(options: BrowserContextOptions = {}): Promise { - const {browserContextId} = await this._connection.send('Target.createBrowserContext'); - // TODO: move ignoreHTTPSErrors to browser context level. - if (options.ignoreHTTPSErrors) - await this._connection.send('Browser.setIgnoreHTTPSErrors', { enabled: true }); - const context = this._createBrowserContext(browserContextId, options); - this._contexts.set(browserContextId, context); - return context; - } - - browserContexts(): Array { - return [this._defaultContext, ...Array.from(this._contexts.values())]; - } - - defaultContext() { - return this._defaultContext; - } - - async _waitForTarget(predicate: (target: Target) => boolean, options: { timeout?: number; } = {}): Promise { - const { - timeout = 30000 - } = options; - const existingTarget = this._allTargets().find(predicate); - if (existingTarget) - return existingTarget; - let resolve: (t: Target) => void; - const targetPromise = new Promise(x => resolve = x); - this.on('targetchanged', check); - try { - if (!timeout) - return await targetPromise; - return await helper.waitWithTimeout(targetPromise, 'target', timeout); - } finally { - this.removeListener('targetchanged', check); - } - - function check(target: Target) { - if (predicate(target)) - resolve(target); - } - } - - _allTargets() { - return Array.from(this._targets.values()); - } - - async _onTargetCreated({targetId, url, browserContextId, openerId, type}) { - const context = browserContextId ? this._contexts.get(browserContextId) : this._defaultContext; - const target = new Target(this._connection, this, context, targetId, type, url, openerId); - this._targets.set(targetId, target); - if (target.opener() && target.opener()._pagePromise) { - const openerPage = await target.opener()._pagePromise; - if (openerPage.listenerCount(CommonEvents.Page.Popup)) { - const popupPage = await target.page(); - openerPage.emit(CommonEvents.Page.Popup, popupPage); - } - } - } - - _onTargetDestroyed({targetId}) { - const target = this._targets.get(targetId); - this._targets.delete(targetId); - target._didClose(); - } - - _onTargetInfoChanged({targetId, url}) { - const target = this._targets.get(targetId); - target._url = url; - } - - async close() { - helper.removeEventListeners(this._eventListeners); - await this._connection.send('Browser.close'); - } - - _createBrowserContext(browserContextId: string | null, options: BrowserContextOptions): BrowserContext { - const context = new BrowserContext({ - pages: async (): Promise => { - const targets = this._allTargets().filter(target => target.browserContext() === context && target.type() === 'page'); - const pages = await Promise.all(targets.map(target => target.page())); - return pages.filter(page => !!page); - }, - - newPage: async (): Promise => { - const {targetId} = await this._connection.send('Target.newPage', { - browserContextId: browserContextId || undefined - }); - const target = this._targets.get(targetId); - const page = await target.page(); - const session = (page._delegate as FrameManager)._session; - const promises: Promise[] = []; - if (options.viewport) - promises.push(page._delegate.setViewport(options.viewport)); - if (options.bypassCSP) - promises.push(session.send('Page.setBypassCSP', { enabled: true })); - if (options.javaScriptEnabled === false) - promises.push(session.send('Page.setJavascriptEnabled', { enabled: false })); - if (options.userAgent) - promises.push(session.send('Page.setUserAgent', { userAgent: options.userAgent })); - if (options.mediaType || options.colorScheme) - promises.push(session.send('Page.setEmulatedMedia', { type: options.mediaType, colorScheme: options.colorScheme })); - await Promise.all(promises); - return page; - }, - - close: async (): Promise => { - assert(browserContextId, 'Non-incognito profiles cannot be closed!'); - await this._connection.send('Target.removeBrowserContext', { browserContextId }); - this._contexts.delete(browserContextId); - }, - - cookies: async (): Promise => { - const { cookies } = await this._connection.send('Browser.getCookies', { browserContextId: browserContextId || undefined }); - return cookies.map(c => { - const copy: any = { ... c }; - delete copy.size; - return copy as network.NetworkCookie; - }); - }, - - clearCookies: async (): Promise => { - await this._connection.send('Browser.clearCookies', { browserContextId: browserContextId || undefined }); - }, - - setCookies: async (cookies: network.SetNetworkCookieParam[]): Promise => { - await this._connection.send('Browser.setCookies', { browserContextId: browserContextId || undefined, cookies }); - }, - }, options); - (context as any).permissions = new Permissions(this._connection, browserContextId); - return context; - } -} - -export class Target { - _pagePromise?: Promise; - private _frameManager: FrameManager | null = null; - private _browser: Browser; - _context: BrowserContext; - private _connection: Connection; - private _targetId: string; - private _type: 'page' | 'browser'; - _url: string; - private _openerId: string; - - constructor(connection: any, browser: Browser, context: BrowserContext, targetId: string, type: 'page' | 'browser', url: string, openerId: string | undefined) { - this._browser = browser; - this._context = context; - this._connection = connection; - this._targetId = targetId; - this._type = type; - this._url = url; - this._openerId = openerId; - } - - _didClose() { - if (this._frameManager) - this._frameManager.didClose(); - } - - opener(): Target | null { - return this._openerId ? this._browser._targets.get(this._openerId) : null; - } - - type(): 'page' | 'browser' { - return this._type; - } - - url() { - return this._url; - } - - browserContext(): BrowserContext { - return this._context; - } - - page(): Promise { - if (this._type === 'page' && !this._pagePromise) { - this._pagePromise = new Promise(async f => { - const session = await this._connection.createSession(this._targetId); - this._frameManager = new FrameManager(session, this._context); - const page = this._frameManager._page; - session.once(JugglerSessionEvents.Disconnected, () => page._didDisconnect()); - await this._frameManager._initialize(); - f(page); - }); - } - return this._pagePromise; - } - - browser() { - return this._browser; - } -} diff --git a/src/firefox/Connection.ts b/src/firefox/Connection.ts deleted file mode 100644 index 9a609be27a..0000000000 --- a/src/firefox/Connection.ts +++ /dev/null @@ -1,204 +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 {assert} from '../helper'; -import {EventEmitter} from 'events'; -import * as debug from 'debug'; -import { ConnectionTransport } from '../transport'; -import { Protocol } from './protocol'; -const debugProtocol = debug('playwright:protocol'); - -export const ConnectionEvents = { - Disconnected: Symbol('Disconnected'), -}; - -export class Connection extends EventEmitter { - private _lastId: number; - private _callbacks: Map; - private _transport: ConnectionTransport; - private _sessions: Map; - _closed: boolean; - - constructor(transport: ConnectionTransport) { - super(); - this._transport = transport; - this._lastId = 0; - this._callbacks = new Map(); - - this._transport.onmessage = this._onMessage.bind(this); - this._transport.onclose = this._onClose.bind(this); - this._sessions = new Map(); - this._closed = false; - } - - static fromSession(session: JugglerSession): Connection { - return session._connection; - } - - session(sessionId: string): JugglerSession | null { - return this._sessions.get(sessionId) || null; - } - - send(method: string, params: object | undefined = {}): Promise { - const id = this._rawSend({method, params}); - return new Promise((resolve, reject) => { - this._callbacks.set(id, {resolve, reject, error: new Error(), method}); - }); - } - - _rawSend(message: any): number { - const id = ++this._lastId; - message = JSON.stringify(Object.assign({}, message, {id})); - debugProtocol('SEND ► ' + message); - this._transport.send(message); - return id; - } - - async _onMessage(message: string) { - debugProtocol('◀ RECV ' + message); - const object = JSON.parse(message); - if (object.method === 'Target.attachedToTarget') { - const sessionId = object.params.sessionId; - const session = new JugglerSession(this, object.params.targetInfo.type, sessionId); - this._sessions.set(sessionId, session); - } else if (object.method === 'Browser.detachedFromTarget') { - const session = this._sessions.get(object.params.sessionId); - if (session) { - session._onClosed(); - this._sessions.delete(object.params.sessionId); - } - } - if (object.sessionId) { - const session = this._sessions.get(object.sessionId); - if (session) - session._onMessage(object); - } else if (object.id) { - const callback = this._callbacks.get(object.id); - // Callbacks could be all rejected if someone has called `.dispose()`. - if (callback) { - this._callbacks.delete(object.id); - if (object.error) - callback.reject(createProtocolError(callback.error, callback.method, object)); - else - callback.resolve(object.result); - } - } else { - Promise.resolve().then(() => this.emit(object.method, object.params)); - } - } - - _onClose() { - if (this._closed) - return; - this._closed = true; - this._transport.onmessage = null; - this._transport.onclose = null; - for (const callback of this._callbacks.values()) - callback.reject(rewriteError(callback.error, `Protocol error (${callback.method}): Target closed.`)); - this._callbacks.clear(); - for (const session of this._sessions.values()) - session._onClosed(); - this._sessions.clear(); - Promise.resolve().then(() => this.emit(ConnectionEvents.Disconnected)); - } - - dispose() { - this._onClose(); - this._transport.close(); - } - - async createSession(targetId: string): Promise { - const {sessionId} = await this.send('Target.attachToTarget', {targetId}); - return this._sessions.get(sessionId); - } -} - -export const JugglerSessionEvents = { - Disconnected: Symbol('Disconnected') -}; - -export class JugglerSession extends EventEmitter { - _connection: Connection; - private _callbacks: Map; - private _targetType: string; - private _sessionId: string; - on: (event: T, listener: (payload: T extends symbol ? any : Protocol.Events[T extends keyof Protocol.Events ? T : never]) => void) => this; - addListener: (event: T, listener: (payload: T extends symbol ? any : Protocol.Events[T extends keyof Protocol.Events ? T : never]) => void) => this; - off: (event: T, listener: (payload: T extends symbol ? any : Protocol.Events[T extends keyof Protocol.Events ? T : never]) => void) => this; - removeListener: (event: T, listener: (payload: T extends symbol ? any : Protocol.Events[T extends keyof Protocol.Events ? T : never]) => void) => this; - once: (event: T, listener: (payload: T extends symbol ? any : Protocol.Events[T extends keyof Protocol.Events ? T : never]) => void) => this; - - constructor(connection: Connection, targetType: string, sessionId: string) { - super(); - this._callbacks = new Map(); - this._connection = connection; - this._targetType = targetType; - this._sessionId = sessionId; - } - - send( - method: T, - params?: Protocol.CommandParameters[T] - ): Promise { - if (!this._connection) - return Promise.reject(new Error(`Protocol error (${method}): Session closed. Most likely the ${this._targetType} has been closed.`)); - const id = this._connection._rawSend({sessionId: this._sessionId, method, params}); - return new Promise((resolve, reject) => { - this._callbacks.set(id, {resolve, reject, error: new Error(), method}); - }); - } - - _onMessage(object: { id?: number; method: string; params: object; error: { message: string; data: any; }; result?: any; }) { - if (object.id && this._callbacks.has(object.id)) { - const callback = this._callbacks.get(object.id); - this._callbacks.delete(object.id); - if (object.error) - callback.reject(createProtocolError(callback.error, callback.method, object)); - else - callback.resolve(object.result); - } else { - assert(!object.id); - Promise.resolve().then(() => this.emit(object.method, object.params)); - } - } - - async detach() { - if (!this._connection) - throw new Error(`Session already detached. Most likely the ${this._targetType} has been closed.`); - await this._connection.send('Target.detachFromTarget', {sessionId: this._sessionId}); - } - - _onClosed() { - for (const callback of this._callbacks.values()) - callback.reject(rewriteError(callback.error, `Protocol error (${callback.method}): Target closed.`)); - this._callbacks.clear(); - this._connection = null; - Promise.resolve().then(() => this.emit(JugglerSessionEvents.Disconnected)); - } -} - -function createProtocolError(error: Error, method: string, object: { error: { message: string; data: any; }; }): Error { - let message = `Protocol error (${method}): ${object.error.message}`; - if ('data' in object.error) - message += ` ${object.error.data}`; - return rewriteError(error, message); -} - -function rewriteError(error: Error, message: string): Error { - error.message = message; - return error; -} diff --git a/src/firefox/ExecutionContext.ts b/src/firefox/ExecutionContext.ts deleted file mode 100644 index 4e8a459ea8..0000000000 --- a/src/firefox/ExecutionContext.ts +++ /dev/null @@ -1,183 +0,0 @@ -/** - * Copyright 2019 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 {helper, debugError} from '../helper'; -import * as js from '../javascript'; -import { JugglerSession } from './Connection'; -import { Protocol } from './protocol'; - -export class ExecutionContextDelegate implements js.ExecutionContextDelegate { - _session: JugglerSession; - _executionContextId: string; - - constructor(session: JugglerSession, executionContextId: string) { - this._session = session; - this._executionContextId = executionContextId; - } - - async evaluate(context: js.ExecutionContext, returnByValue: boolean, pageFunction: Function | string, ...args: any[]): Promise { - if (returnByValue) { - try { - const handle = await this.evaluate(context, false /* returnByValue */, pageFunction, ...args as any); - const result = await handle.jsonValue(); - await handle.dispose(); - return result; - } catch (e) { - if (e.message.includes('cyclic object value') || e.message.includes('Object is not serializable')) - return undefined; - throw e; - } - } - - if (helper.isString(pageFunction)) { - const payload = await this._session.send('Runtime.evaluate', { - expression: pageFunction.trim(), - executionContextId: this._executionContextId, - }).catch(rewriteError); - checkException(payload.exceptionDetails); - return context._createHandle(payload.result); - } - if (typeof pageFunction !== 'function') - throw new Error(`Expected to get |string| or |function| as the first argument, but got "${pageFunction}" instead.`); - - let functionText = pageFunction.toString(); - try { - new Function('(' + functionText + ')'); - } catch (e1) { - // This means we might have a function shorthand. Try another - // time prefixing 'function '. - if (functionText.startsWith('async ')) - functionText = 'async function ' + functionText.substring('async '.length); - else - functionText = 'function ' + functionText; - try { - new Function('(' + functionText + ')'); - } catch (e2) { - // We tried hard to serialize, but there's a weird beast here. - throw new Error('Passed function is not well-serializable!'); - } - } - const protocolArgs = args.map(arg => { - if (arg instanceof js.JSHandle) { - if (arg._context !== context) - throw new Error('JSHandles can be evaluated only in the context they were created!'); - if (arg._disposed) - throw new Error('JSHandle is disposed!'); - return this._toCallArgument(arg._remoteObject); - } - if (Object.is(arg, Infinity)) - return {unserializableValue: 'Infinity'}; - if (Object.is(arg, -Infinity)) - return {unserializableValue: '-Infinity'}; - if (Object.is(arg, -0)) - return {unserializableValue: '-0'}; - if (Object.is(arg, NaN)) - return {unserializableValue: 'NaN'}; - return {value: arg}; - }); - let callFunctionPromise; - try { - callFunctionPromise = this._session.send('Runtime.callFunction', { - functionDeclaration: functionText, - args: protocolArgs, - executionContextId: this._executionContextId - }); - } catch (err) { - if (err instanceof TypeError && err.message.startsWith('Converting circular structure to JSON')) - err.message += ' Are you passing a nested JSHandle?'; - throw err; - } - const payload = await callFunctionPromise.catch(rewriteError); - checkException(payload.exceptionDetails); - return context._createHandle(payload.result); - - function rewriteError(error) : never { - if (error.message.includes('Failed to find execution context with id') || error.message.includes('Execution context was destroyed!')) - throw new Error('Execution context was destroyed, most likely because of a navigation.'); - throw error; - } - } - - async getProperties(handle: js.JSHandle): Promise> { - const response = await this._session.send('Runtime.getObjectProperties', { - executionContextId: this._executionContextId, - objectId: handle._remoteObject.objectId, - }); - const result = new Map(); - for (const property of response.properties) - result.set(property.name, handle._context._createHandle(property.value)); - return result; - } - - async releaseHandle(handle: js.JSHandle): Promise { - if (!handle._remoteObject.objectId) - return; - await this._session.send('Runtime.disposeObject', { - executionContextId: this._executionContextId, - objectId: handle._remoteObject.objectId, - }).catch(error => { - // Exceptions might happen in case of a page been navigated or closed. - // Swallow these since they are harmless and we don't leak anything in this case. - debugError(error); - }); - } - - async handleJSONValue(handle: js.JSHandle): Promise { - const payload = handle._remoteObject; - if (!payload.objectId) - return deserializeValue(payload); - const simpleValue = await this._session.send('Runtime.callFunction', { - executionContextId: this._executionContextId, - returnByValue: true, - functionDeclaration: (e => e).toString(), - args: [this._toCallArgument(payload)], - }); - return deserializeValue(simpleValue.result); - } - - handleToString(handle: js.JSHandle, includeType: boolean): string { - const payload = handle._remoteObject; - if (payload.objectId) - return 'JSHandle@' + (payload.subtype || payload.type); - return (includeType ? 'JSHandle:' : '') + deserializeValue(payload); - } - - private _toCallArgument(payload: any): any { - return { value: payload.value, unserializableValue: payload.unserializableValue, objectId: payload.objectId }; - } -} - -function checkException(exceptionDetails?: any) { - if (exceptionDetails) { - if (exceptionDetails.value) - throw new Error('Evaluation failed: ' + JSON.stringify(exceptionDetails.value)); - else - throw new Error('Evaluation failed: ' + exceptionDetails.text + '\n' + exceptionDetails.stack); - } -} - -export function deserializeValue({unserializableValue, value}: Protocol.RemoteObject) { - if (unserializableValue === 'Infinity') - return Infinity; - if (unserializableValue === '-Infinity') - return -Infinity; - if (unserializableValue === '-0') - return -0; - if (unserializableValue === 'NaN') - return NaN; - return value; -} diff --git a/src/firefox/FrameManager.ts b/src/firefox/FrameManager.ts deleted file mode 100644 index 33d5fe9726..0000000000 --- a/src/firefox/FrameManager.ts +++ /dev/null @@ -1,342 +0,0 @@ -/** - * Copyright 2019 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 frames from '../frames'; -import { assert, helper, RegisteredListener, debugError } from '../helper'; -import * as dom from '../dom'; -import { JugglerSession } from './Connection'; -import { ExecutionContextDelegate } from './ExecutionContext'; -import { Page, PageDelegate } from '../page'; -import { NetworkManager } from './NetworkManager'; -import { Events } from '../events'; -import * as dialog from '../dialog'; -import { Protocol } from './protocol'; -import * as input from '../input'; -import { RawMouseImpl, RawKeyboardImpl } from './Input'; -import { BrowserContext } from '../browserContext'; -import { Interception } from './features/interception'; -import { Accessibility } from './features/accessibility'; -import * as network from '../network'; -import * as types from '../types'; - -export class FrameManager implements PageDelegate { - readonly rawMouse: RawMouseImpl; - readonly rawKeyboard: RawKeyboardImpl; - readonly _session: JugglerSession; - readonly _page: Page; - private readonly _networkManager: NetworkManager; - private readonly _contextIdToContext: Map; - private _eventListeners: RegisteredListener[]; - - constructor(session: JugglerSession, browserContext: BrowserContext) { - this._session = session; - this.rawKeyboard = new RawKeyboardImpl(session); - this.rawMouse = new RawMouseImpl(session); - this._contextIdToContext = new Map(); - this._page = new Page(this, browserContext); - this._networkManager = new NetworkManager(session, this._page); - this._eventListeners = [ - helper.addEventListener(this._session, 'Page.eventFired', this._onEventFired.bind(this)), - helper.addEventListener(this._session, 'Page.frameAttached', this._onFrameAttached.bind(this)), - helper.addEventListener(this._session, 'Page.frameDetached', this._onFrameDetached.bind(this)), - helper.addEventListener(this._session, 'Page.navigationAborted', this._onNavigationAborted.bind(this)), - helper.addEventListener(this._session, 'Page.navigationCommitted', this._onNavigationCommitted.bind(this)), - helper.addEventListener(this._session, 'Page.navigationStarted', this._onNavigationStarted.bind(this)), - helper.addEventListener(this._session, 'Page.sameDocumentNavigation', this._onSameDocumentNavigation.bind(this)), - helper.addEventListener(this._session, 'Runtime.executionContextCreated', this._onExecutionContextCreated.bind(this)), - helper.addEventListener(this._session, 'Runtime.executionContextDestroyed', this._onExecutionContextDestroyed.bind(this)), - helper.addEventListener(this._session, 'Page.uncaughtError', this._onUncaughtError.bind(this)), - helper.addEventListener(this._session, 'Runtime.console', this._onConsole.bind(this)), - helper.addEventListener(this._session, 'Page.dialogOpened', this._onDialogOpened.bind(this)), - helper.addEventListener(this._session, 'Page.bindingCalled', this._onBindingCalled.bind(this)), - helper.addEventListener(this._session, 'Page.fileChooserOpened', this._onFileChooserOpened.bind(this)), - ]; - (this._page as any).interception = new Interception(this._networkManager); - (this._page as any).accessibility = new Accessibility(session); - } - - async _initialize() { - await Promise.all([ - this._session.send('Runtime.enable'), - this._session.send('Network.enable'), - this._session.send('Page.enable'), - this._session.send('Page.setInterceptFileChooserDialog', { enabled: true }) - ]); - } - - _onExecutionContextCreated({executionContextId, auxData}) { - const frame = this._page._frameManager.frame(auxData ? auxData.frameId : null); - if (!frame) - return; - const delegate = new ExecutionContextDelegate(this._session, executionContextId); - const context = new dom.FrameExecutionContext(delegate, frame); - frame._contextCreated('main', context); - frame._contextCreated('utility', context); - this._contextIdToContext.set(executionContextId, context); - } - - _onExecutionContextDestroyed({executionContextId}) { - const context = this._contextIdToContext.get(executionContextId); - if (!context) - return; - this._contextIdToContext.delete(executionContextId); - context.frame._contextDestroyed(context as dom.FrameExecutionContext); - } - - _onNavigationStarted() { - } - - _onNavigationAborted(params: Protocol.Page.navigationAbortedPayload) { - const frame = this._page._frameManager.frame(params.frameId); - for (const watcher of this._page._frameManager._lifecycleWatchers) - watcher._onAbortedNewDocumentNavigation(frame, params.navigationId, params.errorText); - } - - _onNavigationCommitted(params: Protocol.Page.navigationCommittedPayload) { - this._page._frameManager.frameCommittedNewDocumentNavigation(params.frameId, params.url, params.name || '', params.navigationId || '', false); - } - - _onSameDocumentNavigation(params: Protocol.Page.sameDocumentNavigationPayload) { - this._page._frameManager.frameCommittedSameDocumentNavigation(params.frameId, params.url); - } - - _onFrameAttached(params: Protocol.Page.frameAttachedPayload) { - this._page._frameManager.frameAttached(params.frameId, params.parentFrameId); - } - - _onFrameDetached(params: Protocol.Page.frameDetachedPayload) { - this._page._frameManager.frameDetached(params.frameId); - } - - _onEventFired({frameId, name}) { - if (name === 'load') - this._page._frameManager.frameLifecycleEvent(frameId, 'load'); - if (name === 'DOMContentLoaded') - this._page._frameManager.frameLifecycleEvent(frameId, 'domcontentloaded'); - } - - _onUncaughtError(params: Protocol.Page.uncaughtErrorPayload) { - const error = new Error(params.message); - error.stack = params.stack; - this._page.emit(Events.Page.PageError, error); - } - - _onConsole({type, args, executionContextId, location}) { - const context = this._contextIdToContext.get(executionContextId); - this._page._addConsoleMessage(type, args.map(arg => context._createHandle(arg)), location); - } - - _onDialogOpened(params: Protocol.Page.dialogOpenedPayload) { - this._page.emit(Events.Page.Dialog, new dialog.Dialog( - params.type as dialog.DialogType, - params.message, - async (accept: boolean, promptText?: string) => { - await this._session.send('Page.handleDialog', { dialogId: params.dialogId, accept, promptText }).catch(debugError); - }, - params.defaultValue)); - } - - _onBindingCalled(event: Protocol.Page.bindingCalledPayload) { - const context = this._contextIdToContext.get(event.executionContextId); - this._page._onBindingCalled(event.payload, context); - } - - async _onFileChooserOpened({executionContextId, element}) { - const context = this._contextIdToContext.get(executionContextId); - const handle = context._createHandle(element).asElement()!; - this._page._onFileChooserOpened(handle); - } - - async exposeBinding(name: string, bindingFunction: string): Promise { - await this._session.send('Page.addBinding', {name: name}); - await this._session.send('Page.addScriptToEvaluateOnNewDocument', {script: bindingFunction}); - await Promise.all(this._page.frames().map(frame => frame.evaluate(bindingFunction).catch(debugError))); - } - - didClose() { - helper.removeEventListeners(this._eventListeners); - this._networkManager.dispose(); - this._page._didClose(); - } - - async navigateFrame(frame: frames.Frame, url: string, referer: string | undefined): Promise { - const response = await this._session.send('Page.navigate', { url, referer, frameId: frame._id }); - return { newDocumentId: response.navigationId, isSameDocument: !response.navigationId }; - } - - needsLifecycleResetOnSetContent(): boolean { - return true; - } - - async setExtraHTTPHeaders(headers: network.Headers): Promise { - const array = []; - for (const [name, value] of Object.entries(headers)) - array.push({ name, value }); - await this._session.send('Network.setExtraHTTPHeaders', { headers: array }); - } - - async setViewport(viewport: types.Viewport): Promise { - const { - width, - height, - isMobile = false, - deviceScaleFactor = 1, - hasTouch = false, - isLandscape = false, - } = viewport; - await this._session.send('Page.setViewport', { - viewport: { width, height, isMobile, deviceScaleFactor, hasTouch, isLandscape }, - }); - } - - async setEmulateMedia(mediaType: input.MediaType | null, mediaColorScheme: input.ColorScheme | null): Promise { - await this._session.send('Page.setEmulatedMedia', { - type: mediaType === null ? undefined : mediaType, - colorScheme: mediaColorScheme === null ? undefined : mediaColorScheme - }); - } - - async setCacheEnabled(enabled: boolean): Promise { - await this._session.send('Page.setCacheDisabled', {cacheDisabled: !enabled}); - } - - async reload(): Promise { - await this._session.send('Page.reload', { frameId: this._page.mainFrame()._id }); - } - - async goBack(): Promise { - const { navigationId } = await this._session.send('Page.goBack', { frameId: this._page.mainFrame()._id }); - return navigationId !== null; - } - - async goForward(): Promise { - const { navigationId } = await this._session.send('Page.goForward', { frameId: this._page.mainFrame()._id }); - return navigationId !== null; - } - - async evaluateOnNewDocument(source: string): Promise { - await this._session.send('Page.addScriptToEvaluateOnNewDocument', { script: source }); - } - - async closePage(runBeforeUnload: boolean): Promise { - await this._session.send('Page.close', { runBeforeUnload }); - } - - getBoundingBoxForScreenshot(handle: dom.ElementHandle): Promise { - const frameId = handle._context.frame._id; - return this._session.send('Page.getBoundingBox', { - frameId, - objectId: handle._remoteObject.objectId, - }); - } - - canScreenshotOutsideViewport(): boolean { - return true; - } - - async setBackgroundColor(color?: { r: number; g: number; b: number; a: number; }): Promise { - if (color) - throw new Error('Not implemented'); - } - - async takeScreenshot(format: 'png' | 'jpeg', options: types.ScreenshotOptions): Promise { - const { data } = await this._session.send('Page.screenshot', { - mimeType: ('image/' + format) as ('image/png' | 'image/jpeg'), - fullPage: options.fullPage, - clip: options.clip, - }); - return Buffer.from(data, 'base64'); - } - - async resetViewport(): Promise { - await this._session.send('Page.setViewport', { viewport: null }); - } - - async getContentFrame(handle: dom.ElementHandle): Promise { - const { frameId } = await this._session.send('Page.contentFrame', { - frameId: handle._context.frame._id, - objectId: toRemoteObject(handle).objectId, - }); - if (!frameId) - return null; - return this._page._frameManager.frame(frameId); - } - - async getOwnerFrame(handle: dom.ElementHandle): Promise { - return handle._context.frame; - } - - isElementHandle(remoteObject: any): boolean { - return remoteObject.subtype === 'node'; - } - - async getBoundingBox(handle: dom.ElementHandle): Promise { - const quads = await this.getContentQuads(handle); - if (!quads || !quads.length) - return null; - let minX = Infinity; - let maxX = -Infinity; - let minY = Infinity; - let maxY = -Infinity; - for (const quad of quads) { - for (const point of quad) { - minX = Math.min(minX, point.x); - maxX = Math.max(maxX, point.x); - minY = Math.min(minY, point.y); - maxY = Math.max(maxY, point.y); - } - } - return { x: minX, y: minY, width: maxX - minX, height: maxY - minY }; - } - - async getContentQuads(handle: dom.ElementHandle): Promise { - const result = await this._session.send('Page.getContentQuads', { - frameId: handle._context.frame._id, - objectId: toRemoteObject(handle).objectId, - }).catch(debugError); - if (!result) - return null; - return result.quads.map(quad => [ quad.p1, quad.p2, quad.p3, quad.p4 ]); - } - - async layoutViewport(): Promise<{ width: number, height: number }> { - return this._page.evaluate(() => ({ width: innerWidth, height: innerHeight })); - } - - async setInputFiles(handle: dom.ElementHandle, files: input.FilePayload[]): Promise { - await handle.evaluate(input.setFileInputFunction, files); - } - - async adoptElementHandle(handle: dom.ElementHandle, to: dom.FrameExecutionContext): Promise> { - assert(false, 'Multiple isolated worlds are not implemented'); - return handle; - } -} - -export function normalizeWaitUntil(waitUntil: frames.LifecycleEvent | frames.LifecycleEvent[]): frames.LifecycleEvent[] { - if (!Array.isArray(waitUntil)) - waitUntil = [waitUntil]; - for (const condition of waitUntil) { - if (condition !== 'load' && condition !== 'domcontentloaded') - throw new Error('Unknown waitUntil condition: ' + condition); - } - return waitUntil; -} - -function toRemoteObject(handle: dom.ElementHandle): Protocol.RemoteObject { - return handle._remoteObject; -} diff --git a/src/firefox/Input.ts b/src/firefox/Input.ts deleted file mode 100644 index 65aad5295f..0000000000 --- a/src/firefox/Input.ts +++ /dev/null @@ -1,137 +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 { JugglerSession } from './Connection'; -import * as input from '../input'; - -function toModifiersMask(modifiers: Set): number { - let mask = 0; - if (modifiers.has('Alt')) - mask |= 1; - if (modifiers.has('Control')) - mask |= 2; - if (modifiers.has('Shift')) - mask |= 4; - if (modifiers.has('Meta')) - mask |= 8; - return mask; -} - -function toButtonNumber(button: input.Button): number { - if (button === 'left') - return 0; - if (button === 'middle') - return 1; - if (button === 'right') - return 2; -} - -function toButtonsMask(buttons: Set): number { - let mask = 0; - if (buttons.has('left')) - mask |= 1; - if (buttons.has('right')) - mask |= 2; - if (buttons.has('middle')) - mask |= 4; - return mask; -} - -export class RawKeyboardImpl implements input.RawKeyboard { - private _client: JugglerSession; - - constructor(client: JugglerSession) { - this._client = client; - } - - async keydown(modifiers: Set, code: string, keyCode: number, keyCodeWithoutLocation: number, key: string, location: number, autoRepeat: boolean, text: string | undefined): Promise { - if (code === 'MetaLeft') - code = 'OSLeft'; - if (code === 'MetaRight') - code = 'OSRight'; - await this._client.send('Page.dispatchKeyEvent', { - type: 'keydown', - keyCode: keyCodeWithoutLocation, - code, - key, - repeat: autoRepeat, - location - }); - } - - async keyup(modifiers: Set, code: string, keyCode: number, keyCodeWithoutLocation: number, key: string, location: number): Promise { - if (code === 'MetaLeft') - code = 'OSLeft'; - if (code === 'MetaRight') - code = 'OSRight'; - await this._client.send('Page.dispatchKeyEvent', { - type: 'keyup', - key, - keyCode: keyCodeWithoutLocation, - code, - location, - repeat: false - }); - } - - async sendText(text: string): Promise { - await this._client.send('Page.insertText', { text }); - } -} - -export class RawMouseImpl implements input.RawMouse { - private _client: JugglerSession; - - constructor(client: JugglerSession) { - this._client = client; - } - - async move(x: number, y: number, button: input.Button | 'none', buttons: Set, modifiers: Set): Promise { - await this._client.send('Page.dispatchMouseEvent', { - type: 'mousemove', - button: 0, - buttons: toButtonsMask(buttons), - x, - y, - modifiers: toModifiersMask(modifiers) - }); - } - - async down(x: number, y: number, button: input.Button, buttons: Set, modifiers: Set, clickCount: number): Promise { - await this._client.send('Page.dispatchMouseEvent', { - type: 'mousedown', - button: toButtonNumber(button), - buttons: toButtonsMask(buttons), - x, - y, - modifiers: toModifiersMask(modifiers), - clickCount - }); - } - - async up(x: number, y: number, button: input.Button, buttons: Set, modifiers: Set, clickCount: number): Promise { - await this._client.send('Page.dispatchMouseEvent', { - type: 'mouseup', - button: toButtonNumber(button), - buttons: toButtonsMask(buttons), - x, - y, - modifiers: toModifiersMask(modifiers), - clickCount - }); - } -} diff --git a/src/firefox/Launcher.ts b/src/firefox/Launcher.ts deleted file mode 100644 index 44b38d7330..0000000000 --- a/src/firefox/Launcher.ts +++ /dev/null @@ -1,400 +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 os from 'os'; -import * as path from 'path'; -import { Browser } from './Browser'; -import { BrowserFetcher, BrowserFetcherOptions } from '../browserFetcher'; -import * as fs from 'fs'; -import * as util from 'util'; -import { assert } from '../helper'; -import { TimeoutError } from '../errors'; -import { WebSocketTransport, SlowMoTransport } from '../transport'; -import { launchProcess, waitForLine } from '../processLauncher'; -import { BrowserServer } from '../browser'; - -const mkdtempAsync = util.promisify(fs.mkdtemp); -const writeFileAsync = util.promisify(fs.writeFile); - -const DEFAULT_ARGS = [ - '-no-remote', -]; - -export class Launcher { - private _projectRoot: string; - private _preferredRevision: string; - constructor(projectRoot, preferredRevision) { - this._projectRoot = projectRoot; - this._preferredRevision = 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 launch(options: any = {}): Promise> { - const { - ignoreDefaultArgs = false, - args = [], - dumpio = false, - executablePath = null, - env = process.env, - handleSIGHUP = true, - handleSIGINT = true, - handleSIGTERM = true, - slowMo = 0, - timeout = 30000, - } = options; - - const firefoxArguments = []; - if (!ignoreDefaultArgs) - firefoxArguments.push(...this.defaultArgs(options)); - else if (Array.isArray(ignoreDefaultArgs)) - firefoxArguments.push(...this.defaultArgs(options).filter(arg => !ignoreDefaultArgs.includes(arg))); - else - firefoxArguments.push(...args); - - if (!firefoxArguments.includes('-juggler')) - firefoxArguments.unshift('-juggler', '0'); - - let temporaryProfileDir = null; - if (!firefoxArguments.includes('-profile') && !firefoxArguments.includes('--profile')) { - temporaryProfileDir = await createProfile(); - firefoxArguments.unshift(`-profile`, temporaryProfileDir); - } - - let firefoxExecutable = executablePath; - if (!firefoxExecutable) { - const {missingText, executablePath} = this._resolveExecutablePath(); - if (missingText) - throw new Error(missingText); - firefoxExecutable = executablePath; - } - const launchedProcess = await launchProcess({ - executablePath: firefoxExecutable, - args: firefoxArguments, - env: os.platform() === 'linux' ? { - ...env, - // On linux Juggler ships the libstdc++ it was linked against. - LD_LIBRARY_PATH: `${path.dirname(firefoxExecutable)}:${process.env.LD_LIBRARY_PATH}`, - } : env, - handleSIGINT, - handleSIGTERM, - handleSIGHUP, - dumpio, - pipe: false, - tempDir: temporaryProfileDir - }, () => { - if (temporaryProfileDir || !browser) - return Promise.reject(); - browser.close(); - }); - - let browser: Browser | 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 WebSocketTransport.create(url); - browser = await Browser.create(SlowMoTransport.wrap(transport, slowMo)); - await browser._waitForTarget(t => t.type() === 'page'); - return new BrowserServer(browser, launchedProcess, url); - } catch (e) { - if (browser) - await browser.close(); - throw e; - } - } - - executablePath(): string { - return this._resolveExecutablePath().executablePath; - } - - _resolveExecutablePath() { - const browserFetcher = createBrowserFetcher(this._projectRoot); - const revisionInfo = browserFetcher.revisionInfo(this._preferredRevision); - const missingText = !revisionInfo.local ? `Firefox revision is not downloaded. Run "npm install" or "yarn install"` : null; - 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 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 DUMMY_UMA_SERVER = 'dummy.test'; -const DEFAULT_PREFERENCES = { - // Make sure Shield doesn't hit the network. - 'app.normandy.api_url': '', - // Disable Firefox old build background check - 'app.update.checkInstallTime': false, - // Disable automatically upgrading Firefox - 'app.update.disabledForTesting': true, - - // Increase the APZ content response timeout to 1 minute - 'apz.content_response_timeout': 60000, - - // Prevent various error message on the console - // jest-puppeteer asserts that no error message is emitted by the console - 'browser.contentblocking.features.standard': '-tp,tpPrivate,cookieBehavior0,-cm,-fp', - - - // Enable the dump function: which sends messages to the system - // console - // https://bugzilla.mozilla.org/show_bug.cgi?id=1543115 - 'browser.dom.window.dump.enabled': true, - // Disable topstories - 'browser.newtabpage.activity-stream.feeds.section.topstories': false, - // Always display a blank page - 'browser.newtabpage.enabled': false, - // Background thumbnails in particular cause grief: and disabling - // thumbnails in general cannot hurt - 'browser.pagethumbnails.capturing_disabled': true, - - // Disable safebrowsing components. - 'browser.safebrowsing.blockedURIs.enabled': false, - 'browser.safebrowsing.downloads.enabled': false, - 'browser.safebrowsing.malware.enabled': false, - 'browser.safebrowsing.passwords.enabled': false, - 'browser.safebrowsing.phishing.enabled': false, - - // Disable updates to search engines. - 'browser.search.update': false, - // Do not restore the last open set of tabs if the browser has crashed - 'browser.sessionstore.resume_from_crash': false, - // Skip check for default browser on startup - 'browser.shell.checkDefaultBrowser': false, - - // Disable newtabpage - 'browser.startup.homepage': 'about:blank', - // Do not redirect user when a milstone upgrade of Firefox is detected - 'browser.startup.homepage_override.mstone': 'ignore', - // Start with a blank page about:blank - 'browser.startup.page': 0, - - // Do not allow background tabs to be zombified on Android: otherwise for - // tests that open additional tabs: the test harness tab itself might get - // unloaded - 'browser.tabs.disableBackgroundZombification': false, - // Do not warn when closing all other open tabs - 'browser.tabs.warnOnCloseOtherTabs': false, - // Do not warn when multiple tabs will be opened - 'browser.tabs.warnOnOpen': false, - - // Disable the UI tour. - 'browser.uitour.enabled': false, - // Turn off search suggestions in the location bar so as not to trigger - // network connections. - 'browser.urlbar.suggest.searches': false, - // Disable first run splash page on Windows 10 - 'browser.usedOnWindows10.introURL': '', - // Do not warn on quitting Firefox - 'browser.warnOnQuit': false, - - // Do not show datareporting policy notifications which can - // interfere with tests - 'datareporting.healthreport.about.reportUrl': `http://${DUMMY_UMA_SERVER}/dummy/abouthealthreport/`, - 'datareporting.healthreport.documentServerURI': `http://${DUMMY_UMA_SERVER}/dummy/healthreport/`, - 'datareporting.healthreport.logging.consoleEnabled': false, - 'datareporting.healthreport.service.enabled': false, - 'datareporting.healthreport.service.firstRun': false, - 'datareporting.healthreport.uploadEnabled': false, - 'datareporting.policy.dataSubmissionEnabled': false, - 'datareporting.policy.dataSubmissionPolicyAccepted': false, - 'datareporting.policy.dataSubmissionPolicyBypassNotification': true, - - // DevTools JSONViewer sometimes fails to load dependencies with its require.js. - // This doesn't affect Puppeteer but spams console (Bug 1424372) - 'devtools.jsonview.enabled': false, - - // Disable popup-blocker - 'dom.disable_open_during_load': false, - - // Enable the support for File object creation in the content process - // Required for |Page.setFileInputFiles| protocol method. - 'dom.file.createInChild': true, - - // Disable the ProcessHangMonitor - 'dom.ipc.reportProcessHangs': false, - - // Disable slow script dialogues - 'dom.max_chrome_script_run_time': 0, - 'dom.max_script_run_time': 0, - - // Only load extensions from the application and user profile - // AddonManager.SCOPE_PROFILE + AddonManager.SCOPE_APPLICATION - 'extensions.autoDisableScopes': 0, - 'extensions.enabledScopes': 5, - - // Disable metadata caching for installed add-ons by default - 'extensions.getAddons.cache.enabled': false, - - // Disable installing any distribution extensions or add-ons. - 'extensions.installDistroAddons': false, - - // Disabled screenshots extension - 'extensions.screenshots.disabled': true, - - // Turn off extension updates so they do not bother tests - 'extensions.update.enabled': false, - - // Turn off extension updates so they do not bother tests - 'extensions.update.notifyUser': false, - - // Make sure opening about:addons will not hit the network - 'extensions.webservice.discoverURL': `http://${DUMMY_UMA_SERVER}/dummy/discoveryURL`, - - // Allow the application to have focus even it runs in the background - 'focusmanager.testmode': true, - // Disable useragent updates - 'general.useragent.updates.enabled': false, - // Always use network provider for geolocation tests so we bypass the - // macOS dialog raised by the corelocation provider - 'geo.provider.testing': true, - // Do not scan Wifi - 'geo.wifi.scan': false, - // No hang monitor - 'hangmonitor.timeout': 0, - // Show chrome errors and warnings in the error console - 'javascript.options.showInConsole': true, - - // Disable download and usage of OpenH264: and Widevine plugins - 'media.gmp-manager.updateEnabled': false, - // Prevent various error message on the console - // jest-puppeteer asserts that no error message is emitted by the console - 'network.cookie.cookieBehavior': 0, - - // Do not prompt for temporary redirects - 'network.http.prompt-temp-redirect': false, - - // Disable speculative connections so they are not reported as leaking - // when they are hanging around - 'network.http.speculative-parallel-limit': 0, - - // Do not automatically switch between offline and online - 'network.manage-offline-status': false, - - // Make sure SNTP requests do not hit the network - 'network.sntp.pools': DUMMY_UMA_SERVER, - - // Disable Flash. - 'plugin.state.flash': 0, - - 'privacy.trackingprotection.enabled': false, - - // Enable Remote Agent - // https://bugzilla.mozilla.org/show_bug.cgi?id=1544393 - 'remote.enabled': true, - - // Don't do network connections for mitm priming - 'security.certerrors.mitm.priming.enabled': false, - // Local documents have access to all other local documents, - // including directory listings - 'security.fileuri.strict_origin_policy': false, - // Do not wait for the notification button security delay - 'security.notification_enable_delay': 0, - - // Ensure blocklist updates do not hit the network - 'services.settings.server': `http://${DUMMY_UMA_SERVER}/dummy/blocklist/`, - - // Do not automatically fill sign-in forms with known usernames and - // passwords - 'signon.autofillForms': false, - // Disable password capture, so that tests that include forms are not - // influenced by the presence of the persistent doorhanger notification - 'signon.rememberSignons': false, - - // Disable first-run welcome page - 'startup.homepage_welcome_url': 'about:blank', - - // Disable first-run welcome page - 'startup.homepage_welcome_url.additional': '', - - // Disable browser animations (tabs, fullscreen, sliding alerts) - 'toolkit.cosmeticAnimations.enabled': false, - - // We want to collect telemetry, but we don't want to send in the results - 'toolkit.telemetry.server': `https://${DUMMY_UMA_SERVER}/dummy/telemetry/`, - // Prevent starting into safe mode after application crashes - 'toolkit.startup.max_resumed_crashes': -1, -}; - -async function createProfile(extraPrefs?: object): Promise { - const profilePath = await mkdtempAsync(path.join(os.tmpdir(), 'playwright_dev_firefox_profile-')); - const prefsJS = []; - const userJS = []; - - const prefs = { ...DEFAULT_PREFERENCES, ...extraPrefs }; - for (const [key, value] of Object.entries(prefs)) - userJS.push(`user_pref(${JSON.stringify(key)}, ${JSON.stringify(value)});`); - - await writeFileAsync(path.join(profilePath, 'user.js'), userJS.join('\n')); - await writeFileAsync(path.join(profilePath, 'prefs.js'), prefsJS.join('\n')); - return profilePath; -} diff --git a/src/firefox/NetworkManager.ts b/src/firefox/NetworkManager.ts deleted file mode 100644 index b93cbe5c61..0000000000 --- a/src/firefox/NetworkManager.ts +++ /dev/null @@ -1,198 +0,0 @@ -/** - * Copyright 2019 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 { assert, debugError, helper, RegisteredListener } from '../helper'; -import { JugglerSession } from './Connection'; -import { Page } from '../page'; -import * as network from '../network'; -import * as frames from '../frames'; - -export class NetworkManager { - private _session: JugglerSession; - private _requests: Map; - private _page: Page; - private _eventListeners: RegisteredListener[]; - - constructor(session: JugglerSession, page: Page) { - this._session = session; - - this._requests = new Map(); - this._page = page; - - this._eventListeners = [ - helper.addEventListener(session, 'Network.requestWillBeSent', this._onRequestWillBeSent.bind(this)), - helper.addEventListener(session, 'Network.responseReceived', this._onResponseReceived.bind(this)), - helper.addEventListener(session, 'Network.requestFinished', this._onRequestFinished.bind(this)), - helper.addEventListener(session, 'Network.requestFailed', this._onRequestFailed.bind(this)), - ]; - } - - dispose() { - helper.removeEventListeners(this._eventListeners); - } - - async setRequestInterception(enabled) { - await this._session.send('Network.setRequestInterception', {enabled}); - } - - _onRequestWillBeSent(event) { - const redirected = event.redirectedFrom ? this._requests.get(event.redirectedFrom) : null; - const frame = redirected ? redirected.request.frame() : (event.frameId ? this._page._frameManager.frame(event.frameId) : null); - if (!frame) - return; - let redirectChain: network.Request[] = []; - if (redirected) { - redirectChain = redirected.request._redirectChain; - redirectChain.push(redirected.request); - this._requests.delete(redirected._id); - } - const request = new InterceptableRequest(this._session, frame, redirectChain, event); - this._requests.set(request._id, request); - this._page._frameManager.requestStarted(request.request); - } - - _onResponseReceived(event) { - const request = this._requests.get(event.requestId); - if (!request) - return; - const remoteAddress: network.RemoteAddress = { ip: event.remoteIPAddress, port: event.remotePort }; - const getResponseBody = async () => { - const response = await this._session.send('Network.getResponseBody', { - requestId: request._id - }); - if (response.evicted) - throw new Error(`Response body for ${request.request.method()} ${request.request.url()} was evicted!`); - return Buffer.from(response.base64body, 'base64'); - }; - const headers: network.Headers = {}; - for (const {name, value} of event.headers) - headers[name.toLowerCase()] = value; - const response = new network.Response(request.request, event.status, event.statusText, headers, remoteAddress, getResponseBody); - this._page._frameManager.requestReceivedResponse(response); - } - - _onRequestFinished(event) { - const request = this._requests.get(event.requestId); - if (!request) - return; - const response = request.request.response(); - // Keep redirected requests in the map for future reference in redirectChain. - const isRedirected = response.status() >= 300 && response.status() <= 399; - if (isRedirected) { - response._requestFinished(new Error('Response body is unavailable for redirect responses')); - } else { - this._requests.delete(request._id); - response._requestFinished(); - } - this._page._frameManager.requestFinished(request.request); - } - - _onRequestFailed(event) { - const request = this._requests.get(event.requestId); - if (!request) - return; - this._requests.delete(request._id); - if (request.request.response()) - request.request.response()._requestFinished(); - request.request._setFailureText(event.errorCode); - this._page._frameManager.requestFailed(request.request, event.errorCode === 'NS_BINDING_ABORTED'); - } -} - -const causeToResourceType = { - TYPE_INVALID: 'other', - TYPE_OTHER: 'other', - TYPE_SCRIPT: 'script', - TYPE_IMAGE: 'image', - TYPE_STYLESHEET: 'stylesheet', - TYPE_OBJECT: 'other', - TYPE_DOCUMENT: 'document', - TYPE_SUBDOCUMENT: 'document', - TYPE_REFRESH: 'document', - TYPE_XBL: 'other', - TYPE_PING: 'other', - TYPE_XMLHTTPREQUEST: 'xhr', - TYPE_OBJECT_SUBREQUEST: 'other', - TYPE_DTD: 'other', - TYPE_FONT: 'font', - TYPE_MEDIA: 'media', - TYPE_WEBSOCKET: 'websocket', - TYPE_CSP_REPORT: 'other', - TYPE_XSLT: 'other', - TYPE_BEACON: 'other', - TYPE_FETCH: 'fetch', - TYPE_IMAGESET: 'images', - TYPE_WEB_MANIFEST: 'manifest', -}; - -const interceptableRequestSymbol = Symbol('interceptableRequest'); - -export function toInterceptableRequest(request: network.Request): InterceptableRequest { - return (request as any)[interceptableRequestSymbol]; -} - -class InterceptableRequest { - readonly request: network.Request; - _id: string; - private _session: JugglerSession; - private _suspended: boolean; - private _interceptionHandled: boolean; - - constructor(session: JugglerSession, frame: frames.Frame, redirectChain: network.Request[], payload: any) { - this._id = payload.requestId; - this._session = session; - this._suspended = payload.suspended; - this._interceptionHandled = false; - - const headers: network.Headers = {}; - for (const {name, value} of payload.headers) - headers[name.toLowerCase()] = value; - - this.request = new network.Request(frame, redirectChain, payload.navigationId, - payload.url, causeToResourceType[payload.cause] || 'other', payload.method, payload.postData, headers); - (this.request as any)[interceptableRequestSymbol] = this; - } - - async continue(overrides: {url?: string, method?: string, postData?: string, headers?: {[key: string]: string}} = {}) { - assert(!overrides.url, 'Playwright-Firefox does not support overriding URL'); - assert(!overrides.method, 'Playwright-Firefox does not support overriding method'); - assert(!overrides.postData, 'Playwright-Firefox does not support overriding postData'); - assert(this._suspended, 'Request Interception is not enabled!'); - assert(!this._interceptionHandled, 'Request is already handled!'); - this._interceptionHandled = true; - const { - headers, - } = overrides; - await this._session.send('Network.resumeSuspendedRequest', { - requestId: this._id, - headers: headers ? Object.entries(headers).filter(([, value]) => !Object.is(value, undefined)).map(([name, value]) => ({name, value})) : undefined, - }).catch(error => { - debugError(error); - }); - } - - async abort() { - assert(this._suspended, 'Request Interception is not enabled!'); - assert(!this._interceptionHandled, 'Request is already handled!'); - this._interceptionHandled = true; - await this._session.send('Network.abortSuspendedRequest', { - requestId: this._id, - }).catch(error => { - debugError(error); - }); - } -} diff --git a/src/firefox/Playwright.ts b/src/firefox/Playwright.ts deleted file mode 100644 index d8f5dd58d5..0000000000 --- a/src/firefox/Playwright.ts +++ /dev/null @@ -1,82 +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 { Browser } from './Browser'; -import { BrowserFetcher, BrowserFetcherOptions, OnProgressCallback, BrowserFetcherRevisionInfo } from '../browserFetcher'; -import { WebSocketTransport, SlowMoTransport } from '../transport'; -import { DeviceDescriptors, DeviceDescriptor } from '../deviceDescriptors'; -import * as Errors from '../errors'; -import { Launcher, createBrowserFetcher } from './Launcher'; - -type Devices = { [name: string]: DeviceDescriptor } & DeviceDescriptor[]; - -export class Playwright { - private _projectRoot: string; - private _launcher: Launcher; - readonly _revision: string; - - constructor(projectRoot: string, preferredRevision: string) { - this._projectRoot = projectRoot; - this._launcher = new Launcher(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 WebSocketTransport.create(options.browserWSEndpoint); - return Browser.create(SlowMoTransport.wrap(transport, options.slowMo || 0)); - } - - executablePath(): string { - return this._launcher.executablePath(); - } - - get devices(): Devices { - const result = DeviceDescriptors.slice() as Devices; - for (const device of DeviceDescriptors) - result[device.name] = device; - return result; - } - - 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/api.ts b/src/firefox/api.ts index 393cbadd8e..e2791c2aa5 100644 --- a/src/firefox/api.ts +++ b/src/firefox/api.ts @@ -7,9 +7,9 @@ export { BrowserContext } from '../browserContext'; export { Dialog } from '../dialog'; export { JSHandle } from '../javascript'; export { ElementHandle } from '../dom'; -export { Accessibility } from './features/accessibility'; -export { Interception } from './features/interception'; -export { Permissions } from './features/permissions'; +export { FFAccessibility as Accessibility } from './features/ffAccessibility'; +export { FFInterception as Interception } from './features/ffInterception'; +export { FFPermissions as Permissions } from './features/ffPermissions'; export { Frame } from '../frames'; export { Request, Response } from '../network'; export { Page } from '../page'; diff --git a/src/firefox/events.ts b/src/firefox/events.ts deleted file mode 100644 index 3faa0cbfeb..0000000000 --- a/src/firefox/events.ts +++ /dev/null @@ -1,22 +0,0 @@ -/** - * Copyright 2019 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. - */ - -export const Events = { - Browser: { - Disconnected: 'disconnected' - }, -}; diff --git a/src/firefox/features/accessibility.ts b/src/firefox/features/accessibility.ts deleted file mode 100644 index 0556b4c1fe..0000000000 --- a/src/firefox/features/accessibility.ts +++ /dev/null @@ -1,281 +0,0 @@ -/** - * Copyright 2018 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. - */ - -interface SerializedAXNode { - role: string; - - name?: string; - value?: string|number; - description?: string; - - keyshortcuts?: string; - roledescription?: string; - valuetext?: string; - - disabled?: boolean; - expanded?: boolean; - focused?: boolean; - modal?: boolean; - multiline?: boolean; - multiselectable?: boolean; - readonly?: boolean; - required?: boolean; - selected?: boolean; - - checked?: boolean|'mixed'; - pressed?: boolean|'mixed'; - - level?: number; - - autocomplete?: string; - haspopup?: string; - invalid?: string; - orientation?: string; - - children?: Array; -} -export class Accessibility { - _session: any; - constructor(session) { - this._session = session; - } - async snapshot(options: { interestingOnly?: boolean; } | undefined = {}): Promise { - const { interestingOnly = true } = options; - const { tree } = await this._session.send('Accessibility.getFullAXTree'); - const root = new AXNode(tree); - if (!interestingOnly) - return serializeTree(root)[0]; - const interestingNodes: Set = new Set(); - collectInterestingNodes(interestingNodes, root, false); - return serializeTree(root, interestingNodes)[0]; - } -} -function collectInterestingNodes(collection: Set, node: AXNode, insideControl: boolean) { - if (node.isInteresting(insideControl)) - collection.add(node); - if (node.isLeafNode()) - return; - insideControl = insideControl || node.isControl(); - for (const child of node._children) - collectInterestingNodes(collection, child, insideControl); -} -function serializeTree(node: AXNode, whitelistedNodes?: Set): Array { - const children: Array = []; - for (const child of node._children) - children.push(...serializeTree(child, whitelistedNodes)); - if (whitelistedNodes && !whitelistedNodes.has(node)) - return children; - const serializedNode = node.serialize(); - if (children.length) - serializedNode.children = children; - return [serializedNode]; -} -class AXNode { - _children: AXNode[]; - private _payload: any; - private _editable: boolean; - private _richlyEditable: boolean; - private _focusable: boolean; - private _expanded: boolean; - private _name: string; - private _role: string; - private _cachedHasFocusableChild: boolean|undefined; - - constructor(payload) { - this._payload = payload; - this._children = (payload.children || []).map(x => new AXNode(x)); - this._editable = payload.editable; - this._richlyEditable = this._editable && (payload.tag !== 'textarea' && payload.tag !== 'input'); - this._focusable = payload.focusable; - this._expanded = payload.expanded; - this._name = this._payload.name; - this._role = this._payload.role; - this._cachedHasFocusableChild; - } - - _isPlainTextField(): boolean { - if (this._richlyEditable) - return false; - if (this._editable) - return true; - return this._role === 'entry'; - } - - _isTextOnlyObject(): boolean { - const role = this._role; - return (role === 'text leaf' || role === 'text' || role === 'statictext'); - } - - _hasFocusableChild(): boolean { - if (this._cachedHasFocusableChild === undefined) { - this._cachedHasFocusableChild = false; - for (const child of this._children) { - if (child._focusable || child._hasFocusableChild()) { - this._cachedHasFocusableChild = true; - break; - } - } - } - return this._cachedHasFocusableChild; - } - - isLeafNode(): boolean { - if (!this._children.length) - return true; - // These types of objects may have children that we use as internal - // implementation details, but we want to expose them as leaves to platform - // accessibility APIs because screen readers might be confused if they find - // any children. - if (this._isPlainTextField() || this._isTextOnlyObject()) - return true; - // Roles whose children are only presentational according to the ARIA and - // HTML5 Specs should be hidden from screen readers. - // (Note that whilst ARIA buttons can have only presentational children, HTML5 - // buttons are allowed to have content.) - switch (this._role) { - case 'graphic': - case 'scrollbar': - case 'slider': - case 'separator': - case 'progressbar': - return true; - default: - break; - } - // Here and below: Android heuristics - if (this._hasFocusableChild()) - return false; - if (this._focusable && this._name) - return true; - if (this._role === 'heading' && this._name) - return true; - return false; - } - - isControl(): boolean { - switch (this._role) { - case 'checkbutton': - case 'check menu item': - case 'check rich option': - case 'combobox': - case 'combobox option': - case 'color chooser': - case 'listbox': - case 'listbox option': - case 'listbox rich option': - case 'popup menu': - case 'menupopup': - case 'menuitem': - case 'menubar': - case 'button': - case 'pushbutton': - case 'radiobutton': - case 'radio menuitem': - case 'scrollbar': - case 'slider': - case 'spinbutton': - case 'switch': - case 'pagetab': - case 'entry': - case 'tree table': - return true; - default: - return false; - } - } - - isInteresting(insideControl: boolean): boolean { - if (this._focusable || this._richlyEditable) - return true; - // If it's not focusable but has a control role, then it's interesting. - if (this.isControl()) - return true; - // A non focusable child of a control is not interesting - if (insideControl) - return false; - return this.isLeafNode() && !!this._name.trim(); - } - - serialize(): SerializedAXNode { - const node: {[x in keyof SerializedAXNode]: any} = { - role: this._role - }; - const userStringProperties: Array = [ - 'name', - 'value', - 'description', - 'roledescription', - 'valuetext', - 'keyshortcuts', - ]; - for (const userStringProperty of userStringProperties) { - if (!(userStringProperty in this._payload)) - continue; - node[userStringProperty] = this._payload[userStringProperty]; - } - const booleanProperties: Array = [ - 'disabled', - 'expanded', - 'focused', - 'modal', - 'multiline', - 'multiselectable', - 'readonly', - 'required', - 'selected', - ]; - for (const booleanProperty of booleanProperties) { - if (this._role === 'document' && booleanProperty === 'focused') - continue; // document focusing is strange - const value = this._payload[booleanProperty]; - if (!value) - continue; - node[booleanProperty] = value; - } - const tristateProperties: Array = [ - 'checked', - 'pressed', - ]; - for (const tristateProperty of tristateProperties) { - if (!(tristateProperty in this._payload)) - continue; - const value = this._payload[tristateProperty]; - node[tristateProperty] = value; - } - const numericalProperties: Array = [ - 'level' - ]; - for (const numericalProperty of numericalProperties) { - if (!(numericalProperty in this._payload)) - continue; - node[numericalProperty] = this._payload[numericalProperty]; - } - const tokenProperties: Array = [ - 'autocomplete', - 'haspopup', - 'invalid', - 'orientation', - ]; - for (const tokenProperty of tokenProperties) { - const value = this._payload[tokenProperty]; - if (!value || value === 'false') - continue; - node[tokenProperty] = value; - } - return node; - } -} diff --git a/src/firefox/features/interception.ts b/src/firefox/features/interception.ts deleted file mode 100644 index 0ececc9e20..0000000000 --- a/src/firefox/features/interception.ts +++ /dev/null @@ -1,33 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. - -import { NetworkManager, toInterceptableRequest } from '../NetworkManager'; -import * as network from '../../network'; - -export class Interception { - private _networkManager: NetworkManager; - - constructor(networkManager: NetworkManager) { - this._networkManager = networkManager; - } - - async enable() { - await this._networkManager.setRequestInterception(true); - } - - async disable() { - await this._networkManager.setRequestInterception(false); - } - - async continue(request: network.Request, overrides: { url?: string; method?: string; postData?: string; headers?: {[key: string]: string}; } = {}) { - return toInterceptableRequest(request).continue(overrides); - } - - async fulfill(request: network.Request, response: { status: number; headers: {[key: string]: string}; contentType: string; body: (string | Buffer); }) { - throw new Error('Not implemented'); - } - - async abort(request: network.Request, errorCode: string = 'failed') { - return toInterceptableRequest(request).abort(); - } -} diff --git a/src/firefox/features/permissions.ts b/src/firefox/features/permissions.ts deleted file mode 100644 index 43e69ec95f..0000000000 --- a/src/firefox/features/permissions.ts +++ /dev/null @@ -1,49 +0,0 @@ -/** - * Copyright 2018 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 { Connection } from '../Connection'; - -export class Permissions { - private _connection: Connection; - private _browserContextId: string; - - constructor(connection: Connection, browserContextId: string | null) { - this._connection = connection; - this._browserContextId = browserContextId; - } - - - async override(origin: string, permissions: Array) { - const webPermissionToProtocol = new Map([ - ['geolocation', 'geo'], - ['microphone', 'microphone'], - ['camera', 'camera'], - ['notifications', 'desktop-notifications'], - ]); - permissions = permissions.map(permission => { - const protocolPermission = webPermissionToProtocol.get(permission); - if (!protocolPermission) - throw new Error('Unknown permission: ' + permission); - return protocolPermission; - }); - await this._connection.send('Browser.grantPermissions', {origin, browserContextId: this._browserContextId || undefined, permissions}); - } - - async clearOverrides() { - await this._connection.send('Browser.resetPermissions', {browserContextId: this._browserContextId || undefined}); - } -} diff --git a/src/webkit/Browser.ts b/src/webkit/Browser.ts deleted file mode 100644 index 29797f3a58..0000000000 --- a/src/webkit/Browser.ts +++ /dev/null @@ -1,217 +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 { EventEmitter } from 'events'; -import { helper, RegisteredListener, debugError, assert } from '../helper'; -import * as browser from '../browser'; -import * as network from '../network'; -import { Connection, ConnectionEvents, TargetSession } from './Connection'; -import { Page } from '../page'; -import { Target } from './Target'; -import { Protocol } from './protocol'; -import { Events } from '../events'; -import { BrowserContext, BrowserContextOptions } from '../browserContext'; -import { ConnectionTransport } from '../transport'; - -export class Browser extends EventEmitter implements browser.Browser { - readonly _connection: Connection; - private _defaultContext: BrowserContext; - private _contexts = new Map(); - _targets = new Map(); - private _eventListeners: RegisteredListener[]; - private _privateEvents = new EventEmitter(); - - constructor(transport: ConnectionTransport) { - super(); - this._connection = new Connection(transport); - - /** @type {!Map} */ - this._targets = new Map(); - - this._defaultContext = this._createBrowserContext(undefined, {}); - /** @type {!Map} */ - this._contexts = new Map(); - - this._eventListeners = [ - helper.addEventListener(this._connection, ConnectionEvents.TargetCreated, this._onTargetCreated.bind(this)), - helper.addEventListener(this._connection, ConnectionEvents.TargetDestroyed, this._onTargetDestroyed.bind(this)), - helper.addEventListener(this._connection, ConnectionEvents.DidCommitProvisionalTarget, this._onProvisionalTargetCommitted.bind(this)), - ]; - - // Intercept provisional targets during cross-process navigation. - this._connection.send('Target.setPauseOnStart', { pauseOnStart: true }).catch(e => { - debugError(e); - throw e; - }); - } - - async newContext(options: BrowserContextOptions = {}): Promise { - const { browserContextId } = await this._connection.send('Browser.createContext'); - const context = this._createBrowserContext(browserContextId, options); - if (options.ignoreHTTPSErrors) - await this._connection.send('Browser.setIgnoreCertificateErrors', { browserContextId, ignore: true }); - this._contexts.set(browserContextId, context); - return context; - } - - browserContexts(): BrowserContext[] { - return [this._defaultContext, ...Array.from(this._contexts.values())]; - } - - defaultContext(): BrowserContext { - return this._defaultContext; - } - - async _waitForTarget(predicate: (arg0: Target) => boolean, options: { timeout?: number; } | undefined = {}): Promise { - const { - timeout = 30000 - } = options; - const existingTarget = Array.from(this._targets.values()).find(predicate); - if (existingTarget) - return existingTarget; - let resolve : (a: Target) => void; - const targetPromise = new Promise(x => resolve = x); - this._privateEvents.on(BrowserEvents.TargetCreated, check); - try { - if (!timeout) - return await targetPromise; - return await helper.waitWithTimeout(targetPromise, 'target', timeout); - } finally { - this._privateEvents.removeListener(BrowserEvents.TargetCreated, check); - } - - function check(target: Target) { - if (predicate(target)) - resolve(target); - } - } - - _onTargetCreated(session: TargetSession, targetInfo: Protocol.Target.TargetInfo) { - let context = null; - if (targetInfo.browserContextId) { - // FIXME: we don't know about the default context id, so assume that all targets from - // unknown contexts are created in the 'default' context which can in practice be represented - // by multiple actual contexts in WebKit. Solving this properly will require adding context - // lifecycle events. - context = this._contexts.get(targetInfo.browserContextId); - // if (!context) - // throw new Error(`Target ${targetId} created in unknown browser context ${browserContextId}.`); - } - if (!context) - context = this._defaultContext; - const target = new Target(this, session, targetInfo, context); - this._targets.set(targetInfo.targetId, target); - if (targetInfo.isProvisional) { - const oldTarget = this._targets.get(targetInfo.oldTargetId); - if (oldTarget) - oldTarget._initializeSession(session); - } - this._privateEvents.emit(BrowserEvents.TargetCreated, target); - if (!targetInfo.oldTargetId && targetInfo.openerId) { - const opener = this._targets.get(targetInfo.openerId); - if (!opener) - return; - const openerPage = opener._frameManager ? opener._frameManager._page : null; - if (!openerPage || !openerPage.listenerCount(Events.Page.Popup)) - return; - target.page().then(page => openerPage.emit(Events.Page.Popup, page)); - } - if (targetInfo.isPaused) - this._connection.send('Target.resume', { targetId: targetInfo.targetId }).catch(debugError); - } - - _onTargetDestroyed({targetId}) { - const target = this._targets.get(targetId); - this._targets.delete(targetId); - target._didClose(); - } - - _closePage(page: Page, runBeforeUnload: boolean) { - this._connection.send('Target.close', { - targetId: Target.fromPage(page)._targetId, - runBeforeUnload - }).catch(debugError); - } - - async _activatePage(page: Page): Promise { - await this._connection.send('Target.activate', { targetId: Target.fromPage(page)._targetId }); - } - - async _onProvisionalTargetCommitted({oldTargetId, newTargetId}) { - const oldTarget = this._targets.get(oldTargetId); - const newTarget = this._targets.get(newTargetId); - newTarget._swapWith(oldTarget); - } - - disconnect() { - throw new Error('Unsupported operation'); - } - - isConnected(): boolean { - return true; - } - - async close() { - helper.removeEventListeners(this._eventListeners); - await this._connection.send('Browser.close'); - } - - _createBrowserContext(browserContextId: string | undefined, options: BrowserContextOptions): BrowserContext { - const context = new BrowserContext({ - pages: async (): Promise => { - const targets = Array.from(this._targets.values()).filter(target => target._browserContext === context && target._type === 'page'); - const pages = await Promise.all(targets.map(target => target.page())); - return pages.filter(page => !!page); - }, - - newPage: async (): Promise => { - const { targetId } = await this._connection.send('Browser.createPage', { browserContextId }); - const target = this._targets.get(targetId); - return await target.page(); - }, - - close: async (): Promise => { - assert(browserContextId, 'Non-incognito profiles cannot be closed!'); - await this._connection.send('Browser.deleteContext', { browserContextId }); - this._contexts.delete(browserContextId); - }, - - cookies: async (): Promise => { - const { cookies } = await this._connection.send('Browser.getAllCookies', { browserContextId }); - return cookies.map((c: network.NetworkCookie) => ({ - ...c, - expires: c.expires === 0 ? -1 : c.expires - })); - }, - - clearCookies: async (): Promise => { - await this._connection.send('Browser.deleteAllCookies', { browserContextId }); - }, - - setCookies: async (cookies: network.SetNetworkCookieParam[]): Promise => { - const cc = cookies.map(c => ({ ...c, session: c.expires === -1 || c.expires === undefined })) as Protocol.Browser.SetCookieParam[]; - await this._connection.send('Browser.setCookies', { cookies: cc, browserContextId }); - }, - }, options); - return context; - } -} - -const BrowserEvents = { - TargetCreated: Symbol('BrowserEvents.TargetCreated'), - TargetDestroyed: Symbol('BrowserEvents.TargetDestroyed'), -}; diff --git a/src/webkit/Connection.ts b/src/webkit/Connection.ts deleted file mode 100644 index 5c413bfaef..0000000000 --- a/src/webkit/Connection.ts +++ /dev/null @@ -1,264 +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 { assert } from '../helper'; -import * as debug from 'debug'; -import { EventEmitter } from 'events'; -import { ConnectionTransport } from '../transport'; -import { Protocol } from './protocol'; - -const debugProtocol = debug('playwright:protocol'); -const debugWrappedMessage = require('debug')('wrapped'); - -export const ConnectionEvents = { - TargetCreated: Symbol('ConnectionEvents.TargetCreated'), - TargetDestroyed: Symbol('Connection.TargetDestroyed'), - DidCommitProvisionalTarget: Symbol('Connection.DidCommitProvisionalTarget') -}; - -export class Connection extends EventEmitter { - _lastId = 0; - private readonly _callbacks = new Map void, reject: (e: Error) => void, error: Error, method: string}>(); - private readonly _transport: ConnectionTransport; - private readonly _sessions = new Map(); - - _closed = false; - - constructor(transport: ConnectionTransport) { - super(); - this._transport = transport; - this._transport.onmessage = this._dispatchMessage.bind(this); - this._transport.onclose = this._onClose.bind(this); - } - - static fromSession(session: TargetSession): Connection { - return session._connection; - } - - send( - method: T, - params?: Protocol.CommandParameters[T] - ): Promise { - const id = this._rawSend({method, params}); - return new Promise((resolve, reject) => { - this._callbacks.set(id, {resolve, reject, error: new Error(), method}); - }); - } - - _rawSend(message: any): number { - const id = ++this._lastId; - message = JSON.stringify(Object.assign({}, message, {id})); - debugProtocol('SEND ► ' + message); - this._transport.send(message); - return id; - } - - private _dispatchMessage(message: string) { - debugProtocol('◀ RECV ' + message); - const object = JSON.parse(message); - this._dispatchTargetMessageToSession(object, message); - if (object.id) { - const callback = this._callbacks.get(object.id); - // Callbacks could be all rejected if someone has called `.dispose()`. - if (callback) { - this._callbacks.delete(object.id); - if (object.error) - callback.reject(createProtocolError(callback.error, callback.method, object)); - else - callback.resolve(object.result); - } else { - assert(this._closed, 'Received response for unknown callback: ' + object.id); - } - } else { - Promise.resolve().then(() => this.emit(object.method, object.params)); - } - } - - _dispatchTargetMessageToSession(object: {method: string, params: any}, wrappedMessage: string) { - if (object.method === 'Target.targetCreated') { - const targetInfo = object.params.targetInfo as Protocol.Target.TargetInfo; - const session = new TargetSession(this, targetInfo); - this._sessions.set(session._sessionId, session); - Promise.resolve().then(() => this.emit(ConnectionEvents.TargetCreated, session, object.params.targetInfo)); - } else if (object.method === 'Target.targetDestroyed') { - const session = this._sessions.get(object.params.targetId); - if (session) { - session._onClosed(); - this._sessions.delete(object.params.targetId); - } - Promise.resolve().then(() => this.emit(ConnectionEvents.TargetDestroyed, { targetId: object.params.targetId })); - } else if (object.method === 'Target.dispatchMessageFromTarget') { - const {targetId, message} = object.params as Protocol.Target.dispatchMessageFromTargetPayload; - const session = this._sessions.get(targetId); - if (!session) - throw new Error('Unknown target: ' + targetId); - if (session.isProvisional()) - session._addProvisionalMessage(message); - else - session._dispatchMessageFromTarget(message); - } else if (object.method === 'Target.didCommitProvisionalTarget') { - const {oldTargetId, newTargetId} = object.params as Protocol.Target.didCommitProvisionalTargetPayload; - Promise.resolve().then(() => this.emit(ConnectionEvents.DidCommitProvisionalTarget, { oldTargetId, newTargetId })); - const newSession = this._sessions.get(newTargetId); - if (!newSession) - throw new Error('Unknown new target: ' + newTargetId); - const oldSession = this._sessions.get(oldTargetId); - if (!oldSession) - throw new Error('Unknown old target: ' + oldTargetId); - oldSession._swappedOut = true; - for (const message of newSession._takeProvisionalMessagesAndCommit()) - newSession._dispatchMessageFromTarget(message); - } - } - - _onClose() { - if (this._closed) - return; - this._closed = true; - this._transport.onmessage = null; - this._transport.onclose = null; - for (const callback of this._callbacks.values()) - callback.reject(rewriteError(callback.error, `Protocol error (${callback.method}): Target closed.`)); - this._callbacks.clear(); - for (const session of this._sessions.values()) - session._onClosed(); - this._sessions.clear(); - } - - dispose() { - this._onClose(); - this._transport.close(); - } -} - -export const TargetSessionEvents = { - Disconnected: Symbol('TargetSessionEvents.Disconnected') -}; - -export class TargetSession extends EventEmitter { - _connection: Connection; - private _callbacks = new Map void, reject: (e: Error) => void, error: Error, method: string}>(); - private _targetType: string; - _sessionId: string; - _swappedOut = false; - private _provisionalMessages?: string[]; - on: (event: T, listener: (payload: T extends symbol ? any : Protocol.Events[T extends keyof Protocol.Events ? T : never]) => void) => this; - addListener: (event: T, listener: (payload: T extends symbol ? any : Protocol.Events[T extends keyof Protocol.Events ? T : never]) => void) => this; - off: (event: T, listener: (payload: T extends symbol ? any : Protocol.Events[T extends keyof Protocol.Events ? T : never]) => void) => this; - removeListener: (event: T, listener: (payload: T extends symbol ? any : Protocol.Events[T extends keyof Protocol.Events ? T : never]) => void) => this; - once: (event: T, listener: (payload: T extends symbol ? any : Protocol.Events[T extends keyof Protocol.Events ? T : never]) => void) => this; - - constructor(connection: Connection, targetInfo: Protocol.Target.TargetInfo) { - super(); - const {targetId, type, isProvisional} = targetInfo; - this._connection = connection; - this._targetType = type; - this._sessionId = targetId; - if (isProvisional) - this._provisionalMessages = []; - } - - isProvisional() : boolean { - return !!this._provisionalMessages; - } - - send( - method: T, - params?: Protocol.CommandParameters[T] - ): Promise { - if (!this._connection) - return Promise.reject(new Error(`Protocol error (${method}): Session closed. Most likely the ${this._targetType} has been closed.`)); - const innerId = ++this._connection._lastId; - const messageObj = { - id: innerId, - method, - params - }; - debugWrappedMessage('SEND ► ' + JSON.stringify(messageObj, null, 2)); - // Serialize message before adding callback in case JSON throws. - const message = JSON.stringify(messageObj); - const result = new Promise((resolve, reject) => { - this._callbacks.set(innerId, {resolve, reject, error: new Error(), method}); - }); - this._connection.send('Target.sendMessageToTarget', { - message: message, targetId: this._sessionId - }).catch(e => { - // There is a possible race of the connection closure. We may have received - // targetDestroyed notification before response for the command, in that - // case it's safe to swallow the exception. - const callback = this._callbacks.get(innerId); - assert(!callback, 'Callback was not rejected when target was destroyed.'); - }); - return result; - } - - _addProvisionalMessage(message: string) { - this._provisionalMessages.push(message); - } - - _takeProvisionalMessagesAndCommit() : string[] { - const messages = this._provisionalMessages; - this._provisionalMessages = undefined; - return messages; - } - - _dispatchMessageFromTarget(message: string) { - console.assert(!this.isProvisional()); - const object = JSON.parse(message); - debugWrappedMessage('◀ RECV ' + JSON.stringify(object, null, 2)); - if (object.id && this._callbacks.has(object.id)) { - const callback = this._callbacks.get(object.id); - this._callbacks.delete(object.id); - if (object.error) - callback.reject(createProtocolError(callback.error, callback.method, object)); - else - callback.resolve(object.result); - } else { - assert(!object.id); - Promise.resolve().then(() => this.emit(object.method, object.params)); - } - } - - _onClosed() { - for (const callback of this._callbacks.values()) { - // TODO: make some calls like screenshot catch swapped out error and retry. - if (this._swappedOut) - callback.reject(rewriteError(callback.error, `Protocol error (${callback.method}): Target was swapped out.`)); - else - callback.reject(rewriteError(callback.error, `Protocol error (${callback.method}): Target closed.`)); - } - this._callbacks.clear(); - this._connection = null; - Promise.resolve().then(() => this.emit(TargetSessionEvents.Disconnected)); - } -} - -function createProtocolError(error: Error, method: string, object: { error: { message: string; data: any; }; }): Error { - let message = `Protocol error (${method}): ${object.error.message}`; - if ('data' in object.error) - message += ` ${object.error.data}`; - return rewriteError(error, message); -} - -function rewriteError(error: Error, message: string): Error { - error.message = message; - return error; -} - -export function isSwappedOutError(e: Error) { - return e.message.includes('Target was swapped out.'); -} diff --git a/src/webkit/ExecutionContext.ts b/src/webkit/ExecutionContext.ts deleted file mode 100644 index 2dece987cf..0000000000 --- a/src/webkit/ExecutionContext.ts +++ /dev/null @@ -1,324 +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 { TargetSession, isSwappedOutError } from './Connection'; -import { helper } from '../helper'; -import { valueFromRemoteObject, releaseObject } from './protocolHelper'; -import { Protocol } from './protocol'; -import * as js from '../javascript'; - -export const EVALUATION_SCRIPT_URL = '__playwright_evaluation_script__'; -const SOURCE_URL_REGEX = /^[\040\t]*\/\/[@#] sourceURL=\s*(\S*?)\s*$/m; - -export class ExecutionContextDelegate implements js.ExecutionContextDelegate { - private _globalObjectId?: Promise; - _session: TargetSession; - _contextId: number; - private _contextDestroyedCallback: () => void; - private _executionContextDestroyedPromise: Promise; - - constructor(client: TargetSession, contextPayload: Protocol.Runtime.ExecutionContextDescription) { - this._session = client; - this._contextId = contextPayload.id; - this._contextDestroyedCallback = null; - this._executionContextDestroyedPromise = new Promise((resolve, reject) => { - this._contextDestroyedCallback = resolve; - }); - } - - _dispose() { - this._contextDestroyedCallback(); - } - - async evaluate(context: js.ExecutionContext, returnByValue: boolean, pageFunction: Function | string, ...args: any[]): Promise { - if (helper.isString(pageFunction)) { - const contextId = this._contextId; - const expression: string = pageFunction as string; - const expressionWithSourceUrl = SOURCE_URL_REGEX.test(expression) ? expression : expression + '\n' + suffix; - return this._session.send('Runtime.evaluate', { - expression: expressionWithSourceUrl, - contextId, - returnByValue: false, - emulateUserGesture: true - }).then(response => { - if (response.result.type === 'object' && response.result.className === 'Promise') { - return Promise.race([ - this._executionContextDestroyedPromise.then(() => contextDestroyedResult), - this._awaitPromise(response.result.objectId), - ]); - } - return response; - }).then(response => { - if (response.wasThrown) - throw new Error('Evaluation failed: ' + response.result.description); - if (!returnByValue) - return context._createHandle(response.result); - if (response.result.objectId) - return this._returnObjectByValue(response.result.objectId); - return valueFromRemoteObject(response.result); - }).catch(rewriteError); - } - - if (typeof pageFunction !== 'function') - throw new Error(`Expected to get |string| or |function| as the first argument, but got "${pageFunction}" instead.`); - - let functionText = pageFunction.toString(); - try { - new Function('(' + functionText + ')'); - } catch (e1) { - // This means we might have a function shorthand. Try another - // time prefixing 'function '. - if (functionText.startsWith('async ')) - functionText = 'async function ' + functionText.substring('async '.length); - else - functionText = 'function ' + functionText; - try { - new Function('(' + functionText + ')'); - } catch (e2) { - // We tried hard to serialize, but there's a weird beast here. - throw new Error('Passed function is not well-serializable!'); - } - } - - let serializableArgs; - if (args.some(isUnserializable)) { - serializableArgs = []; - const paramStrings = []; - for (const arg of args) { - if (isUnserializable(arg)) { - paramStrings.push(unserializableToString(arg)); - } else { - paramStrings.push('arguments[' + serializableArgs.length + ']'); - serializableArgs.push(arg); - } - } - functionText = `() => (${functionText})(${paramStrings.join(',')})`; - } else { - serializableArgs = args; - } - - const thisObjectId = await this._contextGlobalObjectId(); - let callFunctionOnPromise; - try { - callFunctionOnPromise = this._session.send('Runtime.callFunctionOn', { - functionDeclaration: functionText + '\n' + suffix + '\n', - objectId: thisObjectId, - arguments: serializableArgs.map((arg: any) => this._convertArgument(arg)), - returnByValue: false, - emulateUserGesture: true - }); - } catch (err) { - if (err instanceof TypeError && err.message.startsWith('Converting circular structure to JSON')) - err.message += ' Are you passing a nested JSHandle?'; - throw err; - } - return callFunctionOnPromise.then(response => { - if (response.result.type === 'object' && response.result.className === 'Promise') { - return Promise.race([ - this._executionContextDestroyedPromise.then(() => contextDestroyedResult), - this._awaitPromise(response.result.objectId), - ]); - } - return response; - }).then(response => { - if (response.wasThrown) - throw new Error('Evaluation failed: ' + response.result.description); - if (!returnByValue) - return context._createHandle(response.result); - if (response.result.objectId) - return this._returnObjectByValue(response.result.objectId); - return valueFromRemoteObject(response.result); - }).catch(rewriteError); - - function unserializableToString(arg) { - if (Object.is(arg, -0)) - return '-0'; - if (Object.is(arg, Infinity)) - return 'Infinity'; - if (Object.is(arg, -Infinity)) - return '-Infinity'; - if (Object.is(arg, NaN)) - return 'NaN'; - if (arg instanceof js.JSHandle) { - const remoteObj = toRemoteObject(arg); - if (!remoteObj.objectId) - return valueFromRemoteObject(remoteObj); - } - throw new Error('Unsupported value: ' + arg + ' (' + (typeof arg) + ')'); - } - - function isUnserializable(arg) { - if (typeof arg === 'bigint') - return true; - if (Object.is(arg, -0)) - return true; - if (Object.is(arg, Infinity)) - return true; - if (Object.is(arg, -Infinity)) - return true; - if (Object.is(arg, NaN)) - return true; - if (arg instanceof js.JSHandle) { - const remoteObj = toRemoteObject(arg); - if (!remoteObj.objectId) - return !Object.is(valueFromRemoteObject(remoteObj), remoteObj.value); - } - return false; - } - - /** - * @param {!Error} error - * @return {!Protocol.Runtime.evaluateReturnValue} - */ - function rewriteError(error) { - if (error.message.includes('Object couldn\'t be returned by value')) - return {result: {type: 'undefined'}}; - - if (error.message.includes('Missing injected script for given')) - throw new Error('Execution context was destroyed, most likely because of a navigation.'); - throw error; - } - } - - private _contextGlobalObjectId() { - if (!this._globalObjectId) { - this._globalObjectId = this._session.send('Runtime.evaluate', { - expression: 'this', - contextId: this._contextId - }).catch(e => { - if (isSwappedOutError(e)) - throw new Error('Execution context was destroyed, most likely because of a navigation.'); - throw e; - }).then(response => { - return response.result.objectId; - }); - } - return this._globalObjectId; - } - - private _awaitPromise(objectId: Protocol.Runtime.RemoteObjectId) { - return this._session.send('Runtime.awaitPromise', { - promiseObjectId: objectId, - returnByValue: false - }).catch(e => { - if (isSwappedOutError(e)) - return contextDestroyedResult; - throw e; - }); - } - - private _returnObjectByValue(objectId: Protocol.Runtime.RemoteObjectId) { - const serializeFunction = function() { - try { - return JSON.stringify(this); - } catch (e) { - if (e instanceof TypeError) - return void 0; - throw e; - } - }; - return this._session.send('Runtime.callFunctionOn', { - // Serialize object using standard JSON implementation to correctly pass 'undefined'. - functionDeclaration: serializeFunction + '\n' + suffix + '\n', - objectId: objectId, - returnByValue: true - }).catch(e => { - if (isSwappedOutError(e)) - return contextDestroyedResult; - throw e; - }).then(serializeResponse => { - if (serializeResponse.wasThrown) - throw new Error('Serialization failed: ' + serializeResponse.result.description); - // This is the case of too long property chain, not serializable to json string. - if (serializeResponse.result.type === 'undefined') - return undefined; - if (serializeResponse.result.type !== 'string') - throw new Error('Unexpected result of JSON.stringify: ' + JSON.stringify(serializeResponse, null, 2)); - return JSON.parse(serializeResponse.result.value); - }); - } - - async getProperties(handle: js.JSHandle): Promise> { - const response = await this._session.send('Runtime.getProperties', { - objectId: toRemoteObject(handle).objectId, - ownProperties: true - }); - const result = new Map(); - for (const property of response.properties) { - if (!property.enumerable) - continue; - result.set(property.name, handle._context._createHandle(property.value)); - } - return result; - } - - async releaseHandle(handle: js.JSHandle): Promise { - await releaseObject(this._session, toRemoteObject(handle)); - } - - async handleJSONValue(handle: js.JSHandle): Promise { - const remoteObject = toRemoteObject(handle); - if (remoteObject.objectId) { - const response = await this._session.send('Runtime.callFunctionOn', { - functionDeclaration: 'function() { return this; }', - objectId: remoteObject.objectId, - returnByValue: true - }); - return valueFromRemoteObject(response.result); - } - return valueFromRemoteObject(remoteObject); - } - - handleToString(handle: js.JSHandle, includeType: boolean): string { - const object = toRemoteObject(handle); - if (object.objectId) { - let type: string = object.subtype || object.type; - // FIXME: promise doesn't have special subtype in WebKit. - if (object.className === 'Promise') - type = 'promise'; - return 'JSHandle@' + type; - } - return (includeType ? 'JSHandle:' : '') + valueFromRemoteObject(object); - } - - private _convertArgument(arg: js.JSHandle | any) : Protocol.Runtime.CallArgument { - const objectHandle = arg && (arg instanceof js.JSHandle) ? arg : null; - if (objectHandle) { - if (objectHandle._context._delegate !== this) - throw new Error('JSHandles can be evaluated only in the context they were created!'); - if (objectHandle._disposed) - throw new Error('JSHandle is disposed!'); - const remoteObject = toRemoteObject(arg); - if (!remoteObject.objectId) - return { value: valueFromRemoteObject(remoteObject) }; - return { objectId: remoteObject.objectId }; - } - return { value: arg }; - } -} - -const suffix = `//# sourceURL=${EVALUATION_SCRIPT_URL}`; -const contextDestroyedResult = { - wasThrown: true, - result: { - description: 'Protocol error: Execution context was destroyed, most likely because of a navigation.' - } as Protocol.Runtime.RemoteObject -}; - -function toRemoteObject(handle: js.JSHandle): Protocol.Runtime.RemoteObject { - return handle._remoteObject as Protocol.Runtime.RemoteObject; -} diff --git a/src/webkit/FrameManager.ts b/src/webkit/FrameManager.ts deleted file mode 100644 index 5bb790b6e6..0000000000 --- a/src/webkit/FrameManager.ts +++ /dev/null @@ -1,440 +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 frames from '../frames'; -import { debugError, helper, RegisteredListener } from '../helper'; -import * as dom from '../dom'; -import * as network from '../network'; -import { TargetSession } from './Connection'; -import { Events } from '../events'; -import { ExecutionContextDelegate, EVALUATION_SCRIPT_URL } from './ExecutionContext'; -import { NetworkManager } from './NetworkManager'; -import { Page, PageDelegate } from '../page'; -import { Protocol } from './protocol'; -import * as dialog from '../dialog'; -import { Browser } from './Browser'; -import { BrowserContext } from '../browserContext'; -import { RawMouseImpl, RawKeyboardImpl } from './Input'; -import * as input from '../input'; -import * as types from '../types'; -import * as jpeg from 'jpeg-js'; -import { PNG } from 'pngjs'; - -const UTILITY_WORLD_NAME = '__playwright_utility_world__'; -const BINDING_CALL_MESSAGE = '__playwright_binding_call__'; - -export class FrameManager implements PageDelegate { - readonly rawMouse: RawMouseImpl; - readonly rawKeyboard: RawKeyboardImpl; - _session: TargetSession; - readonly _page: Page; - private _browser: Browser; - private readonly _networkManager: NetworkManager; - private readonly _contextIdToContext: Map; - private _isolatedWorlds: Set; - private _sessionListeners: RegisteredListener[] = []; - private readonly _bootstrapScripts: string[] = []; - - constructor(browser: Browser, browserContext: BrowserContext) { - this._browser = browser; - this.rawKeyboard = new RawKeyboardImpl(); - this.rawMouse = new RawMouseImpl(); - this._contextIdToContext = new Map(); - this._isolatedWorlds = new Set(); - this._page = new Page(this, browserContext); - this._networkManager = new NetworkManager(this._page); - } - - setSession(session: TargetSession) { - helper.removeEventListeners(this._sessionListeners); - this.disconnectFromTarget(); - this._session = session; - this.rawKeyboard.setSession(session); - this.rawMouse.setSession(session); - this._addSessionListeners(); - this._networkManager.setSession(session); - this._isolatedWorlds = new Set(); - } - - // This method is called for provisional targets as well. The session passed as the parameter - // may be different from the current session and may be destroyed without becoming current. - async _initializeSession(session: TargetSession) { - const promises : Promise[] = [ - // Page agent must be enabled before Runtime. - session.send('Page.enable'), - session.send('Page.getResourceTree').then(({frameTree}) => this._handleFrameTree(frameTree)), - // Resource tree should be received before first execution context. - session.send('Runtime.enable').then(() => this._ensureIsolatedWorld(UTILITY_WORLD_NAME)), - session.send('Console.enable'), - session.send('Page.setInterceptFileChooserDialog', { enabled: true }), - this._networkManager.initializeSession(session), - ]; - if (!session.isProvisional()) { - // FIXME: move dialog agent to web process. - // Dialog agent resides in the UI process and should not be re-enabled on navigation. - promises.push(session.send('Dialog.enable')); - } - const contextOptions = this._page.browserContext()._options; - if (contextOptions.userAgent) - promises.push(session.send('Page.overrideUserAgent', { value: contextOptions.userAgent })); - if (this._page._state.mediaType || this._page._state.colorScheme) - promises.push(this._setEmulateMedia(session, this._page._state.mediaType, this._page._state.colorScheme)); - if (contextOptions.javaScriptEnabled === false) - promises.push(session.send('Emulation.setJavaScriptEnabled', { enabled: false })); - if (contextOptions.bypassCSP) - promises.push(session.send('Page.setBypassCSP', { enabled: true })); - if (this._page._state.extraHTTPHeaders !== null) - promises.push(this._setExtraHTTPHeaders(session, this._page._state.extraHTTPHeaders)); - if (this._page._state.viewport) - promises.push(FrameManager._setViewport(session, this._page._state.viewport)); - await Promise.all(promises); - } - - didClose() { - helper.removeEventListeners(this._sessionListeners); - this._networkManager.dispose(); - this.disconnectFromTarget(); - this._page._didClose(); - } - - _addSessionListeners() { - this._sessionListeners = [ - helper.addEventListener(this._session, 'Page.frameNavigated', event => this._onFrameNavigated(event.frame, false)), - helper.addEventListener(this._session, 'Page.navigatedWithinDocument', event => this._onFrameNavigatedWithinDocument(event.frameId, event.url)), - helper.addEventListener(this._session, 'Page.frameAttached', event => this._onFrameAttached(event.frameId, event.parentFrameId)), - helper.addEventListener(this._session, 'Page.frameDetached', event => this._onFrameDetached(event.frameId)), - helper.addEventListener(this._session, 'Page.frameStoppedLoading', event => this._onFrameStoppedLoading(event.frameId)), - helper.addEventListener(this._session, 'Page.loadEventFired', event => this._onLifecycleEvent(event.frameId, 'load')), - helper.addEventListener(this._session, 'Page.domContentEventFired', event => this._onLifecycleEvent(event.frameId, 'domcontentloaded')), - helper.addEventListener(this._session, 'Runtime.executionContextCreated', event => this._onExecutionContextCreated(event.context)), - helper.addEventListener(this._session, 'Console.messageAdded', event => this._onConsoleMessage(event)), - helper.addEventListener(this._session, 'Dialog.javascriptDialogOpening', event => this._onDialog(event)), - helper.addEventListener(this._session, 'Page.fileChooserOpened', event => this._onFileChooserOpened(event)) - ]; - } - - disconnectFromTarget() { - for (const context of this._contextIdToContext.values()) { - (context._delegate as ExecutionContextDelegate)._dispose(); - context.frame._contextDestroyed(context); - } - this._contextIdToContext.clear(); - } - - _onFrameStoppedLoading(frameId: string) { - this._page._frameManager.frameStoppedLoading(frameId); - } - - _onLifecycleEvent(frameId: string, event: frames.LifecycleEvent) { - this._page._frameManager.frameLifecycleEvent(frameId, event); - } - - _handleFrameTree(frameTree: Protocol.Page.FrameResourceTree) { - this._onFrameAttached(frameTree.frame.id, frameTree.frame.parentId); - this._onFrameNavigated(frameTree.frame, true); - if (!frameTree.childFrames) - return; - - for (const child of frameTree.childFrames) - this._handleFrameTree(child); - } - - _onFrameAttached(frameId: string, parentFrameId: string | null) { - this._page._frameManager.frameAttached(frameId, parentFrameId); - } - - _onFrameNavigated(framePayload: Protocol.Page.Frame, initial: boolean) { - const frame = this._page._frameManager.frame(framePayload.id); - for (const [contextId, context] of this._contextIdToContext) { - if (context.frame === frame) { - (context._delegate as ExecutionContextDelegate)._dispose(); - this._contextIdToContext.delete(contextId); - frame._contextDestroyed(context); - } - } - // Append session id to avoid cross-process loaderId clash. - const documentId = this._session._sessionId + '::' + framePayload.loaderId; - this._page._frameManager.frameCommittedNewDocumentNavigation(framePayload.id, framePayload.url, framePayload.name || '', documentId, initial); - } - - _onFrameNavigatedWithinDocument(frameId: string, url: string) { - this._page._frameManager.frameCommittedSameDocumentNavigation(frameId, url); - } - - _onFrameDetached(frameId: string) { - this._page._frameManager.frameDetached(frameId); - } - - _onExecutionContextCreated(contextPayload : Protocol.Runtime.ExecutionContextDescription) { - if (this._contextIdToContext.has(contextPayload.id)) - return; - const frame = this._page._frameManager.frame(contextPayload.frameId); - if (!frame) - return; - const delegate = new ExecutionContextDelegate(this._session, contextPayload); - const context = new dom.FrameExecutionContext(delegate, frame); - if (contextPayload.isPageContext) - frame._contextCreated('main', context); - else if (contextPayload.name === UTILITY_WORLD_NAME) - frame._contextCreated('utility', context); - this._contextIdToContext.set(contextPayload.id, context); - } - - async navigateFrame(frame: frames.Frame, url: string, referrer: string | undefined): Promise { - await this._session.send('Page.navigate', { url, frameId: frame._id, referrer }); - return {}; // We cannot get loaderId of cross-process navigation in advance. - } - - needsLifecycleResetOnSetContent(): boolean { - return true; - } - - async _onConsoleMessage(event: Protocol.Console.messageAddedPayload) { - const { type, level, text, parameters, url, line: lineNumber, column: columnNumber } = event.message; - if (level === 'debug' && parameters && parameters[0].value === BINDING_CALL_MESSAGE) { - const parsedObjectId = JSON.parse(parameters[1].objectId); - const context = this._contextIdToContext.get(parsedObjectId.injectedScriptId); - this._page._onBindingCalled(parameters[2].value, context); - return; - } - let derivedType: string = type; - if (type === 'log') - derivedType = level; - else if (type === 'timing') - derivedType = 'timeEnd'; - - const mainFrameContext = await this._page.mainFrame()._mainContext(); - const handles = (parameters || []).map(p => { - let context: dom.FrameExecutionContext | null = null; - if (p.objectId) { - const objectId = JSON.parse(p.objectId); - context = this._contextIdToContext.get(objectId.injectedScriptId); - } else { - context = mainFrameContext; - } - return context._createHandle(p); - }); - this._page._addConsoleMessage(derivedType, handles, { url, lineNumber: lineNumber - 1, columnNumber: columnNumber - 1 }, handles.length ? undefined : text); - } - - _onDialog(event: Protocol.Dialog.javascriptDialogOpeningPayload) { - this._page.emit(Events.Page.Dialog, new dialog.Dialog( - event.type as dialog.DialogType, - event.message, - async (accept: boolean, promptText?: string) => { - await this._session.send('Dialog.handleJavaScriptDialog', { accept, promptText }); - }, - event.defaultPrompt)); - } - - async _onFileChooserOpened(event: {frameId: Protocol.Network.FrameId, element: Protocol.Runtime.RemoteObject}) { - const context = await this._page._frameManager.frame(event.frameId)._mainContext(); - const handle = context._createHandle(event.element).asElement()!; - this._page._onFileChooserOpened(handle); - } - - async _ensureIsolatedWorld(name: string) { - if (this._isolatedWorlds.has(name)) - return; - this._isolatedWorlds.add(name); - await this._session.send('Page.createIsolatedWorld', { - name, - source: `//# sourceURL=${EVALUATION_SCRIPT_URL}` - }); - } - - private async _setExtraHTTPHeaders(session: TargetSession, headers: network.Headers): Promise { - await session.send('Network.setExtraHTTPHeaders', { headers }); - } - - private async _setEmulateMedia(session: TargetSession, mediaType: input.MediaType | null, mediaColorScheme: input.ColorScheme | null): Promise { - const promises = []; - promises.push(session.send('Page.setEmulatedMedia', { media: mediaType || '' })); - if (mediaColorScheme !== null) { - let appearance: any = ''; - switch (mediaColorScheme) { - case 'light': appearance = 'Light'; break; - case 'dark': appearance = 'Dark'; break; - } - promises.push(session.send('Page.setForcedAppearance', { appearance })); - } - await Promise.all(promises); - } - - async setExtraHTTPHeaders(headers: network.Headers): Promise { - await this._setExtraHTTPHeaders(this._session, headers); - } - - async setEmulateMedia(mediaType: input.MediaType | null, mediaColorScheme: input.ColorScheme | null): Promise { - await this._setEmulateMedia(this._session, mediaType, mediaColorScheme); - } - - async setViewport(viewport: types.Viewport): Promise { - return FrameManager._setViewport(this._session, viewport); - } - - private static async _setViewport(session: TargetSession, viewport: types.Viewport): Promise { - if (viewport.isMobile || viewport.isLandscape || viewport.hasTouch) - throw new Error('Not implemented'); - const width = viewport.width; - const height = viewport.height; - await session.send('Emulation.setDeviceMetricsOverride', { width, height, deviceScaleFactor: viewport.deviceScaleFactor || 1 }); - } - - setCacheEnabled(enabled: boolean): Promise { - return this._networkManager.setCacheEnabled(enabled); - } - - async reload(): Promise { - await this._session.send('Page.reload'); - } - - goBack(): Promise { - return this._session.send('Page.goBack').then(() => true).catch(error => { - if (error instanceof Error && error.message.includes(`Protocol error (Page.goBack): Failed to go`)) - return false; - throw error; - }); - } - - goForward(): Promise { - return this._session.send('Page.goForward').then(() => true).catch(error => { - if (error instanceof Error && error.message.includes(`Protocol error (Page.goForward): Failed to go`)) - return false; - throw error; - }); - } - - async exposeBinding(name: string, bindingFunction: string): Promise { - const script = `self.${name} = (param) => console.debug('${BINDING_CALL_MESSAGE}', {}, param); ${bindingFunction}`; - this._bootstrapScripts.unshift(script); - const source = this._bootstrapScripts.join(';'); - await this._session.send('Page.setBootstrapScript', { source }); - await Promise.all(this._page.frames().map(frame => frame.evaluate(script).catch(debugError))); - } - - async evaluateOnNewDocument(script: string): Promise { - this._bootstrapScripts.push(script); - const source = this._bootstrapScripts.join(';'); - // TODO(yurys): support process swap on navigation. - await this._session.send('Page.setBootstrapScript', { source }); - } - - async closePage(runBeforeUnload: boolean): Promise { - this._browser._closePage(this._page, runBeforeUnload); - } - - getBoundingBoxForScreenshot(handle: dom.ElementHandle): Promise { - return handle.boundingBox(); - } - - canScreenshotOutsideViewport(): boolean { - return false; - } - - async setBackgroundColor(color?: { r: number; g: number; b: number; a: number; }): Promise { - // TODO: line below crashes, sort it out. - this._session.send('Page.setDefaultBackgroundColorOverride', { color }); - } - - async takeScreenshot(format: string, options: types.ScreenshotOptions, viewport: types.Viewport): Promise { - const rect = options.clip || { x: 0, y: 0, width: viewport.width, height: viewport.height }; - const result = await this._session.send('Page.snapshotRect', { ...rect, coordinateSystem: options.fullPage ? 'Page' : 'Viewport' }); - const prefix = 'data:image/png;base64,'; - let buffer = Buffer.from(result.dataURL.substr(prefix.length), 'base64'); - if (format === 'jpeg') - buffer = jpeg.encode(PNG.sync.read(buffer)).data; - return buffer; - } - - async resetViewport(oldSize: types.Size): Promise { - await this._session.send('Emulation.setDeviceMetricsOverride', { ...oldSize, deviceScaleFactor: 0 }); - } - - async getContentFrame(handle: dom.ElementHandle): Promise { - const nodeInfo = await this._session.send('DOM.describeNode', { - objectId: toRemoteObject(handle).objectId - }); - if (!nodeInfo.contentFrameId) - return null; - return this._page._frameManager.frame(nodeInfo.contentFrameId); - } - - async getOwnerFrame(handle: dom.ElementHandle): Promise { - return handle._context.frame; - } - - isElementHandle(remoteObject: any): boolean { - return (remoteObject as Protocol.Runtime.RemoteObject).subtype === 'node'; - } - - async getBoundingBox(handle: dom.ElementHandle): Promise { - const quads = await this.getContentQuads(handle); - if (!quads || !quads.length) - return null; - let minX = Infinity; - let maxX = -Infinity; - let minY = Infinity; - let maxY = -Infinity; - for (const quad of quads) { - for (const point of quad) { - minX = Math.min(minX, point.x); - maxX = Math.max(maxX, point.x); - minY = Math.min(minY, point.y); - maxY = Math.max(maxY, point.y); - } - } - return { x: minX, y: minY, width: maxX - minX, height: maxY - minY }; - } - - async getContentQuads(handle: dom.ElementHandle): Promise { - const result = await this._session.send('DOM.getContentQuads', { - objectId: toRemoteObject(handle).objectId - }).catch(debugError); - if (!result) - return null; - return result.quads.map(quad => [ - { x: quad[0], y: quad[1] }, - { x: quad[2], y: quad[3] }, - { x: quad[4], y: quad[5] }, - { x: quad[6], y: quad[7] } - ]); - } - - async layoutViewport(): Promise<{ width: number, height: number }> { - return this._page.evaluate(() => ({ width: innerWidth, height: innerHeight })); - } - - async setInputFiles(handle: dom.ElementHandle, files: input.FilePayload[]): Promise { - const objectId = toRemoteObject(handle).objectId; - await this._session.send('DOM.setInputFiles', { objectId, files }); - } - - async adoptElementHandle(handle: dom.ElementHandle, to: dom.FrameExecutionContext): Promise> { - const result = await this._session.send('DOM.resolveNode', { - objectId: toRemoteObject(handle).objectId, - executionContextId: (to._delegate as ExecutionContextDelegate)._contextId - }).catch(debugError); - if (!result || result.object.subtype === 'null') - throw new Error('Unable to adopt element handle from a different document'); - return to._createHandle(result.object) as dom.ElementHandle; - } -} - -function toRemoteObject(handle: dom.ElementHandle): Protocol.Runtime.RemoteObject { - return handle._remoteObject as Protocol.Runtime.RemoteObject; -} diff --git a/src/webkit/Input.ts b/src/webkit/Input.ts deleted file mode 100644 index d14a546ec6..0000000000 --- a/src/webkit/Input.ts +++ /dev/null @@ -1,123 +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 input from '../input'; -import { helper } from '../helper'; -import { macEditingCommands } from '../usKeyboardLayout'; -import { TargetSession } from './Connection'; - -function toModifiersMask(modifiers: Set): number { - // From Source/WebKit/Shared/WebEvent.h - let mask = 0; - if (modifiers.has('Shift')) - mask |= 1; - if (modifiers.has('Control')) - mask |= 2; - if (modifiers.has('Alt')) - mask |= 4; - if (modifiers.has('Meta')) - mask |= 8; - return mask; -} - -export class RawKeyboardImpl implements input.RawKeyboard { - private _session: TargetSession; - - setSession(session: TargetSession) { - this._session = session; - } - - async keydown(modifiers: Set, code: string, keyCode: number, keyCodeWithoutLocation: number, key: string, location: number, autoRepeat: boolean, text: string | undefined): Promise { - const parts = []; - for (const modifier of (['Shift', 'Control', 'Alt', 'Meta']) as input.Modifier[]) { - if (modifiers.has(modifier)) - parts.push(modifier); - } - parts.push(code); - const shortcut = parts.join('+'); - let commands = macEditingCommands[shortcut]; - if (helper.isString(commands)) - commands = [commands]; - await this._session.send('Input.dispatchKeyEvent', { - type: 'keyDown', - modifiers: toModifiersMask(modifiers), - windowsVirtualKeyCode: keyCode, - code, - key, - text, - unmodifiedText: text, - autoRepeat, - macCommands: commands, - isKeypad: location === input.keypadLocation - }); - } - - async keyup(modifiers: Set, code: string, keyCode: number, keyCodeWithoutLocation: number, key: string, location: number): Promise { - await this._session.send('Input.dispatchKeyEvent', { - type: 'keyUp', - modifiers: toModifiersMask(modifiers), - key, - windowsVirtualKeyCode: keyCode, - code, - isKeypad: location === input.keypadLocation - }); - } - - async sendText(text: string): Promise { - await this._session.send('Page.insertText', { text }); - } -} - -export class RawMouseImpl implements input.RawMouse { - private _client: TargetSession; - - setSession(client: TargetSession) { - this._client = client; - } - - async move(x: number, y: number, button: input.Button | 'none', buttons: Set, modifiers: Set): Promise { - await this._client.send('Input.dispatchMouseEvent', { - type: 'move', - button, - x, - y, - modifiers: toModifiersMask(modifiers) - }); - } - - async down(x: number, y: number, button: input.Button, buttons: Set, modifiers: Set, clickCount: number): Promise { - await this._client.send('Input.dispatchMouseEvent', { - type: 'down', - button, - x, - y, - modifiers: toModifiersMask(modifiers), - clickCount - }); - } - - async up(x: number, y: number, button: input.Button, buttons: Set, modifiers: Set, clickCount: number): Promise { - await this._client.send('Input.dispatchMouseEvent', { - type: 'up', - button, - x, - y, - modifiers: toModifiersMask(modifiers), - clickCount - }); - } -} diff --git a/src/webkit/Launcher.ts b/src/webkit/Launcher.ts deleted file mode 100644 index cd6b14bc64..0000000000 --- a/src/webkit/Launcher.ts +++ /dev/null @@ -1,179 +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 { assert } from '../helper'; -import { Browser } from './Browser'; -import { BrowserFetcher, BrowserFetcherOptions } from '../browserFetcher'; -import { PipeTransport, SlowMoTransport } from '../transport'; -import { execSync } from 'child_process'; -import * as path from 'path'; -import * as util from 'util'; -import * as os from 'os'; -import { launchProcess } from '../processLauncher'; -import { BrowserServer } from '../browser'; - -const DEFAULT_ARGS = [ -]; - -export class Launcher { - private _projectRoot: string; - private _preferredRevision: string; - - constructor(projectRoot: string, preferredRevision: string) { - this._projectRoot = projectRoot; - this._preferredRevision = preferredRevision; - } - - defaultArgs(options: any = {}) { - const { - args = [], - } = options; - const webkitArguments = [...DEFAULT_ARGS]; - webkitArguments.push(...args); - return webkitArguments; - } - - async launch(options: LauncherLaunchOptions = {}): Promise> { - const { - ignoreDefaultArgs = false, - args = [], - dumpio = false, - executablePath = null, - env = process.env, - handleSIGINT = true, - handleSIGTERM = true, - handleSIGHUP = true, - slowMo = 0, - } = options; - - const webkitArguments = []; - if (!ignoreDefaultArgs) - webkitArguments.push(...this.defaultArgs(options)); - else - webkitArguments.push(...args); - - let webkitExecutable = executablePath; - if (!executablePath) { - const {missingText, executablePath} = this._resolveExecutablePath(); - if (missingText) - throw new Error(missingText); - webkitExecutable = executablePath; - } - webkitArguments.push('--inspector-pipe'); - // Headless options is only implemented on Mac at the moment. - if (process.platform === 'darwin' && options.headless !== false) - webkitArguments.push('--headless'); - - const launchedProcess = await launchProcess({ - executablePath: webkitExecutable, - args: webkitArguments, - env, - handleSIGINT, - handleSIGTERM, - handleSIGHUP, - dumpio, - pipe: true, - tempDir: null - }, () => { - if (!browser) - return Promise.reject(); - browser.close(); - }); - - let browser: Browser | undefined; - try { - const transport = new PipeTransport(launchedProcess.stdio[3] as NodeJS.WritableStream, launchedProcess.stdio[4] as NodeJS.ReadableStream); - browser = new Browser(SlowMoTransport.wrap(transport, slowMo)); - await browser._waitForTarget(t => t._type === 'page'); - return new BrowserServer(browser, launchedProcess, ''); - } catch (e) { - if (browser) - await browser.close(); - throw e; - } - } - - executablePath(): string { - return this._resolveExecutablePath().executablePath; - } - - _resolveExecutablePath(): { executablePath: string; missingText: string | null; } { - const browserFetcher = createBrowserFetcher(this._projectRoot); - const revisionInfo = browserFetcher.revisionInfo(this._preferredRevision); - const missingText = !revisionInfo.local ? `WebKit revision is not downloaded. Run "npm install" or "yarn install"` : null; - return {executablePath: revisionInfo.executablePath, missingText}; - } - -} - -export type LauncherLaunchOptions = { - ignoreDefaultArgs?: boolean, - args?: string[], - executablePath?: string, - handleSIGINT?: boolean, - handleSIGTERM?: boolean, - handleSIGHUP?: boolean, - headless?: boolean, - dumpio?: boolean, - env?: {[key: string]: string} | undefined, - slowMo?: number, -}; - -let cachedMacVersion = undefined; -function getMacVersion() { - if (!cachedMacVersion) { - const [major, minor] = execSync('sw_vers -productVersion').toString('utf8').trim().split('.'); - cachedMacVersion = major + '.' + minor; - } - return cachedMacVersion; -} - -export function createBrowserFetcher(projectRoot: string, options: BrowserFetcherOptions = {}): BrowserFetcher { - const downloadURLs = { - linux: '%s/builds/webkit/%s/minibrowser-linux.zip', - mac: '%s/builds/webkit/%s/minibrowser-mac-%s.zip', - }; - - const defaultOptions = { - path: path.join(projectRoot, '.local-webkit'), - 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 'linux'; // Windows gets linux binaries and uses WSL - return platform; - })() - }; - options = { - ...defaultOptions, - ...options, - }; - assert(!!downloadURLs[options.platform], 'Unsupported platform: ' + options.platform); - - return new BrowserFetcher(options.path, options.platform, (platform: string, revision: string) => { - return { - downloadUrl: (platform === 'mac') ? - util.format(downloadURLs[platform], options.host, revision, getMacVersion()) : - util.format(downloadURLs[platform], options.host, revision), - executablePath: 'pw_run.sh', - }; - }); -} diff --git a/src/webkit/NetworkManager.ts b/src/webkit/NetworkManager.ts deleted file mode 100644 index 7f4873e4ed..0000000000 --- a/src/webkit/NetworkManager.ts +++ /dev/null @@ -1,173 +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 { TargetSession } from './Connection'; -import { Page } from '../page'; -import { helper, RegisteredListener } from '../helper'; -import { Protocol } from './protocol'; -import * as network from '../network'; -import * as frames from '../frames'; - -export class NetworkManager { - private _session: TargetSession; - private _page: Page; - private _requestIdToRequest = new Map(); - private _attemptedAuthentications = new Set(); - private _userCacheDisabled = false; - private _sessionListeners: RegisteredListener[] = []; - - constructor(page: Page) { - this._page = page; - } - - setSession(session: TargetSession) { - helper.removeEventListeners(this._sessionListeners); - this._session = session; - this._sessionListeners = [ - helper.addEventListener(this._session, 'Network.requestWillBeSent', this._onRequestWillBeSent.bind(this)), - helper.addEventListener(this._session, 'Network.responseReceived', this._onResponseReceived.bind(this)), - helper.addEventListener(this._session, 'Network.loadingFinished', this._onLoadingFinished.bind(this)), - helper.addEventListener(this._session, 'Network.loadingFailed', this._onLoadingFailed.bind(this)), - ]; - } - - async initializeSession(session: TargetSession) { - await session.send('Network.enable'); - } - - dispose() { - helper.removeEventListeners(this._sessionListeners); - } - - async setCacheEnabled(enabled: boolean) { - this._userCacheDisabled = !enabled; - await this._updateProtocolCacheDisabled(); - } - - async _updateProtocolCacheDisabled() { - await this._session.send('Network.setResourceCachingDisabled', { - disabled: this._userCacheDisabled - }); - } - - _onRequestWillBeSent(event: Protocol.Network.requestWillBeSentPayload) { - let redirectChain: network.Request[] = []; - if (event.redirectResponse) { - const request = this._requestIdToRequest.get(event.requestId); - // If we connect late to the target, we could have missed the requestWillBeSent event. - if (request) { - this._handleRequestRedirect(request, event.redirectResponse); - redirectChain = request.request._redirectChain; - } - } - const frame = this._page._frameManager.frame(event.frameId); - // TODO(einbinder) this will fail if we are an XHR document request - const isNavigationRequest = event.type === 'Document'; - const documentId = isNavigationRequest ? this._session._sessionId + '::' + event.loaderId : undefined; - const request = new InterceptableRequest(frame, undefined, event, redirectChain, documentId); - this._requestIdToRequest.set(event.requestId, request); - this._page._frameManager.requestStarted(request.request); - } - - _createResponse(request: InterceptableRequest, responsePayload: Protocol.Network.Response): network.Response { - const remoteAddress: network.RemoteAddress = { ip: '', port: 0 }; - const getResponseBody = async () => { - const response = await this._session.send('Network.getResponseBody', { requestId: request._requestId }); - return Buffer.from(response.body, response.base64Encoded ? 'base64' : 'utf8'); - }; - return new network.Response(request.request, responsePayload.status, responsePayload.statusText, headersObject(responsePayload.headers), remoteAddress, getResponseBody); - } - - _handleRequestRedirect(request: InterceptableRequest, responsePayload: Protocol.Network.Response) { - const response = this._createResponse(request, responsePayload); - request.request._redirectChain.push(request.request); - response._requestFinished(new Error('Response body is unavailable for redirect responses')); - this._requestIdToRequest.delete(request._requestId); - this._attemptedAuthentications.delete(request._interceptionId); - this._page._frameManager.requestReceivedResponse(response); - this._page._frameManager.requestFinished(request.request); - } - - _onResponseReceived(event: Protocol.Network.responseReceivedPayload) { - const request = this._requestIdToRequest.get(event.requestId); - // FileUpload sends a response without a matching request. - if (!request) - return; - const response = this._createResponse(request, event.response); - this._page._frameManager.requestReceivedResponse(response); - } - - _onLoadingFinished(event: Protocol.Network.loadingFinishedPayload) { - const request = this._requestIdToRequest.get(event.requestId); - // For certain requestIds we never receive requestWillBeSent event. - // @see https://crbug.com/750469 - if (!request) - return; - - // Under certain conditions we never get the Network.responseReceived - // event from protocol. @see https://crbug.com/883475 - if (request.request.response()) - request.request.response()._requestFinished(); - this._requestIdToRequest.delete(request._requestId); - this._attemptedAuthentications.delete(request._interceptionId); - this._page._frameManager.requestFinished(request.request); - } - - _onLoadingFailed(event: Protocol.Network.loadingFailedPayload) { - const request = this._requestIdToRequest.get(event.requestId); - // For certain requestIds we never receive requestWillBeSent event. - // @see https://crbug.com/750469 - if (!request) - return; - const response = request.request.response(); - if (response) - response._requestFinished(); - this._requestIdToRequest.delete(request._requestId); - this._attemptedAuthentications.delete(request._interceptionId); - request.request._setFailureText(event.errorText); - this._page._frameManager.requestFailed(request.request, event.errorText.includes('cancelled')); - } -} - -const interceptableRequestSymbol = Symbol('interceptableRequest'); - -export function toInterceptableRequest(request: network.Request): InterceptableRequest { - return (request as any)[interceptableRequestSymbol]; -} - -class InterceptableRequest { - readonly request: network.Request; - _requestId: string; - _interceptionId: string; - _documentId: string | undefined; - - constructor(frame: frames.Frame | null, interceptionId: string, event: Protocol.Network.requestWillBeSentPayload, redirectChain: network.Request[], documentId: string | undefined) { - this._requestId = event.requestId; - this._interceptionId = interceptionId; - this._documentId = documentId; - this.request = new network.Request(frame, redirectChain, documentId, event.request.url, - event.type ? event.type.toLowerCase() : 'Unknown', event.request.method, event.request.postData, headersObject(event.request.headers)); - (this.request as any)[interceptableRequestSymbol] = this; - } -} - -function headersObject(headers: Protocol.Network.Headers): network.Headers { - const result: network.Headers = {}; - for (const key of Object.keys(headers)) - result[key.toLowerCase()] = headers[key]; - return result; -} diff --git a/src/webkit/Playwright.ts b/src/webkit/Playwright.ts deleted file mode 100644 index c2a035e0cb..0000000000 --- a/src/webkit/Playwright.ts +++ /dev/null @@ -1,74 +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 { BrowserFetcher, BrowserFetcherOptions, OnProgressCallback, BrowserFetcherRevisionInfo } from '../browserFetcher'; -import { DeviceDescriptors } from '../deviceDescriptors'; -import * as Errors from '../errors'; -import { Launcher, LauncherLaunchOptions, createBrowserFetcher } from './Launcher'; -import { Browser } from './Browser'; - -export class Playwright { - private _projectRoot: string; - private _launcher: Launcher; - readonly _revision: string; - - constructor(projectRoot: string, preferredRevision: string) { - this._projectRoot = projectRoot; - this._launcher = new Launcher(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: (LauncherLaunchOptions) | undefined): Promise { - const server = await this._launcher.launch(options); - return server.connect(); - } - - async launchServer(options: (LauncherLaunchOptions) | undefined): Promise> { - return this._launcher.launch(options); - } - - executablePath(): string { - return this._launcher.executablePath(); - } - - get devices(): any { - const result = DeviceDescriptors.slice(); - for (const device of DeviceDescriptors) - result[device.name] = device; - return result; - } - - 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/webkit/Target.ts b/src/webkit/Target.ts deleted file mode 100644 index 40c89b2792..0000000000 --- a/src/webkit/Target.ts +++ /dev/null @@ -1,103 +0,0 @@ -/** - * Copyright 2019 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 { BrowserContext } from '../browserContext'; -import { Page } from '../page'; -import { Protocol } from './protocol'; -import { TargetSession, TargetSessionEvents } from './Connection'; -import { FrameManager } from './FrameManager'; -import { Browser } from './Browser'; - -const targetSymbol = Symbol('target'); - -export class Target { - readonly _browserContext: BrowserContext; - readonly _targetId: string; - readonly _type: 'page' | 'service-worker' | 'worker'; - private readonly _session: TargetSession; - private _pagePromise: Promise | null = null; - private _browser: Browser; - _frameManager: FrameManager | null = null; - - static fromPage(page: Page): Target { - return (page as any)[targetSymbol]; - } - - constructor(browser: Browser, session: TargetSession, targetInfo: Protocol.Target.TargetInfo, browserContext: BrowserContext) { - const {targetId, type} = targetInfo; - this._browser = browser; - this._session = session; - this._browserContext = browserContext; - this._targetId = targetId; - this._type = type; - /** @type {?Promise} */ - this._pagePromise = null; - } - - _didClose() { - if (this._frameManager) - this._frameManager.didClose(); - } - - async _initializeSession(session: TargetSession) { - if (!this._frameManager) - return; - await this._frameManager._initializeSession(session).catch(e => { - // Swallow initialization errors due to newer target swap in, - // since we will reinitialize again. - if (this._frameManager) - throw e; - }); - } - - async _swapWith(oldTarget: Target) { - if (!oldTarget._pagePromise) - return; - this._pagePromise = oldTarget._pagePromise; - this._frameManager = oldTarget._frameManager; - // Swapped out target should not be accessed by anyone. Reset page promise so that - // old target does not close the page on connection reset. - oldTarget._pagePromise = null; - oldTarget._frameManager = null; - this._adoptPage(); - } - - private _adoptPage() { - (this._frameManager._page as any)[targetSymbol] = this; - this._session.once(TargetSessionEvents.Disconnected, () => { - // Once swapped out, we reset _page and won't call _didDisconnect for old session. - if (this._frameManager) - this._frameManager._page._didDisconnect(); - }); - this._frameManager.setSession(this._session); - } - - async page(): Promise { - if (this._type === 'page' && !this._pagePromise) { - this._frameManager = new FrameManager(this._browser, this._browserContext); - // Reference local page variable as |this._frameManager| may be - // cleared on swap. - const page = this._frameManager._page; - this._pagePromise = new Promise(async f => { - this._adoptPage(); - await this._initializeSession(this._session); - f(page); - }); - } - return this._pagePromise; - } -} diff --git a/src/webkit/events.ts b/src/webkit/events.ts deleted file mode 100644 index 3faa0cbfeb..0000000000 --- a/src/webkit/events.ts +++ /dev/null @@ -1,22 +0,0 @@ -/** - * Copyright 2019 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. - */ - -export const Events = { - Browser: { - Disconnected: 'disconnected' - }, -}; diff --git a/src/webkit/protocolHelper.ts b/src/webkit/protocolHelper.ts index fc7ddf4c8c..2bc6995163 100644 --- a/src/webkit/protocolHelper.ts +++ b/src/webkit/protocolHelper.ts @@ -16,7 +16,7 @@ */ import { assert, debugError } from '../helper'; -import { TargetSession } from './Connection'; +import { WKTargetSession } from './wkConnection'; import { Protocol } from './protocol'; export function valueFromRemoteObject(remoteObject: Protocol.Runtime.RemoteObject): any { @@ -43,7 +43,7 @@ export function valueFromRemoteObject(remoteObject: Protocol.Runtime.RemoteObjec return remoteObject.value; } -export async function releaseObject(client: TargetSession, remoteObject: Protocol.Runtime.RemoteObject) { +export async function releaseObject(client: WKTargetSession, remoteObject: Protocol.Runtime.RemoteObject) { if (!remoteObject.objectId) return; await client.send('Runtime.releaseObject', {objectId: remoteObject.objectId}).catch(error => { diff --git a/webkit.js b/webkit.js index 081c1e6e4e..aa8f1bec1f 100644 --- a/webkit.js +++ b/webkit.js @@ -22,7 +22,7 @@ for (const className in api.WebKit) { helper.installAsyncStackHooks(api.WebKit[className]); } -const {Playwright} = require('./lib/webkit/Playwright'); +const { WKPlaywright } = require('./lib/webkit/wkPlaywright'); const packageJson = require('./package.json'); -module.exports = new Playwright(__dirname, packageJson.playwright.webkit_revision); +module.exports = new WKPlaywright(__dirname, packageJson.playwright.webkit_revision);