feat(Page): introduce workers (#2560)
This adds `page.workers()`, and two events `workercreated` and `workerdestroyed`. It also forwards logs from a worker into the page `console` event. Only dedicated workers are supported for now, ServiceWorkers will probably work differently because they aren't necessarily associated with a single page. Fixes #2350.
This commit is contained in:
parent
b474f2ce87
commit
93fe2b57d6
43
docs/api.md
43
docs/api.md
@ -67,6 +67,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)
|
||||
* [page.$$eval(selector, pageFunction[, ...args])](#pageevalselector-pagefunction-args)
|
||||
@ -128,6 +130,10 @@
|
||||
* [page.waitForNavigation(options)](#pagewaitfornavigationoptions)
|
||||
* [page.waitForSelector(selector[, options])](#pagewaitforselectorselector-options)
|
||||
* [page.waitForXPath(xpath[, options])](#pagewaitforxpathxpath-options)
|
||||
* [page.workers()](#pageworkers)
|
||||
- [class: Worker](#class-worker)
|
||||
* [worker.executionContext()](#workerexecutioncontext)
|
||||
* [worker.url()](#workerurl)
|
||||
- [class: Keyboard](#class-keyboard)
|
||||
* [keyboard.down(key[, options])](#keyboarddownkey-options)
|
||||
* [keyboard.press(key[, options])](#keyboardpresskey-options)
|
||||
@ -736,6 +742,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]>>
|
||||
@ -1596,6 +1612,32 @@ puppeteer.launch().then(async browser => {
|
||||
```
|
||||
Shortcut for [page.mainFrame().waitForXPath(xpath[, options])](#framewaitforxpathxpath-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: Worker
|
||||
|
||||
The Worker class represents a [WebWorker](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API).
|
||||
The events `workercreated` and `workerdestroyed` are emitted on the page object to signal the worker lifecycle.
|
||||
|
||||
```js
|
||||
page.on('workercreated', worker => console.log('Worker created: ' + worker.url()));
|
||||
page.on('workerdestroyed', worker => console.log('Worker destroyed: ' + worker.url()));
|
||||
|
||||
console.log('Current workers:');
|
||||
for (const worker of page.workers())
|
||||
console.log(' ' + worker.url());
|
||||
```
|
||||
|
||||
#### worker.executionContext()
|
||||
- returns: <[Promise]<[ExecutionContext]>>
|
||||
|
||||
#### worker.url()
|
||||
- returns: <[string]>
|
||||
|
||||
### class: Keyboard
|
||||
|
||||
Keyboard provides an api for managing a virtual keyboard. The high level api is [`keyboard.type`](#keyboardtypetext-options), which takes raw characters and generates proper keydown, keypress/input, and keyup events on your page.
|
||||
@ -2884,3 +2926,4 @@ reported.
|
||||
[xpath]: https://developer.mozilla.org/en-US/docs/Web/XPath "xpath"
|
||||
[UnixTime]: https://en.wikipedia.org/wiki/Unix_time "Unix Time"
|
||||
[SecurityDetails]: #class-securitydetails "SecurityDetails"
|
||||
[Worker]: #class-worker "Worker"
|
||||
|
@ -163,7 +163,7 @@ class Connection extends EventEmitter {
|
||||
|
||||
class CDPSession extends EventEmitter {
|
||||
/**
|
||||
* @param {!Connection} connection
|
||||
* @param {!Connection|!CDPSession} connection
|
||||
* @param {string} targetId
|
||||
* @param {string} sessionId
|
||||
*/
|
||||
@ -175,6 +175,8 @@ class CDPSession extends EventEmitter {
|
||||
this._connection = connection;
|
||||
this._targetId = targetId;
|
||||
this._sessionId = sessionId;
|
||||
/** @type {!Map<string, !CDPSession>}*/
|
||||
this._sessions = new Map();
|
||||
}
|
||||
|
||||
/**
|
||||
@ -215,6 +217,17 @@ class CDPSession extends EventEmitter {
|
||||
else
|
||||
callback.resolve(object.result);
|
||||
} else {
|
||||
if (object.method === 'Target.receivedMessageFromTarget') {
|
||||
const session = this._sessions.get(object.params.sessionId);
|
||||
if (session)
|
||||
session._onMessage(object.params.message);
|
||||
} else if (object.method === 'Target.detachedFromTarget') {
|
||||
const session = this._sessions.get(object.params.sessionId);
|
||||
if (session) {
|
||||
session._onClosed();
|
||||
this._sessions.delete(object.params.sessionId);
|
||||
}
|
||||
}
|
||||
console.assert(!object.id);
|
||||
this.emit(object.method, object.params);
|
||||
}
|
||||
@ -230,6 +243,16 @@ class CDPSession extends EventEmitter {
|
||||
this._callbacks.clear();
|
||||
this._connection = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} targetId
|
||||
* @param {string} sessionId
|
||||
*/
|
||||
_createSession(targetId, sessionId) {
|
||||
const session = new CDPSession(this, targetId, sessionId);
|
||||
this._sessions.set(sessionId, session);
|
||||
return session;
|
||||
}
|
||||
}
|
||||
helper.tracePublicAPI(CDPSession);
|
||||
|
||||
|
@ -20,7 +20,7 @@ class ExecutionContext {
|
||||
/**
|
||||
* @param {!Puppeteer.CDPSession} client
|
||||
* @param {!Protocol.Runtime.ExecutionContextDescription} contextPayload
|
||||
* @param {function(*):!JSHandle} objectHandleFactory
|
||||
* @param {function(!Protocol.Runtime.RemoteObject):!JSHandle} objectHandleFactory
|
||||
* @param {?Puppeteer.Frame} frame
|
||||
*/
|
||||
constructor(client, contextPayload, objectHandleFactory, frame) {
|
||||
|
@ -215,7 +215,7 @@ class FrameManager extends EventEmitter {
|
||||
|
||||
/**
|
||||
* @param {number} contextId
|
||||
* @param {*} remoteObject
|
||||
* @param {!Protocol.Runtime.RemoteObject} remoteObject
|
||||
* @return {!JSHandle}
|
||||
*/
|
||||
createJSHandle(contextId, remoteObject) {
|
||||
|
45
lib/Page.js
45
lib/Page.js
@ -26,6 +26,7 @@ const {Keyboard, Mouse, Touchscreen} = require('./Input');
|
||||
const Tracing = require('./Tracing');
|
||||
const {helper, debugError} = require('./helper');
|
||||
const {Coverage} = require('./Coverage');
|
||||
const Worker = require('./Worker');
|
||||
|
||||
const writeFileAsync = helper.promisify(fs.writeFile);
|
||||
|
||||
@ -45,6 +46,7 @@ class Page extends EventEmitter {
|
||||
const page = new Page(client, target, frameTree, ignoreHTTPSErrors, screenshotTaskQueue);
|
||||
|
||||
await Promise.all([
|
||||
client.send('Target.setAutoAttach', {autoAttach: true, waitForDebuggerOnStart: false}),
|
||||
client.send('Page.setLifecycleEventsEnabled', { enabled: true }),
|
||||
client.send('Network.enable', {}),
|
||||
client.send('Runtime.enable', {}),
|
||||
@ -87,6 +89,30 @@ class Page extends EventEmitter {
|
||||
|
||||
this._screenshotTaskQueue = screenshotTaskQueue;
|
||||
|
||||
/** @type {!Map<string, Worker>} */
|
||||
this._workers = new Map();
|
||||
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
|
||||
});
|
||||
return;
|
||||
}
|
||||
const session = client._createSession(event.targetInfo.targetId, event.sessionId);
|
||||
const worker = new Worker(session, event.targetInfo.url, this._onLogEntryAdded.bind(this, session));
|
||||
this._workers.set(event.sessionId, worker);
|
||||
this.emit(Page.Events.WorkerCreated, worker);
|
||||
|
||||
});
|
||||
client.on('Target.detachedFromTarget', event => {
|
||||
const worker = this._workers.get(event.sessionId);
|
||||
if (!worker)
|
||||
return;
|
||||
this.emit(Page.Events.WorkerDestroyed, worker);
|
||||
this._workers.delete(event.sessionId);
|
||||
});
|
||||
|
||||
this._frameManager.on(FrameManager.Events.FrameAttached, event => this.emit(Page.Events.FrameAttached, event));
|
||||
this._frameManager.on(FrameManager.Events.FrameDetached, event => this.emit(Page.Events.FrameDetached, event));
|
||||
this._frameManager.on(FrameManager.Events.FrameNavigated, event => this.emit(Page.Events.FrameNavigated, event));
|
||||
@ -104,7 +130,7 @@ class Page extends EventEmitter {
|
||||
client.on('Security.certificateError', event => this._onCertificateError(event));
|
||||
client.on('Inspector.targetCrashed', event => this._onTargetCrashed());
|
||||
client.on('Performance.metrics', event => this._emitMetrics(event));
|
||||
client.on('Log.entryAdded', event => this._onLogEntryAdded(event));
|
||||
client.on('Log.entryAdded', event => this._onLogEntryAdded(this._client, event));
|
||||
this._target._isClosedPromise.then(() => this.emit(Page.Events.Close));
|
||||
}
|
||||
|
||||
@ -126,10 +152,14 @@ class Page extends EventEmitter {
|
||||
this.emit('error', new Error('Page crashed!'));
|
||||
}
|
||||
|
||||
_onLogEntryAdded(event) {
|
||||
/**
|
||||
* @param {!Puppeteer.CDPSession} session
|
||||
* @param {!Protocol.Log.entryAddedPayload} event
|
||||
*/
|
||||
_onLogEntryAdded(session, event) {
|
||||
const {level, text, args} = event.entry;
|
||||
if (args)
|
||||
args.map(arg => helper.releaseObject(this._client, arg));
|
||||
args.map(arg => helper.releaseObject(session, arg));
|
||||
|
||||
this.emit(Page.Events.Console, new ConsoleMessage(level, text));
|
||||
}
|
||||
@ -176,6 +206,13 @@ class Page extends EventEmitter {
|
||||
return this._frameManager.frames();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {!Array<!Worker>}
|
||||
*/
|
||||
workers() {
|
||||
return Array.from(this._workers.values());
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {boolean} value
|
||||
*/
|
||||
@ -1019,6 +1056,8 @@ Page.Events = {
|
||||
FrameNavigated: 'framenavigated',
|
||||
Load: 'load',
|
||||
Metrics: 'metrics',
|
||||
WorkerCreated: 'workercreated',
|
||||
WorkerDestroyed: 'workerdestroyed',
|
||||
};
|
||||
|
||||
/**
|
||||
|
59
lib/Worker.js
Normal file
59
lib/Worker.js
Normal file
@ -0,0 +1,59 @@
|
||||
/**
|
||||
* Copyright 2018 Google Inc. 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.
|
||||
*/
|
||||
const EventEmitter = require('events');
|
||||
const {helper, debugError} = require('./helper');
|
||||
const {ExecutionContext, JSHandle} = require('./ExecutionContext');
|
||||
|
||||
class Worker extends EventEmitter {
|
||||
/**
|
||||
* @param {Puppeteer.CDPSession} client
|
||||
* @param {string} url
|
||||
* @param {function(!Protocol.Log.entryAddedPayload)} logEntryAdded
|
||||
*/
|
||||
constructor(client, url, logEntryAdded) {
|
||||
super();
|
||||
this._client = client;
|
||||
this._url = url;
|
||||
this._executionContextPromise = new Promise(x => this._executionContextCallback = x);
|
||||
this._client.on('Runtime.executionContextCreated', event => {
|
||||
const jsHandleFactory = remoteObject => new JSHandle(executionContext, client, remoteObject);
|
||||
const executionContext = new ExecutionContext(client, event.context, jsHandleFactory, null);
|
||||
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('Log.entryAdded', logEntryAdded);
|
||||
this._client.send('Log.enable', {}).catch(debugError);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {string}
|
||||
*/
|
||||
url() {
|
||||
return this._url;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {!Promise<ExecutionContext>}
|
||||
*/
|
||||
async executionContext() {
|
||||
return this._executionContextPromise;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Worker;
|
||||
helper.tracePublicAPI(Worker);
|
14
test/assets/worker/worker.html
Normal file
14
test/assets/worker/worker.html
Normal file
@ -0,0 +1,14 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Worker test</title>
|
||||
</head>
|
||||
<body>
|
||||
<script>
|
||||
var worker = new Worker('worker.js');
|
||||
worker.onmessage = function(message) {
|
||||
console.log(message.data);
|
||||
};
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
16
test/assets/worker/worker.js
Normal file
16
test/assets/worker/worker.js
Normal file
@ -0,0 +1,16 @@
|
||||
console.log('hello from the worker');
|
||||
|
||||
function workerFunction() {
|
||||
return 'worker function result';
|
||||
}
|
||||
|
||||
self.addEventListener('message', event => {
|
||||
console.log('got this data: ' + event.data);
|
||||
});
|
||||
|
||||
(async function() {
|
||||
while (true) {
|
||||
self.postMessage(workerFunction.toString());
|
||||
await new Promise(x => setTimeout(x, 100));
|
||||
}
|
||||
})();
|
@ -1611,6 +1611,33 @@ module.exports.addTests = function({testRunner, expect, puppeteer, DeviceDescrip
|
||||
await closedPromise;
|
||||
});
|
||||
});
|
||||
describe('Workers', function() {
|
||||
it('Page.workers', async function({page, server}) {
|
||||
await page.goto(server.PREFIX + '/worker/worker.html');
|
||||
await page.waitForFunction(() => !!worker);
|
||||
const worker = page.workers()[0];
|
||||
expect(worker.url()).toContain('worker.js');
|
||||
const executionContext = await worker.executionContext();
|
||||
expect(await executionContext.evaluate(() => self.workerFunction())).toBe('worker function result');
|
||||
|
||||
await page.goto(server.EMPTY_PAGE);
|
||||
expect(page.workers()).toEqual([]);
|
||||
});
|
||||
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 worker = await workerCreatedPromise;
|
||||
const workerDestroyedPromise = new Promise(x => page.once('workerdestroyed', x));
|
||||
await page.evaluate(workerObj => workerObj.terminate(), workerObj);
|
||||
expect(await workerDestroyedPromise).toBe(worker);
|
||||
});
|
||||
it('should report 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)`));
|
||||
const log = await logPromise;
|
||||
expect(log.text()).toBe('1');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Page.browser', function() {
|
||||
it('should return the correct browser instance', async function({ page, browser }) {
|
||||
|
Loading…
Reference in New Issue
Block a user