Bug 1145541. r=mikedeboer, a=sledru
authorMark Banner <standard8@mozilla.com>
Tue, 21 Apr 2015 21:53:15 +0100
changeset 260243 db41e8e267ed
parent 260242 645fc5aa6a49
child 260244 df5d106c2607
push id724
push userryanvm@gmail.com
push date2015-04-23 01:08 +0000
treeherdermozilla-release@db41e8e267ed [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmikedeboer, sledru
bugs1145541
milestone38.0
Bug 1145541. r=mikedeboer, a=sledru
browser/components/loop/MozLoopAPI.jsm
browser/components/loop/MozLoopService.jsm
browser/components/loop/content/js/contacts.js
browser/components/loop/content/js/contacts.jsx
browser/components/loop/content/js/panel.js
browser/components/loop/content/js/panel.jsx
browser/components/loop/test/desktop-local/panel_test.js
--- a/browser/components/loop/MozLoopAPI.jsm
+++ b/browser/components/loop/MozLoopAPI.jsm
@@ -683,16 +683,29 @@ function injectLoopAPI(targetWindow) {
       enumerable: true,
       writable: true,
       value: function(aSrc) {
         return MozLoopService.openGettingStartedTour(aSrc);
       },
     },
 
     /**
+     * Opens a URL in a new tab in the browser.
+     *
+     * @param {String} url The new url to open
+     */
+    openURL: {
+      enumerable: true,
+      writable: true,
+      value: function(url) {
+        return MozLoopService.openURL(url);
+      }
+    },
+
+    /**
      * Copies passed string onto the system clipboard.
      *
      * @param {String} str The string to copy
      */
     copyString: {
       enumerable: true,
       writable: true,
       value: function(str) {
--- a/browser/components/loop/MozLoopService.jsm
+++ b/browser/components/loop/MozLoopService.jsm
@@ -1627,16 +1627,26 @@ this.MozLoopService = {
         replaceQueryString: true,
       });
     } catch (ex) {
       log.error("Error opening Getting Started tour", ex);
     }
   }),
 
   /**
+   * Opens a URL in a new tab in the browser.
+   *
+   * @param {String} url The new url to open
+   */
+  openURL: function(url) {
+    let win = Services.wm.getMostRecentWindow("navigator:browser");
+    win.openUILinkIn(url, "tab");
+  },
+
+  /**
    * Performs a hawk based request to the loop server.
    *
    * @param {LOOP_SESSION_TYPE} sessionType The type of session to use for the request.
    *                                        One of the LOOP_SESSION_TYPE members.
    * @param {String} path The path to make the request to.
    * @param {String} method The request method, e.g. 'POST', 'GET'.
    * @param {Object} payloadObj An object which is converted to JSON and
    *                            transmitted with the request.
--- a/browser/components/loop/content/js/contacts.js
+++ b/browser/components/loop/content/js/contacts.js
@@ -6,16 +6,18 @@
 
 /*jshint newcap:false*/
 /*global loop:true, React */
 
 var loop = loop || {};
 loop.contacts = (function(_, mozL10n) {
   "use strict";
 
+  var sharedMixins = loop.shared.mixins;
+
   const Button = loop.shared.views.Button;
   const ButtonGroup = loop.shared.views.ButtonGroup;
   const CALL_TYPES = loop.shared.utils.CALL_TYPES;
 
   // Number of contacts to add to the list at the same time.
   const CONTACTS_CHUNK_SIZE = 100;
 
   // At least this number of contacts should be present for the filter to appear.
@@ -77,32 +79,44 @@ loop.contacts = (function(_, mozL10n) {
         contact[field][i].value = value;
         return;
       }
     }
     contact[field][0].value = value;
   };
 
   const GravatarPromo = React.createClass({displayName: "GravatarPromo",
+    mixins: [sharedMixins.WindowCloseMixin],
+
     propTypes: {
       handleUse: React.PropTypes.func.isRequired
     },
 
     getInitialState: function() {
       return {
         showMe: navigator.mozLoop.getLoopPref("contacts.gravatars.promo") &&
           !navigator.mozLoop.getLoopPref("contacts.gravatars.show")
       };
     },
 
     handleCloseButtonClick: function() {
       navigator.mozLoop.setLoopPref("contacts.gravatars.promo", false);
       this.setState({ showMe: false });
     },
 
+    handleLinkClick: function(event) {
+      if (!event.target || !event.target.href) {
+        return;
+      }
+
+      event.preventDefault();
+      navigator.mozLoop.openURL(event.target.href);
+      this.closeWindow();
+    },
+
     handleUseButtonClick: function() {
       navigator.mozLoop.setLoopPref("contacts.gravatars.promo", false);
       navigator.mozLoop.setLoopPref("contacts.gravatars.show", true);
       this.setState({ showMe: false });
       this.props.handleUse();
     },
 
     render: function() {
@@ -116,17 +130,18 @@ loop.contacts = (function(_, mozL10n) {
           React.createElement("a", {href: privacyUrl, target: "_blank"}, 
             mozL10n.get("gravatars_promo_message_learnmore")
           )
         )
       });
       return (
         React.createElement("div", {className: "contacts-gravatar-promo"}, 
           React.createElement(Button, {additionalClass: "button-close", onClick: this.handleCloseButtonClick}), 
-          React.createElement("p", {dangerouslySetInnerHTML: {__html: message}}), 
+          React.createElement("p", {dangerouslySetInnerHTML: {__html: message}, 
+             onClick: this.handleLinkClick}), 
           React.createElement(ButtonGroup, null, 
             React.createElement(Button, {caption: mozL10n.get("gravatars_promo_button_nothanks"), 
                     onClick: this.handleCloseButtonClick}), 
             React.createElement(Button, {caption: mozL10n.get("gravatars_promo_button_use"), 
                     additionalClass: "button-accept", 
                     onClick: this.handleUseButtonClick})
           )
         )
--- a/browser/components/loop/content/js/contacts.jsx
+++ b/browser/components/loop/content/js/contacts.jsx
@@ -6,16 +6,18 @@
 
 /*jshint newcap:false*/
 /*global loop:true, React */
 
 var loop = loop || {};
 loop.contacts = (function(_, mozL10n) {
   "use strict";
 
+  var sharedMixins = loop.shared.mixins;
+
   const Button = loop.shared.views.Button;
   const ButtonGroup = loop.shared.views.ButtonGroup;
   const CALL_TYPES = loop.shared.utils.CALL_TYPES;
 
   // Number of contacts to add to the list at the same time.
   const CONTACTS_CHUNK_SIZE = 100;
 
   // At least this number of contacts should be present for the filter to appear.
@@ -77,32 +79,44 @@ loop.contacts = (function(_, mozL10n) {
         contact[field][i].value = value;
         return;
       }
     }
     contact[field][0].value = value;
   };
 
   const GravatarPromo = React.createClass({
+    mixins: [sharedMixins.WindowCloseMixin],
+
     propTypes: {
       handleUse: React.PropTypes.func.isRequired
     },
 
     getInitialState: function() {
       return {
         showMe: navigator.mozLoop.getLoopPref("contacts.gravatars.promo") &&
           !navigator.mozLoop.getLoopPref("contacts.gravatars.show")
       };
     },
 
     handleCloseButtonClick: function() {
       navigator.mozLoop.setLoopPref("contacts.gravatars.promo", false);
       this.setState({ showMe: false });
     },
 
+    handleLinkClick: function(event) {
+      if (!event.target || !event.target.href) {
+        return;
+      }
+
+      event.preventDefault();
+      navigator.mozLoop.openURL(event.target.href);
+      this.closeWindow();
+    },
+
     handleUseButtonClick: function() {
       navigator.mozLoop.setLoopPref("contacts.gravatars.promo", false);
       navigator.mozLoop.setLoopPref("contacts.gravatars.show", true);
       this.setState({ showMe: false });
       this.props.handleUse();
     },
 
     render: function() {
@@ -116,17 +130,18 @@ loop.contacts = (function(_, mozL10n) {
           <a href={privacyUrl} target="_blank">
             {mozL10n.get("gravatars_promo_message_learnmore")}
           </a>
         )
       });
       return (
         <div className="contacts-gravatar-promo">
           <Button additionalClass="button-close" onClick={this.handleCloseButtonClick}/>
-          <p dangerouslySetInnerHTML={{__html: message}}></p>
+          <p dangerouslySetInnerHTML={{__html: message}}
+             onClick={this.handleLinkClick}></p>
           <ButtonGroup>
             <Button caption={mozL10n.get("gravatars_promo_button_nothanks")}
                     onClick={this.handleCloseButtonClick}/>
             <Button caption={mozL10n.get("gravatars_promo_button_use")}
                     additionalClass="button-accept"
                     onClick={this.handleUseButtonClick}/>
           </ButtonGroup>
         </div>
--- a/browser/components/loop/content/js/panel.js
+++ b/browser/components/loop/content/js/panel.js
@@ -211,26 +211,38 @@ loop.panel = (function(_, mozL10n) {
                   onClick: this.handleButtonClick, 
                   caption: mozL10n.get("first_time_experience_button_label")})
         )
       );
     }
   });
 
   var ToSView = React.createClass({displayName: "ToSView",
+    mixins: [sharedMixins.WindowCloseMixin],
+
     getInitialState: function() {
       var getPref = navigator.mozLoop.getLoopPref.bind(navigator.mozLoop);
 
       return {
         seenToS: getPref("seenToS"),
         gettingStartedSeen: getPref("gettingStarted.seen"),
         showPartnerLogo: getPref("showPartnerLogo")
       };
     },
 
+    handleLinkClick: function(event) {
+      if (!event.target || !event.target.href) {
+        return;
+      }
+
+      event.preventDefault();
+      navigator.mozLoop.openURL(event.target.href);
+      this.closeWindow();
+    },
+
     renderPartnerLogo: function() {
       if (!this.state.showPartnerLogo) {
         return null;
       }
 
       var locale = mozL10n.getLanguage();
       navigator.mozLoop.setLoopPref('showPartnerLogo', false);
       return (
@@ -257,17 +269,18 @@ loop.panel = (function(_, mozL10n) {
             React.createElement("a", {href: privacy_notice_url, target: "_blank"}, 
               mozL10n.get("legal_text_privacy")
             )
           ),
         });
         return React.createElement("div", {id: "powered-by-wrapper"}, 
           this.renderPartnerLogo(), 
           React.createElement("p", {className: "terms-service", 
-             dangerouslySetInnerHTML: {__html: tosHTML}})
+             dangerouslySetInnerHTML: {__html: tosHTML}, 
+             onClick: this.handleLinkClick})
          );
       } else {
         return React.createElement("div", null);
       }
     }
   });
 
   /**
@@ -299,47 +312,52 @@ loop.panel = (function(_, mozL10n) {
       );
     }
   });
 
   /**
    * Panel settings (gear) menu.
    */
   var SettingsDropdown = React.createClass({displayName: "SettingsDropdown",
+    propTypes: {
+      mozLoop: React.PropTypes.object.isRequired
+    },
+
     mixins: [sharedMixins.DropdownMenuMixin, sharedMixins.WindowCloseMixin],
 
     handleClickSettingsEntry: function() {
       // XXX to be implemented at the same time as unhiding the entry
     },
 
     handleClickAccountEntry: function() {
-      navigator.mozLoop.openFxASettings();
+      this.props.mozLoop.openFxASettings();
+      this.closeWindow();
     },
 
     handleClickAuthEntry: function() {
       if (this._isSignedIn()) {
-        navigator.mozLoop.logOutFromFxA();
+        this.props.mozLoop.logOutFromFxA();
       } else {
-        navigator.mozLoop.logInToFxA();
+        this.props.mozLoop.logInToFxA();
       }
     },
 
     handleHelpEntry: function(event) {
       event.preventDefault();
-      var helloSupportUrl = navigator.mozLoop.getLoopPref('support_url');
-      window.open(helloSupportUrl);
-      window.close();
+      var helloSupportUrl = this.props.mozLoop.getLoopPref("support_url");
+      this.props.mozLoop.openURL(helloSupportUrl);
+      this.closeWindow();
     },
 
     _isSignedIn: function() {
-      return !!navigator.mozLoop.userProfile;
+      return !!this.props.mozLoop.userProfile;
     },
 
     openGettingStartedTour: function() {
-      navigator.mozLoop.openGettingStartedTour("settings-menu");
+      this.props.mozLoop.openGettingStartedTour("settings-menu");
       this.closeWindow();
     },
 
     render: function() {
       var cx = React.addons.classSet;
 
       return (
         React.createElement("div", {className: "settings-menu dropdown"}, 
@@ -349,25 +367,25 @@ loop.panel = (function(_, mozL10n) {
               onMouseLeave: this.hideDropdownMenu}, 
             React.createElement(SettingsDropdownEntry, {label: mozL10n.get("settings_menu_item_settings"), 
                                    onClick: this.handleClickSettingsEntry, 
                                    displayed: false, 
                                    icon: "settings"}), 
             React.createElement(SettingsDropdownEntry, {label: mozL10n.get("settings_menu_item_account"), 
                                    onClick: this.handleClickAccountEntry, 
                                    icon: "account", 
-                                   displayed: this._isSignedIn() && navigator.mozLoop.fxAEnabled}), 
+                                   displayed: this._isSignedIn() && this.props.mozLoop.fxAEnabled}), 
             React.createElement(SettingsDropdownEntry, {icon: "tour", 
                                    label: mozL10n.get("tour_label"), 
                                    onClick: this.openGettingStartedTour}), 
             React.createElement(SettingsDropdownEntry, {label: this._isSignedIn() ?
                                           mozL10n.get("settings_menu_item_signout") :
                                           mozL10n.get("settings_menu_item_signin"), 
                                    onClick: this.handleClickAuthEntry, 
-                                   displayed: navigator.mozLoop.fxAEnabled, 
+                                   displayed: this.props.mozLoop.fxAEnabled, 
                                    icon: this._isSignedIn() ? "signout" : "signin"}), 
             React.createElement(SettingsDropdownEntry, {label: mozL10n.get("help_label"), 
                                    onClick: this.handleHelpEntry, 
                                    icon: "help"})
           )
         )
       );
     }
@@ -685,17 +703,17 @@ loop.panel = (function(_, mozL10n) {
     propTypes: {
       notifications: React.PropTypes.object.isRequired,
       // Mostly used for UI components showcase and unit tests
       userProfile: React.PropTypes.object,
       // Used only for unit tests.
       showTabButtons: React.PropTypes.bool,
       selectedTab: React.PropTypes.string,
       dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
-      mozLoop: React.PropTypes.object,
+      mozLoop: React.PropTypes.object.isRequired,
       roomStore:
         React.PropTypes.instanceOf(loop.store.RoomStore).isRequired
     },
 
     getInitialState: function() {
       return {
         userProfile: this.props.userProfile || this.props.mozLoop.userProfile,
         gettingStartedSeen: this.props.mozLoop.getLoopPref("gettingStarted.seen"),
@@ -843,17 +861,17 @@ loop.panel = (function(_, mozL10n) {
           React.createElement("div", {className: "footer"}, 
             React.createElement("div", {className: "user-details"}, 
               React.createElement(UserIdentity, {displayName: this._getUserDisplayName()}), 
               React.createElement(AvailabilityDropdown, null)
             ), 
             React.createElement("div", {className: "signin-details"}, 
               React.createElement(AuthLink, null), 
               React.createElement("div", {className: "footer-signin-separator"}), 
-              React.createElement(SettingsDropdown, null)
+              React.createElement(SettingsDropdown, {mozLoop: this.props.mozLoop})
             )
           )
         )
       );
     }
   });
 
   /**
--- a/browser/components/loop/content/js/panel.jsx
+++ b/browser/components/loop/content/js/panel.jsx
@@ -211,26 +211,38 @@ loop.panel = (function(_, mozL10n) {
                   onClick={this.handleButtonClick}
                   caption={mozL10n.get("first_time_experience_button_label")} />
         </div>
       );
     }
   });
 
   var ToSView = React.createClass({
+    mixins: [sharedMixins.WindowCloseMixin],
+
     getInitialState: function() {
       var getPref = navigator.mozLoop.getLoopPref.bind(navigator.mozLoop);
 
       return {
         seenToS: getPref("seenToS"),
         gettingStartedSeen: getPref("gettingStarted.seen"),
         showPartnerLogo: getPref("showPartnerLogo")
       };
     },
 
+    handleLinkClick: function(event) {
+      if (!event.target || !event.target.href) {
+        return;
+      }
+
+      event.preventDefault();
+      navigator.mozLoop.openURL(event.target.href);
+      this.closeWindow();
+    },
+
     renderPartnerLogo: function() {
       if (!this.state.showPartnerLogo) {
         return null;
       }
 
       var locale = mozL10n.getLanguage();
       navigator.mozLoop.setLoopPref('showPartnerLogo', false);
       return (
@@ -257,17 +269,18 @@ loop.panel = (function(_, mozL10n) {
             <a href={privacy_notice_url} target="_blank">
               {mozL10n.get("legal_text_privacy")}
             </a>
           ),
         });
         return <div id="powered-by-wrapper">
           {this.renderPartnerLogo()}
           <p className="terms-service"
-             dangerouslySetInnerHTML={{__html: tosHTML}}></p>
+             dangerouslySetInnerHTML={{__html: tosHTML}}
+             onClick={this.handleLinkClick}></p>
          </div>;
       } else {
         return <div />;
       }
     }
   });
 
   /**
@@ -299,47 +312,52 @@ loop.panel = (function(_, mozL10n) {
       );
     }
   });
 
   /**
    * Panel settings (gear) menu.
    */
   var SettingsDropdown = React.createClass({
+    propTypes: {
+      mozLoop: React.PropTypes.object.isRequired
+    },
+
     mixins: [sharedMixins.DropdownMenuMixin, sharedMixins.WindowCloseMixin],
 
     handleClickSettingsEntry: function() {
       // XXX to be implemented at the same time as unhiding the entry
     },
 
     handleClickAccountEntry: function() {
-      navigator.mozLoop.openFxASettings();
+      this.props.mozLoop.openFxASettings();
+      this.closeWindow();
     },
 
     handleClickAuthEntry: function() {
       if (this._isSignedIn()) {
-        navigator.mozLoop.logOutFromFxA();
+        this.props.mozLoop.logOutFromFxA();
       } else {
-        navigator.mozLoop.logInToFxA();
+        this.props.mozLoop.logInToFxA();
       }
     },
 
     handleHelpEntry: function(event) {
       event.preventDefault();
-      var helloSupportUrl = navigator.mozLoop.getLoopPref('support_url');
-      window.open(helloSupportUrl);
-      window.close();
+      var helloSupportUrl = this.props.mozLoop.getLoopPref("support_url");
+      this.props.mozLoop.openURL(helloSupportUrl);
+      this.closeWindow();
     },
 
     _isSignedIn: function() {
-      return !!navigator.mozLoop.userProfile;
+      return !!this.props.mozLoop.userProfile;
     },
 
     openGettingStartedTour: function() {
-      navigator.mozLoop.openGettingStartedTour("settings-menu");
+      this.props.mozLoop.openGettingStartedTour("settings-menu");
       this.closeWindow();
     },
 
     render: function() {
       var cx = React.addons.classSet;
 
       return (
         <div className="settings-menu dropdown">
@@ -349,25 +367,25 @@ loop.panel = (function(_, mozL10n) {
               onMouseLeave={this.hideDropdownMenu}>
             <SettingsDropdownEntry label={mozL10n.get("settings_menu_item_settings")}
                                    onClick={this.handleClickSettingsEntry}
                                    displayed={false}
                                    icon="settings" />
             <SettingsDropdownEntry label={mozL10n.get("settings_menu_item_account")}
                                    onClick={this.handleClickAccountEntry}
                                    icon="account"
-                                   displayed={this._isSignedIn() && navigator.mozLoop.fxAEnabled} />
+                                   displayed={this._isSignedIn() && this.props.mozLoop.fxAEnabled} />
             <SettingsDropdownEntry icon="tour"
                                    label={mozL10n.get("tour_label")}
                                    onClick={this.openGettingStartedTour} />
             <SettingsDropdownEntry label={this._isSignedIn() ?
                                           mozL10n.get("settings_menu_item_signout") :
                                           mozL10n.get("settings_menu_item_signin")}
                                    onClick={this.handleClickAuthEntry}
-                                   displayed={navigator.mozLoop.fxAEnabled}
+                                   displayed={this.props.mozLoop.fxAEnabled}
                                    icon={this._isSignedIn() ? "signout" : "signin"} />
             <SettingsDropdownEntry label={mozL10n.get("help_label")}
                                    onClick={this.handleHelpEntry}
                                    icon="help" />
           </ul>
         </div>
       );
     }
@@ -685,17 +703,17 @@ loop.panel = (function(_, mozL10n) {
     propTypes: {
       notifications: React.PropTypes.object.isRequired,
       // Mostly used for UI components showcase and unit tests
       userProfile: React.PropTypes.object,
       // Used only for unit tests.
       showTabButtons: React.PropTypes.bool,
       selectedTab: React.PropTypes.string,
       dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
-      mozLoop: React.PropTypes.object,
+      mozLoop: React.PropTypes.object.isRequired,
       roomStore:
         React.PropTypes.instanceOf(loop.store.RoomStore).isRequired
     },
 
     getInitialState: function() {
       return {
         userProfile: this.props.userProfile || this.props.mozLoop.userProfile,
         gettingStartedSeen: this.props.mozLoop.getLoopPref("gettingStarted.seen"),
@@ -843,17 +861,17 @@ loop.panel = (function(_, mozL10n) {
           <div className="footer">
             <div className="user-details">
               <UserIdentity displayName={this._getUserDisplayName()} />
               <AvailabilityDropdown />
             </div>
             <div className="signin-details">
               <AuthLink />
               <div className="footer-signin-separator" />
-              <SettingsDropdown />
+              <SettingsDropdown mozLoop={this.props.mozLoop}/>
             </div>
           </div>
         </div>
       );
     }
   });
 
   /**
--- a/browser/components/loop/test/desktop-local/panel_test.js
+++ b/browser/components/loop/test/desktop-local/panel_test.js
@@ -56,17 +56,18 @@ describe("loop.panel", function() {
       },
       rooms: {
         getAll: function(version, callback) {
           callback(null, []);
         },
         on: sandbox.stub()
       },
       confirm: sandbox.stub(),
-      notifyUITour: sandbox.stub()
+      notifyUITour: sandbox.stub(),
+      openURL: sandbox.stub()
     };
 
     document.mozL10n.initialize(navigator.mozLoop);
   });
 
   afterEach(function() {
     delete navigator.mozLoop;
     loop.shared.mixins.setRootObject(window);
@@ -173,16 +174,29 @@ describe("loop.panel", function() {
           client: fakeClient,
           showTabButtons: true,
           mozLoop: fakeMozLoop,
           dispatcher: dispatcher,
           roomStore: roomStore
         }));
     }
 
+    it("should hide the account entry when FxA is not enabled", function() {
+      navigator.mozLoop.userProfile = {email: "test@example.com"};
+      navigator.mozLoop.fxAEnabled = false;
+
+      var view = TestUtils.renderIntoDocument(
+        React.createElement(loop.panel.SettingsDropdown, {
+          mozLoop: fakeMozLoop
+        }));
+
+      expect(view.getDOMNode().querySelectorAll(".icon-account"))
+        .to.have.length.of(0);
+    });
+
     describe('TabView', function() {
       var view, callTab, roomsTab, contactsTab;
 
       beforeEach(function() {
         navigator.mozLoop.getLoopPref = function(pref) {
           if (pref === "gettingStarted.seen") {
             return true;
           }
@@ -253,142 +267,144 @@ describe("loop.panel", function() {
         function() {
           navigator.mozLoop.fxAEnabled = false;
           var view = TestUtils.renderIntoDocument(
             React.createElement(loop.panel.AuthLink));
           expect(view.getDOMNode()).to.be.null;
       });
     });
 
-    it("should hide the account entry when FxA is not enabled", function() {
-        navigator.mozLoop.userProfile = {email: "test@example.com"};
-        navigator.mozLoop.fxAEnabled = false;
+    describe("SettingsDropdown", function() {
+      function mountTestComponent() {
+        return TestUtils.renderIntoDocument(
+          React.createElement(loop.panel.SettingsDropdown, {
+            mozLoop: fakeMozLoop
+          }));
+      }
 
-        var view = TestUtils.renderIntoDocument(
-          React.createElement(loop.panel.SettingsDropdown));
-
-        expect(view.getDOMNode().querySelectorAll(".icon-account"))
-          .to.have.length.of(0);
-      });
-
-    describe("SettingsDropdown", function() {
       beforeEach(function() {
         navigator.mozLoop.logInToFxA = sandbox.stub();
         navigator.mozLoop.logOutFromFxA = sandbox.stub();
         navigator.mozLoop.openFxASettings = sandbox.stub();
       });
 
       afterEach(function() {
         navigator.mozLoop.fxAEnabled = true;
       });
 
       it("should show a signin entry when user is not authenticated",
         function() {
           navigator.mozLoop.loggedInToFxA = false;
 
-          var view = TestUtils.renderIntoDocument(
-            React.createElement(loop.panel.SettingsDropdown));
+          var view = mountTestComponent();
 
           expect(view.getDOMNode().querySelectorAll(".icon-signout"))
             .to.have.length.of(0);
           expect(view.getDOMNode().querySelectorAll(".icon-signin"))
             .to.have.length.of(1);
         });
 
       it("should show a signout entry when user is authenticated", function() {
         navigator.mozLoop.userProfile = {email: "test@example.com"};
 
-        var view = TestUtils.renderIntoDocument(
-          React.createElement(loop.panel.SettingsDropdown));
+        var view = mountTestComponent();
 
         expect(view.getDOMNode().querySelectorAll(".icon-signout"))
           .to.have.length.of(1);
         expect(view.getDOMNode().querySelectorAll(".icon-signin"))
           .to.have.length.of(0);
       });
 
       it("should show an account entry when user is authenticated", function() {
         navigator.mozLoop.userProfile = {email: "test@example.com"};
 
-        var view = TestUtils.renderIntoDocument(
-          React.createElement(loop.panel.SettingsDropdown));
+        var view = mountTestComponent();
 
         expect(view.getDOMNode().querySelectorAll(".icon-account"))
           .to.have.length.of(1);
       });
 
       it("should open the FxA settings when the account entry is clicked", function() {
         navigator.mozLoop.userProfile = {email: "test@example.com"};
 
-        var view = TestUtils.renderIntoDocument(
-          React.createElement(loop.panel.SettingsDropdown));
+        var view = mountTestComponent();
 
         TestUtils.Simulate.click(
           view.getDOMNode().querySelector(".icon-account"));
 
         sinon.assert.calledOnce(navigator.mozLoop.openFxASettings);
       });
 
       it("should hide any account entry when user is not authenticated",
         function() {
           navigator.mozLoop.loggedInToFxA = false;
 
-          var view = TestUtils.renderIntoDocument(
-            React.createElement(loop.panel.SettingsDropdown));
+          var view = mountTestComponent();
 
           expect(view.getDOMNode().querySelectorAll(".icon-account"))
             .to.have.length.of(0);
         });
 
       it("should sign in the user on click when unauthenticated", function() {
         navigator.mozLoop.loggedInToFxA = false;
-        var view = TestUtils.renderIntoDocument(
-          React.createElement(loop.panel.SettingsDropdown));
+        var view = mountTestComponent();
 
         TestUtils.Simulate.click(
           view.getDOMNode().querySelector(".icon-signin"));
 
         sinon.assert.calledOnce(navigator.mozLoop.logInToFxA);
       });
 
       it("should sign out the user on click when authenticated", function() {
         navigator.mozLoop.userProfile = {email: "test@example.com"};
-        var view = TestUtils.renderIntoDocument(
-          React.createElement(loop.panel.SettingsDropdown));
+        var view = mountTestComponent();
 
         TestUtils.Simulate.click(
           view.getDOMNode().querySelector(".icon-signout"));
 
         sinon.assert.calledOnce(navigator.mozLoop.logOutFromFxA);
       });
     });
 
     describe("Help", function() {
-      var supportUrl = "https://example.com";
+      var view, supportUrl;
+
+      function mountTestComponent() {
+        return TestUtils.renderIntoDocument(
+          React.createElement(loop.panel.SettingsDropdown, {
+            mozLoop: fakeMozLoop
+          }));
+      }
 
       beforeEach(function() {
+        supportUrl = "https://example.com";
         navigator.mozLoop.getLoopPref = function(pref) {
           if (pref === "support_url")
             return supportUrl;
           return "unseen";
         };
-
-        sandbox.stub(window, "open");
-        sandbox.stub(window, "close");
       });
 
       it("should open a tab to the support page", function() {
-        var view = TestUtils.renderIntoDocument(
-          React.createElement(loop.panel.SettingsDropdown));
+        view = mountTestComponent();
 
         TestUtils.Simulate
           .click(view.getDOMNode().querySelector(".icon-help"));
 
-        sinon.assert.calledOnce(window.open);
-        sinon.assert.calledWithExactly(window.open, supportUrl);
+        sinon.assert.calledOnce(fakeMozLoop.openURL);
+        sinon.assert.calledWithExactly(fakeMozLoop.openURL, supportUrl);
+      });
+
+      it("should close the panel", function() {
+        view = mountTestComponent();
+
+        TestUtils.Simulate
+          .click(view.getDOMNode().querySelector(".icon-help"));
+
+        sinon.assert.calledOnce(fakeWindow.close);
       });
     });
 
     describe("#render", function() {
       it("should render a ToSView", function() {
         var view = createTestPanelView();
 
         TestUtils.findRenderedComponentWithType(view, loop.panel.ToSView);