Bug 988229 - Shared notifier component. r=dmose

This commit is contained in:
Nicolas Perriault
2014-05-29 21:20:11 +01:00
parent b54433e3c2
commit 7db75c21d0
6 changed files with 235 additions and 42 deletions

View File

@@ -9,6 +9,8 @@ loop.shared = loop.shared || {};
loop.shared.views = (function(TB) {
"use strict";
var sharedModels = loop.shared.models;
/**
* Base Backbone view.
*/
@@ -127,10 +129,39 @@ loop.shared.views = (function(TB) {
* Notification list view.
*/
var NotificationListView = Backbone.View.extend({
initialize: function() {
/**
* Constructor.
*
* Available options:
* - {loop.shared.models.NotificationCollection} collection Notifications
* collection
*
* @param {Object} options Options object
*/
initialize: function(options) {
options = options || {};
if (!options.collection) {
this.collection = new sharedModels.NotificationCollection();
}
this.listenTo(this.collection, "reset add remove", this.render);
},
/**
* Clears the notification stack.
*/
clear: function() {
this.collection.reset();
},
/**
* Adds a new notification to the stack, triggering rendering of it.
*
* @param {Object|NotificationModel} notification Notification data.
*/
notify: function(notification) {
this.collection.add(notification);
},
render: function() {
this.$el.html(this.collection.map(function(notification) {
return new NotificationView({

View File

@@ -17,6 +17,8 @@
<h1>Loop</h1>
</header>
<div id="messages"></div>
<div id="home">
<p data-l10n-id="welcome">Welcome to the Loop web client.</p>
</div>

View File

@@ -5,7 +5,7 @@
/* global loop:true */
var loop = loop || {};
loop.webapp = (function($, TB) {
loop.webapp = (function($, TB, webl10n) {
"use strict";
/**
@@ -19,7 +19,9 @@ loop.webapp = (function($, TB) {
sharedViews = loop.shared.views,
// XXX this one should be configurable
// see https://bugzilla.mozilla.org/show_bug.cgi?id=987086
baseServerUrl = "http://localhost:5000";
baseServerUrl = "http://localhost:5000",
// aliasing translation function as __ for concision
__ = webl10n.get;
/**
* App router.
@@ -51,11 +53,36 @@ loop.webapp = (function($, TB) {
"submit": "initiate"
},
initialize: function() {
this.listenTo(this.model, "session:error", function(error) {
// XXX: display a proper error notification to end user, probably
// reusing the BB notification system from the Loop desktop client.
alert(error);
/**
* Constructor.
*
* Required options:
* - {loop.shared.model.ConversationModel} model Conversation model.
* - {loop.shared.views.NotificationListView} notifier Notifier component.
*
* @param {Object} options Options object.
*/
initialize: function(options) {
options = options || {};
if (!options.model) {
throw new Error("missing required model");
}
this.model = options.model;
if (!options.notifier) {
throw new Error("missing required notifier");
}
this.notifier = options.notifier;
this.listenTo(this.model, "session:error", this._onSessionError);
},
_onSessionError: function(error) {
console.error(error);
this.notifier.notify({
message: __("unable_retrieve_call_info"),
level: "error"
});
},
@@ -75,8 +102,18 @@ loop.webapp = (function($, TB) {
* @link http://mikeygee.com/blog/backbone.html
*/
var WebappRouter = loop.shared.router.BaseRouter.extend({
/**
* Current conversation.
* @type {loop.shared.models.ConversationModel}
*/
_conversation: undefined,
/**
* Notifications dispatcher.
* @type {loop.shared.views.NotificationListView}
*/
_notifier: undefined,
routes: {
"": "home",
"call/ongoing": "conversation",
@@ -90,6 +127,11 @@ loop.webapp = (function($, TB) {
}
this._conversation = options.conversation;
if (!options.notifier) {
throw new Error("missing required notifier");
}
this._notifier = options.notifier;
this.listenTo(this._conversation, "session:ready", this._onSessionReady);
this.listenTo(this._conversation, "session:ended", this._onSessionEnded);
@@ -126,7 +168,10 @@ loop.webapp = (function($, TB) {
*/
initiate: function(loopToken) {
this._conversation.set("loopToken", loopToken);
this.loadView(new ConversationFormView({model: this._conversation}));
this.loadView(new ConversationFormView({
model: this._conversation,
notifier: this._notifier
}));
},
/**
@@ -139,15 +184,17 @@ loop.webapp = (function($, TB) {
if (loopToken) {
return this.navigate("call/" + loopToken, {trigger: true});
} else {
// XXX: notify user that a call token is missing
this._notifier.notify({
message: __("Missing conversation information"),
level: "error"
});
return this.navigate("home", {trigger: true});
}
}
this.loadView(
new sharedViews.ConversationView({
sdk: TB,
model: this._conversation
}));
this.loadView(new sharedViews.ConversationView({
sdk: TB,
model: this._conversation
}));
}
});
@@ -156,14 +203,18 @@ loop.webapp = (function($, TB) {
*/
function init() {
conversation = new sharedModels.ConversationModel();
router = new WebappRouter({conversation: conversation});
router = new WebappRouter({
conversation: conversation,
notifier: new sharedViews.NotificationListView({el: "#messages"})
});
Backbone.history.start();
}
return {
baseServerUrl: baseServerUrl,
ConversationFormView: ConversationFormView,
HomeView: HomeView,
init: init,
WebappRouter: WebappRouter
};
})(jQuery, window.TB);
})(jQuery, window.TB, document.webL10n);

View File

@@ -1,10 +1,14 @@
[en]
missing_conversation_info=Missing conversation information.
unable_retrieve_call_info=Unable to retrieve conversation information.
start=Start
stop=Stop
start_call=Start the call
welcome=Welcome to the Loop web client.
[fr]
missing_conversation_info=Informations de communication manquantes.
unable_retrieve_call_info=Impossible de récupérer les informations liées à cet appel.
start=Démarrer
stop=Arrêter
start_call=Démarrer l'appel

View File

@@ -111,26 +111,79 @@ describe("loop.shared.views", function() {
});
describe("NotificationListView", function() {
describe("Collection events", function() {
var coll, testNotif, view;
var coll, notifData, testNotif;
beforeEach(function() {
notifData = {level: "error", message: "plop"};
testNotif = new sharedModels.NotificationModel(notifData);
coll = new sharedModels.NotificationCollection();
});
describe("#initialize", function() {
it("should accept a collection option", function() {
var view = new sharedViews.NotificationListView({collection: coll});
expect(view.collection).to.be.an.instanceOf(
sharedModels.NotificationCollection);
});
it("should set a default collection when none is passed", function() {
var view = new sharedViews.NotificationListView();
expect(view.collection).to.be.an.instanceOf(
sharedModels.NotificationCollection);
});
});
describe("#clear", function() {
it("should clear all notifications from the collection", function() {
var view = new sharedViews.NotificationListView();
view.notify(testNotif);
view.clear();
expect(coll).to.have.length.of(0);
});
});
describe("#notify", function() {
var view;
beforeEach(function() {
sandbox.stub(sharedViews.NotificationListView.prototype, "render");
testNotif = new sharedModels.NotificationModel({
level: "error",
message: "plop"
});
coll = new sharedModels.NotificationCollection();
view = new sharedViews.NotificationListView({collection: coll});
});
it("should render on notification added to the collection", function() {
coll.add(testNotif);
describe("adds a new notification to the stack", function() {
it("using a plain object", function() {
view.notify(notifData);
sinon.assert.calledOnce(view.render);
expect(coll).to.have.length.of(1);
});
it("using a NotificationModel instance", function() {
view.notify(testNotif);
expect(coll).to.have.length.of(1);
});
});
});
describe("Collection events", function() {
var view;
beforeEach(function() {
sandbox.stub(sharedViews.NotificationListView.prototype, "render");
view = new sharedViews.NotificationListView({collection: coll});
});
it("should render on notification removed from the collection",
it("should render when a notification is added to the collection",
function() {
coll.add(testNotif);
sinon.assert.calledOnce(view.render);
});
it("should render when a notification is removed from the collection",
function() {
coll.add(testNotif);
coll.remove(testNotif);
@@ -138,12 +191,11 @@ describe("loop.shared.views", function() {
sinon.assert.calledTwice(view.render);
});
it("should render on collection reset",
function() {
coll.reset();
it("should render when the collection is reset", function() {
coll.reset();
sinon.assert.calledOnce(view.render);
});
sinon.assert.calledOnce(view.render);
});
});
});
});

View File

@@ -21,9 +21,10 @@ describe("loop.webapp", function() {
});
describe("WebappRouter", function() {
var conversation, fakeSessionData;
var conversation, notifier, fakeSessionData;
beforeEach(function() {
notifier = {notify: sandbox.spy()};
conversation = new sharedModels.ConversationModel();
fakeSessionData = {
sessionId: "sessionId",
@@ -38,6 +39,12 @@ describe("loop.webapp", function() {
new loop.webapp.WebappRouter();
}).to.Throw(Error, /missing required conversation/);
});
it("should require a notifier", function() {
expect(function() {
new loop.webapp.WebappRouter({conversation: {}});
}).to.Throw(Error, /missing required notifier/);
});
});
describe("constructed", function() {
@@ -57,7 +64,10 @@ describe("loop.webapp", function() {
var router;
beforeEach(function() {
router = new loop.webapp.WebappRouter({conversation: conversation});
router = new loop.webapp.WebappRouter({
conversation: conversation,
notifier: notifier
});
sandbox.stub(router, "loadView");
});
@@ -125,7 +135,8 @@ describe("loop.webapp", function() {
function() {
sandbox.stub(loop.webapp.WebappRouter.prototype, "navigate");
var router = new loop.webapp.WebappRouter({
conversation: conversation
conversation: conversation,
notifier: notifier
});
conversation.setReady(fakeSessionData);
@@ -137,12 +148,30 @@ describe("loop.webapp", function() {
});
describe("ConversationFormView", function() {
describe("#initialize", function() {
it("should require a conversation option", function() {
expect(function() {
new loop.webapp.WebappRouter();
}).to.Throw(Error, /missing required conversation/);
});
it("should require a notifier option", function() {
expect(function() {
new loop.webapp.WebappRouter({conversation: {}});
}).to.Throw(Error, /missing required notifier/);
});
});
describe("#initiate", function() {
var conversation, initiate, view, fakeSubmitEvent;
var notifier, conversation, initiate, view, fakeSubmitEvent;
beforeEach(function() {
notifier = {notify: sandbox.spy()};
conversation = new sharedModels.ConversationModel();
view = new loop.webapp.ConversationFormView({model: conversation});
view = new loop.webapp.ConversationFormView({
model: conversation,
notifier: notifier
});
fakeSubmitEvent = {preventDefault: sinon.spy()};
});
@@ -154,13 +183,37 @@ describe("loop.webapp", function() {
sinon.assert.calledOnce(fakeSubmitEvent.preventDefault);
sinon.assert.calledOnce(initiate);
// XXX host should be configurable
// see https://bugzilla.mozilla.org/show_bug.cgi?id=987086
sinon.assert.calledWith(initiate, {
baseServerUrl: "http://localhost:5000",
baseServerUrl: loop.webapp.baseServerUrl,
outgoing: true
});
});
});
describe("Events", function() {
var notifier, conversation, view;
beforeEach(function() {
notifier = {notify: sandbox.spy()};
conversation = new sharedModels.ConversationModel({
loopToken: "fake"
});
view = new loop.webapp.ConversationFormView({
model: conversation,
notifier: notifier
});
});
it("should trigger a notication when a session:error model event is " +
" received", function() {
conversation.trigger("session:error", "tech error");
sinon.assert.calledOnce(notifier.notify);
// XXX We should test for the actual message content, but webl10n gets
// in the way as translated messages are all empty because matching
// DOM elements are missing.
sinon.assert.calledWithMatch(notifier.notify, {level: "error"});
});
});
});
});