Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(worker): Allow Workers to emit their network events #2717

Closed
wants to merge 5 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
26 changes: 26 additions & 0 deletions docs/api.md
Expand Up @@ -132,6 +132,10 @@
* [page.waitForXPath(xpath[, options])](#pagewaitforxpathxpath-options)
* [page.workers()](#pageworkers)
- [class: Worker](#class-worker)
* [event: 'request'](#event-request-1)
* [event: 'requestfailed'](#event-requestfailed-1)
* [event: 'requestfinished'](#event-requestfinished-1)
* [event: 'response'](#event-response-1)
* [worker.evaluate(pageFunction, ...args)](#workerevaluatepagefunction-args)
* [worker.evaluateHandle(pageFunction, ...args)](#workerevaluatehandlepagefunction-args)
* [worker.executionContext()](#workerexecutioncontext)
Expand Down Expand Up @@ -1635,6 +1639,8 @@ This method returns all of the dedicated [WebWorkers](https://developer.mozilla.

### class: Worker

* extends: [`EventEmitter`](https://nodejs.org/api/events.html#events_class_eventemitter)

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.

Expand All @@ -1647,6 +1653,26 @@ for (const worker of page.workers())
console.log(' ' + worker.url());
```

#### event: 'request'
- <[Request]>

Emitted when a worker issues a request. The [request] object is read-only.

#### event: 'requestfailed'
- <[Request]>

Emitted when a request fails, for example by timing out.

#### event: 'requestfinished'
- <[Request]>

Emitted when a request finishes successfully.

#### event: 'response'
- <[Response]>

Emitted when a [response] is received.

#### worker.evaluate(pageFunction, ...args)
- `pageFunction` <[function]|[string]> Function to be evaluated in the worker context
- `...args` <...[Serializable]|[JSHandle]> Arguments to pass to `pageFunction`
Expand Down
2 changes: 1 addition & 1 deletion lib/NetworkManager.js
Expand Up @@ -211,7 +211,7 @@ class NetworkManager extends EventEmitter {
*/
_handleRequestStart(requestId, interceptionId, url, isNavigationRequest, resourceType, requestPayload, frameId, redirectChain) {
let frame = null;
if (frameId)
if (frameId && this._frameManager)
frame = this._frameManager.frame(frameId);
const request = new Request(this._client, requestId, interceptionId, isNavigationRequest, this._userRequestInterceptionEnabled, url, resourceType, requestPayload, frame, redirectChain);
if (requestId)
Expand Down
22 changes: 20 additions & 2 deletions lib/Worker.js
Expand Up @@ -16,6 +16,7 @@
const EventEmitter = require('events');
const {helper, debugError} = require('./helper');
const {ExecutionContext, JSHandle} = require('./ExecutionContext');
const {NetworkManager} = require('./NetworkManager');

class Worker extends EventEmitter {
/**
Expand All @@ -28,17 +29,27 @@ class Worker extends EventEmitter {
this._client = client;
this._url = url;
this._executionContextPromise = new Promise(x => this._executionContextCallback = x);
this._networkManager = new NetworkManager(client, null);
/** @type {function(!Protocol.Runtime.RemoteObject):!JSHandle} */
let jsHandleFactory;
this._client.once('Runtime.executionContextCreated', async event => {
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('Runtime.consoleAPICalled', event => consoleAPICalled(event.type, event.args.map(jsHandleFactory)));

this._networkManager.on(NetworkManager.Events.Request, event => this.emit(Worker.Events.Request, event));
this._networkManager.on(NetworkManager.Events.Response, event => this.emit(Worker.Events.Response, event));
this._networkManager.on(NetworkManager.Events.RequestFailed, event => this.emit(Worker.Events.RequestFailed, event));
this._networkManager.on(NetworkManager.Events.RequestFinished, event => this.emit(Worker.Events.RequestFinished, event));

// This might fail if the target is closed before we recieve all execution contexts.
Promise.all([
client.send('Network.enable', {}),
client.send('Runtime.enable', {}),
]).catch(debugError);
}

/**
Expand Down Expand Up @@ -74,5 +85,12 @@ class Worker extends EventEmitter {
}
}

Worker.Events = {
Request: 'request',
Response: 'response',
RequestFailed: 'requestfailed',
RequestFinished: 'requestfinished',
};

module.exports = Worker;
helper.tracePublicAPI(Worker);
10 changes: 10 additions & 0 deletions test/assets/worker/worker.js
Expand Up @@ -4,11 +4,21 @@ function workerFunction() {
return 'worker function result';
}

function remoteFetch(url) {
const request = new XMLHttpRequest();
request.open('GET', url, false);
request.send();
return request.responseText;
}

self.addEventListener('message', event => {
console.log('got this data: ' + event.data);
});

(async function() {
// so that lint doesn't complain about remoteFetch being unused
remoteFetch.toString();

while (true) {
self.postMessage(workerFunction.toString());
await new Promise(x => setTimeout(x, 100));
Expand Down
45 changes: 45 additions & 0 deletions test/worker.spec.js
Expand Up @@ -14,9 +14,54 @@ module.exports.addTests = function({testRunner, expect}) {

expect(await worker.evaluate(() => self.workerFunction())).toBe('worker function result');

const fetchTxtPath = '/fetch.txt';
server.setRoute(fetchTxtPath, (req, res) => {
res.statusCode = 200;
res.end('fetched text');
});

const requestPromise = new Promise(x => worker.once('request', x));
const responsePromise = new Promise(x => worker.once('response', x));
const requestFinishedPromise = new Promise(x => worker.once('requestfinished', x));
expect(await worker.evaluate(x => self.remoteFetch(x), server.PREFIX + fetchTxtPath)).toBe('fetched text');
expect(await requestPromise);
expect(await responsePromise);
expect(await requestFinishedPromise);

await page.goto(server.EMPTY_PAGE);
expect(page.workers()).toEqual([]);
});
it('should have workers emit request, requestfinished, and response events', 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];

const fetchTxtPath = '/fetch.txt';
server.setRoute(fetchTxtPath, (req, res) => {
res.statusCode = 200;
res.end('fetched text');
});

const requestPromise = new Promise(x => worker.once('request', x));
const responsePromise = new Promise(x => worker.once('response', x));
const requestFinishedPromise = new Promise(x => worker.once('requestfinished', x));
expect(await worker.evaluate(x => self.remoteFetch(x), server.PREFIX + fetchTxtPath)).toBe('fetched text');
expect(await requestPromise).toBeTruthy();
expect(await responsePromise).toBeTruthy();
expect(await requestFinishedPromise).toBeTruthy();
});
it('should have workers emit requestfailed events', async function({page, server, httpsServer}) {
await Promise.all([
new Promise(x => page.once('workercreated', x)),
page.goto(server.PREFIX + '/worker/worker.html')]);
const worker = page.workers()[0];

worker.on('requestfailed', request => expect(request).toBeTruthy());
let error = null;
await worker.evaluate(x => self.remoteFetch(x), httpsServer.EMPTY_PAGE).catch(e => error = e);
expect(error.message).toContain('DOMException: Failed to execute \'send\'');
});
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'));
Expand Down