mirror of
https://github.com/microsoft/playwright.git
synced 2024-12-15 06:02:57 +03:00
feat(workers): support workers in webkit (#400)
This commit is contained in:
parent
92b14cf996
commit
f75ac45c42
38
docs/api.md
38
docs/api.md
@ -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)
|
||||
|
@ -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';
|
||||
|
@ -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();
|
||||
}
|
||||
|
||||
|
@ -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,
|
@ -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 {
|
||||
|
@ -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,
|
@ -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
45
src/chromium/crWorkers.ts
Normal 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));
|
||||
}
|
||||
}
|
@ -20,10 +20,5 @@ export const Events = {
|
||||
TargetCreated: 'targetcreated',
|
||||
TargetDestroyed: 'targetdestroyed',
|
||||
TargetChanged: 'targetchanged',
|
||||
},
|
||||
|
||||
CRPage: {
|
||||
WorkerCreated: 'workercreated',
|
||||
WorkerDestroyed: 'workerdestroyed',
|
||||
}
|
||||
};
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
@ -38,5 +38,7 @@ export const Events = {
|
||||
FrameNavigated: 'framenavigated',
|
||||
Load: 'load',
|
||||
Popup: 'popup',
|
||||
},
|
||||
WorkerCreated: 'workercreated',
|
||||
WorkerDestroyed: 'workerdestroyed',
|
||||
}
|
||||
};
|
||||
|
50
src/page.ts
50
src/page.ts
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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);
|
||||
|
@ -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
131
src/webkit/wkWorkers.ts
Normal 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;
|
||||
}
|
||||
}
|
@ -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) {
|
||||
|
@ -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);
|
||||
});
|
||||
});
|
||||
};
|
Loading…
Reference in New Issue
Block a user