mirror of
https://gitlab.winehq.org/wine/wine-gecko.git
synced 2024-09-13 09:24:08 -07:00
Bug 1048850 - Part 1: add a client part that can be used to request data from the Loop API or subscribe to incoming push messages. r=Standard8
This commit is contained in:
parent
4745d19f23
commit
b8d9a3acb4
197
browser/components/loop/content/shared/js/loopapi-client.js
Normal file
197
browser/components/loop/content/shared/js/loopapi-client.js
Normal file
@ -0,0 +1,197 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
* You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
var loop = loop || {};
|
||||
(function() {
|
||||
"use strict";
|
||||
|
||||
var kMessageName = "Loop:Message";
|
||||
var kPushMessageName = "Loop:Message:Push";
|
||||
var kBatchMessage = "Batch";
|
||||
var kReplyTimeoutMs = 5000;
|
||||
var gListeningForMessages = false;
|
||||
var gListenersMap = {};
|
||||
var gListeningForPushMessages = false;
|
||||
var gSubscriptionsMap = {};
|
||||
var gRootObj = window;
|
||||
|
||||
loop._lastMessageID = 0;
|
||||
|
||||
/**
|
||||
* Builds a proper request payload that adheres to the following conditions:
|
||||
* - The first element (index 0) should be a numeric, unique sequence identifier.
|
||||
* - The second element (index 1) should be the request command name in CapitalCase.
|
||||
* - The rest of the elements (indices 2 and higher) contain the request command
|
||||
* arguments.
|
||||
*
|
||||
* @param {Array} args The raw request data
|
||||
* @return {Array} The request payload in the correct format, as explained above
|
||||
*/
|
||||
function buildRequestArray(args) {
|
||||
var command = args.shift();
|
||||
// Normalize the command name to look like 'SomeCommand'.
|
||||
command = command.charAt(0).toUpperCase() + command.substr(1);
|
||||
|
||||
var seq = ++loop._lastMessageID;
|
||||
args.unshift(seq, command);
|
||||
|
||||
return args;
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a request to chrome, running in the main process.
|
||||
*
|
||||
* Note: it's not necessary for a request to receive a reply; some commands are
|
||||
* as simple as fire-and-forget. Therefore each request will be resolved
|
||||
* after a maximum timeout of `kReplyTimeoutMs` milliseconds.
|
||||
*
|
||||
* @param {String} command Required name of the command to send.
|
||||
* @return {Promise} Gets resolved with the result of the command, IF the chrome
|
||||
* script sent a reply. It never gets rejected.
|
||||
*/
|
||||
loop.request = function request() {
|
||||
var args = Array.slice(arguments);
|
||||
|
||||
return new Promise(function(resolve) {
|
||||
var payload = buildRequestArray(args);
|
||||
var seq = payload[0];
|
||||
|
||||
if (!gListeningForMessages) {
|
||||
gListeningForMessages = function listener(message) {
|
||||
var replySeq = message.data[0];
|
||||
if (!gListenersMap[replySeq]) {
|
||||
return;
|
||||
}
|
||||
gListenersMap[replySeq](message.data[1]);
|
||||
delete gListenersMap[replySeq];
|
||||
};
|
||||
|
||||
gRootObj.addMessageListener(kMessageName, gListeningForMessages);
|
||||
}
|
||||
|
||||
gListenersMap[seq] = resolve;
|
||||
|
||||
gRootObj.sendAsyncMessage(kMessageName, payload);
|
||||
|
||||
gRootObj.setTimeout(function() {
|
||||
// Check if the promise was already resolved before by the message handler.
|
||||
if (!gListenersMap[seq]) {
|
||||
return;
|
||||
}
|
||||
resolve();
|
||||
delete gListenersMap[seq];
|
||||
}, kReplyTimeoutMs);
|
||||
});
|
||||
};
|
||||
|
||||
// These functions should only be used in unit tests.
|
||||
loop.request.getReplyTimeoutMs = function() { return kReplyTimeoutMs; };
|
||||
loop.request.inspect = function() { return _.extend({}, gListenersMap); };
|
||||
loop.request.reset = function() {
|
||||
gListeningForMessages = false;
|
||||
gListenersMap = {};
|
||||
};
|
||||
|
||||
/**
|
||||
* Send multiple requests at once as a batch.
|
||||
*
|
||||
* @param {...Array} command An unlimited amount of Arrays may be passed as
|
||||
* individual arguments of this function. They are
|
||||
* bundled together and sent across.
|
||||
* @return {Promise} Gets resolved when all commands have finished with their
|
||||
* accumulated results.
|
||||
*/
|
||||
loop.requestMulti = function requestMulti() {
|
||||
if (!arguments.length) {
|
||||
throw new Error("loop.requestMulti: please pass in a list of calls to process in parallel.");
|
||||
}
|
||||
|
||||
var calls = Array.slice(arguments);
|
||||
calls.forEach(function(call) {
|
||||
if (!Array.isArray(call)) {
|
||||
throw new Error("loop.requestMulti: each call must be an array of options, " +
|
||||
"exactly the same as the argument signature of `loop.request()`");
|
||||
}
|
||||
|
||||
buildRequestArray(call);
|
||||
});
|
||||
|
||||
return new Promise(function(resolve) {
|
||||
loop.request(kBatchMessage, calls).then(function(resultSet) {
|
||||
if (!resultSet) {
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
|
||||
// Collect result as a sequenced array and pass it back.
|
||||
var result = Object.getOwnPropertyNames(resultSet).map(function(seq) {
|
||||
return resultSet[seq];
|
||||
});
|
||||
|
||||
resolve(result);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Subscribe to push messages coming from chrome scripts. Since these may arrive
|
||||
* at any time, the subscriptions are permanently registered.
|
||||
*
|
||||
* @param {String} name Name of the push message
|
||||
* @param {Function} callback Function invoked when the push message arrives
|
||||
*/
|
||||
loop.subscribe = function subscribe(name, callback) {
|
||||
if (!gListeningForPushMessages) {
|
||||
gRootObj.addMessageListener(kPushMessageName, gListeningForPushMessages = function(message) {
|
||||
var eventName = message.data[0];
|
||||
if (!gSubscriptionsMap[eventName]) {
|
||||
return;
|
||||
}
|
||||
gSubscriptionsMap[eventName].forEach(function(cb) {
|
||||
cb.apply(null, message.data[1]);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
if (!gSubscriptionsMap[name]) {
|
||||
gSubscriptionsMap[name] = [];
|
||||
}
|
||||
gSubscriptionsMap[name].push(callback);
|
||||
};
|
||||
|
||||
// These functions should only be used in unit tests.
|
||||
loop.subscribe.inspect = function() { return _.extend({}, gSubscriptionsMap); };
|
||||
loop.subscribe.reset = function() {
|
||||
gListeningForPushMessages = false;
|
||||
gSubscriptionsMap = {};
|
||||
};
|
||||
|
||||
/**
|
||||
* Cancel a subscription to a specific push message.
|
||||
*
|
||||
* @param {String} name Name of the push message
|
||||
* @param {Function} callback Handler function
|
||||
*/
|
||||
loop.unsubscribe = function unsubscribe(name, callback) {
|
||||
if (!gSubscriptionsMap[name]) {
|
||||
return;
|
||||
}
|
||||
var idx = gSubscriptionsMap[name].indexOf(callback);
|
||||
if (idx === -1) {
|
||||
return;
|
||||
}
|
||||
gSubscriptionsMap[name].splice(idx, 1);
|
||||
};
|
||||
|
||||
/**
|
||||
* Cancel all running subscriptions.
|
||||
*/
|
||||
loop.unsubscribeAll = function unsubscribeAll() {
|
||||
gSubscriptionsMap = {};
|
||||
if (gListeningForPushMessages) {
|
||||
gRootObj.removeMessageListener(kPushMessageName, gListeningForPushMessages);
|
||||
gListeningForPushMessages = false;
|
||||
}
|
||||
};
|
||||
})();
|
@ -95,11 +95,12 @@ browser.jar:
|
||||
content/browser/loop/shared/js/store.js (content/shared/js/store.js)
|
||||
content/browser/loop/shared/js/activeRoomStore.js (content/shared/js/activeRoomStore.js)
|
||||
content/browser/loop/shared/js/dispatcher.js (content/shared/js/dispatcher.js)
|
||||
content/browser/loop/shared/js/linkifiedTextView.js (content/shared/js/linkifiedTextView.js)
|
||||
content/browser/loop/shared/js/loopapi-client.js (content/shared/js/loopapi-client.js)
|
||||
content/browser/loop/shared/js/models.js (content/shared/js/models.js)
|
||||
content/browser/loop/shared/js/mixins.js (content/shared/js/mixins.js)
|
||||
content/browser/loop/shared/js/otSdkDriver.js (content/shared/js/otSdkDriver.js)
|
||||
content/browser/loop/shared/js/views.js (content/shared/js/views.js)
|
||||
content/browser/loop/shared/js/linkifiedTextView.js (content/shared/js/linkifiedTextView.js)
|
||||
content/browser/loop/shared/js/textChatStore.js (content/shared/js/textChatStore.js)
|
||||
content/browser/loop/shared/js/textChatView.js (content/shared/js/textChatView.js)
|
||||
content/browser/loop/shared/js/urlRegExps.js (content/shared/js/urlRegExps.js)
|
||||
|
@ -18,6 +18,7 @@ module.exports = function(config) {
|
||||
"content/shared/libs/sdk.js",
|
||||
"test/shared/vendor/*.js",
|
||||
"test/karma/head.js", // Add test fixture container
|
||||
"content/shared/js/loopapi-client.js",
|
||||
"content/shared/js/utils.js",
|
||||
"content/shared/js/store.js",
|
||||
"content/shared/js/models.js",
|
||||
|
@ -47,6 +47,7 @@
|
||||
</script>
|
||||
|
||||
<!-- App scripts -->
|
||||
<script src="../../content/shared/js/loopapi-client.js"></script>
|
||||
<script src="../../content/shared/js/utils.js"></script>
|
||||
<script src="../../content/shared/js/models.js"></script>
|
||||
<script src="../../content/shared/js/mixins.js"></script>
|
||||
@ -78,6 +79,7 @@
|
||||
<script src="textChatStore_test.js"></script>
|
||||
<script src="textChatView_test.js"></script>
|
||||
<script src="linkifiedTextView_test.js"></script>
|
||||
<script src="loopapi-client_test.js"></script>
|
||||
|
||||
<script>
|
||||
describe("Uncaught Error Check", function() {
|
||||
|
277
browser/components/loop/test/shared/loopapi-client_test.js
Normal file
277
browser/components/loop/test/shared/loopapi-client_test.js
Normal file
@ -0,0 +1,277 @@
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
* http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
describe("loopapi-client", function() {
|
||||
"use strict";
|
||||
|
||||
var expect = chai.expect;
|
||||
var sandbox, clock, replyTimeoutMs;
|
||||
var sharedMixins = loop.shared.mixins;
|
||||
var TestUtils = React.addons.TestUtils;
|
||||
|
||||
beforeEach(function() {
|
||||
sandbox = sinon.sandbox.create();
|
||||
window.addMessageListener = sinon.stub();
|
||||
window.removeMessageListener = sinon.stub();
|
||||
window.sendAsyncMessage = sinon.stub();
|
||||
clock = sandbox.useFakeTimers();
|
||||
replyTimeoutMs = loop.request.getReplyTimeoutMs();
|
||||
});
|
||||
|
||||
afterEach(function() {
|
||||
loop.request.reset();
|
||||
loop.subscribe.reset();
|
||||
sandbox.restore();
|
||||
});
|
||||
|
||||
describe("loop.request", function() {
|
||||
it("should send a message", function() {
|
||||
var promise = loop.request("GetLoopPref", "enabled");
|
||||
|
||||
expect(promise).to.be.an.instanceof(Promise);
|
||||
sinon.assert.calledOnce(window.sendAsyncMessage);
|
||||
sinon.assert.calledWithExactly(window.sendAsyncMessage, "Loop:Message",
|
||||
[loop._lastMessageID, "GetLoopPref", "enabled"]);
|
||||
sinon.assert.calledOnce(window.addMessageListener);
|
||||
|
||||
clock.tick(replyTimeoutMs);
|
||||
return promise;
|
||||
});
|
||||
|
||||
it("should correct the command name", function() {
|
||||
var promise = loop.request("getLoopPref", "enabled");
|
||||
|
||||
sinon.assert.calledWithExactly(window.sendAsyncMessage, "Loop:Message",
|
||||
[loop._lastMessageID, "GetLoopPref", "enabled"]);
|
||||
|
||||
clock.tick(replyTimeoutMs);
|
||||
return promise;
|
||||
});
|
||||
|
||||
it("should pass all arguments in-order", function() {
|
||||
var promise = loop.request("SetLoopPref", "enabled", false, 1, 2, 3);
|
||||
|
||||
sinon.assert.calledWithExactly(window.sendAsyncMessage, "Loop:Message",
|
||||
[loop._lastMessageID, "SetLoopPref", "enabled", false, 1, 2, 3]);
|
||||
|
||||
clock.tick(replyTimeoutMs);
|
||||
return promise;
|
||||
});
|
||||
|
||||
it("should resolve the promise when a response is received", function() {
|
||||
var listener;
|
||||
window.addMessageListener = function(name, callback) {
|
||||
listener = callback;
|
||||
};
|
||||
|
||||
var promise = loop.request("GetLoopPref", "enabled").then(function(result) {
|
||||
expect(result).to.eql("result");
|
||||
});
|
||||
|
||||
listener({
|
||||
data: [loop._lastMessageID, "result"]
|
||||
});
|
||||
|
||||
return promise;
|
||||
});
|
||||
|
||||
it("should cancel the message listener when no reply is received in time", function() {
|
||||
var promise = loop.request("GetLoopPref", "enabled");
|
||||
|
||||
promise.then(function(result) {
|
||||
expect(result).to.eql(undefined);
|
||||
});
|
||||
|
||||
clock.tick(replyTimeoutMs);
|
||||
return promise;
|
||||
});
|
||||
|
||||
it("should not start listening for messages more than once", function() {
|
||||
return new Promise(function(resolve) {
|
||||
loop.request("GetLoopPref", "enabled").then(function() {
|
||||
sinon.assert.calledOnce(window.addMessageListener);
|
||||
|
||||
loop.request("GetLoopPref", "enabled").then(function() {
|
||||
sinon.assert.calledOnce(window.addMessageListener);
|
||||
|
||||
resolve();
|
||||
});
|
||||
|
||||
clock.tick(replyTimeoutMs);
|
||||
});
|
||||
|
||||
clock.tick(replyTimeoutMs);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("loop.requestMulti", function() {
|
||||
it("should send a batch of messages", function() {
|
||||
var promise = loop.requestMulti(
|
||||
["GetLoopPref", "enabled"],
|
||||
["GetLoopPref", "e10s.enabled"]
|
||||
);
|
||||
|
||||
expect(promise).to.be.an.instanceof(Promise);
|
||||
sinon.assert.calledOnce(window.sendAsyncMessage);
|
||||
sinon.assert.calledWithExactly(window.sendAsyncMessage, "Loop:Message",
|
||||
[loop._lastMessageID, "Batch", [
|
||||
[loop._lastMessageID - 2, "GetLoopPref", "enabled"],
|
||||
[loop._lastMessageID - 1, "GetLoopPref", "e10s.enabled"]]
|
||||
]);
|
||||
|
||||
clock.tick(replyTimeoutMs);
|
||||
return promise;
|
||||
});
|
||||
|
||||
it("should correct command names", function() {
|
||||
var promise = loop.requestMulti(
|
||||
["GetLoopPref", "enabled"],
|
||||
// Use lowercase 'g' on purpose, it should get corrected:
|
||||
["getLoopPref", "e10s.enabled"]
|
||||
);
|
||||
|
||||
sinon.assert.calledWithExactly(window.sendAsyncMessage, "Loop:Message",
|
||||
[loop._lastMessageID, "Batch", [
|
||||
[loop._lastMessageID - 2, "GetLoopPref", "enabled"],
|
||||
[loop._lastMessageID - 1, "GetLoopPref", "e10s.enabled"]]
|
||||
]);
|
||||
|
||||
clock.tick(replyTimeoutMs);
|
||||
return promise;
|
||||
});
|
||||
|
||||
it("should resolve the promise when a response is received", function() {
|
||||
var listener;
|
||||
window.addMessageListener = function(name, callback) {
|
||||
listener = callback;
|
||||
};
|
||||
|
||||
var promise = loop.requestMulti(
|
||||
["GetLoopPref", "enabled"],
|
||||
["GetLoopPref", "e10s.enabled"]
|
||||
).then(function(result) {
|
||||
expect(result).to.eql(["result1", "result2"]);
|
||||
});
|
||||
|
||||
listener({
|
||||
data: [
|
||||
loop._lastMessageID, {
|
||||
"1": "result1",
|
||||
"2": "result2"
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
return promise;
|
||||
});
|
||||
|
||||
it("should throw an error when no requests are passed in", function() {
|
||||
expect(loop.requestMulti).to.throw(Error, /please pass in a list of calls/);
|
||||
});
|
||||
|
||||
it("should throw when invalid request is passed in", function() {
|
||||
expect(loop.requestMulti.bind(null, ["command"], null)).to
|
||||
.throw(Error, /each call must be an array of options/);
|
||||
|
||||
expect(loop.requestMulti.bind(null, null, ["command"])).to
|
||||
.throw(Error, /each call must be an array of options/);
|
||||
});
|
||||
});
|
||||
|
||||
describe("loop.subscribe", function() {
|
||||
var sendMessage = null;
|
||||
var callCount = 0;
|
||||
|
||||
beforeEach(function() {
|
||||
callCount = 0;
|
||||
window.addMessageListener = function(name, callback) {
|
||||
sendMessage = callback;
|
||||
++callCount;
|
||||
};
|
||||
});
|
||||
|
||||
afterEach(function() {
|
||||
sendMessage = null;
|
||||
});
|
||||
|
||||
it("subscribe to a push message", function() {
|
||||
loop.subscribe("LoopStatusChanged", function() {});
|
||||
var subscriptions = loop.subscribe.inspect();
|
||||
|
||||
expect(callCount).to.eql(1);
|
||||
expect(Object.getOwnPropertyNames(subscriptions).length).to.eql(1);
|
||||
expect(subscriptions.LoopStatusChanged.length).to.eql(1);
|
||||
});
|
||||
|
||||
it("should start listening for push messages when a subscriber registers", function() {
|
||||
loop.subscribe("LoopStatusChanged", function() {});
|
||||
expect(callCount).to.eql(1);
|
||||
|
||||
loop.subscribe("LoopStatusChanged", function() {});
|
||||
expect(callCount).to.eql(1);
|
||||
|
||||
loop.subscribe("Test", function() {});
|
||||
expect(callCount).to.eql(1);
|
||||
});
|
||||
|
||||
it("incoming push messages should invoke subscriptions", function() {
|
||||
var stub1 = sinon.stub();
|
||||
var stub2 = sinon.stub();
|
||||
var stub3 = sinon.stub();
|
||||
|
||||
loop.subscribe("LoopStatusChanged", stub1);
|
||||
loop.subscribe("LoopStatusChanged", stub2);
|
||||
loop.subscribe("LoopStatusChanged", stub3);
|
||||
|
||||
sendMessage({ data: ["Foo", ["bar"]] });
|
||||
|
||||
sinon.assert.notCalled(stub1);
|
||||
sinon.assert.notCalled(stub2);
|
||||
sinon.assert.notCalled(stub3);
|
||||
|
||||
sendMessage({ data: ["LoopStatusChanged", ["Foo", "Bar"]] });
|
||||
|
||||
sinon.assert.calledOnce(stub1);
|
||||
sinon.assert.calledWithExactly(stub1, "Foo", "Bar");
|
||||
sinon.assert.calledOnce(stub2);
|
||||
sinon.assert.calledWithExactly(stub2, "Foo", "Bar");
|
||||
sinon.assert.calledOnce(stub3);
|
||||
sinon.assert.calledWithExactly(stub3, "Foo", "Bar");
|
||||
});
|
||||
});
|
||||
|
||||
describe("unsubscribe", function() {
|
||||
it("should remove subscriptions from the map", function() {
|
||||
var handler = function() {};
|
||||
loop.subscribe("LoopStatusChanged", handler);
|
||||
|
||||
loop.unsubscribe("LoopStatusChanged", handler);
|
||||
expect(loop.subscribe.inspect().LoopStatusChanged.length).to.eql(0);
|
||||
});
|
||||
|
||||
it("should not remove a subscription when a different handler is passed in", function() {
|
||||
var handler = function() {};
|
||||
loop.subscribe("LoopStatusChanged", handler);
|
||||
|
||||
loop.unsubscribe("LoopStatusChanged", function() {});
|
||||
expect(loop.subscribe.inspect().LoopStatusChanged.length).to.eql(1);
|
||||
});
|
||||
|
||||
it("should not throw when unsubscribing from an unknown subscription", function() {
|
||||
loop.unsubscribe("foobar");
|
||||
});
|
||||
});
|
||||
|
||||
describe("unsubscribeAll", function() {
|
||||
it("should clear all present subscriptions", function() {
|
||||
loop.subscribe("LoopStatusChanged", function() {});
|
||||
|
||||
expect(Object.getOwnPropertyNames(loop.subscribe.inspect()).length).to.eql(1);
|
||||
|
||||
loop.unsubscribeAll();
|
||||
|
||||
expect(Object.getOwnPropertyNames(loop.subscribe.inspect()).length).to.eql(0);
|
||||
});
|
||||
});
|
||||
});
|
Loading…
Reference in New Issue
Block a user