Skip to content

fix: wrap onRequestFinished to use promises #250

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

Merged
merged 1 commit into from
Dec 10, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 44 additions & 7 deletions src/browser-polyfill.js
Original file line number Diff line number Diff line change
Expand Up @@ -77,13 +77,17 @@ if (typeof browser === "undefined" || Object.getPrototypeOf(browser) !== Object.
* promise.
* @param {function} promise.resolve
* The promise's resolution function.
* @param {function} promise.rejection
* @param {function} promise.reject
* The promise's rejection function.
* @param {object} metadata
* Metadata about the wrapped method which has created the callback.
* @param {integer} metadata.maxResolvedArgs
* The maximum number of arguments which may be passed to the
* callback created by the wrapped async function.
* @param {boolean} metadata.singleCallbackArg
* Whether or not the promise is resolved with only the first
* argument of the callback, alternatively an array of all the
* callback arguments is resolved. By default, if the callback
* function is invoked with only a single argument, that will be
* resolved to the promise, while all arguments will be resolved as
* an array if multiple are given.
*
* @returns {function}
* The generated callback function.
Expand Down Expand Up @@ -118,9 +122,13 @@ if (typeof browser === "undefined" || Object.getPrototypeOf(browser) !== Object.
* The maximum number of arguments which may be passed to the
* function. If called with more than this number of arguments, the
* wrapper will raise an exception.
* @param {integer} metadata.maxResolvedArgs
* The maximum number of arguments which may be passed to the
* callback created by the wrapped async function.
* @param {boolean} metadata.singleCallbackArg
* Whether or not the promise is resolved with only the first
* argument of the callback, alternatively an array of all the
* callback arguments is resolved. By default, if the callback
* function is invoked with only a single argument, that will be
* resolved to the promise, while all arguments will be resolved as
* an array if multiple are given.
*
* @returns {function(object, ...*)}
* The generated wrapper function.
Expand Down Expand Up @@ -345,6 +353,30 @@ if (typeof browser === "undefined" || Object.getPrototypeOf(browser) !== Object.
},
});

const onRequestFinishedWrappers = new DefaultWeakMap(listener => {
if (typeof listener !== "function") {
return listener;
}

/**
* Wraps an onRequestFinished listener function so that it will return a
* `getContent()` property which returns a `Promise` rather than using a
* callback API.
*
* @param {object} req
* The HAR entry object representing the network request.
*/
return function onRequestFinished(req) {
const wrappedReq = wrapObject(req, {} /* wrappers */, {
getContent: {
minArgs: 0,
maxArgs: 0,
},
});
listener(wrappedReq);
};
});

// Keep track if the deprecation warning has been logged at least once.
let loggedSendResponseDeprecationWarning = false;

Expand Down Expand Up @@ -480,6 +512,11 @@ if (typeof browser === "undefined" || Object.getPrototypeOf(browser) !== Object.
};

const staticWrappers = {
devtools: {
network: {
onRequestFinished: wrapEvent(onRequestFinishedWrappers),
},
},
runtime: {
onMessage: wrapEvent(onMessageWrappers),
onMessageExternal: wrapEvent(onMessageWrappers),
Expand Down
119 changes: 119 additions & 0 deletions test/test-onRequestFinished.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
"use strict";

const {deepEqual, equal, ok} = require("chai").assert;
const sinon = require("sinon");

const {setupTestDOMWindow} = require("./setup");

describe("browser-polyfill", () => {
describe("wrapped devtools.network.onRequestFinished listener", () => {
it("does not wrap the listener if it is not a function", () => {
const fakeChrome = {
devtools: {
network: {
onRequestFinished: {
addListener: sinon.spy(),
},
},
},
};

return setupTestDOMWindow(fakeChrome).then(window => {
const fakeNonFunctionListener = {fake: "non function listener"};

const browserOnRequestFinished = window.browser.devtools.network.onRequestFinished;
browserOnRequestFinished.addListener(fakeNonFunctionListener);

const fakeChromeOnRequestFinished = fakeChrome.devtools.network.onRequestFinished;
deepEqual(
fakeChromeOnRequestFinished.addListener.firstCall.args[0],
fakeNonFunctionListener,
"The non-function listener has not been wrapped"
);
});
});

it("promisifies the result", () => {
const fakeChrome = {
devtools: {
network: {
onRequestFinished: {
addListener: sinon.spy(),
hasListener: sinon.stub(),
removeListener: sinon.spy(),
},
},
},
};

return setupTestDOMWindow(fakeChrome).then(window => {
const listener = sinon.spy();

const browserOnRequestFinished = window.browser.devtools.network.onRequestFinished;
browserOnRequestFinished.addListener(listener);

const fakeChromeOnRequestFinished = fakeChrome.devtools.network.onRequestFinished;
ok(fakeChromeOnRequestFinished.addListener.calledOnce,
"devtools.network.onRequestFinished.addListener has been called once");

const wrappedListener = fakeChromeOnRequestFinished.addListener.firstCall.args[0];
wrappedListener({
getContent(cb) {
cb("<html>...</html>", "text/html; charset=utf8");
},
});

ok(listener.calledOnce, "listener has been called once");

const req = listener.firstCall.args[0];
return req.getContent().then(([content, encodingOrMimeType]) => {
equal(content, "<html>...</html>");
// On Chrome this is the encoding ('' or 'base64') while on Firefox
// this is the MIME type of the resource.
// See: https://github.com/mozilla/webextension-polyfill/issues/249#issuecomment-740000461
equal(encodingOrMimeType, "text/html; charset=utf8");
});
});
});

it("promisifies the result with a wrapped Request object", () => {
const fakeChrome = {
devtools: {
network: {
onRequestFinished: {
addListener: sinon.spy(),
hasListener: sinon.stub(),
removeListener: sinon.spy(),
},
},
},
};

return setupTestDOMWindow(fakeChrome).then(window => {
const listener = sinon.spy();

const browserOnRequestFinished = window.browser.devtools.network.onRequestFinished;
browserOnRequestFinished.addListener(listener);

const fakeChromeOnRequestFinished = fakeChrome.devtools.network.onRequestFinished;
ok(fakeChromeOnRequestFinished.addListener.calledOnce,
"devtools.network.onRequestFinished.addListener has been called once");

const request = Object.create({
inheritedProp: true,
getContent(cb) {
cb("", "");
},
});

const wrappedListener = fakeChromeOnRequestFinished.addListener.firstCall.args[0];
wrappedListener(request);

ok(listener.calledOnce, "listener has been called once");

const req = listener.firstCall.args[0];
ok(req.inheritedProp, "Wrapped request inherited prototype properties");
});
});
});
});