From c118b208fa639ad45d94f41488441ee241353e6e Mon Sep 17 00:00:00 2001 From: Andrey Lushnikov Date: Tue, 19 Feb 2019 14:51:56 -0800 Subject: [PATCH] feat(firefox): basic request interception support (#4034) This patch implements `page.setRequestInterception`, `page.continue` and `page.abort` methods. --- .../puppeteer-firefox/lib/NetworkManager.js | 50 +++++++++++++++- experimental/puppeteer-firefox/lib/Page.js | 4 ++ experimental/puppeteer-firefox/package.json | 2 +- test/ignorehttpserrors.spec.js | 2 +- test/network.spec.js | 60 ++++++++++++------- 5 files changed, 93 insertions(+), 25 deletions(-) diff --git a/experimental/puppeteer-firefox/lib/NetworkManager.js b/experimental/puppeteer-firefox/lib/NetworkManager.js index 73c0f774..df134e9c 100644 --- a/experimental/puppeteer-firefox/lib/NetworkManager.js +++ b/experimental/puppeteer-firefox/lib/NetworkManager.js @@ -1,4 +1,4 @@ -const {helper} = require('./helper'); +const {helper, assert, debugError} = require('./helper'); const util = require('util'); const EventEmitter = require('events'); const {Events} = require('./Events'); @@ -15,6 +15,7 @@ class NetworkManager extends EventEmitter { helper.addEventListener(session, 'Network.requestWillBeSent', this._onRequestWillBeSent.bind(this)), helper.addEventListener(session, 'Network.responseReceived', this._onResponseReceived.bind(this)), helper.addEventListener(session, 'Network.requestFinished', this._onRequestFinished.bind(this)), + helper.addEventListener(session, 'Network.requestFailed', this._onRequestFailed.bind(this)), ]; } @@ -26,6 +27,10 @@ class NetworkManager extends EventEmitter { this._frameManager = frameManager; } + async setRequestInterception(enabled) { + await this._session.send('Network.setRequestInterception', {enabled}); + } + _onRequestWillBeSent(event) { const redirected = event.redirectedFrom ? this._requests.get(event.redirectedFrom) : null; const frame = redirected ? redirected.frame() : (this._frameManager && event.frameId ? this._frameManager.frame(event.frameId) : null); @@ -37,7 +42,7 @@ class NetworkManager extends EventEmitter { redirectChain.push(redirected); this._requests.delete(redirected._id); } - const request = new Request(frame, redirectChain, event); + const request = new Request(this._session, frame, redirectChain, event); this._requests.set(request._id, request); this.emit(Events.NetworkManager.Request, request); } @@ -61,6 +66,15 @@ class NetworkManager extends EventEmitter { this._requests.delete(request._id); this.emit(Events.NetworkManager.RequestFinished, request); } + + _onRequestFailed(event) { + const request = this._requests.get(event.requestId); + if (!request) + return; + this._requests.delete(request._id); + request._errorText = event.errorCode; + this.emit(Events.NetworkManager.RequestFailed, request); + } } /** @@ -94,21 +108,51 @@ const causeToResourceType = { }; class Request { - constructor(frame, redirectChain, payload) { + constructor(session, frame, redirectChain, payload) { + this._session = session; this._frame = frame; this._id = payload.requestId; this._redirectChain = redirectChain; this._url = payload.url; this._postData = payload.postData; + this._suspended = payload.suspended; this._response = null; + this._errorText = null; this._isNavigationRequest = payload.isNavigationRequest; this._method = payload.method; this._resourceType = causeToResourceType[payload.cause] || 'other'; this._headers = {}; + this._interceptionHandled = false; for (const {name, value} of payload.headers) this._headers[name.toLowerCase()] = value; } + failure() { + return this._errorText ? {errorText: this._errorText} : null; + } + + async continue() { + assert(this._suspended, 'Request Interception is not enabled!'); + assert(!this._interceptionHandled, 'Request is already handled!'); + this._interceptionHandled = true; + await this._session.send('Network.resumeSuspendedRequest', { + requestId: this._id, + }).catch(error => { + debugError(error); + }); + } + + async abort() { + assert(this._suspended, 'Request Interception is not enabled!'); + assert(!this._interceptionHandled, 'Request is already handled!'); + this._interceptionHandled = true; + await this._session.send('Network.abortSuspendedRequest', { + requestId: this._id, + }).catch(error => { + debugError(error); + }); + } + postData() { return this._postData; } diff --git a/experimental/puppeteer-firefox/lib/Page.js b/experimental/puppeteer-firefox/lib/Page.js index 906f3688..1e7e1665 100644 --- a/experimental/puppeteer-firefox/lib/Page.js +++ b/experimental/puppeteer-firefox/lib/Page.js @@ -73,6 +73,10 @@ class Page extends EventEmitter { }); } + async setRequestInterception(enabled) { + await this._networkManager.setRequestInterception(enabled); + } + /** * @param {(string|Function)} urlOrPredicate * @param {!{timeout?: number}=} options diff --git a/experimental/puppeteer-firefox/package.json b/experimental/puppeteer-firefox/package.json index 99894e2d..745f28a2 100644 --- a/experimental/puppeteer-firefox/package.json +++ b/experimental/puppeteer-firefox/package.json @@ -9,7 +9,7 @@ "node": ">=8.9.4" }, "puppeteer": { - "firefox_revision": "387ac6bbbe5357d174e9fb3aa9b6f935113c315d" + "firefox_revision": "98116977b3f0c936c92e917bdd571c340167a536" }, "scripts": { "install": "node install.js", diff --git a/test/ignorehttpserrors.spec.js b/test/ignorehttpserrors.spec.js index 98761930..c524c52b 100644 --- a/test/ignorehttpserrors.spec.js +++ b/test/ignorehttpserrors.spec.js @@ -69,7 +69,7 @@ module.exports.addTests = function({testRunner, expect, defaultBrowserOptions, p expect(error).toBe(null); expect(response.ok()).toBe(true); }); - it_fails_ffox('should work with request interception', async({page, server, httpsServer}) => { + it('should work with request interception', async({page, server, httpsServer}) => { await page.setRequestInterception(true); page.on('request', request => request.continue()); const response = await page.goto(httpsServer.EMPTY_PAGE); diff --git a/test/network.spec.js b/test/network.spec.js index 9aba5cff..0753cd7b 100644 --- a/test/network.spec.js +++ b/test/network.spec.js @@ -243,7 +243,7 @@ module.exports.addTests = function({testRunner, expect, CHROME}) { await new Promise(x => serverResponse.end('ld!', x)); expect(await responseText).toBe('hello world!'); }); - it_fails_ffox('Page.Events.RequestFailed', async({page, server}) => { + it('Page.Events.RequestFailed', async({page, server}) => { await page.setRequestInterception(true); page.on('request', request => { if (request.url().endsWith('css')) @@ -258,7 +258,10 @@ module.exports.addTests = function({testRunner, expect, CHROME}) { expect(failedRequests[0].url()).toContain('one-style.css'); expect(failedRequests[0].response()).toBe(null); expect(failedRequests[0].resourceType()).toBe('stylesheet'); - expect(failedRequests[0].failure().errorText).toBe('net::ERR_FAILED'); + if (CHROME) + expect(failedRequests[0].failure().errorText).toBe('net::ERR_FAILED'); + else + expect(failedRequests[0].failure().errorText).toBe('NS_ERROR_FAILURE'); expect(failedRequests[0].frame()).toBeTruthy(); }); it('Page.Events.RequestFinished', async({page, server}) => { @@ -317,7 +320,7 @@ module.exports.addTests = function({testRunner, expect, CHROME}) { expect(requests.get('script.js').isNavigationRequest()).toBe(false); expect(requests.get('style.css').isNavigationRequest()).toBe(false); }); - it_fails_ffox('should work with request interception', async({page, server}) => { + it('should work with request interception', async({page, server}) => { const requests = new Map(); page.on('request', request => { requests.set(request.url().split('/').pop(), request); @@ -340,10 +343,14 @@ module.exports.addTests = function({testRunner, expect, CHROME}) { }); }); - describe_fails_ffox('Page.setRequestInterception', function() { + describe('Page.setRequestInterception', function() { it('should intercept', async({page, server}) => { await page.setRequestInterception(true); page.on('request', request => { + if (utils.isFavicon(request)) { + request.continue(); + return; + } expect(request.url()).toContain('empty.html'); expect(request.headers()['user-agent']).toBeTruthy(); expect(request.method()).toBe('GET'); @@ -358,7 +365,8 @@ module.exports.addTests = function({testRunner, expect, CHROME}) { expect(response.ok()).toBe(true); expect(response.remoteAddress().port).toBe(server.PORT); }); - it('should work with intervention headers', async({page, server}) => { + // Intervention headers are chrome-specific. + (CHROME ? it : xit)('should work with intervention headers', async({page, server}) => { server.setRoute('/intervention', (req, res) => res.end(`