Merge mozilla-central to mozilla-inbound on a CLOSED TREE
authorCarsten "Tomcat" Book <cbook@mozilla.com>
Fri, 15 Jul 2016 16:16:45 +0200
changeset 330123 0d82d5d030afa2e8f48dd68e86eb75efd0947a5c
parent 330122 5a9c26f8bb9d599e80c92f6a7f30ad91bd54a854 (current diff)
parent 330112 2f9e69c982f1e67887a1834b36ff0af4ababb3af (diff)
child 330124 4c05938a64a7fde3ac2d7f4493aee1c5f2ad8a0a
child 330166 5e2477a249db1cf1c5f1f5a1409ce72cc01257d2
child 330191 0c27d48b278dba5493f9c98581acfd71889f7b63
child 330198 0a961f12af55e9e54dfc6fcb3caf4155244c9e5e
push id9858
push userjlund@mozilla.com
push dateMon, 01 Aug 2016 14:37:10 +0000
treeherdermozilla-aurora@203106ef6cb6 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
milestone50.0a1
Merge mozilla-central to mozilla-inbound on a CLOSED TREE
devtools/client/shared/components/reps/url.js
devtools/client/themes/images/firebug/filter.svg
devtools/client/themes/images/firebug/timeline-filter.svg
devtools/client/themes/images/magnifying-glass-light.png
devtools/client/themes/images/magnifying-glass-light@2x.png
--- a/browser/base/content/browser.js
+++ b/browser/base/content/browser.js
@@ -6642,16 +6642,24 @@ var gIdentityHandler = {
   get _identityIcon () {
     delete this._identityIcon;
     return this._identityIcon = document.getElementById("identity-icon");
   },
   get _permissionList () {
     delete this._permissionList;
     return this._permissionList = document.getElementById("identity-popup-permission-list");
   },
+  get _permissionAnchors () {
+    delete this._permissionAnchors;
+    let permissionAnchors = {};
+    for (let anchor of document.getElementById("blocked-permissions-container").children) {
+      permissionAnchors[anchor.getAttribute("data-permission-id")] = anchor;
+    }
+    return this._permissionAnchors = permissionAnchors;
+  },
 
   /**
    * Handler for mouseclicks on the "More Information" button in the
    * "identity-popup" panel.
    */
   handleMoreInfoClick : function(event) {
     displaySecurityInfo();
     event.stopPropagation();
@@ -6899,17 +6907,42 @@ var gIdentityHandler = {
     }
 
     if (this._isCertUserOverridden) {
       this._identityBox.classList.add("certUserOverridden");
       // Cert is trusted because of a security exception, verifier is a special string.
       tooltip = gNavigatorBundle.getString("identity.identified.verified_by_you");
     }
 
-    if (SitePermissions.hasGrantedPermissions(this._uri)) {
+    let permissionAnchors = this._permissionAnchors;
+
+    // hide all permission icons
+    for (let icon of Object.values(permissionAnchors)) {
+      icon.removeAttribute("showing");
+    }
+
+    // keeps track if we should show an indicator that there are active permissions
+    let hasGrantedPermissions = false;
+
+    // show permission icons
+    for (let permission of SitePermissions.getAllByURI(this._uri)) {
+      if (permission.state === SitePermissions.BLOCK) {
+
+        let icon = permissionAnchors[permission.id];
+        if (icon) {
+          icon.setAttribute("showing", "true");
+        }
+
+      } else if (permission.state === SitePermissions.ALLOW ||
+                 permission.state === SitePermissions.SESSION) {
+        hasGrantedPermissions = true;
+      }
+    }
+
+    if (hasGrantedPermissions) {
       this._identityBox.classList.add("grantedPermissions");
     }
 
     // Push the appropriate strings out to the UI
     this._identityBox.tooltipText = tooltip;
     this._identityIcon.tooltipText = gNavigatorBundle.getString("identity.icon.tooltip");
     this._identityIconLabel.value = icon_label;
     this._identityIconCountryLabel.value = icon_country_label;
@@ -7219,17 +7252,17 @@ var gIdentityHandler = {
   },
 
   updateSitePermissions: function () {
     while (this._permissionList.hasChildNodes())
       this._permissionList.removeChild(this._permissionList.lastChild);
 
     let uri = gBrowser.currentURI;
 
-    for (let permission of SitePermissions.getPermissionsByURI(uri)) {
+    for (let permission of SitePermissions.getPermissionDetailsByURI(uri)) {
       let item = this._createPermissionItem(permission);
       this._permissionList.appendChild(item);
     }
   },
 
   setPermission: function (aPermission, aState) {
     if (aState == SitePermissions.getDefault(aPermission))
       SitePermissions.remove(gBrowser.currentURI, aPermission);
--- a/browser/base/content/browser.xul
+++ b/browser/base/content/browser.xul
@@ -720,16 +720,32 @@
                    align="center"
                    aria-label="&urlbar.viewSiteInfo.label;"
                    onclick="gIdentityHandler.handleIdentityButtonEvent(event);"
                    onkeypress="gIdentityHandler.handleIdentityButtonEvent(event);"
                    ondragstart="gIdentityHandler.onDragStart(event);">
                 <image id="identity-icon"
                        consumeanchor="identity-box"
                        onclick="PageProxyClickHandler(event);"/>
+                <box id="blocked-permissions-container" align="center">
+                  <image data-permission-id="geo" class="notification-anchor-icon geo-icon blocked" role="button"
+                         aria-label="&urlbar.geolocationNotificationAnchor.label;"/>
+                  <image data-permission-id="desktop-notification" class="notification-anchor-icon desktop-notification-icon blocked" role="button"
+                         aria-label="&urlbar.webNotsNotificationAnchor3.label;"/>
+                  <image data-permission-id="camera" class="notification-anchor-icon camera-icon blocked" role="button"
+                         aria-label="&urlbar.webRTCShareDevicesNotificationAnchor.label;"/>
+                  <image data-permission-id="indexedDB" class="notification-anchor-icon indexedDB-icon blocked" role="button"
+                         aria-label="&urlbar.indexedDBNotificationAnchor.label;"/>
+                  <image data-permission-id="microphone" class="notification-anchor-icon microphone-icon blocked" role="button"
+                         aria-label="&urlbar.webRTCShareMicrophoneNotificationAnchor.label;"/>
+                  <image data-permission-id="screen" class="notification-anchor-icon screen-icon blocked" role="button"
+                         aria-label="&urlbar.webRTCShareScreenNotificationAnchor.label;"/>
+                  <image data-permission-id="pointerLock" class="notification-anchor-icon pointerLock-icon blocked" role="button"
+                         aria-label="&urlbar.pointerLockNotificationAnchor.label;"/>
+                </box>
                 <box id="notification-popup-box"
                      hidden="true"
                      tooltiptext=""
                      onmouseover="document.getElementById('identity-icon').classList.add('no-hover');"
                      onmouseout="document.getElementById('identity-icon').classList.remove('no-hover');"
                      align="center">
                   <image id="default-notification-icon" class="notification-anchor-icon" role="button"
                          aria-label="&urlbar.defaultNotificationAnchor.label;"/>
--- a/browser/base/content/test/general/browser_permissions.js
+++ b/browser/base/content/test/general/browser_permissions.js
@@ -6,16 +6,17 @@ var {classes: Cc, interfaces: Ci, utils:
 const PERMISSIONS_PAGE = "http://example.com/browser/browser/base/content/test/general/permissions.html";
 var {SitePermissions} = Cu.import("resource:///modules/SitePermissions.jsm", {});
 
 registerCleanupFunction(function() {
   SitePermissions.remove(gBrowser.currentURI, "install");
   SitePermissions.remove(gBrowser.currentURI, "cookie");
   SitePermissions.remove(gBrowser.currentURI, "geo");
   SitePermissions.remove(gBrowser.currentURI, "camera");
+  SitePermissions.remove(gBrowser.currentURI, "microphone");
 
   while (gBrowser.tabs.length > 1) {
     gBrowser.removeCurrentTab();
   }
 });
 
 add_task(function* testMainViewVisible() {
   let {gIdentityHandler} = gBrowser.ownerGlobal;
@@ -59,25 +60,53 @@ add_task(function* testMainViewVisible()
 add_task(function* testIdentityIcon() {
   let {gIdentityHandler} = gBrowser.ownerGlobal;
   let tab = gBrowser.selectedTab = gBrowser.addTab();
   yield promiseTabLoadEvent(tab, PERMISSIONS_PAGE);
 
   gIdentityHandler.setPermission("geo", SitePermissions.ALLOW);
 
   ok(gIdentityHandler._identityBox.classList.contains("grantedPermissions"),
-    "identity-box signals granted permssions");
+    "identity-box signals granted permissions");
 
   gIdentityHandler.setPermission("geo", SitePermissions.getDefault("geo"));
 
   ok(!gIdentityHandler._identityBox.classList.contains("grantedPermissions"),
-    "identity-box doesn't signal granted permssions");
+    "identity-box doesn't signal granted permissions");
 
   gIdentityHandler.setPermission("camera", SitePermissions.BLOCK);
 
   ok(!gIdentityHandler._identityBox.classList.contains("grantedPermissions"),
-    "identity-box doesn't signal granted permssions");
+    "identity-box doesn't signal granted permissions");
 
   gIdentityHandler.setPermission("cookie", SitePermissions.SESSION);
 
   ok(gIdentityHandler._identityBox.classList.contains("grantedPermissions"),
-    "identity-box signals granted permssions");
+    "identity-box signals granted permissions");
 });
+
+add_task(function* testPermissionIcons() {
+  let {gIdentityHandler} = gBrowser.ownerGlobal;
+  let tab = gBrowser.selectedTab = gBrowser.addTab();
+  yield promiseTabLoadEvent(tab, PERMISSIONS_PAGE);
+
+  gIdentityHandler.setPermission("camera", SitePermissions.ALLOW);
+  gIdentityHandler.setPermission("geo", SitePermissions.BLOCK);
+  gIdentityHandler.setPermission("microphone", SitePermissions.SESSION);
+
+  let geoIcon = gIdentityHandler._identityBox.querySelector("[data-permission-id='geo']");
+  ok(geoIcon.hasAttribute("showing"), "blocked permission icon is shown");
+  ok(geoIcon.classList.contains("blocked"),
+    "blocked permission icon is shown as blocked");
+
+  let cameraIcon = gIdentityHandler._identityBox.querySelector("[data-permission-id='camera']");
+  ok(!cameraIcon.hasAttribute("showing"),
+    "allowed permission icon is not shown");
+
+  let microphoneIcon  = gIdentityHandler._identityBox.querySelector("[data-permission-id='microphone']");
+  ok(!microphoneIcon.hasAttribute("showing"),
+    "allowed permission icon is not shown");
+
+  gIdentityHandler.setPermission("geo", SitePermissions.getDefault("geo"));
+
+  ok(!geoIcon.hasAttribute("showing"),
+    "blocked permission icon is not shown after reset");
+});
--- a/browser/locales/en-US/chrome/browser/browser.dtd
+++ b/browser/locales/en-US/chrome/browser/browser.dtd
@@ -124,28 +124,33 @@ These should match what Safari and other
      fullscreenWarning.afterDomain.label): these two strings are used
      respectively before and after the domain requiring fullscreen.
      Localizers can use one of them, or both, to better adapt this
      sentence to their language. -->
 <!ENTITY fullscreenWarning.beforeDomain.label "">
 <!ENTITY fullscreenWarning.afterDomain.label "is now full screen">
 <!ENTITY fullscreenWarning.generic.label "This document is now full screen">
 
-<!ENTITY pointerlockWarning.beforeDomain.label "">
-<!ENTITY pointerlockWarning.afterDomain.label "has control of your pointer. Press Esc to take back control.">
-<!ENTITY pointerlockWarning.generic.label "This document has control of your pointer. Press Esc to take back control">
-
 <!-- LOCALIZATION NOTE (exitDOMFullscreen.button,
      exitDOMFullscreenMac.button): the "escape" button on PC keyboards
      is uppercase, while on Mac keyboards it is lowercase -->
 <!ENTITY exitDOMFullscreen.button "Exit Full Screen (Esc)">
 <!ENTITY exitDOMFullscreenMac.button "Exit Full Screen (esc)">
 <!ENTITY leaveDOMFullScreen.label "Exit Full Screen">
 <!ENTITY leaveDOMFullScreen.accesskey "u">
 
+<!-- LOCALIZATION NOTE (pointerlockWarning.beforeDomain.label,
+     pointerlockWarning.afterDomain.label): these two strings are used
+     respectively before and after the domain requiring pointerlock.
+     Localizers can use one of them, or both, to better adapt this
+     sentence to their language. -->
+<!ENTITY pointerlockWarning.beforeDomain.label "">
+<!ENTITY pointerlockWarning.afterDomain.label "has control of your pointer. Press Esc to take back control.">
+<!ENTITY pointerlockWarning.generic.label "This document has control of your pointer. Press Esc to take back control.">
+
 <!ENTITY closeWindow.label "Close Window">
 <!ENTITY closeWindow.accesskey "d">
 
 <!ENTITY bookmarksMenu.label "Bookmarks">
 <!ENTITY bookmarksMenu.accesskey "B">
 <!ENTITY bookmarkThisPageCmd.label "Bookmark This Page">
 <!ENTITY editThisBookmarkCmd.label "Edit This Bookmark">
 <!ENTITY bookmarkThisPageCmd.commandkey "d">
--- a/browser/modules/ReaderParent.jsm
+++ b/browser/modules/ReaderParent.jsm
@@ -40,17 +40,20 @@ var ReaderParent = {
       case "Reader:ArticleGet":
         this._getArticle(message.data.url, message.target).then((article) => {
           // Make sure the target browser is still alive before trying to send data back.
           if (message.target.messageManager) {
             message.target.messageManager.sendAsyncMessage("Reader:ArticleData", { article: article });
           }
         }, e => {
           if (e && e.newURL) {
-            message.target.loadURI("about:reader?url=" + encodeURIComponent(e.newURL));
+            // Make sure the target browser is still alive before trying to send data back.
+            if (message.target.messageManager) {
+              message.target.messageManager.sendAsyncMessage("Reader:ArticleData", { newURL: e.newURL });
+            }
           }
         });
         break;
 
       case "Reader:FaviconRequest": {
         if (message.target.messageManager) {
           let faviconUrl = PlacesUtils.promiseFaviconLinkUrl(message.data.url);
           faviconUrl.then(function onResolution(favicon) {
--- a/browser/modules/SitePermissions.jsm
+++ b/browser/modules/SitePermissions.jsm
@@ -11,73 +11,72 @@ var gStringBundle =
 
 this.SitePermissions = {
 
   UNKNOWN: Services.perms.UNKNOWN_ACTION,
   ALLOW: Services.perms.ALLOW_ACTION,
   BLOCK: Services.perms.DENY_ACTION,
   SESSION: Components.interfaces.nsICookiePermission.ACCESS_SESSION,
 
+  /* Returns all custom permissions for a given URI, the return
+   * type is a list of objects with the keys:
+   * - id: the permissionId of the permission
+   * - state: a constant representing the current permission state
+   *   (e.g. SitePermissions.ALLOW)
+   *
+   * To receive a more detailed, albeit less performant listing see
+   * SitePermissions.getPermissionDetailsByURI().
+   */
+  getAllByURI: function (aURI) {
+    let result = [];
+    if (!this.isSupportedURI(aURI)) {
+      return result;
+    }
+
+    let permissions = Services.perms.getAllForURI(aURI);
+    while (permissions.hasMoreElements()) {
+      let permission = permissions.getNext();
+
+      // filter out unknown permissions
+      if (gPermissionObject[permission.type]) {
+        result.push({
+          id: permission.type,
+          state: permission.capability,
+        });
+      }
+    }
+
+    return result;
+  },
+
   /* Returns a list of objects representing all permissions that are currently
    * set for the given URI. Each object contains the following keys:
    * - id: the permissionID of the permission
    * - label: the localized label
    * - state: a constant representing the current permission state
    *   (e.g. SitePermissions.ALLOW)
    * - availableStates: an array of all available states for that permission,
    *   represented as objects with the keys:
    *   - id: the state constant
    *   - label: the translated label of that state
    */
-  getPermissionsByURI: function (aURI) {
-    if (!this.isSupportedURI(aURI)) {
-      return [];
-    }
-
+  getPermissionDetailsByURI: function (aURI) {
     let permissions = [];
-    for (let permission of kPermissionIDs) {
-      let state = this.get(aURI, permission);
-      if (state === this.UNKNOWN) {
-        continue;
-      }
+    for (let {state, id} of this.getAllByURI(aURI)) {
+      let availableStates = this.getAvailableStates(id).map( state => {
+        return { id: state, label: this.getStateLabel(id, state) };
+      });
+      let label = this.getPermissionLabel(id);
 
-      let availableStates = this.getAvailableStates(permission).map( state => {
-        return { id: state, label: this.getStateLabel(permission, state) };
-      });
-      let label = this.getPermissionLabel(permission);
-
-      permissions.push({
-        id: permission,
-        label: label,
-        state: state,
-        availableStates: availableStates,
-      });
+      permissions.push({id, label, state, availableStates});
     }
 
     return permissions;
   },
 
-  /* Returns a boolean indicating whether there are any granted
-   * (meaning allowed or session-allowed) permissions for the given URI.
-   * Will return false for invalid URIs (such as file:// URLs).
-   */
-  hasGrantedPermissions: function (aURI) {
-    if (!this.isSupportedURI(aURI)) {
-      return false;
-    }
-
-    for (let permission of kPermissionIDs) {
-      let state = this.get(aURI, permission);
-      if (state === this.ALLOW || state === this.SESSION) {
-        return true;
-      }
-    }
-    return false;
-  },
-
   /* Checks whether a UI for managing permissions should be exposed for a given
    * URI. This excludes file URIs, for instance, as they don't have a host,
    * even though nsIPermissionManager can still handle them.
    */
   isSupportedURI: function (aURI) {
     return aURI.schemeIs("http") || aURI.schemeIs("https");
   },
 
--- a/browser/modules/test/xpcshell/test_SitePermissions.js
+++ b/browser/modules/test/xpcshell/test_SitePermissions.js
@@ -8,84 +8,79 @@ Components.utils.import("resource://gre/
 
 add_task(function* testPermissionsListing() {
   Assert.deepEqual(SitePermissions.listPermissions().sort(),
     ["camera","cookie","desktop-notification","geo","image",
      "indexedDB","install","microphone","popup"],
     "Correct list of all permissions");
 });
 
-add_task(function* testHasGrantedPermissions() {
-  // check that it returns false on an invalid URI
-  // like a file URI, which doesn't support site permissions
-  let wrongURI = Services.io.newURI("file:///example.js", null, null)
-  Assert.equal(SitePermissions.hasGrantedPermissions(wrongURI), false);
-
-  let uri = Services.io.newURI("https://example.com", null, null)
-  Assert.equal(SitePermissions.hasGrantedPermissions(uri), false);
-
-  // check that ALLOW states return true
-  SitePermissions.set(uri, "camera", SitePermissions.ALLOW);
-  Assert.equal(SitePermissions.hasGrantedPermissions(uri), true);
-
-  // removing the ALLOW state should revert to false
-  SitePermissions.remove(uri, "camera");
-  Assert.equal(SitePermissions.hasGrantedPermissions(uri), false);
-
-  // check that SESSION states return true
-  SitePermissions.set(uri, "microphone", SitePermissions.SESSION);
-  Assert.equal(SitePermissions.hasGrantedPermissions(uri), true);
-
-  // removing the SESSION state should revert to false
-  SitePermissions.remove(uri, "microphone");
-  Assert.equal(SitePermissions.hasGrantedPermissions(uri), false);
-
-  // check that a combination of ALLOW and BLOCK states returns true
-  SitePermissions.set(uri, "geo", SitePermissions.ALLOW);
-  Assert.equal(SitePermissions.hasGrantedPermissions(uri), true);
-
-  // check that a combination of SESSION and BLOCK states returns true
-  SitePermissions.set(uri, "geo", SitePermissions.SESSION);
-  Assert.equal(SitePermissions.hasGrantedPermissions(uri), true);
-
-  // check that only BLOCK states will not return true
-  SitePermissions.remove(uri, "geo");
-  Assert.equal(SitePermissions.hasGrantedPermissions(uri), false);
-
-});
-
-add_task(function* testGetPermissionsByURI() {
+add_task(function* testGetAllByURI() {
   // check that it returns an empty array on an invalid URI
   // like a file URI, which doesn't support site permissions
   let wrongURI = Services.io.newURI("file:///example.js", null, null)
-  Assert.deepEqual(SitePermissions.getPermissionsByURI(wrongURI), []);
+  Assert.deepEqual(SitePermissions.getAllByURI(wrongURI), []);
+
+  let uri = Services.io.newURI("https://example.com", null, null)
+  Assert.deepEqual(SitePermissions.getAllByURI(uri), []);
+
+  SitePermissions.set(uri, "camera", SitePermissions.ALLOW);
+  Assert.deepEqual(SitePermissions.getAllByURI(uri), [
+      { id: "camera", state: SitePermissions.ALLOW }
+  ]);
+
+  SitePermissions.set(uri, "microphone", SitePermissions.SESSION);
+  SitePermissions.set(uri, "desktop-notification", SitePermissions.BLOCK);
+
+  Assert.deepEqual(SitePermissions.getAllByURI(uri), [
+      { id: "camera", state: SitePermissions.ALLOW },
+      { id: "microphone", state: SitePermissions.SESSION },
+      { id: "desktop-notification", state: SitePermissions.BLOCK }
+  ]);
+
+  SitePermissions.remove(uri, "microphone");
+  Assert.deepEqual(SitePermissions.getAllByURI(uri), [
+      { id: "camera", state: SitePermissions.ALLOW },
+      { id: "desktop-notification", state: SitePermissions.BLOCK }
+  ]);
+
+  SitePermissions.remove(uri, "camera");
+  SitePermissions.remove(uri, "desktop-notification");
+  Assert.deepEqual(SitePermissions.getAllByURI(uri), []);
+});
+
+add_task(function* testGetPermissionDetailsByURI() {
+  // check that it returns an empty array on an invalid URI
+  // like a file URI, which doesn't support site permissions
+  let wrongURI = Services.io.newURI("file:///example.js", null, null)
+  Assert.deepEqual(SitePermissions.getPermissionDetailsByURI(wrongURI), []);
 
   let uri = Services.io.newURI("https://example.com", null, null)
 
   SitePermissions.set(uri, "camera", SitePermissions.ALLOW);
   SitePermissions.set(uri, "cookie", SitePermissions.SESSION);
   SitePermissions.set(uri, "popup", SitePermissions.BLOCK);
 
-  let permissions = SitePermissions.getPermissionsByURI(uri);
+  let permissions = SitePermissions.getPermissionDetailsByURI(uri);
 
   let camera = permissions.find(({id}) => id === "camera");
   Assert.deepEqual(camera, {
     id: "camera",
     label: "Use the Camera",
     state: SitePermissions.ALLOW,
     availableStates: [
       { id: SitePermissions.UNKNOWN, label: "Always Ask" },
       { id: SitePermissions.ALLOW, label: "Allow" },
       { id: SitePermissions.BLOCK, label: "Block" },
     ]
   });
 
   // check that removed permissions (State.UNKNOWN) are skipped
   SitePermissions.remove(uri, "camera");
-  permissions = SitePermissions.getPermissionsByURI(uri);
+  permissions = SitePermissions.getPermissionDetailsByURI(uri);
 
   camera = permissions.find(({id}) => id === "camera");
   Assert.equal(camera, undefined);
 
   // check that different available state values are represented
 
   let cookie = permissions.find(({id}) => id === "cookie");
   Assert.deepEqual(cookie, {
--- a/browser/themes/linux/searchbar.css
+++ b/browser/themes/linux/searchbar.css
@@ -206,18 +206,18 @@ menuitem[cmd="cmd_clearhistory"][disable
   display: -moz-box;
   margin-inline-end: 0;
   width: 16px;
   height: 16px;
 }
 
 .addengine-item {
   -moz-appearance: none;
-  background-color: Menu;
-  color: MenuText;
+  background-color: transparent;
+  color: inherit;
   border: none;
   height: 32px;
   margin: 0;
   padding: 0 10px;
 }
 
 .addengine-item > .button-box {
   -moz-box-pack: start;
--- a/devtools/client/animationinspector/components/animation-timeline.js
+++ b/devtools/client/animationinspector/components/animation-timeline.js
@@ -68,17 +68,17 @@ AnimationsTimeline.prototype = {
       parent: containerEl,
       attributes: {
         "class": "animation-timeline"
       }
     });
 
     let scrubberContainer = createNode({
       parent: this.rootWrapperEl,
-      attributes: {"class": "scrubber-wrapper track-container"}
+      attributes: {"class": "scrubber-wrapper"}
     });
 
     this.scrubberEl = createNode({
       parent: scrubberContainer,
       attributes: {
         "class": "scrubber"
       }
     });
@@ -87,25 +87,40 @@ AnimationsTimeline.prototype = {
       parent: this.scrubberEl,
       attributes: {
         "class": "scrubber-handle"
       }
     });
     this.scrubberHandleEl.addEventListener("mousedown",
       this.onScrubberMouseDown);
 
+    this.headerWrapper = createNode({
+      parent: this.rootWrapperEl,
+      attributes: {
+        "class": "header-wrapper"
+      }
+    });
+
     this.timeHeaderEl = createNode({
-      parent: this.rootWrapperEl,
+      parent: this.headerWrapper,
       attributes: {
         "class": "time-header track-container"
       }
     });
+
     this.timeHeaderEl.addEventListener("mousedown",
       this.onScrubberMouseDown);
 
+    this.timeTickEl = createNode({
+      parent: this.rootWrapperEl,
+      attributes: {
+        "class": "time-body track-container"
+      }
+    });
+
     this.animationsEl = createNode({
       parent: this.rootWrapperEl,
       nodeType: "ul",
       attributes: {
         "class": "animations"
       }
     });
 
@@ -449,24 +464,39 @@ AnimationsTimeline.prototype = {
     let animationDuration = TimeScale.maxEndTime - TimeScale.minStartTime;
     let minTimeInterval = TIME_GRADUATION_MIN_SPACING *
                           animationDuration / width;
     let intervalLength = findOptimalTimeInterval(minTimeInterval);
     let intervalWidth = intervalLength * width / animationDuration;
 
     // And the time graduation header.
     this.timeHeaderEl.innerHTML = "";
+    this.timeTickEl.innerHTML = "";
 
     for (let i = 0; i <= width / intervalWidth; i++) {
       let pos = 100 * i * intervalWidth / width;
 
+      // This element is the header of time tick for displaying animation
+      // duration time.
       createNode({
         parent: this.timeHeaderEl,
         nodeType: "span",
         attributes: {
-          "class": "time-tick",
+          "class": "header-item",
           "style": `left:${pos}%`
         },
         textContent: TimeScale.formatTime(TimeScale.distanceToRelativeTime(pos))
       });
+
+      // This element is displayed as a vertical line separator corresponding
+      // the header of time tick for indicating time slice for animation
+      // iterations.
+      createNode({
+        parent: this.timeTickEl,
+        nodeType: "span",
+        attributes: {
+          "class": "time-tick",
+          "style": `left:${pos}%`
+        }
+      });
     }
   }
 };
--- a/devtools/client/animationinspector/test/browser_animation_timeline_header.js
+++ b/devtools/client/animationinspector/test/browser_animation_timeline_header.js
@@ -11,33 +11,39 @@ requestLongerTimeout(2);
 const {findOptimalTimeInterval, TimeScale} = require("devtools/client/animationinspector/utils");
 
 // Should be kept in sync with TIME_GRADUATION_MIN_SPACING in
 // animation-timeline.js
 const TIME_GRADUATION_MIN_SPACING = 40;
 
 add_task(function* () {
   yield addTab(URL_ROOT + "doc_simple_animation.html");
+
+  // System scrollbar is enabled by default on our testing envionment and it
+  // would shrink width of inspector and affect number of time-ticks causing
+  // unexpected results. So, we set it wider to avoid this kind of edge case.
+  yield pushPref("devtools.toolsidebar-width.inspector", 350);
+
   let {panel} = yield openAnimationInspector();
 
   let timeline = panel.animationsTimelineComponent;
   let headerEl = timeline.timeHeaderEl;
 
   info("Find out how many time graduations should there be");
   let width = headerEl.offsetWidth;
 
   let animationDuration = TimeScale.maxEndTime - TimeScale.minStartTime;
   let minTimeInterval = TIME_GRADUATION_MIN_SPACING * animationDuration / width;
 
   // Note that findOptimalTimeInterval is tested separately in xpcshell test
   // test_findOptimalTimeInterval.js, so we assume that it works here.
   let interval = findOptimalTimeInterval(minTimeInterval);
   let nb = Math.ceil(animationDuration / interval);
 
-  is(headerEl.querySelectorAll(".time-tick").length, nb,
+  is(headerEl.querySelectorAll(".header-item").length, nb,
      "The expected number of time ticks were found");
 
   info("Make sure graduations are evenly distributed and show the right times");
   [...headerEl.querySelectorAll(".time-tick")].forEach((tick, i) => {
     let left = parseFloat(tick.style.left);
     let expectedPos = i * interval * 100 / animationDuration;
     is(Math.round(left), Math.round(expectedPos),
       `Graduation ${i} is positioned correctly`);
--- a/devtools/client/animationinspector/test/browser_animation_timeline_ui.js
+++ b/devtools/client/animationinspector/test/browser_animation_timeline_ui.js
@@ -12,17 +12,17 @@ add_task(function* () {
   yield addTab(URL_ROOT + "doc_simple_animation.html");
   let {panel} = yield openAnimationInspector();
 
   let timeline = panel.animationsTimelineComponent;
   let el = timeline.rootWrapperEl;
 
   ok(el.querySelector(".time-header"),
      "The header element is in the DOM of the timeline");
-  ok(el.querySelectorAll(".time-header .time-tick").length,
+  ok(el.querySelectorAll(".time-header .header-item").length,
      "The header has some time graduations");
 
   ok(el.querySelector(".animations"),
      "The animations container is in the DOM of the timeline");
   is(el.querySelectorAll(".animations .animation").length,
      timeline.animations.length,
      "The number of animations displayed matches the number of animations");
 
--- a/devtools/client/framework/test/browser.ini
+++ b/devtools/client/framework/test/browser.ini
@@ -59,16 +59,17 @@ skip-if = true # Bug 1177463 - Temporari
 [browser_toolbox_select_event.js]
 skip-if = e10s # Bug 1069044 - destroyInspector may hang during shutdown
 [browser_toolbox_selected_tool_unavailable.js]
 [browser_toolbox_sidebar.js]
 [browser_toolbox_sidebar_events.js]
 [browser_toolbox_sidebar_existing_tabs.js]
 [browser_toolbox_sidebar_overflow_menu.js]
 [browser_toolbox_split_console.js]
+[browser_toolbox_target.js]
 [browser_toolbox_tabsswitch_shortcuts.js]
 [browser_toolbox_textbox_context_menu.js]
 [browser_toolbox_theme_registration.js]
 [browser_toolbox_toggle.js]
 [browser_toolbox_tool_ready.js]
 [browser_toolbox_tool_remote_reopen.js]
 [browser_toolbox_transport_events.js]
 [browser_toolbox_view_source_01.js]
--- a/devtools/client/framework/test/browser_target_from_url.js
+++ b/devtools/client/framework/test/browser_target_from_url.js
@@ -33,20 +33,20 @@ add_task(function* () {
   assertIsTabTarget(target);
 
   info("Test tab with chrome privileges");
   target = yield targetFromURL(new URL("http://foo?type=tab&id=" + windowId + "&chrome"));
   assertIsTabTarget(target, true);
 
   info("Test invalid tab id");
   try {
-    yield targetFromURL(new URL("http://foo?type=tab&id=1"));
+    yield targetFromURL(new URL("http://foo?type=tab&id=10000"));
     ok(false, "Shouldn't pass");
   } catch (e) {
-    is(e.message, "targetFromURL, tab with outerWindowID:'1' doesn't exist");
+    is(e.message, "targetFromURL, tab with outerWindowID:'10000' doesn't exist");
   }
 
   info("Test parent process");
   target = yield targetFromURL(new URL("http://foo?type=process"));
   let topWindow = Services.wm.getMostRecentWindow("navigator:browser");
   is(target.url, topWindow.location.href);
   is(target.isLocalTab, false);
   is(target.chrome, true);
new file mode 100644
--- /dev/null
+++ b/devtools/client/framework/test/browser_toolbox_target.js
@@ -0,0 +1,60 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Test about:devtools-toolbox?target which allows opening a toolbox in an
+// iframe while defining which document to debug by setting a `target`
+// attribute refering to the document to debug.
+
+add_task(function *() {
+  // iframe loads the document to debug
+  let iframe = document.createElement("browser");
+  iframe.setAttribute("type", "content");
+  document.documentElement.appendChild(iframe);
+
+  let onLoad = once(iframe, "load", true);
+  iframe.setAttribute("src", "data:text/html,document to debug");
+  yield onLoad;
+  is(iframe.contentWindow.document.body.innerHTML, "document to debug");
+
+  // toolbox loads the toolbox document
+  let toolboxIframe = document.createElement("iframe");
+  document.documentElement.appendChild(toolboxIframe);
+
+  // Important step to define which target to debug
+  toolboxIframe.target = iframe;
+
+  let onToolboxReady = gDevTools.once("toolbox-ready");
+
+  onLoad = once(toolboxIframe, "load", true);
+  toolboxIframe.setAttribute("src", "about:devtools-toolbox?target");
+  yield onLoad;
+
+  // Also wait for toolbox-ready, as toolbox document load isn't enough, there
+  // is plenty of asynchronous steps during toolbox load
+  info("Waiting for toolbox-ready");
+  let toolbox = yield onToolboxReady;
+
+  let onToolboxDestroyed = gDevTools.once("toolbox-destroyed");
+  let onTabActorDetached = once(toolbox.target.client, "tabDetached");
+
+  info("Removing the iframes");
+  toolboxIframe.remove();
+
+  // And wait for toolbox-destroyed as toolbox unload is also full of
+  // asynchronous operation that outlast unload event
+  info("Waiting for toolbox-destroyed");
+  yield onToolboxDestroyed;
+  info("Toolbox destroyed");
+
+  // Also wait for tabDetached. Toolbox destroys the Target which calls
+  // TabActor.detach(). But Target doesn't wait for detach's end to resolve.
+  // Whereas it is quite important as it is a significant part of toolbox
+  // cleanup. If we do not wait for it and starts removing debugged document,
+  // the actor is still considered as being attached and continues processing
+  // events.
+  yield onTabActorDetached;
+
+  iframe.remove();
+});
--- a/devtools/client/inspector/rules/models/element-style.js
+++ b/devtools/client/inspector/rules/models/element-style.js
@@ -1,30 +1,21 @@
 /* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
 /* vim: set ts=2 et sw=2 tw=80: */
 /* 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/. */
 
 "use strict";
 
-const {Cc, Ci} = require("chrome");
 const promise = require("promise");
 const {Rule} = require("devtools/client/inspector/rules/models/rule");
 const {promiseWarn} = require("devtools/client/inspector/shared/utils");
 const {ELEMENT_STYLE} = require("devtools/shared/specs/styles");
-const {XPCOMUtils} = require("resource://gre/modules/XPCOMUtils.jsm");
-
-loader.lazyGetter(this, "PSEUDO_ELEMENTS", () => {
-  return domUtils.getCSSPseudoElementNames();
-});
-
-XPCOMUtils.defineLazyGetter(this, "domUtils", function () {
-  return Cc["@mozilla.org/inspector/dom-utils;1"].getService(Ci.inIDOMUtils);
-});
+const {getCssProperties} = require("devtools/shared/fronts/css-properties");
 
 /**
  * ElementStyle is responsible for the following:
  *   Keeps track of which properties are overridden.
  *   Maintains a list of Rule objects for a given element.
  *
  * @param {Element} element
  *        The element whose style we are viewing.
@@ -43,16 +34,17 @@ XPCOMUtils.defineLazyGetter(this, "domUt
 function ElementStyle(element, ruleView, store, pageStyle,
     showUserAgentStyles) {
   this.element = element;
   this.ruleView = ruleView;
   this.store = store || {};
   this.pageStyle = pageStyle;
   this.showUserAgentStyles = showUserAgentStyles;
   this.rules = [];
+  this.cssProperties = getCssProperties(this.ruleView.inspector.toolbox);
 
   // We don't want to overwrite this.store.userProperties so we only create it
   // if it doesn't already exist.
   if (!("userProperties" in this.store)) {
     this.store.userProperties = new UserProperties();
   }
 
   if (!("disabled" in this.store)) {
@@ -203,17 +195,17 @@ ElementStyle.prototype = {
     return true;
   },
 
   /**
    * Calls markOverridden with all supported pseudo elements
    */
   markOverriddenAll: function () {
     this.markOverridden();
-    for (let pseudo of PSEUDO_ELEMENTS) {
+    for (let pseudo of this.cssProperties.pseudoElements) {
       this.markOverridden(pseudo);
     }
   },
 
   /**
    * Mark the properties listed in this.rules for a given pseudo element
    * with an overridden flag if an earlier property overrides it.
    *
--- a/devtools/client/jar.mn
+++ b/devtools/client/jar.mn
@@ -205,20 +205,16 @@ devtools.jar:
     skin/shadereditor.css (themes/shadereditor.css)
     skin/storage.css (themes/storage.css)
     skin/splitview.css (themes/splitview.css)
     skin/styleeditor.css (themes/styleeditor.css)
     skin/webaudioeditor.css (themes/webaudioeditor.css)
     skin/components-frame.css (themes/components-frame.css)
     skin/components-h-split-box.css (themes/components-h-split-box.css)
     skin/jit-optimizations.css (themes/jit-optimizations.css)
-    skin/images/magnifying-glass.png (themes/images/magnifying-glass.png)
-    skin/images/magnifying-glass@2x.png (themes/images/magnifying-glass@2x.png)
-    skin/images/magnifying-glass-light.png (themes/images/magnifying-glass-light.png)
-    skin/images/magnifying-glass-light@2x.png (themes/images/magnifying-glass-light@2x.png)
     skin/images/filter.svg (themes/images/filter.svg)
     skin/images/search.svg (themes/images/search.svg)
     skin/images/itemToggle.svg (themes/images/itemToggle.svg)
     skin/images/itemArrow-dark-rtl.svg (themes/images/itemArrow-dark-rtl.svg)
     skin/images/itemArrow-dark-ltr.svg (themes/images/itemArrow-dark-ltr.svg)
     skin/images/itemArrow-rtl.svg (themes/images/itemArrow-rtl.svg)
     skin/images/itemArrow-ltr.svg (themes/images/itemArrow-ltr.svg)
     skin/images/noise.png (themes/images/noise.png)
@@ -338,21 +334,19 @@ devtools.jar:
     # Firebug Theme
     skin/images/firebug/read-only.svg (themes/images/firebug/read-only.svg)
     skin/images/firebug/spinner.png (themes/images/firebug/spinner.png)
     skin/images/firebug/twisty-closed-firebug.svg (themes/images/firebug/twisty-closed-firebug.svg)
     skin/images/firebug/twisty-open-firebug.svg (themes/images/firebug/twisty-open-firebug.svg)
     skin/images/firebug/arrow-down.svg (themes/images/firebug/arrow-down.svg)
     skin/images/firebug/arrow-up.svg (themes/images/firebug/arrow-up.svg)
     skin/images/firebug/close.svg (themes/images/firebug/close.svg)
-    skin/images/firebug/filter.svg (themes/images/firebug/filter.svg)
     skin/images/firebug/pause.svg (themes/images/firebug/pause.svg)
     skin/images/firebug/play.svg (themes/images/firebug/play.svg)
     skin/images/firebug/rewind.svg (themes/images/firebug/rewind.svg)
-    skin/images/firebug/timeline-filter.svg (themes/images/firebug/timeline-filter.svg)
     skin/images/firebug/disable.svg (themes/images/firebug/disable.svg)
     skin/images/firebug/breadcrumbs-divider.svg (themes/images/firebug/breadcrumbs-divider.svg)
     skin/images/firebug/breakpoint.svg (themes/images/firebug/breakpoint.svg)
     skin/images/firebug/tool-options.svg (themes/images/firebug/tool-options.svg)
     skin/images/firebug/debugger-step-in.svg (themes/images/firebug/debugger-step-in.svg)
     skin/images/firebug/debugger-step-out.svg (themes/images/firebug/debugger-step-out.svg)
     skin/images/firebug/debugger-step-over.svg (themes/images/firebug/debugger-step-over.svg)
     skin/images/firebug/pane-collapse.svg (themes/images/firebug/pane-collapse.svg)
--- a/devtools/client/jsonview/css/search-box.css
+++ b/devtools/client/jsonview/css/search-box.css
@@ -3,44 +3,22 @@
  * 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/. */
 
 /******************************************************************************/
 /* Search Box */
 
 .searchBox {
   height: 18px;
-  font-size: 12px;
-  margin-top: 0;
-  border: 1px solid rgb(170, 188, 207);
-  width: 200px;
-  position: fixed;
-  right: 5px;
-  background-image: url("search.svg");
+  font: message-box;
+  background-color: var(--theme-body-background);
+  background-image: url("chrome://devtools/skin/images/filter.svg");
   background-repeat: no-repeat;
   background-position: 2px center;
+  border: 1px solid var(--theme-splitter-color);
+  border-radius: 2px;
+  color: var(--theme-content-color1);
+  width: 200px;
+  margin-top: 0;
+  position: fixed;
+  right: 1px;
   padding-left: 20px;
 }
-
-/******************************************************************************/
-/* Light Theme & Dark Theme*/
-
-.theme-dark .searchBox,
-.theme-light .searchBox {
-  border: 1px solid rgb(170, 170, 170);
-  background-image: url("chrome://devtools/skin/images/magnifying-glass-light.png");
-  background-position: 8px center;
-  border-radius: 2px;
-  padding-left: 25px;
-  margin-top: 1px;
-  height: 16px;
-  font-style: italic;
-}
-
-/******************************************************************************/
-/* Dark Theme */
-
-.theme-dark .searchBox {
-  background-color: rgba(24, 29, 32, 1);
-  color: rgba(184, 200, 217, 1);
-  border-color: var(--theme-splitter-color);
-  background-image: url("chrome://devtools/skin/images/magnifying-glass.png");
-}
--- a/devtools/client/shared/components/reps/document.js
+++ b/devtools/client/shared/components/reps/document.js
@@ -6,19 +6,18 @@
 "use strict";
 
 // Make this available to both AMD and CJS environments
 define(function (require, exports, module) {
   // ReactJS
   const React = require("devtools/client/shared/vendor/react");
 
   // Reps
-  const { createFactories, isGrip } = require("./rep-utils");
+  const { createFactories, isGrip, getFileName } = require("./rep-utils");
   const { ObjectBox } = createFactories(require("./object-box"));
-  const { getFileName } = require("./url");
 
   // Shortcuts
   const { span } = React.DOM;
 
   /**
    * Renders DOM document object.
    */
   let Document = React.createClass({
--- a/devtools/client/shared/components/reps/function.js
+++ b/devtools/client/shared/components/reps/function.js
@@ -29,17 +29,17 @@ define(function (require, exports, modul
         return this.props.objectLink({
           object: grip
         }, "function");
       }
       return "";
     },
 
     summarizeFunction: function (grip) {
-      let name = grip.displayName || grip.name || "function";
+      let name = grip.userDisplayName || grip.displayName || grip.name || "function";
       return cropString(name + "()", 100);
     },
 
     render: function () {
       let grip = this.props.object;
 
       return (
         ObjectBox({className: "function"},
--- a/devtools/client/shared/components/reps/moz.build
+++ b/devtools/client/shared/components/reps/moz.build
@@ -26,11 +26,10 @@ DevToolsModules(
     'regexp.js',
     'rep-utils.js',
     'rep.js',
     'reps.css',
     'string.js',
     'stylesheet.js',
     'text-node.js',
     'undefined.js',
-    'url.js',
     'window.js',
 )
--- a/devtools/client/shared/components/reps/number.js
+++ b/devtools/client/shared/components/reps/number.js
@@ -15,31 +15,36 @@ define(function (require, exports, modul
 
   /**
    * Renders a number
    */
   const Number = React.createClass({
     displayName: "Number",
 
     stringify: function (object) {
-      return (Object.is(object, -0) ? "-0" : String(object));
+      let isNegativeZero = Object.is(object, -0) ||
+        (object.type && object.type == "-0");
+
+      return (isNegativeZero ? "-0" : String(object));
     },
 
     render: function () {
       let value = this.props.object;
+
       return (
         ObjectBox({className: "number"},
           this.stringify(value)
         )
       );
     }
   });
 
   function supportsObject(object, type) {
-    return type == "boolean" || type == "number";
+    return type == "boolean" || type == "number" ||
+      (type == "object" && object.type == "-0");
   }
 
   // Exports from this module
 
   exports.Number = {
     rep: Number,
     supportsObject: supportsObject
   };
--- a/devtools/client/shared/components/reps/object-with-text.js
+++ b/devtools/client/shared/components/reps/object-with-text.js
@@ -38,17 +38,17 @@ define(function (require, exports, modul
       return "";
     },
 
     getType: function (grip) {
       return grip.class;
     },
 
     getDescription: function (grip) {
-      return (grip.preview.kind == "ObjectWithText") ? grip.preview.text : "";
+      return "\"" + grip.preview.text + "\"";
     },
 
     render: function () {
       let grip = this.props.object;
       return (
         ObjectBox({className: this.getType(grip)},
           this.getTitle(grip),
           span({className: "objectPropValue"},
--- a/devtools/client/shared/components/reps/rep-utils.js
+++ b/devtools/client/shared/components/reps/rep-utils.js
@@ -1,8 +1,9 @@
+/* globals URLSearchParams */
 /* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
 /* vim: set ft=javascript ts=2 et sw=2 tw=80: */
 /* 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/. */
 
 "use strict";
 
@@ -68,14 +69,82 @@ define(function (require, exports, modul
     if (text.length > limit) {
       return text.substr(0, Math.ceil(halfLimit)) + alternativeText +
         text.substr(text.length - Math.floor(halfLimit));
     }
 
     return text;
   }
 
+  function parseURLParams(url) {
+    url = new URL(url);
+    return parseURLEncodedText(url.searchParams);
+  }
+
+  function parseURLEncodedText(text) {
+    let params = [];
+
+    // In case the text is empty just return the empty parameters
+    if (text == "") {
+      return params;
+    }
+
+    let searchParams = new URLSearchParams(text);
+    let entries = [...searchParams.entries()];
+    return entries.map(entry => {
+      return {
+        name: entry[0],
+        value: entry[1]
+      };
+    });
+  }
+
+  function getFileName(url) {
+    let split = splitURLBase(url);
+    return split.name;
+  }
+
+  function splitURLBase(url) {
+    if (!isDataURL(url)) {
+      return splitURLTrue(url);
+    }
+    return {};
+  }
+
+  function isDataURL(url) {
+    return (url && url.substr(0, 5) == "data:");
+  }
+
+  function splitURLTrue(url) {
+    const reSplitFile = /(.*?):\/{2,3}([^\/]*)(.*?)([^\/]*?)($|\?.*)/;
+    let m = reSplitFile.exec(url);
+
+    if (!m) {
+      return {
+        name: url,
+        path: url
+      };
+    } else if (m[4] == "" && m[5] == "") {
+      return {
+        protocol: m[1],
+        domain: m[2],
+        path: m[3],
+        name: m[3] != "/" ? m[3] : m[2]
+      };
+    }
+
+    return {
+      protocol: m[1],
+      domain: m[2],
+      path: m[2] + m[3],
+      name: m[4] + m[5]
+    };
+  }
+
   // Exports from this module
   exports.createFactories = createFactories;
   exports.isGrip = isGrip;
   exports.cropString = cropString;
   exports.cropMultipleLines = cropMultipleLines;
+  exports.parseURLParams = parseURLParams;
+  exports.parseURLEncodedText = parseURLEncodedText;
+  exports.getFileName = getFileName;
 });
--- a/devtools/client/shared/components/reps/stylesheet.js
+++ b/devtools/client/shared/components/reps/stylesheet.js
@@ -6,19 +6,18 @@
 "use strict";
 
 // Make this available to both AMD and CJS environments
 define(function (require, exports, module) {
   // ReactJS
   const React = require("devtools/client/shared/vendor/react");
 
   // Reps
-  const { createFactories, isGrip } = require("./rep-utils");
+  const { createFactories, isGrip, getFileName } = require("./rep-utils");
   const { ObjectBox } = createFactories(require("./object-box"));
-  const { getFileName } = require("./url");
 
   // Shortcuts
   const DOM = React.DOM;
 
   /**
    * Renders a grip representing CSSStyleSheet
    */
   let StyleSheet = React.createClass({
deleted file mode 100644
--- a/devtools/client/shared/components/reps/url.js
+++ /dev/null
@@ -1,81 +0,0 @@
-/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
-/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
-/* 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/. */
-/* global URLSearchParams */
-
-"use strict";
-
-// Make this available to both AMD and CJS environments
-define(function (require, exports, module) {
-  function parseURLParams(url) {
-    url = new URL(url);
-    return parseURLEncodedText(url.searchParams);
-  }
-
-  function parseURLEncodedText(text) {
-    let params = [];
-
-    // In case the text is empty just return the empty parameters
-    if (text == "") {
-      return params;
-    }
-
-    let searchParams = new URLSearchParams(text);
-    let entries = [...searchParams.entries()];
-    return entries.map(entry => {
-      return {
-        name: entry[0],
-        value: entry[1]
-      };
-    });
-  }
-
-  function getFileName(url) {
-    let split = splitURLBase(url);
-    return split.name;
-  }
-
-  function splitURLBase(url) {
-    if (!isDataURL(url)) {
-      return splitURLTrue(url);
-    }
-    return {};
-  }
-
-  function isDataURL(url) {
-    return (url && url.substr(0, 5) == "data:");
-  }
-
-  function splitURLTrue(url) {
-    const reSplitFile = /(.*?):\/{2,3}([^\/]*)(.*?)([^\/]*?)($|\?.*)/;
-    let m = reSplitFile.exec(url);
-
-    if (!m) {
-      return {
-        name: url,
-        path: url
-      };
-    } else if (m[4] == "" && m[5] == "") {
-      return {
-        protocol: m[1],
-        domain: m[2],
-        path: m[3],
-        name: m[3] != "/" ? m[3] : m[2]
-      };
-    }
-
-    return {
-      protocol: m[1],
-      domain: m[2],
-      path: m[2] + m[3],
-      name: m[4] + m[5]
-    };
-  }
-
-  // Exports from this module
-  exports.parseURLParams = parseURLParams;
-  exports.parseURLEncodedText = parseURLEncodedText;
-  exports.getFileName = getFileName;
-});
--- a/devtools/client/shared/components/test/mochitest/test_reps_function.html
+++ b/devtools/client/shared/components/test/mochitest/test_reps_function.html
@@ -47,16 +47,32 @@ window.onload = Task.async(function* () 
         mode: undefined,
         expectedOutput: defaultOutput,
       }
     ];
 
     testRepRenderModes(modeTests, testName, componentUnderTest, getGripStub(testName));
   }
 
+  function testUserNamed() {
+    // Test declaration: `function testName{ let innerVar = "foo" }`
+    const testName = "testUserNamed";
+
+    const defaultOutput = `testUserName()`;
+
+    const modeTests = [
+      {
+        mode: undefined,
+        expectedOutput: defaultOutput,
+      }
+    ];
+
+    testRepRenderModes(modeTests, testName, componentUnderTest, getGripStub(testName));
+  }
+
   function testVarNamed() {
     // Test declaration: `let testVarName = function() { }`
     const testName = "testVarNamed";
 
     const defaultOutput = `testVarName()`;
 
     const modeTests = [
       {
@@ -113,16 +129,33 @@ window.onload = Task.async(function* () 
           "name": "testName",
           "displayName": "testName",
           "location": {
             "url": "debugger eval code",
             "line": 1
           }
         };
 
+      case "testUserNamed":
+        return {
+          "type": "object",
+          "class": "Function",
+          "actor": "server1.conn6.obj35",
+          "extensible": true,
+          "frozen": false,
+          "sealed": false,
+          "name": "testName",
+          "userDisplayName": "testUserName",
+          "displayName": "testName",
+          "location": {
+            "url": "debugger eval code",
+            "line": 1
+          }
+        };
+
       case "testVarNamed":
         return {
           "type": "object",
           "class": "Function",
           "actor": "server1.conn7.obj41",
           "extensible": true,
           "frozen": false,
           "sealed": false,
--- a/devtools/client/shared/components/test/mochitest/test_reps_number.html
+++ b/devtools/client/shared/components/test/mochitest/test_reps_number.html
@@ -16,16 +16,17 @@ Test Number rep
 <script type="application/javascript;version=1.8">
 window.onload = Task.async(function* () {
   let { Rep } = browserRequire("devtools/client/shared/components/reps/rep");
   let { Number } = browserRequire("devtools/client/shared/components/reps/number");
 
   try {
     yield testInt();
     yield testBoolean();
+    yield testNegativeZero();
     yield testUnsafeInt();
   } catch(e) {
     ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
   } finally {
     SimpleTest.finish();
   }
 
 
@@ -43,32 +44,51 @@ window.onload = Task.async(function* () 
 
     let renderedComponent = renderComponent(Number.rep, { object: getGripStub("testTrue") });
     is(renderedComponent.textContent, "true", "Number rep has expected text content for boolean true");
 
     renderedComponent = renderComponent(Number.rep, { object: getGripStub("testFalse") });
     is(renderedComponent.textContent, "false", "Number rep has expected text content for boolean false");
   }
 
+  function testNegativeZero() {
+    const renderedRep = shallowRenderComponent(Rep, { object: getGripStub("testNegZeroGrip") });
+    is(renderedRep.type, Number.rep, `Rep correctly selects ${Number.rep.displayName} for negative zero value`);
+
+    let renderedComponent = renderComponent(Number.rep, { object: getGripStub("testNegZeroGrip") });
+    is(renderedComponent.textContent, "-0", "Number rep has expected text content for negative zero grip");
+
+    renderedComponent = renderComponent(Number.rep, { object: getGripStub("testNegZeroValue") });
+    is(renderedComponent.textContent, "-0", "Number rep has expected text content for negative zero value");
+  }
+
   function testUnsafeInt() {
     const renderedComponent = renderComponent(Number.rep, { object: getGripStub("testUnsafeInt") });
     is(renderedComponent.textContent, "900719925474099100", "Number rep has expected text content for a long number");
   }
 
   function getGripStub(name) {
     switch (name) {
       case "testInt":
         return 5;
 
       case "testTrue":
         return true;
 
       case "testFalse":
         return false;
 
+      case "testNegZeroValue":
+        return -0;
+
+      case "testNegZeroGrip":
+        return {
+          "type": "-0"
+        };
+
       case "testUnsafeInt":
         return 900719925474099122;
     }
   }
 });
 </script>
 </pre>
 </body>
--- a/devtools/client/shared/components/test/mochitest/test_reps_object-with-text.html
+++ b/devtools/client/shared/components/test/mochitest/test_reps_object-with-text.html
@@ -34,17 +34,17 @@ window.onload = Task.async(function* () 
     };
 
     // Test that correct rep is chosen
     const renderedRep = shallowRenderComponent(Rep, { object: gripStub });
     is(renderedRep.type, ObjectWithText.rep, `Rep correctly selects ${ObjectWithText.rep.displayName}`);
 
     // Test rendering
     const renderedComponent = renderComponent(ObjectWithText.rep, { object: gripStub });
-    is(renderedComponent.textContent, ".Shadow", "ObjectWithText rep has expected text content");
+    is(renderedComponent.textContent, "\".Shadow\"", "ObjectWithText rep has expected text content");
   } catch(e) {
     ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
   } finally {
     SimpleTest.finish();
   }
 });
 </script>
 </pre>
--- a/devtools/client/shared/css-angle.js
+++ b/devtools/client/shared/css-angle.js
@@ -5,16 +5,18 @@
 "use strict";
 
 const SPECIALVALUES = new Set([
   "initial",
   "inherit",
   "unset"
 ]);
 
+const {getCSSLexer} = require("devtools/shared/css-lexer");
+
 /**
  * This module is used to convert between various angle units.
  *
  * Usage:
  *   let {require} = Cu.import("resource://devtools/shared/Loader.jsm", {});
  *   let {angleUtils} = require("devtools/client/shared/css-angle");
  *   let angle = new angleUtils.CssAngle("180deg");
  *
@@ -61,17 +63,22 @@ CssAngle.prototype = {
     return this._angleUnit;
   },
 
   set angleUnit(unit) {
     this._angleUnit = unit;
   },
 
   get valid() {
-    return /^-?\d+\.?\d*(deg|rad|grad|turn)$/gi.test(this.authored);
+    let token = getCSSLexer(this.authored).nextToken();
+    if (!token) {
+      return false;
+    }
+    return (token.tokenType === "dimension"
+      && token.text.toLowerCase() in CssAngle.ANGLEUNIT);
   },
 
   get specialValue() {
     return SPECIALVALUES.has(this.lowerCased) ? this.authored : null;
   },
 
   get deg() {
     let invalidOrSpecialValue = this._getInvalidOrSpecialValue();
--- a/devtools/client/shared/output-parser.js
+++ b/devtools/client/shared/output-parser.js
@@ -151,17 +151,17 @@ OutputParser.prototype = {
 
     let colorOK = function () {
       return options.supportsColor ||
         (options.expectFilter && parenDepth === 1 &&
          outerMostFunctionTakesColor);
     };
 
     let angleOK = function (angle) {
-      return /^-?\d+\.?\d*(deg|rad|grad|turn)$/gi.test(angle);
+      return (new angleUtils.CssAngle(angle)).valid;
     };
 
     while (true) {
       let token = tokenStream.nextToken();
       if (!token) {
         break;
       }
       if (token.tokenType === "comment") {
--- a/devtools/client/shared/test/browser_css_angle.js
+++ b/devtools/client/shared/test/browser_css_angle.js
@@ -8,16 +8,17 @@ const TEST_URI = "data:text/html;charset
 var {angleUtils} = require("devtools/client/shared/css-angle");
 
 add_task(function* () {
   yield addTab("about:blank");
   let [host] = yield createHost("bottom", TEST_URI);
 
   info("Starting the test");
   testAngleUtils();
+  testAngleValidity();
 
   host.destroy();
   gBrowser.removeCurrentTab();
 });
 
 function testAngleUtils() {
   let data = getTestData();
 
@@ -30,30 +31,77 @@ function testAngleUtils() {
     is(angle.rad, rad, "color.rad === rad");
     is(angle.grad, grad, "color.grad === grad");
     is(angle.turn, turn, "color.turn === turn");
 
     testToString(angle, deg, rad, grad, turn);
   }
 }
 
+function testAngleValidity() {
+  let data = getAngleValidityData();
+
+  for (let {angle, result} of data) {
+    let testAngle = new angleUtils.CssAngle(angle);
+
+    is(testAngle.valid, result, `Testing that "${angle}" is ${testAngle.valid ? " a valid" : "an invalid" } angle`);
+  }
+}
+
 function testToString(angle, deg, rad, grad, turn) {
   angle.angleUnit = angleUtils.CssAngle.ANGLEUNIT.deg;
   is(angle.toString(), deg, "toString() with deg type");
 
   angle.angleUnit = angleUtils.CssAngle.ANGLEUNIT.rad;
   is(angle.toString(), rad, "toString() with rad type");
 
   angle.angleUnit = angleUtils.CssAngle.ANGLEUNIT.grad;
   is(angle.toString(), grad, "toString() with grad type");
 
   angle.angleUnit = angleUtils.CssAngle.ANGLEUNIT.turn;
   is(angle.toString(), turn, "toString() with turn type");
 }
 
+function getAngleValidityData() {
+  return [{
+    angle: "0.2turn",
+    result: true
+  }, {
+    angle: "-0.2turn",
+    result: true
+  }, {
+    angle: "-.2turn",
+    result: true
+  }, {
+    angle: "1e02turn",
+    result: true
+  }, {
+    angle: "-2e2turn",
+    result: true
+  }, {
+    angle: ".2turn",
+    result: true
+  }, {
+    angle: "0.2aaturn",
+    result: false
+  }, {
+    angle: "2dega",
+    result: false
+  }, {
+    angle: "0.deg",
+    result: false
+  }, {
+    angle: ".deg",
+    result: false
+  }, {
+    angle: "..2turn",
+    result: false
+  }];
+}
+
 function getTestData() {
   return [{
     authored: "0deg",
     deg: "0deg",
     rad: "0rad",
     grad: "0grad",
     turn: "0turn"
   }, {
--- a/devtools/client/themes/animationinspector.css
+++ b/devtools/client/themes/animationinspector.css
@@ -102,17 +102,18 @@ body {
 [timeline] #timeline-toolbar {
   display: flex;
 }
 
 /* The main animations container */
 
 #players {
   height: calc(100% - var(--toolbar-height));
-  overflow: auto;
+  overflow-x: hidden;
+  overflow-y: auto;
 }
 
 [empty] #players {
   display: none;
 }
 
 /* The error message, shown when an invalid/unanimated element is selected */
 
@@ -195,18 +196,16 @@ body {
 #timeline-rate {
   position: relative;
   width: 4.5em;
 }
 
 /* Animation timeline component */
 
 .animation-timeline {
-  height: 100%;
-  overflow: hidden;
   position: relative;
   display: flex;
   flex-direction: column;
 }
 
 /* Useful for positioning animations or keyframes in the timeline */
 .animation-timeline .track-container {
   position: absolute;
@@ -214,77 +213,104 @@ body {
   left: var(--timeline-sidebar-width);
   /* Leave the width of a marker right of a track so the 100% markers can be
      selected easily */
   right: var(--keyframes-marker-size);
   height: var(--timeline-animation-height);
 }
 
 .animation-timeline .scrubber-wrapper {
-  z-index: 2;
-  pointer-events: none;
+  position: absolute;
+  left: var(--timeline-sidebar-width);
+  /* Leave the width of a marker right of a track so the 100% markers can be
+     selected easily */
+  right: var(--keyframes-marker-size);
   height: 100%;
 }
 
 .animation-timeline .scrubber {
+  z-index: 5;
+  pointer-events: none;
   position: absolute;
-  height: 100%;
+  /* Make the scrubber as tall as the viewport minus the toolbar height and the
+     header-wrapper's borders */
+  height: calc(100vh - var(--toolbar-height) - 1px);
+  min-height: 100%;
   width: 0;
   border-right: 1px solid red;
   box-sizing: border-box;
 }
 
-.animation-timeline .scrubber::before {
-  content: "";
-  position: absolute;
-  top: 0;
-  width: 1px;
-  right: -6px;
-  border-top: 5px solid red;
-  border-left: 5px solid transparent;
-  border-right: 5px solid transparent;
-}
-
 /* The scrubber handle is a transparent element displayed on top of the scrubber
    line that allows users to drag it */
 .animation-timeline .scrubber .scrubber-handle {
   position: absolute;
   height: 100%;
-  top: 0;
   /* Make it thick enough for easy dragging */
   width: 6px;
-  right: -3px;
+  right: -1.5px;
   cursor: col-resize;
   pointer-events: all;
 }
 
+.animation-timeline .scrubber .scrubber-handle::before {
+  content: "";
+  position: sticky;
+  top: 0;
+  width: 1px;
+  border-top: 5px solid red;
+  border-left: 5px solid transparent;
+  border-right: 5px solid transparent;
+}
+
 .animation-timeline .time-header {
-  min-height: var(--toolbar-height);
+  min-height: var(--timeline-animation-height);
   cursor: col-resize;
   -moz-user-select: none;
 }
 
-.animation-timeline .time-header .time-tick {
+.animation-timeline .time-header .header-item {
   position: absolute;
+  height: 100%;
+  padding-top: 3px;
+  border-left: 0.5px solid var(--time-graduation-border-color);
+}
+
+.animation-timeline .header-wrapper {
+  position: sticky;
   top: 0;
-  height: 100vh;
-  padding-top: 3px;
+  background-color: var(--theme-body-background);
+  border-bottom: 1px solid var(--time-graduation-border-color);
+  z-index: 3;
+  height: var(--timeline-animation-height);
+  overflow: hidden;
+}
+
+.animation-timeline .time-body {
+  height: 100%;
+}
+
+.animation-timeline .time-body .time-tick {
+  -moz-user-select: none;
+  position: absolute;
+  width: 0;
+  /* When scroll bar is shown, make it covers entire time-body */
+  height: 100%;
+  /* When scroll bar is hidden, make it as tall as the viewport minus the
+     timeline animation height and the header-wrapper's borders */
+  min-height: calc(100vh - var(--timeline-animation-height) - 1px);
   border-left: 0.5px solid var(--time-graduation-border-color);
 }
 
 .animation-timeline .animations {
   width: 100%;
   height: 100%;
-  overflow-y: auto;
-  overflow-x: hidden;
-  /* Leave some space for the header */
-  margin-top: var(--timeline-animation-height);
   padding: 0;
   list-style-type: none;
-  border-top: 1px solid var(--time-graduation-border-color);
+  margin-top: 0;
 }
 
 /* Animation block widgets */
 
 .animation-timeline .animation {
   margin: 2px 0;
   height: var(--timeline-animation-height);
   position: relative;
deleted file mode 100644
--- a/devtools/client/themes/images/firebug/filter.svg
+++ /dev/null
@@ -1,25 +0,0 @@
-<!-- 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" xmlns:xlink="http://www.w3.org/1999/xlink" width="12" height="12" viewBox="0 0 12 12">
-  <defs>
-    <linearGradient id="b">
-      <stop offset="0" stop-color="#646464"/>
-      <stop offset=".734" stop-color="#c8c8c8"/>
-      <stop offset="1" stop-color="#b4b4b4"/>
-    </linearGradient>
-    <linearGradient id="a">
-      <stop offset="0" stop-color="#787878"/>
-      <stop offset=".764" stop-color="#dcdcdc"/>
-      <stop offset="1" stop-color="#c8c8c8"/>
-    </linearGradient>
-    <linearGradient xlink:href="#a" id="e" x1="7.336" y1="8.379" x2="4.585" y2="8.379" gradientUnits="userSpaceOnUse" gradientTransform="matrix(.991 0 0 1 .083 1040.346)"/>
-    <linearGradient xlink:href="#b" id="f" x1="7.715" y1="7.526" x2="4.206" y2="7.526" gradientUnits="userSpaceOnUse" gradientTransform="matrix(.991 0 0 1 .083 1040.346)"/>
-    <linearGradient xlink:href="#b" id="d" x1="9.755" y1="1044.158" x2="2.188" y2="1044.158" gradientUnits="userSpaceOnUse" gradientTransform="scale(1.005 1)"/>
-    <linearGradient xlink:href="#a" id="c" x1="9.597" y1="1042.528" x2="2.261" y2="1042.528" gradientUnits="userSpaceOnUse" gradientTransform="scale(1.005 1)"/>
-  </defs>
-  <g stroke-width=".4" stroke-linejoin="round">
-    <path d="M.2 1041.536l3.975 5.244h3.65l3.975-5.242z" fill="url(#c)" stroke="url(#d)" transform="translate(0 -1040.362)"/>
-    <path d="M7.8 1046.764H4.2v3.898h3.6z" fill="url(#e)" stroke="url(#f)" transform="translate(0 -1040.362)"/>
-  </g>
-</svg>
deleted file mode 100644
--- a/devtools/client/themes/images/firebug/timeline-filter.svg
+++ /dev/null
@@ -1,6 +0,0 @@
-<!-- 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="16" height="16" viewBox="0 0 16 16">
-  <path d="M2 2v3l5 4v6h2V9l5-4V2z" fill="#3bace5"/>
-</svg>
deleted file mode 100644
index e8c1841588a52bd11935724ef92112a58e2f3c7e..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
GIT binary patch
literal 0
Hc$@<O00001
deleted file mode 100644
index d784870c380da76e4897aad41c4c87cc848dd50e..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
GIT binary patch
literal 0
Hc$@<O00001
--- a/devtools/client/themes/performance.css
+++ b/devtools/client/themes/performance.css
@@ -5,35 +5,32 @@
 
 /* CSS Variables specific to this panel that aren't defined by the themes */
 .theme-dark {
   --cell-border-color: rgba(255,255,255,0.15);
   --cell-border-color-light: rgba(255,255,255,0.1);
   --focus-cell-border-color: rgba(255,255,255,0.5);
   --row-alt-background-color: rgba(86, 117, 185, 0.15);
   --row-hover-background-color: rgba(86, 117, 185, 0.25);
-  --filter-image: url(chrome://devtools/skin/images/filter.svg);
 }
 
 .theme-light {
   --cell-border-color: rgba(0,0,0,0.15);
   --cell-border-color-light: rgba(0,0,0,0.1);
   --focus-cell-border-color: rgba(0,0,0,0.3);
   --row-alt-background-color: rgba(76,158,217,0.1);
   --row-hover-background-color: rgba(76,158,217,0.2);
-  --filter-image: url(chrome://devtools/skin/images/filter.svg);
 }
 
 .theme-firebug {
   --cell-border-color: rgba(0,0,0,0.15);
   --cell-border-color-light: rgba(0,0,0,0.1);
   --focus-cell-border-color: rgba(0,0,0,0.3);
   --row-alt-background-color: rgba(76,158,217,0.1);
   --row-hover-background-color: rgba(76,158,217,0.2);
-  --filter-image: url(chrome://devtools/skin/images/firebug/timeline-filter.svg);
 }
 
 /**
  * A generic class to hide elements, replacing the `element.hidden` attribute
  * that we use to hide elements that can later be active
  */
 .hidden {
   display: none;
@@ -48,17 +45,17 @@
 }
 
 #performance-toolbar-controls-detail-views .toolbarbutton-text {
   padding-inline-start: 4px;
   padding-inline-end: 8px;
 }
 
 #filter-button {
-  list-style-image: var(--filter-image);
+  list-style-image: url(images/filter.svg);
 }
 
 #performance-filter-menupopup > menuitem .menu-iconic-left::after {
   content: "";
   display: block;
   width: 8px;
   height: 8px;
   margin: 0 8px;
--- a/devtools/client/themes/rules.css
+++ b/devtools/client/themes/rules.css
@@ -1,26 +1,26 @@
 /* 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/. */
 
 /* CSS Variables specific to this panel that aren't defined by the themes */
 .theme-light {
   --rule-highlight-background-color: #ffee99;
-  --rule-filter-icon: url(images/magnifying-glass-light.png);
+  --rule-filter-icon: url(images/filter.svg);
 }
 
 .theme-dark {
   --rule-highlight-background-color: #594724;
-  --rule-filter-icon: url(images/magnifying-glass.png);
+  --rule-filter-icon: url(images/filter.svg);
 }
 
 .theme-firebug {
   --rule-highlight-background-color: #ffee99;
-  --rule-filter-icon: url(images/magnifying-glass-light.png);
+  --rule-filter-icon: url(images/filter.svg);
   --rule-property-name: darkgreen;
   --rule-property-value: darkblue;
 }
 
 /* Rule View Tabpanel */
 
 .theme-firebug .ruleview {
   font-family: monospace;
--- a/devtools/client/themes/toolbars.css
+++ b/devtools/client/themes/toolbars.css
@@ -9,91 +9,39 @@
   --toolbar-tab-hover-active: rgba(170, 170, 170, .4);
   --searchbox-background-color: #ffee99;
   --searchbox-border-color: #ffbf00;
   --searcbox-no-match-background-color: #ffe5e5;
   --searcbox-no-match-border-color: #e52e2e;
   --magnifying-glass-image: url(images/search.svg);
   --filter-image: url(images/filter.svg);
   --tool-options-image: url(images/tool-options.svg);
-  --close-button-image: url(chrome://devtools/skin/images/close.svg);
   --icon-filter: invert(1);
-  --dock-bottom-image: url(chrome://devtools/skin/images/dock-bottom.svg);
-  --dock-side-image: url(chrome://devtools/skin/images/dock-side.svg);
-  --dock-undock-image: url(chrome://devtools/skin/images/dock-undock.svg);
   --toolbar-button-border-color: rgba(170, 170, 170, .5);
-
-  /* Toolbox buttons */
-  --command-paintflashing-image: url(images/command-paintflashing.svg);
-  --command-screenshot-image: url(images/command-screenshot.svg);
-  --command-responsive-image: url(images/command-responsivemode.svg);
-  --command-scratchpad-image: url(images/command-scratchpad.svg);
-  --command-pick-image: url(images/command-pick.svg);
-  --command-frames-image: url(images/command-frames.svg);
-  --command-splitconsole-image: url(images/command-console.svg);
-  --command-noautohide-image: url(images/command-noautohide.svg);
-  --command-eyedropper-image: url(images/command-eyedropper.svg);
-  --command-rulers-image: url(images/command-rulers.svg);
-  --command-measure-image: url(images/command-measure.svg);
 }
 
 .theme-dark {
   --toolbar-tab-hover: hsla(206, 37%, 4%, .2);
   --toolbar-tab-hover-active: hsla(206, 37%, 4%, .4);
   --searchbox-background-color: #4d4222;
   --searchbox-border-color: #d99f2b;
   --searcbox-no-match-background-color: #402325;
   --searcbox-no-match-border-color: #cc3d3d;
   --magnifying-glass-image: url(images/search.svg);
   --filter-image: url(images/filter.svg);
   --tool-options-image: url(images/tool-options.svg);
-  --close-button-image: url(chrome://devtools/skin/images/close.svg);
   --icon-filter: none;
-  --dock-bottom-image: url(chrome://devtools/skin/images/dock-bottom.svg);
-  --dock-side-image: url(chrome://devtools/skin/images/dock-side.svg);
-  --dock-undock-image: url(chrome://devtools/skin/images/dock-undock.svg);
   --toolbar-button-border-color: rgba(0, 0, 0, .4);
-
-  /* Toolbox buttons */
-  --command-paintflashing-image: url(images/command-paintflashing.svg);
-  --command-screenshot-image: url(images/command-screenshot.svg);
-  --command-responsive-image: url(images/command-responsivemode.svg);
-  --command-scratchpad-image: url(images/command-scratchpad.svg);
-  --command-pick-image: url(images/command-pick.svg);
-  --command-frames-image: url(images/command-frames.svg);
-  --command-splitconsole-image: url(images/command-console.svg);
-  --command-noautohide-image: url(images/command-noautohide.svg);
-  --command-eyedropper-image: url(images/command-eyedropper.svg);
-  --command-rulers-image: url(images/command-rulers.svg);
-  --command-measure-image: url(images/command-measure.svg);
 }
 
 .theme-firebug {
-  --magnifying-glass-image: url(images/firebug/filter.svg);
-  --magnifying-glass-image-2x: url(images/firebug/filter.svg);
+  --magnifying-glass-image: url(images/search.svg);
   --tool-options-image: url(images/firebug/tool-options.svg);
-  --close-button-image: url(chrome://devtools/skin/images/firebug/close.svg);
   --icon-filter: invert(1);
-  --dock-bottom-image: url(chrome://devtools/skin/images/firebug/dock-bottom.svg);
-  --dock-side-image: url(chrome://devtools/skin/images/firebug/dock-side.svg);
-  --dock-undock-image: url(chrome://devtools/skin/images/firebug/dock-undock.svg);
   --toolbar-button-border-color: rgba(170, 170, 170, .5);
-
-  /* Toolbox buttons */
-  --command-paintflashing-image: url(images/firebug/command-paintflashing.svg);
-  --command-screenshot-image: url(images/firebug/command-screenshot.svg);
-  --command-responsive-image: url(images/firebug/command-responsivemode.svg);
-  --command-scratchpad-image: url(images/firebug/command-scratchpad.svg);
-  --command-pick-image: url(images/firebug/command-pick.svg);
-  --command-frames-image: url(images/firebug/command-frames.svg);
-  --command-splitconsole-image: url(images/firebug/command-console.svg);
-  --command-noautohide-image: url(images/firebug/command-noautohide.svg);
-  --command-eyedropper-image: url(images/firebug/command-eyedropper.svg);
-  --command-rulers-image: url(images/firebug/command-rulers.svg);
-  --command-measure-image: url(images/firebug/command-measure.svg);
 }
 
 
 /* Toolbars */
 .devtools-toolbar,
 .devtools-sidebar-tabs tabs {
   -moz-appearance: none;
   padding: 0;
@@ -122,16 +70,25 @@
 .devtools-toolbar checkbox .checkbox-label-box {
   border: none !important; /* overrides .checkbox-label-box from checkbox.css */
 }
 .devtools-toolbar checkbox .checkbox-label-box .checkbox-label {
   margin: 0 6px !important; /* overrides .checkbox-label from checkbox.css */
   padding: 0;
 }
 
+.devtools-separator {
+  margin: 0 2px;
+  width: 2px;
+  background-image: linear-gradient(transparent 15%, var(--theme-splitter-color) 15%, var(--theme-splitter-color) 85%, transparent 85%);
+  background-size: 1px 100%;
+  background-repeat: no-repeat;
+  background-position: 0, 1px, 2px;
+}
+
 /* Toolbar buttons */
 
 .devtools-menulist,
 .devtools-toolbarbutton,
 .devtools-button {
   -moz-appearance: none;
   background: transparent;
   min-height: 18px;
@@ -540,22 +497,16 @@
   margin-bottom: 0;
 }
 
 .devtools-searchinput > .textbox-input-box > .textbox-search-icons > .textbox-search-clear:hover,
 .devtools-filterinput > .textbox-input-box > .textbox-search-icons > .textbox-search-clear:hover {
   -moz-image-region: rect(0, 32px, 16px, 16px);
 }
 
-/* Close button */
-
-#toolbox-close::before {
-  background-image: var(--close-button-image);
-}
-
 /* In-tools sidebar */
 .devtools-sidebar-tabs {
   -moz-appearance: none;
   margin: 0;
 }
 
 .devtools-sidebar-tabs > tabpanels {
   -moz-appearance: none;
@@ -639,291 +590,16 @@
 }
 
 .devtools-sidebar-tabs tabs > tab[selected],
 .devtools-sidebar-tabs tabs > tab[selected]:hover:active {
   color: var(--theme-selection-color);
   background: var(--theme-selection-background);
 }
 
-/* Toolbox - moved from toolbox.css.
- * Rules that apply to the global toolbox like command buttons,
- * devtools tabs, docking buttons, etc. */
-
-#toolbox-controls > button,
-#toolbox-dock-buttons > button {
-  -moz-appearance: none;
-  -moz-user-focus: normal;
-  border: none;
-  margin: 0 4px;
-  min-width: 16px;
-  width: 16px;
-}
-
-/* Save space in Firebug theme */
-.theme-firebug #toolbox-controls button {
-  margin-inline-start: 0 !important;
-  min-width: 12px;
-  margin: 0 1px;
-}
-
-#toolbox-dock-bottom::before {
-  background-image: var(--dock-bottom-image);
-}
-
-#toolbox-dock-side::before {
-  background-image: var(--dock-side-image);
-}
-
-#toolbox-dock-window::before {
-  background-image: var(--dock-undock-image);
-}
-
-#toolbox-dock-bottom-minimize {
-  /* Bug 1177463 - The minimize button is currently hidden until we agree on
-     the UI for it, and until bug 1173849 is fixed too. */
-  display: none;
-}
-
-#toolbox-dock-bottom-minimize::before {
-  background-image: url("chrome://devtools/skin/images/dock-bottom-minimize@2x.png");
-}
-
-#toolbox-dock-bottom-minimize.minimized::before {
-  background-image: url("chrome://devtools/skin/images/dock-bottom-maximize@2x.png");
-}
-
-#toolbox-dock-window,
-#toolbox-dock-bottom,
-#toolbox-dock-side {
-  opacity: 0.8;
-}
-
-#toolbox-dock-window:hover,
-#toolbox-dock-bottom:hover,
-#toolbox-dock-side:hover {
-  opacity: 1;
-}
-
-.devtools-separator {
-  margin: 0 2px;
-  width: 2px;
-  background-image: linear-gradient(transparent 15%, var(--theme-splitter-color) 15%, var(--theme-splitter-color) 85%, transparent 85%);
-  background-size: 1px 100%;
-  background-repeat: no-repeat;
-  background-position: 0, 1px, 2px;
-}
-
-#toolbox-buttons:empty + .devtools-separator,
-.devtools-separator[invisible] {
-  visibility: hidden;
-}
-
-#toolbox-controls-separator {
-  margin: 0;
-}
-
-/* Command buttons */
-
-.command-button {
-  padding: 0;
-  margin: 0;
-  position: relative;
-  -moz-user-focus: normal;
-}
-
-.command-button::before {
-  opacity: 0.7;
-}
-
-.command-button:hover {
-  background-color: var(--toolbar-tab-hover);
-}
-
-.command-button:hover:active,
-.command-button[checked=true]:not(:hover) {
-  background-color: var(--toolbar-tab-hover-active)
-}
-
-.command-button:hover::before {
-  opacity: 0.85;
-}
-
-.command-button:hover:active::before,
-.command-button[checked=true]::before,
-.command-button[open=true]::before {
-  opacity: 1;
-}
-
-/* Tabs */
-
-.devtools-tabbar {
-  -moz-appearance: none;
-  min-height: 24px;
-  border: 0px solid;
-  border-bottom-width: 1px;
-  padding: 0;
-  background: var(--theme-tab-toolbar-background);
-  border-bottom-color: var(--theme-splitter-color);
-}
-
-#toolbox-tabs {
-  margin: 0;
-}
-
-.toolbox-panel {
-  display: -moz-box;
-  -moz-box-flex: 1;
-  visibility: collapse;
-}
-
-.toolbox-panel[selected] {
-  visibility: visible;
-}
-
-.devtools-tab {
-  -moz-appearance: none;
-  -moz-binding: url("chrome://global/content/bindings/general.xml#control-item");
-  -moz-box-align: center;
-  min-width: 32px;
-  min-height: 24px;
-  max-width: 100px;
-  margin: 0;
-  padding: 0;
-  border-style: solid;
-  border-width: 0;
-  border-inline-start-width: 1px;
-  -moz-box-align: center;
-  -moz-user-focus: normal;
-  -moz-box-flex: 1;
-}
-
-/* Save space on the tab-strip in Firebug theme */
-.theme-firebug .devtools-tab {
-  -moz-box-flex: initial;
-}
-
-.theme-dark .devtools-tab {
-  color: var(--theme-body-color-alt);
-  border-color: #42484f;
-}
-
-.theme-light .devtools-tab {
-  color: var(--theme-body-color);
-  border-color: var(--theme-splitter-color);
-}
-
-.theme-dark .devtools-tab:hover {
-  color: #ced3d9;
-}
-
-.devtools-tab:hover {
-  background-color: var(--toolbar-tab-hover);
-}
-
-.theme-dark .devtools-tab:hover:active {
-  color: var(--theme-selection-color);
-}
-
-.devtools-tab:hover:active {
-  background-color: var(--toolbar-tab-hover-active);
-}
-
-.theme-dark .devtools-tab:not([selected])[highlighted] {
-  background-color: hsla(99, 100%, 14%, .3);
-}
-
-.theme-light .devtools-tab:not([selected])[highlighted] {
-  background-color: rgba(44, 187, 15, .2);
-}
-
-/* Display execution pointer in the Debugger tab to indicate
-   that the debugger is paused. */
-.theme-firebug #toolbox-tab-jsdebugger.devtools-tab:not([selected])[highlighted] {
-  background-color: rgba(89, 178, 234, .2);
-  background-image: url(chrome://devtools/skin/images/firebug/tool-debugger-paused.svg);
-  background-repeat: no-repeat;
-  padding-left: 13px !important;
-  background-position: 3px 6px;
-}
-
-.devtools-tab > image {
-  border: none;
-  margin: 0;
-  margin-inline-start: 4px;
-  opacity: 0.6;
-  max-height: 16px;
-  width: 16px; /* Prevents collapse during theme switching */
-}
-
-.devtools-tab > label {
-  white-space: nowrap;
-  margin: 0 4px;
-}
-
-.devtools-tab:hover > image {
-  opacity: 0.8;
-}
-
-.devtools-tab:active > image,
-.devtools-tab[selected] > image {
-  opacity: 1;
-}
-
-.devtools-tabbar .devtools-tab[selected],
-.devtools-tabbar .devtools-tab[selected]:hover:active {
-  color: var(--theme-selection-color);
-  background-color: var(--theme-selection-background);
-}
-
-#toolbox-tabs .devtools-tab[selected],
-#toolbox-tabs .devtools-tab[highlighted] {
-  border-width: 0;
-  padding-inline-start: 1px;
-}
-
-#toolbox-tabs .devtools-tab[selected]:last-child,
-#toolbox-tabs .devtools-tab[highlighted]:last-child {
-  padding-inline-end: 1px;
-}
-
-#toolbox-tabs .devtools-tab[selected] + .devtools-tab,
-#toolbox-tabs .devtools-tab[highlighted] + .devtools-tab {
-  border-inline-start-width: 0;
-  padding-inline-start: 1px;
-}
-
-#toolbox-tabs .devtools-tab:first-child[selected] {
-  border-inline-start-width: 0;
-}
-
-#toolbox-tabs .devtools-tab:last-child {
-  border-inline-end-width: 1px;
-}
-
-.devtools-tab:not([highlighted]) > .highlighted-icon,
-.devtools-tab[selected] > .highlighted-icon,
-.devtools-tab:not([selected])[highlighted] > .default-icon {
-  visibility: collapse;
-}
-
-/* The options tab is special - it doesn't have the same parent
-   as the other tabs (toolbox-option-container vs toolbox-tabs) */
-#toolbox-option-container .devtools-tab:not([selected]) {
-  background-color: transparent;
-}
-#toolbox-option-container .devtools-tab {
-  border-color: transparent;
-  border-width: 0;
-  padding-inline-start: 1px;
-}
-#toolbox-tab-options > image {
-  margin: 0 8px;
-}
-
 /* Invert the colors of certain dark theme images for displaying
  * inside of the light theme.
  */
 .theme-light .devtools-tab[icon-invertable] > image,
 .theme-light .devtools-toolbarbutton > image,
 .theme-light .devtools-button::before,
 .theme-light #breadcrumb-separator-normal,
 .theme-light .scrollbutton-up > .toolbarbutton-icon,
@@ -943,41 +619,31 @@
 
 /* Since selected backgrounds are blue, we want to use the normal
  * (light) icons. */
 .theme-light .devtools-tab[icon-invertable][selected] > image,
 .theme-light .devtools-tab[icon-invertable][highlighted] > image {
   filter: none !important;
 }
 
-.theme-light .command-button:hover {
-  background-color: inherit;
-}
-
-.theme-light .command-button:hover:active,
-.theme-light .command-button[checked=true]:not(:hover) {
-  background-color: inherit;
-}
-
 .hidden-labels-box:not(.visible) > label,
 .hidden-labels-box.visible ~ .hidden-labels-box > label:last-child {
   display: none;
 }
 
 .devtools-invisible-splitter {
   border-color: transparent;
   background-color: transparent;
 }
 
 .devtools-horizontal-splitter,
 .devtools-side-splitter {
   background-color: var(--theme-splitter-color);
 }
 
-
 /* Throbbers */
 .devtools-throbber::before {
   content: "";
   display: inline-block;
   vertical-align: bottom;
   margin-inline-end: 0.5em;
   width: 1em;
   height: 1em;
--- a/devtools/client/themes/toolbox.css
+++ b/devtools/client/themes/toolbox.css
@@ -1,63 +1,370 @@
 /* vim:set ts=2 sw=2 sts=2 et: */
 /* 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/. */
 
- /* Toolbox command buttons */
+:root {
+  --close-button-image: url(chrome://devtools/skin/images/close.svg);
+  --dock-bottom-image: url(chrome://devtools/skin/images/dock-bottom.svg);
+  --dock-side-image: url(chrome://devtools/skin/images/dock-side.svg);
+  --dock-undock-image: url(chrome://devtools/skin/images/dock-undock.svg);
+
+  --command-paintflashing-image: url(images/command-paintflashing.svg);
+  --command-screenshot-image: url(images/command-screenshot.svg);
+  --command-responsive-image: url(images/command-responsivemode.svg);
+  --command-scratchpad-image: url(images/command-scratchpad.svg);
+  --command-pick-image: url(images/command-pick.svg);
+  --command-frames-image: url(images/command-frames.svg);
+  --command-splitconsole-image: url(images/command-console.svg);
+  --command-noautohide-image: url(images/command-noautohide.svg);
+  --command-eyedropper-image: url(images/command-eyedropper.svg);
+  --command-rulers-image: url(images/command-rulers.svg);
+  --command-measure-image: url(images/command-measure.svg);
+}
+
+.theme-firebug {
+  --close-button-image: url(chrome://devtools/skin/images/firebug/close.svg);
+  --dock-bottom-image: url(chrome://devtools/skin/images/firebug/dock-bottom.svg);
+  --dock-side-image: url(chrome://devtools/skin/images/firebug/dock-side.svg);
+  --dock-undock-image: url(chrome://devtools/skin/images/firebug/dock-undock.svg);
+
+  --command-paintflashing-image: url(images/firebug/command-paintflashing.svg);
+  --command-screenshot-image: url(images/firebug/command-screenshot.svg);
+  --command-responsive-image: url(images/firebug/command-responsivemode.svg);
+  --command-scratchpad-image: url(images/firebug/command-scratchpad.svg);
+  --command-pick-image: url(images/firebug/command-pick.svg);
+  --command-frames-image: url(images/firebug/command-frames.svg);
+  --command-splitconsole-image: url(images/firebug/command-console.svg);
+  --command-noautohide-image: url(images/firebug/command-noautohide.svg);
+  --command-eyedropper-image: url(images/firebug/command-eyedropper.svg);
+  --command-rulers-image: url(images/firebug/command-rulers.svg);
+  --command-measure-image: url(images/firebug/command-measure.svg);
+}
+
+/* Toolbox tabbar */
+
+.devtools-tabbar {
+  -moz-appearance: none;
+  min-height: 24px;
+  border: 0px solid;
+  border-bottom-width: 1px;
+  padding: 0;
+  background: var(--theme-tab-toolbar-background);
+  border-bottom-color: var(--theme-splitter-color);
+}
+
+#toolbox-tabs {
+  margin: 0;
+}
+
+/* Toolbox tabs */
+
+.devtools-tab {
+  -moz-appearance: none;
+  -moz-binding: url("chrome://global/content/bindings/general.xml#control-item");
+  -moz-box-align: center;
+  min-width: 32px;
+  min-height: 24px;
+  max-width: 100px;
+  margin: 0;
+  padding: 0;
+  border-style: solid;
+  border-width: 0;
+  border-inline-start-width: 1px;
+  -moz-box-align: center;
+  -moz-user-focus: normal;
+  -moz-box-flex: 1;
+}
+
+/* Save space on the tab-strip in Firebug theme */
+.theme-firebug .devtools-tab {
+  -moz-box-flex: initial;
+}
+
+.theme-dark .devtools-tab {
+  color: var(--theme-body-color-alt);
+  border-color: #42484f;
+}
+
+.theme-light .devtools-tab {
+  color: var(--theme-body-color);
+  border-color: var(--theme-splitter-color);
+}
+
+.theme-dark .devtools-tab:hover {
+  color: #ced3d9;
+}
+
+.devtools-tab:hover {
+  background-color: var(--toolbar-tab-hover);
+}
+
+.theme-dark .devtools-tab:hover:active {
+  color: var(--theme-selection-color);
+}
+
+.devtools-tab:hover:active {
+  background-color: var(--toolbar-tab-hover-active);
+}
+
+.theme-dark .devtools-tab:not([selected])[highlighted] {
+  background-color: hsla(99, 100%, 14%, .3);
+}
+
+.theme-light .devtools-tab:not([selected])[highlighted] {
+  background-color: rgba(44, 187, 15, .2);
+}
+
+/* Display execution pointer in the Debugger tab to indicate
+   that the debugger is paused. */
+.theme-firebug #toolbox-tab-jsdebugger.devtools-tab:not([selected])[highlighted] {
+  background-color: rgba(89, 178, 234, .2);
+  background-image: url(chrome://devtools/skin/images/firebug/tool-debugger-paused.svg);
+  background-repeat: no-repeat;
+  padding-left: 13px !important;
+  background-position: 3px 6px;
+}
+
+.devtools-tab > image {
+  border: none;
+  margin: 0;
+  margin-inline-start: 4px;
+  opacity: 0.6;
+  max-height: 16px;
+  width: 16px; /* Prevents collapse during theme switching */
+}
+
+.devtools-tab > label {
+  white-space: nowrap;
+  margin: 0 4px;
+}
+
+.devtools-tab:hover > image {
+  opacity: 0.8;
+}
+
+.devtools-tab:active > image,
+.devtools-tab[selected] > image {
+  opacity: 1;
+}
+
+.devtools-tabbar .devtools-tab[selected],
+.devtools-tabbar .devtools-tab[selected]:hover:active {
+  color: var(--theme-selection-color);
+  background-color: var(--theme-selection-background);
+}
+
+#toolbox-tabs .devtools-tab[selected],
+#toolbox-tabs .devtools-tab[highlighted] {
+  border-width: 0;
+  padding-inline-start: 1px;
+}
+
+#toolbox-tabs .devtools-tab[selected]:last-child,
+#toolbox-tabs .devtools-tab[highlighted]:last-child {
+  padding-inline-end: 1px;
+}
+
+#toolbox-tabs .devtools-tab[selected] + .devtools-tab,
+#toolbox-tabs .devtools-tab[highlighted] + .devtools-tab {
+  border-inline-start-width: 0;
+  padding-inline-start: 1px;
+}
+
+#toolbox-tabs .devtools-tab:first-child[selected] {
+  border-inline-start-width: 0;
+}
+
+#toolbox-tabs .devtools-tab:last-child {
+  border-inline-end-width: 1px;
+}
+
+.devtools-tab:not([highlighted]) > .highlighted-icon,
+.devtools-tab[selected] > .highlighted-icon,
+.devtools-tab:not([selected])[highlighted] > .default-icon {
+  visibility: collapse;
+}
+
+/* The options tab is special - it doesn't have the same parent
+   as the other tabs (toolbox-option-container vs toolbox-tabs) */
+#toolbox-option-container .devtools-tab:not([selected]) {
+  background-color: transparent;
+}
+#toolbox-option-container .devtools-tab {
+  border-color: transparent;
+  border-width: 0;
+  padding-inline-start: 1px;
+}
+#toolbox-tab-options > image {
+  margin: 0 8px;
+}
+
+/* Toolbox controls */
+
+#toolbox-controls > button,
+#toolbox-dock-buttons > button {
+  -moz-appearance: none;
+  -moz-user-focus: normal;
+  border: none;
+  margin: 0 4px;
+  min-width: 16px;
+  width: 16px;
+}
+
+/* Save space in Firebug theme */
+.theme-firebug #toolbox-controls button {
+  margin-inline-start: 0 !important;
+  min-width: 12px;
+  margin: 0 1px;
+}
+
+#toolbox-close::before {
+  background-image: var(--close-button-image);
+}
+
+#toolbox-dock-bottom::before {
+  background-image: var(--dock-bottom-image);
+}
+
+#toolbox-dock-side::before {
+  background-image: var(--dock-side-image);
+}
+
+#toolbox-dock-window::before {
+  background-image: var(--dock-undock-image);
+}
+
+#toolbox-dock-bottom-minimize {
+  /* Bug 1177463 - The minimize button is currently hidden until we agree on
+     the UI for it, and until bug 1173849 is fixed too. */
+  display: none;
+}
+
+#toolbox-dock-bottom-minimize::before {
+  background-image: url("chrome://devtools/skin/images/dock-bottom-minimize@2x.png");
+}
+
+#toolbox-dock-bottom-minimize.minimized::before {
+  background-image: url("chrome://devtools/skin/images/dock-bottom-maximize@2x.png");
+}
+
+#toolbox-buttons:empty + .devtools-separator,
+.devtools-separator[invisible] {
+  visibility: hidden;
+}
+
+#toolbox-controls-separator {
+  margin: 0;
+}
+
+/* Command buttons */
+
+.command-button {
+  padding: 0;
+  margin: 0;
+  position: relative;
+  -moz-user-focus: normal;
+}
+
+.command-button::before {
+  opacity: 0.7;
+}
+
+.command-button:hover {
+  background-color: var(--toolbar-tab-hover);
+}
+
+.theme-light .command-button:hover {
+  background-color: inherit;
+}
+
+.command-button:hover:active,
+.command-button[checked=true]:not(:hover) {
+  background-color: var(--toolbar-tab-hover-active)
+}
+
+.theme-light .command-button:hover:active,
+.theme-light .command-button[checked=true]:not(:hover) {
+  background-color: inherit;
+}
+
+.command-button:hover::before {
+  opacity: 0.85;
+}
+
+.command-button:hover:active::before,
+.command-button[checked=true]::before,
+.command-button[open=true]::before {
+  opacity: 1;
+}
+
+/* Command button images */
 
 #command-button-paintflashing::before {
-   background-image: var(--command-paintflashing-image);
- }
+  background-image: var(--command-paintflashing-image);
+}
 
 #command-button-screenshot::before {
-   background-image: var(--command-screenshot-image);
- }
+  background-image: var(--command-screenshot-image);
+}
 
 #command-button-responsive::before {
- background-image: var(--command-responsive-image);
+  background-image: var(--command-responsive-image);
 }
 
 #command-button-scratchpad::before {
- background-image: var(--command-scratchpad-image);
+  background-image: var(--command-scratchpad-image);
 }
 
 #command-button-pick::before {
- background-image: var(--command-pick-image);
+  background-image: var(--command-pick-image);
 }
 
 #command-button-splitconsole::before {
- background-image: var(--command-splitconsole-image);
+  background-image: var(--command-splitconsole-image);
 }
 
 #command-button-noautohide::before {
- background-image: var(--command-noautohide-image);
+  background-image: var(--command-noautohide-image);
 }
 
 #command-button-eyedropper::before {
- background-image: var(--command-eyedropper-image);
+  background-image: var(--command-eyedropper-image);
 }
 
 #command-button-rulers::before {
- background-image: var(--command-rulers-image);
+  background-image: var(--command-rulers-image);
 }
 
 #command-button-measure::before {
- background-image: var(--command-measure-image);
+  background-image: var(--command-measure-image);
 }
 
 #command-button-frames::before {
- background-image: var(--command-frames-image);
+  background-image: var(--command-frames-image);
 }
 
 #command-button-frames {
- background: url("chrome://devtools/skin/images/dropmarker.svg") no-repeat right;
+  background: url("chrome://devtools/skin/images/dropmarker.svg") no-repeat right;
 
- /* Override background-size from the command-button.
+  /* Override background-size from the command-button.
    The drop down arrow is smaller */
- background-size: 8px 4px !important;
- min-width: 32px;
+  background-size: 8px 4px !important;
+  min-width: 32px;
 }
 
 #command-button-frames:-moz-dir(rtl) {
- background-position: left;
+  background-position: left;
 }
+
+/* Toolbox panels */
+
+.toolbox-panel {
+  display: -moz-box;
+  -moz-box-flex: 1;
+  visibility: collapse;
+}
+
+.toolbox-panel[selected] {
+  visibility: visible;
+}
\ No newline at end of file
--- a/devtools/client/themes/tooltips.css
+++ b/devtools/client/themes/tooltips.css
@@ -200,18 +200,23 @@
 .tooltip-arrow {
   position: relative;
   height: 16px;
   width: 32px;
   overflow: hidden;
   flex-shrink: 0;
 }
 
-.tooltip-arrow:-moz-locale-dir(rtl) {
-  align-self: flex-end;
+/* In RTL locales, only use RTL on the tooltip content, keep LTR for positioning */
+.tooltip-container:-moz-locale-dir(rtl) {
+  direction: ltr;
+}
+
+.tooltip-panel:-moz-locale-dir(rtl) {
+  direction: rtl;
 }
 
 .tooltip-top .tooltip-arrow {
   margin-top: -3px;
 }
 
 .tooltip-bottom .tooltip-arrow {
   margin-bottom: -3px;
--- a/devtools/client/webconsole/net/components/post-tab.js
+++ b/devtools/client/webconsole/net/components/post-tab.js
@@ -1,18 +1,17 @@
 /* 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/. */
 "use strict";
 
 const React = require("devtools/client/shared/vendor/react");
 
 // Reps
-const { createFactories } = require("devtools/client/shared/components/reps/rep-utils");
-const { parseURLEncodedText } = require("devtools/client/shared/components/reps/url");
+const { createFactories, parseURLEncodedText } = require("devtools/client/shared/components/reps/rep-utils");
 const TreeView = React.createFactory(require("devtools/client/shared/components/tree/tree-view"));
 const { Rep } = createFactories(require("devtools/client/shared/components/reps/rep"));
 
 // Network
 const NetInfoParams = React.createFactory(require("./net-info-params"));
 const NetInfoGroupList = React.createFactory(require("./net-info-group-list"));
 const Spinner = React.createFactory(require("./spinner"));
 const SizeLimit = React.createFactory(require("./size-limit"));
--- a/devtools/client/webconsole/net/net-request.js
+++ b/devtools/client/webconsole/net/net-request.js
@@ -3,17 +3,17 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 "use strict";
 
 // React
 const React = require("devtools/client/shared/vendor/react");
 const ReactDOM = require("devtools/client/shared/vendor/react-dom");
 
 // Reps
-const { parseURLParams } = require("devtools/client/shared/components/reps/url");
+const { parseURLParams } = require("devtools/client/shared/components/reps/rep-utils");
 
 // Network
 const { cancelEvent, isLeftClick } = require("./utils/events");
 const NetInfoBody = React.createFactory(require("./components/net-info-body"));
 const DataProvider = require("./data-provider");
 
 // Constants
 const XHTML_NS = "http://www.w3.org/1999/xhtml";
--- a/devtools/server/actors/css-properties.js
+++ b/devtools/server/actors/css-properties.js
@@ -23,44 +23,57 @@ exports.CssPropertiesActor = ActorClassW
     this.parent = parent;
   },
 
   destroy() {
     Actor.prototype.destroy.call(this);
   },
 
   getCSSDatabase() {
-    const db = {};
-    const properties = DOMUtils.getCSSPropertyNames(DOMUtils.INCLUDE_ALIASES);
-
-    properties.forEach(name => {
-      // Get the list of CSS types this property supports.
-      let supports = [];
-      for (let type in CSS_TYPES) {
-        if (safeCssPropertySupportsType(name, DOMUtils["TYPE_" + type])) {
-          supports.push(CSS_TYPES[type]);
-        }
-      }
+    const properties = generateCssProperties();
+    const pseudoElements = DOMUtils.getCSSPseudoElementNames();
 
-      // In order to maintain any backwards compatible changes when debugging older
-      // clients, take the definition from the static CSS properties database, and fill it
-      // in with the most recent property definition from the server.
-      const clientDefinition = CSS_PROPERTIES[name] || {};
-      const serverDefinition = {
-        isInherited: DOMUtils.isInheritedProperty(name),
-        supports
-      };
-      db[name] = Object.assign(clientDefinition, serverDefinition);
-    });
-
-    return db;
+    return { properties, pseudoElements };
   }
 });
 
 /**
+ * Generate the CSS properties object. Every key is the property name, while
+ * the values are objects that contain information about that property.
+ *
+ * @return {Object}
+ */
+function generateCssProperties() {
+  const properties = {};
+  const propertyNames = DOMUtils.getCSSPropertyNames(DOMUtils.INCLUDE_ALIASES);
+
+  propertyNames.forEach(name => {
+    // Get the list of CSS types this property supports.
+    let supports = [];
+    for (let type in CSS_TYPES) {
+      if (safeCssPropertySupportsType(name, DOMUtils["TYPE_" + type])) {
+        supports.push(CSS_TYPES[type]);
+      }
+    }
+
+    // In order to maintain any backwards compatible changes when debugging older
+    // clients, take the definition from the static CSS properties database, and fill it
+    // in with the most recent property definition from the server.
+    const clientDefinition = CSS_PROPERTIES[name] || {};
+    const serverDefinition = {
+      isInherited: DOMUtils.isInheritedProperty(name),
+      supports
+    };
+    properties[name] = Object.assign(clientDefinition, serverDefinition);
+  });
+
+  return properties;
+}
+
+/**
  * Test if a CSS is property is known using server-code.
  *
  * @param {string} name
  * @return {Boolean}
  */
 function isCssPropertyKnown(name) {
   try {
     // If the property name is unknown, the cssPropertyIsShorthand
--- a/devtools/server/actors/highlighters.css
+++ b/devtools/server/actors/highlighters.css
@@ -166,31 +166,35 @@
 }
 
 /* Text container */
 
 :-moz-native-anonymous .box-model-nodeinfobar-text {
   overflow: hidden;
   white-space: nowrap;
   direction: ltr;
-  text-align: center;
   padding-bottom: 1px;
+  display: flex;
 }
 
 :-moz-native-anonymous .box-model-nodeinfobar-tagname {
   color: hsl(285,100%, 75%);
 }
 
 :-moz-native-anonymous .box-model-nodeinfobar-id {
   color: hsl(103, 46%, 54%);
+  overflow: hidden;
+  text-overflow: ellipsis;
 }
 
 :-moz-native-anonymous .box-model-nodeinfobar-classes,
 :-moz-native-anonymous .box-model-nodeinfobar-pseudo-classes {
   color: hsl(200, 74%, 57%);
+  overflow: hidden;
+  text-overflow: ellipsis;
 }
 
 :-moz-native-anonymous .box-model-nodeinfobar-dimensions {
   color: hsl(210, 30%, 85%);
   border-inline-start: 1px solid #5a6169;
   margin-inline-start: 6px;
   padding-inline-start: 6px;
 }
--- a/devtools/server/actors/webbrowser.js
+++ b/devtools/server/actors/webbrowser.js
@@ -377,16 +377,35 @@ BrowserTabList.prototype._getActorForBro
   actor = new BrowserTabActor(this._connection, browser);
   this._actorByBrowser.set(browser, actor);
   this._checkListening();
   return promise.resolve(actor);
 };
 
 BrowserTabList.prototype.getTab = function ({ outerWindowID, tabId }) {
   if (typeof outerWindowID == "number") {
+    // First look for in-process frames with this ID
+    let window = Services.wm.getOuterWindowWithId(outerWindowID);
+    // Safety check to prevent debugging top level window via getTab
+    if (window instanceof Ci.nsIDOMChromeWindow) {
+      return promise.reject({
+        error: "forbidden",
+        message: "Window with outerWindowID '" + outerWindowID + "' is chrome"
+      });
+    }
+    if (window) {
+      let iframe = window.QueryInterface(Ci.nsIInterfaceRequestor)
+                         .getInterface(Ci.nsIDOMWindowUtils)
+                         .containerElement;
+      if (iframe) {
+        return this._getActorForBrowser(iframe);
+      }
+    }
+    // Then also look on registered <xul:browsers> when using outerWindowID for
+    // OOP tabs
     for (let browser of this._getBrowsers()) {
       if (browser.outerWindowID == outerWindowID) {
         return this._getActorForBrowser(browser);
       }
     }
     return promise.reject({
       error: "noTab",
       message: "Unable to find tab with outerWindowID '" + outerWindowID + "'"
@@ -1120,20 +1139,17 @@ TabActor.prototype = {
     }
 
     // Tell the thread actor that the tab is closed, so that it may terminate
     // instead of resuming the debuggee script.
     if (this._attached) {
       this.threadActor._tabClosed = true;
     }
 
-    if (this._detach()) {
-      this.conn.send({ from: this.actorID,
-                       type: "tabDetached" });
-    }
+    this._detach();
 
     Object.defineProperty(this, "docShell", {
       value: null,
       configurable: true
     });
 
     this._extraActors = null;
 
@@ -1515,16 +1531,19 @@ TabActor.prototype = {
 
     if (this._workerActorPool !== null) {
       this.conn.removeActorPool(this._workerActorPool);
       this._workerActorPool = null;
     }
 
     this._attached = false;
 
+    this.conn.send({ from: this.actorID,
+                     type: "tabDetached" });
+
     return true;
   },
 
   // Protocol Request Handlers
 
   onAttach(request) {
     if (this.exited) {
       return { type: "exited" };
--- a/devtools/shared/css-properties-db.js
+++ b/devtools/shared/css-properties-db.js
@@ -40,16 +40,31 @@ exports.COLOR_TAKING_FUNCTIONS = ["linea
  */
 exports.ANGLE_TAKING_FUNCTIONS = ["linear-gradient", "-moz-linear-gradient",
                                   "repeating-linear-gradient",
                                   "-moz-repeating-linear-gradient", "rotate", "rotateX",
                                   "rotateY", "rotateZ", "rotate3d", "skew", "skewX",
                                   "skewY", "hue-rotate"];
 
 /**
+ * The list of all CSS Pseudo Elements. This list can be generated from:
+ *
+ * let domUtils = Cc["@mozilla.org/inspector/dom-utils;1"].getService(Ci.inIDOMUtils);
+ * domUtils.getCSSPseudoElementNames();
+ */
+exports.PSEUDO_ELEMENTS = [":after", ":before", ":backdrop", ":first-letter",
+                           ":first-line", ":-moz-selection", ":-moz-focus-inner",
+                           ":-moz-focus-outer", ":-moz-list-bullet",
+                           ":-moz-list-number", ":-moz-math-anonymous",
+                           ":-moz-progress-bar", ":-moz-range-track",
+                           ":-moz-range-progress", ":-moz-range-thumb",
+                           ":-moz-meter-bar", ":-moz-placeholder",
+                           ":-moz-color-swatch"];
+
+/**
  * This list is generated from the output of the CssPropertiesActor. If a server
  * does not support the actor, this is loaded as a backup. This list does not
  * guarantee that the server actually supports these CSS properties.
  */
 exports.CSS_PROPERTIES = {
   "align-content": {
     isInherited: false,
     supports: []
@@ -1738,8 +1753,13 @@ exports.CSS_PROPERTIES = {
     isInherited: false,
     supports: []
   },
   "-webkit-user-select": {
     isInherited: false,
     supports: []
   }
 };
+
+exports.CSS_PROPERTIES_DB = {
+  properties: exports.CSS_PROPERTIES,
+  pseudoElements: exports.PSEUDO_ELEMENTS
+};
--- a/devtools/shared/fronts/css-properties.js
+++ b/devtools/shared/fronts/css-properties.js
@@ -1,17 +1,17 @@
 /* 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/. */
 "use strict";
 
 const { FrontClassWithSpec, Front } = require("devtools/shared/protocol");
 const { cssPropertiesSpec } = require("devtools/shared/specs/css-properties");
 const { Task } = require("devtools/shared/task");
-const { CSS_PROPERTIES } = require("devtools/shared/css-properties-db");
+const { CSS_PROPERTIES_DB } = require("devtools/shared/css-properties-db");
 
 /**
  * Build up a regular expression that matches a CSS variable token. This is an
  * ident token that starts with two dashes "--".
  *
  * https://www.w3.org/TR/css-syntax-3/#ident-token-diagram
  */
 var NON_ASCII = "[^\\x00-\\x7F]";
@@ -48,24 +48,25 @@ const CssPropertiesFront = FrontClassWit
 exports.CssPropertiesFront = CssPropertiesFront;
 
 /**
  * Ask questions to a CSS database. This class does not care how the database
  * gets loaded in, only the questions that you can ask to it.
  * Prototype functions are bound to 'this' so they can be passed around as helper
  * functions.
  *
- * @param {Array}  propertiesList
- *                 A list of known properties.
+ * @param {Object} db
+ *                 A database of CSS properties
  * @param {Object} inheritedList
  *                 The key is the property name, the value is whether or not
  *                 that property is inherited.
  */
-function CssProperties(properties) {
-  this.properties = properties;
+function CssProperties(db) {
+  this.properties = db.properties;
+  this.pseudoElements = db.pseudoElements;
 
   this.isKnown = this.isKnown.bind(this);
   this.isInherited = this.isInherited.bind(this);
   this.supportsType = this.supportsType.bind(this);
 }
 
 CssProperties.prototype = {
   /**
@@ -123,31 +124,41 @@ exports.initCssProperties = Task.async(f
 
   let db, front;
 
   // Get the list dynamically if the cssProperties actor exists.
   if (toolbox.target.hasActor("cssProperties")) {
     front = CssPropertiesFront(client, toolbox.target.form);
     db = yield front.getCSSDatabase();
 
-    // Even if the target has the cssProperties actor, it may not be the latest version.
-    // So, the "supports" data may be missing.
-    // Start with the server's list (because that's the correct one), and add the supports
-    // information if required.
-    if (!db.color.supports) {
-      for (let name in db) {
-        if (typeof CSS_PROPERTIES[name] === "object") {
-          db[name].supports = CSS_PROPERTIES[name].supports;
+    // Even if the target has the cssProperties actor, the returned data may
+    // not be in the same shape or have all of the data we need. The following
+    // code normalizes this data.
+
+    // Firefox 49's getCSSDatabase() just returned the properties object, but
+    // now it returns an object with multiple types of CSS information.
+    if (!db.properties) {
+      db = { properties: db };
+    }
+
+    // Fill in any missing DB information from the static database.
+    db = Object.assign({}, CSS_PROPERTIES_DB, db);
+
+    // Add "supports" information to the css properties if it's missing.
+    if (!db.properties.color.supports) {
+      for (let name in db.properties) {
+        if (typeof CSS_PROPERTIES_DB.properties[name] === "object") {
+          db.properties[name].supports = CSS_PROPERTIES_DB.properties[name].supports;
         }
       }
     }
   } else {
     // The target does not support this actor, so require a static list of supported
     // properties.
-    db = CSS_PROPERTIES;
+    db = CSS_PROPERTIES_DB;
   }
 
   const cssProperties = new CssProperties(db);
   cachedCssProperties.set(client, {cssProperties, front});
   return {cssProperties, front};
 });
 
 /**
new file mode 100644
--- /dev/null
+++ b/devtools/shared/tests/unit/test_css-properties-db.js
@@ -0,0 +1,36 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Test that css-properties-db matches platform.
+
+"use strict";
+
+const DOMUtils = Components.classes["@mozilla.org/inspector/dom-utils;1"]
+                           .getService(Components.interfaces.inIDOMUtils);
+
+const {PSEUDO_ELEMENTS} = require("devtools/shared/css-properties-db");
+
+function run_test() {
+  // Check that the platform and client match for pseudo elements.
+  let foundPseudoElements = 0;
+  const platformPseudoElements = DOMUtils.getCSSPseudoElementNames();
+  const instructions = "If this assertion fails then it means that pseudo elements " +
+                       "have been added, removed, or changed on the platform and need " +
+                       "to be updated on the static client-side list of pseudo " +
+                       "elements within the devtools. " +
+                       "See devtools/shared/css-properties-db.js and " +
+                       "exports.PSEUDO_ELEMENTS for information on how to update the " +
+                       "list of pseudo elements to fix this test.";
+
+  for (let element of PSEUDO_ELEMENTS) {
+    const hasElement = platformPseudoElements.includes(element);
+    ok(hasElement,
+       `"${element}" pseudo element from the client-side CSS properties database was ` +
+       `found on the platform. ${instructions}`);
+    foundPseudoElements += hasElement ? 1 : 0;
+  }
+
+  equal(foundPseudoElements, platformPseudoElements.length,
+        `The client side CSS properties database of psuedo element names should match ` +
+        `those found on the platform. ${instructions}`);
+}
--- a/devtools/shared/tests/unit/xpcshell.ini
+++ b/devtools/shared/tests/unit/xpcshell.ini
@@ -4,16 +4,17 @@ head = head_devtools.js
 tail =
 firefox-appdir = browser
 skip-if = toolkit == 'android' || toolkit == 'gonk'
 support-files =
   exposeLoader.js
 
 [test_assert.js]
 [test_csslexer.js]
+[test_css-properties-db.js]
 [test_fetch-chrome.js]
 [test_fetch-file.js]
 [test_fetch-http.js]
 [test_fetch-resource.js]
 [test_flatten.js]
 [test_indentation.js]
 [test_independent_loaders.js]
 [test_invisible_loader.js]
--- a/dom/events/IMEStateManager.cpp
+++ b/dom/events/IMEStateManager.cpp
@@ -1391,16 +1391,17 @@ IMEStateManager::NotifyIME(const IMENoti
       sFocusedIMEWidget = nullptr;
       sRemoteHasFocus = false;
       return focusedIMEWidget->NotifyIME(IMENotification(NOTIFY_IME_OF_BLUR));
     }
     case NOTIFY_IME_OF_SELECTION_CHANGE:
     case NOTIFY_IME_OF_TEXT_CHANGE:
     case NOTIFY_IME_OF_POSITION_CHANGE:
     case NOTIFY_IME_OF_MOUSE_BUTTON_EVENT:
+    case NOTIFY_IME_OF_COMPOSITION_EVENT_HANDLED:
       if (!sRemoteHasFocus && aOriginIsRemote) {
         MOZ_LOG(sISMLog, LogLevel::Info,
           ("  NotifyIME(), received content change "
            "notification from the remote but it's already lost focus"));
         return NS_OK;
       }
       if (NS_WARN_IF(sRemoteHasFocus && !aOriginIsRemote)) {
         MOZ_LOG(sISMLog, LogLevel::Error,
@@ -1444,30 +1445,16 @@ IMEStateManager::NotifyIME(const IMENoti
 
   switch (aNotification.mMessage) {
     case REQUEST_TO_COMMIT_COMPOSITION:
       return composition ?
         composition->RequestToCommit(aWidget, false) : NS_OK;
     case REQUEST_TO_CANCEL_COMPOSITION:
       return composition ?
         composition->RequestToCommit(aWidget, true) : NS_OK;
-    case NOTIFY_IME_OF_COMPOSITION_EVENT_HANDLED:
-      if (!aOriginIsRemote && (!composition || isSynthesizedForTests)) {
-        MOZ_LOG(sISMLog, LogLevel::Info,
-          ("  NotifyIME(), FAILED, received content "
-           "change notification from this process but there is no compostion"));
-        return NS_OK;
-      }
-      if (!sRemoteHasFocus && aOriginIsRemote) {
-        MOZ_LOG(sISMLog, LogLevel::Info,
-          ("  NotifyIME(), received content change "
-           "notification from the remote but it's already lost focus"));
-        return NS_OK;
-      }
-      return aWidget->NotifyIME(aNotification);
     default:
       MOZ_CRASH("Unsupported notification");
   }
   MOZ_CRASH(
     "Failed to handle the notification for non-synthesized composition");
   return NS_ERROR_FAILURE;
 }
 
--- a/dom/html/HTMLMediaElement.cpp
+++ b/dom/html/HTMLMediaElement.cpp
@@ -3072,19 +3072,22 @@ HTMLMediaElement::ReportTelemetry()
     }
   }
 
   Telemetry::Accumulate(Telemetry::VIDEO_UNLOAD_STATE, state);
   LOG(LogLevel::Debug, ("%p VIDEO_UNLOAD_STATE = %d", this, state));
 
   if (HTMLVideoElement* vid = HTMLVideoElement::FromContentOrNull(this)) {
     RefPtr<VideoPlaybackQuality> quality = vid->GetVideoPlaybackQuality();
-    uint64_t totalFrames = quality->TotalVideoFrames();
-    uint64_t droppedFrames = quality->DroppedVideoFrames();
+    uint32_t totalFrames = quality->TotalVideoFrames();
     if (totalFrames) {
+      uint32_t droppedFrames = quality->DroppedVideoFrames();
+      MOZ_ASSERT(droppedFrames <= totalFrames);
+      // Dropped frames <= total frames, so 'percentage' cannot be higher than
+      // 100 and therefore can fit in a uint32_t (that Telemetry takes).
       uint32_t percentage = 100 * droppedFrames / totalFrames;
       LOG(LogLevel::Debug,
           ("Reporting telemetry DROPPED_FRAMES_IN_VIDEO_PLAYBACK"));
       Telemetry::Accumulate(Telemetry::VIDEO_DROPPED_FRAMES_PROPORTION,
                             percentage);
     }
   }
 
--- a/dom/html/HTMLVideoElement.cpp
+++ b/dom/html/HTMLVideoElement.cpp
@@ -224,30 +224,32 @@ HTMLVideoElement::NotifyOwnerDocumentAct
   UpdateScreenWakeLock();
   return pauseElement;
 }
 
 already_AddRefed<VideoPlaybackQuality>
 HTMLVideoElement::GetVideoPlaybackQuality()
 {
   DOMHighResTimeStamp creationTime = 0;
-  uint64_t totalFrames = 0;
-  uint64_t droppedFrames = 0;
-  uint64_t corruptedFrames = 0;
+  uint32_t totalFrames = 0;
+  uint32_t droppedFrames = 0;
+  uint32_t corruptedFrames = 0;
 
   if (sVideoStatsEnabled) {
     if (nsPIDOMWindowInner* window = OwnerDoc()->GetInnerWindow()) {
       Performance* perf = window->GetPerformance();
       if (perf) {
         creationTime = perf->Now();
       }
     }
 
     if (mDecoder) {
       FrameStatistics& stats = mDecoder->GetFrameStatistics();
+      static_assert(sizeof(uint32_t) >= sizeof (stats.GetParsedFrames()),
+                    "possible truncation from FrameStatistics to VideoPlaybackQuality");
       totalFrames = stats.GetParsedFrames();
       droppedFrames = stats.GetDroppedFrames();
       corruptedFrames = 0;
     }
   }
 
   RefPtr<VideoPlaybackQuality> playbackQuality =
     new VideoPlaybackQuality(this, creationTime, totalFrames, droppedFrames,
--- a/dom/media/MediaDecoderStateMachine.cpp
+++ b/dom/media/MediaDecoderStateMachine.cpp
@@ -212,103 +212,89 @@ static void
 InitSuspendBackgroundPref()
 {
   MOZ_ASSERT(NS_IsMainThread(), "Must be on main thread.");
 
   sSuspendBackgroundVideoDelay = TimeDuration::FromMilliseconds(
       MediaPrefs::MDSMSuspendBackgroundVideoDelay());
 }
 
+#define INIT_WATCHABLE(name, val) \
+  name(val, "MediaDecoderStateMachine::" #name)
+#define INIT_MIRROR(name, val) \
+  name(mTaskQueue, val, "MediaDecoderStateMachine::" #name " (Mirror)")
+#define INIT_CANONICAL(name, val) \
+  name(mTaskQueue, val, "MediaDecoderStateMachine::" #name " (Canonical)")
+
 MediaDecoderStateMachine::MediaDecoderStateMachine(MediaDecoder* aDecoder,
                                                    MediaDecoderReader* aReader,
                                                    bool aRealTime) :
   mDecoderID(aDecoder),
   mFrameStats(&aDecoder->GetFrameStatistics()),
   mVideoFrameContainer(aDecoder->GetVideoFrameContainer()),
   mAudioChannel(aDecoder->GetAudioChannel()),
   mTaskQueue(new TaskQueue(GetMediaThreadPool(MediaThreadType::PLAYBACK),
                            /* aSupportsTailDispatch = */ true)),
   mWatchManager(this, mTaskQueue),
   mRealTime(aRealTime),
   mDispatchedStateMachine(false),
   mDelayedScheduler(mTaskQueue),
-  mState(DECODER_STATE_DECODING_METADATA, "MediaDecoderStateMachine::mState"),
+  INIT_WATCHABLE(mState, DECODER_STATE_DECODING_METADATA),
   mCurrentFrameID(0),
-  mObservedDuration(TimeUnit(), "MediaDecoderStateMachine::mObservedDuration"),
+  INIT_WATCHABLE(mObservedDuration, TimeUnit()),
   mFragmentEndTime(-1),
   mReader(new MediaDecoderReaderWrapper(aRealTime, mTaskQueue, aReader)),
   mDecodedAudioEndTime(0),
   mDecodedVideoEndTime(0),
   mPlaybackRate(1.0),
   mLowAudioThresholdUsecs(detail::LOW_AUDIO_USECS),
   mAmpleAudioThresholdUsecs(detail::AMPLE_AUDIO_USECS),
   mQuickBufferingLowDataThresholdUsecs(detail::QUICK_BUFFERING_LOW_DATA_USECS),
   mIsAudioPrerolling(false),
   mIsVideoPrerolling(false),
   mAudioCaptured(false),
-  mAudioCompleted(false, "MediaDecoderStateMachine::mAudioCompleted"),
-  mVideoCompleted(false, "MediaDecoderStateMachine::mVideoCompleted"),
+  INIT_WATCHABLE(mAudioCompleted, false),
+  INIT_WATCHABLE(mVideoCompleted, false),
   mNotifyMetadataBeforeFirstFrame(false),
   mDispatchedEventToDecode(false),
   mQuickBuffering(false),
   mMinimizePreroll(false),
   mDecodeThreadWaiting(false),
   mDecodingFirstFrame(true),
   mSentLoadedMetadataEvent(false),
   mSentFirstFrameLoadedEvent(false),
   mSentPlaybackEndedEvent(false),
   mVideoDecodeSuspended(false),
   mVideoDecodeSuspendTimer(mTaskQueue),
   mOutputStreamManager(new OutputStreamManager()),
   mResource(aDecoder->GetResource()),
   mAudioOffloading(false),
-  mBuffered(mTaskQueue, TimeIntervals(),
-            "MediaDecoderStateMachine::mBuffered (Mirror)"),
-  mIsReaderSuspended(mTaskQueue, true,
-               "MediaDecoderStateMachine::mIsReaderSuspended (Mirror)"),
-  mEstimatedDuration(mTaskQueue, NullableTimeUnit(),
-                    "MediaDecoderStateMachine::mEstimatedDuration (Mirror)"),
-  mExplicitDuration(mTaskQueue, Maybe<double>(),
-                    "MediaDecoderStateMachine::mExplicitDuration (Mirror)"),
-  mPlayState(mTaskQueue, MediaDecoder::PLAY_STATE_LOADING,
-             "MediaDecoderStateMachine::mPlayState (Mirror)"),
-  mNextPlayState(mTaskQueue, MediaDecoder::PLAY_STATE_PAUSED,
-                 "MediaDecoderStateMachine::mNextPlayState (Mirror)"),
-  mVolume(mTaskQueue, 1.0, "MediaDecoderStateMachine::mVolume (Mirror)"),
-  mLogicalPlaybackRate(mTaskQueue, 1.0,
-                       "MediaDecoderStateMachine::mLogicalPlaybackRate (Mirror)"),
-  mPreservesPitch(mTaskQueue, true,
-                  "MediaDecoderStateMachine::mPreservesPitch (Mirror)"),
-  mSameOriginMedia(mTaskQueue, false,
-                   "MediaDecoderStateMachine::mSameOriginMedia (Mirror)"),
-  mMediaPrincipalHandle(mTaskQueue, PRINCIPAL_HANDLE_NONE,
-                        "MediaDecoderStateMachine::mMediaPrincipalHandle (Mirror)"),
-  mPlaybackBytesPerSecond(mTaskQueue, 0.0,
-                          "MediaDecoderStateMachine::mPlaybackBytesPerSecond (Mirror)"),
-  mPlaybackRateReliable(mTaskQueue, true,
-                        "MediaDecoderStateMachine::mPlaybackRateReliable (Mirror)"),
-  mDecoderPosition(mTaskQueue, 0,
-                   "MediaDecoderStateMachine::mDecoderPosition (Mirror)"),
-  mMediaSeekable(mTaskQueue, true,
-                 "MediaDecoderStateMachine::mMediaSeekable (Mirror)"),
-  mMediaSeekableOnlyInBufferedRanges(mTaskQueue, false,
-                 "MediaDecoderStateMachine::mMediaSeekableOnlyInBufferedRanges (Mirror)"),
-  mIsVisible(mTaskQueue, true, "MediaDecoderStateMachine::mIsVisible (Mirror)"),
-  mDuration(mTaskQueue, NullableTimeUnit(),
-            "MediaDecoderStateMachine::mDuration (Canonical"),
-  mIsShutdown(mTaskQueue, false,
-              "MediaDecoderStateMachine::mIsShutdown (Canonical)"),
-  mNextFrameStatus(mTaskQueue, MediaDecoderOwner::NEXT_FRAME_UNINITIALIZED,
-                   "MediaDecoderStateMachine::mNextFrameStatus (Canonical)"),
-  mCurrentPosition(mTaskQueue, 0,
-                   "MediaDecoderStateMachine::mCurrentPosition (Canonical)"),
-  mPlaybackOffset(mTaskQueue, 0,
-                  "MediaDecoderStateMachine::mPlaybackOffset (Canonical)"),
-  mIsAudioDataAudible(mTaskQueue, false,
-                     "MediaDecoderStateMachine::mIsAudioDataAudible (Canonical)")
+  INIT_MIRROR(mBuffered, TimeIntervals()),
+  INIT_MIRROR(mIsReaderSuspended, true),
+  INIT_MIRROR(mEstimatedDuration, NullableTimeUnit()),
+  INIT_MIRROR(mExplicitDuration, Maybe<double>()),
+  INIT_MIRROR(mPlayState, MediaDecoder::PLAY_STATE_LOADING),
+  INIT_MIRROR(mNextPlayState, MediaDecoder::PLAY_STATE_PAUSED),
+  INIT_MIRROR(mVolume, 1.0),
+  INIT_MIRROR(mLogicalPlaybackRate, 1.0),
+  INIT_MIRROR(mPreservesPitch, true),
+  INIT_MIRROR(mSameOriginMedia, false),
+  INIT_MIRROR(mMediaPrincipalHandle, PRINCIPAL_HANDLE_NONE),
+  INIT_MIRROR(mPlaybackBytesPerSecond, 0.0),
+  INIT_MIRROR(mPlaybackRateReliable, true),
+  INIT_MIRROR(mDecoderPosition, 0),
+  INIT_MIRROR(mMediaSeekable, true),
+  INIT_MIRROR(mMediaSeekableOnlyInBufferedRanges, false),
+  INIT_MIRROR(mIsVisible, true),
+  INIT_CANONICAL(mDuration, NullableTimeUnit()),
+  INIT_CANONICAL(mIsShutdown, false),
+  INIT_CANONICAL(mNextFrameStatus, MediaDecoderOwner::NEXT_FRAME_UNINITIALIZED),
+  INIT_CANONICAL(mCurrentPosition, 0),
+  INIT_CANONICAL(mPlaybackOffset, 0),
+  INIT_CANONICAL(mIsAudioDataAudible, false)
 {
   MOZ_COUNT_CTOR(MediaDecoderStateMachine);
   NS_ASSERTION(NS_IsMainThread(), "Should be on main thread.");
 
   InitVideoQueuePrefs();
   InitSuspendBackgroundPref();
 
   mBufferingWait = IsRealTime() ? 0 : 15;
@@ -319,16 +305,20 @@ MediaDecoderStateMachine::MediaDecoderSt
   // machine isn't woken up at reliable intervals to set the next frame,
   // and we drop frames while painting. Note that multiple calls to this
   // function per-process is OK, provided each call is matched by a corresponding
   // timeEndPeriod() call.
   timeBeginPeriod(1);
 #endif
 }
 
+#undef INIT_WATCHABLE
+#undef INIT_MIRROR
+#undef INIT_CANONICAL
+
 MediaDecoderStateMachine::~MediaDecoderStateMachine()
 {
   MOZ_ASSERT(NS_IsMainThread(), "Should be on main thread.");
   MOZ_COUNT_DTOR(MediaDecoderStateMachine);
 
 #ifdef XP_WIN
   timeEndPeriod(1);
 #endif
--- a/dom/media/MediaDecoderStateMachine.h
+++ b/dom/media/MediaDecoderStateMachine.h
@@ -405,17 +405,17 @@ protected:
   // May not be invoked when mReader->UseBufferingHeuristics() is false.
   bool HasLowDecodedData(int64_t aAudioUsecs);
 
   bool OutOfDecodedAudio();
 
   bool OutOfDecodedVideo()
   {
     MOZ_ASSERT(OnTaskQueue());
-    return IsVideoDecoding() && !VideoQueue().IsFinished() && VideoQueue().GetSize() <= 1;
+    return IsVideoDecoding() && VideoQueue().GetSize() <= 1;
   }
 
 
   // Returns true if we're running low on data which is not yet decoded.
   // The decoder monitor must be held.
   bool HasLowUndecodedData();
 
   // Returns true if we have less than aUsecs of undecoded data available.
--- a/dom/media/VideoPlaybackQuality.cpp
+++ b/dom/media/VideoPlaybackQuality.cpp
@@ -11,19 +11,19 @@
 #include "nsCycleCollectionParticipant.h"
 #include "nsWrapperCache.h"
 
 namespace mozilla {
 namespace dom {
 
 VideoPlaybackQuality::VideoPlaybackQuality(HTMLMediaElement* aElement,
                                            DOMHighResTimeStamp aCreationTime,
-                                           uint64_t aTotalFrames,
-                                           uint64_t aDroppedFrames,
-                                           uint64_t aCorruptedFrames)
+                                           uint32_t aTotalFrames,
+                                           uint32_t aDroppedFrames,
+                                           uint32_t aCorruptedFrames)
   : mElement(aElement)
   , mCreationTime(aCreationTime)
   , mTotalFrames(aTotalFrames)
   , mDroppedFrames(aDroppedFrames)
   , mCorruptedFrames(aCorruptedFrames)
 {
 }
 
--- a/dom/media/VideoPlaybackQuality.h
+++ b/dom/media/VideoPlaybackQuality.h
@@ -16,50 +16,52 @@ namespace mozilla {
 namespace dom {
 
 class VideoPlaybackQuality final : public nsWrapperCache
 {
 public:
   NS_INLINE_DECL_CYCLE_COLLECTING_NATIVE_REFCOUNTING(VideoPlaybackQuality)
   NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_NATIVE_CLASS(VideoPlaybackQuality)
 
-  VideoPlaybackQuality(HTMLMediaElement* aElement, DOMHighResTimeStamp aCreationTime,
-                       uint64_t aTotalFrames, uint64_t aDroppedFrames,
-                       uint64_t aCorruptedFrames);
+  VideoPlaybackQuality(HTMLMediaElement* aElement,
+                       DOMHighResTimeStamp aCreationTime,
+                       uint32_t aTotalFrames,
+                       uint32_t aDroppedFrames,
+                       uint32_t aCorruptedFrames);
 
   HTMLMediaElement* GetParentObject() const;
 
   JSObject* WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto) override;
 
   DOMHighResTimeStamp CreationTime() const
   {
     return mCreationTime;
   }
 
-  uint64_t TotalVideoFrames()
+  uint32_t TotalVideoFrames()
   {
     return mTotalFrames;
   }
 
-  uint64_t DroppedVideoFrames()
+  uint32_t DroppedVideoFrames()
   {
     return mDroppedFrames;
   }
 
-  uint64_t CorruptedVideoFrames()
+  uint32_t CorruptedVideoFrames()
   {
     return mCorruptedFrames;
   }
 
 private:
   ~VideoPlaybackQuality() {}
 
   RefPtr<HTMLMediaElement> mElement;
   DOMHighResTimeStamp mCreationTime;
-  uint64_t mTotalFrames;
-  uint64_t mDroppedFrames;
-  uint64_t mCorruptedFrames;
+  uint32_t mTotalFrames;
+  uint32_t mDroppedFrames;
+  uint32_t mCorruptedFrames;
 };
 
 } // namespace dom
 } // namespace mozilla
 
 #endif /* mozilla_dom_VideoPlaybackQuality_h_ */
--- a/extensions/cookie/nsPermissionManager.cpp
+++ b/extensions/cookie/nsPermissionManager.cpp
@@ -2240,16 +2240,46 @@ NS_IMETHODIMP nsPermissionManager::GetEn
                          permEntry.mExpireType,
                          permEntry.mExpireTime));
     }
   }
 
   return NS_NewArrayEnumerator(aEnum, array);
 }
 
+NS_IMETHODIMP nsPermissionManager::GetAllForURI(nsIURI* aURI, nsISimpleEnumerator **aEnum)
+{
+  nsCOMArray<nsIPermission> array;
+
+  nsCOMPtr<nsIPrincipal> principal;
+  nsresult rv = GetPrincipal(aURI, getter_AddRefs(principal));
+  NS_ENSURE_SUCCESS(rv, rv);
+
+  RefPtr<PermissionKey> key = new PermissionKey(principal);
+  PermissionHashKey* entry = mPermissionTable.GetEntry(key);
+
+  if (entry) {
+    for (const auto& permEntry : entry->GetPermissions()) {
+      // Only return custom permissions
+      if (permEntry.mPermission == nsIPermissionManager::UNKNOWN_ACTION) {
+        continue;
+      }
+
+      array.AppendObject(
+        new nsPermission(principal,
+                         mTypeArray.ElementAt(permEntry.mType),
+                         permEntry.mPermission,
+                         permEntry.mExpireType,
+                         permEntry.mExpireTime));
+    }
+  }
+
+  return NS_NewArrayEnumerator(aEnum, array);
+}
+
 NS_IMETHODIMP nsPermissionManager::Observe(nsISupports *aSubject, const char *aTopic, const char16_t *someData)
 {
   ENSURE_NOT_CHILD_PROCESS;
 
   if (!nsCRT::strcmp(aTopic, "profile-before-change")) {
     // The profile is about to change,
     // or is going away because the application is shutting down.
     mIsShuttingDown = true;
new file mode 100644
--- /dev/null
+++ b/extensions/cookie/test/unit/test_permmanager_getAllForURI.js
@@ -0,0 +1,78 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+function check_enumerator(uri, permissions) {
+  let pm = Cc["@mozilla.org/permissionmanager;1"]
+           .getService(Ci.nsIPermissionManager);
+
+  let enumerator = pm.getAllForURI(uri);
+  for ([type, capability] of permissions) {
+    let perm = enumerator.getNext();
+    do_check_true(perm != null);
+    do_check_true(perm.principal.URI.equals(uri));
+    do_check_eq(perm.type, type);
+    do_check_eq(perm.capability, capability);
+    do_check_eq(perm.expireType, pm.EXPIRE_NEVER);
+  }
+  do_check_false(enumerator.hasMoreElements());
+}
+
+function run_test() {
+  let pm = Cc["@mozilla.org/permissionmanager;1"]
+           .getService(Ci.nsIPermissionManager);
+
+  let uri = NetUtil.newURI("http://example.com");
+  let sub = NetUtil.newURI("http://sub.example.com");
+
+  check_enumerator(uri, [ ]);
+
+  pm.add(uri, "test/getallforuri", pm.ALLOW_ACTION);
+  check_enumerator(uri, [
+    [ "test/getallforuri", pm.ALLOW_ACTION ]
+  ]);
+
+  // check that uris are matched exactly
+  check_enumerator(sub, [ ]);
+
+  pm.add(sub, "test/getallforuri", pm.PROMPT_ACTION);
+  pm.add(sub, "test/getallforuri2", pm.DENY_ACTION);
+
+  check_enumerator(sub, [
+    [ "test/getallforuri", pm.PROMPT_ACTION ],
+    [ "test/getallforuri2", pm.DENY_ACTION ]
+  ]);
+
+  // check that the original uri list has not changed
+  check_enumerator(uri, [
+    [ "test/getallforuri", pm.ALLOW_ACTION ]
+  ]);
+
+  // check that UNKNOWN_ACTION permissions are ignored
+  pm.add(uri, "test/getallforuri2", pm.UNKNOWN_ACTION);
+  pm.add(uri, "test/getallforuri3", pm.DENY_ACTION);
+
+  check_enumerator(uri, [
+    [ "test/getallforuri", pm.ALLOW_ACTION ],
+    [ "test/getallforuri3", pm.DENY_ACTION ]
+  ]);
+
+  // check that permission updates are reflected
+  pm.add(uri, "test/getallforuri", pm.PROMPT_ACTION);
+
+  check_enumerator(uri, [
+    [ "test/getallforuri", pm.PROMPT_ACTION ],
+    [ "test/getallforuri3", pm.DENY_ACTION ]
+  ]);
+
+  // check that permission removals are reflected
+  pm.remove(uri, "test/getallforuri");
+
+  check_enumerator(uri, [
+    [ "test/getallforuri3", pm.DENY_ACTION ]
+  ]);
+
+  pm.removeAll();
+  check_enumerator(uri, [ ]);
+  check_enumerator(sub, [ ]);
+}
+
--- a/extensions/cookie/test/unit/xpcshell.ini
+++ b/extensions/cookie/test/unit/xpcshell.ini
@@ -17,16 +17,17 @@ skip-if = true # Bug 863738
 [test_cookies_read.js]
 [test_cookies_sync_failure.js]
 [test_cookies_thirdparty.js]
 [test_cookies_thirdparty_session.js]
 [test_domain_eviction.js]
 [test_eviction.js]
 [test_permmanager_defaults.js]
 [test_permmanager_expiration.js]
+[test_permmanager_getAllForURI.js]
 [test_permmanager_getPermissionObject.js]
 [test_permmanager_notifications.js]
 [test_permmanager_removeall.js]
 [test_permmanager_removesince.js]
 [test_permmanager_removeforapp.js]
 [test_permmanager_load_invalid_entries.js]
 skip-if = debug == true
 [test_permmanager_idn.js]
new file mode 100644
--- /dev/null
+++ b/layout/tools/reftest/mach_test_package_commands.py
@@ -0,0 +1,53 @@
+# 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/.
+
+from __future__ import unicode_literals
+
+import os
+from functools import partial
+
+from mach.decorators import (
+    CommandProvider,
+    Command,
+)
+
+
+def run_reftest(context, **kwargs):
+    kwargs['certPath'] = context.certs_dir
+    kwargs['utilityPath'] = context.bin_dir
+    kwargs['extraProfileFiles'].append(os.path.join(context.bin_dir, 'plugins'))
+
+    if not kwargs['app']:
+        # This could still return None in which case --appname must be used
+        # to specify the firefox binary.
+        kwargs['app'] = context.find_firefox()
+
+    if not kwargs['tests']:
+        kwargs['tests'] = [os.path.join('layout', 'reftests', 'reftest.list')]
+
+    test_root = os.path.join(context.package_root, 'reftest', 'tests')
+    normalize = partial(context.normalize_test_path, test_root)
+    kwargs['tests'] = map(normalize, kwargs['tests'])
+
+    from runreftest import run as run_test_harness
+    return run_test_harness(**kwargs)
+
+
+def setup_argument_parser():
+    from reftestcommandline import DesktopArgumentsParser
+    return DesktopArgumentsParser()
+
+
+@CommandProvider
+class ReftestCommands(object):
+
+    def __init__(self, context):
+        self.context = context
+
+    @Command('reftest', category='testing',
+             description='Run the reftest harness.',
+             parser=setup_argument_parser)
+    def reftest(self, **kwargs):
+        kwargs['suite'] = 'reftest'
+        return run_reftest(self.context, **kwargs)
--- a/layout/tools/reftest/moz.build
+++ b/layout/tools/reftest/moz.build
@@ -17,16 +17,17 @@ GENERATED_FILES += ['automation.py']
 TEST_HARNESS_FILES.reftest += [
     '!automation.py',
     '/build/mobile/b2gautomation.py',
     '/build/mobile/remoteautomation.py',
     '/build/pgo/server-locations.txt',
     '/testing/mochitest/server.js',
     'b2g_start_script.js',
     'gaia_lock_screen.js',
+    'mach_test_package_commands.py',
     'output.py',
     'reftest-preferences.js',
     'reftestcommandline.py',
     'remotereftest.py',
     'runreftest.py',
     'runreftestb2g.py',
     'runreftestmulet.py',
 ]
--- a/layout/tools/reftest/reftestcommandline.py
+++ b/layout/tools/reftest/reftestcommandline.py
@@ -367,28 +367,16 @@ class DesktopArgumentsParser(ReftestArgu
         if options.app is None:
             bin_dir = (self.build_obj.get_binary_path() if
                        self.build_obj and self.build_obj.substs[
                            'MOZ_BUILD_APP'] != 'mobile/android'
                        else None)
 
             if bin_dir:
                 options.app = bin_dir
-            else:
-                self.error(
-                    "could not find the application path, --appname must be specified")
-
-        options.app = reftest.getFullPath(options.app)
-        if not os.path.exists(options.app):
-            self.error("""Error: Path %(app)s doesn't exist.
-            Are you executing $objdir/_tests/reftest/runreftest.py?"""
-                       % {"app": options.app})
-
-        if options.xrePath is None:
-            options.xrePath = os.path.dirname(options.app)
 
         if options.symbolsPath and len(urlparse(options.symbolsPath).scheme) < 2:
             options.symbolsPath = reftest.getFullPath(options.symbolsPath)
 
         options.utilityPath = reftest.getFullPath(options.utilityPath)
 
 
 class B2GArgumentParser(ReftestArgumentsParser):
--- a/layout/tools/reftest/runreftest.py
+++ b/layout/tools/reftest/runreftest.py
@@ -32,18 +32,21 @@ import mozprocess
 import mozprofile
 import mozrunner
 from mozrunner.utils import get_stack_fixer_function, test_environment
 from mozscreenshot import printstatus, dump_screen
 
 try:
     from marionette import Marionette
     from marionette_driver.addons import Addons
-except ImportError:
-    Marionette=None
+except ImportError, e:
+    # Defer ImportError until attempt to use Marionette
+    def reraise(*args, **kwargs):
+        raise(e)
+    Marionette = reraise
 
 from output import OutputHandler, ReftestFormatter
 import reftestcommandline
 
 here = os.path.abspath(os.path.dirname(__file__))
 
 try:
     from mozbuild.base import MozbuildObject
@@ -720,13 +723,27 @@ def run(**kwargs):
     if 'tests' in kwargs:
         options = parser.parse_args(kwargs["tests"])
     else:
         options = parser.parse_args()
 
     reftest = RefTest()
     parser.validate(options, reftest)
 
+    # We have to validate options.app here for the case when the mach
+    # command is able to find it after argument parsing. This can happen
+    # when running from a tests.zip.
+    if not options.app:
+        parser.error("could not find the application path, --appname must be specified")
+
+    options.app = reftest.getFullPath(options.app)
+    if not os.path.exists(options.app):
+        parser.error("Error: Path %(app)s doesn't exist. Are you executing "
+                     "$objdir/_tests/reftest/runreftest.py?" % {"app": options.app})
+
+    if options.xrePath is None:
+        options.xrePath = os.path.dirname(options.app)
+
     return reftest.runTests(options.tests, options)
 
 
 if __name__ == "__main__":
     sys.exit(run())
--- a/mobile/android/base/java/org/mozilla/gecko/GeckoApp.java
+++ b/mobile/android/base/java/org/mozilla/gecko/GeckoApp.java
@@ -191,16 +191,97 @@ public abstract class GeckoApp
     private View mFullScreenPluginView;
 
     private final HashMap<String, PowerManager.WakeLock> mWakeLocks = new HashMap<String, PowerManager.WakeLock>();
 
     protected boolean mLastSessionCrashed;
     protected boolean mShouldRestore;
     private boolean mSessionRestoreParsingFinished = false;
 
+    private static final class LastSessionParser extends SessionParser {
+        private JSONArray tabs;
+        private JSONObject windowObject;
+        private boolean isExternalURL;
+
+        private boolean selectNextTab;
+        private boolean tabsWereSkipped;
+        private boolean tabsWereProcessed;
+
+        public LastSessionParser(JSONArray tabs, JSONObject windowObject, boolean isExternalURL) {
+            this.tabs = tabs;
+            this.windowObject = windowObject;
+            this.isExternalURL = isExternalURL;
+        }
+
+        public boolean allTabsSkipped() {
+            return tabsWereSkipped && !tabsWereProcessed;
+        }
+
+        @Override
+        public void onTabRead(final SessionTab sessionTab) {
+            if (sessionTab.isAboutHomeWithoutHistory()) {
+                // This is a tab pointing to about:home with no history. We won't restore
+                // this tab. If we end up restoring no tabs then the browser will decide
+                // whether it needs to open about:home or a different 'homepage'. If we'd
+                // always restore about:home only tabs then we'd never open the homepage.
+                // See bug 1261008.
+
+                if (sessionTab.isSelected()) {
+                    // Unfortunately this tab is the selected tab. Let's just try to select
+                    // the first tab. If we haven't restored any tabs so far then remember
+                    // to select the next tab that gets restored.
+
+                    if (!Tabs.getInstance().selectLastTab()) {
+                        selectNextTab = true;
+                    }
+                }
+
+                // Do not restore this tab.
+                tabsWereSkipped = true;
+                return;
+            }
+
+            tabsWereProcessed = true;
+
+            JSONObject tabObject = sessionTab.getTabObject();
+
+            int flags = Tabs.LOADURL_NEW_TAB;
+            flags |= ((isExternalURL || !sessionTab.isSelected()) ? Tabs.LOADURL_DELAY_LOAD : 0);
+            flags |= (tabObject.optBoolean("desktopMode") ? Tabs.LOADURL_DESKTOP : 0);
+            flags |= (tabObject.optBoolean("isPrivate") ? Tabs.LOADURL_PRIVATE : 0);
+
+            final Tab tab = Tabs.getInstance().loadUrl(sessionTab.getUrl(), flags);
+
+            if (selectNextTab) {
+                // We did not restore the selected tab previously. Now let's select this tab.
+                Tabs.getInstance().selectTab(tab.getId());
+                selectNextTab = false;
+            }
+
+            ThreadUtils.postToUiThread(new Runnable() {
+                @Override
+                public void run() {
+                    tab.updateTitle(sessionTab.getTitle());
+                }
+            });
+
+            try {
+                tabObject.put("tabId", tab.getId());
+            } catch (JSONException e) {
+                Log.e(LOGTAG, "JSON error", e);
+            }
+            tabs.put(tabObject);
+        }
+
+        @Override
+        public void onClosedTabsRead(final JSONArray closedTabData) throws JSONException {
+            windowObject.put("closedTabs", closedTabData);
+        }
+    };
+
     protected boolean mInitialized;
     protected boolean mWindowFocusInitialized;
     private Telemetry.Timer mJavaUiStartupTimer;
     private Telemetry.Timer mGeckoReadyStartupTimer;
 
     private String mPrivateBrowsingSession;
 
     private volatile HealthRecorder mHealthRecorder;
@@ -1283,16 +1364,27 @@ public abstract class GeckoApp
                         // of the tab stubs into the JSON data (which holds the session
                         // history). This JSON data is then sent to Gecko so session
                         // history can be restored for each tab.
                         final SafeIntent intent = new SafeIntent(getIntent());
                         restoreMessage = restoreSessionTabs(invokedWithExternalURL(getIntentURI(intent)));
                     } catch (SessionRestoreException e) {
                         // If restore failed, do a normal startup
                         Log.e(LOGTAG, "An error occurred during restore", e);
+                        // If mShouldRestore was already set to false in restoreSessionTabs(),
+                        // this means that we intentionally skipped all tabs read from the
+                        // session file, so we don't have to report this exception in telemetry
+                        // and can ignore the following bit.
+                        if (mShouldRestore && getProfile().sessionFileExistsAndNotEmptyWindow()) {
+                            // If we got a SessionRestoreException even though the file exists and its
+                            // length doesn't match the known length of an intentionally empty file,
+                            // it's very likely we've encountered a damaged/corrupt session store file.
+                            Log.d(LOGTAG, "Suspecting a damaged session store file.");
+                            Telemetry.addToHistogram("FENNEC_SESSIONSTORE_DAMAGED_SESSION_FILE", 1);
+                        }
                         mShouldRestore = false;
                     }
                 }
 
                 synchronized (GeckoApp.this) {
                     mSessionRestoreParsingFinished = true;
                     GeckoApp.this.notifyAll();
                 }
@@ -1675,88 +1767,35 @@ public abstract class GeckoApp
             }
 
             // If we are doing an OOM restore, parse the session data and
             // stub the restored tabs immediately. This allows the UI to be
             // updated before Gecko has restored.
             if (mShouldRestore) {
                 final JSONArray tabs = new JSONArray();
                 final JSONObject windowObject = new JSONObject();
-                SessionParser parser = new SessionParser() {
-                    private boolean selectNextTab;
-
-                    @Override
-                    public void onTabRead(final SessionTab sessionTab) {
-                        if (sessionTab.isAboutHomeWithoutHistory()) {
-                            // This is a tab pointing to about:home with no history. We won't restore
-                            // this tab. If we end up restoring no tabs then the browser will decide
-                            // whether it needs to open about:home or a different 'homepage'. If we'd
-                            // always restore about:home only tabs then we'd never open the homepage.
-                            // See bug 1261008.
-
-                            if (sessionTab.isSelected()) {
-                                // Unfortunately this tab is the selected tab. Let's just try to select
-                                // the first tab. If we haven't restored any tabs so far then remember
-                                // to select the next tab that gets restored.
-
-                                if (!Tabs.getInstance().selectLastTab()) {
-                                    selectNextTab = true;
-                                }
-                            }
-
-                            // Do not restore this tab.
-                            return;
-                        }
-
-                        JSONObject tabObject = sessionTab.getTabObject();
-
-                        int flags = Tabs.LOADURL_NEW_TAB;
-                        flags |= ((isExternalURL || !sessionTab.isSelected()) ? Tabs.LOADURL_DELAY_LOAD : 0);
-                        flags |= (tabObject.optBoolean("desktopMode") ? Tabs.LOADURL_DESKTOP : 0);
-                        flags |= (tabObject.optBoolean("isPrivate") ? Tabs.LOADURL_PRIVATE : 0);
-
-                        final Tab tab = Tabs.getInstance().loadUrl(sessionTab.getUrl(), flags);
-
-                        if (selectNextTab) {
-                            // We did not restore the selected tab previously. Now let's select this tab.
-                            Tabs.getInstance().selectTab(tab.getId());
-                            selectNextTab = false;
-                        }
-
-                        ThreadUtils.postToUiThread(new Runnable() {
-                            @Override
-                            public void run() {
-                                tab.updateTitle(sessionTab.getTitle());
-                            }
-                        });
-
-                        try {
-                            tabObject.put("tabId", tab.getId());
-                        } catch (JSONException e) {
-                            Log.e(LOGTAG, "JSON error", e);
-                        }
-                        tabs.put(tabObject);
-                    }
-
-                    @Override
-                    public void onClosedTabsRead(final JSONArray closedTabData) throws JSONException {
-                        windowObject.put("closedTabs", closedTabData);
-                    }
-                };
+
+                LastSessionParser parser = new LastSessionParser(tabs, windowObject, isExternalURL);
 
                 if (mPrivateBrowsingSession == null) {
                     parser.parse(sessionString);
                 } else {
                     parser.parse(sessionString, mPrivateBrowsingSession);
                 }
 
                 if (tabs.length() > 0) {
                     windowObject.put("tabs", tabs);
                     sessionString = new JSONObject().put("windows", new JSONArray().put(windowObject)).toString();
                 } else {
+                    if (parser.allTabsSkipped()) {
+                        // If we intentionally skipped all tabs we've read from the session file, we
+                        // set mShouldRestore back to false at this point already, so the calling code
+                        // can infer that the exception wasn't due to a damaged session store file.
+                        mShouldRestore = false;
+                    }
                     throw new SessionRestoreException("No tabs could be read from session file");
                 }
             }
 
             JSONObject restoreData = new JSONObject();
             restoreData.put("sessionString", sessionString);
             return restoreData.toString();
         } catch (JSONException e) {
--- a/mobile/android/base/java/org/mozilla/gecko/GeckoProfile.java
+++ b/mobile/android/base/java/org/mozilla/gecko/GeckoProfile.java
@@ -71,16 +71,17 @@ public final class GeckoProfile {
     // Profile is using a custom directory outside of the Mozilla directory.
     public static final String CUSTOM_PROFILE = "";
     public static final String GUEST_PROFILE_DIR = "guest";
 
     // Session store
     private static final String SESSION_FILE = "sessionstore.js";
     private static final String SESSION_FILE_BACKUP = "sessionstore.bak";
     private static final long MAX_BACKUP_FILE_AGE = 1000 * 3600 * 24; // 24 hours
+    private static final int SESSION_STORE_EMPTY_JSON_LENGTH = 14; // length of {"windows":[]}
 
     private boolean mOldSessionDataProcessed = false;
 
     private static final ConcurrentHashMap<String, GeckoProfile> sProfileCache =
             new ConcurrentHashMap<String, GeckoProfile>(
                     /* capacity */ 4, /* load factor */ 0.75f, /* concurrency */ 2);
     private static String sDefaultProfileName;
 
@@ -649,16 +650,29 @@ public final class GeckoProfile {
             }
         } catch (IOException ioe) {
             Log.e(LOGTAG, "Unable to read session file", ioe);
         }
         return null;
     }
 
     /**
+     * Checks whether the session store file exists and that its length
+     * doesn't match the known length of a session store file containing
+     * only an empty window.
+     */
+    public boolean sessionFileExistsAndNotEmptyWindow() {
+        File sessionFile = getFile(SESSION_FILE);
+
+        return sessionFile != null &&
+               sessionFile.exists() &&
+               sessionFile.length() != SESSION_STORE_EMPTY_JSON_LENGTH;
+    }
+
+    /**
      * Ensures the parent director(y|ies) of the given filename exist by making them
      * if they don't already exist..
      *
      * @param filename The path to the file whose parents should be made directories
      * @return true if the parent directory exists, false otherwise
      */
     @WorkerThread
     protected boolean ensureParentDirs(final String filename) {
--- a/mobile/android/base/java/org/mozilla/gecko/GeckoProfileDirectories.java
+++ b/mobile/android/base/java/org/mozilla/gecko/GeckoProfileDirectories.java
@@ -133,24 +133,24 @@ public class GeckoProfileDirectories {
      *
      * @return null if there is no "Default" entry in profiles.ini, or the profile
      *         name if there is.
      * @throws NoMozillaDirectoryException
      *             if the Mozilla directory did not exist and could not be created.
      */
     static String findDefaultProfileName(final Context context) throws NoMozillaDirectoryException {
       final INIParser parser = GeckoProfileDirectories.getProfilesINI(getMozillaDirectory(context));
-
-      for (Enumeration<INISection> e = parser.getSections().elements(); e.hasMoreElements();) {
-          final INISection section = e.nextElement();
-          if (section.getIntProperty("Default") == 1) {
-              return section.getStringProperty("Name");
+      if (parser.getSections() != null) {
+          for (Enumeration<INISection> e = parser.getSections().elements(); e.hasMoreElements(); ) {
+              final INISection section = e.nextElement();
+              if (section.getIntProperty("Default") == 1) {
+                  return section.getStringProperty("Name");
+              }
           }
       }
-
       return null;
     }
 
     static Map<String, String> getDefaultProfile(final File mozillaDir) {
         return getMatchingProfiles(mozillaDir, sectionIsDefault, true);
     }
 
     static Map<String, String> getProfilesNamed(final File mozillaDir, final String name) {
@@ -186,43 +186,45 @@ public class GeckoProfileDirectories {
      *            matches the predicate; if false, all matching results are
      *            included.
      * @return a {@link Map} from name to path.
      */
     public static Map<String, String> getMatchingProfiles(final File mozillaDir, INISectionPredicate predicate, boolean stopOnSuccess) {
         final HashMap<String, String> result = new HashMap<String, String>();
         final INIParser parser = GeckoProfileDirectories.getProfilesINI(mozillaDir);
 
-        for (Enumeration<INISection> e = parser.getSections().elements(); e.hasMoreElements();) {
-            final INISection section = e.nextElement();
-            if (predicate == null || predicate.matches(section)) {
-                final String name = section.getStringProperty("Name");
-                final String pathString = section.getStringProperty("Path");
-                final boolean isRelative = section.getIntProperty("IsRelative") == 1;
-                final File path = isRelative ? new File(mozillaDir, pathString) : new File(pathString);
-                result.put(name, path.getAbsolutePath());
+        if (parser.getSections() != null) {
+            for (Enumeration<INISection> e = parser.getSections().elements(); e.hasMoreElements(); ) {
+                final INISection section = e.nextElement();
+                if (predicate == null || predicate.matches(section)) {
+                    final String name = section.getStringProperty("Name");
+                    final String pathString = section.getStringProperty("Path");
+                    final boolean isRelative = section.getIntProperty("IsRelative") == 1;
+                    final File path = isRelative ? new File(mozillaDir, pathString) : new File(pathString);
+                    result.put(name, path.getAbsolutePath());
 
-                if (stopOnSuccess) {
-                    return result;
+                    if (stopOnSuccess) {
+                        return result;
+                    }
                 }
             }
         }
         return result;
     }
 
     public static File findProfileDir(final File mozillaDir, final String profileName) throws NoSuchProfileException {
         // Open profiles.ini to find the correct path.
         final INIParser parser = GeckoProfileDirectories.getProfilesINI(mozillaDir);
-
-        for (Enumeration<INISection> e = parser.getSections().elements(); e.hasMoreElements();) {
-            final INISection section = e.nextElement();
-            final String name = section.getStringProperty("Name");
-            if (name != null && name.equals(profileName)) {
-                if (section.getIntProperty("IsRelative") == 1) {
-                    return new File(mozillaDir, section.getStringProperty("Path"));
+        if (parser.getSections() != null) {
+            for (Enumeration<INISection> e = parser.getSections().elements(); e.hasMoreElements(); ) {
+                final INISection section = e.nextElement();
+                final String name = section.getStringProperty("Name");
+                if (name != null && name.equals(profileName)) {
+                    if (section.getIntProperty("IsRelative") == 1) {
+                        return new File(mozillaDir, section.getStringProperty("Path"));
+                    }
+                    return new File(section.getStringProperty("Path"));
                 }
-                return new File(section.getStringProperty("Path"));
             }
         }
-
         throw new NoSuchProfileException("No profile " + profileName);
     }
 }
--- a/mobile/android/base/locales/en-US/android_strings.dtd
+++ b/mobile/android/base/locales/en-US/android_strings.dtd
@@ -271,17 +271,17 @@
 
 <!ENTITY pref_whats_new_notification "What\'s new in &brandShortName;">
 <!ENTITY pref_whats_new_notification_summary "Learn about new features after an update">
 
 <!-- Custom Tabs is an Android API for allowing third-party apps to open URLs in a customized UI.
      Instead of switching to the browser it appears as if the user stays in the third-party app.
      For more see: https://developer.chrome.com/multidevice/android/customtabs -->
 <!ENTITY pref_custom_tabs "Custom Tabs">
-<!ENTITY pref_custom_tabs_summary "Allow third-party apps to open URLs with a customized look and feel. ">
+<!ENTITY pref_custom_tabs_summary "Allow third-party apps to open URLs with a customized look and feel.">
 
 <!ENTITY tracking_protection_prompt_title "Now with Tracking Protection">
 <!ENTITY tracking_protection_prompt_text "Actively block tracking elements so you don\'t have to worry.">
 <!ENTITY tracking_protection_prompt_tip_text "Visit Privacy settings to learn more">
 <!ENTITY tracking_protection_prompt_action_button "Got it!">
 
 <!ENTITY tab_queue_toast_message3 "Tab saved in &brandShortName;">
 <!ENTITY tab_queue_toast_action "Open now">
--- a/netwerk/base/nsIPermissionManager.idl
+++ b/netwerk/base/nsIPermissionManager.idl
@@ -89,16 +89,25 @@ interface nsIPermissionManager : nsISupp
    */
   void add(in nsIURI uri,
            in string type,
            in uint32_t permission,
            [optional] in uint32_t expireType,
            [optional] in int64_t expireTime);
 
   /**
+   * Get all custom permissions for a given URI. This will return
+   * an enumerator of all permissions which are not set to default
+   * and which belong to the matching prinicpal of the given URI.
+   *
+   * @param uri  the URI to get all permissions for
+   */
+  nsISimpleEnumerator getAllForURI(in nsIURI uri);
+
+  /**
    * Add permission information for a given principal.
    * It is internally calling the other add() method using the nsIURI from the
    * principal.
    * Passing a system principal will be a no-op because they will always be
    * granted permissions.
    */
   void addFromPrincipal(in nsIPrincipal principal, in string typed,
                         in uint32_t permission,
--- a/python/mozbuild/mozbuild/action/test_archive.py
+++ b/python/mozbuild/mozbuild/action/test_archive.py
@@ -358,16 +358,17 @@ ARCHIVE_FILES = {
             'pattern': '**',
             'dest': 'xpcshell/tests',
         },
         {
             'source': buildconfig.topsrcdir,
             'base': 'testing/xpcshell',
             'patterns': [
                 'head.js',
+                'mach_test_package_commands.py',
                 'moz-http2/**',
                 'moz-spdy/**',
                 'node-http2/**',
                 'node-spdy/**',
                 'remotexpcshelltests.py',
                 'runtestsb2g.py',
                 'runxpcshelltests.py',
                 'xpcshellcommandline.py',
--- a/testing/tools/mach_test_package_bootstrap.py
+++ b/testing/tools/mach_test_package_bootstrap.py
@@ -1,49 +1,56 @@
 # 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/.
 
 from __future__ import print_function, unicode_literals
 
+import json
 import os
 import platform
 import sys
+import types
 
 
 SEARCH_PATHS = [
     'marionette',
     'marionette/marionette/runner/mixins/browsermob-proxy-py',
     'marionette/client',
     'mochitest',
+    'mozbase/manifestparser',
     'mozbase/mozcrash',
     'mozbase/mozdebug',
     'mozbase/mozdevice',
     'mozbase/mozfile',
     'mozbase/mozhttpd',
+    'mozbase/mozinfo',
+    'mozbase/mozinstall',
     'mozbase/mozleak',
     'mozbase/mozlog',
     'mozbase/moznetwork',
     'mozbase/mozprocess',
     'mozbase/mozprofile',
     'mozbase/mozrunner',
+    'mozbase/mozscreenshot',
     'mozbase/mozsystemmonitor',
-    'mozbase/mozinfo',
-    'mozbase/mozscreenshot',
     'mozbase/moztest',
     'mozbase/mozversion',
-    'mozbase/manifestparser',
+    'reftest',
     'tools/mach',
     'tools/wptserve',
+    'xpcshell',
 ]
 
 # Individual files providing mach commands.
 MACH_MODULES = [
     'mochitest/mach_test_package_commands.py',
+    'reftest/mach_test_package_commands.py',
     'tools/mach/mach/commands/commandinfo.py',
+    'xpcshell/mach_test_package_commands.py',
 ]
 
 
 CATEGORIES = {
     'testing': {
         'short': 'Testing',
         'long': 'Run tests.',
         'priority': 30,
@@ -55,22 +62,70 @@ CATEGORIES = {
     },
     'misc': {
         'short': 'Potpourri',
         'long': 'Potent potables and assorted snacks.',
         'priority': 10,
     },
     'disabled': {
         'short': 'Disabled',
-        'long': 'The disabled commands are hidden by default. Use -v to display them. These commands are unavailable for your current context, run "mach <command>" to see why.',
+        'long': 'The disabled commands are hidden by default. Use -v to display them. '
+                'These commands are unavailable for your current context, '
+                'run "mach <command>" to see why.',
         'priority': 0,
     }
 }
 
 
+def ancestors(path, depth=0):
+    """Emit the parent directories of a path."""
+    count = 1
+    while path and count != depth:
+        yield path
+        newpath = os.path.dirname(path)
+        if newpath == path:
+            break
+        path = newpath
+        count += 1
+
+
+def find_firefox(context):
+    """Try to automagically find the firefox binary."""
+    import mozinstall
+    search_paths = []
+
+    # Check for a mozharness setup
+    if context.mozharness_config:
+        with open(context.mozharness_config, 'r') as f:
+            config = json.load(f)
+        workdir = os.path.join(config['base_work_dir'], config['work_dir'])
+        search_paths.append(os.path.join(workdir, 'application'))
+
+    # Check for test-stage setup
+    dist_bin = os.path.join(os.path.dirname(context.package_root), 'bin')
+    if os.path.isdir(dist_bin):
+        search_paths.append(dist_bin)
+
+    for path in search_paths:
+        try:
+            return mozinstall.get_binary(path, 'firefox')
+        except mozinstall.InvalidBinary:
+            continue
+
+
+def normalize_test_path(test_root, path):
+    if os.path.isabs(path) or os.path.exists(path):
+        return os.path.normpath(os.path.abspath(path))
+
+    for parent in ancestors(test_root):
+        test_path = os.path.join(parent, path)
+        if os.path.exists(test_path):
+            return os.path.normpath(os.path.abspath(test_path))
+
+
 def bootstrap(test_package_root):
     test_package_root = os.path.abspath(test_package_root)
 
     # Ensure we are running Python 2.7+. We put this check here so we generate a
     # user-friendly error message rather than a cryptic stack trace on module
     # import.
     if sys.version_info[0] != 2 or sys.version_info[1] < 7:
         print('Python 2.7 or above (but not Python 3) is required to run mach.')
@@ -80,20 +135,32 @@ def bootstrap(test_package_root):
     sys.path[0:0] = [os.path.join(test_package_root, path) for path in SEARCH_PATHS]
     import mach.main
 
     def populate_context(context, key=None):
         if key is not None:
             return
 
         context.package_root = test_package_root
+        context.bin_dir = os.path.join(test_package_root, 'bin')
         context.certs_dir = os.path.join(test_package_root, 'certs')
-        context.bin_dir = os.path.join(test_package_root, 'bin')
         context.modules_dir = os.path.join(test_package_root, 'modules')
 
+        context.ancestors = ancestors
+        context.find_firefox = types.MethodType(find_firefox, context)
+        context.normalize_test_path = normalize_test_path
+
+        # Search for a mozharness localconfig.json
+        context.mozharness_config = None
+        for dir_path in ancestors(test_package_root):
+            mozharness_config = os.path.join(dir_path, 'logs', 'localconfig.json')
+            if os.path.isfile(mozharness_config):
+                context.mozharness_config = mozharness_config
+                break
+
     mach = mach.main.Mach(os.getcwd())
     mach.populate_context_handler = populate_context
 
     for category, meta in CATEGORIES.items():
         mach.define_category(category, meta['short'], meta['long'],
                              meta['priority'])
 
     for path in MACH_MODULES:
new file mode 100644
--- /dev/null
+++ b/testing/xpcshell/mach_test_package_commands.py
@@ -0,0 +1,65 @@
+# 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/.
+
+from __future__ import unicode_literals
+
+import os
+import sys
+from argparse import Namespace
+from functools import partial
+
+
+import mozlog
+from xpcshellcommandline import parser_desktop
+
+from mach.decorators import (
+    CommandProvider,
+    Command,
+)
+
+
+def run_xpcshell(context, **kwargs):
+    args = Namespace(**kwargs)
+    args.utility_path = context.bin_dir
+    args.testingModulesDir = context.modules_dir
+
+    if not args.appPath:
+        args.appPath = os.path.dirname(context.find_firefox())
+
+    if not args.xpcshell:
+        args.xpcshell = os.path.join(args.appPath, 'xpcshell')
+
+    if not args.pluginsPath:
+        for path in context.ancestors(args.appPath, depth=2):
+            test = os.path.join(path, 'plugins')
+            if os.path.isdir(test):
+                args.pluginsPath = test
+                break
+
+    log = mozlog.commandline.setup_logging("XPCShellTests",
+                                           args,
+                                           {"mach": sys.stdout},
+                                           {"verbose": True})
+
+    if args.testPaths:
+        test_root = os.path.join(context.package_root, 'xpcshell', 'tests')
+        normalize = partial(context.normalize_test_path, test_root)
+        args.testPaths = map(normalize, args.testPaths)
+
+    import runxpcshelltests
+    xpcshell = runxpcshelltests.XPCShellTests(log=log)
+    return xpcshell.runTests(**vars(args))
+
+
+@CommandProvider
+class MochitestCommands(object):
+
+    def __init__(self, context):
+        self.context = context
+
+    @Command('xpcshell-test', category='testing',
+             description='Run the xpcshell harness.',
+             parser=parser_desktop)
+    def xpcshell(self, **kwargs):
+        return run_xpcshell(self.context, **kwargs)
--- a/toolkit/components/reader/AboutReader.jsm
+++ b/toolkit/components/reader/AboutReader.jsm
@@ -609,39 +609,50 @@ AboutReader.prototype = {
   _loadArticle: Task.async(function* () {
     let url = this._getOriginalUrl();
     this._showProgressDelayed();
 
     let article;
     if (this._articlePromise) {
       article = yield this._articlePromise;
     } else {
-      article = yield this._getArticle(url);
+      try {
+        article = yield this._getArticle(url);
+      } catch (e) {
+        if (e && e.newURL) {
+          let readerURL = "about:reader?url=" + encodeURIComponent(e.newURL);
+          this._win.location.replace(readerURL);
+          return;
+        }
+      }
     }
 
     if (this._windowUnloaded) {
       return;
     }
 
-    if (article) {
-      this._showContent(article);
-    } else if (this._articlePromise) {
-      // If we were promised an article, show an error message if there's a failure.
+    // Replace the loading message with an error message if there's a failure.
+    // Users are supposed to navigate away by themselves (because we cannot
+    // remove ourselves from session history.)
+    if (!article) {
       this._showError();
-    } else {
-      // Otherwise, just load the original URL. We can encounter this case when
-      // loading an about:reader URL directly (e.g. opening a reading list item).
-      this._win.location.href = url;
+      return;
     }
+
+    this._showContent(article);
   }),
 
   _getArticle: function(url) {
     return new Promise((resolve, reject) => {
       let listener = (message) => {
         this._mm.removeMessageListener("Reader:ArticleData", listener);
+        if (message.data.newURL) {
+          reject({ newURL: message.data.newURL });
+          return;
+        }
         resolve(message.data.article);
       };
       this._mm.addMessageListener("Reader:ArticleData", listener);
       this._mm.sendAsyncMessage("Reader:ArticleGet", { url: url });
     });
   },
 
   _requestFavicon: function() {
--- a/toolkit/components/search/nsSearchService.js
+++ b/toolkit/components/search/nsSearchService.js
@@ -1618,88 +1618,83 @@ Engine.prototype = {
       Services.ww.getNewPrompter(null).alert(title, text);
     }
 
     if (!aBytes) {
       promptError();
       return;
     }
 
-    var engineToUpdate = null;
-    if (aEngine._engineToUpdate) {
-      engineToUpdate = aEngine._engineToUpdate.wrappedJSObject;
-
-      // Make this new engine use the old engine's shortName,
-      // to preserve user-set metadata.
-      aEngine._shortName = engineToUpdate._shortName;
-    }
-
     var parser = Cc["@mozilla.org/xmlextras/domparser;1"].
                  createInstance(Ci.nsIDOMParser);
     var doc = parser.parseFromBuffer(aBytes, aBytes.length, "text/xml");
     aEngine._data = doc.documentElement;
 
     try {
       // Initialize the engine from the obtained data
       aEngine._initFromData();
     } catch (ex) {
       LOG("_onLoad: Failed to init engine!\n" + ex);
       // Report an error to the user
       promptError();
       return;
     }
 
-    // Check that when adding a new engine (e.g., not updating an
-    // existing one), a duplicate engine does not already exist.
-    if (!engineToUpdate) {
+    if (aEngine._engineToUpdate) {
+      let engineToUpdate = aEngine._engineToUpdate.wrappedJSObject;
+
+      // Make this new engine use the old engine's shortName, and preserve
+      // metadata.
+      aEngine._shortName = engineToUpdate._shortName;
+      Object.keys(engineToUpdate._metaData).forEach(key => {
+        aEngine.setAttr(key, engineToUpdate.getAttr(key));
+      });
+      aEngine._loadPath = engineToUpdate._loadPath;
+
+      // Keep track of the last modified date, so that we can make conditional
+      // requests for future updates.
+      aEngine.setAttr("updatelastmodified", (new Date()).toUTCString());
+
+      // Set the new engine's icon, if it doesn't yet have one.
+      if (!aEngine._iconURI && engineToUpdate._iconURI)
+        aEngine._iconURI = engineToUpdate._iconURI;
+    } else {
+      // Check that when adding a new engine (e.g., not updating an
+      // existing one), a duplicate engine does not already exist.
       if (Services.search.getEngineByName(aEngine.name)) {
         // If we're confirming the engine load, then display a "this is a
         // duplicate engine" prompt; otherwise, fail silently.
         if (aEngine._confirm) {
           promptError({ error: "error_duplicate_engine_msg",
                         title: "error_invalid_engine_title"
                       }, Ci.nsISearchInstallCallback.ERROR_DUPLICATE_ENGINE);
         } else {
           onError(Ci.nsISearchInstallCallback.ERROR_DUPLICATE_ENGINE);
         }
         LOG("_onLoad: duplicate engine found, bailing");
         return;
       }
-    }
-
-    // If requested, confirm the addition now that we have the title.
-    // This property is only ever true for engines added via
-    // nsIBrowserSearchService::addEngine.
-    if (aEngine._confirm) {
-      var confirmation = aEngine._confirmAddEngine();
-      LOG("_onLoad: confirm is " + confirmation.confirmed +
-          "; useNow is " + confirmation.useNow);
-      if (!confirmation.confirmed) {
-        onError();
-        return;
+
+      // If requested, confirm the addition now that we have the title.
+      // This property is only ever true for engines added via
+      // nsIBrowserSearchService::addEngine.
+      if (aEngine._confirm) {
+        var confirmation = aEngine._confirmAddEngine();
+        LOG("_onLoad: confirm is " + confirmation.confirmed +
+            "; useNow is " + confirmation.useNow);
+        if (!confirmation.confirmed) {
+          onError();
+          return;
+        }
+        aEngine._useNow = confirmation.useNow;
       }
-      aEngine._useNow = confirmation.useNow;
-    }
-
-    // If we don't yet have a shortName, get one now. We would already have one
-    // if this is an update and _file was set above.
-    if (!aEngine._shortName)
+
       aEngine._shortName = sanitizeName(aEngine.name);
-
-    aEngine._loadPath = aEngine.getAnonymizedLoadPath(null, aEngine._uri);
-    aEngine.setAttr("loadPathHash", getVerificationHash(aEngine._loadPath));
-
-    if (engineToUpdate) {
-      // Keep track of the last modified date, so that we can make conditional
-      // requests for future updates.
-      aEngine.setAttr("updatelastmodified", (new Date()).toUTCString());
-
-      // Set the new engine's icon, if it doesn't yet have one.
-      if (!aEngine._iconURI && engineToUpdate._iconURI)
-        aEngine._iconURI = engineToUpdate._iconURI;
+      aEngine._loadPath = aEngine.getAnonymizedLoadPath(null, aEngine._uri);
+      aEngine.setAttr("loadPathHash", getVerificationHash(aEngine._loadPath));
     }
 
     // Notify the search service of the successful load. It will deal with
     // updates by checking aEngine._engineToUpdate.
     notifyAction(aEngine, SEARCH_ENGINE_LOADED);
 
     // Notify the callback if needed
     if (aEngine._installCallback) {
new file mode 100644
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/test_engineUpdate.js
@@ -0,0 +1,50 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/* Test that user-set metadata isn't lost on engine update */
+
+"use strict";
+
+function run_test() {
+  updateAppInfo();
+  useHttpServer();
+
+  run_next_test();
+}
+
+add_task(function* test_engineUpdate() {
+  const KEYWORD = "keyword";
+  const FILENAME = "engine.xml"
+  const TOPIC = "browser-search-engine-modified";
+  const ONE_DAY_IN_MS = 24 * 60 * 60 * 1000;
+
+  yield asyncInit();
+
+  let [engine] = yield addTestEngines([
+    { name: "Test search engine", xmlFileName: FILENAME },
+  ]);
+
+  engine.alias = KEYWORD;
+  Services.search.moveEngine(engine, 0);
+  // can't have an accurate updateURL in the file since we can't know the test
+  // server origin, so manually set it
+  engine.wrappedJSObject._updateURL = gDataUrl + FILENAME;
+
+  yield new Promise(resolve => {
+    Services.obs.addObserver(function obs(subject, topic, data) {
+      if (data == "engine-loaded") {
+        let engine = subject.QueryInterface(Ci.nsISearchEngine);
+        let rawEngine = engine.wrappedJSObject;
+        equal(engine.alias, KEYWORD, "Keyword not cleared by update");
+        equal(rawEngine.getAttr("order"), 1, "Order not cleared by update");
+        Services.obs.removeObserver(obs, TOPIC, false);
+        resolve();
+      }
+    }, TOPIC, false);
+
+    // set last update to 8 days ago, since the default interval is 7, then
+    // trigger an update
+    engine.wrappedJSObject.setAttr("updateexpir", Date.now() - (ONE_DAY_IN_MS * 8));
+    Services.search.QueryInterface(Components.interfaces.nsITimerCallback).notify(null);
+  });
+});
--- a/toolkit/components/search/tests/xpcshell/xpcshell.ini
+++ b/toolkit/components/search/tests/xpcshell/xpcshell.ini
@@ -95,8 +95,9 @@ tags = addons
 [test_currentEngine_fallback.js]
 [test_require_engines_in_cache.js]
 [test_update_telemetry.js]
 [test_svg_icon.js]
 [test_searchReset.js]
 [test_addEngineWithDetails.js]
 [test_chromeresource_icon1.js]
 [test_chromeresource_icon2.js]
+[test_engineUpdate.js]
--- a/toolkit/components/telemetry/Histograms.json
+++ b/toolkit/components/telemetry/Histograms.json
@@ -10017,10 +10017,18 @@
     "description": "Tracking the unique number of opened Containers."
   },
   "TOTAL_CONTAINERS_OPENED": {
     "alert_emails": ["amarchesini@mozilla.com"],
     "expires_in_version": "never",
     "bug_numbers": [1276006],
     "kind": "count",
     "description": "Tracking the total number of opened Containers."
+  },
+  "FENNEC_SESSIONSTORE_DAMAGED_SESSION_FILE": {
+    "alert_emails": ["jh+bugzilla@buttercookie.de"],
+    "expires_in_version": "56",
+    "kind": "flag",
+    "bug_numbers": [1284017],
+    "description": "When restoring tabs on startup, reading from sessionstore.js failed, even though the file exists and is not containing an explicitly empty window.",
+    "cpp_guard": "ANDROID"
   }
 }
--- a/toolkit/components/url-classifier/content/listmanager.js
+++ b/toolkit/components/url-classifier/content/listmanager.js
@@ -109,28 +109,21 @@ PROT_ListManager.prototype.registerTable
   this.tablesData[tableName] = {};
   this.tablesData[tableName].updateUrl = updateUrl;
   this.tablesData[tableName].gethashUrl = gethashUrl;
   this.tablesData[tableName].provider = providerName;
 
   // Keep track of all of our update URLs.
   if (!this.needsUpdate_[updateUrl]) {
     this.needsUpdate_[updateUrl] = {};
-    /* Backoff interval should be between 30 and 60 minutes. */
-    var backoffInterval = 30 * 60 * 1000;
-    backoffInterval += Math.floor(Math.random() * (30 * 60 * 1000));
 
-    log("Creating request backoff for " + updateUrl);
-    this.requestBackoffs_[updateUrl] = new RequestBackoff(2 /* max errors */,
-                                      60*1000 /* retry interval, 1 min */,
+    // Using the V4 backoff algorithm for both V2 and V4. See bug 1273398.
+    this.requestBackoffs_[updateUrl] = new RequestBackoffV4(
                                             4 /* num requests */,
-                                   60*60*1000 /* request time, 60 min */,
-                              backoffInterval /* backoff interval, 60 min */,
-                                 8*60*60*1000 /* max backoff, 8hr */);
-
+                                   60*60*1000 /* request time, 60 min */);
   }
   this.needsUpdate_[updateUrl][tableName] = false;
 
   return true;
 }
 
 PROT_ListManager.prototype.getGethashUrl = function(tableName) {
   if (this.tablesData[tableName] && this.tablesData[tableName].gethashUrl) {
--- a/toolkit/components/url-classifier/nsUrlClassifierHashCompleter.js
+++ b/toolkit/components/url-classifier/nsUrlClassifierHashCompleter.js
@@ -8,28 +8,16 @@ const Cr = Components.results;
 const Cu = Components.utils;
 
 // COMPLETE_LENGTH and PARTIAL_LENGTH copied from nsUrlClassifierDBService.h,
 // they correspond to the length, in bytes, of a hash prefix and the total
 // hash.
 const COMPLETE_LENGTH = 32;
 const PARTIAL_LENGTH = 4;
 
-// These backoff related constants are taken from v2 of the Google Safe Browsing
-// API. All times are in milliseconds.
-// BACKOFF_ERRORS: the number of errors incurred until we start to back off.
-// BACKOFF_INTERVAL: the initial time to wait once we start backing
-//                   off.
-// BACKOFF_MAX: as the backoff time doubles after each failure, this is a
-//              ceiling on the time to wait.
-
-const BACKOFF_ERRORS = 2;
-const BACKOFF_INTERVAL = 30 * 60 * 1000;
-const BACKOFF_MAX = 8 * 60 * 60 * 1000;
-
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/NetUtil.jsm");
 
 
 // Log only if browser.safebrowsing.debug is true
 function log(...stuff) {
   let logging = null;
@@ -203,23 +191,21 @@ HashCompleter.prototype = {
       this._pendingRequests[aGethashUrl].add(aPartialHash, aCallback);
     }
 
     if (!this._backoffs[aGethashUrl]) {
       // Initialize request backoffs separately, since requests are deleted
       // after they are dispatched.
       var jslib = Cc["@mozilla.org/url-classifier/jslib;1"]
                   .getService().wrappedJSObject;
-      this._backoffs[aGethashUrl] = new jslib.RequestBackoff(
-        BACKOFF_ERRORS /* max errors */,
-        60*1000 /* retry interval, 1 min */,
+
+      // Using the V4 backoff algorithm for both V2 and V4. See bug 1273398.
+      this._backoffs[aGethashUrl] = new jslib.RequestBackoffV4(
         10 /* keep track of max requests */,
-        0 /* don't throttle on successful requests per time period */,
-        BACKOFF_INTERVAL /* backoff interval, 60 min */,
-        BACKOFF_MAX /* max backoff, 8hr */);
+        0  /* don't throttle on successful requests per time period */);
     }
     // Start off this request. Without dispatching to a thread, every call to
     // complete makes an individual HTTP request.
     Services.tm.currentThread.dispatch(this, Ci.nsIThread.DISPATCH_NORMAL);
   },
 
   // This is called after several calls to |complete|, or after the
   // currentRequest has finished.  It starts off the HTTP request by making a
--- a/toolkit/components/url-classifier/nsUrlClassifierLib.js
+++ b/toolkit/components/url-classifier/nsUrlClassifierLib.js
@@ -18,16 +18,33 @@ Components.utils.import("resource://gre/
 #include ./content/moz/alarm.js
 #include ./content/moz/cryptohasher.js
 #include ./content/moz/observer.js
 #include ./content/moz/protocol4.js
 
 #include ./content/request-backoff.js
 #include ./content/xml-fetcher.js
 
+// Wrap a general-purpose |RequestBackoff| to a v4-specific one
+// since both listmanager and hashcompleter would use it.
+// Note that |maxRequests| and |requestPeriod| is still configurable
+// to throttle pending requests.
+function RequestBackoffV4(maxRequests, requestPeriod) {
+  let rand = Math.random();
+  let retryInterval = Math.floor(15 * 60 * 1000 * (rand + 1));   // 15 ~ 30 min.
+  let backoffInterval = Math.floor(30 * 60 * 1000 * (rand + 1)); // 30 ~ 60 min.
+
+  return new RequestBackoff(2 /* max errors */,
+                retryInterval /* retry interval, 15~30 min */,
+                  maxRequests /* num requests */,
+                requestPeriod /* request time, 60 min */,
+              backoffInterval /* backoff interval, 60 min */,
+          24 * 60 * 60 * 1000 /* max backoff, 24hr */);
+}
+
 // Expose this whole component.
 var lib = this;
 
 function UrlClassifierLib() {
   this.wrappedJSObject = lib;
 }
 UrlClassifierLib.prototype.classID = Components.ID("{26a4a019-2827-4a89-a85c-5931a678823a}");
 UrlClassifierLib.prototype.QueryInterface = XPCOMUtils.generateQI([]);
--- a/toolkit/components/url-classifier/nsUrlClassifierListManager.js
+++ b/toolkit/components/url-classifier/nsUrlClassifierListManager.js
@@ -24,17 +24,17 @@ function Init() {
   modScope.G_PreferenceObserver = jslib.G_PreferenceObserver;
   modScope.G_ObserverServiceObserver = jslib.G_ObserverServiceObserver;
   modScope.G_Debug = jslib.G_Debug;
   modScope.G_Assert = jslib.G_Assert;
   modScope.G_debugService = jslib.G_debugService;
   modScope.G_Alarm = jslib.G_Alarm;
   modScope.BindToObject = jslib.BindToObject;
   modScope.PROT_XMLFetcher = jslib.PROT_XMLFetcher;
-  modScope.RequestBackoff = jslib.RequestBackoff;
+  modScope.RequestBackoffV4 = jslib.RequestBackoffV4;
 
   // We only need to call Init once.
   modScope.Init = function() {};
 }
 
 function RegistrationData()
 {
 }