Merge m-c to inbound, a=merge CLOSED TREE
authorWes Kocher <wkocher@mozilla.com>
Thu, 01 Sep 2016 17:54:19 -0700
changeset 312366 e3bc47928a98bf521be6d2a68d5789e8cae9f52d
parent 312358 02c88cba55722b71a249740c1f7ee20f36f6f344 (current diff)
parent 312324 475f0ee625239e06c5794d4cf34e88d6ee2fde31 (diff)
child 312368 78bf92d3a0f138ff39eeb20062f206eb48cc9b88
push id20447
push userkwierso@gmail.com
push dateFri, 02 Sep 2016 20:36:44 +0000
treeherderfx-team@969397f22187 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmerge
milestone51.0a1
Merge m-c to inbound, a=merge CLOSED TREE
--- a/browser/components/downloads/content/downloadsOverlay.xul
+++ b/browser/components/downloads/content/downloadsOverlay.xul
@@ -111,17 +111,16 @@
       <panelmultiview id="downloadsPanel-multiView"
                       mainViewId="downloadsPanel-mainView"
                       align="stretch">
 
         <panelview id="downloadsPanel-mainView"
                    flex="1"
                    align="stretch">
           <richlistbox id="downloadsListBox"
-                       class="plain"
                        context="downloadsContextMenu"
                        onmouseover="DownloadsView.onDownloadMouseOver(event);"
                        onmouseout="DownloadsView.onDownloadMouseOut(event);"
                        oncontextmenu="DownloadsView.onDownloadContextMenu(event);"
                        ondragstart="DownloadsView.onDownloadDragStart(event);"/>
           <description id="emptyDownloads"
                        mousethrough="always">
              &downloadsPanelEmpty.label;
@@ -147,25 +146,25 @@
                                  max="100"
                                  mode="normal" />
                   <description id="downloadsSummaryDetails"
                                crop="end"/>
                 </vbox>
               </hbox>
               <hbox id="downloadsFooterButtons">
                 <button id="downloadsHistory"
-                        class="plain downloadsPanelFooterButton"
+                        class="downloadsPanelFooterButton"
                         label="&downloadsHistory.label;"
                         accesskey="&downloadsHistory.accesskey;"
                         flex="1"
                         oncommand="DownloadsPanel.showDownloadsHistory();"/>
                 <toolbarseparator id="downloadsFooterButtonsSplitter"
                         class="downloadsDropmarkerSplitter"/>
                 <button id="downloadsFooterDropmarker"
-                        class="plain downloadsPanelFooterButton downloadsDropmarker"
+                        class="downloadsPanelFooterButton downloadsDropmarker"
                         type="menu">
                   <menupopup id="downloadSubPanel"
                              onpopupshowing="DownloadsPanel.onFooterPopupShowing(event);"
                              onpopuphidden="DownloadsPanel.onFooterPopupHidden(event);"
                              position="after_end">
                     <menuitem id="downloadsDropdownItemClearList"
                               command="downloadsCmd_clearList"
                               label="&cmd.clearList2.label;"/>
@@ -185,22 +184,22 @@
           <description id="downloadsPanel-blockedSubview-title"/>
           <description id="downloadsPanel-blockedSubview-details1"/>
           <description id="downloadsPanel-blockedSubview-details2"/>
           <spacer flex="1"/>
           <hbox id="downloadsPanel-blockedSubview-buttons"
                 class="downloadsPanelFooter"
                 align="stretch">
             <button id="downloadsPanel-blockedSubview-openButton"
-                    class="plain downloadsPanelFooterButton"
+                    class="downloadsPanelFooterButton"
                     command="downloadsCmd_unblockAndOpen"
                     flex="1"/>
             <toolbarseparator/>
             <button id="downloadsPanel-blockedSubview-deleteButton"
-                    class="plain downloadsPanelFooterButton"
+                    class="downloadsPanelFooterButton"
                     oncommand="DownloadsBlockedSubview.confirmBlock();"
                     default="true"
                     flex="1"/>
           </hbox>
         </panelview>
 
       </panelmultiview>
 
--- a/browser/extensions/pdfjs/README.mozilla
+++ b/browser/extensions/pdfjs/README.mozilla
@@ -1,3 +1,3 @@
 This is the pdf.js project output, https://github.com/mozilla/pdf.js
 
-Current extension version is: 1.5.385
+Current extension version is: 1.5.413
--- a/browser/extensions/pdfjs/content/PdfJs.jsm
+++ b/browser/extensions/pdfjs/content/PdfJs.jsm
@@ -89,16 +89,17 @@ function initializeDefaultPreferences() 
   "disableStream": false,
   "disableAutoFetch": false,
   "disableFontFace": false,
   "disableTextLayer": false,
   "useOnlyCssZoom": false,
   "externalLinkTarget": 0
 }
 
+
   var defaultBranch = Services.prefs.getDefaultBranch(PREF_PREFIX + '.');
   var defaultValue;
   for (var key in DEFAULT_PREFERENCES) {
     defaultValue = DEFAULT_PREFERENCES[key];
     switch (typeof defaultValue) {
       case 'boolean':
         defaultBranch.setBoolPref(key, defaultValue);
         break;
--- a/browser/extensions/pdfjs/content/PdfjsChromeUtils.jsm
+++ b/browser/extensions/pdfjs/content/PdfjsChromeUtils.jsm
@@ -47,16 +47,17 @@ var DEFAULT_PREFERENCES =
   "disableStream": false,
   "disableAutoFetch": false,
   "disableFontFace": false,
   "disableTextLayer": false,
   "useOnlyCssZoom": false,
   "externalLinkTarget": 0
 }
 
+
 var PdfjsChromeUtils = {
   // For security purposes when running remote, we restrict preferences
   // content can access.
   _allowedPrefNames: Object.keys(DEFAULT_PREFERENCES),
   _ppmm: null,
   _mmg: null,
 
   /*
--- a/browser/extensions/pdfjs/content/build/pdf.js
+++ b/browser/extensions/pdfjs/content/build/pdf.js
@@ -23,18 +23,18 @@ define('pdfjs-dist/build/pdf', ['exports
     factory(exports);
   } else {
 factory((root.pdfjsDistBuildPdf = {}));
   }
 }(this, function (exports) {
   // Use strict in our context only - users might not want it
   'use strict';
 
-var pdfjsVersion = '1.5.385';
-var pdfjsBuild = 'a9c37c2';
+var pdfjsVersion = '1.5.413';
+var pdfjsBuild = '6bb95e3';
 
   var pdfjsFilePath =
     typeof document !== 'undefined' && document.currentScript ?
       document.currentScript.src : null;
 
   var pdfjsLibs = {};
 
   (function pdfjsWrapper() {
@@ -2835,17 +2835,18 @@ var renderTextLayer = (function renderTe
   var MAX_TEXT_DIVS_TO_RENDER = 100000;
 
   var NonWhitespaceRegexp = /\S/;
 
   function isAllWhitespace(str) {
     return !NonWhitespaceRegexp.test(str);
   }
 
-  function appendText(textDivs, viewport, geom, styles) {
+  function appendText(textDivs, viewport, geom, styles, bounds,
+                      enhanceTextSelection) {
     var style = styles[geom.fontName];
     var textDiv = document.createElement('div');
     textDivs.push(textDiv);
     if (isAllWhitespace(geom.str)) {
       textDiv.dataset.isWhitespace = true;
       return;
     }
     var tx = Util.transform(viewport.transform, geom.transform);
@@ -2891,30 +2892,59 @@ var renderTextLayer = (function renderTe
     // lots of such divs a lot faster.
     if (geom.str.length > 1) {
       if (style.vertical) {
         textDiv.dataset.canvasWidth = geom.height * viewport.scale;
       } else {
         textDiv.dataset.canvasWidth = geom.width * viewport.scale;
       }
     }
+    if (enhanceTextSelection) {
+      var angleCos = 1, angleSin = 0;
+      if (angle !== 0) {
+        angleCos = Math.cos(angle);
+        angleSin = Math.sin(angle);
+      }
+      var divWidth = (style.vertical ? geom.height : geom.width) *
+                     viewport.scale;
+      var divHeight = fontHeight;
+
+      var m, b;
+      if (angle !== 0) {
+        m = [angleCos, angleSin, -angleSin, angleCos, left, top];
+        b = Util.getAxialAlignedBoundingBox([0, 0, divWidth, divHeight], m);
+      } else {
+        b = [left, top, left + divWidth, top + divHeight];
+      }
+
+      bounds.push({
+        left: b[0],
+        top: b[1],
+        right: b[2],
+        bottom: b[3],
+        div: textDiv,
+        size: [divWidth, divHeight],
+        m: m
+      });
+    }
   }
 
   function render(task) {
     if (task._canceled) {
       return;
     }
     var textLayerFrag = task._container;
     var textDivs = task._textDivs;
     var capability = task._capability;
     var textDivsLength = textDivs.length;
 
     // No point in rendering many divs as it would make the browser
     // unusable even after the divs are rendered.
     if (textDivsLength > MAX_TEXT_DIVS_TO_RENDER) {
+      task._renderingDone = true;
       capability.resolve();
       return;
     }
 
     var canvas = document.createElement('canvas');
     canvas.mozOpaque = true;
     var ctx = canvas.getContext('2d', {alpha: false});
 
@@ -2932,54 +2962,326 @@ var renderTextLayer = (function renderTe
       // Only build font string and set to context if different from last.
       if (fontSize !== lastFontSize || fontFamily !== lastFontFamily) {
         ctx.font = fontSize + ' ' + fontFamily;
         lastFontSize = fontSize;
         lastFontFamily = fontFamily;
       }
 
       var width = ctx.measureText(textDiv.textContent).width;
+      textDiv.dataset.originalWidth = width;
       textLayerFrag.appendChild(textDiv);
-      var transform;
-      if (textDiv.dataset.canvasWidth !== undefined && width > 0) {
-        // Dataset values come of type string.
-        var textScale = textDiv.dataset.canvasWidth / width;
-        transform = 'scaleX(' + textScale + ')';
-      } else {
-        transform = '';
-      }
-      var rotation = textDiv.dataset.angle;
-      if (rotation) {
-        transform = 'rotate(' + rotation + 'deg) ' + transform;
-      }
-      if (transform) {
-        CustomStyle.setProp('transform' , textDiv, transform);
-      }
-    }
+       var transform;
+       if (textDiv.dataset.canvasWidth !== undefined && width > 0) {
+        //  Dataset values come of type string.
+         var textScale = textDiv.dataset.canvasWidth / width;
+         transform = 'scaleX(' + textScale + ')';
+       } else {
+         transform = '';
+       }
+       var rotation = textDiv.dataset.angle;
+       if (rotation) {
+         transform = 'rotate(' + rotation + 'deg) ' + transform;
+       }
+       if (transform) {
+         textDiv.dataset.originalTransform = transform;
+         CustomStyle.setProp('transform' , textDiv, transform);
+       }
+    }
+    task._renderingDone = true;
     capability.resolve();
   }
 
+  function expand(bounds, viewport) {
+    var expanded = expandBounds(viewport.width, viewport.height, bounds);
+    for (var i = 0; i < expanded.length; i++) {
+      var div = bounds[i].div;
+      if (!div.dataset.angle) {
+        div.dataset.paddingLeft = bounds[i].left - expanded[i].left;
+        div.dataset.paddingTop = bounds[i].top - expanded[i].top;
+        div.dataset.paddingRight = expanded[i].right - bounds[i].right;
+        div.dataset.paddingBottom = expanded[i].bottom - bounds[i].bottom;
+        continue;
+      }
+      // Box is rotated -- trying to find padding so rotated div will not
+      // exceed its expanded bounds.
+      var e = expanded[i], b = bounds[i];
+      var m = b.m, c = m[0], s = m[1];
+      // Finding intersections with expanded box.
+      var points = [[0, 0], [0, b.size[1]], [b.size[0], 0], b.size];
+      var ts = new Float64Array(64);
+      points.forEach(function (p, i) {
+        var t = Util.applyTransform(p, m);
+        ts[i + 0] = c && (e.left - t[0]) / c;
+        ts[i + 4] = s && (e.top - t[1]) / s;
+        ts[i + 8] = c && (e.right - t[0]) / c;
+        ts[i + 12] = s && (e.bottom - t[1]) / s;
+
+        ts[i + 16] = s && (e.left - t[0]) / -s;
+        ts[i + 20] = c && (e.top - t[1]) / c;
+        ts[i + 24] = s && (e.right - t[0]) / -s;
+        ts[i + 28] = c && (e.bottom - t[1]) / c;
+
+        ts[i + 32] = c && (e.left - t[0]) / -c;
+        ts[i + 36] = s && (e.top - t[1]) / -s;
+        ts[i + 40] = c && (e.right - t[0]) / -c;
+        ts[i + 44] = s && (e.bottom - t[1]) / -s;
+
+        ts[i + 48] = s && (e.left - t[0]) / s;
+        ts[i + 52] = c && (e.top - t[1]) / -c;
+        ts[i + 56] = s && (e.right - t[0]) / s;
+        ts[i + 60] = c && (e.bottom - t[1]) / -c;
+      });
+      var findPositiveMin = function (ts, offset, count) {
+        var result = 0;
+          for (var i = 0; i < count; i++) {
+            var t = ts[offset++];
+            if (t > 0) {
+              result = result ? Math.min(t, result) : t;
+            }
+          }
+          return result;
+      };
+      // Not based on math, but to simplify calculations, using cos and sin
+      // absolute values to not exceed the box (it can but insignificantly).
+      var boxScale = 1 + Math.min(Math.abs(c), Math.abs(s));
+      div.dataset.paddingLeft = findPositiveMin(ts, 32, 16) / boxScale;
+      div.dataset.paddingTop = findPositiveMin(ts, 48, 16) / boxScale;
+      div.dataset.paddingRight = findPositiveMin(ts, 0, 16) / boxScale;
+      div.dataset.paddingBottom = findPositiveMin(ts, 16, 16) / boxScale;
+    }
+  }
+
+  function expandBounds(width, height, boxes) {
+    var bounds = boxes.map(function (box, i) {
+      return {
+        x1: box.left,
+        y1: box.top,
+        x2: box.right,
+        y2: box.bottom,
+        index: i,
+        x1New: undefined,
+        x2New: undefined
+      };
+    });
+    expandBoundsLTR(width, bounds);
+    var expanded = new Array(boxes.length);
+    bounds.forEach(function (b) {
+      var i = b.index;
+      expanded[i] = {
+        left: b.x1New,
+        top: 0,
+        right: b.x2New,
+        bottom: 0
+      };
+    });
+
+    // Rotating on 90 degrees and extending extended boxes. Reusing the bounds
+    // array and objects.
+    boxes.map(function (box, i) {
+      var e = expanded[i], b = bounds[i];
+      b.x1 = box.top;
+      b.y1 = width - e.right;
+      b.x2 = box.bottom;
+      b.y2 = width - e.left;
+      b.index = i;
+      b.x1New = undefined;
+      b.x2New = undefined;
+    });
+    expandBoundsLTR(height, bounds);
+
+    bounds.forEach(function (b) {
+      var i = b.index;
+      expanded[i].top = b.x1New;
+      expanded[i].bottom = b.x2New;
+    });
+    return expanded;
+  }
+
+  function expandBoundsLTR(width, bounds) {
+    // Sorting by x1 coordinate and walk by the bounds in the same order.
+    bounds.sort(function (a, b) { return a.x1 - b.x1 || a.index - b.index; });
+
+    // First we see on the horizon is a fake boundary.
+    var fakeBoundary = {
+      x1: -Infinity,
+      y1: -Infinity,
+      x2: 0,
+      y2: Infinity,
+      index: -1,
+      x1New: 0,
+      x2New: 0
+    };
+    var horizon = [{
+      start: -Infinity,
+      end: Infinity,
+      boundary: fakeBoundary
+    }];
+
+    bounds.forEach(function (boundary) {
+      // Searching for the affected part of horizon.
+      // TODO red-black tree or simple binary search
+      var i = 0;
+      while (i < horizon.length && horizon[i].end <= boundary.y1) {
+        i++;
+      }
+      var j = horizon.length - 1;
+      while(j >= 0 && horizon[j].start >= boundary.y2) {
+        j--;
+      }
+
+      var horizonPart, affectedBoundary;
+      var q, k, maxXNew = -Infinity;
+      for (q = i; q <= j; q++) {
+        horizonPart = horizon[q];
+        affectedBoundary = horizonPart.boundary;
+        var xNew;
+        if (affectedBoundary.x2 > boundary.x1) {
+          // In the middle of the previous element, new x shall be at the
+          // boundary start. Extending if further if the affected bondary
+          // placed on top of the current one.
+          xNew = affectedBoundary.index > boundary.index ?
+            affectedBoundary.x1New : boundary.x1;
+        } else if (affectedBoundary.x2New === undefined) {
+          // We have some space in between, new x in middle will be a fair
+          // choice.
+          xNew = (affectedBoundary.x2 + boundary.x1) / 2;
+        } else {
+          // Affected boundary has x2new set, using it as new x.
+          xNew = affectedBoundary.x2New;
+        }
+        if (xNew > maxXNew) {
+          maxXNew = xNew;
+        }
+      }
+
+      // Set new x1 for current boundary.
+      boundary.x1New = maxXNew;
+
+      // Adjusts new x2 for the affected boundaries.
+      for (q = i; q <= j; q++) {
+        horizonPart = horizon[q];
+        affectedBoundary = horizonPart.boundary;
+        if (affectedBoundary.x2New === undefined) {
+          // Was not set yet, choosing new x if possible.
+          if (affectedBoundary.x2 > boundary.x1) {
+            // Current and affected boundaries intersect. If affected boundary
+            // is placed on top of the current, shrinking the affected.
+            if (affectedBoundary.index > boundary.index) {
+              affectedBoundary.x2New = affectedBoundary.x2;
+            }
+          } else {
+            affectedBoundary.x2New = maxXNew;
+          }
+        } else if (affectedBoundary.x2New > maxXNew) {
+          // Affected boundary is touching new x, pushing it back.
+          affectedBoundary.x2New = Math.max(maxXNew, affectedBoundary.x2);
+        }
+      }
+
+      // Fixing the horizon.
+      var changedHorizon = [], lastBoundary = null;
+      for (q = i; q <= j; q++) {
+        horizonPart = horizon[q];
+        affectedBoundary = horizonPart.boundary;
+        // Checking which boundary will be visible.
+        var useBoundary = affectedBoundary.x2 > boundary.x2 ?
+          affectedBoundary : boundary;
+        if (lastBoundary === useBoundary) {
+          // Merging with previous.
+          changedHorizon[changedHorizon.length - 1].end = horizonPart.end;
+        } else {
+          changedHorizon.push({
+            start: horizonPart.start,
+            end: horizonPart.end,
+            boundary: useBoundary
+          });
+          lastBoundary = useBoundary;
+        }
+      }
+      if (horizon[i].start < boundary.y1) {
+        changedHorizon[0].start = boundary.y1;
+        changedHorizon.unshift({
+          start: horizon[i].start,
+          end: boundary.y1,
+          boundary: horizon[i].boundary
+        });
+      }
+      if (boundary.y2 < horizon[j].end) {
+        changedHorizon[changedHorizon.length - 1].end = boundary.y2;
+        changedHorizon.push({
+          start: boundary.y2,
+          end: horizon[j].end,
+          boundary: horizon[j].boundary
+        });
+      }
+
+      // Set x2 new of boundary that is no longer visible (see overlapping case
+      // above).
+      // TODO more efficient, e.g. via reference counting.
+      for (q = i; q <= j; q++) {
+        horizonPart = horizon[q];
+        affectedBoundary = horizonPart.boundary;
+        if (affectedBoundary.x2New !== undefined) {
+          continue;
+        }
+        var used = false;
+        for (k = i - 1; !used && k >= 0 &&
+        horizon[k].start >= affectedBoundary.y1; k--) {
+          used = horizon[k].boundary === affectedBoundary;
+        }
+        for (k = j + 1; !used && k < horizon.length &&
+        horizon[k].end <= affectedBoundary.y2; k++) {
+          used = horizon[k].boundary === affectedBoundary;
+        }
+        for (k = 0; !used && k < changedHorizon.length; k++) {
+          used = changedHorizon[k].boundary === affectedBoundary;
+        }
+        if (!used) {
+          affectedBoundary.x2New = maxXNew;
+        }
+      }
+
+      Array.prototype.splice.apply(horizon,
+        [i, j - i + 1].concat(changedHorizon));
+    });
+
+    // Set new x2 for all unset boundaries.
+    horizon.forEach(function (horizonPart) {
+      var affectedBoundary = horizonPart.boundary;
+      if (affectedBoundary.x2New === undefined) {
+        affectedBoundary.x2New = Math.max(width, affectedBoundary.x2);
+      }
+    });
+  }
+
   /**
    * Text layer rendering task.
    *
    * @param {TextContent} textContent
    * @param {HTMLElement} container
    * @param {PageViewport} viewport
    * @param {Array} textDivs
+   * @param {boolean} enhanceTextSelection
    * @private
    */
-  function TextLayerRenderTask(textContent, container, viewport, textDivs) {
+  function TextLayerRenderTask(textContent, container, viewport, textDivs,
+                               enhanceTextSelection) {
     this._textContent = textContent;
     this._container = container;
     this._viewport = viewport;
     textDivs = textDivs || [];
     this._textDivs = textDivs;
+    this._renderingDone = false;
     this._canceled = false;
     this._capability = createPromiseCapability();
     this._renderTimer = null;
+    this._bounds = [];
+    this._enhanceTextSelection = !!enhanceTextSelection;
+    this._expanded = false;
   }
   TextLayerRenderTask.prototype = {
     get promise() {
       return this._capability.promise;
     },
 
     cancel: function TextLayer_cancel() {
       this._canceled = true;
@@ -2990,44 +3292,104 @@ var renderTextLayer = (function renderTe
       this._capability.reject('canceled');
     },
 
     _render: function TextLayer_render(timeout) {
       var textItems = this._textContent.items;
       var styles = this._textContent.styles;
       var textDivs = this._textDivs;
       var viewport = this._viewport;
+      var enhanceTextSelection = this._enhanceTextSelection;
+
       for (var i = 0, len = textItems.length; i < len; i++) {
-        appendText(textDivs, viewport, textItems[i], styles);
+        appendText(textDivs, viewport, textItems[i], styles, this._bounds,
+                   enhanceTextSelection);
       }
 
       if (!timeout) { // Render right away
         render(this);
       } else { // Schedule
         var self = this;
         this._renderTimer = setTimeout(function() {
           render(self);
           self._renderTimer = null;
         }, timeout);
       }
-    }
+    },
+
+    expandTextDivs: function TextLayer_expandTextDivs(expandDivs) {
+      if (!this._enhanceTextSelection || !this._renderingDone) {
+        return;
+      }
+      if (!this._expanded) {
+        expand(this._bounds, this._viewport);
+        this._expanded = true;
+        this._bounds.length = 0;
+      }
+      if (expandDivs) {
+        for (var i = 0, ii = this._textDivs.length; i < ii; i++) {
+          var div = this._textDivs[i];
+          var transform;
+          var width = div.dataset.originalWidth;
+          if (div.dataset.canvasWidth !== undefined && width > 0) {
+            // Dataset values come of type string.
+            var textScale = div.dataset.canvasWidth / width;
+            transform = 'scaleX(' + textScale + ')';
+          } else {
+            transform = '';
+          }
+          var rotation = div.dataset.angle;
+          if (rotation) {
+            transform = 'rotate(' + rotation + 'deg) ' + transform;
+          }
+          if (div.dataset.paddingLeft) {
+            div.style.paddingLeft =
+              (div.dataset.paddingLeft / textScale) + 'px';
+            transform += ' translateX(' +
+              (-div.dataset.paddingLeft / textScale) + 'px)';
+          }
+          if (div.dataset.paddingTop) {
+            div.style.paddingTop = div.dataset.paddingTop + 'px';
+            transform += ' translateY(' + (-div.dataset.paddingTop) + 'px)';
+          }
+          if (div.dataset.paddingRight) {
+            div.style.paddingRight =
+            div.dataset.paddingRight / textScale + 'px';
+          }
+          if (div.dataset.paddingBottom) {
+            div.style.paddingBottom = div.dataset.paddingBottom + 'px';
+          }
+          if (transform) {
+            CustomStyle.setProp('transform' , div, transform);
+          }
+        }
+      } else {
+        for (i = 0, ii = this._textDivs.length; i < ii; i++) {
+          div = this._textDivs[i];
+          div.style.padding = 0;
+          transform = div.dataset.originalTransform || '';
+          CustomStyle.setProp('transform', div, transform);
+        }
+      }
+    },
   };
 
 
   /**
    * Starts rendering of the text layer.
    *
    * @param {TextLayerRenderParameters} renderParameters
    * @returns {TextLayerRenderTask}
    */
   function renderTextLayer(renderParameters) {
     var task = new TextLayerRenderTask(renderParameters.textContent,
                                        renderParameters.container,
                                        renderParameters.viewport,
-                                       renderParameters.textDivs);
+                                       renderParameters.textDivs,
+                                       renderParameters.enhanceTextSelection);
     task._render(renderParameters.timeout);
     return task;
   }
 
   return renderTextLayer;
 })();
 
 exports.renderTextLayer = renderTextLayer;
@@ -6168,17 +6530,23 @@ var CanvasGraphics = (function CanvasGra
           ctx.clip();
         }
         this.pendingClip = null;
       }
       ctx.beginPath();
     },
     getSinglePixelWidth: function CanvasGraphics_getSinglePixelWidth(scale) {
       if (this.cachedGetSinglePixelWidth === null) {
+        // NOTE: The `save` and `restore` commands used below is a workaround
+        // that is necessary in order to prevent `mozCurrentTransformInverse`
+        // from intermittently returning incorrect values in Firefox, see:
+        // https://github.com/mozilla/pdf.js/issues/7188.
+        this.ctx.save();
         var inverse = this.ctx.mozCurrentTransformInverse;
+        this.ctx.restore();
         // max of the current horizontal and vertical scale
         this.cachedGetSinglePixelWidth = Math.sqrt(Math.max(
           (inverse[0] * inverse[0] + inverse[1] * inverse[1]),
           (inverse[2] * inverse[2] + inverse[3] * inverse[3])));
       }
       return this.cachedGetSinglePixelWidth;
     },
     getCanvasPosition: function CanvasGraphics_getCanvasPosition(x, y) {
--- a/browser/extensions/pdfjs/content/build/pdf.worker.js
+++ b/browser/extensions/pdfjs/content/build/pdf.worker.js
@@ -23,18 +23,18 @@ define('pdfjs-dist/build/pdf.worker', ['
     factory(exports);
   } else {
 factory((root.pdfjsDistBuildPdfWorker = {}));
   }
 }(this, function (exports) {
   // Use strict in our context only - users might not want it
   'use strict';
 
-var pdfjsVersion = '1.5.385';
-var pdfjsBuild = 'a9c37c2';
+var pdfjsVersion = '1.5.413';
+var pdfjsBuild = '6bb95e3';
 
   var pdfjsFilePath =
     typeof document !== 'undefined' && document.currentScript ?
       document.currentScript.src : null;
 
   var pdfjsLibs = {};
 
   (function pdfjsWrapper() {
@@ -27307,16 +27307,17 @@ var ProblematicCharRanges = new Int32Arr
   0x25CC, 0x25CD,
   0x3000, 0x3001,
   // Chars that is used in complex-script shaping.
   0xAA60, 0xAA80,
   // Specials Unicode block.
   0xFFF0, 0x10000
 ]);
 
+
 /**
  * 'Font' is the class the outside world should use, it encapsulate all the font
  * decoding logics whatever type it is (assuming the font type is supported).
  *
  * For example to read a Type1 font and to attach it to the document:
  *   var type1Font = new Font("MyFontName", binaryFile, propertiesObject);
  *   type1Font.bind();
  */
@@ -37263,19 +37264,27 @@ var PartialEvaluator = (function Partial
           }
           textState = stateManager.state;
           var fn = operation.fn;
           args = operation.args;
           var advance, diff;
 
           switch (fn | 0) {
             case OPS.setFont:
+              // Optimization to ignore multiple identical Tf commands.
+              var fontNameArg = args[0].name, fontSizeArg = args[1];
+              if (textState.font && fontNameArg === textState.fontName &&
+                  fontSizeArg === textState.fontSize) {
+                break;
+              }
+
               flushTextContentItem();
-              textState.fontSize = args[1];
-              next(handleSetFont(args[0].name, null));
+              textState.fontName = fontNameArg;
+              textState.fontSize = fontSizeArg;
+              next(handleSetFont(fontNameArg, null));
               return;
             case OPS.setTextRise:
               flushTextContentItem();
               textState.textRise = args[0];
               break;
             case OPS.setHScale:
               flushTextContentItem();
               textState.textHScale = args[0] / 100;
@@ -37482,16 +37491,17 @@ var PartialEvaluator = (function Partial
                 break;
               }
               var gState = extGState.get(dictName.name);
               if (!isDict(gState)) {
                 break;
               }
               var gStateFont = gState.get('Font');
               if (gStateFont) {
+                textState.fontName = null;
                 textState.fontSize = gStateFont[1];
                 next(handleSetFont(null, gStateFont[0]));
                 return;
               }
               break;
           } // switch
         } // while
         if (stop) {
@@ -38401,16 +38411,17 @@ var StateManager = (function StateManage
     }
   };
   return StateManager;
 })();
 
 var TextState = (function TextStateClosure() {
   function TextState() {
     this.ctm = new Float32Array(IDENTITY_MATRIX);
+    this.fontName = null;
     this.fontSize = 0;
     this.font = null;
     this.fontMatrix = FONT_IDENTITY_MATRIX;
     this.textMatrix = IDENTITY_MATRIX.slice();
     this.textLineMatrix = IDENTITY_MATRIX.slice();
     this.charSpacing = 0;
     this.wordSpacing = 0;
     this.leading = 0;
@@ -39167,16 +39178,17 @@ var isInt = sharedUtil.isInt;
 var isValidUrl = sharedUtil.isValidUrl;
 var stringToBytes = sharedUtil.stringToBytes;
 var stringToPDFString = sharedUtil.stringToPDFString;
 var stringToUTF8String = sharedUtil.stringToUTF8String;
 var warn = sharedUtil.warn;
 var Dict = corePrimitives.Dict;
 var isDict = corePrimitives.isDict;
 var isName = corePrimitives.isName;
+var isRef = corePrimitives.isRef;
 var Stream = coreStream.Stream;
 var ColorSpace = coreColorSpace.ColorSpace;
 var ObjectLoader = coreObj.ObjectLoader;
 var FileSpec = coreObj.FileSpec;
 var OperatorList = coreEvaluator.OperatorList;
 
 /**
  * @class
@@ -39184,32 +39196,36 @@ var OperatorList = coreEvaluator.Operato
  */
 function AnnotationFactory() {}
 AnnotationFactory.prototype = /** @lends AnnotationFactory.prototype */ {
   /**
    * @param {XRef} xref
    * @param {Object} ref
    * @returns {Annotation}
    */
-  create: function AnnotationFactory_create(xref, ref) {
+  create: function AnnotationFactory_create(xref, ref,
+                                            uniquePrefix, idCounters) {
     var dict = xref.fetchIfRef(ref);
     if (!isDict(dict)) {
       return;
     }
+    var id = isRef(ref) ? ref.toString() :
+                          'annot_' + (uniquePrefix || '') + (++idCounters.obj);
 
     // Determine the annotation's subtype.
     var subtype = dict.get('Subtype');
     subtype = isName(subtype) ? subtype.name : null;
 
     // Return the right annotation object based on the subtype and field type.
     var parameters = {
       xref: xref,
       dict: dict,
-      ref: ref,
+      ref: isRef(ref) ? ref : null,
       subtype: subtype,
+      id: id,
     };
 
     switch (subtype) {
       case 'Link':
         return new LinkAnnotation(parameters);
 
       case 'Text':
         return new TextAnnotation(parameters);
@@ -39303,17 +39319,17 @@ var Annotation = (function AnnotationClo
     this.setFlags(dict.get('F'));
     this.setRectangle(dict.getArray('Rect'));
     this.setColor(dict.getArray('C'));
     this.setBorderStyle(dict);
     this.appearance = getDefaultAppearance(dict);
 
     // Expose public properties using a data object.
     this.data = {};
-    this.data.id = params.ref.toString();
+    this.data.id = params.id;
     this.data.subtype = params.subtype;
     this.data.annotationFlags = this.flags;
     this.data.rect = this.rectangle;
     this.data.color = this.color;
     this.data.borderStyle = this.borderStyle;
     this.data.hasAppearance = !!this.appearance;
   }
 
@@ -40122,16 +40138,17 @@ var Page = (function PageClosure() {
 
   function Page(pdfManager, xref, pageIndex, pageDict, ref, fontCache) {
     this.pdfManager = pdfManager;
     this.pageIndex = pageIndex;
     this.pageDict = pageDict;
     this.xref = xref;
     this.ref = ref;
     this.fontCache = fontCache;
+    this.uniquePrefix = 'p' + this.pageIndex + '_';
     this.idCounters = {
       obj: 0
     };
     this.evaluatorOptions = pdfManager.evaluatorOptions;
     this.resourcesPromise = null;
   }
 
   Page.prototype = {
@@ -40269,17 +40286,17 @@ var Page = (function PageClosure() {
         'XObject',
         'Font'
         // ProcSet
         // Properties
       ]);
 
       var partialEvaluator = new PartialEvaluator(pdfManager, this.xref,
                                                   handler, this.pageIndex,
-                                                  'p' + this.pageIndex + '_',
+                                                  this.uniquePrefix,
                                                   this.idCounters,
                                                   this.fontCache,
                                                   this.evaluatorOptions);
 
       var dataPromises = Promise.all([contentStreamPromise, resourcesPromise]);
       var pageListPromise = dataPromises.then(function(data) {
         var contentStream = data[0];
         var opList = new OperatorList(intent, handler, self.pageIndex);
@@ -40336,17 +40353,17 @@ var Page = (function PageClosure() {
       ]);
 
       var dataPromises = Promise.all([contentStreamPromise,
                                       resourcesPromise]);
       return dataPromises.then(function(data) {
         var contentStream = data[0];
         var partialEvaluator = new PartialEvaluator(pdfManager, self.xref,
                                                     handler, self.pageIndex,
-                                                    'p' + self.pageIndex + '_',
+                                                    self.uniquePrefix,
                                                     self.idCounters,
                                                     self.fontCache,
                                                     self.evaluatorOptions);
 
         return partialEvaluator.getTextContent(contentStream,
                                                task,
                                                self.resources,
                                                /* stateManager = */ null,
@@ -40371,17 +40388,19 @@ var Page = (function PageClosure() {
     },
 
     get annotations() {
       var annotations = [];
       var annotationRefs = this.getInheritedPageProp('Annots') || [];
       var annotationFactory = new AnnotationFactory();
       for (var i = 0, n = annotationRefs.length; i < n; ++i) {
         var annotationRef = annotationRefs[i];
-        var annotation = annotationFactory.create(this.xref, annotationRef);
+        var annotation = annotationFactory.create(this.xref, annotationRef,
+                                                  this.uniquePrefix,
+                                                  this.idCounters);
         if (annotation) {
           annotations.push(annotation);
         }
       }
       return shadow(this, 'annotations', annotations);
     }
   };
 
--- a/browser/extensions/pdfjs/content/web/viewer.js
+++ b/browser/extensions/pdfjs/content/web/viewer.js
@@ -948,16 +948,17 @@ exports.PDFRenderingQueue = PDFRendering
   "disableRange": false,
   "disableStream": false,
   "disableAutoFetch": false,
   "disableFontFace": false,
   "disableTextLayer": false,
   "useOnlyCssZoom": false,
   "externalLinkTarget": 0
 }
+
   );
 
 function cloneObj(obj) {
   var result = {};
   for (var i in obj) {
     if (Object.prototype.hasOwnProperty.call(obj, i)) {
       result[i] = obj[i];
     }
@@ -3236,17 +3237,17 @@ var PDFFindController = (function PDFFin
       }.bind(this));
     },
 
     updatePage: function PDFFindController_updatePage(index) {
       if (this.selected.pageIdx === index) {
         // If the page is selected, scroll the page into view, which triggers
         // rendering the page, which adds the textLayer. Once the textLayer is
         // build, it will scroll onto the selected match.
-        this.pdfViewer.scrollPageIntoView(index + 1);
+        this.pdfViewer.currentPageNumber = index + 1;
       }
 
       var page = this.pdfViewer.getPageView(index);
       if (page.textLayer) {
         page.textLayer.updateMatches();
       }
     },
 
@@ -4671,17 +4672,20 @@ var PDFLinkService = (function PDFLinkSe
           self._pagesRefCache[destRef.num + ' ' + destRef.gen + ' R'] :
           (destRef + 1);
         if (pageNumber) {
           if (pageNumber > self.pagesCount) {
             console.error('PDFLinkService_navigateTo: ' +
                           'Trying to navigate to a non-existent page.');
             return;
           }
-          self.pdfViewer.scrollPageIntoView(pageNumber, dest);
+          self.pdfViewer.scrollPageIntoView({
+            pageNumber: pageNumber,
+            destArray: dest,
+          });
 
           if (self.pdfHistory) {
             // Update the browsing history.
             self.pdfHistory.push({
               dest: dest,
               hash: destString,
               page: pageNumber
             });
@@ -4797,17 +4801,21 @@ var PDFLinkService = (function PDFLinkSe
               }
             } else {
               console.error('PDFLinkService_setHash: \'' + zoomArg +
                             '\' is not a valid zoom value.');
             }
           }
         }
         if (dest) {
-          this.pdfViewer.scrollPageIntoView(pageNumber || this.page, dest);
+          this.pdfViewer.scrollPageIntoView({
+            pageNumber: pageNumber || this.page,
+            destArray: dest,
+            allowNegativeOffset: true,
+          });
         } else if (pageNumber) {
           this.page = pageNumber; // simple page
         }
         if ('pagemode' in params) {
           this.eventBus.dispatch('pagemode', {
             source: this,
             mode: params.pagemode
           });
@@ -5023,16 +5031,18 @@ var TEXT_LAYER_RENDER_DELAY = 200; // ms
  * @property {HTMLDivElement} container - The viewer element.
  * @property {EventBus} eventBus - The application event bus.
  * @property {number} id - The page unique ID (normally its number).
  * @property {number} scale - The page scale display.
  * @property {PageViewport} defaultViewport - The page viewport.
  * @property {PDFRenderingQueue} renderingQueue - The rendering queue object.
  * @property {IPDFTextLayerFactory} textLayerFactory
  * @property {IPDFAnnotationLayerFactory} annotationLayerFactory
+ * @property {boolean} enhanceTextSelection - Turns on the text selection
+ * enhancement. The default is `false`.
  */
 
 /**
  * @class
  * @implements {IRenderableView}
  */
 var PDFPageView = (function PDFPageViewClosure() {
   /**
@@ -5042,25 +5052,27 @@ var PDFPageView = (function PDFPageViewC
   function PDFPageView(options) {
     var container = options.container;
     var id = options.id;
     var scale = options.scale;
     var defaultViewport = options.defaultViewport;
     var renderingQueue = options.renderingQueue;
     var textLayerFactory = options.textLayerFactory;
     var annotationLayerFactory = options.annotationLayerFactory;
+    var enhanceTextSelection = options.enhanceTextSelection || false;
 
     this.id = id;
     this.renderingId = 'page' + id;
 
     this.rotation = 0;
     this.scale = scale || DEFAULT_SCALE;
     this.viewport = defaultViewport;
     this.pdfPageRotate = defaultViewport.rotation;
     this.hasRestrictedScaling = false;
+    this.enhanceTextSelection = enhanceTextSelection;
 
     this.eventBus = options.eventBus || domEvents.getGlobalEventBus();
     this.renderingQueue = renderingQueue;
     this.textLayerFactory = textLayerFactory;
     this.annotationLayerFactory = annotationLayerFactory;
 
     this.renderingState = RenderingStates.INITIAL;
     this.resume = null;
@@ -5366,19 +5378,19 @@ var PDFPageView = (function PDFPageViewC
         textLayerDiv.style.height = canvasWrapper.style.height;
         if (this.annotationLayer && this.annotationLayer.div) {
           // annotationLayer needs to stay on top
           div.insertBefore(textLayerDiv, this.annotationLayer.div);
         } else {
           div.appendChild(textLayerDiv);
         }
 
-        textLayer = this.textLayerFactory.createTextLayerBuilder(textLayerDiv,
-                                                                 this.id - 1,
-                                                                 this.viewport);
+        textLayer = this.textLayerFactory.
+          createTextLayerBuilder(textLayerDiv, this.id - 1, this.viewport,
+                                 this.enhanceTextSelection);
       }
       this.textLayer = textLayer;
 
       var resolveRenderPromise, rejectRenderPromise;
       var promise = new Promise(function (resolve, reject) {
         resolveRenderPromise = resolve;
         rejectRenderPromise = reject;
       });
@@ -5775,16 +5787,18 @@ exports.PDFThumbnailViewer = PDFThumbnai
 
 /**
  * @typedef {Object} TextLayerBuilderOptions
  * @property {HTMLDivElement} textLayerDiv - The text layer container.
  * @property {EventBus} eventBus - The application event bus.
  * @property {number} pageIndex - The page index.
  * @property {PageViewport} viewport - The viewport of the text layer.
  * @property {PDFFindController} findController
+ * @property {boolean} enhanceTextSelection - Option to turn on improved
+ * text selection.
  */
 
 /**
  * TextLayerBuilder provides text-selection functionality for the PDF.
  * It does this by creating overlay divs over the PDF text. These divs
  * contain text that matches the PDF text they are overlaying. This object
  * also provides a way to highlight text that is being searched for.
  * @class
@@ -5797,26 +5811,29 @@ var TextLayerBuilder = (function TextLay
     this.divContentDone = false;
     this.pageIdx = options.pageIndex;
     this.pageNumber = this.pageIdx + 1;
     this.matches = [];
     this.viewport = options.viewport;
     this.textDivs = [];
     this.findController = options.findController || null;
     this.textLayerRenderTask = null;
+    this.enhanceTextSelection = options.enhanceTextSelection;
     this._bindMouse();
   }
 
   TextLayerBuilder.prototype = {
     _finishRendering: function TextLayerBuilder_finishRendering() {
       this.renderingDone = true;
 
-      var endOfContent = document.createElement('div');
-      endOfContent.className = 'endOfContent';
-      this.textLayerDiv.appendChild(endOfContent);
+      if (!this.enhanceTextSelection) {
+        var endOfContent = document.createElement('div');
+        endOfContent.className = 'endOfContent';
+        this.textLayerDiv.appendChild(endOfContent);
+      }
 
       this.eventBus.dispatch('textlayerrendered', {
         source: this,
         pageNumber: this.pageNumber
       });
     },
 
     /**
@@ -5836,17 +5853,18 @@ var TextLayerBuilder = (function TextLay
 
       this.textDivs = [];
       var textLayerFrag = document.createDocumentFragment();
       this.textLayerRenderTask = pdfjsLib.renderTextLayer({
         textContent: this.textContent,
         container: textLayerFrag,
         viewport: this.viewport,
         textDivs: this.textDivs,
-        timeout: timeout
+        timeout: timeout,
+        enhanceTextSelection: this.enhanceTextSelection,
       });
       this.textLayerRenderTask.promise.then(function () {
         this.textLayerDiv.appendChild(textLayerFrag);
         this._finishRendering();
         this.updateMatches();
       }.bind(this), function (reason) {
         // canceled or failed to render text layer -- skipping errors
       });
@@ -6054,24 +6072,33 @@ var TextLayerBuilder = (function TextLay
 
     /**
      * Fixes text selection: adds additional div where mouse was clicked.
      * This reduces flickering of the content if mouse slowly dragged down/up.
      * @private
      */
     _bindMouse: function TextLayerBuilder_bindMouse() {
       var div = this.textLayerDiv;
+      var self = this;
       div.addEventListener('mousedown', function (e) {
+        if (self.enhanceTextSelection && self.textLayerRenderTask) {
+          self.textLayerRenderTask.expandTextDivs(true);
+          return;
+        }
         var end = div.querySelector('.endOfContent');
         if (!end) {
           return;
         }
         end.classList.add('active');
       });
       div.addEventListener('mouseup', function (e) {
+        if (self.enhanceTextSelection && self.textLayerRenderTask) {
+          self.textLayerRenderTask.expandTextDivs(false);
+          return;
+        }
         var end = div.querySelector('.endOfContent');
         if (!end) {
           return;
         }
         end.classList.remove('active');
       });
     },
   };
@@ -6083,23 +6110,26 @@ var TextLayerBuilder = (function TextLay
  * @implements IPDFTextLayerFactory
  */
 function DefaultTextLayerFactory() {}
 DefaultTextLayerFactory.prototype = {
   /**
    * @param {HTMLDivElement} textLayerDiv
    * @param {number} pageIndex
    * @param {PageViewport} viewport
+   * @param {boolean} enhanceTextSelection
    * @returns {TextLayerBuilder}
    */
-  createTextLayerBuilder: function (textLayerDiv, pageIndex, viewport) {
+  createTextLayerBuilder: function (textLayerDiv, pageIndex, viewport,
+                                    enhanceTextSelection) {
     return new TextLayerBuilder({
       textLayerDiv: textLayerDiv,
       pageIndex: pageIndex,
-      viewport: viewport
+      viewport: viewport,
+      enhanceTextSelection: enhanceTextSelection
     });
   }
 };
 
 exports.TextLayerBuilder = TextLayerBuilder;
 exports.DefaultTextLayerFactory = DefaultTextLayerFactory;
 }));
 
@@ -6267,16 +6297,18 @@ var DEFAULT_CACHE_SIZE = 10;
  * @property {EventBus} eventBus - The application event bus.
  * @property {IPDFLinkService} linkService - The navigation/linking service.
  * @property {DownloadManager} downloadManager - (optional) The download
  *   manager component.
  * @property {PDFRenderingQueue} renderingQueue - (optional) The rendering
  *   queue object.
  * @property {boolean} removePageBorders - (optional) Removes the border shadow
  *   around the pages. The default is false.
+ * @property {boolean} enhanceTextSelection - (optional) Enables the improved
+ *   text selection behaviour. The default is `false`.
  */
 
 /**
  * Simple viewer control to display PDF content/pages.
  * @class
  * @implements {IRenderableView}
  */
 var PDFViewer = (function pdfViewer() {
@@ -6318,16 +6350,17 @@ var PDFViewer = (function pdfViewer() {
    */
   function PDFViewer(options) {
     this.container = options.container;
     this.viewer = options.viewer || options.container.firstElementChild;
     this.eventBus = options.eventBus || domEvents.getGlobalEventBus();
     this.linkService = options.linkService || new SimpleLinkService();
     this.downloadManager = options.downloadManager || null;
     this.removePageBorders = options.removePageBorders || false;
+    this.enhanceTextSelection = options.enhanceTextSelection || false;
 
     this.defaultRenderingQueue = !options.renderingQueue;
     if (this.defaultRenderingQueue) {
       // Custom rendering queue is not specified, using default one
       this.renderingQueue = new PDFRenderingQueue();
       this.renderingQueue.setViewer(this);
     } else {
       this.renderingQueue = options.renderingQueue;
@@ -6543,17 +6576,18 @@ var PDFViewer = (function pdfViewer() {
           var pageView = new PDFPageView({
             container: this.viewer,
             eventBus: this.eventBus,
             id: pageNum,
             scale: scale,
             defaultViewport: viewport.clone(),
             renderingQueue: this.renderingQueue,
             textLayerFactory: textLayerFactory,
-            annotationLayerFactory: this
+            annotationLayerFactory: this,
+            enhanceTextSelection: this.enhanceTextSelection,
           });
           bindOnAfterAndBeforeDraw(pageView);
           this._pages.push(pageView);
         }
 
         var linkService = this.linkService;
 
         // Fetch all the pages since the viewport is needed before printing
@@ -6649,17 +6683,21 @@ var PDFViewer = (function pdfViewer() {
       if (!noScroll) {
         var page = this._currentPageNumber, dest;
         if (this._location && !pdfjsLib.PDFJS.ignoreCurrentPositionOnZoom &&
             !(this.isInPresentationMode || this.isChangingPresentationMode)) {
           page = this._location.pageNumber;
           dest = [null, { name: 'XYZ' }, this._location.left,
                   this._location.top, null];
         }
-        this.scrollPageIntoView(page, dest);
+        this.scrollPageIntoView({
+          pageNumber: page,
+          destArray: dest,
+          allowNegativeOffset: true,
+        });
       }
 
       this._setScaleDispatchEvent(newScale, newValue, preset);
 
       if (this.defaultRenderingQueue) {
         this.update();
       }
     },
@@ -6722,33 +6760,47 @@ var PDFViewer = (function pdfViewer() {
         this._setScale(this._currentScaleValue, true);
       }
 
       var pageView = this._pages[this._currentPageNumber - 1];
       scrollIntoView(pageView.div);
     },
 
     /**
+     * @typedef ScrollPageIntoViewParameters
+     * @param {number} pageNumber - The page number.
+     * @param {Array} destArray - (optional) The original PDF destination array,
+     *   in the format: <page-ref> </XYZ|/FitXXX> <args..>
+     * @param {boolean} allowNegativeOffset - (optional) Allow negative page
+     *   offsets. The default value is `false`.
+     */
+
+    /**
      * Scrolls page into view.
-     * @param {number} pageNumber
-     * @param {Array} dest - (optional) original PDF destination array:
-     *   <page-ref> </XYZ|FitXXX> <args..>
+     * @param {ScrollPageIntoViewParameters} params
      */
-    scrollPageIntoView: function PDFViewer_scrollPageIntoView(pageNumber,
-                                                              dest) {
+    scrollPageIntoView: function PDFViewer_scrollPageIntoView(params) {
       if (!this.pdfDocument) {
         return;
       }
+      var pageNumber = params.pageNumber || 0;
+      var dest = params.destArray || null;
+      var allowNegativeOffset = params.allowNegativeOffset || false;
 
       if (this.isInPresentationMode || !dest) {
         this._setCurrentPageNumber(pageNumber, /* resetCurrentPageView */ true);
         return;
       }
 
       var pageView = this._pages[pageNumber - 1];
+      if (!pageView) {
+        console.error('PDFViewer_scrollPageIntoView: ' +
+                      'Invalid "pageNumber" parameter.');
+        return;
+      }
       var x = 0, y = 0;
       var width = 0, height = 0, widthScale, heightScale;
       var changeOrientation = (pageView.rotation % 180 === 0 ? false : true);
       var pageWidth = (changeOrientation ? pageView.height : pageView.width) /
         pageView.scale / CSS_UNITS;
       var pageHeight = (changeOrientation ? pageView.width : pageView.height) /
         pageView.scale / CSS_UNITS;
       var scale = 0;
@@ -6819,16 +6871,23 @@ var PDFViewer = (function pdfViewer() {
 
       var boundingRect = [
         pageView.viewport.convertToViewportPoint(x, y),
         pageView.viewport.convertToViewportPoint(x + width, y + height)
       ];
       var left = Math.min(boundingRect[0][0], boundingRect[1][0]);
       var top = Math.min(boundingRect[0][1], boundingRect[1][1]);
 
+      if (!allowNegativeOffset) {
+        // Some bad PDF generators will create destinations with e.g. top values
+        // that exceeds the page height. Ensure that offsets are not negative,
+        // to prevent a previous page from becoming visible (fixes bug 874482).
+        left = Math.max(left, 0);
+        top = Math.max(top, 0);
+      }
       scrollIntoView(pageView.div, { left: left, top: top });
     },
 
     _updateLocation: function (firstPage) {
       var currentScale = this._currentScale;
       var currentScaleValue = this._currentScaleValue;
       var normalizedScaleValue =
         parseFloat(currentScaleValue) === currentScale ?
@@ -6989,23 +7048,26 @@ var PDFViewer = (function pdfViewer() {
     },
 
     /**
      * @param {HTMLDivElement} textLayerDiv
      * @param {number} pageIndex
      * @param {PageViewport} viewport
      * @returns {TextLayerBuilder}
      */
-    createTextLayerBuilder: function (textLayerDiv, pageIndex, viewport) {
+    createTextLayerBuilder: function (textLayerDiv, pageIndex, viewport,
+                                      enhanceTextSelection) {
       return new TextLayerBuilder({
         textLayerDiv: textLayerDiv,
         eventBus: this.eventBus,
         pageIndex: pageIndex,
         viewport: viewport,
-        findController: this.isInPresentationMode ? null : this.findController
+        findController: this.isInPresentationMode ? null : this.findController,
+        enhanceTextSelection: this.isInPresentationMode ? false :
+                                                          enhanceTextSelection,
       });
     },
 
     /**
      * @param {HTMLDivElement} pageDiv
      * @param {PDFPage} pdfPage
      * @returns {AnnotationLayerBuilder}
      */
@@ -7086,16 +7148,17 @@ var getGlobalEventBus = domEventsLib.get
 
 var DEFAULT_SCALE_DELTA = 1.1;
 var MIN_SCALE = 0.25;
 var MAX_SCALE = 10.0;
 var SCALE_SELECT_CONTAINER_PADDING = 8;
 var SCALE_SELECT_PADDING = 22;
 var PAGE_NUMBER_LOADING_INDICATOR = 'visiblePageIsLoading';
 var DISABLE_AUTO_FETCH_LOADING_BAR_TIMEOUT = 5000;
+var ENHANCE_TEXT_SELECTION = false;
 
 function configure(PDFJS) {
   PDFJS.imageResourcesPath = './images/';
   PDFJS.workerSrc = '../build/pdf.worker.js';
   PDFJS.cMapUrl = '../web/cmaps/';
   PDFJS.cMapPacked = true;
 }
 
@@ -7186,17 +7249,18 @@ var PDFViewerApplication = {
     var container = appConfig.mainContainer;
     var viewer = appConfig.viewerContainer;
     this.pdfViewer = new PDFViewer({
       container: container,
       viewer: viewer,
       eventBus: eventBus,
       renderingQueue: pdfRenderingQueue,
       linkService: pdfLinkService,
-      downloadManager: downloadManager
+      downloadManager: downloadManager,
+      enhanceTextSelection: ENHANCE_TEXT_SELECTION,
     });
     pdfRenderingQueue.setViewer(this.pdfViewer);
     pdfLinkService.setViewer(this.pdfViewer);
 
     var thumbnailContainer = appConfig.sidebar.thumbnailView;
     this.pdfThumbnailViewer = new PDFThumbnailViewer({
       container: thumbnailContainer,
       renderingQueue: pdfRenderingQueue,
@@ -8079,17 +8143,17 @@ var PDFViewerApplication = {
   rotatePages: function pdfViewRotatePages(delta) {
     var pageNumber = this.page;
     this.pageRotation = (this.pageRotation + 360 + delta) % 360;
     this.pdfViewer.pagesRotation = this.pageRotation;
     this.pdfThumbnailViewer.pagesRotation = this.pageRotation;
 
     this.forceRendering();
 
-    this.pdfViewer.scrollPageIntoView(pageNumber);
+    this.pdfViewer.currentPageNumber = pageNumber;
   },
 
   requestPresentationMode: function pdfViewRequestPresentationMode() {
     if (!this.pdfPresentationMode) {
       return;
     }
     this.pdfPresentationMode.request();
   },
@@ -8103,17 +8167,17 @@ var PDFViewerApplication = {
     }
     this.pdfPresentationMode.mouseScroll(delta);
   },
 
   /**
    * @typedef UpdateUIToolbarParameters
    * @property {number} pageNumber
    * @property {string} scaleValue
-   * @property {scale} scale
+   * @property {number} scale
    * @property {boolean} resetNumPages
    */
 
   /**
    * @param {Object} UpdateUIToolbarParameters
    * @private
    */
   _updateUIToolbar: function (params) {
@@ -8154,18 +8218,18 @@ var PDFViewerApplication = {
     toolbarConfig.pageNumber.value = pageNumber;
 
     toolbarConfig.previous.disabled = (pageNumber <= 1);
     toolbarConfig.next.disabled = (pageNumber >= pagesCount);
 
     toolbarConfig.firstPage.disabled = (pageNumber <= 1);
     toolbarConfig.lastPage.disabled = (pageNumber >= pagesCount);
 
-    toolbarConfig.zoomOut.disabled = (scale === MIN_SCALE);
-    toolbarConfig.zoomIn.disabled = (scale === MAX_SCALE);
+    toolbarConfig.zoomOut.disabled = (scale <= MIN_SCALE);
+    toolbarConfig.zoomIn.disabled = (scale >= MAX_SCALE);
 
     selectScaleOption(scaleValue, scale);
   },
 
   bindEvents: function pdfViewBindEvents() {
     var eventBus = this.eventBus;
 
     eventBus.on('resize', webViewerResize);
@@ -8470,17 +8534,17 @@ function webViewerNamedAction(e) {
   if (!PDFViewerApplication.initialized) {
     return;
   }
   // Processing couple of named actions that might be useful.
   // See also PDFLinkService.executeNamedAction
   var action = e.action;
   switch (action) {
     case 'GoToPage':
-      PDFViewerApplication.appConfig.toolbar.pageNumber.focus();
+      PDFViewerApplication.appConfig.toolbar.pageNumber.select();
       break;
 
     case 'Find':
       if (!PDFViewerApplication.supportsIntegratedFind) {
         PDFViewerApplication.findBar.toggle();
       }
       break;
   }
--- a/browser/themes/osx/controlcenter/panel.css
+++ b/browser/themes/osx/controlcenter/panel.css
@@ -12,16 +12,20 @@
 .identity-popup-expander:-moz-focusring {
   padding: 2px;
 }
 
 .identity-popup-expander:-moz-focusring > .button-box {
   @hudButtonFocused@
 }
 
+.identity-popup-permission-remove-button:-moz-focusring {
+  box-shadow: @focusRingShadow@;
+}
+
 #identity-popup-multiView > .panel-viewcontainer > .panel-viewstack > .panel-subviews {
   border-bottom-right-radius: 3.5px;
 }
 
 #identity-popup-multiView > .panel-viewcontainer > .panel-viewstack > .panel-subviews:-moz-locale-dir(rtl) {
   border-bottom-right-radius: 0;
   border-bottom-left-radius: 3.5px;
 }
--- a/browser/themes/shared/controlcenter/panel.inc.css
+++ b/browser/themes/shared/controlcenter/panel.inc.css
@@ -402,37 +402,37 @@ description#identity-popup-content-verif
   border-width: 0;
   border-radius: 50%;
   min-width: 0;
   padding: 2px;
   background-color: transparent;
 }
 
 .identity-popup-permission-remove-button > .button-box {
-  border-width: 0;
   padding: 0;
+  -moz-appearance: none;
 }
 
 .identity-popup-permission-remove-button > .button-box > .button-icon {
   margin: 0;
   width: 16px;
   height: 16px;
   list-style-image: url(chrome://browser/skin/panel-icons.svg#cancel);
   filter: url(chrome://browser/skin/filters.svg#fill);
   fill: graytext;
 }
 
 .identity-popup-permission-remove-button > .button-box > .button-text {
   display: none;
 }
 
 /* swap foreground / background colors on hover */
-.identity-popup-permission-remove-button:hover {
+.identity-popup-permission-remove-button:not(:-moz-focusring):hover {
   background-color: graytext;
 }
 
-.identity-popup-permission-remove-button:hover > .button-box > .button-icon {
+.identity-popup-permission-remove-button:not(:-moz-focusring):hover > .button-box > .button-icon {
   fill: -moz-field;
 }
 
-.identity-popup-permission-remove-button:hover:active {
+.identity-popup-permission-remove-button:not(:-moz-focusring):hover:active {
   background-color: -moz-fieldtext;
 }
--- a/browser/themes/shared/downloads/downloads.inc.css
+++ b/browser/themes/shared/downloads/downloads.inc.css
@@ -22,16 +22,19 @@
 #downloadsPanel-multiView > .panel-viewcontainer > .panel-viewstack > .panel-subviews {
   padding: 0;
 }
 
 #downloadsListBox {
   background: transparent;
   padding: 4px;
   color: inherit;
+  -moz-appearance: none;
+  margin: 0;
+  border: none;
 }
 
 #emptyDownloads {
   padding: 16px 25px;
   margin: 0;
   /* The panel can be wider than this description after the blocked subview is
      shown, so center the text. */
   text-align: center;
@@ -53,16 +56,17 @@
 .downloadsPanelFooterButton {
   -moz-appearance: none;
   background-color: transparent;
   color: inherit;
   margin: 0;
   padding: 0;
   min-width: 0;
   min-height: 40px;
+  border: none;
 }
 
 .downloadsPanelFooterButton:hover {
   outline: 1px solid hsla(210,4%,10%,.07);
   background-color: hsla(210,4%,10%,.07);
 }
 
 .downloadsPanelFooterButton:hover:active,
@@ -80,41 +84,54 @@
 .downloadsPanelFooterButton[default]:hover {
   background-color: #0675d3;
 }
 
 .downloadsPanelFooterButton[default]:hover:active {
   background-color: #0568ba;
 }
 
-#downloadsPanel[hasdownloads] #downloadsHistory {
-  padding-left: 58px !important;
+.downloadsPanelFooterButton > .button-box {
+  padding: 0;
+  margin: 0;
+  border: none;
+}
+
+#downloadsHistory {
+  padding-inline-start: 10px;
+  padding-inline-end: 10px;
+}
+
+#downloadsPanel[hasdownloads] #downloadsFooterButtons:not(.downloadsHideDropmarker) > #downloadsHistory {
+  padding-inline-start: 68px;
 }
 
 toolbarseparator.downloadsDropmarkerSplitter {
   margin: 7px 0;
 }
 
 #downloadsFooter:hover toolbarseparator.downloadsDropmarkerSplitter,
 #downloadsFooter[showingdropdown] toolbarseparator {
   margin: 0;
 }
 
 .downloadsDropmarker {
-  padding: 0 19px !important;
+  padding: 0 21px;
 }
 
 .downloadsDropmarker > .button-box > hbox {
   display: none;
 }
 
 .downloadsDropmarker > .button-box > .button-menu-dropmarker {
   /* This is to override the linux !important */
   -moz-appearance: none !important;
   display: -moz-box;
+  padding: 0;
+  margin: 0;
 }
 
 .downloadsDropmarker > .button-box > .button-menu-dropmarker > .dropmarker-icon {
   width: 16px;
   height: 16px;
   list-style-image: url("chrome://browser/skin/downloads/menubutton-dropmarker.svg");
   filter: url("chrome://browser/skin/filters.svg#fill");
   fill: currentColor;
--- a/browser/themes/windows/downloads/downloads.css
+++ b/browser/themes/windows/downloads/downloads.css
@@ -50,21 +50,16 @@
   outline: 1px -moz-dialogtext dotted;
   outline-offset: -1px;
 }
 
 @keyfocus@ #downloadsSummary:focus {
   outline-offset: -5px;
 }
 
-.downloadsPanelFooterButton > .button-box {
-  /* Hide the border so we don't display an inner focus ring. */
-  border: none;
-}
-
 /*** List items and similar elements in the summary ***/
 
 :root {
   --downloads-item-height: 7em;
   --downloads-item-border-top-color: hsla(0,0%,100%,.3);
   --downloads-item-border-bottom-color: hsla(220,18%,51%,.25);
   --downloads-item-font-size-factor: 0.9;
   --downloads-item-target-margin-bottom: 6px;
--- a/devtools/client/debugger/test/mochitest/browser_dbg_addonactor.js
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_addonactor.js
@@ -23,17 +23,17 @@ function test() {
     is(aType, "browser",
       "Root actor should identify itself as a browser.");
 
     installAddon()
       .then(attachAddonActorForId.bind(null, gClient, ADDON3_ID))
       .then(attachAddonThread)
       .then(testDebugger)
       .then(testSources)
-      .then(closeConnection)
+      .then(() => gClient.close())
       .then(uninstallAddon)
       .then(finish)
       .then(null, aError => {
         ok(false, "Got an error: " + aError.message + "\n" + aError.stack);
       });
   });
 }
 
@@ -83,19 +83,13 @@ function testSources() {
 
   return deferred.promise;
 }
 
 function uninstallAddon() {
   return removeAddon(gAddon);
 }
 
-function closeConnection() {
-  let deferred = promise.defer();
-  gClient.close(deferred.resolve);
-  return deferred.promise;
-}
-
 registerCleanupFunction(function () {
   gClient = null;
   gAddon = null;
   gThreadClient = null;
 });
--- a/devtools/client/debugger/test/mochitest/browser_dbg_break-on-dom-event-01.js
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_break-on-dom-event-01.js
@@ -26,17 +26,17 @@ function test() {
     addTab(TAB_URL)
       .then(() => attachThreadActorForUrl(gClient, TAB_URL))
       .then(setupGlobals)
       .then(pauseDebuggee)
       .then(testBreakOnAll)
       .then(testBreakOnDisabled)
       .then(testBreakOnNone)
       .then(testBreakOnClick)
-      .then(closeConnection)
+      .then(() => gClient.close())
       .then(finish)
       .then(null, aError => {
         ok(false, "Got an error: " + aError.message + "\n" + aError.stack);
       });
   });
 }
 
 function setupGlobals(aThreadClient) {
@@ -179,22 +179,16 @@ function testBreakOnClick() {
     });
 
     triggerButtonClick();
   });
 
   return deferred.promise;
 }
 
-function closeConnection() {
-  let deferred = promise.defer();
-  gClient.close(deferred.resolve);
-  return deferred.promise;
-}
-
 function unexpectedListener() {
   gClient.removeListener("paused", unexpectedListener);
   ok(false, "An unexpected hidden breakpoint was hit.");
   gThreadClient.resume(testBreakOnClick);
 }
 
 function triggerInputKeyup() {
   // Make sure that the focus is not on the input box so that a focus event
--- a/devtools/client/debugger/test/mochitest/browser_dbg_break-on-dom-event-02.js
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_break-on-dom-event-02.js
@@ -24,17 +24,17 @@ function test() {
     is(aType, "browser",
       "Root actor should identify itself as a browser.");
 
     addTab(TAB_URL)
       .then(() => attachThreadActorForUrl(gClient, TAB_URL))
       .then(aThreadClient => gThreadClient = aThreadClient)
       .then(pauseDebuggee)
       .then(testBreakOnClick)
-      .then(closeConnection)
+      .then(() => gClient.close())
       .then(finish)
       .then(null, aError => {
         ok(false, "Got an error: " + aError.message + "\n" + aError.stack);
       });
   });
 }
 
 function pauseDebuggee() {
@@ -94,18 +94,12 @@ function testBreakOnClick() {
   return deferred.promise;
 }
 
 function triggerButtonClick(aNodeId) {
   let button = content.document.getElementById(aNodeId);
   EventUtils.sendMouseEvent({ type: "click" }, button);
 }
 
-function closeConnection() {
-  let deferred = promise.defer();
-  gClient.close(deferred.resolve);
-  return deferred.promise;
-}
-
 registerCleanupFunction(function () {
   gClient = null;
   gThreadClient = null;
 });
--- a/devtools/client/debugger/test/mochitest/browser_dbg_break-on-dom-event-03.js
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_break-on-dom-event-03.js
@@ -23,17 +23,17 @@ function test() {
     is(aType, "browser",
       "Root actor should identify itself as a browser.");
 
     addTab(TAB_URL)
       .then(() => attachThreadActorForUrl(gClient, TAB_URL))
       .then(aThreadClient => gThreadClient = aThreadClient)
       .then(pauseDebuggee)
       .then(testBreakOnLoad)
-      .then(closeConnection)
+      .then(() => gClient.close())
       .then(finish)
       .then(null, aError => {
         ok(false, "Got an error: " + aError.message + "\n" + aError.stack);
       });
   });
 }
 
 function pauseDebuggee() {
@@ -86,18 +86,12 @@ function testBreakOnLoad() {
   return deferred.promise;
 }
 
 function triggerButtonClick() {
   let button = content.document.querySelector("button");
   EventUtils.sendMouseEvent({ type: "click" }, button);
 }
 
-function closeConnection() {
-  let deferred = promise.defer();
-  gClient.close(deferred.resolve);
-  return deferred.promise;
-}
-
 registerCleanupFunction(function () {
   gClient = null;
   gThreadClient = null;
 });
--- a/devtools/client/debugger/test/mochitest/browser_dbg_chrome-debugging.js
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_chrome-debugging.js
@@ -81,17 +81,17 @@ function onNewSource(aEvent, aPacket) {
 
     gThreadClient.removeListener("newSource", onNewSource);
     gNewChromeSource.resolve();
   }
 }
 
 function resumeAndCloseConnection() {
   let deferred = promise.defer();
-  gThreadClient.resume(() => gClient.close(deferred.resolve));
+  gThreadClient.resume(() => deferred.resolve(gClient.close()));
   return deferred.promise;
 }
 
 registerCleanupFunction(function () {
   gClient = null;
   gThreadClient = null;
   gAttached = null;
   gNewGlobal = null;
--- a/devtools/client/debugger/test/mochitest/browser_dbg_debugger-statement.js
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_debugger-statement.js
@@ -26,17 +26,17 @@ function test() {
 
     addTab(TAB_URL)
       .then((aTab) => {
         gTab = aTab;
         return attachTabActorForUrl(gClient, TAB_URL);
       })
       .then(testEarlyDebuggerStatement)
       .then(testDebuggerStatement)
-      .then(closeConnection)
+      .then(() => gClient.close())
       .then(finish)
       .then(null, aError => {
         ok(false, "Got an error: " + aError.message + "\n" + aError.stack);
       });
   });
 }
 
 function testEarlyDebuggerStatement([aGrip, aResponse]) {
@@ -77,17 +77,11 @@ function testDebuggerStatement([aGrip, a
   });
 
   // Reach around the debugging protocol and execute the debugger statement.
   callInTab(gTab, "runDebuggerStatement");
 
   return deferred.promise;
 }
 
-function closeConnection() {
-  let deferred = promise.defer();
-  gClient.close(deferred.resolve);
-  return deferred.promise;
-}
-
 registerCleanupFunction(function () {
   gClient = null;
 });
--- a/devtools/client/debugger/test/mochitest/browser_dbg_event-listeners-01.js
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_event-listeners-01.js
@@ -26,17 +26,17 @@ function test() {
 
     addTab(TAB_URL)
       .then((aTab) => {
         gTab = aTab;
         return attachThreadActorForUrl(gClient, TAB_URL);
       })
       .then(pauseDebuggee)
       .then(testEventListeners)
-      .then(closeConnection)
+      .then(() => gClient.close())
       .then(finish)
       .then(null, aError => {
         ok(false, "Got an error: " + aError.message + "\n" + aError.stack);
       });
   });
 }
 
 function pauseDebuggee(aThreadClient) {
@@ -137,17 +137,11 @@ function testEventListeners(aThreadClien
 
       aThreadClient.resume(deferred.resolve);
     });
   });
 
   return deferred.promise;
 }
 
-function closeConnection() {
-  let deferred = promise.defer();
-  gClient.close(deferred.resolve);
-  return deferred.promise;
-}
-
 registerCleanupFunction(function () {
   gClient = null;
 });
--- a/devtools/client/debugger/test/mochitest/browser_dbg_event-listeners-02.js
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_event-listeners-02.js
@@ -27,17 +27,17 @@ function test() {
 
     addTab(TAB_URL)
       .then((aTab) => {
         gTab = aTab;
         return attachThreadActorForUrl(gClient, TAB_URL);
       })
       .then(pauseDebuggee)
       .then(testEventListeners)
-      .then(closeConnection)
+      .then(() => gClient.close())
       .then(finish)
       .then(null, aError => {
         ok(false, "Got an error: " + aError.message + "\n" + aError.stack);
       });
   });
 }
 
 function pauseDebuggee(aThreadClient) {
@@ -113,17 +113,11 @@ function testEventListeners(aThreadClien
 
       aThreadClient.resume(deferred.resolve);
     });
   });
 
   return deferred.promise;
 }
 
-function closeConnection() {
-  let deferred = promise.defer();
-  gClient.close(deferred.resolve);
-  return deferred.promise;
-}
-
 registerCleanupFunction(function () {
   gClient = null;
 });
--- a/devtools/client/debugger/test/mochitest/browser_dbg_event-listeners-03.js
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_event-listeners-03.js
@@ -27,17 +27,17 @@ function test() {
 
     addTab(TAB_URL)
       .then((aTab) => {
         gTab = aTab;
         return attachThreadActorForUrl(gClient, TAB_URL);
       })
       .then(pauseDebuggee)
       .then(testEventListeners)
-      .then(closeConnection)
+      .then(() => gClient.close())
       .then(finish)
       .then(null, aError => {
         ok(false, "Got an error: " + aError.message + "\n" + aError.stack);
       });
   });
 }
 
 function pauseDebuggee(aThreadClient) {
@@ -72,17 +72,11 @@ function testEventListeners(aThreadClien
     // and one more from the video element controls.
     is(aPacket.listeners.length, 3, "Found all event listeners.");
     aThreadClient.resume(deferred.resolve);
   });
 
   return deferred.promise;
 }
 
-function closeConnection() {
-  let deferred = promise.defer();
-  gClient.close(deferred.resolve);
-  return deferred.promise;
-}
-
 registerCleanupFunction(function () {
   gClient = null;
 });
--- a/devtools/client/debugger/test/mochitest/browser_dbg_globalactor.js
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_globalactor.js
@@ -48,14 +48,14 @@ function test() {
                 return e.startsWith(actorPrefix);
               }).length;
             }
           }
 
           is(count, 2,
             "Only two actor exists in all pools. One tab actor and one global.");
 
-          gClient.close(finish);
+          gClient.close().then(finish);
         });
       });
     });
   });
 }
--- a/devtools/client/debugger/test/mochitest/browser_dbg_listaddons.js
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_listaddons.js
@@ -25,17 +25,17 @@ function test() {
     is(aType, "browser",
       "Root actor should identify itself as a browser.");
 
     promise.resolve(null)
       .then(testFirstAddon)
       .then(testSecondAddon)
       .then(testRemoveFirstAddon)
       .then(testRemoveSecondAddon)
-      .then(closeConnection)
+      .then(() => gClient.close())
       .then(finish)
       .then(null, aError => {
         ok(false, "Got an error: " + aError.message + "\n" + aError.stack);
       });
   });
 }
 
 function testFirstAddon() {
@@ -98,21 +98,15 @@ function testRemoveSecondAddon() {
   return removeAddon(gAddon2).then(() => {
     return getAddonActorForId(gClient, ADDON2_ID).then(aGrip => {
       ok(addonListChanged, "Should be notified that list of addons changed.");
       ok(!aGrip, "Shouldn't find a addon actor for the second addon anymore.");
     });
   });
 }
 
-function closeConnection() {
-  let deferred = promise.defer();
-  gClient.close(deferred.resolve);
-  return deferred.promise;
-}
-
 registerCleanupFunction(function () {
   gAddon1 = null;
   gAddon1Actor = null;
   gAddon2 = null;
   gAddon2Actor = null;
   gClient = null;
 });
--- a/devtools/client/debugger/test/mochitest/browser_dbg_listtabs-01.js
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_listtabs-01.js
@@ -24,17 +24,17 @@ function test() {
     is(aType, "browser",
       "Root actor should identify itself as a browser.");
 
     promise.resolve(null)
       .then(testFirstTab)
       .then(testSecondTab)
       .then(testRemoveTab)
       .then(testAttachRemovedTab)
-      .then(closeConnection)
+      .then(() => gClient.close())
       .then(finish)
       .then(null, aError => {
         ok(false, "Got an error: " + aError.message + "\n" + aError.stack);
       });
   });
 }
 
 function testFirstTab() {
@@ -84,21 +84,15 @@ function testAttachRemovedTab() {
          "Connection is gone since the tab was removed.");
       deferred.resolve();
     });
 
     return deferred.promise;
   });
 }
 
-function closeConnection() {
-  let deferred = promise.defer();
-  gClient.close(deferred.resolve);
-  return deferred.promise;
-}
-
 registerCleanupFunction(function () {
   gTab1 = null;
   gTab1Actor = null;
   gTab2 = null;
   gTab2Actor = null;
   gClient = null;
 });
--- a/devtools/client/debugger/test/mochitest/browser_dbg_listtabs-03.js
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_listtabs-03.js
@@ -42,26 +42,20 @@ function test() {
     is(newGrip.actor, tabGrip.actor, "Should have the same actor for the same tab");
 
     response = yield gClient.request({ to: tabGrip.actor, type: "attach" });
     is(response.type, "tabAttached", "Should have attached");
     response = yield gClient.request({ to: tabGrip.actor, type: "detach" });
     is(response.type, "detached", "Should have detached");
 
     yield removeTab(tab);
-    yield closeConnection();
+    yield gClient.close();
     finish();
   }));
 }
 
-function closeConnection() {
-  let deferred = promise.defer();
-  gClient.close(deferred.resolve);
-  return deferred.promise;
-}
-
 registerCleanupFunction(function () {
   gTab1 = null;
   gTab1Actor = null;
   gTab2 = null;
   gTab2Actor = null;
   gClient = null;
 });
--- a/devtools/client/debugger/test/mochitest/browser_dbg_multiple-windows.js
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_multiple-windows.js
@@ -28,17 +28,17 @@ function test() {
 
     promise.resolve(null)
       .then(() => addTab(TAB1_URL))
       .then(testFirstTab)
       .then(() => addWindow(TAB2_URL))
       .then(testNewWindow)
       .then(testFocusFirst)
       .then(testRemoveTab)
-      .then(closeConnection)
+      .then(() => gClient.close())
       .then(finish)
       .then(null, aError => {
         ok(false, "Got an error: " + aError.message + "\n" + aError.stack);
       });
   });
 }
 
 function testFirstTab(aTab) {
@@ -153,19 +153,13 @@ function continue_remove_tab(deferred)
 
     is(aResponse.selected, 0,
       "The original tab is selected.");
 
     deferred.resolve();
   });
 }
 
-function closeConnection() {
-  let deferred = promise.defer();
-  gClient.close(deferred.resolve);
-  return deferred.promise;
-}
-
 registerCleanupFunction(function () {
   gNewTab = null;
   gNewWindow = null;
   gClient = null;
 });
--- a/devtools/client/debugger/test/mochitest/browser_dbg_navigation.js
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_navigation.js
@@ -58,17 +58,17 @@ function testNavigate([aGrip, aResponse]
 }
 
 function testDetach(aActor) {
   let deferred = promise.defer();
 
   gClient.addOneTimeListener("tabDetached", (aType, aPacket) => {
     ok(true, "Got a tab detach notification.");
     is(aPacket.from, aActor, "tab detach message comes from the expected actor");
-    gClient.close(deferred.resolve);
+    deferred.resolve(gClient.close());
   });
 
   removeTab(gBrowser.selectedTab);
   return deferred.promise;
 }
 
 registerCleanupFunction(function () {
   gClient = null;
--- a/devtools/client/debugger/test/mochitest/browser_dbg_tabactor-01.js
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_tabactor-01.js
@@ -25,17 +25,17 @@ function test() {
   gClient.connect().then(([aType, aTraits]) => {
     is(aType, "browser",
       "Root actor should identify itself as a browser.");
 
     addTab(TAB_URL)
       .then(() => attachTabActorForUrl(gClient, TAB_URL))
       .then(testTabActor)
       .then(closeTab)
-      .then(closeConnection)
+      .then(() => gClient.close())
       .then(finish)
       .then(null, aError => {
         ok(false, "Got an error: " + aError.message + "\n" + aError.stack);
       });
   });
 }
 
 function testTabActor([aGrip, aResponse]) {
@@ -55,17 +55,11 @@ function testTabActor([aGrip, aResponse]
 
   return deferred.promise;
 }
 
 function closeTab() {
   return removeTab(gBrowser.selectedTab);
 }
 
-function closeConnection() {
-  let deferred = promise.defer();
-  gClient.close(deferred.resolve);
-  return deferred.promise;
-}
-
 registerCleanupFunction(function () {
   gClient = null;
 });
--- a/devtools/client/debugger/test/mochitest/browser_dbg_tabactor-02.js
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_tabactor-02.js
@@ -25,17 +25,17 @@ function test() {
   gClient.connect().then(([aType, aTraits]) => {
     is(aType, "browser",
       "Root actor should identify itself as a browser.");
 
     addTab(TAB_URL)
       .then(() => attachTabActorForUrl(gClient, TAB_URL))
       .then(testTabActor)
       .then(closeTab)
-      .then(closeConnection)
+      .then(() => gClient.close())
       .then(finish)
       .then(null, aError => {
         ok(false, "Got an error: " + aError.message + "\n" + aError.stack);
       });
   });
 }
 
 function testTabActor([aGrip, aResponse]) {
@@ -69,17 +69,11 @@ function closeTab(aTestActor) {
       is(e.message, "'ping' request packet has no destination.", "testTabActor1 went away.");
       deferred.resolve();
     }
 
     return deferred.promise;
   });
 }
 
-function closeConnection() {
-  let deferred = promise.defer();
-  gClient.close(deferred.resolve);
-  return deferred.promise;
-}
-
 registerCleanupFunction(function () {
   gClient = null;
 });
--- a/devtools/client/debugger/test/mochitest/head.js
+++ b/devtools/client/debugger/test/mochitest/head.js
@@ -657,19 +657,17 @@ AddonDebugger.prototype = {
     this.debuggerPanel = toolbox.getCurrentPanel();
     yield waitForSourceShown(this.debuggerPanel, "");
 
     prepareDebugger(this.debuggerPanel);
     yield this._attachConsole();
   }),
 
   destroy: Task.async(function* () {
-    let deferred = promise.defer();
-    this.client.close(deferred.resolve);
-    yield deferred.promise;
+    yield this.client.close();
     yield this.debuggerPanel._toolbox.destroy();
     this.frame.remove();
     window.removeEventListener("message", this._onMessage);
   }),
 
   _attachConsole: function () {
     let deferred = promise.defer();
     this.client.attachConsole(this.target.form.consoleActor, ["ConsoleAPI"], (aResponse, aWebConsoleClient) => {
@@ -1073,21 +1071,17 @@ function generateMouseClickInTab(tab, pa
 
 function connect(client) {
   info("Connecting client.");
   return client.connect();
 }
 
 function close(client) {
   info("Waiting for client to close.\n");
-  return new Promise(function (resolve) {
-    client.close(() => {
-      resolve();
-    });
-  });
+  return client.close();
 }
 
 function listTabs(client) {
   info("Listing tabs.");
   return client.listTabs();
 }
 
 function findTab(tabs, url) {
--- a/devtools/client/framework/target.js
+++ b/devtools/client/framework/target.js
@@ -581,17 +581,17 @@ TabTarget.prototype = {
     } else if (this._client) {
       // If, on the other hand, this target was remoted, the promise will be
       // resolved after the remote connection is closed.
       this._teardownRemoteListeners();
 
       if (this.isLocalTab) {
         // We started with a local tab and created the client ourselves, so we
         // should close it.
-        this._client.close(cleanupAndResolve);
+        this._client.close().then(cleanupAndResolve);
       } else if (this.activeTab) {
         // The client was handed to us, so we are not responsible for closing
         // it. We just need to detach from the tab, if already attached.
         // |detach| may fail if the connection is already dead, so proceed with
         // cleanup directly after this.
         this.activeTab.detach();
         cleanupAndResolve();
       } else {
--- a/devtools/client/framework/test/browser_two_tabs.js
+++ b/devtools/client/framework/test/browser_two_tabs.js
@@ -141,12 +141,12 @@ function checkFirstTabActor() {
   });
 }
 
 function cleanup() {
   let container = gBrowser.tabContainer;
   container.addEventListener("TabClose", function onTabClose() {
     container.removeEventListener("TabClose", onTabClose);
 
-    gClient.close(finish);
+    gClient.close().then(finish);
   });
   gBrowser.removeTab(gTab1);
 }
--- a/devtools/client/responsive.html/manager.js
+++ b/devtools/client/responsive.html/manager.js
@@ -370,23 +370,21 @@ ResponsiveUI.prototype = {
     let swap = this.swap;
     this.browserWindow = null;
     this.tab = null;
     this.inited = null;
     this.toolWindow = null;
     this.swap = null;
 
     // Close the debugger client used to speak with emulation actor
-    let clientClosed = new Promise((resolve, reject) => {
-      this.client.close(resolve);
-      this.client = this.emulationFront = null;
-    });
+    let clientClosed = this.client.close();
     if (!isTabClosing) {
       yield clientClosed;
     }
+    this.client = this.emulationFront = null;
 
     // Undo the swap and return the content back to a normal tab
     swap.stop();
 
     this.destroyed = true;
 
     return true;
   }),
--- a/devtools/client/responsivedesign/responsivedesign.jsm
+++ b/devtools/client/responsivedesign/responsivedesign.jsm
@@ -383,20 +383,18 @@ ResponsiveUI.prototype = {
     this.container.removeAttribute("responsivemode");
     this.stack.removeAttribute("responsivemode");
 
     ActiveTabs.delete(this.tab);
     if (this.touchEventSimulator) {
       this.touchEventSimulator.stop();
     }
 
-    yield new Promise((resolve, reject) => {
-      this.client.close(resolve);
-      this.client = this.emulationFront = null;
-    });
+    yield this.client.close();
+    this.client = this.emulationFront = null;
 
     this._telemetry.toolClosed("responsive");
 
     if (this.tab.linkedBrowser.messageManager) {
       let stopped = this.waitForMessage("ResponsiveMode:Stop:Done");
       this.tab.linkedBrowser.messageManager.sendAsyncMessage("ResponsiveMode:Stop");
       yield stopped;
     }
--- a/devtools/client/webconsole/hudservice.js
+++ b/devtools/client/webconsole/hudservice.js
@@ -693,17 +693,17 @@ BrowserConsole.prototype = extend(WebCon
     }
 
     this._telemetry.toolClosed("browserconsole");
 
     this._bc_destroyer = promise.defer();
 
     let chromeWindow = this.chromeWindow;
     this.$destroy().then(() =>
-      this.target.client.close(() => {
+      this.target.client.close().then(() => {
         HUDService._browserConsoleID = null;
         chromeWindow.close();
         this._bc_destroyer.resolve(null);
       }));
 
     return this._bc_destroyer.promise;
   },
 });
--- a/devtools/server/tests/browser/browser_animation_emitMutations.js
+++ b/devtools/server/tests/browser/browser_animation_emitMutations.js
@@ -52,11 +52,11 @@ add_task(function* () {
   ok(true, "The mutations event was emitted");
   is(changes.length, 2, "There are 2 changes in the mutation event");
   ok(changes.every(({type}) => type === "removed"), "Both are removals");
   ok(changes[0].player === p1 || changes[0].player === p2,
     "The first removed player was one of the previously added players");
   ok(changes[1].player === p1 || changes[1].player === p2,
     "The second removed player was one of the previously added players");
 
-  yield closeDebuggerClient(client);
+  yield client.close();
   gBrowser.removeCurrentTab();
 });
--- a/devtools/server/tests/browser/browser_animation_getFrames.js
+++ b/devtools/server/tests/browser/browser_animation_getFrames.js
@@ -22,11 +22,11 @@ add_task(function* () {
   let frames = yield player.getFrames();
   is(frames.length, 2, "The correct number of keyframes was retrieved");
   ok(frames[0].transform, "Frame 0 has the transform property");
   ok(frames[1].transform, "Frame 1 has the transform property");
   // Note that we don't really test the content of the frame object here on
   // purpose. This object comes straight out of the web animations API
   // unmodified.
 
-  yield closeDebuggerClient(client);
+  yield client.close();
   gBrowser.removeCurrentTab();
 });
--- a/devtools/server/tests/browser/browser_animation_getMultipleStates.js
+++ b/devtools/server/tests/browser/browser_animation_getMultipleStates.js
@@ -8,17 +8,17 @@
 // multiple animations.
 
 add_task(function* () {
   let {client, walker, animations} =
     yield initAnimationsFrontForUrl(MAIN_DOMAIN + "animation.html");
 
   yield playerHasAnInitialState(walker, animations);
 
-  yield closeDebuggerClient(client);
+  yield client.close();
   gBrowser.removeCurrentTab();
 });
 
 function* playerHasAnInitialState(walker, animations) {
   let state = yield getAnimationStateForNode(walker, animations,
     ".delayed-multiple-animations", 0);
 
   ok(state.duration, 50000,
--- a/devtools/server/tests/browser/browser_animation_getPlayers.js
+++ b/devtools/server/tests/browser/browser_animation_getPlayers.js
@@ -8,17 +8,17 @@
 
 add_task(function* () {
   let {client, walker, animations} =
     yield initAnimationsFrontForUrl(MAIN_DOMAIN + "animation.html");
 
   yield theRightNumberOfPlayersIsReturned(walker, animations);
   yield playersCanBePausedAndResumed(walker, animations);
 
-  yield closeDebuggerClient(client);
+  yield client.close();
   gBrowser.removeCurrentTab();
 });
 
 function* theRightNumberOfPlayersIsReturned(walker, animations) {
   let node = yield walker.querySelector(walker.rootNode, ".not-animated");
   let players = yield animations.getAnimationPlayersForNode(node);
   is(players.length, 0,
      "0 players were returned for the unanimated node");
--- a/devtools/server/tests/browser/browser_animation_getProperties.js
+++ b/devtools/server/tests/browser/browser_animation_getProperties.js
@@ -26,11 +26,11 @@ add_task(function* () {
 
   is(propertyObject.values.length, 2,
     "The correct number of property values was retrieved");
 
   // Note that we don't really test the content of the frame object here on
   // purpose. This object comes straight out of the web animations API
   // unmodified.
 
-  yield closeDebuggerClient(client);
+  yield client.close();
   gBrowser.removeCurrentTab();
 });
--- a/devtools/server/tests/browser/browser_animation_getStateAfterFinished.js
+++ b/devtools/server/tests/browser/browser_animation_getStateAfterFinished.js
@@ -45,11 +45,11 @@ add_task(function* () {
 
   is(players[1].state.duration, 300000,
      "The duration of the second animation is correct");
   is(players[1].state.delay, 1000,
      "The delay of the second animation is correct");
   is(players[1].state.iterationCount, 100,
      "The iterationCount of the second animation is correct");
 
-  yield closeDebuggerClient(client);
+  yield client.close();
   gBrowser.removeCurrentTab();
 });
--- a/devtools/server/tests/browser/browser_animation_getSubTreeAnimations.js
+++ b/devtools/server/tests/browser/browser_animation_getSubTreeAnimations.js
@@ -28,11 +28,11 @@ add_task(function* () {
   players = yield animations.getAnimationPlayersForNode(frameBody);
 
   // Testing for a hard-coded number of animations here would intermittently
   // fail depending on how fast or slow the test is (indeed, the test page
   // contains short transitions, and delayed animations). So just make sure we
   // at least have the infinitely running animations.
   ok(players.length >= 4, "All subtree animations were retrieved");
 
-  yield closeDebuggerClient(client);
+  yield client.close();
   gBrowser.removeCurrentTab();
 });
--- a/devtools/server/tests/browser/browser_animation_keepFinished.js
+++ b/devtools/server/tests/browser/browser_animation_keepFinished.js
@@ -38,17 +38,17 @@ add_task(function* () {
   players = yield animations.getAnimationPlayersForNode(node);
   is(players.length, 0, "The added animation is surely finished");
 
   is(reportedMutations.length, 1, "Only one mutation was reported");
   is(reportedMutations[0].type, "added", "The mutation was an addition");
 
   animations.off("mutations", onMutations);
 
-  yield closeDebuggerClient(client);
+  yield client.close();
   gBrowser.removeCurrentTab();
 });
 
 function wait(ms) {
   return new Promise(resolve => {
     setTimeout(resolve, ms);
   });
 }
--- a/devtools/server/tests/browser/browser_animation_playPauseIframe.js
+++ b/devtools/server/tests/browser/browser_animation_playPauseIframe.js
@@ -26,17 +26,17 @@ add_task(function* () {
   yield checkState(animations, nodeInFrame1, "paused");
   yield checkState(animations, nodeInFrame2, "paused");
 
   info("Play all animations in the test document");
   yield animations.playAll();
   yield checkState(animations, nodeInFrame1, "running");
   yield checkState(animations, nodeInFrame2, "running");
 
-  yield closeDebuggerClient(client);
+  yield client.close();
   gBrowser.removeCurrentTab();
 });
 
 function* checkState(animations, nodeFront, playState) {
   info("Getting the AnimationPlayerFront for the test node");
   let [player] = yield animations.getAnimationPlayersForNode(nodeFront);
   yield player.ready;
   let state = yield player.getCurrentState();
--- a/devtools/server/tests/browser/browser_animation_playPauseSeveral.js
+++ b/devtools/server/tests/browser/browser_animation_playPauseSeveral.js
@@ -59,17 +59,17 @@ add_task(function* () {
   yield animations.toggleSeveral([players[0]], false);
   state1 = yield players[0].getCurrentState();
   is(state1.playState, "running",
     "The playState of the first player is running");
   state2 = yield players[1].getCurrentState();
   is(state2.playState, "running",
     "The playState of the second player is running");
 
-  yield closeDebuggerClient(client);
+  yield client.close();
   gBrowser.removeCurrentTab();
 });
 
 function* checkStates(walker, animations, selectors, playState) {
   info("Checking the playState of all the nodes that have infinite running " +
        "animations");
 
   for (let selector of selectors) {
--- a/devtools/server/tests/browser/browser_animation_playerState.js
+++ b/devtools/server/tests/browser/browser_animation_playerState.js
@@ -8,17 +8,17 @@
 
 add_task(function* () {
   let {client, walker, animations} =
     yield initAnimationsFrontForUrl(MAIN_DOMAIN + "animation.html");
 
   yield playerHasAnInitialState(walker, animations);
   yield playerStateIsCorrect(walker, animations);
 
-  yield closeDebuggerClient(client);
+  yield client.close();
   gBrowser.removeCurrentTab();
 });
 
 function* playerHasAnInitialState(walker, animations) {
   let node = yield walker.querySelector(walker.rootNode, ".simple-animation");
   let [player] = yield animations.getAnimationPlayersForNode(node);
 
   ok(player.initialState, "The player front has an initial state");
--- a/devtools/server/tests/browser/browser_animation_reconstructState.js
+++ b/devtools/server/tests/browser/browser_animation_reconstructState.js
@@ -8,17 +8,17 @@
 // state that change, the front reconstructs the whole state everytime.
 
 add_task(function* () {
   let {client, walker, animations} =
     yield initAnimationsFrontForUrl(MAIN_DOMAIN + "animation.html");
 
   yield playerHasCompleteStateAtAllTimes(walker, animations);
 
-  yield closeDebuggerClient(client);
+  yield client.close();
   gBrowser.removeCurrentTab();
 });
 
 function* playerHasCompleteStateAtAllTimes(walker, animations) {
   let node = yield walker.querySelector(walker.rootNode, ".simple-animation");
   let [player] = yield animations.getAnimationPlayersForNode(node);
   yield player.ready();
 
--- a/devtools/server/tests/browser/browser_animation_refreshTransitions.js
+++ b/devtools/server/tests/browser/browser_animation_refreshTransitions.js
@@ -39,17 +39,17 @@ add_task(function* () {
   reportedMutations = yield onMutations;
 
   is(reportedMutations.length, 4, "4 new mutation events were received");
   is(reportedMutations.filter(m => m.type === "removed").length, 2,
     "2 'removed' events were sent (for the old transitions)");
   is(reportedMutations.filter(m => m.type === "added").length, 2,
     "2 'added' events were sent (for the new transitions)");
 
-  yield closeDebuggerClient(client);
+  yield client.close();
   gBrowser.removeCurrentTab();
 });
 
 function expectMutationEvents(animationsFront, nbOfEvents) {
   return new Promise(resolve => {
     let reportedMutations = [];
     function onMutations(mutations) {
       reportedMutations = [...reportedMutations, ...mutations];
--- a/devtools/server/tests/browser/browser_animation_setCurrentTime.js
+++ b/devtools/server/tests/browser/browser_animation_setCurrentTime.js
@@ -9,17 +9,17 @@
 
 add_task(function* () {
   let {client, walker, animations} =
     yield initAnimationsFrontForUrl(MAIN_DOMAIN + "animation.html");
 
   yield testSetCurrentTime(walker, animations);
   yield testSetCurrentTimes(walker, animations);
 
-  yield closeDebuggerClient(client);
+  yield client.close();
   gBrowser.removeCurrentTab();
 });
 
 function* testSetCurrentTime(walker, animations) {
   info("Retrieve an animated node");
   let node = yield walker.querySelector(walker.rootNode, ".simple-animation");
 
   info("Retrieve the animation player for the node");
--- a/devtools/server/tests/browser/browser_animation_setPlaybackRate.js
+++ b/devtools/server/tests/browser/browser_animation_setPlaybackRate.js
@@ -41,11 +41,11 @@ add_task(function* () {
   yield animations.setPlaybackRates(players, .5);
 
   info("Query their states and check they are correct");
   for (let player of players) {
     let state = yield player.getCurrentState();
     is(state.playbackRate, .5, "The playbackRate was updated");
   }
 
-  yield closeDebuggerClient(client);
+  yield client.close();
   gBrowser.removeCurrentTab();
 });
--- a/devtools/server/tests/browser/browser_animation_simple.js
+++ b/devtools/server/tests/browser/browser_animation_simple.js
@@ -25,11 +25,11 @@ add_task(function* () {
   }
   ok(didThrow, "An exception was thrown for a missing NodeActor");
 
   let invalidNode = yield walker.querySelector(walker.rootNode, "title");
   let players = yield animations.getAnimationPlayersForNode(invalidNode);
   ok(Array.isArray(players), "An array of players was returned");
   is(players.length, 0, "0 players have been returned for the invalid node");
 
-  yield closeDebuggerClient(client);
+  yield client.close();
   gBrowser.removeCurrentTab();
 });
--- a/devtools/server/tests/browser/browser_animation_updatedState.js
+++ b/devtools/server/tests/browser/browser_animation_updatedState.js
@@ -7,17 +7,17 @@
 // Check the animation player's updated state
 
 add_task(function* () {
   let {client, walker, animations} =
     yield initAnimationsFrontForUrl(MAIN_DOMAIN + "animation.html");
 
   yield playStateIsUpdatedDynamically(walker, animations);
 
-  yield closeDebuggerClient(client);
+  yield client.close();
   gBrowser.removeCurrentTab();
 });
 
 function* playStateIsUpdatedDynamically(walker, animations) {
   info("Getting the test node (which runs a very long animation)");
   // The animation lasts for 100s, to avoid intermittents.
   let node = yield walker.querySelector(walker.rootNode, ".long-animation");
 
--- a/devtools/server/tests/browser/browser_directorscript_actors.js
+++ b/devtools/server/tests/browser/browser_directorscript_actors.js
@@ -18,17 +18,17 @@ add_task(function* () {
   DirectorRegistry.clear();
   let directorManager = DirectorManagerFront(client, form);
 
   yield testDirectorScriptAttachEventAttributes(directorManager);
   yield testDirectorScriptMessagePort(directorManager);
   yield testDirectorScriptWindowEval(directorManager);
   yield testDirectorScriptUnloadOnDetach(directorManager);
 
-  yield closeDebuggerClient(client);
+  yield client.close();
   gBrowser.removeCurrentTab();
   DirectorRegistry.clear();
 });
 
 function* testDirectorScriptAttachEventAttributes(directorManager) {
   let attachEvent = yield installAndEnableDirectorScript(directorManager, {
     scriptId: "testDirectorScript_attachEventAttributes",
     scriptCode: "(" + (function () {
--- a/devtools/server/tests/browser/browser_directorscript_actors_error_events.js
+++ b/devtools/server/tests/browser/browser_directorscript_actors_error_events.js
@@ -18,17 +18,17 @@ add_task(function* () {
   DirectorRegistry.clear();
   let directorManager = DirectorManagerFront(client, form);
 
   yield testErrorOnRequire(directorManager);
   yield testErrorOnEvaluate(directorManager);
   yield testErrorOnAttach(directorManager);
   yield testErrorOnDetach(directorManager);
 
-  yield closeDebuggerClient(client);
+  yield client.close();
   gBrowser.removeCurrentTab();
   DirectorRegistry.clear();
 });
 
 function* testErrorOnRequire(directorManager) {
   // director script require method should raise a "not implemented" exception
   let errorOnRequire = yield installAndEnableDirectorScript(directorManager, {
     scriptId: "testDirectorScript_errorOnRequire",
--- a/devtools/server/tests/browser/browser_directorscript_actors_exports.js
+++ b/devtools/server/tests/browser/browser_directorscript_actors_exports.js
@@ -52,17 +52,17 @@ add_task(function* () {
     scriptOptions: {
       attachMethod: "attach"
     }
   });
   let { message } = errorUndefinedAttachMethod;
   ok(!!message, "testDirectorScript_undefinedAttachMethod error event received");
   assertIsDirectorScriptError(errorUndefinedAttachMethod);
 
-  yield closeDebuggerClient(client);
+  yield client.close();
   gBrowser.removeCurrentTab();
   DirectorRegistry.clear();
 });
 
 function assertIsDirectorScriptError(error) {
   ok(!!error.message, "errors should contain a message");
   ok(!!error.stack, "errors should contain a stack trace");
   ok(!!error.fileName, "errors should contain a fileName");
--- a/devtools/server/tests/browser/browser_markers-cycle-collection.js
+++ b/devtools/server/tests/browser/browser_markers-cycle-collection.js
@@ -23,11 +23,11 @@ add_task(function* () {
   let rec = yield front.startRecording({ withMarkers: true });
 
   let markers = yield waitForMarkerType(front, ["nsCycleCollector::Collect", "nsCycleCollector::ForgetSkippable"]);
   yield front.stopRecording(rec);
 
   ok(markers.some(m => m.name === "nsCycleCollector::Collect"), "got some nsCycleCollector::Collect markers");
   ok(markers.some(m => m.name === "nsCycleCollector::ForgetSkippable"), "got some nsCycleCollector::Collect markers");
 
-  yield closeDebuggerClient(client);
+  yield client.close();
   gBrowser.removeCurrentTab();
 });
--- a/devtools/server/tests/browser/browser_markers-docloading-01.js
+++ b/devtools/server/tests/browser/browser_markers-docloading-01.js
@@ -27,11 +27,11 @@ add_task(function* () {
   yield waitForMarkerType(front, MARKER_NAMES, () => true, e => e, "markers");
   yield front.stop(rec);
 
   ok(true, "Found the required marker names.");
 
   // Wait some more time to make sure the 'doc-loading' events never get fired.
   yield DevToolsUtils.waitForTime(1000);
 
-  yield closeDebuggerClient(client);
+  yield client.close();
   gBrowser.removeCurrentTab();
 });
--- a/devtools/server/tests/browser/browser_markers-docloading-02.js
+++ b/devtools/server/tests/browser/browser_markers-docloading-02.js
@@ -25,11 +25,11 @@ add_task(function* () {
 
   ok(true, "At least one doc-loading event got fired.");
 
   yield waitForMarkerType(front, MARKER_NAMES, () => true, e => e, "markers");
   yield front.stop(rec);
 
   ok(true, "Found the required marker names.");
 
-  yield closeDebuggerClient(client);
+  yield client.close();
   gBrowser.removeCurrentTab();
 });
--- a/devtools/server/tests/browser/browser_markers-docloading-03.js
+++ b/devtools/server/tests/browser/browser_markers-docloading-03.js
@@ -29,11 +29,11 @@ add_task(function* () {
 
   ok(true, "At least one doc-loading event got fired.");
 
   yield front.stop(rec);
 
   // Wait some more time to make sure the 'doc-loading' markers never get fired.
   yield DevToolsUtils.waitForTime(1000);
 
-  yield closeDebuggerClient(client);
+  yield client.close();
   gBrowser.removeCurrentTab();
 });
--- a/devtools/server/tests/browser/browser_markers-gc.js
+++ b/devtools/server/tests/browser/browser_markers-gc.js
@@ -40,11 +40,11 @@ add_task(function* () {
       ok(false, `markers must be in order. ${current.name} marker has later start time (${current.start}) thanprevious: ${previousStart}`);
       ordered = false;
     }
     return current.start;
   });
 
   is(ordered, true, "All GC and non-GC markers are in order by start time.");
 
-  yield closeDebuggerClient(client);
+  yield client.close();
   gBrowser.removeCurrentTab();
 });
--- a/devtools/server/tests/browser/browser_markers-minor-gc.js
+++ b/devtools/server/tests/browser/browser_markers-minor-gc.js
@@ -22,11 +22,11 @@ add_task(function* () {
   let rec = yield front.startRecording({ withMarkers: true });
 
   let markers = yield waitForMarkerType(front, ["MinorGC"]);
   yield front.stopRecording(rec);
 
   ok(markers.some(m => m.name === "MinorGC" && m.causeName),
      "got some MinorGC markers");
 
-  yield closeDebuggerClient(client);
+  yield client.close();
   gBrowser.removeCurrentTab();
 });
--- a/devtools/server/tests/browser/browser_markers-parse-html.js
+++ b/devtools/server/tests/browser/browser_markers-parse-html.js
@@ -19,11 +19,11 @@ add_task(function* () {
   yield front.connect();
   let rec = yield front.startRecording({ withMarkers: true });
 
   let markers = yield waitForMarkerType(front, MARKER_NAME);
   yield front.stopRecording(rec);
 
   ok(markers.some(m => m.name === MARKER_NAME), `got some ${MARKER_NAME} markers`);
 
-  yield closeDebuggerClient(client);
+  yield client.close();
   gBrowser.removeCurrentTab();
 });
--- a/devtools/server/tests/browser/browser_markers-styles.js
+++ b/devtools/server/tests/browser/browser_markers-styles.js
@@ -24,11 +24,11 @@ add_task(function* () {
   });
 
   yield front.stopRecording(rec);
 
   ok(markers.some(m => m.name === MARKER_NAME), `got some ${MARKER_NAME} markers`);
   ok(markers.some(({restyleHint}) => restyleHint != void 0),
     "Some markers have a restyleHint.");
 
-  yield closeDebuggerClient(client);
+  yield client.close();
   gBrowser.removeCurrentTab();
 });
--- a/devtools/server/tests/browser/browser_markers-timestamp.js
+++ b/devtools/server/tests/browser/browser_markers-timestamp.js
@@ -33,11 +33,11 @@ add_task(function* () {
   ok(markers.length === 2, "found 2 TimeStamp markers");
   ok(markers.every(({start, end}) => typeof start === "number" && start === end),
     "All markers have equal start and end times");
   is(markers[0].causeName, void 0, "Unlabeled timestamps have an empty causeName");
   is(markers[1].causeName, "myLabel", "Labeled timestamps have correct causeName");
 
   pmmClearFrameScripts();
 
-  yield closeDebuggerClient(client);
+  yield client.close();
   gBrowser.removeCurrentTab();
 });
--- a/devtools/server/tests/browser/browser_navigateEvents.js
+++ b/devtools/server/tests/browser/browser_navigateEvents.js
@@ -147,14 +147,14 @@ function test() {
 
   });
 }
 
 function cleanup() {
   let browser = gBrowser.selectedBrowser;
   browser.removeEventListener("DOMContentLoaded", onDOMContentLoaded);
   browser.removeEventListener("load", onLoad);
-  client.close(function () {
+  client.close().then(function () {
     Services.obs.addObserver(httpObserver, "http-on-modify-request", false);
     DebuggerServer.destroy();
     finish();
   });
 }
--- a/devtools/server/tests/browser/browser_perf-allocation-data.js
+++ b/devtools/server/tests/browser/browser_perf-allocation-data.js
@@ -28,11 +28,11 @@ add_task(function* () {
 
   let { frames, timestamps, sizes, sites } = rec.getAllocations();
 
   is(timestamps.length, sizes.length, "we have the same amount of timestamps and sizes");
   ok(timestamps.every(time => time > 0 && typeof time === "number"), "all timestamps have numeric values");
   ok(sizes.every(n => n > 0 && typeof n === "number"), "all sizes are positive numbers");
 
   yield front.destroy();
-  yield closeDebuggerClient(client);
+  yield client.close();
   gBrowser.removeCurrentTab();
 });
--- a/devtools/server/tests/browser/browser_perf-profiler-01.js
+++ b/devtools/server/tests/browser/browser_perf-profiler-01.js
@@ -31,15 +31,15 @@ add_task(function* () {
     "The built-in profiler module should still be active (1).");
 
   rec = yield front.startRecording();
   yield front.stopRecording(rec);
   ok((yield pmmIsProfilerActive()),
     "The built-in profiler module should still be active (2).");
 
   yield front.destroy();
-  yield closeDebuggerClient(client);
+  yield client.close();
 
   ok(!(yield pmmIsProfilerActive()),
     "The built-in profiler module should no longer be active.");
 
   gBrowser.removeCurrentTab();
 });
--- a/devtools/server/tests/browser/browser_perf-profiler-02.js
+++ b/devtools/server/tests/browser/browser_perf-profiler-02.js
@@ -27,20 +27,20 @@ add_task(function* () {
   let secondFront = PerformanceFront(client2, form2);
   yield secondFront.connect();
   pmmLoadFrameScripts(gBrowser);
 
   yield secondFront.startRecording();
 
   // Manually teardown the tabs so we can check profiler status
   yield secondFront.destroy();
-  yield closeDebuggerClient(client2);
+  yield client2.close();
   ok((yield pmmIsProfilerActive()),
     "The built-in profiler module should still be active.");
 
   yield firstFront.destroy();
-  yield closeDebuggerClient(client);
+  yield client.close();
   ok(!(yield pmmIsProfilerActive()),
     "The built-in profiler module should no longer be active.");
 
   gBrowser.removeCurrentTab();
   gBrowser.removeCurrentTab();
 });
--- a/devtools/server/tests/browser/browser_perf-profiler-03.js
+++ b/devtools/server/tests/browser/browser_perf-profiler-03.js
@@ -33,22 +33,22 @@ add_task(function* () {
 
   yield addTab(MAIN_DOMAIN + "doc_perf.html");
   let client2 = new DebuggerClient(DebuggerServer.connectPipe());
   let form2 = yield connectDebuggerClient(client2);
   let secondFront = PerformanceFront(client2, form2);
   yield secondFront.connect();
 
   yield secondFront.destroy();
-  yield closeDebuggerClient(client2);
+  yield client2.close();
   ok((yield pmmIsProfilerActive()),
     "The built-in profiler module should still be active.");
 
   yield firstFront.destroy();
-  yield closeDebuggerClient(client);
+  yield client.close();
   ok(!(yield pmmIsProfilerActive()),
     "The built-in profiler module should have been automatically stopped.");
 
   pmmClearFrameScripts();
 
   gBrowser.removeCurrentTab();
   gBrowser.removeCurrentTab();
 });
--- a/devtools/server/tests/browser/browser_perf-realtime-markers.js
+++ b/devtools/server/tests/browser/browser_perf-realtime-markers.js
@@ -39,17 +39,17 @@ add_task(function* () {
   yield front.stopRecording(rec);
   front.off("timeline-data", handler);
 
   is(counters.markers.length, 1, "one marker event fired.");
   is(counters.memory.length, 3, "three memory events fired.");
   is(counters.ticks.length, 3, "three ticks events fired.");
 
   yield front.destroy();
-  yield closeDebuggerClient(client);
+  yield client.close();
   gBrowser.removeCurrentTab();
 
   function handler(name, data) {
     if (name === "markers") {
       if (counters.markers.length >= 1) { return; }
       ok(data.markers[0].start, "received atleast one marker with `start`");
       ok(data.markers[0].end, "received atleast one marker with `end`");
       ok(data.markers[0].name, "received atleast one marker with `name`");
--- a/devtools/server/tests/browser/browser_perf-recording-actor-01.js
+++ b/devtools/server/tests/browser/browser_perf-recording-actor-01.js
@@ -63,17 +63,17 @@ add_task(function* () {
   ok(importedModel.isCompleted(), "All imported recordings should be completed");
   ok(!importedModel.isRecording(), "All imported recordings should not be recording");
   ok(importedModel.isImported(), "All imported recordings should be considerd imported");
 
   checkSystemInfo(importedModel, "Host");
   checkSystemInfo(importedModel, "Client");
 
   yield front.destroy();
-  yield closeDebuggerClient(client);
+  yield client.close();
   gBrowser.removeCurrentTab();
 });
 
 function checkSystemInfo(recording, type) {
   let data = recording[`get${type}SystemInfo`]();
   for (let field of ["appid", "apptype", "vendor", "name", "version"]) {
     ok(data[field], `get${type}SystemInfo() has ${field} property`);
   }
--- a/devtools/server/tests/browser/browser_perf-recording-actor-02.js
+++ b/devtools/server/tests/browser/browser_perf-recording-actor-02.js
@@ -44,11 +44,11 @@ add_task(function* () {
 
   ok(checkCount >= 1, "atleast 1 event were fired until the buffer was filled");
   is(lastBufferStatus, 1, "buffer usage cannot surpass 100%");
   yield front.stopRecording(model);
 
   is(front.getBufferUsageForRecording(model), null, "buffer usage should be null when no longer recording.");
 
   yield front.destroy();
-  yield closeDebuggerClient(client);
+  yield client.close();
   gBrowser.removeCurrentTab();
 });
--- a/devtools/server/tests/browser/browser_perf-samples-01.js
+++ b/devtools/server/tests/browser/browser_perf-samples-01.js
@@ -53,11 +53,11 @@ add_task(function* () {
     "The second recorded sample times were normalized.");
   ok(secondRecordingSamples[0][TIME_SLOT] > 0,
     "The second recorded sample times were normalized correctly.");
   ok(!secondRecordingSamples.find(e => e[TIME_SLOT] + secondRecordingStartTime <= firstRecording.getDuration()),
     "There should be no samples from the first recording in the second one, " +
     "even though the total number of frames did not overflow.");
 
   yield front.destroy();
-  yield closeDebuggerClient(client);
+  yield client.close();
   gBrowser.removeCurrentTab();
 });
--- a/devtools/server/tests/browser/browser_perf-samples-02.js
+++ b/devtools/server/tests/browser/browser_perf-samples-02.js
@@ -40,17 +40,17 @@ add_task(function* () {
       }
     }
   }
 
   ok(sampleCount > 0,
     "At least some samples have been iterated over, checking for root nodes.");
 
   yield front.destroy();
-  yield closeDebuggerClient(client);
+  yield client.close();
   gBrowser.removeCurrentTab();
 });
 
 /**
  * Inflate a particular sample's stack and return an array of strings.
  */
 function getInflatedStackLocations(thread, sample) {
   let stackTable = thread.stackTable;
--- a/devtools/server/tests/browser/browser_register_actor.js
+++ b/devtools/server/tests/browser/browser_register_actor.js
@@ -27,17 +27,17 @@ function test() {
           var tab = response.tabs[response.selected];
           ok(!!tab.helloActor, "Hello actor must exist");
 
           // Make sure actor's state is maintained across listTabs requests.
           checkActorState(tab.helloActor, () => {
 
             // Clean up
             actorFront.unregister().then(() => {
-              gClient.close(() => {
+              gClient.close().then(() => {
                 DebuggerServer.destroy();
                 gClient = null;
                 finish();
               });
             });
           });
         });
       });
--- a/devtools/server/tests/browser/browser_stylesheets_getTextEmpty.js
+++ b/devtools/server/tests/browser/browser_stylesheets_getTextEmpty.js
@@ -31,10 +31,10 @@ add_task(function* () {
      "getStyleSheets() returned the correct number of sheets");
 
   let sheet = sheets[0];
   yield sheet.update("", false);
   let longStr = yield sheet.getText();
   let source = yield longStr.string();
   is(source, "", "text is empty");
 
-  yield closeDebuggerClient(client);
+  yield client.close();
 });
--- a/devtools/server/tests/browser/browser_stylesheets_nested-iframes.js
+++ b/devtools/server/tests/browser/browser_stylesheets_nested-iframes.js
@@ -29,10 +29,10 @@ add_task(function* () {
 
   // Bug 285395 limits the number of nested iframes to 10. There's one sheet per
   // frame so we should get 10 sheets. However, the limit might change in the
   // future so it's better not to rely on the limit. Asserting > 2 ensures that
   // the test page is actually loading nested iframes and this test is doing
   // something sensible (if we got this far, the test has served its purpose).
   ok(sheets.length > 2, sheets.length + " sheets found (expected 3 or more).");
 
-  yield closeDebuggerClient(client);
+  yield client.close();
 });
--- a/devtools/server/tests/browser/browser_timeline.js
+++ b/devtools/server/tests/browser/browser_timeline.js
@@ -53,11 +53,11 @@ add_task(function* () {
 
   ok(markers.length > 0, "markers were returned");
 
   yield front.stop();
 
   isActive = yield front.isRecording();
   ok(!isActive, "Not recording after stop()");
 
-  yield closeDebuggerClient(client);
+  yield client.close();
   gBrowser.removeCurrentTab();
 });
--- a/devtools/server/tests/browser/browser_timeline_actors.js
+++ b/devtools/server/tests/browser/browser_timeline_actors.js
@@ -41,17 +41,17 @@ add_task(function* () {
 
   ok((yield waitUntil(() => updatedMemory > 1)),
     "Some memory measurements were emitted.");
   ok((yield waitUntil(() => updatedTicks > 1)),
     "Some refresh driver ticks were emitted.");
 
   info("Stop timeline marker recording");
   yield front.stop();
-  yield closeDebuggerClient(client);
+  yield client.close();
   gBrowser.removeCurrentTab();
 });
 
 /**
  * Waits until a predicate returns true.
  *
  * @param function predicate
  *        Invoked once in a while until it returns true.
--- a/devtools/server/tests/browser/browser_timeline_iframes.js
+++ b/devtools/server/tests/browser/browser_timeline_iframes.js
@@ -26,16 +26,16 @@ add_task(function* () {
   for (let i = 0; i < 3; i++) {
     yield wait(300); // That's the time the child frame waits before changing styles.
     let markers = yield once(front, "markers");
     ok(markers.length, "Markers were received for operations in the child frame");
   }
 
   info("Stop timeline marker recording");
   yield front.stop();
-  yield closeDebuggerClient(client);
+  yield client.close();
   gBrowser.removeCurrentTab();
 });
 
 function wait(ms) {
   return new Promise(resolve =>
     setTimeout(resolve, ms));
 }
--- a/devtools/server/tests/browser/head.js
+++ b/devtools/server/tests/browser/head.js
@@ -78,25 +78,16 @@ function connectDebuggerClient(client) {
   return client.connect()
     .then(() => client.listTabs())
     .then(tabs => {
       return tabs.tabs[tabs.selected];
     });
 }
 
 /**
- * Close a debugger client's connection.
- * @param {DebuggerClient}
- * @return {Promise} Resolves when the connection is closed.
- */
-function closeDebuggerClient(client) {
-  return new Promise(resolve => client.close(resolve));
-}
-
-/**
  * Wait for eventName on target.
  * @param {Object} target An observable object that either supports on/off or
  * addEventListener/removeEventListener
  * @param {String} eventName
  * @param {Boolean} useCapture Optional, for addEventListener/removeEventListener
  * @return A promise that resolves when the event has been handled
  */
 function once(target, eventName, useCapture = false) {
--- a/devtools/server/tests/mochitest/memory-helpers.js
+++ b/devtools/server/tests/mochitest/memory-helpers.js
@@ -27,17 +27,17 @@ function startServerAndGetSelectedTabMem
       var form = response.tabs[response.selected];
       var memory = MemoryFront(client, form, response);
 
       return { memory, client };
     });
 }
 
 function destroyServerAndFinish(client) {
-  client.close(() => {
+  client.close().then(() => {
     DebuggerServer.destroy();
     SimpleTest.finish();
   });
 }
 
 function waitForTime(ms) {
   return new Promise((resolve, reject) => {
     setTimeout(resolve, ms);
--- a/devtools/server/tests/mochitest/test_device.html
+++ b/devtools/server/tests/mochitest/test_device.html
@@ -72,17 +72,17 @@ window.onload = function() {
         var profileDir = currProfD.path;
         ok(profileDir.indexOf(desc.profile.length > 0 && desc.profile) != -1, "valid profile name");
 
         var a = JSON.stringify(PermissionsTable);
         var b = JSON.stringify(permissions.rawPermissionsTable);
 
         is(a, b, "Permissions Tables is valid");
 
-        client.close(() => {
+        client.close().then(() => {
           DebuggerServer.destroy();
           SimpleTest.finish()
         });
       }
 
 
       d.getDescription().then((v) => desc = v)
       .then(() => d.getRawPermissionsTable())
--- a/devtools/server/tests/mochitest/test_framerate_01.html
+++ b/devtools/server/tests/mochitest/test_framerate_01.html
@@ -124,17 +124,17 @@ window.onload = function() {
         "The start and end framerate values should be equal.");
 
       is(typeof currFramerateStart, "number", "All values should be numbers.");
       ok(currFramerateStart <= 60, "All values were correctly clamped.")
 
       prevFramerateValue = currFramerateStart;
     }
 
-    client.close(() => {
+    client.close().then(() => {
       DebuggerServer.destroy();
       SimpleTest.finish()
     });
   }
 }
 </script>
 </pre>
 </body>
--- a/devtools/server/tests/mochitest/test_framerate_02.html
+++ b/devtools/server/tests/mochitest/test_framerate_02.html
@@ -94,17 +94,17 @@ window.onload = function() {
         is(timeline[0].value, 0,
           "The first framerate value should be 0.");
 
         is(timeline[1].delta, 100,
           "The last time delta should be 100 (the default interval value).");
         is(timeline[1].value, 0,
           "The last framerate value should be 0.");
 
-        client.close(() => {
+        client.close().then(() => {
           DebuggerServer.destroy();
           SimpleTest.finish()
         });
       });
     });
   });
 }
 </script>
--- a/devtools/server/tests/mochitest/test_framerate_03.html
+++ b/devtools/server/tests/mochitest/test_framerate_03.html
@@ -65,17 +65,17 @@ window.onload = function() {
     ok(!rawData.find(e => e > STOP_TICK),
       "There should be no tick after 3000ms.");
 
     for (var tick of rawData) {
       info("Testing tick: " + tick);
       is(typeof tick, "number", "All values should be numbers.");
     }
 
-    client.close(() => {
+    client.close().then(() => {
       DebuggerServer.destroy();
       SimpleTest.finish()
     });
   }
 }
 </script>
 </pre>
 </body>
--- a/devtools/server/tests/mochitest/test_framerate_04.html
+++ b/devtools/server/tests/mochitest/test_framerate_04.html
@@ -54,17 +54,17 @@ window.onload = function() {
     info("Difference in ticks: " + diff);
     ok(diff > 0, "More ticks should be recorded in the second batch.");
 
     ok(firstBatch.every((e) => secondBatch.indexOf(e) != -1),
       "All the ticks in the first batch should be in the second batch as well.");
     ok(secondBatch.every((e, i, array) => i < array.length - 1 ? e < array[i + 1] : true),
       "All the ticks in the final batch should be ascending in value.");
 
-    client.close(() => {
+    client.close().then(() => {
       DebuggerServer.destroy();
       SimpleTest.finish()
     });
   }
 }
 </script>
 </pre>
 <a id="testContent" target="_blank" href="inspector_getImageData.html">Test Document</a>
--- a/devtools/server/tests/mochitest/test_framerate_05.html
+++ b/devtools/server/tests/mochitest/test_framerate_05.html
@@ -53,17 +53,17 @@ window.onload = function() {
                   "The returned pending ticks should be empty (2).");
 
                 front.stopRecording().then(rawData => {
                   ok(rawData,
                     "The returned raw data should be an empty array (1).");
                   is(rawData.length, 0,
                     "The returned raw data should be an empty array (2).");
 
-                  client.close(() => {
+                  client.close().then(() => {
                     DebuggerServer.destroy();
                     SimpleTest.finish()
                   });
                 });
               });
             }, 1000);
           });
         }, 1000);
--- a/devtools/server/tests/mochitest/test_framerate_06.html
+++ b/devtools/server/tests/mochitest/test_framerate_06.html
@@ -64,17 +64,17 @@ window.onload = function() {
     // when we get ticks from other frames they're usually at diffs of < 1. Sometimes
     // ticks can still be less than 16ms even on one frame (usually following a very slow
     // frame), so use a low number (2) to be our threshold
     var THRESHOLD = 2;
     ok(ticks.length >= 20, "we should have atleast 20 ticks over the course of two seconds.");
     var belowThreshold = diffs.filter(v => v <= THRESHOLD);
     ok(belowThreshold.length <= 10, "we should have very few frames less than the threshold");
 
-    client.close(() => {
+    client.close().then(() => {
       DebuggerServer.destroy();
       SimpleTest.finish()
     });
   }
 }
 </script>
 </pre>
 <a id="testContent" target="_blank" href="inspector-traversal-data.html">Test Document</a>
--- a/devtools/server/tests/mochitest/test_getProcess.html
+++ b/devtools/server/tests/mochitest/test_getProcess.html
@@ -99,17 +99,17 @@ function runTests() {
           ok(response.result, 42, "console.eval worked");
           cleanup();
         });
       });
     });
   }
 
   function cleanup() {
-    client.close(function () {
+    client.close().then(function () {
       DebuggerServer.destroy();
       iframe.remove();
       SimpleTest.finish()
     });
   }
 
   connect();
 }
--- a/devtools/server/tests/mochitest/test_preference.html
+++ b/devtools/server/tests/mochitest/test_preference.html
@@ -70,17 +70,17 @@ function runTests() {
           is(prefs.allPrefs[key].hasUserValue, Services.prefs.prefHasUserValue(key), "valid hasUserValue (" + key + ")");
         });
 
         ["test.bool", "test.int", "test.string"].forEach(function(key) {
           ok(!prefs.allPrefs.hasOwnProperty(key), "expect no pref (" + key + ")");
           is(Services.prefs.getPrefType(key), Ci.nsIPrefBranch.PREF_INVALID, "pref (" + key + ") is clear");
         });
 
-        client.close(() => {
+        client.close().then(() => {
           DebuggerServer.destroy();
           SimpleTest.finish()
         });
       }
 
 
       p.getAllPrefs().then((json) => prefs["allPrefs"]  = json)
       .then(() => p.setBoolPref("test.bool", localPref.boolPref))
--- a/devtools/server/tests/mochitest/test_settings.html
+++ b/devtools/server/tests/mochitest/test_settings.html
@@ -76,17 +76,17 @@ function runTests() {
         is(settings["app.reportCrashes"], localSetting["app.reportCrashes"], "updated string setting");
         is(JSON.stringify(settings["app.someObject"]), JSON.stringify(localSetting["app.someObject"]), "updated object as string setting");
 
         is(resetSettings["wifi.enabled"], fakeSettings["wifi.enabled"], "reset to original bool setting");
         is(resetSettings["audio.volume.alarm"], fakeSettings["audio.volume.alarm"], "reset to original int setting");
         is(resetSettings["app.reportCrashes"], fakeSettings["app.reportCrashes"], "reset to original string setting");
         is(JSON.stringify(resetSettings["app.someObject"]), JSON.stringify(fakeSettings["app.someObject"]), "reset to original object setting");
 
-        client.close(() => {
+        client.close().then(() => {
           DebuggerServer.destroy();
           SimpleTest.finish();
         });
       }
 
       // settings.json doesn't exist outside of b2g so we will fake it.
       _setDefaultSettings(fakeSettings);
       s.setSetting("wifi.enabled", fakeSettings["wifi.enabled"])
--- a/devtools/server/tests/mochitest/test_setupInParentChild.html
+++ b/devtools/server/tests/mochitest/test_setupInParentChild.html
@@ -91,17 +91,17 @@ function runTests() {
     DebuggerServer.setupInChild({
       module: "chrome://mochitests/content/chrome/devtools/server/tests/mochitest/setup-in-child.js",
       setupChild: "setupChild",
       args: [1, "two", {three: true}]
     });
   });
 
   function cleanup() {
-    client.close(function () {
+    client.close().then(function () {
       DebuggerServer.destroy();
       iframe.remove();
       SimpleTest.finish()
     });
   }
 
 }
 </script>
--- a/devtools/server/tests/unit/head_dbg.js
+++ b/devtools/server/tests/unit/head_dbg.js
@@ -159,21 +159,17 @@ function createTestGlobal(name) {
 
 function connect(client) {
   dump("Connecting client.\n");
   return client.connect();
 }
 
 function close(client) {
   dump("Closing client.\n");
-  return new Promise(function (resolve) {
-    client.close(function () {
-      resolve();
-    });
-  });
+  return client.close();
 }
 
 function listTabs(client) {
   dump("Listing tabs.\n");
   return client.listTabs();
 }
 
 function findTab(tabs, title) {
--- a/devtools/server/tests/unit/test_add_actors.js
+++ b/devtools/server/tests/unit/test_add_actors.js
@@ -98,10 +98,10 @@ function test_stable_global_actor_instan
   gClient.listTabs(function onListTabs(aResponse) {
     do_check_eq(postInitGlobalActor, getActorInstance(connID, aResponse.postInitGlobalActor));
     do_check_eq(preInitGlobalActor, getActorInstance(connID, aResponse.preInitGlobalActor));
     run_next_test();
   });
 }
 
 function close_client() {
-  gClient.close(() => run_next_test());
+  gClient.close().then(() => run_next_test());
 }
--- a/devtools/server/tests/unit/test_breakpoint-01.js
+++ b/devtools/server/tests/unit/test_breakpoint-01.js
@@ -49,17 +49,17 @@ function test_simple_breakpoint()
         do_check_eq(aPacket.why.actors[0], bpClient.actor);
         // Check that the breakpoint worked.
         do_check_eq(gDebuggee.a, 1);
         do_check_eq(gDebuggee.b, undefined);
 
         // Remove the breakpoint.
         bpClient.remove(function (aResponse) {
           gThreadClient.resume(function () {
-            gClient.close(gCallback);
+            gClient.close().then(gCallback);
           });
         });
 
       });
 
       // Continue until the breakpoint is hit.
       gThreadClient.resume();
     });
--- a/devtools/server/tests/unit/test_breakpoint-02.js
+++ b/devtools/server/tests/unit/test_breakpoint-02.js
@@ -47,17 +47,17 @@ function test_breakpoint_running()
 
     let source = gThreadClient.source(aPacket.frame.where.source);
     source.setBreakpoint(location, function (aResponse) {
       // Eval scripts don't stick around long enough for the breakpoint to be set,
       // so just make sure we got the expected response from the actor.
       do_check_neq(aResponse.error, "noScript");
 
       do_execute_soon(function () {
-        gClient.close(gCallback);
+        gClient.close().then(gCallback);
       });
     });
   });
 
   Cu.evalInSandbox(
     "var line0 = Error().lineNumber;\n" +
     "debugger;\n" +
     "var a = 1;\n" +  // line0 + 2
--- a/devtools/server/tests/unit/test_breakpoint-03.js
+++ b/devtools/server/tests/unit/test_breakpoint-03.js
@@ -57,17 +57,17 @@ function test_skip_breakpoint()
         do_check_eq(aPacket.why.actors[0], bpClient.actor);
         // Check that the breakpoint worked.
         do_check_eq(gDebuggee.a, 1);
         do_check_eq(gDebuggee.b, undefined);
 
         // Remove the breakpoint.
         bpClient.remove(function (aResponse) {
           gThreadClient.resume(function () {
-            gClient.close(gCallback);
+            gClient.close().then(gCallback);
           });
         });
       });
 
       gThreadClient.resume();
     });
   });
 
--- a/devtools/server/tests/unit/test_breakpoint-04.js
+++ b/devtools/server/tests/unit/test_breakpoint-04.js
@@ -51,17 +51,17 @@ function test_child_breakpoint()
         do_check_eq(aPacket.why.actors[0], bpClient.actor);
         // Check that the breakpoint worked.
         do_check_eq(gDebuggee.a, 1);
         do_check_eq(gDebuggee.b, undefined);
 
         // Remove the breakpoint.
         bpClient.remove(function (aResponse) {
           gThreadClient.resume(function () {
-            gClient.close(gCallback);
+            gClient.close().then(gCallback);
           });
         });
       });
 
       // Continue until the breakpoint is hit.
       gThreadClient.resume();
     });
 
--- a/devtools/server/tests/unit/test_breakpoint-05.js
+++ b/devtools/server/tests/unit/test_breakpoint-05.js
@@ -53,17 +53,17 @@ function test_child_skip_breakpoint()
         do_check_eq(aPacket.why.actors[0], bpClient.actor);
         // Check that the breakpoint worked.
         do_check_eq(gDebuggee.a, 1);
         do_check_eq(gDebuggee.b, undefined);
 
         // Remove the breakpoint.
         bpClient.remove(function (aResponse) {
           gThreadClient.resume(function () {
-            gClient.close(gCallback);
+            gClient.close().then(gCallback);
           });
         });
       });
 
       // Continue until the breakpoint is hit.
       gThreadClient.resume();
     });
   });
--- a/devtools/server/tests/unit/test_breakpoint-06.js
+++ b/devtools/server/tests/unit/test_breakpoint-06.js
@@ -53,17 +53,17 @@ function test_nested_breakpoint()
         do_check_eq(aPacket.why.actors[0], bpClient.actor);
         // Check that the breakpoint worked.
         do_check_eq(gDebuggee.a, 1);
         do_check_eq(gDebuggee.b, undefined);
 
         // Remove the breakpoint.
         bpClient.remove(function (aResponse) {
           gThreadClient.resume(function () {
-            gClient.close(gCallback);
+            gClient.close().then(gCallback);
           });
         });
       });
 
       // Continue until the breakpoint is hit.
       gThreadClient.resume();
     });
 
--- a/devtools/server/tests/unit/test_breakpoint-07.js
+++ b/devtools/server/tests/unit/test_breakpoint-07.js
@@ -53,17 +53,17 @@ function test_second_child_skip_breakpoi
         do_check_eq(aPacket.why.actors[0], bpClient.actor);
         // Check that the breakpoint worked.
         do_check_eq(gDebuggee.a, 1);
         do_check_eq(gDebuggee.b, undefined);
 
         // Remove the breakpoint.
         bpClient.remove(function (aResponse) {
           gThreadClient.resume(function () {
-            gClient.close(gCallback);
+            gClient.close().then(gCallback);
           });
         });
       });
 
       // Continue until the breakpoint is hit.
       gThreadClient.resume();
     });
   });
--- a/devtools/server/tests/unit/test_breakpoint-08.js
+++ b/devtools/server/tests/unit/test_breakpoint-08.js
@@ -61,17 +61,17 @@ function test_child_skip_breakpoint()
           do_check_eq(aPacket.why.actors[0], bpClient.actor);
           // Check that the breakpoint worked.
           do_check_eq(gDebuggee.a, 1);
           do_check_eq(gDebuggee.b, undefined);
 
           // Remove the breakpoint.
           bpClient.remove(function (aResponse) {
             gThreadClient.resume(function () {
-              gClient.close(gCallback);
+              gClient.close().then(gCallback);
             });
           });
         });
 
         // Continue until the breakpoint is hit.
         gThreadClient.resume();
       });
     }
--- a/devtools/server/tests/unit/test_breakpoint-09.js
+++ b/devtools/server/tests/unit/test_breakpoint-09.js
@@ -79,10 +79,10 @@ function test_remove_breakpoint()
                    "  foo(true);\n" +         // line0 + 5
                    "}\n" +                    // line0 + 6
                    "debugger;\n" +            // line1 + 7
                    "foo();\n",                // line1 + 8
                    gDebuggee);
   if (!done) {
     do_check_true(false);
   }
-  gClient.close(gCallback);
+  gClient.close().then(gCallback);
 }
--- a/devtools/server/tests/unit/test_breakpoint-10.js
+++ b/devtools/server/tests/unit/test_breakpoint-10.js
@@ -57,17 +57,17 @@ function test_child_breakpoint()
           do_check_eq(aPacket.why.type, "breakpoint");
           do_check_eq(aPacket.why.actors[0], bpClient.actor);
           // Check that the breakpoint worked.
           do_check_eq(gDebuggee.i, 1);
 
           // Remove the breakpoint.
           bpClient.remove(function (aResponse) {
             gThreadClient.resume(function () {
-              gClient.close(gCallback);
+              gClient.close().then(gCallback);
             });
           });
         });
 
         // Continue until the breakpoint is hit again.
         gThreadClient.resume();
 
       });
--- a/devtools/server/tests/unit/test_breakpoint-11.js
+++ b/devtools/server/tests/unit/test_breakpoint-11.js
@@ -58,17 +58,17 @@ function test_child_breakpoint()
           do_check_eq(aPacket.why.actors[0], bpClient.actor);
           // Check that the breakpoint worked.
           do_check_eq(gDebuggee.a.b, 1);
           do_check_eq(gDebuggee.res, undefined);
 
           // Remove the breakpoint.
           bpClient.remove(function (aResponse) {
             gThreadClient.resume(function () {
-              gClient.close(gCallback);
+              gClient.close().then(gCallback);
             });
           });
         });
 
         // Continue until the breakpoint is hit again.
         gThreadClient.resume();
 
       });
--- a/devtools/server/tests/unit/test_breakpoint-12.js
+++ b/devtools/server/tests/unit/test_breakpoint-12.js
@@ -96,17 +96,17 @@ function set_breakpoints(source, locatio
 
       gThreadClient.addOneTimeListener("paused", function (aEvent, aPacket) {
         // We don't expect any more pauses after the breakpoint was hit once.
         do_check_true(false);
       });
       gThreadClient.resume(function () {
         // Give any remaining breakpoints a chance to trigger.
         do_timeout(1000, function () {
-          gClient.close(gCallback);
+          gClient.close().then(gCallback);
         });
       });
 
     });
     // Continue until the breakpoint is hit.
     gThreadClient.resume();
   });
 
--- a/devtools/server/tests/unit/test_breakpoint-13.js
+++ b/devtools/server/tests/unit/test_breakpoint-13.js
@@ -94,17 +94,17 @@ function test_simple_breakpoint()
         let packet = yield waiter;
         callback(packet);
       }
 
       // Remove the breakpoint and finish.
       let waiter = waitForPause(gThreadClient);
       gThreadClient.stepIn();
       yield waiter;
-      bpClient.remove(() => gThreadClient.resume(() => gClient.close(gCallback)));
+      bpClient.remove(() => gThreadClient.resume(() => gClient.close().then(gCallback)));
     }));
   });
 
   Cu.evalInSandbox("var line0 = Error().lineNumber;\n" +
                    "function foo() {\n" + // line0 + 1
                    "  this.a = 1;\n" +    // line0 + 2 <-- Breakpoint is set here.
                    "}\n" +                // line0 + 3
                    "debugger;\n" +        // line0 + 4
--- a/devtools/server/tests/unit/test_breakpoint-14.js
+++ b/devtools/server/tests/unit/test_breakpoint-14.js
@@ -92,17 +92,17 @@ function test_simple_breakpoint()
         let packet = yield waiter;
         callback(packet);
       }
 
       // Remove the breakpoint and finish.
       let waiter = waitForPause(gThreadClient);
       gThreadClient.stepOver();
       yield waiter;
-      bpClient.remove(() => gThreadClient.resume(() => gClient.close(gCallback)));
+      bpClient.remove(() => gThreadClient.resume(() => gClient.close().then(gCallback)));
     }));
   });
 
   Cu.evalInSandbox("var line0 = Error().lineNumber;\n" +
                    "function foo() {\n" + // line0 + 1
                    "  this.a = 1;\n" +    // line0 + 2 <-- Breakpoint is set here.
                    "}\n" +                // line0 + 3
                    "debugger;\n" +        // line0 + 4
--- a/devtools/server/tests/unit/test_breakpoint-16.js
+++ b/devtools/server/tests/unit/test_breakpoint-16.js
@@ -56,17 +56,17 @@ function test_column_breakpoint()
 
         do_check_eq(gDebuggee.acc, timesBreakpointHit);
         do_check_eq(aPacket.frame.environment.bindings.variables.i.value,
                     timesBreakpointHit);
 
         if (++timesBreakpointHit === 3) {
           gThreadClient.removeListener("paused", onPaused);
           bpClient.remove(function (aResponse) {
-            gThreadClient.resume(() => gClient.close(gCallback));
+            gThreadClient.resume(() => gClient.close().then(gCallback));
           });
         } else {
           gThreadClient.resume();
         }
       });
 
       // Continue until the breakpoint is hit.
       gThreadClient.resume();
--- a/devtools/server/tests/unit/test_breakpoint-17.js
+++ b/devtools/server/tests/unit/test_breakpoint-17.js
@@ -103,17 +103,17 @@ function test_remove_one(aFirst, aSecond
         return;
       }
 
       if (why.type == "debuggerStatement") {
         gClient.removeListener("paused", _onPaused);
         do_check_true(hitSecond,
                       "We should still hit `second`, but not `first`.");
 
-        gClient.close(gCallback);
+        gClient.close().then(gCallback);
         return;
       }
 
       do_check_true(false, "Should never get here");
     });
 
     gThreadClient.resume(() => gDebuggee.foo());
   });
--- a/devtools/server/tests/unit/test_breakpoint-18.js
+++ b/devtools/server/tests/unit/test_breakpoint-18.js
@@ -73,10 +73,10 @@ function testBPHit(event, { why }) {
 }
 
 function testDbgStatement(event, { why }) {
   // Should continue to the debugger statement.
   do_check_eq(why.type, "debuggerStatement");
   // Not break on another offset from the same line (that isn't an entry point
   // to the line)
   do_check_neq(why.type, "breakpoint");
-  gClient.close(gCallback);
+  gClient.close().then(gCallback);
 }
--- a/devtools/server/tests/unit/test_client_close.js
+++ b/devtools/server/tests/unit/test_client_close.js
@@ -24,16 +24,16 @@ function test_close(aTransport)
 {
   // Check that, if we fake a transport shutdown
   // (like if a device is unplugged)
   // the client is automatically closed,
   // and we can still call client.close.
   let onClosed = function () {
     gClient.removeListener("closed", onClosed);
     ok(true, "Client emitted 'closed' event");
-    gClient.close(function () {
+    gClient.close().then(function () {
       ok(true, "client.close() successfully called its callback");
       do_test_finished();
     });
   };
   gClient.addListener("closed", onClosed);
   aTransport.close();
 }
--- a/devtools/server/tests/unit/test_client_request.js
+++ b/devtools/server/tests/unit/test_client_request.js
@@ -155,17 +155,17 @@ function test_close_client_while_sending
 
   let expectReply = promise.defer();
   gClient.expectReply("root", function (response) {
     do_check_eq(response.error, "connectionClosed");
     do_check_eq(response.message, "server side packet can't be received as the connection just closed.");
     expectReply.resolve();
   });
 
-  gClient.close(() => {
+  gClient.close().then(() => {
     activeRequest.then(() => {
       ok(false, "First request unexpectedly succeed while closing the connection");
     }, response => {
       do_check_eq(response.error, "connectionClosed");
       do_check_eq(response.message, "'hello' active request packet to '" + gActorId + "' can't be sent as the connection just closed.");
     })
     .then(() => pendingRequest)
     .then(() => {
--- a/devtools/server/tests/unit/test_memory_footprint.js
+++ b/devtools/server/tests/unit/test_memory_footprint.js
@@ -46,10 +46,10 @@ function connect_client() {
 
 function list_tabs() {
   gClient.listTabs(function onListTabs(aResponse) {
     check_footprint("DebuggerClient.listTabs()", 3800);
   });
 }
 
 function close_client() {
-  gClient.close(run_next_test);
+  gClient.close().then(run_next_test);
 }
--- a/devtools/server/tests/unit/test_objectgrips-01.js
+++ b/devtools/server/tests/unit/test_objectgrips-01.js
@@ -42,17 +42,17 @@ function test_object_grip()
     let objClient = gThreadClient.pauseGrip(args[0]);
     objClient.getOwnPropertyNames(function (aResponse) {
       do_check_eq(aResponse.ownPropertyNames.length, 3);
       do_check_eq(aResponse.ownPropertyNames[0], "a");
       do_check_eq(aResponse.ownPropertyNames[1], "b");
       do_check_eq(aResponse.ownPropertyNames[2], "c");
 
       gThreadClient.resume(function () {
-        gClient.close(gCallback);
+        gClient.close().then(gCallback);
       });
     });
 
   });
 
   gDebuggee.eval("stopMe({ a: 1, b: true, c: 'foo' })");
 }
 
--- a/devtools/server/tests/unit/test_objectgrips-02.js
+++ b/devtools/server/tests/unit/test_objectgrips-02.js
@@ -45,17 +45,17 @@ function test_object_grip()
 
       let protoClient = gThreadClient.pauseGrip(aResponse.prototype);
       protoClient.getOwnPropertyNames(function (aResponse) {
         do_check_eq(aResponse.ownPropertyNames.length, 2);
         do_check_eq(aResponse.ownPropertyNames[0], "b");
         do_check_eq(aResponse.ownPropertyNames[1], "c");
 
         gThreadClient.resume(function () {
-          gClient.close(gCallback);
+          gClient.close().then(gCallback);
         });
       });
     });
 
   });
 
   gDebuggee.eval(function Constr() {
     this.a = 1;
--- a/devtools/server/tests/unit/test_objectgrips-03.js
+++ b/devtools/server/tests/unit/test_objectgrips-03.js
@@ -55,17 +55,17 @@ function test_object_grip()
         objClient.getProperty("a", function (aResponse) {
           do_check_eq(aResponse.descriptor.configurable, true);
           do_check_eq(aResponse.descriptor.enumerable, true);
           do_check_eq(aResponse.descriptor.get.type, "object");
           do_check_eq(aResponse.descriptor.get.class, "Function");
           do_check_eq(aResponse.descriptor.set.type, "undefined");
 
           gThreadClient.resume(function () {
-            gClient.close(gCallback);
+            gClient.close().then(gCallback);
           });
         });
       });
     });
 
   });
 
   gDebuggee.eval("stopMe({ x: 10, y: 'kaiju', get a() { return 42; } })");
--- a/devtools/server/tests/unit/test_objectgrips-04.js
+++ b/devtools/server/tests/unit/test_objectgrips-04.js
@@ -59,17 +59,17 @@ function test_object_grip()
 
       do_check_true(aResponse.prototype != undefined);
 
       let protoClient = gThreadClient.pauseGrip(aResponse.prototype);
       protoClient.getOwnPropertyNames(function (aResponse) {
         do_check_true(aResponse.ownPropertyNames.toString != undefined);
 
         gThreadClient.resume(function () {
-          gClient.close(gCallback);
+          gClient.close().then(gCallback);
         });
       });
     });
 
   });
 
   gDebuggee.eval("stopMe({ x: 10, y: 'kaiju', get a() { return 42; } })");
 }
--- a/devtools/server/tests/unit/test_objectgrips-05.js
+++ b/devtools/server/tests/unit/test_objectgrips-05.js
@@ -48,17 +48,17 @@ function test_object_grip()
 
     let obj2 = aPacket.frame.arguments[1];
     do_check_false(obj2.frozen);
 
     let obj2Client = gThreadClient.pauseGrip(obj2);
     do_check_false(obj2Client.isFrozen);
 
     gThreadClient.resume(_ => {
-      gClient.close(gCallback);
+      gClient.close().then(gCallback);
     });
   });
 
   gDebuggee.eval("(" + function () {
     let obj1 = {};
     Object.freeze(obj1);
     stopMe(obj1, {});
   } + "())");
--- a/devtools/server/tests/unit/test_objectgrips-06.js
+++ b/devtools/server/tests/unit/test_objectgrips-06.js
@@ -48,17 +48,17 @@ function test_object_grip()
 
     let obj2 = aPacket.frame.arguments[1];
     do_check_false(obj2.sealed);
 
     let obj2Client = gThreadClient.pauseGrip(obj2);
     do_check_false(obj2Client.isSealed);
 
     gThreadClient.resume(_ => {
-      gClient.close(gCallback);
+      gClient.close().then(gCallback);
     });
   });
 
   gDebuggee.eval("(" + function () {
     let obj1 = {};
     Object.seal(obj1);
     stopMe(obj1, {});
   } + "())");
--- a/devtools/server/tests/unit/test_objectgrips-07.js
+++ b/devtools/server/tests/unit/test_objectgrips-07.js
@@ -52,17 +52,17 @@ function test_object_grip()
 
     do_check_false(ne.extensible);
     do_check_false(neClient.isExtensible);
 
     do_check_true(e.extensible);
     do_check_true(eClient.isExtensible);
 
     gThreadClient.resume(_ => {
-      gClient.close(gCallback);
+      gClient.close().then(gCallback);
     });
   });
 
   gDebuggee.eval("(" + function () {
     let f = {};
     Object.freeze(f);
     let s = {};
     Object.seal(s);
--- a/devtools/server/tests/unit/test_objectgrips-08.js
+++ b/devtools/server/tests/unit/test_objectgrips-08.js
@@ -57,16 +57,16 @@ function test_object_grip()
       do_check_eq(aResponse.ownProperties.c.value.type, "NaN");
 
       do_check_eq(aResponse.ownProperties.d.configurable, true);
       do_check_eq(aResponse.ownProperties.d.enumerable, true);
       do_check_eq(aResponse.ownProperties.d.writable, true);
       do_check_eq(aResponse.ownProperties.d.value.type, "-0");
 
       gThreadClient.resume(function () {
-        gClient.close(gCallback);
+        gClient.close().then(gCallback);
       });
     });
   });
 
   gDebuggee.eval("stopMe({ a: Infinity, b: -Infinity, c: NaN, d: -0 })");
 }
 
--- a/devtools/server/tests/unit/test_objectgrips-09.js
+++ b/devtools/server/tests/unit/test_objectgrips-09.js
@@ -58,17 +58,17 @@ function test_object_grip()
       do_check_eq(obj2.ownProperties.z.enumerable, true);
       do_check_eq(obj2.ownProperties.z.writable, true);
       do_check_eq(obj2.ownProperties.z.value, 123);
 
       do_check_true(obj1.prototype != undefined);
       do_check_true(obj2.prototype != undefined);
 
       gThreadClient.resume(function () {
-        gClient.close(gCallback);
+        gClient.close().then(gCallback);
       });
     });
 
   });
 
   gDebuggee.eval("stopMe({ x: 10, y: 'kaiju'}, { z: 123 })");
 }
 
--- a/devtools/server/tests/unit/test_profiler_activation-02.js
+++ b/devtools/server/tests/unit/test_profiler_activation-02.js
@@ -16,17 +16,17 @@ function run_test()
   // Ensure the profiler is already running when the test starts.
   Profiler.StartProfiler(1000000, 1, ["js"], 1);
 
   DevToolsUtils.waitForTime(WAIT_TIME).then(() => {
 
     get_chrome_actors((client, form) => {
       let actor = form.profilerActor;
       test_start_time(client, actor, () => {
-        client.close(do_test_finished);
+        client.close().then(do_test_finished);
       });
     });
   });
 
   do_test_pending();
 }
 
 function test_start_time(client, actor, callback) {
--- a/devtools/server/tests/unit/test_profiler_bufferstatus.js
+++ b/devtools/server/tests/unit/test_profiler_bufferstatus.js
@@ -20,17 +20,17 @@ function run_test()
 
   get_chrome_actors((client, form) => {
     let actor = form.profilerActor;
     check_empty_buffer(client, actor, () => {
       activate_profiler(client, actor, startTime => {
         wait_for_samples(client, actor, () => {
           check_buffer(client, actor, () => {
             deactivate_profiler(client, actor, () => {
-              client.close(do_test_finished);
+              client.close().then(do_test_finished);
             });
           });
         });
       });
     });
   });
 
   do_test_pending();
--- a/devtools/server/tests/unit/test_profiler_data.js
+++ b/devtools/server/tests/unit/test_profiler_data.js
@@ -14,17 +14,17 @@ const MAX_WAIT_TIME = 20000; // ms
 
 function run_test()
 {
   get_chrome_actors((client, form) => {
     let actor = form.profilerActor;
     activate_profiler(client, actor, startTime => {
       test_data(client, actor, startTime, () => {
         deactivate_profiler(client, actor, () => {
-          client.close(do_test_finished);
+          client.close().then(do_test_finished);
         });
       });
     });
   });
 
   do_test_pending();
 }
 
--- a/devtools/server/tests/unit/test_profiler_getbufferinfo.js
+++ b/devtools/server/tests/unit/test_profiler_getbufferinfo.js
@@ -20,17 +20,17 @@ function run_test()
 
   get_chrome_actors((client, form) => {
     let actor = form.profilerActor;
     check_empty_buffer(client, actor, () => {
       activate_profiler(client, actor, startTime => {
         wait_for_samples(client, actor, () => {
           check_buffer(client, actor, () => {
             deactivate_profiler(client, actor, () => {
-              client.close(do_test_finished);
+              client.close().then(do_test_finished);
             });
           });
         });
       });
     });
   });
 
   do_test_pending();
--- a/devtools/server/tests/unit/test_profiler_getfeatures.js
+++ b/devtools/server/tests/unit/test_profiler_getfeatures.js
@@ -9,17 +9,17 @@
 
 const Profiler = Cc["@mozilla.org/tools/profiler;1"].getService(Ci.nsIProfiler);
 
 function run_test()
 {
   get_chrome_actors((client, form) => {
     let actor = form.profilerActor;
     test_getfeatures(client, actor, () => {
-      client.close(() => {
+      client.close().then(() => {
         do_test_finished();
       });
     });
   });
 
   do_test_pending();
 }
 
--- a/devtools/server/tests/unit/test_profiler_getsharedlibraryinformation.js
+++ b/devtools/server/tests/unit/test_profiler_getsharedlibraryinformation.js
@@ -9,17 +9,17 @@
 
 const Profiler = Cc["@mozilla.org/tools/profiler;1"].getService(Ci.nsIProfiler);
 
 function run_test()
 {
   get_chrome_actors((client, form) => {
     let actor = form.profilerActor;
     test_getsharedlibraryinformation(client, actor, () => {
-      client.close(() => {
+      client.close().then(() => {
         do_test_finished();
       });
     });
   });
 
   do_test_pending();
 }
 
--- a/devtools/server/tests/unit/test_protocol_async.js
+++ b/devtools/server/tests/unit/test_protocol_async.js
@@ -170,15 +170,15 @@ function run_test()
     calls.push(rootClient.simpleReturn().then(ret => {
       return deferAfterRejection.promise.then(function () {
         do_check_eq(sequence, 7); // Check right return order
         do_check_eq(ret, sequence++); // Check request handling order
       });
     }));
 
     promise.all(calls).then(() => {
-      client.close(() => {
+      client.close().then(() => {
         do_test_finished();
       });
     });
   });
   do_test_pending();
 }
--- a/devtools/server/tests/unit/test_protocol_children.js
+++ b/devtools/server/tests/unit/test_protocol_children.js
@@ -543,17 +543,17 @@ function run_test()
       };
       return rootFront.getChildren2(f());
     }).then(ret => {
       do_check_eq(ret.length, 2);
       do_check_true(ret[0] === childFront);
       do_check_true(ret[1] !== childFront);
       do_check_true(ret[1] instanceof ChildFront);
     }).then(() => {
-      client.close(() => {
+      client.close().then(() => {
         do_test_finished();
       });
     }).then(null, err => {
       do_report_unexpected_exception(err, "Failure executing test");
     });
   });
   do_test_pending();
 }
--- a/devtools/server/tests/unit/test_protocol_longstring.js
+++ b/devtools/server/tests/unit/test_protocol_longstring.js
@@ -202,17 +202,17 @@ function run_test()
       do_check_eq(value, LONG_STR);
     }).then(() => {
       return strfront.release();
     }).then(() => {
       trace.expectSend({"type":"release", "to":"<actorid>"});
       trace.expectReceive({"from":"<actorid>"});
       expectRootChildren(0);
     }).then(() => {
-      client.close(() => {
+      client.close().then(() => {
         do_test_finished();
       });
     }).then(null, err => {
       do_report_unexpected_exception(err, "Failure executing test");
     });
   });
   do_test_pending();
 }
--- a/devtools/server/tests/unit/test_protocol_simple.js
+++ b/devtools/server/tests/unit/test_protocol_simple.js
@@ -303,17 +303,17 @@ function run_test()
 
         do_check_true(res.zero === 0);
         do_check_true(res.farce === false);
         deferred.resolve();
       });
       rootClient.emitFalsyOptions();
       return deferred.promise;
     }).then(() => {
-      client.close(() => {
+      client.close().then(() => {
         do_test_finished();
       });
     }).then(null, err => {
       do_report_unexpected_exception(err, "Failure executing test");
     });
   });
   do_test_pending();
 }
--- a/devtools/server/tests/unit/test_protocol_stack.js
+++ b/devtools/server/tests/unit/test_protocol_stack.js
@@ -83,16 +83,16 @@ function run_test() {
           return;
         }
         stack = stack.asyncCaller || stack.caller;
       }
       ok(false, "Incomplete stack");
     }, () => {
       ok(false, "Request failed unexpectedly");
     }).then(() => {
-      client.close(() => {
+      client.close().then(() => {
         do_test_finished();
       });
     });
   });
 
   do_test_pending();
 }
--- a/devtools/server/tests/unit/test_reattach-thread.js
+++ b/devtools/server/tests/unit/test_reattach-thread.js
@@ -49,10 +49,10 @@ function test_reattach()
     do_check_eq(aThreadClient.state, "paused");
     do_check_eq(gTabClient.thread, aThreadClient);
     aThreadClient.resume(cleanup);
   });
 }
 
 function cleanup()
 {
-  gClient.close(do_test_finished);
+  gClient.close().then(do_test_finished);
 }
--- a/devtools/server/tests/unit/test_registerClient.js
+++ b/devtools/server/tests/unit/test_registerClient.js
@@ -81,15 +81,15 @@ function test_client_events()
     do_check_eq(type, "foo");
     do_check_eq(data.hello, "world");
     run_next_test();
   });
   gTestClient.start();
 }
 
 function close_client() {
-  gClient.close(() => {
+  gClient.close().then(() => {
     // Check that client.detach method is call on client destruction
     do_check_true(gTestClient.detached);
     run_next_test();
   });
 }
 
--- a/devtools/server/tests/unit/test_register_actor.js
+++ b/devtools/server/tests/unit/test_register_actor.js
@@ -91,17 +91,17 @@ function test_lazy_api() {
   function onRequest(aResponse) {
     do_check_eq(aResponse, "world");
 
     // Finally, the actor is loaded on the first request being made to it
     do_check_true(isActorLoaded);
     do_check_true(isActorInstanciated);
 
     Services.obs.removeObserver(onActorEvent, "actor", false);
-    client.close(() => run_next_test());
+    client.close().then(() => run_next_test());
   }
 }
 
 function cleanup() {
   DebuggerServer.destroy();
 
   // Check that all actors are unregistered on server destruction
   check_actors(false);
--- a/devtools/server/tests/unit/test_requestTypes.js
+++ b/devtools/server/tests/unit/test_requestTypes.js
@@ -11,17 +11,17 @@ function test_requestTypes_request(aClie
     var expectedRequestTypes = Object.keys(RootActor.
                                            prototype.
                                            requestTypes);
 
     do_check_true(Array.isArray(aResponse.requestTypes));
     do_check_eq(JSON.stringify(aResponse.requestTypes),
                 JSON.stringify(expectedRequestTypes));
 
-    aClient.close(() => {
+    aClient.close().then(() => {
       do_test_finished();
     });
   });
 }
 
 function run_test()
 {
   DebuggerServer.init();
--- a/devtools/server/tests/unit/test_stepping-01.js
+++ b/devtools/server/tests/unit/test_stepping-01.js
@@ -59,17 +59,17 @@ function test_simple_stepping()
           // When leaving a stack frame the line number doesn't change.
           do_check_eq(aPacket.frame.where.line, gDebuggee.line0 + 3);
           do_check_eq(aPacket.why.type, "resumeLimit");
           // Check that stepping worked.
           do_check_eq(gDebuggee.a, 1);
           do_check_eq(gDebuggee.b, 2);
 
           gThreadClient.resume(function () {
-            gClient.close(gCallback);
+            gClient.close().then(gCallback);
           });
         });
         gThreadClient.stepOver();
       });
       gThreadClient.stepOver();
 
     });
     gThreadClient.stepOver();
--- a/devtools/server/tests/unit/test_stepping-02.js
+++ b/devtools/server/tests/unit/test_stepping-02.js
@@ -59,17 +59,17 @@ function test_simple_stepping()
           // When leaving a stack frame the line number doesn't change.
           do_check_eq(aPacket.frame.where.line, gDebuggee.line0 + 3);
           do_check_eq(aPacket.why.type, "resumeLimit");
           // Check that stepping worked.
           do_check_eq(gDebuggee.a, 1);
           do_check_eq(gDebuggee.b, 2);
 
           gThreadClient.resume(function () {
-            gClient.close(gCallback);
+            gClient.close().then(gCallback);
           });
         });
         gThreadClient.stepIn();
       });
       gThreadClient.stepIn();
 
     });
     gThreadClient.stepIn();
--- a/devtools/server/tests/unit/test_stepping-03.js
+++ b/devtools/server/tests/unit/test_stepping-03.js
@@ -40,17 +40,17 @@ function test_simple_stepping()
       do_check_eq(aPacket.type, "paused");
       do_check_eq(aPacket.frame.where.line, gDebuggee.line0 + 5);
       do_check_eq(aPacket.why.type, "resumeLimit");
       // Check that stepping worked.
       do_check_eq(gDebuggee.a, 1);
       do_check_eq(gDebuggee.b, 2);
 
       gThreadClient.resume(function () {
-        gClient.close(gCallback);
+        gClient.close().then(gCallback);
       });
     });
     gThreadClient.stepOut();
 
   });
 
   gDebuggee.eval("var line0 = Error().lineNumber;\n" +
                  "function f() {\n" + // line0 + 1
--- a/devtools/server/tests/unit/test_stepping-04.js
+++ b/devtools/server/tests/unit/test_stepping-04.js
@@ -49,17 +49,17 @@ function test_simple_stepping()
         do_check_eq(aPacket.type, "paused");
         do_check_eq(aPacket.frame.where.line, gDebuggee.line0 + 6);
         do_check_eq(aPacket.why.type, "resumeLimit");
         // Check that stepping worked.
         do_check_eq(gDebuggee.a, 1);
         do_check_eq(gDebuggee.b, undefined);
 
         gThreadClient.resume(function () {
-          gClient.close(gCallback);
+          gClient.close().then(gCallback);
         });
       });
       gThreadClient.stepOver();
 
     });
     gThreadClient.stepOver();
 
   });
--- a/devtools/server/tests/unit/test_stepping-05.js
+++ b/devtools/server/tests/unit/test_stepping-05.js
@@ -88,14 +88,14 @@ function test_next_pause()
 {
   gThreadClient.addOneTimeListener("paused", function (aEvent, aPacket) {
     // Check the return value.
     do_check_eq(aPacket.type, "paused");
     // Before fixing bug 785689, the type was resumeLimit.
     do_check_eq(aPacket.why.type, "debuggerStatement");
 
     gThreadClient.resume(function () {
-      gClient.close(gCallback);
+      gClient.close().then(gCallback);
     });
   });
 
   gDebuggee.eval("debugger;");
 }
--- a/devtools/server/tests/unit/test_stepping-06.js
+++ b/devtools/server/tests/unit/test_stepping-06.js
@@ -59,17 +59,17 @@ function test_simple_stepping()
             gThreadClient.addOneTimeListener("paused", function (aEvent, aPacket) {
               // Check that the exception was thrown.
               do_check_eq(aPacket.type, "paused");
               do_check_eq(aPacket.frame.where.line, gDebuggee.line0 + 11);
               do_check_eq(aPacket.why.type, "resumeLimit");
               do_check_eq(aPacket.why.frameFinished.throw, "ah");
 
               gThreadClient.resume(function () {
-                gClient.close(gCallback);
+                gClient.close().then(gCallback);
               });
             });
             gThreadClient.stepOut();
           });
           gThreadClient.resume();
         });
         gThreadClient.stepOut();
       });
--- a/devtools/shared/client/main.js
+++ b/devtools/shared/client/main.js
@@ -349,44 +349,46 @@ DebuggerClient.prototype = {
     return deferred.promise;
   },
 
   /**
    * Shut down communication with the debugging server.
    *
    * @param aOnClosed function
    *        If specified, will be called when the debugging connection
-   *        has been closed.
+   *        has been closed. This parameter is deprecated - please use
+   *        the returned Promise.
+   * @return Promise
+   *         Resolves after the underlying transport is closed.
    */
   close: function (aOnClosed) {
+    let deferred = promise.defer();
+    if (aOnClosed) {
+      deferred.promise.then(aOnClosed);
+    }
+
     // Disable detach event notifications, because event handlers will be in a
     // cleared scope by the time they run.
     this._eventsEnabled = false;
 
     let cleanup = () => {
       this._transport.close();
       this._transport = null;
     };
 
     // If the connection is already closed,
     // there is no need to detach client
     // as we won't be able to send any message.
     if (this._closed) {
       cleanup();
-      if (aOnClosed) {
-        aOnClosed();
-      }
-      return;
+      deferred.resolve();
+      return deferred.promise;
     }
 
-    if (aOnClosed) {
-      this.addOneTimeListener("closed", function (aEvent) {
-        aOnClosed();
-      });
-    }
+    this.addOneTimeListener("closed", deferred.resolve);
 
     // Call each client's `detach` method by calling
     // lastly registered ones first to give a chance
     // to detach child clients first.
     let clients = [...this._clients.values()];
     this._clients.clear();
     const detachClients = () => {
       let client = clients.pop();
@@ -397,16 +399,18 @@ DebuggerClient.prototype = {
       }
       if (client.detach) {
         client.detach(detachClients);
         return;
       }
       detachClients();
     };
     detachClients();
+
+    return deferred.promise;
   },
 
   /*
    * This function exists only to preserve DebuggerClient's interface;
    * new code should say 'client.mainRoot.listTabs()'.
    */
   listTabs: function (aOnResponse) { return this.mainRoot.listTabs(aOnResponse); },
 
--- a/devtools/shared/transport/tests/unit/head_dbg.js
+++ b/devtools/shared/transport/tests/unit/head_dbg.js
@@ -180,17 +180,17 @@ function initTestDebuggerServer() {
     type: { global: true, tab: true }
   });
   DebuggerServer.registerModule("xpcshell-test/testactors");
   // Allow incoming connections.
   DebuggerServer.init();
 }
 
 function finishClient(aClient) {
-  aClient.close(function () {
+  aClient.close().then(function () {
     do_test_finished();
   });
 }
 
 /**
  * Takes a relative file path and returns the absolute file url for it.
  */
 function getFileUrl(aName, aAllowMissing = false) {
--- a/devtools/shared/webconsole/test/common.js
+++ b/devtools/shared/webconsole/test/common.js
@@ -140,17 +140,17 @@ function _attachConsole(aListeners, aCal
         });
       });
     }
   });
 }
 
 function closeDebugger(aState, aCallback)
 {
-  aState.dbgClient.close(aCallback);
+  aState.dbgClient.close().then(aCallback);
   aState.dbgClient = null;
   aState.client = null;
 }
 
 function checkConsoleAPICalls(consoleCalls, expectedConsoleCalls)
 {
   is(consoleCalls.length, expectedConsoleCalls.length,
     "received correct number of console calls");
--- a/layout/generic/nsGfxScrollFrame.cpp
+++ b/layout/generic/nsGfxScrollFrame.cpp
@@ -5887,28 +5887,36 @@ ScrollFrameHelper::SaveState() const
 {
   nsIScrollbarMediator* mediator = do_QueryFrame(GetScrolledFrame());
   if (mediator) {
     // child handles its own scroll state, so don't bother saving state here
     return nullptr;
   }
 
   // Don't store a scroll state if we never have been scrolled or restored
-  // a previous scroll state.
-  if (!mHasBeenScrolled && !mDidHistoryRestore) {
+  // a previous scroll state, and we're not in the middle of a smooth scroll.
+  bool isInSmoothScroll = IsProcessingAsyncScroll() || mLastSmoothScrollOrigin;
+  if (!mHasBeenScrolled && !mDidHistoryRestore && !isInSmoothScroll) {
     return nullptr;
   }
 
   nsPresState* state = new nsPresState();
   // Save mRestorePos instead of our actual current scroll position, if it's
   // valid and we haven't moved since the last update of mLastPos (same check
   // that ScrollToRestoredPosition uses). This ensures if a reframe occurs
   // while we're in the process of loading content to scroll to a restored
-  // position, we'll keep trying after the reframe.
+  // position, we'll keep trying after the reframe. Similarly, if we're in the
+  // middle of a smooth scroll, store the destination so that when we restore
+  // we'll jump straight to the end of the scroll animation, rather than
+  // effectively dropping it. Note that the mRestorePos will override the
+  // smooth scroll destination if both are present.
   nsPoint pt = GetLogicalScrollPosition();
+  if (isInSmoothScroll) {
+    pt = mDestination;
+  }
   if (mRestorePos.y != -1 && pt == mLastPos) {
     pt = mRestorePos;
   }
   state->SetScrollState(pt);
   if (mIsRoot) {
     // Only save resolution properties for root scroll frames
     nsIPresShell* shell = mOuter->PresContext()->PresShell();
     state->SetResolution(shell->GetResolution());
--- a/layout/generic/test/mochitest.ini
+++ b/layout/generic/test/mochitest.ini
@@ -141,8 +141,9 @@ support-files = selection_expanding_xbl.
 [test_selection_preventDefault.html]
 skip-if = buildapp == 'mulet'
 [test_selection_splitText-normalize.html]
 [test_selection_touchevents.html]
 [test_taintedfilters.html]
 support-files = file_taintedfilters_feDisplacementMap-tainted-1.svg file_taintedfilters_feDisplacementMap-tainted-2.svg file_taintedfilters_feDisplacementMap-tainted-3.svg file_taintedfilters_feDisplacementMap-tainted-ref.svg file_taintedfilters_feDisplacementMap-untainted-ref.svg file_taintedfilters_feDisplacementMap-untainted-1.svg file_taintedfilters_feDisplacementMap-untainted-2.svg file_taintedfilters_red-flood-for-feImage-cors.svg file_taintedfilters_red-flood-for-feImage-cors.svg^headers^ file_taintedfilters_red-flood-for-feImage.svg
 [test_scroll_position_restore.html]
 support-files = file_scroll_position_restore.html
+[test_scroll_animation_restore.html]
new file mode 100644
--- /dev/null
+++ b/layout/generic/test/test_scroll_animation_restore.html
@@ -0,0 +1,128 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1247074
+-->
+<head>
+  <meta charset="utf-8">
+  <title>Test for Bug 1247074</title>
+  <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+  <script type="text/javascript" src="/tests/SimpleTest/paint_listener.js"></script>
+  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+  <style>
+  .outer {
+      direction: ltr;
+      height: 400px;
+      width: 415px;
+      overflow: hidden;
+      position: relative;
+  }
+  .inner {
+      height: 100%;
+      outline: none;
+      overflow-x: hidden;
+      overflow-y: scroll;
+      position: relative;
+      scroll-behavior: smooth;
+  }
+  .outer.contentBefore::before {
+      top: 0;
+      content: '';
+      display: block;
+      height: 2px;
+      position: absolute;
+      width: 100%;
+      z-index: 99;
+  }
+  </style>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1247074">Mozilla Bug 1247074</a>
+<p id="display"></p>
+<div class="outer">
+  <div class="inner">
+   <ol>
+    <li>Some text</li>
+    <li>Some text</li>
+    <li>Some text</li>
+    <li>Some text</li>
+    <li>Some text</li>
+    <li>Some text</li>
+    <li>Some text</li>
+    <li>Some text</li>
+    <li>Some text</li>
+    <li>Some text</li>
+    <li>Some text</li>
+    <li>Some text</li>
+    <li>Some text</li>
+    <li>Some text</li>
+    <li>Some text</li>
+    <li>Some text</li>
+    <li>Some text</li>
+    <li>Some text</li>
+    <li>Some text</li>
+    <li>Some text</li>
+    <li>Some text</li>
+    <li>Some text</li>
+    <li>Some text</li>
+    <li>Some text</li>
+    <li>Some text</li>
+    <li>Some text</li>
+    <li>Some text</li>
+    <li>Some text</li>
+    <li>Some text</li>
+    <li>Some text</li>
+    <li>Some text</li>
+    <li>Some text</li>
+    <li>Some text</li>
+    <li>Some text</li>
+    <li>Some text</li>
+    <li>Some text</li>
+    <li>Some text</li>
+    <li>Some text</li>
+    <li>Some text</li>
+    <li>Some text</li>
+    <li>Some text</li>
+    <li>Some text</li>
+    <li>Some text</li>
+    <li>Some text</li>
+    <li>Some text</li>
+    <li>Some text</li>
+    <li>Some text</li>
+    <li>Some text</li>
+    <li>Some text</li>
+    <li>Some text</li>
+    <li>Some text</li>
+    <li>Some text</li>
+   </ol>
+  </div>
+</div>
+<script>
+SimpleTest.waitForExplicitFinish();
+window.onload = function() {
+  var elm = document.getElementsByClassName('inner')[0];
+
+  // Take control of the refresh driver
+  var utils = SpecialPowers.DOMWindowUtils;
+  utils.advanceTimeAndRefresh(0);
+
+  // Start a smooth scroll and advance a couple of frames so we're in the
+  // middle of the scroll animation
+  elm.scrollTop = 500;
+  utils.advanceTimeAndRefresh(16);
+  utils.advanceTimeAndRefresh(16);
+
+  // Trigger a frame reconstruction
+  elm.parentNode.classList.add('contentBefore');
+
+  // Reach a stable state and verify the scroll position is 500
+  utils.restoreNormalRefresh();
+  waitForAllPaintsFlushed(function() {
+    SimpleTest.is(elm.scrollTop, 500, "Scroll position ended up at 500");
+    SimpleTest.finish();
+  });
+}
+
+</script>
+</body>
+</html>
--- a/mobile/android/config/tooltool-manifests/android/releng.manifest
+++ b/mobile/android/config/tooltool-manifests/android/releng.manifest
@@ -82,10 +82,18 @@
 {
 "version": "rust stdlib repack 1.8.0 (db2939409 2016-04-11)",
 "size": 17874585,
 "visibility": "public",
 "digest": "bea72d352a70411240d95c7ab33e97d85be9cf0548807968dc721b14d816c70f4fd77a0ca1b43b851772ec06c0c1395b474ad342d506f8987fdbb070393c81f6",
 "algorithm": "sha512",
 "filename": "rust-std-lib.tar.bz2",
 "unpack": true
+},
+{
+"algorithm": "sha512",
+"visibility": "public",
+"filename": "dotgradle.tar.xz",
+"unpack": true,
+"digest": "9f082ccd71ad18991eb71fcad355c6990f50a72a09ab9b79696521485656083a72faf5a8d4714de9c4b901ee2319b6786a51964846bb7075061642a8505501c2",
+"size": 512
 }
 ]
--- a/services/sync/tests/unit/test_bookmark_order.js
+++ b/services/sync/tests/unit/test_bookmark_order.js
@@ -1,75 +1,64 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 _("Making sure after processing incoming bookmarks, they show up in the right order");
-Cu.import("resource://gre/modules/PlacesUtils.jsm", this);
+Cu.import("resource://gre/modules/PlacesUtils.jsm");
+Cu.import("resource://gre/modules/Task.jsm");
 Cu.import("resource://services-sync/engines/bookmarks.js");
 Cu.import("resource://services-sync/service.js");
 Cu.import("resource://services-sync/util.js");
 
-function getBookmarks(folderId) {
-  let bookmarks = [];
-
-  let pos = 0;
-  while (true) {
-    let itemId = PlacesUtils.bookmarks.getIdForItemAt(folderId, pos);
-    _("Got itemId", itemId, "under", folderId, "at", pos);
-    if (itemId == -1)
-      break;
+var check = Task.async(function* (expected, message) {
+  let root = yield PlacesUtils.promiseBookmarksTree();
 
-    let isOrphan = PlacesUtils.annotations.itemHasAnnotation(itemId,
-      "sync/parent");
-    switch (PlacesUtils.bookmarks.getItemType(itemId)) {
-      case PlacesUtils.bookmarks.TYPE_BOOKMARK:
-        let title = PlacesUtils.bookmarks.getItemTitle(itemId);
-        if (isOrphan) {
-          let requestedParent = PlacesUtils.annotations.getItemAnnotation(
-            itemId, "sync/parent");
-          bookmarks.push({ title, requestedParent });
-        } else {
-          bookmarks.push(title);
+  let bookmarks = (function mapTree(children) {
+    return children.map(child => {
+      let result = {
+        guid: child.guid,
+        index: child.index,
+      };
+      if (child.children) {
+        result.children = mapTree(child.children);
+      }
+      if (child.annos) {
+        let orphanAnno = child.annos.find(
+          anno => anno.name == "sync/parent");
+        if (orphanAnno) {
+          result.requestedParent = orphanAnno.value;
         }
-        break;
-      case PlacesUtils.bookmarks.TYPE_FOLDER:
-        let titles = getBookmarks(itemId);
-        if (isOrphan) {
-          let requestedParent = PlacesUtils.annotations.getItemAnnotation(
-            itemId, "sync/parent");
-          bookmarks.push({ titles, requestedParent });
-        } else {
-          bookmarks.push(titles);
-        }
-        break;
-      default:
-        _("Unsupported item type..");
-    }
-
-    pos++;
-  }
-
-  return bookmarks;
-}
-
-function check(expected) {
-  let bookmarks = getBookmarks(PlacesUtils.bookmarks.unfiledBookmarksFolder);
+      }
+      return result;
+    });
+  }(root.children));
 
   _("Checking if the bookmark structure is", JSON.stringify(expected));
   _("Got bookmarks:", JSON.stringify(bookmarks));
-  do_check_true(Utils.deepEquals(bookmarks, expected));
-}
+  deepEqual(bookmarks, expected);
+});
 
-function run_test() {
+add_task(function* test_bookmark_order() {
   let store = new BookmarksEngine(Service)._store;
   initTestLogging("Trace");
 
   _("Starting with a clean slate of no bookmarks");
   store.wipe();
-  check([]);
+  yield check([{
+    guid: PlacesUtils.bookmarks.menuGuid,
+    index: 0,
+  }, {
+    guid: PlacesUtils.bookmarks.toolbarGuid,
+    index: 1,
+  }, {
+    // Index 2 is the tags root. (Root indices depend on the order of the
+    // `CreateRoot` calls in `Database::CreateBookmarkRoots`).
+    guid: PlacesUtils.bookmarks.unfiledGuid,
+    index: 3,
+  }], "clean slate");
 
   function bookmark(name, parent) {
     let bookmark = new Bookmark("http://weave.server/my-bookmark");
     bookmark.id = name;
     bookmark.title = name;
     bookmark.bmkUri = "http://uri/";
     bookmark.parentid = parent || "unfiled";
     bookmark.tags = [];
@@ -89,73 +78,413 @@ function run_test() {
     store._childrenToOrder = {};
     store.applyIncoming(record);
     store._orderChildren();
     delete store._childrenToOrder;
   }
   let id10 = "10_aaaaaaaaa";
   _("basic add first bookmark");
   apply(bookmark(id10, ""));
-  check([id10]);
+  yield check([{
+    guid: PlacesUtils.bookmarks.menuGuid,
+    index: 0,
+  }, {
+    guid: PlacesUtils.bookmarks.toolbarGuid,
+    index: 1,
+  }, {
+    guid: PlacesUtils.bookmarks.unfiledGuid,
+    index: 3,
+    children: [{
+      guid: id10,
+      index: 0,
+    }],
+  }], "basic add first bookmark");
   let id20 = "20_aaaaaaaaa";
   _("basic append behind 10");
   apply(bookmark(id20, ""));
-  check([id10, id20]);
+  yield check([{
+    guid: PlacesUtils.bookmarks.menuGuid,
+    index: 0,
+  }, {
+    guid: PlacesUtils.bookmarks.toolbarGuid,
+    index: 1,
+  }, {
+    guid: PlacesUtils.bookmarks.unfiledGuid,
+    index: 3,
+    children: [{
+      guid: id10,
+      index: 0,
+    }, {
+      guid: id20,
+      index: 1,
+    }],
+  }], "basic append behind 10");
 
   let id31 = "31_aaaaaaaaa";
   let id30 = "f30_aaaaaaaa";
   _("basic create in folder");
   apply(bookmark(id31, id30));
   let f30 = folder(id30, "", [id31]);
   apply(f30);
-  check([id10, id20, [id31]]);
+  yield check([{
+    guid: PlacesUtils.bookmarks.menuGuid,
+    index: 0,
+  }, {
+    guid: PlacesUtils.bookmarks.toolbarGuid,
+    index: 1,
+  }, {
+    guid: PlacesUtils.bookmarks.unfiledGuid,
+    index: 3,
+    children: [{
+      guid: id10,
+      index: 0,
+    }, {
+      guid: id20,
+      index: 1,
+    }, {
+      guid: id30,
+      index: 2,
+      children: [{
+        guid: id31,
+        index: 0,
+      }],
+    }],
+  }], "basic create in folder");
 
   let id41 = "41_aaaaaaaaa";
   let id40 = "f40_aaaaaaaa";
   _("insert missing parent -> append to unfiled");
   apply(bookmark(id41, id40));
-  check([id10, id20, [id31], { title: id41, requestedParent: id40 }]);
+  yield check([{
+    guid: PlacesUtils.bookmarks.menuGuid,
+    index: 0,
+  }, {
+    guid: PlacesUtils.bookmarks.toolbarGuid,
+    index: 1,
+  }, {
+    guid: PlacesUtils.bookmarks.unfiledGuid,
+    index: 3,
+    children: [{
+      guid: id10,
+      index: 0,
+    }, {
+      guid: id20,
+      index: 1,
+    }, {
+      guid: id30,
+      index: 2,
+      children: [{
+        guid: id31,
+        index: 0,
+      }],
+    }, {
+      guid: id41,
+      index: 3,
+      requestedParent: id40,
+    }],
+  }], "insert missing parent -> append to unfiled");
 
   let id42 = "42_aaaaaaaaa";
 
   _("insert another missing parent -> append");
   apply(bookmark(id42, id40));
-  check([id10, id20, [id31], { title: id41, requestedParent: id40 },
-    { title: id42, requestedParent: id40 }]);
+  yield check([{
+    guid: PlacesUtils.bookmarks.menuGuid,
+    index: 0,
+  }, {
+    guid: PlacesUtils.bookmarks.toolbarGuid,
+    index: 1,
+  }, {
+    guid: PlacesUtils.bookmarks.unfiledGuid,
+    index: 3,
+    children: [{
+      guid: id10,
+      index: 0,
+    }, {
+      guid: id20,
+      index: 1,
+    }, {
+      guid: id30,
+      index: 2,
+      children: [{
+        guid: id31,
+        index: 0,
+      }],
+    }, {
+      guid: id41,
+      index: 3,
+      requestedParent: id40,
+    }, {
+      guid: id42,
+      index: 4,
+      requestedParent: id40,
+    }],
+  }], "insert another missing parent -> append");
 
   _("insert folder -> move children and followers");
   let f40 = folder(id40, "", [id41, id42]);
   apply(f40);
-  check([id10, id20, [id31], [id41, id42]]);
+  yield check([{
+    guid: PlacesUtils.bookmarks.menuGuid,
+    index: 0,
+  }, {
+    guid: PlacesUtils.bookmarks.toolbarGuid,
+    index: 1,
+  }, {
+    guid: PlacesUtils.bookmarks.unfiledGuid,
+    index: 3,
+    children: [{
+      guid: id10,
+      index: 0,
+    }, {
+      guid: id20,
+      index: 1,
+    }, {
+      guid: id30,
+      index: 2,
+      children: [{
+        guid: id31,
+        index: 0,
+      }],
+    }, {
+      guid: id40,
+      index: 3,
+      children: [{
+        guid: id41,
+        index: 0,
+      }, {
+        guid: id42,
+        index: 1,
+      }]
+    }],
+  }], "insert folder -> move children and followers");
 
   _("Moving 41 behind 42 -> update f40");
   f40.children = [id42, id41];
   apply(f40);
-  check([id10, id20, [id31], [id42, id41]]);
+  yield check([{
+    guid: PlacesUtils.bookmarks.menuGuid,
+    index: 0,
+  }, {
+    guid: PlacesUtils.bookmarks.toolbarGuid,
+    index: 1,
+  }, {
+    guid: PlacesUtils.bookmarks.unfiledGuid,
+    index: 3,
+    children: [{
+      guid: id10,
+      index: 0,
+    }, {
+      guid: id20,
+      index: 1,
+    }, {
+      guid: id30,
+      index: 2,
+      children: [{
+        guid: id31,
+        index: 0,
+      }],
+    }, {
+      guid: id40,
+      index: 3,
+      children: [{
+        guid: id42,
+        index: 0,
+      }, {
+        guid: id41,
+        index: 1,
+      }]
+    }],
+  }], "Moving 41 behind 42 -> update f40");
 
   _("Moving 10 back to front -> update 10, 20");
   f40.children = [id41, id42];
   apply(f40);
-  check([id10, id20, [id31], [id41, id42]]);
+  yield check([{
+    guid: PlacesUtils.bookmarks.menuGuid,
+    index: 0,
+  }, {
+    guid: PlacesUtils.bookmarks.toolbarGuid,
+    index: 1,
+  }, {
+    guid: PlacesUtils.bookmarks.unfiledGuid,
+    index: 3,
+    children: [{
+      guid: id10,
+      index: 0,
+    }, {
+      guid: id20,
+      index: 1,
+    }, {
+      guid: id30,
+      index: 2,
+      children: [{
+        guid: id31,
+        index: 0,
+      }],
+    }, {
+      guid: id40,
+      index: 3,
+      children: [{
+        guid: id41,
+        index: 0,
+      }, {
+        guid: id42,
+        index: 1,
+      }]
+    }],
+  }], "Moving 10 back to front -> update 10, 20");
 
   _("Moving 20 behind 42 in f40 -> update 50");
   apply(bookmark(id20, id40));
-  check([id10, [id31], [id41, id42, id20]]);
+  yield check([{
+    guid: PlacesUtils.bookmarks.menuGuid,
+    index: 0,
+  }, {
+    guid: PlacesUtils.bookmarks.toolbarGuid,
+    index: 1,
+  }, {
+    guid: PlacesUtils.bookmarks.unfiledGuid,
+    index: 3,
+    children: [{
+      guid: id10,
+      index: 0,
+    }, {
+      guid: id30,
+      index: 1,
+      children: [{
+        guid: id31,
+        index: 0,
+      }],
+    }, {
+      guid: id40,
+      index: 2,
+      children: [{
+        guid: id41,
+        index: 0,
+      }, {
+        guid: id42,
+        index: 1,
+      }, {
+        guid: id20,
+        index: 2,
+      }]
+    }],
+  }], "Moving 20 behind 42 in f40 -> update 50");
 
   _("Moving 10 in front of 31 in f30 -> update 10, f30");
   apply(bookmark(id10, id30));
   f30.children = [id10, id31];
   apply(f30);
-  check([[id10, id31], [id41, id42, id20]]);
+  yield check([{
+    guid: PlacesUtils.bookmarks.menuGuid,
+    index: 0,
+  }, {
+    guid: PlacesUtils.bookmarks.toolbarGuid,
+    index: 1,
+  }, {
+    guid: PlacesUtils.bookmarks.unfiledGuid,
+    index: 3,
+    children: [{
+      guid: id30,
+      index: 0,
+      children: [{
+        guid: id10,
+        index: 0,
+      }, {
+        guid: id31,
+        index: 1,
+      }],
+    }, {
+      guid: id40,
+      index: 1,
+      children: [{
+        guid: id41,
+        index: 0,
+      }, {
+        guid: id42,
+        index: 1,
+      }, {
+        guid: id20,
+        index: 2,
+      }]
+    }],
+  }], "Moving 10 in front of 31 in f30 -> update 10, f30");
 
   _("Moving 20 from f40 to f30 -> update 20, f30");
   apply(bookmark(id20, id30));
   f30.children = [id10, id20, id31];
   apply(f30);
-  check([[id10, id20, id31], [id41, id42]]);
+  yield check([{
+    guid: PlacesUtils.bookmarks.menuGuid,
+    index: 0,
+  }, {
+    guid: PlacesUtils.bookmarks.toolbarGuid,
+    index: 1,
+  }, {
+    guid: PlacesUtils.bookmarks.unfiledGuid,
+    index: 3,
+    children: [{
+      guid: id30,
+      index: 0,
+      children: [{
+        guid: id10,
+        index: 0,
+      }, {
+        guid: id20,
+        index: 1,
+      }, {
+        guid: id31,
+        index: 2,
+      }],
+    }, {
+      guid: id40,
+      index: 1,
+      children: [{
+        guid: id41,
+        index: 0,
+      }, {
+        guid: id42,
+        index: 1,
+      }]
+    }],
+  }], "Moving 20 from f40 to f30 -> update 20, f30");
 
   _("Move 20 back to front -> update 20, f30");
   apply(bookmark(id20, ""));
   f30.children = [id10, id31];
   apply(f30);
-  check([[id10, id31], [id41, id42], id20]);
+  yield check([{
+    guid: PlacesUtils.bookmarks.menuGuid,
+    index: 0,
+  }, {
+    guid: PlacesUtils.bookmarks.toolbarGuid,
+    index: 1,
+  }, {
+    guid: PlacesUtils.bookmarks.unfiledGuid,
+    index: 3,
+    children: [{
+      guid: id30,
+      index: 0,
+      children: [{
+        guid: id10,
+        index: 0,
+      }, {
+        guid: id31,
+        index: 1,
+      }],
+    }, {
+      guid: id40,
+      index: 1,
+      children: [{
+        guid: id41,
+        index: 0,
+      }, {
+        guid: id42,
+        index: 1,
+      }],
+    }, {
+      guid: id20,
+      index: 2,
+    }],
+  }], "Move 20 back to front -> update 20, f30");
 
-}
+});
--- a/taskcluster/ci/legacy/tasks/builds/android_api_15_gradle.yml
+++ b/taskcluster/ci/legacy/tasks/builds/android_api_15_gradle.yml
@@ -35,16 +35,17 @@ task:
       MOZHARNESS_CONFIG: >
           builds/releng_base_android_64_builds.py
           disable_signing.py
           platform_supports_post_upload_to_latest.py
       MOZHARNESS_ACTIONS: "get-secrets build multi-l10n update"
       MH_CUSTOM_BUILD_VARIANT_CFG: api-15-gradle
       MH_BRANCH: {{project}}
       MH_BUILD_POOL: taskcluster
+      GRADLE_USER_HOME: '/home/worker/workspace/build/src/dotgradle'
 
     command: ["/bin/bash", "bin/build.sh"]
 
   extra:
     treeherderEnv:
       - production
       - staging
     treeherder:
--- a/testing/docker/android-gradle-build/Dockerfile
+++ b/testing/docker/android-gradle-build/Dockerfile
@@ -1,18 +1,30 @@
-FROM          taskcluster/centos6-build-upd:0.1.3.20160222133000
+# TODO remove VOLUME below when the base image is updated next.
+FROM          taskcluster/centos6-build-upd:0.1.6.20160329195300
 MAINTAINER    Nick Alexander <nalexander@mozilla.com>
 
 # BEGIN ../desktop-build/Dockerfile
 
+# TODO remove when base image is updated
+VOLUME /home/worker/workspace
+VOLUME /home/worker/tooltool-cache
+
 # Add build scripts; these are the entry points from the taskcluster worker, and
 # operate on environment variables
 ADD             bin /home/worker/bin
 RUN             chmod +x /home/worker/bin/*
 
+# Add wrapper scripts for xvfb allowing tasks to easily retry starting up xvfb
+# %include testing/docker/recipes/xvfb.sh
+ADD topsrcdir/testing/docker/recipes/xvfb.sh /home/worker/scripts/xvfb.sh
+
+# Add configuration
+COPY            dot-config                    /home/worker/.config
+
 # Generate machine uuid file
 RUN dbus-uuidgen --ensure=/var/lib/dbus/machine-id
 
 # Stubbed out credentials; mozharness looks for this file an issues a WARNING
 # if it's not found, which causes the build to fail.  Note that this needs to
 # be in the parent of the workspace directory and in the directory where
 # mozharness is run (not its --work-dir).  See Bug 1169652.
 ADD           oauth.txt /home/worker/
@@ -34,21 +46,24 @@ WORKDIR /
 USER root
 
 # Update base.
 RUN yum upgrade -y
 
 # Install JDK and Sonatype Nexus.  Cribbed directly from
 # https://github.com/sonatype/docker-nexus/blob/fffd2c61b2368292040910c055cf690c8e76a272/oss/Dockerfile.
 
+# Install the screen package here to use with xvfb.
+# Move installation to base centos6-build image once Bug 1272629 is fixed
 RUN yum install -y \
   createrepo \
   curl \
   java-1.7.0-openjdk-devel \
   java-1.7.0-openjdk \
+  screen \
   sudo \
   tar \
   unzip \
   wget \
   zip \
   && yum clean all
 
 ENV NEXUS_VERSION 2.12.0-01
@@ -72,8 +87,11 @@ RUN tar zxf nexus-${NEXUS_VERSION}-bundl
 
 # Install tooltool directly from github.
 RUN mkdir /build
 ADD https://raw.githubusercontent.com/mozilla/build-tooltool/master/tooltool.py /build/tooltool.py
 RUN chmod +rx /build/tooltool.py
 
 # Back to the centos6-build workdir, matching desktop-build.
 WORKDIR /home/worker
+
+# Set a default command useful for debugging
+CMD ["/bin/bash", "--login"]
copy from testing/docker/desktop-build/dot-config/pip/pip.conf
copy to testing/docker/android-gradle-build/dot-config/pip/pip.conf
--- a/toolkit/components/places/Bookmarks.jsm
+++ b/toolkit/components/places/Bookmarks.jsm
@@ -1178,24 +1178,25 @@ function reorderChildren(parent, ordered
       // when no more existing GUIDs have been provided.
       let valuesTable = children.map((child, i) => `("${child.guid}", ${i})`)
                                 .join();
       yield db.execute(
         `WITH sorting(g, p) AS (
            VALUES ${valuesTable}
          )
          UPDATE moz_bookmarks SET position = (
-           SELECT CASE count(a.g) WHEN 0 THEN -position
-                                  ELSE count(a.g) - 1
+           SELECT CASE count(*) WHEN 0 THEN -position
+                                       ELSE count(*) - 1
                   END
            FROM sorting a
            JOIN sorting b ON b.p <= a.p
            WHERE a.g = guid
-             AND parent = :parentId
-        )`, { parentId: parent._id});
+         )
+         WHERE parent = :parentId
+        `, { parentId: parent._id});
 
       // Update position of items that could have been inserted in the meanwhile.
       // Since this can happen rarely and it's only done for schema coherence
       // resonds, we won't notify about these changes.
       yield db.executeCached(
         `CREATE TEMP TRIGGER moz_bookmarks_reorder_trigger
            AFTER UPDATE OF position ON moz_bookmarks
            WHEN NEW.position = -1
--- a/toolkit/components/places/PlacesSyncUtils.jsm
+++ b/toolkit/components/places/PlacesSyncUtils.jsm
@@ -96,75 +96,44 @@ const BookmarkSyncUtils = PlacesSyncUtil
 
     if (parentGuid == PlacesUtils.bookmarks.rootGuid) {
       // Reordering roots doesn't make sense, but Sync will do this on the
       // first sync.
       return Promise.resolve();
     }
     return PlacesUtils.withConnectionWrapper("BookmarkSyncUtils: order",
       Task.async(function* (db) {
-        let children;
-
-        yield db.executeTransaction(function* () {
-          children = yield fetchAllChildren(db, parentGuid);
-          if (!children.length) {
-            return;
-          }
-          for (let child of children) {
-            // Note the current index for notifying observers. This can
-            // be removed once we switch to `reorder`.
-            child.oldIndex = child.index;
-          }
-
-          // Reorder the list, ignoring missing children.
-          let delta = 0;
-          for (let i = 0; i < childGuids.length; ++i) {
-            let guid = childGuids[i];
-            let child = findChildByGuid(children, guid);
-            if (!child) {
-              delta++;
-              BookmarkSyncLog.trace(`order: Ignoring missing child ${guid}`);
-              continue;
-            }
-            let newIndex = i - delta;
-            updateChildIndex(children, child, newIndex);
-          }
-          children.sort((a, b) => a.index - b.index);
+        let children = yield fetchAllChildren(db, parentGuid);
+        if (!children.length) {
+          return;
+        }
+        for (let child of children) {
+          // Note the current index for notifying observers. This can
+          // be removed once we switch to `reorder`.
+          child.oldIndex = child.index;
+        }
 
-          // Update positions. We use a custom query instead of
-          // `PlacesUtils.bookmarks.reorder` because `reorder` introduces holes
-          // (bug 1293365). Once it's fixed, we can uncomment this code and
-          // remove the transaction, query, and observer notification code.
-
-          /*
-          let orderedChildrenGuids = children.map(({ guid }) => guid);
-          yield PlacesUtils.bookmarks.reorder(parentGuid, orderedChildrenGuids);
-          */
+        // Reorder the list, ignoring missing children.
+        let delta = 0;
+        for (let i = 0; i < childGuids.length; ++i) {
+          let guid = childGuids[i];
+          let child = findChildByGuid(children, guid);
+          if (!child) {
+            delta++;
+            BookmarkSyncLog.trace(`order: Ignoring missing child ${guid}`);
+            continue;
+          }
+          let newIndex = i - delta;
+          updateChildIndex(children, child, newIndex);
+        }
+        children.sort((a, b) => a.index - b.index);
 
-          yield db.executeCached(`WITH sorting(g, p) AS (
-            VALUES ${children.map(
-              (child, i) => `("${child.guid}", ${i})`
-            ).join()}
-          ) UPDATE moz_bookmarks SET position = (
-            SELECT p FROM sorting WHERE g = guid
-          ) WHERE parent = (
-            SELECT id FROM moz_bookmarks WHERE guid = :parentGuid
-          )`,
-          { parentGuid });
-        });
-
-        // Notify observers.
-        let observers = PlacesUtils.bookmarks.getObservers();
-        for (let child of children) {
-          notify(observers, "onItemMoved", [ child.id, child.parentId,
-                                             child.oldIndex, child.parentId,
-                                             child.index, child.type,
-                                             child.guid, parentGuid,
-                                             parentGuid, SOURCE_SYNC ]);
-        }
+        // Update positions.
+        let orderedChildrenGuids = children.map(({ guid }) => guid);
+        yield PlacesUtils.bookmarks.reorder(parentGuid, orderedChildrenGuids);
       })
     );
   }),
 
   /**
    * Removes an item from the database.
    */
   remove: Task.async(function* (guid) {
--- a/toolkit/components/places/tests/bookmarks/test_bookmarks_reorder.js
+++ b/toolkit/components/places/tests/bookmarks/test_bookmarks_reorder.js
@@ -37,32 +37,29 @@ add_task(function* invalid_input_throws(
 add_task(function* reorder_nonexistent_guid() {
   yield Assert.rejects(PlacesUtils.bookmarks.reorder("123456789012", [ "012345678901" ]),
                        /No folder found for the provided GUID/,
                        "Should throw for nonexisting guid");
 });
 
 add_task(function* reorder() {
   let bookmarks = [
-    { type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
-      url: "http://example1.com/",
+    { url: "http://example1.com/",
       parentGuid: PlacesUtils.bookmarks.unfiledGuid
     },
     { type: PlacesUtils.bookmarks.TYPE_FOLDER,
       parentGuid: PlacesUtils.bookmarks.unfiledGuid
     },
     { type: PlacesUtils.bookmarks.TYPE_SEPARATOR,
       parentGuid: PlacesUtils.bookmarks.unfiledGuid
     },
-    { type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
-      url: "http://example2.com/",
+    { url: "http://example2.com/",
       parentGuid: PlacesUtils.bookmarks.unfiledGuid
     },
-    { type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
-      url: "http://example3.com/",
+    { url: "http://example3.com/",
       parentGuid: PlacesUtils.bookmarks.unfiledGuid
     }
   ];
 
   let sorted = [];
   for (let bm of bookmarks) {
     sorted.push(yield PlacesUtils.bookmarks.insert(bm));
   }
@@ -104,11 +101,77 @@ add_task(function* reorder() {
   let rows = yield db.execute(
     `SELECT parent
      FROM moz_bookmarks
      GROUP BY parent
      HAVING (SUM(DISTINCT position + 1) - (count(*) * (count(*) + 1) / 2)) <> 0`);
   Assert.equal(rows.length, 0, "All the bookmarks should have consistent positions");
 });
 
-function run_test() {
-  run_next_test();
-}
+add_task(function* move_and_reorder() {
+  // Start clean.
+  yield PlacesUtils.bookmarks.eraseEverything();
+
+  let bm1 = yield PlacesUtils.bookmarks.insert({
+    url: "http://example1.com/",
+    parentGuid: PlacesUtils.bookmarks.unfiledGuid
+  });
+  let f1 = yield PlacesUtils.bookmarks.insert({
+    type: PlacesUtils.bookmarks.TYPE_FOLDER,
+    parentGuid: PlacesUtils.bookmarks.unfiledGuid
+  });
+  let bm2 = yield PlacesUtils.bookmarks.insert({
+    url: "http://example2.com/",
+    parentGuid: f1.guid
+  });
+  let f2 = yield PlacesUtils.bookmarks.insert({
+    type: PlacesUtils.bookmarks.TYPE_FOLDER,
+    parentGuid: PlacesUtils.bookmarks.unfiledGuid
+  });
+  let bm3 = yield PlacesUtils.bookmarks.insert({
+    url: "http://example3.com/",
+    parentGuid: f2.guid
+  });
+  let bm4 = yield PlacesUtils.bookmarks.insert({
+    url: "http://example4.com/",
+    parentGuid: f2.guid
+  });
+  let bm5 = yield PlacesUtils.bookmarks.insert({
+    url: "http://example5.com/",
+    parentGuid: f2.guid
+  });
+
+  // Invert f2 children.
+  // This is critical to reproduce the bug, cause it inverts the position
+  // compared to the natural insertion order.
+  yield PlacesUtils.bookmarks.reorder(f2.guid, [bm5.guid, bm4.guid, bm3.guid]);
+
+  bm1.parentGuid = f1.guid;
+  bm1.index = 0;
+  yield PlacesUtils.bookmarks.update(bm1);
+
+  bm1 = yield PlacesUtils.bookmarks.fetch(bm1.guid);
+  Assert.equal(bm1.index, 0);
+  bm2 = yield PlacesUtils.bookmarks.fetch(bm2.guid);
+  Assert.equal(bm2.index, 1);
+  bm3 = yield PlacesUtils.bookmarks.fetch(bm3.guid);
+  Assert.equal(bm3.index, 2);
+  bm4 = yield PlacesUtils.bookmarks.fetch(bm4.guid);
+  Assert.equal(bm4.index, 1);
+  bm5 = yield PlacesUtils.bookmarks.fetch(bm5.guid);
+  Assert.equal(bm5.index, 0);
+
+  // No-op reorder on f1 children.
+  // Nothing should change. Though, due to bug 1293365 this was causing children
+  // of other folders to get messed up.
+  yield PlacesUtils.bookmarks.reorder(f1.guid, [bm1.guid, bm2.guid]);
+
+  bm1 = yield PlacesUtils.bookmarks.fetch(bm1.guid);
+  Assert.equal(bm1.index, 0);
+  bm2 = yield PlacesUtils.bookmarks.fetch(bm2.guid);
+  Assert.equal(bm2.index, 1);
+  bm3 = yield PlacesUtils.bookmarks.fetch(bm3.guid);
+  Assert.equal(bm3.index, 2);
+  bm4 = yield PlacesUtils.bookmarks.fetch(bm4.guid);
+  Assert.equal(bm4.index, 1);
+  bm5 = yield PlacesUtils.bookmarks.fetch(bm5.guid);
+  Assert.equal(bm5.index, 0);
+});
--- a/tools/lint/eslint.lint
+++ b/tools/lint/eslint.lint
@@ -304,18 +304,23 @@ def lint(paths, binary=None, fix=None, s
                 '--ext', '[{}]'.format(','.join(EXTENSIONS)),
                 '--format', 'json',
                 ] + extra_args + paths
 
     # eslint requires that --fix be set before the --ext argument.
     if fix:
         cmd_args.insert(1, '--fix')
 
+    shell = False
+    if os.environ.get('MSYSTEM') in ('MINGW32', 'MINGW64'):
+        # The eslint binary needs to be run from a shell with msys
+        shell = True
+
     orig = signal.signal(signal.SIGINT, signal.SIG_IGN)
-    proc = ProcessHandler(cmd_args, env=os.environ, stream=None)
+    proc = ProcessHandler(cmd_args, env=os.environ, stream=None, shell=shell)
     proc.run()
     signal.signal(signal.SIGINT, orig)
 
     try:
         proc.wait()
     except KeyboardInterrupt:
         proc.kill()
         return []