Bug 1147609 - Make Loop's standalone UI work with roomName as an unecrypted parameter or as an encrypted part of context. r=mikedeboer

This commit is contained in:
Mark Banner 2015-04-01 12:04:08 +01:00
parent 8769806b40
commit a68bae1728
15 changed files with 348 additions and 36 deletions

View File

@ -972,11 +972,14 @@ html, .fx-embedded, #main,
max-width: 400px; max-width: 400px;
} }
.standalone .room-conversation h2.room-name { .standalone .room-conversation h2.room-name,
.standalone .room-conversation h2.room-info-failure {
position: absolute; position: absolute;
display: inline-block; display: inline-block;
top: 0; top: 0;
right: 0; right: 10px;
/* 20px is 10px for left and right margins. */
width: calc(25% - 20px);
color: #fff; color: #fff;
z-index: 2000000; z-index: 2000000;
font-size: 1.2em; font-size: 1.2em;

View File

@ -43,7 +43,8 @@ loop.shared.actions = (function() {
* Extract the token information and type for the standalone window * Extract the token information and type for the standalone window
*/ */
ExtractTokenInfo: Action.define("extractTokenInfo", { ExtractTokenInfo: Action.define("extractTokenInfo", {
windowPath: String windowPath: String,
windowHash: String
}), }),
/** /**
@ -65,6 +66,7 @@ loop.shared.actions = (function() {
* token. * token.
*/ */
FetchServerData: Action.define("fetchServerData", { FetchServerData: Action.define("fetchServerData", {
// cryptoKey: String - Optional.
token: String, token: String,
windowType: String windowType: String
}), }),
@ -386,6 +388,7 @@ loop.shared.actions = (function() {
* @see https://wiki.mozilla.org/Loop/Architecture/Rooms#GET_.2Frooms.2F.7Btoken.7D * @see https://wiki.mozilla.org/Loop/Architecture/Rooms#GET_.2Frooms.2F.7Btoken.7D
*/ */
UpdateRoomInfo: Action.define("updateRoomInfo", { UpdateRoomInfo: Action.define("updateRoomInfo", {
// context: Object - Optional.
// roomName: String - Optional. // roomName: String - Optional.
roomOwner: String, roomOwner: String,
roomUrl: String roomUrl: String

View File

@ -11,6 +11,7 @@ loop.store.ActiveRoomStore = (function() {
"use strict"; "use strict";
var sharedActions = loop.shared.actions; var sharedActions = loop.shared.actions;
var crypto = loop.crypto;
var FAILURE_DETAILS = loop.shared.utils.FAILURE_DETAILS; var FAILURE_DETAILS = loop.shared.utils.FAILURE_DETAILS;
var SCREEN_SHARE_STATES = loop.shared.utils.SCREEN_SHARE_STATES; var SCREEN_SHARE_STATES = loop.shared.utils.SCREEN_SHARE_STATES;
@ -20,6 +21,8 @@ loop.store.ActiveRoomStore = (function() {
var ROOM_STATES = loop.store.ROOM_STATES; var ROOM_STATES = loop.store.ROOM_STATES;
var ROOM_INFO_FAILURES = loop.shared.utils.ROOM_INFO_FAILURES;
/** /**
* Active room store. * Active room store.
* *
@ -76,7 +79,11 @@ loop.store.ActiveRoomStore = (function() {
localVideoDimensions: {}, localVideoDimensions: {},
remoteVideoDimensions: {}, remoteVideoDimensions: {},
screenSharingState: SCREEN_SHARE_STATES.INACTIVE, screenSharingState: SCREEN_SHARE_STATES.INACTIVE,
receivingScreenShare: false receivingScreenShare: false,
// The roomCryptoKey to decode the context data if necessary.
roomCryptoKey: null,
// Room information failed to be obtained for a reason. See ROOM_INFO_FAILURES.
roomInfoFailure: null
}; };
}, },
@ -199,6 +206,7 @@ loop.store.ActiveRoomStore = (function() {
this.setStoreState({ this.setStoreState({
roomToken: actionData.token, roomToken: actionData.token,
roomCryptoKey: actionData.cryptoKey,
roomState: ROOM_STATES.READY roomState: ROOM_STATES.READY
}); });
@ -207,17 +215,64 @@ loop.store.ActiveRoomStore = (function() {
this._mozLoop.rooms.on("delete:" + actionData.roomToken, this._mozLoop.rooms.on("delete:" + actionData.roomToken,
this._handleRoomDelete.bind(this)); this._handleRoomDelete.bind(this));
this._mozLoop.rooms.get(this._storeState.roomToken, this._getRoomDataForStandalone();
function(err, result) { },
if (err) {
// XXX Bug 1110937 will want to handle the error results here
// e.g. room expired/invalid.
console.error("Failed to get room data:", err);
return;
}
this.dispatcher.dispatch(new sharedActions.UpdateRoomInfo(result)); _getRoomDataForStandalone: function() {
}.bind(this)); this._mozLoop.rooms.get(this._storeState.roomToken, function(err, result) {
if (err) {
// XXX Bug 1110937 will want to handle the error results here
// e.g. room expired/invalid.
console.error("Failed to get room data:", err);
return;
}
var roomInfoData = new sharedActions.UpdateRoomInfo({
roomOwner: result.roomOwner,
roomUrl: result.roomUrl
});
if (!result.context && !result.roomName) {
roomInfoData.roomInfoFailure = ROOM_INFO_FAILURES.NO_DATA;
this.dispatcher.dispatch(roomInfoData);
return;
}
// This handles 'legacy', non-encrypted room names.
if (result.roomName && !result.context) {
roomInfoData.roomName = result.roomName;
this.dispatcher.dispatch(roomInfoData);
return;
}
if (!crypto.isSupported()) {
roomInfoData.roomInfoFailure = ROOM_INFO_FAILURES.WEB_CRYPTO_UNSUPPORTED;
this.dispatcher.dispatch(roomInfoData);
return;
}
var roomCryptoKey = this.getStoreState("roomCryptoKey");
if (!roomCryptoKey) {
roomInfoData.roomInfoFailure = ROOM_INFO_FAILURES.NO_CRYPTO_KEY;
this.dispatcher.dispatch(roomInfoData);
return;
}
var dispatcher = this.dispatcher;
crypto.decryptBytes(roomCryptoKey, result.context.value)
.then(function(decryptedResult) {
var realResult = JSON.parse(decryptedResult);
roomInfoData.roomName = realResult.roomName;
dispatcher.dispatch(roomInfoData);
}, function(err) {
roomInfoData.roomInfoFailure = ROOM_INFO_FAILURES.DECRYPT_FAILED;
dispatcher.dispatch(roomInfoData);
});
}.bind(this));
}, },
/** /**
@ -254,6 +309,7 @@ loop.store.ActiveRoomStore = (function() {
*/ */
updateRoomInfo: function(actionData) { updateRoomInfo: function(actionData) {
this.setStoreState({ this.setStoreState({
roomInfoFailure: actionData.roomInfoFailure,
roomName: actionData.roomName, roomName: actionData.roomName,
roomOwner: actionData.roomOwner, roomOwner: actionData.roomOwner,
roomUrl: actionData.roomUrl roomUrl: actionData.roomUrl

View File

@ -43,6 +43,17 @@ loop.shared.utils = (function(mozL10n) {
UNKNOWN: "reason-unknown" UNKNOWN: "reason-unknown"
}; };
var ROOM_INFO_FAILURES = {
// There's no data available from the server.
NO_DATA: "no_data",
// WebCrypto is unsupported in this browser.
WEB_CRYPTO_UNSUPPORTED: "web_crypto_unsupported",
// The room is missing the crypto key information.
NO_CRYPTO_KEY: "no_crypto_key",
// Decryption failed.
DECRYPT_FAILED: "decrypt_failed"
};
var STREAM_PROPERTIES = { var STREAM_PROPERTIES = {
VIDEO_DIMENSIONS: "videoDimensions", VIDEO_DIMENSIONS: "videoDimensions",
HAS_AUDIO: "hasAudio", HAS_AUDIO: "hasAudio",
@ -397,6 +408,7 @@ loop.shared.utils = (function(mozL10n) {
WEBSOCKET_REASONS: WEBSOCKET_REASONS, WEBSOCKET_REASONS: WEBSOCKET_REASONS,
STREAM_PROPERTIES: STREAM_PROPERTIES, STREAM_PROPERTIES: STREAM_PROPERTIES,
SCREEN_SHARE_STATES: SCREEN_SHARE_STATES, SCREEN_SHARE_STATES: SCREEN_SHARE_STATES,
ROOM_INFO_FAILURES: ROOM_INFO_FAILURES,
composeCallUrlEmail: composeCallUrlEmail, composeCallUrlEmail: composeCallUrlEmail,
formatDate: formatDate, formatDate: formatDate,
getBoolPreference: getBoolPreference, getBoolPreference: getBoolPreference,

View File

@ -108,6 +108,7 @@
<!-- app scripts --> <!-- app scripts -->
<script type="text/javascript" src="config.js"></script> <script type="text/javascript" src="config.js"></script>
<script type="text/javascript" src="shared/js/utils.js"></script> <script type="text/javascript" src="shared/js/utils.js"></script>
<script type="text/javascript" src="shared/js/crypto.js"></script>
<script type="text/javascript" src="shared/js/models.js"></script> <script type="text/javascript" src="shared/js/models.js"></script>
<script type="text/javascript" src="shared/js/mixins.js"></script> <script type="text/javascript" src="shared/js/mixins.js"></script>
<script type="text/javascript" src="shared/js/feedbackApiClient.js"></script> <script type="text/javascript" src="shared/js/feedbackApiClient.js"></script>

View File

@ -95,9 +95,21 @@ loop.store.StandaloneAppStore = (function() {
return [windowType, match && match[1] ? match[1] : null]; return [windowType, match && match[1] ? match[1] : null];
}, },
/**
* Extracts the crypto key from the hash for the page.
*/
_extractCryptoKey: function(windowHash) {
if (windowHash && windowHash[0] === "#") {
return windowHash.substring(1, windowHash.length);
}
return null;
},
/** /**
* Handles the extract token info action - obtains the token information * Handles the extract token info action - obtains the token information
* and its type; updates the store and notifies interested components. * and its type; extracts any crypto information; updates the store and
* notifies interested components.
* *
* @param {sharedActions.GetWindowData} actionData The action data * @param {sharedActions.GetWindowData} actionData The action data
*/ */
@ -135,6 +147,7 @@ loop.store.StandaloneAppStore = (function() {
// it. // it.
if (token) { if (token) {
this._dispatcher.dispatch(new loop.shared.actions.FetchServerData({ this._dispatcher.dispatch(new loop.shared.actions.FetchServerData({
cryptoKey: this._extractCryptoKey(actionData.windowHash),
token: token, token: token,
windowType: windowType windowType: windowType
})); }));

View File

@ -12,6 +12,7 @@ loop.standaloneRoomViews = (function(mozL10n) {
"use strict"; "use strict";
var FAILURE_DETAILS = loop.shared.utils.FAILURE_DETAILS; var FAILURE_DETAILS = loop.shared.utils.FAILURE_DETAILS;
var ROOM_INFO_FAILURES = loop.shared.utils.ROOM_INFO_FAILURES;
var ROOM_STATES = loop.store.ROOM_STATES; var ROOM_STATES = loop.store.ROOM_STATES;
var sharedActions = loop.shared.actions; var sharedActions = loop.shared.actions;
var sharedMixins = loop.shared.mixins; var sharedMixins = loop.shared.mixins;
@ -198,6 +199,29 @@ loop.standaloneRoomViews = (function(mozL10n) {
} }
}); });
var StandaloneRoomContextView = React.createClass({displayName: "StandaloneRoomContextView",
propTypes: {
roomName: React.PropTypes.string,
roomInfoFailure: React.PropTypes.string
},
render: function() {
if (this.props.roomInfoFailure === ROOM_INFO_FAILURES.WEB_CRYPTO_UNSUPPORTED) {
return (React.createElement("h2", {className: "room-info-failure"},
mozL10n.get("room_information_failure_unsupported_browser")
));
} else if (this.props.roomInfoFailure) {
return (React.createElement("h2", {className: "room-info-failure"},
mozL10n.get("room_information_failure_not_available")
));
}
return (
React.createElement("h2", {className: "room-name"}, this.props.roomName)
);
}
});
var StandaloneRoomView = React.createClass({displayName: "StandaloneRoomView", var StandaloneRoomView = React.createClass({displayName: "StandaloneRoomView",
mixins: [ mixins: [
Backbone.Events, Backbone.Events,
@ -458,7 +482,8 @@ loop.standaloneRoomViews = (function(mozL10n) {
roomUsed: this.state.used}), roomUsed: this.state.used}),
React.createElement("div", {className: "video-layout-wrapper"}, React.createElement("div", {className: "video-layout-wrapper"},
React.createElement("div", {className: "conversation room-conversation"}, React.createElement("div", {className: "conversation room-conversation"},
React.createElement("h2", {className: "room-name"}, this.state.roomName), React.createElement(StandaloneRoomContextView, {roomName: this.state.roomName,
roomInfoFailure: this.state.roomInfoFailure}),
React.createElement("div", {className: "media nested"}, React.createElement("div", {className: "media nested"},
React.createElement("span", {className: "self-view-hidden-message"}, React.createElement("span", {className: "self-view-hidden-message"},
mozL10n.get("self_view_hidden_message") mozL10n.get("self_view_hidden_message")
@ -491,6 +516,7 @@ loop.standaloneRoomViews = (function(mozL10n) {
}); });
return { return {
StandaloneRoomContextView: StandaloneRoomContextView,
StandaloneRoomView: StandaloneRoomView StandaloneRoomView: StandaloneRoomView
}; };
})(navigator.mozL10n); })(navigator.mozL10n);

View File

@ -12,6 +12,7 @@ loop.standaloneRoomViews = (function(mozL10n) {
"use strict"; "use strict";
var FAILURE_DETAILS = loop.shared.utils.FAILURE_DETAILS; var FAILURE_DETAILS = loop.shared.utils.FAILURE_DETAILS;
var ROOM_INFO_FAILURES = loop.shared.utils.ROOM_INFO_FAILURES;
var ROOM_STATES = loop.store.ROOM_STATES; var ROOM_STATES = loop.store.ROOM_STATES;
var sharedActions = loop.shared.actions; var sharedActions = loop.shared.actions;
var sharedMixins = loop.shared.mixins; var sharedMixins = loop.shared.mixins;
@ -198,6 +199,29 @@ loop.standaloneRoomViews = (function(mozL10n) {
} }
}); });
var StandaloneRoomContextView = React.createClass({
propTypes: {
roomName: React.PropTypes.string,
roomInfoFailure: React.PropTypes.string
},
render: function() {
if (this.props.roomInfoFailure === ROOM_INFO_FAILURES.WEB_CRYPTO_UNSUPPORTED) {
return (<h2 className="room-info-failure">
{mozL10n.get("room_information_failure_unsupported_browser")}
</h2>);
} else if (this.props.roomInfoFailure) {
return (<h2 className="room-info-failure">
{mozL10n.get("room_information_failure_not_available")}
</h2>);
}
return (
<h2 className="room-name">{this.props.roomName}</h2>
);
}
});
var StandaloneRoomView = React.createClass({ var StandaloneRoomView = React.createClass({
mixins: [ mixins: [
Backbone.Events, Backbone.Events,
@ -458,7 +482,8 @@ loop.standaloneRoomViews = (function(mozL10n) {
roomUsed={this.state.used} /> roomUsed={this.state.used} />
<div className="video-layout-wrapper"> <div className="video-layout-wrapper">
<div className="conversation room-conversation"> <div className="conversation room-conversation">
<h2 className="room-name">{this.state.roomName}</h2> <StandaloneRoomContextView roomName={this.state.roomName}
roomInfoFailure={this.state.roomInfoFailure} />
<div className="media nested"> <div className="media nested">
<span className="self-view-hidden-message"> <span className="self-view-hidden-message">
{mozL10n.get("self_view_hidden_message")} {mozL10n.get("self_view_hidden_message")}
@ -491,6 +516,7 @@ loop.standaloneRoomViews = (function(mozL10n) {
}); });
return { return {
StandaloneRoomContextView: StandaloneRoomContextView,
StandaloneRoomView: StandaloneRoomView StandaloneRoomView: StandaloneRoomView
}; };
})(navigator.mozL10n); })(navigator.mozL10n);

View File

@ -1109,7 +1109,8 @@ loop.webapp = (function($, _, OT, mozL10n) {
var locationData = sharedUtils.locationData(); var locationData = sharedUtils.locationData();
dispatcher.dispatch(new sharedActions.ExtractTokenInfo({ dispatcher.dispatch(new sharedActions.ExtractTokenInfo({
windowPath: locationData.pathname windowPath: locationData.pathname,
windowHash: locationData.hash
})); }));
} }

View File

@ -1109,7 +1109,8 @@ loop.webapp = (function($, _, OT, mozL10n) {
var locationData = sharedUtils.locationData(); var locationData = sharedUtils.locationData();
dispatcher.dispatch(new sharedActions.ExtractTokenInfo({ dispatcher.dispatch(new sharedActions.ExtractTokenInfo({
windowPath: locationData.pathname windowPath: locationData.pathname,
windowHash: locationData.hash
})); }));
} }

View File

@ -127,6 +127,8 @@ rooms_room_join_label=Join the conversation
rooms_display_name_guest=Guest rooms_display_name_guest=Guest
rooms_unavailable_notification_message=Sorry, you cannot join this conversation. The link may be expired or invalid. rooms_unavailable_notification_message=Sorry, you cannot join this conversation. The link may be expired or invalid.
rooms_media_denied_message=We could not get access to your microphone or camera. Please reload the page to try again. rooms_media_denied_message=We could not get access to your microphone or camera. Please reload the page to try again.
room_information_failure_not_available=No information about this conversation is available. Please request a new link from the person who sent it to you.
room_information_failure_unsupported_browser=Your browser cannot access any information about this conversation. Please make sure you're using the latest version.
## LOCALIZATION_NOTE(standalone_title_with_status): {{clientShortname}} will be ## LOCALIZATION_NOTE(standalone_title_with_status): {{clientShortname}} will be
## replaced by the brand name and {{currentStatus}} will be replaced ## replaced by the brand name and {{currentStatus}} will be replaced

View File

@ -10,6 +10,7 @@ describe("loop.store.ActiveRoomStore", function () {
var ROOM_STATES = loop.store.ROOM_STATES; var ROOM_STATES = loop.store.ROOM_STATES;
var FAILURE_DETAILS = loop.shared.utils.FAILURE_DETAILS; var FAILURE_DETAILS = loop.shared.utils.FAILURE_DETAILS;
var SCREEN_SHARE_STATES = loop.shared.utils.SCREEN_SHARE_STATES; var SCREEN_SHARE_STATES = loop.shared.utils.SCREEN_SHARE_STATES;
var ROOM_INFO_FAILURES = loop.shared.utils.ROOM_INFO_FAILURES;
var sandbox, dispatcher, store, fakeMozLoop, fakeSdkDriver; var sandbox, dispatcher, store, fakeMozLoop, fakeSdkDriver;
var fakeMultiplexGum; var fakeMultiplexGum;
@ -347,20 +348,127 @@ describe("loop.store.ActiveRoomStore", function () {
sinon.assert.calledOnce(fakeMozLoop.rooms.get); sinon.assert.calledOnce(fakeMozLoop.rooms.get);
}); });
it("should dispatch UpdateRoomInfo if mozLoop.rooms.get is successful", function() { it("should dispatch an UpdateRoomInfo message with 'no data' failure if neither roomName nor context are supplied", function() {
var roomDetails = { fakeMozLoop.rooms.get.callsArgWith(1, null, {
roomName: "fakeName", roomOwner: "Dan",
roomUrl: "http://invalid", roomUrl: "http://invalid"
roomOwner: "gavin" });
};
fakeMozLoop.rooms.get.callsArgWith(1, null, roomDetails);
store.fetchServerData(fetchServerAction); store.fetchServerData(fetchServerAction);
sinon.assert.calledOnce(dispatcher.dispatch); sinon.assert.calledOnce(dispatcher.dispatch);
sinon.assert.calledWithExactly(dispatcher.dispatch, sinon.assert.calledWithExactly(dispatcher.dispatch,
new sharedActions.UpdateRoomInfo(roomDetails)); new sharedActions.UpdateRoomInfo({
roomInfoFailure: ROOM_INFO_FAILURES.NO_DATA,
roomOwner: "Dan",
roomUrl: "http://invalid"
}));
});
describe("mozLoop.rooms.get returns roomName as a separate field (no context)", function() {
it("should dispatch UpdateRoomInfo if mozLoop.rooms.get is successful", function() {
var roomDetails = {
roomName: "fakeName",
roomUrl: "http://invalid",
roomOwner: "gavin"
};
fakeMozLoop.rooms.get.callsArgWith(1, null, roomDetails);
store.fetchServerData(fetchServerAction);
sinon.assert.calledOnce(dispatcher.dispatch);
sinon.assert.calledWithExactly(dispatcher.dispatch,
new sharedActions.UpdateRoomInfo(roomDetails));
});
});
describe("mozLoop.rooms.get returns encryptedContext", function() {
var roomDetails, expectedDetails;
beforeEach(function() {
roomDetails = {
context: {
value: "fakeContext"
},
roomUrl: "http://invalid",
roomOwner: "Mark"
};
expectedDetails = {
roomUrl: "http://invalid",
roomOwner: "Mark"
};
fakeMozLoop.rooms.get.callsArgWith(1, null, roomDetails);
sandbox.stub(loop.crypto, "isSupported").returns(true);
});
it("should dispatch UpdateRoomInfo message with 'unsupported' failure if WebCrypto is unsupported", function() {
loop.crypto.isSupported.returns(false);
store.fetchServerData(fetchServerAction);
sinon.assert.calledOnce(dispatcher.dispatch);
sinon.assert.calledWithExactly(dispatcher.dispatch,
new sharedActions.UpdateRoomInfo(_.extend({
roomInfoFailure: ROOM_INFO_FAILURES.WEB_CRYPTO_UNSUPPORTED
}, expectedDetails)));
});
it("should dispatch UpdateRoomInfo message with 'no crypto key' failure if there is no crypto key", function() {
store.fetchServerData(fetchServerAction);
sinon.assert.calledOnce(dispatcher.dispatch);
sinon.assert.calledWithExactly(dispatcher.dispatch,
new sharedActions.UpdateRoomInfo(_.extend({
roomInfoFailure: ROOM_INFO_FAILURES.NO_CRYPTO_KEY
}, expectedDetails)));
});
it("should dispatch UpdateRoomInfo message with 'decrypt failed' failure if decryption failed", function() {
fetchServerAction.cryptoKey = "fakeKey";
// This is a work around to turn promise into a sync action to make handling test failures
// easier.
sandbox.stub(loop.crypto, "decryptBytes", function() {
return {
then: function(resolve, reject) {
reject(new Error("Operation unsupported"));
}
};
});
store.fetchServerData(fetchServerAction);
sinon.assert.calledOnce(dispatcher.dispatch);
sinon.assert.calledWithExactly(dispatcher.dispatch,
new sharedActions.UpdateRoomInfo(_.extend({
roomInfoFailure: ROOM_INFO_FAILURES.DECRYPT_FAILED
}, expectedDetails)));
});
it("should dispatch UpdateRoomInfo message with the room name if decryption was successful", function() {
fetchServerAction.cryptoKey = "fakeKey";
// This is a work around to turn promise into a sync action to make handling test failures
// easier.
sandbox.stub(loop.crypto, "decryptBytes", function() {
return {
then: function(resolve, reject) {
resolve(JSON.stringify({roomName: "The wonderful Loopy room"}));
}
};
});
store.fetchServerData(fetchServerAction);
sinon.assert.calledOnce(dispatcher.dispatch);
sinon.assert.calledWithExactly(dispatcher.dispatch,
new sharedActions.UpdateRoomInfo(_.extend({
roomName: "The wonderful Loopy room"
}, expectedDetails)));
});
}); });
}); });

View File

@ -54,7 +54,8 @@ describe("loop.store.StandaloneAppStore", function () {
beforeEach(function() { beforeEach(function() {
fakeGetWindowData = { fakeGetWindowData = {
windowPath: "" windowPath: "",
windowHash: ""
}; };
sandbox.stub(loop.shared.utils, "getUnsupportedPlatform").returns(); sandbox.stub(loop.shared.utils, "getUnsupportedPlatform").returns();
@ -177,7 +178,7 @@ describe("loop.store.StandaloneAppStore", function () {
}); });
}); });
it("should set the loopToken on the conversation for call paths", it("should dispatch a FetchServerData action for call paths",
function() { function() {
fakeGetWindowData.windowPath = "/c/fakecalltoken"; fakeGetWindowData.windowPath = "/c/fakecalltoken";
@ -187,14 +188,15 @@ describe("loop.store.StandaloneAppStore", function () {
sinon.assert.calledOnce(dispatcher.dispatch); sinon.assert.calledOnce(dispatcher.dispatch);
sinon.assert.calledWithExactly(dispatcher.dispatch, sinon.assert.calledWithExactly(dispatcher.dispatch,
new sharedActions.FetchServerData({ new sharedActions.FetchServerData({
cryptoKey: null,
windowType: "outgoing", windowType: "outgoing",
token: "fakecalltoken" token: "fakecalltoken"
})); }));
}); });
it("should set the loopToken on the conversation for room paths", it("should dispatch a FetchServerData action for room paths",
function() { function() {
fakeGetWindowData.windowPath = "/c/fakeroomtoken"; fakeGetWindowData.windowPath = "/fakeroomtoken";
store.extractTokenInfo( store.extractTokenInfo(
new sharedActions.ExtractTokenInfo(fakeGetWindowData)); new sharedActions.ExtractTokenInfo(fakeGetWindowData));
@ -202,11 +204,29 @@ describe("loop.store.StandaloneAppStore", function () {
sinon.assert.calledOnce(dispatcher.dispatch); sinon.assert.calledOnce(dispatcher.dispatch);
sinon.assert.calledWithExactly(dispatcher.dispatch, sinon.assert.calledWithExactly(dispatcher.dispatch,
new sharedActions.FetchServerData({ new sharedActions.FetchServerData({
windowType: "outgoing", cryptoKey: null,
windowType: "room",
token: "fakeroomtoken" token: "fakeroomtoken"
})); }));
}); });
it("should dispatch a FetchServerData action with a crypto key extracted from the hash", function() {
fakeGetWindowData = {
windowPath: "/fakeroomtoken",
windowHash: "#fakeKey"
};
store.extractTokenInfo(
new sharedActions.ExtractTokenInfo(fakeGetWindowData));
sinon.assert.calledOnce(dispatcher.dispatch);
sinon.assert.calledWithExactly(dispatcher.dispatch,
new sharedActions.FetchServerData({
cryptoKey: "fakeKey",
windowType: "room",
token: "fakeroomtoken"
}));
});
}); });
}); });

View File

@ -11,6 +11,7 @@ describe("loop.standaloneRoomViews", function() {
var ROOM_STATES = loop.store.ROOM_STATES; var ROOM_STATES = loop.store.ROOM_STATES;
var FEEDBACK_STATES = loop.store.FEEDBACK_STATES; var FEEDBACK_STATES = loop.store.FEEDBACK_STATES;
var ROOM_INFO_FAILURES = loop.shared.utils.ROOM_INFO_FAILURES;
var sharedActions = loop.shared.actions; var sharedActions = loop.shared.actions;
var sandbox, dispatcher, activeRoomStore, feedbackStore, dispatch; var sandbox, dispatcher, activeRoomStore, feedbackStore, dispatch;
@ -38,6 +39,44 @@ describe("loop.standaloneRoomViews", function() {
sandbox.restore(); sandbox.restore();
}); });
describe("StandaloneRoomContextView", function() {
beforeEach(function() {
sandbox.stub(navigator.mozL10n, "get").returnsArg(0);
});
function mountTestComponent(props) {
return TestUtils.renderIntoDocument(
React.createElement(
loop.standaloneRoomViews.StandaloneRoomContextView, props));
}
it("should display the room name if no failures are known", function() {
var view = mountTestComponent({
roomName: "Mike's room"
});
expect(view.getDOMNode().textContent).eql("Mike's room");
});
it("should display an unsupported browser message if crypto is unsupported", function() {
var view = mountTestComponent({
roomName: "Mark's room",
roomInfoFailure: ROOM_INFO_FAILURES.WEB_CRYPTO_UNSUPPORTED
});
expect(view.getDOMNode().textContent).match(/unsupported/);
});
it("should display a general error message for any other failure", function() {
var view = mountTestComponent({
roomName: "Mark's room",
roomInfoFailure: ROOM_INFO_FAILURES.NO_DATA
});
expect(view.getDOMNode().textContent).match(/not_available/);
});
});
describe("StandaloneRoomView", function() { describe("StandaloneRoomView", function() {
function mountTestComponent() { function mountTestComponent() {
return TestUtils.renderIntoDocument( return TestUtils.renderIntoDocument(

View File

@ -71,10 +71,10 @@ describe("loop.webapp", function() {
})); }));
}); });
it("should dispatch a ExtractTokenInfo action with the path", it("should dispatch a ExtractTokenInfo action with the path and hash",
function() { function() {
sandbox.stub(loop.shared.utils, "locationData").returns({ sandbox.stub(loop.shared.utils, "locationData").returns({
hash: "", hash: "#fakeKey",
pathname: "/c/faketoken" pathname: "/c/faketoken"
}); });
@ -83,7 +83,8 @@ describe("loop.webapp", function() {
sinon.assert.calledOnce(loop.Dispatcher.prototype.dispatch); sinon.assert.calledOnce(loop.Dispatcher.prototype.dispatch);
sinon.assert.calledWithExactly(loop.Dispatcher.prototype.dispatch, sinon.assert.calledWithExactly(loop.Dispatcher.prototype.dispatch,
new sharedActions.ExtractTokenInfo({ new sharedActions.ExtractTokenInfo({
windowPath: "/c/faketoken" windowPath: "/c/faketoken",
windowHash: "#fakeKey"
})); }));
}); });
}); });