Bug 1094128 Convert the Loop Standalone controller app view to be based on the Flux style. r=nperriault

This commit is contained in:
Mark Banner
2014-11-05 14:59:40 +00:00
parent 2925355599
commit 7c6ccf288f
11 changed files with 661 additions and 129 deletions

View File

@@ -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.

View File

@@ -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);
},

View File

@@ -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>

View File

@@ -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;
})();

View File

@@ -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
};
})();

View File

@@ -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
};
})();

View File

@@ -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 {

View File

@@ -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 {

View File

@@ -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>

View File

@@ -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"
}));
});
});
});

View File

@@ -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,