Merge fx-team to m-c. a=merge
authorRyan VanderMeulen <ryanvm@gmail.com>
Wed, 17 Jun 2015 15:01:24 -0400
changeset 280169 a3f280b6f8d5c1a2894421f92a4e308769de5bea
parent 280153 31d1aea7dc85d06787fc9063c5bc49f0c1e4f8f7 (current diff)
parent 280168 06d8b833c6a2c277d8c69c81f250c05bb2005c71 (diff)
child 280170 df807da6ed3c4ae92ebbd8d3e098cd61f0f7b076
child 280192 6876a553b8982a2d03ed252c6a56e811eb3c05fb
child 280232 b40af190753c5fe1298b2e4ea03b99fdc4b5d42e
push id4932
push userjlund@mozilla.com
push dateMon, 10 Aug 2015 18:23:06 +0000
treeherdermozilla-beta@6dd5a4f5f745 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmerge
milestone41.0a1
first release with
nightly linux32
a3f280b6f8d5 / 41.0a1 / 20150618030206 / files
nightly linux64
a3f280b6f8d5 / 41.0a1 / 20150618030206 / files
nightly mac
a3f280b6f8d5 / 41.0a1 / 20150618030206 / files
nightly win32
a3f280b6f8d5 / 41.0a1 / 20150618030206 / files
nightly win64
a3f280b6f8d5 / 41.0a1 / 20150618030206 / files
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
releases
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Merge fx-team to m-c. a=merge
mobile/android/base/resources/drawable-hdpi/menu_pb.png
mobile/android/base/resources/drawable-mdpi/menu_pb.png
mobile/android/base/resources/drawable-xhdpi/menu_pb.png
mobile/android/base/resources/drawable/menu_level.xml
--- a/b2g/components/test/unit/test_logcapture_gonk.js
+++ b/b2g/components/test/unit/test_logcapture_gonk.js
@@ -28,17 +28,27 @@ add_test(function test_readLogFile() {
 
   run_next_test();
 });
 
 add_test(function test_readProperties() {
   let propertiesLog = LogCapture.readProperties();
   notEqual(propertiesLog, null, "Properties should not be null");
   notEqual(propertiesLog, undefined, "Properties should not be undefined");
-  equal(propertiesLog["ro.kernel.qemu"], "1", "QEMU property should be 1");
+
+  for (let propertyName in propertiesLog) {
+    equal(typeof(propertiesLog[propertyName]), "string",
+          "Property " + propertyName + " should be a string");
+  }
+
+  equal(propertiesLog["ro.product.locale.language"], "en",
+        "Locale language should be read correctly. See bug 1171577.");
+
+  equal(propertiesLog["ro.product.locale.region"], "US",
+        "Locale region should be read correctly. See bug 1171577.");
 
   run_next_test();
 });
 
 add_test(function test_readAppIni() {
   let appIni = LogCapture.readLogFile("/system/b2g/application.ini");
   verifyLog(appIni);
 
--- a/browser/components/loop/.eslintrc
+++ b/browser/components/loop/.eslintrc
@@ -46,17 +46,16 @@
     "no-console": 0,              // Leave as 0. We use console logging in content code.
     "no-empty": 0,                // TODO: Remove (use default)
     "no-extra-bind": 0,           // Leave as 0
     "no-extra-boolean-cast": 0,   // TODO: Remove (use default)
     "no-multi-spaces": 0,         // TBD.
     "no-new": 0,                  // TODO: Remove (use default)
     "no-redeclare": 0,            // TODO: Remove (use default)
     "no-return-assign": 0,        // TODO: Remove (use default)
-    "no-shadow": 0,               // TODO: Remove (use default)
     "no-underscore-dangle": 0,    // Leave as 0. Commonly used for private variables.
     "no-unneeded-ternary": 2,
     "no-unused-expressions": 0,   // TODO: Remove (use default)
     "no-unused-vars": 0,          // TODO: Remove (use default)
     "no-use-before-define": 0,    // TODO: Remove (use default)
     "quotes": [2, "double", "avoid-escape"],
     "strict": 0,                  // [2, "function"],
     // eslint-plugin-react rules. These are documented at
--- a/browser/components/loop/content/js/client.js
+++ b/browser/components/loop/content/js/client.js
@@ -101,19 +101,19 @@ loop.Client = (function($) {
 
           try {
             var postData = JSON.parse(responseText);
 
             var outgoingCallData = this._validate(postData,
               expectedPostCallProperties);
 
             cb(null, outgoingCallData);
-          } catch (err) {
-            console.log("Error requesting call info", err);
-            cb(err);
+          } catch (ex) {
+            console.log("Error requesting call info", ex);
+            cb(ex);
           }
         }.bind(this)
       );
     }
   };
 
   return Client;
 })(jQuery);
--- a/browser/components/loop/content/js/contacts.jsx
+++ b/browser/components/loop/content/js/contacts.jsx
@@ -480,19 +480,19 @@ loop.contacts = (function(_, mozL10n) {
         case "edit":
           this.props.startForm("contacts_edit", contact);
           break;
         case "remove":
           navigator.mozLoop.confirm({
             message: mozL10n.get("confirm_delete_contact_alert"),
             okButton: mozL10n.get("confirm_delete_contact_remove_button"),
             cancelButton: mozL10n.get("confirm_delete_contact_cancel_button")
-          }, (err, result) => {
-            if (err) {
-              throw err;
+          }, (error, result) => {
+            if (error) {
+              throw error;
             }
 
             if (!result) {
               return;
             }
 
             navigator.mozLoop.contacts.remove(contact._guid, err => {
               if (err) {
--- a/browser/components/loop/content/js/panel.js
+++ b/browser/components/loop/content/js/panel.js
@@ -715,16 +715,22 @@ loop.panel = (function(_, mozL10n) {
 
     onDocumentVisible: function() {
       // We would use onDocumentHidden to null out the data ready for the next
       // opening. However, this seems to cause an awkward glitch in the display
       // when opening the panel, and it seems cleaner just to update the data
       // even if there's a small delay.
 
       this.props.mozLoop.getSelectedTabMetadata(function callback(metadata) {
+        // Bail out when the component is not mounted (anymore).
+        // This occurs during test runs. See bug 1174611 for more info.
+        if (!this.isMounted()) {
+          return;
+        }
+
         var previewImage = metadata.favicon || "";
         var description = metadata.title || metadata.description;
         var url = metadata.url;
         this.setState({
           previewImage: previewImage,
           description: description,
           url: url
         });
--- a/browser/components/loop/content/js/panel.jsx
+++ b/browser/components/loop/content/js/panel.jsx
@@ -715,16 +715,22 @@ loop.panel = (function(_, mozL10n) {
 
     onDocumentVisible: function() {
       // We would use onDocumentHidden to null out the data ready for the next
       // opening. However, this seems to cause an awkward glitch in the display
       // when opening the panel, and it seems cleaner just to update the data
       // even if there's a small delay.
 
       this.props.mozLoop.getSelectedTabMetadata(function callback(metadata) {
+        // Bail out when the component is not mounted (anymore).
+        // This occurs during test runs. See bug 1174611 for more info.
+        if (!this.isMounted()) {
+          return;
+        }
+
         var previewImage = metadata.favicon || "";
         var description = metadata.title || metadata.description;
         var url = metadata.url;
         this.setState({
           previewImage: previewImage,
           description: description,
           url: url
         });
--- a/browser/components/loop/content/js/roomStore.js
+++ b/browser/components/loop/content/js/roomStore.js
@@ -518,19 +518,19 @@ loop.store = loop.store || {};
           setTimeout(function() {
             this.dispatchAction(new sharedActions.UpdateRoomContextDone());
           }.bind(this), 0);
           return;
         }
 
         this.setStoreState({error: null});
         this._mozLoop.rooms.update(actionData.roomToken, roomData,
-          function(err, data) {
-            var action = err ?
-              new sharedActions.UpdateRoomContextError({ error: err }) :
+          function(error, data) {
+            var action = error ?
+              new sharedActions.UpdateRoomContextError({ error: error }) :
               new sharedActions.UpdateRoomContextDone();
             this.dispatchAction(action);
           }.bind(this));
       }.bind(this));
     },
 
     /**
      * Handles the updateRoomContextDone action.
--- a/browser/components/loop/content/js/roomViews.jsx
+++ b/browser/components/loop/content/js/roomViews.jsx
@@ -87,19 +87,20 @@ loop.roomViews = (function(mozL10n) {
 
       this.props.dispatcher.dispatch(new sharedActions.AddSocialShareProvider());
     },
 
     handleProviderClick: function(event) {
       event.preventDefault();
 
       var origin = event.currentTarget.dataset.provider;
-      var provider = this.props.socialShareProviders.filter(function(provider) {
-        return provider.origin == origin;
-      })[0];
+      var provider = this.props.socialShareProviders
+                         .filter(function(socialProvider) {
+                           return socialProvider.origin == origin;
+                         })[0];
 
       this.props.dispatcher.dispatch(new sharedActions.ShareRoomUrl({
         provider: provider,
         roomUrl: this.props.roomUrl,
         previews: []
       }));
     },
 
@@ -299,22 +300,22 @@ loop.roomViews = (function(mozL10n) {
         newState.editMode = nextProps.editMode;
         // If we're switching to edit mode, fetch the metadata of the current tab.
         // But _only_ if there's no context currently attached to the room; the
         // checkbox will be disabled in that case.
         if (nextProps.editMode) {
           this.props.mozLoop.getSelectedTabMetadata(function(metadata) {
             var previewImage = metadata.favicon || "";
             var description = metadata.title || metadata.description;
-            var url = metadata.url;
+            var metaUrl = metadata.url;
             this.setState({
               availableContext: {
                 previewImage: previewImage,
                 description: description,
-                url: url
+                url: metaUrl
               }
            });
           }.bind(this));
         }
       }
       // When we receive an update for the `roomData` property, make sure that
       // the current form fields reflect reality. This is necessary, because the
       // form state is maintained in the components' state.
--- a/browser/components/loop/content/shared/js/activeRoomStore.js
+++ b/browser/components/loop/content/shared/js/activeRoomStore.js
@@ -315,17 +315,17 @@ loop.store.ActiveRoomStore = (function()
               .then(function(decryptedResult) {
           var realResult = JSON.parse(decryptedResult);
 
           roomInfoData.description = realResult.description;
           roomInfoData.urls = realResult.urls;
           roomInfoData.roomName = realResult.roomName;
 
           dispatcher.dispatch(roomInfoData);
-        }, function(err) {
+        }, function(error) {
           roomInfoData.roomInfoFailure = ROOM_INFO_FAILURES.DECRYPT_FAILED;
           dispatcher.dispatch(roomInfoData);
         });
       }.bind(this));
     },
 
     /**
      * Handles the setupRoomInfo action. Sets up the initial room data and
--- a/browser/components/loop/content/shared/js/otSdkDriver.js
+++ b/browser/components/loop/content/shared/js/otSdkDriver.js
@@ -675,43 +675,43 @@ loop.OTSdkDriver = (function() {
         if (err) {
           console.error(err);
           return;
         }
 
         this._publisherChannel = channel;
 
         channel.on({
-          close: function(event) {
+          close: function(e) {
             // XXX We probably want to dispatch and handle this somehow.
             console.log("Published data channel closed!");
           }
         });
 
         this._checkDataChannelsAvailable();
       }.bind(this));
 
       this.subscriber._.getDataChannel("text", {}, function(err, channel) {
         // Sends will queue until the channel is fully open.
         if (err) {
           console.error(err);
           return;
         }
 
         channel.on({
-          message: function(event) {
+          message: function(ev) {
             try {
               this.dispatcher.dispatch(
-                new sharedActions.ReceivedTextChatMessage(JSON.parse(event.data)));
+                new sharedActions.ReceivedTextChatMessage(JSON.parse(ev.data)));
             } catch (ex) {
               console.error("Failed to process incoming chat message", ex);
             }
           }.bind(this),
 
-          close: function(event) {
+          close: function(e) {
             // XXX We probably want to dispatch and handle this somehow.
             console.log("Subscribed data channel closed!");
           }
         });
 
         this._subscriberChannel = channel;
         this._checkDataChannelsAvailable();
       }.bind(this));
--- a/browser/components/loop/content/shared/js/views.jsx
+++ b/browser/components/loop/content/shared/js/views.jsx
@@ -386,24 +386,24 @@ loop.shared.views = (function(_, l10n) {
       var outgoing = this.getDOMNode().querySelector(".local");
 
       // XXX move this into its StreamingVideo component?
       this.publisher = this.props.sdk.initPublisher(
         outgoing, this.getDefaultPublisherConfig({publishVideo: this.props.video.enabled}));
 
       // Suppress OT GuM custom dialog, see bug 1018875
       this.listenTo(this.publisher, "accessDialogOpened accessDenied",
-                    function(event) {
-                      event.preventDefault();
+                    function(ev) {
+                      ev.preventDefault();
                     });
 
-      this.listenTo(this.publisher, "streamCreated", function(event) {
+      this.listenTo(this.publisher, "streamCreated", function(ev) {
         this.setState({
-          audio: {enabled: event.stream.hasAudio},
-          video: {enabled: event.stream.hasVideo}
+          audio: {enabled: ev.stream.hasAudio},
+          video: {enabled: ev.stream.hasVideo}
         });
       }.bind(this));
 
       this.listenTo(this.publisher, "streamDestroyed", function() {
         this.setState({
           audio: {enabled: false},
           video: {enabled: false}
         });
--- a/browser/components/loop/modules/LoopContacts.jsm
+++ b/browser/components/loop/modules/LoopContacts.jsm
@@ -421,19 +421,19 @@ let LoopContactsInternal = Object.freeze
    */
   remove: function(guid, callback) {
     this.get(guid, (err, contact) => {
       if (err) {
         callback(err);
         return;
       }
 
-      LoopStorage.getStore(kObjectStoreName, (err, store) => {
-        if (err) {
-          callback(err);
+      LoopStorage.getStore(kObjectStoreName, (error, store) => {
+        if (error) {
+          callback(error);
           return;
         }
 
         let request;
         try {
           request = store.delete(guid);
         } catch (ex) {
           callback(ex);
@@ -679,19 +679,19 @@ let LoopContactsInternal = Object.freeze
       }
 
       if (!contact) {
         callback(new Error("Contact with " + kKeyPath + " '" +
                            guid + "' could not be found"));
         return;
       }
 
-      LoopStorage.getStore(kObjectStoreName, (err, store) => {
-        if (err) {
-          callback(err);
+      LoopStorage.getStore(kObjectStoreName, (error, store) => {
+        if (error) {
+          callback(error);
           return;
         }
 
         let previous = extend({}, contact);
         // Update the contact with properties provided by `details`.
         extend(contact, details);
 
         details._date_lch = Date.now();
--- a/browser/components/loop/modules/MozLoopAPI.jsm
+++ b/browser/components/loop/modules/MozLoopAPI.jsm
@@ -119,16 +119,35 @@ const cloneValueInto = function(value, t
     MozLoopService.log.debug("Failed to clone value:", value);
     throw ex;
   }
 
   return clone;
 };
 
 /**
+ * Guarded callback invocation that reports when a callback function doesn't
+ * exist anymore.
+ *
+ * @param {Function} callback Callback function to be invoked.
+ * @param {...mixed} args     Rest param of callback function arguments.
+ */
+const invokeCallback = function(callback, ...args) {
+  if (typeof callback != "function") {
+    // We log an error, because it will have a stack trace attached which will
+    // be helpful whilst debugging.
+    MozLoopService.log.error.apply(MozLoopService.log,
+      [new Error("Callback function was lost!"), ...args]);
+    return;
+  }
+
+  return callback.apply(null, args);
+};
+
+/**
  * Get the two-digit hexadecimal code for a byte
  *
  * @param {byte} charCode
  */
 const toHexString = function(charCode) {
   return ("0" + charCode.toString(16)).slice(-2);
 };
 
@@ -429,17 +448,17 @@ function injectLoopAPI(targetWindow) {
      *                            `Error` object or `null`. The second argument will
      *                            be the result of the operation, if successfull.
      */
     startImport: {
       enumerable: true,
       writable: true,
       value: function(options, callback) {
         LoopContacts.startImport(options, getChromeWindow(targetWindow), function(...results) {
-          callback(...[cloneValueInto(r, targetWindow) for (r of results)]);
+          invokeCallback(callback, ...[cloneValueInto(r, targetWindow) for (r of results)]);
         });
       }
     },
 
     /**
      * Returns translated strings associated with an element. Designed
      * for use with l10n.js
      *
@@ -489,27 +508,27 @@ function injectLoopAPI(targetWindow) {
         let buttonFlags;
         if (options.okButton && options.cancelButton) {
           buttonFlags =
             (Ci.nsIPrompt.BUTTON_POS_0 * Ci.nsIPrompt.BUTTON_TITLE_IS_STRING) +
             (Ci.nsIPrompt.BUTTON_POS_1 * Ci.nsIPrompt.BUTTON_TITLE_IS_STRING);
         } else if (!options.okButton && !options.cancelButton) {
           buttonFlags = Services.prompt.STD_YES_NO_BUTTONS;
         } else {
-          callback(cloneValueInto(new Error("confirm: missing button options"), targetWindow));
+          invokeCallback(callback, cloneValueInto(new Error("confirm: missing button options"), targetWindow));
         }
 
         try {
           let chosenButton = Services.prompt.confirmEx(null, "",
             options.message, buttonFlags, options.okButton, options.cancelButton,
             null, null, {});
 
-          callback(null, chosenButton == 0);
+          invokeCallback(callback, null, chosenButton == 0);
         } catch (ex) {
-          callback(cloneValueInto(ex, targetWindow));
+          invokeCallback(callback, cloneValueInto(ex, targetWindow));
         }
       }
     },
 
     /**
      * Set any preference under "loop."
      *
      * @param {String} prefName The name of the pref without the preceding "loop."
@@ -612,30 +631,23 @@ function injectLoopAPI(targetWindow) {
      *                            transmitted with the request.
      * @param {Function} callback Called when the request completes.
      */
     hawkRequest: {
       enumerable: true,
       writable: true,
       value: function(sessionType, path, method, payloadObj, callback) {
         // XXX Should really return a DOM promise here.
-        let callbackIsFunction = (typeof callback == "function");
         MozLoopService.hawkRequest(sessionType, path, method, payloadObj).then((response) => {
-          callback(null, response.body);
+          invokeCallback(callback, null, response.body);
         }, hawkError => {
-          // When the function was garbage collected due to async events, like
-          // closing a window, we want to circumvent a JS error.
-          if (callbackIsFunction && typeof callback != "function") {
-            MozLoopService.log.error("hawkRequest: callback function was lost.", hawkError);
-            return;
-          }
           // The hawkError.error property, while usually a string representing
           // an HTTP response status message, may also incorrectly be a native
           // error object that will cause the cloning function to fail.
-          callback(Cu.cloneInto({
+          invokeCallback(callback, Cu.cloneInto({
             error: (hawkError.error && typeof hawkError.error == "string")
                    ? hawkError.error : "Unexpected exception",
             message: hawkError.message,
             code: hawkError.code,
             errno: hawkError.errno,
           }, targetWindow));
         }).catch(Cu.reportError);
       }
@@ -838,22 +850,22 @@ function injectLoopAPI(targetWindow) {
                         .createInstance(Ci.nsIXMLHttpRequest);
         let url = `chrome://browser/content/loop/shared/sounds/${name}.ogg`;
 
         request.open("GET", url, true);
         request.responseType = "arraybuffer";
         request.onload = () => {
           if (request.status < 200 || request.status >= 300) {
             let error = new Error(request.status + " " + request.statusText);
-            callback(cloneValueInto(error, targetWindow));
+            invokeCallback(callback, cloneValueInto(error, targetWindow));
             return;
           }
 
           let blob = new Blob([request.response], {type: "audio/ogg"});
-          callback(null, cloneValueInto(blob, targetWindow));
+          invokeCallback(callback, null, cloneValueInto(blob, targetWindow));
         };
 
         request.send();
       }
     },
 
     /**
      * Compose a URL pointing to the location of an avatar by email address.
@@ -907,17 +919,17 @@ function injectLoopAPI(targetWindow) {
           win.LoopUI.getFavicon(function(err, favicon) {
             if (err) {
               MozLoopService.log.error("Error occurred whilst fetching favicon", err);
               // We don't return here intentionally to make sure the callback is
               // invoked at all times. We just report the error here.
             }
             pageData.favicon = favicon || null;
 
-            callback(cloneValueInto(pageData, targetWindow));
+            invokeCallback(callback, cloneValueInto(pageData, targetWindow));
           });
         });
         win.gBrowser.selectedBrowser.messageManager.sendAsyncMessage("PageMetadata:GetPageData");
       }
     },
 
     /**
      * Associates a session-id and a call-id with a window for debugging.
--- a/browser/components/loop/modules/MozLoopService.jsm
+++ b/browser/components/loop/modules/MozLoopService.jsm
@@ -352,18 +352,18 @@ let MozLoopServiceInternal = {
    *                   with this channel from the PushServer.
    * @returns {Promise} A promise that is resolved with no params on completion, or
    *                    rejected with an error code or string.
    */
   createNotificationChannel: function(channelID, sessionType, serviceType, onNotification) {
     log.debug("createNotificationChannel", channelID, sessionType, serviceType);
     // Wrap the push notification registration callback in a Promise.
     return new Promise((resolve, reject) => {
-      let onRegistered = (error, pushURL, channelID) => {
-        log.debug("createNotificationChannel onRegistered:", error, pushURL, channelID);
+      let onRegistered = (error, pushURL, chID) => {
+        log.debug("createNotificationChannel onRegistered:", error, pushURL, chID);
         if (error) {
           reject(Error(error));
         } else {
           resolve(this.registerWithLoopServer(sessionType, serviceType, pushURL));
         }
       };
 
       this.pushHandler.register(channelID, onRegistered, onNotification);
@@ -506,36 +506,36 @@ let MozLoopServiceInternal = {
     }
 
     let error,
         pushURLs = this.pushURLs.get(sessionType),
         callsPushURL = pushURLs ? pushURLs.calls : null,
         roomsPushURL = pushURLs ? pushURLs.rooms : null;
     this.pushURLs.delete(sessionType);
 
-    let unregister = (sessionType, pushURL) => {
+    let unregister = (sessType, pushURL) => {
       if (!pushURL) {
         return Promise.resolve("no pushURL of this type to unregister");
       }
 
       let unregisterURL = "/registration?simplePushURL=" + encodeURIComponent(pushURL);
-      return this.hawkRequestInternal(sessionType, unregisterURL, "DELETE").then(
+      return this.hawkRequestInternal(sessType, unregisterURL, "DELETE").then(
         () => {
-          log.debug("Successfully unregistered from server for sessionType = ", sessionType);
-          return "unregistered sessionType " + sessionType;
+          log.debug("Successfully unregistered from server for sessionType = ", sessType);
+          return "unregistered sessionType " + sessType;
         },
-        error => {
-          if (error.code === 401) {
+        err => {
+          if (err.code === 401) {
             // Authorization failed, invalid token. This is fine since it may mean we already logged out.
-            log.debug("already unregistered - invalid token", sessionType);
-            return "already unregistered, sessionType = " + sessionType;
+            log.debug("already unregistered - invalid token", sessType);
+            return "already unregistered, sessionType = " + sessType;
           }
 
           log.error("Failed to unregister with the loop server. Error: ", error);
-          throw error;
+          throw err;
         });
     };
 
     return Promise.all([unregister(sessionType, callsPushURL), unregister(sessionType, roomsPushURL)]);
   },
 
   /**
    * Performs a hawk based request to the loop server - there is no pre-registration
@@ -880,20 +880,20 @@ let MozLoopServiceInternal = {
         window.addEventListener("socialFrameAttached", socialFrameChanged.bind(null, "Loop:ChatWindowAttached"));
         window.addEventListener("unload", socialFrameChanged.bind(null, "Loop:ChatWindowClosed"));
 
         const kSizeMap = {
           LoopChatEnabled: "loopChatEnabled",
           LoopChatMessageAppended: "loopChatMessageAppended"
         };
 
-        function onChatEvent(event) {
+        function onChatEvent(ev) {
           // When the chat box or messages are shown, resize the panel or window
           // to be slightly higher to accomodate them.
-          let customSize = kSizeMap[event.type];
+          let customSize = kSizeMap[ev.type];
           if (customSize) {
             chatbox.setAttribute("customSize", customSize);
             chatbox.parentNode.setAttribute("customSize", customSize);
           }
         }
 
         window.addEventListener("LoopChatEnabled", onChatEvent);
         window.addEventListener("LoopChatMessageAppended", onChatEvent);
@@ -904,18 +904,18 @@ let MozLoopServiceInternal = {
             .getInterface(Ci.nsIDOMWindowUtils).currentInnerWindowID;
 
         let onPCLifecycleChange = (pc, winID, type) => {
           if (winID != ourID) {
             return;
           }
 
           // Chat Window Id, this is different that the internal winId
-          let windowId = window.location.hash.slice(1);
-          var context = this.conversationContexts.get(windowId);
+          let chatWindowId = window.location.hash.slice(1);
+          var context = this.conversationContexts.get(chatWindowId);
           var exists = pc.id.match(/session=(\S+)/);
           if (context && !exists) {
             // Not ideal but insert our data amidst existing data like this:
             // - 000 (id=00 url=http)
             // + 000 (session=000 call=000 id=00 url=http)
             var pair = pc.id.split("(");  //)
             if (pair.length == 2) {
               pc.id = pair[0] + "(session=" + context.sessionId +
@@ -937,26 +937,27 @@ let MozLoopServiceInternal = {
 
         let pc_static = new window.mozRTCPeerConnectionStatic();
         pc_static.registerPeerConnectionLifecycleCallback(onPCLifecycleChange);
 
         UITour.notify("Loop:ChatWindowOpened");
       }.bind(this), true);
     };
 
-    let chatbox = Chat.open(null, origin, "", url, undefined, undefined, callback);
-    if (!chatbox) {
+    let chatboxInstance = Chat.open(null, origin, "", url, undefined, undefined,
+                                    callback);
+    if (!chatboxInstance) {
       return null;
     // It's common for unit tests to overload Chat.open.
-    } else if (chatbox.setAttribute) {
+    } else if (chatboxInstance.setAttribute) {
       // Set properties that influence visual appeara nce of the chatbox right
       // away to circumvent glitches.
-      chatbox.setAttribute("dark", true);
-      chatbox.setAttribute("customSize", "loopDefault");
-      chatbox.parentNode.setAttribute("customSize", "loopDefault");
+      chatboxInstance.setAttribute("dark", true);
+      chatboxInstance.setAttribute("customSize", "loopDefault");
+      chatboxInstance.parentNode.setAttribute("customSize", "loopDefault");
     }
     return windowId;
   },
 
   /**
    * Fetch Firefox Accounts (FxA) OAuth parameters from the Loop Server.
    *
    * @return {Promise} resolved with the body of the hawk request for OAuth parameters.
@@ -1227,20 +1228,20 @@ this.MozLoopService = {
     if (!this.getLoopPref("gettingStarted.resumeOnFirstJoin")) {
       return;
     }
 
     if (!room.participants) {
       return;
     }
 
-    // The particpant that joined isn't necessarily included in room.participants (depending on
+    // The participant that joined isn't necessarily included in room.participants (depending on
     // when the broadcast happens) so concatenate.
-    for (let participant of room.participants.concat(participant)) {
-      if (participant.owner) {
+    for (let roomParticipant of room.participants.concat(participant)) {
+      if (roomParticipant.owner) {
         isOwnerInRoom = true;
       } else {
         isOtherInRoom = true;
       }
     }
 
     if (!isOwnerInRoom || !isOtherInRoom) {
       return;
--- a/browser/components/loop/standalone/content/js/standaloneAppStore.js
+++ b/browser/components/loop/standalone/content/js/standaloneAppStore.js
@@ -16,17 +16,18 @@ loop.store.StandaloneAppStore = (functio
   var sharedUtils = loop.shared.utils;
 
   var CALL_REGEXP = /\/c\/([\w\-]+)$/;
   var ROOM_REGEXP = /\/([\w\-]+)$/;
 
   /**
    * Constructor
    *
-   * @param {Object} options Options for the store. Should contain the dispatcher.
+   * @param {Object} options Options for the store. Should contain the
+   *                         dispatcher.
    */
   var StandaloneAppStore = function(options) {
     if (!options.dispatcher) {
       throw new Error("Missing option dispatcher");
     }
     if (!options.sdk) {
       throw new Error("Missing option sdk");
     }
@@ -64,19 +65,19 @@ loop.store.StandaloneAppStore = (functio
       this.trigger("change");
     },
 
     _extractWindowDataFromPath: function(windowPath) {
       var match;
       var windowType = "home";
 
       function extractId(path, regexp) {
-        var match = path.match(regexp);
-        if (match && match[1]) {
-          return match;
+        var pathMatch = path.match(regexp);
+        if (pathMatch && pathMatch[1]) {
+          return pathMatch;
         }
         return null;
       }
 
       if (windowPath) {
         match = extractId(windowPath, CALL_REGEXP);
 
         if (match) {
@@ -136,18 +137,18 @@ loop.store.StandaloneAppStore = (functio
       }
 
       this.setStoreState({
         windowType: windowType,
         isFirefox: sharedUtils.isFirefox(navigator.userAgent),
         unsupportedPlatform: unsupportedPlatform
       });
 
-      // If we've not got a window ID, don't dispatch the action, as we don't need
-      // it.
+      // If we've not got a window ID, don't dispatch the action, as we don't
+      // need it.
       if (token) {
         this._dispatcher.dispatch(new loop.shared.actions.FetchServerData({
           cryptoKey: this._extractCryptoKey(actionData.windowHash),
           token: token,
           windowType: windowType
         }));
       }
     }
--- a/browser/components/loop/test/desktop-local/conversationViews_test.js
+++ b/browser/components/loop/test/desktop-local/conversationViews_test.js
@@ -19,18 +19,18 @@ describe("loop.conversationViews", funct
   var WEBSOCKET_REASONS = loop.shared.utils.WEBSOCKET_REASONS;
 
   // XXX refactor to Just Work with "sandbox.stubComponent" or else
   // just pass in the sandbox and put somewhere generally usable
 
   function stubComponent(obj, component, mockTagName){
     var reactClass = React.createClass({
       render: function() {
-        var mockTagName = mockTagName || "div";
-        return React.DOM[mockTagName](null, this.props.children);
+        var tagName = mockTagName || "div";
+        return React.DOM[tagName](null, this.props.children);
       }
     });
     return sandbox.stub(obj, component, reactClass);
   }
 
   beforeEach(function() {
     sandbox = sinon.sandbox.create();
     sandbox.useFakeTimers();
@@ -265,17 +265,17 @@ describe("loop.conversationViews", funct
         sinon.assert.calledWithMatch(dispatcher.dispatch,
           sinon.match.hasOwn("name", "cancelCall"));
       });
   });
 
   describe("CallFailedView", function() {
     var fakeAudio;
 
-    var contact = {email: [{value: "test@test.tld"}]};
+    var fakeContact = {email: [{value: "test@test.tld"}]};
 
     function mountTestComponent(options) {
       options = options || {};
       return TestUtils.renderIntoDocument(
         React.createElement(loop.conversationViews.CallFailedView, {
           dispatcher: dispatcher,
           contact: options.contact
         }));
@@ -287,43 +287,43 @@ describe("loop.conversationViews", funct
         pause: sinon.spy(),
         removeAttribute: sinon.spy()
       };
       sandbox.stub(window, "Audio").returns(fakeAudio);
     });
 
     it("should dispatch a retryCall action when the retry button is pressed",
       function() {
-        view = mountTestComponent({contact: contact});
+        view = mountTestComponent({contact: fakeContact});
 
         var retryBtn = view.getDOMNode().querySelector(".btn-retry");
 
         React.addons.TestUtils.Simulate.click(retryBtn);
 
         sinon.assert.calledOnce(dispatcher.dispatch);
         sinon.assert.calledWithMatch(dispatcher.dispatch,
           sinon.match.hasOwn("name", "retryCall"));
       });
 
     it("should dispatch a cancelCall action when the cancel button is pressed",
       function() {
-        view = mountTestComponent({contact: contact});
+        view = mountTestComponent({contact: fakeContact});
 
         var cancelBtn = view.getDOMNode().querySelector(".btn-cancel");
 
         React.addons.TestUtils.Simulate.click(cancelBtn);
 
         sinon.assert.calledOnce(dispatcher.dispatch);
         sinon.assert.calledWithMatch(dispatcher.dispatch,
           sinon.match.hasOwn("name", "cancelCall"));
       });
 
     it("should dispatch a fetchRoomEmailLink action when the email button is pressed",
       function() {
-        view = mountTestComponent({contact: contact});
+        view = mountTestComponent({contact: fakeContact});
 
         var emailLinkBtn = view.getDOMNode().querySelector(".btn-email");
 
         React.addons.TestUtils.Simulate.click(emailLinkBtn);
 
         sinon.assert.calledOnce(dispatcher.dispatch);
         sinon.assert.calledWithMatch(dispatcher.dispatch,
           sinon.match.hasOwn("name", "fetchRoomEmailLink"));
@@ -346,127 +346,130 @@ describe("loop.conversationViews", funct
 
         sinon.assert.calledOnce(dispatcher.dispatch);
         sinon.assert.calledWithMatch(dispatcher.dispatch,
           sinon.match.hasOwn("roomName", "Mr Fake ContactName"));
       });
 
     it("should disable the email link button once the action is dispatched",
       function() {
-        view = mountTestComponent({contact: contact});
+        view = mountTestComponent({contact: fakeContact});
         var emailLinkBtn = view.getDOMNode().querySelector(".btn-email");
         React.addons.TestUtils.Simulate.click(emailLinkBtn);
 
         expect(view.getDOMNode().querySelector(".btn-email").disabled).eql(true);
       });
 
     it("should compose an email once the email link is received", function() {
       var composeCallUrlEmail = sandbox.stub(sharedUtils, "composeCallUrlEmail");
-      view = mountTestComponent({contact: contact});
+      view = mountTestComponent({contact: fakeContact});
       conversationStore.setStoreState({emailLink: "http://fake.invalid/"});
 
       sinon.assert.calledOnce(composeCallUrlEmail);
       sinon.assert.calledWithExactly(composeCallUrlEmail,
         "http://fake.invalid/", "test@test.tld");
     });
 
     it("should close the conversation window once the email link is received",
       function() {
-        view = mountTestComponent({contact: contact});
+        view = mountTestComponent({contact: fakeContact});
 
         conversationStore.setStoreState({emailLink: "http://fake.invalid/"});
 
         sinon.assert.calledOnce(fakeWindow.close);
       });
 
     it("should display an error message in case email link retrieval failed",
       function() {
-        view = mountTestComponent({contact: contact});
+        view = mountTestComponent({contact: fakeContact});
 
         conversationStore.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({contact: contact});
+        view = mountTestComponent({contact: fakeContact});
 
         conversationStore.trigger("error:emailLink");
 
         expect(view.getDOMNode().querySelector(".btn-email").disabled).eql(false);
       });
 
     it("should play a failure sound, once", function() {
-      view = mountTestComponent({contact: contact});
+      view = mountTestComponent({contact: fakeContact});
 
       sinon.assert.calledOnce(navigator.mozLoop.getAudioBlob);
       sinon.assert.calledWithExactly(navigator.mozLoop.getAudioBlob,
                                      "failure", sinon.match.func);
       sinon.assert.calledOnce(fakeAudio.play);
       expect(fakeAudio.loop).to.equal(false);
     });
 
     it("should show 'something went wrong' when the reason is WEBSOCKET_REASONS.MEDIA_FAIL",
       function () {
         conversationStore.setStoreState({callStateReason: WEBSOCKET_REASONS.MEDIA_FAIL});
 
-        view = mountTestComponent({contact: contact});
+        view = mountTestComponent({contact: fakeContact});
 
         sinon.assert.calledWith(document.mozL10n.get, "generic_failure_title");
       });
 
     it("should show 'contact unavailable' when the reason is WEBSOCKET_REASONS.REJECT",
       function () {
         conversationStore.setStoreState({callStateReason: WEBSOCKET_REASONS.REJECT});
 
-        view = mountTestComponent({contact: contact});
+        view = mountTestComponent({contact: fakeContact});
 
         sinon.assert.calledWithExactly(document.mozL10n.get,
           "contact_unavailable_title",
-          {contactName: loop.conversationViews._getContactDisplayName(contact)});
+          {contactName: loop.conversationViews
+                            ._getContactDisplayName(fakeContact)});
       });
 
     it("should show 'contact unavailable' when the reason is WEBSOCKET_REASONS.BUSY",
       function () {
         conversationStore.setStoreState({callStateReason: WEBSOCKET_REASONS.BUSY});
 
-        view = mountTestComponent({contact: contact});
+        view = mountTestComponent({contact: fakeContact});
 
         sinon.assert.calledWithExactly(document.mozL10n.get,
           "contact_unavailable_title",
-          {contactName: loop.conversationViews._getContactDisplayName(contact)});
+          {contactName: loop.conversationViews
+                            ._getContactDisplayName(fakeContact)});
       });
 
     it("should show 'something went wrong' when the reason is 'setup'",
       function () {
         conversationStore.setStoreState({callStateReason: "setup"});
 
-        view = mountTestComponent({contact: contact});
+        view = mountTestComponent({contact: fakeContact});
 
         sinon.assert.calledWithExactly(document.mozL10n.get,
           "generic_failure_title");
       });
 
     it("should show 'contact unavailable' when the reason is REST_ERRNOS.USER_UNAVAILABLE",
       function () {
         conversationStore.setStoreState({callStateReason: REST_ERRNOS.USER_UNAVAILABLE});
 
-        view = mountTestComponent({contact: contact});
+        view = mountTestComponent({contact: fakeContact});
 
         sinon.assert.calledWithExactly(document.mozL10n.get,
           "contact_unavailable_title",
-          {contactName: loop.conversationViews._getContactDisplayName(contact)});
+          {contactName: loop.conversationViews
+                            ._getContactDisplayName(fakeContact)});
       });
 
     it("should show 'no media' when the reason is FAILURE_DETAILS.UNABLE_TO_PUBLISH_MEDIA",
       function () {
         conversationStore.setStoreState({callStateReason: FAILURE_DETAILS.UNABLE_TO_PUBLISH_MEDIA});
 
-        view = mountTestComponent({contact: contact});
+        view = mountTestComponent({contact: fakeContact});
 
         sinon.assert.calledWithExactly(document.mozL10n.get, "no_media_failure_message");
       });
 
     it("should display a generic contact unavailable msg when the reason is" +
        " WEBSOCKET_REASONS.BUSY and no display name is available", function() {
         conversationStore.setStoreState({callStateReason: WEBSOCKET_REASONS.BUSY});
         var phoneOnlyContact = {
@@ -760,185 +763,185 @@ describe("loop.conversationViews", funct
         conversationStore.setStoreState({callState: CALL_STATES.TERMINATED});
 
         TestUtils.findRenderedComponentWithType(view,
           loop.conversationViews.CallFailedView);
     });
   });
 
   describe("AcceptCallView", function() {
-    var view;
+    var callView;
 
     function mountTestComponent(extraProps) {
       var props = _.extend({dispatcher: dispatcher, mozLoop: fakeMozLoop}, extraProps);
       return TestUtils.renderIntoDocument(
         React.createElement(loop.conversationViews.AcceptCallView, props));
     }
 
     afterEach(function() {
-      view = null;
+      callView = null;
     });
 
     it("should start alerting on display", function() {
-      view = mountTestComponent({
+      callView = mountTestComponent({
         callType: CALL_TYPES.AUDIO_VIDEO,
         callerId: "fake@invalid.com"
       });
 
       sinon.assert.calledOnce(fakeMozLoop.startAlerting);
     });
 
     it("should stop alerting when removed from the display", function() {
-      view = mountTestComponent({
+      callView = mountTestComponent({
         callType: CALL_TYPES.AUDIO_VIDEO,
         callerId: "fake@invalid.com"
       });
 
-      view.componentWillUnmount();
+      callView.componentWillUnmount();
 
       sinon.assert.calledOnce(fakeMozLoop.stopAlerting);
     });
 
     describe("default answer mode", function() {
       it("should display video as primary answer mode", function() {
-        view = mountTestComponent({
+        callView = mountTestComponent({
           callType: CALL_TYPES.AUDIO_VIDEO,
           callerId: "fake@invalid.com"
         });
 
-        var primaryBtn = view.getDOMNode()
-                                  .querySelector(".fx-embedded-btn-icon-video");
+        var primaryBtn = callView.getDOMNode()
+                                 .querySelector(".fx-embedded-btn-icon-video");
 
         expect(primaryBtn).not.to.eql(null);
       });
 
       it("should display audio as primary answer mode", function() {
-        view = mountTestComponent({
+        callView = mountTestComponent({
           callType: CALL_TYPES.AUDIO_ONLY,
           callerId: "fake@invalid.com"
         });
 
-        var primaryBtn = view.getDOMNode()
-                                  .querySelector(".fx-embedded-btn-icon-audio");
+        var primaryBtn = callView.getDOMNode()
+                                 .querySelector(".fx-embedded-btn-icon-audio");
 
         expect(primaryBtn).not.to.eql(null);
       });
 
       it("should accept call with video", function() {
-        view = mountTestComponent({
+        callView = mountTestComponent({
           callType: CALL_TYPES.AUDIO_VIDEO,
           callerId: "fake@invalid.com"
         });
 
-        var primaryBtn = view.getDOMNode()
-                                  .querySelector(".fx-embedded-btn-icon-video");
+        var primaryBtn = callView.getDOMNode()
+                                 .querySelector(".fx-embedded-btn-icon-video");
 
         React.addons.TestUtils.Simulate.click(primaryBtn);
 
         sinon.assert.calledOnce(dispatcher.dispatch);
         sinon.assert.calledWithExactly(dispatcher.dispatch,
           new sharedActions.AcceptCall({
             callType: CALL_TYPES.AUDIO_VIDEO
           }));
       });
 
       it("should accept call with audio", function() {
-        view = mountTestComponent({
+        callView = mountTestComponent({
           callType: CALL_TYPES.AUDIO_ONLY,
           callerId: "fake@invalid.com"
         });
 
-        var primaryBtn = view.getDOMNode()
-                                  .querySelector(".fx-embedded-btn-icon-audio");
+        var primaryBtn = callView.getDOMNode()
+                                 .querySelector(".fx-embedded-btn-icon-audio");
 
         React.addons.TestUtils.Simulate.click(primaryBtn);
 
         sinon.assert.calledOnce(dispatcher.dispatch);
         sinon.assert.calledWithExactly(dispatcher.dispatch,
           new sharedActions.AcceptCall({
             callType: CALL_TYPES.AUDIO_ONLY
           }));
       });
 
       it("should accept call with video when clicking on secondary btn",
         function() {
-          view = mountTestComponent({
+          callView = mountTestComponent({
             callType: CALL_TYPES.AUDIO_ONLY,
             callerId: "fake@invalid.com"
           });
 
-          var secondaryBtn = view.getDOMNode()
+          var secondaryBtn = callView.getDOMNode()
           .querySelector(".fx-embedded-btn-video-small");
 
           React.addons.TestUtils.Simulate.click(secondaryBtn);
 
           sinon.assert.calledOnce(dispatcher.dispatch);
           sinon.assert.calledWithExactly(dispatcher.dispatch,
             new sharedActions.AcceptCall({
               callType: CALL_TYPES.AUDIO_VIDEO
             }));
         });
 
       it("should accept call with audio when clicking on secondary btn",
         function() {
-          view = mountTestComponent({
+          callView = mountTestComponent({
             callType: CALL_TYPES.AUDIO_VIDEO,
             callerId: "fake@invalid.com"
           });
 
-          var secondaryBtn = view.getDOMNode()
+          var secondaryBtn = callView.getDOMNode()
           .querySelector(".fx-embedded-btn-audio-small");
 
           React.addons.TestUtils.Simulate.click(secondaryBtn);
 
           sinon.assert.calledOnce(dispatcher.dispatch);
           sinon.assert.calledWithExactly(dispatcher.dispatch,
             new sharedActions.AcceptCall({
               callType: CALL_TYPES.AUDIO_ONLY
             }));
         });
     });
 
     describe("click event on .btn-decline", function() {
       it("should dispatch a DeclineCall action", function() {
-        view = mountTestComponent({
+        callView = mountTestComponent({
           callType: CALL_TYPES.AUDIO_VIDEO,
           callerId: "fake@invalid.com"
         });
 
-        var buttonDecline = view.getDOMNode().querySelector(".btn-decline");
+        var buttonDecline = callView.getDOMNode().querySelector(".btn-decline");
 
         TestUtils.Simulate.click(buttonDecline);
 
         sinon.assert.calledOnce(dispatcher.dispatch);
         sinon.assert.calledWithExactly(dispatcher.dispatch,
           new sharedActions.DeclineCall({blockCaller: false}));
       });
     });
 
     describe("click event on .btn-block", function() {
       it("should dispatch a DeclineCall action with blockCaller true", function() {
-        view = mountTestComponent({
+        callView = mountTestComponent({
           callType: CALL_TYPES.AUDIO_VIDEO,
           callerId: "fake@invalid.com"
         });
 
-        var buttonBlock = view.getDOMNode().querySelector(".btn-block");
+        var buttonBlock = callView.getDOMNode().querySelector(".btn-block");
 
         TestUtils.Simulate.click(buttonBlock);
 
         sinon.assert.calledOnce(dispatcher.dispatch);
         sinon.assert.calledWithExactly(dispatcher.dispatch,
           new sharedActions.DeclineCall({blockCaller: true}));
       });
     });
   });
 
   describe("GenericFailureView", function() {
-    var view, fakeAudio;
+    var callView, fakeAudio;
 
     function mountTestComponent(props) {
       return TestUtils.renderIntoDocument(
         React.createElement(loop.conversationViews.GenericFailureView, props));
     }
 
     beforeEach(function() {
       fakeAudio = {
@@ -946,48 +949,53 @@ describe("loop.conversationViews", funct
         pause: sinon.spy(),
         removeAttribute: sinon.spy()
       };
       navigator.mozLoop.doNotDisturb = false;
       sandbox.stub(window, "Audio").returns(fakeAudio);
     });
 
     it("should play a failure sound, once", function() {
-      view = mountTestComponent({cancelCall: function() {}});
+      callView = mountTestComponent({cancelCall: function() {}});
 
       sinon.assert.calledOnce(navigator.mozLoop.getAudioBlob);
       sinon.assert.calledWithExactly(navigator.mozLoop.getAudioBlob,
                                      "failure", sinon.match.func);
       sinon.assert.calledOnce(fakeAudio.play);
       expect(fakeAudio.loop).to.equal(false);
     });
 
     it("should set the title to generic_failure_title", function() {
-      view = mountTestComponent({cancelCall: function() {}});
+      callView = mountTestComponent({cancelCall: function() {}});
 
       expect(fakeWindow.document.title).eql("generic_failure_title");
     });
 
-    it("should show 'no media' for FAILURE_DETAILS.UNABLE_TO_PUBLISH_MEDIA reason", function() {
-      view = mountTestComponent({
-        cancelCall: function() {},
-        failureReason: FAILURE_DETAILS.UNABLE_TO_PUBLISH_MEDIA
-      });
+    it("should show 'no media' for FAILURE_DETAILS.UNABLE_TO_PUBLISH_MEDIA reason",
+       function() {
+         callView = mountTestComponent({
+           cancelCall: function() {},
+           failureReason: FAILURE_DETAILS.UNABLE_TO_PUBLISH_MEDIA
+         });
 
-      expect(view.getDOMNode().querySelector("h2").textContent).eql("no_media_failure_message");
-    });
+         expect(callView.getDOMNode().querySelector("h2").textContent)
+         .eql("no_media_failure_message");
+     });
 
     it("should show 'no media' for FAILURE_DETAILS.NO_MEDIA reason", function() {
-      view = mountTestComponent({
+      callView = mountTestComponent({
         cancelCall: function() {},
         failureReason: FAILURE_DETAILS.NO_MEDIA
       });
 
-      expect(view.getDOMNode().querySelector("h2").textContent).eql("no_media_failure_message");
+      expect(callView.getDOMNode().querySelector("h2").textContent)
+          .eql("no_media_failure_message");
     });
 
-    it("should show 'generic_failure_title' when no reason is specified", function() {
-      view = mountTestComponent({cancelCall: function() {}});
+    it("should show 'generic_failure_title' when no reason is specified",
+       function() {
+         callView = mountTestComponent({cancelCall: function() {}});
 
-      expect(view.getDOMNode().querySelector("h2").textContent).eql("generic_failure_title");
-    });
+         expect(callView.getDOMNode().querySelector("h2").textContent)
+            .eql("generic_failure_title");
+     });
   });
 });
--- a/browser/components/loop/test/desktop-local/roomStore_test.js
+++ b/browser/components/loop/test/desktop-local/roomStore_test.js
@@ -544,41 +544,41 @@ describe("loop.store.RoomStore", functio
 
         store.getAllRooms();
 
         expect(store.getStoreState().pendingInitialRetrieval).eql(false);
       });
     });
 
     describe("ActiveRoomStore substore", function() {
-      var store, activeRoomStore;
+      var fakeStore, activeRoomStore;
 
       beforeEach(function() {
         activeRoomStore = new loop.store.ActiveRoomStore(dispatcher, {
           mozLoop: fakeMozLoop,
           sdkDriver: {}
         });
-        store = new loop.store.RoomStore(dispatcher, {
+        fakeStore = new loop.store.RoomStore(dispatcher, {
           mozLoop: fakeMozLoop,
           activeRoomStore: activeRoomStore
         });
       });
 
       it("should subscribe to substore changes", function() {
         var fakeServerData = {fake: true};
 
         activeRoomStore.setStoreState({serverData: fakeServerData});
 
-        expect(store.getStoreState().activeRoom.serverData)
+        expect(fakeStore.getStoreState().activeRoom.serverData)
           .eql(fakeServerData);
       });
 
       it("should trigger a change event when the substore is updated",
         function(done) {
-          store.once("change:activeRoom", function() {
+          fakeStore.once("change:activeRoom", function() {
             done();
           });
 
           activeRoomStore.setStoreState({serverData: {}});
         });
     });
   });
 
--- a/browser/components/loop/test/desktop-local/roomViews_test.js
+++ b/browser/components/loop/test/desktop-local/roomViews_test.js
@@ -434,40 +434,40 @@ describe("loop.roomViews", function () {
       var hangupBtn = view.getDOMNode().querySelector(".btn-hangup");
 
       React.addons.TestUtils.Simulate.click(hangupBtn);
 
       sinon.assert.calledOnce(fakeWindow.close);
     });
 
     describe("#componentWillUpdate", function() {
-      function expectActionDispatched(view) {
+      function expectActionDispatched(component) {
         sinon.assert.calledOnce(dispatcher.dispatch);
         sinon.assert.calledWithExactly(dispatcher.dispatch,
           sinon.match.instanceOf(sharedActions.SetupStreamElements));
       }
 
       it("should dispatch a `SetupStreamElements` action when the MEDIA_WAIT state " +
         "is entered", function() {
           activeRoomStore.setStoreState({roomState: ROOM_STATES.READY});
-          var view = mountTestComponent();
+          var component = mountTestComponent();
 
           activeRoomStore.setStoreState({roomState: ROOM_STATES.MEDIA_WAIT});
 
-          expectActionDispatched(view);
+          expectActionDispatched(component);
         });
 
       it("should dispatch a `SetupStreamElements` action on MEDIA_WAIT state is " +
         "re-entered", function() {
           activeRoomStore.setStoreState({roomState: ROOM_STATES.ENDED});
-          var view = mountTestComponent();
+          var component = mountTestComponent();
 
           activeRoomStore.setStoreState({roomState: ROOM_STATES.MEDIA_WAIT});
 
-          expectActionDispatched(view);
+          expectActionDispatched(component);
         });
     });
 
     describe("#render", function() {
       it("should set document.title to store.serverData.roomName", function() {
         mountTestComponent();
 
         activeRoomStore.setStoreState({roomName: "fakeName"});
--- a/browser/components/loop/test/mochitest/browser_LoopContacts.js
+++ b/browser/components/loop/test/mochitest/browser_LoopContacts.js
@@ -84,19 +84,19 @@ const promiseLoadContacts = function() {
   return new Promise((resolve, reject) => {
     LoopContacts.removeAll(err => {
       if (err) {
         reject(err);
         return;
       }
 
       gExpectedAdds.push(...kContacts);
-      LoopContacts.addMany(kContacts, (err, contacts) => {
-        if (err) {
-          reject(err);
+      LoopContacts.addMany(kContacts, (error, contacts) => {
+        if (error) {
+          reject(error);
           return;
         }
         resolve(contacts);
       });
     });
   });
 };
 
@@ -118,17 +118,17 @@ const compareContacts = function(contact
 };
 
 // LoopContacts emits various events. Test if they work as expected here.
 let gExpectedAdds = [];
 let gExpectedRemovals = [];
 let gExpectedUpdates = [];
 
 const onContactAdded = function(e, contact) {
-  let expectedIds = gExpectedAdds.map(contact => contact.id);
+  let expectedIds = gExpectedAdds.map(contactEntry => contactEntry.id);
   let idx = expectedIds.indexOf(contact.id);
   Assert.ok(idx > -1, "Added contact should be expected");
   let expected = gExpectedAdds[idx];
   compareContacts(contact, expected);
   gExpectedAdds.splice(idx, 1);
 };
 
 const onContactRemoved = function(e, contact) {
@@ -164,34 +164,34 @@ add_task(function* () {
   info("Add a contact.");
   yield new Promise((resolve, reject) => {
     gExpectedAdds.push(kDanglingContact);
     LoopContacts.add(kDanglingContact, (err, contact) => {
       Assert.ok(!err, "There shouldn't be an error");
       compareContacts(contact, kDanglingContact);
 
       info("Check if it's persisted.");
-      LoopContacts.get(contact._guid, (err, contact) => {
-        Assert.ok(!err, "There shouldn't be an error");
-        compareContacts(contact, kDanglingContact);
+      LoopContacts.get(contact._guid, (error, contactEntry) => {
+        Assert.ok(!error, "There shouldn't be an error");
+        compareContacts(contactEntry, kDanglingContact);
         resolve();
       });
     });
   });
 });
 
 add_task(function* () {
   info("Test removing all contacts.");
   let contacts = yield promiseLoadContacts();
 
   yield new Promise((resolve, reject) => {
     LoopContacts.removeAll(function(err) {
       Assert.ok(!err, "There shouldn't be an error");
-      LoopContacts.getAll(function(err, found) {
-        Assert.ok(!err, "There shouldn't be an error");
+      LoopContacts.getAll(function(error, found) {
+        Assert.ok(!error, "There shouldn't be an error");
         Assert.equal(found.length, 0, "There shouldn't be any contacts left");
         resolve();
       });
     });
   });
 });
 
 // Test retrieving a contact.
@@ -234,20 +234,20 @@ add_task(function* () {
         compareContacts(found[0], contact);
       }
       resolve();
     });
   });
 
   info("Get all contacts.");
   yield new Promise((resolve, reject) => {
-    LoopContacts.getAll((err, contacts) => {
+    LoopContacts.getAll((err, allContacts) => {
       Assert.ok(!err, "There shouldn't be an error");
-      for (let i = 0, l = contacts.length; i < l; ++i) {
-        compareContacts(contacts[i], kContacts[i]);
+      for (let i = 0, l = allContacts.length; i < l; ++i) {
+        compareContacts(allContacts[i], kContacts[i]);
       }
       resolve();
     });
   });
 
   info("Get a non-existent contact.");
   return new Promise((resolve, reject) => {
     LoopContacts.get(1000, (err, contact) => {
@@ -264,18 +264,18 @@ add_task(function* () {
 
   info("Remove a single contact.");
   yield new Promise((resolve, reject) => {
     let toRemove = contacts[2]._guid;
     gExpectedRemovals.push(toRemove);
     LoopContacts.remove(toRemove, err => {
       Assert.ok(!err, "There shouldn't be an error");
 
-      LoopContacts.get(toRemove, (err, contact) => {
-        Assert.ok(!err, "There shouldn't be an error");
+      LoopContacts.get(toRemove, (error, contact) => {
+        Assert.ok(!error, "There shouldn't be an error");
         Assert.ok(!contact, "There shouldn't be a contact");
         resolve();
       });
     });
   });
 
   info("Remove a non-existing contact.");
   yield new Promise((resolve, reject) => {
@@ -288,19 +288,19 @@ add_task(function* () {
 
   info("Remove multiple contacts.");
   yield new Promise((resolve, reject) => {
     let toRemove = [contacts[0]._guid, contacts[1]._guid];
     gExpectedRemovals.push(...toRemove);
     LoopContacts.removeMany(toRemove, err => {
       Assert.ok(!err, "There shouldn't be an error");
 
-      LoopContacts.getAll((err, contacts) => {
-        Assert.ok(!err, "There shouldn't be an error");
-        let ids = contacts.map(contact => contact._guid);
+      LoopContacts.getAll((error, allContacts) => {
+        Assert.ok(!error, "There shouldn't be an error");
+        let ids = allContacts.map(contact => contact._guid);
         Assert.equal(ids.indexOf(toRemove[0]), -1, "Contact '" + toRemove[0] +
                                                    "' shouldn't be there");
         Assert.equal(ids.indexOf(toRemove[1]), -1, "Contact '" + toRemove[1] +
                                                    "' shouldn't be there");
         resolve();
       });
     });
   });
@@ -318,18 +318,18 @@ add_task(function* () {
       _guid: contacts[2]._guid,
       bday: newBday
     };
     gExpectedUpdates.push(contacts[2]._guid);
     LoopContacts.update(toUpdate, (err, result) => {
       Assert.ok(!err, "There shouldn't be an error");
       Assert.equal(result, toUpdate._guid, "Result should be the same as the contact ID");
 
-      LoopContacts.get(toUpdate._guid, (err, contact) => {
-        Assert.ok(!err, "There shouldn't be an error");
+      LoopContacts.get(toUpdate._guid, (error, contact) => {
+        Assert.ok(!error, "There shouldn't be an error");
         Assert.equal(contact.bday, newBday, "Birthday should be the same");
         info("Check that all other properties were left intact.");
         contacts[2].bday = newBday;
         compareContacts(contact, contacts[2]);
         resolve();
       });
     });
   });
@@ -356,18 +356,18 @@ add_task(function* () {
   info("Block contact.");
   yield new Promise((resolve, reject) => {
     let toBlock = contacts[1]._guid;
     gExpectedUpdates.push(toBlock);
     LoopContacts.block(toBlock, (err, result) => {
       Assert.ok(!err, "There shouldn't be an error");
       Assert.equal(result, toBlock, "Result should be the same as the contact ID");
 
-      LoopContacts.get(toBlock, (err, contact) => {
-        Assert.ok(!err, "There shouldn't be an error");
+      LoopContacts.get(toBlock, (error, contact) => {
+        Assert.ok(!error, "There shouldn't be an error");
         Assert.strictEqual(contact.blocked, true, "Blocked status should be set");
         info("Check that all other properties were left intact.");
         delete contact.blocked;
         compareContacts(contact, contacts[1]);
         resolve();
       });
     });
   });
@@ -385,18 +385,18 @@ add_task(function* () {
   info("Unblock a contact.");
   yield new Promise((resolve, reject) => {
     let toUnblock = contacts[1]._guid;
     gExpectedUpdates.push(toUnblock);
     LoopContacts.unblock(toUnblock, (err, result) => {
       Assert.ok(!err, "There shouldn't be an error");
       Assert.equal(result, toUnblock, "Result should be the same as the contact ID");
 
-      LoopContacts.get(toUnblock, (err, contact) => {
-        Assert.ok(!err, "There shouldn't be an error");
+      LoopContacts.get(toUnblock, (error, contact) => {
+        Assert.ok(!error, "There shouldn't be an error");
         Assert.strictEqual(contact.blocked, false, "Blocked status should be set");
         info("Check that all other properties were left intact.");
         delete contact.blocked;
         compareContacts(contact, contacts[1]);
         resolve();
       });
     });
   });
--- a/browser/components/loop/test/mochitest/browser_mozLoop_sharingListeners.js
+++ b/browser/components/loop/test/mochitest/browser_mozLoop_sharingListeners.js
@@ -32,20 +32,20 @@ function promiseWindowIdReceivedOnAdd(ha
   return new Promise(resolve => {
     handler.resolve = resolve;
     gMozLoopAPI.addBrowserSharingListener(handler.listener);
   });
 }
 
 let createdTabs = [];
 
-function promiseWindowIdReceivedNewTab(handlers = []) {
+function promiseWindowIdReceivedNewTab(handlersParam = []) {
   let promiseHandlers = [];
 
-  handlers.forEach(handler => {
+  handlersParam.forEach(handler => {
     promiseHandlers.push(new Promise(resolve => {
       handler.resolve = resolve;
     }));
   });
 
   let createdTab = gBrowser.selectedTab = gBrowser.addTab();
   createdTabs.push(createdTab);
 
--- a/browser/components/loop/test/mochitest/head.js
+++ b/browser/components/loop/test/mochitest/head.js
@@ -179,18 +179,18 @@ function promiseDeletedOAuthParams(baseU
     xhr.addEventListener("load", () => resolve(xhr));
     xhr.addEventListener("error", reject);
     xhr.send();
   });
 }
 
 function promiseObserverNotified(aTopic, aExpectedData = null) {
   return new Promise((resolve, reject) => {
-    Services.obs.addObserver(function onNotification(aSubject, aTopic, aData) {
-      Services.obs.removeObserver(onNotification, aTopic);
+    Services.obs.addObserver(function onNotification(aSubject, topic, aData) {
+      Services.obs.removeObserver(onNotification, topic);
       is(aData, aExpectedData, "observer data should match expected data");
       resolve({subject: aSubject, data: aData});
     }, aTopic, false);
   });
 }
 
 /**
  * Get the last registration on the test server.
--- a/browser/components/loop/test/shared/conversationStore_test.js
+++ b/browser/components/loop/test/shared/conversationStore_test.js
@@ -725,24 +725,24 @@ describe("loop.store.ConversationStore",
               sinon.match.hasOwn("reason", "websocket-setup"));
            });
         });
       });
     });
   });
 
   describe("#hangupCall", function() {
-    var wsMediaFailSpy, wsCloseSpy;
+    var wsMediaFailSpy, wsHangupSpy;
     beforeEach(function() {
       wsMediaFailSpy = sinon.spy();
-      wsCloseSpy = sinon.spy();
+      wsHangupSpy = sinon.spy();
 
       store._websocket = {
         mediaFail: wsMediaFailSpy,
-        close: wsCloseSpy
+        close: wsHangupSpy
       };
       store.setStoreState({callState: CALL_STATES.ONGOING});
       store.setStoreState({windowId: "42"});
     });
 
     it("should disconnect the session", function() {
       store.hangupCall(new sharedActions.HangupCall());
 
@@ -753,17 +753,17 @@ describe("loop.store.ConversationStore",
       store.hangupCall(new sharedActions.HangupCall());
 
       sinon.assert.calledOnce(wsMediaFailSpy);
     });
 
     it("should ensure the websocket is closed", function() {
       store.hangupCall(new sharedActions.HangupCall());
 
-      sinon.assert.calledOnce(wsCloseSpy);
+      sinon.assert.calledOnce(wsHangupSpy);
     });
 
     it("should set the callState to finished", function() {
       store.hangupCall(new sharedActions.HangupCall());
 
       expect(store.getStoreState("callState")).eql(CALL_STATES.FINISHED);
     });
 
@@ -772,24 +772,24 @@ describe("loop.store.ConversationStore",
 
       sinon.assert.calledOnce(fakeMozLoop.calls.clearCallInProgress);
       sinon.assert.calledWithExactly(
         fakeMozLoop.calls.clearCallInProgress, "42");
     });
   });
 
   describe("#remotePeerDisconnected", function() {
-    var wsMediaFailSpy, wsCloseSpy;
+    var wsMediaFailSpy, wsDisconnectSpy;
     beforeEach(function() {
       wsMediaFailSpy = sinon.spy();
-      wsCloseSpy = sinon.spy();
+      wsDisconnectSpy = sinon.spy();
 
       store._websocket = {
         mediaFail: wsMediaFailSpy,
-        close: wsCloseSpy
+        close: wsDisconnectSpy
       };
       store.setStoreState({callState: CALL_STATES.ONGOING});
       store.setStoreState({windowId: "42"});
     });
 
     it("should disconnect the session", function() {
       store.remotePeerDisconnected(new sharedActions.RemotePeerDisconnected({
         peerHungup: true
@@ -798,17 +798,17 @@ describe("loop.store.ConversationStore",
       sinon.assert.calledOnce(sdkDriver.disconnectSession);
     });
 
     it("should ensure the websocket is closed", function() {
       store.remotePeerDisconnected(new sharedActions.RemotePeerDisconnected({
         peerHungup: true
       }));
 
-      sinon.assert.calledOnce(wsCloseSpy);
+      sinon.assert.calledOnce(wsDisconnectSpy);
     });
 
     it("should release mozLoop callsData", function() {
       store.remotePeerDisconnected(new sharedActions.RemotePeerDisconnected({
         peerHungup: true
       }));
 
       sinon.assert.calledOnce(fakeMozLoop.calls.clearCallInProgress);
@@ -1050,48 +1050,48 @@ describe("loop.store.ConversationStore",
         }));
 
         sinon.assert.calledOnce(trigger);
         sinon.assert.calledWithExactly(trigger, "error:emailLink");
       });
   });
 
   describe("#windowUnload", function() {
-    var fakeWebsocket;
+    var fakeWs;
 
     beforeEach(function() {
-      fakeWebsocket = store._websocket = {
+      fakeWs = store._websocket = {
         close: sinon.stub(),
         decline: sinon.stub()
       };
 
       store.setStoreState({windowId: 42});
     });
 
     it("should decline the connection on the websocket for incoming calls if the state is alerting", function() {
       store.setStoreState({
         callState: CALL_STATES.ALERTING,
         outgoing: false
       });
 
       store.windowUnload();
 
-      sinon.assert.calledOnce(fakeWebsocket.decline);
+      sinon.assert.calledOnce(fakeWs.decline);
     });
 
     it("should disconnect the sdk session", function() {
       store.windowUnload();
 
       sinon.assert.calledOnce(sdkDriver.disconnectSession);
     });
 
     it("should close the websocket", function() {
       store.windowUnload();
 
-      sinon.assert.calledOnce(fakeWebsocket.close);
+      sinon.assert.calledOnce(fakeWs.close);
     });
 
     it("should clear the call in progress for the backend", function() {
       store.windowUnload();
 
       sinon.assert.calledOnce(fakeMozLoop.calls.clearCallInProgress);
       sinon.assert.calledWithExactly(fakeMozLoop.calls.clearCallInProgress, 42);
     });
--- a/browser/components/loop/test/shared/feedbackStore_test.js
+++ b/browser/components/loop/test/shared/feedbackStore_test.js
@@ -30,21 +30,22 @@ describe("loop.store.FeedbackStore", fun
   describe("#constructor", function() {
     it("should throw an error if feedbackClient is missing", function() {
       expect(function() {
         new loop.store.FeedbackStore(dispatcher);
       }).to.Throw(/feedbackClient/);
     });
 
     it("should set the store to the INIT feedback state", function() {
-      var store = new loop.store.FeedbackStore(dispatcher, {
+      var fakeStore = new loop.store.FeedbackStore(dispatcher, {
         feedbackClient: feedbackClient
       });
 
-      expect(store.getStoreState("feedbackState")).eql(FEEDBACK_STATES.INIT);
+      expect(fakeStore.getStoreState("feedbackState"))
+          .eql(FEEDBACK_STATES.INIT);
     });
   });
 
   describe("#requireFeedbackDetails", function() {
     it("should transition to DETAILS state", function() {
       store.requireFeedbackDetails(new sharedActions.RequireFeedbackDetails());
 
       expect(store.getStoreState("feedbackState")).eql(FEEDBACK_STATES.DETAILS);
--- a/browser/components/loop/test/shared/feedbackViews_test.js
+++ b/browser/components/loop/test/shared/feedbackViews_test.js
@@ -26,40 +26,40 @@ describe("loop.shared.views.FeedbackView
       React.createElement(sharedViews.FeedbackView));
   });
 
   afterEach(function() {
     sandbox.restore();
   });
 
   // local test helpers
-  function clickHappyFace(comp) {
-    var happyFace = comp.getDOMNode().querySelector(".face-happy");
+  function clickHappyFace(component) {
+    var happyFace = component.getDOMNode().querySelector(".face-happy");
     TestUtils.Simulate.click(happyFace);
   }
 
-  function clickSadFace(comp) {
-    var sadFace = comp.getDOMNode().querySelector(".face-sad");
+  function clickSadFace(component) {
+    var sadFace = component.getDOMNode().querySelector(".face-sad");
     TestUtils.Simulate.click(sadFace);
   }
 
-  function fillSadFeedbackForm(comp, category, text) {
+  function fillSadFeedbackForm(component, category, text) {
     TestUtils.Simulate.change(
-      comp.getDOMNode().querySelector("[value='" + category + "']"));
+      component.getDOMNode().querySelector("[value='" + category + "']"));
 
     if (text) {
       TestUtils.Simulate.change(
-        comp.getDOMNode().querySelector("[name='description']"), {
+        component.getDOMNode().querySelector("[name='description']"), {
           target: {value: "fake reason"}
         });
     }
   }
 
-  function submitSadFeedbackForm(comp, category, text) {
-    TestUtils.Simulate.submit(comp.getDOMNode().querySelector("form"));
+  function submitSadFeedbackForm(component, category, text) {
+    TestUtils.Simulate.submit(component.getDOMNode().querySelector("form"));
   }
 
   describe("Happy feedback", function() {
     it("should dispatch a SendFeedback action", function() {
       var dispatch = sandbox.stub(dispatcher, "dispatch");
 
       clickHappyFace(comp);
 
--- a/browser/components/loop/test/shared/mixins_test.js
+++ b/browser/components/loop/test/shared/mixins_test.js
@@ -466,17 +466,17 @@ describe("loop.shared.mixins", function(
       sinon.assert.calledWithExactly(navigator.mozLoop.getAudioBlob,
                                      "failure", sinon.match.func);
       sinon.assert.calledOnce(fakeAudio.play);
       expect(fakeAudio.loop).to.equal(false);
     });
   });
 
   describe("loop.shared.mixins.RoomsAudioMixin", function() {
-    var view, fakeAudioMixin, TestComp, comp;
+    var view, fakeAudioMixin, comp;
 
     function createTestComponent(initialState) {
       var TestComp = React.createClass({
         mixins: [loop.shared.mixins.RoomsAudioMixin],
         render: function() {
           return React.DOM.div();
         },
 
--- a/browser/components/loop/test/shared/otSdkDriver_test.js
+++ b/browser/components/loop/test/shared/otSdkDriver_test.js
@@ -754,57 +754,57 @@ describe("loop.OTSdkDriver", function ()
 
         sinon.assert.calledWith(driver._noteConnectionLengthIfNeeded, startTime,
           endTime);
       });
 
     });
 
     describe("streamCreated (publisher/local)", function() {
-      var fakeStream, fakeMockVideo;
+      var stream, fakeMockVideo;
 
       beforeEach(function() {
         driver._mockPublisherEl = document.createElement("div");
         fakeMockVideo = document.createElement("video");
 
         driver._mockPublisherEl.appendChild(fakeMockVideo);
-        fakeStream = {
+        stream = {
           hasVideo: true,
           videoType: "camera",
           videoDimensions: {width: 1, height: 2}
         };
       });
 
       it("should dispatch a VideoDimensionsChanged action", function() {
-        publisher.trigger("streamCreated", { stream: fakeStream });
+        publisher.trigger("streamCreated", { stream: stream });
 
         sinon.assert.called(dispatcher.dispatch);
         sinon.assert.calledWithExactly(dispatcher.dispatch,
           new sharedActions.VideoDimensionsChanged({
             isLocal: true,
             videoType: "camera",
             dimensions: {width: 1, height: 2}
           }));
       });
 
       it("should dispatch a LocalVideoEnabled action", function() {
-        publisher.trigger("streamCreated", { stream: fakeStream });
+        publisher.trigger("streamCreated", { stream: stream });
 
         sinon.assert.called(dispatcher.dispatch);
         sinon.assert.calledWithExactly(dispatcher.dispatch,
           new sharedActions.LocalVideoEnabled({
             srcVideoObject: fakeMockVideo
           }));
       });
 
       it("should dispatch a ConnectionStatus action", function() {
         driver._metrics.recvStreams = 1;
         driver._metrics.connections = 2;
 
-        publisher.trigger("streamCreated", {stream: fakeStream});
+        publisher.trigger("streamCreated", {stream: stream});
 
         sinon.assert.called(dispatcher.dispatch);
         sinon.assert.calledWithExactly(dispatcher.dispatch,
           new sharedActions.ConnectionStatus({
             event: "Publisher.streamCreated",
             state: "sendrecv",
             connections: 2,
             recvStreams: 1,
@@ -1013,91 +1013,91 @@ describe("loop.OTSdkDriver", function ()
             connections: 2,
             recvStreams: 1,
             sendStreams: 0
           }));
       });
     });
 
     describe("streamDestroyed: session/remote", function() {
-      var fakeStream;
+      var stream;
 
       beforeEach(function() {
-        fakeStream = {
+        stream = {
           videoType: "screen"
         };
       });
 
       it("should dispatch a ReceivingScreenShare action", function() {
-        session.trigger("streamDestroyed", { stream: fakeStream });
+        session.trigger("streamDestroyed", { stream: stream });
 
         sinon.assert.called(dispatcher.dispatch);
         sinon.assert.calledWithExactly(dispatcher.dispatch,
           new sharedActions.ReceivingScreenShare({
             receiving: false
           }));
       });
 
       it("should dispatch a ConnectionStatus action", function() {
         driver._metrics.connections = 2;
         driver._metrics.sendStreams = 1;
         driver._metrics.recvStreams = 1;
 
-        session.trigger("streamDestroyed", {stream: fakeStream});
+        session.trigger("streamDestroyed", {stream: stream});
 
         sinon.assert.called(dispatcher.dispatch);
         sinon.assert.calledWithExactly(dispatcher.dispatch,
           new sharedActions.ConnectionStatus({
             event: "Session.streamDestroyed",
             state: "sending",
             connections: 2,
             recvStreams: 0,
             sendStreams: 1
           }));
       });
 
       it("should not dispatch an action if the videoType is camera", function() {
-        fakeStream.videoType = "camera";
+        stream.videoType = "camera";
 
-        session.trigger("streamDestroyed", { stream: fakeStream });
+        session.trigger("streamDestroyed", { stream: stream });
 
         sinon.assert.neverCalledWithMatch(dispatcher.dispatch,
           sinon.match.hasOwn("name", "receivingScreenShare"));
       });
     });
 
     describe("streamPropertyChanged", function() {
-      var fakeStream = {
+      var stream = {
         connection: { id: "fake" },
         videoType: "screen",
         videoDimensions: {
           width: 320,
           height: 160
         }
       };
 
       it("should not dispatch a VideoDimensionsChanged action for other properties", function() {
         session.trigger("streamPropertyChanged", {
-          stream: fakeStream,
+          stream: stream,
           changedProperty: STREAM_PROPERTIES.HAS_AUDIO
         });
         session.trigger("streamPropertyChanged", {
-          stream: fakeStream,
+          stream: stream,
           changedProperty: STREAM_PROPERTIES.HAS_VIDEO
         });
 
         sinon.assert.notCalled(dispatcher.dispatch);
       });
 
       it("should dispatch a VideoDimensionsChanged action", function() {
         session.connection = {
           id: "localUser"
         };
         session.trigger("streamPropertyChanged", {
-          stream: fakeStream,
+          stream: stream,
           changedProperty: STREAM_PROPERTIES.VIDEO_DIMENSIONS
         });
 
         sinon.assert.calledOnce(dispatcher.dispatch);
         sinon.assert.calledWithMatch(dispatcher.dispatch,
           sinon.match.hasOwn("name", "videoDimensionsChanged"));
       });
     });
--- a/browser/components/loop/test/shared/views_test.js
+++ b/browser/components/loop/test/shared/views_test.js
@@ -499,50 +499,50 @@ describe("loop.shared.views", function()
             comp.stopPublishing();
 
             // Note: Backbone.Events#stopListening calls off() on passed object.
             sinon.assert.calledOnce(fakePublisher.off);
           });
       });
 
       describe("#publishStream", function() {
-        var comp;
+        var component;
 
         beforeEach(function() {
-          comp = mountTestComponent({
+          component = mountTestComponent({
             sdk: fakeSDK,
             model: model,
             video: {enabled: false}
           });
-          comp.startPublishing();
+          component.startPublishing();
         });
 
         it("should start streaming local audio", function() {
-          comp.publishStream("audio", true);
+          component.publishStream("audio", true);
 
           sinon.assert.calledOnce(fakePublisher.publishAudio);
           sinon.assert.calledWithExactly(fakePublisher.publishAudio, true);
         });
 
         it("should stop streaming local audio", function() {
-          comp.publishStream("audio", false);
+          component.publishStream("audio", false);
 
           sinon.assert.calledOnce(fakePublisher.publishAudio);
           sinon.assert.calledWithExactly(fakePublisher.publishAudio, false);
         });
 
         it("should start streaming local video", function() {
-          comp.publishStream("video", true);
+          component.publishStream("video", true);
 
           sinon.assert.calledOnce(fakePublisher.publishVideo);
           sinon.assert.calledWithExactly(fakePublisher.publishVideo, true);
         });
 
         it("should stop streaming local video", function() {
-          comp.publishStream("video", false);
+          component.publishStream("video", false);
 
           sinon.assert.calledOnce(fakePublisher.publishVideo);
           sinon.assert.calledWithExactly(fakePublisher.publishVideo, false);
         });
       });
 
       describe("Model events", function() {
 
--- a/browser/components/loop/test/standalone/standaloneMozLoop_test.js
+++ b/browser/components/loop/test/standalone/standaloneMozLoop_test.js
@@ -147,65 +147,68 @@ describe("loop.StandaloneMozLoop", funct
                           JSON.stringify(fakeServerErrorDescription));
       sinon.assert.calledWithMatch(callback, sinon.match(function(err) {
         return /HTTP 401 Unauthorized/.test(err.message);
       }));
     });
   });
 
   describe("#rooms.refreshMembership", function() {
-    var mozLoop, fakeServerErrorDescription;
+    var standaloneMozLoop, fakeServerErrDescription;
 
     beforeEach(function() {
-      mozLoop = new loop.StandaloneMozLoop({
+      standaloneMozLoop = new loop.StandaloneMozLoop({
         baseServerUrl: fakeBaseServerUrl
       });
 
-      fakeServerErrorDescription = {
+      fakeServerErrDescription = {
         code: 401,
         errno: 101,
         error: "error",
         message: "invalid token",
         info: "error info"
       };
     });
 
     it("should POST to the server", function() {
-      mozLoop.rooms.refreshMembership("fakeToken", "fakeSessionToken", callback);
+      standaloneMozLoop.rooms.refreshMembership("fakeToken", "fakeSessionToken",
+                                                callback);
 
       expect(requests).to.have.length.of(1);
       expect(requests[0].url).eql(fakeBaseServerUrl + "/rooms/fakeToken");
       expect(requests[0].method).eql("POST");
       expect(requests[0].requestHeaders.Authorization)
         .eql("Basic " + btoa("fakeSessionToken"));
 
       var requestData = JSON.parse(requests[0].requestBody);
       expect(requestData.action).eql("refresh");
       expect(requestData.sessionToken).eql("fakeSessionToken");
     });
 
     it("should call the callback with success parameters", function() {
-      mozLoop.rooms.refreshMembership("fakeToken", "fakeSessionToken", callback);
+      standaloneMozLoop.rooms.refreshMembership("fakeToken", "fakeSessionToken",
+                                                callback);
 
       var responseData = {
         expires: 20
       };
 
       requests[0].respond(200, {"Content-Type": "application/json"},
         JSON.stringify(responseData));
 
       sinon.assert.calledOnce(callback);
       sinon.assert.calledWithExactly(callback, null, responseData);
     });
 
     it("should call the callback with failure parameters", function() {
-      mozLoop.rooms.refreshMembership("fakeToken", "fakeSessionToken", callback);
+      standaloneMozLoop.rooms.refreshMembership("fakeToken", "fakeSessionToken",
+                                                callback);
 
       requests[0].respond(401, {"Content-Type": "application/json"},
-                          JSON.stringify(fakeServerErrorDescription));
+                          JSON.stringify(fakeServerErrDescription));
       sinon.assert.calledWithMatch(callback, sinon.match(function(err) {
         return /HTTP 401 Unauthorized/.test(err.message);
       }));
     });
   });
 
   describe("#rooms.leave", function() {
     it("should POST to the server", function() {
--- a/browser/components/loop/test/standalone/standaloneRoomViews_test.js
+++ b/browser/components/loop/test/standalone/standaloneRoomViews_test.js
@@ -610,18 +610,18 @@ describe("loop.standaloneRoomViews", fun
             activeRoomStore.setStoreState({roomState: ROOM_STATES.FAILED});
 
             expect(view.getDOMNode().querySelector(".btn-info"))
               .not.eql(null);
           });
       });
 
       describe("Join button", function() {
-        function getJoinButton(view) {
-          return view.getDOMNode().querySelector(".btn-join");
+        function getJoinButton(elem) {
+          return elem.getDOMNode().querySelector(".btn-join");
         }
 
         it("should render the Join button when room isn't active", function() {
           activeRoomStore.setStoreState({roomState: ROOM_STATES.READY});
 
           expect(getJoinButton(view)).not.eql(null);
         });
 
@@ -733,18 +733,18 @@ describe("loop.standaloneRoomViews", fun
             mediaConnected: true
           });
 
           expect(view.getDOMNode().querySelector(".remote .avatar")).not.eql(null);
         });
       });
 
       describe("Leave button", function() {
-        function getLeaveButton(view) {
-          return view.getDOMNode().querySelector(".btn-hangup");
+        function getLeaveButton(elem) {
+          return elem.getDOMNode().querySelector(".btn-hangup");
         }
 
         it("should disable the Leave button when the room state is READY",
           function() {
             activeRoomStore.setStoreState({roomState: ROOM_STATES.READY});
 
             expect(getLeaveButton(view).disabled).eql(true);
           });
--- a/browser/components/loop/test/standalone/webapp_test.js
+++ b/browser/components/loop/test/standalone/webapp_test.js
@@ -581,42 +581,42 @@ describe("loop.webapp", function() {
             expect(ocView.state.callStatus).eql("failure");
           });
       });
 
 
     });
 
     describe("FailedConversationView", function() {
-      var view, conversation, client, fakeAudio;
+      var view, fakeConversation, fakeClient, fakeAudio;
 
       beforeEach(function() {
         sandbox.stub(window, "XMLHttpRequest").returns(fakeAudioXHR);
 
         fakeAudio = {
           play: sinon.spy(),
           pause: sinon.spy(),
           removeAttribute: sinon.spy()
         };
         sandbox.stub(window, "Audio").returns(fakeAudio);
 
-        client = new loop.StandaloneClient({
+        fakeClient = new loop.StandaloneClient({
           baseServerUrl: "http://fake.example.com"
         });
-        conversation = new sharedModels.ConversationModel({}, {
+        fakeConversation = new sharedModels.ConversationModel({}, {
           sdk: {}
         });
-        conversation.set("loopToken", "fakeToken");
+        fakeConversation.set("loopToken", "fakeToken");
 
-        sandbox.stub(client, "requestCallUrlInfo");
+        sandbox.stub(fakeClient, "requestCallUrlInfo");
         view = React.addons.TestUtils.renderIntoDocument(
           React.createElement(
             loop.webapp.FailedConversationView, {
-              conversation: conversation,
-              client: client,
+              conversation: fakeConversation,
+              client: fakeClient,
               notifications: notifications
             }));
       });
 
       it("should play a failure sound, once", function() {
         fakeAudioXHR.onload();
 
         sinon.assert.called(fakeAudioXHR.open);
@@ -1115,35 +1115,35 @@ describe("loop.webapp", function() {
       sandbox.stub(client, "requestCallInfo");
       conversation = new sharedModels.ConversationModel({}, {
         sdk: {},
         pendingCallTimeout: 1000
       });
     });
 
     describe("Setup call", function() {
-      var conversation, setupOutgoingCall, view, requestCallUrlInfo;
+      var fakeConversation, setupOutgoingCall, view, requestCallUrlInfo;
 
       beforeEach(function() {
-        conversation = new loop.webapp.FxOSConversationModel({
+        fakeConversation = new loop.webapp.FxOSConversationModel({
           loopToken: "fakeToken"
         });
-        setupOutgoingCall = sandbox.stub(conversation, "setupOutgoingCall");
+        setupOutgoingCall = sandbox.stub(fakeConversation, "setupOutgoingCall");
 
         var standaloneClientStub = {
           requestCallUrlInfo: function(token, cb) {
             cb(null, {urlCreationDate: 0});
           },
           settings: {baseServerUrl: loop.webapp.baseServerUrl}
         };
 
         view = React.addons.TestUtils.renderIntoDocument(
           React.createElement(
             loop.webapp.StartConversationView, {
-              conversation: conversation,
+              conversation: fakeConversation,
               notifications: notifications,
               client: standaloneClientStub
             }));
 
         // default to succeeding with a null local media object
         stubGetPermsAndCacheMedia.callsArgWith(1, {});
       });
 
--- a/browser/components/loop/test/xpcshell/test_looppush_initialize.js
+++ b/browser/components/loop/test/xpcshell/test_looppush_initialize.js
@@ -64,19 +64,19 @@ add_test(function test_register_twice_sa
       Assert.equal(mockWebSocket.origin, kServerPushUrl,
                    "Should have the origin url from preferences");
       Assert.equal(mockWebSocket.protocol, "push-notification",
                    "Should have the protocol set to push-notifications");
 
       // Register again for the same channel
       MozLoopPushHandler.register(
         "chan-2",
-        function(err, url, id) {
-          Assert.equal(err, null, "Should return null for success");
-          Assert.equal(id, "chan-2", "Should have channel id = chan-2");
+        function(error, newUrl, newId) {
+          Assert.equal(error, null, "Should return null for success");
+          Assert.equal(newId, "chan-2", "Should have channel id = chan-2");
           run_next_test();
         },
         dummyCallback
       );
     },
     dummyCallback
   );
 });
--- a/browser/components/loop/ui/ui-showcase.jsx
+++ b/browser/components/loop/ui/ui-showcase.jsx
@@ -95,19 +95,19 @@
   /**
    * Every view that uses an activeRoomStore needs its own; if they shared
    * an active store, they'd interfere with each other.
    *
    * @param options
    * @returns {loop.store.ActiveRoomStore}
    */
   function makeActiveRoomStore(options) {
-    var dispatcher = new loop.Dispatcher();
+    var roomDispatcher = new loop.Dispatcher();
 
-    var store = new loop.store.ActiveRoomStore(dispatcher, {
+    var store = new loop.store.ActiveRoomStore(roomDispatcher, {
       mozLoop: navigator.mozLoop,
       sdkDriver: mockSDK
     });
 
     if (!("remoteVideoEnabled" in options)) {
       options.remoteVideoEnabled = true;
     }
 
@@ -990,17 +990,17 @@
       }
     } catch(err) {
       console.error(err);
       uncaughtError = err;
     }
 
     // Wait until all the FramedExamples have been fully loaded.
     setTimeout(function waitForQueuedFrames() {
-      if (window.queuedFrames.length != 0) {
+      if (window.queuedFrames.length !== 0) {
         setTimeout(waitForQueuedFrames, 500);
         return;
       }
       // Put the title back, in case views changed it.
       document.title = "Loop UI Components Showcase";
 
       // This simulates the mocha layout for errors which means we can run
       // this alongside our other unit tests but use the same harness.
--- a/browser/devtools/animationinspector/components.js
+++ b/browser/devtools/animationinspector/components.js
@@ -77,16 +77,26 @@ PlayerMetaDataHeader.prototype = {
     let metaData = createNode({
       parent: this.el,
       nodeType: "span",
       attributes: {
         "class": "meta-data"
       }
     });
 
+    // Animation is running on compositor
+    this.compositorIcon = createNode({
+      parent: metaData,
+      nodeType: "span",
+      attributes: {
+        "class": "compositor-icon",
+        "title": L10N.getStr("player.runningOnCompositorTooltip")
+      }
+    });
+
     // Animation duration.
     this.durationLabel = createNode({
       parent: metaData,
       nodeType: "span",
       textContent: L10N.getStr("player.animationDurationLabel")
     });
 
     this.durationValue = createNode({
@@ -132,16 +142,17 @@ PlayerMetaDataHeader.prototype = {
   destroy: function() {
     this.state = null;
     this.el.remove();
     this.el = null;
     this.nameLabel = this.nameValue = null;
     this.durationLabel = this.durationValue = null;
     this.delayLabel = this.delayValue = null;
     this.iterationLabel = this.iterationValue = null;
+    this.compositorIcon = null;
   },
 
   render: function(state) {
     // Update the name if needed.
     if (state.name !== this.state.name) {
       if (state.name) {
         // Animations (and transitions since bug 1122414) have names.
         this.nameLabel.textContent = L10N.getStr("player.animationNameLabel");
@@ -184,16 +195,26 @@ PlayerMetaDataHeader.prototype = {
         this.iterationValue.innerHTML = count;
       } else {
         // Hide the iteration elements if iteration is 1.
         this.iterationLabel.style.display = "none";
         this.iterationValue.style.display = "none";
       }
     }
 
+    // Show the Running on compositor icon if needed.
+    if (state.isRunningOnCompositor !== this.state.isRunningOnCompositor) {
+      if (state.isRunningOnCompositor) {
+        this.compositorIcon.style.display = "inline";
+      } else {
+        // Hide the compositor icon
+        this.compositorIcon.style.display = "none";
+      }
+    }
+
     this.state = state;
   }
 };
 
 /**
  * UI component responsible for displaying the playback rate drop-down in each
  * player widget, updating it when the state changes, and emitting events when
  * the user selects a new value.
--- a/browser/devtools/animationinspector/test/browser.ini
+++ b/browser/devtools/animationinspector/test/browser.ini
@@ -9,16 +9,17 @@ support-files =
 
 [browser_animation_empty_on_invalid_nodes.js]
 [browser_animation_iterationCount_hidden_by_default.js]
 [browser_animation_panel_exists.js]
 [browser_animation_participate_in_inspector_update.js]
 [browser_animation_play_pause_button.js]
 [browser_animation_playerFronts_are_refreshed.js]
 [browser_animation_playerWidgets_appear_on_panel_init.js]
+[browser_animation_playerWidgets_compositor_icon.js]
 [browser_animation_playerWidgets_destroy.js]
 [browser_animation_playerWidgets_disables_on_finished.js]
 [browser_animation_playerWidgets_dont_show_time_after_duration.js]
 [browser_animation_playerWidgets_have_control_buttons.js]
 [browser_animation_playerWidgets_meta_data.js]
 [browser_animation_playerWidgets_scrubber_delayed.js]
 [browser_animation_playerWidgets_scrubber_enabled.js]
 [browser_animation_playerWidgets_scrubber_moves.js]
new file mode 100644
--- /dev/null
+++ b/browser/devtools/animationinspector/test/browser_animation_playerWidgets_compositor_icon.js
@@ -0,0 +1,24 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test that player widgets show the right player meta-data for the
+// isRunningOnCompositor property.
+
+add_task(function*() {
+  yield addTab(TEST_URL_ROOT + "doc_simple_animation.html");
+  let {inspector, panel} = yield openAnimationInspector();
+
+  info("Select the simple animated node");
+  yield selectNode(".animated", inspector);
+
+  let compositorEl = panel.playerWidgets[0]
+                     .el.querySelector(".compositor-icon");
+
+  ok(compositorEl, "The compositor-icon element exists");
+  ok(isNodeVisible(compositorEl),
+     "The compositor icon is visible, since the animation is running on " +
+     "compositor thread");
+});
--- a/browser/devtools/debugger/test/browser_dbg_WorkerActor.attachThread.js
+++ b/browser/devtools/debugger/test/browser_dbg_WorkerActor.attachThread.js
@@ -38,16 +38,27 @@ function test() {
 
     let [, threadClient2] = yield attachThread(workerClient2);
     let sources2 = yield getSources(threadClient2);
     let sourceClient2 = threadClient2.source(findSource(sources2,
                                                         EXAMPLE_URL + WORKER_URL));
     let [, breakpointClient2] = yield setBreakpoint(sourceClient2, location);
     yield resume(threadClient2);
 
+    let packet = yield source(sourceClient1);
+    let text = (yield new Promise(function (resolve) {
+      let request = new XMLHttpRequest();
+      request.open("GET", EXAMPLE_URL + WORKER_URL, true);
+      request.send();
+      request.onload = function () {
+        resolve(request.responseText);
+      };
+    }));
+    is(packet.source, text);
+
     postMessageToWorkerInTab(tab, WORKER_URL, "ping");
     yield Promise.all([
       waitForPause(threadClient1).then((packet) => {
         is(packet.type, "paused");
         let why = packet.why;
         is(why.type, "breakpoint");
         is(why.actors.length, 1);
         is(why.actors[0], breakpointClient1.actor);
--- a/browser/devtools/debugger/test/head.js
+++ b/browser/devtools/debugger/test/head.js
@@ -929,20 +929,23 @@ function attachAddonActorForUrl(aClient,
     });
   });
 
   return deferred.promise;
 }
 
 function rdpInvoke(aClient, aMethod, ...args) {
   return promiseInvoke(aClient, aMethod, ...args)
-    .then(({error, message }) => {
+    .then((packet) => {
+      let { error, message } = packet;
       if (error) {
         throw new Error(error + ": " + message);
       }
+
+      return packet;
     });
 }
 
 function doResume(aPanel) {
   const threadClient = aPanel.panelWin.gThreadClient;
   return rdpInvoke(threadClient, threadClient.resume);
 }
 
@@ -1180,25 +1183,16 @@ function findSource(sources, url) {
   for (let source of sources) {
     if (source.url === url) {
       return source;
     }
   }
   return null;
 }
 
-function setBreakpoint(sourceClient, location) {
-  info("Setting breakpoint.\n");
-  return new Promise(function (resolve) {
-    sourceClient.setBreakpoint(location, function (response, breakpointClient) {
-      resolve([response, breakpointClient]);
-    });
-  });
-}
-
 function waitForEvent(client, type, predicate) {
   return new Promise(function (resolve) {
     function listener(type, packet) {
       if (!predicate(packet)) {
         return;
       }
       client.removeListener(listener);
       resolve(packet);
@@ -1213,8 +1207,18 @@ function waitForEvent(client, type, pred
     }
   });
 }
 
 function waitForPause(threadClient) {
   info("Waiting for pause.\n");
   return waitForEvent(threadClient, "paused");
 }
+
+function setBreakpoint(sourceClient, location) {
+  info("Setting breakpoint.\n");
+  return rdpInvoke(sourceClient, sourceClient.setBreakpoint, location);
+}
+
+function source(sourceClient) {
+  info("Getting source.\n");
+  return rdpInvoke(sourceClient, sourceClient.source);
+}
--- a/browser/devtools/framework/toolbox-hosts.js
+++ b/browser/devtools/framework/toolbox-hosts.js
@@ -101,17 +101,22 @@ BottomHost.prototype = {
    * means that the toolbox won't be visible at all once minimized.
    */
   minimize: function(height=0) {
     if (this.isMinimized) {
       return;
     }
     this.isMinimized = true;
 
-    let onTransitionEnd = () => {
+    let onTransitionEnd = event => {
+      if (event.propertyName !== "margin-bottom") {
+        // Ignore transitionend on unrelated properties.
+        return;
+      }
+
       this.frame.removeEventListener("transitionend", onTransitionEnd);
       this.emit("minimized");
     };
     this.frame.addEventListener("transitionend", onTransitionEnd);
     this.frame.style.marginBottom = -this.frame.height + height + "px";
     this._splitter.classList.add("disabled");
   },
 
@@ -120,17 +125,22 @@ BottomHost.prototype = {
    * maximized to the height it previously had).
    */
   maximize: function() {
     if (!this.isMinimized) {
       return;
     }
     this.isMinimized = false;
 
-    let onTransitionEnd = () => {
+    let onTransitionEnd = event => {
+      if (event.propertyName !== "margin-bottom") {
+        // Ignore transitionend on unrelated properties.
+        return;
+      }
+
       this.frame.removeEventListener("transitionend", onTransitionEnd);
       this.emit("maximized");
     };
     this.frame.addEventListener("transitionend", onTransitionEnd);
     this.frame.style.marginBottom = "0";
     this._splitter.classList.remove("disabled");
   },
 
--- a/browser/devtools/netmonitor/netmonitor-controller.js
+++ b/browser/devtools/netmonitor/netmonitor-controller.js
@@ -708,39 +708,17 @@ NetworkEventsHandler.prototype = {
    *        The long string grip containing the corresponding actor.
    *        If you pass in a plain string (by accident or because you're lazy),
    *        then a promise of the same string is simply returned.
    * @return object Promise
    *         A promise that is resolved when the full string contents
    *         are available, or rejected if something goes wrong.
    */
   getString: function(aStringGrip) {
-    // Make sure this is a long string.
-    if (typeof aStringGrip != "object" || aStringGrip.type != "longString") {
-      return promise.resolve(aStringGrip); // Go home string, you're drunk.
-    }
-    // Fetch the long string only once.
-    if (aStringGrip._fullText) {
-      return aStringGrip._fullText.promise;
-    }
-
-    let deferred = aStringGrip._fullText = promise.defer();
-    let { actor, initial, length } = aStringGrip;
-    let longStringClient = this.webConsoleClient.longString(aStringGrip);
-
-    longStringClient.substring(initial.length, length, aResponse => {
-      if (aResponse.error) {
-        Cu.reportError(aResponse.error + ": " + aResponse.message);
-        deferred.reject(aResponse);
-        return;
-      }
-      deferred.resolve(initial + aResponse.substring);
-    });
-
-    return deferred.promise;
+    return this.webConsoleClient.getString(aStringGrip);
   }
 };
 
 /**
  * Localization convenience methods.
  */
 let L10N = new ViewHelpers.L10N(NET_STRINGS_URI);
 let PKI_L10N = new ViewHelpers.L10N(PKI_STRINGS_URI);
--- a/browser/locales/en-US/chrome/browser/devtools/animationinspector.properties
+++ b/browser/locales/en-US/chrome/browser/devtools/animationinspector.properties
@@ -48,13 +48,18 @@ player.infiniteIterationCount=&#8734;
 player.timeLabel=%Ss
 
 # LOCALIZATION NOTE (player.playbackRateLabel):
 # This string is displayed in each animation player widget, as the label of
 # drop-down list items that can be used to change the rate at which the
 # animation runs (1x being the default, 2x being twice as fast).
 player.playbackRateLabel=%Sx
 
+# LOCALIZATION NOTE (player.runningOnCompositorTooltip):
+# This string is displayed as a tooltip for the icon that indicates that the
+# animation is running on the compositor thread.
+player.runningOnCompositorTooltip=This animation is running on compositor thread
+
 # LOCALIZATION NOTE (timeline.timeGraduationLabel):
 # This string is displayed at the top of the animation panel, next to each time
 # graduation, to indicate what duration (in milliseconds) this graduation
 # corresponds to.
 timeline.timeGraduationLabel=%Sms
--- a/browser/themes/linux/jar.mn
+++ b/browser/themes/linux/jar.mn
@@ -267,16 +267,17 @@ browser.jar:
 * skin/classic/browser/devtools/dark-theme.css        (../shared/devtools/dark-theme.css)
 * skin/classic/browser/devtools/light-theme.css       (../shared/devtools/light-theme.css)
   skin/classic/browser/devtools/add.svg               (../shared/devtools/images/add.svg)
   skin/classic/browser/devtools/filters.svg           (../shared/devtools/filters.svg)
   skin/classic/browser/devtools/filter-swatch.svg     (../shared/devtools/images/filter-swatch.svg)
   skin/classic/browser/devtools/pseudo-class.svg      (../shared/devtools/images/pseudo-class.svg)
   skin/classic/browser/devtools/controls.png          (../shared/devtools/images/controls.png)
   skin/classic/browser/devtools/controls@2x.png       (../shared/devtools/images/controls@2x.png)
+  skin/classic/browser/devtools/animation-fast-track.svg (../shared/devtools/images/animation-fast-track.svg)
   skin/classic/browser/devtools/performance-icons.svg  (../shared/devtools/images/performance-icons.svg)
   skin/classic/browser/devtools/newtab.png             (../shared/devtools/images/newtab.png)
   skin/classic/browser/devtools/newtab@2x.png          (../shared/devtools/images/newtab@2x.png)
   skin/classic/browser/devtools/newtab-inverted.png    (../shared/devtools/images/newtab-inverted.png)
   skin/classic/browser/devtools/newtab-inverted@2x.png (../shared/devtools/images/newtab-inverted@2x.png)
 * skin/classic/browser/devtools/widgets.css           (devtools/widgets.css)
   skin/classic/browser/devtools/power.svg                     (../shared/devtools/images/power.svg)
   skin/classic/browser/devtools/filetype-dir-close.svg        (../shared/devtools/images/filetypes/dir-close.svg)
--- a/browser/themes/osx/jar.mn
+++ b/browser/themes/osx/jar.mn
@@ -369,16 +369,17 @@ browser.jar:
 * skin/classic/browser/devtools/dark-theme.css              (../shared/devtools/dark-theme.css)
 * skin/classic/browser/devtools/light-theme.css             (../shared/devtools/light-theme.css)
   skin/classic/browser/devtools/add.svg                     (../shared/devtools/images/add.svg)
   skin/classic/browser/devtools/filters.svg                 (../shared/devtools/filters.svg)
   skin/classic/browser/devtools/filter-swatch.svg           (../shared/devtools/images/filter-swatch.svg)
   skin/classic/browser/devtools/pseudo-class.svg            (../shared/devtools/images/pseudo-class.svg)
   skin/classic/browser/devtools/controls.png                (../shared/devtools/images/controls.png)
   skin/classic/browser/devtools/controls@2x.png             (../shared/devtools/images/controls@2x.png)
+  skin/classic/browser/devtools/animation-fast-track.svg    (../shared/devtools/images/animation-fast-track.svg)
   skin/classic/browser/devtools/performance-icons.svg       (../shared/devtools/images/performance-icons.svg)
   skin/classic/browser/devtools/newtab.png                  (../shared/devtools/images/newtab.png)
   skin/classic/browser/devtools/newtab@2x.png               (../shared/devtools/images/newtab@2x.png)
   skin/classic/browser/devtools/newtab-inverted.png         (../shared/devtools/images/newtab-inverted.png)
   skin/classic/browser/devtools/newtab-inverted@2x.png      (../shared/devtools/images/newtab-inverted@2x.png)
 * skin/classic/browser/devtools/widgets.css                 (devtools/widgets.css)
   skin/classic/browser/devtools/power.svg                   (../shared/devtools/images/power.svg)
   skin/classic/browser/devtools/filetype-dir-close.svg      (../shared/devtools/images/filetypes/dir-close.svg)
--- a/browser/themes/shared/devtools/animationinspector.css
+++ b/browser/themes/shared/devtools/animationinspector.css
@@ -307,16 +307,27 @@ body {
 .animation-title .meta-data {
   float: right;
 }
 
 .animation-title strong {
   margin: 0 .5em;
 }
 
+.animation-title .meta-data .compositor-icon {
+    display: none;
+    background-image: url("animation-fast-track.svg");
+    background-repeat: no-repeat;
+    padding-left: 12px;
+    /* Make sure the icon is positioned above the timeline range input so that
+       its tooltip appears on hover */
+    z-index: 1;
+    position: relative;
+}
+
 /* Timeline wiget */
 
 .timeline {
   height: 20px;
   width: 100%;
   display: flex;
   flex-direction: row;
   border-bottom: 1px solid var(--theme-splitter-color);
new file mode 100644
--- /dev/null
+++ b/browser/themes/shared/devtools/images/animation-fast-track.svg
@@ -0,0 +1,9 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+   - License, v. 2.0. If a copy of the MPL was not distributed with this
+   - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<svg xmlns="http://www.w3.org/2000/svg" width="9" height="12">
+  <g transform="matrix(1.0251088,0,0,0.85613344,-3.1546734,-888.94343)">
+    <path d="m 5.1284819,1038.3667 6.4950901,0 -2.7147491,4.6651 2.9438561,0 -8.1148915,9.3081 1.6126718,-6.8973 -2.2701022,0 z" style="fill:#4cb0e1;"/>
+  </g>
+</svg>
--- a/browser/themes/windows/jar.mn
+++ b/browser/themes/windows/jar.mn
@@ -359,16 +359,17 @@ browser.jar:
 *       skin/classic/browser/devtools/dark-theme.css                (../shared/devtools/dark-theme.css)
 *       skin/classic/browser/devtools/light-theme.css               (../shared/devtools/light-theme.css)
         skin/classic/browser/devtools/add.svg                       (../shared/devtools/images/add.svg)
         skin/classic/browser/devtools/filters.svg                   (../shared/devtools/filters.svg)
         skin/classic/browser/devtools/filter-swatch.svg             (../shared/devtools/images/filter-swatch.svg)
         skin/classic/browser/devtools/pseudo-class.svg              (../shared/devtools/images/pseudo-class.svg)
         skin/classic/browser/devtools/controls.png                  (../shared/devtools/images/controls.png)
         skin/classic/browser/devtools/controls@2x.png               (../shared/devtools/images/controls@2x.png)
+        skin/classic/browser/devtools/animation-fast-track.svg      (../shared/devtools/images/animation-fast-track.svg)
         skin/classic/browser/devtools/performance-icons.svg         (../shared/devtools/images/performance-icons.svg)
         skin/classic/browser/devtools/newtab.png                    (../shared/devtools/images/newtab.png)
         skin/classic/browser/devtools/newtab@2x.png                 (../shared/devtools/images/newtab@2x.png)
         skin/classic/browser/devtools/newtab-inverted.png           (../shared/devtools/images/newtab-inverted.png)
         skin/classic/browser/devtools/newtab-inverted@2x.png        (../shared/devtools/images/newtab-inverted@2x.png)
 *       skin/classic/browser/devtools/widgets.css                   (devtools/widgets.css)
         skin/classic/browser/devtools/power.svg                     (../shared/devtools/images/power.svg)
         skin/classic/browser/devtools/filetype-dir-close.svg        (../shared/devtools/images/filetypes/dir-close.svg)
deleted file mode 100644
index af190aa7a01c32d430bcffbf82dd774027e9c2e5..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
GIT binary patch
literal 0
Hc$@<O00001
deleted file mode 100644
index aa79b84abfd94f69a3b30e82d181978cae140860..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
GIT binary patch
literal 0
Hc$@<O00001
deleted file mode 100644
index ce7edea6361a093f502fceee7acb14bf9d43b3d2..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
GIT binary patch
literal 0
Hc$@<O00001
deleted file mode 100644
--- a/mobile/android/base/resources/drawable/menu_level.xml
+++ /dev/null
@@ -1,22 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!-- This Source Code Form is subject to the terms of the Mozilla Public
-   - License, v. 2.0. If a copy of the MPL was not distributed with this
-   - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
-
-<level-list xmlns:android="http://schemas.android.com/apk/res/android"
-            xmlns:gecko="http://schemas.android.com/apk/res-auto">
-
-    <item android:maxLevel="1">
-
-        <selector>
-
-            <item gecko:state_private="true" android:drawable="@drawable/menu_pb"/>
-            <item android:drawable="@drawable/menu"/>
-
-        </selector>
-
-    </item>
-
-    <item android:maxLevel="2" android:drawable="@android:color/transparent"/>
-
-</level-list>
--- a/mobile/android/base/resources/layout/browser_toolbar.xml
+++ b/mobile/android/base/resources/layout/browser_toolbar.xml
@@ -43,17 +43,17 @@
                                             android:background="@drawable/shaped_button"
                                             android:visibility="gone"/>
 
     <org.mozilla.gecko.widget.ThemedImageView android:id="@+id/menu_icon"
                                               style="@style/UrlBar.ImageButton"
                                               android:layout_alignLeft="@id/menu"
                                               android:layout_alignRight="@id/menu"
                                               android:gravity="center_vertical"
-                                              android:src="@drawable/menu_level"
+                                              android:src="@drawable/menu"
                                               android:visibility="gone"/>
 
     <org.mozilla.gecko.toolbar.PhoneTabsButton android:id="@+id/tabs"
                                                style="@style/UrlBar.ImageButton"
                                                android:layout_width="64dip"
                                                android:layout_toLeftOf="@id/menu"
                                                android:layout_alignWithParentIfMissing="true"
                                                android:background="@drawable/shaped_button"/>
--- a/mobile/android/base/resources/layout/find_in_page_content.xml
+++ b/mobile/android/base/resources/layout/find_in_page_content.xml
@@ -23,18 +23,17 @@
 
     <TextView android:id="@+id/find_status"
               android:layout_width="wrap_content"
               android:layout_height="wrap_content"
               android:layout_marginRight="@dimen/find_in_page_status_margin_right"
               android:textColor="@color/tabs_tray_icon_grey"
               android:visibility="gone"/>
 
-    <CheckedTextView xmlns:android="http://schemas.android.com/apk/res/android"
-                     android:id="@+id/find_matchcase"
+    <CheckedTextView android:id="@+id/find_matchcase"
                      android:layout_width="wrap_content"
                      android:layout_height="wrap_content"
                      android:padding="@dimen/find_in_page_matchcase_padding"
                      android:checked="false"
                      android:text="@string/find_matchcase"
                      android:textColor="@drawable/find_matchcase_selector"/>
 
     <ImageButton android:id="@+id/find_prev"
--- a/mobile/android/base/resources/layout/fxaccount_status_error_preference.xml
+++ b/mobile/android/base/resources/layout/fxaccount_status_error_preference.xml
@@ -19,17 +19,17 @@
             android:layout_width="wrap_content"
             android:layout_height="wrap_content"
             android:layout_gravity="center"
             android:minWidth="48dip"
             android:padding="10dip" />
     </LinearLayout>
 
     <RelativeLayout
-        android:layout_width="wrap_content"
+        android:layout_width="0dp"
         android:layout_height="wrap_content"
         android:layout_marginBottom="6dip"
         android:layout_marginLeft="15dip"
         android:layout_marginRight="6dip"
         android:layout_marginTop="6dip"
         android:layout_weight="1" >
 
         <TextView
--- a/mobile/android/base/resources/layout/gecko_app.xml
+++ b/mobile/android/base/resources/layout/gecko_app.xml
@@ -58,17 +58,18 @@
             </FrameLayout>
 
             <LinearLayout android:id="@+id/doorhanger_overlay"
                           android:layout_width="match_parent"
                           android:layout_height="match_parent"
                           android:background="@color/dark_transparent_overlay"
                           android:visibility="gone"
                           android:alpha="0"
-                          android:layerType="hardware"/>
+                          android:layerType="hardware"
+                          android:orientation="horizontal" />
 
         </RelativeLayout>
 
         <org.mozilla.gecko.FindInPageBar android:id="@+id/find_in_page"
                                          android:layout_width="match_parent"
                                          android:layout_height="wrap_content"
                                          android:layout_alignParentBottom="true"
                                          style="@style/FindBar"
--- a/mobile/android/base/resources/layout/home_remote_tabs_group.xml
+++ b/mobile/android/base/resources/layout/home_remote_tabs_group.xml
@@ -19,17 +19,17 @@
         android:layout_width="50dp"
         android:layout_height="50dp"
         android:layout_marginLeft="5dp"
         android:layout_gravity="center_vertical"
         android:scaleType="center"
         tools:src="@drawable/sync_mobile"/>
 
     <LinearLayout
-        android:layout_width="match_parent"
+        android:layout_width="0dp"
         android:layout_height="wrap_content"
         android:layout_weight="1"
         android:orientation="vertical">
 
         <org.mozilla.gecko.widget.FadedSingleColorTextView
             android:id="@+id/client"
             style="@style/Widget.TwoLinePageRow.Title"
             android:layout_width="match_parent"
--- a/mobile/android/base/resources/layout/keyguard_widget.xml
+++ b/mobile/android/base/resources/layout/keyguard_widget.xml
@@ -16,17 +16,16 @@
         android:background="@drawable/widget_button_left"
         android:layout_height="match_parent"
         android:src="@drawable/widget_icon"/>
 
     <LinearLayout android:id="@+id/new_tab_button"
         android:layout_width="0dp"
         android:layout_weight="1"
         android:layout_height="match_parent"
-        android:layout_centerVertical="true"
         android:contentDescription="@string/new_tab"
         android:gravity="center"
         android:orientation="horizontal"
         android:background="@drawable/widget_button_right">
 
         <TextView android:id="@+id/new_tab_button_label"
             android:layout_width="wrap_content"
             android:layout_height="wrap_content"
--- a/mobile/android/base/resources/layout/menu_popup.xml
+++ b/mobile/android/base/resources/layout/menu_popup.xml
@@ -4,13 +4,14 @@
    - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
 
 <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
               android:id="@+id/menu_panel"
               android:layout_width="@dimen/menu_popup_width"
               android:layout_height="wrap_content"
               android:layout_alignParentRight="true"
               android:minWidth="@dimen/menu_popup_width"
-              android:background="@drawable/menu_popup_bg">
+              android:background="@drawable/menu_popup_bg"
+              android:orientation="horizontal">
 
     <!-- MenuPanel will be added here dynamically -->
 
 </LinearLayout>
--- a/mobile/android/base/resources/layout/notification_icon_text.xml
+++ b/mobile/android/base/resources/layout/notification_icon_text.xml
@@ -15,17 +15,17 @@
 
         <ImageView android:id="@+id/notification_image"
                    android:layout_width="25dp"
                    android:layout_height="25dp"
                    android:scaleType="fitCenter"/>
 
         <TextView android:id="@+id/notification_title"
                   android:textAppearance="@android:style/TextAppearance.StatusBar.EventContent.Title"
-                  android:layout_width="match_parent"
+                  android:layout_width="0dp"
                   android:layout_height="wrap_content"
                   android:layout_weight="1"
                   android:singleLine="true"
                   android:ellipsize="marquee"
                   android:fadingEdge="horizontal"
                   android:paddingLeft="4dp"/>
 
     </LinearLayout>
--- a/mobile/android/base/resources/layout/notification_progress.xml
+++ b/mobile/android/base/resources/layout/notification_progress.xml
@@ -16,17 +16,17 @@
 
         <ImageView android:id="@+id/notification_image"
                    android:layout_width="25dp"
                    android:layout_height="25dp"
                    android:scaleType="fitCenter"/>
 
         <TextView android:id="@+id/notification_title"
                   android:textAppearance="@android:style/TextAppearance.StatusBar.EventContent.Title"
-                  android:layout_width="match_parent"
+                  android:layout_width="0dp"
                   android:layout_height="wrap_content"
                   android:layout_weight="1"
                   android:singleLine="true"
                   android:ellipsize="marquee"
                   android:fadingEdge="horizontal"
                   android:paddingLeft="10dp"/>
 
     </LinearLayout>
@@ -41,13 +41,12 @@
                   android:paddingLeft="3dp"/>
         <ProgressBar android:id="@+id/notification_progressbar"
                      style="?android:attr/progressBarStyleHorizontal"
                      android:layout_width="match_parent"
                      android:layout_height="wrap_content"
                      android:layout_marginTop="1dip"
                      android:layout_marginBottom="1dip"
                      android:layout_marginLeft="4dip"
-                     android:layout_marginRight="10dip"
-                     android:layout_centerHorizontal="true"/>
+                     android:layout_marginRight="10dip" />
     </LinearLayout>
 
 </LinearLayout>
--- a/mobile/android/base/resources/layout/notification_progress_text.xml
+++ b/mobile/android/base/resources/layout/notification_progress_text.xml
@@ -15,35 +15,34 @@
 
         <ImageView android:id="@+id/notification_image"
                    android:layout_width="25dp"
                    android:layout_height="25dp"
                    android:scaleType="fitCenter"/>
 
         <TextView android:id="@+id/notification_title"
                   android:textAppearance="@android:style/TextAppearance.StatusBar.EventContent.Title"
-                  android:layout_width="match_parent"
+                  android:layout_width="0dp"
                   android:layout_height="wrap_content"
                   android:layout_weight="1"
                   android:singleLine="true"
                   android:ellipsize="marquee"
                   android:fadingEdge="horizontal"
                   android:paddingLeft="4dp"/>
 
     </LinearLayout>
 
     <ProgressBar android:id="@+id/notification_progressbar"
                  style="?android:attr/progressBarStyleHorizontal"
                  android:layout_width="match_parent"
                  android:layout_height="16dip"
                  android:layout_marginTop="1dip"
                  android:layout_marginBottom="1dip"
                  android:layout_marginLeft="10dip"
-                 android:layout_marginRight="10dip"
-                 android:layout_centerHorizontal="true" />
+                 android:layout_marginRight="10dip" />
 
     <TextView android:id="@+id/notification_text"
               android:textAppearance="@android:style/TextAppearance.StatusBar.EventContent"
               android:layout_width="match_parent"
               android:layout_height="wrap_content"
               android:paddingLeft="4dp"/>
 
 </LinearLayout>
--- a/mobile/android/base/resources/layout/preference_rightalign_icon.xml
+++ b/mobile/android/base/resources/layout/preference_rightalign_icon.xml
@@ -8,17 +8,17 @@
 
 <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
       android:layout_width="match_parent"
       android:layout_height="wrap_content"
       android:orientation="horizontal"
       android:paddingRight="?android:attr/scrollbarSize">
 
     <RelativeLayout
-        android:layout_width="match_parent"
+        android:layout_width="0dp"
         android:layout_height="wrap_content"
         android:gravity="right"
         android:layout_marginLeft="15dip"
         android:layout_marginRight="6dip"
         android:layout_marginTop="6dip"
         android:layout_marginBottom="6dip"
         android:paddingRight="6dip"
         android:layout_weight="1">
--- a/mobile/android/base/resources/layout/preference_search_tip.xml
+++ b/mobile/android/base/resources/layout/preference_search_tip.xml
@@ -6,17 +6,17 @@
 <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
               android:orientation="horizontal"
               android:layout_width="match_parent"
               android:layout_height="match_parent"
               android:paddingRight="?android:attr/scrollbarSize">
 
     <TextView android:id="@+id/label_search_hint"
               android:layout_height="wrap_content"
-              android:layout_width="wrap_content"
+              android:layout_width="0dp"
               android:text="@string/pref_search_hint"
               android:layout_marginTop="5dip"
               android:layout_marginBottom="6dip"
               android:layout_marginLeft="15dip"
               android:layout_marginRight="6dip"
               android:paddingTop="8dp"
               android:paddingBottom="8dp"
               android:paddingRight="6dip"
--- a/mobile/android/base/resources/layout/search_suggestions_row.xml
+++ b/mobile/android/base/resources/layout/search_suggestions_row.xml
@@ -8,17 +8,17 @@
     android:layout_height="wrap_content"
     android:background="@drawable/search_row_background"
     android:padding="@dimen/search_row_padding"
     android:descendantFocusability="blocksDescendants"
     android:orientation="horizontal">
 
     <TextView
         android:id="@+id/auto_complete_row_text"
-        android:layout_width="wrap_content"
+        android:layout_width="0dp"
         android:layout_height="wrap_content"
         android:layout_weight="1"
         android:textSize="@dimen/query_text_size"/>
 
     <ImageButton
         android:id="@+id/auto_complete_row_jump_button"
         android:layout_width="wrap_content"
         android:layout_height="wrap_content"
--- a/mobile/android/base/resources/layout/search_widget.xml
+++ b/mobile/android/base/resources/layout/search_widget.xml
@@ -41,17 +41,16 @@
             android:textColor="@color/toolbar_icon_grey"/>
 
     </LinearLayout>
 
     <LinearLayout android:id="@+id/new_tab_button"
         android:layout_width="0dp"
         android:layout_weight="1"
         android:layout_height="match_parent"
-        android:layout_centerVertical="true"
         android:contentDescription="@string/new_tab"
         android:gravity="center"
         android:orientation="horizontal"
         android:background="@drawable/widget_button_right">
 
         <TextView android:id="@+id/new_tab_button_label"
             android:layout_width="wrap_content"
             android:layout_height="wrap_content"
--- a/mobile/android/base/resources/layout/site_identity.xml
+++ b/mobile/android/base/resources/layout/site_identity.xml
@@ -14,17 +14,17 @@
                   android:padding="@dimen/doorhanger_padding">
 
         <ImageView android:id="@+id/larry"
                    android:layout_width="@dimen/doorhanger_icon_size"
                    android:layout_height="@dimen/doorhanger_icon_size"
                    android:src="@drawable/larry"
                    android:paddingRight="@dimen/doorhanger_padding"/>
 
-        <LinearLayout android:layout_width="match_parent"
+        <LinearLayout android:layout_width="0dp"
                      android:layout_height="wrap_content"
                      android:orientation="vertical"
                      android:layout_weight="1.0">
 
             <include layout="@layout/site_identity_unknown" />
 
             <LinearLayout android:id="@+id/site_identity_known_container"
                           android:layout_width="match_parent"
--- a/mobile/android/base/resources/layout/tab_history_item_row.xml
+++ b/mobile/android/base/resources/layout/tab_history_item_row.xml
@@ -38,17 +38,16 @@
                    android:layout_gravity="center_horizontal"
                    android:background="@color/tab_history_timeline_separator" />
 
     </LinearLayout>
 
     <org.mozilla.gecko.widget.FadedSingleColorTextView
             android:id="@+id/tab_history_title"
             style="@style/Widget.TwoLinePageRow.Title"
-            android:layout_centerVertical="true"
             android:layout_width="match_parent"
             android:layout_height="wrap_content"
             android:layout_gravity="center_vertical"
             android:paddingRight="@dimen/tab_history_title_margin_right"
             android:text="@+id/tab_history_title"
             android:textSize="@dimen/tab_history_title_text_size"
             android:textColor="@color/text_and_tabs_tray_grey"
             gecko:fadeWidth="@dimen/tab_history_title_fading_width"/>
--- a/mobile/android/base/resources/layout/web_app.xml
+++ b/mobile/android/base/resources/layout/web_app.xml
@@ -18,17 +18,16 @@
                   android:layout_height="wrap_content"
                   android:singleLine="true"/>
 
     </LinearLayout>
 
     <RelativeLayout android:id="@+id/gecko_layout"
                     android:layout_width="match_parent"
                     android:layout_height="match_parent"
-                    android:layout_weight="1"
                     android:layout_below="@+id/webapp_titlebar">
 
         <include layout="@layout/shared_ui_components"/>
 
         <RelativeLayout android:id="@+id/splashscreen"
                         android:layout_width="match_parent"
                         android:layout_height="match_parent" >
 
--- a/mobile/android/base/resources/menu/preferences_search_menu.xml
+++ b/mobile/android/base/resources/menu/preferences_search_menu.xml
@@ -1,9 +1,10 @@
 <?xml version="1.0" encoding="utf-8"?>
 <!-- This Source Code Form is subject to the terms of the Mozilla Public
    - License, v. 2.0. If a copy of the MPL was not distributed with this
    - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
 
 <!-- Stub to preserve IDs. -->
 <menu xmlns:android="http://schemas.android.com/apk/res/android">
-    <item android:id="@+id/restore_defaults" />
+    <item android:id="@+id/restore_defaults"
+          android:title="" />
 </menu>
--- a/mobile/android/base/toolbar/BrowserToolbar.java
+++ b/mobile/android/base/toolbar/BrowserToolbar.java
@@ -831,17 +831,16 @@ public abstract class BrowserToolbar ext
     }
 
     @Override
     public void setPrivateMode(boolean isPrivate) {
         super.setPrivateMode(isPrivate);
 
         tabsButton.setPrivateMode(isPrivate);
         menuButton.setPrivateMode(isPrivate);
-        menuIcon.setPrivateMode(isPrivate);
         urlEditLayout.setPrivateMode(isPrivate);
     }
 
     public void show() {
         setVisibility(View.VISIBLE);
     }
 
     public void hide() {
--- a/toolkit/devtools/DevToolsUtils.js
+++ b/toolkit/devtools/DevToolsUtils.js
@@ -440,146 +440,154 @@ exports.defineLazyGetter(this, "NetUtil"
  *        - charset: the charset to use if the channel doesn't provide one
  * @returns Promise
  *        A promise of the document at that URL, as a string.
  *
  * XXX: It may be better to use nsITraceableChannel to get to the sources
  * without relying on caching when we can (not for eval, etc.):
  * http://www.softwareishard.com/blog/firebug/nsitraceablechannel-intercept-http-traffic/
  */
-exports.fetch = function fetch(aURL, aOptions={ loadFromCache: true,
-                                                policy: Ci.nsIContentPolicy.TYPE_OTHER,
-                                                window: null,
-                                                charset: null }) {
-  let deferred = promise.defer();
-  let scheme;
-  let url = aURL.split(" -> ").pop();
-  let charset;
-  let contentType;
 
-  try {
-    scheme = Services.io.extractScheme(url);
-  } catch (e) {
-    // In the xpcshell tests, the script url is the absolute path of the test
-    // file, which will make a malformed URI error be thrown. Add the file
-    // scheme prefix ourselves.
-    url = "file://" + url;
-    scheme = Services.io.extractScheme(url);
-  }
+// Fetch is defined differently depending on whether we are on the main thread
+// or a worker thread.
+if (!this.isWorker) {
+  exports.fetch = function (aURL, aOptions={ loadFromCache: true,
+                                             policy: Ci.nsIContentPolicy.TYPE_OTHER,
+                                             window: null,
+                                             charset: null }) {
+    let deferred = promise.defer();
+    let scheme;
+    let url = aURL.split(" -> ").pop();
+    let charset;
+    let contentType;
+
+    try {
+      scheme = Services.io.extractScheme(url);
+    } catch (e) {
+      // In the xpcshell tests, the script url is the absolute path of the test
+      // file, which will make a malformed URI error be thrown. Add the file
+      // scheme prefix ourselves.
+      url = "file://" + url;
+      scheme = Services.io.extractScheme(url);
+    }
 
-  switch (scheme) {
-    case "file":
-    case "chrome":
-    case "resource":
-      try {
-        NetUtil.asyncFetch({
-          uri: url,
-          loadUsingSystemPrincipal: true
-        }, function onFetch(aStream, aStatus, aRequest) {
-            if (!components.isSuccessCode(aStatus)) {
-              deferred.reject(new Error("Request failed with status code = "
-                                        + aStatus
-                                        + " after NetUtil.asyncFetch for url = "
-                                        + url));
-              return;
-            }
+    switch (scheme) {
+      case "file":
+      case "chrome":
+      case "resource":
+        try {
+          NetUtil.asyncFetch({
+            uri: url,
+            loadUsingSystemPrincipal: true
+          }, function onFetch(aStream, aStatus, aRequest) {
+              if (!components.isSuccessCode(aStatus)) {
+                deferred.reject(new Error("Request failed with status code = "
+                                          + aStatus
+                                          + " after NetUtil.asyncFetch for url = "
+                                          + url));
+                return;
+              }
 
-            let source = NetUtil.readInputStreamToString(aStream, aStream.available());
-            contentType = aRequest.contentType;
-            deferred.resolve(source);
-            aStream.close();
-          });
-      } catch (ex) {
-        deferred.reject(ex);
-      }
-      break;
+              let source = NetUtil.readInputStreamToString(aStream, aStream.available());
+              contentType = aRequest.contentType;
+              deferred.resolve(source);
+              aStream.close();
+            });
+        } catch (ex) {
+          deferred.reject(ex);
+        }
+        break;
 
-    default:
-      let channel;
-      try {
-        channel = Services.io.newChannel2(url,
-                                          null,
-                                          null,
-                                          null,      // aLoadingNode
-                                          Services.scriptSecurityManager.getSystemPrincipal(),
-                                          null,      // aTriggeringPrincipal
-                                          Ci.nsILoadInfo.SEC_NORMAL,
-                                          aOptions.policy);
-      } catch (e) {
-        if (e.name == "NS_ERROR_UNKNOWN_PROTOCOL") {
+      default:
+        let channel;
+        try {
+          channel = Services.io.newChannel2(url,
+                                            null,
+                                            null,
+                                            null,      // aLoadingNode
+                                            Services.scriptSecurityManager.getSystemPrincipal(),
+                                            null,      // aTriggeringPrincipal
+                                            Ci.nsILoadInfo.SEC_NORMAL,
+                                            aOptions.policy);
+        } catch (e if e.name == "NS_ERROR_UNKNOWN_PROTOCOL") {
           // On Windows xpcshell tests, c:/foo/bar can pass as a valid URL, but
           // newChannel won't be able to handle it.
           url = "file:///" + url;
           channel = Services.io.newChannel2(url,
                                             null,
                                             null,
                                             null,      // aLoadingNode
                                             Services.scriptSecurityManager.getSystemPrincipal(),
                                             null,      // aTriggeringPrincipal
                                             Ci.nsILoadInfo.SEC_NORMAL,
                                             aOptions.policy);
-        } else {
-          throw e;
-        }
-      }
-      let chunks = [];
-      let streamListener = {
-        onStartRequest: function(aRequest, aContext, aStatusCode) {
-          if (!components.isSuccessCode(aStatusCode)) {
-            deferred.reject(new Error("Request failed with status code = "
-                                      + aStatusCode
-                                      + " in onStartRequest handler for url = "
-                                      + url));
-          }
-        },
-        onDataAvailable: function(aRequest, aContext, aStream, aOffset, aCount) {
-          chunks.push(NetUtil.readInputStreamToString(aStream, aCount));
-        },
-        onStopRequest: function(aRequest, aContext, aStatusCode) {
-          if (!components.isSuccessCode(aStatusCode)) {
-            deferred.reject(new Error("Request failed with status code = "
-                                      + aStatusCode
-                                      + " in onStopRequest handler for url = "
-                                      + url));
-            return;
-          }
-
-          charset = channel.contentCharset || aOptions.charset;
-          contentType = channel.contentType;
-          deferred.resolve(chunks.join(""));
         }
-      };
+        let chunks = [];
+        let streamListener = {
+          onStartRequest: function(aRequest, aContext, aStatusCode) {
+            if (!components.isSuccessCode(aStatusCode)) {
+              deferred.reject(new Error("Request failed with status code = "
+                                        + aStatusCode
+                                        + " in onStartRequest handler for url = "
+                                        + url));
+            }
+          },
+          onDataAvailable: function(aRequest, aContext, aStream, aOffset, aCount) {
+            chunks.push(NetUtil.readInputStreamToString(aStream, aCount));
+          },
+          onStopRequest: function(aRequest, aContext, aStatusCode) {
+            if (!components.isSuccessCode(aStatusCode)) {
+              deferred.reject(new Error("Request failed with status code = "
+                                        + aStatusCode
+                                        + " in onStopRequest handler for url = "
+                                        + url));
+              return;
+            }
+
+            charset = channel.contentCharset || aOptions.charset;
+            contentType = channel.contentType;
+            deferred.resolve(chunks.join(""));
+          }
+        };
 
-      if (aOptions.window) {
-        // Respect private browsing.
-        channel.loadGroup = aOptions.window.QueryInterface(Ci.nsIInterfaceRequestor)
-                              .getInterface(Ci.nsIWebNavigation)
-                              .QueryInterface(Ci.nsIDocumentLoader)
-                              .loadGroup;
-      }
-      channel.loadFlags = aOptions.loadFromCache
-        ? channel.LOAD_FROM_CACHE
-        : channel.LOAD_BYPASS_CACHE;
-      try {
-        channel.asyncOpen(streamListener, null);
-      } catch(e) {
-        deferred.reject(new Error("Request failed for '"
-                                  + url
-                                  + "': "
-                                  + e.message));
-      }
-      break;
+        if (aOptions.window) {
+          // Respect private browsing.
+          channel.loadGroup = aOptions.window.QueryInterface(Ci.nsIInterfaceRequestor)
+                                .getInterface(Ci.nsIWebNavigation)
+                                .QueryInterface(Ci.nsIDocumentLoader)
+                                .loadGroup;
+        }
+        channel.loadFlags = aOptions.loadFromCache
+          ? channel.LOAD_FROM_CACHE
+          : channel.LOAD_BYPASS_CACHE;
+        try {
+          channel.asyncOpen(streamListener, null);
+        } catch(e) {
+          deferred.reject(new Error("Request failed for '"
+                                    + url
+                                    + "': "
+                                    + e.message));
+        }
+        break;
+    }
+
+    return deferred.promise.then(source => {
+      return {
+        content: convertToUnicode(source, charset),
+        contentType: contentType
+      };
+    });
   }
-
-  return deferred.promise.then(source => {
-    return {
-      content: convertToUnicode(source, charset),
-      contentType: contentType
-    };
-  });
+} else {
+  // Services is not available in worker threads, nor is there any other way
+  // to fetch a URL. We need to enlist the help from the main thread here, by
+  // issuing an rpc request, to fetch the URL on our behalf.
+  exports.fetch = function (url, options) {
+    return rpc("fetch", url, options);
+  }
 }
 
 /**
  * Convert a given string, encoded in a given character set, to unicode.
  *
  * @param string aString
  *        A string.
  * @param string aCharset
--- a/toolkit/devtools/server/main.js
+++ b/toolkit/devtools/server/main.js
@@ -15,16 +15,17 @@ let Services = require("Services");
 let { ActorPool, OriginalLocation, RegisteredActorFactory,
       ObservedActorFactory } = require("devtools/server/actors/common");
 let { LocalDebuggerTransport, ChildDebuggerTransport, WorkerDebuggerTransport } =
   require("devtools/toolkit/transport/transport");
 let DevToolsUtils = require("devtools/toolkit/DevToolsUtils");
 let { dumpn, dumpv, dbg_assert } = DevToolsUtils;
 let EventEmitter = require("devtools/toolkit/event-emitter");
 let Debugger = require("Debugger");
+let Promise = require("promise");
 
 DevToolsUtils.defineLazyGetter(this, "DebuggerSocket", () => {
   let { DebuggerSocket } = require("devtools/toolkit/security/socket");
   return DebuggerSocket;
 });
 DevToolsUtils.defineLazyGetter(this, "Authentication", () => {
   return require("devtools/toolkit/security/auth");
 });
@@ -755,18 +756,63 @@ var DebuggerServer = {
 
     events.on(aConnection, "closed", onClose);
 
     return deferred.promise;
   },
 
   connectToWorker: function (aConnection, aDbg, aId, aOptions) {
     return new Promise((resolve, reject) => {
-      // Step 1: Initialize the worker debugger.
-      aDbg.initialize("resource://gre/modules/devtools/server/worker.js");
+      // Step 1: Ensure the worker debugger is initialized.
+      if (!aDbg.isInitialized) {
+        aDbg.initialize("resource://gre/modules/devtools/server/worker.js");
+
+        // Create a listener for rpc requests from the worker debugger. Only do
+        // this once, when the worker debugger is first initialized, rather than
+        // for each connection.
+        let listener = {
+          onClose: () => {
+            aDbg.removeListener(listener);
+          },
+
+          onMessage: (message) => {
+            let packet = JSON.parse(message);
+            if (packet.type !== "rpc") {
+              return;
+            }
+
+            Promise.resolve().then(() => {
+              let method = {
+                "fetch": DevToolsUtils.fetch,
+              }[packet.method];
+              if (!method) {
+                throw Error("Unknown method: " + packet.method);
+              }
+
+              return method.apply(undefined, packet.params);
+            }).then((value) => {
+              aDbg.postMessage(JSON.stringify({
+                type: "rpc",
+                result: value,
+                error: null,
+                id: packet.id
+              }));
+            }, (reason) => {
+              aDbg.postMessage(JSON.stringify({
+                type: "rpc",
+                result: null,
+                error: reason,
+                id: packet.id
+              }));
+            });
+          }
+        };
+
+        aDbg.addListener(listener);
+      }
 
       // Step 2: Send a connect request to the worker debugger.
       aDbg.postMessage(JSON.stringify({
         type: "connect",
         id: aId,
         options: aOptions
       }));
 
--- a/toolkit/devtools/server/worker.js
+++ b/toolkit/devtools/server/worker.js
@@ -1,32 +1,57 @@
 "use strict"
 
+// This function is used to do remote procedure calls from the worker to the
+// main thread. It is exposed as a built-in global to every module by the
+// worker loader. To make sure the worker loader can access it, it needs to be
+// defined before loading the worker loader script below.
+this.rpc = function (method, ...params) {
+  let id = nextId++;
+
+  postMessage(JSON.stringify({
+    type: "rpc",
+    method: method,
+    params: params,
+    id: id
+  }));
+
+  let deferred = Promise.defer();
+  rpcDeferreds[id] = deferred;
+  return deferred.promise;
+};
+
 loadSubScript("resource://gre/modules/devtools/worker-loader.js");
 
+let Promise = worker.require("promise");
 let { ActorPool } = worker.require("devtools/server/actors/common");
 let { ThreadActor } = worker.require("devtools/server/actors/script");
 let { TabSources } = worker.require("devtools/server/actors/utils/TabSources");
 let makeDebugger = worker.require("devtools/server/actors/utils/make-debugger");
 let { DebuggerServer } = worker.require("devtools/server/main");
 
 DebuggerServer.init();
 DebuggerServer.createRootActor = function () {
   throw new Error("Should never get here!");
 };
 
 let connections = Object.create(null);
+let nextId = 0;
+let rpcDeferreds = [];
 
 this.addEventListener("message",  function (event) {
   let packet = JSON.parse(event.data);
   switch (packet.type) {
   case "connect":
     // Step 3: Create a connection to the parent.
     let connection = DebuggerServer.connectToParent(packet.id, this);
-    connections[packet.id] = connection;
+    connections[packet.id] = {
+      connection : connection,
+      rpcs: []
+    };
 
     // Step 4: Create a thread actor for the connection to the parent.
     let pool = new ActorPool(connection);
     connection.addActorPool(pool);
 
     let sources = null;
 
     let actor = new ThreadActor({
@@ -55,12 +80,21 @@ this.addEventListener("message",  functi
     // This will cause a packet to be sent over the connection to the parent.
     // Because this connection uses WorkerDebuggerTransport internally, this
     // packet will be sent using WorkerDebuggerGlobalScope.postMessage, causing
     // an onMessage event to be fired on the WorkerDebugger in the main thread.
     actor.onAttach({});
     break;
 
   case "disconnect":
-    connections[packet.id].close();
+    connections[packet.id].connection.close();
+    break;
+
+  case "rpc":
+    let deferred = rpcDeferreds[packet.id];
+    delete rpcDeferreds[packet.id];
+    if (packet.error) {
+        deferred.reject(packet.error);
+    }
+    deferred.resolve(packet.result);
     break;
   };
 });
--- a/toolkit/devtools/webconsole/client.js
+++ b/toolkit/devtools/webconsole/client.js
@@ -4,16 +4,17 @@
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
 const {Cc, Ci, Cu} = require("chrome");
 const DevToolsUtils = require("devtools/toolkit/DevToolsUtils");
 const EventEmitter = require("devtools/toolkit/event-emitter");
+const promise = require("promise");
 
 loader.lazyImporter(this, "LongStringClient", "resource://gre/modules/devtools/dbg-client.jsm");
 
 /**
  * A WebConsoleClient is used as a front end for the WebConsoleActor that is
  * created on the server, hiding implementation details.
  *
  * @param object aDebuggerClient
@@ -609,10 +610,50 @@ WebConsoleClient.prototype = {
     this.pendingEvaluationResults.clear();
     this.pendingEvaluationResults = null;
     this.clearNetworkRequests();
     this._networkRequests = null;
   },
 
   clearNetworkRequests: function () {
     this._networkRequests.clear();
+  },
+
+  /**
+   * Fetches the full text of a LongString.
+   *
+   * @param object | string stringGrip
+   *        The long string grip containing the corresponding actor.
+   *        If you pass in a plain string (by accident or because you're lazy),
+   *        then a promise of the same string is simply returned.
+   * @return object Promise
+   *         A promise that is resolved when the full string contents
+   *         are available, or rejected if something goes wrong.
+   */
+  getString: function(stringGrip) {
+    // Make sure this is a long string.
+    if (typeof stringGrip != "object" || stringGrip.type != "longString") {
+      return promise.resolve(stringGrip); // Go home string, you're drunk.
+    }
+
+    // Fetch the long string only once.
+    if (stringGrip._fullText) {
+      return stringGrip._fullText.promise;
+    }
+
+    let deferred = stringGrip._fullText = promise.defer();
+    let { actor, initial, length } = stringGrip;
+    let longStringClient = this.longString(stringGrip);
+
+    longStringClient.substring(initial.length, length, aResponse => {
+      if (aResponse.error) {
+        DevToolsUtils.reportException("getString",
+            aResponse.error + ": " + aResponse.message);
+
+        deferred.reject(aResponse);
+        return;
+      }
+      deferred.resolve(initial + aResponse.substring);
+    });
+
+    return deferred.promise;
   }
 };
--- a/toolkit/devtools/worker-loader.js
+++ b/toolkit/devtools/worker-loader.js
@@ -363,16 +363,17 @@ let loader = {
 // main thread or a worker thread. On the main thread, we use the Components
 // object to implement them. On worker threads, we use the APIs provided by
 // the worker debugger.
 
 let {
   Debugger,
   createSandbox,
   dump,
+  rpc,
   loadSubScript,
   reportError,
   setImmediate,
   xpcInspector
 } = (function () {
   if (typeof Components === "object") { // Main thread
     let {
       Constructor: CC,
@@ -400,16 +401,18 @@ let {
         invisibleToDebugger: true,
         sandboxName: name,
         sandboxPrototype: prototype,
         wantComponents: false,
         wantXrays: false
       });
     };
 
+    let rpc = undefined;
+
     let subScriptLoader = Cc['@mozilla.org/moz/jssubscript-loader;1'].
                  getService(Ci.mozIJSSubScriptLoader);
 
     let loadSubScript = function (url, sandbox) {
       subScriptLoader.loadSubScript(url, sandbox, "UTF-8");
     };
 
     let reportError = Cu.reportError;
@@ -422,16 +425,17 @@ let {
 
     let xpcInspector = Cc["@mozilla.org/jsinspector;1"].
                        getService(Ci.nsIJSInspector);
 
     return {
       Debugger,
       createSandbox,
       dump,
+      rpc,
       loadSubScript,
       reportError,
       setImmediate,
       xpcInspector
     };
   } else { // Worker thread
     let requestors = [];
 
@@ -454,16 +458,17 @@ let {
         return requestors.length;
       }
     };
 
     return {
       Debugger: this.Debugger,
       createSandbox: this.createSandbox,
       dump: this.dump,
+      rpc: this.rpc,
       loadSubScript: this.loadSubScript,
       reportError: this.reportError,
       setImmediate: this.setImmediate,
       xpcInspector: xpcInspector
     };
   }
 }).call(this);
 
@@ -472,16 +477,17 @@ let {
 
 this.worker = new WorkerDebuggerLoader({
   createSandbox: createSandbox,
   globals: {
     "isWorker": true,
     "dump": dump,
     "loader": loader,
     "reportError": reportError,
+    "rpc": rpc,
     "setImmediate": setImmediate
   },
   loadSubScript: loadSubScript,
   modules: {
     "Debugger": Debugger,
     "PromiseDebugging": PromiseDebugging,
     "Services": Object.create(null),
     "chrome": chrome,