Bug 1020448 - Add Loop pending call timeout for the link clicker UI. r=Standard8,ui-review=dhenein

This commit is contained in:
Nicolas Perriault 2014-07-16 15:15:08 +01:00
Родитель 8bd6dc13b4
Коммит 54c330ca18
10 изменённых файлов: 226 добавлений и 48 удалений

Просмотреть файл

@ -14,6 +14,7 @@ loop.shared.models = (function() {
*/
var ConversationModel = Backbone.Model.extend({
defaults: {
connected: false, // Session connected flag
ongoing: false, // Ongoing call flag
callerId: undefined, // Loop caller id
loopToken: undefined, // Loop conversation token
@ -38,11 +39,29 @@ loop.shared.models = (function() {
*/
session: undefined,
/**
* Pending call timeout value.
* @type {Number}
*/
pendingCallTimeout: undefined,
/**
* Pending call timer.
* @type {Number}
*/
_pendingCallTimer: undefined,
/**
* Constructor.
*
* Required options:
* - {OT} sdk: SDK object.
* Options:
*
* Required:
* - {OT} sdk: OT SDK object.
*
* Optional:
* - {Number} pendingCallTimeout: Pending call timeout in milliseconds
* (default: 20000).
*
* @param {Object} attributes Attributes object.
* @param {Object} options Options object.
@ -53,6 +72,10 @@ loop.shared.models = (function() {
throw new Error("missing required sdk");
}
this.sdk = options.sdk;
this.pendingCallTimeout = options.pendingCallTimeout || 20000;
// Ensure that any pending call timer is cleared on disconnect/error
this.on("session:ended session:error", this._clearPendingCallTimer, this);
},
/**
@ -79,21 +102,38 @@ loop.shared.models = (function() {
* @param {Object} options Options object
*/
initiate: function(options) {
options = options || {};
// Outgoing call has never reached destination, closing - see bug 1020448
function handleOutgoingCallTimeout() {
/*jshint validthis:true */
if (!this.get("ongoing")) {
this.trigger("timeout").endSession();
}
}
function handleResult(err, sessionData) {
/*jshint validthis:true */
this._clearPendingCallTimer();
if (err) {
this.trigger("session:error", new Error(
"Retrieval of session information failed: HTTP " + err));
return;
}
// XXX For incoming calls we might have more than one call queued.
// For now, we'll just assume the first call is the right information.
// We'll probably really want to be getting this data from the
// background worker on the desktop client.
// Bug 990714 should fix this.
if (!options.outgoing)
if (options.outgoing) {
// Setup pending call timeout.
this._pendingCallTimer = setTimeout(
handleOutgoingCallTimeout.bind(this), this.pendingCallTimeout);
} else {
// XXX For incoming calls we might have more than one call queued.
// For now, we'll just assume the first call is the right information.
// We'll probably really want to be getting this data from the
// background worker on the desktop client.
// Bug 990714 should fix this.
sessionData = sessionData[0];
}
this.setReady(sessionData);
}
@ -156,8 +196,17 @@ loop.shared.models = (function() {
*/
endSession: function() {
this.session.disconnect();
this.once("session:ended", this.stopListening, this);
this.set("ongoing", false);
this.set("ongoing", false)
.once("session:ended", this.stopListening, this);
},
/**
* Clears current pending call timer, if any.
*/
_clearPendingCallTimer: function() {
if (this._pendingCallTimer) {
clearTimeout(this._pendingCallTimer);
}
},
/**
@ -175,7 +224,7 @@ loop.shared.models = (function() {
this.endSession();
} else {
this.trigger("session:connected");
this.set("ongoing", true);
this.set("connected", true);
}
},
@ -186,7 +235,8 @@ loop.shared.models = (function() {
* @param {StreamEvent} event
*/
_streamCreated: function(event) {
this.trigger("session:stream-created", event);
this.set("ongoing", true)
.trigger("session:stream-created", event);
},
/**
@ -196,8 +246,9 @@ loop.shared.models = (function() {
* @param {SessionDisconnectEvent} event
*/
_sessionDisconnected: function(event) {
this.trigger("session:ended");
this.set("ongoing", false);
this.set("connected", false)
.set("ongoing", false)
.trigger("session:ended");
},
/**
@ -207,9 +258,11 @@ loop.shared.models = (function() {
* @param {ConnectionEvent} event
*/
_connectionDestroyed: function(event) {
this.trigger("session:peer-hungup", {
connectionId: event.connection.connectionId
});
this.set("connected", false)
.set("ongoing", false)
.trigger("session:peer-hungup", {
connectionId: event.connection.connectionId
});
this.endSession();
},
@ -220,7 +273,9 @@ loop.shared.models = (function() {
* @param {ConnectionEvent} event
*/
_networkDisconnected: function(event) {
this.trigger("session:network-disconnected");
this.set("connected", false)
.set("ongoing", false)
.trigger("session:network-disconnected");
this.endSession();
},
});

Просмотреть файл

@ -3,6 +3,7 @@
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
LOOP_SERVER_URL := $(shell echo $${LOOP_SERVER_URL-http://localhost:5000})
LOOP_PENDING_CALL_TIMEOUT := $(shell echo $${LOOP_PENDING_CALL_TIMEOUT-20000})
NODE_LOCAL_BIN=./node_modules/.bin
install:
@ -21,4 +22,7 @@ frontend:
@echo "Not implemented yet."
config:
@echo "var loop = loop || {};\nloop.config = {serverUrl: '`echo $(LOOP_SERVER_URL)`'};" > content/config.js
@echo "var loop = loop || {};" > content/config.js
@echo "loop.config = loop.config || {};" >> content/config.js
@echo "loop.config.serverUrl = '`echo $(LOOP_SERVER_URL)`';" >> content/config.js
@echo "loop.config.pendingCallTimeout = `echo $(LOOP_PENDING_CALL_TIMEOUT)`;" >> content/config.js

Просмотреть файл

@ -16,11 +16,15 @@ Configuration
You will need to generate a configuration file, you can do so with:
$ make config
$ make config
It will read the configuration from the `LOOP_SERVER_URL` env variable and
generate the appropriate configuration file. This setting defines the root url
of the loop server, without trailing slash.
It will read the configuration from the following env variables and generate the
appropriate configuration file:
- `LOOP_SERVER_URL` defines the root url of the loop server, without trailing
slash (default: `http://localhost:5000`).
- `LOOP_PENDING_CALL_TIMEOUT` defines the amount of time a pending outgoing call
should be considered timed out, in milliseconds (default: `20000`).
Usage
-----

Просмотреть файл

@ -119,6 +119,8 @@ loop.webapp = (function($, _, OT) {
initialize: function() {
// Load default view
this.loadView(new HomeView());
this.listenTo(this._conversation, "timeout", this._onTimeout);
},
/**
@ -146,6 +148,10 @@ loop.webapp = (function($, _, OT) {
this.navigate(route, {trigger: true});
},
_onTimeout: function() {
this._notifier.errorL10n("call_timeout_notification_text");
},
/**
* Default entry point.
*/
@ -213,8 +219,11 @@ loop.webapp = (function($, _, OT) {
function init() {
var helper = new WebappHelper();
router = new WebappRouter({
conversation: new sharedModels.ConversationModel({}, {sdk: OT}),
notifier: new sharedViews.NotificationListView({el: "#messages"})
notifier: new sharedViews.NotificationListView({el: "#messages"}),
conversation: new sharedModels.ConversationModel({}, {
sdk: OT,
pendingCallTimeout: loop.config.pendingCallTimeout
})
});
Backbone.history.start();
if (helper.isIOS(navigator.platform)) {

Просмотреть файл

@ -1,5 +1,6 @@
[en]
call_has_ended=Your call has ended.
call_timeout_notification_text=Your call did not go through.
missing_conversation_info=Missing conversation information.
network_disconnected=The network connection terminated abruptly.
peer_ended_conversation=Your peer ended the conversation.
@ -21,6 +22,7 @@ connection_error_see_console_notification=Call failed; see console for details.
[fr]
call_has_ended=L'appel est terminé.
call_timeout_notification_text=Votre appel n'a pas abouti.
missing_conversation_info=Informations de communication manquantes.
network_disconnected=La connexion réseau semble avoir été interrompue.
peer_ended_conversation=Votre correspondant a mis fin à la communication.

Просмотреть файл

@ -96,7 +96,10 @@ describe("loop.conversation", function() {
var conversation;
beforeEach(function() {
conversation = new loop.shared.models.ConversationModel({}, {sdk: {}});
conversation = new loop.shared.models.ConversationModel({}, {
sdk: {},
pendingCallTimeout: 1000
});
sandbox.stub(conversation, "initiate");
});
@ -285,7 +288,10 @@ describe("loop.conversation", function() {
var conversation, view;
beforeEach(function() {
conversation = new loop.shared.models.ConversationModel({}, {sdk: {}});
conversation = new loop.shared.models.ConversationModel({}, {
sdk: {},
pendingCallTimeout: 1000
});
view = new loop.conversation.IncomingCallView({model: conversation});
});

Просмотреть файл

@ -14,6 +14,7 @@ describe("loop.shared.models", function() {
beforeEach(function() {
sandbox = sinon.sandbox.create();
sandbox.useFakeTimers();
fakeXHR = sandbox.useFakeXMLHttpRequest();
requests = [];
// https://github.com/cjohansen/Sinon.JS/issues/393
@ -46,9 +47,16 @@ describe("loop.shared.models", function() {
describe("#initialize", function() {
it("should require a sdk option", function() {
expect(function() {
new sharedModels.ConversationModel();
new sharedModels.ConversationModel({}, {});
}).to.Throw(Error, /missing required sdk/);
});
it("should accept a pendingCallTimeout option", function() {
expect(new sharedModels.ConversationModel({}, {
sdk: {},
pendingCallTimeout: 1000
}).pendingCallTimeout).eql(1000);
});
});
describe("constructed", function() {
@ -56,7 +64,10 @@ describe("loop.shared.models", function() {
requestCallInfoStub, requestCallsInfoStub;
beforeEach(function() {
conversation = new sharedModels.ConversationModel({}, {sdk: fakeSDK});
conversation = new sharedModels.ConversationModel({}, {
sdk: fakeSDK,
pendingCallTimeout: 1000
});
conversation.set("loopToken", "fakeToken");
fakeBaseServerUrl = "http://fakeBaseServerUrl";
fakeClient = {
@ -68,6 +79,10 @@ describe("loop.shared.models", function() {
});
describe("#initiate", function() {
beforeEach(function() {
sandbox.stub(conversation, "endSession");
});
it("call requestCallInfo on the client for outgoing calls",
function() {
conversation.initiate({
@ -139,6 +154,35 @@ describe("loop.shared.models", function() {
outgoing: true
});
});
it("should end the session on outgoing call timeout", function() {
requestCallInfoStub.callsArgWith(2, null, fakeSessionData);
conversation.initiate({
client: fakeClient,
outgoing: true
});
sandbox.clock.tick(1001);
sinon.assert.calledOnce(conversation.endSession);
});
it("should trigger a `timeout` event on outgoing call timeout",
function(done) {
requestCallInfoStub.callsArgWith(2, null, fakeSessionData);
conversation.once("timeout", function() {
done();
});
conversation.initiate({
client: fakeClient,
outgoing: true
});
sandbox.clock.tick(1001);
});
});
describe("#setReady", function() {
@ -161,8 +205,11 @@ describe("loop.shared.models", function() {
var model;
beforeEach(function() {
sandbox.stub(sharedModels.ConversationModel.prototype,
"_clearPendingCallTimer");
model = new sharedModels.ConversationModel(fakeSessionData, {
sdk: fakeSDK
sdk: fakeSDK,
pendingCallTimeout: 1000
});
model.startSession();
});
@ -182,16 +229,16 @@ describe("loop.shared.models", function() {
sinon.match.func);
});
it("should set ongoing to true when no error is called back",
it("should set connected to true when no error is called back",
function() {
fakeSession.connect = function(key, token, cb) {
cb(null);
};
sinon.stub(model, "set");
sandbox.stub(model, "set");
model.startSession();
sinon.assert.calledWith(model.set, "ongoing", true);
sinon.assert.calledWith(model.set, "connected", true);
});
it("should trigger session:connected when no error is called back",
@ -215,7 +262,7 @@ describe("loop.shared.models", function() {
error: true
});
};
sinon.stub(model, "endSession");
sandbox.stub(model, "endSession");
model.startSession();
@ -239,6 +286,17 @@ describe("loop.shared.models", function() {
"session:connection-error", sinon.match.object);
});
it("should set the connected attr to true on connection completed",
function() {
fakeSession.connect = function(key, token, cb) {
cb();
};
model.startSession();
expect(model.get("connected")).eql(true);
});
it("should trigger a session:ended event on sessionDisconnected",
function(done) {
model.once("session:ended", function(){ done(); });
@ -246,16 +304,32 @@ describe("loop.shared.models", function() {
fakeSession.trigger("sessionDisconnected", {reason: "ko"});
});
it("should set the ongoing attribute to false on sessionDisconnected",
function(done) {
model.once("session:ended", function() {
expect(model.get("ongoing")).eql(false);
done();
});
it("should set the connected attribute to false on sessionDisconnected",
function() {
fakeSession.trigger("sessionDisconnected", {reason: "ko"});
expect(model.get("connected")).eql(false);
});
it("should set the ongoing attribute to false on sessionDisconnected",
function() {
fakeSession.trigger("sessionDisconnected", {reason: "ko"});
expect(model.get("ongoing")).eql(false);
});
it("should clear a pending timer on session:ended", function() {
model.trigger("session:ended");
sinon.assert.calledOnce(model._clearPendingCallTimer);
});
it("should clear a pending timer on session:error", function() {
model.trigger("session:error");
sinon.assert.calledOnce(model._clearPendingCallTimer);
});
describe("connectionDestroyed event received", function() {
var fakeEvent = {reason: "ko", connection: {connectionId: 42}};
@ -304,7 +378,8 @@ describe("loop.shared.models", function() {
beforeEach(function() {
model = new sharedModels.ConversationModel(fakeSessionData, {
sdk: fakeSDK
sdk: fakeSDK,
pendingCallTimeout: 1000
});
model.startSession();
});
@ -315,6 +390,12 @@ describe("loop.shared.models", function() {
sinon.assert.calledOnce(fakeSession.disconnect);
});
it("should set the connected attribute to false", function() {
model.endSession();
expect(model.get("connected")).eql(false);
});
it("should set the ongoing attribute to false", function() {
model.endSession();

Просмотреть файл

@ -102,7 +102,10 @@ describe("loop.shared.router", function() {
});
conversation = new loop.shared.models.ConversationModel({
loopToken: "fakeToken"
}, {sdk: {}});
}, {
sdk: {},
pendingCallTimeout: 1000
});
});
describe("#constructor", function() {

Просмотреть файл

@ -205,7 +205,8 @@ describe("loop.shared.views", function() {
initSession: sandbox.stub().returns(fakeSession)
};
model = new sharedModels.ConversationModel(fakeSessionData, {
sdk: fakeSDK
sdk: fakeSDK,
pendingCallTimeout: 1000
});
});

Просмотреть файл

@ -23,10 +23,12 @@ describe("loop.webapp", function() {
error: sandbox.spy(),
errorL10n: sandbox.spy(),
};
loop.config.pendingCallTimeout = 1000;
});
afterEach(function() {
sandbox.restore();
delete loop.config.pendingCallTimeout;
});
describe("#init", function() {
@ -69,7 +71,10 @@ describe("loop.webapp", function() {
var router, conversation;
beforeEach(function() {
conversation = new sharedModels.ConversationModel({}, {sdk: {}});
conversation = new sharedModels.ConversationModel({}, {
sdk: {},
pendingCallTimeout: 1000
});
router = new loop.webapp.WebappRouter({
conversation: conversation,
notifier: notifier
@ -253,7 +258,9 @@ describe("loop.webapp", function() {
var conversation;
beforeEach(function() {
conversation = new sharedModels.ConversationModel({}, {sdk: {}});
conversation = new sharedModels.ConversationModel({}, {
sdk: {},
pendingCallTimeout: 1000});
});
describe("#initialize", function() {
@ -268,7 +275,10 @@ describe("loop.webapp", function() {
var conversation, initiate, view, fakeSubmitEvent;
beforeEach(function() {
conversation = new sharedModels.ConversationModel({}, {sdk: {}});
conversation = new sharedModels.ConversationModel({}, {
sdk: {},
pendingCallTimeout: 1000
});
view = new loop.webapp.ConversationFormView({
model: conversation,
notifier: notifier
@ -307,7 +317,10 @@ describe("loop.webapp", function() {
beforeEach(function() {
conversation = new sharedModels.ConversationModel({
loopToken: "fake"
}, {sdk: {}});
}, {
sdk: {},
pendingCallTimeout: 1000
});
view = new loop.webapp.ConversationFormView({
model: conversation,
notifier: notifier