Bug 1094128 Convert the Loop Standalone controller app view to be based on the Flux style. r=nperriault
This commit is contained in:
@@ -37,6 +37,13 @@ loop.shared.actions = (function() {
|
||||
windowId: String
|
||||
}),
|
||||
|
||||
/**
|
||||
* Extract the token information and type for the standalone window
|
||||
*/
|
||||
ExtractTokenInfo: Action.define("extractTokenInfo", {
|
||||
windowPath: String
|
||||
}),
|
||||
|
||||
/**
|
||||
* Used to pass round the window data so that stores can
|
||||
* record the appropriate data.
|
||||
@@ -51,6 +58,15 @@ loop.shared.actions = (function() {
|
||||
// data.
|
||||
}),
|
||||
|
||||
/**
|
||||
* Used to fetch the data from the server for a room or call for the
|
||||
* token.
|
||||
*/
|
||||
FetchServerData: Action.define("fetchServerData", {
|
||||
token: String,
|
||||
windowType: String
|
||||
}),
|
||||
|
||||
/**
|
||||
* Fetch a new call url from the server, intended to be sent over email when
|
||||
* a contact can't be reached.
|
||||
|
||||
@@ -33,10 +33,6 @@ loop.shared.mixins = (function() {
|
||||
* @type {Object}
|
||||
*/
|
||||
var UrlHashChangeMixin = {
|
||||
propTypes: {
|
||||
onUrlHashChange: React.PropTypes.func.isRequired
|
||||
},
|
||||
|
||||
componentDidMount: function() {
|
||||
rootObject.addEventListener("hashchange", this.onUrlHashChange, false);
|
||||
},
|
||||
|
||||
@@ -42,8 +42,13 @@
|
||||
<script type="text/javascript" src="shared/js/mixins.js"></script>
|
||||
<script type="text/javascript" src="shared/js/views.js"></script>
|
||||
<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="js/standaloneAppStore.js"></script>
|
||||
<script type="text/javascript" src="js/standaloneClient.js"></script>
|
||||
<script type="text/javascript" src="js/standaloneRoomViews.js"></script>
|
||||
<script type="text/javascript" src="js/webapp.js"></script>
|
||||
|
||||
<script>
|
||||
|
||||
@@ -0,0 +1,148 @@
|
||||
/* 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 || {};
|
||||
|
||||
/**
|
||||
* Manages the conversation window app controller view. Used to get
|
||||
* the window data and store the window type.
|
||||
*/
|
||||
loop.store.StandaloneAppStore = (function() {
|
||||
"use strict";
|
||||
|
||||
var sharedActions = loop.shared.actions;
|
||||
var sharedUtils = loop.shared.utils;
|
||||
|
||||
var OLD_STYLE_CALL_REGEXP = /\#call\/(.*)/;
|
||||
var NEW_STYLE_CALL_REGEXP = /\/c\/([\w\-]+)$/;
|
||||
var ROOM_REGEXP = /\/([\w\-]+)$/;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param {Object} options Options for the store. Should contain the dispatcher.
|
||||
*/
|
||||
var StandaloneAppStore = function(options) {
|
||||
if (!options.dispatcher) {
|
||||
throw new Error("Missing option dispatcher");
|
||||
}
|
||||
if (!options.sdk) {
|
||||
throw new Error("Missing option sdk");
|
||||
}
|
||||
if (!options.helper) {
|
||||
throw new Error("Missing option helper");
|
||||
}
|
||||
if (!options.conversation) {
|
||||
throw new Error("Missing option conversation");
|
||||
}
|
||||
|
||||
this._dispatcher = options.dispatcher;
|
||||
this._storeState = {};
|
||||
this._sdk = options.sdk;
|
||||
this._helper = options.helper;
|
||||
this._conversation = options.conversation;
|
||||
|
||||
this._dispatcher.register(this, [
|
||||
"extractTokenInfo"
|
||||
]);
|
||||
};
|
||||
|
||||
StandaloneAppStore.prototype = _.extend({
|
||||
/**
|
||||
* Retrieves current store state.
|
||||
*
|
||||
* @return {Object}
|
||||
*/
|
||||
getStoreState: function() {
|
||||
return this._storeState;
|
||||
},
|
||||
|
||||
/**
|
||||
* Updates store states and trigger a "change" event.
|
||||
*
|
||||
* @param {Object} state The new store state.
|
||||
*/
|
||||
setStoreState: function(state) {
|
||||
this._storeState = state;
|
||||
this.trigger("change");
|
||||
},
|
||||
|
||||
_extractWindowDataFromPath: function(windowPath) {
|
||||
var match;
|
||||
var windowType = "home";
|
||||
|
||||
function extractId(path, regexp) {
|
||||
var match = path.match(regexp);
|
||||
if (match && match[1]) {
|
||||
return match;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
if (windowPath) {
|
||||
// Is this a call url (the hash is a backwards-compatible url)?
|
||||
match = extractId(windowPath, OLD_STYLE_CALL_REGEXP) ||
|
||||
extractId(windowPath, NEW_STYLE_CALL_REGEXP);
|
||||
|
||||
if (match) {
|
||||
windowType = "outgoing";
|
||||
} else {
|
||||
// Is this a room url?
|
||||
match = extractId(windowPath, ROOM_REGEXP);
|
||||
|
||||
if (match) {
|
||||
windowType = "room";
|
||||
}
|
||||
}
|
||||
}
|
||||
return [windowType, match && match[1] ? match[1] : null];
|
||||
},
|
||||
|
||||
/**
|
||||
* Handles the extract token info action - obtains the token information
|
||||
* and its type; updates the store and notifies interested components.
|
||||
*
|
||||
* @param {sharedActions.GetWindowData} actionData The action data
|
||||
*/
|
||||
extractTokenInfo: function(actionData) {
|
||||
var windowType = "home";
|
||||
var token;
|
||||
|
||||
// Check if we're on a supported device/platform.
|
||||
if (this._helper.isIOS(navigator.platform)) {
|
||||
windowType = "unsupportedDevice";
|
||||
} else if (!this._sdk.checkSystemRequirements()) {
|
||||
windowType = "unsupportedBrowser";
|
||||
} else if (actionData.windowPath) {
|
||||
// ES6 not used in standalone yet.
|
||||
var result = this._extractWindowDataFromPath(actionData.windowPath);
|
||||
windowType = result[0];
|
||||
token = result[1];
|
||||
}
|
||||
// Else type is home.
|
||||
|
||||
if (token) {
|
||||
this._conversation.set({loopToken: token});
|
||||
}
|
||||
|
||||
this.setStoreState({
|
||||
windowType: windowType
|
||||
});
|
||||
|
||||
// If we've not got a window ID, don't dispatch the action, as we don't need
|
||||
// it.
|
||||
if (token) {
|
||||
this._dispatcher.dispatch(new loop.shared.actions.FetchServerData({
|
||||
token: token,
|
||||
windowType: windowType
|
||||
}));
|
||||
}
|
||||
}
|
||||
}, Backbone.Events);
|
||||
|
||||
return StandaloneAppStore;
|
||||
})();
|
||||
@@ -0,0 +1,22 @@
|
||||
/** @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.standaloneRoomViews = (function() {
|
||||
"use strict";
|
||||
|
||||
var StandaloneRoomView = React.createClass({displayName: 'StandaloneRoomView',
|
||||
render: function() {
|
||||
return (React.DOM.div(null, "Room"));
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
StandaloneRoomView: StandaloneRoomView
|
||||
};
|
||||
})();
|
||||
@@ -0,0 +1,22 @@
|
||||
/** @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.standaloneRoomViews = (function() {
|
||||
"use strict";
|
||||
|
||||
var StandaloneRoomView = React.createClass({
|
||||
render: function() {
|
||||
return (<div>Room</div>);
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
StandaloneRoomView: StandaloneRoomView
|
||||
};
|
||||
})();
|
||||
@@ -14,6 +14,7 @@ loop.webapp = (function($, _, OT, mozL10n) {
|
||||
loop.config = loop.config || {};
|
||||
loop.config.serverUrl = loop.config.serverUrl || "http://localhost:5000";
|
||||
|
||||
var sharedActions = loop.shared.actions;
|
||||
var sharedMixins = loop.shared.mixins;
|
||||
var sharedModels = loop.shared.models;
|
||||
var sharedViews = loop.shared.views;
|
||||
@@ -877,7 +878,8 @@ loop.webapp = (function($, _, OT, mozL10n) {
|
||||
var WebappRootView = React.createClass({displayName: 'WebappRootView',
|
||||
|
||||
mixins: [sharedMixins.UrlHashChangeMixin,
|
||||
sharedMixins.DocumentLocationMixin],
|
||||
sharedMixins.DocumentLocationMixin,
|
||||
Backbone.Events],
|
||||
|
||||
propTypes: {
|
||||
client: React.PropTypes.instanceOf(loop.StandaloneClient).isRequired,
|
||||
@@ -889,14 +891,25 @@ loop.webapp = (function($, _, OT, mozL10n) {
|
||||
notifications: React.PropTypes.instanceOf(sharedModels.NotificationCollection)
|
||||
.isRequired,
|
||||
sdk: React.PropTypes.object.isRequired,
|
||||
feedbackApiClient: React.PropTypes.object.isRequired
|
||||
feedbackApiClient: React.PropTypes.object.isRequired,
|
||||
|
||||
// XXX New types for flux style
|
||||
standaloneAppStore: React.PropTypes.instanceOf(
|
||||
loop.store.StandaloneAppStore).isRequired
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
return {
|
||||
unsupportedDevice: this.props.helper.isIOS(navigator.platform),
|
||||
unsupportedBrowser: !this.props.sdk.checkSystemRequirements(),
|
||||
};
|
||||
return this.props.standaloneAppStore.getStoreState();
|
||||
},
|
||||
|
||||
componentWillMount: function() {
|
||||
this.listenTo(this.props.standaloneAppStore, "change", function() {
|
||||
this.setState(this.props.standaloneAppStore.getStoreState());
|
||||
}, this);
|
||||
},
|
||||
|
||||
componentWillUnmount: function() {
|
||||
this.stopListening(this.props.standaloneAppStore);
|
||||
},
|
||||
|
||||
onUrlHashChange: function() {
|
||||
@@ -904,23 +917,36 @@ loop.webapp = (function($, _, OT, mozL10n) {
|
||||
},
|
||||
|
||||
render: function() {
|
||||
if (this.state.unsupportedDevice) {
|
||||
return UnsupportedDeviceView(null);
|
||||
} else if (this.state.unsupportedBrowser) {
|
||||
return UnsupportedBrowserView(null);
|
||||
} else if (this.props.conversation.get("loopToken")) {
|
||||
return (
|
||||
OutgoingConversationView({
|
||||
client: this.props.client,
|
||||
conversation: this.props.conversation,
|
||||
helper: this.props.helper,
|
||||
notifications: this.props.notifications,
|
||||
sdk: this.props.sdk,
|
||||
feedbackApiClient: this.props.feedbackApiClient}
|
||||
)
|
||||
);
|
||||
} else {
|
||||
return HomeView(null);
|
||||
switch (this.state.windowType) {
|
||||
case "unsupportedDevice": {
|
||||
return UnsupportedDeviceView(null);
|
||||
}
|
||||
case "unsupportedBrowser": {
|
||||
return UnsupportedBrowserView(null);
|
||||
}
|
||||
case "outgoing": {
|
||||
return (
|
||||
OutgoingConversationView({
|
||||
client: this.props.client,
|
||||
conversation: this.props.conversation,
|
||||
helper: this.props.helper,
|
||||
notifications: this.props.notifications,
|
||||
sdk: this.props.sdk,
|
||||
feedbackApiClient: this.props.feedbackApiClient}
|
||||
)
|
||||
);
|
||||
}
|
||||
case "room": {
|
||||
return loop.standaloneRoomViews.StandaloneRoomView(null);
|
||||
}
|
||||
case "home": {
|
||||
return HomeView(null);
|
||||
}
|
||||
default: {
|
||||
// The state hasn't been initialised yet, so don't display
|
||||
// anything to avoid flicker.
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -930,9 +956,8 @@ loop.webapp = (function($, _, OT, mozL10n) {
|
||||
*/
|
||||
function init() {
|
||||
var helper = new sharedUtils.Helper();
|
||||
var client = new loop.StandaloneClient({
|
||||
baseServerUrl: loop.config.serverUrl
|
||||
});
|
||||
|
||||
// Older non-flux based items.
|
||||
var notifications = new sharedModels.NotificationCollection();
|
||||
var conversation
|
||||
if (helper.isFirefoxOS(navigator.userAgent)) {
|
||||
@@ -950,24 +975,18 @@ loop.webapp = (function($, _, OT, mozL10n) {
|
||||
url: document.location.origin
|
||||
});
|
||||
|
||||
// Obtain the loopToken
|
||||
// New flux items.
|
||||
var dispatcher = new loop.Dispatcher();
|
||||
var client = new loop.StandaloneClient({
|
||||
baseServerUrl: loop.config.serverUrl
|
||||
});
|
||||
|
||||
var match;
|
||||
|
||||
// locationHash supports the old format urls.
|
||||
var locationData = helper.locationData();
|
||||
if (locationData.hash) {
|
||||
match = locationData.hash.match(/\#call\/(.*)/);
|
||||
} else if (locationData.pathname) {
|
||||
// Otherwise, we're expecting a url such as /c/<token> for calls.
|
||||
match = locationData.pathname.match(/\/c\/([\w\-]+)/);
|
||||
}
|
||||
// XXX Supporting '/\/([\w\-]+)/' is for rooms which are to be implemented
|
||||
// in bug 1074701.
|
||||
|
||||
if (match && match[1]) {
|
||||
conversation.set({loopToken: match[1]});
|
||||
}
|
||||
var standaloneAppStore = new loop.store.StandaloneAppStore({
|
||||
conversation: conversation,
|
||||
dispatcher: dispatcher,
|
||||
helper: helper,
|
||||
sdk: OT
|
||||
});
|
||||
|
||||
React.renderComponent(WebappRootView({
|
||||
client: client,
|
||||
@@ -975,13 +994,20 @@ loop.webapp = (function($, _, OT, mozL10n) {
|
||||
helper: helper,
|
||||
notifications: notifications,
|
||||
sdk: OT,
|
||||
feedbackApiClient: feedbackApiClient}
|
||||
feedbackApiClient: feedbackApiClient,
|
||||
standaloneAppStore: standaloneAppStore}
|
||||
), 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({
|
||||
// We pass the hash or the pathname - the hash was used for the original
|
||||
// urls, the pathname for later ones.
|
||||
windowPath: helper.locationData().hash || helper.locationData().pathname
|
||||
}));
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
@@ -14,6 +14,7 @@ loop.webapp = (function($, _, OT, mozL10n) {
|
||||
loop.config = loop.config || {};
|
||||
loop.config.serverUrl = loop.config.serverUrl || "http://localhost:5000";
|
||||
|
||||
var sharedActions = loop.shared.actions;
|
||||
var sharedMixins = loop.shared.mixins;
|
||||
var sharedModels = loop.shared.models;
|
||||
var sharedViews = loop.shared.views;
|
||||
@@ -877,7 +878,8 @@ loop.webapp = (function($, _, OT, mozL10n) {
|
||||
var WebappRootView = React.createClass({
|
||||
|
||||
mixins: [sharedMixins.UrlHashChangeMixin,
|
||||
sharedMixins.DocumentLocationMixin],
|
||||
sharedMixins.DocumentLocationMixin,
|
||||
Backbone.Events],
|
||||
|
||||
propTypes: {
|
||||
client: React.PropTypes.instanceOf(loop.StandaloneClient).isRequired,
|
||||
@@ -889,14 +891,25 @@ loop.webapp = (function($, _, OT, mozL10n) {
|
||||
notifications: React.PropTypes.instanceOf(sharedModels.NotificationCollection)
|
||||
.isRequired,
|
||||
sdk: React.PropTypes.object.isRequired,
|
||||
feedbackApiClient: React.PropTypes.object.isRequired
|
||||
feedbackApiClient: React.PropTypes.object.isRequired,
|
||||
|
||||
// XXX New types for flux style
|
||||
standaloneAppStore: React.PropTypes.instanceOf(
|
||||
loop.store.StandaloneAppStore).isRequired
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
return {
|
||||
unsupportedDevice: this.props.helper.isIOS(navigator.platform),
|
||||
unsupportedBrowser: !this.props.sdk.checkSystemRequirements(),
|
||||
};
|
||||
return this.props.standaloneAppStore.getStoreState();
|
||||
},
|
||||
|
||||
componentWillMount: function() {
|
||||
this.listenTo(this.props.standaloneAppStore, "change", function() {
|
||||
this.setState(this.props.standaloneAppStore.getStoreState());
|
||||
}, this);
|
||||
},
|
||||
|
||||
componentWillUnmount: function() {
|
||||
this.stopListening(this.props.standaloneAppStore);
|
||||
},
|
||||
|
||||
onUrlHashChange: function() {
|
||||
@@ -904,23 +917,36 @@ loop.webapp = (function($, _, OT, mozL10n) {
|
||||
},
|
||||
|
||||
render: function() {
|
||||
if (this.state.unsupportedDevice) {
|
||||
return <UnsupportedDeviceView />;
|
||||
} else if (this.state.unsupportedBrowser) {
|
||||
return <UnsupportedBrowserView />;
|
||||
} else if (this.props.conversation.get("loopToken")) {
|
||||
return (
|
||||
<OutgoingConversationView
|
||||
client={this.props.client}
|
||||
conversation={this.props.conversation}
|
||||
helper={this.props.helper}
|
||||
notifications={this.props.notifications}
|
||||
sdk={this.props.sdk}
|
||||
feedbackApiClient={this.props.feedbackApiClient}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
return <HomeView />;
|
||||
switch (this.state.windowType) {
|
||||
case "unsupportedDevice": {
|
||||
return <UnsupportedDeviceView />;
|
||||
}
|
||||
case "unsupportedBrowser": {
|
||||
return <UnsupportedBrowserView />;
|
||||
}
|
||||
case "outgoing": {
|
||||
return (
|
||||
<OutgoingConversationView
|
||||
client={this.props.client}
|
||||
conversation={this.props.conversation}
|
||||
helper={this.props.helper}
|
||||
notifications={this.props.notifications}
|
||||
sdk={this.props.sdk}
|
||||
feedbackApiClient={this.props.feedbackApiClient}
|
||||
/>
|
||||
);
|
||||
}
|
||||
case "room": {
|
||||
return <loop.standaloneRoomViews.StandaloneRoomView/>;
|
||||
}
|
||||
case "home": {
|
||||
return <HomeView />;
|
||||
}
|
||||
default: {
|
||||
// The state hasn't been initialised yet, so don't display
|
||||
// anything to avoid flicker.
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -930,9 +956,8 @@ loop.webapp = (function($, _, OT, mozL10n) {
|
||||
*/
|
||||
function init() {
|
||||
var helper = new sharedUtils.Helper();
|
||||
var client = new loop.StandaloneClient({
|
||||
baseServerUrl: loop.config.serverUrl
|
||||
});
|
||||
|
||||
// Older non-flux based items.
|
||||
var notifications = new sharedModels.NotificationCollection();
|
||||
var conversation
|
||||
if (helper.isFirefoxOS(navigator.userAgent)) {
|
||||
@@ -950,24 +975,18 @@ loop.webapp = (function($, _, OT, mozL10n) {
|
||||
url: document.location.origin
|
||||
});
|
||||
|
||||
// Obtain the loopToken
|
||||
// New flux items.
|
||||
var dispatcher = new loop.Dispatcher();
|
||||
var client = new loop.StandaloneClient({
|
||||
baseServerUrl: loop.config.serverUrl
|
||||
});
|
||||
|
||||
var match;
|
||||
|
||||
// locationHash supports the old format urls.
|
||||
var locationData = helper.locationData();
|
||||
if (locationData.hash) {
|
||||
match = locationData.hash.match(/\#call\/(.*)/);
|
||||
} else if (locationData.pathname) {
|
||||
// Otherwise, we're expecting a url such as /c/<token> for calls.
|
||||
match = locationData.pathname.match(/\/c\/([\w\-]+)/);
|
||||
}
|
||||
// XXX Supporting '/\/([\w\-]+)/' is for rooms which are to be implemented
|
||||
// in bug 1074701.
|
||||
|
||||
if (match && match[1]) {
|
||||
conversation.set({loopToken: match[1]});
|
||||
}
|
||||
var standaloneAppStore = new loop.store.StandaloneAppStore({
|
||||
conversation: conversation,
|
||||
dispatcher: dispatcher,
|
||||
helper: helper,
|
||||
sdk: OT
|
||||
});
|
||||
|
||||
React.renderComponent(<WebappRootView
|
||||
client={client}
|
||||
@@ -976,12 +995,19 @@ loop.webapp = (function($, _, OT, mozL10n) {
|
||||
notifications={notifications}
|
||||
sdk={OT}
|
||||
feedbackApiClient={feedbackApiClient}
|
||||
standaloneAppStore={standaloneAppStore}
|
||||
/>, 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({
|
||||
// We pass the hash or the pathname - the hash was used for the original
|
||||
// urls, the pathname for later ones.
|
||||
windowPath: helper.locationData().hash || helper.locationData().pathname
|
||||
}));
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
@@ -37,11 +37,17 @@
|
||||
<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/actions.js"></script>
|
||||
<script src="../../content/shared/js/validate.js"></script>
|
||||
<script src="../../content/shared/js/dispatcher.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/standaloneRoomViews.js"></script>
|
||||
<script src="../../standalone/content/js/webapp.js"></script>
|
||||
<!-- Test scripts -->
|
||||
<script src="standalone_client_test.js"></script>
|
||||
<script src="standaloneAppStore_test.js"></script>
|
||||
<script src="webapp_test.js"></script>
|
||||
<script src="multiplexGum_test.js"></script>
|
||||
<script>
|
||||
|
||||
@@ -0,0 +1,246 @@
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
* http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
var expect = chai.expect;
|
||||
|
||||
describe("loop.store.StandaloneAppStore", function () {
|
||||
var sharedActions = loop.shared.actions;
|
||||
var sharedUtils = loop.shared.utils;
|
||||
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.StandaloneAppStore({
|
||||
sdk: {},
|
||||
helper: {},
|
||||
conversation: {}
|
||||
});
|
||||
}).to.Throw(/dispatcher/);
|
||||
});
|
||||
|
||||
it("should throw an error if sdk is missing", function() {
|
||||
expect(function() {
|
||||
new loop.store.StandaloneAppStore({
|
||||
dispatcher: dispatcher,
|
||||
helper: {},
|
||||
conversation: {}
|
||||
});
|
||||
}).to.Throw(/sdk/);
|
||||
});
|
||||
|
||||
it("should throw an error if helper is missing", function() {
|
||||
expect(function() {
|
||||
new loop.store.StandaloneAppStore({
|
||||
dispatcher: dispatcher,
|
||||
sdk: {},
|
||||
conversation: {}
|
||||
});
|
||||
}).to.Throw(/helper/);
|
||||
});
|
||||
|
||||
it("should throw an error if conversation is missing", function() {
|
||||
expect(function() {
|
||||
new loop.store.StandaloneAppStore({
|
||||
dispatcher: dispatcher,
|
||||
sdk: {},
|
||||
helper: {}
|
||||
});
|
||||
}).to.Throw(/conversation/);
|
||||
});
|
||||
});
|
||||
|
||||
describe("#extractTokenInfo", function() {
|
||||
var store, fakeGetWindowData, fakeSdk, fakeConversation, helper;
|
||||
|
||||
beforeEach(function() {
|
||||
fakeGetWindowData = {
|
||||
windowPath: ""
|
||||
};
|
||||
|
||||
helper = new sharedUtils.Helper();
|
||||
sandbox.stub(helper, "isIOS").returns(false);
|
||||
|
||||
fakeSdk = {
|
||||
checkSystemRequirements: sinon.stub().returns(true)
|
||||
};
|
||||
|
||||
fakeConversation = {
|
||||
set: sinon.spy()
|
||||
};
|
||||
|
||||
sandbox.stub(dispatcher, "dispatch");
|
||||
|
||||
store = new loop.store.StandaloneAppStore({
|
||||
dispatcher: dispatcher,
|
||||
sdk: fakeSdk,
|
||||
helper: helper,
|
||||
conversation: fakeConversation
|
||||
});
|
||||
});
|
||||
|
||||
it("should set windowType to `unsupportedDevice` for IOS", function() {
|
||||
// The stub should return true for this test.
|
||||
helper.isIOS.returns(true);
|
||||
|
||||
store.extractTokenInfo(
|
||||
new sharedActions.ExtractTokenInfo(fakeGetWindowData));
|
||||
|
||||
expect(store.getStoreState()).eql({
|
||||
windowType: "unsupportedDevice"
|
||||
});
|
||||
});
|
||||
|
||||
it("should set windowType to `unsupportedBrowser` for browsers the sdk does not support",
|
||||
function() {
|
||||
// The stub should return false for this test.
|
||||
fakeSdk.checkSystemRequirements.returns(false);
|
||||
|
||||
store.extractTokenInfo(
|
||||
new sharedActions.ExtractTokenInfo(fakeGetWindowData));
|
||||
|
||||
expect(store.getStoreState()).eql({
|
||||
windowType: "unsupportedBrowser"
|
||||
});
|
||||
});
|
||||
|
||||
it("should set windowType to `outgoing` for old style call hashes", function() {
|
||||
fakeGetWindowData.windowPath = "#call/faketoken";
|
||||
|
||||
store.extractTokenInfo(
|
||||
new sharedActions.ExtractTokenInfo(fakeGetWindowData));
|
||||
|
||||
expect(store.getStoreState()).eql({
|
||||
windowType: "outgoing"
|
||||
});
|
||||
});
|
||||
|
||||
it("should set windowType to `outgoing` for new style call paths", function() {
|
||||
fakeGetWindowData.windowPath = "/c/fakecalltoken";
|
||||
|
||||
store.extractTokenInfo(
|
||||
new sharedActions.ExtractTokenInfo(fakeGetWindowData));
|
||||
|
||||
expect(store.getStoreState()).eql({
|
||||
windowType: "outgoing"
|
||||
});
|
||||
});
|
||||
|
||||
it("should set windowType to `room` for room paths", function() {
|
||||
fakeGetWindowData.windowPath = "/fakeroomtoken";
|
||||
|
||||
store.extractTokenInfo(
|
||||
new sharedActions.ExtractTokenInfo(fakeGetWindowData));
|
||||
|
||||
expect(store.getStoreState()).eql({
|
||||
windowType: "room"
|
||||
});
|
||||
});
|
||||
|
||||
it("should set windowType to `home` for unknown paths", function() {
|
||||
fakeGetWindowData.windowPath = "/";
|
||||
|
||||
store.extractTokenInfo(
|
||||
new sharedActions.ExtractTokenInfo(fakeGetWindowData));
|
||||
|
||||
expect(store.getStoreState()).eql({
|
||||
windowType: "home"
|
||||
});
|
||||
});
|
||||
|
||||
it("should set the loopToken on the conversation for old style call hashes",
|
||||
function() {
|
||||
fakeGetWindowData.windowPath = "#call/faketoken";
|
||||
|
||||
store.extractTokenInfo(
|
||||
new sharedActions.ExtractTokenInfo(fakeGetWindowData));
|
||||
|
||||
sinon.assert.calledOnce(fakeConversation.set);
|
||||
sinon.assert.calledWithExactly(fakeConversation.set, {
|
||||
loopToken: "faketoken"
|
||||
});
|
||||
});
|
||||
|
||||
it("should set the loopToken on the conversation for new style call paths",
|
||||
function() {
|
||||
fakeGetWindowData.windowPath = "/c/fakecalltoken";
|
||||
|
||||
store.extractTokenInfo(
|
||||
new sharedActions.ExtractTokenInfo(fakeGetWindowData));
|
||||
|
||||
sinon.assert.calledOnce(fakeConversation.set);
|
||||
sinon.assert.calledWithExactly(fakeConversation.set, {
|
||||
loopToken: "fakecalltoken"
|
||||
});
|
||||
});
|
||||
|
||||
it("should set the loopToken on the conversation for room paths",
|
||||
function() {
|
||||
fakeGetWindowData.windowPath = "/c/fakeroomtoken";
|
||||
|
||||
store.extractTokenInfo(
|
||||
new sharedActions.ExtractTokenInfo(fakeGetWindowData));
|
||||
|
||||
sinon.assert.calledOnce(fakeConversation.set);
|
||||
sinon.assert.calledWithExactly(fakeConversation.set, {
|
||||
loopToken: "fakeroomtoken"
|
||||
});
|
||||
});
|
||||
|
||||
it("should dispatch a SetupWindowData action for old style call hashes",
|
||||
function() {
|
||||
fakeGetWindowData.windowPath = "#call/faketoken";
|
||||
|
||||
store.extractTokenInfo(
|
||||
new sharedActions.ExtractTokenInfo(fakeGetWindowData));
|
||||
|
||||
sinon.assert.calledOnce(dispatcher.dispatch);
|
||||
sinon.assert.calledWithExactly(dispatcher.dispatch,
|
||||
new sharedActions.FetchServerData({
|
||||
windowType: "outgoing",
|
||||
token: "faketoken"
|
||||
}));
|
||||
});
|
||||
|
||||
it("should set the loopToken on the conversation for new style call paths",
|
||||
function() {
|
||||
fakeGetWindowData.windowPath = "/c/fakecalltoken";
|
||||
|
||||
store.extractTokenInfo(
|
||||
new sharedActions.ExtractTokenInfo(fakeGetWindowData));
|
||||
|
||||
sinon.assert.calledOnce(dispatcher.dispatch);
|
||||
sinon.assert.calledWithExactly(dispatcher.dispatch,
|
||||
new sharedActions.FetchServerData({
|
||||
windowType: "outgoing",
|
||||
token: "fakecalltoken"
|
||||
}));
|
||||
});
|
||||
|
||||
it("should set the loopToken on the conversation for room paths",
|
||||
function() {
|
||||
fakeGetWindowData.windowPath = "/c/fakeroomtoken";
|
||||
|
||||
store.extractTokenInfo(
|
||||
new sharedActions.ExtractTokenInfo(fakeGetWindowData));
|
||||
|
||||
sinon.assert.calledOnce(dispatcher.dispatch);
|
||||
sinon.assert.calledWithExactly(dispatcher.dispatch,
|
||||
new sharedActions.FetchServerData({
|
||||
windowType: "outgoing",
|
||||
token: "fakeroomtoken"
|
||||
}));
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
@@ -10,6 +10,7 @@ var TestUtils = React.addons.TestUtils;
|
||||
describe("loop.webapp", function() {
|
||||
"use strict";
|
||||
|
||||
var sharedActions = loop.shared.actions;
|
||||
var sharedModels = loop.shared.models,
|
||||
sharedViews = loop.shared.views,
|
||||
sharedUtils = loop.shared.utils,
|
||||
@@ -35,13 +36,10 @@ describe("loop.webapp", function() {
|
||||
});
|
||||
|
||||
describe("#init", function() {
|
||||
var conversationSetStub;
|
||||
|
||||
beforeEach(function() {
|
||||
sandbox.stub(React, "renderComponent");
|
||||
loop.config.feedbackApiUrl = "http://fake.invalid";
|
||||
conversationSetStub =
|
||||
sandbox.stub(sharedModels.ConversationModel.prototype, "set");
|
||||
sandbox.stub(loop.Dispatcher.prototype, "dispatch");
|
||||
});
|
||||
|
||||
it("should create the WebappRootView", function() {
|
||||
@@ -55,33 +53,36 @@ describe("loop.webapp", function() {
|
||||
}));
|
||||
});
|
||||
|
||||
it("should set the loopToken on the conversation for old-style call urls",
|
||||
function() {
|
||||
sandbox.stub(sharedUtils.Helper.prototype,
|
||||
"locationData").returns({
|
||||
hash: "#call/fake-Token",
|
||||
pathname: "/"
|
||||
});
|
||||
|
||||
loop.webapp.init();
|
||||
|
||||
sinon.assert.called(conversationSetStub);
|
||||
sinon.assert.calledWithExactly(conversationSetStub, {loopToken: "fake-Token"});
|
||||
it("should dispatch a ExtractTokenInfo action with the hash", function() {
|
||||
sandbox.stub(loop.shared.utils.Helper.prototype, "locationData").returns({
|
||||
hash: "#call/faketoken",
|
||||
pathname: "invalid"
|
||||
});
|
||||
|
||||
it("should set the loopToken on the conversation for new-style call urls",
|
||||
loop.webapp.init();
|
||||
|
||||
sinon.assert.calledOnce(loop.Dispatcher.prototype.dispatch);
|
||||
sinon.assert.calledWithExactly(loop.Dispatcher.prototype.dispatch,
|
||||
new sharedActions.ExtractTokenInfo({
|
||||
windowPath: "#call/faketoken"
|
||||
}));
|
||||
});
|
||||
|
||||
it("should dispatch a ExtractTokenInfo action with the path if there is no hash",
|
||||
function() {
|
||||
sandbox.stub(sharedUtils.Helper.prototype,
|
||||
"locationData").returns({
|
||||
hash: "",
|
||||
pathname: "/c/abc123-_Tes"
|
||||
});
|
||||
sandbox.stub(loop.shared.utils.Helper.prototype, "locationData").returns({
|
||||
hash: "",
|
||||
pathname: "/c/faketoken"
|
||||
});
|
||||
|
||||
loop.webapp.init();
|
||||
loop.webapp.init();
|
||||
|
||||
sinon.assert.called(conversationSetStub);
|
||||
sinon.assert.calledWithExactly(conversationSetStub, {loopToken: "abc123-_Tes"});
|
||||
});
|
||||
sinon.assert.calledOnce(loop.Dispatcher.prototype.dispatch);
|
||||
sinon.assert.calledWithExactly(loop.Dispatcher.prototype.dispatch,
|
||||
new sharedActions.ExtractTokenInfo({
|
||||
windowPath: "/c/faketoken"
|
||||
}));
|
||||
});
|
||||
});
|
||||
|
||||
describe("OutgoingConversationView", function() {
|
||||
@@ -544,7 +545,8 @@ describe("loop.webapp", function() {
|
||||
});
|
||||
|
||||
describe("WebappRootView", function() {
|
||||
var helper, sdk, conversationModel, client, props;
|
||||
var helper, sdk, conversationModel, client, props, standaloneAppStore;
|
||||
var dispatcher;
|
||||
|
||||
function mountTestComponent() {
|
||||
return TestUtils.renderIntoDocument(
|
||||
@@ -555,7 +557,7 @@ describe("loop.webapp", function() {
|
||||
sdk: sdk,
|
||||
conversation: conversationModel,
|
||||
feedbackApiClient: feedbackApiClient,
|
||||
onUrlHashChange: sandbox.stub()
|
||||
standaloneAppStore: standaloneAppStore
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -570,14 +572,21 @@ describe("loop.webapp", function() {
|
||||
client = new loop.StandaloneClient({
|
||||
baseServerUrl: "fakeUrl"
|
||||
});
|
||||
dispatcher = new loop.Dispatcher();
|
||||
standaloneAppStore = new loop.store.StandaloneAppStore({
|
||||
dispatcher: dispatcher,
|
||||
sdk: sdk,
|
||||
helper: helper,
|
||||
conversation: conversationModel
|
||||
});
|
||||
// Stub this to stop the StartConversationView kicking in the request and
|
||||
// follow-ups.
|
||||
sandbox.stub(client, "requestCallUrlInfo");
|
||||
});
|
||||
|
||||
it("should mount the unsupportedDevice view if the device is running iOS",
|
||||
it("should display the UnsupportedDeviceView for `unsupportedDevice` window type",
|
||||
function() {
|
||||
sandbox.stub(helper, "isIOS").returns(true);
|
||||
standaloneAppStore.setStoreState({windowType: "unsupportedDevice"});
|
||||
|
||||
var webappRootView = mountTestComponent();
|
||||
|
||||
@@ -585,11 +594,9 @@ describe("loop.webapp", function() {
|
||||
loop.webapp.UnsupportedDeviceView);
|
||||
});
|
||||
|
||||
it("should mount the unsupportedBrowser view if the sdk detects " +
|
||||
"the browser is unsupported", function() {
|
||||
sdk.checkSystemRequirements = function() {
|
||||
return false;
|
||||
};
|
||||
it("should display the UnsupportedBrowserView for `unsupportedBrowser` window type",
|
||||
function() {
|
||||
standaloneAppStore.setStoreState({windowType: "unsupportedBrowser"});
|
||||
|
||||
var webappRootView = mountTestComponent();
|
||||
|
||||
@@ -597,9 +604,9 @@ describe("loop.webapp", function() {
|
||||
loop.webapp.UnsupportedBrowserView);
|
||||
});
|
||||
|
||||
it("should mount the OutgoingConversationView view if there is a loopToken",
|
||||
it("should display the OutgoingConversationView for `outgoing` window type",
|
||||
function() {
|
||||
conversationModel.set("loopToken", "fakeToken");
|
||||
standaloneAppStore.setStoreState({windowType: "outgoing"});
|
||||
|
||||
var webappRootView = mountTestComponent();
|
||||
|
||||
@@ -607,7 +614,19 @@ describe("loop.webapp", function() {
|
||||
loop.webapp.OutgoingConversationView);
|
||||
});
|
||||
|
||||
it("should mount the Home view there is no loopToken", function() {
|
||||
it("should display the StandaloneRoomView for `room` window type",
|
||||
function() {
|
||||
standaloneAppStore.setStoreState({windowType: "room"});
|
||||
|
||||
var webappRootView = mountTestComponent();
|
||||
|
||||
TestUtils.findRenderedComponentWithType(webappRootView,
|
||||
loop.standaloneRoomViews.StandaloneRoomView);
|
||||
});
|
||||
|
||||
it("should display the HomeView for `home` window type", function() {
|
||||
standaloneAppStore.setStoreState({windowType: "home"});
|
||||
|
||||
var webappRootView = mountTestComponent();
|
||||
|
||||
TestUtils.findRenderedComponentWithType(webappRootView,
|
||||
|
||||
Reference in New Issue
Block a user