chore: rename the world

This commit is contained in:
Pavel 2019-12-19 16:40:44 -08:00
parent b746733306
commit 91c309797d
47 changed files with 27 additions and 7426 deletions

View File

@ -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);

View File

@ -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);

View File

@ -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<string, BrowserContext>();
_targets = new Map<string, Target>();
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<Page[]> => {
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<Page> => {
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<any>[] = [ 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<void> => {
assert(contextId, 'Non-incognito profiles cannot be closed!');
await this._client.send('Target.disposeBrowserContext', {browserContextId: contextId || undefined});
this._contexts.delete(contextId);
},
cookies: async (): Promise<network.NetworkCookie[]> => {
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<void> => {
await this._client.send('Storage.clearCookies', { browserContextId: contextId || undefined });
},
setCookies: async (cookies: network.SetNetworkCookieParam[]): Promise<void> => {
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<BrowserContext> {
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<Target> {
const {
timeout = 30000
} = options;
const existingTarget = this._allTargets().find(predicate);
if (existingTarget)
return existingTarget;
let resolve: (target: Target) => void;
const targetPromise = new Promise<Target>(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<Worker | null> {
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<Buffer> {
assert(this._tracingClient, 'Tracing was not started.');
let fulfill: (buffer: Buffer) => void;
const contentPromise = new Promise<Buffer>(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;
}
}

View File

@ -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<string, CDPSession>();
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<CDPSession> {
const { sessionId } = await this.rootSession.send('Target.attachToTarget', { targetId: targetInfo.targetId, flatten: true });
return this._sessions.get(sessionId);
}
async createBrowserSession(): Promise<CDPSession> {
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<number, {resolve:(o: any) => void, reject: (e: Error) => void, error: Error, method: string}>();
private _targetType: string;
private _sessionId: string;
on: <T extends keyof Protocol.Events | symbol>(event: T, listener: (payload: T extends symbol ? any : Protocol.Events[T extends keyof Protocol.Events ? T : never]) => void) => this;
addListener: <T extends keyof Protocol.Events | symbol>(event: T, listener: (payload: T extends symbol ? any : Protocol.Events[T extends keyof Protocol.Events ? T : never]) => void) => this;
off: <T extends keyof Protocol.Events | symbol>(event: T, listener: (payload: T extends symbol ? any : Protocol.Events[T extends keyof Protocol.Events ? T : never]) => void) => this;
removeListener: <T extends keyof Protocol.Events | symbol>(event: T, listener: (payload: T extends symbol ? any : Protocol.Events[T extends keyof Protocol.Events ? T : never]) => void) => this;
once: <T extends keyof Protocol.Events | symbol>(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<T extends keyof Protocol.CommandParameters>(
method: T,
params?: Protocol.CommandParameters[T]
): Promise<Protocol.CommandReturnValues[T]> {
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;
}

View File

@ -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<any> {
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<Map<string, js.JSHandle>> {
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<void> {
await releaseObject(this._client, toRemoteObject(handle));
}
async handleJSONValue<T>(handle: js.JSHandle<T>): Promise<T> {
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;
}

View File

@ -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<number, dom.FrameExecutionContext>();
private _isolatedWorlds = new Set<string>();
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<frames.GotoResult> {
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<void> {
await this._client.send('Network.setExtraHTTPHeaders', { headers });
}
async setViewport(viewport: types.Viewport): Promise<void> {
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<void> {
const features = mediaColorScheme ? [{ name: 'prefers-color-scheme', value: mediaColorScheme }] : [];
await this._client.send('Emulation.setEmulatedMedia', { media: mediaType || '', features });
}
setCacheEnabled(enabled: boolean): Promise<void> {
return this._networkManager.setCacheEnabled(enabled);
}
async reload(): Promise<void> {
await this._client.send('Page.reload');
}
private async _go(delta: number): Promise<boolean> {
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<boolean> {
return this._go(-1);
}
goForward(): Promise<boolean> {
return this._go(+1);
}
async evaluateOnNewDocument(source: string): Promise<void> {
await this._client.send('Page.addScriptToEvaluateOnNewDocument', { source });
}
async closePage(runBeforeUnload: boolean): Promise<void> {
if (runBeforeUnload)
await this._client.send('Page.close');
else
await this._browser._closePage(this._page);
}
async getBoundingBoxForScreenshot(handle: dom.ElementHandle<Node>): Promise<types.Rect | null> {
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<void> {
await this._client.send('Emulation.setDefaultBackgroundColorOverride', { color });
}
async takeScreenshot(format: 'png' | 'jpeg', options: types.ScreenshotOptions): Promise<Buffer> {
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<void> {
await this._client.send('Emulation.setDeviceMetricsOverride', { mobile: false, width: 0, height: 0, deviceScaleFactor: 0 });
}
async getContentFrame(handle: dom.ElementHandle): Promise<frames.Frame | null> {
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<frames.Frame | null> {
// 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<types.Rect | null> {
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<types.Quad[] | null> {
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<void> {
await handle.evaluate(input.setFileInputFunction, files);
}
async adoptElementHandle<T extends Node>(handle: dom.ElementHandle<T>, to: dom.FrameExecutionContext): Promise<dom.ElementHandle<T>> {
const nodeInfo = await this._client.send('DOM.describeNode', {
objectId: toRemoteObject(handle).objectId,
});
return this.adoptBackendNodeId(nodeInfo.node.backendNodeId, to) as Promise<dom.ElementHandle<T>>;
}
async adoptBackendNodeId(backendNodeId: Protocol.DOM.BackendNodeId, to: dom.FrameExecutionContext): Promise<dom.ElementHandle> {
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;
}

View File

@ -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<input.Modifier>): 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<input.Modifier>, code: string, keyCode: number, keyCodeWithoutLocation: number, key: string, location: number, autoRepeat: boolean, text: string | undefined): Promise<void> {
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<input.Modifier>, code: string, keyCode: number, keyCodeWithoutLocation: number, key: string, location: number): Promise<void> {
await this._client.send('Input.dispatchKeyEvent', {
type: 'keyUp',
modifiers: toModifiersMask(modifiers),
key,
windowsVirtualKeyCode: keyCodeWithoutLocation,
code,
location
});
}
async sendText(text: string): Promise<void> {
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<input.Button>, modifiers: Set<input.Modifier>): Promise<void> {
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<input.Button>, modifiers: Set<input.Modifier>, clickCount: number): Promise<void> {
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<input.Button>, modifiers: Set<input.Modifier>, clickCount: number): Promise<void> {
await this._client.send('Input.dispatchMouseEvent', {
type: 'mouseReleased',
button,
x,
y,
modifiers: toModifiersMask(modifiers),
clickCount
});
}
}

View File

@ -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<BrowserServer<Browser>> {
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
};
});
}

View File

@ -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<string, InterceptableRequest>();
private _requestIdToRequestWillBeSentEvent = new Map<string, Protocol.Network.requestWillBeSentPayload>();
private _offline = false;
private _credentials: {username: string, password: string} | null = null;
private _attemptedAuthentications = new Set<string>();
private _userRequestInterceptionEnabled = false;
private _protocolRequestInterceptionEnabled = false;
private _userCacheDisabled = false;
private _requestIdToInterceptionId = new Map<string, string>();
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',
};

View File

@ -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<BrowserFetcherRevisionInfo> {
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<browsers.Browser> {
const server = await this._launcher.launch(options);
return server.connect();
}
async launchServer(options: (LauncherLaunchOptions & LauncherChromeArgOptions & ConnectionOptions) = {}): Promise<browsers.BrowserServer<Browser>> {
return this._launcher.launch(options);
}
async connect(options: (ConnectionOptions & {
browserWSEndpoint?: string;
browserURL?: string;
transport?: ConnectionTransport; })): Promise<Browser> {
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<string> {
let resolve: (url: string) => void;
let reject: (e: Error) => void;
const promise = new Promise<string>((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;
});
}

View File

@ -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<CDPSession>;
private _pagePromise: Promise<Page> | null = null;
private _frameManager: FrameManager | null = null;
private _workerPromise: Promise<Worker> | null = null;
_initializedPromise: Promise<boolean>;
_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<CDPSession>) {
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<Page | null> {
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<Worker | null> {
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<CDPSession> {
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;
}
}
}

View File

@ -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';

View File

@ -17,7 +17,6 @@
export const Events = {
Browser: {
Disconnected: 'disconnected',
TargetCreated: 'targetcreated',
TargetDestroyed: 'targetdestroyed',
TargetChanged: 'targetchanged',

View File

@ -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<SerializedAXNode> {
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<AXNode> = new Set();
collectInterestingNodes(interestingNodes, defaultRoot, false);
if (!interestingNodes.has(needle))
return null;
return serializeTree(needle, interestingNodes)[0];
}
}
function collectInterestingNodes(collection: Set<AXNode>, 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<AXNode>): 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<string, number | string | boolean> = 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<keyof SerializedAXNode> = [
'name',
'value',
'description',
'keyshortcuts',
'roledescription',
'valuetext',
];
for (const userStringProperty of userStringProperties) {
if (!properties.has(userStringProperty))
continue;
node[userStringProperty] = properties.get(userStringProperty);
}
const booleanProperties: Array<keyof SerializedAXNode> = [
'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<keyof SerializedAXNode> = [
'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<keyof SerializedAXNode> = [
'level',
'valuemax',
'valuemin',
];
for (const numericalProperty of numericalProperties) {
if (!properties.has(numericalProperty))
continue;
node[numericalProperty] = properties.get(numericalProperty);
}
const tokenProperties: Array<keyof SerializedAXNode> = [
'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<string, AXNode> = 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;
}
}

View File

@ -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<CoverageEntry[]> {
return await this._jsCoverage.stop();
}
async startCSSCoverage(options: { resetOnNavigation?: boolean; } = {}) {
return await this._cssCoverage.start(options);
}
async stopCSSCoverage(): Promise<CoverageEntry[]> {
return await this._cssCoverage.stop();
}
}
class JSCoverage {
_client: CDPSession;
_enabled: boolean;
_scriptURLs: Map<string, string>;
_scriptSources: Map<string, string>;
_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<CoverageEntry[]> {
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<string, string>;
_stylesheetSources: Map<string, string>;
_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<CoverageEntry[]> {
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);
}

View File

@ -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);
}
}

View File

@ -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<void> {
if (this._geolocation)
await (page._delegate as FrameManager)._client.send('Emulation.setGeolocationOverride', this._geolocation);
}
}

View File

@ -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<Buffer> {
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);
}
}

View File

@ -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<string, Protocol.Browser.PermissionType>([
['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});
}
}

View File

@ -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<string, Worker>();
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<js.ExecutionContext>;
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);
}
}

View File

@ -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<Buffer> {
export async function readProtocolStream(client: CRSession, handle: string, path: string | null): Promise<Buffer> {
let eof = false;
let file;
if (path)

View File

@ -16,6 +16,10 @@
*/
export const Events = {
Browser: {
Disconnected: 'disconnected'
},
Page: {
Close: 'close',
Console: 'console',

View File

@ -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<string, Target>;
private _defaultContext: BrowserContext;
private _contexts: Map<string, BrowserContext>;
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<string>) {
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<BrowserContext> {
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<BrowserContext> {
return [this._defaultContext, ...Array.from(this._contexts.values())];
}
defaultContext() {
return this._defaultContext;
}
async _waitForTarget(predicate: (target: Target) => boolean, options: { timeout?: number; } = {}): Promise<Target> {
const {
timeout = 30000
} = options;
const existingTarget = this._allTargets().find(predicate);
if (existingTarget)
return existingTarget;
let resolve: (t: Target) => void;
const targetPromise = new Promise<Target>(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<Page[]> => {
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<Page> => {
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<any>[] = [];
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<void> => {
assert(browserContextId, 'Non-incognito profiles cannot be closed!');
await this._connection.send('Target.removeBrowserContext', { browserContextId });
this._contexts.delete(browserContextId);
},
cookies: async (): Promise<network.NetworkCookie[]> => {
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<void> => {
await this._connection.send('Browser.clearCookies', { browserContextId: browserContextId || undefined });
},
setCookies: async (cookies: network.SetNetworkCookieParam[]): Promise<void> => {
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<Page>;
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<Page> {
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;
}
}

View File

@ -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<number, {resolve: Function, reject: Function, error: Error, method: string}>;
private _transport: ConnectionTransport;
private _sessions: Map<string, JugglerSession>;
_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<any> {
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<JugglerSession> {
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<number, {resolve: Function, reject: Function, error: Error, method: string}>;
private _targetType: string;
private _sessionId: string;
on: <T extends keyof Protocol.Events | symbol>(event: T, listener: (payload: T extends symbol ? any : Protocol.Events[T extends keyof Protocol.Events ? T : never]) => void) => this;
addListener: <T extends keyof Protocol.Events | symbol>(event: T, listener: (payload: T extends symbol ? any : Protocol.Events[T extends keyof Protocol.Events ? T : never]) => void) => this;
off: <T extends keyof Protocol.Events | symbol>(event: T, listener: (payload: T extends symbol ? any : Protocol.Events[T extends keyof Protocol.Events ? T : never]) => void) => this;
removeListener: <T extends keyof Protocol.Events | symbol>(event: T, listener: (payload: T extends symbol ? any : Protocol.Events[T extends keyof Protocol.Events ? T : never]) => void) => this;
once: <T extends keyof Protocol.Events | symbol>(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<T extends keyof Protocol.CommandParameters>(
method: T,
params?: Protocol.CommandParameters[T]
): Promise<Protocol.CommandReturnValues[T]> {
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;
}

View File

@ -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<any> {
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<Map<string, js.JSHandle>> {
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<void> {
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<T>(handle: js.JSHandle<T>): Promise<T> {
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;
}

View File

@ -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<string, dom.FrameExecutionContext>;
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<void> {
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<frames.GotoResult> {
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<void> {
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<void> {
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<void> {
await this._session.send('Page.setEmulatedMedia', {
type: mediaType === null ? undefined : mediaType,
colorScheme: mediaColorScheme === null ? undefined : mediaColorScheme
});
}
async setCacheEnabled(enabled: boolean): Promise<void> {
await this._session.send('Page.setCacheDisabled', {cacheDisabled: !enabled});
}
async reload(): Promise<void> {
await this._session.send('Page.reload', { frameId: this._page.mainFrame()._id });
}
async goBack(): Promise<boolean> {
const { navigationId } = await this._session.send('Page.goBack', { frameId: this._page.mainFrame()._id });
return navigationId !== null;
}
async goForward(): Promise<boolean> {
const { navigationId } = await this._session.send('Page.goForward', { frameId: this._page.mainFrame()._id });
return navigationId !== null;
}
async evaluateOnNewDocument(source: string): Promise<void> {
await this._session.send('Page.addScriptToEvaluateOnNewDocument', { script: source });
}
async closePage(runBeforeUnload: boolean): Promise<void> {
await this._session.send('Page.close', { runBeforeUnload });
}
getBoundingBoxForScreenshot(handle: dom.ElementHandle<Node>): Promise<types.Rect | null> {
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<void> {
if (color)
throw new Error('Not implemented');
}
async takeScreenshot(format: 'png' | 'jpeg', options: types.ScreenshotOptions): Promise<Buffer> {
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<void> {
await this._session.send('Page.setViewport', { viewport: null });
}
async getContentFrame(handle: dom.ElementHandle): Promise<frames.Frame | null> {
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<frames.Frame | null> {
return handle._context.frame;
}
isElementHandle(remoteObject: any): boolean {
return remoteObject.subtype === 'node';
}
async getBoundingBox(handle: dom.ElementHandle): Promise<types.Rect | null> {
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<types.Quad[] | null> {
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<void> {
await handle.evaluate(input.setFileInputFunction, files);
}
async adoptElementHandle<T extends Node>(handle: dom.ElementHandle<T>, to: dom.FrameExecutionContext): Promise<dom.ElementHandle<T>> {
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;
}

View File

@ -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<input.Modifier>): 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<input.Button>): 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<input.Modifier>, code: string, keyCode: number, keyCodeWithoutLocation: number, key: string, location: number, autoRepeat: boolean, text: string | undefined): Promise<void> {
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<input.Modifier>, code: string, keyCode: number, keyCodeWithoutLocation: number, key: string, location: number): Promise<void> {
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<void> {
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<input.Button>, modifiers: Set<input.Modifier>): Promise<void> {
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<input.Button>, modifiers: Set<input.Modifier>, clickCount: number): Promise<void> {
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<input.Button>, modifiers: Set<input.Modifier>, clickCount: number): Promise<void> {
await this._client.send('Page.dispatchMouseEvent', {
type: 'mouseup',
button: toButtonNumber(button),
buttons: toButtonsMask(buttons),
x,
y,
modifiers: toModifiersMask(modifiers),
clickCount
});
}
}

View File

@ -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<BrowserServer<Browser>> {
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<string> {
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;
}

View File

@ -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<string, InterceptableRequest>;
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);
});
}
}

View File

@ -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<BrowserFetcherRevisionInfo> {
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<Browser> {
const server = await this._launcher.launch(options);
return server.connect();
}
async launchServer(options: any): Promise<browsers.BrowserServer<Browser>> {
return this._launcher.launch(options);
}
async connect(options: { slowMo?: number, browserWSEndpoint: string }): Promise<Browser> {
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);
}
}

View File

@ -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';

View File

@ -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'
},
};

View File

@ -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<SerializedAXNode>;
}
export class Accessibility {
_session: any;
constructor(session) {
this._session = session;
}
async snapshot(options: { interestingOnly?: boolean; } | undefined = {}): Promise<SerializedAXNode> {
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<AXNode> = new Set();
collectInterestingNodes(interestingNodes, root, false);
return serializeTree(root, interestingNodes)[0];
}
}
function collectInterestingNodes(collection: Set<AXNode>, 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<AXNode>): Array<SerializedAXNode> {
const children: Array<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 {
_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<keyof SerializedAXNode> = [
'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<keyof SerializedAXNode> = [
'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<keyof SerializedAXNode> = [
'checked',
'pressed',
];
for (const tristateProperty of tristateProperties) {
if (!(tristateProperty in this._payload))
continue;
const value = this._payload[tristateProperty];
node[tristateProperty] = value;
}
const numericalProperties: Array<keyof SerializedAXNode> = [
'level'
];
for (const numericalProperty of numericalProperties) {
if (!(numericalProperty in this._payload))
continue;
node[numericalProperty] = this._payload[numericalProperty];
}
const tokenProperties: Array<keyof SerializedAXNode> = [
'autocomplete',
'haspopup',
'invalid',
'orientation',
];
for (const tokenProperty of tokenProperties) {
const value = this._payload[tokenProperty];
if (!value || value === 'false')
continue;
node[tokenProperty] = value;
}
return node;
}
}

View File

@ -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();
}
}

View File

@ -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<string>) {
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});
}
}

View File

@ -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<string, BrowserContext>();
_targets = new Map<string, Target>();
private _eventListeners: RegisteredListener[];
private _privateEvents = new EventEmitter();
constructor(transport: ConnectionTransport) {
super();
this._connection = new Connection(transport);
/** @type {!Map<string, !Target>} */
this._targets = new Map();
this._defaultContext = this._createBrowserContext(undefined, {});
/** @type {!Map<string, !BrowserContext>} */
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<BrowserContext> {
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<Target> {
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<Target>(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<void> {
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<Page[]> => {
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<Page> => {
const { targetId } = await this._connection.send('Browser.createPage', { browserContextId });
const target = this._targets.get(targetId);
return await target.page();
},
close: async (): Promise<void> => {
assert(browserContextId, 'Non-incognito profiles cannot be closed!');
await this._connection.send('Browser.deleteContext', { browserContextId });
this._contexts.delete(browserContextId);
},
cookies: async (): Promise<network.NetworkCookie[]> => {
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<void> => {
await this._connection.send('Browser.deleteAllCookies', { browserContextId });
},
setCookies: async (cookies: network.SetNetworkCookieParam[]): Promise<void> => {
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'),
};

View File

@ -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<number, {resolve:(o: any) => void, reject: (e: Error) => void, error: Error, method: string}>();
private readonly _transport: ConnectionTransport;
private readonly _sessions = new Map<string, TargetSession>();
_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<T extends keyof Protocol.CommandParameters>(
method: T,
params?: Protocol.CommandParameters[T]
): Promise<Protocol.CommandReturnValues[T]> {
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<number, {resolve:(o: any) => void, reject: (e: Error) => void, error: Error, method: string}>();
private _targetType: string;
_sessionId: string;
_swappedOut = false;
private _provisionalMessages?: string[];
on: <T extends keyof Protocol.Events | symbol>(event: T, listener: (payload: T extends symbol ? any : Protocol.Events[T extends keyof Protocol.Events ? T : never]) => void) => this;
addListener: <T extends keyof Protocol.Events | symbol>(event: T, listener: (payload: T extends symbol ? any : Protocol.Events[T extends keyof Protocol.Events ? T : never]) => void) => this;
off: <T extends keyof Protocol.Events | symbol>(event: T, listener: (payload: T extends symbol ? any : Protocol.Events[T extends keyof Protocol.Events ? T : never]) => void) => this;
removeListener: <T extends keyof Protocol.Events | symbol>(event: T, listener: (payload: T extends symbol ? any : Protocol.Events[T extends keyof Protocol.Events ? T : never]) => void) => this;
once: <T extends keyof Protocol.Events | symbol>(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<T extends keyof Protocol.CommandParameters>(
method: T,
params?: Protocol.CommandParameters[T]
): Promise<Protocol.CommandReturnValues[T]> {
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<Protocol.CommandReturnValues[T]>((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.');
}

View File

@ -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<string>;
_session: TargetSession;
_contextId: number;
private _contextDestroyedCallback: () => void;
private _executionContextDestroyedPromise: Promise<unknown>;
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<any> {
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<Map<string, js.JSHandle>> {
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<void> {
await releaseObject(this._session, toRemoteObject(handle));
}
async handleJSONValue<T>(handle: js.JSHandle<T>): Promise<T> {
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;
}

View File

@ -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<number, dom.FrameExecutionContext>;
private _isolatedWorlds: Set<string>;
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<any>[] = [
// 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<frames.GotoResult> {
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<void> {
await session.send('Network.setExtraHTTPHeaders', { headers });
}
private async _setEmulateMedia(session: TargetSession, mediaType: input.MediaType | null, mediaColorScheme: input.ColorScheme | null): Promise<void> {
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<void> {
await this._setExtraHTTPHeaders(this._session, headers);
}
async setEmulateMedia(mediaType: input.MediaType | null, mediaColorScheme: input.ColorScheme | null): Promise<void> {
await this._setEmulateMedia(this._session, mediaType, mediaColorScheme);
}
async setViewport(viewport: types.Viewport): Promise<void> {
return FrameManager._setViewport(this._session, viewport);
}
private static async _setViewport(session: TargetSession, viewport: types.Viewport): Promise<void> {
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<void> {
return this._networkManager.setCacheEnabled(enabled);
}
async reload(): Promise<void> {
await this._session.send('Page.reload');
}
goBack(): Promise<boolean> {
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<boolean> {
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<void> {
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<void> {
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<void> {
this._browser._closePage(this._page, runBeforeUnload);
}
getBoundingBoxForScreenshot(handle: dom.ElementHandle<Node>): Promise<types.Rect | null> {
return handle.boundingBox();
}
canScreenshotOutsideViewport(): boolean {
return false;
}
async setBackgroundColor(color?: { r: number; g: number; b: number; a: number; }): Promise<void> {
// TODO: line below crashes, sort it out.
this._session.send('Page.setDefaultBackgroundColorOverride', { color });
}
async takeScreenshot(format: string, options: types.ScreenshotOptions, viewport: types.Viewport): Promise<Buffer> {
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<void> {
await this._session.send('Emulation.setDeviceMetricsOverride', { ...oldSize, deviceScaleFactor: 0 });
}
async getContentFrame(handle: dom.ElementHandle): Promise<frames.Frame | null> {
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<frames.Frame | null> {
return handle._context.frame;
}
isElementHandle(remoteObject: any): boolean {
return (remoteObject as Protocol.Runtime.RemoteObject).subtype === 'node';
}
async getBoundingBox(handle: dom.ElementHandle): Promise<types.Rect | null> {
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<types.Quad[] | null> {
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<void> {
const objectId = toRemoteObject(handle).objectId;
await this._session.send('DOM.setInputFiles', { objectId, files });
}
async adoptElementHandle<T extends Node>(handle: dom.ElementHandle<T>, to: dom.FrameExecutionContext): Promise<dom.ElementHandle<T>> {
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<T>;
}
}
function toRemoteObject(handle: dom.ElementHandle): Protocol.Runtime.RemoteObject {
return handle._remoteObject as Protocol.Runtime.RemoteObject;
}

View File

@ -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<input.Modifier>): 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<input.Modifier>, code: string, keyCode: number, keyCodeWithoutLocation: number, key: string, location: number, autoRepeat: boolean, text: string | undefined): Promise<void> {
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<input.Modifier>, code: string, keyCode: number, keyCodeWithoutLocation: number, key: string, location: number): Promise<void> {
await this._session.send('Input.dispatchKeyEvent', {
type: 'keyUp',
modifiers: toModifiersMask(modifiers),
key,
windowsVirtualKeyCode: keyCode,
code,
isKeypad: location === input.keypadLocation
});
}
async sendText(text: string): Promise<void> {
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<input.Button>, modifiers: Set<input.Modifier>): Promise<void> {
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<input.Button>, modifiers: Set<input.Modifier>, clickCount: number): Promise<void> {
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<input.Button>, modifiers: Set<input.Modifier>, clickCount: number): Promise<void> {
await this._client.send('Input.dispatchMouseEvent', {
type: 'up',
button,
x,
y,
modifiers: toModifiersMask(modifiers),
clickCount
});
}
}

View File

@ -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<BrowserServer<Browser>> {
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',
};
});
}

View File

@ -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<string, InterceptableRequest>();
private _attemptedAuthentications = new Set<string>();
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;
}

View File

@ -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<BrowserFetcherRevisionInfo> {
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<Browser> {
const server = await this._launcher.launch(options);
return server.connect();
}
async launchServer(options: (LauncherLaunchOptions) | undefined): Promise<browsers.BrowserServer<Browser>> {
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);
}
}

View File

@ -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<Page> | 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<!Page>} */
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<Page> {
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;
}
}

View File

@ -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'
},
};

View File

@ -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 => {

View File

@ -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);