--- a/browser/components/loop/content/conversation.html
+++ b/browser/components/loop/content/conversation.html
@@ -30,15 +30,17 @@
<script type="text/javascript" src="loop/shared/js/mixins.js"></script>
<script type="text/javascript" src="loop/shared/js/views.js"></script>
<script type="text/javascript" src="loop/shared/js/feedbackApiClient.js"></script>
<script type="text/javascript" src="loop/shared/js/actions.js"></script>
<script type="text/javascript" src="loop/shared/js/validate.js"></script>
<script type="text/javascript" src="loop/shared/js/dispatcher.js"></script>
<script type="text/javascript" src="loop/shared/js/otSdkDriver.js"></script>
<script type="text/javascript" src="loop/shared/js/conversationStore.js"></script>
+ <script type="text/javascript" src="loop/shared/js/localRoomStore.js"></script>
<script type="text/javascript" src="loop/js/conversationViews.js"></script>
<script type="text/javascript" src="loop/shared/js/websocket.js"></script>
<script type="text/javascript" src="loop/js/client.js"></script>
<script type="text/javascript" src="loop/js/conversationViews.js"></script>
+ <script type="text/javascript" src="loop/js/roomViews.js"></script>
<script type="text/javascript" src="loop/js/conversation.js"></script>
</body>
</html>
--- a/browser/components/loop/content/js/conversation.js
+++ b/browser/components/loop/content/js/conversation.js
@@ -9,18 +9,21 @@
var loop = loop || {};
loop.conversation = (function(mozL10n) {
"use strict";
var sharedViews = loop.shared.views;
var sharedMixins = loop.shared.mixins;
var sharedModels = loop.shared.models;
+ var sharedActions = loop.shared.actions;
+
var OutgoingConversationView = loop.conversationViews.OutgoingConversationView;
var CallIdentifierView = loop.conversationViews.CallIdentifierView;
+ var EmptyRoomView = loop.roomViews.EmptyRoomView;
var IncomingCallView = React.createClass({displayName: 'IncomingCallView',
mixins: [sharedMixins.DropdownMenuMixin],
propTypes: {
model: React.PropTypes.object.isRequired,
video: React.PropTypes.bool.isRequired
},
@@ -475,40 +478,52 @@ loop.conversation = (function(mozL10n) {
console.error("Failed initiating the call session.");
},
});
/**
* Master controller view for handling if incoming or outgoing calls are
* in progress, and hence, which view to display.
*/
- var ConversationControllerView = React.createClass({displayName: 'ConversationControllerView',
+ var AppControllerView = React.createClass({displayName: 'AppControllerView',
propTypes: {
// XXX Old types required for incoming call view.
client: React.PropTypes.instanceOf(loop.Client).isRequired,
conversation: React.PropTypes.instanceOf(sharedModels.ConversationModel)
.isRequired,
sdk: React.PropTypes.object.isRequired,
// XXX New types for OutgoingConversationView
store: React.PropTypes.instanceOf(loop.store.ConversationStore).isRequired,
- dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired
+ dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
+
+ // if not passed, this is not a room view
+ localRoomStore: React.PropTypes.instanceOf(loop.store.LocalRoomStore)
},
getInitialState: function() {
return this.props.store.attributes;
},
componentWillMount: function() {
this.props.store.on("change:outgoing", function() {
this.setState(this.props.store.attributes);
}, this);
},
render: function() {
+ if (this.props.localRoomStore) {
+ return (
+ EmptyRoomView({
+ mozLoop: navigator.mozLoop,
+ localRoomStore: this.props.localRoomStore}
+ )
+ );
+ }
+
// Don't display anything, until we know what type of call we are.
if (this.state.outgoing === undefined) {
return null;
}
if (this.state.outgoing) {
return (OutgoingConversationView({
store: this.props.store,
@@ -564,53 +579,72 @@ loop.conversation = (function(mozL10n) {
{sdk: window.OT} // Model dependencies
);
// Obtain the callId and pass it through
var helper = new loop.shared.utils.Helper();
var locationHash = helper.locationHash();
var callId;
var outgoing;
+ var localRoomStore;
- var hash = locationHash.match(/\#incoming\/(.*)/);
+ // XXX removeMe, along with noisy comment at the beginning of
+ // conversation_test.js "when locationHash begins with #room".
+ if (navigator.mozLoop.getLoopBoolPref("test.alwaysUseRooms")) {
+ locationHash = "#room/32";
+ }
+
+ var hash = locationHash.match(/#incoming\/(.*)/);
if (hash) {
callId = hash[1];
outgoing = false;
+ } else if (hash = locationHash.match(/#room\/(.*)/)) {
+ localRoomStore = new loop.store.LocalRoomStore({
+ dispatcher: dispatcher,
+ mozLoop: navigator.mozLoop
+ });
} else {
- hash = locationHash.match(/\#outgoing\/(.*)/);
+ hash = locationHash.match(/#outgoing\/(.*)/);
if (hash) {
callId = hash[1];
outgoing = true;
}
}
conversation.set({callId: callId});
window.addEventListener("unload", function(event) {
// Handle direct close of dialog box via [x] control.
navigator.mozLoop.releaseCallData(callId);
});
document.body.classList.add(loop.shared.utils.getTargetPlatform());
- React.renderComponent(ConversationControllerView({
+ React.renderComponent(AppControllerView({
+ localRoomStore: localRoomStore,
store: conversationStore,
client: client,
conversation: conversation,
dispatcher: dispatcher,
sdk: window.OT}
), document.querySelector('#main'));
+ if (localRoomStore) {
+ dispatcher.dispatch(
+ new sharedActions.SetupEmptyRoom({localRoomId: hash[1]}));
+ return;
+ }
+
dispatcher.dispatch(new loop.shared.actions.GatherCallData({
callId: callId,
outgoing: outgoing
}));
}
return {
- ConversationControllerView: ConversationControllerView,
+ AppControllerView: AppControllerView,
IncomingConversationView: IncomingConversationView,
IncomingCallView: IncomingCallView,
init: init
};
})(document.mozL10n);
document.addEventListener('DOMContentLoaded', loop.conversation.init);
--- a/browser/components/loop/content/js/conversation.jsx
+++ b/browser/components/loop/content/js/conversation.jsx
@@ -9,18 +9,21 @@
var loop = loop || {};
loop.conversation = (function(mozL10n) {
"use strict";
var sharedViews = loop.shared.views;
var sharedMixins = loop.shared.mixins;
var sharedModels = loop.shared.models;
+ var sharedActions = loop.shared.actions;
+
var OutgoingConversationView = loop.conversationViews.OutgoingConversationView;
var CallIdentifierView = loop.conversationViews.CallIdentifierView;
+ var EmptyRoomView = loop.roomViews.EmptyRoomView;
var IncomingCallView = React.createClass({
mixins: [sharedMixins.DropdownMenuMixin],
propTypes: {
model: React.PropTypes.object.isRequired,
video: React.PropTypes.bool.isRequired
},
@@ -475,40 +478,52 @@ loop.conversation = (function(mozL10n) {
console.error("Failed initiating the call session.");
},
});
/**
* Master controller view for handling if incoming or outgoing calls are
* in progress, and hence, which view to display.
*/
- var ConversationControllerView = React.createClass({
+ var AppControllerView = React.createClass({
propTypes: {
// XXX Old types required for incoming call view.
client: React.PropTypes.instanceOf(loop.Client).isRequired,
conversation: React.PropTypes.instanceOf(sharedModels.ConversationModel)
.isRequired,
sdk: React.PropTypes.object.isRequired,
// XXX New types for OutgoingConversationView
store: React.PropTypes.instanceOf(loop.store.ConversationStore).isRequired,
- dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired
+ dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
+
+ // if not passed, this is not a room view
+ localRoomStore: React.PropTypes.instanceOf(loop.store.LocalRoomStore)
},
getInitialState: function() {
return this.props.store.attributes;
},
componentWillMount: function() {
this.props.store.on("change:outgoing", function() {
this.setState(this.props.store.attributes);
}, this);
},
render: function() {
+ if (this.props.localRoomStore) {
+ return (
+ <EmptyRoomView
+ mozLoop={navigator.mozLoop}
+ localRoomStore={this.props.localRoomStore}
+ />
+ );
+ }
+
// Don't display anything, until we know what type of call we are.
if (this.state.outgoing === undefined) {
return null;
}
if (this.state.outgoing) {
return (<OutgoingConversationView
store={this.props.store}
@@ -564,53 +579,72 @@ loop.conversation = (function(mozL10n) {
{sdk: window.OT} // Model dependencies
);
// Obtain the callId and pass it through
var helper = new loop.shared.utils.Helper();
var locationHash = helper.locationHash();
var callId;
var outgoing;
+ var localRoomStore;
- var hash = locationHash.match(/\#incoming\/(.*)/);
+ // XXX removeMe, along with noisy comment at the beginning of
+ // conversation_test.js "when locationHash begins with #room".
+ if (navigator.mozLoop.getLoopBoolPref("test.alwaysUseRooms")) {
+ locationHash = "#room/32";
+ }
+
+ var hash = locationHash.match(/#incoming\/(.*)/);
if (hash) {
callId = hash[1];
outgoing = false;
+ } else if (hash = locationHash.match(/#room\/(.*)/)) {
+ localRoomStore = new loop.store.LocalRoomStore({
+ dispatcher: dispatcher,
+ mozLoop: navigator.mozLoop
+ });
} else {
- hash = locationHash.match(/\#outgoing\/(.*)/);
+ hash = locationHash.match(/#outgoing\/(.*)/);
if (hash) {
callId = hash[1];
outgoing = true;
}
}
conversation.set({callId: callId});
window.addEventListener("unload", function(event) {
// Handle direct close of dialog box via [x] control.
navigator.mozLoop.releaseCallData(callId);
});
document.body.classList.add(loop.shared.utils.getTargetPlatform());
- React.renderComponent(<ConversationControllerView
+ React.renderComponent(<AppControllerView
+ localRoomStore={localRoomStore}
store={conversationStore}
client={client}
conversation={conversation}
dispatcher={dispatcher}
sdk={window.OT}
/>, document.querySelector('#main'));
+ if (localRoomStore) {
+ dispatcher.dispatch(
+ new sharedActions.SetupEmptyRoom({localRoomId: hash[1]}));
+ return;
+ }
+
dispatcher.dispatch(new loop.shared.actions.GatherCallData({
callId: callId,
outgoing: outgoing
}));
}
return {
- ConversationControllerView: ConversationControllerView,
+ AppControllerView: AppControllerView,
IncomingConversationView: IncomingConversationView,
IncomingCallView: IncomingCallView,
init: init
};
})(document.mozL10n);
document.addEventListener('DOMContentLoaded', loop.conversation.init);
new file mode 100644
--- /dev/null
+++ b/browser/components/loop/content/js/roomViews.js
@@ -0,0 +1,109 @@
+/** @jsx React.DOM */
+
+/* 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, React */
+
+var loop = loop || {};
+loop.roomViews = (function(mozL10n) {
+ "use strict";
+
+ /**
+ * Root object, by default set to window.
+ * @type {DOMWindow|Object}
+ */
+ var rootObject = window;
+
+ /**
+ * Sets a new root object. This is useful for testing native DOM events so we
+ * can fake them.
+ *
+ * @param {Object}
+ */
+ function setRootObject(obj) {
+ rootObject = obj;
+ }
+
+ var EmptyRoomView = React.createClass({displayName: 'EmptyRoomView',
+ mixins: [Backbone.Events],
+
+ propTypes: {
+ mozLoop:
+ React.PropTypes.object.isRequired,
+ localRoomStore:
+ React.PropTypes.instanceOf(loop.store.LocalRoomStore).isRequired,
+ },
+
+ getInitialState: function() {
+ return this.props.localRoomStore.getStoreState();
+ },
+
+ componentWillMount: function() {
+ this.listenTo(this.props.localRoomStore, "change",
+ this._onLocalRoomStoreChanged);
+ },
+
+ componentDidMount: function() {
+ // XXXremoveMe (just the conditional itself) in patch 2 for bug 1074686,
+ // once the addCallback stuff lands
+ if (this.props.mozLoop.rooms && this.props.mozLoop.rooms.addCallback) {
+ this.props.mozLoop.rooms.addCallback(
+ this.state.localRoomId,
+ "RoomCreationError", this.onCreationError);
+ }
+ },
+
+ /**
+ * Attached to the "RoomCreationError" with mozLoop.rooms.addCallback,
+ * which is fired mozLoop.rooms.createRoom from the panel encounters an
+ * error while attempting to create the room for this view.
+ *
+ * @param {Error} err - JS Error object with info about the problem
+ */
+ onCreationError: function(err) {
+ // XXX put up a user friendly error instead of this
+ rootObject.console.error("EmptyRoomView creation error: ", err);
+ },
+
+ /**
+ * Handles a "change" event on the localRoomStore, and updates this.state
+ * to match the store.
+ *
+ * @private
+ */
+ _onLocalRoomStoreChanged: function() {
+ this.setState(this.props.localRoomStore.getStoreState());
+ },
+
+ componentWillUnmount: function() {
+ this.stopListening(this.props.localRoomStore);
+
+ // XXXremoveMe (just the conditional itself) in patch 2 for bug 1074686,
+ // once the addCallback stuff lands
+ if (this.props.mozLoop.rooms && this.props.mozLoop.rooms.removeCallback) {
+ this.props.mozLoop.rooms.removeCallback(
+ this.state.localRoomId,
+ "RoomCreationError", this.onCreationError);
+ }
+ },
+
+ render: function() {
+ // XXX switch this to use the document title mixin once bug 1081079 lands
+ if (this.state.serverData && this.state.serverData.roomName) {
+ rootObject.document.title = this.state.serverData.roomName;
+ }
+
+ return (
+ React.DOM.div({className: "goat"})
+ );
+ }
+ });
+
+ return {
+ setRootObject: setRootObject,
+ EmptyRoomView: EmptyRoomView
+ };
+
+})(document.mozL10n || navigator.mozL10n);;
new file mode 100644
--- /dev/null
+++ b/browser/components/loop/content/js/roomViews.jsx
@@ -0,0 +1,109 @@
+/** @jsx React.DOM */
+
+/* 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, React */
+
+var loop = loop || {};
+loop.roomViews = (function(mozL10n) {
+ "use strict";
+
+ /**
+ * Root object, by default set to window.
+ * @type {DOMWindow|Object}
+ */
+ var rootObject = window;
+
+ /**
+ * Sets a new root object. This is useful for testing native DOM events so we
+ * can fake them.
+ *
+ * @param {Object}
+ */
+ function setRootObject(obj) {
+ rootObject = obj;
+ }
+
+ var EmptyRoomView = React.createClass({
+ mixins: [Backbone.Events],
+
+ propTypes: {
+ mozLoop:
+ React.PropTypes.object.isRequired,
+ localRoomStore:
+ React.PropTypes.instanceOf(loop.store.LocalRoomStore).isRequired,
+ },
+
+ getInitialState: function() {
+ return this.props.localRoomStore.getStoreState();
+ },
+
+ componentWillMount: function() {
+ this.listenTo(this.props.localRoomStore, "change",
+ this._onLocalRoomStoreChanged);
+ },
+
+ componentDidMount: function() {
+ // XXXremoveMe (just the conditional itself) in patch 2 for bug 1074686,
+ // once the addCallback stuff lands
+ if (this.props.mozLoop.rooms && this.props.mozLoop.rooms.addCallback) {
+ this.props.mozLoop.rooms.addCallback(
+ this.state.localRoomId,
+ "RoomCreationError", this.onCreationError);
+ }
+ },
+
+ /**
+ * Attached to the "RoomCreationError" with mozLoop.rooms.addCallback,
+ * which is fired mozLoop.rooms.createRoom from the panel encounters an
+ * error while attempting to create the room for this view.
+ *
+ * @param {Error} err - JS Error object with info about the problem
+ */
+ onCreationError: function(err) {
+ // XXX put up a user friendly error instead of this
+ rootObject.console.error("EmptyRoomView creation error: ", err);
+ },
+
+ /**
+ * Handles a "change" event on the localRoomStore, and updates this.state
+ * to match the store.
+ *
+ * @private
+ */
+ _onLocalRoomStoreChanged: function() {
+ this.setState(this.props.localRoomStore.getStoreState());
+ },
+
+ componentWillUnmount: function() {
+ this.stopListening(this.props.localRoomStore);
+
+ // XXXremoveMe (just the conditional itself) in patch 2 for bug 1074686,
+ // once the addCallback stuff lands
+ if (this.props.mozLoop.rooms && this.props.mozLoop.rooms.removeCallback) {
+ this.props.mozLoop.rooms.removeCallback(
+ this.state.localRoomId,
+ "RoomCreationError", this.onCreationError);
+ }
+ },
+
+ render: function() {
+ // XXX switch this to use the document title mixin once bug 1081079 lands
+ if (this.state.serverData && this.state.serverData.roomName) {
+ rootObject.document.title = this.state.serverData.roomName;
+ }
+
+ return (
+ <div className="goat"/>
+ );
+ }
+ });
+
+ return {
+ setRootObject: setRootObject,
+ EmptyRoomView: EmptyRoomView
+ };
+
+})(document.mozL10n || navigator.mozL10n);;
--- a/browser/components/loop/content/shared/js/actions.js
+++ b/browser/components/loop/content/shared/js/actions.js
@@ -120,11 +120,21 @@ loop.shared.actions = (function() {
enabled: Boolean
}),
/**
* Retrieves room list.
* XXX: should move to some roomActions module - refs bug 1079284
*/
GetAllRooms: Action.define("getAllRooms", {
- })
+ }),
+
+ /**
+ * Primes localRoomStore with roomLocalId, which triggers the EmptyRoomView
+ * to do any necessary setup.
+ *
+ * XXX should move to localRoomActions module
+ */
+ SetupEmptyRoom: Action.define("setupEmptyRoom", {
+ localRoomId: String
+ }),
};
})();
new file mode 100644
--- /dev/null
+++ b/browser/components/loop/content/shared/js/localRoomStore.js
@@ -0,0 +1,113 @@
+/* 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.store = loop.store || {};
+loop.store.LocalRoomStore = (function() {
+ "use strict";
+
+ var sharedActions = loop.shared.actions;
+
+ /**
+ * Store for things that are local to this instance (in this profile, on
+ * this machine) of this roomRoom store, in addition to a mirror of some
+ * remote-state.
+ *
+ * @extends {Backbone.Events}
+ *
+ * @param {Object} options - Options object
+ * @param {loop.Dispatcher} options.dispatch - The dispatcher for dispatching
+ * actions and registering to consume them.
+ * @param {MozLoop} options.mozLoop - MozLoop API provider object
+ */
+ function LocalRoomStore(options) {
+ options = options || {};
+
+ if (!options.dispatcher) {
+ throw new Error("Missing option dispatcher");
+ }
+ this.dispatcher = options.dispatcher;
+
+ if (!options.mozLoop) {
+ throw new Error("Missing option mozLoop");
+ }
+ this.mozLoop = options.mozLoop;
+
+ this.dispatcher.register(this, ["setupEmptyRoom"]);
+ }
+
+ LocalRoomStore.prototype = _.extend({
+
+ /**
+ * Stored data reflecting the local state of a given room, used to drive
+ * the room's views.
+ *
+ * @property {Object} serverData - local cache of the data returned by
+ * MozLoop.getRoomData for this room.
+ * @see https://wiki.mozilla.org/Loop/Architecture/Rooms#GET_.2Frooms.2F.7Btoken.7D
+ *
+ * @property {Error=} error - if the room is an error state, this will be
+ * set to an Error object reflecting the problem;
+ * otherwise it will be unset.
+ *
+ * @property {String} localRoomId - profile-local identifier used with
+ * the MozLoop API.
+ */
+ _storeState: {
+ },
+
+ getStoreState: function() {
+ return this._storeState;
+ },
+
+ setStoreState: function(state) {
+ this._storeState = state;
+ this.trigger("change");
+ },
+
+ /**
+ * Proxy to mozLoop.rooms.getRoomData for setupEmptyRoom action.
+ *
+ * XXXremoveMe Can probably be removed when bug 1074664 lands.
+ *
+ * @param {sharedActions.setupEmptyRoom} actionData
+ * @param {Function} cb Callback(error, roomData)
+ */
+ _fetchRoomData: function(actionData, cb) {
+ if (this.mozLoop.rooms && this.mozLoop.rooms.getRoomData) {
+ this.mozLoop.rooms.getRoomData(actionData.localRoomId, cb);
+ } else {
+ cb(null, {roomName: "Donkeys"});
+ }
+ },
+
+ /**
+ * Execute setupEmptyRoom event action from the dispatcher. This primes
+ * the store with the localRoomId, and calls MozLoop.getRoomData on that
+ * ID. This will return either a reflection of state on the server, or,
+ * if the createRoom call hasn't yet returned, it will have at least the
+ * roomName as specified to the createRoom method.
+ *
+ * When the room name gets set, that will trigger the view to display
+ * that name.
+ *
+ * @param {sharedActions.setupEmptyRoom} actionData
+ */
+ setupEmptyRoom: function(actionData) {
+ this._fetchRoomData(actionData, function(error, roomData) {
+ this.setStoreState({
+ error: error,
+ localRoomId: actionData.localRoomId,
+ serverData: roomData
+ });
+ }.bind(this));
+ }
+
+ }, Backbone.Events);
+
+ return LocalRoomStore;
+
+})();
--- a/browser/components/loop/jar.mn
+++ b/browser/components/loop/jar.mn
@@ -12,16 +12,17 @@ browser.jar:
# Desktop script
content/browser/loop/js/client.js (content/js/client.js)
content/browser/loop/js/conversation.js (content/js/conversation.js)
content/browser/loop/js/otconfig.js (content/js/otconfig.js)
content/browser/loop/js/panel.js (content/js/panel.js)
content/browser/loop/js/contacts.js (content/js/contacts.js)
content/browser/loop/js/conversationViews.js (content/js/conversationViews.js)
+ content/browser/loop/js/roomViews.js (content/js/roomViews.js)
# Shared styles
content/browser/loop/shared/css/reset.css (content/shared/css/reset.css)
content/browser/loop/shared/css/common.css (content/shared/css/common.css)
content/browser/loop/shared/css/panel.css (content/shared/css/panel.css)
content/browser/loop/shared/css/conversation.css (content/shared/css/conversation.css)
content/browser/loop/shared/css/contacts.css (content/shared/css/contacts.css)
@@ -52,16 +53,17 @@ browser.jar:
content/browser/loop/shared/img/icons-10x10.svg (content/shared/img/icons-10x10.svg)
content/browser/loop/shared/img/icons-14x14.svg (content/shared/img/icons-14x14.svg)
content/browser/loop/shared/img/icons-16x16.svg (content/shared/img/icons-16x16.svg)
# Shared scripts
content/browser/loop/shared/js/actions.js (content/shared/js/actions.js)
content/browser/loop/shared/js/conversationStore.js (content/shared/js/conversationStore.js)
content/browser/loop/shared/js/roomListStore.js (content/shared/js/roomListStore.js)
+ content/browser/loop/shared/js/localRoomStore.js (content/shared/js/localRoomStore.js)
content/browser/loop/shared/js/dispatcher.js (content/shared/js/dispatcher.js)
content/browser/loop/shared/js/feedbackApiClient.js (content/shared/js/feedbackApiClient.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/utils.js (content/shared/js/utils.js)
content/browser/loop/shared/js/validate.js (content/shared/js/validate.js)
--- a/browser/components/loop/test/desktop-local/conversationViews_test.js
+++ b/browser/components/loop/test/desktop-local/conversationViews_test.js
@@ -1,14 +1,16 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
var expect = chai.expect;
describe("loop.conversationViews", function () {
+ "use strict";
+
var sandbox, oldTitle, view, dispatcher, contact;
var CALL_STATES = loop.store.CALL_STATES;
beforeEach(function() {
sandbox = sinon.sandbox.create();
oldTitle = document.title;
--- a/browser/components/loop/test/desktop-local/conversation_test.js
+++ b/browser/components/loop/test/desktop-local/conversation_test.js
@@ -85,35 +85,72 @@ describe("loop.conversation", function()
overrideGuidStorage: sinon.stub()
};
});
afterEach(function() {
delete window.OT;
});
- it("should initalize L10n", function() {
+ it("should initialize L10n", function() {
loop.conversation.init();
sinon.assert.calledOnce(document.mozL10n.initialize);
sinon.assert.calledWithExactly(document.mozL10n.initialize,
navigator.mozLoop);
});
- it("should create the ConversationControllerView", function() {
+ it("should create the AppControllerView", function() {
loop.conversation.init();
sinon.assert.calledOnce(React.renderComponent);
sinon.assert.calledWith(React.renderComponent,
sinon.match(function(value) {
return TestUtils.isDescriptorOfType(value,
- loop.conversation.ConversationControllerView);
+ loop.conversation.AppControllerView);
}));
});
+ describe("when locationHash begins with #room", function () {
+ // XXX must stay in sync with "test.alwaysUseRooms" pref check
+ // in conversation.jsx:init until we remove that code, which should
+ // happen in the second patch in bug 1074686, at which time this comment
+ // can go away as well.
+ var fakeRoomID = "32";
+
+ beforeEach(function() {
+ loop.shared.utils.Helper.prototype.locationHash
+ .returns("#room/" + fakeRoomID);
+
+ sandbox.stub(loop.store, "LocalRoomStore");
+ });
+
+ it("should create a localRoomStore", function() {
+ loop.conversation.init();
+
+ sinon.assert.calledOnce(loop.store.LocalRoomStore);
+ sinon.assert.calledWithNew(loop.store.LocalRoomStore);
+ sinon.assert.calledWithExactly(loop.store.LocalRoomStore,
+ sinon.match({
+ dispatcher: sinon.match.instanceOf(loop.Dispatcher),
+ mozLoop: sinon.match.same(navigator.mozLoop)
+ }));
+ });
+
+ it("should dispatch SetupEmptyRoom with localRoomId from locationHash",
+ function() {
+
+ loop.conversation.init();
+
+ sinon.assert.calledOnce(loop.Dispatcher.prototype.dispatch);
+ sinon.assert.calledWithExactly(loop.Dispatcher.prototype.dispatch,
+ new loop.shared.actions.SetupEmptyRoom({localRoomId: fakeRoomID}));
+ });
+ });
+
it("should trigger a gatherCallData action", function() {
loop.conversation.init();
sinon.assert.calledOnce(loop.Dispatcher.prototype.dispatch);
sinon.assert.calledWithExactly(loop.Dispatcher.prototype.dispatch,
new loop.shared.actions.GatherCallData({
callId: "42",
outgoing: false
@@ -133,21 +170,22 @@ describe("loop.conversation", function()
outgoing: true
}));
});
});
describe("ConversationControllerView", function() {
var store, conversation, client, ccView, oldTitle, dispatcher;
- function mountTestComponent() {
+ function mountTestComponent(localRoomStore) {
return TestUtils.renderIntoDocument(
- loop.conversation.ConversationControllerView({
+ loop.conversation.AppControllerView({
client: client,
conversation: conversation,
+ localRoomStore: localRoomStore,
sdk: {},
store: store
}));
}
beforeEach(function() {
oldTitle = document.title;
client = new loop.Client();
@@ -188,16 +226,32 @@ describe("loop.conversation", function()
it("should display the IncomingConversationView for incoming calls", function() {
store.set({outgoing: false});
ccView = mountTestComponent();
TestUtils.findRenderedComponentWithType(ccView,
loop.conversation.IncomingConversationView);
});
+
+ it("should display the EmptyRoomView for rooms", function() {
+ navigator.mozLoop.rooms = {
+ addCallback: function() {},
+ removeCallback: function() {}
+ };
+ var localRoomStore = new loop.store.LocalRoomStore({
+ mozLoop: navigator.mozLoop,
+ dispatcher: dispatcher
+ });
+
+ ccView = mountTestComponent(localRoomStore);
+
+ TestUtils.findRenderedComponentWithType(ccView,
+ loop.roomViews.EmptyRoomView);
+ });
});
describe("IncomingConversationView", function() {
var conversation, client, icView, oldTitle;
function mountTestComponent() {
return TestUtils.renderIntoDocument(
loop.conversation.IncomingConversationView({
--- a/browser/components/loop/test/desktop-local/index.html
+++ b/browser/components/loop/test/desktop-local/index.html
@@ -40,25 +40,28 @@
<script src="../../content/shared/js/views.js"></script>
<script src="../../content/shared/js/websocket.js"></script>
<script src="../../content/shared/js/actions.js"></script>
<script src="../../content/shared/js/validate.js"></script>
<script src="../../content/shared/js/dispatcher.js"></script>
<script src="../../content/shared/js/otSdkDriver.js"></script>
<script src="../../content/shared/js/roomListStore.js"></script>
<script src="../../content/js/client.js"></script>
+ <script src="../../content/shared/js/localRoomStore.js"></script>
+ <script src="../../content/js/roomViews.js"></script>
<script src="../../content/js/conversationViews.js"></script>
<script src="../../content/js/conversation.js"></script>
<script type="text/javascript;version=1.8" src="../../content/js/contacts.js"></script>
<script src="../../content/js/panel.js"></script>
<!-- Test scripts -->
<script src="client_test.js"></script>
<script src="conversation_test.js"></script>
<script src="panel_test.js"></script>
+ <script src="roomViews_test.js"></script>
<script src="conversationViews_test.js"></script>
<script>
// Stop the default init functions running to avoid conflicts in tests
document.removeEventListener('DOMContentLoaded', loop.panel.init);
document.removeEventListener('DOMContentLoaded', loop.conversation.init);
mocha.run(function () {
$("#mocha").append("<p id='complete'>Complete.</p>");
});
new file mode 100644
--- /dev/null
+++ b/browser/components/loop/test/desktop-local/roomViews_test.js
@@ -0,0 +1,94 @@
+var expect = chai.expect;
+
+describe("loop.roomViews", function () {
+ "use strict";
+
+ var store, fakeWindow, sandbox, fakeAddCallback, fakeMozLoop,
+ fakeRemoveCallback, fakeRoomId, fakeWindow;
+
+ beforeEach(function() {
+ sandbox = sinon.sandbox.create();
+
+ fakeRoomId = "fakeRoomId";
+ fakeAddCallback =
+ sandbox.stub().withArgs(fakeRoomId, "RoomCreationError");
+ fakeRemoveCallback =
+ sandbox.stub().withArgs(fakeRoomId, "RoomCreationError");
+ fakeMozLoop = { rooms: { addCallback: fakeAddCallback,
+ removeCallback: fakeRemoveCallback } };
+
+ fakeWindow = { document: {} };
+ loop.roomViews.setRootObject(fakeWindow);
+
+ store = new loop.store.LocalRoomStore({
+ dispatcher: { register: function() {} },
+ mozLoop: fakeMozLoop
+ });
+ store.setStoreState({localRoomId: fakeRoomId});
+ });
+
+ afterEach(function() {
+ sinon.sandbox.restore();
+ loop.roomViews.setRootObject(window);
+ });
+
+ describe("EmptyRoomView", function() {
+ function mountTestComponent() {
+ return TestUtils.renderIntoDocument(
+ new loop.roomViews.EmptyRoomView({
+ mozLoop: fakeMozLoop,
+ localRoomStore: store
+ }));
+ }
+
+ describe("#componentDidMount", function() {
+ it("should add #onCreationError using mozLoop.rooms.addCallback",
+ function() {
+
+ var testComponent = mountTestComponent();
+
+ sinon.assert.calledOnce(fakeMozLoop.rooms.addCallback);
+ sinon.assert.calledWithExactly(fakeMozLoop.rooms.addCallback,
+ fakeRoomId, "RoomCreationError", testComponent.onCreationError);
+ });
+ });
+
+ describe("#componentWillUnmount", function () {
+ it("should remove #onCreationError using mozLoop.rooms.addCallback",
+ function () {
+ var testComponent = mountTestComponent();
+
+ testComponent.componentWillUnmount();
+
+ sinon.assert.calledOnce(fakeMozLoop.rooms.removeCallback);
+ sinon.assert.calledWithExactly(fakeMozLoop.rooms.removeCallback,
+ fakeRoomId, "RoomCreationError", testComponent.onCreationError);
+ });
+ });
+
+ describe("#onCreationError", function() {
+ it("should log an error using console.error", function() {
+ fakeWindow.console = { error: sandbox.stub() };
+ var testComponent = mountTestComponent();
+
+ testComponent.onCreationError(new Error("fake error"));
+
+ sinon.assert.calledOnce(fakeWindow.console.error);
+ });
+ });
+
+ describe("#render", function() {
+ it("should set document.title to store.serverData.roomName",
+ function() {
+ var fakeRoomName = "Monkey";
+ store.setStoreState({serverData: {roomName: fakeRoomName},
+ localRoomId: fakeRoomId});
+
+ mountTestComponent();
+
+ expect(fakeWindow.document.title).to.equal(fakeRoomName);
+ })
+ });
+
+ });
+});
--- a/browser/components/loop/test/shared/conversationStore_test.js
+++ b/browser/components/loop/test/shared/conversationStore_test.js
@@ -1,14 +1,14 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
var expect = chai.expect;
-describe("loop.ConversationStore", function () {
+describe("loop.store.ConversationStore", function () {
"use strict";
var CALL_STATES = loop.store.CALL_STATES;
var WS_STATES = loop.store.WS_STATES;
var sharedActions = loop.shared.actions;
var sharedUtils = loop.shared.utils;
var sandbox, dispatcher, client, store, fakeSessionData, sdkDriver;
var contact;
--- a/browser/components/loop/test/shared/index.html
+++ b/browser/components/loop/test/shared/index.html
@@ -38,28 +38,30 @@
<script src="../../content/shared/js/mixins.js"></script>
<script src="../../content/shared/js/views.js"></script>
<script src="../../content/shared/js/websocket.js"></script>
<script src="../../content/shared/js/feedbackApiClient.js"></script>
<script src="../../content/shared/js/validate.js"></script>
<script src="../../content/shared/js/actions.js"></script>
<script src="../../content/shared/js/dispatcher.js"></script>
<script src="../../content/shared/js/otSdkDriver.js"></script>
+ <script src="../../content/shared/js/localRoomStore.js"></script>
<script src="../../content/shared/js/conversationStore.js"></script>
<script src="../../content/shared/js/roomListStore.js"></script>
<!-- Test scripts -->
<script src="models_test.js"></script>
<script src="mixins_test.js"></script>
<script src="utils_test.js"></script>
<script src="views_test.js"></script>
<script src="websocket_test.js"></script>
<script src="feedbackApiClient_test.js"></script>
<script src="validate_test.js"></script>
<script src="dispatcher_test.js"></script>
+ <script src="localRoomStore_test.js"></script>
<script src="conversationStore_test.js"></script>
<script src="otSdkDriver_test.js"></script>
<script src="roomListStore_test.js"></script>
<script>
mocha.run(function () {
$("#mocha").append("<p id='complete'>Complete.</p>");
});
</script>
new file mode 100644
--- /dev/null
+++ b/browser/components/loop/test/shared/localRoomStore_test.js
@@ -0,0 +1,110 @@
+/* global chai */
+
+var expect = chai.expect;
+var sharedActions = loop.shared.actions;
+
+describe("loop.store.LocalRoomStore", function () {
+ "use strict";
+
+ var sandbox, dispatcher;
+
+ beforeEach(function() {
+ sandbox = sinon.sandbox.create();
+ dispatcher = new loop.Dispatcher();
+ });
+
+ afterEach(function() {
+ sandbox.restore();
+ });
+
+ describe("#constructor", function() {
+ it("should throw an error if the dispatcher is missing", function() {
+ expect(function() {
+ new loop.store.LocalRoomStore({mozLoop: {}});
+ }).to.Throw(/dispatcher/);
+ });
+
+ it("should throw an error if mozLoop is missing", function() {
+ expect(function() {
+ new loop.store.LocalRoomStore({dispatcher: dispatcher});
+ }).to.Throw(/mozLoop/);
+ });
+ });
+
+ describe("#setupEmptyRoom", function() {
+ var store, fakeMozLoop, fakeRoomId, fakeRoomName;
+
+ beforeEach(function() {
+ fakeRoomId = "337-ff-54";
+ fakeRoomName = "Monkeys";
+ fakeMozLoop = {
+ rooms: { getRoomData: sandbox.stub() }
+ };
+
+ store = new loop.store.LocalRoomStore(
+ {mozLoop: fakeMozLoop, dispatcher: dispatcher});
+ fakeMozLoop.rooms.getRoomData.
+ withArgs(fakeRoomId).
+ callsArgOnWith(1, // index of callback argument
+ store, // |this| to call it on
+ null, // args to call the callback with...
+ {roomName: fakeRoomName}
+ );
+ });
+
+ it("should trigger a change event", function(done) {
+ store.on("change", function() {
+ done();
+ });
+
+ dispatcher.dispatch(new sharedActions.SetupEmptyRoom(
+ {localRoomId: fakeRoomId}));
+ });
+
+ it("should set localRoomId on the store from the action data",
+ function(done) {
+
+ store.once("change", function () {
+ expect(store.getStoreState()).
+ to.have.property('localRoomId', fakeRoomId);
+ done();
+ });
+
+ dispatcher.dispatch(
+ new sharedActions.SetupEmptyRoom({localRoomId: fakeRoomId}));
+ });
+
+ it("should set serverData.roomName from the getRoomData callback",
+ function(done) {
+
+ store.once("change", function () {
+ expect(store.getStoreState()).to.have.deep.property(
+ 'serverData.roomName', fakeRoomName);
+ done();
+ });
+
+ dispatcher.dispatch(
+ new sharedActions.SetupEmptyRoom({localRoomId: fakeRoomId}));
+ });
+
+ it("should set error on the store when getRoomData calls back an error",
+ function(done) {
+
+ var fakeError = new Error("fake error");
+ fakeMozLoop.rooms.getRoomData.
+ withArgs(fakeRoomId).
+ callsArgOnWith(1, // index of callback argument
+ store, // |this| to call it on
+ fakeError); // args to call the callback with...
+
+ store.once("change", function() {
+ expect(this.getStoreState()).to.have.property('error', fakeError);
+ done();
+ });
+
+ dispatcher.dispatch(
+ new sharedActions.SetupEmptyRoom({localRoomId: fakeRoomId}));
+ });
+
+ });
+});
--- a/browser/components/loop/test/shared/mixins_test.js
+++ b/browser/components/loop/test/shared/mixins_test.js
@@ -33,16 +33,20 @@ describe("loop.shared.mixins", function(
onDocumentHidden: onDocumentHiddenStub,
onDocumentVisible: onDocumentVisibleStub,
render: function() {
return React.DOM.div();
}
});
});
+ afterEach(function() {
+ loop.shared.mixins.setRootObject(window);
+ });
+
function setupFakeVisibilityEventDispatcher(event) {
loop.shared.mixins.setRootObject({
document: {
addEventListener: function(_, fn) {
fn(event);
},
removeEventListener: sandbox.stub()
}
--- a/browser/components/loop/ui/index.html
+++ b/browser/components/loop/ui/index.html
@@ -37,16 +37,17 @@
<script src="../content/shared/js/models.js"></script>
<script src="../content/shared/js/mixins.js"></script>
<script src="../content/shared/js/views.js"></script>
<script src="../content/shared/js/websocket.js"></script>
<script src="../content/shared/js/validate.js"></script>
<script src="../content/shared/js/dispatcher.js"></script>
<script src="../content/shared/js/conversationStore.js"></script>
<script src="../content/shared/js/roomListStore.js"></script>
+ <script src="../content/js/roomViews.js"></script>
<script src="../content/js/conversationViews.js"></script>
<script src="../content/js/client.js"></script>
<script src="../standalone/content/js/webapp.js"></script>
<script type="text/javascript;version=1.8" src="../content/js/contacts.js"></script>
<script>
if (!loop.contacts) {
// For browsers that don't support ES6 without special flags (all but Fx
// at the moment), we shim the contacts namespace with its most barebone