268 lines
11 KiB
JavaScript
268 lines
11 KiB
JavaScript
"use strict";
|
|
|
|
const {deepEqual, equal, ok} = require("chai").assert;
|
|
const sinon = require("sinon");
|
|
|
|
const {setupTestDOMWindow} = require("./setup");
|
|
|
|
describe("browser-polyfill", () => {
|
|
describe("wrapped runtime.onMessage listener", () => {
|
|
it("does not wrap the listener if it is not a function", () => {
|
|
const fakeChrome = {
|
|
runtime: {
|
|
lastError: null,
|
|
onMessage: {
|
|
addListener: sinon.spy(),
|
|
hasListener: sinon.stub(),
|
|
removeListener: sinon.spy(),
|
|
},
|
|
},
|
|
};
|
|
|
|
return setupTestDOMWindow(fakeChrome).then(window => {
|
|
const fakeNonFunctionListener = {fake: "non function listener"};
|
|
|
|
window.browser.runtime.onMessage.addListener(fakeNonFunctionListener);
|
|
|
|
deepEqual(fakeChrome.runtime.onMessage.addListener.firstCall.args[0],
|
|
fakeNonFunctionListener,
|
|
"The non-function listener has not been wrapped");
|
|
});
|
|
});
|
|
|
|
it("keeps track of the listeners added", () => {
|
|
const messageListener = sinon.spy();
|
|
const fakeChromeListeners = new Set();
|
|
const fakeChrome = {
|
|
runtime: {
|
|
lastError: null,
|
|
onMessage: {
|
|
addListener: sinon.spy((listener, ...args) => {
|
|
fakeChromeListeners.add(listener);
|
|
}),
|
|
hasListener: sinon.spy(listener => fakeChromeListeners.has(listener)),
|
|
removeListener: sinon.spy(listener => {
|
|
fakeChromeListeners.delete(listener);
|
|
}),
|
|
},
|
|
},
|
|
};
|
|
|
|
return setupTestDOMWindow(fakeChrome).then(window => {
|
|
equal(window.browser.runtime.onMessage.hasListener(messageListener),
|
|
false, "Got hasListener==false before the listener has been added");
|
|
|
|
window.browser.runtime.onMessage.addListener(messageListener);
|
|
|
|
equal(window.browser.runtime.onMessage.hasListener(messageListener),
|
|
true, "Got hasListener==true once the listener has been added");
|
|
|
|
// Add the same listener again to test that it will be called with the
|
|
// same wrapped listener.
|
|
window.browser.runtime.onMessage.addListener(messageListener);
|
|
|
|
ok(fakeChrome.runtime.onMessage.addListener.calledTwice,
|
|
"addListener has been called twice");
|
|
equal(fakeChrome.runtime.onMessage.addListener.secondCall.args[0],
|
|
fakeChrome.runtime.onMessage.addListener.firstCall.args[0],
|
|
"both the addListener calls received the same wrapped listener");
|
|
|
|
// Retrieve the wrapped listener and execute it to fake a received message.
|
|
const wrappedListener = fakeChrome.runtime.onMessage.addListener.firstCall.args[0];
|
|
wrappedListener("msg", {name: "sender"}, function sendResponse() {});
|
|
ok(messageListener.calledOnce, "The listener has been called once");
|
|
|
|
// Remove the listener.
|
|
window.browser.runtime.onMessage.removeListener(messageListener);
|
|
ok(fakeChrome.runtime.onMessage.removeListener.calledOnce,
|
|
"removeListener has been called once");
|
|
equal(fakeChrome.runtime.onMessage.addListener.secondCall.args[0],
|
|
fakeChrome.runtime.onMessage.removeListener.firstCall.args[0],
|
|
"both the addListener and removeListenercalls received the same wrapped listener");
|
|
equal(fakeChrome.runtime.onMessage.hasListener(messageListener), false,
|
|
"Got hasListener==false once the listener has been removed");
|
|
});
|
|
});
|
|
|
|
it("generates different wrappers for different listeners", () => {
|
|
const fakeChromeListeners = [];
|
|
const fakeChrome = {
|
|
runtime: {
|
|
lastError: null,
|
|
onMessage: {
|
|
addListener: sinon.spy((listener, ...args) => {
|
|
fakeChromeListeners.push(listener);
|
|
}),
|
|
hasListener: sinon.spy(),
|
|
removeListener: sinon.spy(),
|
|
},
|
|
},
|
|
};
|
|
|
|
return setupTestDOMWindow(fakeChrome).then(window => {
|
|
const firstMessageListener = sinon.spy();
|
|
const secondMessageListener = sinon.spy();
|
|
|
|
window.browser.runtime.onMessage.addListener(firstMessageListener);
|
|
window.browser.runtime.onMessage.addListener(secondMessageListener);
|
|
|
|
equal(fakeChromeListeners.length, 2, "Got two wrapped listeners");
|
|
|
|
fakeChromeListeners[0]("call first wrapper");
|
|
ok(firstMessageListener.calledOnce);
|
|
equal(firstMessageListener.firstCall.args[0], "call first wrapper");
|
|
|
|
fakeChromeListeners[1]("call second wrapper");
|
|
ok(secondMessageListener.calledOnce);
|
|
equal(secondMessageListener.firstCall.args[0], "call second wrapper");
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("sendResponse callback", () => {
|
|
it("ignores the sendResponse calls when the listener returns a promise", () => {
|
|
const fakeChrome = {
|
|
runtime: {
|
|
lastError: null,
|
|
onMessage: {
|
|
addListener: sinon.spy(),
|
|
},
|
|
},
|
|
};
|
|
|
|
return setupTestDOMWindow(fakeChrome).then(window => {
|
|
const listener = sinon.spy((msg, sender, sendResponse) => {
|
|
sendResponse("Ignored sendReponse callback on returned Promise");
|
|
|
|
return Promise.resolve("listener resolved value");
|
|
});
|
|
|
|
const sendResponseSpy = sinon.spy();
|
|
|
|
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 => {
|
|
const listenerReturnsFalse = sinon.spy((msg, sender, sendResponse) => {
|
|
waitPromises.push(Promise.resolve().then(() => {
|
|
sendResponse("Ignored sendReponse callback on returned false");
|
|
}));
|
|
|
|
return false;
|
|
});
|
|
|
|
const listenerReturnsValue = sinon.spy((msg, sender, sendResponse) => {
|
|
waitPromises.push(Promise.resolve().then(() => {
|
|
sendResponse("Ignored sendReponse callback on non boolean/thenable return values");
|
|
}));
|
|
|
|
// 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";
|
|
});
|
|
|
|
const listenerReturnsTrue = sinon.spy((msg, sender, sendResponse) => {
|
|
waitPromises.push(Promise.resolve().then(() => {
|
|
sendResponse("expected sendResponse value");
|
|
}));
|
|
|
|
// Expect the asynchronous sendResponse call to be used to send a response
|
|
// when the listener returns true.
|
|
return true;
|
|
});
|
|
|
|
const sendResponseSpy = sinon.spy();
|
|
|
|
window.browser.runtime.onMessage.addListener(listenerReturnsFalse);
|
|
window.browser.runtime.onMessage.addListener(listenerReturnsValue);
|
|
window.browser.runtime.onMessage.addListener(listenerReturnsTrue);
|
|
|
|
equal(fakeChrome.runtime.onMessage.addListener.callCount, 3,
|
|
"runtime.onMessage.addListener should have been called 3 times");
|
|
|
|
let wrappedListenerReturnsFalse = fakeChrome.runtime.onMessage.addListener.firstCall.args[0];
|
|
let wrappedListenerReturnsValue = fakeChrome.runtime.onMessage.addListener.secondCall.args[0];
|
|
let wrappedListenerReturnsTrue = fakeChrome.runtime.onMessage.addListener.thirdCall.args[0];
|
|
|
|
let returnedValue = wrappedListenerReturnsFalse("test message", {name: "fake sender"}, sendResponseSpy);
|
|
equal(returnedValue, false, "the first wrapped listener should return false");
|
|
|
|
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");
|
|
});
|
|
});
|
|
});
|
|
|
|
it("resolves to undefined when no listeners reply", () => {
|
|
const fakeChrome = {
|
|
runtime: {
|
|
// This error message is defined as CHROME_SEND_MESSAGE_CALLBACK_NO_RESPONSE_MESSAGE
|
|
// in the polyfill sources and it is used to recognize when Chrome has detected that
|
|
// none of the listeners replied.
|
|
lastError: {
|
|
message: "The message port closed before a response was received.",
|
|
},
|
|
sendMessage: sinon.stub(),
|
|
},
|
|
};
|
|
|
|
fakeChrome.runtime.sendMessage.onFirstCall().callsArgWith(1, [undefined]);
|
|
|
|
return setupTestDOMWindow(fakeChrome).then(window => {
|
|
const promise = window.browser.runtime.sendMessage("some_message");
|
|
ok(fakeChrome.runtime.sendMessage.calledOnce, "sendMessage has been called once");
|
|
|
|
return promise.then(reply => {
|
|
deepEqual(reply, undefined, "sendMessage promise should be resolved to undefined");
|
|
});
|
|
});
|
|
});
|
|
});
|
|
});
|