feat(workers): support workers in webkit (#400)

This commit is contained in:
Pavel Feldman 2020-01-07 12:59:01 -08:00 committed by GitHub
parent 92b14cf996
commit f75ac45c42
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 351 additions and 199 deletions

View File

@ -138,6 +138,8 @@
* [event: 'requestfailed'](#event-requestfailed)
* [event: 'requestfinished'](#event-requestfinished)
* [event: 'response'](#event-response)
* [event: 'workercreated'](#event-workercreated)
* [event: 'workerdestroyed'](#event-workerdestroyed)
* [page.$(selector)](#pageselector)
* [page.$$(selector)](#pageselector-1)
* [page.$$eval(selector, pageFunction[, ...args])](#pageevalselector-pagefunction-args)
@ -192,6 +194,7 @@
* [page.waitForRequest(urlOrPredicate[, options])](#pagewaitforrequesturlorpredicate-options)
* [page.waitForResponse(urlOrPredicate[, options])](#pagewaitforresponseurlorpredicate-options)
* [page.waitForSelector(selector[, options])](#pagewaitforselectorselector-options)
* [page.workers()](#pageworkers)
- [class: Request](#class-request)
* [request.abort([errorCode])](#requestaborterrorcode)
* [request.continue([overrides])](#requestcontinueoverrides)
@ -248,13 +251,10 @@
* [chromiumPlaywright.launch([options])](#chromiumplaywrightlaunchoptions)
* [chromiumPlaywright.launchServer([options])](#chromiumplaywrightlaunchserveroptions)
- [class: ChromiumPage](#class-chromiumpage)
* [event: 'workercreated'](#event-workercreated)
* [event: 'workerdestroyed'](#event-workerdestroyed)
* [chromiumPage.accessibility](#chromiumpageaccessibility)
* [chromiumPage.coverage](#chromiumpagecoverage)
* [chromiumPage.interception](#chromiumpageinterception)
* [chromiumPage.pdf([options])](#chromiumpagepdfoptions)
* [chromiumPage.workers()](#chromiumpageworkers)
- [class: ChromiumSession](#class-chromiumsession)
* [chromiumSession.detach()](#chromiumsessiondetach)
* [chromiumSession.send(method[, params])](#chromiumsessionsendmethod-params)
@ -1842,6 +1842,16 @@ Emitted when a request finishes successfully.
Emitted when a [response] is received.
#### event: 'workercreated'
- <[Worker]>
Emitted when a dedicated [WebWorker](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API) is spawned by the page.
#### event: 'workerdestroyed'
- <[Worker]>
Emitted when a dedicated [WebWorker](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API) is terminated.
#### page.$(selector)
- `selector` <[string]> A selector to query page for
- returns: <[Promise]<?[ElementHandle]>>
@ -2738,6 +2748,12 @@ const playwright = require('playwright');
```
Shortcut for [page.mainFrame().waitForSelector(selector[, options])](#framewaitforselectorselector-options).
#### page.workers()
- returns: <[Array]<[Worker]>>
This method returns all of the dedicated [WebWorkers](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API) associated with the page.
> **NOTE** This does not contain ServiceWorkers
### class: Request
Whenever the page sends a request, such as for a network resource, the following events are emitted by playwright's page:
@ -3336,16 +3352,6 @@ const browser = await playwright.launch({
The ChromiumPage class represents a Chromium-specific extension of the page.
#### event: 'workercreated'
- <[Worker]>
Emitted when a dedicated [WebWorker](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API) is spawned by the page.
#### event: 'workerdestroyed'
- <[Worker]>
Emitted when a dedicated [WebWorker](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API) is terminated.
#### chromiumPage.accessibility
- returns: <[Accessibility]>
@ -3424,12 +3430,6 @@ The `format` options are:
> 1. Script tags inside templates are not evaluated.
> 2. Page styles are not visible inside templates.
#### chromiumPage.workers()
- returns: <[Array]<[Worker]>>
This method returns all of the dedicated [WebWorkers](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API) associated with the page.
> **NOTE** This does not contain ServiceWorkers
### class: ChromiumSession
* extends: [EventEmitter](https://nodejs.org/api/events.html#events_class_eventemitter)

View File

@ -19,5 +19,4 @@ export { CRSession as ChromiumSession } from './crConnection';
export { ChromiumPage } from './crPage';
export { CRPlaywright as ChromiumPlaywright } from './crPlaywright';
export { CRTarget as ChromiumTarget } from './crTarget';
export { CRCoverage as ChromiumCoverage } from './features/crCoverage';
export { CRWorker as ChromiumWorker } from './features/crWorkers';
export { CRCoverage as ChromiumCoverage } from './crCoverage';

View File

@ -20,7 +20,7 @@ import { Events as CommonEvents } from '../events';
import { assert, helper } from '../helper';
import { BrowserContext, BrowserContextOptions } from '../browserContext';
import { CRConnection, ConnectionEvents, CRSession } from './crConnection';
import { Page } from '../page';
import { Page, Worker } from '../page';
import { CRTarget } from './crTarget';
import { Protocol } from './protocol';
import { CRPage } from './crPage';
@ -28,7 +28,6 @@ import * as browser from '../browser';
import * as network from '../network';
import * as types from '../types';
import * as platform from '../platform';
import { CRWorker } from './features/crWorkers';
import { ConnectionTransport } from '../transport';
import { readProtocolStream } from './crProtocolHelper';
@ -238,7 +237,7 @@ export class CRBrowser extends browser.Browser {
return [...this._targets.values()].find(t => t.type() === 'browser');
}
serviceWorker(target: CRTarget): Promise<CRWorker | null> {
serviceWorker(target: CRTarget): Promise<Worker | null> {
return target._worker();
}

View File

@ -15,11 +15,11 @@
* limitations under the License.
*/
import { CRSession } from '../crConnection';
import { assert, debugError, helper, RegisteredListener } from '../../helper';
import { Protocol } from '../protocol';
import { CRSession } from './crConnection';
import { assert, debugError, helper, RegisteredListener } from '../helper';
import { Protocol } from './protocol';
import { EVALUATION_SCRIPT_URL } from '../crExecutionContext';
import { EVALUATION_SCRIPT_URL } from './crExecutionContext';
type CoverageEntry = {
url: string,

View File

@ -30,9 +30,9 @@ import * as dialog from '../dialog';
import { PageDelegate } from '../page';
import { RawMouseImpl, RawKeyboardImpl } from './crInput';
import { getAccessibilityTree } from './crAccessibility';
import { CRCoverage } from './features/crCoverage';
import { CRPDF, PDFOptions } from './features/crPdf';
import { CRWorkers, CRWorker } from './features/crWorkers';
import { CRCoverage } from './crCoverage';
import { CRPDF, PDFOptions } from './crPdf';
import { CRWorkers } from './crWorkers';
import { CRBrowser } from './crBrowser';
import { BrowserContext } from '../browserContext';
import * as types from '../types';
@ -46,6 +46,7 @@ export class CRPage implements PageDelegate {
_client: CRSession;
private readonly _page: ChromiumPage;
readonly _networkManager: CRNetworkManager;
private _workers: CRWorkers;
private _contextIdToContext = new Map<number, dom.FrameExecutionContext>();
private _isolatedWorlds = new Set<string>();
private _eventListeners: RegisteredListener[];
@ -59,7 +60,8 @@ export class CRPage implements PageDelegate {
this.rawKeyboard = new RawKeyboardImpl(client);
this.rawMouse = new RawMouseImpl(client);
this._page = new ChromiumPage(client, this, browserContext);
this._networkManager = this._page._networkManager;
this._networkManager = new CRNetworkManager(client, this._page);
this._workers = new CRWorkers(client, this._page);
this._eventListeners = [
helper.addEventListener(client, 'Inspector.targetCrashed', event => this._onTargetCrashed()),
@ -484,24 +486,16 @@ export class CRPage implements PageDelegate {
export class ChromiumPage extends Page {
readonly coverage: CRCoverage;
private _pdf: CRPDF;
private _workers: CRWorkers;
_networkManager: CRNetworkManager;
constructor(client: CRSession, delegate: CRPage, browserContext: BrowserContext) {
super(delegate, browserContext);
this.coverage = new CRCoverage(client);
this._pdf = new CRPDF(client);
this._workers = new CRWorkers(client, this, this._addConsoleMessage.bind(this), error => this.emit(Events.Page.PageError, error));
this._networkManager = new CRNetworkManager(client, this);
}
async pdf(options?: PDFOptions): Promise<platform.BufferType> {
return this._pdf.generate(options);
}
workers(): CRWorker[] {
return this._workers.list();
}
}
function toRemoteObject(handle: dom.ElementHandle): Protocol.Runtime.RemoteObject {

View File

@ -15,10 +15,10 @@
* limitations under the License.
*/
import { assert, helper } from '../../helper';
import { CRSession } from '../crConnection';
import { readProtocolStream } from '../crProtocolHelper';
import * as platform from '../../platform';
import { assert, helper } from '../helper';
import * as platform from '../platform';
import { CRSession } from './crConnection';
import { readProtocolStream } from './crProtocolHelper';
export type PDFOptions = {
scale?: number,

View File

@ -19,11 +19,11 @@ import { CRBrowser } from './crBrowser';
import { BrowserContext } from '../browserContext';
import { CRSession, CRSessionEvents } from './crConnection';
import { Events } from '../events';
import { CRWorker } from './features/crWorkers';
import { Page } from '../page';
import { Page, Worker } from '../page';
import { Protocol } from './protocol';
import { debugError } from '../helper';
import { CRPage } from './crPage';
import { CRExecutionContext } from './crExecutionContext';
const targetSymbol = Symbol('target');
@ -35,7 +35,7 @@ export class CRTarget {
private _sessionFactory: () => Promise<CRSession>;
private _pagePromise: Promise<Page> | null = null;
_crPage: CRPage | null = null;
private _workerPromise: Promise<CRWorker> | null = null;
private _workerPromise: Promise<Worker> | null = null;
readonly _initializedPromise: Promise<boolean>;
_initializedCallback: (value?: unknown) => void;
_isInitialized: boolean;
@ -98,13 +98,20 @@ export class CRTarget {
return this._pagePromise;
}
async _worker(): Promise<CRWorker | null> {
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 CRWorker(client, this._targetInfo.url, () => { } /* consoleAPICalled */, () => { } /* exceptionThrown */));
this._workerPromise = this._sessionFactory().then(session => {
const worker = new Worker(this._targetInfo.url);
session.once('Runtime.executionContextCreated', async event => {
worker._createExecutionContext(new CRExecutionContext(session, event.context));
});
// This might fail if the target is closed before we recieve all execution contexts.
session.send('Runtime.enable', {}).catch(debugError);
return worker;
});
}
return this._workerPromise;
}

45
src/chromium/crWorkers.ts Normal file
View File

@ -0,0 +1,45 @@
/**
* 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 { Events } from '../events';
import { debugError } from '../helper';
import { Worker } from '../page';
import { CRConnection, CRSession } from './crConnection';
import { CRExecutionContext } from './crExecutionContext';
import { ChromiumPage } from './crPage';
import { exceptionToError, toConsoleMessageLocation } from './crProtocolHelper';
export class CRWorkers {
constructor(client: CRSession, page: ChromiumPage) {
client.on('Target.attachedToTarget', event => {
if (event.targetInfo.type !== 'worker')
return;
const url = event.targetInfo.url;
const session = CRConnection.fromSession(client).session(event.sessionId);
const worker = new Worker(url);
page._addWorker(event.sessionId, worker);
session.once('Runtime.executionContextCreated', async event => {
worker._createExecutionContext(new CRExecutionContext(session, event.context));
});
// This might fail if the target is closed before we recieve all execution contexts.
session.send('Runtime.enable', {}).catch(debugError);
session.on('Runtime.consoleAPICalled', event => page._addConsoleMessage(event.type, event.args.map(o => worker._existingExecutionContext._createHandle(o)), toConsoleMessageLocation(event.stackTrace)));
session.on('Runtime.exceptionThrown', exception => page.emit(Events.Page.PageError, exceptionToError(exception.exceptionDetails)));
});
client.on('Target.detachedFromTarget', event => page._removeWorker(event.sessionId));
}
}

View File

@ -20,10 +20,5 @@ export const Events = {
TargetCreated: 'targetcreated',
TargetDestroyed: 'targetdestroyed',
TargetChanged: 'targetchanged',
},
CRPage: {
WorkerCreated: 'workercreated',
WorkerDestroyed: 'workerdestroyed',
}
};

View File

@ -1,94 +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 '../../platform';
import { CRSession, CRConnection } from '../crConnection';
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 { CRExecutionContext } from '../crExecutionContext';
import { toConsoleMessageLocation, exceptionToError } from '../crProtocolHelper';
import { ChromiumPage } from '../crPage';
type AddToConsoleCallback = (type: string, args: js.JSHandle[], location: console.ConsoleMessageLocation) => void;
type HandleExceptionCallback = (error: Error) => void;
export class CRWorkers {
private _workers = new Map<string, CRWorker>();
constructor(client: CRSession, page: ChromiumPage, addToConsole: AddToConsoleCallback, handleException: HandleExceptionCallback) {
client.on('Target.attachedToTarget', event => {
if (event.targetInfo.type !== 'worker')
return;
const session = CRConnection.fromSession(client).session(event.sessionId);
const worker = new CRWorker(session, event.targetInfo.url, addToConsole, handleException);
this._workers.set(event.sessionId, worker);
page.emit(Events.CRPage.WorkerCreated, worker);
});
client.on('Target.detachedFromTarget', event => {
const worker = this._workers.get(event.sessionId);
if (!worker)
return;
page.emit(Events.CRPage.WorkerDestroyed, worker);
this._workers.delete(event.sessionId);
});
}
list(): CRWorker[] {
return Array.from(this._workers.values());
}
}
export class CRWorker extends EventEmitter {
private _client: CRSession;
private _url: string;
private _executionContextPromise: Promise<js.ExecutionContext>;
private _executionContextCallback: (value?: js.ExecutionContext) => void;
constructor(client: CRSession, 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 CRExecutionContext(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

@ -38,5 +38,7 @@ export const Events = {
FrameNavigated: 'framenavigated',
Load: 'load',
Popup: 'popup',
},
WorkerCreated: 'workercreated',
WorkerDestroyed: 'workerdestroyed',
}
};

View File

@ -104,6 +104,7 @@ export class Page extends platform.EventEmitter {
readonly _screenshotter: Screenshotter;
readonly _frameManager: frames.FrameManager;
readonly accessibility: accessibility.Accessibility;
private _workers = new Map<string, Worker>();
constructor(delegate: PageDelegate, browserContext: BrowserContext) {
super();
@ -495,4 +496,53 @@ export class Page extends platform.EventEmitter {
async $wait(selector: string, pageFunction: Function | string, options?: types.WaitForFunctionOptions, ...args: any[]): Promise<js.JSHandle> {
return this.mainFrame().$wait(selector, pageFunction, options, ...args);
}
workers(): Worker[] {
return [...this._workers.values()];
}
_addWorker(workerId: string, worker: Worker) {
this._workers.set(workerId, worker);
this.emit(Events.Page.WorkerCreated, worker);
}
_removeWorker(workerId: string) {
const worker = this._workers.get(workerId);
if (!worker)
return;
this.emit(Events.Page.WorkerDestroyed, worker);
this._workers.delete(workerId);
}
_clearWorkers() {
this._workers.clear();
}
}
export class Worker {
private _url: string;
private _executionContextPromise: Promise<js.ExecutionContext>;
private _executionContextCallback: (value?: js.ExecutionContext) => void;
_existingExecutionContext: js.ExecutionContext | null;
constructor(url: string) {
this._url = url;
this._executionContextPromise = new Promise(x => this._executionContextCallback = x);
}
_createExecutionContext(delegate: js.ExecutionContextDelegate) {
this._existingExecutionContext = new js.ExecutionContext(delegate);
this._executionContextCallback(this._existingExecutionContext);
}
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

@ -214,19 +214,45 @@ export class WKPageProxySession extends platform.EventEmitter {
}
}
export class WKTargetSession extends platform.EventEmitter {
_pageProxySession: WKPageProxySession;
private readonly _callbacks = new Map<number, {resolve:(o: any) => void, reject: (e: Error) => void, error: Error, method: string}>();
private readonly _targetType: string;
readonly _sessionId: string;
_swappedOut = false;
private _provisionalMessages?: string[];
export class WKSession extends platform.EventEmitter {
readonly _callbacks = new Map<number, {resolve:(o: any) => void, reject: (e: Error) => void, error: Error, method: 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;
send<T extends keyof Protocol.CommandParameters>(
method: T,
params?: Protocol.CommandParameters[T]
): Promise<Protocol.CommandReturnValues[T]> {
throw new Error('Not implemented');
}
protected _dispatchMessage(message: string) {
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));
}
}
}
export class WKTargetSession extends WKSession {
_pageProxySession: WKPageProxySession;
private readonly _targetType: string;
readonly _sessionId: string;
_swappedOut = false;
private _provisionalMessages?: string[];
constructor(pageProxySession: WKPageProxySession, targetInfo: Protocol.Target.TargetInfo) {
super();
const {targetId, type, isProvisional} = targetInfo;
@ -287,19 +313,7 @@ export class WKTargetSession extends platform.EventEmitter {
_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));
}
this._dispatchMessage(message);
}
_onClosed() {
@ -316,14 +330,14 @@ export class WKTargetSession extends platform.EventEmitter {
}
}
function createProtocolError(error: Error, method: string, object: { error: { message: string; data: any; }; }): Error {
export 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 {
export function rewriteError(error: Error, message: string): Error {
error.message = message;
return error;
}

View File

@ -15,7 +15,7 @@
* limitations under the License.
*/
import { WKTargetSession, isSwappedOutError } from './wkConnection';
import { WKSession, isSwappedOutError } from './wkConnection';
import { helper } from '../helper';
import { valueFromRemoteObject, releaseObject } from './wkProtocolHelper';
import { Protocol } from './protocol';
@ -26,15 +26,15 @@ const SOURCE_URL_REGEX = /^[\040\t]*\/\/[@#] sourceURL=\s*(\S*?)\s*$/m;
export class WKExecutionContext implements js.ExecutionContextDelegate {
private _globalObjectId?: Promise<string>;
_session: WKTargetSession;
_contextId: number;
_session: WKSession;
_contextId: number | undefined;
private _contextDestroyedCallback: () => void;
private _executionContextDestroyedPromise: Promise<unknown>;
_jsonStringifyObjectId: Protocol.Runtime.RemoteObjectId | undefined;
constructor(client: WKTargetSession, contextPayload: Protocol.Runtime.ExecutionContextDescription) {
constructor(client: WKSession, contextId: number | undefined) {
this._session = client;
this._contextId = contextPayload.id;
this._contextId = contextId;
this._contextDestroyedCallback = null;
this._executionContextDestroyedPromise = new Promise((resolve, reject) => {
this._contextDestroyedCallback = resolve;

View File

@ -23,6 +23,7 @@ import { WKTargetSession, WKTargetSessionEvents } from './wkConnection';
import { Events } from '../events';
import { WKExecutionContext, EVALUATION_SCRIPT_URL } from './wkExecutionContext';
import { WKNetworkManager } from './wkNetworkManager';
import { WKWorkers } from './wkWorkers';
import { Page, PageDelegate } from '../page';
import { Protocol } from './protocol';
import * as dialog from '../dialog';
@ -43,11 +44,12 @@ export class WKPage implements PageDelegate {
_session: WKTargetSession;
readonly _page: Page;
private _browser: WKBrowser;
private readonly _networkManager: WKNetworkManager;
private readonly _contextIdToContext: Map<number, dom.FrameExecutionContext>;
private _networkManager: WKNetworkManager;
private _workers: WKWorkers;
private _contextIdToContext: Map<number, dom.FrameExecutionContext>;
private _isolatedWorlds: Set<string>;
private _sessionListeners: RegisteredListener[] = [];
private readonly _bootstrapScripts: string[] = [];
private _bootstrapScripts: string[] = [];
constructor(browser: WKBrowser, browserContext: BrowserContext) {
this._browser = browser;
@ -57,6 +59,7 @@ export class WKPage implements PageDelegate {
this._isolatedWorlds = new Set();
this._page = new Page(this, browserContext);
this._networkManager = new WKNetworkManager(this._page);
this._workers = new WKWorkers(this._page);
}
setSession(session: WKTargetSession) {
@ -67,6 +70,8 @@ export class WKPage implements PageDelegate {
this.rawMouse.setSession(session);
this._addSessionListeners();
this._networkManager.setSession(session);
this._workers.setSession(session);
this._page._clearWorkers();
this._isolatedWorlds = new Set();
// New bootstrap scripts may have been added during provisional load, push them
// again to be on the safe side.
@ -86,6 +91,7 @@ export class WKPage implements PageDelegate {
session.send('Console.enable'),
session.send('Page.setInterceptFileChooserDialog', { enabled: true }),
this._networkManager.initializeSession(session, this._page._state.interceptNetwork, this._page._state.offlineMode, this._page._state.credentials),
this._workers.initializeSession(session)
];
if (!session.isProvisional()) {
// FIXME: move dialog agent to web process.
@ -195,7 +201,7 @@ export class WKPage implements PageDelegate {
const frame = this._page._frameManager.frame(contextPayload.frameId);
if (!frame)
return;
const delegate = new WKExecutionContext(this._session, contextPayload);
const delegate = new WKExecutionContext(this._session, contextPayload.id);
const context = new dom.FrameExecutionContext(delegate, frame);
if (contextPayload.isPageContext)
frame._contextCreated('main', context);

View File

@ -16,7 +16,7 @@
*/
import { assert, debugError } from '../helper';
import { WKTargetSession } from './wkConnection';
import { WKSession } 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: WKTargetSession, remoteObject: Protocol.Runtime.RemoteObject) {
export async function releaseObject(client: WKSession, remoteObject: Protocol.Runtime.RemoteObject) {
if (!remoteObject.objectId)
return;
await client.send('Runtime.releaseObject', {objectId: remoteObject.objectId}).catch(error => {

131
src/webkit/wkWorkers.ts Normal file
View File

@ -0,0 +1,131 @@
/**
* Copyright 2019 Microsoft Corporation All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { assert, helper, RegisteredListener } from '../helper';
import { Page, Worker } from '../page';
import { Protocol } from './protocol';
import { rewriteError, WKSession, WKTargetSession } from './wkConnection';
import { WKExecutionContext } from './wkExecutionContext';
export class WKWorkers {
private _sessionListeners: RegisteredListener[] = [];
private _page: Page;
constructor(page: Page) {
this._page = page;
}
setSession(session: WKTargetSession) {
helper.removeEventListeners(this._sessionListeners);
this._sessionListeners = [
helper.addEventListener(session, 'Worker.workerCreated', async (event: Protocol.Worker.workerCreatedPayload) => {
const worker = new Worker(event.url);
const workerSession = new WKWorkerSession(session, event.workerId);
worker._createExecutionContext(new WKExecutionContext(workerSession, undefined));
this._page._addWorker(event.workerId, worker);
workerSession.on('Console.messageAdded', event => this._onConsoleMessage(worker, event));
try {
Promise.all([
workerSession.send('Runtime.enable'),
workerSession.send('Console.enable'),
session.send('Worker.initialized', { workerId: event.workerId }).catch(e => {
this._page._removeWorker(event.workerId);
})
]);
} catch (e) {
// Worker can go as we are initializing it.
}
}),
helper.addEventListener(session, 'Worker.workerTerminated', (event: Protocol.Worker.workerTerminatedPayload) => {
this._page._removeWorker(event.workerId);
})
];
}
async initializeSession(session: WKTargetSession) {
await session.send('Worker.enable');
}
async _onConsoleMessage(worker: Worker, event: Protocol.Console.messageAddedPayload) {
const { type, level, text, parameters, url, line: lineNumber, column: columnNumber } = event.message;
let derivedType: string = type;
if (type === 'log')
derivedType = level;
else if (type === 'timing')
derivedType = 'timeEnd';
const handles = (parameters || []).map(p => {
return worker._existingExecutionContext._createHandle(p);
});
this._page._addConsoleMessage(derivedType, handles, { url, lineNumber: lineNumber - 1, columnNumber: columnNumber - 1 }, handles.length ? undefined : text);
}
}
export class WKWorkerSession extends WKSession {
private _targetSession: WKTargetSession | null;
private _workerId: string;
private _lastId = 1001;
constructor(targetSession: WKTargetSession, workerId: string) {
super();
this._targetSession = targetSession;
this._workerId = workerId;
this._targetSession.on('Worker.dispatchMessageFromWorker', event => {
if (event.workerId === workerId)
this._dispatchMessage(event.message);
});
this._targetSession.on('Worker.workerTerminated', event => {
if (event.workerId === workerId)
this._workerTerminated();
});
}
send<T extends keyof Protocol.CommandParameters>(
method: T,
params?: Protocol.CommandParameters[T]
): Promise<Protocol.CommandReturnValues[T]> {
if (!this._targetSession)
return Promise.reject(new Error(`Protocol error (${method}): Most likely the worker has been closed.`));
const innerId = ++this._lastId;
const messageObj = {
id: innerId,
method,
params
};
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._targetSession.send('Worker.sendMessageToWorker', {
workerId: this._workerId,
message: message
}).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 worker was terminated.');
});
return result;
}
_workerTerminated() {
for (const callback of this._callbacks.values())
callback.reject(rewriteError(callback.error, `Protocol error (${callback.method}): Worker terminated.`));
this._callbacks.clear();
this._targetSession = null;
}
}

View File

@ -168,13 +168,13 @@ module.exports.describe = ({testRunner, product, playwrightPath}) => {
testRunner.loadTests(require('./waittask.spec.js'), testOptions);
testRunner.loadTests(require('./interception.spec.js'), testOptions);
testRunner.loadTests(require('./geolocation.spec.js'), testOptions);
testRunner.loadTests(require('./workers.spec.js'), testOptions);
if (CHROME) {
testRunner.loadTests(require('./chromium/chromium.spec.js'), testOptions);
testRunner.loadTests(require('./chromium/coverage.spec.js'), testOptions);
testRunner.loadTests(require('./chromium/pdf.spec.js'), testOptions);
testRunner.loadTests(require('./chromium/session.spec.js'), testOptions);
testRunner.loadTests(require('./chromium/workers.spec.js'), testOptions);
}
if (CHROME || FFOX) {

View File

@ -15,7 +15,7 @@
* limitations under the License.
*/
const utils = require('../utils');
const utils = require('./utils');
const { waitEvent } = utils;
module.exports.describe = function({testRunner, expect, FFOX, CHROME, WEBKIT}) {
@ -23,10 +23,10 @@ module.exports.describe = function({testRunner, expect, FFOX, CHROME, WEBKIT}) {
const {it, fit, xit, dit} = testRunner;
const {beforeAll, beforeEach, afterAll, afterEach} = testRunner;
describe('Workers', function() {
describe.skip(FFOX)('Workers', function() {
it('Page.workers', async function({page, server}) {
await Promise.all([
new Promise(x => page.once('workercreated', x)),
page.waitForEvent('workercreated'),
page.goto(server.PREFIX + '/worker/worker.html')]);
const worker = page.workers()[0];
expect(worker.url()).toContain('worker.js');
@ -37,8 +37,8 @@ module.exports.describe = function({testRunner, expect, FFOX, CHROME, WEBKIT}) {
expect(page.workers().length).toBe(0);
});
it('should emit created and destroyed events', async function({page}) {
const workerCreatedPromise = new Promise(x => page.once('workercreated', x));
const workerObj = await page.evaluateHandle(() => new Worker('data:text/javascript,1'));
const workerCreatedPromise = page.waitForEvent('workercreated');
const workerObj = await page.evaluateHandle(() => new Worker(URL.createObjectURL(new Blob(['1'], {type: 'application/javascript'}))));
const worker = await workerCreatedPromise;
const workerThisObj = await worker.evaluateHandle(() => this);
const workerDestroyedPromise = new Promise(x => page.once('workerdestroyed', x));
@ -50,34 +50,38 @@ module.exports.describe = function({testRunner, expect, FFOX, CHROME, WEBKIT}) {
it('should report console logs', async function({page}) {
const [message] = await Promise.all([
waitEvent(page, 'console'),
page.evaluate(() => new Worker(`data:text/javascript,console.log(1)`)),
page.evaluate(() => new Worker(URL.createObjectURL(new Blob(['console.log(1)'], {type: 'application/javascript'})))),
]);
expect(message.text()).toBe('1');
expect(message.location()).toEqual({
url: 'data:text/javascript,console.log(1)',
lineNumber: 0,
columnNumber: 8,
});
});
it('should have JSHandles for console logs', async function({page}) {
const logPromise = new Promise(x => page.on('console', x));
await page.evaluate(() => new Worker(`data:text/javascript,console.log(1,2,3,this)`));
await page.evaluate(() => new Worker(URL.createObjectURL(new Blob(['console.log(1,2,3,this)'], {type: 'application/javascript'}))));
const log = await logPromise;
expect(log.text()).toBe('1 2 3 JSHandle@object');
expect(log.args().length).toBe(4);
expect(await (await log.args()[3].getProperty('origin')).jsonValue()).toBe('null');
});
it('should evaluate', async function({page}) {
const workerCreatedPromise = new Promise(x => page.once('workercreated', x));
await page.evaluate(() => new Worker(`data:text/javascript,console.log(1)`));
const workerCreatedPromise = page.waitForEvent('workercreated');
page.evaluate(() => new Worker(URL.createObjectURL(new Blob(['console.log(1)'], {type: 'application/javascript'}))));
const worker = await workerCreatedPromise;
expect(await worker.evaluate('1+1')).toBe(2);
});
it('should report errors', async function({page}) {
const errorPromise = new Promise(x => page.on('pageerror', x));
await page.evaluate(() => new Worker(`data:text/javascript, throw new Error('this is my error');`));
page.evaluate(() => new Worker(URL.createObjectURL(new Blob([`setTimeout(() => { throw new Error('this is my error'); })`], {type: 'application/javascript'}))));
const errorLog = await errorPromise;
expect(errorLog.message).toContain('this is my error');
});
it('should clear upon navigation', async function({server, page}) {
await page.goto(server.EMPTY_PAGE);
const workerCreatedPromise = page.waitForEvent('workercreated');
page.evaluate(() => new Worker(URL.createObjectURL(new Blob(['console.log(1)'], {type: 'application/javascript'}))));
await workerCreatedPromise;
expect(page.workers().length).toBe(1);
await page.goto(server.CROSS_PROCESS_PREFIX + '/empty.html');
expect(page.workers().length).toBe(0);
});
});
};