Bug 1048162 Part 2 - Display an error message if fetching an email link fails r=standard8,darrin a=lmandel
authorNicolas Perriault <nperriault@gmail.com>
Thu, 16 Oct 2014 21:29:18 -0400
changeset 225735 3fc523fcc7da
parent 225734 191b3ce44bea
child 225736 2edc9ed56fa4
push id3995
push userrjesup@wgate.com
push date2014-10-20 00:58 +0000
treeherdermozilla-beta@8c42ccaf8aa1 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersstandard8, darrin, lmandel
bugs1048162
milestone34.0
Bug 1048162 Part 2 - Display an error message if fetching an email link fails r=standard8,darrin a=lmandel
browser/components/loop/content/js/conversationViews.js
browser/components/loop/content/js/conversationViews.jsx
browser/components/loop/content/shared/css/conversation.css
browser/components/loop/content/shared/js/conversationStore.js
browser/components/loop/test/desktop-local/conversationViews_test.js
browser/components/loop/test/shared/conversationStore_test.js
browser/components/loop/ui/ui-showcase.js
browser/components/loop/ui/ui-showcase.jsx
--- a/browser/components/loop/content/js/conversationViews.js
+++ b/browser/components/loop/content/js/conversationViews.js
@@ -166,22 +166,20 @@ loop.conversationViews = (function(mozL1
       });
 
       return (
         ConversationDetailView({contact: this.props.contact}, 
 
           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: btnCancelStyles, 
-                      onClick: this.cancelCall}, 
-                mozL10n.get("initiate_call_cancel_button")
-              ), 
-            React.DOM.div({className: "fx-embedded-call-button-spacer"})
+            React.DOM.button({className: btnCancelStyles, 
+                    onClick: this.cancelCall}, 
+              mozL10n.get("initiate_call_cancel_button")
+            )
           )
 
         )
       );
     }
   });
 
   /**
@@ -189,60 +187,86 @@ loop.conversationViews = (function(mozL1
    */
   var CallFailedView = React.createClass({displayName: 'CallFailedView',
     mixins: [Backbone.Events],
 
     propTypes: {
       dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
       store: React.PropTypes.instanceOf(
         loop.store.ConversationStore).isRequired,
-      contact: React.PropTypes.object.isRequired
+      contact: React.PropTypes.object.isRequired,
+      // This is used by the UI showcase.
+      emailLinkError: React.PropTypes.bool,
     },
 
     getInitialState: function() {
-      return {emailLinkButtonDisabled: false};
+      return {
+        emailLinkError: this.props.emailLinkError,
+        emailLinkButtonDisabled: false
+      };
     },
 
     componentDidMount: function() {
       this.listenTo(this.props.store, "change:emailLink",
                     this._onEmailLinkReceived);
+      this.listenTo(this.props.store, "error:emailLink",
+                    this._onEmailLinkError);
     },
 
     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();
     },
 
+    _onEmailLinkError: function() {
+      this.setState({
+        emailLinkError: true,
+        emailLinkButtonDisabled: false
+      });
+    },
+
+    _renderError: function() {
+      if (!this.state.emailLinkError) {
+        return;
+      }
+      return React.DOM.p({className: "error"}, mozL10n.get("unable_retrieve_url"));
+    },
+
     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.setState({
+        emailLinkError: false,
+        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_with_reason2")), 
 
+          this._renderError(), 
+
           React.DOM.div({className: "btn-group call-action-group"}, 
             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")
--- a/browser/components/loop/content/js/conversationViews.jsx
+++ b/browser/components/loop/content/js/conversationViews.jsx
@@ -166,22 +166,20 @@ loop.conversationViews = (function(mozL1
       });
 
       return (
         <ConversationDetailView contact={this.props.contact}>
 
           <p className="btn-label">{pendingStateString}</p>
 
           <div className="btn-group call-action-group">
-            <div className="fx-embedded-call-button-spacer"></div>
-              <button className={btnCancelStyles}
-                      onClick={this.cancelCall}>
-                {mozL10n.get("initiate_call_cancel_button")}
-              </button>
-            <div className="fx-embedded-call-button-spacer"></div>
+            <button className={btnCancelStyles}
+                    onClick={this.cancelCall}>
+              {mozL10n.get("initiate_call_cancel_button")}
+            </button>
           </div>
 
         </ConversationDetailView>
       );
     }
   });
 
   /**
@@ -189,60 +187,86 @@ loop.conversationViews = (function(mozL1
    */
   var CallFailedView = React.createClass({
     mixins: [Backbone.Events],
 
     propTypes: {
       dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
       store: React.PropTypes.instanceOf(
         loop.store.ConversationStore).isRequired,
-      contact: React.PropTypes.object.isRequired
+      contact: React.PropTypes.object.isRequired,
+      // This is used by the UI showcase.
+      emailLinkError: React.PropTypes.bool,
     },
 
     getInitialState: function() {
-      return {emailLinkButtonDisabled: false};
+      return {
+        emailLinkError: this.props.emailLinkError,
+        emailLinkButtonDisabled: false
+      };
     },
 
     componentDidMount: function() {
       this.listenTo(this.props.store, "change:emailLink",
                     this._onEmailLinkReceived);
+      this.listenTo(this.props.store, "error:emailLink",
+                    this._onEmailLinkError);
     },
 
     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();
     },
 
+    _onEmailLinkError: function() {
+      this.setState({
+        emailLinkError: true,
+        emailLinkButtonDisabled: false
+      });
+    },
+
+    _renderError: function() {
+      if (!this.state.emailLinkError) {
+        return;
+      }
+      return <p className="error">{mozL10n.get("unable_retrieve_url")}</p>;
+    },
+
     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.setState({
+        emailLinkError: false,
+        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_with_reason2")}</p>
 
+          {this._renderError()}
+
           <div className="btn-group call-action-group">
             <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")}
--- a/browser/components/loop/content/shared/css/conversation.css
+++ b/browser/components/loop/content/shared/css/conversation.css
@@ -239,16 +239,22 @@
   justify-content: space-between;
   min-height: 230px;
 }
 
 .call-window > .btn-label {
   text-align: center;
 }
 
+.call-window > .error {
+  text-align: center;
+  color: #f00;
+  font-size: 90%;
+}
+
 .call-action-group {
   display: flex;
   padding: 2.5em 4px 0 4px;
   width: 100%;
 }
 
 .call-action-group > .btn {
   height: 26px;
--- a/browser/components/loop/content/shared/js/conversationStore.js
+++ b/browser/components/loop/content/shared/js/conversationStore.js
@@ -308,18 +308,17 @@ loop.store = (function() {
      * 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);
+          this.trigger("error:emailLink");
           return;
         }
         this.set("emailLink", callUrlData.callUrl);
       }.bind(this));
     },
 
     /**
      * Obtains the outgoing call data from the server and handles the
--- a/browser/components/loop/test/desktop-local/conversationViews_test.js
+++ b/browser/components/loop/test/desktop-local/conversationViews_test.js
@@ -281,16 +281,34 @@ describe("loop.conversationViews", funct
       function() {
         sandbox.stub(window, "close");
         view = mountTestComponent();
 
         store.set("emailLink", "http://fake.invalid/");
 
         sinon.assert.calledOnce(window.close);
       });
+
+    it("should display an error message in case email link retrieval failed",
+      function() {
+        view = mountTestComponent();
+
+        store.trigger("error:emailLink");
+
+        expect(view.getDOMNode().querySelector(".error")).not.eql(null);
+      });
+
+    it("should allow retrying to get a call url if it failed previously",
+      function() {
+        view = mountTestComponent();
+
+        store.trigger("error:emailLink");
+
+        expect(view.getDOMNode().querySelector(".btn-email").disabled).eql(false);
+      });
   });
 
   describe("OngoingConversationView", function() {
     function mountTestComponent(props) {
       return TestUtils.renderIntoDocument(
         loop.conversationViews.OngoingConversationView(props));
     }
 
--- a/browser/components/loop/test/shared/conversationStore_test.js
+++ b/browser/components/loop/test/shared/conversationStore_test.js
@@ -653,18 +653,27 @@ describe("loop.ConversationStore", funct
         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");
+    it("should trigger an error:emailLink event in case of failure",
+      function() {
+        var trigger = sandbox.stub(store, "trigger");
+        client.requestCallUrl = function(callId, cb) {
+          cb("error");
+        };
+        dispatcher.dispatch(new sharedActions.FetchEmailLink());
+
+        sinon.assert.calledOnce(trigger);
+        sinon.assert.calledWithExactly(trigger, "error:emailLink");
+      });
   });
 
   describe("Events", function() {
     describe("Websocket progress", function() {
       beforeEach(function() {
         dispatcher.dispatch(
           new sharedActions.ConnectCall({sessionData: fakeSessionData}));
 
--- a/browser/components/loop/ui/ui-showcase.js
+++ b/browser/components/loop/ui/ui-showcase.js
@@ -271,16 +271,22 @@
           ), 
 
           Section({name: "CallFailedView"}, 
             Example({summary: "Call Failed", dashed: "true", 
                      style: {width: "260px", height: "265px"}}, 
               React.DOM.div({className: "fx-embedded"}, 
                 CallFailedView(null)
               )
+            ), 
+            Example({summary: "Call Failed — with call URL error", dashed: "true", 
+                     style: {width: "260px", height: "265px"}}, 
+              React.DOM.div({className: "fx-embedded"}, 
+                CallFailedView({emailLinkError: true})
+              )
             )
           ), 
 
           Section({name: "StartConversationView"}, 
             Example({summary: "Start conversation view", dashed: "true"}, 
               React.DOM.div({className: "standalone"}, 
                 StartConversationView({conversation: mockConversationModel, 
                                        client: mockClient, 
--- a/browser/components/loop/ui/ui-showcase.jsx
+++ b/browser/components/loop/ui/ui-showcase.jsx
@@ -272,16 +272,22 @@
 
           <Section name="CallFailedView">
             <Example summary="Call Failed" dashed="true"
                      style={{width: "260px", height: "265px"}}>
               <div className="fx-embedded">
                 <CallFailedView />
               </div>
             </Example>
+            <Example summary="Call Failed — with call URL error" dashed="true"
+                     style={{width: "260px", height: "265px"}}>
+              <div className="fx-embedded">
+                <CallFailedView emailLinkError={true} />
+              </div>
+            </Example>
           </Section>
 
           <Section name="StartConversationView">
             <Example summary="Start conversation view" dashed="true">
               <div className="standalone">
                 <StartConversationView conversation={mockConversationModel}
                                        client={mockClient}
                                        notifications={notifications} />