diff --git a/docs/api.md b/docs/api.md index 1bc66a58..3ed5ca33 100644 --- a/docs/api.md +++ b/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) @@ -595,7 +601,7 @@ will be closed. #### browserContext.isIncognito() - returns: <[boolean]> -Returns whether BrowserContext is incognito. +Returns whether BrowserContext is incognito. The default browser context is the only non-incognito browser context. > **NOTE** the default browser context cannot be closed. @@ -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]> @@ -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" diff --git a/lib/Connection.js b/lib/Connection.js index c85749a0..f4b283ea 100644 --- a/lib/Connection.js +++ b/lib/Connection.js @@ -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}*/ + 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); diff --git a/lib/ExecutionContext.js b/lib/ExecutionContext.js index f326d315..1f3a2549 100644 --- a/lib/ExecutionContext.js +++ b/lib/ExecutionContext.js @@ -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) { diff --git a/lib/FrameManager.js b/lib/FrameManager.js index e01c7aff..f395b1e6 100644 --- a/lib/FrameManager.js +++ b/lib/FrameManager.js @@ -215,7 +215,7 @@ class FrameManager extends EventEmitter { /** * @param {number} contextId - * @param {*} remoteObject + * @param {!Protocol.Runtime.RemoteObject} remoteObject * @return {!JSHandle} */ createJSHandle(contextId, remoteObject) { diff --git a/lib/Page.js b/lib/Page.js index f3d9dbee..169c8123 100644 --- a/lib/Page.js +++ b/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} */ + 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} + */ + 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', }; /** diff --git a/lib/Worker.js b/lib/Worker.js new file mode 100644 index 00000000..42a47f89 --- /dev/null +++ b/lib/Worker.js @@ -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} + */ + async executionContext() { + return this._executionContextPromise; + } +} + +module.exports = Worker; +helper.tracePublicAPI(Worker); diff --git a/test/assets/worker/worker.html b/test/assets/worker/worker.html new file mode 100644 index 00000000..7de2d9fd --- /dev/null +++ b/test/assets/worker/worker.html @@ -0,0 +1,14 @@ + + + + Worker test + + + + + \ No newline at end of file diff --git a/test/assets/worker/worker.js b/test/assets/worker/worker.js new file mode 100644 index 00000000..d0d229a1 --- /dev/null +++ b/test/assets/worker/worker.js @@ -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)); + } +})(); \ No newline at end of file diff --git a/test/page.spec.js b/test/page.spec.js index 29191232..950b82f2 100644 --- a/test/page.spec.js +++ b/test/page.spec.js @@ -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 }) {