--- a/browser/components/loop/content/shared/js/actions.js
+++ b/browser/components/loop/content/shared/js/actions.js
@@ -268,11 +268,17 @@ loop.shared.actions = (function() {
*
* @see https://wiki.mozilla.org/Loop/Architecture/Rooms#Joining_a_Room
*/
JoinedRoom: Action.define("joinedRoom", {
apiKey: String,
sessionToken: String,
sessionId: String,
expires: Number
+ }),
+
+ /**
+ * Used to indicate the user wishes to leave the room.
+ */
+ LeaveRoom: Action.define("leaveRoom", {
})
};
})();
--- a/browser/components/loop/content/shared/js/activeRoomStore.js
+++ b/browser/components/loop/content/shared/js/activeRoomStore.js
@@ -47,20 +47,22 @@ loop.store.ActiveRoomStore = (function()
if (!options.mozLoop) {
throw new Error("Missing option mozLoop");
}
this._mozLoop = options.mozLoop;
this._dispatcher.register(this, [
"roomFailure",
"setupWindowData",
+ "fetchServerData",
"updateRoomInfo",
"joinRoom",
"joinedRoom",
- "windowUnload"
+ "windowUnload",
+ "leaveRoom"
]);
/**
* Stored data reflecting the local state of a given room, used to drive
* the room's views.
*
* @see https://wiki.mozilla.org/Loop/Architecture/Rooms#GET_.2Frooms.2F.7Btoken.7D
* for the main data. Additional properties below.
@@ -147,16 +149,36 @@ loop.store.ActiveRoomStore = (function()
// For the conversation window, we need to automatically
// join the room.
this._dispatcher.dispatch(new sharedActions.JoinRoom());
}.bind(this));
},
/**
+ * Execute fetchServerData event action from the dispatcher. Although
+ * this is to fetch the server data - for rooms on the standalone client,
+ * we don't actually need to get any data. Therefore we just save the
+ * data that is given to us for when the user chooses to join the room.
+ *
+ * @param {sharedActions.FetchServerData} actionData
+ */
+ fetchServerData: function(actionData) {
+ if (actionData.windowType !== "room") {
+ // Nothing for us to do here, leave it to other stores.
+ return;
+ }
+
+ this.setStoreState({
+ roomToken: actionData.token,
+ roomState: ROOM_STATES.READY
+ });
+ },
+
+ /**
* Handles the updateRoomInfo action. Updates the room data and
* sets the state to `READY`.
*
* @param {sharedActions.UpdateRoomInfo} actionData
*/
updateRoomInfo: function(actionData) {
this.setStoreState({
roomName: actionData.roomName,
@@ -209,16 +231,23 @@ loop.store.ActiveRoomStore = (function()
/**
* Handles the window being unloaded. Ensures the room is left.
*/
windowUnload: function() {
this._leaveRoom();
},
/**
+ * Handles a room being left.
+ */
+ leaveRoom: function() {
+ this._leaveRoom();
+ },
+
+ /**
* Handles setting of the refresh timeout callback.
*
* @param {Integer} expireTime The time until expiry (in seconds).
*/
_setRefreshTimeout: function(expireTime) {
this._timeout = setTimeout(this._refreshMembership.bind(this),
expireTime * this.expiresTimeFactor * 1000);
},
--- a/browser/components/loop/standalone/content/index.html
+++ b/browser/components/loop/standalone/content/index.html
@@ -94,16 +94,17 @@
<script type="text/javascript" src="shared/js/feedbackApiClient.js"></script>
<script type="text/javascript" src="shared/js/actions.js"></script>
<script type="text/javascript" src="shared/js/validate.js"></script>
<script type="text/javascript" src="shared/js/dispatcher.js"></script>
<script type="text/javascript" src="shared/js/websocket.js"></script>
<script type="text/javascript" src="shared/js/activeRoomStore.js"></script>
<script type="text/javascript" src="js/standaloneAppStore.js"></script>
<script type="text/javascript" src="js/standaloneClient.js"></script>
+ <script type="text/javascript" src="js/standaloneMozLoop.js"></script>
<script type="text/javascript" src="js/standaloneRoomViews.js"></script>
<script type="text/javascript" src="js/webapp.js"></script>
<script>
// Wait for all the localization notes to load
window.addEventListener('localized', function() {
loop.webapp.init();
}, false);
new file mode 100644
--- /dev/null
+++ b/browser/components/loop/standalone/content/js/standaloneMozLoop.js
@@ -0,0 +1,188 @@
+/* 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 */
+
+/**
+ * The StandaloneMozLoop implementation reflects that of the mozLoop API for Loop
+ * in the desktop code. Not all functions are implemented.
+ */
+var loop = loop || {};
+loop.StandaloneMozLoop = (function(mozL10n) {
+ "use strict";
+
+ /**
+ * The maximum number of clients that we currently support.
+ */
+ var ROOM_MAX_CLIENTS = 2;
+
+
+ /**
+ * Validates a data object to confirm it has the specified properties.
+ *
+ * @param {Object} data The data object to verify
+ * @param {Array} schema The validation schema
+ * @return Returns all properties if valid, or an empty object if no properties
+ * have been specified.
+ */
+ function validate(data, schema) {
+ if (!schema) {
+ return {};
+ }
+
+ return new loop.validate.Validator(schema).validate(data);
+ }
+
+ /**
+ * Generic handler for XHR failures.
+ *
+ * @param {Function} callback Callback(err)
+ * @param jqXHR See jQuery docs
+ * @param textStatus See jQuery docs
+ * @param errorThrown See jQuery docs
+ */
+ function failureHandler(callback, jqXHR, textStatus, errorThrown) {
+ var jsonErr = jqXHR && jqXHR.responseJSON || {};
+ var message = "HTTP " + jqXHR.status + " " + errorThrown;
+
+ // Create an error with server error `errno` code attached as a property
+ var err = new Error(message);
+ err.errno = jsonErr.errno;
+
+ callback(err);
+ }
+
+ /**
+ * StandaloneMozLoopRooms is used as part of StandaloneMozLoop to define
+ * the rooms sub-object. We do it this way so that we can share the options
+ * information from the parent.
+ */
+ var StandaloneMozLoopRooms = function(options) {
+ options = options || {};
+ if (!options.baseServerUrl) {
+ throw new Error("missing required baseServerUrl");
+ }
+
+ this._baseServerUrl = options.baseServerUrl;
+ };
+
+ StandaloneMozLoopRooms.prototype = {
+ /**
+ * Internal function to actually perform a post to a room.
+ *
+ * @param {String} roomToken The rom token.
+ * @param {String} sessionToken The sessionToken for the room if known
+ * @param {Object} roomData The data to send with the request
+ * @param {Array} expectedProps The expected properties we should receive from the
+ * server
+ * @param {Function} callback The callback for when the request completes. The
+ * first parameter is non-null on error, the second parameter
+ * is the response data.
+ */
+ _postToRoom: function(roomToken, sessionToken, roomData, expectedProps, callback) {
+ var req = $.ajax({
+ url: this._baseServerUrl + "/rooms/" + roomToken,
+ method: "POST",
+ contentType: "application/json",
+ dataType: "json",
+ data: JSON.stringify(roomData),
+ beforeSend: function(xhr) {
+ if (sessionToken) {
+ xhr.setRequestHeader("Authorization", "Basic " + btoa(sessionToken));
+ }
+ }
+ });
+
+ req.done(function(responseData) {
+ try {
+ callback(null, validate(responseData, expectedProps));
+ } catch (err) {
+ console.error("Error requesting call info", err.message);
+ callback(err);
+ }
+ }.bind(this));
+
+ req.fail(failureHandler.bind(this, callback));
+ },
+
+ /**
+ * Joins a room
+ *
+ * @param {String} roomToken The room token.
+ * @param {Function} callback Function that will be invoked once the operation
+ * finished. The first argument passed will be an
+ * `Error` object or `null`.
+ */
+ join: function(roomToken, callback) {
+ this._postToRoom(roomToken, null, {
+ action: "join",
+ displayName: mozL10n.get("rooms_display_name_guest"),
+ clientMaxSize: ROOM_MAX_CLIENTS
+ }, {
+ apiKey: String,
+ sessionId: String,
+ sessionToken: String,
+ expires: Number
+ }, callback);
+ },
+
+ /**
+ * Refreshes a room
+ *
+ * @param {String} roomToken The room token.
+ * @param {String} sessionToken The session token for the session that has been
+ * joined
+ * @param {Function} callback Function that will be invoked once the operation
+ * finished. The first argument passed will be an
+ * `Error` object or `null`.
+ */
+ refreshMembership: function(roomToken, sessionToken, callback) {
+ this._postToRoom(roomToken, sessionToken, {
+ action: "refresh",
+ sessionToken: sessionToken
+ }, {
+ expires: Number
+ }, callback);
+ },
+
+ /**
+ * Leaves a room. Although this is an sync function, no data is returned
+ * from the server.
+ *
+ * @param {String} roomToken The room token.
+ * @param {String} sessionToken The session token for the session that has been
+ * joined
+ * @param {Function} callback Optional. Function that will be invoked once the operation
+ * finished. The first argument passed will be an
+ * `Error` object or `null`.
+ */
+ leave: function(roomToken, sessionToken, callback) {
+ if (!callback) {
+ callback = function(error) {
+ if (error) {
+ console.error(error);
+ }
+ };
+ }
+
+ this._postToRoom(roomToken, sessionToken, {
+ action: "leave",
+ sessionToken: sessionToken
+ }, null, callback);
+ }
+ };
+
+ var StandaloneMozLoop = function(options) {
+ options = options || {};
+ if (!options.baseServerUrl) {
+ throw new Error("missing required baseServerUrl");
+ }
+
+ this._baseServerUrl = options.baseServerUrl;
+
+ this.rooms = new StandaloneMozLoopRooms(options);
+ };
+
+ return StandaloneMozLoop;
+})(navigator.mozL10n);
--- a/browser/components/loop/standalone/content/js/standaloneRoomViews.js
+++ b/browser/components/loop/standalone/content/js/standaloneRoomViews.js
@@ -5,22 +5,26 @@
* You can obtain one at http://mozilla.org/MPL/2.0/. */
/* global loop:true, React */
var loop = loop || {};
loop.standaloneRoomViews = (function() {
"use strict";
+ var ROOM_STATES = loop.store.ROOM_STATES;
+ var sharedActions = loop.shared.actions;
+
var StandaloneRoomView = React.createClass({displayName: 'StandaloneRoomView',
mixins: [Backbone.Events],
propTypes: {
activeRoomStore:
- React.PropTypes.instanceOf(loop.store.ActiveRoomStore).isRequired
+ React.PropTypes.instanceOf(loop.store.ActiveRoomStore).isRequired,
+ dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
},
getInitialState: function() {
return this.props.activeRoomStore.getStoreState();
},
componentWillMount: function() {
this.listenTo(this.props.activeRoomStore, "change",
@@ -36,21 +40,43 @@ loop.standaloneRoomViews = (function() {
_onActiveRoomStateChanged: function() {
this.setState(this.props.activeRoomStore.getStoreState());
},
componentWillUnmount: function() {
this.stopListening(this.props.activeRoomStore);
},
+ joinRoom: function() {
+ this.props.dispatcher.dispatch(new sharedActions.JoinRoom());
+ },
+
+ leaveRoom: function() {
+ this.props.dispatcher.dispatch(new sharedActions.LeaveRoom());
+ },
+
+ // XXX Implement tests for this view when we do the proper views
+ // - bug 1074705 and others
render: function() {
- return (
- React.DOM.div(null,
- React.DOM.div(null, this.state.roomState)
- )
- );
+ switch(this.state.roomState) {
+ case ROOM_STATES.READY: {
+ return (
+ React.DOM.div(null, React.DOM.button({onClick: this.joinRoom}, "Join"))
+ );
+ }
+ case ROOM_STATES.JOINED: {
+ return (
+ React.DOM.div(null, React.DOM.button({onClick: this.leaveRoom}, "Leave"))
+ );
+ }
+ default: {
+ return (
+ React.DOM.div(null, this.state.roomState)
+ );
+ }
+ }
}
});
return {
StandaloneRoomView: StandaloneRoomView
};
})();
--- a/browser/components/loop/standalone/content/js/standaloneRoomViews.jsx
+++ b/browser/components/loop/standalone/content/js/standaloneRoomViews.jsx
@@ -5,22 +5,26 @@
* You can obtain one at http://mozilla.org/MPL/2.0/. */
/* global loop:true, React */
var loop = loop || {};
loop.standaloneRoomViews = (function() {
"use strict";
+ var ROOM_STATES = loop.store.ROOM_STATES;
+ var sharedActions = loop.shared.actions;
+
var StandaloneRoomView = React.createClass({
mixins: [Backbone.Events],
propTypes: {
activeRoomStore:
- React.PropTypes.instanceOf(loop.store.ActiveRoomStore).isRequired
+ React.PropTypes.instanceOf(loop.store.ActiveRoomStore).isRequired,
+ dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
},
getInitialState: function() {
return this.props.activeRoomStore.getStoreState();
},
componentWillMount: function() {
this.listenTo(this.props.activeRoomStore, "change",
@@ -36,21 +40,43 @@ loop.standaloneRoomViews = (function() {
_onActiveRoomStateChanged: function() {
this.setState(this.props.activeRoomStore.getStoreState());
},
componentWillUnmount: function() {
this.stopListening(this.props.activeRoomStore);
},
+ joinRoom: function() {
+ this.props.dispatcher.dispatch(new sharedActions.JoinRoom());
+ },
+
+ leaveRoom: function() {
+ this.props.dispatcher.dispatch(new sharedActions.LeaveRoom());
+ },
+
+ // XXX Implement tests for this view when we do the proper views
+ // - bug 1074705 and others
render: function() {
- return (
- <div>
- <div>{this.state.roomState}</div>
- </div>
- );
+ switch(this.state.roomState) {
+ case ROOM_STATES.READY: {
+ return (
+ <div><button onClick={this.joinRoom}>Join</button></div>
+ );
+ }
+ case ROOM_STATES.JOINED: {
+ return (
+ <div><button onClick={this.leaveRoom}>Leave</button></div>
+ );
+ }
+ default: {
+ return (
+ <div>{this.state.roomState}</div>
+ );
+ }
+ }
}
});
return {
StandaloneRoomView: StandaloneRoomView
};
})();
--- a/browser/components/loop/standalone/content/js/webapp.js
+++ b/browser/components/loop/standalone/content/js/webapp.js
@@ -888,17 +888,18 @@ loop.webapp = (function($, _, OT, mozL10
.isRequired,
sdk: React.PropTypes.object.isRequired,
feedbackApiClient: React.PropTypes.object.isRequired,
// XXX New types for flux style
standaloneAppStore: React.PropTypes.instanceOf(
loop.store.StandaloneAppStore).isRequired,
activeRoomStore: React.PropTypes.instanceOf(
- loop.store.ActiveRoomStore).isRequired
+ loop.store.ActiveRoomStore).isRequired,
+ dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired
},
getInitialState: function() {
return this.props.standaloneAppStore.getStoreState();
},
componentWillMount: function() {
this.listenTo(this.props.standaloneAppStore, "change", function() {
@@ -932,17 +933,18 @@ loop.webapp = (function($, _, OT, mozL10
sdk: this.props.sdk,
feedbackApiClient: this.props.feedbackApiClient}
)
);
}
case "room": {
return (
loop.standaloneRoomViews.StandaloneRoomView({
- activeRoomStore: this.props.activeRoomStore}
+ activeRoomStore: this.props.activeRoomStore,
+ dispatcher: this.props.dispatcher}
)
);
}
case "home": {
return HomeView(null);
}
default: {
// The state hasn't been initialised yet, so don't display
@@ -953,16 +955,19 @@ loop.webapp = (function($, _, OT, mozL10
}
});
/**
* App initialization.
*/
function init() {
var helper = new sharedUtils.Helper();
+ var standaloneMozLoop = new loop.StandaloneMozLoop({
+ baseServerUrl: loop.config.serverUrl
+ });
// Older non-flux based items.
var notifications = new sharedModels.NotificationCollection();
var conversation
if (helper.isFirefoxOS(navigator.userAgent)) {
conversation = new FxOSConversationModel();
} else {
conversation = new sharedModels.ConversationModel({}, {
@@ -986,30 +991,33 @@ loop.webapp = (function($, _, OT, mozL10
var standaloneAppStore = new loop.store.StandaloneAppStore({
conversation: conversation,
dispatcher: dispatcher,
helper: helper,
sdk: OT
});
var activeRoomStore = new loop.store.ActiveRoomStore({
dispatcher: dispatcher,
- // XXX Bug 1074702 will introduce a mozLoop compatible object for
- // the standalone rooms.
- mozLoop: {}
+ mozLoop: standaloneMozLoop
+ });
+
+ window.addEventListener("unload", function() {
+ dispatcher.dispatch(new sharedActions.WindowUnload());
});
React.renderComponent(WebappRootView({
client: client,
conversation: conversation,
helper: helper,
notifications: notifications,
sdk: OT,
feedbackApiClient: feedbackApiClient,
standaloneAppStore: standaloneAppStore,
- activeRoomStore: activeRoomStore}
+ activeRoomStore: activeRoomStore,
+ dispatcher: dispatcher}
), document.querySelector("#main"));
// Set the 'lang' and 'dir' attributes to <html> when the page is translated
document.documentElement.lang = mozL10n.language.code;
document.documentElement.dir = mozL10n.language.direction;
document.title = mozL10n.get("clientShortname2");
dispatcher.dispatch(new sharedActions.ExtractTokenInfo({
--- a/browser/components/loop/standalone/content/js/webapp.jsx
+++ b/browser/components/loop/standalone/content/js/webapp.jsx
@@ -888,17 +888,18 @@ loop.webapp = (function($, _, OT, mozL10
.isRequired,
sdk: React.PropTypes.object.isRequired,
feedbackApiClient: React.PropTypes.object.isRequired,
// XXX New types for flux style
standaloneAppStore: React.PropTypes.instanceOf(
loop.store.StandaloneAppStore).isRequired,
activeRoomStore: React.PropTypes.instanceOf(
- loop.store.ActiveRoomStore).isRequired
+ loop.store.ActiveRoomStore).isRequired,
+ dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired
},
getInitialState: function() {
return this.props.standaloneAppStore.getStoreState();
},
componentWillMount: function() {
this.listenTo(this.props.standaloneAppStore, "change", function() {
@@ -933,16 +934,17 @@ loop.webapp = (function($, _, OT, mozL10
feedbackApiClient={this.props.feedbackApiClient}
/>
);
}
case "room": {
return (
<loop.standaloneRoomViews.StandaloneRoomView
activeRoomStore={this.props.activeRoomStore}
+ dispatcher={this.props.dispatcher}
/>
);
}
case "home": {
return <HomeView />;
}
default: {
// The state hasn't been initialised yet, so don't display
@@ -953,16 +955,19 @@ loop.webapp = (function($, _, OT, mozL10
}
});
/**
* App initialization.
*/
function init() {
var helper = new sharedUtils.Helper();
+ var standaloneMozLoop = new loop.StandaloneMozLoop({
+ baseServerUrl: loop.config.serverUrl
+ });
// Older non-flux based items.
var notifications = new sharedModels.NotificationCollection();
var conversation
if (helper.isFirefoxOS(navigator.userAgent)) {
conversation = new FxOSConversationModel();
} else {
conversation = new sharedModels.ConversationModel({}, {
@@ -986,30 +991,33 @@ loop.webapp = (function($, _, OT, mozL10
var standaloneAppStore = new loop.store.StandaloneAppStore({
conversation: conversation,
dispatcher: dispatcher,
helper: helper,
sdk: OT
});
var activeRoomStore = new loop.store.ActiveRoomStore({
dispatcher: dispatcher,
- // XXX Bug 1074702 will introduce a mozLoop compatible object for
- // the standalone rooms.
- mozLoop: {}
+ mozLoop: standaloneMozLoop
+ });
+
+ window.addEventListener("unload", function() {
+ dispatcher.dispatch(new sharedActions.WindowUnload());
});
React.renderComponent(<WebappRootView
client={client}
conversation={conversation}
helper={helper}
notifications={notifications}
sdk={OT}
feedbackApiClient={feedbackApiClient}
standaloneAppStore={standaloneAppStore}
activeRoomStore={activeRoomStore}
+ dispatcher={dispatcher}
/>, document.querySelector("#main"));
// Set the 'lang' and 'dir' attributes to <html> when the page is translated
document.documentElement.lang = mozL10n.language.code;
document.documentElement.dir = mozL10n.language.direction;
document.title = mozL10n.get("clientShortname2");
dispatcher.dispatch(new sharedActions.ExtractTokenInfo({
--- a/browser/components/loop/standalone/content/l10n/loop.en-US.properties
+++ b/browser/components/loop/standalone/content/l10n/loop.en-US.properties
@@ -110,16 +110,17 @@ rooms_name_this_room_label=Name this con
rooms_new_room_button_label=Start a conversation
rooms_only_occupant_label=You're the first one here.
rooms_panel_title=Choose a conversation or start a new one
rooms_room_full_label=There are already two people in this conversation.
rooms_room_full_call_to_action_nonFx_label=Download {{brandShortname}} to start your own
rooms_room_full_call_to_action_label=Learn more about {{clientShortname}} »
rooms_room_joined_label=Someone has joined the conversation!
rooms_room_join_label=Join the conversation
+rooms_display_name_guest=Guest
## LOCALIZATION_NOTE(standalone_title_with_status): {{clientShortname}} will be
## replaced by the brand name and {{currentStatus}} will be replaced
## by the current call status (Connecting, Ringing, etc.)
standalone_title_with_status={{clientShortname}} — {{currentStatus}}
status_in_conversation=In conversation
status_conversation_ended=Conversation ended
status_error=Something went wrong
--- a/browser/components/loop/test/shared/activeRoomStore_test.js
+++ b/browser/components/loop/test/shared/activeRoomStore_test.js
@@ -154,36 +154,56 @@ describe("loop.store.ActiveRoomStore", f
sinon.assert.calledOnce(dispatcher.dispatch);
sinon.assert.calledWithExactly(dispatcher.dispatch,
new sharedActions.RoomFailure({
error: fakeError
}));
});
});
+ describe("#fetchServerData", function() {
+ it("should save the token", function() {
+ store.fetchServerData(new sharedActions.FetchServerData({
+ windowType: "room",
+ token: "fakeToken"
+ }));
+
+ expect(store.getStoreState().roomToken).eql("fakeToken");
+ });
+
+ it("should set the state to `READY`", function() {
+ store.fetchServerData(new sharedActions.FetchServerData({
+ windowType: "room",
+ token: "fakeToken"
+ }));
+
+ expect(store.getStoreState().roomState).eql(ROOM_STATES.READY);
+ });
+ });
+
describe("#updateRoomInfo", function() {
var fakeRoomInfo;
beforeEach(function() {
fakeRoomInfo = {
roomName: "Its a room",
roomOwner: "Me",
roomToken: "fakeToken",
roomUrl: "http://invalid"
};
});
it("should set the state to READY", function() {
- store.updateRoomInfo(fakeRoomInfo);
+ store.updateRoomInfo(new sharedActions.UpdateRoomInfo(fakeRoomInfo));
expect(store._storeState.roomState).eql(ROOM_STATES.READY);
});
it("should save the room information", function() {
- store.updateRoomInfo(fakeRoomInfo);
+ store.updateRoomInfo(new sharedActions.UpdateRoomInfo(fakeRoomInfo));
var state = store.getStoreState();
expect(state.roomName).eql(fakeRoomInfo.roomName);
expect(state.roomOwner).eql(fakeRoomInfo.roomOwner);
expect(state.roomToken).eql(fakeRoomInfo.roomToken);
expect(state.roomUrl).eql(fakeRoomInfo.roomUrl);
});
});
@@ -242,47 +262,47 @@ describe("loop.store.ActiveRoomStore", f
};
store.setStoreState({
roomToken: "fakeToken"
});
});
it("should set the state to `JOINED`", function() {
- store.joinedRoom(fakeJoinedData);
+ store.joinedRoom(new sharedActions.JoinedRoom(fakeJoinedData));
expect(store._storeState.roomState).eql(ROOM_STATES.JOINED);
});
it("should store the session and api values", function() {
- store.joinedRoom(fakeJoinedData);
+ store.joinedRoom(new sharedActions.JoinedRoom(fakeJoinedData));
var state = store.getStoreState();
expect(state.apiKey).eql(fakeJoinedData.apiKey);
expect(state.sessionToken).eql(fakeJoinedData.sessionToken);
expect(state.sessionId).eql(fakeJoinedData.sessionId);
});
it("should call mozLoop.rooms.refreshMembership before the expiresTime",
function() {
- store.joinedRoom(fakeJoinedData);
+ store.joinedRoom(new sharedActions.JoinedRoom(fakeJoinedData));
sandbox.clock.tick(fakeJoinedData.expires * 1000);
sinon.assert.calledOnce(fakeMozLoop.rooms.refreshMembership);
sinon.assert.calledWith(fakeMozLoop.rooms.refreshMembership,
"fakeToken", "12563478");
});
it("should call mozLoop.rooms.refreshMembership before the next expiresTime",
function() {
fakeMozLoop.rooms.refreshMembership.callsArgWith(2,
null, {expires: 40});
- store.joinedRoom(fakeJoinedData);
+ store.joinedRoom(new sharedActions.JoinedRoom(fakeJoinedData));
// Clock tick for the first expiry time (which
// sets up the refreshMembership).
sandbox.clock.tick(fakeJoinedData.expires * 1000);
// Clock tick for expiry time in the refresh membership response.
sandbox.clock.tick(40000);
@@ -291,17 +311,17 @@ describe("loop.store.ActiveRoomStore", f
"fakeToken", "12563478");
});
it("should dispatch `RoomFailure` if the refreshMembership call failed",
function() {
var fakeError = new Error("fake");
fakeMozLoop.rooms.refreshMembership.callsArgWith(2, fakeError);
- store.joinedRoom(fakeJoinedData);
+ store.joinedRoom(new sharedActions.JoinedRoom(fakeJoinedData));
// Clock tick for the first expiry time (which
// sets up the refreshMembership).
sandbox.clock.tick(fakeJoinedData.expires * 1000);
sinon.assert.calledOnce(dispatcher.dispatch);
sinon.assert.calledWith(dispatcher.dispatch,
new sharedActions.RoomFailure({
@@ -337,9 +357,42 @@ describe("loop.store.ActiveRoomStore", f
});
it("should set the state to ready", function() {
store.windowUnload();
expect(store._storeState.roomState).eql(ROOM_STATES.READY);
});
});
+
+ describe("#leaveRoom", function() {
+ beforeEach(function() {
+ store.setStoreState({
+ roomState: ROOM_STATES.JOINED,
+ roomToken: "fakeToken",
+ sessionToken: "1627384950"
+ });
+ });
+
+ it("should clear any existing timeout", function() {
+ sandbox.stub(window, "clearTimeout");
+ store._timeout = {};
+
+ store.leaveRoom();
+
+ sinon.assert.calledOnce(clearTimeout);
+ });
+
+ it("should call mozLoop.rooms.leave", function() {
+ store.leaveRoom();
+
+ sinon.assert.calledOnce(fakeMozLoop.rooms.leave);
+ sinon.assert.calledWithExactly(fakeMozLoop.rooms.leave,
+ "fakeToken", "1627384950");
+ });
+
+ it("should set the state to ready", function() {
+ store.leaveRoom();
+
+ expect(store._storeState.roomState).eql(ROOM_STATES.READY);
+ });
+ });
});
--- a/browser/components/loop/test/standalone/index.html
+++ b/browser/components/loop/test/standalone/index.html
@@ -39,21 +39,23 @@
<script src="../../content/shared/js/feedbackApiClient.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/activeRoomStore.js"></script>
<script src="../../standalone/content/js/multiplexGum.js"></script>
<script src="../../standalone/content/js/standaloneAppStore.js"></script>
<script src="../../standalone/content/js/standaloneClient.js"></script>
+ <script src="../../standalone/content/js/standaloneMozLoop.js"></script>
<script src="../../standalone/content/js/standaloneRoomViews.js"></script>
<script src="../../standalone/content/js/webapp.js"></script>
- <!-- Test scripts -->
+ <!-- Test scripts -->
<script src="standalone_client_test.js"></script>
<script src="standaloneAppStore_test.js"></script>
+ <script src="standaloneMozLoop_test.js"></script>
<script src="webapp_test.js"></script>
<script src="multiplexGum_test.js"></script>
<script>
mocha.run(function () {
$("#mocha").append("<p id='complete'>Complete.</p>");
});
</script>
</body>
new file mode 100644
--- /dev/null
+++ b/browser/components/loop/test/standalone/standaloneMozLoop_test.js
@@ -0,0 +1,178 @@
+/* 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 expect = chai.expect;
+
+describe("loop.StandaloneMozLoop", function() {
+ "use strict";
+
+ var sandbox, fakeXHR, requests, callback, mozLoop;
+ var fakeToken, fakeBaseServerUrl, fakeServerErrorDescription;
+
+ beforeEach(function() {
+ sandbox = sinon.sandbox.create();
+ fakeXHR = sandbox.useFakeXMLHttpRequest();
+ requests = [];
+ // https://github.com/cjohansen/Sinon.JS/issues/393
+ fakeXHR.xhr.onCreate = function (xhr) {
+ requests.push(xhr);
+ };
+ fakeBaseServerUrl = "http://fake.api";
+ fakeServerErrorDescription = {
+ code: 401,
+ errno: 101,
+ error: "error",
+ message: "invalid token",
+ info: "error info"
+ };
+
+ callback = sinon.spy();
+
+ mozLoop = new loop.StandaloneMozLoop({
+ baseServerUrl: fakeBaseServerUrl
+ });
+ });
+
+ afterEach(function() {
+ sandbox.restore();
+ });
+
+ describe("#constructor", function() {
+ it("should require a baseServerUrl setting", function() {
+ expect(function() {
+ new loop.StandaloneMozLoop();
+ }).to.Throw(Error, /required/);
+ });
+ });
+
+ describe("#rooms.join", function() {
+ it("should POST to the server", function() {
+ mozLoop.rooms.join("fakeToken", callback);
+
+ expect(requests).to.have.length.of(1);
+ expect(requests[0].url).eql(fakeBaseServerUrl + "/rooms/fakeToken");
+ expect(requests[0].method).eql("POST");
+
+ var requestData = JSON.parse(requests[0].requestBody);
+ expect(requestData.action).eql("join");
+ });
+
+ it("should call the callback with success parameters", function() {
+ mozLoop.rooms.join("fakeToken", callback);
+
+ var sessionData = {
+ apiKey: "12345",
+ sessionId: "54321",
+ sessionToken: "another token",
+ expires: 20
+ };
+
+ requests[0].respond(200, {"Content-Type": "application/json"},
+ JSON.stringify(sessionData));
+
+ sinon.assert.calledOnce(callback);
+ sinon.assert.calledWithExactly(callback, null, sessionData);
+ });
+
+ it("should call the callback with failure parameters", function() {
+ mozLoop.rooms.join("fakeToken", callback);
+
+ requests[0].respond(401, {"Content-Type": "application/json"},
+ JSON.stringify(fakeServerErrorDescription));
+ sinon.assert.calledWithMatch(callback, sinon.match(function(err) {
+ return /HTTP 401 Unauthorized/.test(err.message);
+ }));
+ });
+ });
+
+ describe("#rooms.refreshMembership", function() {
+ var mozLoop, fakeServerErrorDescription;
+
+ beforeEach(function() {
+ mozLoop = new loop.StandaloneMozLoop({
+ baseServerUrl: fakeBaseServerUrl
+ });
+
+ fakeServerErrorDescription = {
+ code: 401,
+ errno: 101,
+ error: "error",
+ message: "invalid token",
+ info: "error info"
+ };
+ });
+
+ it("should POST to the server", function() {
+ mozLoop.rooms.refreshMembership("fakeToken", "fakeSessionToken", callback);
+
+ expect(requests).to.have.length.of(1);
+ expect(requests[0].url).eql(fakeBaseServerUrl + "/rooms/fakeToken");
+ expect(requests[0].method).eql("POST");
+ expect(requests[0].requestHeaders.Authorization)
+ .eql("Basic " + btoa("fakeSessionToken"));
+
+ var requestData = JSON.parse(requests[0].requestBody);
+ expect(requestData.action).eql("refresh");
+ expect(requestData.sessionToken).eql("fakeSessionToken");
+ });
+
+ it("should call the callback with success parameters", function() {
+ mozLoop.rooms.refreshMembership("fakeToken", "fakeSessionToken", callback);
+
+ var responseData = {
+ expires: 20
+ };
+
+ requests[0].respond(200, {"Content-Type": "application/json"},
+ JSON.stringify(responseData));
+
+ sinon.assert.calledOnce(callback);
+ sinon.assert.calledWithExactly(callback, null, responseData);
+ });
+
+ it("should call the callback with failure parameters", function() {
+ mozLoop.rooms.refreshMembership("fakeToken", "fakeSessionToken", callback);
+
+ requests[0].respond(401, {"Content-Type": "application/json"},
+ JSON.stringify(fakeServerErrorDescription));
+ sinon.assert.calledWithMatch(callback, sinon.match(function(err) {
+ return /HTTP 401 Unauthorized/.test(err.message);
+ }));
+ });
+ });
+
+ describe("#rooms.leave", function() {
+ it("should POST to the server", function() {
+ mozLoop.rooms.leave("fakeToken", "fakeSessionToken", callback);
+
+ expect(requests).to.have.length.of(1);
+ expect(requests[0].url).eql(fakeBaseServerUrl + "/rooms/fakeToken");
+ expect(requests[0].method).eql("POST");
+ expect(requests[0].requestHeaders.Authorization)
+ .eql("Basic " + btoa("fakeSessionToken"));
+
+ var requestData = JSON.parse(requests[0].requestBody);
+ expect(requestData.action).eql("leave");
+ });
+
+ it("should call the callback with success parameters", function() {
+ mozLoop.rooms.leave("fakeToken", "fakeSessionToken", callback);
+
+ requests[0].respond(204);
+
+ sinon.assert.calledOnce(callback);
+ sinon.assert.calledWithExactly(callback, null, {});
+ });
+
+ it("should call the callback with failure parameters", function() {
+ mozLoop.rooms.leave("fakeToken", "fakeSessionToken", callback);
+
+ requests[0].respond(401, {"Content-Type": "application/json"},
+ JSON.stringify(fakeServerErrorDescription));
+ sinon.assert.calledWithMatch(callback, sinon.match(function(err) {
+ return /HTTP 401 Unauthorized/.test(err.message);
+ }));
+ });
+ });
+});