[Aurora only] Bug 1073215: make sure that the Loop button shows up in the navbar when unthrottled. r=Unfocused,abr. a=lsblakk
authorMike de Boer <mdeboer@mozilla.com>
Thu, 06 Nov 2014 23:08:12 +0100
changeset 235002 34839eeada8d3e89b8b7da6d11f50c5936a41531
parent 235001 ccde6ac141db424d210739653c79d317cbd9c8e3
child 235003 cfcd437076bca19b10dd1c866eae04c88359dcee
child 235056 2b8c082c1e55fc928d9f0134b18aebbc557addda
push id611
push userraliiev@mozilla.com
push dateMon, 05 Jan 2015 23:23:16 +0000
treeherdermozilla-release@345cd3b9c445 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersUnfocused, abr, lsblakk
bugs1073215
milestone35.0a2
[Aurora only] Bug 1073215: make sure that the Loop button shows up in the navbar when unthrottled. r=Unfocused,abr. a=lsblakk
browser/app/profile/firefox.js
browser/base/content/browser-loop.js
browser/base/content/browser.js
browser/base/content/browser.xul
browser/components/customizableui/CustomizableUI.jsm
browser/components/customizableui/CustomizableWidgets.jsm
browser/components/customizableui/CustomizeMode.jsm
browser/components/loop/MozLoopService.jsm
browser/components/loop/test/functional/test_1_browser_call.py
browser/components/loop/test/mochitest/browser_fxa_login.js
browser/components/loop/test/mochitest/browser_mozLoop_softStart.js
browser/components/loop/test/mochitest/head.js
browser/modules/UITour.jsm
browser/themes/linux/browser.css
browser/themes/osx/browser.css
browser/themes/shared/menupanel.inc.css
browser/themes/shared/toolbarbuttons.inc.css
browser/themes/windows/browser.css
--- a/browser/app/profile/firefox.js
+++ b/browser/app/profile/firefox.js
@@ -1591,20 +1591,20 @@ pref("shumway.disabled", true);
 // The maximum amount of decoded image data we'll willingly keep around (we
 // might keep around more than this, but we'll try to get down to this value).
 // (This is intentionally on the high side; see bug 746055.)
 pref("image.mem.max_decoded_image_kb", 256000);
 
 // Enable by default development builds up until early beta
 #ifdef EARLY_BETA_OR_EARLIER
 pref("loop.enabled", true);
-pref("loop.throttled", false);
+pref("loop.throttled2", false);
 #else
 pref("loop.enabled", true);
-pref("loop.throttled", true);
+pref("loop.throttled2", true);
 pref("loop.soft_start_ticket_number", -1);
 pref("loop.soft_start_hostname", "soft-start.loop.services.mozilla.com");
 #endif
 
 pref("loop.server", "https://loop.services.mozilla.com");
 pref("loop.seenToS", "unseen");
 pref("loop.learnMoreUrl", "https://www.firefox.com/hello/");
 pref("loop.legal.ToS_url", "https://hello.firefox.com/legal/terms/");
--- a/browser/base/content/browser-loop.js
+++ b/browser/base/content/browser-loop.js
@@ -10,17 +10,17 @@ XPCOMUtils.defineLazyModuleGetter(this, 
 XPCOMUtils.defineLazyModuleGetter(this, "PanelFrame", "resource:///modules/PanelFrame.jsm");
 
 
 (function() {
 
   LoopUI = {
     get toolbarButton() {
       delete this.toolbarButton;
-      return this.toolbarButton = CustomizableUI.getWidget("loop-call-button").forWindow(window);
+      return this.toolbarButton = CustomizableUI.getWidget("loop-button-throttled").forWindow(window);
     },
 
     /**
      * Opens the panel for Loop and sizes it appropriately.
      *
      * @param {event} event The event opening the panel, used to anchor
      *                      the panel to the button which triggers it.
      */
@@ -39,31 +39,19 @@ XPCOMUtils.defineLazyModuleGetter(this, 
                            "about:looppanel", null, callback);
     },
 
     /**
      * Triggers the initialization of the loop service.  Called by
      * delayedStartup.
      */
     init: function() {
-      if (!Services.prefs.getBoolPref("loop.enabled")) {
-        this.toolbarButton.node.hidden = true;
-        return;
-      }
-
       // Add observer notifications before the service is initialized
       Services.obs.addObserver(this, "loop-status-changed", false);
 
-      // If we're throttled, check to see if it's our turn to be unthrottled
-      if (Services.prefs.getBoolPref("loop.throttled")) {
-        this.toolbarButton.node.hidden = true;
-        MozLoopService.checkSoftStart(this.toolbarButton.node);
-        return;
-      }
-
       MozLoopService.initialize();
       this.updateToolbarState();
     },
 
     uninit: function() {
       Services.obs.removeObserver(this, "loop-status-changed");
     },
 
--- a/browser/base/content/browser.js
+++ b/browser/base/content/browser.js
@@ -1276,16 +1276,29 @@ var gBrowserInit = {
     gFxAccounts.init();
 #endif
 
 #ifdef MOZ_DATA_REPORTING
     gDataNotificationInfoBar.init();
 #endif
 
     LoopUI.init();
+    // Loop throttling support.
+    const kWidgetId = "loop-button-throttled";
+    // If we're throttled, check to see if it's our turn to be unthrottled
+    if (Services.prefs.getBoolPref("loop.throttled2")) {
+      MozLoopService.checkSoftStart(() => {
+        // If the check unthrottled us and the button was not customized to an
+        // area by the user, move it to the nav-bar.
+        let widget = CustomizableUI.getWidget(kWidgetId);
+        if (!Services.prefs.getBoolPref("loop.throttled2") && !widget.areaType) {
+          CustomizableUI.addWidgetToArea(kWidgetId, CustomizableUI.AREA_NAVBAR);
+        }
+      });
+    }
 
     gBrowserThumbnails.init();
 
     // Add Devtools menuitems and listeners
     gDevToolsBrowser.registerBrowserWindow(window);
 
     window.addEventListener("mousemove", MousePosTracker, false);
     window.addEventListener("dragover", MousePosTracker, false);
--- a/browser/base/content/browser.xul
+++ b/browser/base/content/browser.xul
@@ -674,19 +674,19 @@
            to the default placements of buttons in CustomizableUI.jsm, so the
            customization code doesn't get confused.
       -->
     <toolbar id="nav-bar" class="toolbar-primary chromeclass-toolbar"
              aria-label="&navbarCmd.label;"
              fullscreentoolbar="true" mode="icons" customizable="true"
              iconsize="small"
 #ifdef MOZ_DEV_EDITION
-             defaultset="urlbar-container,search-container,developer-button,bookmarks-menu-button,downloads-button,home-button,loop-call-button"
+             defaultset="urlbar-container,search-container,developer-button,bookmarks-menu-button,downloads-button,home-button,loop-button-throttled"
 #else
-             defaultset="urlbar-container,search-container,bookmarks-menu-button,downloads-button,home-button,loop-call-button"
+             defaultset="urlbar-container,search-container,bookmarks-menu-button,downloads-button,home-button,loop-button-throttled"
 #endif
              customizationtarget="nav-bar-customization-target"
              overflowable="true"
              overflowbutton="nav-bar-overflow-button"
              overflowtarget="widget-overflow-list"
              overflowpanel="widget-overflow"
              context="toolbar-context-menu">
 
--- a/browser/components/customizableui/CustomizableUI.jsm
+++ b/browser/components/customizableui/CustomizableUI.jsm
@@ -49,17 +49,17 @@ const kSubviewEvents = [
   "ViewShowing",
   "ViewHiding"
 ];
 
 /**
  * The current version. We can use this to auto-add new default widgets as necessary.
  * (would be const but isn't because of testing purposes)
  */
-let kVersion = 1;
+let kVersion = 3;
 
 /**
  * gPalette is a map of every widget that CustomizableUI.jsm knows about, keyed
  * on their IDs.
  */
 let gPalette = new Map();
 
 /**
@@ -205,17 +205,17 @@ let CustomizableUIInternal = {
       "urlbar-container",
       "search-container",
 #ifdef MOZ_DEV_EDITION
       "developer-button",
 #endif
       "bookmarks-menu-button",
       "downloads-button",
       "home-button",
-      "loop-call-button",
+      "loop-button-throttled",
     ];
 
     if (Services.prefs.getBoolPref(kPrefWebIDEInNavbar)) {
       navbarPlacements.push("webide-button");
     }
 
     this.registerArea(CustomizableUI.AREA_NAVBAR, {
       legacy: true,
@@ -303,16 +303,21 @@ let CustomizableUIInternal = {
         let futurePlacements = gFuturePlacements.get(widget.defaultArea);
         if (futurePlacements) {
           futurePlacements.add(id);
         } else {
           gFuturePlacements.set(widget.defaultArea, new Set([id]));
         }
       }
     }
+
+    if (currentVersion < 2) {
+      // Nuke the old 'loop-call-button' out of orbit.
+      CustomizableUI.removeWidgetFromArea("loop-call-button");
+    }
   },
 
   wrapWidget: function(aWidgetId) {
     if (gGroupWrapperCache.has(aWidgetId)) {
       return gGroupWrapperCache.get(aWidgetId);
     }
 
     let provider = this.getWidgetProvider(aWidgetId);
--- a/browser/components/customizableui/CustomizableWidgets.jsm
+++ b/browser/components/customizableui/CustomizableWidgets.jsm
@@ -920,34 +920,40 @@ const CustomizableWidgets = [
   }, {
     id: "email-link-button",
     tooltiptext: "email-link-button.tooltiptext3",
     onCommand: function(aEvent) {
       let win = aEvent.view;
       win.MailIntegration.sendLinkForWindow(win.content);
     }
   }, {
-    id: "loop-call-button",
+    id: "loop-button-throttled",
     type: "custom",
     label: "Hello",
     tooltiptext: "loop-call-button2.tooltiptext",
-    defaultArea: CustomizableUI.AREA_NAVBAR,
-    introducedInVersion: 1,
+    defaultArea: !Services.prefs.getBoolPref("loop.throttled2") && CustomizableUI.AREA_NAVBAR,
+    introducedInVersion: 3,
     onBuild: function(aDocument) {
+      // If we're not supposed to see the button, return zip.
+      if (!Services.prefs.getBoolPref("loop.enabled")) {
+        return null;
+      }
+
       let node = aDocument.createElementNS(kNSXUL, "toolbarbutton");
       node.setAttribute("id", this.id);
       node.classList.add("toolbarbutton-1");
       node.classList.add("chromeclass-toolbar-additional");
       node.classList.add("badged-button");
       node.setAttribute("label", CustomizableUI.getLocalizedProperty(this, "label"));
       node.setAttribute("tooltiptext", CustomizableUI.getLocalizedProperty(this, "tooltiptext"));
       node.setAttribute("removable", "true");
       node.addEventListener("command", function(event) {
         aDocument.defaultView.LoopUI.openCallPanel(event);
       });
+
       return node;
     }
   }, {
     id: "web-apps-button",
     label: "web-apps-button.label",
     tooltiptext: "web-apps-button.tooltiptext",
     onCommand: function(aEvent) {
       let win = aEvent.target &&
--- a/browser/components/customizableui/CustomizeMode.jsm
+++ b/browser/components/customizableui/CustomizeMode.jsm
@@ -736,16 +736,19 @@ CustomizeMode.prototype = {
   populatePalette: function() {
     let fragment = this.document.createDocumentFragment();
     let toolboxPalette = this.window.gNavToolbox.palette;
 
     try {
       let unusedWidgets = CustomizableUI.getUnusedWidgets(toolboxPalette);
       for (let widget of unusedWidgets) {
         let paletteItem = this.makePaletteItem(widget, "palette");
+        if (!paletteItem) {
+          continue;
+        }
         fragment.appendChild(paletteItem);
       }
 
       this.visiblePalette.appendChild(fragment);
       this._stowedPalette = this.window.gNavToolbox.palette;
       this.window.gNavToolbox.palette = this.visiblePalette;
     } catch (ex) {
       ERROR(ex);
@@ -753,16 +756,25 @@ CustomizeMode.prototype = {
   },
 
   //XXXunf Maybe this should use -moz-element instead of wrapping the node?
   //       Would ensure no weird interactions/event handling from original node,
   //       and makes it possible to put this in a lazy-loaded iframe/real tab
   //       while still getting rid of the need for overlays.
   makePaletteItem: function(aWidget, aPlace) {
     let widgetNode = aWidget.forWindow(this.window).node;
+    if (!widgetNode) {
+      ERROR("Widget with id " + aWidget.id + " does not return a valid node");
+      return null;
+    }
+    // Do not build a palette item for hidden widgets; there's not much to show.
+    if (widgetNode.hidden) {
+      return null;
+    }
+
     let wrapper = this.createOrUpdateWrapper(widgetNode, aPlace);
     wrapper.appendChild(widgetNode);
     return wrapper;
   },
 
   depopulatePalette: function() {
     return Task.spawn(function() {
       this.visiblePalette.hidden = true;
--- a/browser/components/loop/MozLoopService.jsm
+++ b/browser/components/loop/MozLoopService.jsm
@@ -1202,19 +1202,22 @@ this.MozLoopService = {
    *
    * @return {Promise}
    */
   initialize: Task.async(function*(mockPushHandler, mockWebSocket) {
     // Do this here, rather than immediately after definition, so that we can
     // stub out API functions for unit testing
     Object.freeze(this);
 
+    // Clear the old throttling mechanism.
+    Services.prefs.clearUserPref("loop.throttled");
+
     // Don't do anything if loop is not enabled.
     if (!Services.prefs.getBoolPref("loop.enabled") ||
-        Services.prefs.getBoolPref("loop.throttled")) {
+        Services.prefs.getBoolPref("loop.throttled2")) {
       return Promise.reject("loop is not enabled");
     }
 
     if (Services.prefs.getPrefType("loop.fxa.enabled") == Services.prefs.PREF_BOOL) {
       gFxAEnabled = Services.prefs.getBoolPref("loop.fxa.enabled");
       if (!gFxAEnabled) {
         yield this.logOutFromFxA();
       }
@@ -1239,24 +1242,21 @@ this.MozLoopService = {
     });
   }),
 
   /**
    * If we're operating the service in "soft start" mode, and this browser
    * isn't already activated, check whether it's time for it to become active.
    * If so, activate the loop service.
    *
-   * @param {Object} buttonNode DOM node representing the Loop button -- if we
-   *                            change from inactive to active, we need this
-   *                            in order to unhide the Loop button.
    * @param {Function} doneCb   [optional] Callback that is called when the
    *                            check has completed.
    */
-  checkSoftStart(buttonNode, doneCb) {
-    if (!Services.prefs.getBoolPref("loop.throttled")) {
+  checkSoftStart(doneCb) {
+    if (!Services.prefs.getBoolPref("loop.throttled2")) {
       if (typeof(doneCb) == "function") {
         doneCb(new Error("Throttling is not active"));
       }
       return;
     }
 
     if (Services.io.offline) {
       if (typeof(doneCb) == "function") {
@@ -1309,18 +1309,17 @@ this.MozLoopService = {
       // operations as 32-bit *signed* integers.
       let now_serving = ((parseInt(address[1]) * 0x10000) +
                          (parseInt(address[2]) * 0x100) +
                          parseInt(address[3]));
 
       if (now_serving > ticket) {
         // Hot diggity! It's our turn! Activate the service.
         log.info("MozLoopService: Activating Loop via soft-start");
-        Services.prefs.setBoolPref("loop.throttled", false);
-        buttonNode.hidden = false;
+        Services.prefs.setBoolPref("loop.throttled2", false);
         this.initialize();
       }
       if (typeof(doneCb) == "function") {
         doneCb(null);
       }
     };
 
     // We use DNS to propagate the slow-start value, since it has well-known
@@ -1349,17 +1348,17 @@ this.MozLoopService = {
    */
   register: function(mockPushHandler, mockWebSocket) {
     log.debug("registering");
     // Don't do anything if loop is not enabled.
     if (!Services.prefs.getBoolPref("loop.enabled")) {
       throw new Error("Loop is not enabled");
     }
 
-    if (Services.prefs.getBoolPref("loop.throttled")) {
+    if (Services.prefs.getBoolPref("loop.throttled2")) {
       throw new Error("Loop is disabled by the soft-start mechanism");
     }
 
     return MozLoopServiceInternal.promiseRegisteredWithServers(mockPushHandler, mockWebSocket);
   },
 
   /**
    * Used to note a call url expiry time. If the time is later than the current
--- a/browser/components/loop/test/functional/test_1_browser_call.py
+++ b/browser/components/loop/test/functional/test_1_browser_call.py
@@ -42,17 +42,17 @@ class Test1BrowserCall(MarionetteTestCas
     # XXX workaround for Marionette bug 1055309
     def wait_for_element_exists(self, by, locator, timeout=None):
         Wait(self.marionette, timeout,
              ignored_exceptions=[NoSuchElementException, StaleElementException]) \
             .until(lambda m: m.find_element(by, locator))
         return self.marionette.find_element(by, locator)
 
     def switch_to_panel(self):
-        button = self.marionette.find_element(By.ID, "loop-call-button")
+        button = self.marionette.find_element(By.ID, "loop-button-throttled")
 
         # click the element
         button.click()
 
         # switch to the frame
         frame = self.marionette.find_element(By.ID, "loop")
         self.marionette.switch_to_frame(frame)
 
--- a/browser/components/loop/test/mochitest/browser_fxa_login.js
+++ b/browser/components/loop/test/mochitest/browser_fxa_login.js
@@ -13,17 +13,17 @@ function* checkFxA401() {
   let err = MozLoopService.errors.get("login");
   ise(err.code, 401, "Check error code");
   ise(err.friendlyMessage, getLoopString("could_not_authenticate"),
       "Check friendlyMessage");
   ise(err.friendlyDetails, getLoopString("password_changed_question"),
       "Check friendlyDetails");
   ise(err.friendlyDetailsButtonLabel, getLoopString("retry_button"),
       "Check friendlyDetailsButtonLabel");
-  let loopButton = document.getElementById("loop-call-button");
+  let loopButton = document.getElementById("loop-button-throttled");
   is(loopButton.getAttribute("state"), "error",
      "state of loop button should be error after a 401 with login");
 
   let loopPanel = document.getElementById("loop-notification-panel");
   yield loadLoopPanel({loopURL: BASE_URL });
   let loopDoc = document.getElementById("loop").contentDocument;
   is(loopDoc.querySelector(".alert-error .message").textContent,
      getLoopString("could_not_authenticate"),
@@ -260,17 +260,17 @@ add_task(function* basicAuthorizationAnd
 
   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");
+  let loopButton = document.getElementById("loop-button-throttled");
   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", "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");
 
--- a/browser/components/loop/test/mochitest/browser_mozLoop_softStart.js
+++ b/browser/components/loop/test/mochitest/browser_mozLoop_softStart.js
@@ -34,102 +34,89 @@ let MockDNSService = {
 let LoopService = {};
 for (var prop in MozLoopService) {
   if (MozLoopService.hasOwnProperty(prop)) {
     LoopService[prop] = MozLoopService[prop];
   }
 }
 LoopService._DNSService = MockDNSService;
 
-let MockButton = {
-  hidden: true
-};
-
 let runCheck = function(expectError) {
   return new Promise((resolve, reject) => {
-    LoopService.checkSoftStart(MockButton, error => {
+    LoopService.checkSoftStart(error => {
       if ((!!error) != (!!expectError)) {
         reject(error);
       } else {
         resolve(error);
       }
     })
   });
 }
 
 add_task(function* test_mozLoop_softStart() {
-  let orig_throttled = Services.prefs.getBoolPref("loop.throttled");
+  let orig_throttled = Services.prefs.getBoolPref("loop.throttled2");
 
   // Set associated variables to proper values
-  Services.prefs.setBoolPref("loop.throttled", true);
+  Services.prefs.setBoolPref("loop.throttled2", true);
   Services.prefs.setCharPref("loop.soft_start_hostname", SOFT_START_HOSTNAME);
   Services.prefs.setIntPref("loop.soft_start_ticket_number", -1);
 
   registerCleanupFunction(function () {
-    Services.prefs.setBoolPref("loop.throttled", orig_throttled);
+    Services.prefs.setBoolPref("loop.throttled2", orig_throttled);
     Services.prefs.clearUserPref("loop.soft_start_ticket_number");
     Services.prefs.clearUserPref("loop.soft_start_hostname");
   });
 
   let throttled;
   let ticket;
 
   info("Ensure that we pick a valid ticket number.");
   yield runCheck();
-  throttled = Services.prefs.getBoolPref("loop.throttled");
+  throttled = Services.prefs.getBoolPref("loop.throttled2");
   ticket = Services.prefs.getIntPref("loop.soft_start_ticket_number");
-  Assert.equal(MockButton.hidden, true, "Button should still be hidden");
   Assert.equal(throttled, true, "Feature should still be throttled");
   Assert.notEqual(ticket, -1, "Ticket should be changed");
   Assert.ok((ticket < 16777214 && ticket > 0), "Ticket should be in range");
 
   // Try some "interesting" ticket numbers
   for (ticket of [1, 256, 65535, 10000000, 16777214]) {
-    MockButton.hidden = true;
-    Services.prefs.setBoolPref("loop.throttled", true);
+    Services.prefs.setBoolPref("loop.throttled2", true);
     Services.prefs.setIntPref("loop.soft_start_ticket_number", ticket);
 
     info("Ensure that we don't activate when the now serving " +
          "number is less than our value.");
     MockDNSService.nowServing = ticket - 1;
     yield runCheck();
-    throttled = Services.prefs.getBoolPref("loop.throttled");
-    Assert.equal(MockButton.hidden, true, "Button should still be hidden");
+    throttled = Services.prefs.getBoolPref("loop.throttled2");
     Assert.equal(throttled, true, "Feature should still be throttled");
 
     info("Ensure that we don't activate when the now serving " +
          "number is equal to our value");
     MockDNSService.nowServing = ticket;
     yield runCheck();
-    throttled = Services.prefs.getBoolPref("loop.throttled");
-    Assert.equal(MockButton.hidden, true, "Button should still be hidden");
+    throttled = Services.prefs.getBoolPref("loop.throttled2");
     Assert.equal(throttled, true, "Feature should still be throttled");
 
     info("Ensure that we *do* activate when the now serving " +
          "number is greater than our value");
     MockDNSService.nowServing = ticket + 1;
     yield runCheck();
-    throttled = Services.prefs.getBoolPref("loop.throttled");
-    Assert.equal(MockButton.hidden, false, "Button should not be hidden");
+    throttled = Services.prefs.getBoolPref("loop.throttled2");
     Assert.equal(throttled, false, "Feature should be unthrottled");
   }
 
   info("Check DNS error behavior");
   MockDNSService.nowServing = 0;
   MockDNSService.resultCode = 0x80000000;
-  Services.prefs.setBoolPref("loop.throttled", true);
-  MockButton.hidden = true;
+  Services.prefs.setBoolPref("loop.throttled2", true);
   yield runCheck(true);
-  throttled = Services.prefs.getBoolPref("loop.throttled");
-  Assert.equal(MockButton.hidden, true, "Button should be hidden");
+  throttled = Services.prefs.getBoolPref("loop.throttled2");
   Assert.equal(throttled, true, "Feature should be throttled");
 
   info("Check DNS misconfiguration behavior");
   MockDNSService.nowServing = ticket + 1;
   MockDNSService.resultCode = 0;
   MockDNSService.ipFirstOctet = 6;
-  Services.prefs.setBoolPref("loop.throttled", true);
-  MockButton.hidden = true;
+  Services.prefs.setBoolPref("loop.throttled2", true);
   yield runCheck(true);
-  throttled = Services.prefs.getBoolPref("loop.throttled");
-  Assert.equal(MockButton.hidden, true, "Button should be hidden");
+  throttled = Services.prefs.getBoolPref("loop.throttled2");
   Assert.equal(throttled, true, "Feature should be throttled");
 });
--- a/browser/components/loop/test/mochitest/head.js
+++ b/browser/components/loop/test/mochitest/head.js
@@ -12,17 +12,17 @@ const {
 // if offline mode is requested multiple times in a test run.
 const WAS_OFFLINE = Services.io.offline;
 
 var gMozLoopAPI;
 
 function promiseGetMozLoopAPI() {
   let deferred = Promise.defer();
   let loopPanel = document.getElementById("loop-notification-panel");
-  let btn = document.getElementById("loop-call-button");
+  let btn = document.getElementById("loop-button-throttled");
 
   // Wait for the popup to be shown if it's not already, then we can get the iframe and
   // wait for the iframe's load to be completed.
   if (loopPanel.state == "closing" || loopPanel.state == "closed") {
     loopPanel.addEventListener("popupshown", () => {
       loopPanel.removeEventListener("popupshown", onpopupshown, true);
       onpopupshown();
     }, true);
@@ -209,16 +209,23 @@ let mockPushHandler = {
   /**
    * Test-only API to simplify notifying a push notification result.
    */
   notify: function(version) {
     this._notificationCallback(version);
   }
 };
 
+// Add the Loop button to the navbar.
+CustomizableUI.addWidgetToArea("loop-button-throttled", CustomizableUI.AREA_NAVBAR);
+
+registerCleanupFunction(function() {
+  CustomizableUI.reset();
+});
+
 const mockDb = {
   _store: { },
   _next_guid: 1,
 
   get size() {
     return Object.getOwnPropertyNames(this._store).length;
   },
 
--- a/browser/modules/UITour.jsm
+++ b/browser/modules/UITour.jsm
@@ -97,17 +97,17 @@ this.UITour = {
         return aDocument.getAnonymousElementByAttribute(customizeButton,
                                                         "class",
                                                         "toolbarbutton-icon");
       },
       widgetName: "PanelUI-customize",
     }],
     ["help",        {query: "#PanelUI-help"}],
     ["home",        {query: "#home-button"}],
-    ["loop",        {query: "#loop-call-button"}],
+    ["loop",        {query: "#loop-button-throttled"}],
     ["devtools",    {query: "#developer-button"}],
     ["webide",      {query: "#webide-button"}],
     ["forget", {
       query: "#panic-button",
       widgetName: "panic-button",
       allowAdd: true }],
     ["privateWindow",  {query: "#privatebrowsing-button"}],
     ["quit",        {query: "#PanelUI-quit"}],
--- a/browser/themes/linux/browser.css
+++ b/browser/themes/linux/browser.css
@@ -1586,17 +1586,17 @@ richlistitem[type~="action"][actiontype=
 }
 
 /* Popup blocker button */
 #page-report-button {
   list-style-image: url("chrome://browser/skin/Info.png");
 }
 
 /* Loop */
-#loop-call-button {
+#loop-button-throttled {
   list-style-image: url("chrome://global/skin/loop/loop-call.png");
 }
 
 /* social share panel */
 
 .social-share-frame {
   background: linear-gradient(to bottom, rgba(242,242,242,.99), rgba(242,242,242,.95));
   border-left: 1px solid #f8f8f8;
--- a/browser/themes/osx/browser.css
+++ b/browser/themes/osx/browser.css
@@ -1393,83 +1393,83 @@ toolbar .toolbarbutton-1 > .toolbarbutto
     width: 16px;
   }
 
   #add-share-provider {
     list-style-image: url(chrome://browser/skin/menuPanel-small@2x.png);
     -moz-image-region: rect(0px, 192px, 32px, 160px);
   }
 
-  #loop-call-button > .toolbarbutton-badge-container {
+  #loop-button-throttled > .toolbarbutton-badge-container {
     list-style-image: url("chrome://browser/skin/loop/toolbar@2x.png");
     -moz-image-region: rect(0, 36px, 36px, 0);
   }
 
-  toolbar[brighttext] #loop-call-button > .toolbarbutton-badge-container {
+  toolbar[brighttext] #loop-button-throttled > .toolbarbutton-badge-container {
     list-style-image: url("chrome://browser/skin/loop/toolbar-inverted@2x.png");
   }
 
-  #loop-call-button[state="disabled"] > .toolbarbutton-badge-container,
-  #loop-call-button[disabled="true"] > .toolbarbutton-badge-container {
+  #loop-button-throttled[state="disabled"] > .toolbarbutton-badge-container,
+  #loop-button-throttled[disabled="true"] > .toolbarbutton-badge-container {
     -moz-image-region: rect(0, 72px, 36px, 36px);
   }
 
-  #loop-call-button:not([disabled="true"])[state="error"] > .toolbarbutton-badge-container {
+  #loop-button-throttled:not([disabled="true"])[state="error"] > .toolbarbutton-badge-container {
     -moz-image-region: rect(0, 108px, 36px, 72px);
   }
 
-  #loop-call-button:not([disabled="true"])[state="action"] > .toolbarbutton-badge-container {
+  #loop-button-throttled:not([disabled="true"])[state="action"] > .toolbarbutton-badge-container {
     -moz-image-region: rect(0, 144px, 36px, 108px);
   }
 
-  #loop-call-button:not([disabled="true"])[state="action"]:-moz-any(:hover,:hover:active,[open]) > .toolbarbutton-badge-container {
+  #loop-button-throttled:not([disabled="true"])[state="action"]:-moz-any(:hover,:hover:active,[open]) > .toolbarbutton-badge-container {
     -moz-image-region: rect(0, 180px, 36px, 144px);
   }
 
-  #loop-call-button:not([disabled="true"])[state="active"] > .toolbarbutton-badge-container {
+  #loop-button-throttled:not([disabled="true"])[state="active"] > .toolbarbutton-badge-container {
     -moz-image-region: rect(0, 216px, 36px, 180px);
   }
 
-  #loop-call-button:not([disabled="true"])[state="active"]:-moz-any(:hover,:hover:active,[open]) > .toolbarbutton-badge-container {
+  #loop-button-throttled:not([disabled="true"])[state="active"]:-moz-any(:hover,:hover:active,[open]) > .toolbarbutton-badge-container {
     -moz-image-region: rect(0, 252px, 36px, 216px);
   }
 
-  #loop-call-button[cui-areatype="menu-panel"] > .toolbarbutton-badge-container,
-  toolbarpaletteitem[place="palette"] > #loop-call-button > .toolbarbutton-badge-container {
+  #loop-button-throttled[cui-areatype="menu-panel"] > .toolbarbutton-badge-container,
+  toolbarpaletteitem[place="palette"] > #loop-button-throttled > .toolbarbutton-badge-container {
     list-style-image: url(chrome://browser/skin/loop/menuPanel@2x.png);
     -moz-image-region: rect(0, 64px, 64px, 0);
   }
 
   /* Make sure that the state icons are not shown in the customization palette. */
-  toolbarpaletteitem[place="palette"] > #loop-call-button > .toolbarbutton-badge-container {
+  toolbarpaletteitem[place="palette"] > #loop-button-throttled > .toolbarbutton-badge-container {
     -moz-image-region: rect(0, 64px, 64px, 0) !important;
   }
 
-  #loop-call-button[cui-areatype="menu-panel"][state="disabled"] > .toolbarbutton-badge-container,
-  #loop-call-button[cui-areatype="menu-panel"][disabled="true"] > .toolbarbutton-badge-container {
+  #loop-button-throttled[cui-areatype="menu-panel"][state="disabled"] > .toolbarbutton-badge-container,
+  #loop-button-throttled[cui-areatype="menu-panel"][disabled="true"] > .toolbarbutton-badge-container {
     -moz-image-region: rect(0, 128px, 64px, 64px);
   }
 
-  #loop-call-button[cui-areatype="menu-panel"]:not([disabled="true"])[state="error"] > .toolbarbutton-badge-container {
+  #loop-button-throttled[cui-areatype="menu-panel"]:not([disabled="true"])[state="error"] > .toolbarbutton-badge-container {
     -moz-image-region: rect(0, 192px, 64px, 128px);
   }
 
-  #loop-call-button[cui-areatype="menu-panel"]:not([disabled="true"])[state="action"] > .toolbarbutton-badge-container {
+  #loop-button-throttled[cui-areatype="menu-panel"]:not([disabled="true"])[state="action"] > .toolbarbutton-badge-container {
     -moz-image-region: rect(0, 256px, 64px, 192px);
   }
 
-  #loop-call-button[cui-areatype="menu-panel"]:not([disabled="true"])[state="action"]:-moz-any(:hover,:hover:active,[open]) > .toolbarbutton-badge-container {
+  #loop-button-throttled[cui-areatype="menu-panel"]:not([disabled="true"])[state="action"]:-moz-any(:hover,:hover:active,[open]) > .toolbarbutton-badge-container {
     -moz-image-region: rect(0, 320px, 64px, 256px);
   }
 
-  #loop-call-button[cui-areatype="menu-panel"]:not([disabled="true"])[state="active"] > .toolbarbutton-badge-container {
+  #loop-button-throttled[cui-areatype="menu-panel"]:not([disabled="true"])[state="active"] > .toolbarbutton-badge-container {
     -moz-image-region: rect(0, 384px, 64px, 320px);
   }
 
-  #loop-call-button[cui-areatype="menu-panel"]:not([disabled="true"])[state="active"]:-moz-any(:hover,:hover:active,[open]) > .toolbarbutton-badge-container {
+  #loop-button-throttled[cui-areatype="menu-panel"]:not([disabled="true"])[state="active"]:-moz-any(:hover,:hover:active,[open]) > .toolbarbutton-badge-container {
     -moz-image-region: rect(0, 448px, 64px, 384px);
   }
 }
 
 toolbar .toolbarbutton-1:not([type="menu-button"]),
 toolbar .toolbarbutton-1 > .toolbarbutton-menubutton-button {
   min-width: 28px;
 }
--- a/browser/themes/shared/menupanel.inc.css
+++ b/browser/themes/shared/menupanel.inc.css
@@ -165,49 +165,49 @@ toolbarpaletteitem[place="palette"] > #w
 toolbarpaletteitem[place="palette"] > #webide-button {
   -moz-image-region: rect(0px, 960px, 32px, 928px);
 }
 
 toolbaritem[sdkstylewidget="true"] > toolbarbutton {
   -moz-image-region: rect(0, 832px, 32px, 800px);
 }
 
-#loop-call-button[cui-areatype="menu-panel"] > .toolbarbutton-badge-container,
-toolbarpaletteitem[place="palette"] > #loop-call-button > .toolbarbutton-badge-container {
+#loop-button-throttled[cui-areatype="menu-panel"] > .toolbarbutton-badge-container,
+toolbarpaletteitem[place="palette"] > #loop-button-throttled > .toolbarbutton-badge-container {
   list-style-image: url(chrome://browser/skin/loop/menuPanel.png);
   -moz-image-region: rect(0, 32px, 32px, 0);
 }
 
 /* Make sure that the state icons are not shown in the customization palette. */
-toolbarpaletteitem[place="palette"] > #loop-call-button > .toolbarbutton-badge-container {
+toolbarpaletteitem[place="palette"] > #loop-button-throttled > .toolbarbutton-badge-container {
   -moz-image-region: rect(0, 32px, 32px, 0) !important;
 }
 
-#loop-call-button[cui-areatype="menu-panel"][state="disabled"] > .toolbarbutton-badge-container,
-#loop-call-button[cui-areatype="menu-panel"][disabled="true"] > .toolbarbutton-badge-container {
+#loop-button-throttled[cui-areatype="menu-panel"][state="disabled"] > .toolbarbutton-badge-container,
+#loop-button-throttled[cui-areatype="menu-panel"][disabled="true"] > .toolbarbutton-badge-container {
   -moz-image-region: rect(0, 64px, 32px, 32px);
 }
 
-#loop-call-button[cui-areatype="menu-panel"]:not([disabled="true"])[state="error"] > .toolbarbutton-badge-container {
+#loop-button-throttled[cui-areatype="menu-panel"]:not([disabled="true"])[state="error"] > .toolbarbutton-badge-container {
   -moz-image-region: rect(0, 96px, 32px, 64px);
 }
 
-#loop-call-button[cui-areatype="menu-panel"]:not([disabled="true"])[state="action"] > .toolbarbutton-badge-container {
+#loop-button-throttled[cui-areatype="menu-panel"]:not([disabled="true"])[state="action"] > .toolbarbutton-badge-container {
   -moz-image-region: rect(0, 128px, 32px, 96px);
 }
 
-#loop-call-button[cui-areatype="menu-panel"]:not([disabled="true"])[state="action"]:-moz-any(:hover,:hover:active,[open]) > .toolbarbutton-badge-container {
+#loop-button-throttled[cui-areatype="menu-panel"]:not([disabled="true"])[state="action"]:-moz-any(:hover,:hover:active,[open]) > .toolbarbutton-badge-container {
   -moz-image-region: rect(0, 160px, 32px, 128px);
 }
 
-#loop-call-button[cui-areatype="menu-panel"]:not([disabled="true"])[state="active"] > .toolbarbutton-badge-container {
+#loop-button-throttled[cui-areatype="menu-panel"]:not([disabled="true"])[state="active"] > .toolbarbutton-badge-container {
   -moz-image-region: rect(0, 192px, 32px, 160px);
 }
 
-#loop-call-button[cui-areatype="menu-panel"]:not([disabled="true"])[state="active"]:-moz-any(:hover,:hover:active,[open]) > .toolbarbutton-badge-container {
+#loop-button-throttled[cui-areatype="menu-panel"]:not([disabled="true"])[state="active"]:-moz-any(:hover,:hover:active,[open]) > .toolbarbutton-badge-container {
   -moz-image-region: rect(0, 224px, 32px, 192px);
 }
 
 /* Wide panel control icons */
 
 #edit-controls@inAnyPanel@ > toolbarbutton,
 #zoom-controls@inAnyPanel@ > toolbarbutton,
 toolbarpaletteitem[place="palette"] > #edit-controls > toolbarbutton,
--- a/browser/themes/shared/toolbarbuttons.inc.css
+++ b/browser/themes/shared/toolbarbuttons.inc.css
@@ -195,45 +195,45 @@ toolbar[brighttext] #sync-button[status=
 }
 
 %ifdef XP_MACOSX
 #web-apps-button[cui-areatype="toolbar"]:hover:active:not([disabled="true"]) {
   -moz-image-region: rect(18px, 720px, 36px, 702px);
 }
 %endif
 
-#loop-call-button > .toolbarbutton-badge-container {
+#loop-button-throttled > .toolbarbutton-badge-container {
   list-style-image: url(chrome://browser/skin/loop/toolbar.png);
   -moz-image-region: rect(0, 18px, 18px, 0);
 }
 
-toolbar[brighttext] #loop-call-button > .toolbarbutton-badge-container {
+toolbar[brighttext] #loop-button-throttled > .toolbarbutton-badge-container {
   list-style-image: url(chrome://browser/skin/loop/toolbar-inverted.png);
 }
 
-#loop-call-button[state="disabled"] > .toolbarbutton-badge-container,
-#loop-call-button[disabled="true"] > .toolbarbutton-badge-container {
+#loop-button-throttled[state="disabled"] > .toolbarbutton-badge-container,
+#loop-button-throttled[disabled="true"] > .toolbarbutton-badge-container {
   -moz-image-region: rect(0, 36px, 18px, 18px);
 }
 
-#loop-call-button:not([disabled="true"])[state="error"] > .toolbarbutton-badge-container {
+#loop-button-throttled:not([disabled="true"])[state="error"] > .toolbarbutton-badge-container {
   -moz-image-region: rect(0, 54px, 18px, 36px);
 }
 
-#loop-call-button:not([disabled="true"])[state="action"] > .toolbarbutton-badge-container {
+#loop-button-throttled:not([disabled="true"])[state="action"] > .toolbarbutton-badge-container {
   -moz-image-region: rect(0, 72px, 18px, 54px);
 }
 
-#loop-call-button:not([disabled="true"])[state="action"]:-moz-any(:hover,:hover:active,[open]) > .toolbarbutton-badge-container {
+#loop-button-throttled:not([disabled="true"])[state="action"]:-moz-any(:hover,:hover:active,[open]) > .toolbarbutton-badge-container {
   -moz-image-region: rect(0, 90px, 18px, 72px);
 }
 
-#loop-call-button:not([disabled="true"])[state="active"] > .toolbarbutton-badge-container {
+#loop-button-throttled:not([disabled="true"])[state="active"] > .toolbarbutton-badge-container {
   -moz-image-region: rect(0, 108px, 18px, 90px);
 }
 
-#loop-call-button:not([disabled="true"])[state="active"]:-moz-any(:hover,:hover:active,[open]) > .toolbarbutton-badge-container {
+#loop-button-throttled:not([disabled="true"])[state="active"]:-moz-any(:hover,:hover:active,[open]) > .toolbarbutton-badge-container {
   -moz-image-region: rect(0, 126px, 18px, 108px);
 }
 
 #webide-button[cui-areatype="toolbar"] {
   -moz-image-region: rect(0, 738px, 18px, 720px);
 }
--- a/browser/themes/windows/browser.css
+++ b/browser/themes/windows/browser.css
@@ -569,17 +569,17 @@ menuitem.bookmark-item {
 
 %ifndef WINDOWS_AERO
 @media (-moz-windows-theme: luna-silver) {
   :-moz-any(@primaryToolbarButtons@),
   #bookmarks-menu-button.toolbarbutton-1 > .toolbarbutton-menubutton-dropmarker > .dropmarker-icon {
     list-style-image: url("chrome://browser/skin/Toolbar-lunaSilver.png");
   }
 
-  #loop-call-button > .toolbarbutton-badge-container {
+  #loop-button-throttled > .toolbarbutton-badge-container {
     list-style-image: url(chrome://browser/skin/loop/toolbar-lunaSilver.png)
   }
 }
 %endif
 
 #main-window:not([customizing]) .toolbarbutton-1[disabled=true] > .toolbarbutton-icon,
 #main-window:not([customizing]) .toolbarbutton-1[disabled=true] > .toolbarbutton-menu-dropmarker,
 #main-window:not([customizing]) .toolbarbutton-1[disabled=true] > .toolbarbutton-menubutton-dropmarker,