Bug 985596 - Refactored shared assets & tests. r=dmose

--HG--
rename : static/shared/README.md => browser/components/loop/content/shared/README.md
rename : static/shared/css/common.css => browser/components/loop/content/shared/css/common.css
rename : static/shared/css/conversation.css => browser/components/loop/content/shared/css/conversation.css
rename : static/shared/css/panel.css => browser/components/loop/content/shared/css/panel.css
rename : static/shared/css/readme.html => browser/components/loop/content/shared/css/readme.html
rename : static/shared/img/icon_32.png => browser/components/loop/content/shared/img/icon_32.png
rename : static/shared/img/icon_64.png => browser/components/loop/content/shared/img/icon_64.png
rename : static/shared/js/models.js => browser/components/loop/content/shared/js/models.js
rename : static/shared/js/views.js => browser/components/loop/content/shared/js/views.js
rename : static/shared/libs/backbone-1.1.2.js => browser/components/loop/content/shared/libs/backbone-1.1.2.js
rename : static/shared/libs/jquery-2.1.0.js => browser/components/loop/content/shared/libs/jquery-2.1.0.js
rename : static/shared/libs/lodash-2.4.1.js => browser/components/loop/content/shared/libs/lodash-2.4.1.js
rename : static/shared/libs/sdk.js => browser/components/loop/content/shared/libs/sdk.js
rename : static/shared/libs/webl10n-20130617.js => browser/components/loop/content/shared/libs/webl10n-20130617.js
rename : static/css/webapp.css => browser/components/loop/standalone/content/css/webapp.css
rename : static/index.html => browser/components/loop/standalone/content/index.html
rename : static/js/webapp.js => browser/components/loop/standalone/content/js/webapp.js
rename : static/l10n/data.ini => browser/components/loop/standalone/content/l10n/data.ini
rename : test/webapp_test.js => browser/components/loop/test/standalone/webapp_test.js
extra : transplant_source : %DA%D9%3A%E9%C6%E0d%13%84%C1%BEps%C8b%F09o%D7m
This commit is contained in:
Nicolas Perriault 2014-05-29 21:20:11 +01:00
parent 49c12d9a48
commit 4ac3aa169a
29 changed files with 395 additions and 104 deletions

View File

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 2.3 KiB

View File

Before

Width:  |  Height:  |  Size: 4.0 KiB

After

Width:  |  Height:  |  Size: 4.0 KiB

View File

@ -14,6 +14,7 @@ loop.shared.models = (function() {
*/
var ConversationModel = Backbone.Model.extend({
defaults: {
callerId: undefined, // Loop caller id
loopToken: undefined, // Loop conversation token
sessionId: undefined, // TB session id
sessionToken: undefined, // TB session token
@ -31,12 +32,11 @@ loop.shared.models = (function() {
* retrieved from the server;
* - `session:error` when the request failed.
*
* @param {Object} baseServerUrl The server URL
* @throws {Error} If no baseServerUrl is given
* @throws {Error} If no conversation token is set
* @param {String} baseServerUrl The server URL
* @throws {Error} If no baseServerUrl is given
* @throws {Error} If no conversation token is set
*/
initiate: function(baseServerUrl) {
if (!baseServerUrl) {
throw new Error("baseServerUrl arg must be passed to initiate()");
}
@ -95,7 +95,26 @@ loop.shared.models = (function() {
}
});
/**
* Notification model.
*/
var NotificationModel = Backbone.Model.extend({
defaults: {
level: "info",
message: ""
}
});
/**
* Notification collection
*/
var NotificationCollection = Backbone.Collection.extend({
model: NotificationModel
});
return {
ConversationModel: ConversationModel
ConversationModel: ConversationModel,
NotificationCollection: NotificationCollection,
NotificationModel: NotificationModel
};
})();

View File

@ -0,0 +1,37 @@
/* 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/. */
/* global loop:true */
var loop = loop || {};
loop.shared = loop.shared || {};
loop.shared.router = (function() {
"use strict";
/**
* Base Router. Allows defining a main active view and ease toggling it when
* the active route changes.
*
* @link http://mikeygee.com/blog/backbone.html
*/
var BaseRouter = Backbone.Router.extend({
activeView: undefined,
/**
* Loads and render current active view.
*
* @param {loop.shared.views.BaseView} view View.
*/
loadView : function(view) {
if (this.activeView) {
this.activeView.hide();
}
this.activeView = view.render().show();
}
});
return {
BaseRouter: BaseRouter
};
})();

View File

@ -4,8 +4,6 @@
/* global loop:true */
// XXX This file needs unit tests.
var loop = loop || {};
loop.shared = loop.shared || {};
loop.shared.views = (function(TB) {
@ -42,15 +40,23 @@ loop.shared.views = (function(TB) {
var ConversationView = BaseView.extend({
el: "#conversation",
initialize: function() {
this.videoStyles = { width: "100%", height: "100%" };
videoStyles: { width: "100%", height: "100%" },
// XXX this feels like to be moved to the ConversationModel, but as it's
/**
* Establishes webrtc communication using TB sdk.
*/
initialize: function(options) {
options = options || {};
if (!options.sdk) {
throw new Error("missing required sdk");
}
this.sdk = options.sdk;
// XXX: this feels like to be moved to the ConversationModel, but as it's
// tighly coupled with the DOM (element ids to receive streams), we'd need
// an abstraction we probably don't want yet.
this.session = TB.initSession(this.model.get("sessionId"));
this.publisher = TB.initPublisher(this.model.get("apiKey"), "outgoing",
this.videoStyles);
this.session = this.sdk.initSession(this.model.get("sessionId"));
this.publisher = this.sdk.initPublisher(this.model.get("apiKey"),
"outgoing", this.videoStyles);
this.session.connect(this.model.get("apiKey"),
this.model.get("sessionToken"));
@ -85,8 +91,59 @@ loop.shared.views = (function(TB) {
}
});
/**
* Notification view.
*/
var NotificationView = Backbone.View.extend({
template: _.template([
'<div class="alert alert-<%- level %>">',
' <button class="close"></button>',
' <p class="message"><%- message %></p>',
'</div>'
].join("")),
events: {
"click .close": "dismiss"
},
dismiss: function(event) {
event.preventDefault();
this.$el.addClass("fade-out");
setTimeout(function() {
this.collection.remove(this.model);
this.remove();
}.bind(this), 500); // XXX make timeout value configurable
},
render: function() {
this.$el.html(this.template(this.model.toJSON()));
return this;
}
});
/**
* Notification list view.
*/
var NotificationListView = Backbone.View.extend({
initialize: function() {
this.listenTo(this.collection, "reset add remove", this.render);
},
render: function() {
this.$el.html(this.collection.map(function(notification) {
return new NotificationView({
model: notification,
collection: this.collection
}).render().$el;
}.bind(this)));
return this;
}
});
return {
BaseView: BaseView,
ConversationView: ConversationView
ConversationView: ConversationView,
NotificationListView: NotificationListView,
NotificationView: NotificationView
};
})(window.TB);

View File

@ -1,4 +1,4 @@
node_modules
static/shared/libs
content/shared/libs
test/shared/vendor

View File

@ -11,7 +11,7 @@ test:
@echo "Not implemented yet."
lint:
@$(NODE_LOCAL_BIN)/jshint *.js static test
@$(NODE_LOCAL_BIN)/jshint *.js content test
runserver:
@node server.js

View File

@ -20,11 +20,11 @@ For development, run a local static file server:
Then point your browser at:
- `http://localhost:3000/static/` for public web contents,
- `http://localhost:3000/content/` for all public webapp contents,
- `http://localhost:3000/test/` for tests.
**Note:** the provided static file server is **not** intended for production
use.
**Note:** the provided static file server for web contents is **not** intended
for production use.
Code linting
------------

View File

@ -56,6 +56,7 @@
<!-- app scripts -->
<script type="text/javascript" src="shared/js/models.js"></script>
<script type="text/javascript" src="shared/js/views.js"></script>
<script type="text/javascript" src="shared/js/router.js"></script>
<script type="text/javascript" src="js/webapp.js"></script>
<script>

View File

@ -15,11 +15,15 @@ loop.webapp = (function($, TB) {
*
* @type {String}
*/
var baseApiUrl = "http://localhost:5000";
var sharedModels = loop.shared.models,
sharedViews = loop.shared.views,
// XXX this one should be configurable
// see https://bugzilla.mozilla.org/show_bug.cgi?id=987086
baseServerUrl = "http://localhost:5000";
/**
* App router.
* @type {loop.webapp.Router}
* @type {loop.webapp.WebappRouter}
*/
var router;
@ -32,7 +36,7 @@ loop.webapp = (function($, TB) {
/**
* Homepage view.
*/
var HomeView = loop.shared.views.BaseView.extend({
var HomeView = sharedViews.BaseView.extend({
el: "#home"
});
@ -40,7 +44,7 @@ loop.webapp = (function($, TB) {
* Conversation launcher view. A ConversationModel is associated and attached
* as a `model` property.
*/
var ConversationFormView = loop.shared.views.BaseView.extend({
var ConversationFormView = sharedViews.BaseView.extend({
el: "#conversation-form",
events: {
@ -57,18 +61,18 @@ loop.webapp = (function($, TB) {
initiate: function(event) {
event.preventDefault();
this.model.initiate(baseApiUrl);
this.model.initiate(baseServerUrl);
}
});
/**
* App Router. Allows defining a main active view and ease toggling it when
* the active route changes.
* Webapp Router. Allows defining a main active view and easily toggling it
* when the active route changes.
*
* @link http://mikeygee.com/blog/backbone.html
*/
var Router = Backbone.Router.extend({
var WebappRouter = loop.shared.router.BaseRouter.extend({
_conversation: undefined,
activeView: undefined,
routes: {
"": "home",
@ -104,18 +108,6 @@ loop.webapp = (function($, TB) {
this.navigate("call/" + this._conversation.get("token"), {trigger: true});
},
/**
* Loads and render current active view.
*
* @param {loop.shared.BaseView} view View.
*/
loadView : function(view) {
if (this.activeView) {
this.activeView.hide();
}
this.activeView = view.render().show();
},
/**
* Default entry point.
*/
@ -149,7 +141,10 @@ loop.webapp = (function($, TB) {
}
}
this.loadView(
new loop.shared.views.ConversationView({model: this._conversation}));
new sharedViews.ConversationView({
sdk: TB,
model: this._conversation
}));
}
});
@ -157,8 +152,8 @@ loop.webapp = (function($, TB) {
* App initialization.
*/
function init() {
conversation = new loop.shared.models.ConversationModel();
router = new Router({conversation: conversation});
conversation = new sharedModels.ConversationModel();
router = new WebappRouter({conversation: conversation});
Backbone.history.start();
}
@ -166,6 +161,6 @@ loop.webapp = (function($, TB) {
ConversationFormView: ConversationFormView,
HomeView: HomeView,
init: init,
Router: Router
WebappRouter: WebappRouter
};
})(jQuery, window.TB);

View File

@ -9,6 +9,6 @@ app.use(express.static(__dirname + '/'));
app.listen(3000);
console.log("Serving repository root over HTTP at http://localhost:3000/");
console.log("Static contents are available at http://localhost:3000/static/");
console.log("Static contents are available at http://localhost:3000/content/");
console.log("Tests are viewable at http://localhost:3000/test/");
console.log("Use this for development only.");

View File

@ -3,16 +3,13 @@
- 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/. -->
<html>
<head>
<meta charset="utf-8">
<title>Loop mocha tests</title>
<link rel="stylesheet" media="all" href="vendor/mocha-1.17.1.css">
<link rel="prefetch" type="application/l10n" href="../../static/l10n/data.ini">
<link rel="prefetch" type="application/l10n" href="../../content/l10n/data.ini">
</head>
<body>
<div id="mocha">
<p><a href=".">Index</a></p>
</div>
@ -20,10 +17,10 @@
<div id="fixtures"></div>
<!-- libs -->
<script src="../../static/shared/libs/webl10n-20130617.js"></script>
<script src="../../static/shared/libs/jquery-2.1.0.js"></script>
<script src="../../static/shared/libs/lodash-2.4.1.js"></script>
<script src="../../static/shared/libs/backbone-1.1.2.js"></script>
<script src="../../content/shared/libs/webl10n-20130617.js"></script>
<script src="../../content/shared/libs/jquery-2.1.0.js"></script>
<script src="../../content/shared/libs/lodash-2.4.1.js"></script>
<script src="../../content/shared/libs/backbone-1.1.2.js"></script>
<!-- test dependencies -->
<script src="vendor/mocha-1.17.1.js"></script>
@ -35,17 +32,17 @@
</script>
<!-- App scripts -->
<script src="../../static/shared/js/models.js"></script>
<script src="../../static/shared/js/views.js"></script>
<script src="../../content/shared/js/models.js"></script>
<script src="../../content/shared/js/views.js"></script>
<script src="../../content/shared/js/router.js"></script>
<!-- Test scripts -->
<script src="models_test.js"></script>
<script src="views_test.js"></script>
<script src="router_test.js"></script>
<script>
mocha.run(function () {
$("#mocha").append("<p>Complete.</p>");
});
</script>
</body>
</html>

View File

@ -9,7 +9,8 @@ var expect = chai.expect;
describe("loop.shared.models", function() {
"use strict";
var sandbox, fakeXHR, requests = [];
var sharedModels = loop.shared.models,
sandbox, fakeXHR, requests = [];
beforeEach(function() {
sandbox = sinon.sandbox.create();
@ -29,7 +30,7 @@ describe("loop.shared.models", function() {
var conversation, fakeSessionData, fakeBaseServerUrl;
beforeEach(function() {
conversation = new loop.shared.models.ConversationModel();
conversation = new sharedModels.ConversationModel();
fakeSessionData = {
sessionId: "sessionId",
sessionToken: "sessionToken",
@ -39,7 +40,6 @@ describe("loop.shared.models", function() {
});
describe("#initiate", function() {
it("should throw an Error if no baseServerUrl argument is passed",
function () {
expect(function() {
@ -69,8 +69,8 @@ describe("loop.shared.models", function() {
it("should update conversation session information from server data",
function() {
conversation.set("loopToken", "fakeToken");
conversation.initiate(fakeBaseServerUrl);
requests[0].respond(200, {"Content-Type": "application/json"},
JSON.stringify(fakeSessionData));
@ -122,4 +122,3 @@ describe("loop.shared.models", function() {
});
});
});

View File

@ -0,0 +1,40 @@
/* 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/. */
/* global loop, sinon */
var expect = chai.expect;
describe("loop.shared.router", function() {
"use strict";
var sandbox;
beforeEach(function() {
sandbox = sinon.sandbox.create();
});
afterEach(function() {
sandbox.restore();
});
describe("BaseRouter", function() {
var router;
beforeEach(function() {
router = new loop.shared.router.BaseRouter();
});
describe("#loadView", function() {
it("should set the active view", function() {
var TestView = loop.shared.views.BaseView.extend({});
var view = new TestView();
router.loadView(view);
expect(router.activeView).eql(view);
});
});
});
});

View File

@ -2,23 +2,149 @@
* 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/. */
/* global sinon */
/* global loop, sinon */
var expect = chai.expect;
describe("loop.shared.views", function() {
"use strict";
var sandbox;
var sharedModels = loop.shared.models,
sharedViews = loop.shared.views,
sandbox;
beforeEach(function() {
sandbox = sinon.sandbox.create();
sandbox.useFakeTimers(); // exposes sandbox.clock as a fake timer
});
afterEach(function() {
$("#fixtures").empty();
sandbox.restore();
});
describe("ConversationView", function() {
it("should be tested");
var fakeSDK, fakeSession;
beforeEach(function() {
fakeSession = _.extend({
connect: sandbox.spy()
}, Backbone.Events);
fakeSDK = {
initPublisher: sandbox.spy(),
initSession: sandbox.stub().returns(fakeSession)
};
});
describe("initialize", function() {
it("should require an sdk object", function() {
expect(function() {
new sharedViews.ConversationView();
}).to.Throw(Error, /sdk/);
});
it("should initiate a session", function() {
new sharedViews.ConversationView({
sdk: fakeSDK,
model: new sharedModels.ConversationModel()
});
sinon.assert.calledOnce(fakeSession.connect);
});
it("should trigger a session:ended model event when connectionDestroyed" +
" event is received/", function(done) {
// XXX remove when we implement proper notifications
sandbox.stub(window, "alert");
var model = new sharedModels.ConversationModel();
new sharedViews.ConversationView({
sdk: fakeSDK,
model: model
});
model.once("session:ended", function() {
done();
});
fakeSession.trigger("connectionDestroyed", {reason: "ko"});
});
});
});
describe("NotificationView", function() {
var collection, model, view;
beforeEach(function() {
$("#fixtures").append('<div id="test-notif"></div>');
model = new sharedModels.NotificationModel({
level: "error",
message: "plop"
});
collection = new sharedModels.NotificationCollection([model]);
view = new sharedViews.NotificationView({
el: $("#test-notif"),
collection: collection,
model: model
});
});
describe("#dismiss", function() {
it("should automatically dismiss notification after 500ms", function() {
view.render().dismiss({preventDefault: sandbox.spy()});
expect(view.$(".message").text()).eql("plop");
sandbox.clock.tick(500);
expect(collection).to.have.length.of(0);
expect($("#test-notif").html()).eql(undefined);
});
});
describe("#render", function() {
it("should render template with model attribute values", function() {
view.render();
expect(view.$(".message").text()).eql("plop");
});
});
});
describe("NotificationListView", function() {
describe("Collection events", function() {
var coll, testNotif, 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);
sinon.assert.calledOnce(view.render);
});
it("should render on notification removed from the collection",
function() {
coll.add(testNotif);
coll.remove(testNotif);
sinon.assert.calledTwice(view.render);
});
it("should render on collection reset",
function() {
coll.reset();
sinon.assert.calledOnce(view.render);
});
});
});
});

View File

@ -0,0 +1,45 @@
<!DOCTYPE html>
<!-- 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/. -->
<html>
<head>
<meta charset="utf-8">
<title>Loop mocha tests</title>
<link rel="stylesheet" media="all" href="../shared/vendor/mocha-1.17.1.css">
<link rel="prefetch" type="application/l10n" href="../../content/l10n/data.ini">
</head>
<body>
<div id="mocha">
<p><a href=".">Index</a></p>
<p><a href="../shared/">Shared Tests</a></p>
</div>
<div id="messages"></div>
<div id="fixtures"></div>
<!-- libs -->
<script src="../../content/shared/libs/webl10n-20130617.js"></script>
<script src="../../content/shared/libs/jquery-2.1.0.js"></script>
<script src="../../content/shared/libs/lodash-2.4.1.js"></script>
<script src="../../content/shared/libs/backbone-1.1.2.js"></script>
<!-- test dependencies -->
<script src="../shared/vendor/mocha-1.17.1.js"></script>
<script src="../shared/vendor/chai-1.9.0.js"></script>
<script src="../shared/vendor/sinon-1.9.0.js"></script>
<script>
chai.Assertion.includeStack = true;
mocha.setup('bdd');
</script>
<!-- App scripts -->
<script src="../../content/shared/js/models.js"></script>
<script src="../../content/shared/js/views.js"></script>
<script src="../../content/shared/js/router.js"></script>
<script src="../../content/js/webapp.js"></script>
<!-- Test scripts -->
<script src="webapp_test.js"></script>
<script>
mocha.run(function () {
$("#mocha").append("<p>Complete.</p>");
});
</script>
</body>
</html>

View File

@ -9,7 +9,8 @@ var expect = chai.expect;
describe("loop.webapp", function() {
"use strict";
var sandbox;
var sharedModels = loop.shared.models,
sandbox;
beforeEach(function() {
sandbox = sinon.sandbox.create();
@ -19,11 +20,11 @@ describe("loop.webapp", function() {
sandbox.restore();
});
describe("Router", function() {
describe("WebappRouter", function() {
var conversation, fakeSessionData;
beforeEach(function() {
conversation = new loop.shared.models.ConversationModel();
conversation = new sharedModels.ConversationModel();
fakeSessionData = {
sessionId: "sessionId",
sessionToken: "sessionToken",
@ -34,14 +35,14 @@ describe("loop.webapp", function() {
describe("#constructor", function() {
it("should require a ConversationModel instance", function() {
expect(function() {
new loop.webapp.Router();
new loop.webapp.WebappRouter();
}).to.Throw(Error, /missing required conversation/);
});
it("should load the HomeView", function() {
sandbox.stub(loop.webapp.Router.prototype, "loadView");
sandbox.stub(loop.webapp.WebappRouter.prototype, "loadView");
var router = new loop.webapp.Router({conversation: conversation});
var router = new loop.webapp.WebappRouter({conversation: conversation});
sinon.assert.calledOnce(router.loadView);
sinon.assert.calledWithMatch(router.loadView,
@ -53,7 +54,7 @@ describe("loop.webapp", function() {
var router;
beforeEach(function() {
router = new loop.webapp.Router({conversation: conversation});
router = new loop.webapp.WebappRouter({conversation: conversation});
});
describe("#loadView", function() {
@ -66,7 +67,7 @@ describe("loop.webapp", function() {
var router;
beforeEach(function() {
router = new loop.webapp.Router({conversation: conversation});
router = new loop.webapp.WebappRouter({conversation: conversation});
sandbox.stub(router, "loadView");
});
@ -132,8 +133,10 @@ describe("loop.webapp", function() {
describe("Events", function() {
it("should navigate to call/ongoing once the call session is ready",
function() {
sandbox.stub(loop.webapp.Router.prototype, "navigate");
var router = new loop.webapp.Router({conversation: conversation});
sandbox.stub(loop.webapp.WebappRouter.prototype, "navigate");
var router = new loop.webapp.WebappRouter({
conversation: conversation
});
conversation.setReady(fakeSessionData);
@ -143,40 +146,12 @@ describe("loop.webapp", function() {
});
});
describe("Router", function() {
var router, conversation;
beforeEach(function() {
conversation = new loop.shared.models.ConversationModel({
loopToken: "fake"
});
router = new loop.webapp.Router({conversation: conversation});
});
describe("#constructor", function() {
it("should define a default active view", function() {
expect(router.activeView).to.be.an.instanceOf(loop.webapp.HomeView);
});
});
describe("#loadView", function() {
it("should set the active view", function() {
router.loadView(new loop.webapp.ConversationFormView({
model: conversation
}));
expect(router.activeView).to.be.an.instanceOf(
loop.webapp.ConversationFormView);
});
});
});
describe("ConversationFormView", function() {
describe("#initiate", function() {
var conversation, initiate, view, fakeSubmitEvent;
beforeEach(function() {
conversation = new loop.shared.models.ConversationModel();
conversation = new sharedModels.ConversationModel();
view = new loop.webapp.ConversationFormView({model: conversation});
fakeSubmitEvent = {preventDefault: sinon.spy()};
});