mirror of
https://gitlab.winehq.org/wine/wine-gecko.git
synced 2024-09-13 09:24:08 -07:00
merge fx-team to mozilla-central a=merge
This commit is contained in:
commit
c474a3418d
@ -1613,6 +1613,7 @@ pref("loop.retry_delay.limit", 300000);
|
||||
pref("loop.feedback.baseUrl", "https://input.mozilla.org/api/v1/feedback");
|
||||
pref("loop.feedback.product", "Loop");
|
||||
pref("loop.debug.loglevel", "Error");
|
||||
pref("loop.debug.dispatcher", false);
|
||||
pref("loop.debug.websocket", false);
|
||||
pref("loop.debug.sdk", false);
|
||||
|
||||
|
@ -20,24 +20,22 @@ let gUpdater = {
|
||||
// Find all sites that remain in the grid.
|
||||
let sites = this._findRemainingSites(links);
|
||||
|
||||
let self = this;
|
||||
|
||||
// Remove sites that are no longer in the grid.
|
||||
this._removeLegacySites(sites, function () {
|
||||
this._removeLegacySites(sites, () => {
|
||||
// Freeze all site positions so that we can move their DOM nodes around
|
||||
// without any visual impact.
|
||||
self._freezeSitePositions(sites);
|
||||
this._freezeSitePositions(sites);
|
||||
|
||||
// Move the sites' DOM nodes to their new position in the DOM. This will
|
||||
// have no visual effect as all the sites have been frozen and will
|
||||
// remain in their current position.
|
||||
self._moveSiteNodes(sites);
|
||||
this._moveSiteNodes(sites);
|
||||
|
||||
// Now it's time to animate the sites actually moving to their new
|
||||
// positions.
|
||||
self._rearrangeSites(sites, function () {
|
||||
this._rearrangeSites(sites, () => {
|
||||
// Try to fill empty cells and finish.
|
||||
self._fillEmptyCells(links, aCallback);
|
||||
this._fillEmptyCells(links, aCallback);
|
||||
|
||||
// Update other pages that might be open to keep them synced.
|
||||
gAllPages.update(gPage);
|
||||
|
@ -30,8 +30,14 @@
|
||||
<script type="text/javascript" src="loop/shared/js/mixins.js"></script>
|
||||
<script type="text/javascript" src="loop/shared/js/views.js"></script>
|
||||
<script type="text/javascript" src="loop/shared/js/feedbackApiClient.js"></script>
|
||||
<script type="text/javascript" src="loop/shared/js/actions.js"></script>
|
||||
<script type="text/javascript" src="loop/shared/js/validate.js"></script>
|
||||
<script type="text/javascript" src="loop/shared/js/dispatcher.js"></script>
|
||||
<script type="text/javascript" src="loop/shared/js/conversationStore.js"></script>
|
||||
<script type="text/javascript" src="loop/js/conversationViews.js"></script>
|
||||
<script type="text/javascript" src="loop/shared/js/websocket.js"></script>
|
||||
<script type="text/javascript" src="loop/js/client.js"></script>
|
||||
<script type="text/javascript" src="loop/js/conversationViews.js"></script>
|
||||
<script type="text/javascript" src="loop/js/conversation.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
@ -15,6 +15,12 @@ loop.Client = (function($) {
|
||||
// The expected properties to be returned from the GET /calls request.
|
||||
var expectedCallProperties = ["calls"];
|
||||
|
||||
// THe expected properties to be returned from the POST /calls request.
|
||||
var expectedPostCallProperties = [
|
||||
"apiKey", "callId", "progressURL",
|
||||
"sessionId", "sessionToken", "websocketToken"
|
||||
];
|
||||
|
||||
/**
|
||||
* Loop server client.
|
||||
*
|
||||
@ -209,6 +215,44 @@ loop.Client = (function($) {
|
||||
}.bind(this));
|
||||
},
|
||||
|
||||
/**
|
||||
* Sets up an outgoing call, getting the relevant data from the server.
|
||||
*
|
||||
* Callback parameters:
|
||||
* - err null on successful registration, non-null otherwise.
|
||||
* - result an object of the obtained data for starting the call, if successful
|
||||
*
|
||||
* @param {Array} calleeIds an array of emails and phone numbers.
|
||||
* @param {String} callType the type of call.
|
||||
* @param {Function} cb Callback(err, result)
|
||||
*/
|
||||
setupOutgoingCall: function(calleeIds, callType, cb) {
|
||||
this.mozLoop.hawkRequest(this.mozLoop.LOOP_SESSION_TYPE.FXA,
|
||||
"/calls", "POST", {
|
||||
calleeId: calleeIds,
|
||||
callType: callType
|
||||
},
|
||||
function (err, responseText) {
|
||||
if (err) {
|
||||
this._failureHandler(cb, err);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
var postData = JSON.parse(responseText);
|
||||
|
||||
var outgoingCallData = this._validate(postData,
|
||||
expectedPostCallProperties);
|
||||
|
||||
cb(null, outgoingCallData);
|
||||
} catch (err) {
|
||||
console.log("Error requesting call info", err);
|
||||
cb(err);
|
||||
}
|
||||
}.bind(this)
|
||||
);
|
||||
},
|
||||
|
||||
/**
|
||||
* Adds a value to a telemetry histogram, ignoring errors.
|
||||
*
|
||||
|
@ -11,8 +11,9 @@ var loop = loop || {};
|
||||
loop.conversation = (function(mozL10n) {
|
||||
"use strict";
|
||||
|
||||
var sharedViews = loop.shared.views,
|
||||
sharedModels = loop.shared.models;
|
||||
var sharedViews = loop.shared.views;
|
||||
var sharedModels = loop.shared.models;
|
||||
var OutgoingConversationView = loop.conversationViews.OutgoingConversationView;
|
||||
|
||||
var IncomingCallView = React.createClass({displayName: 'IncomingCallView',
|
||||
|
||||
@ -109,26 +110,23 @@ loop.conversation = (function(mozL10n) {
|
||||
|
||||
render: function() {
|
||||
/* jshint ignore:start */
|
||||
var btnClassAccept = "btn btn-accept";
|
||||
var btnClassDecline = "btn btn-error btn-decline";
|
||||
var conversationPanelClass = "incoming-call";
|
||||
var dropdownMenuClassesDecline = React.addons.classSet({
|
||||
"native-dropdown-menu": true,
|
||||
"conversation-window-dropdown": true,
|
||||
"visually-hidden": !this.state.showDeclineMenu
|
||||
});
|
||||
return (
|
||||
React.DOM.div({className: conversationPanelClass},
|
||||
React.DOM.div({className: "call-window"},
|
||||
React.DOM.h2(null, mozL10n.get("incoming_call_title2")),
|
||||
React.DOM.div({className: "btn-group incoming-call-action-group"},
|
||||
React.DOM.div({className: "btn-group call-action-group"},
|
||||
|
||||
React.DOM.div({className: "fx-embedded-incoming-call-button-spacer"}),
|
||||
React.DOM.div({className: "fx-embedded-call-button-spacer"}),
|
||||
|
||||
React.DOM.div({className: "btn-chevron-menu-group"},
|
||||
React.DOM.div({className: "btn-group-chevron"},
|
||||
React.DOM.div({className: "btn-group"},
|
||||
|
||||
React.DOM.button({className: btnClassDecline,
|
||||
React.DOM.button({className: "btn btn-error btn-decline",
|
||||
onClick: this._handleDecline},
|
||||
mozL10n.get("incoming_call_cancel_button")
|
||||
),
|
||||
@ -146,11 +144,11 @@ loop.conversation = (function(mozL10n) {
|
||||
)
|
||||
),
|
||||
|
||||
React.DOM.div({className: "fx-embedded-incoming-call-button-spacer"}),
|
||||
React.DOM.div({className: "fx-embedded-call-button-spacer"}),
|
||||
|
||||
AcceptCallButton({mode: this._answerModeProps()}),
|
||||
|
||||
React.DOM.div({className: "fx-embedded-incoming-call-button-spacer"})
|
||||
React.DOM.div({className: "fx-embedded-call-button-spacer"})
|
||||
|
||||
)
|
||||
)
|
||||
@ -370,8 +368,10 @@ loop.conversation = (function(mozL10n) {
|
||||
websocketToken: this.props.conversation.get("websocketToken"),
|
||||
callId: this.props.conversation.get("callId"),
|
||||
});
|
||||
this._websocket.promiseConnect().then(function() {
|
||||
this.setState({callStatus: "incoming"});
|
||||
this._websocket.promiseConnect().then(function(progressStatus) {
|
||||
this.setState({
|
||||
callStatus: progressStatus === "terminated" ? "close" : "incoming"
|
||||
});
|
||||
}.bind(this), function() {
|
||||
this._handleSessionError();
|
||||
return;
|
||||
@ -489,6 +489,55 @@ loop.conversation = (function(mozL10n) {
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Master controller view for handling if incoming or outgoing calls are
|
||||
* in progress, and hence, which view to display.
|
||||
*/
|
||||
var ConversationControllerView = React.createClass({displayName: 'ConversationControllerView',
|
||||
propTypes: {
|
||||
// XXX Old types required for incoming call view.
|
||||
client: React.PropTypes.instanceOf(loop.Client).isRequired,
|
||||
conversation: React.PropTypes.instanceOf(sharedModels.ConversationModel)
|
||||
.isRequired,
|
||||
notifications: React.PropTypes.instanceOf(sharedModels.NotificationCollection)
|
||||
.isRequired,
|
||||
sdk: React.PropTypes.object.isRequired,
|
||||
|
||||
// XXX New types for OutgoingConversationView
|
||||
store: React.PropTypes.instanceOf(loop.store.ConversationStore).isRequired
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
return this.props.store.attributes;
|
||||
},
|
||||
|
||||
componentWillMount: function() {
|
||||
this.props.store.on("change:outgoing", function() {
|
||||
this.setState(this.props.store.attributes);
|
||||
}, this);
|
||||
},
|
||||
|
||||
render: function() {
|
||||
// Don't display anything, until we know what type of call we are.
|
||||
if (this.state.outgoing === undefined) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (this.state.outgoing) {
|
||||
return (OutgoingConversationView({
|
||||
store: this.props.store}
|
||||
));
|
||||
}
|
||||
|
||||
return (IncomingConversationView({
|
||||
client: this.props.client,
|
||||
conversation: this.props.conversation,
|
||||
notifications: this.props.notifications,
|
||||
sdk: this.props.sdk}
|
||||
));
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Panel initialisation.
|
||||
*/
|
||||
@ -509,36 +558,57 @@ loop.conversation = (function(mozL10n) {
|
||||
}
|
||||
});
|
||||
|
||||
document.body.classList.add(loop.shared.utils.getTargetPlatform());
|
||||
|
||||
var dispatcher = new loop.Dispatcher();
|
||||
var client = new loop.Client();
|
||||
var conversationStore = new loop.store.ConversationStore({}, {
|
||||
client: client,
|
||||
dispatcher: dispatcher
|
||||
});
|
||||
|
||||
// XXX For now key this on the pref, but this should really be
|
||||
// set by the information from the mozLoop API when we can get it (bug 1072323).
|
||||
var outgoingEmail = navigator.mozLoop.getLoopCharPref("outgoingemail");
|
||||
|
||||
// XXX Old class creation for the incoming conversation view, whilst
|
||||
// we transition across (bug 1072323).
|
||||
var conversation = new sharedModels.ConversationModel(
|
||||
{}, // Model attributes
|
||||
{sdk: window.OT} // Model dependencies
|
||||
);
|
||||
var notifications = new sharedModels.NotificationCollection();
|
||||
|
||||
// Obtain the callId and pass it through
|
||||
var helper = new loop.shared.utils.Helper();
|
||||
var locationHash = helper.locationHash();
|
||||
var callId;
|
||||
if (locationHash) {
|
||||
callId = locationHash.match(/\#incoming\/(.*)/)[1]
|
||||
conversation.set("callId", callId);
|
||||
}
|
||||
|
||||
window.addEventListener("unload", function(event) {
|
||||
// Handle direct close of dialog box via [x] control.
|
||||
navigator.mozLoop.releaseCallData(conversation.get("callId"));
|
||||
});
|
||||
|
||||
// Obtain the callId and pass it to the conversation
|
||||
var helper = new loop.shared.utils.Helper();
|
||||
var locationHash = helper.locationHash();
|
||||
if (locationHash) {
|
||||
conversation.set("callId", locationHash.match(/\#incoming\/(.*)/)[1]);
|
||||
}
|
||||
document.body.classList.add(loop.shared.utils.getTargetPlatform());
|
||||
|
||||
React.renderComponent(IncomingConversationView({
|
||||
React.renderComponent(ConversationControllerView({
|
||||
store: conversationStore,
|
||||
client: client,
|
||||
conversation: conversation,
|
||||
notifications: notifications,
|
||||
sdk: window.OT}
|
||||
), document.querySelector('#main'));
|
||||
|
||||
dispatcher.dispatch(new loop.shared.actions.GatherCallData({
|
||||
callId: callId,
|
||||
calleeId: outgoingEmail
|
||||
}));
|
||||
}
|
||||
|
||||
return {
|
||||
ConversationControllerView: ConversationControllerView,
|
||||
IncomingConversationView: IncomingConversationView,
|
||||
IncomingCallView: IncomingCallView,
|
||||
init: init
|
||||
|
@ -11,8 +11,9 @@ var loop = loop || {};
|
||||
loop.conversation = (function(mozL10n) {
|
||||
"use strict";
|
||||
|
||||
var sharedViews = loop.shared.views,
|
||||
sharedModels = loop.shared.models;
|
||||
var sharedViews = loop.shared.views;
|
||||
var sharedModels = loop.shared.models;
|
||||
var OutgoingConversationView = loop.conversationViews.OutgoingConversationView;
|
||||
|
||||
var IncomingCallView = React.createClass({
|
||||
|
||||
@ -109,26 +110,23 @@ loop.conversation = (function(mozL10n) {
|
||||
|
||||
render: function() {
|
||||
/* jshint ignore:start */
|
||||
var btnClassAccept = "btn btn-accept";
|
||||
var btnClassDecline = "btn btn-error btn-decline";
|
||||
var conversationPanelClass = "incoming-call";
|
||||
var dropdownMenuClassesDecline = React.addons.classSet({
|
||||
"native-dropdown-menu": true,
|
||||
"conversation-window-dropdown": true,
|
||||
"visually-hidden": !this.state.showDeclineMenu
|
||||
});
|
||||
return (
|
||||
<div className={conversationPanelClass}>
|
||||
<div className="call-window">
|
||||
<h2>{mozL10n.get("incoming_call_title2")}</h2>
|
||||
<div className="btn-group incoming-call-action-group">
|
||||
<div className="btn-group call-action-group">
|
||||
|
||||
<div className="fx-embedded-incoming-call-button-spacer"></div>
|
||||
<div className="fx-embedded-call-button-spacer"></div>
|
||||
|
||||
<div className="btn-chevron-menu-group">
|
||||
<div className="btn-group-chevron">
|
||||
<div className="btn-group">
|
||||
|
||||
<button className={btnClassDecline}
|
||||
<button className="btn btn-error btn-decline"
|
||||
onClick={this._handleDecline}>
|
||||
{mozL10n.get("incoming_call_cancel_button")}
|
||||
</button>
|
||||
@ -146,11 +144,11 @@ loop.conversation = (function(mozL10n) {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="fx-embedded-incoming-call-button-spacer"></div>
|
||||
<div className="fx-embedded-call-button-spacer"></div>
|
||||
|
||||
<AcceptCallButton mode={this._answerModeProps()} />
|
||||
|
||||
<div className="fx-embedded-incoming-call-button-spacer"></div>
|
||||
<div className="fx-embedded-call-button-spacer"></div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
@ -370,8 +368,10 @@ loop.conversation = (function(mozL10n) {
|
||||
websocketToken: this.props.conversation.get("websocketToken"),
|
||||
callId: this.props.conversation.get("callId"),
|
||||
});
|
||||
this._websocket.promiseConnect().then(function() {
|
||||
this.setState({callStatus: "incoming"});
|
||||
this._websocket.promiseConnect().then(function(progressStatus) {
|
||||
this.setState({
|
||||
callStatus: progressStatus === "terminated" ? "close" : "incoming"
|
||||
});
|
||||
}.bind(this), function() {
|
||||
this._handleSessionError();
|
||||
return;
|
||||
@ -489,6 +489,55 @@ loop.conversation = (function(mozL10n) {
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Master controller view for handling if incoming or outgoing calls are
|
||||
* in progress, and hence, which view to display.
|
||||
*/
|
||||
var ConversationControllerView = React.createClass({
|
||||
propTypes: {
|
||||
// XXX Old types required for incoming call view.
|
||||
client: React.PropTypes.instanceOf(loop.Client).isRequired,
|
||||
conversation: React.PropTypes.instanceOf(sharedModels.ConversationModel)
|
||||
.isRequired,
|
||||
notifications: React.PropTypes.instanceOf(sharedModels.NotificationCollection)
|
||||
.isRequired,
|
||||
sdk: React.PropTypes.object.isRequired,
|
||||
|
||||
// XXX New types for OutgoingConversationView
|
||||
store: React.PropTypes.instanceOf(loop.store.ConversationStore).isRequired
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
return this.props.store.attributes;
|
||||
},
|
||||
|
||||
componentWillMount: function() {
|
||||
this.props.store.on("change:outgoing", function() {
|
||||
this.setState(this.props.store.attributes);
|
||||
}, this);
|
||||
},
|
||||
|
||||
render: function() {
|
||||
// Don't display anything, until we know what type of call we are.
|
||||
if (this.state.outgoing === undefined) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (this.state.outgoing) {
|
||||
return (<OutgoingConversationView
|
||||
store={this.props.store}
|
||||
/>);
|
||||
}
|
||||
|
||||
return (<IncomingConversationView
|
||||
client={this.props.client}
|
||||
conversation={this.props.conversation}
|
||||
notifications={this.props.notifications}
|
||||
sdk={this.props.sdk}
|
||||
/>);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Panel initialisation.
|
||||
*/
|
||||
@ -509,36 +558,57 @@ loop.conversation = (function(mozL10n) {
|
||||
}
|
||||
});
|
||||
|
||||
document.body.classList.add(loop.shared.utils.getTargetPlatform());
|
||||
|
||||
var dispatcher = new loop.Dispatcher();
|
||||
var client = new loop.Client();
|
||||
var conversationStore = new loop.store.ConversationStore({}, {
|
||||
client: client,
|
||||
dispatcher: dispatcher
|
||||
});
|
||||
|
||||
// XXX For now key this on the pref, but this should really be
|
||||
// set by the information from the mozLoop API when we can get it (bug 1072323).
|
||||
var outgoingEmail = navigator.mozLoop.getLoopCharPref("outgoingemail");
|
||||
|
||||
// XXX Old class creation for the incoming conversation view, whilst
|
||||
// we transition across (bug 1072323).
|
||||
var conversation = new sharedModels.ConversationModel(
|
||||
{}, // Model attributes
|
||||
{sdk: window.OT} // Model dependencies
|
||||
);
|
||||
var notifications = new sharedModels.NotificationCollection();
|
||||
|
||||
// Obtain the callId and pass it through
|
||||
var helper = new loop.shared.utils.Helper();
|
||||
var locationHash = helper.locationHash();
|
||||
var callId;
|
||||
if (locationHash) {
|
||||
callId = locationHash.match(/\#incoming\/(.*)/)[1]
|
||||
conversation.set("callId", callId);
|
||||
}
|
||||
|
||||
window.addEventListener("unload", function(event) {
|
||||
// Handle direct close of dialog box via [x] control.
|
||||
navigator.mozLoop.releaseCallData(conversation.get("callId"));
|
||||
});
|
||||
|
||||
// Obtain the callId and pass it to the conversation
|
||||
var helper = new loop.shared.utils.Helper();
|
||||
var locationHash = helper.locationHash();
|
||||
if (locationHash) {
|
||||
conversation.set("callId", locationHash.match(/\#incoming\/(.*)/)[1]);
|
||||
}
|
||||
document.body.classList.add(loop.shared.utils.getTargetPlatform());
|
||||
|
||||
React.renderComponent(<IncomingConversationView
|
||||
React.renderComponent(<ConversationControllerView
|
||||
store={conversationStore}
|
||||
client={client}
|
||||
conversation={conversation}
|
||||
notifications={notifications}
|
||||
sdk={window.OT}
|
||||
/>, document.querySelector('#main'));
|
||||
|
||||
dispatcher.dispatch(new loop.shared.actions.GatherCallData({
|
||||
callId: callId,
|
||||
calleeId: outgoingEmail
|
||||
}));
|
||||
}
|
||||
|
||||
return {
|
||||
ConversationControllerView: ConversationControllerView,
|
||||
IncomingConversationView: IncomingConversationView,
|
||||
IncomingCallView: IncomingCallView,
|
||||
init: init
|
||||
|
126
browser/components/loop/content/js/conversationViews.js
Normal file
126
browser/components/loop/content/js/conversationViews.js
Normal file
@ -0,0 +1,126 @@
|
||||
/** @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.conversationViews = (function(mozL10n) {
|
||||
|
||||
var CALL_STATES = loop.store.CALL_STATES;
|
||||
|
||||
/**
|
||||
* Displays details of the incoming/outgoing conversation
|
||||
* (name, link, audio/video type etc).
|
||||
*
|
||||
* Allows the view to be extended with different buttons and progress
|
||||
* via children properties.
|
||||
*/
|
||||
var ConversationDetailView = React.createClass({displayName: 'ConversationDetailView',
|
||||
propTypes: {
|
||||
calleeId: React.PropTypes.string,
|
||||
},
|
||||
|
||||
render: function() {
|
||||
document.title = this.props.calleeId;
|
||||
|
||||
return (
|
||||
React.DOM.div({className: "call-window"},
|
||||
React.DOM.h2(null, this.props.calleeId),
|
||||
React.DOM.div(null, this.props.children)
|
||||
)
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* View for pending conversations. Displays a cancel button and appropriate
|
||||
* pending/ringing strings.
|
||||
*/
|
||||
var PendingConversationView = React.createClass({displayName: 'PendingConversationView',
|
||||
propTypes: {
|
||||
callState: React.PropTypes.string,
|
||||
calleeId: React.PropTypes.string,
|
||||
},
|
||||
|
||||
render: function() {
|
||||
var pendingStateString;
|
||||
if (this.props.callState === CALL_STATES.ALERTING) {
|
||||
pendingStateString = mozL10n.get("call_progress_ringing_description");
|
||||
} else {
|
||||
pendingStateString = mozL10n.get("call_progress_connecting_description");
|
||||
}
|
||||
|
||||
return (
|
||||
ConversationDetailView({calleeId: this.props.calleeId},
|
||||
|
||||
React.DOM.p({className: "btn-label"}, pendingStateString),
|
||||
|
||||
React.DOM.div({className: "btn-group call-action-group"},
|
||||
React.DOM.div({className: "fx-embedded-call-button-spacer"}),
|
||||
React.DOM.button({className: "btn btn-cancel"},
|
||||
mozL10n.get("initiate_call_cancel_button")
|
||||
),
|
||||
React.DOM.div({className: "fx-embedded-call-button-spacer"})
|
||||
)
|
||||
|
||||
)
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Call failed view. Displayed when a call fails.
|
||||
*/
|
||||
var CallFailedView = React.createClass({displayName: 'CallFailedView',
|
||||
render: function() {
|
||||
return (
|
||||
React.DOM.div({className: "call-window"},
|
||||
React.DOM.h2(null, mozL10n.get("generic_failure_title"))
|
||||
)
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Master View Controller for outgoing calls. This manages
|
||||
* the different views that need displaying.
|
||||
*/
|
||||
var OutgoingConversationView = React.createClass({displayName: 'OutgoingConversationView',
|
||||
propTypes: {
|
||||
store: React.PropTypes.instanceOf(
|
||||
loop.store.ConversationStore).isRequired
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
return this.props.store.attributes;
|
||||
},
|
||||
|
||||
componentWillMount: function() {
|
||||
this.props.store.on("change", function() {
|
||||
this.setState(this.props.store.attributes);
|
||||
}, this);
|
||||
},
|
||||
|
||||
render: function() {
|
||||
if (this.state.callState === CALL_STATES.TERMINATED) {
|
||||
return (CallFailedView(null));
|
||||
}
|
||||
|
||||
return (PendingConversationView({
|
||||
callState: this.state.callState,
|
||||
calleeId: this.state.calleeId}
|
||||
))
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
PendingConversationView: PendingConversationView,
|
||||
ConversationDetailView: ConversationDetailView,
|
||||
CallFailedView: CallFailedView,
|
||||
OutgoingConversationView: OutgoingConversationView
|
||||
};
|
||||
|
||||
})(document.mozL10n || navigator.mozL10n);
|
126
browser/components/loop/content/js/conversationViews.jsx
Normal file
126
browser/components/loop/content/js/conversationViews.jsx
Normal file
@ -0,0 +1,126 @@
|
||||
/** @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.conversationViews = (function(mozL10n) {
|
||||
|
||||
var CALL_STATES = loop.store.CALL_STATES;
|
||||
|
||||
/**
|
||||
* Displays details of the incoming/outgoing conversation
|
||||
* (name, link, audio/video type etc).
|
||||
*
|
||||
* Allows the view to be extended with different buttons and progress
|
||||
* via children properties.
|
||||
*/
|
||||
var ConversationDetailView = React.createClass({
|
||||
propTypes: {
|
||||
calleeId: React.PropTypes.string,
|
||||
},
|
||||
|
||||
render: function() {
|
||||
document.title = this.props.calleeId;
|
||||
|
||||
return (
|
||||
<div className="call-window">
|
||||
<h2>{this.props.calleeId}</h2>
|
||||
<div>{this.props.children}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* View for pending conversations. Displays a cancel button and appropriate
|
||||
* pending/ringing strings.
|
||||
*/
|
||||
var PendingConversationView = React.createClass({
|
||||
propTypes: {
|
||||
callState: React.PropTypes.string,
|
||||
calleeId: React.PropTypes.string,
|
||||
},
|
||||
|
||||
render: function() {
|
||||
var pendingStateString;
|
||||
if (this.props.callState === CALL_STATES.ALERTING) {
|
||||
pendingStateString = mozL10n.get("call_progress_ringing_description");
|
||||
} else {
|
||||
pendingStateString = mozL10n.get("call_progress_connecting_description");
|
||||
}
|
||||
|
||||
return (
|
||||
<ConversationDetailView calleeId={this.props.calleeId}>
|
||||
|
||||
<p className="btn-label">{pendingStateString}</p>
|
||||
|
||||
<div className="btn-group call-action-group">
|
||||
<div className="fx-embedded-call-button-spacer"></div>
|
||||
<button className="btn btn-cancel">
|
||||
{mozL10n.get("initiate_call_cancel_button")}
|
||||
</button>
|
||||
<div className="fx-embedded-call-button-spacer"></div>
|
||||
</div>
|
||||
|
||||
</ConversationDetailView>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Call failed view. Displayed when a call fails.
|
||||
*/
|
||||
var CallFailedView = React.createClass({
|
||||
render: function() {
|
||||
return (
|
||||
<div className="call-window">
|
||||
<h2>{mozL10n.get("generic_failure_title")}</h2>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Master View Controller for outgoing calls. This manages
|
||||
* the different views that need displaying.
|
||||
*/
|
||||
var OutgoingConversationView = React.createClass({
|
||||
propTypes: {
|
||||
store: React.PropTypes.instanceOf(
|
||||
loop.store.ConversationStore).isRequired
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
return this.props.store.attributes;
|
||||
},
|
||||
|
||||
componentWillMount: function() {
|
||||
this.props.store.on("change", function() {
|
||||
this.setState(this.props.store.attributes);
|
||||
}, this);
|
||||
},
|
||||
|
||||
render: function() {
|
||||
if (this.state.callState === CALL_STATES.TERMINATED) {
|
||||
return (<CallFailedView />);
|
||||
}
|
||||
|
||||
return (<PendingConversationView
|
||||
callState={this.state.callState}
|
||||
calleeId={this.state.calleeId}
|
||||
/>)
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
PendingConversationView: PendingConversationView,
|
||||
ConversationDetailView: ConversationDetailView,
|
||||
CallFailedView: CallFailedView,
|
||||
OutgoingConversationView: OutgoingConversationView
|
||||
};
|
||||
|
||||
})(document.mozL10n || navigator.mozL10n);
|
@ -223,14 +223,14 @@
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* Incoming call */
|
||||
/* General Call (incoming or outgoing). */
|
||||
|
||||
/*
|
||||
* Height matches the height of the docked window
|
||||
* but the UI breaks when you pop out
|
||||
* Bug 1040985
|
||||
*/
|
||||
.incoming-call {
|
||||
.call-window {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
@ -238,26 +238,27 @@
|
||||
min-height: 230px;
|
||||
}
|
||||
|
||||
.incoming-call-action-group {
|
||||
.call-action-group {
|
||||
display: flex;
|
||||
padding: 2.5em 0 0 0;
|
||||
width: 100%;
|
||||
justify-content: space-around;
|
||||
}
|
||||
|
||||
.incoming-call-action-group > .btn {
|
||||
.call-action-group > .btn {
|
||||
margin-left: .5em;
|
||||
height: 26px;
|
||||
}
|
||||
|
||||
.incoming-call-action-group .btn-group-chevron,
|
||||
.incoming-call-action-group .btn-group {
|
||||
.call-action-group .btn-group-chevron,
|
||||
.call-action-group .btn-group {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* XXX Once we get the incoming call avatar, bug 1047435, the H2 should
|
||||
* disappear from our markup, and we should remove this rule entirely.
|
||||
*/
|
||||
.incoming-call h2 {
|
||||
.call-window h2 {
|
||||
font-size: 1.5em;
|
||||
font-weight: normal;
|
||||
|
||||
@ -266,7 +267,7 @@
|
||||
margin: 0.83em 0;
|
||||
}
|
||||
|
||||
.fx-embedded-incoming-call-button-spacer {
|
||||
.fx-embedded-call-button-spacer {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
}
|
||||
|
78
browser/components/loop/content/shared/js/actions.js
Normal file
78
browser/components/loop/content/shared/js/actions.js
Normal file
@ -0,0 +1,78 @@
|
||||
/* 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.shared = loop.shared || {};
|
||||
loop.shared.actions = (function() {
|
||||
"use strict";
|
||||
|
||||
/**
|
||||
* Actions are events that are triggered by the user, e.g. clicking a button,
|
||||
* or by an async event, e.g. status received.
|
||||
*
|
||||
* They should be dispatched to stores via the dispatcher.
|
||||
*/
|
||||
|
||||
function Action(name, schema, values) {
|
||||
var validatedData = new loop.validate.Validator(schema || {})
|
||||
.validate(values || {});
|
||||
for (var prop in validatedData)
|
||||
this[prop] = validatedData[prop];
|
||||
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
Action.define = function(name, schema) {
|
||||
return Action.bind(null, name, schema);
|
||||
};
|
||||
|
||||
return {
|
||||
/**
|
||||
* Used to trigger gathering of initial call data.
|
||||
*/
|
||||
GatherCallData: Action.define("gatherCallData", {
|
||||
// XXX This may change when bug 1072323 is implemented.
|
||||
// Optional: Specify the calleeId for an outgoing call
|
||||
calleeId: [String, null],
|
||||
// Specify the callId for an incoming call.
|
||||
callId: [String, null]
|
||||
}),
|
||||
|
||||
/**
|
||||
* Used to cancel call setup.
|
||||
*/
|
||||
CancelCall: Action.define("cancelCall", {
|
||||
}),
|
||||
|
||||
/**
|
||||
* Used to initiate connecting of a call with the relevant
|
||||
* sessionData.
|
||||
*/
|
||||
ConnectCall: Action.define("connectCall", {
|
||||
// This object contains the necessary details for the
|
||||
// connection of the websocket, and the SDK
|
||||
sessionData: Object
|
||||
}),
|
||||
|
||||
/**
|
||||
* Used for notifying of connection progress state changes.
|
||||
* The connection refers to the overall connection flow as indicated
|
||||
* on the websocket.
|
||||
*/
|
||||
ConnectionProgress: Action.define("connectionProgress", {
|
||||
// The new connection state
|
||||
state: String
|
||||
}),
|
||||
|
||||
/**
|
||||
* Used for notifying of connection failures.
|
||||
*/
|
||||
ConnectionFailure: Action.define("connectionFailure", {
|
||||
// A string relating to the reason the connection failed.
|
||||
reason: String
|
||||
})
|
||||
};
|
||||
})();
|
244
browser/components/loop/content/shared/js/conversationStore.js
Normal file
244
browser/components/loop/content/shared/js/conversationStore.js
Normal file
@ -0,0 +1,244 @@
|
||||
/* 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 = (function() {
|
||||
|
||||
var sharedActions = loop.shared.actions;
|
||||
var sharedUtils = loop.shared.utils;
|
||||
|
||||
var CALL_STATES = {
|
||||
// The initial state of the view.
|
||||
INIT: "init",
|
||||
// The store is gathering the call data from the server.
|
||||
GATHER: "gather",
|
||||
// The websocket has connected to the server and is waiting
|
||||
// for the other peer to connect to the websocket.
|
||||
CONNECTING: "connecting",
|
||||
// The websocket has received information that we're now alerting
|
||||
// the peer.
|
||||
ALERTING: "alerting",
|
||||
// The call was terminated due to an issue during connection.
|
||||
TERMINATED: "terminated"
|
||||
};
|
||||
|
||||
|
||||
var ConversationStore = Backbone.Model.extend({
|
||||
defaults: {
|
||||
// The current state of the call
|
||||
callState: CALL_STATES.INIT,
|
||||
// The reason if a call was terminated
|
||||
callStateReason: undefined,
|
||||
// The error information, if there was a failure
|
||||
error: undefined,
|
||||
// True if the call is outgoing, false if not, undefined if unknown
|
||||
outgoing: undefined,
|
||||
// The id of the person being called for outgoing calls
|
||||
calleeId: undefined,
|
||||
// The call type for the call.
|
||||
// XXX Don't hard-code, this comes from the data in bug 1072323
|
||||
callType: sharedUtils.CALL_TYPES.AUDIO_VIDEO,
|
||||
|
||||
// Call Connection information
|
||||
// The call id from the loop-server
|
||||
callId: undefined,
|
||||
// The connection progress url to connect the websocket
|
||||
progressURL: undefined,
|
||||
// The websocket token that allows connection to the progress url
|
||||
websocketToken: undefined,
|
||||
// SDK API key
|
||||
apiKey: undefined,
|
||||
// SDK session ID
|
||||
sessionId: undefined,
|
||||
// SDK session token
|
||||
sessionToken: undefined
|
||||
},
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* Options:
|
||||
* - {loop.Dispatcher} dispatcher The dispatcher for dispatching actions and
|
||||
* registering to consume actions.
|
||||
* - {Object} client A client object for communicating with the server.
|
||||
*
|
||||
* @param {Object} attributes Attributes object.
|
||||
* @param {Object} options Options object.
|
||||
*/
|
||||
initialize: function(attributes, options) {
|
||||
options = options || {};
|
||||
|
||||
if (!options.dispatcher) {
|
||||
throw new Error("Missing option dispatcher");
|
||||
}
|
||||
if (!options.client) {
|
||||
throw new Error("Missing option client");
|
||||
}
|
||||
|
||||
this.client = options.client;
|
||||
this.dispatcher = options.dispatcher;
|
||||
|
||||
this.dispatcher.register(this, [
|
||||
"connectionFailure",
|
||||
"connectionProgress",
|
||||
"gatherCallData",
|
||||
"connectCall"
|
||||
]);
|
||||
},
|
||||
|
||||
/**
|
||||
* Handles the connection failure action, setting the state to
|
||||
* terminated.
|
||||
*
|
||||
* @param {sharedActions.ConnectionFailure} actionData The action data.
|
||||
*/
|
||||
connectionFailure: function(actionData) {
|
||||
this.set({
|
||||
callState: CALL_STATES.TERMINATED,
|
||||
callStateReason: actionData.reason
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Handles the connection progress action, setting the next state
|
||||
* appropriately.
|
||||
*
|
||||
* @param {sharedActions.ConnectionProgress} actionData The action data.
|
||||
*/
|
||||
connectionProgress: function(actionData) {
|
||||
// XXX Turn this into a state machine?
|
||||
if (actionData.state === "alerting" &&
|
||||
(this.get("callState") === CALL_STATES.CONNECTING ||
|
||||
this.get("callState") === CALL_STATES.GATHER)) {
|
||||
this.set({
|
||||
callState: CALL_STATES.ALERTING
|
||||
});
|
||||
}
|
||||
if (actionData.state === "connecting" &&
|
||||
this.get("callState") === CALL_STATES.GATHER) {
|
||||
this.set({
|
||||
callState: CALL_STATES.CONNECTING
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Handles the gather call data action, setting the state
|
||||
* and starting to get the appropriate data for the type of call.
|
||||
*
|
||||
* @param {sharedActions.GatherCallData} actionData The action data.
|
||||
*/
|
||||
gatherCallData: function(actionData) {
|
||||
this.set({
|
||||
calleeId: actionData.calleeId,
|
||||
outgoing: !!actionData.calleeId,
|
||||
callId: actionData.callId,
|
||||
callState: CALL_STATES.GATHER
|
||||
});
|
||||
|
||||
if (this.get("outgoing")) {
|
||||
this._setupOutgoingCall();
|
||||
} // XXX Else, other types aren't supported yet.
|
||||
},
|
||||
|
||||
/**
|
||||
* Handles the connect call action, this saves the appropriate
|
||||
* data and starts the connection for the websocket to notify the
|
||||
* server of progress.
|
||||
*
|
||||
* @param {sharedActions.ConnectCall} actionData The action data.
|
||||
*/
|
||||
connectCall: function(actionData) {
|
||||
this.set(actionData.sessionData);
|
||||
this._connectWebSocket();
|
||||
},
|
||||
|
||||
/**
|
||||
* Obtains the outgoing call data from the server and handles the
|
||||
* result.
|
||||
*/
|
||||
_setupOutgoingCall: function() {
|
||||
// XXX For now, we only have one calleeId, so just wrap that in an array.
|
||||
this.client.setupOutgoingCall([this.get("calleeId")],
|
||||
this.get("callType"),
|
||||
function(err, result) {
|
||||
if (err) {
|
||||
console.error("Failed to get outgoing call data", err);
|
||||
this.dispatcher.dispatch(
|
||||
new sharedActions.ConnectionFailure({reason: "setup"}));
|
||||
return;
|
||||
}
|
||||
|
||||
// Success, dispatch a new action.
|
||||
this.dispatcher.dispatch(
|
||||
new sharedActions.ConnectCall({sessionData: result}));
|
||||
}.bind(this)
|
||||
);
|
||||
},
|
||||
|
||||
/**
|
||||
* Sets up and connects the websocket to the server. The websocket
|
||||
* deals with sending and obtaining status via the server about the
|
||||
* setup of the call.
|
||||
*/
|
||||
_connectWebSocket: function() {
|
||||
this._websocket = new loop.CallConnectionWebSocket({
|
||||
url: this.get("progressURL"),
|
||||
callId: this.get("callId"),
|
||||
websocketToken: this.get("websocketToken")
|
||||
});
|
||||
|
||||
this._websocket.promiseConnect().then(
|
||||
function() {
|
||||
this.dispatcher.dispatch(new sharedActions.ConnectionProgress({
|
||||
// This is the websocket call state, i.e. waiting for the
|
||||
// other end to connect to the server.
|
||||
state: "connecting"
|
||||
}));
|
||||
}.bind(this),
|
||||
function(error) {
|
||||
console.error("Websocket failed to connect", error);
|
||||
this.dispatcher.dispatch(new sharedActions.ConnectionFailure({
|
||||
reason: "websocket-setup"
|
||||
}));
|
||||
}.bind(this)
|
||||
);
|
||||
|
||||
this._websocket.on("progress", this._handleWebSocketProgress, this);
|
||||
},
|
||||
|
||||
/**
|
||||
* Used to handle any progressed received from the websocket. This will
|
||||
* dispatch new actions so that the data can be handled appropriately.
|
||||
*/
|
||||
_handleWebSocketProgress: function(progressData) {
|
||||
var action;
|
||||
|
||||
switch(progressData.state) {
|
||||
case "terminated":
|
||||
action = new sharedActions.ConnectionFailure({
|
||||
reason: progressData.reason
|
||||
});
|
||||
break;
|
||||
case "alerting":
|
||||
action = new sharedActions.ConnectionProgress({
|
||||
state: progressData.state
|
||||
});
|
||||
break;
|
||||
default:
|
||||
console.warn("Received unexpected state in _handleWebSocketProgress", progressData.state);
|
||||
return;
|
||||
}
|
||||
|
||||
this.dispatcher.dispatch(action);
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
CALL_STATES: CALL_STATES,
|
||||
ConversationStore: ConversationStore
|
||||
};
|
||||
})();
|
84
browser/components/loop/content/shared/js/dispatcher.js
Normal file
84
browser/components/loop/content/shared/js/dispatcher.js
Normal file
@ -0,0 +1,84 @@
|
||||
/* 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 dispatcher for actions. This dispatches actions to stores registered
|
||||
* for those actions.
|
||||
*
|
||||
* If stores need to perform async operations for actions, they should return
|
||||
* straight away, and set up a new action for the changes if necessary.
|
||||
*
|
||||
* It is an error if a returned promise rejects - they should always pass.
|
||||
*/
|
||||
var loop = loop || {};
|
||||
loop.Dispatcher = (function() {
|
||||
|
||||
function Dispatcher() {
|
||||
this._eventData = {};
|
||||
this._actionQueue = [];
|
||||
this._debug = loop.shared.utils.getBoolPreference("debug.dispatcher");
|
||||
}
|
||||
|
||||
Dispatcher.prototype = {
|
||||
/**
|
||||
* Register a store to receive notifications of specific actions.
|
||||
*
|
||||
* @param {Object} store The store object to register
|
||||
* @param {Array} eventTypes An array of action names
|
||||
*/
|
||||
register: function(store, eventTypes) {
|
||||
eventTypes.forEach(function(type) {
|
||||
if (this._eventData.hasOwnProperty(type)) {
|
||||
this._eventData[type].push(store);
|
||||
} else {
|
||||
this._eventData[type] = [store];
|
||||
}
|
||||
}.bind(this));
|
||||
},
|
||||
|
||||
/**
|
||||
* Dispatches an action to all registered stores.
|
||||
*/
|
||||
dispatch: function(action) {
|
||||
// Always put it on the queue, to make it simpler.
|
||||
this._actionQueue.push(action);
|
||||
this._dispatchNextAction();
|
||||
},
|
||||
|
||||
/**
|
||||
* Dispatches the next action in the queue if one is not already active.
|
||||
*/
|
||||
_dispatchNextAction: function() {
|
||||
if (!this._actionQueue.length || this._active) {
|
||||
return;
|
||||
}
|
||||
|
||||
var action = this._actionQueue.shift();
|
||||
var type = action.name;
|
||||
|
||||
var registeredStores = this._eventData[type];
|
||||
if (!registeredStores) {
|
||||
console.warn("No stores registered for event type ", type);
|
||||
return;
|
||||
}
|
||||
|
||||
this._active = true;
|
||||
|
||||
if (this._debug) {
|
||||
console.log("[Dispatcher] Dispatching action", action);
|
||||
}
|
||||
|
||||
registeredStores.forEach(function(store) {
|
||||
store[type](action);
|
||||
});
|
||||
|
||||
this._active = false;
|
||||
this._dispatchNextAction();
|
||||
}
|
||||
};
|
||||
|
||||
return Dispatcher;
|
||||
})();
|
@ -9,6 +9,14 @@ loop.shared = loop.shared || {};
|
||||
loop.shared.utils = (function() {
|
||||
"use strict";
|
||||
|
||||
/**
|
||||
* Call types used for determining if a call is audio/video or audio-only.
|
||||
*/
|
||||
var CALL_TYPES = {
|
||||
AUDIO_VIDEO: "audio-video",
|
||||
AUDIO_ONLY: "audio"
|
||||
};
|
||||
|
||||
/**
|
||||
* Used for adding different styles to the panel
|
||||
* @returns {String} Corresponds to the client platform
|
||||
@ -77,6 +85,7 @@ loop.shared.utils = (function() {
|
||||
};
|
||||
|
||||
return {
|
||||
CALL_TYPES: CALL_TYPES,
|
||||
Helper: Helper,
|
||||
getTargetPlatform: getTargetPlatform,
|
||||
getBoolPreference: getBoolPreference
|
||||
|
127
browser/components/loop/content/shared/js/validate.js
Normal file
127
browser/components/loop/content/shared/js/validate.js
Normal file
@ -0,0 +1,127 @@
|
||||
/* 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/. */
|
||||
|
||||
/* jshint unused:false */
|
||||
|
||||
var loop = loop || {};
|
||||
loop.validate = (function() {
|
||||
"use strict";
|
||||
|
||||
/**
|
||||
* Computes the difference between two arrays.
|
||||
*
|
||||
* @param {Array} arr1 First array
|
||||
* @param {Array} arr2 Second array
|
||||
* @return {Array} Array difference
|
||||
*/
|
||||
function difference(arr1, arr2) {
|
||||
return arr1.filter(function(item) {
|
||||
return arr2.indexOf(item) === -1;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the type name of an object or constructor. Fallback to "unknown"
|
||||
* when it fails.
|
||||
*
|
||||
* @param {Object} obj
|
||||
* @return {String}
|
||||
*/
|
||||
function typeName(obj) {
|
||||
if (obj === null)
|
||||
return "null";
|
||||
if (typeof obj === "function")
|
||||
return obj.name || obj.toString().match(/^function\s?([^\s(]*)/)[1];
|
||||
if (typeof obj.constructor === "function")
|
||||
return typeName(obj.constructor);
|
||||
return "unknown";
|
||||
}
|
||||
|
||||
/**
|
||||
* Simple typed values validator.
|
||||
*
|
||||
* @constructor
|
||||
* @param {Object} schema Validation schema
|
||||
*/
|
||||
function Validator(schema) {
|
||||
this.schema = schema || {};
|
||||
}
|
||||
|
||||
Validator.prototype = {
|
||||
/**
|
||||
* Validates all passed values against declared dependencies.
|
||||
*
|
||||
* @param {Object} values The values object
|
||||
* @return {Object} The validated values object
|
||||
* @throws {TypeError} If validation fails
|
||||
*/
|
||||
validate: function(values) {
|
||||
this._checkRequiredProperties(values);
|
||||
this._checkRequiredTypes(values);
|
||||
return values;
|
||||
},
|
||||
|
||||
/**
|
||||
* Checks if any of Object values matches any of current dependency type
|
||||
* requirements.
|
||||
*
|
||||
* @param {Object} values The values object
|
||||
* @throws {TypeError}
|
||||
*/
|
||||
_checkRequiredTypes: function(values) {
|
||||
Object.keys(this.schema).forEach(function(name) {
|
||||
var types = this.schema[name];
|
||||
types = Array.isArray(types) ? types : [types];
|
||||
if (!this._dependencyMatchTypes(values[name], types)) {
|
||||
throw new TypeError("invalid dependency: " + name +
|
||||
"; expected " + types.map(typeName).join(", ") +
|
||||
", got " + typeName(values[name]));
|
||||
}
|
||||
}, this);
|
||||
},
|
||||
|
||||
/**
|
||||
* Checks if a values object owns the required keys defined in dependencies.
|
||||
* Values attached to these properties shouldn't be null nor undefined.
|
||||
*
|
||||
* @param {Object} values The values object
|
||||
* @throws {TypeError} If any dependency is missing.
|
||||
*/
|
||||
_checkRequiredProperties: function(values) {
|
||||
var definedProperties = Object.keys(values).filter(function(name) {
|
||||
return typeof values[name] !== "undefined";
|
||||
});
|
||||
var diff = difference(Object.keys(this.schema), definedProperties);
|
||||
if (diff.length > 0)
|
||||
throw new TypeError("missing required " + diff.join(", "));
|
||||
},
|
||||
|
||||
/**
|
||||
* Checks if a given value matches any of the provided type requirements.
|
||||
*
|
||||
* @param {Object} value The value to check
|
||||
* @param {Array} types The list of types to check the value against
|
||||
* @return {Boolean}
|
||||
* @throws {TypeError} If the value doesn't match any types.
|
||||
*/
|
||||
_dependencyMatchTypes: function(value, types) {
|
||||
return types.some(function(Type) {
|
||||
/*jshint eqeqeq:false*/
|
||||
try {
|
||||
return typeof Type === "undefined" || // skip checking
|
||||
Type === null && value === null || // null type
|
||||
value.constructor == Type || // native type
|
||||
Type.prototype.isPrototypeOf(value) || // custom type
|
||||
typeName(value) === typeName(Type); // type string eq.
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
Validator: Validator
|
||||
};
|
||||
})();
|
@ -99,10 +99,13 @@ loop.CallConnectionWebSocket = (function() {
|
||||
* Internal function called to resolve the connection promise.
|
||||
*
|
||||
* It will log an error if no promise is found.
|
||||
*
|
||||
* @param {String} progressState The current state of progress of the
|
||||
* websocket.
|
||||
*/
|
||||
_completeConnection: function() {
|
||||
_completeConnection: function(progressState) {
|
||||
if (this.connectDetails && this.connectDetails.resolve) {
|
||||
this.connectDetails.resolve();
|
||||
this.connectDetails.resolve(progressState);
|
||||
this._clearConnectionFlags();
|
||||
return;
|
||||
}
|
||||
@ -227,7 +230,7 @@ loop.CallConnectionWebSocket = (function() {
|
||||
|
||||
switch(msg.messageType) {
|
||||
case "hello":
|
||||
this._completeConnection();
|
||||
this._completeConnection(msg.state);
|
||||
break;
|
||||
case "progress":
|
||||
this.trigger("progress:" + msg.state);
|
||||
|
@ -16,6 +16,7 @@ browser.jar:
|
||||
content/browser/loop/js/otconfig.js (content/js/otconfig.js)
|
||||
content/browser/loop/js/panel.js (content/js/panel.js)
|
||||
content/browser/loop/js/contacts.js (content/js/contacts.js)
|
||||
content/browser/loop/js/conversationViews.js (content/js/conversationViews.js)
|
||||
|
||||
# Shared styles
|
||||
content/browser/loop/shared/css/reset.css (content/shared/css/reset.css)
|
||||
@ -52,11 +53,15 @@ browser.jar:
|
||||
content/browser/loop/shared/img/icons-16x16.svg (content/shared/img/icons-16x16.svg)
|
||||
|
||||
# Shared scripts
|
||||
content/browser/loop/shared/js/actions.js (content/shared/js/actions.js)
|
||||
content/browser/loop/shared/js/conversationStore.js (content/shared/js/conversationStore.js)
|
||||
content/browser/loop/shared/js/dispatcher.js (content/shared/js/dispatcher.js)
|
||||
content/browser/loop/shared/js/feedbackApiClient.js (content/shared/js/feedbackApiClient.js)
|
||||
content/browser/loop/shared/js/models.js (content/shared/js/models.js)
|
||||
content/browser/loop/shared/js/mixins.js (content/shared/js/mixins.js)
|
||||
content/browser/loop/shared/js/views.js (content/shared/js/views.js)
|
||||
content/browser/loop/shared/js/utils.js (content/shared/js/utils.js)
|
||||
content/browser/loop/shared/js/validate.js (content/shared/js/validate.js)
|
||||
content/browser/loop/shared/js/websocket.js (content/shared/js/websocket.js)
|
||||
|
||||
# Shared libs
|
||||
|
@ -253,5 +253,70 @@ describe("loop.Client", function() {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("#setupOutgoingCall", function() {
|
||||
var calleeIds, callType;
|
||||
|
||||
beforeEach(function() {
|
||||
calleeIds = [
|
||||
"fakeemail", "fake phone"
|
||||
];
|
||||
callType = "audio";
|
||||
});
|
||||
|
||||
it("should make a POST call to /calls", function() {
|
||||
client.setupOutgoingCall(calleeIds, callType);
|
||||
|
||||
sinon.assert.calledOnce(hawkRequestStub);
|
||||
sinon.assert.calledWith(hawkRequestStub,
|
||||
mozLoop.LOOP_SESSION_TYPE.FXA,
|
||||
"/calls",
|
||||
"POST",
|
||||
{ calleeId: calleeIds, callType: callType }
|
||||
);
|
||||
});
|
||||
|
||||
it("should call the callback if the request is successful", function() {
|
||||
var requestData = {
|
||||
apiKey: "fake",
|
||||
callId: "fakeCall",
|
||||
progressURL: "fakeurl",
|
||||
sessionId: "12345678",
|
||||
sessionToken: "15263748",
|
||||
websocketToken: "13572468"
|
||||
};
|
||||
|
||||
hawkRequestStub.callsArgWith(4, null, JSON.stringify(requestData));
|
||||
|
||||
client.setupOutgoingCall(calleeIds, callType, callback);
|
||||
|
||||
sinon.assert.calledOnce(callback);
|
||||
sinon.assert.calledWithExactly(callback, null, requestData);
|
||||
});
|
||||
|
||||
it("should send an error when the request fails", function() {
|
||||
hawkRequestStub.callsArgWith(4, fakeErrorRes);
|
||||
|
||||
client.setupOutgoingCall(calleeIds, callType, callback);
|
||||
|
||||
sinon.assert.calledOnce(callback);
|
||||
sinon.assert.calledWithExactly(callback, sinon.match(function(err) {
|
||||
return /400.*invalid token/.test(err.message);
|
||||
}));
|
||||
});
|
||||
|
||||
it("should send an error if the data is not valid", function() {
|
||||
// Sets up the hawkRequest stub to trigger the callback with
|
||||
// an error
|
||||
hawkRequestStub.callsArgWith(4, null, "{}");
|
||||
|
||||
client.setupOutgoingCall(calleeIds, callType, callback);
|
||||
|
||||
sinon.assert.calledOnce(callback);
|
||||
sinon.assert.calledWithMatch(callback, sinon.match(function(err) {
|
||||
return /Invalid data received/.test(err.message);
|
||||
}));
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -0,0 +1,131 @@
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
* http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
var expect = chai.expect;
|
||||
|
||||
describe("loop.conversationViews", function () {
|
||||
var sandbox, oldTitle, view;
|
||||
|
||||
var CALL_STATES = loop.store.CALL_STATES;
|
||||
|
||||
beforeEach(function() {
|
||||
sandbox = sinon.sandbox.create();
|
||||
|
||||
oldTitle = document.title;
|
||||
sandbox.stub(document.mozL10n, "get", function(x) {
|
||||
return x;
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(function() {
|
||||
document.title = oldTitle;
|
||||
view = undefined;
|
||||
sandbox.restore();
|
||||
});
|
||||
|
||||
describe("ConversationDetailView", function() {
|
||||
function mountTestComponent(props) {
|
||||
return TestUtils.renderIntoDocument(
|
||||
loop.conversationViews.ConversationDetailView(props));
|
||||
}
|
||||
|
||||
it("should set the document title to the calledId", function() {
|
||||
mountTestComponent({calleeId: "mrsmith"});
|
||||
|
||||
expect(document.title).eql("mrsmith");
|
||||
});
|
||||
|
||||
it("should set display the calledId", function() {
|
||||
view = mountTestComponent({calleeId: "mrsmith"});
|
||||
|
||||
expect(TestUtils.findRenderedDOMComponentWithTag(
|
||||
view, "h2").props.children).eql("mrsmith");
|
||||
});
|
||||
});
|
||||
|
||||
describe("PendingConversationView", function() {
|
||||
function mountTestComponent(props) {
|
||||
return TestUtils.renderIntoDocument(
|
||||
loop.conversationViews.PendingConversationView(props));
|
||||
}
|
||||
|
||||
it("should set display connecting string when the state is not alerting",
|
||||
function() {
|
||||
view = mountTestComponent({
|
||||
callState: CALL_STATES.CONNECTING,
|
||||
calleeId: "mrsmith"
|
||||
});
|
||||
|
||||
var label = TestUtils.findRenderedDOMComponentWithClass(
|
||||
view, "btn-label").props.children;
|
||||
|
||||
expect(label).to.have.string("connecting");
|
||||
});
|
||||
|
||||
it("should set display ringing string when the state is alerting",
|
||||
function() {
|
||||
view = mountTestComponent({
|
||||
callState: CALL_STATES.ALERTING,
|
||||
calleeId: "mrsmith"
|
||||
});
|
||||
|
||||
var label = TestUtils.findRenderedDOMComponentWithClass(
|
||||
view, "btn-label").props.children;
|
||||
|
||||
expect(label).to.have.string("ringing");
|
||||
});
|
||||
});
|
||||
|
||||
describe("OutgoingConversationView", function() {
|
||||
var store;
|
||||
|
||||
function mountTestComponent() {
|
||||
return TestUtils.renderIntoDocument(
|
||||
loop.conversationViews.OutgoingConversationView({
|
||||
store: store
|
||||
}));
|
||||
}
|
||||
|
||||
beforeEach(function() {
|
||||
store = new loop.store.ConversationStore({}, {
|
||||
dispatcher: new loop.Dispatcher(),
|
||||
client: {}
|
||||
});
|
||||
});
|
||||
|
||||
it("should render the CallFailedView when the call state is 'terminated'",
|
||||
function() {
|
||||
store.set({callState: CALL_STATES.TERMINATED});
|
||||
|
||||
view = mountTestComponent();
|
||||
|
||||
TestUtils.findRenderedComponentWithType(view,
|
||||
loop.conversationViews.CallFailedView);
|
||||
});
|
||||
|
||||
it("should render the PendingConversationView when the call state is connecting",
|
||||
function() {
|
||||
store.set({callState: CALL_STATES.CONNECTING});
|
||||
|
||||
view = mountTestComponent();
|
||||
|
||||
TestUtils.findRenderedComponentWithType(view,
|
||||
loop.conversationViews.PendingConversationView);
|
||||
});
|
||||
|
||||
it("should update the rendered views when the state is changed.",
|
||||
function() {
|
||||
store.set({callState: CALL_STATES.CONNECTING});
|
||||
|
||||
view = mountTestComponent();
|
||||
|
||||
TestUtils.findRenderedComponentWithType(view,
|
||||
loop.conversationViews.PendingConversationView);
|
||||
|
||||
store.set({callState: CALL_STATES.TERMINATED});
|
||||
|
||||
TestUtils.findRenderedComponentWithType(view,
|
||||
loop.conversationViews.CallFailedView);
|
||||
});
|
||||
});
|
||||
});
|
@ -41,7 +41,7 @@ describe("loop.conversation", function() {
|
||||
return "en-US";
|
||||
},
|
||||
setLoopCharPref: sinon.stub(),
|
||||
getLoopCharPref: sinon.stub(),
|
||||
getLoopCharPref: sinon.stub().returns(null),
|
||||
getLoopBoolPref: sinon.stub(),
|
||||
getCallData: sinon.stub(),
|
||||
releaseCallData: sinon.stub(),
|
||||
@ -68,8 +68,6 @@ describe("loop.conversation", function() {
|
||||
});
|
||||
|
||||
describe("#init", function() {
|
||||
var oldTitle;
|
||||
|
||||
beforeEach(function() {
|
||||
sandbox.stub(React, "renderComponent");
|
||||
sandbox.stub(document.mozL10n, "initialize");
|
||||
@ -77,6 +75,11 @@ describe("loop.conversation", function() {
|
||||
sandbox.stub(loop.shared.models.ConversationModel.prototype,
|
||||
"initialize");
|
||||
|
||||
sandbox.stub(loop.Dispatcher.prototype, "dispatch");
|
||||
|
||||
sandbox.stub(loop.shared.utils.Helper.prototype,
|
||||
"locationHash").returns("#incoming/42");
|
||||
|
||||
window.OT = {
|
||||
overrideGuidStorage: sinon.stub()
|
||||
};
|
||||
@ -94,17 +97,78 @@ describe("loop.conversation", function() {
|
||||
navigator.mozLoop);
|
||||
});
|
||||
|
||||
it("should create the IncomingConversationView", function() {
|
||||
it("should create the ConversationControllerView", function() {
|
||||
loop.conversation.init();
|
||||
|
||||
sinon.assert.calledOnce(React.renderComponent);
|
||||
sinon.assert.calledWith(React.renderComponent,
|
||||
sinon.match(function(value) {
|
||||
return TestUtils.isDescriptorOfType(value,
|
||||
loop.conversation.IncomingConversationView);
|
||||
loop.conversation.ConversationControllerView);
|
||||
}));
|
||||
});
|
||||
|
||||
it("should trigger a gatherCallData action", function() {
|
||||
loop.conversation.init();
|
||||
|
||||
sinon.assert.calledOnce(loop.Dispatcher.prototype.dispatch);
|
||||
sinon.assert.calledWithExactly(loop.Dispatcher.prototype.dispatch,
|
||||
new loop.shared.actions.GatherCallData({
|
||||
calleeId: null,
|
||||
callId: "42"
|
||||
}));
|
||||
});
|
||||
});
|
||||
|
||||
describe("ConversationControllerView", function() {
|
||||
var store, conversation, client, ccView, oldTitle, dispatcher;
|
||||
|
||||
function mountTestComponent() {
|
||||
return TestUtils.renderIntoDocument(
|
||||
loop.conversation.ConversationControllerView({
|
||||
client: client,
|
||||
conversation: conversation,
|
||||
notifications: notifications,
|
||||
sdk: {},
|
||||
store: store
|
||||
}));
|
||||
}
|
||||
|
||||
beforeEach(function() {
|
||||
oldTitle = document.title;
|
||||
client = new loop.Client();
|
||||
conversation = new loop.shared.models.ConversationModel({}, {
|
||||
sdk: {}
|
||||
});
|
||||
dispatcher = new loop.Dispatcher();
|
||||
store = new loop.store.ConversationStore({}, {
|
||||
client: client,
|
||||
dispatcher: dispatcher
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(function() {
|
||||
ccView = undefined;
|
||||
document.title = oldTitle;
|
||||
});
|
||||
|
||||
it("should display the OutgoingConversationView for outgoing calls", function() {
|
||||
store.set({outgoing: true});
|
||||
|
||||
ccView = mountTestComponent();
|
||||
|
||||
TestUtils.findRenderedComponentWithType(ccView,
|
||||
loop.conversationViews.OutgoingConversationView);
|
||||
});
|
||||
|
||||
it("should display the IncomingConversationView for incoming calls", function() {
|
||||
store.set({outgoing: false});
|
||||
|
||||
ccView = mountTestComponent();
|
||||
|
||||
TestUtils.findRenderedComponentWithType(ccView,
|
||||
loop.conversation.IncomingConversationView);
|
||||
});
|
||||
});
|
||||
|
||||
describe("IncomingConversationView", function() {
|
||||
@ -231,7 +295,7 @@ describe("loop.conversation", function() {
|
||||
|
||||
it("should set the state to incoming on success", function(done) {
|
||||
icView = mountTestComponent();
|
||||
resolveWebSocketConnect();
|
||||
resolveWebSocketConnect("incoming");
|
||||
|
||||
promise.then(function () {
|
||||
expect(icView.state.callStatus).eql("incoming");
|
||||
@ -239,6 +303,17 @@ describe("loop.conversation", function() {
|
||||
});
|
||||
});
|
||||
|
||||
it("should set the state to close on success if the progress " +
|
||||
"state is terminated", function(done) {
|
||||
icView = mountTestComponent();
|
||||
resolveWebSocketConnect("terminated");
|
||||
|
||||
promise.then(function () {
|
||||
expect(icView.state.callStatus).eql("close");
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it("should display an error if the websocket failed to connect", function(done) {
|
||||
sandbox.stub(notifications, "errorL10n");
|
||||
|
||||
|
@ -34,11 +34,16 @@
|
||||
<!-- App scripts -->
|
||||
<script src="../../content/shared/js/utils.js"></script>
|
||||
<script src="../../content/shared/js/feedbackApiClient.js"></script>
|
||||
<script src="../../content/shared/js/conversationStore.js"></script>
|
||||
<script src="../../content/shared/js/models.js"></script>
|
||||
<script src="../../content/shared/js/mixins.js"></script>
|
||||
<script src="../../content/shared/js/views.js"></script>
|
||||
<script src="../../content/shared/js/websocket.js"></script>
|
||||
<script src="../../content/shared/js/actions.js"></script>
|
||||
<script src="../../content/shared/js/validate.js"></script>
|
||||
<script src="../../content/shared/js/dispatcher.js"></script>
|
||||
<script src="../../content/js/client.js"></script>
|
||||
<script src="../../content/js/conversationViews.js"></script>
|
||||
<script src="../../content/js/conversation.js"></script>
|
||||
<script type="text/javascript;version=1.8" src="../../content/js/contacts.js"></script>
|
||||
<script src="../../content/js/panel.js"></script>
|
||||
@ -47,6 +52,7 @@
|
||||
<script src="client_test.js"></script>
|
||||
<script src="conversation_test.js"></script>
|
||||
<script src="panel_test.js"></script>
|
||||
<script src="conversationViews_test.js"></script>
|
||||
<script>
|
||||
// Stop the default init functions running to avoid conflicts in tests
|
||||
document.removeEventListener('DOMContentLoaded', loop.panel.init);
|
||||
|
321
browser/components/loop/test/shared/conversationStore_test.js
Normal file
321
browser/components/loop/test/shared/conversationStore_test.js
Normal file
@ -0,0 +1,321 @@
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
* http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
var expect = chai.expect;
|
||||
|
||||
describe("loop.ConversationStore", function () {
|
||||
"use strict";
|
||||
|
||||
var CALL_STATES = loop.store.CALL_STATES;
|
||||
var sharedActions = loop.shared.actions;
|
||||
var sharedUtils = loop.shared.utils;
|
||||
var sandbox, dispatcher, client, store, fakeSessionData;
|
||||
var connectPromise, resolveConnectPromise, rejectConnectPromise;
|
||||
|
||||
function checkFailures(done, f) {
|
||||
try {
|
||||
f();
|
||||
done();
|
||||
} catch (err) {
|
||||
done(err);
|
||||
}
|
||||
}
|
||||
|
||||
beforeEach(function() {
|
||||
sandbox = sinon.sandbox.create();
|
||||
|
||||
dispatcher = new loop.Dispatcher();
|
||||
client = {
|
||||
setupOutgoingCall: sinon.stub()
|
||||
};
|
||||
store = new loop.store.ConversationStore({}, {
|
||||
client: client,
|
||||
dispatcher: dispatcher
|
||||
});
|
||||
fakeSessionData = {
|
||||
apiKey: "fakeKey",
|
||||
callId: "142536",
|
||||
sessionId: "321456",
|
||||
sessionToken: "341256",
|
||||
websocketToken: "543216",
|
||||
progressURL: "fakeURL"
|
||||
};
|
||||
|
||||
var dummySocket = {
|
||||
close: sinon.spy(),
|
||||
send: sinon.spy()
|
||||
};
|
||||
|
||||
connectPromise = new Promise(function(resolve, reject) {
|
||||
resolveConnectPromise = resolve;
|
||||
rejectConnectPromise = reject;
|
||||
});
|
||||
|
||||
sandbox.stub(loop.CallConnectionWebSocket.prototype,
|
||||
"promiseConnect").returns(connectPromise);
|
||||
});
|
||||
|
||||
afterEach(function() {
|
||||
sandbox.restore();
|
||||
});
|
||||
|
||||
describe("#initialize", function() {
|
||||
it("should throw an error if the dispatcher is missing", function() {
|
||||
expect(function() {
|
||||
new loop.store.ConversationStore({}, {client: client});
|
||||
}).to.Throw(/dispatcher/);
|
||||
});
|
||||
|
||||
it("should throw an error if the client is missing", function() {
|
||||
expect(function() {
|
||||
new loop.store.ConversationStore({}, {dispatcher: dispatcher});
|
||||
}).to.Throw(/client/);
|
||||
});
|
||||
});
|
||||
|
||||
describe("#connectionFailure", function() {
|
||||
it("should set the state to 'terminated'", function() {
|
||||
store.set({callState: CALL_STATES.ALERTING});
|
||||
|
||||
dispatcher.dispatch(
|
||||
new sharedActions.ConnectionFailure({reason: "fake"}));
|
||||
|
||||
expect(store.get("callState")).eql(CALL_STATES.TERMINATED);
|
||||
expect(store.get("callStateReason")).eql("fake");
|
||||
});
|
||||
});
|
||||
|
||||
describe("#connectionProgress", function() {
|
||||
describe("progress: connecting", function() {
|
||||
it("should change the state from 'gather' to 'connecting'", function() {
|
||||
store.set({callState: CALL_STATES.GATHER});
|
||||
|
||||
dispatcher.dispatch(
|
||||
new sharedActions.ConnectionProgress({state: "connecting"}));
|
||||
|
||||
expect(store.get("callState")).eql(CALL_STATES.CONNECTING);
|
||||
});
|
||||
});
|
||||
|
||||
describe("progress: alerting", function() {
|
||||
it("should set the state from 'gather' to 'alerting'", function() {
|
||||
store.set({callState: CALL_STATES.GATHER});
|
||||
|
||||
dispatcher.dispatch(
|
||||
new sharedActions.ConnectionProgress({state: "alerting"}));
|
||||
|
||||
expect(store.get("callState")).eql(CALL_STATES.ALERTING);
|
||||
});
|
||||
|
||||
it("should set the state from 'connecting' to 'alerting'", function() {
|
||||
store.set({callState: CALL_STATES.CONNECTING});
|
||||
|
||||
dispatcher.dispatch(
|
||||
new sharedActions.ConnectionProgress({state: "alerting"}));
|
||||
|
||||
expect(store.get("callState")).eql(CALL_STATES.ALERTING);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("#gatherCallData", function() {
|
||||
beforeEach(function() {
|
||||
store.set({callState: CALL_STATES.INIT});
|
||||
});
|
||||
|
||||
it("should set the state to 'gather'", function() {
|
||||
dispatcher.dispatch(
|
||||
new sharedActions.GatherCallData({
|
||||
calleeId: "",
|
||||
callId: "76543218"
|
||||
}));
|
||||
|
||||
expect(store.get("callState")).eql(CALL_STATES.GATHER);
|
||||
});
|
||||
|
||||
it("should save the basic call information", function() {
|
||||
dispatcher.dispatch(
|
||||
new sharedActions.GatherCallData({
|
||||
calleeId: "fake",
|
||||
callId: "123456"
|
||||
}));
|
||||
|
||||
expect(store.get("calleeId")).eql("fake");
|
||||
expect(store.get("callId")).eql("123456");
|
||||
expect(store.get("outgoing")).eql(true);
|
||||
});
|
||||
|
||||
describe("outgoing calls", function() {
|
||||
var outgoingCallData;
|
||||
|
||||
beforeEach(function() {
|
||||
outgoingCallData = {
|
||||
calleeId: "fake",
|
||||
callId: "135246"
|
||||
};
|
||||
});
|
||||
|
||||
it("should request the outgoing call data", function() {
|
||||
dispatcher.dispatch(
|
||||
new sharedActions.GatherCallData(outgoingCallData));
|
||||
|
||||
sinon.assert.calledOnce(client.setupOutgoingCall);
|
||||
sinon.assert.calledWith(client.setupOutgoingCall,
|
||||
["fake"], sharedUtils.CALL_TYPES.AUDIO_VIDEO);
|
||||
});
|
||||
|
||||
describe("server response handling", function() {
|
||||
beforeEach(function() {
|
||||
sandbox.stub(dispatcher, "dispatch");
|
||||
});
|
||||
|
||||
it("should dispatch a connect call action on success", function() {
|
||||
var callData = {
|
||||
apiKey: "fakeKey"
|
||||
};
|
||||
|
||||
client.setupOutgoingCall.callsArgWith(2, null, callData);
|
||||
|
||||
store.gatherCallData(
|
||||
new sharedActions.GatherCallData(outgoingCallData));
|
||||
|
||||
sinon.assert.calledOnce(dispatcher.dispatch);
|
||||
// Can't use instanceof here, as that matches any action
|
||||
sinon.assert.calledWithMatch(dispatcher.dispatch,
|
||||
sinon.match.hasOwn("name", "connectCall"));
|
||||
sinon.assert.calledWithMatch(dispatcher.dispatch,
|
||||
sinon.match.hasOwn("sessionData", callData));
|
||||
});
|
||||
|
||||
it("should dispatch a connection failure action on failure", function() {
|
||||
client.setupOutgoingCall.callsArgWith(2, {});
|
||||
|
||||
store.gatherCallData(
|
||||
new sharedActions.GatherCallData(outgoingCallData));
|
||||
|
||||
sinon.assert.calledOnce(dispatcher.dispatch);
|
||||
// Can't use instanceof here, as that matches any action
|
||||
sinon.assert.calledWithMatch(dispatcher.dispatch,
|
||||
sinon.match.hasOwn("name", "connectionFailure"));
|
||||
sinon.assert.calledWithMatch(dispatcher.dispatch,
|
||||
sinon.match.hasOwn("reason", "setup"));
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("#connectCall", function() {
|
||||
it("should save the call session data", function() {
|
||||
dispatcher.dispatch(
|
||||
new sharedActions.ConnectCall({sessionData: fakeSessionData}));
|
||||
|
||||
expect(store.get("apiKey")).eql("fakeKey");
|
||||
expect(store.get("callId")).eql("142536");
|
||||
expect(store.get("sessionId")).eql("321456");
|
||||
expect(store.get("sessionToken")).eql("341256");
|
||||
expect(store.get("websocketToken")).eql("543216");
|
||||
expect(store.get("progressURL")).eql("fakeURL");
|
||||
});
|
||||
|
||||
it("should initialize the websocket", function() {
|
||||
sandbox.stub(loop, "CallConnectionWebSocket").returns({
|
||||
promiseConnect: function() { return connectPromise; },
|
||||
on: sinon.spy()
|
||||
});
|
||||
|
||||
dispatcher.dispatch(
|
||||
new sharedActions.ConnectCall({sessionData: fakeSessionData}));
|
||||
|
||||
sinon.assert.calledOnce(loop.CallConnectionWebSocket);
|
||||
sinon.assert.calledWithExactly(loop.CallConnectionWebSocket, {
|
||||
url: "fakeURL",
|
||||
callId: "142536",
|
||||
websocketToken: "543216"
|
||||
});
|
||||
});
|
||||
|
||||
it("should connect the websocket to the server", function() {
|
||||
dispatcher.dispatch(
|
||||
new sharedActions.ConnectCall({sessionData: fakeSessionData}));
|
||||
|
||||
sinon.assert.calledOnce(store._websocket.promiseConnect);
|
||||
});
|
||||
|
||||
describe("WebSocket connection result", function() {
|
||||
beforeEach(function() {
|
||||
dispatcher.dispatch(
|
||||
new sharedActions.ConnectCall({sessionData: fakeSessionData}));
|
||||
|
||||
sandbox.stub(dispatcher, "dispatch");
|
||||
});
|
||||
|
||||
it("should dispatch a connection progress action on success", function(done) {
|
||||
resolveConnectPromise();
|
||||
|
||||
connectPromise.then(function() {
|
||||
checkFailures(done, function() {
|
||||
sinon.assert.calledOnce(dispatcher.dispatch);
|
||||
// Can't use instanceof here, as that matches any action
|
||||
sinon.assert.calledWithMatch(dispatcher.dispatch,
|
||||
sinon.match.hasOwn("name", "connectionProgress"));
|
||||
sinon.assert.calledWithMatch(dispatcher.dispatch,
|
||||
sinon.match.hasOwn("state", "connecting"));
|
||||
});
|
||||
}, function() {
|
||||
done(new Error("Promise should have been resolve, not rejected"));
|
||||
});
|
||||
});
|
||||
|
||||
it("should dispatch a connection failure action on failure", function(done) {
|
||||
rejectConnectPromise();
|
||||
|
||||
connectPromise.then(function() {
|
||||
done(new Error("Promise should have been rejected, not resolved"));
|
||||
}, function() {
|
||||
checkFailures(done, function() {
|
||||
sinon.assert.calledOnce(dispatcher.dispatch);
|
||||
// Can't use instanceof here, as that matches any action
|
||||
sinon.assert.calledWithMatch(dispatcher.dispatch,
|
||||
sinon.match.hasOwn("name", "connectionFailure"));
|
||||
sinon.assert.calledWithMatch(dispatcher.dispatch,
|
||||
sinon.match.hasOwn("reason", "websocket-setup"));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
describe("Events", function() {
|
||||
describe("Websocket progress", function() {
|
||||
beforeEach(function() {
|
||||
dispatcher.dispatch(
|
||||
new sharedActions.ConnectCall({sessionData: fakeSessionData}));
|
||||
|
||||
sandbox.stub(dispatcher, "dispatch");
|
||||
});
|
||||
|
||||
it("should dispatch a connection failure action on 'terminate'", function() {
|
||||
store._websocket.trigger("progress", {state: "terminated", reason: "reject"});
|
||||
|
||||
sinon.assert.calledOnce(dispatcher.dispatch);
|
||||
// Can't use instanceof here, as that matches any action
|
||||
sinon.assert.calledWithMatch(dispatcher.dispatch,
|
||||
sinon.match.hasOwn("name", "connectionFailure"));
|
||||
sinon.assert.calledWithMatch(dispatcher.dispatch,
|
||||
sinon.match.hasOwn("reason", "reject"));
|
||||
});
|
||||
|
||||
it("should dispatch a connection progress action on 'alerting'", function() {
|
||||
store._websocket.trigger("progress", {state: "alerting"});
|
||||
|
||||
sinon.assert.calledOnce(dispatcher.dispatch);
|
||||
// Can't use instanceof here, as that matches any action
|
||||
sinon.assert.calledWithMatch(dispatcher.dispatch,
|
||||
sinon.match.hasOwn("name", "connectionProgress"));
|
||||
sinon.assert.calledWithMatch(dispatcher.dispatch,
|
||||
sinon.match.hasOwn("state", "alerting"));
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
140
browser/components/loop/test/shared/dispatcher_test.js
Normal file
140
browser/components/loop/test/shared/dispatcher_test.js
Normal file
@ -0,0 +1,140 @@
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
* http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
var expect = chai.expect;
|
||||
|
||||
describe("loop.Dispatcher", function () {
|
||||
"use strict";
|
||||
|
||||
var sharedActions = loop.shared.actions;
|
||||
var dispatcher, sandbox;
|
||||
|
||||
beforeEach(function() {
|
||||
sandbox = sinon.sandbox.create();
|
||||
dispatcher = new loop.Dispatcher();
|
||||
});
|
||||
|
||||
afterEach(function() {
|
||||
sandbox.restore();
|
||||
});
|
||||
|
||||
describe("#register", function() {
|
||||
it("should register a store against an action name", function() {
|
||||
var object = { fake: true };
|
||||
|
||||
dispatcher.register(object, ["gatherCallData"]);
|
||||
|
||||
expect(dispatcher._eventData["gatherCallData"][0]).eql(object);
|
||||
});
|
||||
|
||||
it("should register multiple store against an action name", function() {
|
||||
var object1 = { fake: true };
|
||||
var object2 = { fake2: true };
|
||||
|
||||
dispatcher.register(object1, ["gatherCallData"]);
|
||||
dispatcher.register(object2, ["gatherCallData"]);
|
||||
|
||||
expect(dispatcher._eventData["gatherCallData"][0]).eql(object1);
|
||||
expect(dispatcher._eventData["gatherCallData"][1]).eql(object2);
|
||||
});
|
||||
});
|
||||
|
||||
describe("#dispatch", function() {
|
||||
var gatherStore1, gatherStore2, cancelStore1, connectStore1;
|
||||
var gatherAction, cancelAction, connectAction, resolveCancelStore1;
|
||||
|
||||
beforeEach(function() {
|
||||
gatherAction = new sharedActions.GatherCallData({
|
||||
callId: "42",
|
||||
calleeId: null
|
||||
});
|
||||
|
||||
cancelAction = new sharedActions.CancelCall();
|
||||
connectAction = new sharedActions.ConnectCall({
|
||||
sessionData: {}
|
||||
});
|
||||
|
||||
gatherStore1 = {
|
||||
gatherCallData: sinon.stub()
|
||||
};
|
||||
gatherStore2 = {
|
||||
gatherCallData: sinon.stub()
|
||||
};
|
||||
cancelStore1 = {
|
||||
cancelCall: sinon.stub()
|
||||
};
|
||||
connectStore1 = {
|
||||
connectCall: function() {}
|
||||
};
|
||||
|
||||
dispatcher.register(gatherStore1, ["gatherCallData"]);
|
||||
dispatcher.register(gatherStore2, ["gatherCallData"]);
|
||||
dispatcher.register(cancelStore1, ["cancelCall"]);
|
||||
dispatcher.register(connectStore1, ["connectCall"]);
|
||||
});
|
||||
|
||||
it("should dispatch an action to the required object", function() {
|
||||
dispatcher.dispatch(cancelAction);
|
||||
|
||||
sinon.assert.notCalled(gatherStore1.gatherCallData);
|
||||
|
||||
sinon.assert.calledOnce(cancelStore1.cancelCall);
|
||||
sinon.assert.calledWithExactly(cancelStore1.cancelCall, cancelAction);
|
||||
|
||||
sinon.assert.notCalled(gatherStore2.gatherCallData);
|
||||
});
|
||||
|
||||
it("should dispatch actions to multiple objects", function() {
|
||||
dispatcher.dispatch(gatherAction);
|
||||
|
||||
sinon.assert.calledOnce(gatherStore1.gatherCallData);
|
||||
sinon.assert.calledWithExactly(gatherStore1.gatherCallData, gatherAction);
|
||||
|
||||
sinon.assert.notCalled(cancelStore1.cancelCall);
|
||||
|
||||
sinon.assert.calledOnce(gatherStore2.gatherCallData);
|
||||
sinon.assert.calledWithExactly(gatherStore2.gatherCallData, gatherAction);
|
||||
});
|
||||
|
||||
it("should dispatch multiple actions", function() {
|
||||
dispatcher.dispatch(cancelAction);
|
||||
dispatcher.dispatch(gatherAction);
|
||||
|
||||
sinon.assert.calledOnce(cancelStore1.cancelCall);
|
||||
sinon.assert.calledOnce(gatherStore1.gatherCallData);
|
||||
sinon.assert.calledOnce(gatherStore2.gatherCallData);
|
||||
});
|
||||
|
||||
describe("Queued actions", function() {
|
||||
beforeEach(function() {
|
||||
// Restore the stub, so that we can easily add a function to be
|
||||
// returned. Unfortunately, sinon doesn't make this easy.
|
||||
sandbox.stub(connectStore1, "connectCall", function() {
|
||||
dispatcher.dispatch(gatherAction);
|
||||
|
||||
sinon.assert.notCalled(gatherStore1.gatherCallData);
|
||||
sinon.assert.notCalled(gatherStore2.gatherCallData);
|
||||
});
|
||||
});
|
||||
|
||||
it("should not dispatch an action if the previous action hasn't finished", function() {
|
||||
// Dispatch the first action. The action handler dispatches the second
|
||||
// action - see the beforeEach above.
|
||||
dispatcher.dispatch(connectAction);
|
||||
|
||||
sinon.assert.calledOnce(connectStore1.connectCall);
|
||||
});
|
||||
|
||||
it("should dispatch an action when the previous action finishes", function() {
|
||||
// Dispatch the first action. The action handler dispatches the second
|
||||
// action - see the beforeEach above.
|
||||
dispatcher.dispatch(connectAction);
|
||||
|
||||
sinon.assert.calledOnce(connectStore1.connectCall);
|
||||
// These should be called, because the dispatcher synchronously queues actions.
|
||||
sinon.assert.calledOnce(gatherStore1.gatherCallData);
|
||||
sinon.assert.calledOnce(gatherStore2.gatherCallData);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
@ -39,6 +39,10 @@
|
||||
<script src="../../content/shared/js/views.js"></script>
|
||||
<script src="../../content/shared/js/websocket.js"></script>
|
||||
<script src="../../content/shared/js/feedbackApiClient.js"></script>
|
||||
<script src="../../content/shared/js/validate.js"></script>
|
||||
<script src="../../content/shared/js/actions.js"></script>
|
||||
<script src="../../content/shared/js/dispatcher.js"></script>
|
||||
<script src="../../content/shared/js/conversationStore.js"></script>
|
||||
|
||||
<!-- Test scripts -->
|
||||
<script src="models_test.js"></script>
|
||||
@ -47,6 +51,9 @@
|
||||
<script src="views_test.js"></script>
|
||||
<script src="websocket_test.js"></script>
|
||||
<script src="feedbackApiClient_test.js"></script>
|
||||
<script src="validate_test.js"></script>
|
||||
<script src="dispatcher_test.js"></script>
|
||||
<script src="conversationStore_test.js"></script>
|
||||
<script>
|
||||
mocha.run(function () {
|
||||
$("#mocha").append("<p id='complete'>Complete.</p>");
|
||||
|
82
browser/components/loop/test/shared/validate_test.js
Normal file
82
browser/components/loop/test/shared/validate_test.js
Normal file
@ -0,0 +1,82 @@
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
* http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
/*global chai, validate */
|
||||
|
||||
var expect = chai.expect;
|
||||
|
||||
describe("Validator", function() {
|
||||
"use strict";
|
||||
|
||||
// test helpers
|
||||
function create(dependencies, values) {
|
||||
var validator = new loop.validate.Validator(dependencies);
|
||||
return validator.validate.bind(validator, values);
|
||||
}
|
||||
|
||||
// test types
|
||||
function X(){}
|
||||
function Y(){}
|
||||
|
||||
describe("#validate", function() {
|
||||
it("should check for a single required dependency when no option passed",
|
||||
function() {
|
||||
expect(create({x: Number}, {}))
|
||||
.to.Throw(TypeError, /missing required x$/);
|
||||
});
|
||||
|
||||
it("should check for a missing required dependency, undefined passed",
|
||||
function() {
|
||||
expect(create({x: Number}, {x: undefined}))
|
||||
.to.Throw(TypeError, /missing required x$/);
|
||||
});
|
||||
|
||||
it("should check for multiple missing required dependencies", function() {
|
||||
expect(create({x: Number, y: String}, {}))
|
||||
.to.Throw(TypeError, /missing required x, y$/);
|
||||
});
|
||||
|
||||
it("should check for required dependency types", function() {
|
||||
expect(create({x: Number}, {x: "woops"})).to.Throw(
|
||||
TypeError, /invalid dependency: x; expected Number, got String$/);
|
||||
});
|
||||
|
||||
it("should check for a dependency to match at least one of passed types",
|
||||
function() {
|
||||
expect(create({x: [X, Y]}, {x: 42})).to.Throw(
|
||||
TypeError, /invalid dependency: x; expected X, Y, got Number$/);
|
||||
expect(create({x: [X, Y]}, {x: new Y()})).to.not.Throw();
|
||||
});
|
||||
|
||||
it("should skip type check if required dependency type is undefined",
|
||||
function() {
|
||||
expect(create({x: undefined}, {x: /whatever/})).not.to.Throw();
|
||||
});
|
||||
|
||||
it("should check for a String dependency", function() {
|
||||
expect(create({foo: String}, {foo: 42})).to.Throw(
|
||||
TypeError, /invalid dependency: foo/);
|
||||
});
|
||||
|
||||
it("should check for a Number dependency", function() {
|
||||
expect(create({foo: Number}, {foo: "x"})).to.Throw(
|
||||
TypeError, /invalid dependency: foo/);
|
||||
});
|
||||
|
||||
it("should check for a custom constructor dependency", function() {
|
||||
expect(create({foo: X}, {foo: null})).to.Throw(
|
||||
TypeError, /invalid dependency: foo; expected X, got null$/);
|
||||
});
|
||||
|
||||
it("should check for a native constructor dependency", function() {
|
||||
expect(create({foo: mozRTCSessionDescription}, {foo: "x"}))
|
||||
.to.Throw(TypeError,
|
||||
/invalid dependency: foo; expected mozRTCSessionDescription/);
|
||||
});
|
||||
|
||||
it("should check for a null dependency", function() {
|
||||
expect(create({foo: null}, {foo: "x"})).to.Throw(
|
||||
TypeError, /invalid dependency: foo; expected null, got String$/);
|
||||
});
|
||||
});
|
||||
});
|
@ -128,8 +128,11 @@ describe("loop.CallConnectionWebSocket", function() {
|
||||
data: '{"messageType":"hello", "state":"init"}'
|
||||
});
|
||||
|
||||
promise.then(function() {
|
||||
promise.then(function(state) {
|
||||
expect(state).eql("init");
|
||||
done();
|
||||
}, function() {
|
||||
done(new Error("shouldn't have rejected the promise"));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -32,10 +32,13 @@
|
||||
<script src="../content/shared/libs/lodash-2.4.1.js"></script>
|
||||
<script src="../content/shared/libs/backbone-1.1.2.js"></script>
|
||||
<script src="../content/shared/js/feedbackApiClient.js"></script>
|
||||
<script src="../content/shared/js/conversationStore.js"></script>
|
||||
<script src="../content/shared/js/utils.js"></script>
|
||||
<script src="../content/shared/js/models.js"></script>
|
||||
<script src="../content/shared/js/mixins.js"></script>
|
||||
<script src="../content/shared/js/views.js"></script>
|
||||
<script src="../content/shared/js/websocket.js"></script>
|
||||
<script src="../content/js/conversationViews.js"></script>
|
||||
<script src="../content/js/client.js"></script>
|
||||
<script src="../standalone/content/js/webapp.js"></script>
|
||||
<script type="text/javascript;version=1.8" src="../content/js/contacts.js"></script>
|
||||
|
@ -138,8 +138,8 @@
|
||||
background-size: cover;
|
||||
}
|
||||
|
||||
.incoming-call-action-group .btn-group-chevron,
|
||||
.incoming-call-action-group .btn-group {
|
||||
.call-action-group .btn-group-chevron,
|
||||
.call-action-group .btn-group {
|
||||
/* Prevent box overflow due to long string */
|
||||
max-width: 120px;
|
||||
}
|
||||
|
@ -15,6 +15,7 @@
|
||||
var PanelView = loop.panel.PanelView;
|
||||
// 1.2. Conversation Window
|
||||
var IncomingCallView = loop.conversation.IncomingCallView;
|
||||
var DesktopPendingConversationView = loop.conversationViews.PendingConversationView;
|
||||
|
||||
// 2. Standalone webapp
|
||||
var HomeView = loop.webapp.HomeView;
|
||||
@ -63,6 +64,12 @@
|
||||
});
|
||||
mockConversationModel.startSession = noop;
|
||||
|
||||
var mockWebSocket = new loop.CallConnectionWebSocket({
|
||||
url: "fake",
|
||||
callId: "fakeId",
|
||||
websocketToken: "fakeToken"
|
||||
});
|
||||
|
||||
var notifications = new loop.shared.models.NotificationCollection();
|
||||
var errNotifications = new loop.shared.models.NotificationCollection();
|
||||
errNotifications.error("Error!");
|
||||
@ -223,12 +230,21 @@
|
||||
Section({name: "PendingConversationView"},
|
||||
Example({summary: "Pending conversation view (connecting)", dashed: "true"},
|
||||
React.DOM.div({className: "standalone"},
|
||||
PendingConversationView(null)
|
||||
PendingConversationView({websocket: mockWebSocket})
|
||||
)
|
||||
),
|
||||
Example({summary: "Pending conversation view (ringing)", dashed: "true"},
|
||||
React.DOM.div({className: "standalone"},
|
||||
PendingConversationView({callState: "ringing"})
|
||||
PendingConversationView({websocket: mockWebSocket, callState: "ringing"})
|
||||
)
|
||||
)
|
||||
),
|
||||
|
||||
Section({name: "PendingConversationView (Desktop)"},
|
||||
Example({summary: "Connecting", dashed: "true",
|
||||
style: {width: "260px", height: "265px"}},
|
||||
React.DOM.div({className: "fx-embedded"},
|
||||
DesktopPendingConversationView({callState: "gather", calleeId: "Mr Smith"})
|
||||
)
|
||||
)
|
||||
),
|
||||
@ -446,6 +462,9 @@
|
||||
React.renderComponent(App(null), body);
|
||||
|
||||
_renderComponentsInIframes();
|
||||
|
||||
// Put the title back, in case views changed it.
|
||||
document.title = "Loop UI Components Showcase";
|
||||
});
|
||||
|
||||
})();
|
||||
|
@ -15,6 +15,7 @@
|
||||
var PanelView = loop.panel.PanelView;
|
||||
// 1.2. Conversation Window
|
||||
var IncomingCallView = loop.conversation.IncomingCallView;
|
||||
var DesktopPendingConversationView = loop.conversationViews.PendingConversationView;
|
||||
|
||||
// 2. Standalone webapp
|
||||
var HomeView = loop.webapp.HomeView;
|
||||
@ -63,6 +64,12 @@
|
||||
});
|
||||
mockConversationModel.startSession = noop;
|
||||
|
||||
var mockWebSocket = new loop.CallConnectionWebSocket({
|
||||
url: "fake",
|
||||
callId: "fakeId",
|
||||
websocketToken: "fakeToken"
|
||||
});
|
||||
|
||||
var notifications = new loop.shared.models.NotificationCollection();
|
||||
var errNotifications = new loop.shared.models.NotificationCollection();
|
||||
errNotifications.error("Error!");
|
||||
@ -223,12 +230,21 @@
|
||||
<Section name="PendingConversationView">
|
||||
<Example summary="Pending conversation view (connecting)" dashed="true">
|
||||
<div className="standalone">
|
||||
<PendingConversationView />
|
||||
<PendingConversationView websocket={mockWebSocket}/>
|
||||
</div>
|
||||
</Example>
|
||||
<Example summary="Pending conversation view (ringing)" dashed="true">
|
||||
<div className="standalone">
|
||||
<PendingConversationView callState="ringing"/>
|
||||
<PendingConversationView websocket={mockWebSocket} callState="ringing"/>
|
||||
</div>
|
||||
</Example>
|
||||
</Section>
|
||||
|
||||
<Section name="PendingConversationView (Desktop)">
|
||||
<Example summary="Connecting" dashed="true"
|
||||
style={{width: "260px", height: "265px"}}>
|
||||
<div className="fx-embedded">
|
||||
<DesktopPendingConversationView callState={"gather"} calleeId="Mr Smith" />
|
||||
</div>
|
||||
</Example>
|
||||
</Section>
|
||||
@ -446,6 +462,9 @@
|
||||
React.renderComponent(<App />, body);
|
||||
|
||||
_renderComponentsInIframes();
|
||||
|
||||
// Put the title back, in case views changed it.
|
||||
document.title = "Loop UI Components Showcase";
|
||||
});
|
||||
|
||||
})();
|
||||
|
@ -4,6 +4,7 @@
|
||||
|
||||
package org.mozilla.gecko.menu;
|
||||
|
||||
import org.mozilla.gecko.NewTabletUI;
|
||||
import org.mozilla.gecko.R;
|
||||
|
||||
import android.content.Context;
|
||||
@ -20,7 +21,9 @@ public class MenuItemActionBar extends ImageButton
|
||||
}
|
||||
|
||||
public MenuItemActionBar(Context context, AttributeSet attrs) {
|
||||
this(context, attrs, R.attr.menuItemActionBarStyle);
|
||||
// TODO: Remove this branch (and associated attr) when old tablet is removed.
|
||||
this(context, attrs, (NewTabletUI.isEnabled(context)) ?
|
||||
R.attr.menuItemActionBarStyleNewTablet : R.attr.menuItemActionBarStyle);
|
||||
}
|
||||
|
||||
public MenuItemActionBar(Context context, AttributeSet attrs, int defStyle) {
|
||||
|
@ -51,12 +51,10 @@
|
||||
android:paddingLeft="6dip"
|
||||
android:paddingRight="4dip"/>
|
||||
|
||||
<!-- TODO: The reload asset is too small (bug 1072466) so we have white-space above and below the menu item.
|
||||
We add marginTop to center and compensate: remove this when the final asset is added. -->
|
||||
<LinearLayout android:id="@+id/menu_items"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_marginTop="2dp"
|
||||
android:gravity="center_vertical"
|
||||
android:layout_marginLeft="6dp"
|
||||
android:orientation="horizontal"
|
||||
android:layout_toLeftOf="@id/tabs"/>
|
||||
|
@ -15,6 +15,8 @@
|
||||
<dimen name="forward_default_offset">-13dip</dimen>
|
||||
<dimen name="new_tablet_forward_default_offset">-6dp</dimen>
|
||||
|
||||
<dimen name="new_tablet_browser_toolbar_menu_item_padding">19dp</dimen>
|
||||
|
||||
<dimen name="tabs_counter_size">26sp</dimen>
|
||||
<dimen name="panel_grid_view_column_width">200dp</dimen>
|
||||
|
||||
|
@ -70,6 +70,13 @@
|
||||
<item name="android:scaleType">fitCenter</item>
|
||||
</style>
|
||||
|
||||
<style name="Widget.MenuItemActionBar.NewTablet">
|
||||
<item name="android:layout_width">wrap_content</item>
|
||||
<item name="android:layout_height">wrap_content</item>
|
||||
<item name="android:padding">@dimen/new_tablet_browser_toolbar_menu_item_padding</item>
|
||||
<item name="android:scaleType">center</item>
|
||||
</style>
|
||||
|
||||
<style name="Widget.BookmarksListView" parent="Widget.HomeListView">
|
||||
<item name="android:scrollbarStyle">outsideOverlay</item>
|
||||
</style>
|
||||
|
@ -14,6 +14,9 @@
|
||||
<!-- Style for MenuItemActionBar -->
|
||||
<attr name="menuItemActionBarStyle" format="reference"/>
|
||||
|
||||
<!-- Style for MenuItemActionBar -->
|
||||
<attr name="menuItemActionBarStyleNewTablet" format="reference"/>
|
||||
|
||||
<!-- Style for MenuItemActionBar -->
|
||||
<attr name="menuItemActionModeStyle" format="reference"/>
|
||||
|
||||
|
@ -93,6 +93,7 @@
|
||||
<item name="geckoMenuListViewStyle">@style/Widget.GeckoMenuListView</item>
|
||||
<item name="homeListViewStyle">@style/Widget.HomeListView</item>
|
||||
<item name="menuItemActionBarStyle">@style/Widget.MenuItemActionBar</item>
|
||||
<item name="menuItemActionBarStyleNewTablet">@style/Widget.MenuItemActionBar.NewTablet</item>
|
||||
<item name="menuItemActionModeStyle">@style/GeckoActionBar.Button</item>
|
||||
<item name="menuItemShareActionButtonStyle">@style/Widget.MenuItemSecondaryActionBar</item>
|
||||
<item name="panelGridViewStyle">@style/Widget.PanelGridView</item>
|
||||
|
@ -15,6 +15,8 @@ import org.mozilla.gecko.FennecTalosAssert;
|
||||
import org.mozilla.gecko.AppConstants;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.os.PowerManager;
|
||||
import android.test.ActivityInstrumentationTestCase2;
|
||||
import android.util.Log;
|
||||
|
||||
@ -107,4 +109,13 @@ public abstract class BaseRobocopTest extends ActivityInstrumentationTestCase2<A
|
||||
mAsserter.setLogFile(mLogFile);
|
||||
mAsserter.setTestName(getClass().getName());
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure that the screen on the test device is powered on during tests.
|
||||
*/
|
||||
public void throwIfScreenNotOn() {
|
||||
final PowerManager pm = (PowerManager) getActivity().getSystemService(Context.POWER_SERVICE);
|
||||
mAsserter.ok(pm.isScreenOn(),
|
||||
"Robocop tests need the test device screen to be powered on.", "");
|
||||
}
|
||||
}
|
||||
|
@ -121,6 +121,9 @@ abstract class BaseTest extends BaseRobocopTest {
|
||||
mActions = new FennecNativeActions(mActivity, mSolo, getInstrumentation(), mAsserter);
|
||||
mDevice = new Device();
|
||||
mDatabaseHelper = new DatabaseHelper(mActivity, mAsserter);
|
||||
|
||||
// Ensure Robocop tests are run with Display powered on.
|
||||
throwIfScreenNotOn();
|
||||
}
|
||||
|
||||
protected void initializeProfile() {
|
||||
|
@ -74,6 +74,9 @@ abstract class UITest extends BaseRobocopTest
|
||||
// Helpers depend on components so initialize them first.
|
||||
initComponents();
|
||||
initHelpers();
|
||||
|
||||
// Ensure Robocop tests are run with Display powered on.
|
||||
throwIfScreenNotOn();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -561,12 +561,12 @@ this.SocialService = {
|
||||
let requestingWindow = aDOMDocument.defaultView.top;
|
||||
let chromeWin = this._getChromeWindow(requestingWindow).wrappedJSObject;
|
||||
let browser = chromeWin.gBrowser.getBrowserForDocument(aDOMDocument);
|
||||
let requestingURI = Services.io.newURI(aDOMDocument.location.href, null, null);
|
||||
let requestingURI = Services.io.newURI(aAddonInstaller.addon.manifest.origin, null, null);
|
||||
|
||||
let productName = brandBundle.GetStringFromName("brandShortName");
|
||||
|
||||
let message = browserBundle.formatStringFromName("service.install.description",
|
||||
[aAddonInstaller.addon.manifest.name, productName], 2);
|
||||
[requestingURI.host, productName], 2);
|
||||
|
||||
let action = {
|
||||
label: browserBundle.GetStringFromName("service.install.ok.label"),
|
||||
|
@ -40,6 +40,7 @@ wizardpage {
|
||||
|
||||
.wizard-buttons button {
|
||||
-moz-appearance: toolbarbutton;
|
||||
color: ButtonText;
|
||||
min-height: 22px;
|
||||
margin: 0 6px;
|
||||
padding: 0;
|
||||
|
Loading…
Reference in New Issue
Block a user