--- a/browser/components/loop/MozLoopAPI.jsm
+++ b/browser/components/loop/MozLoopAPI.jsm
@@ -596,25 +596,28 @@ function injectLoopAPI(targetWindow) {
}
return appVersionInfo;
}
},
/**
* Composes an email via the external protocol service.
*
- * @param {String} subject Subject of the email to send
- * @param {String} body Body message of the email to send
+ * @param {String} subject Subject of the email to send
+ * @param {String} body Body message of the email to send
+ * @param {String} recipient Recipient email address (optional)
*/
composeEmail: {
enumerable: true,
writable: true,
- value: function(subject, body) {
- let mailtoURL = "mailto:?subject=" + encodeURIComponent(subject) + "&" +
- "body=" + encodeURIComponent(body);
+ value: function(subject, body, recipient) {
+ recipient = recipient || "";
+ let mailtoURL = "mailto:" + encodeURIComponent(recipient) +
+ "?subject=" + encodeURIComponent(subject) +
+ "&body=" + encodeURIComponent(body);
extProtocolSvc.loadURI(CommonUtils.makeURI(mailtoURL));
}
},
/**
* Adds a value to a telemetry histogram.
*
* @param {string} histogramId Name of the telemetry histogram to update.
--- a/browser/components/loop/content/js/conversationViews.js
+++ b/browser/components/loop/content/js/conversationViews.js
@@ -7,18 +7,30 @@
/* global loop:true, React */
var loop = loop || {};
loop.conversationViews = (function(mozL10n) {
var CALL_STATES = loop.store.CALL_STATES;
var CALL_TYPES = loop.shared.utils.CALL_TYPES;
var sharedActions = loop.shared.actions;
+ var sharedUtils = loop.shared.utils;
var sharedViews = loop.shared.views;
+ // This duplicates a similar function in contacts.jsx that isn't used in the
+ // conversation window. If we get too many of these, we might want to consider
+ // finding a logical place for them to be shared.
+ function _getPreferredEmail(contact) {
+ // A contact may not contain email addresses, but only a phone number.
+ if (!contact.email || contact.email.length === 0) {
+ return { value: "" };
+ }
+ return contact.email.find(e => e.pref) || contact.email[0];
+ }
+
/**
* Displays information about the call
* Caller avatar, name & conversation creation date
*/
var CallIdentifierView = React.createClass({displayName: 'CallIdentifierView',
propTypes: {
peerIdentifier: React.PropTypes.string,
showIcons: React.PropTypes.bool.isRequired,
@@ -88,35 +100,24 @@ loop.conversationViews = (function(mozL1
* Allows the view to be extended with different buttons and progress
* via children properties.
*/
var ConversationDetailView = React.createClass({displayName: 'ConversationDetailView',
propTypes: {
contact: React.PropTypes.object
},
- // This duplicates a similar function in contacts.jsx that isn't used in the
- // conversation window. If we get too many of these, we might want to consider
- // finding a logical place for them to be shared.
- _getPreferredEmail: function(contact) {
- // A contact may not contain email addresses, but only a phone number.
- if (!contact.email || contact.email.length == 0) {
- return { value: "" };
- }
- return contact.email.find(e => e.pref) || contact.email[0];
- },
-
render: function() {
var contactName;
if (this.props.contact.name &&
this.props.contact.name[0]) {
contactName = this.props.contact.name[0];
} else {
- contactName = this._getPreferredEmail(this.props.contact).value;
+ contactName = _getPreferredEmail(this.props.contact).value;
}
document.title = contactName;
return (
React.DOM.div({className: "call-window"},
CallIdentifierView({
peerIdentifier: contactName,
@@ -182,47 +183,80 @@ loop.conversationViews = (function(mozL1
);
}
});
/**
* Call failed view. Displayed when a call fails.
*/
var CallFailedView = React.createClass({displayName: 'CallFailedView',
+ mixins: [Backbone.Events],
+
propTypes: {
- dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired
+ dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
+ store: React.PropTypes.instanceOf(
+ loop.store.ConversationStore).isRequired,
+ contact: React.PropTypes.object.isRequired
+ },
+
+ getInitialState: function() {
+ return {emailLinkButtonDisabled: false};
+ },
+
+ componentDidMount: function() {
+ this.listenTo(this.props.store, "change:emailLink",
+ this._onEmailLinkReceived);
+ },
+
+ componentWillUnmount: function() {
+ this.stopListening(this.props.store);
+ },
+
+ _onEmailLinkReceived: function() {
+ var emailLink = this.props.store.get("emailLink");
+ var contactEmail = _getPreferredEmail(this.props.contact).value;
+ sharedUtils.composeCallUrlEmail(emailLink, contactEmail);
+ window.close();
},
retryCall: function() {
this.props.dispatcher.dispatch(new sharedActions.RetryCall());
},
cancelCall: function() {
this.props.dispatcher.dispatch(new sharedActions.CancelCall());
},
+ emailLink: function() {
+ this.setState({emailLinkButtonDisabled: true});
+
+ this.props.dispatcher.dispatch(new sharedActions.FetchEmailLink());
+ },
+
render: function() {
return (
React.DOM.div({className: "call-window"},
React.DOM.h2(null, mozL10n.get("generic_failure_title")),
- React.DOM.p({className: "btn-label"}, mozL10n.get("generic_failure_no_reason2")),
+ React.DOM.p({className: "btn-label"}, mozL10n.get("generic_failure_with_reason2")),
React.DOM.div({className: "btn-group call-action-group"},
- React.DOM.div({className: "fx-embedded-call-button-spacer"}),
- React.DOM.button({className: "btn btn-accept btn-retry",
- onClick: this.retryCall},
- mozL10n.get("retry_call_button")
- ),
- React.DOM.div({className: "fx-embedded-call-button-spacer"}),
- React.DOM.button({className: "btn btn-cancel",
- onClick: this.cancelCall},
- mozL10n.get("cancel_button")
- ),
- React.DOM.div({className: "fx-embedded-call-button-spacer"})
+ React.DOM.button({className: "btn btn-cancel",
+ onClick: this.cancelCall},
+ mozL10n.get("cancel_button")
+ ),
+ React.DOM.button({className: "btn btn-info btn-retry",
+ onClick: this.retryCall},
+ mozL10n.get("retry_call_button")
+ ),
+ React.DOM.button({className: "btn btn-info btn-email",
+ onClick: this.emailLink,
+ disabled: this.state.emailLinkButtonDisabled},
+ mozL10n.get("share_button2")
+ )
)
)
);
}
});
var OngoingConversationView = React.createClass({displayName: 'OngoingConversationView',
propTypes: {
@@ -420,17 +454,19 @@ loop.conversationViews = (function(mozL1
render: function() {
switch (this.state.callState) {
case CALL_STATES.CLOSE: {
this._closeWindow();
return null;
}
case CALL_STATES.TERMINATED: {
return (CallFailedView({
- dispatcher: this.props.dispatcher}
+ dispatcher: this.props.dispatcher,
+ store: this.props.store,
+ contact: this.state.contact}
));
}
case CALL_STATES.ONGOING: {
return (OngoingConversationView({
dispatcher: this.props.dispatcher,
video: {enabled: !this.state.videoMuted},
audio: {enabled: !this.state.audioMuted}}
)
@@ -440,17 +476,17 @@ loop.conversationViews = (function(mozL1
return this._renderFeedbackView();
}
default: {
return (PendingConversationView({
dispatcher: this.props.dispatcher,
callState: this.state.callState,
contact: this.state.contact,
enableCancelButton: this._isCancellable()}
- ))
+ ));
}
}
},
});
return {
PendingConversationView: PendingConversationView,
CallIdentifierView: CallIdentifierView,
--- a/browser/components/loop/content/js/conversationViews.jsx
+++ b/browser/components/loop/content/js/conversationViews.jsx
@@ -7,18 +7,30 @@
/* global loop:true, React */
var loop = loop || {};
loop.conversationViews = (function(mozL10n) {
var CALL_STATES = loop.store.CALL_STATES;
var CALL_TYPES = loop.shared.utils.CALL_TYPES;
var sharedActions = loop.shared.actions;
+ var sharedUtils = loop.shared.utils;
var sharedViews = loop.shared.views;
+ // This duplicates a similar function in contacts.jsx that isn't used in the
+ // conversation window. If we get too many of these, we might want to consider
+ // finding a logical place for them to be shared.
+ function _getPreferredEmail(contact) {
+ // A contact may not contain email addresses, but only a phone number.
+ if (!contact.email || contact.email.length === 0) {
+ return { value: "" };
+ }
+ return contact.email.find(e => e.pref) || contact.email[0];
+ }
+
/**
* Displays information about the call
* Caller avatar, name & conversation creation date
*/
var CallIdentifierView = React.createClass({
propTypes: {
peerIdentifier: React.PropTypes.string,
showIcons: React.PropTypes.bool.isRequired,
@@ -88,35 +100,24 @@ loop.conversationViews = (function(mozL1
* Allows the view to be extended with different buttons and progress
* via children properties.
*/
var ConversationDetailView = React.createClass({
propTypes: {
contact: React.PropTypes.object
},
- // This duplicates a similar function in contacts.jsx that isn't used in the
- // conversation window. If we get too many of these, we might want to consider
- // finding a logical place for them to be shared.
- _getPreferredEmail: function(contact) {
- // A contact may not contain email addresses, but only a phone number.
- if (!contact.email || contact.email.length == 0) {
- return { value: "" };
- }
- return contact.email.find(e => e.pref) || contact.email[0];
- },
-
render: function() {
var contactName;
if (this.props.contact.name &&
this.props.contact.name[0]) {
contactName = this.props.contact.name[0];
} else {
- contactName = this._getPreferredEmail(this.props.contact).value;
+ contactName = _getPreferredEmail(this.props.contact).value;
}
document.title = contactName;
return (
<div className="call-window">
<CallIdentifierView
peerIdentifier={contactName}
@@ -182,47 +183,80 @@ loop.conversationViews = (function(mozL1
);
}
});
/**
* Call failed view. Displayed when a call fails.
*/
var CallFailedView = React.createClass({
+ mixins: [Backbone.Events],
+
propTypes: {
- dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired
+ dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
+ store: React.PropTypes.instanceOf(
+ loop.store.ConversationStore).isRequired,
+ contact: React.PropTypes.object.isRequired
+ },
+
+ getInitialState: function() {
+ return {emailLinkButtonDisabled: false};
+ },
+
+ componentDidMount: function() {
+ this.listenTo(this.props.store, "change:emailLink",
+ this._onEmailLinkReceived);
+ },
+
+ componentWillUnmount: function() {
+ this.stopListening(this.props.store);
+ },
+
+ _onEmailLinkReceived: function() {
+ var emailLink = this.props.store.get("emailLink");
+ var contactEmail = _getPreferredEmail(this.props.contact).value;
+ sharedUtils.composeCallUrlEmail(emailLink, contactEmail);
+ window.close();
},
retryCall: function() {
this.props.dispatcher.dispatch(new sharedActions.RetryCall());
},
cancelCall: function() {
this.props.dispatcher.dispatch(new sharedActions.CancelCall());
},
+ emailLink: function() {
+ this.setState({emailLinkButtonDisabled: true});
+
+ this.props.dispatcher.dispatch(new sharedActions.FetchEmailLink());
+ },
+
render: function() {
return (
<div className="call-window">
<h2>{mozL10n.get("generic_failure_title")}</h2>
- <p className="btn-label">{mozL10n.get("generic_failure_no_reason2")}</p>
+ <p className="btn-label">{mozL10n.get("generic_failure_with_reason2")}</p>
<div className="btn-group call-action-group">
- <div className="fx-embedded-call-button-spacer"></div>
- <button className="btn btn-accept btn-retry"
- onClick={this.retryCall}>
- {mozL10n.get("retry_call_button")}
- </button>
- <div className="fx-embedded-call-button-spacer"></div>
- <button className="btn btn-cancel"
- onClick={this.cancelCall}>
- {mozL10n.get("cancel_button")}
- </button>
- <div className="fx-embedded-call-button-spacer"></div>
+ <button className="btn btn-cancel"
+ onClick={this.cancelCall}>
+ {mozL10n.get("cancel_button")}
+ </button>
+ <button className="btn btn-info btn-retry"
+ onClick={this.retryCall}>
+ {mozL10n.get("retry_call_button")}
+ </button>
+ <button className="btn btn-info btn-email"
+ onClick={this.emailLink}
+ disabled={this.state.emailLinkButtonDisabled}>
+ {mozL10n.get("share_button2")}
+ </button>
</div>
</div>
);
}
});
var OngoingConversationView = React.createClass({
propTypes: {
@@ -421,16 +455,18 @@ loop.conversationViews = (function(mozL1
switch (this.state.callState) {
case CALL_STATES.CLOSE: {
this._closeWindow();
return null;
}
case CALL_STATES.TERMINATED: {
return (<CallFailedView
dispatcher={this.props.dispatcher}
+ store={this.props.store}
+ contact={this.state.contact}
/>);
}
case CALL_STATES.ONGOING: {
return (<OngoingConversationView
dispatcher={this.props.dispatcher}
video={{enabled: !this.state.videoMuted}}
audio={{enabled: !this.state.audioMuted}}
/>
@@ -440,17 +476,17 @@ loop.conversationViews = (function(mozL1
return this._renderFeedbackView();
}
default: {
return (<PendingConversationView
dispatcher={this.props.dispatcher}
callState={this.state.callState}
contact={this.state.contact}
enableCancelButton={this._isCancellable()}
- />)
+ />);
}
}
},
});
return {
PendingConversationView: PendingConversationView,
CallIdentifierView: CallIdentifierView,
--- a/browser/components/loop/content/js/panel.js
+++ b/browser/components/loop/content/js/panel.js
@@ -10,16 +10,17 @@
var loop = loop || {};
loop.panel = (function(_, mozL10n) {
"use strict";
var sharedViews = loop.shared.views;
var sharedModels = loop.shared.models;
var sharedMixins = loop.shared.mixins;
var sharedActions = loop.shared.actions;
+ var sharedUtils = loop.shared.utils;
var Button = sharedViews.Button;
var ButtonGroup = sharedViews.ButtonGroup;
var ContactsList = loop.contacts.ContactsList;
var ContactDetailsForm = loop.contacts.ContactDetailsForm;
var __ = mozL10n.get; // aliasing translation function as __ for concision
var TabView = React.createClass({displayName: 'TabView',
propTypes: {
@@ -357,21 +358,17 @@ loop.panel = (function(_, mozL10n) {
this.setState(this.getInitialState());
}
}
},
handleEmailButtonClick: function(event) {
this.handleLinkExfiltration(event);
- navigator.mozLoop.composeEmail(
- __("share_email_subject4", { clientShortname: __("clientShortname2")}),
- __("share_email_body4", { callUrl: this.state.callUrl,
- clientShortname: __("clientShortname2"),
- learnMoreUrl: navigator.mozLoop.getLoopCharPref("learnMoreUrl") }));
+ sharedUtils.composeCallUrlEmail(this.state.callUrl);
},
handleCopyButtonClick: function(event) {
this.handleLinkExfiltration(event);
// XXX the mozLoop object should be passed as a prop, to ease testing and
// using a fake implementation in UI components showcase.
navigator.mozLoop.copyString(this.state.callUrl);
this.setState({copied: true});
--- a/browser/components/loop/content/js/panel.jsx
+++ b/browser/components/loop/content/js/panel.jsx
@@ -10,16 +10,17 @@
var loop = loop || {};
loop.panel = (function(_, mozL10n) {
"use strict";
var sharedViews = loop.shared.views;
var sharedModels = loop.shared.models;
var sharedMixins = loop.shared.mixins;
var sharedActions = loop.shared.actions;
+ var sharedUtils = loop.shared.utils;
var Button = sharedViews.Button;
var ButtonGroup = sharedViews.ButtonGroup;
var ContactsList = loop.contacts.ContactsList;
var ContactDetailsForm = loop.contacts.ContactDetailsForm;
var __ = mozL10n.get; // aliasing translation function as __ for concision
var TabView = React.createClass({
propTypes: {
@@ -357,21 +358,17 @@ loop.panel = (function(_, mozL10n) {
this.setState(this.getInitialState());
}
}
},
handleEmailButtonClick: function(event) {
this.handleLinkExfiltration(event);
- navigator.mozLoop.composeEmail(
- __("share_email_subject4", { clientShortname: __("clientShortname2")}),
- __("share_email_body4", { callUrl: this.state.callUrl,
- clientShortname: __("clientShortname2"),
- learnMoreUrl: navigator.mozLoop.getLoopCharPref("learnMoreUrl") }));
+ sharedUtils.composeCallUrlEmail(this.state.callUrl);
},
handleCopyButtonClick: function(event) {
this.handleLinkExfiltration(event);
// XXX the mozLoop object should be passed as a prop, to ease testing and
// using a fake implementation in UI components showcase.
navigator.mozLoop.copyString(this.state.callUrl);
this.setState({copied: true});
--- a/browser/components/loop/content/shared/css/conversation.css
+++ b/browser/components/loop/content/shared/css/conversation.css
@@ -235,26 +235,31 @@
.call-window {
display: flex;
flex-direction: column;
align-items: center;
justify-content: space-between;
min-height: 230px;
}
+.call-window > .btn-label {
+ text-align: center;
+}
+
.call-action-group {
display: flex;
- padding: 2.5em 0 0 0;
+ padding: 2.5em 4px 0 4px;
width: 100%;
- justify-content: space-around;
}
.call-action-group > .btn {
- margin-left: .5em;
height: 26px;
+ border-radius: 2px;
+ margin: 0 4px;
+ min-width: 64px;
}
.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
--- a/browser/components/loop/content/shared/js/actions.js
+++ b/browser/components/loop/content/shared/js/actions.js
@@ -26,16 +26,23 @@ loop.shared.actions = (function() {
}
Action.define = function(name, schema) {
return Action.bind(null, name, schema);
};
return {
/**
+ * Fetch a new call url from the server, intended to be sent over email when
+ * a contact can't be reached.
+ */
+ FetchEmailLink: Action.define("fetchEmailLink", {
+ }),
+
+ /**
* Used to trigger gathering of initial call data.
*/
GatherCallData: Action.define("gatherCallData", {
// Specify the callId for an incoming call.
callId: [String, null],
outgoing: Boolean
}),
--- a/browser/components/loop/content/shared/js/conversationStore.js
+++ b/browser/components/loop/content/shared/js/conversationStore.js
@@ -121,17 +121,18 @@ loop.store.ConversationStore = (function
"connectionProgress",
"gatherCallData",
"connectCall",
"hangupCall",
"peerHungupCall",
"cancelCall",
"retryCall",
"mediaConnected",
- "setMute"
+ "setMute",
+ "fetchEmailLink"
]);
},
/**
* Handles the connection failure action, setting the state to
* terminated.
*
* @param {sharedActions.ConnectionFailure} actionData The action data.
@@ -299,16 +300,33 @@ loop.store.ConversationStore = (function
* @param {sharedActions.setMute} actionData The mute state for the stream type.
*/
setMute: function(actionData) {
var muteType = actionData.type + "Muted";
this.set(muteType, !actionData.enabled);
},
/**
+ * Fetches a new call URL intended to be sent over email when a contact
+ * can't be reached.
+ */
+ fetchEmailLink: function() {
+ // XXX This is an empty string as a conversation identifier. Bug 1015938 implements
+ // a user-set string.
+ this.client.requestCallUrl("", function(err, callUrlData) {
+ if (err) {
+ // XXX better error reporting in the UI
+ console.error(err);
+ return;
+ }
+ this.set("emailLink", callUrlData.callUrl);
+ }.bind(this));
+ },
+
+ /**
* Obtains the outgoing call data from the server and handles the
* result.
*/
_setupOutgoingCall: function() {
var contactAddresses = [];
this.get("contact").email.forEach(function(address) {
contactAddresses.push(address.value);
--- a/browser/components/loop/content/shared/js/utils.js
+++ b/browser/components/loop/content/shared/js/utils.js
@@ -1,17 +1,17 @@
/* 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.utils = (function() {
+loop.shared.utils = (function(mozL10n) {
"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"
@@ -91,16 +91,42 @@ loop.shared.utils = (function() {
return this._iOSRegex.test(platform);
},
locationHash: function() {
return window.location.hash;
}
};
+ /**
+ * Generates and opens a mailto: url with call URL information prefilled.
+ * Note: This only works for Desktop.
+ *
+ * @param {String} callUrl The call URL.
+ * @param {String} recipient The recipient email address (optional).
+ */
+ function composeCallUrlEmail(callUrl, recipient) {
+ if (typeof navigator.mozLoop === "undefined") {
+ console.warn("composeCallUrlEmail isn't available for Loop standalone.");
+ return;
+ }
+ navigator.mozLoop.composeEmail(
+ mozL10n.get("share_email_subject4", {
+ clientShortname: mozL10n.get("clientShortname2")
+ }),
+ mozL10n.get("share_email_body4", {
+ callUrl: callUrl,
+ clientShortname: mozL10n.get("clientShortname2"),
+ learnMoreUrl: navigator.mozLoop.getLoopCharPref("learnMoreUrl")
+ }),
+ recipient
+ );
+ }
+
return {
CALL_TYPES: CALL_TYPES,
Helper: Helper,
+ composeCallUrlEmail: composeCallUrlEmail,
formatDate: formatDate,
getTargetPlatform: getTargetPlatform,
getBoolPreference: getBoolPreference
};
-})();
+})(document.mozL10n || navigator.mozL10n);
--- a/browser/components/loop/test/desktop-local/conversationViews_test.js
+++ b/browser/components/loop/test/desktop-local/conversationViews_test.js
@@ -1,16 +1,17 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
var expect = chai.expect;
describe("loop.conversationViews", function () {
"use strict";
+ var sharedUtils = loop.shared.utils;
var sandbox, oldTitle, view, dispatcher, contact;
var CALL_STATES = loop.store.CALL_STATES;
beforeEach(function() {
sandbox = sinon.sandbox.create();
oldTitle = document.title;
@@ -196,23 +197,35 @@ describe("loop.conversationViews", funct
sinon.assert.calledOnce(dispatcher.dispatch);
sinon.assert.calledWithMatch(dispatcher.dispatch,
sinon.match.hasOwn("name", "cancelCall"));
});
});
describe("CallFailedView", function() {
+ var store;
+
function mountTestComponent(props) {
return TestUtils.renderIntoDocument(
loop.conversationViews.CallFailedView({
- dispatcher: dispatcher
+ dispatcher: dispatcher,
+ store: store,
+ contact: {email: [{value: "test@test.tld"}]}
}));
}
+ beforeEach(function() {
+ store = new loop.store.ConversationStore({}, {
+ dispatcher: dispatcher,
+ client: {},
+ sdkDriver: {}
+ });
+ });
+
it("should dispatch a retryCall action when the retry button is pressed",
function() {
view = mountTestComponent();
var retryBtn = view.getDOMNode().querySelector('.btn-retry');
React.addons.TestUtils.Simulate.click(retryBtn);
@@ -228,16 +241,58 @@ describe("loop.conversationViews", funct
var cancelBtn = view.getDOMNode().querySelector('.btn-cancel');
React.addons.TestUtils.Simulate.click(cancelBtn);
sinon.assert.calledOnce(dispatcher.dispatch);
sinon.assert.calledWithMatch(dispatcher.dispatch,
sinon.match.hasOwn("name", "cancelCall"));
});
+
+ it("should dispatch a fetchEmailLink action when the cancel button is pressed",
+ function() {
+ view = mountTestComponent();
+
+ var emailLinkBtn = view.getDOMNode().querySelector('.btn-email');
+
+ React.addons.TestUtils.Simulate.click(emailLinkBtn);
+
+ sinon.assert.calledOnce(dispatcher.dispatch);
+ sinon.assert.calledWithMatch(dispatcher.dispatch,
+ sinon.match.hasOwn("name", "fetchEmailLink"));
+ });
+
+ it("should disable the email link button once the action is dispatched",
+ function() {
+ view = mountTestComponent();
+ var emailLinkBtn = view.getDOMNode().querySelector('.btn-email');
+ React.addons.TestUtils.Simulate.click(emailLinkBtn);
+
+ expect(view.getDOMNode().querySelector(".btn-email").disabled).eql(true);
+ });
+
+ it("should compose an email once the email link is received", function() {
+ var composeCallUrlEmail = sandbox.stub(sharedUtils, "composeCallUrlEmail");
+ view = mountTestComponent();
+ store.set("emailLink", "http://fake.invalid/");
+
+ sinon.assert.calledOnce(composeCallUrlEmail);
+ sinon.assert.calledWithExactly(composeCallUrlEmail,
+ "http://fake.invalid/", "test@test.tld");
+ });
+
+ it("should close the conversation window once the email link is received",
+ function() {
+ sandbox.stub(window, "close");
+ view = mountTestComponent();
+
+ store.set("emailLink", "http://fake.invalid/");
+
+ sinon.assert.calledOnce(window.close);
+ });
});
describe("OngoingConversationView", function() {
function mountTestComponent(props) {
return TestUtils.renderIntoDocument(
loop.conversationViews.OngoingConversationView(props));
}
--- a/browser/components/loop/test/desktop-local/panel_test.js
+++ b/browser/components/loop/test/desktop-local/panel_test.js
@@ -3,16 +3,17 @@
* You can obtain one at http://mozilla.org/MPL/2.0/. */
/*jshint newcap:false*/
/*global loop, sinon */
var expect = chai.expect;
var TestUtils = React.addons.TestUtils;
var sharedActions = loop.shared.actions;
+var sharedUtils = loop.shared.utils;
describe("loop.panel", function() {
"use strict";
var sandbox, notifications, fakeXHR, requests = [];
beforeEach(function(done) {
sandbox = sinon.sandbox.create();
@@ -444,25 +445,28 @@ describe("loop.panel", function() {
});
it("should have 0 pending notifications", function() {
expect(view.props.notifications.length).eql(0);
});
it("should display a share button for email", function() {
fakeClient.requestCallUrl = sandbox.stub();
+ var composeCallUrlEmail = sandbox.stub(sharedUtils, "composeCallUrlEmail");
var view = TestUtils.renderIntoDocument(loop.panel.CallUrlResult({
notifications: notifications,
client: fakeClient
}));
view.setState({pending: false, callUrl: "http://example.com"});
TestUtils.findRenderedDOMComponentWithClass(view, "button-email");
TestUtils.Simulate.click(view.getDOMNode().querySelector(".button-email"));
- sinon.assert.calledOnce(navigator.mozLoop.composeEmail);
+
+ sinon.assert.calledOnce(composeCallUrlEmail);
+ sinon.assert.calledWithExactly(composeCallUrlEmail, "http://example.com");
});
it("should feature a copy button capable of copying the call url when clicked", function() {
fakeClient.requestCallUrl = sandbox.stub();
var view = TestUtils.renderIntoDocument(loop.panel.CallUrlResult({
notifications: notifications,
client: fakeClient
}));
--- a/browser/components/loop/test/shared/conversationStore_test.js
+++ b/browser/components/loop/test/shared/conversationStore_test.js
@@ -33,17 +33,18 @@ describe("loop.store.ConversationStore",
type: "home",
value: "fakeEmail",
pref: true
}]
};
dispatcher = new loop.Dispatcher();
client = {
- setupOutgoingCall: sinon.stub()
+ setupOutgoingCall: sinon.stub(),
+ requestCallUrl: sinon.stub()
};
sdkDriver = {
connectSession: sinon.stub(),
disconnectSession: sinon.stub()
};
wsCancelSpy = sinon.spy();
wsCloseSpy = sinon.spy();
@@ -561,16 +562,38 @@ describe("loop.store.ConversationStore",
type: "video",
enabled: false
}));
expect(store.get("videoMuted")).eql(true);
});
});
+ describe("#fetchEmailLink", function() {
+ it("should request a new call url to the server", function() {
+ dispatcher.dispatch(new sharedActions.FetchEmailLink());
+
+ sinon.assert.calledOnce(client.requestCallUrl);
+ sinon.assert.calledWith(client.requestCallUrl, "");
+ });
+
+ it("should update the emailLink attribute when the new call url is received",
+ function() {
+ client.requestCallUrl = function(callId, cb) {
+ cb(null, {callUrl: "http://fake.invalid/"});
+ };
+ dispatcher.dispatch(new sharedActions.FetchEmailLink());
+
+ expect(store.get("emailLink")).eql("http://fake.invalid/");
+ });
+
+ // XXX bug 1048162 Part 2
+ it.skip("should trigger an error in case of failure");
+ });
+
describe("Events", function() {
describe("Websocket progress", function() {
beforeEach(function() {
dispatcher.dispatch(
new sharedActions.ConnectCall({sessionData: fakeSessionData}));
sandbox.stub(dispatcher, "dispatch");
});
--- a/browser/components/loop/test/shared/utils_test.js
+++ b/browser/components/loop/test/shared/utils_test.js
@@ -13,16 +13,17 @@ describe("loop.shared.utils", function()
var sandbox;
var sharedUtils = loop.shared.utils;
beforeEach(function() {
sandbox = sinon.sandbox.create();
});
afterEach(function() {
+ navigator.mozLoop = undefined;
sandbox.restore();
});
describe("Helper", function() {
var helper;
beforeEach(function() {
helper = new sharedUtils.Helper();
@@ -105,17 +106,16 @@ describe("loop.shared.utils", function()
it("should return the formatted string", function() {
expect(sharedUtils.formatDate(1000)).eql("fake result");
});
});
describe("#getBoolPreference", function() {
afterEach(function() {
- navigator.mozLoop = undefined;
localStorage.removeItem("test.true");
});
describe("mozLoop set", function() {
beforeEach(function() {
navigator.mozLoop = {
getLoopBoolPref: function(prefName) {
return prefName === "test.true";
@@ -137,9 +137,36 @@ describe("loop.shared.utils", function()
describe("mozLoop not set", function() {
it("should return the localStorage value", function() {
localStorage.setItem("test.true", true);
expect(sharedUtils.getBoolPreference("test.true")).eql(true);
});
});
});
+
+ describe("#composeCallUrlEmail", function() {
+ var composeEmail;
+
+ beforeEach(function() {
+ // fake mozL10n
+ sandbox.stub(navigator.mozL10n, "get", function(id) {
+ switch(id) {
+ case "share_email_subject4": return "subject";
+ case "share_email_body4": return "body";
+ }
+ });
+ composeEmail = sandbox.spy();
+ navigator.mozLoop = {
+ getLoopCharPref: sandbox.spy(),
+ composeEmail: composeEmail
+ };
+ });
+
+ it("should compose a call url email", function() {
+ sharedUtils.composeCallUrlEmail("http://invalid", "fake@invalid.tld");
+
+ sinon.assert.calledOnce(composeEmail);
+ sinon.assert.calledWith(composeEmail,
+ "subject", "body", "fake@invalid.tld");
+ });
+ });
});