feat: worker convenience methods (#2677)

This patch:
- adds `worker.evaluate` and `worker.evaluateHandle` methods as a shortcut to their execution context equivalents.
- makes the error messages a bit nicer when interacting with a closed worker (as opposed to a closed page).
- moves the worker tests into their own spec file.
This commit is contained in:
Joel Einbinder 2018-06-06 18:16:17 -07:00 committed by Andrey Lushnikov
parent 3e82a54fd1
commit 2ff0adcad8
8 changed files with 100 additions and 41 deletions

View File

@ -132,6 +132,8 @@
* [page.waitForXPath(xpath[, options])](#pagewaitforxpathxpath-options) * [page.waitForXPath(xpath[, options])](#pagewaitforxpathxpath-options)
* [page.workers()](#pageworkers) * [page.workers()](#pageworkers)
- [class: Worker](#class-worker) - [class: Worker](#class-worker)
* [worker.evaluate(pageFunction, ...args)](#workerevaluatepagefunction-args)
* [worker.evaluateHandle(pageFunction, ...args)](#workerevaluatehandlepagefunction-args)
* [worker.executionContext()](#workerexecutioncontext) * [worker.executionContext()](#workerexecutioncontext)
* [worker.url()](#workerurl) * [worker.url()](#workerurl)
- [class: Keyboard](#class-keyboard) - [class: Keyboard](#class-keyboard)
@ -1645,6 +1647,28 @@ for (const worker of page.workers())
console.log(' ' + worker.url()); console.log(' ' + worker.url());
``` ```
#### worker.evaluate(pageFunction, ...args)
- `pageFunction` <[function]|[string]> Function to be evaluated in the worker context
- `...args` <...[Serializable]|[JSHandle]> Arguments to pass to `pageFunction`
- returns: <[Promise]<[Serializable]>> Promise which resolves to the return value of `pageFunction`
If the function passed to the `worker.evaluate` returns a [Promise], then `worker.evaluate` would wait for the promise to resolve and return its value.
If the function passed to the `worker.evaluate` returns a non-[Serializable] value, then `worker.evaluate` resolves to `undefined`.
Shortcut for [(await worker.executionContext()).evaluate(pageFunction, ...args)](#executioncontextevaluatepagefunction-args).
#### worker.evaluateHandle(pageFunction, ...args)
- `pageFunction` <[function]|[string]> Function to be evaluated in the page context
- `...args` <...[Serializable]|[JSHandle]> Arguments to pass to `pageFunction`
- returns: <[Promise]<[JSHandle]>> Promise which resolves to the return value of `pageFunction` as in-page object (JSHandle)
The only difference between `worker.evaluate` and `worker.evaluateHandle` is that `worker.evaluateHandle` returns in-page object (JSHandle).
If the function passed to the `worker.evaluateHandle` returns a [Promise], then `worker.evaluateHandle` would wait for the promise to resolve and return its value.
Shortcut for [(await worker.executionContext()).evaluateHandle(pageFunction, ...args)](#executioncontextevaluatehandlepagefunction-args).
#### worker.executionContext() #### worker.executionContext()
- returns: <[Promise]<[ExecutionContext]>> - returns: <[Promise]<[ExecutionContext]>>

View File

@ -107,7 +107,7 @@ class Browser extends EventEmitter {
const {browserContextId} = targetInfo; const {browserContextId} = targetInfo;
const context = (browserContextId && this._contexts.has(browserContextId)) ? this._contexts.get(browserContextId) : this._defaultContext; const context = (browserContextId && this._contexts.has(browserContextId)) ? this._contexts.get(browserContextId) : this._defaultContext;
const target = new Target(targetInfo, context, () => this._connection.createSession(targetInfo.targetId), this._ignoreHTTPSErrors, this._setDefaultViewport, this._screenshotTaskQueue); const target = new Target(targetInfo, context, () => this._connection.createSession(targetInfo), this._ignoreHTTPSErrors, this._setDefaultViewport, this._screenshotTaskQueue);
assert(!this._targets.has(event.targetInfo.targetId), 'Target should not exist before targetCreated'); assert(!this._targets.has(event.targetInfo.targetId), 'Target should not exist before targetCreated');
this._targets.set(event.targetInfo.targetId, target); this._targets.set(event.targetInfo.targetId, target);

View File

@ -150,12 +150,12 @@ class Connection extends EventEmitter {
} }
/** /**
* @param {string} targetId * @param {Protocol.Target.TargetInfo} targetInfo
* @return {!Promise<!CDPSession>} * @return {!Promise<!CDPSession>}
*/ */
async createSession(targetId) { async createSession(targetInfo) {
const {sessionId} = await this.send('Target.attachToTarget', {targetId}); const {sessionId} = await this.send('Target.attachToTarget', {targetId: targetInfo.targetId});
const session = new CDPSession(this, targetId, sessionId); const session = new CDPSession(this, targetInfo.type, sessionId);
this._sessions.set(sessionId, session); this._sessions.set(sessionId, session);
return session; return session;
} }
@ -164,16 +164,16 @@ class Connection extends EventEmitter {
class CDPSession extends EventEmitter { class CDPSession extends EventEmitter {
/** /**
* @param {!Connection|!CDPSession} connection * @param {!Connection|!CDPSession} connection
* @param {string} targetId * @param {string} targetType
* @param {string} sessionId * @param {string} sessionId
*/ */
constructor(connection, targetId, sessionId) { constructor(connection, targetType, sessionId) {
super(); super();
this._lastId = 0; this._lastId = 0;
/** @type {!Map<number, {resolve: function, reject: function, error: !Error, method: string}>}*/ /** @type {!Map<number, {resolve: function, reject: function, error: !Error, method: string}>}*/
this._callbacks = new Map(); this._callbacks = new Map();
this._connection = connection; this._connection = connection;
this._targetId = targetId; this._targetType = targetType;
this._sessionId = sessionId; this._sessionId = sessionId;
/** @type {!Map<string, !CDPSession>}*/ /** @type {!Map<string, !CDPSession>}*/
this._sessions = new Map(); this._sessions = new Map();
@ -186,7 +186,7 @@ class CDPSession extends EventEmitter {
*/ */
send(method, params = {}) { send(method, params = {}) {
if (!this._connection) if (!this._connection)
return Promise.reject(new Error(`Protocol error (${method}): Session closed. Most likely the page has been closed.`)); return Promise.reject(new Error(`Protocol error (${method}): Session closed. Most likely the ${this._targetType} has been closed.`));
const id = ++this._lastId; const id = ++this._lastId;
const message = JSON.stringify({id, method, params}); const message = JSON.stringify({id, method, params});
debugSession('SEND ► ' + message); debugSession('SEND ► ' + message);
@ -245,11 +245,11 @@ class CDPSession extends EventEmitter {
} }
/** /**
* @param {string} targetId * @param {string} targetType
* @param {string} sessionId * @param {string} sessionId
*/ */
_createSession(targetId, sessionId) { _createSession(targetType, sessionId) {
const session = new CDPSession(this, targetId, sessionId); const session = new CDPSession(this, targetType, sessionId);
this._sessions.set(sessionId, session); this._sessions.set(sessionId, session);
return session; return session;
} }

View File

@ -100,7 +100,7 @@ class Page extends EventEmitter {
}).catch(debugError); }).catch(debugError);
return; return;
} }
const session = client._createSession(event.targetInfo.targetId, event.sessionId); const session = client._createSession(event.targetInfo.type, event.sessionId);
const worker = new Worker(session, event.targetInfo.url, this._onLogEntryAdded.bind(this, session)); const worker = new Worker(session, event.targetInfo.url, this._onLogEntryAdded.bind(this, session));
this._workers.set(event.sessionId, worker); this._workers.set(event.sessionId, worker);
this.emit(Page.Events.WorkerCreated, worker); this.emit(Page.Events.WorkerCreated, worker);

View File

@ -53,6 +53,24 @@ class Worker extends EventEmitter {
async executionContext() { async executionContext() {
return this._executionContextPromise; return this._executionContextPromise;
} }
/**
* @param {function()|string} pageFunction
* @param {!Array<*>} args
* @return {!Promise<*>}
*/
async evaluate(pageFunction, ...args) {
return (await this._executionContextPromise).evaluate(pageFunction, ...args);
}
/**
* @param {function()|string} pageFunction
* @param {!Array<*>} args
* @return {!Promise<!JSHandle>}
*/
async evaluateHandle(pageFunction, ...args) {
return (await this._executionContextPromise).evaluateHandle(pageFunction, ...args);
}
} }
module.exports = Worker; module.exports = Worker;

View File

@ -1625,34 +1625,6 @@ module.exports.addTests = function({testRunner, expect, puppeteer, DeviceDescrip
await closedPromise; await closedPromise;
}); });
}); });
describe('Workers', function() {
it('Page.workers', async function({page, server}) {
await Promise.all([
new Promise(x => page.once('workercreated', x)),
page.goto(server.PREFIX + '/worker/worker.html')]);
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 }) {

View File

@ -145,6 +145,7 @@ describe('Page', function() {
require('./page.spec.js').addTests({testRunner, expect, puppeteer, DeviceDescriptors, headless}); require('./page.spec.js').addTests({testRunner, expect, puppeteer, DeviceDescriptors, headless});
require('./target.spec.js').addTests({testRunner, expect, puppeteer}); require('./target.spec.js').addTests({testRunner, expect, puppeteer});
require('./tracing.spec.js').addTests({testRunner, expect}); require('./tracing.spec.js').addTests({testRunner, expect});
require('./worker.spec.js').addTests({testRunner, expect});
}); });
// Top-level tests that launch Browser themselves. // Top-level tests that launch Browser themselves.

44
test/worker.spec.js Normal file
View File

@ -0,0 +1,44 @@
module.exports.addTests = function({testRunner, expect}) {
const {describe, xdescribe, fdescribe} = testRunner;
const {it, fit, xit} = testRunner;
const {beforeAll, beforeEach, afterAll, afterEach} = testRunner;
describe('Workers', function() {
it('Page.workers', async function({page, server}) {
await Promise.all([
new Promise(x => page.once('workercreated', x)),
page.goto(server.PREFIX + '/worker/worker.html')]);
const worker = page.workers()[0];
expect(worker.url()).toContain('worker.js');
expect(await worker.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 workerThisObj = await worker.evaluateHandle(() => this);
const workerDestroyedPromise = new Promise(x => page.once('workerdestroyed', x));
await page.evaluate(workerObj => workerObj.terminate(), workerObj);
expect(await workerDestroyedPromise).toBe(worker);
const error = await workerThisObj.getProperty('self').catch(error => error);
expect(error.message).toContain('Most likely the worker has been closed.');
});
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');
});
it('should have an execution context', async function({page}) {
const workerCreatedPromise = new Promise(x => page.once('workercreated', x));
await page.evaluate(() => new Worker(`data:text/javascript,console.log(1)`));
const worker = await workerCreatedPromise;
expect(await (await worker.executionContext()).evaluate('1+1')).toBe(2);
});
});
};