Bug 1047181 - Change the Loop toolbar button when FxA sign in completes. r=MattN
☠☠ backed out by 10a23714dd6a ☠ ☠
authorJared Wein <jwein@mozilla.com>
Thu, 18 Sep 2014 16:14:44 -0400
changeset 206141 5710731f09e99074e79ed0c4420b2598dd42f535
parent 206140 b541be2a5459b70bae82bb8ec95b0fd687e12cf3
child 206142 43b24197d25adaf2030e7b5ce1ea5fa0bdfbc820
push id27514
push usercbook@mozilla.com
push dateFri, 19 Sep 2014 12:24:09 +0000
treeherdermozilla-central@3475e6a1665a [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersMattN
bugs1047181
milestone35.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1047181 - Change the Loop toolbar button when FxA sign in completes. r=MattN
browser/base/content/browser-loop.js
browser/components/loop/MozLoopService.jsm
browser/components/loop/test/mochitest/browser_fxa_login.js
browser/components/loop/test/mochitest/browser_toolbarbutton.js
browser/components/loop/test/mochitest/head.js
--- a/browser/base/content/browser-loop.js
+++ b/browser/base/content/browser-loop.js
@@ -27,16 +27,19 @@ XPCOMUtils.defineLazyModuleGetter(this, 
     openCallPanel: function(event) {
       let callback = iframe => {
         iframe.addEventListener("DOMContentLoaded", function documentDOMLoaded() {
           iframe.removeEventListener("DOMContentLoaded", documentDOMLoaded, true);
           injectLoopAPI(iframe.contentWindow);
         }, true);
       };
 
+      // Used to clear the temporary "login" state from the button.
+      Services.obs.notifyObservers(null, "loop-status-changed", null);
+
       PanelFrame.showPopup(window, event.target, "loop", null,
                            "about:looppanel", null, callback);
     },
 
     /**
      * Triggers the initialization of the loop service.  Called by
      * delayedStartup.
      */
@@ -64,22 +67,34 @@ XPCOMUtils.defineLazyModuleGetter(this, 
       Services.obs.removeObserver(this, "loop-status-changed");
     },
 
     // Implements nsIObserver
     observe: function(subject, topic, data) {
       if (topic != "loop-status-changed") {
         return;
       }
-      this.updateToolbarState();
+      this.updateToolbarState(data);
     },
 
-    updateToolbarState: function() {
+    /**
+     * Updates the toolbar/menu-button state to reflect Loop status.
+     *
+     * @param {string} [aReason] Some states are only shown if
+     *                           a related reason is provided.
+     *
+     *                 aReason="login": Used after a login is completed
+     *                   successfully. This is used so the state can be
+     *                   temporarily shown until the next state change.
+     */
+    updateToolbarState: function(aReason = null) {
       let state = "";
       if (MozLoopService.errors.size) {
         state = "error";
+      } else if (aReason == "login" && MozLoopService.userProfile) {
+        state = "active";
       } else if (MozLoopService.doNotDisturb) {
         state = "disabled";
       }
       this.toolbarButton.node.setAttribute("state", state);
     },
   };
 })();
--- a/browser/components/loop/MozLoopService.jsm
+++ b/browser/components/loop/MozLoopService.jsm
@@ -155,18 +155,18 @@ let MozLoopServiceInternal = {
    *
    * @param {Boolean} aFlag
    */
   set doNotDisturb(aFlag) {
     Services.prefs.setBoolPref("loop.do_not_disturb", Boolean(aFlag));
     this.notifyStatusChanged();
   },
 
-  notifyStatusChanged: function() {
-    Services.obs.notifyObservers(null, "loop-status-changed", null);
+  notifyStatusChanged: function(aReason = null) {
+    Services.obs.notifyObservers(null, "loop-status-changed", aReason);
   },
 
   /**
    * @param {String} errorType a key to identify the type of error. Only one
    *                           error of a type will be saved at a time.
    * @param {Object} error     an object describing the error in the format from Hawk errors
    */
   setError: function(errorType, error) {
@@ -1019,17 +1019,17 @@ this.MozLoopService = {
       }));
     }).then(tokenData => {
       let client = new FxAccountsProfileClient({
         serverURL: gFxAOAuthClient.parameters.profile_uri,
         token: tokenData.access_token
       });
       client.fetchProfile().then(result => {
         gFxAOAuthProfile = result;
-        MozLoopServiceInternal.notifyStatusChanged();
+        MozLoopServiceInternal.notifyStatusChanged("login");
       }, error => {
         console.error("Failed to retrieve profile", error);
         gFxAOAuthProfile = null;
         MozLoopServiceInternal.notifyStatusChanged();
       });
       return tokenData;
     }).catch(error => {
       gFxAOAuthTokenData = null;
--- a/browser/components/loop/test/mochitest/browser_fxa_login.js
+++ b/browser/components/loop/test/mochitest/browser_fxa_login.js
@@ -214,40 +214,56 @@ add_task(function* basicAuthorizationAnd
     oauth_uri: BASE_URL + "/oauth",
     profile_uri: BASE_URL + "/profile",
     state: "state",
   };
   yield promiseOAuthParamsSetup(BASE_URL, params);
 
   info("registering");
   mockPushHandler.pushUrl = "https://localhost/pushUrl/guest";
+  // Notification observed due to the error being cleared upon successful registration.
+  let statusChangedPromise = promiseObserverNotified("loop-status-changed");
   yield MozLoopService.register(mockPushHandler);
+  yield statusChangedPromise;
 
   // Normally the same pushUrl would be registered but we change it in the test
   // to be able to check for success on the second registration.
   mockPushHandler.pushUrl = "https://localhost/pushUrl/fxa";
 
+  statusChangedPromise = promiseObserverNotified("loop-status-changed");
   yield loadLoopPanel({loopURL: BASE_URL, stayOnline: true});
+  yield statusChangedPromise;
   let loopDoc = document.getElementById("loop").contentDocument;
   let visibleEmail = loopDoc.getElementsByClassName("user-identity")[0];
   is(visibleEmail.textContent, "Guest", "Guest should be displayed on the panel when not logged in");
   is(MozLoopService.userProfile, null, "profile should be null before log-in");
+  let loopButton = document.getElementById("loop-call-button");
+  is(loopButton.getAttribute("state"), "", "state of loop button should be empty when not logged in");
 
   let tokenData = yield MozLoopService.logInToFxA();
-  yield promiseObserverNotified("loop-status-changed");
+  yield promiseObserverNotified("loop-status-changed", "login");
   ise(tokenData.access_token, "code1_access_token", "Check access_token");
   ise(tokenData.scope, "profile", "Check scope");
   ise(tokenData.token_type, "bearer", "Check token_type");
 
   is(MozLoopService.userProfile.email, "test@example.com", "email should exist in the profile data");
   is(MozLoopService.userProfile.uid, "1234abcd", "uid should exist in the profile data");
   is(visibleEmail.textContent, "test@example.com", "the email should be correct on the panel");
+  is(loopButton.getAttribute("state"), "active", "state of loop button should be active when logged in");
 
   let registrationResponse = yield promiseOAuthGetRegistration(BASE_URL);
   ise(registrationResponse.response.simplePushURL, "https://localhost/pushUrl/fxa", "Check registered push URL");
+
+  let loopPanel = document.getElementById("loop-notification-panel");
+  loopPanel.hidePopup();
+  statusChangedPromise = promiseObserverNotified("loop-status-changed");
+  yield loadLoopPanel({loopURL: BASE_URL, stayOnline: true});
+  yield statusChangedPromise;
+  is(loopButton.getAttribute("state"), "", "state of loop button should return to empty after panel is opened");
+  loopPanel.hidePopup();
 });
 
 add_task(function* loginWithParams401() {
   resetFxA();
   let params = {
     client_id: "client_id",
     content_uri: BASE_URL + "/content",
     oauth_uri: BASE_URL + "/oauth",
--- a/browser/components/loop/test/mochitest/browser_toolbarbutton.js
+++ b/browser/components/loop/test/mochitest/browser_toolbarbutton.js
@@ -4,24 +4,74 @@
 /**
  * Test the toolbar button states.
  */
 
 "use strict";
 
 registerCleanupFunction(function*() {
   MozLoopService.doNotDisturb = false;
+  setInternalLoopGlobal("gFxAOAuthProfile", null);
   yield MozLoopServiceInternal.clearError("testing");
 });
 
 add_task(function* test_doNotDisturb() {
+  Assert.strictEqual(LoopUI.toolbarButton.node.getAttribute("state"), "", "Check button is in default state");
   yield MozLoopService.doNotDisturb = true;
   Assert.strictEqual(LoopUI.toolbarButton.node.getAttribute("state"), "disabled", "Check button is in disabled state");
   yield MozLoopService.doNotDisturb = false;
   Assert.notStrictEqual(LoopUI.toolbarButton.node.getAttribute("state"), "disabled", "Check button is not in disabled state");
 });
 
+add_task(function* test_doNotDisturb_with_login() {
+  Assert.strictEqual(LoopUI.toolbarButton.node.getAttribute("state"), "", "Check button is in default state");
+  yield MozLoopService.doNotDisturb = true;
+  Assert.strictEqual(LoopUI.toolbarButton.node.getAttribute("state"), "disabled", "Check button is in disabled state");
+  setInternalLoopGlobal("gFxAOAuthProfile", {email: "test@example.com", uid: "abcd1234"});
+  yield MozLoopServiceInternal.notifyStatusChanged("login");
+  Assert.strictEqual(LoopUI.toolbarButton.node.getAttribute("state"), "active", "Check button is in active state");
+  yield loadLoopPanel();
+  Assert.strictEqual(LoopUI.toolbarButton.node.getAttribute("state"), "disabled", "Check button is in disabled state after opening panel");
+  let loopPanel = document.getElementById("loop-notification-panel");
+  loopPanel.hidePopup();
+  yield MozLoopService.doNotDisturb = false;
+  Assert.strictEqual(LoopUI.toolbarButton.node.getAttribute("state"), "", "Check button is in default state");
+  setInternalLoopGlobal("gFxAOAuthProfile", null);
+  yield MozLoopServiceInternal.notifyStatusChanged();
+  Assert.strictEqual(LoopUI.toolbarButton.node.getAttribute("state"), "", "Check button is in default state");
+});
+
 add_task(function* test_error() {
+  Assert.strictEqual(LoopUI.toolbarButton.node.getAttribute("state"), "", "Check button is in default state");
   yield MozLoopServiceInternal.setError("testing", {});
   Assert.strictEqual(LoopUI.toolbarButton.node.getAttribute("state"), "error", "Check button is in error state");
   yield MozLoopServiceInternal.clearError("testing");
   Assert.notStrictEqual(LoopUI.toolbarButton.node.getAttribute("state"), "error", "Check button is not in error state");
 });
+
+add_task(function* test_error_with_login() {
+  Assert.strictEqual(LoopUI.toolbarButton.node.getAttribute("state"), "", "Check button is in default state");
+  yield MozLoopServiceInternal.setError("testing", {});
+  Assert.strictEqual(LoopUI.toolbarButton.node.getAttribute("state"), "error", "Check button is in error state");
+  setInternalLoopGlobal("gFxAOAuthProfile", {email: "test@example.com", uid: "abcd1234"});
+  MozLoopServiceInternal.notifyStatusChanged("login");
+  Assert.strictEqual(LoopUI.toolbarButton.node.getAttribute("state"), "error", "Check button is in error state");
+  yield MozLoopServiceInternal.clearError("testing");
+  Assert.strictEqual(LoopUI.toolbarButton.node.getAttribute("state"), "", "Check button is in default state");
+  setInternalLoopGlobal("gFxAOAuthProfile", null);
+  MozLoopServiceInternal.notifyStatusChanged();
+  Assert.strictEqual(LoopUI.toolbarButton.node.getAttribute("state"), "", "Check button is in default state");
+});
+
+add_task(function* test_active() {
+  Assert.strictEqual(LoopUI.toolbarButton.node.getAttribute("state"), "", "Check button is in default state");
+  setInternalLoopGlobal("gFxAOAuthProfile", {email: "test@example.com", uid: "abcd1234"});
+  yield MozLoopServiceInternal.notifyStatusChanged("login");
+  Assert.strictEqual(LoopUI.toolbarButton.node.getAttribute("state"), "active", "Check button is in active state");
+  yield loadLoopPanel();
+  Assert.strictEqual(LoopUI.toolbarButton.node.getAttribute("state"), "", "Check button is in default state after opening panel");
+  let loopPanel = document.getElementById("loop-notification-panel");
+  loopPanel.hidePopup();
+  setInternalLoopGlobal("gFxAOAuthProfile", null);
+  MozLoopServiceInternal.notifyStatusChanged();
+  Assert.strictEqual(LoopUI.toolbarButton.node.getAttribute("state"), "", "Check button is in default state");
+});
+
--- a/browser/components/loop/test/mochitest/head.js
+++ b/browser/components/loop/test/mochitest/head.js
@@ -41,17 +41,21 @@ function promiseGetMozLoopAPI() {
   // Now we're setup, click the button.
   btn.click();
 
   // Remove the iframe after each test. This also avoids mochitest complaining
   // about leaks on shutdown as we intentionally hold the iframe open for the
   // life of the application.
   registerCleanupFunction(function() {
     loopPanel.hidePopup();
-    loopPanel.removeChild(document.getElementById(btn.getAttribute("notificationFrameId")));
+    let frameId = btn.getAttribute("notificationFrameId");
+    let frame = document.getElementById(frameId);
+    if (frame) {
+      loopPanel.removeChild(frame);
+    }
   });
 
   return deferred.promise;
 }
 
 /**
  * Loads the loop panel by clicking the button and waits for its open to complete.
  * It also registers
@@ -105,34 +109,40 @@ function resetFxA() {
   global.gFxAOAuthClientPromise = null;
   global.gFxAOAuthClient = null;
   global.gFxAOAuthTokenData = null;
   global.gFxAOAuthProfile = null;
   const fxASessionPref = MozLoopServiceInternal.getSessionTokenPrefName(LOOP_SESSION_TYPE.FXA);
   Services.prefs.clearUserPref(fxASessionPref);
 }
 
+function setInternalLoopGlobal(aName, aValue) {
+  let global = Cu.import("resource:///modules/loop/MozLoopService.jsm", {});
+  global[aName] = aValue;
+}
+
 function promiseDeletedOAuthParams(baseURL) {
   let deferred = Promise.defer();
   let xhr = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"].
               createInstance(Ci.nsIXMLHttpRequest);
   xhr.open("DELETE", baseURL + "/setup_params", true);
   xhr.addEventListener("load", () => deferred.resolve(xhr));
   xhr.addEventListener("error", deferred.reject);
   xhr.send();
 
   return deferred.promise;
 }
 
-function promiseObserverNotified(aTopic) {
+function promiseObserverNotified(aTopic, aExpectedData = null) {
   let deferred = Promise.defer();
   Services.obs.addObserver(function onNotification(aSubject, aTopic, aData) {
     Services.obs.removeObserver(onNotification, aTopic);
-      deferred.resolve({subject: aSubject, data: aData});
-    }, aTopic, false);
+    is(aData, aExpectedData, "observer data should match expected data")
+    deferred.resolve({subject: aSubject, data: aData});
+  }, aTopic, false);
   return deferred.promise;
 }
 
 /**
  * Get the last registration on the test server.
  */
 function promiseOAuthGetRegistration(baseURL) {
   let deferred = Promise.defer();