feat: Added support for the sendResponse callback in the runtime.onMessage listeners (#97)

This commit is contained in:
Luca Greco 2018-05-14 20:38:21 +02:00 committed by GitHub
parent 6f9cfdf6cf
commit 76eeeaccc9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 201 additions and 70 deletions

View File

@ -361,18 +361,51 @@ if (typeof browser === "undefined") {
* yield a response. False otherwise. * yield a response. False otherwise.
*/ */
return function onMessage(message, sender, sendResponse) { return function onMessage(message, sender, sendResponse) {
let result = listener(message, sender); let didCallSendResponse = false;
if (isThenable(result)) { let wrappedSendResponse;
let sendResponsePromise = new Promise(resolve => {
wrappedSendResponse = function(response) {
didCallSendResponse = true;
resolve(response);
};
});
let result = listener(message, sender, wrappedSendResponse);
const isResultThenable = result !== true && isThenable(result);
// If the listener didn't returned true or a Promise, or called
// wrappedSendResponse synchronously, we can exit earlier
// because there will be no response sent from this listener.
if (result !== true && !isResultThenable && !didCallSendResponse) {
return false;
}
// If the listener returned a Promise, send the resolved value as a
// result, otherwise wait the promise related to the wrappedSendResponse
// callback to resolve and send it as a response.
if (isResultThenable) {
result.then(sendResponse, error => { result.then(sendResponse, error => {
console.error(error); console.error(error);
sendResponse(error); // TODO: the error object is not serializable and so for now we just send
// `undefined`. Nevertheless, as being discussed in #97, this is not yet
// providing the expected behavior (the promise received from the sender should
// be rejected when the promise returned by the listener is being rejected).
sendResponse(undefined);
});
} else {
sendResponsePromise.then(sendResponse, error => {
console.error(error);
// TODO: same as above, we are currently sending `undefined` in this scenario
// because the error oject is not serializable, but it is not yet the behavior
// that this scenario should present.
sendResponse(undefined);
}); });
return true;
} else if (result !== undefined) {
sendResponse(result);
} }
// Let Chrome know that the listener is replying.
return true;
}; };
}); });

View File

@ -2,16 +2,49 @@ const {name} = browser.runtime.getManifest();
console.log(name, "background page loaded"); console.log(name, "background page loaded");
browser.runtime.onMessage.addListener((msg, sender) => { browser.runtime.onMessage.addListener((msg, sender, sendResponse) => {
console.log(name, "background received msg", {msg, sender}); console.log(name, "background received msg", {msg, sender});
try { switch (msg) {
browser.pageAction.show(sender.tab.id); case "test - sendMessage with returned Promise reply":
} catch (err) { try {
return Promise.resolve(`Unexpected error on pageAction.show: ${err}`); browser.pageAction.show(sender.tab.id);
} } catch (err) {
return Promise.resolve(`Unexpected error on pageAction.show: ${err}`);
}
return Promise.resolve("background page reply"); return Promise.resolve("bg page reply 1");
case "test - sendMessage with returned value reply":
// This is supposed to be ignored and the sender should receive
// a reply from the second listener.
return "Unexpected behavior: a plain return value should not be sent as a result";
case "test - sendMessage with synchronous sendResponse":
sendResponse("bg page reply 3");
return "value returned after calling sendResponse synchrously";
case "test - sendMessage with asynchronous sendResponse":
setTimeout(() => sendResponse("bg page reply 4"), 50);
return true;
case "test - second listener if the first does not reply":
// This is supposed to be ignored and the sender should receive
// a reply from the second listener.
return false;
default:
return Promise.resolve(
`Unxpected message received by the background page: ${JSON.stringify(msg)}\n`);
}
});
browser.runtime.onMessage.addListener((msg, sender, sendResponse) => {
setTimeout(() => {
sendResponse("second listener reply");
}, 100);
return true;
}); });
console.log(name, "background page ready to receive a content script message..."); console.log(name, "background page ready to receive a content script message...");

View File

@ -1,9 +1,27 @@
const {name} = browser.runtime.getManifest(); const {name} = browser.runtime.getManifest();
async function runTest() {
let reply;
reply = await browser.runtime.sendMessage("test - sendMessage with returned Promise reply");
console.log(name, "test - returned resolved Promise - received", reply);
reply = await browser.runtime.sendMessage("test - sendMessage with returned value reply");
console.log(name, "test - returned value - received", reply);
reply = await browser.runtime.sendMessage("test - sendMessage with synchronous sendResponse");
console.log(name, "test - synchronous sendResponse - received", reply);
reply = await browser.runtime.sendMessage("test - sendMessage with asynchronous sendResponse");
console.log(name, "test - asynchronous sendResponse - received", reply);
reply = await browser.runtime.sendMessage("test - second listener if the first does not reply");
console.log(name, "test - second listener sendResponse - received", reply);
console.log(name, "content script messages sent");
}
console.log(name, "content script loaded"); console.log(name, "content script loaded");
browser.runtime.sendMessage("content script message").then(reply => { runTest().catch((err) => {
console.log(name, "content script received reply", {reply}); console.error("content script error", err);
}); });
console.log(name, "content script message sent");

View File

@ -40,8 +40,12 @@ describe("browser.runtime.onMessage/sendMessage", function() {
const expectedConsoleMessages = [ const expectedConsoleMessages = [
[extensionName, "content script loaded"], [extensionName, "content script loaded"],
[extensionName, "content script message sent"], [extensionName, "test - returned resolved Promise - received", "bg page reply 1"],
[extensionName, "content script received reply", {"reply": "background page reply"}], [extensionName, "test - returned value - received", "second listener reply"],
[extensionName, "test - synchronous sendResponse - received", "bg page reply 3"],
[extensionName, "test - asynchronous sendResponse - received", "bg page reply 4"],
[extensionName, "test - second listener sendResponse - received", "second listener reply"],
[extensionName, "content script messages sent"],
]; ];
const lastExpectedMessage = expectedConsoleMessages.slice(-1).pop(); const lastExpectedMessage = expectedConsoleMessages.slice(-1).pop();

View File

@ -117,8 +117,10 @@ describe("browser-polyfill", () => {
equal(secondMessageListener.firstCall.args[0], "call second wrapper"); equal(secondMessageListener.firstCall.args[0], "call second wrapper");
}); });
}); });
});
it("sends the returned value as a message response", () => { describe("sendResponse callback", () => {
it("ignores the sendResponse calls when the listener returns a promise", () => {
const fakeChrome = { const fakeChrome = {
runtime: { runtime: {
lastError: null, lastError: null,
@ -128,70 +130,111 @@ describe("browser-polyfill", () => {
}, },
}; };
// Plain value returned. return setupTestDOMWindow(fakeChrome).then(window => {
const messageListener = sinon.stub(); const listener = sinon.spy((msg, sender, sendResponse) => {
const firstResponse = "fake reply"; sendResponse("Ignored sendReponse callback on returned Promise");
// Resolved Promise returned.
const secondResponse = Promise.resolve("fake reply 2");
// Rejected Promise returned.
const thirdResponse = Promise.reject("fake error 3");
const sendResponseSpy = sinon.spy(); return Promise.resolve("listener resolved value");
});
messageListener const sendResponseSpy = sinon.spy();
.onFirstCall().returns(firstResponse)
.onSecondCall().returns(secondResponse)
.onThirdCall().returns(thirdResponse);
let wrappedListener; window.browser.runtime.onMessage.addListener(listener);
ok(fakeChrome.runtime.onMessage.addListener.calledOnce,
"runtime.onMessage.addListener should have been called once");
let wrappedListener = fakeChrome.runtime.onMessage.addListener.firstCall.args[0];
let returnedValue = wrappedListener("test message", {name: "fake sender"}, sendResponseSpy);
equal(returnedValue, true, "the wrapped listener should have returned true");
ok(listener.calledOnce, "listener has been called once");
// Wait a promise to be resolved and then check the wrapped listener behaviors.
return Promise.resolve().then(() => {
ok(sendResponseSpy.calledOnce, "sendResponse callback called once");
equal(sendResponseSpy.firstCall.args[0], "listener resolved value",
"sendResponse has been called with the expected value");
});
});
});
it("ignores asynchronous sendResponse calls if the listener does not return true", () => {
const fakeChrome = {
runtime: {
lastError: null,
onMessage: {
addListener: sinon.spy(),
},
},
};
const waitPromises = [];
return setupTestDOMWindow(fakeChrome).then(window => { return setupTestDOMWindow(fakeChrome).then(window => {
window.browser.runtime.onMessage.addListener(messageListener); const listenerReturnsFalse = sinon.spy((msg, sender, sendResponse) => {
waitPromises.push(Promise.resolve().then(() => {
sendResponse("Ignored sendReponse callback on returned false");
}));
ok(fakeChrome.runtime.onMessage.addListener.calledOnce); return false;
});
wrappedListener = fakeChrome.runtime.onMessage.addListener.firstCall.args[0]; const listenerReturnsValue = sinon.spy((msg, sender, sendResponse) => {
waitPromises.push(Promise.resolve().then(() => {
sendResponse("Ignored sendReponse callback on non boolean/thenable return values");
}));
wrappedListener("fake message", {name: "fake sender"}, sendResponseSpy); // Any return value that is not a promise should not be sent as a response,
// and any return value that is not true should make the sendResponse
// calls to be ignored.
return "Ignored return value";
});
ok(messageListener.calledOnce, "The unwrapped message listener has been called"); const listenerReturnsTrue = sinon.spy((msg, sender, sendResponse) => {
deepEqual(messageListener.firstCall.args, waitPromises.push(Promise.resolve().then(() => {
["fake message", {name: "fake sender"}], sendResponse("expected sendResponse value");
"The unwrapped message listener has received the expected parameters"); }));
ok(sendResponseSpy.calledOnce, "The sendResponse function has been called"); // Expect the asynchronous sendResponse call to be used to send a response
equal(sendResponseSpy.firstCall.args[0], "fake reply", // when the listener returns true.
"sendResponse callback has been called with the expected parameters"); return true;
});
wrappedListener("fake message2", {name: "fake sender2"}, sendResponseSpy); const sendResponseSpy = sinon.spy();
// Wait the second response promise to be resolved. window.browser.runtime.onMessage.addListener(listenerReturnsFalse);
return secondResponse; window.browser.runtime.onMessage.addListener(listenerReturnsValue);
}).then(() => { window.browser.runtime.onMessage.addListener(listenerReturnsTrue);
ok(messageListener.calledTwice,
"The unwrapped message listener has been called");
deepEqual(messageListener.secondCall.args,
["fake message2", {name: "fake sender2"}],
"The unwrapped listener has received the expected parameters");
ok(sendResponseSpy.calledTwice, "The sendResponse function has been called"); equal(fakeChrome.runtime.onMessage.addListener.callCount, 3,
equal(sendResponseSpy.secondCall.args[0], "fake reply 2", "runtime.onMessage.addListener should have been called 3 times");
"sendResponse callback has been called with the expected parameters");
}).then(() => {
wrappedListener("fake message3", {name: "fake sender3"}, sendResponseSpy);
// Wait the third response promise to be rejected. let wrappedListenerReturnsFalse = fakeChrome.runtime.onMessage.addListener.firstCall.args[0];
return thirdResponse.catch(err => { let wrappedListenerReturnsValue = fakeChrome.runtime.onMessage.addListener.secondCall.args[0];
equal(messageListener.callCount, 3, let wrappedListenerReturnsTrue = fakeChrome.runtime.onMessage.addListener.thirdCall.args[0];
"The unwrapped message listener has been called");
deepEqual(messageListener.thirdCall.args,
["fake message3", {name: "fake sender3"}],
"The unwrapped listener has received the expected parameters");
equal(sendResponseSpy.callCount, 3, let returnedValue = wrappedListenerReturnsFalse("test message", {name: "fake sender"}, sendResponseSpy);
"The sendResponse function has been called"); equal(returnedValue, false, "the first wrapped listener should return false");
equal(sendResponseSpy.thirdCall.args[0], err,
"sendResponse callback has been called with the expected parameters"); returnedValue = wrappedListenerReturnsValue("test message2", {name: "fake sender"}, sendResponseSpy);
equal(returnedValue, false, "the second wrapped listener should return false");
returnedValue = wrappedListenerReturnsTrue("test message3", {name: "fake sender"}, sendResponseSpy);
equal(returnedValue, true, "the third wrapped listener should return true");
ok(listenerReturnsFalse.calledOnce, "first listener has been called once");
ok(listenerReturnsValue.calledOnce, "second listener has been called once");
ok(listenerReturnsTrue.calledOnce, "third listener has been called once");
// Wait all the collected promises to be resolved and then check the wrapped listener behaviors.
return Promise.all(waitPromises).then(() => {
ok(sendResponseSpy.calledOnce, "sendResponse callback should have been called once");
equal(sendResponseSpy.firstCall.args[0], "expected sendResponse value",
"sendResponse has been called with the expected value");
}); });
}); });
}); });