mirror of
https://github.com/puppeteer/puppeteer
synced 2024-06-14 14:02:48 +00:00
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: 'requestfailed'](#event-requestfailed)
|
||||||
* [event: 'requestfinished'](#event-requestfinished)
|
* [event: 'requestfinished'](#event-requestfinished)
|
||||||
* [event: 'response'](#event-response)
|
* [event: 'response'](#event-response)
|
||||||
|
* [event: 'workercreated'](#event-workercreated)
|
||||||
|
* [event: 'workerdestroyed'](#event-workerdestroyed)
|
||||||
* [page.$(selector)](#pageselector)
|
* [page.$(selector)](#pageselector)
|
||||||
* [page.$$(selector)](#pageselector)
|
* [page.$$(selector)](#pageselector)
|
||||||
* [page.$$eval(selector, pageFunction[, ...args])](#pageevalselector-pagefunction-args)
|
* [page.$$eval(selector, pageFunction[, ...args])](#pageevalselector-pagefunction-args)
|
||||||
@ -128,6 +130,10 @@
|
|||||||
* [page.waitForNavigation(options)](#pagewaitfornavigationoptions)
|
* [page.waitForNavigation(options)](#pagewaitfornavigationoptions)
|
||||||
* [page.waitForSelector(selector[, options])](#pagewaitforselectorselector-options)
|
* [page.waitForSelector(selector[, options])](#pagewaitforselectorselector-options)
|
||||||
* [page.waitForXPath(xpath[, options])](#pagewaitforxpathxpath-options)
|
* [page.waitForXPath(xpath[, options])](#pagewaitforxpathxpath-options)
|
||||||
|
* [page.workers()](#pageworkers)
|
||||||
|
- [class: Worker](#class-worker)
|
||||||
|
* [worker.executionContext()](#workerexecutioncontext)
|
||||||
|
* [worker.url()](#workerurl)
|
||||||
- [class: Keyboard](#class-keyboard)
|
- [class: Keyboard](#class-keyboard)
|
||||||
* [keyboard.down(key[, options])](#keyboarddownkey-options)
|
* [keyboard.down(key[, options])](#keyboarddownkey-options)
|
||||||
* [keyboard.press(key[, options])](#keyboardpresskey-options)
|
* [keyboard.press(key[, options])](#keyboardpresskey-options)
|
||||||
@ -736,6 +742,16 @@ Emitted when a request finishes successfully.
|
|||||||
|
|
||||||
Emitted when a [response] is received.
|
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)
|
#### page.$(selector)
|
||||||
- `selector` <[string]> A [selector] to query page for
|
- `selector` <[string]> A [selector] to query page for
|
||||||
- returns: <[Promise]<?[ElementHandle]>>
|
- returns: <[Promise]<?[ElementHandle]>>
|
||||||
@ -1596,6 +1612,32 @@ puppeteer.launch().then(async browser => {
|
|||||||
```
|
```
|
||||||
Shortcut for [page.mainFrame().waitForXPath(xpath[, options])](#framewaitforxpathxpath-options).
|
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
|
### 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.
|
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"
|
[xpath]: https://developer.mozilla.org/en-US/docs/Web/XPath "xpath"
|
||||||
[UnixTime]: https://en.wikipedia.org/wiki/Unix_time "Unix Time"
|
[UnixTime]: https://en.wikipedia.org/wiki/Unix_time "Unix Time"
|
||||||
[SecurityDetails]: #class-securitydetails "SecurityDetails"
|
[SecurityDetails]: #class-securitydetails "SecurityDetails"
|
||||||
|
[Worker]: #class-worker "Worker"
|
||||||
|
@ -163,7 +163,7 @@ class Connection extends EventEmitter {
|
|||||||
|
|
||||||
class CDPSession extends EventEmitter {
|
class CDPSession extends EventEmitter {
|
||||||
/**
|
/**
|
||||||
* @param {!Connection} connection
|
* @param {!Connection|!CDPSession} connection
|
||||||
* @param {string} targetId
|
* @param {string} targetId
|
||||||
* @param {string} sessionId
|
* @param {string} sessionId
|
||||||
*/
|
*/
|
||||||
@ -175,6 +175,8 @@ class CDPSession extends EventEmitter {
|
|||||||
this._connection = connection;
|
this._connection = connection;
|
||||||
this._targetId = targetId;
|
this._targetId = targetId;
|
||||||
this._sessionId = sessionId;
|
this._sessionId = sessionId;
|
||||||
|
/** @type {!Map<string, !CDPSession>}*/
|
||||||
|
this._sessions = new Map();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -215,6 +217,17 @@ class CDPSession extends EventEmitter {
|
|||||||
else
|
else
|
||||||
callback.resolve(object.result);
|
callback.resolve(object.result);
|
||||||
} else {
|
} 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);
|
console.assert(!object.id);
|
||||||
this.emit(object.method, object.params);
|
this.emit(object.method, object.params);
|
||||||
}
|
}
|
||||||
@ -230,6 +243,16 @@ class CDPSession extends EventEmitter {
|
|||||||
this._callbacks.clear();
|
this._callbacks.clear();
|
||||||
this._connection = null;
|
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);
|
helper.tracePublicAPI(CDPSession);
|
||||||
|
|
||||||
|
@ -20,7 +20,7 @@ class ExecutionContext {
|
|||||||
/**
|
/**
|
||||||
* @param {!Puppeteer.CDPSession} client
|
* @param {!Puppeteer.CDPSession} client
|
||||||
* @param {!Protocol.Runtime.ExecutionContextDescription} contextPayload
|
* @param {!Protocol.Runtime.ExecutionContextDescription} contextPayload
|
||||||
* @param {function(*):!JSHandle} objectHandleFactory
|
* @param {function(!Protocol.Runtime.RemoteObject):!JSHandle} objectHandleFactory
|
||||||
* @param {?Puppeteer.Frame} frame
|
* @param {?Puppeteer.Frame} frame
|
||||||
*/
|
*/
|
||||||
constructor(client, contextPayload, objectHandleFactory, frame) {
|
constructor(client, contextPayload, objectHandleFactory, frame) {
|
||||||
|
@ -215,7 +215,7 @@ class FrameManager extends EventEmitter {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {number} contextId
|
* @param {number} contextId
|
||||||
* @param {*} remoteObject
|
* @param {!Protocol.Runtime.RemoteObject} remoteObject
|
||||||
* @return {!JSHandle}
|
* @return {!JSHandle}
|
||||||
*/
|
*/
|
||||||
createJSHandle(contextId, remoteObject) {
|
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 Tracing = require('./Tracing');
|
||||||
const {helper, debugError} = require('./helper');
|
const {helper, debugError} = require('./helper');
|
||||||
const {Coverage} = require('./Coverage');
|
const {Coverage} = require('./Coverage');
|
||||||
|
const Worker = require('./Worker');
|
||||||
|
|
||||||
const writeFileAsync = helper.promisify(fs.writeFile);
|
const writeFileAsync = helper.promisify(fs.writeFile);
|
||||||
|
|
||||||
@ -45,6 +46,7 @@ class Page extends EventEmitter {
|
|||||||
const page = new Page(client, target, frameTree, ignoreHTTPSErrors, screenshotTaskQueue);
|
const page = new Page(client, target, frameTree, ignoreHTTPSErrors, screenshotTaskQueue);
|
||||||
|
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
|
client.send('Target.setAutoAttach', {autoAttach: true, waitForDebuggerOnStart: false}),
|
||||||
client.send('Page.setLifecycleEventsEnabled', { enabled: true }),
|
client.send('Page.setLifecycleEventsEnabled', { enabled: true }),
|
||||||
client.send('Network.enable', {}),
|
client.send('Network.enable', {}),
|
||||||
client.send('Runtime.enable', {}),
|
client.send('Runtime.enable', {}),
|
||||||
@ -87,6 +89,30 @@ class Page extends EventEmitter {
|
|||||||
|
|
||||||
this._screenshotTaskQueue = screenshotTaskQueue;
|
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.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.FrameDetached, event => this.emit(Page.Events.FrameDetached, event));
|
||||||
this._frameManager.on(FrameManager.Events.FrameNavigated, event => this.emit(Page.Events.FrameNavigated, 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('Security.certificateError', event => this._onCertificateError(event));
|
||||||
client.on('Inspector.targetCrashed', event => this._onTargetCrashed());
|
client.on('Inspector.targetCrashed', event => this._onTargetCrashed());
|
||||||
client.on('Performance.metrics', event => this._emitMetrics(event));
|
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));
|
this._target._isClosedPromise.then(() => this.emit(Page.Events.Close));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -126,10 +152,14 @@ class Page extends EventEmitter {
|
|||||||
this.emit('error', new Error('Page crashed!'));
|
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;
|
const {level, text, args} = event.entry;
|
||||||
if (args)
|
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));
|
this.emit(Page.Events.Console, new ConsoleMessage(level, text));
|
||||||
}
|
}
|
||||||
@ -176,6 +206,13 @@ class Page extends EventEmitter {
|
|||||||
return this._frameManager.frames();
|
return this._frameManager.frames();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return {!Array<!Worker>}
|
||||||
|
*/
|
||||||
|
workers() {
|
||||||
|
return Array.from(this._workers.values());
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {boolean} value
|
* @param {boolean} value
|
||||||
*/
|
*/
|
||||||
@ -1019,6 +1056,8 @@ Page.Events = {
|
|||||||
FrameNavigated: 'framenavigated',
|
FrameNavigated: 'framenavigated',
|
||||||
Load: 'load',
|
Load: 'load',
|
||||||
Metrics: 'metrics',
|
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;
|
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() {
|
describe('Page.browser', function() {
|
||||||
it('should return the correct browser instance', async function({ page, browser }) {
|
it('should return the correct browser instance', async function({ page, browser }) {
|
||||||
|
Loading…
Reference in New Issue
Block a user