Browse Source

more backports

Frank-Rainer Grahl 11 months ago
parent
commit
b6b709e5c8
100 changed files with 31514 additions and 62 deletions
  1. 35 0
      frg/mozilla-release/work-js/1167238-1-60a1.patch
  2. 694 0
      frg/mozilla-release/work-js/1167238-2-60a1.patch
  3. 393 0
      frg/mozilla-release/work-js/1167238-3-60a1.patch
  4. 739 0
      frg/mozilla-release/work-js/1167238-4-60a1.patch
  5. 310 0
      frg/mozilla-release/work-js/1167238-5-60a1.patch
  6. 90 0
      frg/mozilla-release/work-js/1167238-6-60a1.patch
  7. 579 0
      frg/mozilla-release/work-js/1252998-01-61a1.patch
  8. 30 0
      frg/mozilla-release/work-js/1252998-02-61a1.patch
  9. 89 0
      frg/mozilla-release/work-js/1252998-03-61a1.patch
  10. 573 0
      frg/mozilla-release/work-js/1252998-04-61a1.patch
  11. 364 0
      frg/mozilla-release/work-js/1252998-05-61a1.patch
  12. 101 0
      frg/mozilla-release/work-js/1252998-06-61a1.patch
  13. 200 0
      frg/mozilla-release/work-js/1252998-07-61a1.patch
  14. 84 0
      frg/mozilla-release/work-js/1252998-08-61a1.patch
  15. 314 0
      frg/mozilla-release/work-js/1252998-09-61a1.patch
  16. 446 0
      frg/mozilla-release/work-js/1252998-10-61a1.patch
  17. 129 0
      frg/mozilla-release/work-js/1252998-11-61a1.patch
  18. 301 0
      frg/mozilla-release/work-js/1348223-1-61a1.patch
  19. 286 0
      frg/mozilla-release/work-js/1348223-2-61a1.patch
  20. 327 0
      frg/mozilla-release/work-js/1348223-3-61a1.patch
  21. 171 0
      frg/mozilla-release/work-js/1348223-4-61a1.patch
  22. 28 0
      frg/mozilla-release/work-js/1363957-59a1.patch
  23. 1605 0
      frg/mozilla-release/work-js/1370881-60a1.patch
  24. 635 0
      frg/mozilla-release/work-js/1372592-57a1.patch
  25. 3296 0
      frg/mozilla-release/work-js/1382841-1-60a1.patch
  26. 282 0
      frg/mozilla-release/work-js/1382841-2-60a1.patch
  27. 83 0
      frg/mozilla-release/work-js/1382841-3-60a1.patch
  28. 1008 0
      frg/mozilla-release/work-js/1382841-4-60a1.patch
  29. 95 0
      frg/mozilla-release/work-js/1382841-5-60a1.patch
  30. 50 0
      frg/mozilla-release/work-js/1390675-57a1.patch
  31. 158 0
      frg/mozilla-release/work-js/1399699-57a1.patch
  32. 202 0
      frg/mozilla-release/work-js/1410416-1-58a1.patch
  33. 911 0
      frg/mozilla-release/work-js/1410416-2-58a1.patch
  34. 89 0
      frg/mozilla-release/work-js/1414751-60a1.patch
  35. 205 0
      frg/mozilla-release/work-js/1415342-1only-61a1.patch
  36. 85 0
      frg/mozilla-release/work-js/1416193-59a1.patch
  37. 60 0
      frg/mozilla-release/work-js/1419226-1-61a1.patch
  38. 154 0
      frg/mozilla-release/work-js/1419226-2-61a1.patch
  39. 338 0
      frg/mozilla-release/work-js/1419382-1-59a1.patch
  40. 225 0
      frg/mozilla-release/work-js/1419382-2-59a1.patch
  41. 440 0
      frg/mozilla-release/work-js/1419382-3-59a1.patch
  42. 120 0
      frg/mozilla-release/work-js/1419382-4-59a1.patch
  43. 390 0
      frg/mozilla-release/work-js/1420419-1-59a1.patch
  44. 29 0
      frg/mozilla-release/work-js/1420419-2-59a1.patch
  45. 246 0
      frg/mozilla-release/work-js/1420594-1-59a1.patch
  46. 286 0
      frg/mozilla-release/work-js/1420594-2-59a1.patch
  47. 21 0
      frg/mozilla-release/work-js/1420594-3-59a1.patch
  48. 0 0
      frg/mozilla-release/work-js/1420894-62a1.patch
  49. 407 0
      frg/mozilla-release/work-js/1421737-1-60a1.patch
  50. 339 0
      frg/mozilla-release/work-js/1421737-2-60a1.patch
  51. 440 0
      frg/mozilla-release/work-js/1421737-3-60a1.patch
  52. 1361 0
      frg/mozilla-release/work-js/1421737-4-60a1.patch
  53. 1159 0
      frg/mozilla-release/work-js/1422160-60a1.patch
  54. 597 0
      frg/mozilla-release/work-js/1422163-1-60a1.patch
  55. 1668 0
      frg/mozilla-release/work-js/1422163-2-60a1.patch
  56. 211 0
      frg/mozilla-release/work-js/1422314-59a1.patch
  57. 79 0
      frg/mozilla-release/work-js/1424949-1-59a1.patch
  58. 32 0
      frg/mozilla-release/work-js/1424949-2-59a1.patch
  59. 61 0
      frg/mozilla-release/work-js/1425270-59a1.patch
  60. 100 0
      frg/mozilla-release/work-js/1430654-1-60a1.patch
  61. 34 0
      frg/mozilla-release/work-js/1430654-2-60a1.patch
  62. 86 0
      frg/mozilla-release/work-js/1430654-3-60a1.patch
  63. 36 0
      frg/mozilla-release/work-js/1430745-59a1.patch
  64. 178 0
      frg/mozilla-release/work-js/1431029-1-60a1.patch
  65. 1033 0
      frg/mozilla-release/work-js/1431029-2-60a1.patch
  66. 495 0
      frg/mozilla-release/work-js/1432745-60a1.patch
  67. 175 0
      frg/mozilla-release/work-js/1432759-60a1.patch
  68. 34 0
      frg/mozilla-release/work-js/1433492-1-60a1.patch
  69. 212 0
      frg/mozilla-release/work-js/1433492-2-60a1.patch
  70. 603 0
      frg/mozilla-release/work-js/1434414-60a1.patch
  71. 331 0
      frg/mozilla-release/work-js/1435910-0_2-60a1.patch
  72. 32 0
      frg/mozilla-release/work-js/1437248-60a1.patch
  73. 30 0
      frg/mozilla-release/work-js/1437880-61a1.patch
  74. 29 0
      frg/mozilla-release/work-js/1438556-2-fix-whitespace-62a1.patch
  75. 277 0
      frg/mozilla-release/work-js/1440638-61a1.patch
  76. 387 0
      frg/mozilla-release/work-js/1442183-61a1.patch
  77. 275 0
      frg/mozilla-release/work-js/1444082-60a1.patch
  78. 62 0
      frg/mozilla-release/work-js/1444679-1-60a1.patch
  79. 88 0
      frg/mozilla-release/work-js/1444679-2-60a1.patch
  80. 1677 0
      frg/mozilla-release/work-js/1445188-61a1.patch
  81. 196 0
      frg/mozilla-release/work-js/1445207-61a1.patch
  82. 35 0
      frg/mozilla-release/work-js/1445837-61a1.patch
  83. 689 0
      frg/mozilla-release/work-js/1446342-61a1.patch
  84. 49 0
      frg/mozilla-release/work-js/1447262-61a1.patch
  85. 396 0
      frg/mozilla-release/work-js/1450242-61a1.patch
  86. 254 0
      frg/mozilla-release/work-js/1453589-61a1.patch
  87. 0 0
      frg/mozilla-release/work-js/1457059-62a1.patch
  88. 4 4
      frg/mozilla-release/work-js/1461938-22-62a1.patch
  89. 18 18
      frg/mozilla-release/work-js/1461938-23-62a1.patch
  90. 17 8
      frg/mozilla-release/work-js/1461938-28-62a1.patch
  91. 20 20
      frg/mozilla-release/work-js/1461938-32-62a1.patch
  92. 12 12
      frg/mozilla-release/work-js/1461938-33-62a1.patch
  93. 0 0
      frg/mozilla-release/work-js/1462939-1-62a1.patch
  94. 0 0
      frg/mozilla-release/work-js/1463717-62a1.patch
  95. 0 0
      frg/mozilla-release/work-js/1463723-62a1.patch
  96. 0 0
      frg/mozilla-release/work-js/1463847-1-62a1.patch
  97. 0 0
      frg/mozilla-release/work-js/1463847-2-62a1.patch
  98. 0 0
      frg/mozilla-release/work-js/1463939-1-62a1.patch
  99. 0 0
      frg/mozilla-release/work-js/1463939-2-62a1.patch
  100. 0 0
      frg/mozilla-release/work-js/1463939-3-62a1.patch

+ 35 - 0
frg/mozilla-release/work-js/1167238-1-60a1.patch

@@ -0,0 +1,35 @@
+# HG changeset patch
+# User Johann Hofmann <jhofmann@mozilla.com>
+# Date 1516184946 -3600
+# Node ID d001895822cde6beb0d4c9de120abec43a6d50c4
+# Parent  d56f8c38e62812422ed3a4616ce9909f522fcd50
+Bug 1167238 - Part 1 - Remove Sanitizer.jsm shim. r=mak
+
+MozReview-Commit-ID: I7l9M3sC4pD
+
+diff --git a/browser/modules/Sanitizer.jsm b/browser/modules/Sanitizer.jsm
+deleted file mode 100644
+--- a/browser/modules/Sanitizer.jsm
++++ /dev/null
+@@ -1,21 +0,0 @@
+-/* This Source Code Form is subject to the terms of the Mozilla Public
+- * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+- * You can obtain one at http://mozilla.org/MPL/2.0/. */
+-"use strict";
+-
+-//
+-// A shared module for sanitize.js
+-//
+-// Until bug 1167238 lands, this serves only as a way to ensure that
+-// sanitize is loaded from its own compartment, rather than from that
+-// of the sanitize dialog.
+-//
+-
+-var EXPORTED_SYMBOLS = ["Sanitizer"];
+-
+-ChromeUtils.import("resource://gre/modules/Services.jsm");
+-
+-var scope = {};
+-Services.scriptloader.loadSubScript("chrome://browser/content/sanitize.js", scope);
+-
+-this.Sanitizer = scope.Sanitizer;

+ 694 - 0
frg/mozilla-release/work-js/1167238-2-60a1.patch

@@ -0,0 +1,694 @@
+# HG changeset patch
+# User Johann Hofmann <jhofmann@mozilla.com>
+# Date 1516287483 -3600
+# Node ID 38091dd39b83161746cd6338c5cf12c54b956e84
+# Parent  7e548ae3218466176536820c8e9e43be8c513d72
+Bug 1167238 - Part 2 - Convert sanitize.js to a module. r=mak
+
+MozReview-Commit-ID: IWu7Dg61Vv4
+
+diff --git a/browser/base/jar.mn b/browser/base/jar.mn
+--- a/browser/base/jar.mn
++++ b/browser/base/jar.mn
+@@ -118,17 +118,16 @@ browser.jar:
+         content/browser/pageinfo/feeds.xml            (content/pageinfo/feeds.xml)
+         content/browser/pageinfo/permissions.js       (content/pageinfo/permissions.js)
+         content/browser/pageinfo/security.js          (content/pageinfo/security.js)
+         content/browser/robot.ico                     (content/robot.ico)
+         content/browser/static-robot.png              (content/static-robot.png)
+         content/browser/safeMode.css                  (content/safeMode.css)
+         content/browser/safeMode.js                   (content/safeMode.js)
+         content/browser/safeMode.xul                  (content/safeMode.xul)
+-        content/browser/sanitize.js                   (content/sanitize.js)
+         content/browser/sanitize.xul                  (content/sanitize.xul)
+         content/browser/sanitizeDialog.js             (content/sanitizeDialog.js)
+         content/browser/sanitizeDialog.css            (content/sanitizeDialog.css)
+         content/browser/contentSearchUI.js            (content/contentSearchUI.js)
+         content/browser/contentSearchUI.css           (content/contentSearchUI.css)
+         content/browser/tabbrowser.css                (content/tabbrowser.css)
+         content/browser/tabbrowser.xml                (content/tabbrowser.xml)
+ *       content/browser/urlbarBindings.xml            (content/urlbarBindings.xml)
+diff --git a/browser/base/content/sanitize.js b/browser/modules/Sanitizer.jsm
+rename from browser/base/content/sanitize.js
+rename to browser/modules/Sanitizer.jsm
+--- a/browser/base/content/sanitize.js
++++ b/browser/modules/Sanitizer.jsm
+@@ -1,14 +1,14 @@
+ // -*- indent-tabs-mode: nil; js-indent-level: 2 -*-
+ /* This Source Code Form is subject to the terms of the Mozilla Public
+  * License, v. 2.0. If a copy of the MPL was not distributed with this
+  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+ 
+-/* import-globals-from sanitizeDialog.js */
++var EXPORTED_SYMBOLS = ["Sanitizer"];
+ 
+ ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
+ ChromeUtils.import("resource://gre/modules/Services.jsm");
+ 
+ XPCOMUtils.defineLazyModuleGetters(this, {
+   AppConstants: "resource://gre/modules/AppConstants.jsm",
+   PlacesUtils: "resource://gre/modules/PlacesUtils.jsm",
+   FormHistory: "resource://gre/modules/FormHistory.jsm",
+@@ -27,46 +27,198 @@ XPCOMUtils.defineLazyServiceGetter(this,
+                                    "nsIQuotaManagerService");
+ 
+ /**
+  * A number of iterations after which to yield time back
+  * to the system.
+  */
+ const YIELD_PERIOD = 10;
+ 
+-function Sanitizer() {
+-}
+-Sanitizer.prototype = {
+-  // warning to the caller: this one may raise an exception (e.g. bug #265028)
+-  clearItem(aItemName) {
+-    this.items[aItemName].clear();
++var Sanitizer = {
++  /**
++   * Whether we should sanitize on shutdown.
++   */
++  PREF_SANITIZE_ON_SHUTDOWN: "privacy.sanitize.sanitizeOnShutdown",
++
++  /**
++   * During a sanitization this is set to a json containing the array of items
++   * being sanitized, then cleared once the sanitization is complete.
++   * This allows to retry a sanitization on startup in case it was interrupted
++   * by a crash.
++   */
++  PREF_SANITIZE_IN_PROGRESS: "privacy.sanitize.sanitizeInProgress",
++
++  /**
++   * Whether the previous shutdown sanitization completed successfully.
++   * This is used to detect cases where we were supposed to sanitize on shutdown
++   * but due to a crash we were unable to.  In such cases there may not be any
++   * sanitization in progress, cause we didn't have a chance to start it yet.
++   */
++  PREF_SANITIZE_DID_SHUTDOWN: "privacy.sanitize.didShutdownSanitize",
++
++  /**
++   * The fallback timestamp used when no argument is given to
++   * Sanitizer.getClearRange.
++   */
++  PREF_TIMESPAN: "privacy.sanitize.timeSpan",
++
++  /**
++   * Time span constants corresponding to values of the privacy.sanitize.timeSpan
++   * pref.  Used to determine how much history to clear, for various items
++   */
++  TIMESPAN_EVERYTHING: 0,
++  TIMESPAN_HOUR:       1,
++  TIMESPAN_2HOURS:     2,
++  TIMESPAN_4HOURS:     3,
++  TIMESPAN_TODAY:      4,
++  TIMESPAN_5MIN:       5,
++  TIMESPAN_24HOURS:    6,
++
++  /**
++   * Shows a sanitization dialog to the user.
++   *
++   * @param [optional] parentWindow the window to use as
++   *                   parent for the created dialog.
++   */
++  showUI(parentWindow) {
++    let win = AppConstants.platform == "macosx" ?
++      null : // make this an app-modal window on Mac
++      parentWindow;
++    Services.ww.openWindow(win,
++                           "chrome://browser/content/sanitize.xul",
++                           "Sanitize",
++                           "chrome,titlebar,dialog,centerscreen,modal",
++                           null);
+   },
+ 
+-  prefDomain: "",
++  /**
++   * Performs startup tasks:
++   *  - Checks if sanitization was interrupted during last shutdown.
++   *  - Registers sanitize-on-shutdown.
++   */
++  async onStartup() {
++    // Check if we were interrupted during the last shutdown sanitization.
++    let shutdownSanitizationWasInterrupted =
++      Services.prefs.getBoolPref(Sanitizer.PREF_SANITIZE_ON_SHUTDOWN, false) &&
++      Services.prefs.getPrefType(Sanitizer.PREF_SANITIZE_DID_SHUTDOWN) == Ci.nsIPrefBranch.PREF_INVALID;
++
++    if (Services.prefs.prefHasUserValue(Sanitizer.PREF_SANITIZE_DID_SHUTDOWN)) {
++      // Reset the pref, so that if we crash before having a chance to
++      // sanitize on shutdown, we will do at the next startup.
++      // Flushing prefs has a cost, so do this only if necessary.
++      Services.prefs.clearUserPref(Sanitizer.PREF_SANITIZE_DID_SHUTDOWN);
++      Services.prefs.savePrefFile(null);
++    }
++
++    // Make sure that we are triggered during shutdown.
++    let shutdownClient = PlacesUtils.history.shutdownClient.jsclient;
++    // We need to pass to sanitize() (through sanitizeOnShutdown) a state object
++    // that tracks the status of the shutdown blocker. This `progress` object
++    // will be updated during sanitization and reported with the crash in case of
++    // a shutdown timeout.
++    // We use the `options` argument to pass the `progress` object to sanitize().
++    let progress = { isShutdown: true };
++    shutdownClient.addBlocker("sanitize.js: Sanitize on shutdown",
++      () => sanitizeOnShutdown({ progress }),
++      {
++        fetchState: () => ({ progress })
++      }
++    );
++
++    // Check if Firefox crashed during a sanitization.
++    let lastInterruptedSanitization = Services.prefs.getStringPref(Sanitizer.PREF_SANITIZE_IN_PROGRESS, "");
++    if (lastInterruptedSanitization) {
++      // If the json is invalid this will just throw and reject the Task.
++      let {itemsToClear, options} = JSON.parse(lastInterruptedSanitization);
++      await this.sanitize(itemsToClear, options);
++    } else if (shutdownSanitizationWasInterrupted) {
++      // Otherwise, could be we were supposed to sanitize on shutdown but we
++      // didn't have a chance, due to an earlier crash.
++      // In such a case, just redo a shutdown sanitize now, during startup.
++      await sanitizeOnShutdown();
++    }
++  },
+ 
+-  getNameFromPreference(aPreferenceName) {
+-    return aPreferenceName.substr(this.prefDomain.length);
++  /**
++   * Returns a 2 element array representing the start and end times,
++   * in the uSec-since-epoch format that PRTime likes. If we should
++   * clear everything, this function returns null.
++   *
++   * @param ts [optional] a timespan to convert to start and end time.
++   *                      Falls back to the privacy.sanitize.timeSpan preference
++   *                      if this argument is omitted.
++   *                      If this argument is provided, it has to be one of the
++   *                      Sanitizer.TIMESPAN_* constants. This function will
++   *                      throw an error otherwise.
++   *
++   * @return {Array} a 2-element Array containing the start and end times.
++   */
++  getClearRange(ts) {
++    if (ts === undefined)
++      ts = Services.prefs.getIntPref(Sanitizer.PREF_TIMESPAN);
++    if (ts === Sanitizer.TIMESPAN_EVERYTHING)
++      return null;
++
++    // PRTime is microseconds while JS time is milliseconds
++    var endDate = Date.now() * 1000;
++    switch (ts) {
++      case Sanitizer.TIMESPAN_5MIN :
++        var startDate = endDate - 300000000; // 5*60*1000000
++        break;
++      case Sanitizer.TIMESPAN_HOUR :
++        startDate = endDate - 3600000000; // 1*60*60*1000000
++        break;
++      case Sanitizer.TIMESPAN_2HOURS :
++        startDate = endDate - 7200000000; // 2*60*60*1000000
++        break;
++      case Sanitizer.TIMESPAN_4HOURS :
++        startDate = endDate - 14400000000; // 4*60*60*1000000
++        break;
++      case Sanitizer.TIMESPAN_TODAY :
++        var d = new Date(); // Start with today
++        d.setHours(0); // zero us back to midnight...
++        d.setMinutes(0);
++        d.setSeconds(0);
++        startDate = d.valueOf() * 1000; // convert to epoch usec
++        break;
++      case Sanitizer.TIMESPAN_24HOURS :
++        startDate = endDate - 86400000000; // 24*60*60*1000000
++        break;
++      default:
++        throw "Invalid time span for clear private data: " + ts;
++    }
++    return [startDate, endDate];
+   },
+ 
+   /**
+    * Deletes privacy sensitive data in a batch, according to user preferences.
+    * Returns a promise which is resolved if no errors occurred.  If an error
+    * occurs, a message is reported to the console and all other items are still
+    * cleared before the promise is finally rejected.
+    *
+    * @param [optional] itemsToClear
+    *        Array of items to be cleared. if specified only those
+    *        items get cleared, irrespectively of the preference settings.
+    * @param [optional] options
+-   *        Object whose properties are options for this sanitization.
+-   *        TODO (bug 1167238) document options here.
++   *        Object whose properties are options for this sanitization:
++   *         - ignoreTimespan (default: true): Time span only makes sense in
++   *           certain cases.  Consumers who want to only clear some private
++   *           data can opt in by setting this to false, and can optionally
++   *           specify a specific range.
++   *           If timespan is not ignored, and range is not set, sanitize() will
++   *           use the value of the timespan pref to determine a range.
++   *         - range (default: null)
++   *         - prefDomain (default: "privacy.cpd."): indicates the preferences
++   *           branch to collect the list of items to sanitize from.
++   *         - privateStateForNewWindow (default: "non-private"): when clearing
++   *           open windows, defines the private state for the newly opened window.
+    */
+   async sanitize(itemsToClear = null, options = {}) {
+     let progress = options.progress || {};
+-    let promise = this._sanitize(itemsToClear, progress);
++    let promise = sanitizeInternal(this.items, itemsToClear, progress, options);
+ 
+     // Depending on preferences, the sanitizer may perform asynchronous
+     // work before it starts cleaning up the Places database (e.g. closing
+     // windows). We need to make sure that the connection to that database
+     // hasn't been closed by the time we use it.
+     // Though, if this is a sanitize on shutdown, we already have a blocker.
+     if (!progress.isShutdown) {
+       let shutdownClient = Cc["@mozilla.org/browser/nav-history-service;1"]
+@@ -83,118 +235,16 @@ Sanitizer.prototype = {
+ 
+     try {
+       await promise;
+     } finally {
+       Services.obs.notifyObservers(null, "sanitizer-sanitization-complete");
+     }
+   },
+ 
+-  async _sanitize(aItemsToClear, progress = {}) {
+-    let seenError = false;
+-    let itemsToClear;
+-    if (Array.isArray(aItemsToClear)) {
+-      // Shallow copy the array, as we are going to modify
+-      // it in place later.
+-      itemsToClear = [...aItemsToClear];
+-    } else {
+-      let branch = Services.prefs.getBranch(this.prefDomain);
+-      itemsToClear = Object.keys(this.items).filter(itemName => {
+-        try {
+-          return branch.getBoolPref(itemName);
+-        } catch (ex) {
+-          return false;
+-        }
+-      });
+-    }
+-
+-    // Store the list of items to clear, in case we are killed before we
+-    // get a chance to complete.
+-    Services.prefs.setStringPref(Sanitizer.PREF_SANITIZE_IN_PROGRESS,
+-                                 JSON.stringify(itemsToClear));
+-
+-    // Store the list of items to clear, for debugging/forensics purposes
+-    for (let k of itemsToClear) {
+-      progress[k] = "ready";
+-    }
+-
+-    // Ensure open windows get cleared first, if they're in our list, so that they don't stick
+-    // around in the recently closed windows list, and so we can cancel the whole thing
+-    // if the user selects to keep a window open from a beforeunload prompt.
+-    let openWindowsIndex = itemsToClear.indexOf("openWindows");
+-    if (openWindowsIndex != -1) {
+-      itemsToClear.splice(openWindowsIndex, 1);
+-      await this.items.openWindows.clear();
+-      progress.openWindows = "cleared";
+-    }
+-
+-    // Cache the range of times to clear
+-    let range = null;
+-    // If we ignore timespan, clear everything,
+-    // otherwise, pick a range.
+-    if (!this.ignoreTimespan) {
+-      range = this.range || Sanitizer.getClearRange();
+-    }
+-
+-    // For performance reasons we start all the clear tasks at once, then wait
+-    // for their promises later.
+-    // Some of the clear() calls may raise exceptions (for example bug 265028),
+-    // we catch and store them, but continue to sanitize as much as possible.
+-    // Callers should check returned errors and give user feedback
+-    // about items that could not be sanitized
+-    let refObj = {};
+-    TelemetryStopwatch.start("FX_SANITIZE_TOTAL", refObj);
+-
+-    let annotateError = (name, ex) => {
+-      progress[name] = "failed";
+-      seenError = true;
+-      console.error("Error sanitizing " + name, ex);
+-    };
+-
+-    // Array of objects in form { name, promise }.
+-    // `name` is the item's name and `promise` may be a promise, if the
+-    // sanitization is asynchronous, or the function return value, otherwise.
+-    let handles = [];
+-    for (let name of itemsToClear) {
+-      let item = this.items[name];
+-      try {
+-        // Catch errors here, so later we can just loop through these.
+-        handles.push({ name,
+-                       promise: item.clear(range)
+-                                    .then(() => progress[name] = "cleared",
+-                                          ex => annotateError(name, ex))
+-                     });
+-      } catch (ex) {
+-        annotateError(name, ex);
+-      }
+-    }
+-    for (let handle of handles) {
+-      progress[handle.name] = "blocking";
+-      await handle.promise;
+-    }
+-
+-    // Sanitization is complete.
+-    TelemetryStopwatch.finish("FX_SANITIZE_TOTAL", refObj);
+-    // Reset the inProgress preference since we were not killed during
+-    // sanitization.
+-    Services.prefs.clearUserPref(Sanitizer.PREF_SANITIZE_IN_PROGRESS);
+-    progress = {};
+-    if (seenError) {
+-      throw new Error("Error sanitizing");
+-    }
+-  },
+-
+-  // Time span only makes sense in certain cases.  Consumers who want
+-  // to only clear some private data can opt in by setting this to false,
+-  // and can optionally specify a specific range.  If timespan is not ignored,
+-  // and range is not set, sanitize() will use the value of the timespan
+-  // pref to determine a range
+-  ignoreTimespan: true,
+-  range: null,
+-
+   items: {
+     cache: {
+       async clear(range) {
+         let seenException;
+         let refObj = {};
+         TelemetryStopwatch.start("FX_SANITIZE_CACHE", refObj);
+ 
+         try {
+@@ -546,33 +596,32 @@ Sanitizer.prototype = {
+         TelemetryStopwatch.finish("FX_SANITIZE_SITESETTINGS", refObj);
+         if (seenException) {
+           throw seenException;
+         }
+       }
+     },
+ 
+     openWindows: {
+-      privateStateForNewWindow: "non-private",
+-      _canCloseWindow(aWindow) {
+-        if (aWindow.CanCloseWindow()) {
++      _canCloseWindow(win) {
++        if (win.CanCloseWindow()) {
+           // We already showed PermitUnload for the window, so let's
+           // make sure we don't do it again when we actually close the
+           // window.
+-          aWindow.skipNextCanClose = true;
++          win.skipNextCanClose = true;
+           return true;
+         }
+         return false;
+       },
+-      _resetAllWindowClosures(aWindowList) {
+-        for (let win of aWindowList) {
++      _resetAllWindowClosures(windowList) {
++        for (let win of windowList) {
+           win.skipNextCanClose = false;
+         }
+       },
+-      async clear() {
++      async clear(range, privateStateForNewWindow = "non-private") {
+         // NB: this closes all *browser* windows, not other windows like the library, about window,
+         // browser console, etc.
+ 
+         // Keep track of the time in case we get stuck in la-la-land because of onbeforeunload
+         // dialogs
+         let existingWindow = Services.appShell.hiddenDOMWindow;
+         let startDate = existingWindow.performance.now();
+ 
+@@ -602,17 +651,17 @@ Sanitizer.prototype = {
+ 
+         let refObj = {};
+         TelemetryStopwatch.start("FX_SANITIZE_OPENWINDOWS", refObj);
+ 
+         // First create a new window. We do this first so that on non-mac, we don't
+         // accidentally close the app by closing all the windows.
+         let handler = Cc["@mozilla.org/browser/clh;1"].getService(Ci.nsIBrowserHandler);
+         let defaultArgs = handler.defaultArgs;
+-        let features = "chrome,all,dialog=no," + this.privateStateForNewWindow;
++        let features = "chrome,all,dialog=no," + privateStateForNewWindow;
+         let newWindow = existingWindow.openDialog("chrome://browser/content/", "_blank",
+                                                   features, defaultArgs);
+ 
+         let onFullScreen = null;
+         if (AppConstants.platform == "macosx") {
+           onFullScreen = function(e) {
+             newWindow.removeEventListener("fullscreen", onFullScreen);
+             let docEl = newWindow.document.documentElement;
+@@ -671,163 +720,117 @@ Sanitizer.prototype = {
+         // Start the process of closing windows
+         while (windowList.length) {
+           windowList.pop().close();
+         }
+         newWindow.focus();
+         await promiseReady;
+       }
+     },
+-  }
+-};
+-
+-// The preferences branch for the sanitizer.
+-Sanitizer.PREF_DOMAIN = "privacy.sanitize.";
+-// Whether we should sanitize on shutdown.
+-Sanitizer.PREF_SANITIZE_ON_SHUTDOWN = "privacy.sanitize.sanitizeOnShutdown";
+-// During a sanitization this is set to a json containing the array of items
+-// being sanitized, then cleared once the sanitization is complete.
+-// This allows to retry a sanitization on startup in case it was interrupted
+-// by a crash.
+-Sanitizer.PREF_SANITIZE_IN_PROGRESS = "privacy.sanitize.sanitizeInProgress";
+-// Whether the previous shutdown sanitization completed successfully.
+-// This is used to detect cases where we were supposed to sanitize on shutdown
+-// but due to a crash we were unable to.  In such cases there may not be any
+-// sanitization in progress, cause we didn't have a chance to start it yet.
+-Sanitizer.PREF_SANITIZE_DID_SHUTDOWN = "privacy.sanitize.didShutdownSanitize";
+-
+-// Time span constants corresponding to values of the privacy.sanitize.timeSpan
+-// pref.  Used to determine how much history to clear, for various items
+-Sanitizer.TIMESPAN_EVERYTHING = 0;
+-Sanitizer.TIMESPAN_HOUR       = 1;
+-Sanitizer.TIMESPAN_2HOURS     = 2;
+-Sanitizer.TIMESPAN_4HOURS     = 3;
+-Sanitizer.TIMESPAN_TODAY      = 4;
+-Sanitizer.TIMESPAN_5MIN       = 5;
+-Sanitizer.TIMESPAN_24HOURS    = 6;
+-
+-// Return a 2 element array representing the start and end times,
+-// in the uSec-since-epoch format that PRTime likes.  If we should
+-// clear everything, return null.  Use ts if it is defined; otherwise
+-// use the timeSpan pref.
+-Sanitizer.getClearRange = function(ts) {
+-  if (ts === undefined)
+-    ts = Sanitizer.prefs.getIntPref("timeSpan");
+-  if (ts === Sanitizer.TIMESPAN_EVERYTHING)
+-    return null;
+-
+-  // PRTime is microseconds while JS time is milliseconds
+-  var endDate = Date.now() * 1000;
+-  switch (ts) {
+-    case Sanitizer.TIMESPAN_5MIN :
+-      var startDate = endDate - 300000000; // 5*60*1000000
+-      break;
+-    case Sanitizer.TIMESPAN_HOUR :
+-      startDate = endDate - 3600000000; // 1*60*60*1000000
+-      break;
+-    case Sanitizer.TIMESPAN_2HOURS :
+-      startDate = endDate - 7200000000; // 2*60*60*1000000
+-      break;
+-    case Sanitizer.TIMESPAN_4HOURS :
+-      startDate = endDate - 14400000000; // 4*60*60*1000000
+-      break;
+-    case Sanitizer.TIMESPAN_TODAY :
+-      var d = new Date();  // Start with today
+-      d.setHours(0);      // zero us back to midnight...
+-      d.setMinutes(0);
+-      d.setSeconds(0);
+-      startDate = d.valueOf() * 1000; // convert to epoch usec
+-      break;
+-    case Sanitizer.TIMESPAN_24HOURS :
+-      startDate = endDate - 86400000000; // 24*60*60*1000000
+-      break;
+-    default:
+-      throw "Invalid time span for clear private data: " + ts;
+-  }
+-  return [startDate, endDate];
++  },
+ };
+ 
+-Sanitizer._prefs = null;
+-Sanitizer.__defineGetter__("prefs", function() {
+-  return Sanitizer._prefs ? Sanitizer._prefs
+-    : Sanitizer._prefs = Services.prefs.getBranch(Sanitizer.PREF_DOMAIN);
+-});
+-
+-// Shows sanitization UI
+-Sanitizer.showUI = function(aParentWindow) {
+-  let win = AppConstants.platform == "macosx" ?
+-    null : // make this an app-modal window on Mac
+-    aParentWindow;
+-  Services.ww.openWindow(win,
+-                         "chrome://browser/content/sanitize.xul",
+-                         "Sanitize",
+-                         "chrome,titlebar,dialog,centerscreen,modal",
+-                         null);
+-};
++async function sanitizeInternal(items, aItemsToClear, progress, options = {}) {
++  let { prefDomain = "privacy.cpd.", ignoreTimespan = true, range } = options;
++  let seenError = false;
++  let itemsToClear;
++  if (Array.isArray(aItemsToClear)) {
++    // Shallow copy the array, as we are going to modify
++    // it in place later.
++    itemsToClear = [...aItemsToClear];
++  } else {
++    let branch = Services.prefs.getBranch(prefDomain);
++    itemsToClear = Object.keys(items).filter(itemName => {
++      try {
++        return branch.getBoolPref(itemName);
++      } catch (ex) {
++        return false;
++      }
++    });
++  }
+ 
+-/**
+- * Deletes privacy sensitive data in a batch, optionally showing the
+- * sanitize UI, according to user preferences
+- */
+-Sanitizer.sanitize = function(aParentWindow) {
+-  Sanitizer.showUI(aParentWindow);
+-};
++  // Store the list of items to clear, in case we are killed before we
++  // get a chance to complete.
++  Services.prefs.setStringPref(Sanitizer.PREF_SANITIZE_IN_PROGRESS,
++                               JSON.stringify({itemsToClear, options}));
++
++  // Store the list of items to clear, for debugging/forensics purposes
++  for (let k of itemsToClear) {
++    progress[k] = "ready";
++  }
+ 
+-Sanitizer.onStartup = async function() {
+-  // Check if we were interrupted during the last shutdown sanitization.
+-  let shutownSanitizationWasInterrupted =
+-    Services.prefs.getBoolPref(Sanitizer.PREF_SANITIZE_ON_SHUTDOWN, false) &&
+-    Services.prefs.getPrefType(Sanitizer.PREF_SANITIZE_DID_SHUTDOWN) == Ci.nsIPrefBranch.PREF_INVALID;
++  // Ensure open windows get cleared first, if they're in our list, so that
++  // they don't stick around in the recently closed windows list, and so we
++  // can cancel the whole thing if the user selects to keep a window open
++  // from a beforeunload prompt.
++  let openWindowsIndex = itemsToClear.indexOf("openWindows");
++  if (openWindowsIndex != -1) {
++    itemsToClear.splice(openWindowsIndex, 1);
++    await items.openWindows.clear(null, options);
++    progress.openWindows = "cleared";
++  }
+ 
+-  if (Services.prefs.prefHasUserValue(Sanitizer.PREF_SANITIZE_DID_SHUTDOWN)) {
+-    // Reset the pref, so that if we crash before having a chance to
+-    // sanitize on shutdown, we will do at the next startup.
+-    // Flushing prefs has a cost, so do this only if necessary.
+-    Services.prefs.clearUserPref(Sanitizer.PREF_SANITIZE_DID_SHUTDOWN);
+-    Services.prefs.savePrefFile(null);
++  // If we ignore timespan, clear everything,
++  // otherwise, pick a range.
++  if (!ignoreTimespan && !range) {
++    range = Sanitizer.getClearRange();
+   }
+ 
+-  // Make sure that we are triggered during shutdown.
+-  let shutdownClient = Cc["@mozilla.org/browser/nav-history-service;1"]
+-                         .getService(Ci.nsPIPlacesDatabase)
+-                         .shutdownClient
+-                         .jsclient;
+-  // We need to pass to sanitize() (through sanitizeOnShutdown) a state object
+-  // that tracks the status of the shutdown blocker. This `progress` object
+-  // will be updated during sanitization and reported with the crash in case of
+-  // a shutdown timeout.
+-  // We use the `options` argument to pass the `progress` object to sanitize().
+-  let progress = { isShutdown: true };
+-  shutdownClient.addBlocker("sanitize.js: Sanitize on shutdown",
+-    () => sanitizeOnShutdown({ progress }),
+-    {
+-      fetchState: () => ({ progress })
++  // For performance reasons we start all the clear tasks at once, then wait
++  // for their promises later.
++  // Some of the clear() calls may raise exceptions (for example bug 265028),
++  // we catch and store them, but continue to sanitize as much as possible.
++  // Callers should check returned errors and give user feedback
++  // about items that could not be sanitized
++  let refObj = {};
++  TelemetryStopwatch.start("FX_SANITIZE_TOTAL", refObj);
++
++  let annotateError = (name, ex) => {
++    progress[name] = "failed";
++    seenError = true;
++    console.error("Error sanitizing " + name, ex);
++  };
++
++  // Array of objects in form { name, promise }.
++  // `name` is the item's name and `promise` may be a promise, if the
++  // sanitization is asynchronous, or the function return value, otherwise.
++  let handles = [];
++  for (let name of itemsToClear) {
++    let item = items[name];
++    try {
++      // Catch errors here, so later we can just loop through these.
++      handles.push({ name,
++                     promise: item.clear(range, options)
++                                  .then(() => progress[name] = "cleared",
++                                        ex => annotateError(name, ex))
++                   });
++    } catch (ex) {
++      annotateError(name, ex);
+     }
+-  );
++  }
++  for (let handle of handles) {
++    progress[handle.name] = "blocking";
++    await handle.promise;
++  }
+ 
+-  // Check if Firefox crashed during a sanitization.
+-  let lastInterruptedSanitization = Services.prefs.getStringPref(Sanitizer.PREF_SANITIZE_IN_PROGRESS, "");
+-  if (lastInterruptedSanitization) {
+-    let s = new Sanitizer();
+-    // If the json is invalid this will just throw and reject the Task.
+-    let itemsToClear = JSON.parse(lastInterruptedSanitization);
+-    await s.sanitize(itemsToClear);
+-  } else if (shutownSanitizationWasInterrupted) {
+-    // Otherwise, could be we were supposed to sanitize on shutdown but we
+-    // didn't have a chance, due to an earlier crash.
+-    // In such a case, just redo a shutdown sanitize now, during startup.
+-    await sanitizeOnShutdown();
++  // Sanitization is complete.
++  TelemetryStopwatch.finish("FX_SANITIZE_TOTAL", refObj);
++  // Reset the inProgress preference since we were not killed during
++  // sanitization.
++  Services.prefs.clearUserPref(Sanitizer.PREF_SANITIZE_IN_PROGRESS);
++  progress = {};
++  if (seenError) {
++    throw new Error("Error sanitizing");
+   }
+-};
++}
+ 
+-var sanitizeOnShutdown = async function(options = {}) {
++async function sanitizeOnShutdown(options = {}) {
+   if (!Services.prefs.getBoolPref(Sanitizer.PREF_SANITIZE_ON_SHUTDOWN)) {
+     return;
+   }
+   // Need to sanitize upon shutdown
+-  let s = new Sanitizer();
+-  s.prefDomain = "privacy.clearOnShutdown.";
+-  await s.sanitize(null, options);
++  options.prefDomain = "privacy.clearOnShutdown.";
++  await Sanitizer.sanitize(null, options);
+   // We didn't crash during shutdown sanitization, so annotate it to avoid
+   // sanitizing again on startup.
+   Services.prefs.setBoolPref(Sanitizer.PREF_SANITIZE_DID_SHUTDOWN, true);
+   Services.prefs.savePrefFile(null);
+-};
++}

+ 393 - 0
frg/mozilla-release/work-js/1167238-3-60a1.patch

@@ -0,0 +1,393 @@
+# HG changeset patch
+# User Johann Hofmann <jhofmann@mozilla.com>
+# Date 1516287724 -3600
+# Node ID d81e94b050914a6bf4395fd039246248997149b5
+# Parent  2d3a49eaffb9211e72cf3cd7c1c1611a620b7bf4
+Bug 1167238 - Part 3 - Clean up usage of sanitize.js to properly use Sanitizer.jsm. r=mak
+
+This replaces all non-test usage of sanitize.js or legacy Sanitizer.jsm
+to use the new Sanitizer.jsm module which does not hold internal state
+and instead receives all configuration through function arguments (or by reading prefs).
+
+MozReview-Commit-ID: KitMVptuIG3
+
+diff --git a/browser/base/content/sanitizeDialog.js b/browser/base/content/sanitizeDialog.js
+--- a/browser/base/content/sanitizeDialog.js
++++ b/browser/base/content/sanitizeDialog.js
+@@ -29,19 +29,16 @@ var gSanitizePromptDialog = {
+   get warningBox() {
+     return document.getElementById("sanitizeEverythingWarningBox");
+   },
+ 
+   init() {
+     // This is used by selectByTimespan() to determine if the window has loaded.
+     this._inited = true;
+ 
+-    var s = new Sanitizer();
+-    s.prefDomain = "privacy.cpd.";
+-
+     document.documentElement.getButton("accept").label =
+       this.bundleBrowser.getString("sanitizeButtonOK");
+ 
+     if (this.selectedTimespan === Sanitizer.TIMESPAN_EVERYTHING) {
+       this.prepareWarning();
+       this.warningBox.hidden = false;
+       document.title =
+         this.bundleBrowser.getString("sanitizeDialog2.everything.title");
+@@ -76,37 +73,39 @@ var gSanitizePromptDialog = {
+     }
+     window.document.title =
+       window.document.documentElement.getAttribute("noneverythingtitle");
+   },
+ 
+   sanitize() {
+     // Update pref values before handing off to the sanitizer (bug 453440)
+     this.updatePrefs();
+-    var s = new Sanitizer();
+-    s.prefDomain = "privacy.cpd.";
+-
+-    s.range = Sanitizer.getClearRange(this.selectedTimespan);
+-    s.ignoreTimespan = !s.range;
+ 
+     // As the sanitize is async, we disable the buttons, update the label on
+     // the 'accept' button to indicate things are happening and return false -
+     // once the async operation completes (either with or without errors)
+     // we close the window.
+     let docElt = document.documentElement;
+     let acceptButton = docElt.getButton("accept");
+     acceptButton.disabled = true;
+     acceptButton.setAttribute("label",
+                               this.bundleBrowser.getString("sanitizeButtonClearing"));
+     docElt.getButton("cancel").disabled = true;
+ 
+     try {
+-      s.sanitize().catch(Cu.reportError)
+-                  .then(() => window.close())
+-                  .catch(Cu.reportError);
++      let range = Sanitizer.getClearRange(this.selectedTimespan);
++      let options = {
++        prefDomain: "privacy.cpd.",
++        ignoreTimespan: !range,
++        range,
++      };
++      Sanitizer.sanitize(null, options)
++        .catch(Cu.reportError)
++        .then(() => window.close())
++        .catch(Cu.reportError);
+       return false;
+     } catch (er) {
+       Cu.reportError("Exception during sanitize: " + er);
+       return true; // We *do* want to close immediately on error.
+     }
+   },
+ 
+   /**
+@@ -167,17 +166,17 @@ var gSanitizePromptDialog = {
+    * Sanitizer.prototype.sanitize() requires the prefs to be up-to-date.
+    * Because the type of this prefwindow is "child" -- and that's needed because
+    * without it the dialog has no OK and Cancel buttons -- the prefs are not
+    * updated on dialogaccept on platforms that don't support instant-apply
+    * (i.e., Windows).  We must therefore manually set the prefs from their
+    * corresponding preference elements.
+    */
+   updatePrefs() {
+-    Sanitizer.prefs.setIntPref("timeSpan", this.selectedTimespan);
++    Services.prefs.setIntPref(Sanitizer.PREF_TIMESPAN, this.selectedTimespan);
+ 
+     // Keep the pref for the download history in sync with the history pref.
+     document.getElementById("privacy.cpd.downloads").value =
+       document.getElementById("privacy.cpd.history").value;
+ 
+     // Now manually set the prefs from their corresponding preference
+     // elements.
+     var prefs = this.sanitizePreferences.rootBranch;
+diff --git a/browser/base/moz.build b/browser/base/moz.build
+--- a/browser/base/moz.build
++++ b/browser/base/moz.build
+@@ -27,16 +27,17 @@ BROWSER_CHROME_MANIFESTS += [
+     'content/test/newtab/browser.ini',
+     'content/test/pageinfo/browser.ini',
+     'content/test/performance/browser.ini',
+     'content/test/permissions/browser.ini',
+     'content/test/plugins/browser.ini',
+     'content/test/popupNotifications/browser.ini',
+     'content/test/popups/browser.ini',
+     'content/test/referrer/browser.ini',
++    'content/test/sanitize/browser.ini',
+     'content/test/sidebar/browser.ini',
+     'content/test/siteIdentity/browser.ini',
+     'content/test/static/browser.ini',
+     'content/test/sync/browser.ini',
+     'content/test/tabcrashed/browser.ini',
+     'content/test/tabPrompts/browser.ini',
+     'content/test/tabs/browser.ini',
+     'content/test/touch/browser.ini',
+diff --git a/browser/components/customizableui/CustomizableWidgets.jsm b/browser/components/customizableui/CustomizableWidgets.jsm
+--- a/browser/components/customizableui/CustomizableWidgets.jsm
++++ b/browser/components/customizableui/CustomizableWidgets.jsm
+@@ -13,16 +13,17 @@ ChromeUtils.import("resource://gre/modul
+ 
+ XPCOMUtils.defineLazyModuleGetters(this, {
+   PlacesUtils: "resource://gre/modules/PlacesUtils.jsm",
+   PlacesUIUtils: "resource:///modules/PlacesUIUtils.jsm",
+   RecentlyClosedTabsAndWindowsMenuUtils: "resource:///modules/sessionstore/RecentlyClosedTabsAndWindowsMenuUtils.jsm",
+   ShortcutUtils: "resource://gre/modules/ShortcutUtils.jsm",
+   CharsetMenu: "resource://gre/modules/CharsetMenu.jsm",
+   PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.jsm",
++  Sanitizer: "resource:///modules/Sanitizer.jsm",
+   SyncedTabs: "resource://services-sync/SyncedTabs.jsm",
+   ContextualIdentityService: "resource://gre/modules/ContextualIdentityService.jsm",
+ });
+ 
+ XPCOMUtils.defineLazyGetter(this, "CharsetBundle", function() {
+   const kCharsetBundle = "chrome://global/locale/charsetMenu.properties";
+   return Services.strings.createBundle(kCharsetBundle);
+ });
+@@ -1220,44 +1221,31 @@ if (AppConstants.platform == "win") {
+ }
+ CustomizableWidgets.push(preferencesButton);
+ 
+ if (Services.prefs.getBoolPref("privacy.panicButton.enabled")) {
+   CustomizableWidgets.push({
+     id: "panic-button",
+     type: "view",
+     viewId: "PanelUI-panicView",
+-    _sanitizer: null,
+-    _ensureSanitizer() {
+-      if (!this.sanitizer) {
+-        let scope = {};
+-        Services.scriptloader.loadSubScript("chrome://browser/content/sanitize.js",
+-                                            scope);
+-        this._Sanitizer = scope.Sanitizer;
+-        this._sanitizer = new scope.Sanitizer();
+-        this._sanitizer.ignoreTimespan = false;
+-      }
+-    },
+-    _getSanitizeRange(aDocument) {
+-      let group = aDocument.getElementById("PanelUI-panic-timeSpan");
+-      return this._Sanitizer.getClearRange(+group.value);
+-    },
++
+     forgetButtonCalled(aEvent) {
+       let doc = aEvent.target.ownerDocument;
+-      this._ensureSanitizer();
+-      this._sanitizer.range = this._getSanitizeRange(doc);
+       let group = doc.getElementById("PanelUI-panic-timeSpan");
+       group.selectedItem = doc.getElementById("PanelUI-panic-5min");
+       let itemsToClear = [
+         "cookies", "history", "openWindows", "formdata", "sessions", "cache", "downloads"
+       ];
+       let newWindowPrivateState = PrivateBrowsingUtils.isWindowPrivate(doc.defaultView) ?
+                                   "private" : "non-private";
+-      this._sanitizer.items.openWindows.privateStateForNewWindow = newWindowPrivateState;
+-      let promise = this._sanitizer.sanitize(itemsToClear);
++      let promise = Sanitizer.sanitize(itemsToClear, {
++        ignoreTimespan: false,
++        range: Sanitizer.getClearRange(+group.value),
++        privateStateForNewWindow: newWindowPrivateState,
++      });
+       promise.then(function() {
+         let otherWindow = Services.wm.getMostRecentWindow("navigator:browser");
+         if (otherWindow.closed) {
+           Cu.reportError("Got a closed window!");
+         }
+         if (otherWindow.PanicButtonNotifier) {
+           otherWindow.PanicButtonNotifier.notify();
+         } else {
+diff --git a/browser/components/extensions/ext-browsingData.js b/browser/components/extensions/ext-browsingData.js
+--- a/browser/components/extensions/ext-browsingData.js
++++ b/browser/components/extensions/ext-browsingData.js
+@@ -22,38 +22,30 @@ XPCOMUtils.defineLazyServiceGetter(this,
+                                    "nsIQuotaManagerService");
+ 
+ /**
+ * A number of iterations after which to yield time back
+ * to the system.
+ */
+ const YIELD_PERIOD = 10;
+ 
+-const PREF_DOMAIN = "privacy.cpd.";
+-
+-XPCOMUtils.defineLazyGetter(this, "sanitizer", () => {
+-  let sanitizer = new Sanitizer();
+-  sanitizer.prefDomain = PREF_DOMAIN;
+-  return sanitizer;
+-});
+-
+ const makeRange = options => {
+   return (options.since == null) ?
+     null :
+     [PlacesUtils.toPRTime(options.since), PlacesUtils.toPRTime(Date.now())];
+ };
+ 
+ const clearCache = () => {
+   // Clearing the cache does not support timestamps.
+-  return sanitizer.items.cache.clear();
++  return Sanitizer.items.cache.clear();
+ };
+ 
+ const clearCookies = async function(options) {
+   let cookieMgr = Services.cookies;
+-  // This code has been borrowed from sanitize.js.
++  // This code has been borrowed from Sanitizer.jsm.
+   let yieldCounter = 0;
+ 
+   if (options.since || options.hostnames) {
+     // Iterate through the cookies and delete any created after our cutoff.
+     for (const cookie of XPCOMUtils.IterSimpleEnumerator(cookieMgr.enumerator, Ci.nsICookie2)) {
+       if ((!options.since || cookie.creationTime >= PlacesUtils.toPRTime(options.since)) &&
+           (!options.hostnames || options.hostnames.includes(cookie.host.replace(/^\./, "")))) {
+         // This cookie was created after our cutoff, clear it.
+@@ -67,25 +59,25 @@ const clearCookies = async function(opti
+     }
+   } else {
+     // Remove everything.
+     cookieMgr.removeAll();
+   }
+ };
+ 
+ const clearDownloads = options => {
+-  return sanitizer.items.downloads.clear(makeRange(options));
++  return Sanitizer.items.downloads.clear(makeRange(options));
+ };
+ 
+ const clearFormData = options => {
+-  return sanitizer.items.formdata.clear(makeRange(options));
++  return Sanitizer.items.formdata.clear(makeRange(options));
+ };
+ 
+ const clearHistory = options => {
+-  return sanitizer.items.history.clear(makeRange(options));
++  return Sanitizer.items.history.clear(makeRange(options));
+ };
+ 
+ const clearIndexedDB = async function(options) {
+   let promises = [];
+ 
+   await new Promise(resolve => {
+     quotaManagerService.getUsage(request => {
+       if (request.resultCode != Cr.NS_OK) {
+diff --git a/browser/components/nsBrowserGlue.js b/browser/components/nsBrowserGlue.js
+--- a/browser/components/nsBrowserGlue.js
++++ b/browser/components/nsBrowserGlue.js
+@@ -49,16 +49,17 @@ XPCOMUtils.defineLazyModuleGetters(this,
+   PlacesUtils: "resource://gre/modules/PlacesUtils.jsm",
+   PluralForm: "resource://gre/modules/PluralForm.jsm",
+   PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.jsm",
+   ProcessHangMonitor: "resource:///modules/ProcessHangMonitor.jsm",
+   ReaderParent: "resource:///modules/ReaderParent.jsm",
+   RecentWindow: "resource:///modules/RecentWindow.jsm",
+   RemotePrompt: "resource:///modules/RemotePrompt.jsm",
+   SafeBrowsing: "resource://gre/modules/SafeBrowsing.jsm",
++  Sanitizer: "resource:///modules/Sanitizer.jsm",
+   SessionStore: "resource:///modules/sessionstore/SessionStore.jsm",
+   ShellService: "resource:///modules/ShellService.jsm",
+   TabCrashHandler: "resource:///modules/ContentCrashHandlers.jsm",
+   UIState: "resource://services-sync/UIState.jsm",
+   WebChannel: "resource://gre/modules/WebChannel.jsm",
+   WindowsRegistry: "resource://gre/modules/WindowsRegistry.jsm",
+ });
+ 
+@@ -227,23 +228,16 @@ function BrowserGlue() {
+                                      "@mozilla.org/widget/idleservice;1",
+                                      "nsIIdleService");
+ 
+   XPCOMUtils.defineLazyGetter(this, "_distributionCustomizer", function() {
+                                 ChromeUtils.import("resource:///modules/distribution.js");
+                                 return new DistributionCustomizer();
+                               });
+ 
+-  XPCOMUtils.defineLazyGetter(this, "_sanitizer",
+-    function() {
+-      let sanitizerScope = {};
+-      Services.scriptloader.loadSubScript("chrome://browser/content/sanitize.js", sanitizerScope);
+-      return sanitizerScope.Sanitizer;
+-    });
+-
+   ChromeUtils.defineModuleGetter(this, "fxAccounts", "resource://gre/modules/FxAccounts.jsm");
+   XPCOMUtils.defineLazyServiceGetter(this, "AlertsService", "@mozilla.org/alerts-service;1", "nsIAlertsService");
+ 
+   this._init();
+ }
+ 
+ /*
+  * OS X has the concept of zero-window sessions and therefore ignores the
+@@ -461,17 +455,17 @@ BrowserGlue.prototype = {
+             if (addon.type != "experiment") {
+               this._notifyUnsignedAddonsDisabled();
+               break;
+             }
+           }
+         });
+         break;
+       case "test-initialize-sanitizer":
+-        this._sanitizer.onStartup();
++        Sanitizer.onStartup();
+         break;
+       case "sync-ui-state:update":
+         this._updateFxaBadges();
+         break;
+       case "handlersvc-store-initialized":
+         // Initialize PdfJs when running in-process and remote. This only
+         // happens once since PdfJs registers global hooks. If the PdfJs
+         // extension is installed the init method below will be overridden
+@@ -1106,17 +1100,17 @@ BrowserGlue.prototype = {
+ 
+      // Login reputation depends on the Safe Browsing API.
+      if (Services.prefs.getBoolPref("browser.safebrowsing.passwords.enabled")) {
+        Cc["@mozilla.org/reputationservice/login-reputation-service;1"]
+          .getService(Ci.ILoginReputationService);
+      }
+     }, 5000);
+ 
+-    this._sanitizer.onStartup();
++    Sanitizer.onStartup();
+ 
+     Services.tm.idleDispatchToMainThread(() => {
+       let handlerService = Cc["@mozilla.org/uriloader/handler-service;1"].
+                            getService(Ci.nsIHandlerService);
+       handlerService.asyncInit();
+     });
+ 
+     if (AppConstants.platform == "win") {
+@@ -2107,17 +2101,17 @@ BrowserGlue.prototype = {
+     Services.prefs.setIntPref("browser.migration.version", UI_VERSION);
+   },
+ 
+   // ------------------------------
+   // public nsIBrowserGlue members
+   // ------------------------------
+ 
+   sanitize: function BG_sanitize(aParentWindow) {
+-    this._sanitizer.sanitize(aParentWindow);
++    Sanitizer.showUI(aParentWindow);
+   },
+ 
+   async ensurePlacesDefaultQueriesInitialized() {
+     // This is the current smart bookmarks version, it must be increased every
+     // time they change.
+     // When adding a new smart bookmark below, its newInVersion property must
+     // be set to the version it has been added in.  We will compare its value
+     // to users' smartBookmarksVersion and add new smart bookmarks without
+diff --git a/toolkit/components/places/Shutdown.h b/toolkit/components/places/Shutdown.h
+--- a/toolkit/components/places/Shutdown.h
++++ b/toolkit/components/places/Shutdown.h
+@@ -23,17 +23,17 @@ class Database;
+  * that forwards the notification to the Database instance).
+  * Database::Observe first of all checks if initialization was completed
+  * properly, to avoid race conditions, then it notifies "places-shutdown" to
+  * legacy clients. Legacy clients are supposed to start and complete any
+  * shutdown critical work in the same tick, since we won't wait for them.
+ 
+  * PHASE 2 (Modern clients shutdown)
+  * Modern clients should instead register as a blocker by passing a promise to
+- * nsPIPlacesDatabase::shutdownClient (for example see sanitize.js), so they
++ * nsPIPlacesDatabase::shutdownClient (for example see Sanitizer.jsm), so they
+  * block Places shutdown until the promise is resolved.
+  * When profile-change-teardown is observed by async shutdown, it calls
+  * ClientsShutdownBlocker::BlockShutdown. This class is registered as a teardown
+  * phase blocker in Database::Init (see Database::mClientsShutdown).
+  * ClientsShutdownBlocker::BlockShudown waits for all the clients registered
+  * through nsPIPlacesDatabase::shutdownClient. When all the clients are done,
+  * its `Done` method is invoked, and it stops blocking the shutdown phase, so
+  * that it can continue.

+ 739 - 0
frg/mozilla-release/work-js/1167238-4-60a1.patch

@@ -0,0 +1,739 @@
+# HG changeset patch
+# User Johann Hofmann <jhofmann@mozilla.com>
+# Date 1516287936 -3600
+# Node ID 092f32e30ed952c0c62a7f2b4c5236df5dc42e41
+# Parent  345bf30bcd4240a96a68c271b5dc06058a97e83d
+Bug 1167238 - Part 4 - Move sanitize tests into a topical directory and clean up sanitize.js usage. r=mak
+
+We're changing the way sanitize.js/Sanitizer.jsm works and need to adjust a lot of tests to that.
+I'm using this opportunity to also move the sanitization tests into their
+own topical directory and out of b/b/c/test/general.
+
+MozReview-Commit-ID: GHOqr4hT52b
+
+diff --git a/browser/base/content/moz.build b/browser/base/content/moz.build
+--- a/browser/base/content/moz.build
++++ b/browser/base/content/moz.build
+@@ -62,16 +62,20 @@ with Files("test/popupNotifications/**")
+     BUG_COMPONENT = ("Toolkit", "Notifications and Alerts")
+ 
+ with Files("test/popups/**"):
+     BUG_COMPONENT = ("Toolkit", "Notifications and Alerts")
+ 
+ with Files("test/referrer/**"):
+     BUG_COMPONENT = ("Core", "Document Navigation")
+ 
++# TODO: Update this after bug 1435528 is resolved.
++with Files("test/sanitize/**"):
++    BUG_COMPONENT = ("Firefox", "General")
++
+ with Files("test/siteIdentity/**"):
+     BUG_COMPONENT = ("Firefox", "Site Identity and Permission Panels")
+ 
+ with Files("test/sidebar/**"):
+     BUG_COMPONENT = ("Firefox", "General")
+ 
+ with Files("test/static/**"):
+     BUG_COMPONENT = ("Firefox", "General")
+diff --git a/browser/base/content/test/general/browser.ini b/browser/base/content/test/general/browser.ini
+--- a/browser/base/content/test/general/browser.ini
++++ b/browser/base/content/test/general/browser.ini
+@@ -118,18 +118,16 @@ skip-if = true # browser_bug321000.js is
+ [browser_bug380960.js]
+ # DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
+ [browser_bug386835.js]
+ # DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
+ [browser_bug406216.js]
+ # DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
+ [browser_bug408415.js]
+ # DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
+-[browser_bug409624.js]
+-# DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
+ [browser_bug413915.js]
+ # DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
+ [browser_bug416661.js]
+ # DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
+ [browser_bug417483.js]
+ # DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
+ [browser_bug419612.js]
+ # DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
+@@ -400,18 +398,16 @@ support-files = test_offline_gzip.html g
+ # DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
+ [browser_printpreview.js]
+ skip-if = os == 'win' # Bug 1384127
+ # DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
+ [browser_private_browsing_window.js]
+ # DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
+ [browser_private_no_prompt.js]
+ # DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
+-[browser_purgehistory_clears_sh.js]
+-# DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
+ [browser_PageMetaData_pushstate.js]
+ # DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
+ [browser_refreshBlocker.js]
+ support-files =
+   refresh_header.sjs
+   refresh_meta.sjs
+ # DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
+ [browser_relatedTabs.js]
+@@ -423,24 +419,16 @@ support-files =
+   test_remoteTroubleshoot.html
+ # DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
+ [browser_remoteWebNavigation_postdata.js]
+ # DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
+ [browser_removeTabsToTheEnd.js]
+ # DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
+ [browser_restore_isAppTab.js]
+ # DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
+-[browser_sanitize-passwordDisabledHosts.js]
+-# DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
+-[browser_sanitize-sitepermissions.js]
+-# DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
+-[browser_sanitize-timespans.js]
+-# DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
+-[browser_sanitizeDialog.js]
+-# DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
+ [browser_save_link-perwindowpb.js]
+ skip-if = e10s && debug && os == "win" # Bug 1280505
+ # DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
+ [browser_save_private_link_perwindowpb.js]
+ # DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
+ [browser_save_link_when_window_navigates.js]
+ # DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
+ [browser_save_video.js]
+diff --git a/browser/base/content/test/general/head.js b/browser/base/content/test/general/head.js
+--- a/browser/base/content/test/general/head.js
++++ b/browser/base/content/test/general/head.js
+@@ -264,32 +264,16 @@ function whenTabLoaded(aTab, aCallback) 
+ }
+ 
+ function promiseTabLoaded(aTab) {
+   return new Promise(resolve => {
+     whenTabLoaded(aTab, resolve);
+   });
+ }
+ 
+-/**
+- * Ensures that the specified URIs are either cleared or not.
+- *
+- * @param aURIs
+- *        Array of page URIs
+- * @param aShouldBeCleared
+- *        True if each visit to the URI should be cleared, false otherwise
+- */
+-async function promiseHistoryClearedState(aURIs, aShouldBeCleared) {
+-  for (let uri of aURIs) {
+-    let visited = await PlacesUtils.history.hasVisits(uri);
+-    Assert.equal(visited, !aShouldBeCleared,
+-      `history visit ${uri.spec} should ${aShouldBeCleared ? "no longer" : "still"} exist`);
+-  }
+-}
+-
+ var FullZoomHelper = {
+ 
+   selectTabAndWaitForLocationChange: function selectTabAndWaitForLocationChange(tab) {
+     if (!tab)
+       throw new Error("tab must be given.");
+     if (gBrowser.selectedTab == tab)
+       return Promise.resolve();
+ 
+diff --git a/browser/base/content/test/sanitize/.eslintrc.js b/browser/base/content/test/sanitize/.eslintrc.js
+new file mode 100644
+--- /dev/null
++++ b/browser/base/content/test/sanitize/.eslintrc.js
+@@ -0,0 +1,7 @@
++"use strict";
++
++module.exports = {
++  "extends": [
++    "plugin:mozilla/browser-test"
++  ]
++};
+diff --git a/browser/base/content/test/sanitize/browser.ini b/browser/base/content/test/sanitize/browser.ini
+new file mode 100644
+--- /dev/null
++++ b/browser/base/content/test/sanitize/browser.ini
+@@ -0,0 +1,11 @@
++[DEFAULT]
++support-files=
++  head.js
++  dummy_page.html
++
++[browser_purgehistory_clears_sh.js]
++[browser_sanitize-formhistory.js]
++[browser_sanitize-passwordDisabledHosts.js]
++[browser_sanitize-sitepermissions.js]
++[browser_sanitize-timespans.js]
++[browser_sanitizeDialog.js]
+diff --git a/browser/base/content/test/general/browser_purgehistory_clears_sh.js b/browser/base/content/test/sanitize/browser_purgehistory_clears_sh.js
+rename from browser/base/content/test/general/browser_purgehistory_clears_sh.js
+rename to browser/base/content/test/sanitize/browser_purgehistory_clears_sh.js
+--- a/browser/base/content/test/general/browser_purgehistory_clears_sh.js
++++ b/browser/base/content/test/sanitize/browser_purgehistory_clears_sh.js
+@@ -1,12 +1,12 @@
+ /* Any copyright is dedicated to the Public Domain.
+  * http://creativecommons.org/publicdomain/zero/1.0/ */
+ 
+-const url = "http://example.org/browser/browser/base/content/test/general/dummy_page.html";
++const url = "http://example.org/browser/browser/base/content/test/sanitize/dummy_page.html";
+ 
+ add_task(async function purgeHistoryTest() {
+   await BrowserTestUtils.withNewTab({
+     gBrowser,
+     url,
+   }, async function purgeHistoryTestInner(browser) {
+     let backButton = browser.ownerDocument.getElementById("Browser:Back");
+     let forwardButton = browser.ownerDocument.getElementById("Browser:Forward");
+@@ -30,24 +30,17 @@ add_task(async function purgeHistoryTest
+ 
+     ok(browser.webNavigation.canGoBack, true,
+        "New value for webNavigation.canGoBack");
+     ok(browser.webNavigation.canGoForward, true,
+        "New value for webNavigation.canGoForward");
+     ok(!backButton.hasAttribute("disabled"), "Back button was enabled");
+     ok(!forwardButton.hasAttribute("disabled"), "Forward button was enabled");
+ 
+-
+-    let tmp = {};
+-    Services.scriptloader.loadSubScript("chrome://browser/content/sanitize.js", tmp);
+-
+-    let {Sanitizer} = tmp;
+-    let sanitizer = new Sanitizer();
+-
+-    await sanitizer.sanitize(["history"]);
++    await Sanitizer.sanitize(["history"]);
+ 
+     await ContentTask.spawn(browser, null, async function() {
+       Assert.equal(content.history.length, 1, "SHistory correctly cleared");
+     });
+ 
+     ok(!browser.webNavigation.canGoBack,
+        "webNavigation.canGoBack correctly cleared");
+     ok(!browser.webNavigation.canGoForward,
+diff --git a/browser/base/content/test/general/browser_bug409624.js b/browser/base/content/test/sanitize/browser_sanitize-formhistory.js
+rename from browser/base/content/test/general/browser_bug409624.js
+rename to browser/base/content/test/sanitize/browser_sanitize-formhistory.js
+--- a/browser/base/content/test/general/browser_bug409624.js
++++ b/browser/base/content/test/sanitize/browser_sanitize-formhistory.js
+@@ -1,15 +1,12 @@
+ /* This Source Code Form is subject to the terms of the Mozilla Public
+  *  License, v. 2.0. If a copy of the MPL was not distributed with this
+  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+ 
+-ChromeUtils.defineModuleGetter(this, "FormHistory",
+-                               "resource://gre/modules/FormHistory.jsm");
+-
+ add_task(async function test() {
+   // This test relies on the form history being empty to start with delete
+   // all the items first.
+   await new Promise((resolve, reject) => {
+     FormHistory.update({ op: "remove" },
+                        { handleError(error) {
+                            reject(error);
+                          },
+@@ -18,37 +15,19 @@ add_task(async function test() {
+                              resolve();
+                            } else {
+                              reject();
+                            }
+                          },
+                        });
+   });
+ 
+-
+-  let tempScope = {};
+-  Services.scriptloader.loadSubScript("chrome://browser/content/sanitize.js", tempScope);
+-  let Sanitizer = tempScope.Sanitizer;
+-  let s = new Sanitizer();
+-  s.prefDomain = "privacy.cpd.";
+-  let prefBranch = Services.prefs.getBranch(s.prefDomain);
+-
+-  prefBranch.setBoolPref("cache", false);
+-  prefBranch.setBoolPref("cookies", false);
+-  prefBranch.setBoolPref("downloads", false);
+-  prefBranch.setBoolPref("formdata", true);
+-  prefBranch.setBoolPref("history", false);
+-  prefBranch.setBoolPref("offlineApps", false);
+-  prefBranch.setBoolPref("passwords", false);
+-  prefBranch.setBoolPref("sessions", false);
+-  prefBranch.setBoolPref("siteSettings", false);
+-
+   // Sanitize now so we can test the baseline point.
+-  await s.sanitize();
++  await Sanitizer.sanitize(["formdata"]);
+   ok(!gFindBar.hasTransactions, "pre-test baseline for sanitizer");
+ 
+   gFindBar.getElement("findbar-textbox").value = "m";
+   ok(gFindBar.hasTransactions, "formdata can be cleared after input");
+ 
+-  await s.sanitize();
++  await Sanitizer.sanitize(["formdata"]);
+   is(gFindBar.getElement("findbar-textbox").value, "", "findBar textbox should be empty after sanitize");
+   ok(!gFindBar.hasTransactions, "No transactions after sanitize");
+ });
+diff --git a/browser/base/content/test/general/browser_sanitize-passwordDisabledHosts.js b/browser/base/content/test/sanitize/browser_sanitize-passwordDisabledHosts.js
+rename from browser/base/content/test/general/browser_sanitize-passwordDisabledHosts.js
+rename to browser/base/content/test/sanitize/browser_sanitize-passwordDisabledHosts.js
+--- a/browser/base/content/test/general/browser_sanitize-passwordDisabledHosts.js
++++ b/browser/base/content/test/sanitize/browser_sanitize-passwordDisabledHosts.js
+@@ -1,41 +1,22 @@
+ // Bug 474792 - Clear "Never remember passwords for this site" when
+ // clearing site-specific settings in Clear Recent History dialog
+ 
+-var tempScope = {};
+-Services.scriptloader.loadSubScript("chrome://browser/content/sanitize.js", tempScope);
+-var Sanitizer = tempScope.Sanitizer;
+-
+ add_task(async function() {
+   // getLoginSavingEnabled always returns false if password capture is disabled.
+   await SpecialPowers.pushPrefEnv({"set": [["signon.rememberSignons", true]]});
+ 
+   // Add a disabled host
+   Services.logins.setLoginSavingEnabled("http://example.com", false);
+   // Sanity check
+   is(Services.logins.getLoginSavingEnabled("http://example.com"), false,
+      "example.com should be disabled for password saving since we haven't cleared that yet.");
+ 
+-  // Set up the sanitizer to just clear siteSettings
+-  let s = new Sanitizer();
+-  s.ignoreTimespan = false;
+-  s.prefDomain = "privacy.cpd.";
+-  var itemPrefs = Services.prefs.getBranch(s.prefDomain);
+-  itemPrefs.setBoolPref("history", false);
+-  itemPrefs.setBoolPref("downloads", false);
+-  itemPrefs.setBoolPref("cache", false);
+-  itemPrefs.setBoolPref("cookies", false);
+-  itemPrefs.setBoolPref("formdata", false);
+-  itemPrefs.setBoolPref("offlineApps", false);
+-  itemPrefs.setBoolPref("passwords", false);
+-  itemPrefs.setBoolPref("sessions", false);
+-  itemPrefs.setBoolPref("siteSettings", true);
+-
+   // Clear it
+-  await s.sanitize();
++  await Sanitizer.sanitize(["siteSettings"], {ignoreTimespan: false});
+ 
+   // Make sure it's gone
+   is(Services.logins.getLoginSavingEnabled("http://example.com"), true,
+      "example.com should be enabled for password saving again now that we've cleared.");
+ 
+   await SpecialPowers.popPrefEnv();
+ });
+diff --git a/browser/base/content/test/general/browser_sanitize-sitepermissions.js b/browser/base/content/test/sanitize/browser_sanitize-sitepermissions.js
+rename from browser/base/content/test/general/browser_sanitize-sitepermissions.js
+rename to browser/base/content/test/sanitize/browser_sanitize-sitepermissions.js
+--- a/browser/base/content/test/general/browser_sanitize-sitepermissions.js
++++ b/browser/base/content/test/sanitize/browser_sanitize-sitepermissions.js
+@@ -1,51 +1,33 @@
+ // Bug 380852 - Delete permission manager entries in Clear Recent History
+ 
+-var tempScope = {};
+-Services.scriptloader.loadSubScript("chrome://browser/content/sanitize.js", tempScope);
+-var Sanitizer = tempScope.Sanitizer;
+-
+ function countPermissions() {
+   let result = 0;
+   let enumerator = Services.perms.enumerator;
+   while (enumerator.hasMoreElements()) {
+     result++;
+     enumerator.getNext();
+   }
+   return result;
+ }
+ 
+ add_task(async function test() {
+   // sanitize before we start so we have a good baseline.
+-  // Set up the sanitizer to just clear siteSettings
+-  let s = new Sanitizer();
+-  s.ignoreTimespan = false;
+-  s.prefDomain = "privacy.cpd.";
+-  var itemPrefs = Services.prefs.getBranch(s.prefDomain);
+-  itemPrefs.setBoolPref("history", false);
+-  itemPrefs.setBoolPref("downloads", false);
+-  itemPrefs.setBoolPref("cache", false);
+-  itemPrefs.setBoolPref("cookies", false);
+-  itemPrefs.setBoolPref("formdata", false);
+-  itemPrefs.setBoolPref("offlineApps", false);
+-  itemPrefs.setBoolPref("passwords", false);
+-  itemPrefs.setBoolPref("sessions", false);
+-  itemPrefs.setBoolPref("siteSettings", true);
+-  s.sanitize();
++  await Sanitizer.sanitize(["siteSettings"], {ignoreTimespan: false});
+ 
+   // Count how many permissions we start with - some are defaults that
+   // will not be sanitized.
+   let numAtStart = countPermissions();
+ 
+   // Add a permission entry
+   var pm = Services.perms;
+-  pm.add(makeURI("http://example.com"), "testing", pm.ALLOW_ACTION);
++  pm.add(Services.io.newURI("http://example.com"), "testing", pm.ALLOW_ACTION);
+ 
+   // Sanity check
+   ok(pm.enumerator.hasMoreElements(), "Permission manager should have elements, since we just added one");
+ 
+   // Clear it
+-  await s.sanitize();
++  await Sanitizer.sanitize(["siteSettings"], {ignoreTimespan: false});
+ 
+   // Make sure it's gone
+   is(numAtStart, countPermissions(), "Permission manager should have the same count it started with");
+ });
+diff --git a/browser/base/content/test/general/browser_sanitize-timespans.js b/browser/base/content/test/sanitize/browser_sanitize-timespans.js
+rename from browser/base/content/test/general/browser_sanitize-timespans.js
+rename to browser/base/content/test/sanitize/browser_sanitize-timespans.js
+--- a/browser/base/content/test/general/browser_sanitize-timespans.js
++++ b/browser/base/content/test/sanitize/browser_sanitize-timespans.js
+@@ -1,27 +1,17 @@
+-ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
+-ChromeUtils.import("resource://gre/modules/Services.jsm");
+-
+ requestLongerTimeout(2);
+ 
+ // Bug 453440 - Test the timespan-based logic of the sanitizer code
+ var now_mSec = Date.now();
+ var now_uSec = now_mSec * 1000;
+ 
+ const kMsecPerMin = 60 * 1000;
+ const kUsecPerMin = 60 * 1000000;
+ 
+-var tempScope = {};
+-Services.scriptloader.loadSubScript("chrome://browser/content/sanitize.js", tempScope);
+-var Sanitizer = tempScope.Sanitizer;
+-
+-var FormHistory = (ChromeUtils.import("resource://gre/modules/FormHistory.jsm", {})).FormHistory;
+-var Downloads = (ChromeUtils.import("resource://gre/modules/Downloads.jsm", {})).Downloads;
+-
+ function promiseFormHistoryRemoved() {
+   return new Promise(resolve => {
+     Services.obs.addObserver(function onfh() {
+       Services.obs.removeObserver(onfh, "satchel-storage-changed");
+       resolve();
+     }, "satchel-storage-changed");
+   });
+ }
+@@ -74,38 +64,34 @@ function countEntries(name, message, che
+ 
+ async function onHistoryReady() {
+   var hoursSinceMidnight = new Date().getHours();
+   var minutesSinceMidnight = hoursSinceMidnight * 60 + new Date().getMinutes();
+ 
+   // Should test cookies here, but nsICookieManager/nsICookieService
+   // doesn't let us fake creation times.  bug 463127
+ 
+-  let s = new Sanitizer();
+-  s.ignoreTimespan = false;
+-  s.prefDomain = "privacy.cpd.";
+-  var itemPrefs = Services.prefs.getBranch(s.prefDomain);
++  var itemPrefs = Services.prefs.getBranch("privacy.cpd.");
+   itemPrefs.setBoolPref("history", true);
+   itemPrefs.setBoolPref("downloads", true);
+   itemPrefs.setBoolPref("cache", false);
+   itemPrefs.setBoolPref("cookies", false);
+   itemPrefs.setBoolPref("formdata", true);
+   itemPrefs.setBoolPref("offlineApps", false);
+   itemPrefs.setBoolPref("passwords", false);
+   itemPrefs.setBoolPref("sessions", false);
+   itemPrefs.setBoolPref("siteSettings", false);
+ 
+   let publicList = await Downloads.getList(Downloads.PUBLIC);
+   let downloadPromise = promiseDownloadRemoved(publicList);
+   let formHistoryPromise = promiseFormHistoryRemoved();
+ 
+   // Clear 10 minutes ago
+-  s.range = [now_uSec - 10 * 60 * 1000000, now_uSec];
+-  await s.sanitize();
+-  s.range = null;
++  let range = [now_uSec - 10 * 60 * 1000000, now_uSec];
++  await Sanitizer.sanitize(null, {range, ignoreTimespan: false});
+ 
+   await formHistoryPromise;
+   await downloadPromise;
+ 
+   ok(!(await PlacesUtils.history.hasVisits("http://10minutes.com")),
+      "Pretend visit to 10minutes.com should now be deleted");
+   ok((await PlacesUtils.history.hasVisits("http://1hour.com")),
+      "Pretend visit to 1hour.com should should still exist");
+@@ -151,18 +137,18 @@ async function onHistoryReady() {
+ 
+   if (minutesSinceMidnight > 10)
+     ok((await downloadExists(publicList, "fakefile-today")), "'Today' download should still be present");
+ 
+   downloadPromise = promiseDownloadRemoved(publicList);
+   formHistoryPromise = promiseFormHistoryRemoved();
+ 
+   // Clear 1 hour
+-  Sanitizer.prefs.setIntPref("timeSpan", 1);
+-  await s.sanitize();
++  Services.prefs.setIntPref(Sanitizer.PREF_TIMESPAN, 1);
++  await Sanitizer.sanitize(null, {ignoreTimespan: false});
+ 
+   await formHistoryPromise;
+   await downloadPromise;
+ 
+   ok(!(await PlacesUtils.history.hasVisits("http://1hour.com")),
+      "Pretend visit to 1hour.com should now be deleted");
+   ok((await PlacesUtils.history.hasVisits("http://1hour10minutes.com")),
+      "Pretend visit to 1hour10minutes.com should should still exist");
+@@ -201,19 +187,18 @@ async function onHistoryReady() {
+ 
+   if (hoursSinceMidnight > 1)
+     ok((await downloadExists(publicList, "fakefile-today")), "'Today' download should still be present");
+ 
+   downloadPromise = promiseDownloadRemoved(publicList);
+   formHistoryPromise = promiseFormHistoryRemoved();
+ 
+   // Clear 1 hour 10 minutes
+-  s.range = [now_uSec - 70 * 60 * 1000000, now_uSec];
+-  await s.sanitize();
+-  s.range = null;
++  range = [now_uSec - 70 * 60 * 1000000, now_uSec];
++  await Sanitizer.sanitize(null, {range, ignoreTimespan: false});
+ 
+   await formHistoryPromise;
+   await downloadPromise;
+ 
+   ok(!(await PlacesUtils.history.hasVisits("http://1hour10minutes.com")),
+      "Pretend visit to 1hour10minutes.com should now be deleted");
+   ok((await PlacesUtils.history.hasVisits("http://2hour.com")),
+      "Pretend visit to 2hour.com should should still exist");
+@@ -247,18 +232,18 @@ async function onHistoryReady() {
+   ok((await downloadExists(publicList, "fakefile-4-hour-10-minutes")), "4 hour 10 minute download should still be present");
+   if (minutesSinceMidnight > 70)
+     ok((await downloadExists(publicList, "fakefile-today")), "'Today' download should still be present");
+ 
+   downloadPromise = promiseDownloadRemoved(publicList);
+   formHistoryPromise = promiseFormHistoryRemoved();
+ 
+   // Clear 2 hours
+-  Sanitizer.prefs.setIntPref("timeSpan", 2);
+-  await s.sanitize();
++  Services.prefs.setIntPref(Sanitizer.PREF_TIMESPAN, 2);
++  await Sanitizer.sanitize(null, {ignoreTimespan: false});
+ 
+   await formHistoryPromise;
+   await downloadPromise;
+ 
+   ok(!(await PlacesUtils.history.hasVisits("http://2hour.com")),
+      "Pretend visit to 2hour.com should now be deleted");
+   ok((await PlacesUtils.history.hasVisits("http://2hour10minutes.com")),
+      "Pretend visit to 2hour10minutes.com should should still exist");
+@@ -288,19 +273,18 @@ async function onHistoryReady() {
+   ok((await downloadExists(publicList, "fakefile-4-hour-10-minutes")), "4 hour 10 minute download should still be present");
+   if (hoursSinceMidnight > 2)
+     ok((await downloadExists(publicList, "fakefile-today")), "'Today' download should still be present");
+ 
+   downloadPromise = promiseDownloadRemoved(publicList);
+   formHistoryPromise = promiseFormHistoryRemoved();
+ 
+   // Clear 2 hours 10 minutes
+-  s.range = [now_uSec - 130 * 60 * 1000000, now_uSec];
+-  await s.sanitize();
+-  s.range = null;
++  range = [now_uSec - 130 * 60 * 1000000, now_uSec];
++  await Sanitizer.sanitize(null, {range, ignoreTimespan: false});
+ 
+   await formHistoryPromise;
+   await downloadPromise;
+ 
+   ok(!(await PlacesUtils.history.hasVisits("http://2hour10minutes.com")),
+      "Pretend visit to 2hour10minutes.com should now be deleted");
+   ok((await PlacesUtils.history.hasVisits("http://4hour.com")),
+      "Pretend visit to 4hour.com should should still exist");
+@@ -326,18 +310,18 @@ async function onHistoryReady() {
+   ok((await downloadExists(publicList, "fakefile-old")), "Year old download should still be present");
+   if (minutesSinceMidnight > 130)
+     ok((await downloadExists(publicList, "fakefile-today")), "'Today' download should still be present");
+ 
+   downloadPromise = promiseDownloadRemoved(publicList);
+   formHistoryPromise = promiseFormHistoryRemoved();
+ 
+   // Clear 4 hours
+-  Sanitizer.prefs.setIntPref("timeSpan", 3);
+-  await s.sanitize();
++  Services.prefs.setIntPref(Sanitizer.PREF_TIMESPAN, 3);
++  await Sanitizer.sanitize(null, {ignoreTimespan: false});
+ 
+   await formHistoryPromise;
+   await downloadPromise;
+ 
+   ok(!(await PlacesUtils.history.hasVisits("http://4hour.com")),
+      "Pretend visit to 4hour.com should now be deleted");
+   ok((await PlacesUtils.history.hasVisits("http://4hour10minutes.com")),
+      "Pretend visit to 4hour10minutes.com should should still exist");
+@@ -359,19 +343,18 @@ async function onHistoryReady() {
+   ok((await downloadExists(publicList, "fakefile-old")), "Year old download should still be present");
+   if (hoursSinceMidnight > 4)
+     ok((await downloadExists(publicList, "fakefile-today")), "'Today' download should still be present");
+ 
+   downloadPromise = promiseDownloadRemoved(publicList);
+   formHistoryPromise = promiseFormHistoryRemoved();
+ 
+   // Clear 4 hours 10 minutes
+-  s.range = [now_uSec - 250 * 60 * 1000000, now_uSec];
+-  await s.sanitize();
+-  s.range = null;
++  range = [now_uSec - 250 * 60 * 1000000, now_uSec];
++  await Sanitizer.sanitize(null, {range, ignoreTimespan: false});
+ 
+   await formHistoryPromise;
+   await downloadPromise;
+ 
+   ok(!(await PlacesUtils.history.hasVisits("http://4hour10minutes.com")),
+      "Pretend visit to 4hour10minutes.com should now be deleted");
+   if (minutesSinceMidnight > 250) {
+     ok((await PlacesUtils.history.hasVisits("http://today.com")),
+@@ -395,18 +378,18 @@ async function onHistoryReady() {
+   if (minutesSinceMidnight > 250) {
+     downloadPromise = promiseDownloadRemoved(publicList);
+     formHistoryPromise = promiseFormHistoryRemoved();
+   } else {
+     downloadPromise = formHistoryPromise = Promise.resolve();
+   }
+ 
+   // Clear Today
+-  Sanitizer.prefs.setIntPref("timeSpan", 4);
+-  await s.sanitize();
++  Services.prefs.setIntPref(Sanitizer.PREF_TIMESPAN, 4);
++  await Sanitizer.sanitize(null, {ignoreTimespan: false});
+ 
+   await formHistoryPromise;
+   await downloadPromise;
+ 
+   // Be careful.  If we add our objectss just before midnight, and sanitize
+   // runs immediately after, they won't be expired.  This is expected, but
+   // we should not test in that case.  We cannot just test for opposite
+   // condition because we could cross midnight just one moment after we
+@@ -424,18 +407,18 @@ async function onHistoryReady() {
+      "Pretend visit to before-today.com should still exist");
+   await countEntries("b4today", "b4today form entry should still exist", checkOne);
+   ok((await downloadExists(publicList, "fakefile-old")), "Year old download should still be present");
+ 
+   downloadPromise = promiseDownloadRemoved(publicList);
+   formHistoryPromise = promiseFormHistoryRemoved();
+ 
+   // Choose everything
+-  Sanitizer.prefs.setIntPref("timeSpan", 0);
+-  await s.sanitize();
++  Services.prefs.setIntPref(Sanitizer.PREF_TIMESPAN, 0);
++  await Sanitizer.sanitize(null, {ignoreTimespan: false});
+ 
+   await formHistoryPromise;
+   await downloadPromise;
+ 
+   ok(!(await PlacesUtils.history.hasVisits("http://before-today.com")),
+      "Pretend visit to before-today.com should now be deleted");
+ 
+   await countEntries("b4today", "b4today form entry should be deleted", checkZero);
+diff --git a/browser/base/content/test/general/browser_sanitizeDialog.js b/browser/base/content/test/sanitize/browser_sanitizeDialog.js
+rename from browser/base/content/test/general/browser_sanitizeDialog.js
+rename to browser/base/content/test/sanitize/browser_sanitizeDialog.js
+--- a/browser/base/content/test/general/browser_sanitizeDialog.js
++++ b/browser/base/content/test/sanitize/browser_sanitizeDialog.js
+@@ -4,41 +4,49 @@
+  * License, v. 2.0. If a copy of the MPL was not distributed with this
+  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+ 
+ /**
+  * Tests the sanitize dialog (a.k.a. the clear recent history dialog).
+  * See bug 480169.
+  *
+  * The purpose of this test is not to fully flex the sanitize timespan code;
+- * browser/base/content/test/general/browser_sanitize-timespans.js does that.  This
++ * browser/base/content/test/sanitize/browser_sanitize-timespans.js does that.  This
+  * test checks the UI of the dialog and makes sure it's correctly connected to
+  * the sanitize timespan code.
+  *
+  * Some of this code, especially the history creation parts, was taken from
+- * browser/base/content/test/general/browser_sanitize-timespans.js.
++ * browser/base/content/test/sanitize/browser_sanitize-timespans.js.
+  */
+ 
+ ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
+-ChromeUtils.defineModuleGetter(this, "FormHistory",
+-                               "resource://gre/modules/FormHistory.jsm");
+-ChromeUtils.defineModuleGetter(this, "Downloads",
+-                               "resource://gre/modules/Downloads.jsm");
+ ChromeUtils.defineModuleGetter(this, "Timer",
+                                "resource://gre/modules/Timer.jsm");
+ ChromeUtils.defineModuleGetter(this, "PlacesTestUtils",
+                                "resource://testing-common/PlacesTestUtils.jsm");
+ 
+-var tempScope = {};
+-Services.scriptloader.loadSubScript("chrome://browser/content/sanitize.js", tempScope);
+-var Sanitizer = tempScope.Sanitizer;
+-
+ const kMsecPerMin = 60 * 1000;
+ const kUsecPerMin = 60 * 1000000;
+ 
++/**
++ * Ensures that the specified URIs are either cleared or not.
++ *
++ * @param aURIs
++ *        Array of page URIs
++ * @param aShouldBeCleared
++ *        True if each visit to the URI should be cleared, false otherwise
++ */
++async function promiseHistoryClearedState(aURIs, aShouldBeCleared) {
++  for (let uri of aURIs) {
++    let visited = await PlacesUtils.history.hasVisits(uri);
++    Assert.equal(visited, !aShouldBeCleared,
++      `history visit ${uri.spec} should ${aShouldBeCleared ? "no longer" : "still"} exist`);
++  }
++}
++
+ add_task(async function init() {
+   requestLongerTimeout(3);
+   await blankSlate();
+   registerCleanupFunction(async function() {
+     await blankSlate();
+     await PlacesTestUtils.promiseAsyncUpdates();
+   });
+ });
+@@ -845,17 +853,17 @@ WindowHelper.prototype = {
+    * Toggles the details progressive disclosure button.
+    */
+   toggleDetails() {
+     this.getDetailsButton().click();
+   }
+ };
+ 
+ function promiseSanitizationComplete() {
+-  return promiseTopicObserved("sanitizer-sanitization-complete");
++  return TestUtils.topicObserved("sanitizer-sanitization-complete");
+ }
+ 
+ /**
+  * Adds a download to history.
+  *
+  * @param aMinutesAgo
+  *        The download will be downloaded this many minutes ago
+  */
+diff --git a/browser/base/content/test/sanitize/dummy_page.html b/browser/base/content/test/sanitize/dummy_page.html
+new file mode 100644
+--- /dev/null
++++ b/browser/base/content/test/sanitize/dummy_page.html
+@@ -0,0 +1,9 @@
++<html>
++<head>
++<title>Dummy test page</title>
++<meta http-equiv="Content-Type" content="text/html;charset=utf-8"></meta>
++</head>
++<body>
++<p>Dummy test page</p>
++</body>
++</html>
+diff --git a/browser/base/content/test/sanitize/head.js b/browser/base/content/test/sanitize/head.js
+new file mode 100644
+--- /dev/null
++++ b/browser/base/content/test/sanitize/head.js
+@@ -0,0 +1,9 @@
++ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
++
++XPCOMUtils.defineLazyModuleGetters(this, {
++  Downloads: "resource://gre/modules/Downloads.jsm",
++  FormHistory: "resource://gre/modules/FormHistory.jsm",
++  PlacesUtils: "resource://gre/modules/PlacesUtils.jsm",
++  Sanitizer: "resource:///modules/Sanitizer.jsm",
++});
++

+ 310 - 0
frg/mozilla-release/work-js/1167238-5-60a1.patch

@@ -0,0 +1,310 @@
+# HG changeset patch
+# User Johann Hofmann <jhofmann@mozilla.com>
+# Date 1516288012 -3600
+# Node ID 45425d68c1dd8cc3d662a1d66c6b192bd7a2870f
+# Parent  8b0d975bf2394c1752aa447bf07a4643b4e0c34a
+Bug 1167238 - Part 5 - Clean up sanitize.js usage in remaining tests. r=mak
+
+This cleans up all tests that were not moved into the sanitize directory
+as part of the previous commit, but still use sanitize.js
+
+MozReview-Commit-ID: 1CVa0ByVYDk
+
+diff --git a/browser/base/content/test/newtab/browser_newtab_bug722273.js b/browser/base/content/test/newtab/browser_newtab_bug722273.js
+--- a/browser/base/content/test/newtab/browser_newtab_bug722273.js
++++ b/browser/base/content/test/newtab/browser_newtab_bug722273.js
+@@ -1,29 +1,24 @@
+ /* Any copyright is dedicated to the Public Domain.
+    http://creativecommons.org/publicdomain/zero/1.0/ */
+ 
+ const NOW = Date.now() * 1000;
+ const URL = "http://fake-site.com/";
+ 
+-var tmp = {};
+-Services.scriptloader.loadSubScript("chrome://browser/content/sanitize.js", tmp);
+-
+-var {Sanitizer} = tmp;
+-
+ add_task(async function() {
+-  await promiseSanitizeHistory();
++  await Sanitizer.sanitize(["history"]);
+   await promiseAddFakeVisits();
+   await addNewTabPageTab();
+ 
+   let cellUrl = await performOnCell(0, cell => { return cell.site.url; });
+   is(cellUrl, URL, "first site is our fake site");
+ 
+   let updatedPromise = whenPagesUpdated();
+-  await promiseSanitizeHistory();
++  await Sanitizer.sanitize(["history"]);
+   await updatedPromise;
+ 
+   let isGone = await performOnCell(0, cell => { return cell.site == null; });
+   ok(isGone, "fake site is gone");
+ });
+ 
+ function promiseAddFakeVisits() {
+   let visits = [];
+@@ -46,26 +41,8 @@ function promiseAddFakeVisits() {
+         NewTabUtils.links.populateCache(function() {
+           NewTabUtils.allPages.update();
+           resolve();
+         }, true);
+       }
+     });
+   });
+ }
+-
+-function promiseSanitizeHistory() {
+-  let s = new Sanitizer();
+-  s.prefDomain = "privacy.cpd.";
+-
+-  let prefs = Services.prefs.getBranch(s.prefDomain);
+-  prefs.setBoolPref("history", true);
+-  prefs.setBoolPref("downloads", false);
+-  prefs.setBoolPref("cache", false);
+-  prefs.setBoolPref("cookies", false);
+-  prefs.setBoolPref("formdata", false);
+-  prefs.setBoolPref("offlineApps", false);
+-  prefs.setBoolPref("passwords", false);
+-  prefs.setBoolPref("sessions", false);
+-  prefs.setBoolPref("siteSettings", false);
+-
+-  return s.sanitize();
+-}
+diff --git a/browser/base/content/test/newtab/head.js b/browser/base/content/test/newtab/head.js
+--- a/browser/base/content/test/newtab/head.js
++++ b/browser/base/content/test/newtab/head.js
+@@ -10,22 +10,24 @@ Services.prefs.setBoolPref(PREF_NEWTAB_A
+ 
+ // Opens and closes a new tab to clear any existing preloaded ones. This is
+ // necessary to prevent any left-over activity-stream preloaded new tabs from
+ // affecting these tests.
+ BrowserOpenTab();
+ const initialTab = gBrowser.selectedTab;
+ gBrowser.removeTab(initialTab);
+ 
+-var tmp = {};
+-ChromeUtils.import("resource://gre/modules/NewTabUtils.jsm", tmp);
+-ChromeUtils.import("resource:///modules/DirectoryLinksProvider.jsm", tmp);
+-ChromeUtils.import("resource://testing-common/PlacesTestUtils.jsm", tmp);
+-Services.scriptloader.loadSubScript("chrome://browser/content/sanitize.js", tmp);
+-var {NewTabUtils, Sanitizer, DirectoryLinksProvider, PlacesTestUtils} = tmp;
++ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
++
++XPCOMUtils.defineLazyModuleGetters(this, {
++  NewTabUtils: "resource://gre/modules/NewTabUtils.jsm",
++  DirectoryLinksProvider: "resource:///modules/DirectoryLinksProvider.jsm",
++  PlacesTestUtils: "resource://testing-common/PlacesTestUtils.jsm",
++  Sanitizer: "resource:///modules/Sanitizer.jsm",
++});
+ 
+ var gWindow = window;
+ 
+ // Default to dummy/empty directory links
+ var gDirectorySource = 'data:application/json,{"test":1}';
+ var gOrigDirectorySource;
+ 
+ // The tests assume all 3 rows and all 3 columns of sites are shown, but the
+diff --git a/browser/components/originattributes/test/browser/browser_sanitize.js b/browser/components/originattributes/test/browser/browser_sanitize.js
+--- a/browser/components/originattributes/test/browser/browser_sanitize.js
++++ b/browser/components/originattributes/test/browser/browser_sanitize.js
+@@ -1,20 +1,17 @@
+ /**
+  * Bug 1270338 - Add a mochitest to ensure Sanitizer clears data for all containers
+  */
+ 
+ const CC = Components.Constructor;
+ 
+ const TEST_DOMAIN = "http://example.net/";
+ 
+-let tempScope = {};
+-Services.scriptloader.loadSubScript("chrome://browser/content/sanitize.js",
+-                                    tempScope);
+-let Sanitizer = tempScope.Sanitizer;
++const {Sanitizer} = ChromeUtils.import("resource:///modules/Sanitizer.jsm", {});
+ 
+ function setCookies(aBrowser) {
+   ContentTask.spawn(aBrowser, null, function() {
+     content.document.cookie = "key=value";
+   });
+ }
+ 
+ function cacheDataForContext(loadContextInfo) {
+@@ -72,14 +69,13 @@ add_task(async function setup() {
+ });
+ 
+ // This will set the cookies and the cache.
+ IsolationTestTools.runTests(TEST_DOMAIN, setCookies, () => true);
+ 
+ add_task(checkCacheExists(true));
+ 
+ add_task(async function sanitize() {
+-  let sanitizer = new Sanitizer();
+-  await sanitizer.sanitize(["cookies", "cache"]);
++  await Sanitizer.sanitize(["cookies", "cache"]);
+ });
+ 
+ add_task(checkCacheExists(false));
+ IsolationTestTools.runTests(TEST_DOMAIN, checkCookiesSanitized, () => true);
+diff --git a/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_cache.js b/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_cache.js
+--- a/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_cache.js
++++ b/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_cache.js
+@@ -3,67 +3,31 @@
+  * You can obtain one at http://mozilla.org/MPL/2.0/. */
+ 
+ // Check about:cache after private browsing
+ // This test covers MozTrap test 6047
+ // bug 880621
+ 
+ var tmp = {};
+ 
+-Services.scriptloader.loadSubScript("chrome://browser/content/sanitize.js", tmp);
+-
+-var Sanitizer = tmp.Sanitizer;
++const {Sanitizer} = ChromeUtils.import("resource:///modules/Sanitizer.jsm", {});
+ 
+ function test() {
+ 
+   waitForExplicitFinish();
+ 
+-  sanitizeCache();
++  Sanitizer.sanitize(["cache"], {ignoreTimespan: false});
+ 
+   getStorageEntryCount("regular", function(nrEntriesR1) {
+     is(nrEntriesR1, 0, "Disk cache reports 0KB and has no entries");
+ 
+     get_cache_for_private_window();
+   });
+ }
+ 
+-function cleanup() {
+-  let prefs = Services.prefs.getBranch("privacy.cpd.");
+-
+-  prefs.clearUserPref("history");
+-  prefs.clearUserPref("downloads");
+-  prefs.clearUserPref("cache");
+-  prefs.clearUserPref("cookies");
+-  prefs.clearUserPref("formdata");
+-  prefs.clearUserPref("offlineApps");
+-  prefs.clearUserPref("passwords");
+-  prefs.clearUserPref("sessions");
+-  prefs.clearUserPref("siteSettings");
+-}
+-
+-function sanitizeCache() {
+-
+-  let s = new Sanitizer();
+-  s.ignoreTimespan = false;
+-  s.prefDomain = "privacy.cpd.";
+-
+-  let prefs = Services.prefs.getBranch(s.prefDomain);
+-  prefs.setBoolPref("history", false);
+-  prefs.setBoolPref("downloads", false);
+-  prefs.setBoolPref("cache", true);
+-  prefs.setBoolPref("cookies", false);
+-  prefs.setBoolPref("formdata", false);
+-  prefs.setBoolPref("offlineApps", false);
+-  prefs.setBoolPref("passwords", false);
+-  prefs.setBoolPref("sessions", false);
+-  prefs.setBoolPref("siteSettings", false);
+-
+-  s.sanitize();
+-}
+-
+ function getStorageEntryCount(device, goon) {
+   var storage;
+   switch (device) {
+   case "private":
+     storage = Services.cache2.diskCacheStorage(Services.loadContextInfo.private, false);
+     break;
+   case "regular":
+     storage = Services.cache2.diskCacheStorage(Services.loadContextInfo.default, false);
+@@ -105,18 +69,16 @@ function get_cache_for_private_window() 
+         executeSoon(function() {
+ 
+           getStorageEntryCount("private", function(nrEntriesP) {
+             ok(nrEntriesP >= 1, "Memory cache reports some entries from example.org domain");
+ 
+             getStorageEntryCount("regular", function(nrEntriesR2) {
+               is(nrEntriesR2, 0, "Disk cache reports 0KB and has no entries");
+ 
+-              cleanup();
+-
+               win.close();
+               finish();
+             });
+           });
+         });
+       }, {capture: true, once: true});
+     });
+   });
+diff --git a/toolkit/components/thumbnails/test/browser_thumbnails_storage.js b/toolkit/components/thumbnails/test/browser_thumbnails_storage.js
+--- a/toolkit/components/thumbnails/test/browser_thumbnails_storage.js
++++ b/toolkit/components/thumbnails/test/browser_thumbnails_storage.js
+@@ -1,19 +1,15 @@
+ /* Any copyright is dedicated to the Public Domain.
+    http://creativecommons.org/publicdomain/zero/1.0/ */
+ 
+ const URL = "http://mochi.test:8888/";
+ const URL_COPY = URL + "#copy";
+ 
+-XPCOMUtils.defineLazyGetter(this, "Sanitizer", function() {
+-  let tmp = {};
+-  Services.scriptloader.loadSubScript("chrome://browser/content/sanitize.js", tmp);
+-  return tmp.Sanitizer;
+-});
++const {Sanitizer} = ChromeUtils.import("resource:///modules/Sanitizer.jsm", {});
+ 
+ /**
+  * These tests ensure that the thumbnail storage is working as intended.
+  * Newly captured thumbnails should be saved as files and they should as well
+  * be removed when the user sanitizes their history.
+  */
+ function* runTests() {
+   yield (async function() {
+@@ -67,40 +63,23 @@ async function promiseClearFile(aFile, a
+   // is called again.
+   await PlacesTestUtils.addVisits(makeURI(aURL));
+   await promiseClearHistory(true);
+   // Then retry.
+   return promiseClearFile(aFile, aURL);
+ }
+ 
+ function promiseClearHistory(aUseRange) {
+-  let s = new Sanitizer();
+-  s.prefDomain = "privacy.cpd.";
+-
+-  let prefs = Services.prefs.getBranch(s.prefDomain);
+-  prefs.setBoolPref("history", true);
+-  prefs.setBoolPref("downloads", false);
+-  prefs.setBoolPref("cache", false);
+-  prefs.setBoolPref("cookies", false);
+-  prefs.setBoolPref("formdata", false);
+-  prefs.setBoolPref("offlineApps", false);
+-  prefs.setBoolPref("passwords", false);
+-  prefs.setBoolPref("sessions", false);
+-  prefs.setBoolPref("siteSettings", false);
+-
++  let options = {};
+   if (aUseRange) {
+     let usec = Date.now() * 1000;
+-    s.range = [usec - 10 * 60 * 1000 * 1000, usec];
+-    s.ignoreTimespan = false;
++    options.range = [usec - 10 * 60 * 1000 * 1000, usec];
++    options.ignoreTimespan = false;
+   }
+-
+-  return s.sanitize().then(() => {
+-    s.range = null;
+-    s.ignoreTimespan = true;
+-  });
++  return Sanitizer.sanitize(["history"], options);
+ }
+ 
+ function promiseCreateThumbnail() {
+   return new Promise(resolve => {
+     addTab(URL, function() {
+       gBrowserThumbnails.clearTopSiteURLCache();
+       whenFileExists(URL, function() {
+         gBrowser.removeTab(gBrowser.selectedTab);

+ 90 - 0
frg/mozilla-release/work-js/1167238-6-60a1.patch

@@ -0,0 +1,90 @@
+# HG changeset patch
+# User Johann Hofmann <jhofmann@mozilla.com>
+# Date 1516288118 -3600
+# Node ID 93903673b0eb1951970f412fda5c0386a38a7841
+# Parent  733a9c8043f43b581892a2944ca820d5a76d5eae
+Bug 1167238 - Part 6 - Clean up sanitize.js usage in utils.py. r=whimboo
+
+We're turning Sanitizer.jsm into a proper module and are simplifying
+its API surface, and need to adjust all consumers.
+
+MozReview-Commit-ID: 7xjSpiKeG7d
+
+diff --git a/testing/marionette/puppeteer/firefox/firefox_puppeteer/api/utils.py b/testing/marionette/puppeteer/firefox/firefox_puppeteer/api/utils.py
+--- a/testing/marionette/puppeteer/firefox/firefox_puppeteer/api/utils.py
++++ b/testing/marionette/puppeteer/firefox/firefox_puppeteer/api/utils.py
+@@ -41,66 +41,39 @@ class Utils(BaseLib):
+     def sanitize(self, data_type):
+         """Sanitize user data, including cache, cookies, offlineApps, history, formdata,
+         downloads, passwords, sessions, siteSettings.
+ 
+         Usage:
+         sanitize():  Clears all user data.
+         sanitize({ "sessions": True }): Clears only session user data.
+ 
+-        more: https://dxr.mozilla.org/mozilla-central/source/browser/base/content/sanitize.js
++        more: https://dxr.mozilla.org/mozilla-central/source/browser/modules/Sanitizer.jsm
+ 
+         :param data_type: optional, Information specifying data to be sanitized
+         """
+ 
+         with self.marionette.using_context('chrome'):
+             result = self.marionette.execute_async_script("""
+-              Components.utils.import("resource://gre/modules/Services.jsm");
++              var {Sanitizer} = Components.utils.import("resource:///modules/Sanitizer.jsm", {});
+ 
+               var data_type = arguments[0];
+ 
+-              var data_type = (typeof data_type === "undefined") ? {} : {
+-                cache: data_type.cache || false,
+-                cookies: data_type.cookies || false,
+-                downloads: data_type.downloads || false,
+-                formdata: data_type.formdata || false,
+-                history: data_type.history || false,
+-                offlineApps: data_type.offlineApps || false,
+-                passwords: data_type.passwords || false,
+-                sessions: data_type.sessions || false,
+-                siteSettings: data_type.siteSettings || false
+-              };
+-
+-              // Load the sanitize script
+-              var tempScope = {};
+-              Components.classes["@mozilla.org/moz/jssubscript-loader;1"]
+-              .getService(Components.interfaces.mozIJSSubScriptLoader)
+-              .loadSubScript("chrome://browser/content/sanitize.js", tempScope);
+-
+-              // Instantiate the Sanitizer
+-              var s = new tempScope.Sanitizer();
+-              s.prefDomain = "privacy.cpd.";
+-              var itemPrefs = Services.prefs.getBranch(s.prefDomain);
+-
+               // Apply options for what to sanitize
+-              for (var pref in data_type) {
+-                itemPrefs.setBoolPref(pref, data_type[pref]);
++              var itemsToClear = [];
++              for (var pref of Object.keys(data_type)) {
++                if (data_type[pref]) {
++                    itemsToClear.push(pref);
++                }
+               };
+ 
+               // Sanitize and wait for the promise to resolve
+-              var finished = false;
+-              s.sanitize().then(() => {
+-                for (let pref in data_type) {
+-                  itemPrefs.clearUserPref(pref);
+-                };
++              Sanitizer.sanitize(itemsToClear).then(() => {
+                 marionetteScriptFinished(true);
+               }, aError => {
+-                for (let pref in data_type) {
+-                  itemPrefs.clearUserPref(pref);
+-                };
+                 marionetteScriptFinished(false);
+               });
+             """, script_args=[data_type])
+ 
+             if not result:
+                 raise MarionetteException('Sanitizing of profile data failed.')
+ 
+ 

+ 579 - 0
frg/mozilla-release/work-js/1252998-01-61a1.patch

@@ -0,0 +1,579 @@
+# HG changeset patch
+# User Andrea Marchesini <amarchesini@mozilla.com>
+# Date 1524068350 -7200
+# Node ID 558fef850d425f13465fe7b8ec0fd249d1527981
+# Parent  0ba938d86ee9eda261242f3409fa8bcf094f3881
+Bug 1252998 - StorageActivityService - part 1 - Introduce StorageActivityService to monitor origin activities, r=asuth
+
+diff --git a/dom/interfaces/storage/moz.build b/dom/interfaces/storage/moz.build
+--- a/dom/interfaces/storage/moz.build
++++ b/dom/interfaces/storage/moz.build
+@@ -5,11 +5,12 @@
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ 
+ with Files("**"):
+     BUG_COMPONENT = ("Core", "DOM")
+ 
+ XPIDL_SOURCES += [
+     'nsIDOMStorage.idl',
+     'nsIDOMStorageManager.idl',
++    'nsIStorageActivityService.idl',
+ ]
+ 
+ XPIDL_MODULE = 'dom_storage'
+diff --git a/dom/interfaces/storage/nsIStorageActivityService.idl b/dom/interfaces/storage/nsIStorageActivityService.idl
+new file mode 100644
+--- /dev/null
++++ b/dom/interfaces/storage/nsIStorageActivityService.idl
+@@ -0,0 +1,25 @@
++/* -*- Mode: IDL; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
++/* This Source Code Form is subject to the terms of the Mozilla Public
++ * License, v. 2.0. If a copy of the MPL was not distributed with this
++ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
++
++#include "domstubs.idl"
++
++/**
++ * nsIStorageActivityService is a service that can be used to know which
++ * origins have been active in a time range. This information can be used to
++ * implement "Clear Recent History" or similar features.
++ *
++ * If you are implementing a new Storage component, you should use
++ * QuotaManager. But if you don't do it, remember to call
++ * StorageActivityService methods in order to inform this service about
++ * 'writing' operations executed by origins.
++ */
++[scriptable, builtinclass, uuid(fd1310ba-d1be-4327-988e-92b39fcff6f4)]
++interface nsIStorageActivityService : nsISupports
++{
++};
++
++%{ C++
++#define STORAGE_ACTIVITY_SERVICE_CONTRACTID "@mozilla.org/storage/activity-service;1"
++%}
+diff --git a/dom/storage/StorageActivityService.cpp b/dom/storage/StorageActivityService.cpp
+new file mode 100644
+--- /dev/null
++++ b/dom/storage/StorageActivityService.cpp
+@@ -0,0 +1,209 @@
++/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
++/* vim: set ts=8 sts=2 et sw=2 tw=80: */
++/* This Source Code Form is subject to the terms of the Mozilla Public
++ * License, v. 2.0. If a copy of the MPL was not distributed with this
++ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
++
++#include "StorageActivityService.h"
++
++#include "mozilla/ipc/BackgroundUtils.h"
++#include "mozilla/StaticPtr.h"
++#include "nsXPCOM.h"
++
++// This const is used to know when origin activities should be purged because
++// too old. This value should be in sync with what the UI needs.
++#define TIME_MAX_SECS 86400 /* 24 hours */
++
++namespace mozilla {
++namespace dom {
++
++static StaticRefPtr<StorageActivityService> gStorageActivityService;
++static bool gStorageActivityShutdown = false;
++
++/* static */ void
++StorageActivityService::SendActivity(nsIPrincipal* aPrincipal)
++{
++  MOZ_ASSERT(NS_IsMainThread());
++
++  RefPtr<StorageActivityService> service = GetOrCreate();
++  if (NS_WARN_IF(!service)) {
++    return;
++  }
++
++  service->SendActivityInternal(aPrincipal);
++}
++
++/* static */ void
++StorageActivityService::SendActivity(const mozilla::ipc::PrincipalInfo& aPrincipalInfo)
++{
++  RefPtr<Runnable> r = NS_NewRunnableFunction(
++    "StorageActivityService::SendActivity",
++    [aPrincipalInfo] () {
++      MOZ_ASSERT(NS_IsMainThread());
++
++      nsCOMPtr<nsIPrincipal> principal =
++        mozilla::ipc::PrincipalInfoToPrincipal(aPrincipalInfo);
++
++      StorageActivityService::SendActivity(principal);
++    });
++
++  SystemGroup::Dispatch(TaskCategory::Other, r.forget());
++}
++
++/* static */ already_AddRefed<StorageActivityService>
++StorageActivityService::GetOrCreate()
++{
++  MOZ_ASSERT(NS_IsMainThread());
++
++  if (!gStorageActivityService && !gStorageActivityShutdown) {
++    RefPtr<StorageActivityService> service = new StorageActivityService();
++
++    nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService();
++    if (NS_WARN_IF(!obs)) {
++      return nullptr;
++    }
++
++    nsresult rv = obs->AddObserver(service, NS_XPCOM_SHUTDOWN_OBSERVER_ID, true);
++    if (NS_WARN_IF(NS_FAILED(rv))) {
++      return nullptr;
++    }
++
++    gStorageActivityService = service;
++  }
++
++  RefPtr<StorageActivityService> service = gStorageActivityService;
++  return service.forget();
++}
++
++StorageActivityService::StorageActivityService()
++{
++  MOZ_ASSERT(NS_IsMainThread());
++}
++
++StorageActivityService::~StorageActivityService()
++{
++  MOZ_ASSERT(NS_IsMainThread());
++  MOZ_ASSERT(!mTimer);
++}
++
++void
++StorageActivityService::SendActivityInternal(nsIPrincipal* aPrincipal)
++{
++  MOZ_ASSERT(NS_IsMainThread());
++  MOZ_ASSERT(aPrincipal);
++
++  if (!XRE_IsParentProcess()) {
++    SendActivityToParent(aPrincipal);
++    return;
++  }
++
++  nsAutoCString origin;
++  nsresult rv = aPrincipal->GetOrigin(origin);
++  if (NS_WARN_IF(NS_FAILED(rv))) {
++    return;
++  }
++
++  mActivities.Put(origin, TimeStamp::NowLoRes());
++
++  MaybeStartTimer();
++}
++
++void
++StorageActivityService::SendActivityToParent(nsIPrincipal* aPrincipal)
++{
++  MOZ_ASSERT(NS_IsMainThread());
++  MOZ_ASSERT(!XRE_IsParentProcess());
++
++  PBackgroundChild* actor = BackgroundChild::GetOrCreateForCurrentThread();
++  if (NS_WARN_IF(!actor)) {
++    return;
++  }
++
++  mozilla::ipc::PrincipalInfo principalInfo;
++  nsresult rv =
++    mozilla::ipc::PrincipalToPrincipalInfo(aPrincipal, &principalInfo);
++  if (NS_WARN_IF(NS_FAILED(rv))) {
++    return;
++  }
++
++  actor->SendStorageActivity(principalInfo);
++}
++
++NS_IMETHODIMP
++StorageActivityService::Observe(nsISupports* aSubject, const char* aTopic,
++                                const char16_t* aData)
++{
++  MOZ_ASSERT(NS_IsMainThread());
++  MOZ_ASSERT(!strcmp(aTopic, NS_XPCOM_SHUTDOWN_OBSERVER_ID));
++
++  MaybeStopTimer();
++
++  nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService();
++  if (obs) {
++    obs->RemoveObserver(this, NS_XPCOM_SHUTDOWN_OBSERVER_ID);
++  }
++
++  gStorageActivityShutdown = true;
++  gStorageActivityService = nullptr;
++  return NS_OK;
++}
++
++void
++StorageActivityService::MaybeStartTimer()
++{
++  MOZ_ASSERT(NS_IsMainThread());
++
++  if (!mTimer) {
++    mTimer = do_CreateInstance(NS_TIMER_CONTRACTID);
++    mTimer->InitWithCallback(this,
++                             1000 * 5 * 60 /* any 5 minutes */,
++                             nsITimer::TYPE_REPEATING_SLACK);
++  }
++}
++
++void
++StorageActivityService::MaybeStopTimer()
++{
++  MOZ_ASSERT(NS_IsMainThread());
++
++  if (mTimer) {
++    mTimer->Cancel();
++    mTimer = nullptr;
++  }
++}
++
++NS_IMETHODIMP
++StorageActivityService::Notify(nsITimer* aTimer)
++{
++  MOZ_ASSERT(NS_IsMainThread());
++  MOZ_ASSERT(mTimer == aTimer);
++
++  TimeStamp now = TimeStamp::NowLoRes();
++
++  for (auto iter = mActivities.Iter(); !iter.Done(); iter.Next()) {
++    if ((now - iter.UserData()).ToSeconds() > TIME_MAX_SECS) {
++      iter.Remove();
++    }
++  }
++
++  // If no activities, let's stop the timer.
++  if (mActivities.Count() == 0) {
++    MaybeStopTimer();
++  }
++
++  return NS_OK;
++}
++
++NS_INTERFACE_MAP_BEGIN(StorageActivityService)
++  NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIStorageActivityService)
++  NS_INTERFACE_MAP_ENTRY(nsIStorageActivityService)
++  NS_INTERFACE_MAP_ENTRY(nsIObserver)
++  NS_INTERFACE_MAP_ENTRY(nsITimerCallback)
++  NS_INTERFACE_MAP_ENTRY(nsISupportsWeakReference)
++NS_INTERFACE_MAP_END
++
++NS_IMPL_ADDREF(StorageActivityService)
++NS_IMPL_RELEASE(StorageActivityService)
++
++} // dom namespace
++} // mozilla namespace
+diff --git a/dom/storage/StorageActivityService.h b/dom/storage/StorageActivityService.h
+new file mode 100644
+--- /dev/null
++++ b/dom/storage/StorageActivityService.h
+@@ -0,0 +1,71 @@
++/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
++/* vim: set ts=8 sts=2 et sw=2 tw=80: */
++/* This Source Code Form is subject to the terms of the Mozilla Public
++ * License, v. 2.0. If a copy of the MPL was not distributed with this
++ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
++
++#ifndef mozilla_dom_StorageActivityService_h
++#define mozilla_dom_StorageActivityService_h
++
++#include "nsDataHashtable.h"
++#include "nsIStorageActivityService.h"
++#include "nsITimer.h"
++#include "nsWeakReference.h"
++
++namespace mozilla {
++
++namespace ipc {
++class PrincipalInfo;
++} // ipc
++
++namespace dom {
++
++class StorageActivityService final : public nsIStorageActivityService
++                                   , public nsIObserver
++                                   , public nsITimerCallback
++                                   , public nsSupportsWeakReference
++{
++public:
++  NS_DECL_ISUPPORTS
++  NS_DECL_NSISTORAGEACTIVITYSERVICE
++  NS_DECL_NSIOBSERVER
++  NS_DECL_NSITIMERCALLBACK
++
++  // Main-thread only.
++  static void
++  SendActivity(nsIPrincipal* aPrincipal);
++
++  // Thread-safe.
++  static void
++  SendActivity(const mozilla::ipc::PrincipalInfo& aPrincipalInfo);
++
++  // Used by XPCOM. Don't use it, use SendActivity() instead.
++  static already_AddRefed<StorageActivityService>
++  GetOrCreate();
++
++private:
++  StorageActivityService();
++  ~StorageActivityService();
++
++  void
++  SendActivityInternal(nsIPrincipal* aPrincipal);
++
++  void
++  SendActivityToParent(nsIPrincipal* aPrincipal);
++
++  void
++  MaybeStartTimer();
++
++  void
++  MaybeStopTimer();
++
++  // Activities grouped by origin (+OriginAttributes).
++  nsDataHashtable<nsCStringHashKey, TimeStamp> mActivities;
++
++  nsCOMPtr<nsITimer> mTimer;
++};
++
++} // namespace dom
++} // namespace mozilla
++
++#endif // mozilla_dom_StorageActivityService_h
+diff --git a/dom/storage/moz.build b/dom/storage/moz.build
+--- a/dom/storage/moz.build
++++ b/dom/storage/moz.build
+@@ -7,29 +7,31 @@
+ with Files("**"):
+     BUG_COMPONENT = ("Core", "DOM")
+ 
+ EXPORTS.mozilla.dom += [
+     'LocalStorage.h',
+     'LocalStorageManager.h',
+     'SessionStorageManager.h',
+     'Storage.h',
++    'StorageActivityService.h',
+     'StorageIPC.h',
+     'StorageNotifierService.h',
+     'StorageUtils.h',
+ ]
+ 
+ UNIFIED_SOURCES += [
+     'LocalStorage.cpp',
+     'LocalStorageCache.cpp',
+     'LocalStorageManager.cpp',
+     'SessionStorage.cpp',
+     'SessionStorageCache.cpp',
+     'SessionStorageManager.cpp',
+     'Storage.cpp',
++    'StorageActivityService.cpp',
+     'StorageDBThread.cpp',
+     'StorageDBUpdater.cpp',
+     'StorageIPC.cpp',
+     'StorageNotifierService.cpp',
+     'StorageObserver.cpp',
+     'StorageUtils.cpp',
+ ]
+ 
+diff --git a/ipc/glue/BackgroundParentImpl.cpp b/ipc/glue/BackgroundParentImpl.cpp
+--- a/ipc/glue/BackgroundParentImpl.cpp
++++ b/ipc/glue/BackgroundParentImpl.cpp
+@@ -19,16 +19,17 @@
+ #include "mozilla/dom/FileSystemBase.h"
+ #include "mozilla/dom/FileSystemRequestParent.h"
+ #include "mozilla/dom/GamepadEventChannelParent.h"
+ #include "mozilla/dom/GamepadTestChannelParent.h"
+ #include "mozilla/dom/PGamepadEventChannelParent.h"
+ #include "mozilla/dom/PGamepadTestChannelParent.h"
+ #include "mozilla/dom/MessagePortParent.h"
+ #include "mozilla/dom/ServiceWorkerRegistrar.h"
++#include "mozilla/dom/StorageActivityService.h"
+ #include "mozilla/dom/asmjscache/AsmJSCache.h"
+ #include "mozilla/dom/cache/ActorUtils.h"
+ #include "mozilla/dom/indexedDB/ActorsParent.h"
+ #include "mozilla/dom/ipc/IPCBlobInputStreamParent.h"
+ #include "mozilla/dom/ipc/PendingIPCBlobParent.h"
+ #include "mozilla/dom/ipc/TemporaryIPCBlobParent.h"
+ #include "mozilla/dom/quota/ActorsParent.h"
+ #include "mozilla/dom/StorageIPC.h"
+@@ -983,16 +984,23 @@ BackgroundParentImpl::DeallocPClientMana
+ 
+ mozilla::ipc::IPCResult
+ BackgroundParentImpl::RecvPClientManagerConstructor(mozilla::dom::PClientManagerParent* aActor)
+ {
+   mozilla::dom::InitClientManagerParent(aActor);
+   return IPC_OK();
+ }
+ 
++IPCResult
++BackgroundParentImpl::RecvStorageActivity(const PrincipalInfo& aPrincipalInfo)
++{
++  dom::StorageActivityService::SendActivity(aPrincipalInfo);
++  return IPC_OK();
++}
++
+ } // namespace ipc
+ } // namespace mozilla
+ 
+ void
+ TestParent::ActorDestroy(ActorDestroyReason aWhy)
+ {
+   mozilla::ipc::AssertIsInMainProcess();
+   AssertIsOnBackgroundThread();
+diff --git a/ipc/glue/BackgroundParentImpl.h b/ipc/glue/BackgroundParentImpl.h
+--- a/ipc/glue/BackgroundParentImpl.h
++++ b/ipc/glue/BackgroundParentImpl.h
+@@ -266,14 +266,17 @@ protected:
+   virtual PClientManagerParent*
+   AllocPClientManagerParent() override;
+ 
+   virtual bool
+   DeallocPClientManagerParent(PClientManagerParent* aActor) override;
+ 
+   virtual mozilla::ipc::IPCResult
+   RecvPClientManagerConstructor(PClientManagerParent* aActor) override;
++
++  virtual mozilla::ipc::IPCResult
++  RecvStorageActivity(const PrincipalInfo& aPrincipalInfo) override;
+ };
+ 
+ } // namespace ipc
+ } // namespace mozilla
+ 
+ #endif // mozilla_ipc_backgroundparentimpl_h__
+diff --git a/ipc/glue/PBackground.ipdl b/ipc/glue/PBackground.ipdl
+--- a/ipc/glue/PBackground.ipdl
++++ b/ipc/glue/PBackground.ipdl
+@@ -135,16 +135,20 @@ parent:
+   async PHttpBackgroundChannel(uint64_t channelId);
+ 
+   async PWebAuthnTransaction();
+ 
+   async PTemporaryIPCBlob();
+ 
+   async PClientManager();
+ 
++  // This method is used to propagate storage activities from the child actor
++  // to the parent actor. See StorageActivityService.
++  async StorageActivity(PrincipalInfo principalInfo);
++
+ child:
+   async PCache();
+   async PCacheStreamControl();
+ 
+   async PParentToChildStream();
+ 
+   async PPendingIPCBlob(IPCBlob blob);
+ 
+diff --git a/layout/build/nsLayoutCID.h b/layout/build/nsLayoutCID.h
+--- a/layout/build/nsLayoutCID.h
++++ b/layout/build/nsLayoutCID.h
+@@ -74,9 +74,13 @@
+ // {5a75c25a-5e7e-4d90-8f7c-07eb15cc0aa8}
+ #define QUOTAMANAGER_SERVICE_CID \
+ { 0x5a75c25a, 0x5e7e, 0x4d90, { 0x8f, 0x7c, 0x07, 0xeb, 0x15, 0xcc, 0x0a, 0xa8 } }
+ 
+ // {c74bde32-bcc7-4840-8430-c733351b212a}
+ #define SERVICEWORKERMANAGER_CID \
+ { 0xc74bde32, 0xbcc7, 0x4840, { 0x84, 0x30, 0xc7, 0x33, 0x35, 0x1b, 0x21, 0x2a } }
+ 
++// {69da374a-fda3-4a93-9fbc-d9304f66a7fe}
++#define STORAGEACTIVITYSERVICE_CID \
++{ 0x69da374a, 0xfda3, 0x4a93, { 0x9f, 0xbc, 0xd9, 0x30, 0x4f, 0x66, 0xa7, 0xfe } }
++
+ #endif /* nsLayoutCID_h__ */
+diff --git a/layout/build/nsLayoutModule.cpp b/layout/build/nsLayoutModule.cpp
+--- a/layout/build/nsLayoutModule.cpp
++++ b/layout/build/nsLayoutModule.cpp
+@@ -80,16 +80,17 @@
+ #include "nsZipArchive.h"
+ #include "mozilla/Attributes.h"
+ #include "mozilla/dom/DOMException.h"
+ #include "mozilla/dom/DOMRequest.h"
+ #include "mozilla/dom/LocalStorageManager.h"
+ #include "mozilla/dom/network/UDPSocketChild.h"
+ #include "mozilla/dom/quota/QuotaManagerService.h"
+ #include "mozilla/dom/SessionStorageManager.h"
++#include "mozilla/dom/StorageActivityService.h"
+ #include "mozilla/dom/workers/ServiceWorkerManager.h"
+ #include "mozilla/dom/workers/WorkerDebuggerManager.h"
+ #include "mozilla/dom/Notification.h"
+ #include "mozilla/OSFileConstants.h"
+ #include "mozilla/Services.h"
+ 
+ #ifdef MOZ_WEBSPEECH_TEST_BACKEND
+ #include "mozilla/dom/FakeSpeechRecognitionService.h"
+@@ -211,16 +212,18 @@ NS_GENERIC_FACTORY_CONSTRUCTOR(SessionSt
+ NS_GENERIC_FACTORY_SINGLETON_CONSTRUCTOR(DOMRequestService,
+                                          DOMRequestService::FactoryCreate)
+ NS_GENERIC_FACTORY_SINGLETON_CONSTRUCTOR(QuotaManagerService,
+                                          QuotaManagerService::FactoryCreate)
+ NS_GENERIC_FACTORY_SINGLETON_CONSTRUCTOR(ServiceWorkerManager,
+                                          ServiceWorkerManager::GetInstance)
+ NS_GENERIC_FACTORY_SINGLETON_CONSTRUCTOR(WorkerDebuggerManager,
+                                          WorkerDebuggerManager::GetInstance)
++NS_GENERIC_FACTORY_SINGLETON_CONSTRUCTOR(StorageActivityService,
++                                         StorageActivityService::GetOrCreate)
+ 
+ #ifdef MOZ_WEBSPEECH
+ NS_GENERIC_FACTORY_SINGLETON_CONSTRUCTOR(nsSynthVoiceRegistry,
+                                          nsSynthVoiceRegistry::GetInstanceForService)
+ #endif
+ 
+ NS_GENERIC_FACTORY_CONSTRUCTOR(AudioChannelAgent)
+ 
+@@ -570,16 +573,17 @@ NS_DEFINE_NAMED_CID(NS_XMLHTTPREQUEST_CI
+ NS_DEFINE_NAMED_CID(NS_DOMPARSER_CID);
+ NS_DEFINE_NAMED_CID(NS_DOMSESSIONSTORAGEMANAGER_CID);
+ NS_DEFINE_NAMED_CID(NS_DOMLOCALSTORAGEMANAGER_CID);
+ NS_DEFINE_NAMED_CID(NS_DOMJSON_CID);
+ NS_DEFINE_NAMED_CID(NS_TEXTEDITOR_CID);
+ NS_DEFINE_NAMED_CID(DOMREQUEST_SERVICE_CID);
+ NS_DEFINE_NAMED_CID(QUOTAMANAGER_SERVICE_CID);
+ NS_DEFINE_NAMED_CID(SERVICEWORKERMANAGER_CID);
++NS_DEFINE_NAMED_CID(STORAGEACTIVITYSERVICE_CID);
+ NS_DEFINE_NAMED_CID(PUSHNOTIFIER_CID);
+ NS_DEFINE_NAMED_CID(WORKERDEBUGGERMANAGER_CID);
+ NS_DEFINE_NAMED_CID(NS_AUDIOCHANNELAGENT_CID);
+ NS_DEFINE_NAMED_CID(NS_HTMLEDITOR_CID);
+ NS_DEFINE_NAMED_CID(NS_EDITORCONTROLLER_CID);
+ NS_DEFINE_NAMED_CID(NS_EDITINGCONTROLLER_CID);
+ NS_DEFINE_NAMED_CID(NS_EDITORCOMMANDTABLE_CID);
+ NS_DEFINE_NAMED_CID(NS_EDITINGCOMMANDTABLE_CID);
+@@ -817,16 +821,17 @@ static const mozilla::Module::CIDEntry k
+   { &kNS_DOMPARSER_CID, false, nullptr, DOMParserConstructor },
+   { &kNS_DOMSESSIONSTORAGEMANAGER_CID, false, nullptr, SessionStorageManagerConstructor },
+   { &kNS_DOMLOCALSTORAGEMANAGER_CID, false, nullptr, LocalStorageManagerConstructor },
+   { &kNS_DOMJSON_CID, false, nullptr, NS_NewJSON },
+   { &kNS_TEXTEDITOR_CID, false, nullptr, TextEditorConstructor },
+   { &kDOMREQUEST_SERVICE_CID, false, nullptr, DOMRequestServiceConstructor },
+   { &kQUOTAMANAGER_SERVICE_CID, false, nullptr, QuotaManagerServiceConstructor },
+   { &kSERVICEWORKERMANAGER_CID, false, nullptr, ServiceWorkerManagerConstructor },
++  { &kSTORAGEACTIVITYSERVICE_CID, false, nullptr, StorageActivityServiceConstructor },
+   { &kPUSHNOTIFIER_CID, false, nullptr, PushNotifierConstructor },
+   { &kWORKERDEBUGGERMANAGER_CID, true, nullptr, WorkerDebuggerManagerConstructor },
+   { &kNS_AUDIOCHANNELAGENT_CID, true, nullptr, AudioChannelAgentConstructor },
+   { &kNS_HTMLEDITOR_CID, false, nullptr, HTMLEditorConstructor },
+   { &kNS_EDITORCONTROLLER_CID, false, nullptr, EditorControllerConstructor },
+   { &kNS_EDITINGCONTROLLER_CID, false, nullptr, nsEditingControllerConstructor },
+   { &kNS_EDITORCOMMANDTABLE_CID, false, nullptr, nsEditorCommandTableConstructor },
+   { &kNS_EDITINGCOMMANDTABLE_CID, false, nullptr, nsEditingCommandTableConstructor },
+@@ -934,16 +939,17 @@ static const mozilla::Module::ContractID
+   // Keeping the old ContractID for backward compatibility
+   { "@mozilla.org/dom/storagemanager;1", &kNS_DOMLOCALSTORAGEMANAGER_CID },
+   { "@mozilla.org/dom/sessionStorage-manager;1", &kNS_DOMSESSIONSTORAGEMANAGER_CID },
+   { "@mozilla.org/dom/json;1", &kNS_DOMJSON_CID },
+   { "@mozilla.org/editor/texteditor;1", &kNS_TEXTEDITOR_CID },
+   { DOMREQUEST_SERVICE_CONTRACTID, &kDOMREQUEST_SERVICE_CID },
+   { QUOTAMANAGER_SERVICE_CONTRACTID, &kQUOTAMANAGER_SERVICE_CID },
+   { SERVICEWORKERMANAGER_CONTRACTID, &kSERVICEWORKERMANAGER_CID },
++  { STORAGE_ACTIVITY_SERVICE_CONTRACTID, &kSTORAGEACTIVITYSERVICE_CID },
+   { PUSHNOTIFIER_CONTRACTID, &kPUSHNOTIFIER_CID },
+   { WORKERDEBUGGERMANAGER_CONTRACTID, &kWORKERDEBUGGERMANAGER_CID },
+   { NS_AUDIOCHANNELAGENT_CONTRACTID, &kNS_AUDIOCHANNELAGENT_CID },
+   { "@mozilla.org/editor/htmleditor;1", &kNS_HTMLEDITOR_CID },
+   { "@mozilla.org/editor/editorcontroller;1", &kNS_EDITORCONTROLLER_CID },
+   { "@mozilla.org/editor/editingcontroller;1", &kNS_EDITINGCONTROLLER_CID },
+   { "@mozilla.org/geolocation/service;1", &kNS_GEOLOCATION_SERVICE_CID },
+   { "@mozilla.org/geolocation;1", &kNS_GEOLOCATION_CID },

+ 30 - 0
frg/mozilla-release/work-js/1252998-02-61a1.patch

@@ -0,0 +1,30 @@
+# HG changeset patch
+# User Andrea Marchesini <amarchesini@mozilla.com>
+# Date 1524068350 -7200
+# Node ID 3220024f0ed3188dc15f638b63870459b01f3a71
+# Parent  049a96badca91d4a4d04f15de18bbdf39e984a19
+Bug 1252998 - StorageActivityService - part 2 - Use of StorageActivityService in LocalStorage, r=asuth
+
+diff --git a/ipc/glue/BackgroundParentImpl.cpp b/ipc/glue/BackgroundParentImpl.cpp
+--- a/ipc/glue/BackgroundParentImpl.cpp
++++ b/ipc/glue/BackgroundParentImpl.cpp
+@@ -283,16 +283,19 @@ mozilla::ipc::IPCResult
+ BackgroundParentImpl::RecvBroadcastLocalStorageChange(
+                                             const nsString& aDocumentURI,
+                                             const nsString& aKey,
+                                             const nsString& aOldValue,
+                                             const nsString& aNewValue,
+                                             const PrincipalInfo& aPrincipalInfo,
+                                             const bool& aIsPrivate)
+ {
++  // Let's inform the StorageActivityService about this change.
++  dom::StorageActivityService::SendActivity(aPrincipalInfo);
++
+   nsTArray<PBackgroundParent*> liveActorArray;
+   if (NS_WARN_IF(!BackgroundParent::GetLiveActorArray(this, liveActorArray))) {
+     return IPC_FAIL_NO_REASON(this);
+   }
+ 
+   for (auto* liveActor : liveActorArray) {
+     if (liveActor != this) {
+       Unused << liveActor->SendDispatchLocalStorageChange(

+ 89 - 0
frg/mozilla-release/work-js/1252998-03-61a1.patch

@@ -0,0 +1,89 @@
+# HG changeset patch
+# User Andrea Marchesini <amarchesini@mozilla.com>
+# Date 1524068351 -7200
+# Node ID 903099bf9c40214362e91f6a375d900dc1a23e79
+# Parent  7d077f50e8deca1c307a8f77a8f71875467b324c
+Bug 1252998 - StorageActivityService - part 3 - ServiceWorkerManager must not cleanup data when purge-session-history notification is dispatched, r=asuth
+
+diff --git a/dom/workers/ServiceWorkerManager.cpp b/dom/workers/ServiceWorkerManager.cpp
+--- a/dom/workers/ServiceWorkerManager.cpp
++++ b/dom/workers/ServiceWorkerManager.cpp
+@@ -92,17 +92,16 @@
+ 
+ using namespace mozilla;
+ using namespace mozilla::dom;
+ using namespace mozilla::ipc;
+ 
+ BEGIN_WORKERS_NAMESPACE
+ 
+ #define PURGE_DOMAIN_DATA "browser:purge-domain-data"
+-#define PURGE_SESSION_HISTORY "browser:purge-session-history"
+ #define CLEAR_ORIGIN_DATA "clear-origin-attributes-data"
+ 
+ static_assert(nsIHttpChannelInternal::CORS_MODE_SAME_ORIGIN == static_cast<uint32_t>(RequestMode::Same_origin),
+               "RequestMode enumeration value should match Necko CORS mode value.");
+ static_assert(nsIHttpChannelInternal::CORS_MODE_NO_CORS == static_cast<uint32_t>(RequestMode::No_cors),
+               "RequestMode enumeration value should match Necko CORS mode value.");
+ static_assert(nsIHttpChannelInternal::CORS_MODE_CORS == static_cast<uint32_t>(RequestMode::Cors),
+               "RequestMode enumeration value should match Necko CORS mode value.");
+@@ -282,18 +281,16 @@ ServiceWorkerManager::Init(ServiceWorker
+     MOZ_DIAGNOSTIC_ASSERT(aRegistrar);
+ 
+     nsTArray<ServiceWorkerRegistrationData> data;
+     aRegistrar->GetRegistrations(data);
+     LoadRegistrations(data);
+ 
+     if (obs) {
+       DebugOnly<nsresult> rv;
+-      rv = obs->AddObserver(this, PURGE_SESSION_HISTORY, false /* ownsWeak */);
+-      MOZ_ASSERT(NS_SUCCEEDED(rv));
+       rv = obs->AddObserver(this, PURGE_DOMAIN_DATA, false /* ownsWeak */);
+       MOZ_ASSERT(NS_SUCCEEDED(rv));
+       rv = obs->AddObserver(this, CLEAR_ORIGIN_DATA, false /* ownsWeak */);
+       MOZ_ASSERT(NS_SUCCEEDED(rv));
+     }
+   }
+ 
+   PBackgroundChild* actorChild = BackgroundChild::GetOrCreateForCurrentThread();
+@@ -337,17 +334,16 @@ ServiceWorkerManager::MaybeStartShutdown
+     it1.UserData()->mJobQueues.Clear();
+   }
+ 
+   nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService();
+   if (obs) {
+     obs->RemoveObserver(this, NS_XPCOM_SHUTDOWN_OBSERVER_ID);
+ 
+     if (XRE_IsParentProcess()) {
+-      obs->RemoveObserver(this, PURGE_SESSION_HISTORY);
+       obs->RemoveObserver(this, PURGE_DOMAIN_DATA);
+       obs->RemoveObserver(this, CLEAR_ORIGIN_DATA);
+     }
+   }
+ 
+   if (!mActor) {
+     return;
+   }
+@@ -3838,23 +3834,16 @@ ServiceWorkerManager::ShouldReportToWind
+   return NS_OK;
+ }
+ 
+ NS_IMETHODIMP
+ ServiceWorkerManager::Observe(nsISupports* aSubject,
+                               const char* aTopic,
+                               const char16_t* aData)
+ {
+-  if (strcmp(aTopic, PURGE_SESSION_HISTORY) == 0) {
+-    MOZ_ASSERT(XRE_IsParentProcess());
+-    RemoveAll();
+-    PropagateRemoveAll();
+-    return NS_OK;
+-  }
+-
+   if (strcmp(aTopic, PURGE_DOMAIN_DATA) == 0) {
+     MOZ_ASSERT(XRE_IsParentProcess());
+     nsAutoString domain(aData);
+     RemoveAndPropagate(NS_ConvertUTF16toUTF8(domain));
+     return NS_OK;
+   }
+ 
+   if (strcmp(aTopic, CLEAR_ORIGIN_DATA) == 0) {

+ 573 - 0
frg/mozilla-release/work-js/1252998-04-61a1.patch

@@ -0,0 +1,573 @@
+# HG changeset patch
+# User Andrea Marchesini <amarchesini@mozilla.com>
+# Date 1524068352 -7200
+# Node ID f60bc2fa25660c571283eb63c3475aff4566f2ac
+# Parent  702f5938a2ad23b3fc9ce8cb40a18e95c20d2b61
+Bug 1252998 - StorageActivityService - part 4 - Introduce ServiceWorkerCleanUp.jsm to clean up ServiceWorker data, r=asuth
+
+diff --git a/browser/components/extensions/ext-browsingData.js b/browser/components/extensions/ext-browsingData.js
+--- a/browser/components/extensions/ext-browsingData.js
++++ b/browser/components/extensions/ext-browsingData.js
+@@ -8,16 +8,18 @@ ChromeUtils.defineModuleGetter(this, "Pl
+ ChromeUtils.defineModuleGetter(this, "Preferences",
+                                "resource://gre/modules/Preferences.jsm");
+ ChromeUtils.defineModuleGetter(this, "Sanitizer",
+                                "resource:///modules/Sanitizer.jsm");
+ ChromeUtils.defineModuleGetter(this, "Services",
+                                "resource://gre/modules/Services.jsm");
+ ChromeUtils.defineModuleGetter(this, "setTimeout",
+                                "resource://gre/modules/Timer.jsm");
++ChromeUtils.defineModuleGetter(this, "ServiceWorkerCleanUp",
++                               "resource://gre/modules/ServiceWorkerCleanUp.jsm");
+ 
+ XPCOMUtils.defineLazyServiceGetter(this, "serviceWorkerManager",
+                                    "@mozilla.org/serviceworkers/manager;1",
+                                    "nsIServiceWorkerManager");
+ XPCOMUtils.defineLazyServiceGetter(this, "quotaManagerService",
+                                    "@mozilla.org/dom/quota-manager-service;1",
+                                    "nsIQuotaManagerService");
+ 
+@@ -126,32 +128,16 @@ const clearPasswords = async function(op
+       }
+     }
+   } else {
+     // Remove everything.
+     loginManager.removeAllLogins();
+   }
+ };
+ 
+-const clearServiceWorkers = async function() {
+-  // Clearing service workers does not support timestamps.
+-  let yieldCounter = 0;
+-
+-  // Iterate through the service workers and remove them.
+-  let serviceWorkers = serviceWorkerManager.getAllRegistrations();
+-  for (let i = 0; i < serviceWorkers.length; i++) {
+-    let sw = serviceWorkers.queryElementAt(i, Ci.nsIServiceWorkerRegistrationInfo);
+-    let host = sw.principal.URI.host;
+-    serviceWorkerManager.removeAndPropagate(host);
+-    if (++yieldCounter % YIELD_PERIOD == 0) {
+-      await new Promise(resolve => setTimeout(resolve, 0)); // Don't block the main thread too long.
+-    }
+-  }
+-};
+-
+ const doRemoval = (options, dataToRemove, extension) => {
+   if (options.originTypes &&
+       (options.originTypes.protectedWeb || options.originTypes.extension)) {
+     return Promise.reject(
+       {message: "Firefox does not support protectedWeb or extension as originTypes."});
+   }
+ 
+   let removalPromises = [];
+@@ -179,17 +165,17 @@ const doRemoval = (options, dataToRemove
+           break;
+         case "localStorage":
+           removalPromises.push(clearLocalStorage(options));
+           break;
+         case "passwords":
+           removalPromises.push(clearPasswords(options));
+           break;
+         case "serviceWorkers":
+-          removalPromises.push(clearServiceWorkers());
++          removalPromises.push(ServiceWorkerCleanUp.removeAll());
+           break;
+         default:
+           invalidDataTypes.push(dataType);
+       }
+     }
+   }
+   if (extension && invalidDataTypes.length) {
+     extension.logger.warn(
+diff --git a/browser/modules/Sanitizer.jsm b/browser/modules/Sanitizer.jsm
+--- a/browser/modules/Sanitizer.jsm
++++ b/browser/modules/Sanitizer.jsm
+@@ -12,21 +12,19 @@ XPCOMUtils.defineLazyModuleGetters(this,
+   AppConstants: "resource://gre/modules/AppConstants.jsm",
+   PlacesUtils: "resource://gre/modules/PlacesUtils.jsm",
+   FormHistory: "resource://gre/modules/FormHistory.jsm",
+   Downloads: "resource://gre/modules/Downloads.jsm",
+   DownloadsCommon: "resource:///modules/DownloadsCommon.jsm",
+   TelemetryStopwatch: "resource://gre/modules/TelemetryStopwatch.jsm",
+   console: "resource://gre/modules/Console.jsm",
+   setTimeout: "resource://gre/modules/Timer.jsm",
++  ServiceWorkerCleanUp: "resource://gre/modules/ServiceWorkerCleanUp.jsm",
+ });
+ 
+-XPCOMUtils.defineLazyServiceGetter(this, "serviceWorkerManager",
+-                                   "@mozilla.org/serviceworkers/manager;1",
+-                                   "nsIServiceWorkerManager");
+ XPCOMUtils.defineLazyServiceGetter(this, "quotaManagerService",
+                                    "@mozilla.org/dom/quota-manager-service;1",
+                                    "nsIQuotaManagerService");
+ 
+ // Used as unique id for pending sanitizations.
+ var gPendingSanitizationSerial = 0;
+ 
+ /**
+@@ -362,38 +360,20 @@ var Sanitizer = {
+         ChromeUtils.import("resource:///modules/offlineAppCache.jsm");
+         // This doesn't wait for the cleanup to be complete.
+         OfflineAppCacheHelper.clear();
+ 
+         // LocalStorage
+         Services.obs.notifyObservers(null, "extension:purge-localStorage");
+ 
+         // ServiceWorkers
+-        let promises = [];
+-        let serviceWorkers = serviceWorkerManager.getAllRegistrations();
+-        for (let i = 0; i < serviceWorkers.length; i++) {
+-          let sw = serviceWorkers.queryElementAt(i, Ci.nsIServiceWorkerRegistrationInfo);
+-
+-          promises.push(new Promise(resolve => {
+-            let unregisterCallback = {
+-              unregisterSucceeded: () => { resolve(true); },
+-              // We don't care about failures.
+-              unregisterFailed: () => { resolve(true); },
+-              QueryInterface: XPCOMUtils.generateQI(
+-                [Ci.nsIServiceWorkerUnregisterCallback])
+-            };
+-
+-            serviceWorkerManager.propagateUnregister(sw.principal, unregisterCallback, sw.scope);
+-          }));
+-        }
+-
+-        await Promise.all(promises);
++        await ServiceWorkerCleanUp.removeAll();
+ 
+         // QuotaManager
+-        promises = [];
++        let promises = [];
+         await new Promise(resolve => {
+           quotaManagerService.getUsage(request => {
+             if (request.resultCode != Cr.NS_OK) {
+               // We are probably shutting down. We don't want to propagate the
+               // error, rejecting the promise.
+               resolve();
+               return;
+             }
+diff --git a/browser/modules/SiteDataManager.jsm b/browser/modules/SiteDataManager.jsm
+--- a/browser/modules/SiteDataManager.jsm
++++ b/browser/modules/SiteDataManager.jsm
+@@ -1,18 +1,17 @@
+ "use strict";
+ 
+ ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
+ ChromeUtils.import("resource://gre/modules/Services.jsm");
+ 
+ ChromeUtils.defineModuleGetter(this, "OfflineAppCacheHelper",
+                                "resource:///modules/offlineAppCache.jsm");
+-XPCOMUtils.defineLazyServiceGetter(this, "serviceWorkerManager",
+-                                   "@mozilla.org/serviceworkers/manager;1",
+-                                   "nsIServiceWorkerManager");
++ChromeUtils.defineModuleGetter(this, "ServiceWorkerCleanUp",
++                               "resource://gre/modules/ServiceWorkerCleanUp.jsm");
+ 
+ var EXPORTED_SYMBOLS = [
+   "SiteDataManager"
+ ];
+ 
+ XPCOMUtils.defineLazyGetter(this, "gStringBundle", function() {
+   return Services.strings.createBundle("chrome://browser/locale/siteData.properties");
+ });
+@@ -310,37 +309,21 @@ var SiteDataManager = {
+   _removeCookies(site) {
+     for (let cookie of site.cookies) {
+       Services.cookies.remove(
+         cookie.host, cookie.name, cookie.path, false, cookie.originAttributes);
+     }
+     site.cookies = [];
+   },
+ 
+-  _unregisterServiceWorker(serviceWorker) {
+-    return new Promise(resolve => {
+-      let unregisterCallback = {
+-        unregisterSucceeded: resolve,
+-        unregisterFailed: resolve, // We don't care about failures.
+-        QueryInterface: XPCOMUtils.generateQI([Ci.nsIServiceWorkerUnregisterCallback])
+-      };
+-      serviceWorkerManager.propagateUnregister(serviceWorker.principal, unregisterCallback, serviceWorker.scope);
+-    });
+-  },
+-
+   _removeServiceWorkersForSites(sites) {
+     let promises = [];
+-    let serviceWorkers = serviceWorkerManager.getAllRegistrations();
+-    for (let i = 0; i < serviceWorkers.length; i++) {
+-      let sw = serviceWorkers.queryElementAt(i, Ci.nsIServiceWorkerRegistrationInfo);
+-      // Sites are grouped and removed by host so we unregister service workers by the same host as well
+-      if (sites.has(sw.principal.URI.host)) {
+-        promises.push(this._unregisterServiceWorker(sw));
+-      }
+-    }
++    sites.forEach(s => {
++      promises.push(ServiceWorkerCleanUp.removeFromHost(s.principals[0].URI.host));
++    });
+     return Promise.all(promises);
+   },
+ 
+   /**
+    * Removes all site data for the specified list of hosts.
+    *
+    * @param {Array} a list of hosts to match for removal.
+    * @returns a Promise that resolves when data is removed and the site data
+@@ -443,37 +426,30 @@ var SiteDataManager = {
+    *   - persistent-storage permissions
+    *
+    * @returns a Promise that resolves with the cache size on disk in bytes
+    */
+   async removeSiteData() {
+     Services.cookies.removeAll();
+     OfflineAppCacheHelper.clear();
+ 
+-    // Iterate through the service workers and remove them.
+-    let promises = [];
+-    let serviceWorkers = serviceWorkerManager.getAllRegistrations();
+-    for (let i = 0; i < serviceWorkers.length; i++) {
+-      let sw = serviceWorkers.queryElementAt(i, Ci.nsIServiceWorkerRegistrationInfo);
+-      promises.push(this._unregisterServiceWorker(sw));
+-    }
+-    await Promise.all(promises);
++    await ServiceWorkerCleanUp.removeAll();
+ 
+     // Refresh sites using quota usage again.
+     // This is for the case:
+     //   1. User goes to the about:preferences Site Data section.
+     //   2. With the about:preferences opened, user visits another website.
+     //   3. The website saves to quota usage, like indexedDB.
+     //   4. User goes back to the Site Data section and commands to clear all site data.
+     // For this case, we should refresh the site list so not to miss the website in the step 3.
+     // We don't do "Clear All" on the quota manager like the cookie, appcache, http cache above
+     // because that would clear browser data as well too,
+     // see https://bugzilla.mozilla.org/show_bug.cgi?id=1312361#c9
+     this._sites.clear();
+     await this._getQuotaUsage();
+-    promises = [];
++    let promises = [];
+     for (let site of this._sites.values()) {
+       this._removePermission(site);
+       promises.push(this._removeQuotaUsage(site));
+     }
+     return Promise.all(promises).then(() => this.updateSites());
+   },
+ };
+diff --git a/dom/interfaces/base/nsIServiceWorkerManager.idl b/dom/interfaces/base/nsIServiceWorkerManager.idl
+--- a/dom/interfaces/base/nsIServiceWorkerManager.idl
++++ b/dom/interfaces/base/nsIServiceWorkerManager.idl
+@@ -168,27 +168,16 @@ interface nsIServiceWorkerManager : nsIS
+   [noscript] nsISupports GetActive(in nsPIDOMWindowInner aWindow, in DOMString aScope);
+ 
+   /*
+    * Returns a ServiceWorker object representing the active worker controlling this
+    * window.
+    */
+   [noscript] nsISupports GetDocumentController(in nsPIDOMWindowInner aWindow);
+ 
+-  /*
+-   * Clears ServiceWorker registrations from memory and disk for the specified
+-   * host.
+-   * - All ServiceWorker instances change their state to redundant.
+-   * - Existing ServiceWorker instances handling fetches will keep running.
+-   * - All documents will immediately stop being controlled.
+-   * - Unregister jobs will be queued for all registrations.
+-   *   This eventually results in the registration being deleted from disk too.
+-   */
+-  void removeAndPropagate(in AUTF8String aHost);
+-
+   // Testing
+   DOMString getScopeForUrl(in nsIPrincipal aPrincipal, in DOMString aPath);
+ 
+   // It returns an array of nsIServiceWorkerRegistrationInfos.
+   nsIArray getAllRegistrations();
+ 
+   // It calls softUpdate() for each child process.
+   [implicit_jscontext] void propagateSoftUpdate(in jsval aOriginAttributes,
+diff --git a/dom/workers/ServiceWorkerManager.cpp b/dom/workers/ServiceWorkerManager.cpp
+--- a/dom/workers/ServiceWorkerManager.cpp
++++ b/dom/workers/ServiceWorkerManager.cpp
+@@ -91,17 +91,16 @@
+ #endif
+ 
+ using namespace mozilla;
+ using namespace mozilla::dom;
+ using namespace mozilla::ipc;
+ 
+ BEGIN_WORKERS_NAMESPACE
+ 
+-#define PURGE_DOMAIN_DATA "browser:purge-domain-data"
+ #define CLEAR_ORIGIN_DATA "clear-origin-attributes-data"
+ 
+ static_assert(nsIHttpChannelInternal::CORS_MODE_SAME_ORIGIN == static_cast<uint32_t>(RequestMode::Same_origin),
+               "RequestMode enumeration value should match Necko CORS mode value.");
+ static_assert(nsIHttpChannelInternal::CORS_MODE_NO_CORS == static_cast<uint32_t>(RequestMode::No_cors),
+               "RequestMode enumeration value should match Necko CORS mode value.");
+ static_assert(nsIHttpChannelInternal::CORS_MODE_CORS == static_cast<uint32_t>(RequestMode::Cors),
+               "RequestMode enumeration value should match Necko CORS mode value.");
+@@ -281,18 +280,16 @@ ServiceWorkerManager::Init(ServiceWorker
+     MOZ_DIAGNOSTIC_ASSERT(aRegistrar);
+ 
+     nsTArray<ServiceWorkerRegistrationData> data;
+     aRegistrar->GetRegistrations(data);
+     LoadRegistrations(data);
+ 
+     if (obs) {
+       DebugOnly<nsresult> rv;
+-      rv = obs->AddObserver(this, PURGE_DOMAIN_DATA, false /* ownsWeak */);
+-      MOZ_ASSERT(NS_SUCCEEDED(rv));
+       rv = obs->AddObserver(this, CLEAR_ORIGIN_DATA, false /* ownsWeak */);
+       MOZ_ASSERT(NS_SUCCEEDED(rv));
+     }
+   }
+ 
+   PBackgroundChild* actorChild = BackgroundChild::GetOrCreateForCurrentThread();
+   if (NS_WARN_IF(!actorChild)) {
+     MaybeStartShutdown();
+@@ -334,17 +331,16 @@ ServiceWorkerManager::MaybeStartShutdown
+     it1.UserData()->mJobQueues.Clear();
+   }
+ 
+   nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService();
+   if (obs) {
+     obs->RemoveObserver(this, NS_XPCOM_SHUTDOWN_OBSERVER_ID);
+ 
+     if (XRE_IsParentProcess()) {
+-      obs->RemoveObserver(this, PURGE_DOMAIN_DATA);
+       obs->RemoveObserver(this, CLEAR_ORIGIN_DATA);
+     }
+   }
+ 
+   if (!mActor) {
+     return;
+   }
+ 
+@@ -3596,24 +3592,16 @@ ServiceWorkerManager::ForceUnregister(Re
+     entry.Data()->Cancel();
+     entry.Remove();
+   }
+ 
+   // Since Unregister is async, it is ok to call it in an enumeration.
+   Unregister(aRegistration->mPrincipal, nullptr, NS_ConvertUTF8toUTF16(aRegistration->mScope));
+ }
+ 
+-NS_IMETHODIMP
+-ServiceWorkerManager::RemoveAndPropagate(const nsACString& aHost)
+-{
+-  Remove(aHost);
+-  PropagateRemove(aHost);
+-  return NS_OK;
+-}
+-
+ void
+ ServiceWorkerManager::Remove(const nsACString& aHost)
+ {
+   AssertIsOnMainThread();
+ 
+   for (auto it1 = mRegistrationInfos.Iter(); !it1.Done(); it1.Next()) {
+     ServiceWorkerManager::RegistrationDataPerPrincipal* data = it1.UserData();
+     for (auto it2 = data->mInfos.Iter(); !it2.Done(); it2.Next()) {
+@@ -3834,23 +3822,16 @@ ServiceWorkerManager::ShouldReportToWind
+   return NS_OK;
+ }
+ 
+ NS_IMETHODIMP
+ ServiceWorkerManager::Observe(nsISupports* aSubject,
+                               const char* aTopic,
+                               const char16_t* aData)
+ {
+-  if (strcmp(aTopic, PURGE_DOMAIN_DATA) == 0) {
+-    MOZ_ASSERT(XRE_IsParentProcess());
+-    nsAutoString domain(aData);
+-    RemoveAndPropagate(NS_ConvertUTF16toUTF8(domain));
+-    return NS_OK;
+-  }
+-
+   if (strcmp(aTopic, CLEAR_ORIGIN_DATA) == 0) {
+     MOZ_ASSERT(XRE_IsParentProcess());
+     OriginAttributesPattern pattern;
+     MOZ_ALWAYS_TRUE(pattern.Init(nsAutoString(aData)));
+ 
+     RemoveAllRegistrations(&pattern);
+     return NS_OK;
+   }
+diff --git a/dom/workers/test/serviceworkers/test_sanitize_domain.html b/dom/workers/test/serviceworkers/test_sanitize_domain.html
+--- a/dom/workers/test/serviceworkers/test_sanitize_domain.html
++++ b/dom/workers/test/serviceworkers/test_sanitize_domain.html
+@@ -33,17 +33,17 @@
+       });
+     }
+ 
+     registerSW().then(function() {
+       return testFrame("http://example.com/tests/dom/workers/test/serviceworkers/sanitize/frame.html").then(function(body) {
+         is(body, "intercepted", "Expected serviceworker to intercept request");
+       });
+     }).then(function() {
+-      SpecialPowers.removeServiceWorkerDataForExampleDomain();
++      return SpecialPowers.removeServiceWorkerDataForExampleDomain();
+     }).then(function() {
+       return checkDomainRegistration("prefixexample.com", true /* exists */)
+         .then(function(e) {
+           return checkDomainRegistration("example.com", false /* exists */);
+         }).then(function(e) {
+           SimpleTest.finish();
+         });
+     })
+diff --git a/testing/specialpowers/content/specialpowersAPI.js b/testing/specialpowers/content/specialpowersAPI.js
+--- a/testing/specialpowers/content/specialpowersAPI.js
++++ b/testing/specialpowers/content/specialpowersAPI.js
+@@ -14,16 +14,17 @@ var global = this;
+ 
+ ChromeUtils.import("chrome://specialpowers/content/MockFilePicker.jsm");
+ ChromeUtils.import("chrome://specialpowers/content/MockColorPicker.jsm");
+ ChromeUtils.import("chrome://specialpowers/content/MockPermissionPrompt.jsm");
+ ChromeUtils.import("resource://gre/modules/Services.jsm");
+ ChromeUtils.import("resource://gre/modules/PrivateBrowsingUtils.jsm");
+ ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
+ ChromeUtils.import("resource://gre/modules/NetUtil.jsm");
++ChromeUtils.import("resource://gre/modules/ServiceWorkerCleanUp.jsm");
+ 
+ // We're loaded with "this" not set to the global in some cases, so we
+ // have to play some games to get at the global object here.  Normally
+ // we'd try "this" from a function called with undefined this value,
+ // but this whole file is in strict mode.  So instead fall back on
+ // returning "this" from indirect eval, which returns the global.
+ if (!(function() { var e = eval; return e("this"); })().File) { // eslint-disable-line no-eval
+     Cu.importGlobalProperties(["File"]);
+@@ -1914,21 +1915,21 @@ SpecialPowersAPI.prototype = {
+       "op": "notify",
+       "observerTopic": topic,
+       "observerData": data
+     };
+     this._sendSyncMessage("SPObserverService", msg);
+   },
+ 
+   removeAllServiceWorkerData() {
+-    this.notifyObserversInParentProcess(null, "browser:purge-session-history", "");
++    return wrapIfUnwrapped(ServiceWorkerCleanUp.removeAll());
+   },
+ 
+   removeServiceWorkerDataForExampleDomain() {
+-    this.notifyObserversInParentProcess(null, "browser:purge-domain-data", "example.com");
++    return wrapIfUnwrapped(ServiceWorkerCleanUp.removeFromHost("example.com"));
+   },
+ 
+   cleanUpSTSData(origin, flags) {
+     return this._sendSyncMessage("SPCleanUpSTSData", {origin, flags: flags || 0});
+   },
+ 
+   requestDumpCoverageCounters() {
+     this._sendSyncMessage("SPRequestDumpCoverageCounters", {});
+diff --git a/toolkit/forgetaboutsite/ForgetAboutSite.jsm b/toolkit/forgetaboutsite/ForgetAboutSite.jsm
+--- a/toolkit/forgetaboutsite/ForgetAboutSite.jsm
++++ b/toolkit/forgetaboutsite/ForgetAboutSite.jsm
+@@ -6,16 +6,18 @@
+ 
+ ChromeUtils.import("resource://gre/modules/Services.jsm");
+ ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
+ ChromeUtils.import("resource://gre/modules/NetUtil.jsm");
+ ChromeUtils.defineModuleGetter(this, "PlacesUtils",
+                                "resource://gre/modules/PlacesUtils.jsm");
+ ChromeUtils.defineModuleGetter(this, "Downloads",
+                                "resource://gre/modules/Downloads.jsm");
++ChromeUtils.defineModuleGetter(this, "ServiceWorkerCleanUp",
++                               "resource://gre/modules/ServiceWorkerCleanUp.jsm");
+ 
+ var EXPORTED_SYMBOLS = ["ForgetAboutSite"];
+ 
+ /**
+  * Returns true if the string passed in is part of the root domain of the
+  * current string.  For example, if this is "www.mozilla.org", and we pass in
+  * "mozilla.org", this will return true.  It would return false the other way
+  * around.
+@@ -120,17 +122,21 @@ var ForgetAboutSite = {
+         } catch (ex) {
+           // Ignore entry
+         } finally {
+           resolve();
+         }
+       }));
+     }
+ 
+-    // Offline Storages
++    // ServiceWorkers
++    await ServiceWorkerCleanUp.removeFromHost("http://" + aDomain);
++    await ServiceWorkerCleanUp.removeFromHost("https://" + aDomain);
++
++    // Offline Storages. This must run after the ServiceWorkers promises.
+     promises.push((async function() {
+       // delete data from both HTTP and HTTPS sites
+       let httpURI = NetUtil.newURI("http://" + aDomain);
+       let httpsURI = NetUtil.newURI("https://" + aDomain);
+       // Following code section has been reverted to the state before Bug 1238183,
+       // but added a new argument to clearStoragesForPrincipal() for indicating
+       // clear all storages under a given origin.
+       let httpPrincipal = Services.scriptSecurityManager
+diff --git a/toolkit/forgetaboutsite/ServiceWorkerCleanUp.jsm b/toolkit/forgetaboutsite/ServiceWorkerCleanUp.jsm
+new file mode 100644
+--- /dev/null
++++ b/toolkit/forgetaboutsite/ServiceWorkerCleanUp.jsm
+@@ -0,0 +1,48 @@
++/* This Source Code Form is subject to the terms of the Mozilla Public
++ * License, v. 2.0. If a copy of the MPL was not distributed with this
++ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
++
++"use strict";
++
++ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
++
++XPCOMUtils.defineLazyServiceGetter(this, "serviceWorkerManager",
++                                   "@mozilla.org/serviceworkers/manager;1",
++                                   "nsIServiceWorkerManager");
++
++this.EXPORTED_SYMBOLS = ["ServiceWorkerCleanUp"];
++
++function unregisterServiceWorker(aSW) {
++  return new Promise(resolve => {
++    let unregisterCallback = {
++      unregisterSucceeded: resolve,
++      unregisterFailed: resolve, // We don't care about failures.
++      QueryInterface: XPCOMUtils.generateQI([Ci.nsIServiceWorkerUnregisterCallback])
++    };
++    serviceWorkerManager.propagateUnregister(aSW.principal, unregisterCallback, aSW.scope);
++  });
++}
++
++this.ServiceWorkerCleanUp = {
++  removeFromHost(aHost) {
++    let promises = [];
++    let serviceWorkers = serviceWorkerManager.getAllRegistrations();
++    for (let i = 0; i < serviceWorkers.length; i++) {
++      let sw = serviceWorkers.queryElementAt(i, Ci.nsIServiceWorkerRegistrationInfo);
++      if (sw.principal.URI.host == aHost) {
++        promises.push(unregisterServiceWorker(sw));
++      }
++    }
++    return Promise.all(promises);
++  },
++
++  removeAll() {
++    let promises = [];
++    let serviceWorkers = serviceWorkerManager.getAllRegistrations();
++    for (let i = 0; i < serviceWorkers.length; i++) {
++      let sw = serviceWorkers.queryElementAt(i, Ci.nsIServiceWorkerRegistrationInfo);
++      promises.push(unregisterServiceWorker(sw));
++    }
++    return Promise.all(promises);
++  },
++};
+diff --git a/toolkit/forgetaboutsite/moz.build b/toolkit/forgetaboutsite/moz.build
+--- a/toolkit/forgetaboutsite/moz.build
++++ b/toolkit/forgetaboutsite/moz.build
+@@ -3,12 +3,13 @@
+ # This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ 
+ XPCSHELL_TESTS_MANIFESTS += ['test/unit/xpcshell.ini']
+ 
+ EXTRA_JS_MODULES += [
+     'ForgetAboutSite.jsm',
++    'ServiceWorkerCleanUp.jsm',
+ ]
+ 
+ with Files('**'):
+     BUG_COMPONENT = ('Toolkit', 'Forget About Site')

+ 364 - 0
frg/mozilla-release/work-js/1252998-05-61a1.patch

@@ -0,0 +1,364 @@
+# HG changeset patch
+# User Andrea Marchesini <amarchesini@mozilla.com>
+# Date 1524068353 -7200
+# Node ID 4367167b71d8972d5af696c45a3cd4d5f8f30521
+# Parent  a85f1337649acf0dec29e7102949bf176f3c59cc
+Bug 1252998 - StorageActivityService - part 5 - nsIStorageActivityService::getActiveOrigins, r=asuth
+
+diff --git a/browser/components/customizableui/CustomizableWidgets.jsm b/browser/components/customizableui/CustomizableWidgets.jsm
+--- a/browser/components/customizableui/CustomizableWidgets.jsm
++++ b/browser/components/customizableui/CustomizableWidgets.jsm
+@@ -1227,17 +1227,17 @@ if (Services.prefs.getBoolPref("privacy.
+     type: "view",
+     viewId: "PanelUI-panicView",
+ 
+     forgetButtonCalled(aEvent) {
+       let doc = aEvent.target.ownerDocument;
+       let group = doc.getElementById("PanelUI-panic-timeSpan");
+       group.selectedItem = doc.getElementById("PanelUI-panic-5min");
+       let itemsToClear = [
+-        "cookies", "history", "openWindows", "formdata", "sessions", "cache", "downloads"
++        "cookies", "history", "openWindows", "formdata", "sessions", "cache", "downloads", "offlineApps"
+       ];
+       let newWindowPrivateState = PrivateBrowsingUtils.isWindowPrivate(doc.defaultView) ?
+                                   "private" : "non-private";
+       let promise = Sanitizer.sanitize(itemsToClear, {
+         ignoreTimespan: false,
+         range: Sanitizer.getClearRange(+group.value),
+         privateStateForNewWindow: newWindowPrivateState,
+       });
+diff --git a/browser/modules/Sanitizer.jsm b/browser/modules/Sanitizer.jsm
+--- a/browser/modules/Sanitizer.jsm
++++ b/browser/modules/Sanitizer.jsm
+@@ -13,18 +13,22 @@ XPCOMUtils.defineLazyModuleGetters(this,
+   PlacesUtils: "resource://gre/modules/PlacesUtils.jsm",
+   FormHistory: "resource://gre/modules/FormHistory.jsm",
+   Downloads: "resource://gre/modules/Downloads.jsm",
+   DownloadsCommon: "resource:///modules/DownloadsCommon.jsm",
+   TelemetryStopwatch: "resource://gre/modules/TelemetryStopwatch.jsm",
+   console: "resource://gre/modules/Console.jsm",
+   setTimeout: "resource://gre/modules/Timer.jsm",
+   ServiceWorkerCleanUp: "resource://gre/modules/ServiceWorkerCleanUp.jsm",
++  OfflineAppCacheHelper: "resource:///modules/offlineAppCache.jsm",
+ });
+ 
++XPCOMUtils.defineLazyServiceGetter(this, "sas",
++                                   "@mozilla.org/storage/activity-service;1",
++                                   "nsIStorageActivityService");
+ XPCOMUtils.defineLazyServiceGetter(this, "quotaManagerService",
+                                    "@mozilla.org/dom/quota-manager-service;1",
+                                    "nsIQuotaManagerService");
+ 
+ // Used as unique id for pending sanitizations.
+ var gPendingSanitizationSerial = 0;
+ 
+ /**
+@@ -351,21 +355,50 @@ var Sanitizer = {
+         if (seenException) {
+           throw seenException;
+         }
+       },
+     },
+ 
+     offlineApps: {
+       async clear(range) {
+-        // AppCache
+-        ChromeUtils.import("resource:///modules/offlineAppCache.jsm");
+-        // This doesn't wait for the cleanup to be complete.
++        // AppCache: this doesn't wait for the cleanup to be complete.
+         OfflineAppCacheHelper.clear();
+ 
++        if (range) {
++          let principals = sas.getActiveOrigins(range[0], range[1])
++                              .QueryInterface(Ci.nsIArray);
++
++          let promises = [];
++
++          for (let i = 0; i < principals.length; ++i) {
++            let principal = principals.queryElementAt(i, Ci.nsIPrincipal);
++
++            if (principal.URI.scheme != "http" &&
++                principal.URI.scheme != "https" &&
++                principal.URI.scheme != "file") {
++              continue;
++            }
++
++            // LocalStorage
++            Services.obs.notifyObservers(null, "browser:purge-domain-data", principal.URI.host);
++
++            // ServiceWorkers
++            await ServiceWorkerCleanUp.removeFromPrincipal(principal);
++
++            // QuotaManager
++            promises.push(new Promise(r => {
++              let req = quotaManagerService.clearStoragesForPrincipal(principal, null, false);
++              req.callback = () => { r(); };
++            }));
++          }
++
++          return Promise.all(promises);
++        }
++
+         // LocalStorage
+         Services.obs.notifyObservers(null, "extension:purge-localStorage");
+ 
+         // ServiceWorkers
+         await ServiceWorkerCleanUp.removeAll();
+ 
+         // QuotaManager
+         let promises = [];
+diff --git a/dom/interfaces/storage/nsIStorageActivityService.idl b/dom/interfaces/storage/nsIStorageActivityService.idl
+--- a/dom/interfaces/storage/nsIStorageActivityService.idl
++++ b/dom/interfaces/storage/nsIStorageActivityService.idl
+@@ -1,25 +1,34 @@
+ /* -*- Mode: IDL; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+ /* This Source Code Form is subject to the terms of the Mozilla Public
+  * License, v. 2.0. If a copy of the MPL was not distributed with this
+  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+ 
+ #include "domstubs.idl"
+ 
++interface nsIArray;
++
+ /**
+  * nsIStorageActivityService is a service that can be used to know which
+  * origins have been active in a time range. This information can be used to
+  * implement "Clear Recent History" or similar features.
+  *
+  * If you are implementing a new Storage component, you should use
+  * QuotaManager. But if you don't do it, remember to call
+  * StorageActivityService methods in order to inform this service about
+  * 'writing' operations executed by origins.
+  */
+ [scriptable, builtinclass, uuid(fd1310ba-d1be-4327-988e-92b39fcff6f4)]
+ interface nsIStorageActivityService : nsISupports
+ {
++  // This returns an array of nsIPrincipals, active between |from| and |to|
++  // timestamps. Note activities older than 1 day are forgotten.
++  // Activity details are not persisted, so this only covers activity since
++  // Firefox was started.  All codebase principals are logged, which includes
++  // non-system principals like "moz-extension://ID", "moz-safe-about:home",
++  // "about:newtab", so principals may need to be filtered before being used.
++  nsIArray getActiveOrigins(in PRTime from, in PRTime to);
+ };
+ 
+ %{ C++
+ #define STORAGE_ACTIVITY_SERVICE_CONTRACTID "@mozilla.org/storage/activity-service;1"
+ %}
+diff --git a/dom/storage/StorageActivityService.cpp b/dom/storage/StorageActivityService.cpp
+--- a/dom/storage/StorageActivityService.cpp
++++ b/dom/storage/StorageActivityService.cpp
+@@ -3,16 +3,18 @@
+ /* This Source Code Form is subject to the terms of the Mozilla Public
+  * License, v. 2.0. If a copy of the MPL was not distributed with this
+  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+ 
+ #include "StorageActivityService.h"
+ 
+ #include "mozilla/ipc/BackgroundUtils.h"
+ #include "mozilla/StaticPtr.h"
++#include "nsIMutableArray.h"
++#include "nsSupportsPrimitives.h"
+ #include "nsXPCOM.h"
+ 
+ // This const is used to know when origin activities should be purged because
+ // too old. This value should be in sync with what the UI needs.
+ #define TIME_MAX_SECS 86400 /* 24 hours */
+ 
+ namespace mozilla {
+ namespace dom {
+@@ -20,27 +22,39 @@ namespace dom {
+ static StaticRefPtr<StorageActivityService> gStorageActivityService;
+ static bool gStorageActivityShutdown = false;
+ 
+ /* static */ void
+ StorageActivityService::SendActivity(nsIPrincipal* aPrincipal)
+ {
+   MOZ_ASSERT(NS_IsMainThread());
+ 
++  if (!aPrincipal ||
++      BasePrincipal::Cast(aPrincipal)->Kind() != BasePrincipal::eCodebasePrincipal) {
++    // Only codebase principals.
++    return;
++  }
++
+   RefPtr<StorageActivityService> service = GetOrCreate();
+   if (NS_WARN_IF(!service)) {
+     return;
+   }
+ 
+   service->SendActivityInternal(aPrincipal);
+ }
+ 
+ /* static */ void
+ StorageActivityService::SendActivity(const mozilla::ipc::PrincipalInfo& aPrincipalInfo)
+ {
++  if (aPrincipalInfo.type() !=
++      mozilla::ipc::PrincipalInfo::TContentPrincipalInfo) {
++    // only content principal.
++    return;
++  }
++
+   RefPtr<Runnable> r = NS_NewRunnableFunction(
+     "StorageActivityService::SendActivity",
+     [aPrincipalInfo] () {
+       MOZ_ASSERT(NS_IsMainThread());
+ 
+       nsCOMPtr<nsIPrincipal> principal =
+         mozilla::ipc::PrincipalInfoToPrincipal(aPrincipalInfo);
+ 
+@@ -86,29 +100,30 @@ StorageActivityService::~StorageActivity
+   MOZ_ASSERT(!mTimer);
+ }
+ 
+ void
+ StorageActivityService::SendActivityInternal(nsIPrincipal* aPrincipal)
+ {
+   MOZ_ASSERT(NS_IsMainThread());
+   MOZ_ASSERT(aPrincipal);
++  MOZ_ASSERT(BasePrincipal::Cast(aPrincipal)->Kind() == BasePrincipal::eCodebasePrincipal);
+ 
+   if (!XRE_IsParentProcess()) {
+     SendActivityToParent(aPrincipal);
+     return;
+   }
+ 
+   nsAutoCString origin;
+   nsresult rv = aPrincipal->GetOrigin(origin);
+   if (NS_WARN_IF(NS_FAILED(rv))) {
+     return;
+   }
+ 
+-  mActivities.Put(origin, TimeStamp::NowLoRes());
++  mActivities.Put(origin, PR_Now());
+ 
+   MaybeStartTimer();
+ }
+ 
+ void
+ StorageActivityService::SendActivityToParent(nsIPrincipal* aPrincipal)
+ {
+   MOZ_ASSERT(NS_IsMainThread());
+@@ -173,32 +188,66 @@ StorageActivityService::MaybeStopTimer()
+ }
+ 
+ NS_IMETHODIMP
+ StorageActivityService::Notify(nsITimer* aTimer)
+ {
+   MOZ_ASSERT(NS_IsMainThread());
+   MOZ_ASSERT(mTimer == aTimer);
+ 
+-  TimeStamp now = TimeStamp::NowLoRes();
++  uint64_t now = PR_Now();
+ 
+   for (auto iter = mActivities.Iter(); !iter.Done(); iter.Next()) {
+-    if ((now - iter.UserData()).ToSeconds() > TIME_MAX_SECS) {
++    if ((now - iter.UserData()) / PR_USEC_PER_SEC > TIME_MAX_SECS) {
+       iter.Remove();
+     }
+   }
+ 
+   // If no activities, let's stop the timer.
+   if (mActivities.Count() == 0) {
+     MaybeStopTimer();
+   }
+ 
+   return NS_OK;
+ }
+ 
++NS_IMETHODIMP
++StorageActivityService::GetActiveOrigins(PRTime aFrom, PRTime aTo,
++                                         nsIArray** aRetval)
++{
++  uint64_t now = PR_Now();
++  if (((now - aFrom) / PR_USEC_PER_SEC) > TIME_MAX_SECS ||
++       aFrom >= aTo) {
++    return NS_ERROR_RANGE_ERR;
++  }
++
++  nsresult rv = NS_OK;
++  nsCOMPtr<nsIMutableArray> devices =
++    do_CreateInstance(NS_ARRAY_CONTRACTID, &rv);
++  if (NS_WARN_IF(NS_FAILED(rv))) {
++    return rv;
++  }
++
++  for (auto iter = mActivities.Iter(); !iter.Done(); iter.Next()) {
++    if (iter.UserData() >= aFrom && iter.UserData() <= aTo) {
++      RefPtr<BasePrincipal> principal =
++        BasePrincipal::CreateCodebasePrincipal(iter.Key());
++      MOZ_ASSERT(principal);
++
++      rv = devices->AppendElement(principal);
++      if (NS_WARN_IF(NS_FAILED(rv))) {
++        return rv;
++      }
++    }
++  }
++
++  devices.forget(aRetval);
++  return NS_OK;
++}
++
+ NS_INTERFACE_MAP_BEGIN(StorageActivityService)
+   NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIStorageActivityService)
+   NS_INTERFACE_MAP_ENTRY(nsIStorageActivityService)
+   NS_INTERFACE_MAP_ENTRY(nsIObserver)
+   NS_INTERFACE_MAP_ENTRY(nsITimerCallback)
+   NS_INTERFACE_MAP_ENTRY(nsISupportsWeakReference)
+ NS_INTERFACE_MAP_END
+ 
+diff --git a/dom/storage/StorageActivityService.h b/dom/storage/StorageActivityService.h
+--- a/dom/storage/StorageActivityService.h
++++ b/dom/storage/StorageActivityService.h
+@@ -55,17 +55,17 @@ private:
+ 
+   void
+   MaybeStartTimer();
+ 
+   void
+   MaybeStopTimer();
+ 
+   // Activities grouped by origin (+OriginAttributes).
+-  nsDataHashtable<nsCStringHashKey, TimeStamp> mActivities;
++  nsDataHashtable<nsCStringHashKey, PRTime> mActivities;
+ 
+   nsCOMPtr<nsITimer> mTimer;
+ };
+ 
+ } // namespace dom
+ } // namespace mozilla
+ 
+ #endif // mozilla_dom_StorageActivityService_h
+diff --git a/toolkit/forgetaboutsite/ServiceWorkerCleanUp.jsm b/toolkit/forgetaboutsite/ServiceWorkerCleanUp.jsm
+--- a/toolkit/forgetaboutsite/ServiceWorkerCleanUp.jsm
++++ b/toolkit/forgetaboutsite/ServiceWorkerCleanUp.jsm
+@@ -31,16 +31,28 @@ this.ServiceWorkerCleanUp = {
+       let sw = serviceWorkers.queryElementAt(i, Ci.nsIServiceWorkerRegistrationInfo);
+       if (sw.principal.URI.host == aHost) {
+         promises.push(unregisterServiceWorker(sw));
+       }
+     }
+     return Promise.all(promises);
+   },
+ 
++  removeFromPrincipal(aPrincipal) {
++    let promises = [];
++    let serviceWorkers = serviceWorkerManager.getAllRegistrations();
++    for (let i = 0; i < serviceWorkers.length; i++) {
++      let sw = serviceWorkers.queryElementAt(i, Ci.nsIServiceWorkerRegistrationInfo);
++      if (sw.principal.equals(aPrincipal)) {
++        promises.push(unregisterServiceWorker(sw));
++      }
++    }
++    return Promise.all(promises);
++  },
++
+   removeAll() {
+     let promises = [];
+     let serviceWorkers = serviceWorkerManager.getAllRegistrations();
+     for (let i = 0; i < serviceWorkers.length; i++) {
+       let sw = serviceWorkers.queryElementAt(i, Ci.nsIServiceWorkerRegistrationInfo);
+       promises.push(unregisterServiceWorker(sw));
+     }
+     return Promise.all(promises);

+ 101 - 0
frg/mozilla-release/work-js/1252998-06-61a1.patch

@@ -0,0 +1,101 @@
+# HG changeset patch
+# User Andrea Marchesini <amarchesini@mozilla.com>
+# Date 1524068353 -7200
+# Node ID 185d6fcf4eee894e3a062b21bfddd899f5ef0ba7
+# Parent  2f8894155abb0e331b6b422da66c97e7d1d6e086
+Bug 1252998 - StorageActivityService - part 6 - StorageActivityService in ServiceWorkerRegistrar, r=asuth
+
+diff --git a/dom/workers/ServiceWorkerRegistrar.cpp b/dom/workers/ServiceWorkerRegistrar.cpp
+--- a/dom/workers/ServiceWorkerRegistrar.cpp
++++ b/dom/workers/ServiceWorkerRegistrar.cpp
+@@ -14,16 +14,17 @@
+ #include "nsILineInputStream.h"
+ #include "nsIObserverService.h"
+ #include "nsIOutputStream.h"
+ #include "nsISafeOutputStream.h"
+ 
+ #include "MainThreadUtils.h"
+ #include "mozilla/ClearOnShutdown.h"
+ #include "mozilla/CycleCollectedJSContext.h"
++#include "mozilla/dom/StorageActivityService.h"
+ #include "mozilla/ErrorNames.h"
+ #include "mozilla/ipc/BackgroundChild.h"
+ #include "mozilla/ipc/BackgroundParent.h"
+ #include "mozilla/ipc/PBackgroundChild.h"
+ #include "mozilla/ModuleUtils.h"
+ #include "mozilla/Services.h"
+ #include "mozilla/StaticPtr.h"
+ #include "nsAppDirectoryServiceDefs.h"
+@@ -254,16 +255,17 @@ ServiceWorkerRegistrar::RegisterServiceW
+ 
+   {
+     MonitorAutoLock lock(mMonitor);
+     MOZ_ASSERT(mDataLoaded);
+     RegisterServiceWorkerInternal(aData);
+   }
+ 
+   ScheduleSaveData();
++  StorageActivityService::SendActivity(aData.principal());
+ }
+ 
+ void
+ ServiceWorkerRegistrar::UnregisterServiceWorker(
+                                             const PrincipalInfo& aPrincipalInfo,
+                                             const nsACString& aScope)
+ {
+   AssertIsOnBackgroundThread();
+@@ -289,41 +291,52 @@ ServiceWorkerRegistrar::UnregisterServic
+         deleted = true;
+         break;
+       }
+     }
+   }
+ 
+   if (deleted) {
+     ScheduleSaveData();
++    StorageActivityService::SendActivity(aPrincipalInfo);
+   }
+ }
+ 
+ void
+ ServiceWorkerRegistrar::RemoveAll()
+ {
+   AssertIsOnBackgroundThread();
+ 
+   if (mShuttingDown) {
+     NS_WARNING("Failed to remove all the serviceWorkers during shutting down.");
+     return;
+   }
+ 
+   bool deleted = false;
+ 
++  nsTArray<ServiceWorkerRegistrationData> data;
+   {
+     MonitorAutoLock lock(mMonitor);
+     MOZ_ASSERT(mDataLoaded);
+ 
++    // Let's take a copy in order to inform StorageActivityService.
++    data = mData;
++
+     deleted = !mData.IsEmpty();
+     mData.Clear();
+   }
+ 
+-  if (deleted) {
+-    ScheduleSaveData();
++  if (!deleted) {
++    return;
++  }
++
++  ScheduleSaveData();
++
++  for (uint32_t i = 0, len = data.Length(); i < len; ++i) {
++    StorageActivityService::SendActivity(data[i].principal());
+   }
+ }
+ 
+ void
+ ServiceWorkerRegistrar::LoadData()
+ {
+   MOZ_ASSERT(!NS_IsMainThread());
+   MOZ_ASSERT(!mDataLoaded);

+ 200 - 0
frg/mozilla-release/work-js/1252998-07-61a1.patch

@@ -0,0 +1,200 @@
+# HG changeset patch
+# User Andrea Marchesini <amarchesini@mozilla.com>
+# Date 1524068353 -7200
+# Node ID 830a2e991b0c1a3a9f9684b469172e28ec689383
+# Parent  383f4b83870a82a3a4db819b8da2715ef8809973
+Bug 1252998 - StorageActivityService - part 7 - StorageActivityService in QuotaManager, r=janv
+
+diff --git a/dom/quota/ActorsParent.cpp b/dom/quota/ActorsParent.cpp
+--- a/dom/quota/ActorsParent.cpp
++++ b/dom/quota/ActorsParent.cpp
+@@ -31,16 +31,17 @@
+ #include "mozilla/CondVar.h"
+ #include "mozilla/dom/PContent.h"
+ #include "mozilla/dom/asmjscache/AsmJSCache.h"
+ #include "mozilla/dom/cache/QuotaClient.h"
+ #include "mozilla/dom/indexedDB/ActorsParent.h"
+ #include "mozilla/dom/quota/PQuotaParent.h"
+ #include "mozilla/dom/quota/PQuotaRequestParent.h"
+ #include "mozilla/dom/quota/PQuotaUsageRequestParent.h"
++#include "mozilla/dom/StorageActivityService.h"
+ #include "mozilla/ipc/BackgroundParent.h"
+ #include "mozilla/ipc/BackgroundUtils.h"
+ #include "mozilla/IntegerRange.h"
+ #include "mozilla/Mutex.h"
+ #include "mozilla/Preferences.h"
+ #include "mozilla/Services.h"
+ #include "mozilla/StaticPtr.h"
+ #include "mozilla/TextUtils.h"
+@@ -3017,16 +3018,21 @@ QuotaObject::EnableQuotaCheck()
+ bool
+ QuotaObject::LockedMaybeUpdateSize(int64_t aSize, bool aTruncate)
+ {
+   QuotaManager* quotaManager = QuotaManager::Get();
+   MOZ_ASSERT(quotaManager);
+ 
+   quotaManager->mQuotaMutex.AssertCurrentThreadOwns();
+ 
++  if (mWritingDone == false && mOriginInfo) {
++    mWritingDone = true;
++    StorageActivityService::SendActivity(mOriginInfo->mOrigin);
++  }
++
+   if (mQuotaCheckDisabled) {
+     return true;
+   }
+ 
+   if (mSize == aSize) {
+     return true;
+   }
+ 
+diff --git a/dom/quota/QuotaObject.h b/dom/quota/QuotaObject.h
+--- a/dom/quota/QuotaObject.h
++++ b/dom/quota/QuotaObject.h
+@@ -51,16 +51,17 @@ public:
+   EnableQuotaCheck();
+ 
+ private:
+   QuotaObject(OriginInfo* aOriginInfo, const nsAString& aPath, int64_t aSize)
+     : mOriginInfo(aOriginInfo)
+     , mPath(aPath)
+     , mSize(aSize)
+     , mQuotaCheckDisabled(false)
++    , mWritingDone(false)
+   {
+     MOZ_COUNT_CTOR(QuotaObject);
+   }
+ 
+   ~QuotaObject()
+   {
+     MOZ_COUNT_DTOR(QuotaObject);
+   }
+@@ -81,13 +82,14 @@ private:
+ 
+   mozilla::ThreadSafeAutoRefCnt mRefCnt;
+ 
+   OriginInfo* mOriginInfo;
+   nsString mPath;
+   int64_t mSize;
+ 
+   bool mQuotaCheckDisabled;
++  bool mWritingDone;
+ };
+ 
+ END_QUOTA_NAMESPACE
+ 
+ #endif // mozilla_dom_quota_quotaobject_h__
+diff --git a/dom/storage/StorageActivityService.cpp b/dom/storage/StorageActivityService.cpp
+--- a/dom/storage/StorageActivityService.cpp
++++ b/dom/storage/StorageActivityService.cpp
+@@ -59,16 +59,44 @@ StorageActivityService::SendActivity(con
+         mozilla::ipc::PrincipalInfoToPrincipal(aPrincipalInfo);
+ 
+       StorageActivityService::SendActivity(principal);
+     });
+ 
+   SystemGroup::Dispatch(TaskCategory::Other, r.forget());
+ }
+ 
++/* static */ void
++StorageActivityService::SendActivity(const nsACString& aOrigin)
++{
++  MOZ_ASSERT(XRE_IsParentProcess());
++
++  nsCString origin;
++  origin.Assign(aOrigin);
++
++  RefPtr<Runnable> r = NS_NewRunnableFunction(
++    "StorageActivityService::SendActivity",
++    [origin] () {
++      MOZ_ASSERT(NS_IsMainThread());
++
++      RefPtr<StorageActivityService> service = GetOrCreate();
++      if (NS_WARN_IF(!service)) {
++        return;
++      }
++
++      service->SendActivityInternal(origin);
++    });
++
++  if (NS_IsMainThread()) {
++    Unused << r->Run();
++  } else {
++    SystemGroup::Dispatch(TaskCategory::Other, r.forget());
++  }
++}
++
+ /* static */ already_AddRefed<StorageActivityService>
+ StorageActivityService::GetOrCreate()
+ {
+   MOZ_ASSERT(NS_IsMainThread());
+ 
+   if (!gStorageActivityService && !gStorageActivityShutdown) {
+     RefPtr<StorageActivityService> service = new StorageActivityService();
+ 
+@@ -113,18 +141,25 @@ StorageActivityService::SendActivityInte
+   }
+ 
+   nsAutoCString origin;
+   nsresult rv = aPrincipal->GetOrigin(origin);
+   if (NS_WARN_IF(NS_FAILED(rv))) {
+     return;
+   }
+ 
+-  mActivities.Put(origin, PR_Now());
++  SendActivityInternal(origin);
++}
+ 
++void
++StorageActivityService::SendActivityInternal(const nsACString& aOrigin)
++{
++  MOZ_ASSERT(XRE_IsParentProcess());
++
++  mActivities.Put(aOrigin, PR_Now());
+   MaybeStartTimer();
+ }
+ 
+ void
+ StorageActivityService::SendActivityToParent(nsIPrincipal* aPrincipal)
+ {
+   MOZ_ASSERT(NS_IsMainThread());
+   MOZ_ASSERT(!XRE_IsParentProcess());
+diff --git a/dom/storage/StorageActivityService.h b/dom/storage/StorageActivityService.h
+--- a/dom/storage/StorageActivityService.h
++++ b/dom/storage/StorageActivityService.h
+@@ -34,28 +34,35 @@ public:
+   // Main-thread only.
+   static void
+   SendActivity(nsIPrincipal* aPrincipal);
+ 
+   // Thread-safe.
+   static void
+   SendActivity(const mozilla::ipc::PrincipalInfo& aPrincipalInfo);
+ 
++  // Thread-safe but for parent process only!
++  static void
++  SendActivity(const nsACString& aOrigin);
++
+   // Used by XPCOM. Don't use it, use SendActivity() instead.
+   static already_AddRefed<StorageActivityService>
+   GetOrCreate();
+ 
+ private:
+   StorageActivityService();
+   ~StorageActivityService();
+ 
+   void
+   SendActivityInternal(nsIPrincipal* aPrincipal);
+ 
+   void
++  SendActivityInternal(const nsACString& aOrigin);
++
++  void
+   SendActivityToParent(nsIPrincipal* aPrincipal);
+ 
+   void
+   MaybeStartTimer();
+ 
+   void
+   MaybeStopTimer();
+ 

+ 84 - 0
frg/mozilla-release/work-js/1252998-08-61a1.patch

@@ -0,0 +1,84 @@
+# HG changeset patch
+# User Andrea Marchesini <amarchesini@mozilla.com>
+# Date 1524068353 -7200
+# Node ID 428f49f692ce707cd7e492217f23cfc3a23915b2
+# Parent  43879ad29b7841318d150d4c3cb2f60983f4401f
+Bug 1252998 - StorageActivityService - part 8 - nsIStorageActivityService::moveOriginInTime() for testing, r=asuth
+
+diff --git a/dom/interfaces/storage/nsIStorageActivityService.idl b/dom/interfaces/storage/nsIStorageActivityService.idl
+--- a/dom/interfaces/storage/nsIStorageActivityService.idl
++++ b/dom/interfaces/storage/nsIStorageActivityService.idl
+@@ -1,16 +1,17 @@
+ /* -*- Mode: IDL; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+ /* This Source Code Form is subject to the terms of the Mozilla Public
+  * License, v. 2.0. If a copy of the MPL was not distributed with this
+  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+ 
+ #include "domstubs.idl"
+ 
+ interface nsIArray;
++interface nsIPrincipal;
+ 
+ /**
+  * nsIStorageActivityService is a service that can be used to know which
+  * origins have been active in a time range. This information can be used to
+  * implement "Clear Recent History" or similar features.
+  *
+  * If you are implementing a new Storage component, you should use
+  * QuotaManager. But if you don't do it, remember to call
+@@ -22,13 +23,17 @@ interface nsIStorageActivityService : ns
+ {
+   // This returns an array of nsIPrincipals, active between |from| and |to|
+   // timestamps. Note activities older than 1 day are forgotten.
+   // Activity details are not persisted, so this only covers activity since
+   // Firefox was started.  All codebase principals are logged, which includes
+   // non-system principals like "moz-extension://ID", "moz-safe-about:home",
+   // "about:newtab", so principals may need to be filtered before being used.
+   nsIArray getActiveOrigins(in PRTime from, in PRTime to);
++
++  // NOTE: This method is meant to be used for testing only.
++  // The activity of |origin| is moved to the specified timestamp |when|.
++  void moveOriginInTime(in nsIPrincipal origin, in PRTime when);
+ };
+ 
+ %{ C++
+ #define STORAGE_ACTIVITY_SERVICE_CONTRACTID "@mozilla.org/storage/activity-service;1"
+ %}
+diff --git a/dom/storage/StorageActivityService.cpp b/dom/storage/StorageActivityService.cpp
+--- a/dom/storage/StorageActivityService.cpp
++++ b/dom/storage/StorageActivityService.cpp
+@@ -273,16 +273,34 @@ StorageActivityService::GetActiveOrigins
+       }
+     }
+   }
+ 
+   devices.forget(aRetval);
+   return NS_OK;
+ }
+ 
++NS_IMETHODIMP
++StorageActivityService::MoveOriginInTime(nsIPrincipal* aPrincipal,
++                                         PRTime aWhen)
++{
++  if (!XRE_IsParentProcess()) {
++    return NS_ERROR_FAILURE;
++  }
++
++  nsAutoCString origin;
++  nsresult rv = aPrincipal->GetOrigin(origin);
++  if (NS_WARN_IF(NS_FAILED(rv))) {
++    return rv;
++  }
++
++  mActivities.Put(origin, aWhen / PR_USEC_PER_SEC);
++  return NS_OK;
++}
++
+ NS_INTERFACE_MAP_BEGIN(StorageActivityService)
+   NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIStorageActivityService)
+   NS_INTERFACE_MAP_ENTRY(nsIStorageActivityService)
+   NS_INTERFACE_MAP_ENTRY(nsIObserver)
+   NS_INTERFACE_MAP_ENTRY(nsITimerCallback)
+   NS_INTERFACE_MAP_ENTRY(nsISupportsWeakReference)
+ NS_INTERFACE_MAP_END
+ 

+ 314 - 0
frg/mozilla-release/work-js/1252998-09-61a1.patch

@@ -0,0 +1,314 @@
+# HG changeset patch
+# User Andrea Marchesini <amarchesini@mozilla.com>
+# Date 1522876137 -7200
+# Node ID 38653c75863b9727c86621523d92908aefe09c11
+# Parent  01df62bfa9fe2c11201fc6e10693efac64af3477
+Bug 1252998 - StorageActivityService - part 9 - Test for nsIStorageActivityService, r=gijs, r=asuth
+
+diff --git a/browser/base/content/test/sanitize/browser.ini b/browser/base/content/test/sanitize/browser.ini
+--- a/browser/base/content/test/sanitize/browser.ini
++++ b/browser/base/content/test/sanitize/browser.ini
+@@ -1,11 +1,14 @@
+ [DEFAULT]
+ support-files=
+   head.js
++  dummy.js
+   dummy_page.html
++  sanitize.html
+ 
+ [browser_purgehistory_clears_sh.js]
+ [browser_sanitize-formhistory.js]
++[browser_sanitize-offlineData.js]
+ [browser_sanitize-passwordDisabledHosts.js]
+ [browser_sanitize-sitepermissions.js]
+ [browser_sanitize-timespans.js]
+ [browser_sanitizeDialog.js]
+diff --git a/browser/base/content/test/sanitize/browser_sanitize-offlineData.js b/browser/base/content/test/sanitize/browser_sanitize-offlineData.js
+new file mode 100644
+--- /dev/null
++++ b/browser/base/content/test/sanitize/browser_sanitize-offlineData.js
+@@ -0,0 +1,189 @@
++// Bug 380852 - Delete permission manager entries in Clear Recent History
++
++ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
++const {Sanitizer} = ChromeUtils.import("resource:///modules/Sanitizer.jsm", {});
++
++XPCOMUtils.defineLazyServiceGetter(this, "sas",
++                                   "@mozilla.org/storage/activity-service;1",
++                                   "nsIStorageActivityService");
++XPCOMUtils.defineLazyServiceGetter(this, "swm",
++                                   "@mozilla.org/serviceworkers/manager;1",
++                                   "nsIServiceWorkerManager");
++XPCOMUtils.defineLazyServiceGetter(this, "quotaManagerService",
++                                   "@mozilla.org/dom/quota-manager-service;1",
++                                   "nsIQuotaManagerService");
++
++const oneHour = 3600000000;
++const fiveHours = oneHour * 5;
++
++const itemsToClear = [ "cookies", "offlineApps" ];
++
++function waitForUnregister(host) {
++  return new Promise(resolve => {
++    let listener = {
++      onUnregister: registration => {
++        if (registration.principal.URI.host != host) {
++          return;
++        }
++        let swm = Cc["@mozilla.org/serviceworkers/manager;1"]
++                    .getService(Ci.nsIServiceWorkerManager);
++        swm.removeListener(listener);
++        resolve(registration);
++      }
++    };
++    swm.addListener(listener);
++  });
++}
++
++async function createData(host) {
++  let pageURL = getRootDirectory(gTestPath).replace("chrome://mochitests/content", "http://" + host) + "sanitize.html";
++
++  return BrowserTestUtils.withNewTab(pageURL, async function(browser) {
++    await ContentTask.spawn(browser, null, () => {
++      return new content.window.Promise(resolve => {
++        let id = content.window.setInterval(() => {
++          if ("foobar" in content.window.localStorage) {
++            content.window.clearInterval(id);
++            resolve(true);
++          }
++        }, 1000);
++      });
++    });
++  });
++}
++
++function moveOriginInTime(principals, endDate, host) {
++  for (let i = 0; i < principals.length; ++i) {
++    let principal = principals.queryElementAt(i, Ci.nsIPrincipal);
++    if (principal.URI.host == host) {
++      sas.moveOriginInTime(principal, endDate - fiveHours);
++      return true;
++    }
++  }
++  return false;
++}
++
++async function getData(host) {
++  let dummyURL = getRootDirectory(gTestPath).replace("chrome://mochitests/content", "http://" + host) + "dummy_page.html";
++
++  // LocalStorage + IndexedDB
++  let data = await BrowserTestUtils.withNewTab(dummyURL, async function(browser) {
++    return ContentTask.spawn(browser, null, () => {
++      return new content.window.Promise(resolve => {
++        let obj = {
++          localStorage: "foobar" in content.window.localStorage,
++          indexedDB: true,
++          serviceWorker: false,
++        };
++
++        let request = content.window.indexedDB.open("sanitizer_test", 1);
++        request.onupgradeneeded = event => {
++          obj.indexedDB = false;
++        };
++        request.onsuccess = event => {
++          resolve(obj);
++        };
++      });
++    });
++  });
++
++  // ServiceWorkers
++  let serviceWorkers = swm.getAllRegistrations();
++  for (let i = 0; i < serviceWorkers.length; i++) {
++    let sw = serviceWorkers.queryElementAt(i, Ci.nsIServiceWorkerRegistrationInfo);
++    if (sw.principal.URI.host == host) {
++      data.serviceWorker = true;
++      break;
++    }
++  }
++
++  return data;
++}
++
++add_task(async function testWithRange() {
++  await SpecialPowers.pushPrefEnv({"set": [
++    ["dom.serviceWorkers.enabled", true],
++    ["dom.serviceWorkers.exemptFromPerDomainMax", true],
++    ["dom.serviceWorkers.testing.enabled", true]
++  ]});
++
++  // The service may have picked up activity from prior tests in this run.
++  // Clear it.
++  sas.testOnlyReset();
++
++  let endDate = Date.now() * 1000;
++  let principals = sas.getActiveOrigins(endDate - oneHour, endDate);
++  is(principals.length, 0, "starting from clear activity state");
++
++  info("sanitize: " + itemsToClear.join(", "));
++  await Sanitizer.sanitize(itemsToClear, {ignoreTimespan: false});
++
++  await createData("example.org");
++  await createData("example.com");
++
++  endDate = Date.now() * 1000;
++  principals = sas.getActiveOrigins(endDate - oneHour, endDate);
++  ok(!!principals, "We have an active origin.");
++  ok(principals.length >= 2, "We have an active origin.");
++
++  let found = 0;
++  for (let i = 0; i < principals.length; ++i) {
++    let principal = principals.queryElementAt(i, Ci.nsIPrincipal);
++    if (principal.URI.host == "example.org" ||
++        principal.URI.host == "example.com") {
++      found++;
++    }
++  }
++
++  is(found, 2, "Our origins are active.");
++
++  let dataPre = await getData("example.org");
++  ok(dataPre.localStorage, "We have localStorage data");
++  ok(dataPre.indexedDB, "We have indexedDB data");
++  ok(dataPre.serviceWorker, "We have serviceWorker data");
++
++  dataPre = await getData("example.com");
++  ok(dataPre.localStorage, "We have localStorage data");
++  ok(dataPre.indexedDB, "We have indexedDB data");
++  ok(dataPre.serviceWorker, "We have serviceWorker data");
++
++  // Let's move example.com in the past.
++  ok(moveOriginInTime(principals, endDate, "example.com"), "Operation completed!");
++
++  let p = waitForUnregister("example.org");
++
++  // Clear it
++  info("sanitize: " + itemsToClear.join(", "));
++  await Sanitizer.sanitize(itemsToClear, {ignoreTimespan: false});
++  await p;
++
++  let dataPost = await getData("example.org");
++  ok(!dataPost.localStorage, "We don't have localStorage data");
++  ok(!dataPost.indexedDB, "We don't have indexedDB data");
++  ok(!dataPost.serviceWorker, "We don't have serviceWorker data");
++
++  dataPost = await getData("example.com");
++  ok(dataPost.localStorage, "We still have localStorage data");
++  ok(dataPost.indexedDB, "We still have indexedDB data");
++  ok(dataPost.serviceWorker, "We still have serviceWorker data");
++
++  // We have to move example.com in the past because how we check IDB triggers
++  // a storage activity.
++  ok(moveOriginInTime(principals, endDate, "example.com"), "Operation completed!");
++
++  // Let's call the clean up again.
++  info("sanitize again to ensure clearing doesn't expand the activity scope");
++  await Sanitizer.sanitize(itemsToClear, {ignoreTimespan: false});
++
++  dataPost = await getData("example.com");
++  ok(dataPost.localStorage, "We still have localStorage data");
++  ok(dataPost.indexedDB, "We still have indexedDB data");
++  ok(dataPost.serviceWorker, "We still have serviceWorker data");
++
++  dataPost = await getData("example.org");
++  ok(!dataPost.localStorage, "We don't have localStorage data");
++  ok(!dataPost.indexedDB, "We don't have indexedDB data");
++  ok(!dataPost.serviceWorker, "We don't have serviceWorker data");
++
++  sas.testOnlyReset();
++});
+diff --git a/browser/base/content/test/sanitize/dummy.js b/browser/base/content/test/sanitize/dummy.js
+new file mode 100644
+diff --git a/browser/base/content/test/sanitize/sanitize.html b/browser/base/content/test/sanitize/sanitize.html
+new file mode 100644
+--- /dev/null
++++ b/browser/base/content/test/sanitize/sanitize.html
+@@ -0,0 +1,41 @@
++<html>
++<head>
++  <meta http-equiv="Content-Type" content="text/html;charset=utf-8"></meta>
++</head>
++<body>
++  <script>
++
++// indexedDB
++let p = new Promise(resolve => {
++  let request = indexedDB.open("sanitizer_test", 1);
++  request.onupgradeneeded = event => {
++    let db = event.target.result;
++    event.target.onsuccess = resolve;
++    db.createObjectStore("foo", { autoIncrement: true });
++    db.createObjectStore("bar", { autoIncrement: true });
++  };
++});
++
++// ServiceWorker
++p.then(() => {
++  return navigator.serviceWorker.register("dummy.js")
++                .then(r => {
++    return new Promise(resolve => {
++      let worker = r.installing;
++      worker.addEventListener("statechange", () => {
++        if (worker.state === "installed") {
++          resolve(true);
++        }
++      });
++    });
++  });
++})
++
++// localStorage
++.then(() => {
++  localStorage.foobar = "hello world!";
++});
++
++  </script>
++</body>
++</html>
+diff --git a/dom/interfaces/storage/nsIStorageActivityService.idl b/dom/interfaces/storage/nsIStorageActivityService.idl
+--- a/dom/interfaces/storage/nsIStorageActivityService.idl
++++ b/dom/interfaces/storage/nsIStorageActivityService.idl
+@@ -27,13 +27,16 @@ interface nsIStorageActivityService : ns
+   // Firefox was started.  All codebase principals are logged, which includes
+   // non-system principals like "moz-extension://ID", "moz-safe-about:home",
+   // "about:newtab", so principals may need to be filtered before being used.
+   nsIArray getActiveOrigins(in PRTime from, in PRTime to);
+ 
+   // NOTE: This method is meant to be used for testing only.
+   // The activity of |origin| is moved to the specified timestamp |when|.
+   void moveOriginInTime(in nsIPrincipal origin, in PRTime when);
++
++  // TEST-ONLY method to support clearing all previously known activity.
++  void testOnlyReset();
+ };
+ 
+ %{ C++
+ #define STORAGE_ACTIVITY_SERVICE_CONTRACTID "@mozilla.org/storage/activity-service;1"
+ %}
+diff --git a/dom/storage/StorageActivityService.cpp b/dom/storage/StorageActivityService.cpp
+--- a/dom/storage/StorageActivityService.cpp
++++ b/dom/storage/StorageActivityService.cpp
+@@ -291,16 +291,23 @@ StorageActivityService::MoveOriginInTime
+   if (NS_WARN_IF(NS_FAILED(rv))) {
+     return rv;
+   }
+ 
+   mActivities.Put(origin, aWhen / PR_USEC_PER_SEC);
+   return NS_OK;
+ }
+ 
++NS_IMETHODIMP
++StorageActivityService::TestOnlyReset()
++{
++  mActivities.Clear();
++  return NS_OK;
++}
++
+ NS_INTERFACE_MAP_BEGIN(StorageActivityService)
+   NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIStorageActivityService)
+   NS_INTERFACE_MAP_ENTRY(nsIStorageActivityService)
+   NS_INTERFACE_MAP_ENTRY(nsIObserver)
+   NS_INTERFACE_MAP_ENTRY(nsITimerCallback)
+   NS_INTERFACE_MAP_ENTRY(nsISupportsWeakReference)
+ NS_INTERFACE_MAP_END
+ 

+ 446 - 0
frg/mozilla-release/work-js/1252998-10-61a1.patch

@@ -0,0 +1,446 @@
+# HG changeset patch
+# User Johann Hofmann <jhofmann@mozilla.com>
+# Date 1523458430 -7200
+# Node ID db8bf70e7847af6bcfe5a5829ec894f0ea61abe8
+# Parent  6c43b56f365a976b32fac22cffac9c38f4d407e2
+Bug 1252998 - Fix sanitize-offlineData test failures, move SW utility functions to SiteDataTestUtils.jsm. r=baku
+
+This patch fixes a bunch of intermittent/perma failures in sanitize-offlineData.js by:
+
+- Ignoring localStorage for now. LocalStorage is cleared by sending an
+  observer notification. The flush often happens after several seconds, heavily
+  interfering with our own test or with subsequent tests. We can not reliably wait
+  on the operation to finish. Waiting for "domstorage-test-flushed" after calling
+  Sanitizer.sanitize() fixes the problem, but that notification is intermittently
+  not triggered for other unknown reasons, which makes it not really viable to use.
+
+- Creating and checking indexedDB data in the chrome process (using SiteDataTestUtils).
+
+- Cleaning up after running the test.
+
+- Ignoring a stray NS_ERROR_ABORT that's hard to track down and doesn't seem to
+  do any damage right now.
+
+I've also moved the ServiceWorker utility functions into SiteDataTestUtils,
+which we're planning to use in all other browser tests that handle site data.
+
+diff --git a/browser/base/content/test/sanitize/SiteDataTestUtils.jsm b/browser/base/content/test/sanitize/SiteDataTestUtils.jsm
+--- a/browser/base/content/test/sanitize/SiteDataTestUtils.jsm
++++ b/browser/base/content/test/sanitize/SiteDataTestUtils.jsm
+@@ -1,66 +1,121 @@
+ "use strict";
+ 
+ var EXPORTED_SYMBOLS = [
+   "SiteDataTestUtils",
+ ];
+ 
++ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
+ ChromeUtils.import("resource://gre/modules/Services.jsm");
++ChromeUtils.import("resource://testing-common/ContentTask.jsm");
++ChromeUtils.import("resource://testing-common/BrowserTestUtils.jsm");
++
+ const {Sanitizer} = ChromeUtils.import("resource:///modules/Sanitizer.jsm", {});
+ 
++XPCOMUtils.defineLazyServiceGetter(this, "swm",
++                                   "@mozilla.org/serviceworkers/manager;1",
++                                   "nsIServiceWorkerManager");
++
+ /**
+  * This module assists with tasks around testing functionality that shows
+  * or clears site data.
+  *
+  * Please note that you will have to clean up changes made manually, for
+  * example using SiteDataTestUtils.clear().
+  */
+ var SiteDataTestUtils = {
+ 
+   /**
+    * Adds a new entry to a dummy indexedDB database for the specified origin.
+    *
+    * @param {String} origin - the origin of the site to add test data for
++   * @param {String} name [optional] - the entry key
++   * @param {String} value [optional] - the entry value
+    *
+    * @returns a Promise that resolves when the data was added successfully.
+    */
+-  addToIndexedDB(origin) {
++  addToIndexedDB(origin, key = "foo", value = "bar") {
+     return new Promise(resolve => {
+       let uri = Services.io.newURI(origin);
+       let principal = Services.scriptSecurityManager.createCodebasePrincipal(uri, {});
+       let request = indexedDB.openForPrincipal(principal, "TestDatabase", 1);
+       request.onupgradeneeded = function(e) {
+         let db = e.target.result;
+         db.createObjectStore("TestStore", { keyPath: "id" });
+       };
+       request.onsuccess = function(e) {
+         let db = e.target.result;
+         let tx = db.transaction("TestStore", "readwrite");
+         let store = tx.objectStore("TestStore");
+         tx.oncomplete = resolve;
+-        store.put({ id: performance.now().toString(), description: "IndexedDB Test"});
++        store.put({ id: key, description: value});
+       };
+     });
+   },
+ 
+   /**
+    * Adds a new cookie for the specified origin, with the specified contents.
+    * The cookie will be valid for one day.
+    *
+-   * @param {String} name - the cookie name
+-   * @param {String} value - the cookie value
++   * @param {String} origin - the origin of the site to add test data for
++   * @param {String} name [optional] - the cookie name
++   * @param {String} value [optional] - the cookie value
+    */
+-  addToCookies(origin, name, value) {
++  addToCookies(origin, name = "foo", value = "bar") {
+     let uri = Services.io.newURI(origin);
+     Services.cookies.add(uri.host, uri.pathQueryRef, name, value,
+       false, false, false, Date.now() + 24000 * 60 * 60);
+   },
+ 
+   /**
++   * Adds a new serviceworker with the specified path. Note that this
++   * method will open a new tab at the domain of the SW path to that effect.
++   *
++   * @param {String} path - the path to the service worker to add.
++   *
++   * @returns a Promise that resolves when the service worker was registered
++   */
++  addServiceWorker(path) {
++    let uri = Services.io.newURI(path);
++    // Register a dummy ServiceWorker.
++    return BrowserTestUtils.withNewTab(uri.prePath, async function(browser) {
++      return ContentTask.spawn(browser, {path}, async ({path: p}) => {
++        let r = await content.navigator.serviceWorker.register(p);
++        return new Promise(resolve => {
++          let worker = r.installing;
++          worker.addEventListener("statechange", () => {
++            if (worker.state === "installed") {
++              resolve();
++            }
++          });
++        });
++      });
++    });
++  },
++
++  /**
++   * Checks whether the specified origin has registered ServiceWorkers.
++   *
++   * @param {String} origin - the origin of the site to check
++   *
++   * @returns {Boolean} whether or not the site has ServiceWorkers.
++   */
++  hasServiceWorkers(origin) {
++    let serviceWorkers = swm.getAllRegistrations();
++    for (let i = 0; i < serviceWorkers.length; i++) {
++      let sw = serviceWorkers.queryElementAt(i, Ci.nsIServiceWorkerRegistrationInfo);
++      if (sw.principal.origin == origin) {
++        return true;
++      }
++    }
++    return false;
++  },
++
++  /**
+    * Gets the current quota usage for the specified origin.
+    *
+    * @returns a Promise that resolves to an integer with the total
+    *          amount of disk usage by a given origin.
+    */
+   getQuotaUsage(origin) {
+     return new Promise(resolve => {
+       let uri = Services.io.newURI(origin);
+diff --git a/browser/base/content/test/sanitize/browser.ini b/browser/base/content/test/sanitize/browser.ini
+--- a/browser/base/content/test/sanitize/browser.ini
++++ b/browser/base/content/test/sanitize/browser.ini
+@@ -1,14 +1,13 @@
+ [DEFAULT]
+ support-files=
+   head.js
+   dummy.js
+   dummy_page.html
+-  sanitize.html
+ 
+ [browser_purgehistory_clears_sh.js]
+ [browser_sanitize-formhistory.js]
+ [browser_sanitize-offlineData.js]
+ [browser_sanitize-passwordDisabledHosts.js]
+ [browser_sanitize-sitepermissions.js]
+ [browser_sanitize-timespans.js]
+ [browser_sanitizeDialog.js]
+diff --git a/browser/base/content/test/sanitize/browser_sanitize-offlineData.js b/browser/base/content/test/sanitize/browser_sanitize-offlineData.js
+--- a/browser/base/content/test/sanitize/browser_sanitize-offlineData.js
++++ b/browser/base/content/test/sanitize/browser_sanitize-offlineData.js
+@@ -1,28 +1,42 @@
+ // Bug 380852 - Delete permission manager entries in Clear Recent History
+ 
+ ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
+ const {Sanitizer} = ChromeUtils.import("resource:///modules/Sanitizer.jsm", {});
++const {SiteDataTestUtils} = ChromeUtils.import("resource://testing-common/SiteDataTestUtils.jsm", {});
++const {PromiseTestUtils} = ChromeUtils.import("resource://testing-common/PromiseTestUtils.jsm", {});
+ 
+ XPCOMUtils.defineLazyServiceGetter(this, "sas",
+                                    "@mozilla.org/storage/activity-service;1",
+                                    "nsIStorageActivityService");
+ XPCOMUtils.defineLazyServiceGetter(this, "swm",
+                                    "@mozilla.org/serviceworkers/manager;1",
+                                    "nsIServiceWorkerManager");
+-XPCOMUtils.defineLazyServiceGetter(this, "quotaManagerService",
+-                                   "@mozilla.org/dom/quota-manager-service;1",
+-                                   "nsIQuotaManagerService");
+ 
+ const oneHour = 3600000000;
+ const fiveHours = oneHour * 5;
+ 
+ const itemsToClear = [ "cookies", "offlineApps" ];
+ 
++function hasIndexedDB(origin) {
++  return new Promise(resolve => {
++    let hasData = true;
++    let uri = Services.io.newURI(origin);
++    let principal = Services.scriptSecurityManager.createCodebasePrincipal(uri, {});
++    let request = indexedDB.openForPrincipal(principal, "TestDatabase", 1);
++    request.onupgradeneeded = function(e) {
++      hasData = false;
++    };
++    request.onsuccess = function(e) {
++      resolve(hasData);
++    };
++  });
++}
++
+ function waitForUnregister(host) {
+   return new Promise(resolve => {
+     let listener = {
+       onUnregister: registration => {
+         if (registration.principal.URI.host != host) {
+           return;
+         }
+         let swm = Cc["@mozilla.org/serviceworkers/manager;1"]
+@@ -31,81 +45,40 @@ function waitForUnregister(host) {
+         resolve(registration);
+       }
+     };
+     swm.addListener(listener);
+   });
+ }
+ 
+ async function createData(host) {
+-  let pageURL = getRootDirectory(gTestPath).replace("chrome://mochitests/content", "http://" + host) + "sanitize.html";
++  let origin = "https://" + host;
++  let dummySWURL = getRootDirectory(gTestPath).replace("chrome://mochitests/content", origin) + "dummy.js";
+ 
+-  return BrowserTestUtils.withNewTab(pageURL, async function(browser) {
+-    await ContentTask.spawn(browser, null, () => {
+-      return new content.window.Promise(resolve => {
+-        let id = content.window.setInterval(() => {
+-          if ("foobar" in content.window.localStorage) {
+-            content.window.clearInterval(id);
+-            resolve(true);
+-          }
+-        }, 1000);
+-      });
+-    });
+-  });
++  await SiteDataTestUtils.addToIndexedDB(origin);
++  await SiteDataTestUtils.addServiceWorker(dummySWURL);
+ }
+ 
+ function moveOriginInTime(principals, endDate, host) {
+   for (let i = 0; i < principals.length; ++i) {
+     let principal = principals.queryElementAt(i, Ci.nsIPrincipal);
+     if (principal.URI.host == host) {
+       sas.moveOriginInTime(principal, endDate - fiveHours);
+       return true;
+     }
+   }
+   return false;
+ }
+ 
+-async function getData(host) {
+-  let dummyURL = getRootDirectory(gTestPath).replace("chrome://mochitests/content", "http://" + host) + "dummy_page.html";
+-
+-  // LocalStorage + IndexedDB
+-  let data = await BrowserTestUtils.withNewTab(dummyURL, async function(browser) {
+-    return ContentTask.spawn(browser, null, () => {
+-      return new content.window.Promise(resolve => {
+-        let obj = {
+-          localStorage: "foobar" in content.window.localStorage,
+-          indexedDB: true,
+-          serviceWorker: false,
+-        };
++add_task(async function testWithRange() {
++  // We have intermittent occurrences of NS_ERROR_ABORT being
++  // thrown at closing database instances when using Santizer.sanitize().
++  // This does not seem to impact cleanup, since our tests run fine anyway.
++  PromiseTestUtils.whitelistRejectionsGlobally(/NS_ERROR_ABORT/);
+ 
+-        let request = content.window.indexedDB.open("sanitizer_test", 1);
+-        request.onupgradeneeded = event => {
+-          obj.indexedDB = false;
+-        };
+-        request.onsuccess = event => {
+-          resolve(obj);
+-        };
+-      });
+-    });
+-  });
+-
+-  // ServiceWorkers
+-  let serviceWorkers = swm.getAllRegistrations();
+-  for (let i = 0; i < serviceWorkers.length; i++) {
+-    let sw = serviceWorkers.queryElementAt(i, Ci.nsIServiceWorkerRegistrationInfo);
+-    if (sw.principal.URI.host == host) {
+-      data.serviceWorker = true;
+-      break;
+-    }
+-  }
+-
+-  return data;
+-}
+-
+-add_task(async function testWithRange() {
+   await SpecialPowers.pushPrefEnv({"set": [
+     ["dom.serviceWorkers.enabled", true],
+     ["dom.serviceWorkers.exemptFromPerDomainMax", true],
+     ["dom.serviceWorkers.testing.enabled", true]
+   ]});
+ 
+   // The service may have picked up activity from prior tests in this run.
+   // Clear it.
+@@ -132,58 +105,61 @@ add_task(async function testWithRange() 
+     if (principal.URI.host == "example.org" ||
+         principal.URI.host == "example.com") {
+       found++;
+     }
+   }
+ 
+   is(found, 2, "Our origins are active.");
+ 
+-  let dataPre = await getData("example.org");
+-  ok(dataPre.localStorage, "We have localStorage data");
+-  ok(dataPre.indexedDB, "We have indexedDB data");
+-  ok(dataPre.serviceWorker, "We have serviceWorker data");
++  ok(await hasIndexedDB("https://example.org"),
++    "We have indexedDB data for example.org");
++  ok(SiteDataTestUtils.hasServiceWorkers("https://example.org"),
++    "We have serviceWorker data for example.org");
+ 
+-  dataPre = await getData("example.com");
+-  ok(dataPre.localStorage, "We have localStorage data");
+-  ok(dataPre.indexedDB, "We have indexedDB data");
+-  ok(dataPre.serviceWorker, "We have serviceWorker data");
++  ok(await hasIndexedDB("https://example.com"),
++    "We have indexedDB data for example.com");
++  ok(SiteDataTestUtils.hasServiceWorkers("https://example.com"),
++    "We have serviceWorker data for example.com");
+ 
+   // Let's move example.com in the past.
+   ok(moveOriginInTime(principals, endDate, "example.com"), "Operation completed!");
+ 
+   let p = waitForUnregister("example.org");
+ 
+   // Clear it
+   info("sanitize: " + itemsToClear.join(", "));
+   await Sanitizer.sanitize(itemsToClear, {ignoreTimespan: false});
+   await p;
+ 
+-  let dataPost = await getData("example.org");
+-  ok(!dataPost.localStorage, "We don't have localStorage data");
+-  ok(!dataPost.indexedDB, "We don't have indexedDB data");
+-  ok(!dataPost.serviceWorker, "We don't have serviceWorker data");
++  ok(!(await hasIndexedDB("https://example.org")),
++    "We don't have indexedDB data for example.org");
++  ok(!SiteDataTestUtils.hasServiceWorkers("https://example.org"),
++    "We don't have serviceWorker data for example.org");
+ 
+-  dataPost = await getData("example.com");
+-  ok(dataPost.localStorage, "We still have localStorage data");
+-  ok(dataPost.indexedDB, "We still have indexedDB data");
+-  ok(dataPost.serviceWorker, "We still have serviceWorker data");
++  ok(await hasIndexedDB("https://example.com"),
++    "We still have indexedDB data for example.com");
++  ok(SiteDataTestUtils.hasServiceWorkers("https://example.com"),
++    "We still have serviceWorker data for example.com");
+ 
+   // We have to move example.com in the past because how we check IDB triggers
+   // a storage activity.
+   ok(moveOriginInTime(principals, endDate, "example.com"), "Operation completed!");
+ 
+   // Let's call the clean up again.
+   info("sanitize again to ensure clearing doesn't expand the activity scope");
+   await Sanitizer.sanitize(itemsToClear, {ignoreTimespan: false});
+ 
+-  dataPost = await getData("example.com");
+-  ok(dataPost.localStorage, "We still have localStorage data");
+-  ok(dataPost.indexedDB, "We still have indexedDB data");
+-  ok(dataPost.serviceWorker, "We still have serviceWorker data");
++  ok(await hasIndexedDB("https://example.com"),
++    "We still have indexedDB data for example.com");
++  ok(SiteDataTestUtils.hasServiceWorkers("https://example.com"),
++    "We still have serviceWorker data for example.com");
+ 
+-  dataPost = await getData("example.org");
+-  ok(!dataPost.localStorage, "We don't have localStorage data");
+-  ok(!dataPost.indexedDB, "We don't have indexedDB data");
+-  ok(!dataPost.serviceWorker, "We don't have serviceWorker data");
++  ok(!(await hasIndexedDB("https://example.org")),
++    "We don't have indexedDB data for example.org");
++  ok(!SiteDataTestUtils.hasServiceWorkers("https://example.org"),
++    "We don't have serviceWorker data for example.org");
+ 
+   sas.testOnlyReset();
++
++  // Clean up.
++  await Sanitizer.sanitize(itemsToClear);
+ });
+diff --git a/browser/base/content/test/sanitize/sanitize.html b/browser/base/content/test/sanitize/sanitize.html
+deleted file mode 100644
+--- a/browser/base/content/test/sanitize/sanitize.html
++++ /dev/null
+@@ -1,41 +0,0 @@
+-<html>
+-<head>
+-  <meta http-equiv="Content-Type" content="text/html;charset=utf-8"></meta>
+-</head>
+-<body>
+-  <script>
+-
+-// indexedDB
+-let p = new Promise(resolve => {
+-  let request = indexedDB.open("sanitizer_test", 1);
+-  request.onupgradeneeded = event => {
+-    let db = event.target.result;
+-    event.target.onsuccess = resolve;
+-    db.createObjectStore("foo", { autoIncrement: true });
+-    db.createObjectStore("bar", { autoIncrement: true });
+-  };
+-});
+-
+-// ServiceWorker
+-p.then(() => {
+-  return navigator.serviceWorker.register("dummy.js")
+-                .then(r => {
+-    return new Promise(resolve => {
+-      let worker = r.installing;
+-      worker.addEventListener("statechange", () => {
+-        if (worker.state === "installed") {
+-          resolve(true);
+-        }
+-      });
+-    });
+-  });
+-})
+-
+-// localStorage
+-.then(() => {
+-  localStorage.foobar = "hello world!";
+-});
+-
+-  </script>
+-</body>
+-</html>

+ 129 - 0
frg/mozilla-release/work-js/1252998-11-61a1.patch

@@ -0,0 +1,129 @@
+# HG changeset patch
+# User Johann Hofmann <jhofmann@mozilla.com>
+# Date 1523525047 -7200
+# Node ID 0a880af671625c7a212d30e3158a6c2a9270ce4a
+# Parent  522e9b62a33150cac47e4e7328fc5d58f98cff46
+Bug 1252998 - Use hosts instead of principals to delete ServiceWorkers in the SiteDataManager. r=baku
+
+site.principals is not always guaranteed to contain elements, only if
+the site has quota storage or AppCache. This patch simplifies the function
+to use hosts instead.
+
+diff --git a/browser/modules/SiteDataManager.jsm b/browser/modules/SiteDataManager.jsm
+--- a/browser/modules/SiteDataManager.jsm
++++ b/browser/modules/SiteDataManager.jsm
+@@ -24,17 +24,18 @@ var SiteDataManager = {
+ 
+   _qms: Services.qms,
+ 
+   _appCache: Cc["@mozilla.org/network/application-cache-service;1"].getService(Ci.nsIApplicationCacheService),
+ 
+   // A Map of sites and their disk usage according to Quota Manager and appcache
+   // Key is host (group sites based on host across scheme, port, origin atttributes).
+   // Value is one object holding:
+-  //   - principals: instances of nsIPrincipal.
++  //   - principals: instances of nsIPrincipal (only when the site has
++  //     quota storage or AppCache).
+   //   - persisted: the persistent-storage status.
+   //   - quotaUsage: the usage of indexedDB and localStorage.
+   //   - appCacheList: an array of app cache; instances of nsIApplicationCache
+   _sites: new Map(),
+ 
+   _getCacheSizeObserver: null,
+ 
+   _getCacheSizePromise: null,
+@@ -309,60 +310,47 @@ var SiteDataManager = {
+   _removeCookies(site) {
+     for (let cookie of site.cookies) {
+       Services.cookies.remove(
+         cookie.host, cookie.name, cookie.path, false, cookie.originAttributes);
+     }
+     site.cookies = [];
+   },
+ 
+-  _removeServiceWorkersForSites(sites) {
+-    let promises = [];
+-    sites.forEach(s => {
+-      promises.push(ServiceWorkerCleanUp.removeFromHost(s.principals[0].URI.host));
+-    });
+-    return Promise.all(promises);
+-  },
+-
+   /**
+    * Removes all site data for the specified list of hosts.
+    *
+    * @param {Array} a list of hosts to match for removal.
+    * @returns a Promise that resolves when data is removed and the site data
+    *          manager has been updated.
+    */
+   async remove(hosts) {
+     // Make sure we have up-to-date information.
+     await this._getQuotaUsage();
+     this._updateAppCache();
+ 
+     let unknownHost = "";
+-    let targetSites = new Map();
++    let promises = [];
+     for (let host of hosts) {
+       let site = this._sites.get(host);
+       if (site) {
++        // Clear localstorage.
++        Services.obs.notifyObservers(null, "browser:purge-domain-data", host);
+         this._removePermission(site);
+         this._removeAppCache(site);
+         this._removeCookies(site);
+-        Services.obs.notifyObservers(null, "browser:purge-domain-data", host);
+-        targetSites.set(host, site);
++        promises.push(ServiceWorkerCleanUp.removeFromHost(host));
++        promises.push(this._removeQuotaUsage(site));
+       } else {
+         unknownHost = host;
+         break;
+       }
+     }
+ 
+-    if (targetSites.size > 0) {
+-      await this._removeServiceWorkersForSites(targetSites);
+-      let promises = [];
+-      for (let [, site] of targetSites) {
+-        promises.push(this._removeQuotaUsage(site));
+-      }
+-      await Promise.all(promises);
+-    }
++    await Promise.all(promises);
+ 
+     if (unknownHost) {
+       throw `SiteDataManager: removing unknown site of ${unknownHost}`;
+     }
+ 
+     return this.updateSites();
+   },
+ 
+@@ -416,23 +404,27 @@ var SiteDataManager = {
+   removeCache() {
+     Services.cache2.clear();
+   },
+ 
+   /**
+    * Clears all site data, which currently means
+    *   - Cookies
+    *   - AppCache
++   *   - LocalStorage
+    *   - ServiceWorkers
+    *   - Quota Managed Storage
+    *   - persistent-storage permissions
+    *
+    * @returns a Promise that resolves with the cache size on disk in bytes
+    */
+   async removeSiteData() {
++    // LocalStorage
++    Services.obs.notifyObservers(null, "extension:purge-localStorage");
++
+     Services.cookies.removeAll();
+     OfflineAppCacheHelper.clear();
+ 
+     await ServiceWorkerCleanUp.removeAll();
+ 
+     // Refresh sites using quota usage again.
+     // This is for the case:
+     //   1. User goes to the about:preferences Site Data section.

+ 301 - 0
frg/mozilla-release/work-js/1348223-1-61a1.patch

@@ -0,0 +1,301 @@
+# HG changeset patch
+# User Johann Hofmann <jhofmann@mozilla.com>
+# Date 1521734900 -3600
+# Node ID 7390643a0bc13ca3a59ef2d9da51c0b0850c9662
+# Parent  f62cbe3ee09741328629fde66cf0d7eef73394fe
+Bug 1348223 - Part 1 - Add SiteDataTestUtils.jsm. r=mak
+
+This commit adds a helper module for doing common tasks
+related to site data, such as adding dummy data and getting usage.
+
+There are many places that would potentially need to be cleaned
+up to use this module instead, but I consider that work (and the
+likely try failure fallout) out of scope for this bug.
+
+MozReview-Commit-ID: 5eMDgHhClsO
+
+diff --git a/browser/base/content/test/sanitize/SiteDataTestUtils.jsm b/browser/base/content/test/sanitize/SiteDataTestUtils.jsm
+new file mode 100644
+--- /dev/null
++++ b/browser/base/content/test/sanitize/SiteDataTestUtils.jsm
+@@ -0,0 +1,78 @@
++"use strict";
++
++var EXPORTED_SYMBOLS = [
++  "SiteDataTestUtils",
++];
++
++ChromeUtils.import("resource://gre/modules/Services.jsm");
++const {Sanitizer} = ChromeUtils.import("resource:///modules/Sanitizer.jsm", {});
++
++/**
++ * This module assists with tasks around testing functionality that shows
++ * or clears site data.
++ *
++ * Please note that you will have to clean up changes made manually, for
++ * example using SiteDataTestUtils.clear().
++ */
++var SiteDataTestUtils = {
++
++  /**
++   * Adds a new entry to a dummy indexedDB database for the specified origin.
++   *
++   * @param {String} origin - the origin of the site to add test data for
++   *
++   * @returns a Promise that resolves when the data was added successfully.
++   */
++  addToIndexedDB(origin) {
++    return new Promise(resolve => {
++      let uri = Services.io.newURI(origin);
++      let principal = Services.scriptSecurityManager.createCodebasePrincipal(uri, {});
++      let request = indexedDB.openForPrincipal(principal, "TestDatabase", 1);
++      request.onupgradeneeded = function(e) {
++        let db = e.target.result;
++        db.createObjectStore("TestStore", { keyPath: "id" });
++      };
++      request.onsuccess = function(e) {
++        let db = e.target.result;
++        let tx = db.transaction("TestStore", "readwrite");
++        let store = tx.objectStore("TestStore");
++        tx.oncomplete = resolve;
++        store.put({ id: performance.now().toString(), description: "IndexedDB Test"});
++      };
++    });
++  },
++
++  /**
++   * Adds a new cookie for the specified origin, with the specified contents.
++   * The cookie will be valid for one day.
++   *
++   * @param {String} name - the cookie name
++   * @param {String} value - the cookie value
++   */
++  addToCookies(origin, name, value) {
++    let uri = Services.io.newURI(origin);
++    Services.cookies.add(uri.host, uri.pathQueryRef, name, value,
++      false, false, false, Date.now() + 24000 * 60 * 60);
++  },
++
++  /**
++   * Gets the current quota usage for the specified origin.
++   *
++   * @returns a Promise that resolves to an integer with the total
++   *          amount of disk usage by a given origin.
++   */
++  getQuotaUsage(origin) {
++    return new Promise(resolve => {
++      let uri = Services.io.newURI(origin);
++      let principal = Services.scriptSecurityManager.createCodebasePrincipal(uri, {});
++      Services.qms.getUsageForPrincipal(principal, request => resolve(request.result.usage));
++    });
++  },
++
++  /**
++   * Cleans up all site data.
++   */
++  clear() {
++    return Sanitizer.sanitize(["cookies", "cache", "siteSettings", "offlineApps"]);
++  },
++};
+diff --git a/browser/base/moz.build b/browser/base/moz.build
+--- a/browser/base/moz.build
++++ b/browser/base/moz.build
+@@ -12,16 +12,20 @@ SPHINX_TREES['sslerrorreport'] = 'conten
+ MOCHITEST_MANIFESTS += [
+     'content/test/general/mochitest.ini',
+ ]
+ 
+ MOCHITEST_CHROME_MANIFESTS += [
+     'content/test/chrome/chrome.ini',
+ ]
+ 
++TESTING_JS_MODULES += [
++    'content/test/sanitize/SiteDataTestUtils.jsm',
++]
++
+ BROWSER_CHROME_MANIFESTS += [
+     'content/test/about/browser.ini',
+     'content/test/alerts/browser.ini',
+     'content/test/captivePortal/browser.ini',
+     'content/test/contextMenu/browser.ini',
+     'content/test/forms/browser.ini',
+     'content/test/general/browser.ini',
+     'content/test/newtab/browser.ini',
+diff --git a/browser/components/preferences/in-content-new/tests/siteData/browser_clearSiteData.js b/browser/components/preferences/in-content-new/tests/siteData/browser_clearSiteData.js
+--- a/browser/components/preferences/in-content-new/tests/siteData/browser_clearSiteData.js
++++ b/browser/components/preferences/in-content-new/tests/siteData/browser_clearSiteData.js
+@@ -25,17 +25,17 @@ async function testClearData(clearSiteDa
+   // Register some service workers.
+   await loadServiceWorkerTestPage(TEST_SERVICE_WORKER_URL);
+   await promiseServiceWorkerRegisteredFor(TEST_SERVICE_WORKER_URL);
+ 
+   await openPreferencesViaOpenPreferencesAPI("privacy", { leaveOpen: true });
+ 
+   // Test the initial states.
+   let cacheUsage = await SiteDataManager.getCacheSize();
+-  let quotaUsage = await getQuotaUsage(TEST_QUOTA_USAGE_ORIGIN);
++  let quotaUsage = await SiteDataTestUtils.getQuotaUsage(TEST_QUOTA_USAGE_ORIGIN);
+   let totalUsage = await SiteDataManager.getTotalUsage();
+   Assert.greater(cacheUsage, 0, "The cache usage should not be 0");
+   Assert.greater(quotaUsage, 0, "The quota usage should not be 0");
+   Assert.greater(totalUsage, 0, "The total usage should not be 0");
+ 
+   let initialSizeLabelValue = await ContentTask.spawn(gBrowser.selectedBrowser, null, async function() {
+     let sizeLabel = content.document.getElementById("totalSiteDataSize");
+     return sizeLabel.textContent;
+@@ -122,17 +122,17 @@ async function testClearData(clearSiteDa
+     await cookiesClearedPromise;
+     await promiseServiceWorkersCleared();
+ 
+     TestUtils.waitForCondition(async function() {
+       let usage = await SiteDataManager.getTotalUsage();
+       return usage == 0;
+     }, "The total usage should be removed");
+   } else {
+-    quotaUsage = await getQuotaUsage(TEST_QUOTA_USAGE_ORIGIN);
++    quotaUsage = await SiteDataTestUtils.getQuotaUsage(TEST_QUOTA_USAGE_ORIGIN);
+     totalUsage = await SiteDataManager.getTotalUsage();
+     Assert.greater(quotaUsage, 0, "The quota usage should not be 0");
+     Assert.greater(totalUsage, 0, "The total usage should not be 0");
+   }
+ 
+   if (clearCache || clearSiteData) {
+     // Check that the size label in about:preferences updates after we cleared data.
+     await ContentTask.spawn(gBrowser.selectedBrowser, {initialSizeLabelValue}, async function(opts) {
+diff --git a/browser/components/preferences/in-content-new/tests/siteData/head.js b/browser/components/preferences/in-content-new/tests/siteData/head.js
+--- a/browser/components/preferences/in-content-new/tests/siteData/head.js
++++ b/browser/components/preferences/in-content-new/tests/siteData/head.js
+@@ -12,16 +12,19 @@ const TEST_OFFLINE_URL = getRootDirector
+ const TEST_SERVICE_WORKER_URL = getRootDirectory(gTestPath).replace("chrome://mochitests/content", TEST_OFFLINE_ORIGIN) + "/service_worker_test.html";
+ 
+ const REMOVE_DIALOG_URL = "chrome://browser/content/preferences/siteDataRemoveSelected.xul";
+ 
+ const { DownloadUtils } = ChromeUtils.import("resource://gre/modules/DownloadUtils.jsm", {});
+ const { SiteDataManager } = ChromeUtils.import("resource:///modules/SiteDataManager.jsm", {});
+ const { OfflineAppCacheHelper } = ChromeUtils.import("resource:///modules/offlineAppCache.jsm", {});
+ 
++ChromeUtils.defineModuleGetter(this, "SiteDataTestUtils",
++                               "resource://testing-common/SiteDataTestUtils.jsm");
++
+ XPCOMUtils.defineLazyServiceGetter(this, "serviceWorkerManager", "@mozilla.org/serviceworkers/manager;1", "nsIServiceWorkerManager");
+ 
+ // Tests within /browser/components/preferences/in-content-new/tests/
+ // test the "new" preferences organization, after it was reorganized.
+ // Thus, all of these tests should set the "oldOrganization" to false
+ // before running.
+ Services.prefs.setBoolPref("browser.preferences.useOldOrganization", false);
+ registerCleanupFunction(function() {
+@@ -208,24 +211,16 @@ const mockSiteDataManager = {
+   async unregister() {
+     await this._SiteDataManager.removeAll();
+     this.fakeSites = null;
+     this._SiteDataManager._qms = this._originalQMS;
+     this._SiteDataManager._removeQuotaUsage = this._originalRemoveQuotaUsage;
+   }
+ };
+ 
+-function getQuotaUsage(origin) {
+-  return new Promise(resolve => {
+-    let uri = NetUtil.newURI(origin);
+-    let principal = Services.scriptSecurityManager.createCodebasePrincipal(uri, {});
+-    Services.qms.getUsageForPrincipal(principal, request => resolve(request.result.usage));
+-  });
+-}
+-
+ function promiseCookiesCleared() {
+   return TestUtils.topicObserved("cookie-changed", (subj, data) => {
+     return data === "cleared";
+   });
+ }
+ 
+ async function loadServiceWorkerTestPage(url) {
+   let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, url);
+diff --git a/browser/components/preferences/in-content/tests/siteData/browser_clearSiteData.js b/browser/components/preferences/in-content/tests/siteData/browser_clearSiteData.js
+--- a/browser/components/preferences/in-content/tests/siteData/browser_clearSiteData.js
++++ b/browser/components/preferences/in-content/tests/siteData/browser_clearSiteData.js
+@@ -25,17 +25,17 @@ async function testClearData(clearSiteDa
+   // Register some service workers.
+   await loadServiceWorkerTestPage(TEST_SERVICE_WORKER_URL);
+   await promiseServiceWorkerRegisteredFor(TEST_SERVICE_WORKER_URL);
+ 
+   await openPreferencesViaOpenPreferencesAPI("advanced", "networkTab", { leaveOpen: true });
+ 
+   // Test the initial states.
+   let cacheUsage = await SiteDataManager.getCacheSize();
+-  let quotaUsage = await getQuotaUsage(TEST_QUOTA_USAGE_ORIGIN);
++  let quotaUsage = await SiteDataTestUtils.getQuotaUsage(TEST_QUOTA_USAGE_ORIGIN);
+   let totalUsage = await SiteDataManager.getTotalUsage();
+   Assert.greater(cacheUsage, 0, "The cache usage should not be 0");
+   Assert.greater(quotaUsage, 0, "The quota usage should not be 0");
+   Assert.greater(totalUsage, 0, "The total usage should not be 0");
+ 
+   let initialSizeLabelValue = await ContentTask.spawn(gBrowser.selectedBrowser, null, async function() {
+     let sizeLabel = content.document.getElementById("totalSiteDataSize");
+     return sizeLabel.textContent;
+@@ -122,17 +122,17 @@ async function testClearData(clearSiteDa
+     await cookiesClearedPromise;
+     await promiseServiceWorkersCleared();
+ 
+     TestUtils.waitForCondition(async function() {
+       let usage = await SiteDataManager.getTotalUsage();
+       return usage == 0;
+     }, "The total usage should be removed");
+   } else {
+-    quotaUsage = await getQuotaUsage(TEST_QUOTA_USAGE_ORIGIN);
++    quotaUsage = await SiteDataTestUtils.getQuotaUsage(TEST_QUOTA_USAGE_ORIGIN);
+     totalUsage = await SiteDataManager.getTotalUsage();
+     Assert.greater(quotaUsage, 0, "The quota usage should not be 0");
+     Assert.greater(totalUsage, 0, "The total usage should not be 0");
+   }
+ 
+   if (clearCache || clearSiteData) {
+     // Check that the size label in about:preferences updates after we cleared data.
+     await ContentTask.spawn(gBrowser.selectedBrowser, {initialSizeLabelValue}, async function(opts) {
+diff --git a/browser/components/preferences/in-content/tests/siteData/head.js b/browser/components/preferences/in-content/tests/siteData/head.js
+--- a/browser/components/preferences/in-content/tests/siteData/head.js
++++ b/browser/components/preferences/in-content/tests/siteData/head.js
+@@ -12,16 +12,19 @@ const TEST_OFFLINE_URL = getRootDirector
+ const TEST_SERVICE_WORKER_URL = getRootDirectory(gTestPath).replace("chrome://mochitests/content", TEST_OFFLINE_ORIGIN) + "/service_worker_test.html";
+ 
+ const REMOVE_DIALOG_URL = "chrome://browser/content/preferences/siteDataRemoveSelected.xul";
+ 
+ const { DownloadUtils } = ChromeUtils.import("resource://gre/modules/DownloadUtils.jsm", {});
+ const { SiteDataManager } = ChromeUtils.import("resource:///modules/SiteDataManager.jsm", {});
+ const { OfflineAppCacheHelper } = ChromeUtils.import("resource:///modules/offlineAppCache.jsm", {});
+ 
++ChromeUtils.defineModuleGetter(this, "SiteDataTestUtils",
++                               "resource://testing-common/SiteDataTestUtils.jsm");
++
+ XPCOMUtils.defineLazyServiceGetter(this, "serviceWorkerManager", "@mozilla.org/serviceworkers/manager;1", "nsIServiceWorkerManager");
+ 
+ // Tests within /browser/components/preferences/in-content/tests/
+ // test the "old" preferences organization, before it was reorganized.
+ // Thus, all of these tests should revert back to the "oldOrganization"
+ // before running.
+ Services.prefs.setBoolPref("browser.preferences.useOldOrganization", true);
+ registerCleanupFunction(function() {
+@@ -225,24 +228,16 @@ function promiseSettingsDialogClose() {
+       if (dialogWin.document.documentURI === "chrome://browser/content/preferences/siteDataSettings.xul") {
+         isnot(dialogOverlay.style.visibility, "visible", "The Settings dialog should be hidden");
+         resolve();
+       }
+     }, { once: true });
+   });
+ }
+ 
+-function getQuotaUsage(origin) {
+-  return new Promise(resolve => {
+-    let uri = NetUtil.newURI(origin);
+-    let principal = Services.scriptSecurityManager.createCodebasePrincipal(uri, {});
+-    Services.qms.getUsageForPrincipal(principal, request => resolve(request.result.usage));
+-  });
+-}
+-
+ function promiseCookiesCleared() {
+   return TestUtils.topicObserved("cookie-changed", (subj, data) => {
+     return data === "cleared";
+   });
+ }
+ 
+ async function loadServiceWorkerTestPage(url) {
+   let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, url);

+ 286 - 0
frg/mozilla-release/work-js/1348223-2-61a1.patch

@@ -0,0 +1,286 @@
+# HG changeset patch
+# User Johann Hofmann <jhofmann@mozilla.com>
+# Date 1521737293 -3600
+# Node ID e4715e0cc3fe86fbda7a41dcabee48d77859cdf4
+# Parent  1cad7073c8c310aeaca5e6d413c285cf42aa6d53
+Bug 1348223 - Part 2 - Move SiteDataManager.jsm to components and improve functionality for getting and removing by host. r=florian
+
+This commit is in preparation of using SiteDataManager in the page info
+window to display site data information for a individual hosts.
+
+MozReview-Commit-ID: 3YmUZInvoAT
+
+diff --git a/browser/components/preferences/moz.build b/browser/components/preferences/moz.build
+--- a/browser/components/preferences/moz.build
++++ b/browser/components/preferences/moz.build
+@@ -19,14 +19,10 @@ BROWSER_CHROME_MANIFESTS += [
+ for var in ('MOZ_APP_NAME', 'MOZ_MACBUNDLE_NAME'):
+     DEFINES[var] = CONFIG[var]
+ 
+ if CONFIG['MOZ_WIDGET_TOOLKIT'] in ('windows', 'gtk', 'cocoa'):
+     DEFINES['HAVE_SHELL_SERVICE'] = 1
+ 
+ JAR_MANIFESTS += ['jar.mn']
+ 
+-EXTRA_JS_MODULES += [
+-    'SiteDataManager.jsm'
+-]
+-
+ with Files('**'):
+     BUG_COMPONENT = ('Firefox', 'Preferences')
+diff --git a/browser/components/preferences/siteDataSettings.js b/browser/components/preferences/siteDataSettings.js
+--- a/browser/components/preferences/siteDataSettings.js
++++ b/browser/components/preferences/siteDataSettings.js
+@@ -224,32 +224,19 @@ let gSiteDataSettings = {
+ 
+     if (removals.length > 0) {
+       if (this._sites.length == removals.length) {
+         allowed = SiteDataManager.promptSiteDataRemoval(window);
+         if (allowed) {
+           SiteDataManager.removeAll();
+         }
+       } else {
+-        let args = {
+-          hosts: removals,
+-          allowed: false
+-        };
+-        let features = "centerscreen,chrome,modal,resizable=no";
+-        window.openDialog("chrome://browser/content/preferences/siteDataRemoveSelected.xul", "", features, args);
+-        allowed = args.allowed;
++        allowed = SiteDataManager.promptSiteDataRemoval(window, removals);
+         if (allowed) {
+-          try {
+-            SiteDataManager.remove(removals);
+-          } catch (e) {
+-            // Hit error, maybe remove unknown site.
+-            // Let's print out the error, then proceed to close this settings dialog.
+-            // When we next open again we will once more get sites from the SiteDataManager and refresh the list.
+-            Cu.reportError(e);
+-          }
++          SiteDataManager.remove(removals).catch(Cu.reportError);
+         }
+       }
+     }
+ 
+     // If the user cancelled the confirm dialog keep the site data window open,
+     // they can still press cancel again to exit.
+     if (allowed) {
+       this.close();
+diff --git a/browser/components/preferences/SiteDataManager.jsm b/browser/modules/SiteDataManager.jsm
+rename from browser/components/preferences/SiteDataManager.jsm
+rename to browser/modules/SiteDataManager.jsm
+--- a/browser/components/preferences/SiteDataManager.jsm
++++ b/browser/modules/SiteDataManager.jsm
+@@ -49,17 +49,17 @@ var SiteDataManager = {
+     // Clear old data and requests first
+     this._sites.clear();
+     this._getAllCookies();
+     await this._getQuotaUsage();
+     this._updateAppCache();
+     Services.obs.notifyObservers(null, "sitedatamanager:sites-updated");
+   },
+ 
+-  _getBaseDomainFromHost(host) {
++  getBaseDomainFromHost(host) {
+     let result = host;
+     try {
+       result = Services.eTLD.getBaseDomainFromHost(host);
+     } catch (e) {
+       if (e.result == Cr.NS_ERROR_HOST_IS_IP_ADDRESS ||
+           e.result == Cr.NS_ERROR_INSUFFICIENT_DOMAIN_LEVELS) {
+         // For these 2 expected errors, just take the host as the result.
+         // - NS_ERROR_HOST_IS_IP_ADDRESS: the host is in ipv4/ipv6.
+@@ -71,17 +71,17 @@ var SiteDataManager = {
+     }
+     return result;
+   },
+ 
+   _getOrInsertSite(host) {
+     let site = this._sites.get(host);
+     if (!site) {
+       site = {
+-        baseDomain: this._getBaseDomainFromHost(host),
++        baseDomain: this.getBaseDomainFromHost(host),
+         cookies: [],
+         persisted: false,
+         quotaUsage: 0,
+         lastAccessed: 0,
+         principals: [],
+         appCacheList: [],
+       };
+       this._sites.set(host, site);
+@@ -217,20 +217,38 @@ var SiteDataManager = {
+           usage += cache.usage;
+         }
+         usage += site.quotaUsage;
+       }
+       return usage;
+     });
+   },
+ 
+-  getSites() {
++  /**
++   * Gets all sites that are currently storing site data.
++   *
++   * The list is not automatically up-to-date.
++   * You need to call SiteDataManager.updateSites() before you
++   * can use this method for the first time (and whenever you want
++   * to get an updated set of list.)
++   *
++   * @param {String} [optional] baseDomain - if specified, it will
++   *                            only return data for sites with
++   *                            the specified base domain.
++   *
++   * @returns a Promise that resolves with the list of all sites.
++   */
++  getSites(baseDomain) {
+     return this._getQuotaUsagePromise.then(() => {
+       let list = [];
+       for (let [host, site] of this._sites) {
++        if (baseDomain && site.baseDomain != baseDomain) {
++          continue;
++        }
++
+         let usage = site.quotaUsage;
+         for (let cache of site.appCacheList) {
+           usage += cache.usage;
+         }
+         list.push({
+           baseDomain: site.baseDomain,
+           cookies: site.cookies,
+           host,
+@@ -316,17 +334,28 @@ var SiteDataManager = {
+       // Sites are grouped and removed by host so we unregister service workers by the same host as well
+       if (sites.has(sw.principal.URI.host)) {
+         promises.push(this._unregisterServiceWorker(sw));
+       }
+     }
+     return Promise.all(promises);
+   },
+ 
+-  remove(hosts) {
++  /**
++   * Removes all site data for the specified list of hosts.
++   *
++   * @param {Array} a list of hosts to match for removal.
++   * @returns a Promise that resolves when data is removed and the site data
++   *          manager has been updated.
++   */
++  async remove(hosts) {
++    // Make sure we have up-to-date information.
++    await this._getQuotaUsage();
++    this._updateAppCache();
++
+     let unknownHost = "";
+     let targetSites = new Map();
+     for (let host of hosts) {
+       let site = this._sites.get(host);
+       if (site) {
+         this._removePermission(site);
+         this._removeAppCache(site);
+         this._removeCookies(site);
+@@ -334,40 +363,51 @@ var SiteDataManager = {
+         targetSites.set(host, site);
+       } else {
+         unknownHost = host;
+         break;
+       }
+     }
+ 
+     if (targetSites.size > 0) {
+-      this._removeServiceWorkersForSites(targetSites)
+-          .then(() => {
+-            let promises = [];
+-            for (let [, site] of targetSites) {
+-              promises.push(this._removeQuotaUsage(site));
+-            }
+-            return Promise.all(promises);
+-          })
+-          .then(() => this.updateSites());
++      await this._removeServiceWorkersForSites(targetSites);
++      let promises = [];
++      for (let [, site] of targetSites) {
++        promises.push(this._removeQuotaUsage(site));
++      }
++      await Promise.all(promises);
+     }
++
+     if (unknownHost) {
+       throw `SiteDataManager: removing unknown site of ${unknownHost}`;
+     }
++
++    return this.updateSites();
+   },
+ 
+   /**
+    * In the specified window, shows a prompt for removing
+-   * all site data, warning the user that this may log them
+-   * out of websites.
++   * all site data or the specified list of hosts, warning the
++   * user that this may log them out of websites.
+    *
+    * @param {mozIDOMWindowProxy} a parent DOM window to host the dialog.
++   * @param {Array} [optional] an array of host name strings that will be removed.
+    * @returns a boolean whether the user confirmed the prompt.
+    */
+-  promptSiteDataRemoval(win) {
++  promptSiteDataRemoval(win, removals) {
++    if (removals) {
++      let args = {
++        hosts: removals,
++        allowed: false
++      };
++      let features = "centerscreen,chrome,modal,resizable=no";
++      win.openDialog("chrome://browser/content/preferences/siteDataRemoveSelected.xul", "", features, args);
++      return args.allowed;
++    }
++
+     let brandName = gBrandBundle.GetStringFromName("brandShortName");
+     let flags =
+       Services.prompt.BUTTON_TITLE_IS_STRING * Services.prompt.BUTTON_POS_0 +
+       Services.prompt.BUTTON_TITLE_CANCEL * Services.prompt.BUTTON_POS_1 +
+       Services.prompt.BUTTON_POS_0_DEFAULT;
+     let title = gStringBundle.GetStringFromName("clearSiteDataPromptTitle");
+     let text = gStringBundle.formatStringFromName("clearSiteDataPromptText", [brandName], 1);
+     let btn0Label = gStringBundle.GetStringFromName("clearSiteDataNow");
+diff --git a/browser/modules/moz.build b/browser/modules/moz.build
+--- a/browser/modules/moz.build
++++ b/browser/modules/moz.build
+@@ -83,16 +83,19 @@ with Files("ProcessHangMonitor.jsm"):
+     BUG_COMPONENT = ("Core", "DOM: Content Processes")
+ 
+ with Files("ReaderParent.jsm"):
+     BUG_COMPONENT = ("Toolkit", "Reader Mode")
+ 
+ with Files("Sanitizer.jsm"):
+     BUG_COMPONENT = ("Firefox", "Preferences")
+ 
++with Files("SiteDataManager.jsm"):
++    BUG_COMPONENT = ("Firefox", "Preferences")
++
+ with Files("SitePermissions.jsm"):
+     BUG_COMPONENT = ("Firefox", "Site Identity and Permission Panels")
+ 
+ with Files("TransientPrefs.jsm"):
+     BUG_COMPONENT = ("Firefox", "Preferences")
+ 
+ with Files("Windows8WindowFrameColor.jsm"):
+     BUG_COMPONENT = ("Firefox", "Theme")
+@@ -139,16 +142,17 @@ EXTRA_JS_MODULES += [
+     'PageActions.jsm',
+     'PermissionUI.jsm',
+     'PluginContent.jsm',
+     'ProcessHangMonitor.jsm',
+     'ReaderParent.jsm',
+     'RecentWindow.jsm',
+     'RemotePrompt.jsm',
+     'Sanitizer.jsm',
++    'SiteDataManager.jsm',
+     'SitePermissions.jsm',
+     'TransientPrefs.jsm',
+     'UpdateTopLevelContentWindowIDHelper.jsm',
+     'webrtcUI.jsm',
+     'ZoomUI.jsm',
+ ]
+ 
+ if CONFIG['MOZ_WIDGET_TOOLKIT'] == 'windows':

+ 327 - 0
frg/mozilla-release/work-js/1348223-3-61a1.patch

@@ -0,0 +1,327 @@
+# HG changeset patch
+# User Johann Hofmann <jhofmann@mozilla.com>
+# Date 1521736012 -3600
+# Node ID 8dec8e29532e8e5759fef70bf43d4468740c7347
+# Parent  5557fd7fa8c1db1e47eded0deb09af4294d82de2
+Bug 1348223 - Part 3 - Show and clear all site data in page info (not just cookies). r=florian
+
+This commit changes the way that the page info security section works
+to not only consider cookies when answering the question "Is this website
+storing information about me?".
+
+The associated button used to open the cookie manager. We would like to
+reduce the storage management UI surface (in the interest of having one
+great central location for managing storage), so that button was replaced
+with a simple "clear data" button that clears site data for all origins of
+the base domain of the site.
+
+Clearing for the entire range of that base domain is a trade off between
+(implicit) user privacy and the risk that the user accidentally
+deletes too much data. I prefer to choose increased privacy.
+
+MozReview-Commit-ID: BUgxJHBeFcQ
+
+diff --git a/browser/base/content/pageinfo/pageInfo.xul b/browser/base/content/pageinfo/pageInfo.xul
+--- a/browser/base/content/pageinfo/pageInfo.xul
++++ b/browser/base/content/pageinfo/pageInfo.xul
+@@ -333,74 +333,71 @@
+         <spacer flex="1"/>
+         <!-- Cert button -->
+         <hbox id="security-view-cert-box" pack="end">
+           <button id="security-view-cert" label="&securityView.certView;"
+                   accesskey="&securityView.accesskey;"
+                   oncommand="security.viewCert();"/>
+         </hbox>
+       </groupbox>
+-      
++
+       <!-- Privacy & History section -->
+       <groupbox id="security-privacy-groupbox" flex="1">
+         <caption id="security-privacy" label="&securityView.privacy.header;" />
+         <grid id="security-privacy-grid">
+           <columns>
+             <column flex="1"/>
+             <column flex="1"/>
+           </columns>
+           <rows id="security-privacy-rows">
+             <!-- History -->
+             <row id="security-privacy-history-row">
+               <label id="security-privacy-history-label"
+                            control="security-privacy-history-value"
+                            class="fieldLabel">&securityView.privacy.history;</label>
+-              <textbox id="security-privacy-history-value"
+-                       class="fieldValue"
+-                       value="&securityView.unknown;"
+-                       readonly="true"/>
++              <label id="security-privacy-history-value"
++                     class="fieldValue"
++                     value="&securityView.unknown;"/>
+             </row>
+-            <!-- Cookies -->
+-            <row id="security-privacy-cookies-row">
+-              <label id="security-privacy-cookies-label"
+-                           control="security-privacy-cookies-value"
+-                           class="fieldLabel">&securityView.privacy.cookies;</label>
+-              <hbox id="security-privacy-cookies-box" align="center">
+-                <textbox id="security-privacy-cookies-value"
+-                         class="fieldValue"
+-                         value="&securityView.unknown;"
+-                         flex="1"
+-                         readonly="true"/>
+-                <button id="security-view-cookies"
+-                        label="&securityView.privacy.viewCookies;"
+-                        accesskey="&securityView.privacy.viewCookies.accessKey;"
+-                        oncommand="security.viewCookies();"/>
++            <!-- Site Data & Cookies -->
++            <row id="security-privacy-sitedata-row">
++              <label id="security-privacy-sitedata-label"
++                           control="security-privacy-sitedata-value"
++                           class="fieldLabel">&securityView.privacy.siteData;</label>
++              <hbox id="security-privacy-sitedata-box" align="center">
++                <label id="security-privacy-sitedata-value"
++                       class="fieldValue"
++                       flex="1">&securityView.unknown;</label>
++                <button id="security-clear-sitedata"
++                        disabled="true"
++                        label="&securityView.privacy.clearSiteData;"
++                        accesskey="&securityView.privacy.clearSiteData.accessKey;"
++                        oncommand="security.clearSiteData();"/>
+               </hbox>
+             </row>
+             <!-- Passwords -->
+             <row id="security-privacy-passwords-row">
+               <label id="security-privacy-passwords-label"
+                             control="security-privacy-passwords-value"
+                             class="fieldLabel">&securityView.privacy.passwords;</label>
+               <hbox id="security-privacy-passwords-box" align="center">
+-                <textbox id="security-privacy-passwords-value"
+-                         class="fieldValue"
+-                         value="&securityView.unknown;"
+-                         flex="1"
+-                         readonly="true"/>
++                <label id="security-privacy-passwords-value"
++                       class="fieldValue"
++                       value="&securityView.unknown;"
++                       flex="1"/>
+                 <button id="security-view-password"
+                         label="&securityView.privacy.viewPasswords;"
+                         accesskey="&securityView.privacy.viewPasswords.accessKey;"
+                         oncommand="security.viewPasswords();"/>
+               </hbox>
+             </row>
+           </rows>
+         </grid>
+       </groupbox>
+-      
++
+       <!-- Technical Details section -->
+       <groupbox id="security-technical-groupbox" flex="1">
+         <caption id="security-technical" label="&securityView.technical.header;" />
+         <vbox id="security-technical-box" flex="1">
+           <label id="security-technical-shortform" class="fieldValue"/>
+           <description id="security-technical-longform1" class="fieldLabel"/>
+           <description id="security-technical-longform2" class="fieldLabel"/>
+           <description id="security-technical-certificate-transparency" class="fieldLabel"/>
+diff --git a/browser/base/content/pageinfo/security.js b/browser/base/content/pageinfo/security.js
+--- a/browser/base/content/pageinfo/security.js
++++ b/browser/base/content/pageinfo/security.js
+@@ -1,14 +1,16 @@
+ /* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+ /* This Source Code Form is subject to the terms of the Mozilla Public
+  * License, v. 2.0. If a copy of the MPL was not distributed with this
+  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+ 
+ ChromeUtils.import("resource://gre/modules/BrowserUtils.jsm");
++ChromeUtils.import("resource:///modules/SiteDataManager.jsm");
++ChromeUtils.import("resource://gre/modules/DownloadUtils.jsm");
+ 
+ /* import-globals-from pageInfo.js */
+ 
+ ChromeUtils.defineModuleGetter(this, "LoginHelper",
+                                "resource://gre/modules/LoginHelper.jsm");
+ 
+ var security = {
+   init(uri, windowInfo) {
+@@ -139,36 +141,61 @@ var security = {
+     if (!name) return null;
+ 
+     if (name == "RSA Data Security, Inc.") return "Verisign, Inc.";
+ 
+     // No mapping required
+     return name;
+   },
+ 
+-  /**
+-   * Open the cookie manager window
+-   */
+-  viewCookies() {
+-    var win = Services.wm.getMostRecentWindow("Browser:Cookies");
++  async _updateSiteDataInfo() {
++    // Save site data info for deleting.
++    this.siteData = await SiteDataManager.getSites(
++      SiteDataManager.getBaseDomainFromHost(this.uri.host));
+ 
+-    var eTLD;
+-    try {
+-      eTLD = Services.eTLD.getBaseDomain(this.uri);
+-    } catch (e) {
+-      // getBaseDomain will fail if the host is an IP address or is empty
+-      eTLD = this.uri.asciiHost;
++    let pageInfoBundle = document.getElementById("pageinfobundle");
++    let clearSiteDataButton = document.getElementById("security-clear-sitedata");
++    let siteDataLabel = document.getElementById("security-privacy-sitedata-value");
++
++    if (!this.siteData.length) {
++      let noStr = pageInfoBundle.getString("securitySiteDataNo");
++      siteDataLabel.textContent = noStr;
++      clearSiteDataButton.setAttribute("disabled", "true");
++      return;
+     }
+ 
+-    if (win) {
+-      win.gCookiesWindow.setFilter(eTLD);
+-      win.focus();
+-    } else
+-      window.openDialog("chrome://browser/content/preferences/cookies.xul",
+-                        "Browser:Cookies", "", {filterString: eTLD});
++    let usageText;
++    let usage = this.siteData.reduce((acc, site) => acc + site.usage, 0);
++    if (usage > 0) {
++      let size = DownloadUtils.convertByteUnits(usage);
++      let hasCookies = this.siteData.some(site => site.cookies.length > 0);
++      if (hasCookies) {
++        usageText = pageInfoBundle.getFormattedString("securitySiteDataCookies", size);
++      } else {
++        usageText = pageInfoBundle.getFormattedString("securitySiteDataOnly", size);
++      }
++    } else {
++      // We're storing cookies, else the list would have been empty.
++      usageText = pageInfoBundle.getString("securitySiteDataCookiesOnly");
++    }
++
++    clearSiteDataButton.removeAttribute("disabled");
++    siteDataLabel.textContent = usageText;
++  },
++
++  /**
++   * Clear Site Data and Cookies
++   */
++  clearSiteData() {
++    if (this.siteData && this.siteData.length) {
++      let hosts = this.siteData.map(site => site.host);
++      if (SiteDataManager.promptSiteDataRemoval(window, hosts)) {
++        SiteDataManager.remove(hosts).then(() => this._updateSiteDataInfo());
++      }
++    }
+   },
+ 
+   /**
+    * Open the login manager window
+    */
+   viewPasswords() {
+     LoginHelper.openPasswordManager(window, this._getSecurityInfo().hostName);
+   },
+@@ -233,18 +260,23 @@ function securityOnLoad(uri, windowInfo)
+     viewCert.collapsed = false;
+   } else
+     viewCert.collapsed = true;
+ 
+   /* Set Privacy & History section text */
+   var yesStr = pageInfoBundle.getString("yes");
+   var noStr = pageInfoBundle.getString("no");
+ 
+-  setText("security-privacy-cookies-value",
+-          hostHasCookies(uri) ? yesStr : noStr);
++  // Only show quota usage data for websites, not internal sites.
++  if (uri.scheme == "http" || uri.scheme == "https") {
++    SiteDataManager.updateSites().then(() => security._updateSiteDataInfo());
++  } else {
++    document.getElementById("security-privacy-sitedata-row").hidden = true;
++  }
++
+   setText("security-privacy-passwords-value",
+           realmHasPasswords(uri) ? yesStr : noStr);
+ 
+   var visitCount = previousVisitCount(info.hostName);
+   if (visitCount > 1) {
+     setText("security-privacy-history-value",
+             pageInfoBundle.getFormattedString("securityNVisits", [visitCount.toLocaleString()]));
+   } else if (visitCount == 1) {
+@@ -321,23 +353,16 @@ function viewCertHelper(parent, cert) {
+   if (!cert)
+     return;
+ 
+   var cd = Cc[CERTIFICATEDIALOGS_CONTRACTID].getService(nsICertificateDialogs);
+   cd.viewCert(parent, cert);
+ }
+ 
+ /**
+- * Return true iff we have cookies for uri
+- */
+-function hostHasCookies(uri) {
+-  return Services.cookies.countCookiesFromHost(uri.asciiHost) > 0;
+-}
+-
+-/**
+  * Return true iff realm (proto://host:port) (extracted from uri) has
+  * saved passwords
+  */
+ function realmHasPasswords(uri) {
+   return Services.logins.countLogins(uri.prePath, "", "") > 0;
+ }
+ 
+ /**
+diff --git a/browser/locales/en-US/chrome/browser/pageInfo.dtd b/browser/locales/en-US/chrome/browser/pageInfo.dtd
+--- a/browser/locales/en-US/chrome/browser/pageInfo.dtd
++++ b/browser/locales/en-US/chrome/browser/pageInfo.dtd
+@@ -63,18 +63,18 @@
+ <!ENTITY  securityView.identity.header   "Website Identity">
+ <!ENTITY  securityView.identity.owner    "Owner:">
+ <!ENTITY  securityView.identity.domain   "Website:">
+ <!ENTITY  securityView.identity.verifier "Verified by:">
+ <!ENTITY  securityView.identity.validity "Expires on:">
+ 
+ <!ENTITY  securityView.privacy.header                   "Privacy &amp; History">
+ <!ENTITY  securityView.privacy.history                  "Have I visited this website prior to today?">
+-<!ENTITY  securityView.privacy.cookies                  "Is this website storing information (cookies) on my computer?">
+-<!ENTITY  securityView.privacy.viewCookies              "View Cookies">
+-<!ENTITY  securityView.privacy.viewCookies.accessKey    "k">
++<!ENTITY  securityView.privacy.siteData                 "Is this website storing information on my computer?">
++<!ENTITY  securityView.privacy.clearSiteData            "Clear Cookies and Site Data">
++<!ENTITY  securityView.privacy.clearSiteData.accessKey  "C">
+ <!ENTITY  securityView.privacy.passwords                "Have I saved any passwords for this website?">
+ <!ENTITY  securityView.privacy.viewPasswords            "View Saved Passwords">
+ <!ENTITY  securityView.privacy.viewPasswords.accessKey  "w">
+ 
+ <!ENTITY  securityView.technical.header                 "Technical Details">
+ 
+ <!ENTITY  helpButton.label                              "Help">
+diff --git a/browser/locales/en-US/chrome/browser/pageInfo.properties b/browser/locales/en-US/chrome/browser/pageInfo.properties
+--- a/browser/locales/en-US/chrome/browser/pageInfo.properties
++++ b/browser/locales/en-US/chrome/browser/pageInfo.properties
+@@ -41,16 +41,21 @@ generalMetaTags=Meta (%S tags)
+ feedRss=RSS
+ feedAtom=Atom
+ feedXML=XML
+ 
+ securityNoOwner=This website does not supply ownership information.
+ securityOneVisit=Yes, once
+ securityNVisits=Yes, %S times
+ 
+-# LOCALIZATION NOTE: The next string is for the disk usage of the
+-# database
+-#   e.g. indexedDBUsage : "50.23 MB"
++# LOCALIZATION NOTE(securitySiteDataCookies,securitySiteDataOnly): This is for site data disk usage.
++#   It confirms that a website is indeed using this much space.
++#   e.g. Is this website storing site data? "Yes, 50.23 MB"
+ #   %1$S = size (in bytes or megabytes, ...)
+ #   %2$S = unit of measure (bytes, KB, MB, ...)
+-indexedDBUsage=This website is using %1$S %2$S
++securitySiteDataCookies=Yes, cookies and %1$S %2$S of site data
++securitySiteDataOnly=Yes, %1$S %2$S of site data
++# LOCALIZATION NOTE(securitySiteDataCookiesOnly,securitySiteDataNo):
++# This is for site data and cookies usage. It answers the question "Is this website storing cookies and/or site data?"
++securitySiteDataCookiesOnly=Yes, cookies
++securitySiteDataNo=No
+ 
+ permissions.useDefault=Use Default

+ 171 - 0
frg/mozilla-release/work-js/1348223-4-61a1.patch

@@ -0,0 +1,171 @@
+# HG changeset patch
+# User Johann Hofmann <jhofmann@mozilla.com>
+# Date 1521736191 -3600
+# Node ID 0f1aa8fea3708404f66b24f958afc9dcd15dbfe2
+# Parent  d7e8d09e108ed625797e9b6717f2ad8533408650
+Bug 1348223 - Part 4 - Add and update tests for removing site data and cookies in the page info window. r=florian
+
+MozReview-Commit-ID: gAaComSklW
+
+diff --git a/browser/base/content/test/pageinfo/browser.ini b/browser/base/content/test/pageinfo/browser.ini
+--- a/browser/base/content/test/pageinfo/browser.ini
++++ b/browser/base/content/test/pageinfo/browser.ini
+@@ -7,12 +7,13 @@ support-files =
+ support-files =
+   image.html
+   ../general/audio.ogg
+   ../general/moz.png
+   ../general/video.ogg
+ [browser_pageinfo_images.js]
+ [browser_pageinfo_image_info.js]
+ skip-if = (os == 'linux' && e10s) # bug 1161699
++[browser_pageinfo_security.js]
+ [browser_pageinfo_svg_image.js]
+ support-files =
+   svg_image.html
+   ../general/title_test.svg
+diff --git a/browser/base/content/test/pageinfo/browser_pageinfo_security.js b/browser/base/content/test/pageinfo/browser_pageinfo_security.js
+new file mode 100644
+--- /dev/null
++++ b/browser/base/content/test/pageinfo/browser_pageinfo_security.js
+@@ -0,0 +1,93 @@
++ChromeUtils.import("resource:///modules/SitePermissions.jsm");
++
++ChromeUtils.defineModuleGetter(this, "SiteDataTestUtils",
++                               "resource://testing-common/SiteDataTestUtils.jsm");
++ChromeUtils.defineModuleGetter(this, "DownloadUtils",
++                               "resource://gre/modules/DownloadUtils.jsm");
++
++const TEST_ORIGIN = "https://example.com";
++const TEST_SUB_ORIGIN = "https://test1.example.com";
++const REMOVE_DIALOG_URL = "chrome://browser/content/preferences/siteDataRemoveSelected.xul";
++
++// Test displaying and removing quota managed data.
++add_task(async function test_SiteData() {
++  await SiteDataTestUtils.addToIndexedDB(TEST_ORIGIN);
++
++  await BrowserTestUtils.withNewTab(TEST_ORIGIN, async function(browser) {
++
++    let totalUsage = await SiteDataTestUtils.getQuotaUsage(TEST_ORIGIN);
++    Assert.greater(totalUsage, 0, "The total usage should not be 0");
++
++    let pageInfo = BrowserPageInfo(TEST_ORIGIN, "securityTab");
++    await BrowserTestUtils.waitForEvent(pageInfo, "load");
++
++    let label = pageInfo.document.getElementById("security-privacy-sitedata-value");
++    let clearButton = pageInfo.document.getElementById("security-clear-sitedata");
++
++    let size = DownloadUtils.convertByteUnits(totalUsage);
++
++    // The usage details are filled asynchronously, so we assert that they're present by
++    // waiting for them to be filled in.
++    // We only wait for the right unit to appear, since this number is intermittently
++    // varying by slight amounts on infra machines.
++    await BrowserTestUtils.waitForCondition(() => label.textContent.includes(size[1]),
++      "Should show site data usage in the security section.");
++    let siteDataUpdated = TestUtils.topicObserved("sitedatamanager:sites-updated");
++
++    let removeDialogPromise = BrowserTestUtils.promiseAlertDialogOpen("accept", REMOVE_DIALOG_URL);
++    clearButton.click();
++    await removeDialogPromise;
++
++    await siteDataUpdated;
++
++    totalUsage = await SiteDataTestUtils.getQuotaUsage(TEST_ORIGIN);
++    is(totalUsage, 0, "The total usage should be 0");
++
++    await BrowserTestUtils.waitForCondition(() => label.textContent == "No",
++      "Should show no site data usage in the security section.");
++
++    pageInfo.close();
++  });
++});
++
++// Test displaying and removing cookies.
++add_task(async function test_Cookies() {
++  // Add some test cookies.
++  SiteDataTestUtils.addToCookies(TEST_ORIGIN, "test1", "1");
++  SiteDataTestUtils.addToCookies(TEST_ORIGIN, "test2", "2");
++  SiteDataTestUtils.addToCookies(TEST_SUB_ORIGIN, "test1", "1");
++
++  await BrowserTestUtils.withNewTab(TEST_ORIGIN, async function(browser) {
++    let pageInfo = BrowserPageInfo(TEST_ORIGIN, "securityTab");
++    await BrowserTestUtils.waitForEvent(pageInfo, "load");
++
++    let label = pageInfo.document.getElementById("security-privacy-sitedata-value");
++    let clearButton = pageInfo.document.getElementById("security-clear-sitedata");
++
++    // The usage details are filled asynchronously, so we assert that they're present by
++    // waiting for them to be filled in.
++    await BrowserTestUtils.waitForCondition(() => label.textContent.includes("cookies"),
++      "Should show cookies in the security section.");
++
++    let cookiesCleared = TestUtils.topicObserved("cookie-changed", (subj, data) => data == "deleted");
++
++    let removeDialogPromise = BrowserTestUtils.promiseAlertDialogOpen("accept", REMOVE_DIALOG_URL);
++    clearButton.click();
++    await removeDialogPromise;
++
++    await cookiesCleared;
++
++    let uri = Services.io.newURI(TEST_ORIGIN);
++    is(Services.cookies.countCookiesFromHost(uri.host), 0, "Cookies from the base domain should be cleared");
++
++    await BrowserTestUtils.waitForCondition(() => label.textContent == "No",
++      "Should show no cookies in the security section.");
++
++    pageInfo.close();
++  });
++});
++
++// Clean up in case we missed anything...
++add_task(async function cleanup() {
++  await SiteDataTestUtils.clear();
++});
+diff --git a/testing/firefox-ui/tests/puppeteer/test_page_info_window.py b/testing/firefox-ui/tests/puppeteer/test_page_info_window.py
+--- a/testing/firefox-ui/tests/puppeteer/test_page_info_window.py
++++ b/testing/firefox-ui/tests/puppeteer/test_page_info_window.py
+@@ -41,17 +41,16 @@ class TestPageInfoWindow(PuppeteerMixin,
+ 
+         self.assertEqual(panel.element.get_property('localName'), 'vbox')
+ 
+         self.assertEqual(panel.domain.get_property('localName'), 'textbox')
+         self.assertEqual(panel.owner.get_property('localName'), 'textbox')
+         self.assertEqual(panel.verifier.get_property('localName'), 'textbox')
+ 
+         self.assertEqual(panel.view_certificate.get_property('localName'), 'button')
+-        self.assertEqual(panel.view_cookies.get_property('localName'), 'button')
+         self.assertEqual(panel.view_passwords.get_property('localName'), 'button')
+ 
+     def test_select(self):
+         """Test properties and methods for switching between panels."""
+         page_info = self.browser.open_page_info_window()
+         deck = page_info.deck
+ 
+         self.assertEqual(deck.selected_panel, deck.general)
+diff --git a/testing/marionette/puppeteer/firefox/firefox_puppeteer/ui/pageinfo/deck.py b/testing/marionette/puppeteer/firefox/firefox_puppeteer/ui/pageinfo/deck.py
+--- a/testing/marionette/puppeteer/firefox/firefox_puppeteer/ui/pageinfo/deck.py
++++ b/testing/marionette/puppeteer/firefox/firefox_puppeteer/ui/pageinfo/deck.py
+@@ -185,22 +185,14 @@ class SecurityPanel(PageInfoPanel):
+     def view_certificate(self):
+         """The DOM element which represents the view certificate button.
+ 
+         :returns: Reference to the button element.
+         """
+         return self.element.find_element(By.ID, 'security-view-cert')
+ 
+     @property
+-    def view_cookies(self):
+-        """The DOM element which represents the view cookies button.
+-
+-        :returns: Reference to the button element.
+-        """
+-        return self.element.find_element(By.ID, 'security-view-cookies')
+-
+-    @property
+     def view_passwords(self):
+         """The DOM element which represents the view passwords button.
+ 
+         :returns: Reference to the button element.
+         """
+         return self.element.find_element(By.ID, 'security-view-password')

+ 28 - 0
frg/mozilla-release/work-js/1363957-59a1.patch

@@ -0,0 +1,28 @@
+# HG changeset patch
+# User Joel Maher <jmaher@mozilla.com>
+# Date 1510593601 18000
+# Node ID c90922dbea3633db33df691918c01077dacfe19b
+# Parent  47a48cb45989a7b8d7f6a25b7e8b2d960bcac9be
+Bug 1363957 - Disable dom/animation/test/mozilla/test_deferred_start.html on android/debug and win10 for frequent failures. r=me, a=test-only
+
+diff --git a/dom/animation/test/mochitest.ini b/dom/animation/test/mochitest.ini
+--- a/dom/animation/test/mochitest.ini
++++ b/dom/animation/test/mochitest.ini
+@@ -99,16 +99,17 @@ support-files =
+ [css-transitions/test_keyframeeffect-getkeyframes.html]
+ [css-transitions/test_pseudoElement-get-animations.html]
+ [css-transitions/test_setting-effect.html]
+ [document-timeline/test_document-timeline.html]
+ [document-timeline/test_request_animation_frame.html]
+ [mozilla/test_cascade.html]
+ [mozilla/test_cubic_bezier_limits.html]
+ [mozilla/test_deferred_start.html]
++skip-if = (toolkit == 'android' && debug) || (os == 'win' && bits == 64) # Bug 1363957
+ [mozilla/test_disable_animations_api_core.html]
+ [mozilla/test_disabled_properties.html]
+ [mozilla/test_discrete-animations.html]
+ [mozilla/test_distance_of_basic_shape.html]
+ [mozilla/test_distance_of_filter.html]
+ [mozilla/test_distance_of_transform.html]
+ [mozilla/test_document-timeline-origin-time-range.html]
+ [mozilla/test_hide_and_show.html]

+ 1605 - 0
frg/mozilla-release/work-js/1370881-60a1.patch

@@ -0,0 +1,1605 @@
+# HG changeset patch
+# User hemant <hemantsingh1612@gmail.com>
+# Date 1516807737 -19800
+# Node ID 8bfa9cafcc0dc85df6197014dad2c5a3643760e1
+# Parent  d56f8c38e62812422ed3a4616ce9909f522fcd50
+Bug 1370881 - Replace calls to asyncHistory.isURIVisited and promiseIsURIVisited with PlacesUtils.history.hasVisits r=standard8
+
+MozReview-Commit-ID: BHWkJpdYA0g
+
+diff --git a/browser/base/content/test/general/browser_sanitize-timespans.js b/browser/base/content/test/general/browser_sanitize-timespans.js
+--- a/browser/base/content/test/general/browser_sanitize-timespans.js
++++ b/browser/base/content/test/general/browser_sanitize-timespans.js
+@@ -100,35 +100,35 @@ async function onHistoryReady() {
+   // Clear 10 minutes ago
+   s.range = [now_uSec - 10 * 60 * 1000000, now_uSec];
+   await s.sanitize();
+   s.range = null;
+ 
+   await formHistoryPromise;
+   await downloadPromise;
+ 
+-  ok(!(await promiseIsURIVisited(makeURI("http://10minutes.com"))),
++  ok(!(await PlacesUtils.history.hasVisits("http://10minutes.com")),
+      "Pretend visit to 10minutes.com should now be deleted");
+-  ok((await promiseIsURIVisited(makeURI("http://1hour.com"))),
++  ok((await PlacesUtils.history.hasVisits("http://1hour.com")),
+      "Pretend visit to 1hour.com should should still exist");
+-  ok((await promiseIsURIVisited(makeURI("http://1hour10minutes.com"))),
++  ok((await PlacesUtils.history.hasVisits("http://1hour10minutes.com")),
+      "Pretend visit to 1hour10minutes.com should should still exist");
+-  ok((await promiseIsURIVisited(makeURI("http://2hour.com"))),
++  ok((await PlacesUtils.history.hasVisits("http://2hour.com")),
+      "Pretend visit to 2hour.com should should still exist");
+-  ok((await promiseIsURIVisited(makeURI("http://2hour10minutes.com"))),
++  ok((await PlacesUtils.history.hasVisits("http://2hour10minutes.com")),
+      "Pretend visit to 2hour10minutes.com should should still exist");
+-  ok((await promiseIsURIVisited(makeURI("http://4hour.com"))),
++  ok((await PlacesUtils.history.hasVisits("http://4hour.com")),
+      "Pretend visit to 4hour.com should should still exist");
+-  ok((await promiseIsURIVisited(makeURI("http://4hour10minutes.com"))),
++  ok((await PlacesUtils.history.hasVisits("http://4hour10minutes.com")),
+      "Pretend visit to 4hour10minutes.com should should still exist");
+   if (minutesSinceMidnight > 10) {
+-    ok((await promiseIsURIVisited(makeURI("http://today.com"))),
++    ok((await PlacesUtils.history.hasVisits("http://today.com")),
+        "Pretend visit to today.com should still exist");
+   }
+-  ok((await promiseIsURIVisited(makeURI("http://before-today.com"))),
++  ok((await PlacesUtils.history.hasVisits("http://before-today.com")),
+     "Pretend visit to before-today.com should still exist");
+ 
+   let checkZero = function(num, message) { is(num, 0, message); };
+   let checkOne = function(num, message) { is(num, 1, message); };
+ 
+   await countEntries("10minutes", "10minutes form entry should be deleted", checkZero);
+   await countEntries("1hour", "1hour form entry should still exist", checkOne);
+   await countEntries("1hour10minutes", "1hour10minutes form entry should still exist", checkOne);
+@@ -157,33 +157,33 @@ async function onHistoryReady() {
+ 
+   // Clear 1 hour
+   Sanitizer.prefs.setIntPref("timeSpan", 1);
+   await s.sanitize();
+ 
+   await formHistoryPromise;
+   await downloadPromise;
+ 
+-  ok(!(await promiseIsURIVisited(makeURI("http://1hour.com"))),
++  ok(!(await PlacesUtils.history.hasVisits("http://1hour.com")),
+      "Pretend visit to 1hour.com should now be deleted");
+-  ok((await promiseIsURIVisited(makeURI("http://1hour10minutes.com"))),
++  ok((await PlacesUtils.history.hasVisits("http://1hour10minutes.com")),
+      "Pretend visit to 1hour10minutes.com should should still exist");
+-  ok((await promiseIsURIVisited(makeURI("http://2hour.com"))),
++  ok((await PlacesUtils.history.hasVisits("http://2hour.com")),
+      "Pretend visit to 2hour.com should should still exist");
+-  ok((await promiseIsURIVisited(makeURI("http://2hour10minutes.com"))),
++  ok((await PlacesUtils.history.hasVisits("http://2hour10minutes.com")),
+      "Pretend visit to 2hour10minutes.com should should still exist");
+-  ok((await promiseIsURIVisited(makeURI("http://4hour.com"))),
++  ok((await PlacesUtils.history.hasVisits("http://4hour.com")),
+      "Pretend visit to 4hour.com should should still exist");
+-  ok((await promiseIsURIVisited(makeURI("http://4hour10minutes.com"))),
++  ok((await PlacesUtils.history.hasVisits("http://4hour10minutes.com")),
+      "Pretend visit to 4hour10minutes.com should should still exist");
+   if (hoursSinceMidnight > 1) {
+-    ok((await promiseIsURIVisited(makeURI("http://today.com"))),
++    ok((await PlacesUtils.history.hasVisits("http://today.com")),
+        "Pretend visit to today.com should still exist");
+   }
+-  ok((await promiseIsURIVisited(makeURI("http://before-today.com"))),
++  ok((await PlacesUtils.history.hasVisits("http://before-today.com")),
+     "Pretend visit to before-today.com should still exist");
+ 
+   await countEntries("1hour", "1hour form entry should be deleted", checkZero);
+   await countEntries("1hour10minutes", "1hour10minutes form entry should still exist", checkOne);
+   await countEntries("2hour", "2hour form entry should still exist", checkOne);
+   await countEntries("2hour10minutes", "2hour10minutes form entry should still exist", checkOne);
+   await countEntries("4hour", "4hour form entry should still exist", checkOne);
+   await countEntries("4hour10minutes", "4hour10minutes form entry should still exist", checkOne);
+@@ -208,31 +208,31 @@ async function onHistoryReady() {
+   // Clear 1 hour 10 minutes
+   s.range = [now_uSec - 70 * 60 * 1000000, now_uSec];
+   await s.sanitize();
+   s.range = null;
+ 
+   await formHistoryPromise;
+   await downloadPromise;
+ 
+-  ok(!(await promiseIsURIVisited(makeURI("http://1hour10minutes.com"))),
++  ok(!(await PlacesUtils.history.hasVisits("http://1hour10minutes.com")),
+      "Pretend visit to 1hour10minutes.com should now be deleted");
+-  ok((await promiseIsURIVisited(makeURI("http://2hour.com"))),
++  ok((await PlacesUtils.history.hasVisits("http://2hour.com")),
+      "Pretend visit to 2hour.com should should still exist");
+-  ok((await promiseIsURIVisited(makeURI("http://2hour10minutes.com"))),
++  ok((await PlacesUtils.history.hasVisits("http://2hour10minutes.com")),
+      "Pretend visit to 2hour10minutes.com should should still exist");
+-  ok((await promiseIsURIVisited(makeURI("http://4hour.com"))),
++  ok((await PlacesUtils.history.hasVisits("http://4hour.com")),
+      "Pretend visit to 4hour.com should should still exist");
+-  ok((await promiseIsURIVisited(makeURI("http://4hour10minutes.com"))),
++  ok((await PlacesUtils.history.hasVisits("http://4hour10minutes.com")),
+      "Pretend visit to 4hour10minutes.com should should still exist");
+   if (minutesSinceMidnight > 70) {
+-    ok((await promiseIsURIVisited(makeURI("http://today.com"))),
++    ok((await PlacesUtils.history.hasVisits("http://today.com")),
+        "Pretend visit to today.com should still exist");
+   }
+-  ok((await promiseIsURIVisited(makeURI("http://before-today.com"))),
++  ok((await PlacesUtils.history.hasVisits("http://before-today.com")),
+     "Pretend visit to before-today.com should still exist");
+ 
+   await countEntries("1hour10minutes", "1hour10minutes form entry should be deleted", checkZero);
+   await countEntries("2hour", "2hour form entry should still exist", checkOne);
+   await countEntries("2hour10minutes", "2hour10minutes form entry should still exist", checkOne);
+   await countEntries("4hour", "4hour form entry should still exist", checkOne);
+   await countEntries("4hour10minutes", "4hour10minutes form entry should still exist", checkOne);
+   if (minutesSinceMidnight > 70)
+@@ -253,29 +253,29 @@ async function onHistoryReady() {
+ 
+   // Clear 2 hours
+   Sanitizer.prefs.setIntPref("timeSpan", 2);
+   await s.sanitize();
+ 
+   await formHistoryPromise;
+   await downloadPromise;
+ 
+-  ok(!(await promiseIsURIVisited(makeURI("http://2hour.com"))),
++  ok(!(await PlacesUtils.history.hasVisits("http://2hour.com")),
+      "Pretend visit to 2hour.com should now be deleted");
+-  ok((await promiseIsURIVisited(makeURI("http://2hour10minutes.com"))),
++  ok((await PlacesUtils.history.hasVisits("http://2hour10minutes.com")),
+      "Pretend visit to 2hour10minutes.com should should still exist");
+-  ok((await promiseIsURIVisited(makeURI("http://4hour.com"))),
++  ok((await PlacesUtils.history.hasVisits("http://4hour.com")),
+      "Pretend visit to 4hour.com should should still exist");
+-  ok((await promiseIsURIVisited(makeURI("http://4hour10minutes.com"))),
++  ok((await PlacesUtils.history.hasVisits("http://4hour10minutes.com")),
+      "Pretend visit to 4hour10minutes.com should should still exist");
+   if (hoursSinceMidnight > 2) {
+-    ok((await promiseIsURIVisited(makeURI("http://today.com"))),
++    ok((await PlacesUtils.history.hasVisits("http://today.com")),
+        "Pretend visit to today.com should still exist");
+   }
+-  ok((await promiseIsURIVisited(makeURI("http://before-today.com"))),
++  ok((await PlacesUtils.history.hasVisits("http://before-today.com")),
+     "Pretend visit to before-today.com should still exist");
+ 
+   await countEntries("2hour", "2hour form entry should be deleted", checkZero);
+   await countEntries("2hour10minutes", "2hour10minutes form entry should still exist", checkOne);
+   await countEntries("4hour", "4hour form entry should still exist", checkOne);
+   await countEntries("4hour10minutes", "4hour10minutes form entry should still exist", checkOne);
+   if (hoursSinceMidnight > 2)
+     await countEntries("today", "today form entry should still exist", checkOne);
+@@ -295,27 +295,27 @@ async function onHistoryReady() {
+   // Clear 2 hours 10 minutes
+   s.range = [now_uSec - 130 * 60 * 1000000, now_uSec];
+   await s.sanitize();
+   s.range = null;
+ 
+   await formHistoryPromise;
+   await downloadPromise;
+ 
+-  ok(!(await promiseIsURIVisited(makeURI("http://2hour10minutes.com"))),
++  ok(!(await PlacesUtils.history.hasVisits("http://2hour10minutes.com")),
+      "Pretend visit to 2hour10minutes.com should now be deleted");
+-  ok((await promiseIsURIVisited(makeURI("http://4hour.com"))),
++  ok((await PlacesUtils.history.hasVisits("http://4hour.com")),
+      "Pretend visit to 4hour.com should should still exist");
+-  ok((await promiseIsURIVisited(makeURI("http://4hour10minutes.com"))),
++  ok((await PlacesUtils.history.hasVisits("http://4hour10minutes.com")),
+      "Pretend visit to 4hour10minutes.com should should still exist");
+   if (minutesSinceMidnight > 130) {
+-    ok((await promiseIsURIVisited(makeURI("http://today.com"))),
++    ok((await PlacesUtils.history.hasVisits("http://today.com")),
+        "Pretend visit to today.com should still exist");
+   }
+-  ok((await promiseIsURIVisited(makeURI("http://before-today.com"))),
++  ok((await PlacesUtils.history.hasVisits("http://before-today.com")),
+     "Pretend visit to before-today.com should still exist");
+ 
+   await countEntries("2hour10minutes", "2hour10minutes form entry should be deleted", checkZero);
+   await countEntries("4hour", "4hour form entry should still exist", checkOne);
+   await countEntries("4hour10minutes", "4hour10minutes form entry should still exist", checkOne);
+   if (minutesSinceMidnight > 130)
+     await countEntries("today", "today form entry should still exist", checkOne);
+   await countEntries("b4today", "b4today form entry should still exist", checkOne);
+@@ -332,25 +332,25 @@ async function onHistoryReady() {
+ 
+   // Clear 4 hours
+   Sanitizer.prefs.setIntPref("timeSpan", 3);
+   await s.sanitize();
+ 
+   await formHistoryPromise;
+   await downloadPromise;
+ 
+-  ok(!(await promiseIsURIVisited(makeURI("http://4hour.com"))),
++  ok(!(await PlacesUtils.history.hasVisits("http://4hour.com")),
+      "Pretend visit to 4hour.com should now be deleted");
+-  ok((await promiseIsURIVisited(makeURI("http://4hour10minutes.com"))),
++  ok((await PlacesUtils.history.hasVisits("http://4hour10minutes.com")),
+      "Pretend visit to 4hour10minutes.com should should still exist");
+   if (hoursSinceMidnight > 4) {
+-    ok((await promiseIsURIVisited(makeURI("http://today.com"))),
++    ok((await PlacesUtils.history.hasVisits("http://today.com")),
+        "Pretend visit to today.com should still exist");
+   }
+-  ok((await promiseIsURIVisited(makeURI("http://before-today.com"))),
++  ok((await PlacesUtils.history.hasVisits("http://before-today.com")),
+     "Pretend visit to before-today.com should still exist");
+ 
+   await countEntries("4hour", "4hour form entry should be deleted", checkZero);
+   await countEntries("4hour10minutes", "4hour10minutes form entry should still exist", checkOne);
+   if (hoursSinceMidnight > 4)
+     await countEntries("today", "today form entry should still exist", checkOne);
+   await countEntries("b4today", "b4today form entry should still exist", checkOne);
+ 
+@@ -366,23 +366,23 @@ async function onHistoryReady() {
+   // Clear 4 hours 10 minutes
+   s.range = [now_uSec - 250 * 60 * 1000000, now_uSec];
+   await s.sanitize();
+   s.range = null;
+ 
+   await formHistoryPromise;
+   await downloadPromise;
+ 
+-  ok(!(await promiseIsURIVisited(makeURI("http://4hour10minutes.com"))),
++  ok(!(await PlacesUtils.history.hasVisits("http://4hour10minutes.com")),
+      "Pretend visit to 4hour10minutes.com should now be deleted");
+   if (minutesSinceMidnight > 250) {
+-    ok((await promiseIsURIVisited(makeURI("http://today.com"))),
++    ok((await PlacesUtils.history.hasVisits("http://today.com")),
+        "Pretend visit to today.com should still exist");
+   }
+-  ok((await promiseIsURIVisited(makeURI("http://before-today.com"))),
++  ok((await PlacesUtils.history.hasVisits("http://before-today.com")),
+     "Pretend visit to before-today.com should still exist");
+ 
+   await countEntries("4hour10minutes", "4hour10minutes form entry should be deleted", checkZero);
+   if (minutesSinceMidnight > 250)
+     await countEntries("today", "today form entry should still exist", checkOne);
+   await countEntries("b4today", "b4today form entry should still exist", checkOne);
+ 
+   ok(!(await downloadExists(publicList, "fakefile-4-hour-10-minutes")), "4 hour 10 minute download should now be deleted");
+@@ -408,39 +408,39 @@ async function onHistoryReady() {
+ 
+   // Be careful.  If we add our objectss just before midnight, and sanitize
+   // runs immediately after, they won't be expired.  This is expected, but
+   // we should not test in that case.  We cannot just test for opposite
+   // condition because we could cross midnight just one moment after we
+   // cache our time, then we would have an even worse random failure.
+   var today = isToday(new Date(now_mSec));
+   if (today) {
+-    ok(!(await promiseIsURIVisited(makeURI("http://today.com"))),
++    ok(!(await PlacesUtils.history.hasVisits("http://today.com")),
+        "Pretend visit to today.com should now be deleted");
+ 
+     await countEntries("today", "today form entry should be deleted", checkZero);
+     ok(!(await downloadExists(publicList, "fakefile-today")), "'Today' download should now be deleted");
+   }
+ 
+-  ok((await promiseIsURIVisited(makeURI("http://before-today.com"))),
++  ok((await PlacesUtils.history.hasVisits("http://before-today.com")),
+      "Pretend visit to before-today.com should still exist");
+   await countEntries("b4today", "b4today form entry should still exist", checkOne);
+   ok((await downloadExists(publicList, "fakefile-old")), "Year old download should still be present");
+ 
+   downloadPromise = promiseDownloadRemoved(publicList);
+   formHistoryPromise = promiseFormHistoryRemoved();
+ 
+   // Choose everything
+   Sanitizer.prefs.setIntPref("timeSpan", 0);
+   await s.sanitize();
+ 
+   await formHistoryPromise;
+   await downloadPromise;
+ 
+-  ok(!(await promiseIsURIVisited(makeURI("http://before-today.com"))),
++  ok(!(await PlacesUtils.history.hasVisits("http://before-today.com")),
+      "Pretend visit to before-today.com should now be deleted");
+ 
+   await countEntries("b4today", "b4today form entry should be deleted", checkZero);
+ 
+   ok(!(await downloadExists(publicList, "fakefile-old")), "Year old download should now be deleted");
+ }
+ 
+ function setupHistory() {
+diff --git a/browser/base/content/test/general/head.js b/browser/base/content/test/general/head.js
+--- a/browser/base/content/test/general/head.js
++++ b/browser/base/content/test/general/head.js
+@@ -242,34 +242,16 @@ function waitForAsyncUpdates(aCallback, 
+     handleError() {},
+     handleCompletion(aReason) {
+       aCallback.apply(scope, args);
+     }
+   });
+   commit.finalize();
+ }
+ 
+-/**
+- * Asynchronously check a url is visited.
+-
+- * @param aURI The URI.
+- * @param aExpectedValue The expected value.
+- * @return {Promise}
+- * @resolves When the check has been added successfully.
+- * @rejects JavaScript exception.
+- */
+-function promiseIsURIVisited(aURI, aExpectedValue) {
+-  return new Promise(resolve => {
+-    PlacesUtils.asyncHistory.isURIVisited(aURI, function(unused, aIsVisited) {
+-      resolve(aIsVisited);
+-    });
+-
+-  });
+-}
+-
+ function whenNewTabLoaded(aWindow, aCallback) {
+   aWindow.BrowserOpenTab();
+ 
+   let browser = aWindow.gBrowser.selectedBrowser;
+   if (browser.contentDocument.readyState === "complete") {
+     aCallback();
+     return;
+   }
+@@ -305,17 +287,16 @@ function promiseHistoryClearedState(aURI
+     }
+     aURIs.forEach(function(aURI) {
+       PlacesUtils.asyncHistory.isURIVisited(aURI, function(uri, isVisited) {
+         is(isVisited, !aShouldBeCleared,
+            "history visit " + uri.spec + " should " + niceStr + " exist");
+         callbackDone();
+       });
+     });
+-
+   });
+ }
+ 
+ var FullZoomHelper = {
+ 
+   selectTabAndWaitForLocationChange: function selectTabAndWaitForLocationChange(tab) {
+     if (!tab)
+       throw new Error("tab must be given.");
+diff --git a/browser/components/places/tests/browser/browser_library_commands.js b/browser/components/places/tests/browser/browser_library_commands.js
+--- a/browser/components/places/tests/browser/browser_library_commands.js
++++ b/browser/components/places/tests/browser/browser_library_commands.js
+@@ -64,17 +64,17 @@ add_task(async function test_date_contai
+   PO._places.controller.doCommand("cmd_delete");
+   await promiseURIRemoved;
+ 
+   // Test live update of "History" query.
+   is(historyNode.childCount, 0, "History node has no more children");
+ 
+   historyNode.containerOpen = false;
+ 
+-  ok(!(await promiseIsURIVisited(TEST_URI)), "Visit has been removed");
++  ok(!(await PlacesUtils.history.hasVisits(TEST_URI)), "Visit has been removed");
+ 
+   library.close();
+ });
+ 
+ add_task(async function test_query_on_toolbar() {
+   let library = await promiseLibrary();
+   info("Ensure queries can be cut or deleted");
+ 
+diff --git a/browser/components/places/tests/browser/head.js b/browser/components/places/tests/browser/head.js
+--- a/browser/components/places/tests/browser/head.js
++++ b/browser/components/places/tests/browser/head.js
+@@ -139,23 +139,17 @@ function synthesizeClickOnSelectedTreeCe
+  * Asynchronously check a url is visited.
+  *
+  * @param aURI The URI.
+  * @return {Promise}
+  * @resolves When the check has been added successfully.
+  * @rejects JavaScript exception.
+  */
+ function promiseIsURIVisited(aURI) {
+-  return new Promise(resolve => {
+-
+-    PlacesUtils.asyncHistory.isURIVisited(aURI, function(unused, aIsVisited) {
+-      resolve(aIsVisited);
+-    });
+-
+-  });
++  return PlacesUtils.history.hasVisits(aURI);
+ }
+ 
+ function promiseBookmarksNotification(notification, conditionFn) {
+   info(`promiseBookmarksNotification: waiting for ${notification}`);
+   return new Promise((resolve) => {
+     let proxifiedObserver = new Proxy({}, {
+       get: (target, name) => {
+         if (name == "QueryInterface")
+diff --git a/services/sync/tests/unit/test_corrupt_keys.js b/services/sync/tests/unit/test_corrupt_keys.js
+--- a/services/sync/tests/unit/test_corrupt_keys.js
++++ b/services/sync/tests/unit/test_corrupt_keys.js
+@@ -126,21 +126,21 @@ add_task(async function test_locally_cha
+     // Now syncing should succeed, after one HMAC error.
+     let ping = await wait_for_ping(() => Service.sync(), true);
+     equal(ping.engines.find(e => e.name == "history").incoming.applied, 5);
+ 
+     Assert.equal(hmacErrorCount, 1);
+     _("Keys now: " + Service.collectionKeys.keyForCollection("history").keyPair);
+ 
+     // And look! We downloaded history!
+-    Assert.ok(await promiseIsURIVisited("http://foo/bar?record-no--0"));
+-    Assert.ok(await promiseIsURIVisited("http://foo/bar?record-no--1"));
+-    Assert.ok(await promiseIsURIVisited("http://foo/bar?record-no--2"));
+-    Assert.ok(await promiseIsURIVisited("http://foo/bar?record-no--3"));
+-    Assert.ok(await promiseIsURIVisited("http://foo/bar?record-no--4"));
++    Assert.ok(await PlacesUtils.history.hasVisits("http://foo/bar?record-no--0"));
++    Assert.ok(await PlacesUtils.history.hasVisits("http://foo/bar?record-no--1"));
++    Assert.ok(await PlacesUtils.history.hasVisits("http://foo/bar?record-no--2"));
++    Assert.ok(await PlacesUtils.history.hasVisits("http://foo/bar?record-no--3"));
++    Assert.ok(await PlacesUtils.history.hasVisits("http://foo/bar?record-no--4"));
+     Assert.equal(hmacErrorCount, 1);
+ 
+     _("Busting some new server values.");
+     // Now what happens if we corrupt the HMAC on the server?
+     for (let i = 5; i < 10; i++) {
+       let id = "record-no--" + i;
+       let modified = 1 + (Date.now() / 1000);
+ 
+@@ -171,40 +171,25 @@ add_task(async function test_locally_cha
+     _("Syncing...");
+     ping = await sync_and_validate_telem(true);
+ 
+     Assert.equal(ping.engines.find(e => e.name == "history").incoming.failed, 5);
+     _("Keys now: " + Service.collectionKeys.keyForCollection("history").keyPair);
+     _("Server keys have been updated, and we skipped over 5 more HMAC errors without adjusting history.");
+     Assert.ok(johndoe.modified("crypto") > old_key_time);
+     Assert.equal(hmacErrorCount, 6);
+-    Assert.equal(false, await promiseIsURIVisited("http://foo/bar?record-no--5"));
+-    Assert.equal(false, await promiseIsURIVisited("http://foo/bar?record-no--6"));
+-    Assert.equal(false, await promiseIsURIVisited("http://foo/bar?record-no--7"));
+-    Assert.equal(false, await promiseIsURIVisited("http://foo/bar?record-no--8"));
+-    Assert.equal(false, await promiseIsURIVisited("http://foo/bar?record-no--9"));
++    Assert.equal(false, await PlacesUtils.history.hasVisits("http://foo/bar?record-no--5"));
++    Assert.equal(false, await PlacesUtils.history.hasVisits("http://foo/bar?record-no--6"));
++    Assert.equal(false, await PlacesUtils.history.hasVisits("http://foo/bar?record-no--7"));
++    Assert.equal(false, await PlacesUtils.history.hasVisits("http://foo/bar?record-no--8"));
++    Assert.equal(false, await PlacesUtils.history.hasVisits("http://foo/bar?record-no--9"));
+   } finally {
+     Svc.Prefs.resetBranch("");
+     await promiseStopServer(server);
+   }
+ });
+ 
+ function run_test() {
+   Log.repository.rootLogger.addAppender(new Log.DumpAppender());
+   validate_all_future_pings();
+ 
+   run_next_test();
+ }
+-
+-/**
+- * Asynchronously check a url is visited.
+- * @param url the url
+- * @return {Promise}
+- * @resolves When the check has been added successfully.
+- * @rejects JavaScript exception.
+- */
+-function promiseIsURIVisited(url) {
+-  return new Promise(resolve => {
+-    PlacesUtils.asyncHistory.isURIVisited(Utils.makeURI(url), function(aURI, aIsVisited) {
+-      resolve(aIsVisited);
+-    });
+-  });
+-}
+diff --git a/toolkit/components/jsdownloads/test/unit/common_test_Download.js b/toolkit/components/jsdownloads/test/unit/common_test_Download.js
+--- a/toolkit/components/jsdownloads/test/unit/common_test_Download.js
++++ b/toolkit/components/jsdownloads/test/unit/common_test_Download.js
+@@ -2405,17 +2405,17 @@ add_task(async function test_history() {
+ 
+   // Restart and complete the download after clearing history.
+   await PlacesUtils.history.clear();
+   download.cancel();
+   continueResponses();
+   await download.start();
+ 
+   // The restart should not have added a new history visit.
+-  Assert.equal(false, await promiseIsURIVisited(httpUrl("interruptible.txt")));
++  Assert.equal(false, await PlacesUtils.history.hasVisits(httpUrl("interruptible.txt")));
+ });
+ 
+ /**
+  * Checks that downloads started by nsIHelperAppService are added to the
+  * browsing history when they start.
+  */
+ add_task(async function test_history_tryToKeepPartialData() {
+   // We will wait for the visit to be notified during the download.
+diff --git a/toolkit/components/jsdownloads/test/unit/head.js b/toolkit/components/jsdownloads/test/unit/head.js
+--- a/toolkit/components/jsdownloads/test/unit/head.js
++++ b/toolkit/components/jsdownloads/test/unit/head.js
+@@ -166,37 +166,16 @@ function promiseWaitForVisit(aUrl) {
+       onPageChanged() {},
+       onDeleteVisits() {},
+     });
+ 
+   });
+ }
+ 
+ /**
+- * Check browsing history to see whether the given URI has been visited.
+- *
+- * @param aUrl
+- *        String containing the URI that will be visited.
+- *
+- * @return {Promise}
+- * @resolves Boolean indicating whether the URI has been visited.
+- * @rejects JavaScript exception.
+- */
+-function promiseIsURIVisited(aUrl) {
+-  return new Promise(resolve => {
+-
+-    PlacesUtils.asyncHistory.isURIVisited(NetUtil.newURI(aUrl),
+-      function(aURI, aIsVisited) {
+-        resolve(aIsVisited);
+-      });
+-
+-  });
+-}
+-
+-/**
+  * Creates a new Download object, setting a temporary file as the target.
+  *
+  * @param aSourceUrl
+  *        String containing the URI for the download source, or null to use
+  *        httpUrl("source.txt").
+  *
+  * @return {Promise}
+  * @resolves The newly created Download object.
+diff --git a/toolkit/components/places/nsLivemarkService.js b/toolkit/components/places/nsLivemarkService.js
+--- a/toolkit/components/places/nsLivemarkService.js
++++ b/toolkit/components/places/nsLivemarkService.js
+@@ -8,20 +8,20 @@ ChromeUtils.import("resource://gre/modul
+ ChromeUtils.import("resource://gre/modules/Services.jsm");
+ ChromeUtils.defineModuleGetter(this, "PlacesUtils",
+                                "resource://gre/modules/PlacesUtils.jsm");
+ ChromeUtils.defineModuleGetter(this, "NetUtil",
+                                "resource://gre/modules/NetUtil.jsm");
+ ChromeUtils.defineModuleGetter(this, "Deprecated",
+                                "resource://gre/modules/Deprecated.jsm");
+ 
+-XPCOMUtils.defineLazyGetter(this, "asyncHistory", function() {
++XPCOMUtils.defineLazyGetter(this, "history", function() {
+   // Lazily add an history observer when it's actually needed.
+   PlacesUtils.history.addObserver(PlacesUtils.livemarks, true);
+-  return PlacesUtils.asyncHistory;
++  return PlacesUtils.history;
+ });
+ 
+ // Constants
+ 
+ // Delay between reloads of consecute livemarks.
+ const RELOAD_DELAY_MS = 500;
+ // Expire livemarks after this time.
+ const EXPIRE_TIME_MS = 3600000; // 1 hour.
+@@ -556,18 +556,18 @@ Livemark.prototype = {
+ 
+     // Discard the previous cached nodes, new ones should be generated.
+     for (let container of this._resultObservers.keys()) {
+       this._nodes.delete(container);
+     }
+ 
+     // Update visited status for each entry.
+     for (let child of this._children) {
+-      asyncHistory.isURIVisited(child.uri, (aURI, aIsVisited) => {
+-        this.updateURIVisitedStatus(aURI, aIsVisited);
++      history.hasVisits(child.uri, isVisited => {
++        this.updateURIVisitedStatus(child.uri, isVisited);
+       });
+     }
+ 
+     return this._children;
+   },
+ 
+   _isURIVisited(aURI) {
+     return this.children.some(child => child.uri.equals(aURI) && child.visited);
+diff --git a/toolkit/components/places/tests/browser/browser_bug680727.js b/toolkit/components/places/tests/browser/browser_bug680727.js
+--- a/toolkit/components/places/tests/browser/browser_bug680727.js
++++ b/toolkit/components/places/tests/browser/browser_bug680727.js
+@@ -2,19 +2,16 @@
+    http://creativecommons.org/publicdomain/zero/1.0/ */
+ 
+ /* Ensure that clicking the button in the Offline mode neterror page updates
+    global history. See bug 680727. */
+ /* TEST_PATH=toolkit/components/places/tests/browser/browser_bug680727.js make -C $(OBJDIR) mochitest-browser-chrome */
+ 
+ 
+ const kUniqueURI = Services.io.newURI("http://mochi.test:8888/#bug_680727");
+-var gAsyncHistory =
+-  Cc["@mozilla.org/browser/history;1"].getService(Ci.mozIAsyncHistory);
+-
+ var proxyPrefValue;
+ var ourTab;
+ 
+ function test() {
+   waitForExplicitFinish();
+ 
+   // Tests always connect to localhost, and per bug 87717, localhost is now
+   // reachable in offline mode.  To avoid this, disable any proxy.
+@@ -44,18 +41,20 @@ function errorListener() {
+   ContentTask.spawn(ourTab.linkedBrowser, kUniqueURI.spec, function(uri) {
+     Assert.equal(content.document.documentURI.substring(0, 27),
+       "about:neterror?e=netOffline", "Document URI is the error page.");
+ 
+     // But location bar should show the original request.
+     Assert.equal(content.location.href, uri, "Docshell URI is the original URI.");
+   }).then(() => {
+     // Global history does not record URI of a failed request.
+-    return PlacesTestUtils.promiseAsyncUpdates().then(() => {
+-      gAsyncHistory.isURIVisited(kUniqueURI, errorAsyncListener);
++    PlacesTestUtils.promiseAsyncUpdates().then(() => {
++      PlacesUtils.history.hasVisits(kUniqueURI).then(isVisited => {
++        errorAsyncListener(kUniqueURI, isVisited);
++      });
+     });
+   });
+ }
+ 
+ function errorAsyncListener(aURI, aIsVisited) {
+   ok(kUniqueURI.equals(aURI) && !aIsVisited,
+      "The neterror page is not listed in global history.");
+ 
+@@ -84,17 +83,19 @@ function reloadListener() {
+ 
+   ContentTask.spawn(ourTab.linkedBrowser, kUniqueURI.spec, function(uri) {
+     // This is not an error page.
+     Assert.equal(content.document.documentURI, uri,
+       "Document URI is not the offline-error page, but the original URI.");
+   }).then(() => {
+     // Check if global history remembers the successfully-requested URI.
+     PlacesTestUtils.promiseAsyncUpdates().then(() => {
+-      gAsyncHistory.isURIVisited(kUniqueURI, reloadAsyncListener);
++      PlacesUtils.history.hasVisits(kUniqueURI).then(isVisited => {
++          reloadAsyncListener(kUniqueURI, isVisited);
++      });
+     });
+   });
+ }
+ 
+ function reloadAsyncListener(aURI, aIsVisited) {
+   ok(kUniqueURI.equals(aURI) && aIsVisited, "We have visited the URI.");
+   PlacesTestUtils.clearHistory().then(finish);
+ }
+diff --git a/toolkit/components/places/tests/browser/browser_history_post.js b/toolkit/components/places/tests/browser/browser_history_post.js
+--- a/toolkit/components/places/tests/browser/browser_history_post.js
++++ b/toolkit/components/places/tests/browser/browser_history_post.js
+@@ -10,13 +10,13 @@ add_task(async function() {
+       let p = new Promise((resolve, reject) => {
+         iframe.addEventListener("load", function() {
+           resolve();
+         }, {once: true});
+       });
+       submit.click();
+       await p;
+     });
+-    let visited = await promiseIsURIVisited(SJS_URI);
++    let visited = await PlacesUtils.history.hasVisits(SJS_URI);
+     ok(!visited, "The POST page should not be added to history");
+     ok(!(await PlacesTestUtils.isPageInDB(SJS_URI.spec)), "The page should not be in the database");
+   });
+ });
+diff --git a/toolkit/components/places/tests/browser/head.js b/toolkit/components/places/tests/browser/head.js
+--- a/toolkit/components/places/tests/browser/head.js
++++ b/toolkit/components/places/tests/browser/head.js
+@@ -290,33 +290,16 @@ function DBConn(aForceNewConnection) {
+   return gDBConn.connectionReady ? gDBConn : null;
+ }
+ 
+ function whenNewWindowLoaded(aOptions, aCallback) {
+   BrowserTestUtils.waitForNewWindow().then(aCallback);
+   OpenBrowserWindow(aOptions);
+ }
+ 
+-/**
+- * Asynchronously check a url is visited.
+- *
+- * @param aURI The URI.
+- * @param aExpectedValue The expected value.
+- * @return {Promise}
+- * @resolves When the check has been added successfully.
+- * @rejects JavaScript exception.
+- */
+-function promiseIsURIVisited(aURI, aExpectedValue) {
+-  return new Promise(resolve => {
+-    PlacesUtils.asyncHistory.isURIVisited(aURI, function(unused, aIsVisited) {
+-      resolve(aIsVisited);
+-    });
+-  });
+-}
+-
+ function waitForCondition(condition, nextTest, errorMsg) {
+   let tries = 0;
+   let interval = setInterval(function() {
+     if (tries >= 30) {
+       ok(false, errorMsg);
+       moveOn();
+     }
+     let conditionPassed;
+diff --git a/toolkit/components/places/tests/head_common.js b/toolkit/components/places/tests/head_common.js
+--- a/toolkit/components/places/tests/head_common.js
++++ b/toolkit/components/places/tests/head_common.js
+@@ -798,34 +798,16 @@ NavHistoryResultObserver.prototype = {
+   nodeTitleChanged() {},
+   nodeURIChanged() {},
+   sortingChanged() {},
+   QueryInterface: XPCOMUtils.generateQI([
+     Ci.nsINavHistoryResultObserver,
+   ])
+ };
+ 
+-/**
+- * Asynchronously check a url is visited.
+- *
+- * @param aURI The URI.
+- * @return {Promise}
+- * @resolves When the check has been added successfully.
+- * @rejects JavaScript exception.
+- */
+-function promiseIsURIVisited(aURI) {
+-  return new Promise(resolve => {
+-
+-    PlacesUtils.asyncHistory.isURIVisited(aURI, function(unused, aIsVisited) {
+-      resolve(aIsVisited);
+-    });
+-
+-  });
+-}
+-
+ function checkBookmarkObject(info) {
+   do_check_valid_places_guid(info.guid);
+   do_check_valid_places_guid(info.parentGuid);
+   Assert.ok(typeof info.index == "number", "index should be a number");
+   Assert.ok(info.dateAdded.constructor.name == "Date", "dateAdded should be a Date");
+   Assert.ok(info.lastModified.constructor.name == "Date", "lastModified should be a Date");
+   Assert.ok(info.lastModified >= info.dateAdded, "lastModified should never be smaller than dateAdded");
+   Assert.ok(typeof info.type == "number", "type should be a number");
+diff --git a/toolkit/components/places/tests/history/test_async_history_api.js b/toolkit/components/places/tests/history/test_async_history_api.js
+--- a/toolkit/components/places/tests/history/test_async_history_api.js
++++ b/toolkit/components/places/tests/history/test_async_history_api.js
+@@ -365,80 +365,80 @@ add_task(async function test_non_addable
+ 
+   let placesResult = await promiseUpdatePlaces(places);
+   if (placesResult.results.length > 0) {
+     do_throw("Unexpected success.");
+   }
+   for (let place of placesResult.errors) {
+     info("Checking '" + place.info.uri.spec + "'");
+     Assert.equal(place.resultCode, Cr.NS_ERROR_INVALID_ARG);
+-    Assert.equal(false, await promiseIsURIVisited(place.info.uri));
++    Assert.equal(false, await PlacesUtils.history.hasVisits(place.info.uri));
+   }
+   await PlacesTestUtils.promiseAsyncUpdates();
+ });
+ 
+ add_task(async function test_duplicate_guid_errors() {
+   // This test ensures that trying to add a visit, with a guid already found in
+   // another visit, fails.
+   let place = {
+     uri: NetUtil.newURI(TEST_DOMAIN + "test_duplicate_guid_fails_first"),
+     visits: [
+       new VisitInfo(),
+     ],
+   };
+ 
+-  Assert.equal(false, await promiseIsURIVisited(place.uri));
++  Assert.equal(false, await PlacesUtils.history.hasVisits(place.uri));
+   let placesResult = await promiseUpdatePlaces(place);
+   if (placesResult.errors.length > 0) {
+     do_throw("Unexpected error.");
+   }
+   let placeInfo = placesResult.results[0];
+-  Assert.ok(await promiseIsURIVisited(placeInfo.uri));
++  Assert.ok(await PlacesUtils.history.hasVisits(placeInfo.uri));
+ 
+   let badPlace = {
+     uri: NetUtil.newURI(TEST_DOMAIN + "test_duplicate_guid_fails_second"),
+     visits: [
+       new VisitInfo(),
+     ],
+     guid: placeInfo.guid,
+   };
+ 
+-  Assert.equal(false, await promiseIsURIVisited(badPlace.uri));
++  Assert.equal(false, await PlacesUtils.history.hasVisits(badPlace.uri));
+   placesResult = await promiseUpdatePlaces(badPlace);
+   if (placesResult.results.length > 0) {
+     do_throw("Unexpected success.");
+   }
+   let badPlaceInfo = placesResult.errors[0];
+   Assert.equal(badPlaceInfo.resultCode, Cr.NS_ERROR_STORAGE_CONSTRAINT);
+-  Assert.equal(false, await promiseIsURIVisited(badPlaceInfo.info.uri));
++  Assert.equal(false, await PlacesUtils.history.hasVisits(badPlaceInfo.info.uri));
+ 
+   await PlacesTestUtils.promiseAsyncUpdates();
+ });
+ 
+ add_task(async function test_invalid_referrerURI_ignored() {
+   let place = {
+     uri: NetUtil.newURI(TEST_DOMAIN +
+                         "test_invalid_referrerURI_ignored"),
+     visits: [
+       new VisitInfo(),
+     ],
+   };
+   place.visits[0].referrerURI = NetUtil.newURI(place.uri.spec + "_unvisistedURI");
+-  Assert.equal(false, await promiseIsURIVisited(place.uri));
+-  Assert.equal(false, await promiseIsURIVisited(place.visits[0].referrerURI));
++  Assert.equal(false, await PlacesUtils.history.hasVisits(place.uri));
++  Assert.equal(false, await PlacesUtils.history.hasVisits(place.visits[0].referrerURI));
+ 
+   let placesResult = await promiseUpdatePlaces(place);
+   if (placesResult.errors.length > 0) {
+     do_throw("Unexpected error.");
+   }
+   let placeInfo = placesResult.results[0];
+-  Assert.ok(await promiseIsURIVisited(placeInfo.uri));
++  Assert.ok(await PlacesUtils.history.hasVisits(placeInfo.uri));
+ 
+   // Check to make sure we do not visit the invalid referrer.
+-  Assert.equal(false, await promiseIsURIVisited(place.visits[0].referrerURI));
++  Assert.equal(false, await PlacesUtils.history.hasVisits(place.visits[0].referrerURI));
+ 
+   // Check to make sure from_visit is zero in database.
+   let stmt = DBConn().createStatement(
+     `SELECT from_visit
+      FROM moz_historyvisits
+      WHERE id = :visit_id`
+   );
+   stmt.params.visit_id = placeInfo.visits[0].visitId;
+@@ -453,24 +453,24 @@ add_task(async function test_nonnsIURI_r
+   let place = {
+     uri: NetUtil.newURI(TEST_DOMAIN +
+                         "test_nonnsIURI_referrerURI_ignored"),
+     visits: [
+       new VisitInfo(),
+     ],
+   };
+   place.visits[0].referrerURI = place.uri.spec + "_nonnsIURI";
+-  Assert.equal(false, await promiseIsURIVisited(place.uri));
++  Assert.equal(false, await PlacesUtils.history.hasVisits(place.uri));
+ 
+   let placesResult = await promiseUpdatePlaces(place);
+   if (placesResult.errors.length > 0) {
+     do_throw("Unexpected error.");
+   }
+   let placeInfo = placesResult.results[0];
+-  Assert.ok(await promiseIsURIVisited(placeInfo.uri));
++  Assert.ok(await PlacesUtils.history.hasVisits(placeInfo.uri));
+ 
+   // Check to make sure from_visit is zero in database.
+   let stmt = DBConn().createStatement(
+     `SELECT from_visit
+      FROM moz_historyvisits
+      WHERE id = :visit_id`
+   );
+   stmt.params.visit_id = placeInfo.visits[0].visitId;
+@@ -490,43 +490,43 @@ add_task(async function test_old_referre
+     uri: NetUtil.newURI(TEST_DOMAIN + "test_old_referrer_ignored_referrer"),
+     visits: [
+       new VisitInfo(TRANSITION_LINK, oldTime),
+     ],
+   };
+ 
+   // First we must add our referrer to the history so that it is not ignored
+   // as being invalid.
+-  Assert.equal(false, await promiseIsURIVisited(referrerPlace.uri));
++  Assert.equal(false, await PlacesUtils.history.hasVisits(referrerPlace.uri));
+   let placesResult = await promiseUpdatePlaces(referrerPlace);
+   if (placesResult.errors.length > 0) {
+     do_throw("Unexpected error.");
+   }
+ 
+   // Now that the referrer is added, we can add a page with a valid
+   // referrer to determine if the recency of the referrer is taken into
+   // account.
+-  Assert.ok(await promiseIsURIVisited(referrerPlace.uri));
++  Assert.ok(await PlacesUtils.history.hasVisits(referrerPlace.uri));
+ 
+   let visitInfo = new VisitInfo();
+   visitInfo.referrerURI = referrerPlace.uri;
+   let place = {
+     uri: NetUtil.newURI(TEST_DOMAIN + "test_old_referrer_ignored_page"),
+     visits: [
+       visitInfo,
+     ],
+   };
+ 
+-  Assert.equal(false, await promiseIsURIVisited(place.uri));
++  Assert.equal(false, await PlacesUtils.history.hasVisits(place.uri));
+   placesResult = await promiseUpdatePlaces(place);
+   if (placesResult.errors.length > 0) {
+     do_throw("Unexpected error.");
+   }
+   let placeInfo = placesResult.results[0];
+-  Assert.ok(await promiseIsURIVisited(place.uri));
++  Assert.ok(await PlacesUtils.history.hasVisits(place.uri));
+ 
+   // Though the visit will not contain the referrer, we must examine the
+   // database to be sure.
+   Assert.equal(placeInfo.visits[0].referrerURI, null);
+   let stmt = DBConn().createStatement(
+     `SELECT COUNT(1) AS count
+      FROM moz_historyvisits
+      JOIN moz_places h ON h.id = place_id
+@@ -544,44 +544,44 @@ add_task(async function test_old_referre
+ add_task(async function test_place_id_ignored() {
+   let place = {
+     uri: NetUtil.newURI(TEST_DOMAIN + "test_place_id_ignored_first"),
+     visits: [
+       new VisitInfo(),
+     ],
+   };
+ 
+-  Assert.equal(false, await promiseIsURIVisited(place.uri));
++  Assert.equal(false, await PlacesUtils.history.hasVisits(place.uri));
+   let placesResult = await promiseUpdatePlaces(place);
+   if (placesResult.errors.length > 0) {
+     do_throw("Unexpected error.");
+   }
+   let placeInfo = placesResult.results[0];
+-  Assert.ok(await promiseIsURIVisited(place.uri));
++  Assert.ok(await PlacesUtils.history.hasVisits(place.uri));
+ 
+   let placeId = placeInfo.placeId;
+   Assert.notEqual(placeId, 0);
+ 
+   let badPlace = {
+     uri: NetUtil.newURI(TEST_DOMAIN + "test_place_id_ignored_second"),
+     visits: [
+       new VisitInfo(),
+     ],
+     placeId,
+   };
+ 
+-  Assert.equal(false, await promiseIsURIVisited(badPlace.uri));
++  Assert.equal(false, await PlacesUtils.history.hasVisits(badPlace.uri));
+   placesResult = await promiseUpdatePlaces(badPlace);
+   if (placesResult.errors.length > 0) {
+     do_throw("Unexpected error.");
+   }
+   placeInfo = placesResult.results[0];
+ 
+   Assert.notEqual(placeInfo.placeId, placeId);
+-  Assert.ok(await promiseIsURIVisited(badPlace.uri));
++  Assert.ok(await PlacesUtils.history.hasVisits(badPlace.uri));
+ 
+   await PlacesTestUtils.promiseAsyncUpdates();
+ });
+ 
+ add_task(async function test_handleCompletion_called_when_complete() {
+   // We test a normal visit, and embeded visit, and a uri that would fail
+   // the canAddURI test to make sure that the notification happens after *all*
+   // of them have had a callback.
+@@ -594,18 +594,18 @@ add_task(async function test_handleCompl
+       ],
+     },
+     { uri: NetUtil.newURI("data:,Hello%2C%20World!"),
+       visits: [
+         new VisitInfo(),
+       ],
+     },
+   ];
+-  Assert.equal(false, await promiseIsURIVisited(places[0].uri));
+-  Assert.equal(false, await promiseIsURIVisited(places[1].uri));
++  Assert.equal(false, await PlacesUtils.history.hasVisits(places[0].uri));
++  Assert.equal(false, await PlacesUtils.history.hasVisits(places[1].uri));
+ 
+   const EXPECTED_COUNT_SUCCESS = 2;
+   const EXPECTED_COUNT_FAILURE = 1;
+ 
+   let {results, errors} = await promiseUpdatePlaces(places);
+ 
+   Assert.equal(results.length, EXPECTED_COUNT_SUCCESS);
+   Assert.equal(errors.length, EXPECTED_COUNT_FAILURE);
+@@ -619,25 +619,25 @@ add_task(async function test_add_visit()
+     uri: NetUtil.newURI(TEST_DOMAIN + "test_add_visit"),
+     title: "test_add_visit title",
+     visits: [],
+   };
+   for (let t in PlacesUtils.history.TRANSITIONS) {
+     let transitionType = PlacesUtils.history.TRANSITIONS[t];
+     place.visits.push(new VisitInfo(transitionType, VISIT_TIME));
+   }
+-  Assert.equal(false, await promiseIsURIVisited(place.uri));
++  Assert.equal(false, await PlacesUtils.history.hasVisits(place.uri));
+ 
+   let callbackCount = 0;
+   let placesResult = await promiseUpdatePlaces(place);
+   if (placesResult.errors.length > 0) {
+     do_throw("Unexpected error.");
+   }
+   for (let placeInfo of placesResult.results) {
+-    Assert.ok(await promiseIsURIVisited(place.uri));
++    Assert.ok(await PlacesUtils.history.hasVisits(place.uri));
+ 
+     // Check mozIPlaceInfo properties.
+     Assert.ok(place.uri.equals(placeInfo.uri));
+     Assert.equal(placeInfo.frecency, -1); // We don't pass frecency here!
+     Assert.equal(placeInfo.title, place.title);
+ 
+     // Check mozIVisitInfo properties.
+     let visits = placeInfo.visits;
+@@ -681,28 +681,28 @@ add_task(async function test_properties_
+     let place = {
+       uri: NetUtil.newURI(TEST_DOMAIN + "test_properties_saved/" +
+                           transitionType),
+       title: "test_properties_saved test",
+       visits: [
+         new VisitInfo(transitionType),
+       ],
+     };
+-    Assert.equal(false, await promiseIsURIVisited(place.uri));
++    Assert.equal(false, await PlacesUtils.history.hasVisits(place.uri));
+     places.push(place);
+   }
+ 
+   let callbackCount = 0;
+   let placesResult = await promiseUpdatePlaces(places);
+   if (placesResult.errors.length > 0) {
+     do_throw("Unexpected error.");
+   }
+   for (let placeInfo of placesResult.results) {
+     let uri = placeInfo.uri;
+-    Assert.ok(await promiseIsURIVisited(uri));
++    Assert.ok(await PlacesUtils.history.hasVisits(uri));
+     let visit = placeInfo.visits[0];
+     print("TEST-INFO | test_properties_saved | updatePlaces callback for " +
+           "transition type " + visit.transitionType);
+ 
+     // Note that TRANSITION_EMBED should not be in the database.
+     const EXPECTED_COUNT = visit.transitionType == TRANSITION_EMBED ? 0 : 1;
+ 
+     // mozIVisitInfo::date
+@@ -759,25 +759,25 @@ add_task(async function test_guid_saved(
+   let place = {
+     uri: NetUtil.newURI(TEST_DOMAIN + "test_guid_saved"),
+     guid: "__TESTGUID__",
+     visits: [
+       new VisitInfo(),
+     ],
+   };
+   do_check_valid_places_guid(place.guid);
+-  Assert.equal(false, await promiseIsURIVisited(place.uri));
++  Assert.equal(false, await PlacesUtils.history.hasVisits(place.uri));
+ 
+   let placesResult = await promiseUpdatePlaces(place);
+   if (placesResult.errors.length > 0) {
+     do_throw("Unexpected error.");
+   }
+   let placeInfo = placesResult.results[0];
+   let uri = placeInfo.uri;
+-  Assert.ok(await promiseIsURIVisited(uri));
++  Assert.ok(await PlacesUtils.history.hasVisits(uri));
+   Assert.equal(placeInfo.guid, place.guid);
+   do_check_guid_for_uri(uri, place.guid);
+   await PlacesTestUtils.promiseAsyncUpdates();
+ });
+ 
+ add_task(async function test_referrer_saved() {
+   let places = [
+     { uri: NetUtil.newURI(TEST_DOMAIN + "test_referrer_saved/referrer"),
+@@ -787,27 +787,27 @@ add_task(async function test_referrer_sa
+     },
+     { uri: NetUtil.newURI(TEST_DOMAIN + "test_referrer_saved/test"),
+       visits: [
+         new VisitInfo(),
+       ],
+     },
+   ];
+   places[1].visits[0].referrerURI = places[0].uri;
+-  Assert.equal(false, await promiseIsURIVisited(places[0].uri));
+-  Assert.equal(false, await promiseIsURIVisited(places[1].uri));
++  Assert.equal(false, await PlacesUtils.history.hasVisits(places[0].uri));
++  Assert.equal(false, await PlacesUtils.history.hasVisits(places[1].uri));
+ 
+   let resultCount = 0;
+   let placesResult = await promiseUpdatePlaces(places);
+   if (placesResult.errors.length > 0) {
+     do_throw("Unexpected error.");
+   }
+   for (let placeInfo of placesResult.results) {
+     let uri = placeInfo.uri;
+-    Assert.ok(await promiseIsURIVisited(uri));
++    Assert.ok(await PlacesUtils.history.hasVisits(uri));
+     let visit = placeInfo.visits[0];
+ 
+     // We need to insert all of our visits before we can test conditions.
+     if (++resultCount == places.length) {
+       Assert.ok(places[0].uri.equals(visit.referrerURI));
+ 
+       let stmt = DBConn().createStatement(
+         `SELECT COUNT(1) AS count
+@@ -835,17 +835,17 @@ add_task(async function test_referrer_sa
+ add_task(async function test_guid_change_saved() {
+   // First, add a visit for it.
+   let place = {
+     uri: NetUtil.newURI(TEST_DOMAIN + "test_guid_change_saved"),
+     visits: [
+       new VisitInfo(),
+     ],
+   };
+-  Assert.equal(false, await promiseIsURIVisited(place.uri));
++  Assert.equal(false, await PlacesUtils.history.hasVisits(place.uri));
+ 
+   let placesResult = await promiseUpdatePlaces(place);
+   if (placesResult.errors.length > 0) {
+     do_throw("Unexpected error.");
+   }
+   // Then, change the guid with visits.
+   place.guid = "_GUIDCHANGE_";
+   place.visits = [new VisitInfo()];
+@@ -862,17 +862,17 @@ add_task(async function test_title_chang
+   // First, add a visit for it.
+   let place = {
+     uri: NetUtil.newURI(TEST_DOMAIN + "test_title_change_saved"),
+     title: "original title",
+     visits: [
+       new VisitInfo(),
+     ],
+   };
+-  Assert.equal(false, await promiseIsURIVisited(place.uri));
++  Assert.equal(false, await PlacesUtils.history.hasVisits(place.uri));
+ 
+   let placesResult = await promiseUpdatePlaces(place);
+   if (placesResult.errors.length > 0) {
+     do_throw("Unexpected error.");
+   }
+ 
+   // Now, make sure the empty string clears the title.
+   place.title = "";
+@@ -909,17 +909,17 @@ add_task(async function test_no_title_do
+   // First, add a visit for it.
+   let place = {
+     uri: NetUtil.newURI(TEST_DOMAIN + "test_no_title_does_not_clear_title"),
+     title: TITLE,
+     visits: [
+       new VisitInfo(),
+     ],
+   };
+-  Assert.equal(false, await promiseIsURIVisited(place.uri));
++  Assert.equal(false, await PlacesUtils.history.hasVisits(place.uri));
+ 
+   let placesResult = await promiseUpdatePlaces(place);
+   if (placesResult.errors.length > 0) {
+     do_throw("Unexpected error.");
+   }
+   // Now, make sure that not specifying a title does not clear it.
+   delete place.title;
+   place.visits = [new VisitInfo()];
+@@ -936,17 +936,17 @@ add_task(async function test_title_chang
+   // There are three cases to test.  The first case is to make sure we do not
+   // get notified if we do not specify a title.
+   let place = {
+     uri: NetUtil.newURI(TEST_DOMAIN + "test_title_change_notifies"),
+     visits: [
+       new VisitInfo(),
+     ],
+   };
+-  Assert.equal(false, await promiseIsURIVisited(place.uri));
++  Assert.equal(false, await PlacesUtils.history.hasVisits(place.uri));
+ 
+   let silentObserver =
+     new TitleChangedObserver(place.uri, "DO NOT WANT", function() {
+       do_throw("unexpected callback!");
+     });
+ 
+   PlacesUtils.history.addObserver(silentObserver);
+   let placesResult = await promiseUpdatePlaces(place);
+@@ -1001,17 +1001,17 @@ add_task(async function test_visit_notif
+   // nsINavHistoryObserver and the other is the uri-visit-saved observer topic.
+   let place = {
+     guid: "abcdefghijkl",
+     uri: NetUtil.newURI(TEST_DOMAIN + "test_visit_notifies"),
+     visits: [
+       new VisitInfo(),
+     ],
+   };
+-  Assert.equal(false, await promiseIsURIVisited(place.uri));
++  Assert.equal(false, await PlacesUtils.history.hasVisits(place.uri));
+ 
+   function promiseVisitObserver(aPlace) {
+     return new Promise((resolve, reject) => {
+       let callbackCount = 0;
+       let finisher = function() {
+         if (++callbackCount == 2) {
+           resolve();
+         }
+@@ -1146,33 +1146,33 @@ add_task(async function test_ignore_erro
+   // another visit, fails - but doesn't report if we told it not to.
+   let place = {
+     uri: NetUtil.newURI(TEST_DOMAIN + "test_duplicate_guid_fails_first"),
+     visits: [
+       new VisitInfo(),
+     ],
+   };
+ 
+-  Assert.equal(false, await promiseIsURIVisited(place.uri));
++  Assert.equal(false, await PlacesUtils.history.hasVisits(place.uri));
+   let placesResult = await promiseUpdatePlaces(place);
+   if (placesResult.errors.length > 0) {
+     do_throw("Unexpected error.");
+   }
+   let placeInfo = placesResult.results[0];
+-  Assert.ok(await promiseIsURIVisited(placeInfo.uri));
++  Assert.ok(await PlacesUtils.history.hasVisits(placeInfo.uri));
+ 
+   let badPlace = {
+     uri: NetUtil.newURI(TEST_DOMAIN + "test_duplicate_guid_fails_second"),
+     visits: [
+       new VisitInfo(),
+     ],
+     guid: placeInfo.guid,
+   };
+ 
+-  Assert.equal(false, await promiseIsURIVisited(badPlace.uri));
++  Assert.equal(false, await PlacesUtils.history.hasVisits(badPlace.uri));
+   placesResult = await promiseUpdatePlaces(badPlace, {ignoreErrors: true});
+   if (placesResult.results.length > 0) {
+     do_throw("Unexpected success.");
+   }
+   Assert.equal(placesResult.errors.length, 0,
+                "Should have seen 0 errors because we disabled reporting.");
+   Assert.equal(placesResult.results.length, 0,
+                "Should have seen 0 results because there were none.");
+@@ -1206,23 +1206,23 @@ add_task(async function test_ignore_resu
+   // another visit, fails - but doesn't report if we told it not to.
+   let place = {
+     uri: NetUtil.newURI(TEST_DOMAIN + "test_duplicate_guid_fails_first"),
+     visits: [
+       new VisitInfo(),
+     ],
+   };
+ 
+-  Assert.equal(false, await promiseIsURIVisited(place.uri));
++  Assert.equal(false, await PlacesUtils.history.hasVisits(place.uri));
+   let placesResult = await promiseUpdatePlaces(place);
+   if (placesResult.errors.length > 0) {
+     do_throw("Unexpected error.");
+   }
+   let placeInfo = placesResult.results[0];
+-  Assert.ok(await promiseIsURIVisited(placeInfo.uri));
++  Assert.ok(await PlacesUtils.history.hasVisits(placeInfo.uri));
+ 
+   let badPlace = {
+     uri: NetUtil.newURI(TEST_DOMAIN + "test_duplicate_guid_fails_second"),
+     visits: [
+       new VisitInfo(),
+     ],
+     guid: placeInfo.guid,
+   };
+@@ -1231,17 +1231,17 @@ add_task(async function test_ignore_resu
+       uri: NetUtil.newURI(TEST_DOMAIN + "test_other_successful_item"),
+       visits: [
+         new VisitInfo(),
+       ],
+     },
+     badPlace,
+   ];
+ 
+-  Assert.equal(false, await promiseIsURIVisited(badPlace.uri));
++  Assert.equal(false, await PlacesUtils.history.hasVisits(badPlace.uri));
+   placesResult = await promiseUpdatePlaces(allPlaces, {ignoreErrors: true, ignoreResults: true});
+   Assert.equal(placesResult.errors.length, 0,
+                "Should have seen 0 errors because we disabled reporting.");
+   Assert.equal(placesResult.results.length, 0,
+                "Should have seen 0 results because we disabled reporting.");
+   Assert.equal(placesResult.resultCount, 1,
+                "Should know that we updated 1 item from the completion callback.");
+   await PlacesTestUtils.promiseAsyncUpdates();
+diff --git a/toolkit/components/places/tests/history/test_removeVisits.js b/toolkit/components/places/tests/history/test_removeVisits.js
+--- a/toolkit/components/places/tests/history/test_removeVisits.js
++++ b/toolkit/components/places/tests/history/test_removeVisits.js
+@@ -39,18 +39,18 @@ add_task(async function remove_visits_ou
+   root.containerOpen = true;
+   Assert.equal(root.childCount, 10);
+   for (let i = 0; i < root.childCount; i++) {
+     let visitTime = root.getChild(i).time;
+     Assert.equal(visitTime, DB_NOW - 100000 - (i * 1000));
+   }
+   root.containerOpen = false;
+ 
+-  info("asyncHistory.isURIVisited should return true.");
+-  Assert.ok(await promiseIsURIVisited(TEST_URI));
++  info("PlacesUtils.history.hasVisits should return true.");
++  Assert.ok(await PlacesUtils.history.hasVisits(TEST_URI));
+ 
+   await PlacesTestUtils.promiseAsyncUpdates();
+   info("Frecency should be positive.");
+   Assert.ok(frecencyForUrl(TEST_URI) > 0);
+ 
+   await cleanup();
+ });
+ 
+@@ -91,17 +91,17 @@ add_task(async function remove_visits_ou
+   Assert.equal(root.childCount, 10);
+   for (let i = 0; i < root.childCount; i++) {
+     let visitTime = root.getChild(i).time;
+     Assert.equal(visitTime, DB_NOW - 100000 - (i * 1000));
+   }
+   root.containerOpen = false;
+ 
+   info("asyncHistory.isURIVisited should return true.");
+-  Assert.ok(await promiseIsURIVisited(TEST_URI));
++  Assert.ok(await PlacesUtils.history.hasVisits(TEST_URI));
+   await PlacesTestUtils.promiseAsyncUpdates();
+ 
+   info("Frecency should be positive.");
+   Assert.ok(frecencyForUrl(TEST_URI) > 0);
+ 
+   await cleanup();
+ });
+ 
+@@ -136,17 +136,17 @@ add_task(async function remove_visits_un
+   Assert.equal(root.childCount, 5);
+   for (let i = 0; i < root.childCount; i++) {
+     let visitTime = root.getChild(i).time;
+     Assert.equal(visitTime, DB_NOW - (i * 1000) - 5000);
+   }
+   root.containerOpen = false;
+ 
+   info("asyncHistory.isURIVisited should return true.");
+-  Assert.ok(await promiseIsURIVisited(TEST_URI));
++  Assert.ok(await PlacesUtils.history.hasVisits(TEST_URI));
+   await PlacesTestUtils.promiseAsyncUpdates();
+ 
+   info("Frecency should be positive.");
+   Assert.ok(frecencyForUrl(TEST_URI) > 0);
+ 
+   await cleanup();
+ });
+ 
+@@ -187,17 +187,17 @@ add_task(async function remove_visits_bo
+   Assert.equal(root.childCount, 5);
+   for (let i = 0; i < root.childCount; i++) {
+     let visitTime = root.getChild(i).time;
+     Assert.equal(visitTime, DB_NOW - (i * 1000) - 5000);
+   }
+   root.containerOpen = false;
+ 
+   info("asyncHistory.isURIVisited should return true.");
+-  Assert.ok(await promiseIsURIVisited(TEST_URI));
++  Assert.ok(await PlacesUtils.history.hasVisits(TEST_URI));
+   await PlacesTestUtils.promiseAsyncUpdates();
+ 
+   info("Frecency should be positive.");
+   Assert.ok(frecencyForUrl(TEST_URI) > 0);
+ 
+   await cleanup();
+ });
+ 
+@@ -228,17 +228,17 @@ add_task(async function remove_all_visit
+   opts.resultType = opts.RESULTS_AS_VISIT;
+   opts.sortingMode = opts.SORT_BY_DATE_DESCENDING;
+   let root = PlacesUtils.history.executeQuery(query, opts).root;
+   root.containerOpen = true;
+   Assert.equal(root.childCount, 0);
+   root.containerOpen = false;
+ 
+   info("asyncHistory.isURIVisited should return false.");
+-  Assert.equal(false, await promiseIsURIVisited(TEST_URI));
++  Assert.equal(false, await PlacesUtils.history.hasVisits(TEST_URI));
+ 
+   await cleanup();
+ });
+ 
+ add_task(async function remove_all_visits_bookmarked_uri() {
+   info("*** TEST: Remove all visits from a bookmarked URI");
+ 
+   info("Add some visits for the URI.");
+@@ -272,17 +272,17 @@ add_task(async function remove_all_visit
+   opts.resultType = opts.RESULTS_AS_VISIT;
+   opts.sortingMode = opts.SORT_BY_DATE_DESCENDING;
+   let root = PlacesUtils.history.executeQuery(query, opts).root;
+   root.containerOpen = true;
+   Assert.equal(root.childCount, 0);
+   root.containerOpen = false;
+ 
+   info("asyncHistory.isURIVisited should return false.");
+-  Assert.equal(false, await promiseIsURIVisited(TEST_URI));
++  Assert.equal(false, await PlacesUtils.history.hasVisits(TEST_URI));
+ 
+   info("URI should be bookmarked");
+   Assert.ok(await PlacesUtils.bookmarks.fetch({url: TEST_URI}));
+   await PlacesTestUtils.promiseAsyncUpdates();
+ 
+   info("Frecency should be smaller.")
+   Assert.ok(frecencyForUrl(TEST_URI) < initialFrecency);
+ 
+diff --git a/toolkit/components/places/tests/unit/test_425563.js b/toolkit/components/places/tests/unit/test_425563.js
+--- a/toolkit/components/places/tests/unit/test_425563.js
++++ b/toolkit/components/places/tests/unit/test_425563.js
+@@ -35,20 +35,20 @@ add_task(async function test_execute() {
+     { uri: uri("http://www.test-download.com/"),
+       transition: TRANSITION_DOWNLOAD },
+     { uri: uri("http://www.test-reload.com/"),
+       transition: TRANSITION_RELOAD },
+   ]);
+ 
+   // check that all links are marked as visited
+   for (let visited_uri of count_visited_URIs) {
+-    Assert.ok(await promiseIsURIVisited(uri(visited_uri)));
++    Assert.ok(await PlacesUtils.history.hasVisits(uri(visited_uri)));
+   }
+   for (let visited_uri of notcount_visited_URIs) {
+-    Assert.ok(await promiseIsURIVisited(uri(visited_uri)));
++    Assert.ok(await PlacesUtils.history.hasVisits(uri(visited_uri)));
+   }
+ 
+   // check that visit_count does not take in count embed and downloads
+   // maxVisits query are directly binded to visit_count
+   let options = PlacesUtils.history.getNewQueryOptions();
+   options.sortingMode = options.SORT_BY_VISITCOUNT_DESCENDING;
+   options.resultType = options.RESULTS_AS_VISIT;
+   options.includeHidden = true;
+diff --git a/toolkit/components/places/tests/unit/test_isvisited.js b/toolkit/components/places/tests/unit/test_isvisited.js
+--- a/toolkit/components/places/tests/unit/test_isvisited.js
++++ b/toolkit/components/places/tests/unit/test_isvisited.js
+@@ -6,33 +6,33 @@
+ 
+ add_task(async function test_execute() {
+   var referrer = uri("about:blank");
+ 
+   // add a http:// uri
+   var uri1 = uri("http://mozilla.com");
+   await PlacesTestUtils.addVisits({uri: uri1, referrer});
+   do_check_guid_for_uri(uri1);
+-  Assert.ok(await promiseIsURIVisited(uri1));
++  Assert.ok(await PlacesUtils.history.hasVisits(uri1));
+ 
+   // add a https:// uri
+   var uri2 = uri("https://etrade.com");
+   await PlacesTestUtils.addVisits({uri: uri2, referrer});
+   do_check_guid_for_uri(uri2);
+-  Assert.ok(await promiseIsURIVisited(uri2));
++  Assert.ok(await PlacesUtils.history.hasVisits(uri2));
+ 
+   // add a ftp:// uri
+   var uri3 = uri("ftp://ftp.mozilla.org");
+   await PlacesTestUtils.addVisits({uri: uri3, referrer});
+   do_check_guid_for_uri(uri3);
+-  Assert.ok(await promiseIsURIVisited(uri3));
++  Assert.ok(await PlacesUtils.history.hasVisits(uri3));
+ 
+   // check if a nonexistent uri is visited
+   var uri4 = uri("http://foobarcheese.com");
+-  Assert.equal(false, await promiseIsURIVisited(uri4));
++  Assert.equal(false, await PlacesUtils.history.hasVisits(uri4));
+ 
+   // check that certain schemes never show up as visited
+   // even if we attempt to add them to history
+   // see CanAddURI() in nsNavHistory.cpp
+   const URLS = [
+     "about:config",
+     "imap://cyrus.andrew.cmu.edu/archive.imap",
+     "news://new.mozilla.org/mozilla.dev.apps.firefox",
+@@ -56,12 +56,12 @@ add_task(async function test_execute() {
+       info("Could not construct URI for '" + currentURL + "'; ignoring");
+     }
+     if (cantAddUri) {
+       PlacesTestUtils.addVisits({uri: cantAddUri, referrer}).then(() => {
+         do_throw("Should not have added history for invalid URI.");
+       }, error => {
+         Assert.ok(error.message.includes("No items were added to history"));
+       });
+-      Assert.equal(false, await promiseIsURIVisited(cantAddUri));
++      Assert.equal(false, await PlacesUtils.history.hasVisits(cantAddUri));
+     }
+   }
+ });
+diff --git a/toolkit/components/places/tests/unit/test_preventive_maintenance.js b/toolkit/components/places/tests/unit/test_preventive_maintenance.js
+--- a/toolkit/components/places/tests/unit/test_preventive_maintenance.js
++++ b/toolkit/components/places/tests/unit/test_preventive_maintenance.js
+@@ -1379,19 +1379,19 @@ tests.push({
+                                  Services.scriptSecurityManager.getSystemPrincipal());
+     await PlacesUtils.keywords.insert({ url: this._uri1.spec, keyword: "testkeyword" });
+     as.setPageAnnotation(this._uri2, "anno", "anno", 0, as.EXPIRE_NEVER);
+     as.setItemAnnotation(this._bookmarkId, "anno", "anno", 0, as.EXPIRE_NEVER);
+   },
+ 
+   async check() {
+     // Check that all items are correct
+-    let isVisited = await promiseIsURIVisited(this._uri1);
++    let isVisited = await PlacesUtils.history.hasVisits(this._uri1);
+     Assert.ok(isVisited);
+-    isVisited = await promiseIsURIVisited(this._uri2);
++    isVisited = await PlacesUtils.history.hasVisits(this._uri2);
+     Assert.ok(isVisited);
+ 
+     Assert.equal((await bs.fetch(this._bookmark.guid)).url, this._uri1.spec);
+     let folder = await bs.fetch(this._folder.guid);
+     Assert.equal(folder.index, 0);
+     Assert.equal(folder.type, bs.TYPE_FOLDER);
+     Assert.equal((await bs.fetch(this._separator.guid)).type, bs.TYPE_SEPARATOR);
+ 
+diff --git a/toolkit/forgetaboutsite/test/unit/test_removeDataFromDomain.js b/toolkit/forgetaboutsite/test/unit/test_removeDataFromDomain.js
+--- a/toolkit/forgetaboutsite/test/unit/test_removeDataFromDomain.js
++++ b/toolkit/forgetaboutsite/test/unit/test_removeDataFromDomain.js
+@@ -31,35 +31,16 @@ const LOGIN_PASSWORD_FIELD = "password_f
+ const PERMISSION_TYPE = "test-perm";
+ const PERMISSION_VALUE = Ci.nsIPermissionManager.ALLOW_ACTION;
+ 
+ const PREFERENCE_NAME = "test-pref";
+ 
+ // Utility Functions
+ 
+ /**
+- * Asynchronously check a url is visited.
+- *
+- * @param aURI
+- *        The URI.
+- *
+- * @return {Promise}
+- * @resolves When the check has been added successfully.
+- * @rejects JavaScript exception.
+- */
+-function promiseIsURIVisited(aURI) {
+-  return new Promise(resolve => {
+-    PlacesUtils.asyncHistory.isURIVisited(aURI, function(unused, aIsVisited) {
+-      resolve(aIsVisited);
+-    });
+-
+-  });
+-}
+-
+-/**
+  * Add a cookie to the cookie service.
+  *
+  * @param aDomain
+  */
+ function add_cookie(aDomain) {
+   check_cookie_exists(aDomain, false);
+   Services.cookies.add(aDomain, COOKIE_PATH, COOKIE_NAME, "", false, false, false,
+                        COOKIE_EXPIRY, {});
+@@ -201,39 +182,39 @@ function preference_exists(aURI) {
+   });
+ }
+ 
+ // Test Functions
+ 
+ // History
+ async function test_history_cleared_with_direct_match() {
+   const TEST_URI = Services.io.newURI("http://mozilla.org/foo");
+-  Assert.equal(false, await promiseIsURIVisited(TEST_URI));
++  Assert.equal(false, await PlacesUtils.history.hasVisits(TEST_URI));
+   await PlacesTestUtils.addVisits(TEST_URI);
+-  Assert.ok(await promiseIsURIVisited(TEST_URI));
++  Assert.ok(await PlacesUtils.history.hasVisits(TEST_URI));
+   await ForgetAboutSite.removeDataFromDomain("mozilla.org");
+-  Assert.equal(false, await promiseIsURIVisited(TEST_URI));
++  Assert.equal(false, await PlacesUtils.history.hasVisits(TEST_URI));
+ }
+ 
+ async function test_history_cleared_with_subdomain() {
+   const TEST_URI = Services.io.newURI("http://www.mozilla.org/foo");
+-  Assert.equal(false, await promiseIsURIVisited(TEST_URI));
++  Assert.equal(false, await PlacesUtils.history.hasVisits(TEST_URI));
+   await PlacesTestUtils.addVisits(TEST_URI);
+-  Assert.ok(await promiseIsURIVisited(TEST_URI));
++  Assert.ok(await PlacesUtils.history.hasVisits(TEST_URI));
+   await ForgetAboutSite.removeDataFromDomain("mozilla.org");
+-  Assert.equal(false, await promiseIsURIVisited(TEST_URI));
++  Assert.equal(false, await PlacesUtils.history.hasVisits(TEST_URI));
+ }
+ 
+ async function test_history_not_cleared_with_uri_contains_domain() {
+   const TEST_URI = Services.io.newURI("http://ilovemozilla.org/foo");
+-  Assert.equal(false, await promiseIsURIVisited(TEST_URI));
++  Assert.equal(false, await PlacesUtils.history.hasVisits(TEST_URI));
+   await PlacesTestUtils.addVisits(TEST_URI);
+-  Assert.ok(await promiseIsURIVisited(TEST_URI));
++  Assert.ok(await PlacesUtils.history.hasVisits(TEST_URI));
+   await ForgetAboutSite.removeDataFromDomain("mozilla.org");
+-  Assert.ok(await promiseIsURIVisited(TEST_URI));
++  Assert.ok(await PlacesUtils.history.hasVisits(TEST_URI));
+ 
+   // Clear history since we left something there from this test.
+   await PlacesTestUtils.clearHistory();
+ }
+ 
+ // Cookie Service
+ async function test_cookie_cleared_with_direct_match() {
+   const TEST_DOMAIN = "mozilla.org";

+ 635 - 0
frg/mozilla-release/work-js/1372592-57a1.patch

@@ -0,0 +1,635 @@
+# HG changeset patch
+# User Fischer.json <fischer.json@gmail.com>
+# Date 1503567186 -28800
+# Node ID 6257f5ada1814787d8a1cefebdce151fbe00e837
+# Parent  7bb302c9dcbddcc3535fbb23cdd27a979c8d6682
+Bug 1372592 - Should not display non-persistent-storage sites which stores 0 byte in Preferences Site Data section, r=jaws
+
+MozReview-Commit-ID: BelcT0Lpmba
+
+diff --git a/browser/components/preferences/SiteDataManager.jsm b/browser/components/preferences/SiteDataManager.jsm
+--- a/browser/components/preferences/SiteDataManager.jsm
++++ b/browser/components/preferences/SiteDataManager.jsm
+@@ -44,16 +44,20 @@ var SiteDataManager = {
+   _getQuotaUsage() {
+     // Clear old data and requests first
+     this._sites.clear();
+     this._cancelGetQuotaUsage();
+     this._getQuotaUsagePromise = new Promise(resolve => {
+       let onUsageResult = request => {
+         let items = request.result;
+         for (let item of items) {
++          if (!item.persisted && item.usage <= 0) {
++            // An non-persistent-storage site with 0 byte quota usage is redundant for us so skip it.
++            continue;
++          }
+           let principal =
+             Services.scriptSecurityManager.createCodebasePrincipalFromOrigin(item.origin);
+           let uri = principal.URI;
+           if (uri.scheme == "http" || uri.scheme == "https") {
+             let site = this._sites.get(uri.host);
+             if (!site) {
+               site = {
+                 persisted: false,
+@@ -91,31 +95,35 @@ var SiteDataManager = {
+       this._quotaUsageRequest.cancel();
+       this._quotaUsageRequest = null;
+     }
+   },
+ 
+   _updateAppCache() {
+     let groups = this._appCache.getGroups();
+     for (let group of groups) {
++      let cache = this._appCache.getActiveCache(group);
++      if (cache.usage <= 0) {
++        // A site with 0 byte appcache usage is redundant for us so skip it.
++        continue;
++      }
+       let principal = Services.scriptSecurityManager.createCodebasePrincipalFromOrigin(group);
+       let uri = principal.URI;
+       let site = this._sites.get(uri.host);
+       if (!site) {
+         site = {
+           persisted: false,
+           quotaUsage: 0,
+           principals: [ principal ],
+           appCacheList: [],
+         };
+         this._sites.set(uri.host, site);
+       } else if (!site.principals.some(p => p.origin == principal.origin)) {
+         site.principals.push(principal);
+       }
+-      let cache = this._appCache.getActiveCache(group);
+       site.appCacheList.push(cache);
+     }
+   },
+ 
+   getTotalUsage() {
+     return this._getQuotaUsagePromise.then(() => {
+       let usage = 0;
+       for (let site of this._sites.values()) {
+diff --git a/browser/components/preferences/in-content-new/tests/browser.ini b/browser/components/preferences/in-content-new/tests/browser.ini
+--- a/browser/components/preferences/in-content-new/tests/browser.ini
++++ b/browser/components/preferences/in-content-new/tests/browser.ini
+@@ -60,15 +60,16 @@ skip-if = e10s
+ [browser_privacypane_5.js]
+ [browser_privacypane_8.js]
+ [browser_sanitizeOnShutdown_prefLocked.js]
+ [browser_searchsuggestions.js]
+ [browser_security-1.js]
+ [browser_security-2.js]
+ [browser_siteData.js]
+ [browser_siteData2.js]
++[browser_siteData3.js]
+ [browser_site_login_exceptions.js]
+ [browser_permissions_dialog.js]
+ [browser_cookies_dialog.js]
+ [browser_subdialogs.js]
+ support-files =
+   subdialog.xul
+   subdialog2.xul
+diff --git a/browser/components/preferences/in-content-new/tests/browser_siteData.js b/browser/components/preferences/in-content-new/tests/browser_siteData.js
+--- a/browser/components/preferences/in-content-new/tests/browser_siteData.js
++++ b/browser/components/preferences/in-content-new/tests/browser_siteData.js
+@@ -353,64 +353,8 @@ add_task(async function() {
+       }
+     }
+   }
+ 
+   function findSiteByHost(host) {
+     return mockSiteDataManager.fakeSites.find(site => site.principal.URI.host == host);
+   }
+ });
+-
+-// Test search on the host column
+-add_task(async function() {
+-  mockSiteDataManager.register(SiteDataManager);
+-  mockSiteDataManager.fakeSites = [
+-    {
+-      usage: 1024,
+-      principal: Services.scriptSecurityManager
+-                         .createCodebasePrincipalFromOrigin("https://account.xyz.com"),
+-      persisted: true
+-    },
+-    {
+-      usage: 1024,
+-      principal: Services.scriptSecurityManager
+-                         .createCodebasePrincipalFromOrigin("https://shopping.xyz.com"),
+-      persisted: false
+-    },
+-    {
+-      usage: 1024,
+-      principal: Services.scriptSecurityManager
+-                         .createCodebasePrincipalFromOrigin("http://cinema.bar.com"),
+-      persisted: true
+-    },
+-    {
+-      usage: 1024,
+-      principal: Services.scriptSecurityManager
+-                         .createCodebasePrincipalFromOrigin("http://email.bar.com"),
+-      persisted: false
+-    },
+-  ];
+-  let fakeHosts = mockSiteDataManager.fakeSites.map(site => site.principal.URI.host);
+-
+-  let updatePromise = promiseSiteDataManagerSitesUpdated();
+-  await openPreferencesViaOpenPreferencesAPI("privacy", { leaveOpen: true });
+-  await updatePromise;
+-  await openSiteDataSettingsDialog();
+-
+-  let doc = gBrowser.selectedBrowser.contentDocument;
+-  let frameDoc = content.gSubDialog._topDialog._frame.contentDocument;
+-  let searchBox = frameDoc.getElementById("searchBox");
+-
+-  searchBox.value = "xyz";
+-  searchBox.doCommand();
+-  assertSitesListed(doc, fakeHosts.filter(host => host.includes("xyz")));
+-
+-  searchBox.value = "bar";
+-  searchBox.doCommand();
+-  assertSitesListed(doc, fakeHosts.filter(host => host.includes("bar")));
+-
+-  searchBox.value = "";
+-  searchBox.doCommand();
+-  assertSitesListed(doc, fakeHosts);
+-
+-  mockSiteDataManager.unregister();
+-  await BrowserTestUtils.removeTab(gBrowser.selectedTab);
+-});
+diff --git a/browser/components/preferences/in-content-new/tests/browser_siteData2.js b/browser/components/preferences/in-content-new/tests/browser_siteData2.js
+--- a/browser/components/preferences/in-content-new/tests/browser_siteData2.js
++++ b/browser/components/preferences/in-content-new/tests/browser_siteData2.js
+@@ -1,13 +1,12 @@
+ "use strict";
+ const { SiteDataManager } = ChromeUtils.import("resource:///modules/SiteDataManager.jsm", {});
+ 
+ const REMOVE_DIALOG_URL = "chrome://browser/content/preferences/siteDataRemoveSelected.xul";
+-const { DownloadUtils } = ChromeUtils.import("resource://gre/modules/DownloadUtils.jsm", {});
+ 
+ function promiseSettingsDialogClose() {
+   return new Promise(resolve => {
+     let win = gBrowser.selectedBrowser.contentWindow;
+     let dialogOverlay = win.gSubDialog._topDialog._overlay;
+     let dialogWin = win.gSubDialog._topDialog._frame.contentWindow;
+     dialogWin.addEventListener("unload", function unload() {
+       if (dialogWin.document.documentURI === "chrome://browser/content/preferences/siteDataSettings.xul") {
+@@ -308,76 +307,16 @@ add_task(async function() {
+   await updatePromise;
+   await openSiteDataSettingsDialog();
+   assertSitesListed(doc, fakeHosts.filter(host => !host.includes("xyz")));
+ 
+   mockSiteDataManager.unregister();
+   await BrowserTestUtils.removeTab(gBrowser.selectedTab);
+ });
+ 
+-// Test grouping and listing sites across scheme, port and origin attributes by host
+-add_task(async function() {
+-  const quotaUsage = 1024;
+-  mockSiteDataManager.register(SiteDataManager);
+-  mockSiteDataManager.fakeSites = [
+-    {
+-      usage: quotaUsage,
+-      principal: Services.scriptSecurityManager
+-                         .createCodebasePrincipalFromOrigin("https://account.xyz.com^userContextId=1"),
+-      persisted: true
+-    },
+-    {
+-      usage: quotaUsage,
+-      principal: Services.scriptSecurityManager
+-                         .createCodebasePrincipalFromOrigin("https://account.xyz.com"),
+-      persisted: false
+-    },
+-    {
+-      usage: quotaUsage,
+-      principal: Services.scriptSecurityManager
+-                         .createCodebasePrincipalFromOrigin("https://account.xyz.com:123"),
+-      persisted: false
+-    },
+-    {
+-      usage: quotaUsage,
+-      principal: Services.scriptSecurityManager
+-                         .createCodebasePrincipalFromOrigin("http://account.xyz.com"),
+-      persisted: false
+-    },
+-  ];
+-
+-  let updatedPromise = promiseSiteDataManagerSitesUpdated();
+-  await openPreferencesViaOpenPreferencesAPI("privacy", { leaveOpen: true });
+-  await updatedPromise;
+-  await openSiteDataSettingsDialog();
+-  let win = gBrowser.selectedBrowser.contentWindow;
+-  let dialogFrame = win.gSubDialog._topDialog._frame;
+-  let frameDoc = dialogFrame.contentDocument;
+-
+-  let siteItems = frameDoc.getElementsByTagName("richlistitem");
+-  is(siteItems.length, 1, "Should group sites across scheme, port and origin attributes");
+-
+-  let expected = "account.xyz.com";
+-  let host = siteItems[0].getAttribute("host");
+-  is(host, expected, "Should group and list sites by host");
+-
+-  let prefStrBundle = frameDoc.getElementById("bundlePreferences");
+-  expected = prefStrBundle.getFormattedString("siteUsage",
+-    DownloadUtils.convertByteUnits(quotaUsage * mockSiteDataManager.fakeSites.length));
+-  let usage = siteItems[0].getAttribute("usage");
+-  is(usage, expected, "Should sum up usages across scheme, port and origin attributes");
+-
+-  expected = prefStrBundle.getString("persistent");
+-  let status = siteItems[0].getAttribute("status");
+-  is(status, expected, "Should mark persisted status across scheme, port and origin attributes");
+-
+-  mockSiteDataManager.unregister();
+-  await BrowserTestUtils.removeTab(gBrowser.selectedTab);
+-});
+-
+ // Test dynamically clearing all site data
+ add_task(async function() {
+   mockSiteDataManager.register(SiteDataManager);
+   mockSiteDataManager.fakeSites = [
+     {
+       usage: 1024,
+       principal: Services.scriptSecurityManager
+                          .createCodebasePrincipalFromOrigin("https://account.xyz.com"),
+diff --git a/browser/components/preferences/in-content-new/tests/browser_siteData3.js b/browser/components/preferences/in-content-new/tests/browser_siteData3.js
+new file mode 100644
+--- /dev/null
++++ b/browser/components/preferences/in-content-new/tests/browser_siteData3.js
+@@ -0,0 +1,107 @@
++"use strict";
++const { SiteDataManager } = Cu.import("resource:///modules/SiteDataManager.jsm", {});
++const { DownloadUtils } = Cu.import("resource://gre/modules/DownloadUtils.jsm", {});
++
++// Test not displaying sites which store 0 byte and don't have persistent storage.
++add_task(async function() {
++  await SpecialPowers.pushPrefEnv({set: [["browser.storageManager.enabled", true]]});
++  mockSiteDataManager.register(SiteDataManager);
++  mockSiteDataManager.fakeSites = [
++    {
++      usage: 0,
++      principal: Services.scriptSecurityManager
++                         .createCodebasePrincipalFromOrigin("https://account.xyz.com"),
++      persisted: true
++    },
++    {
++      usage: 0,
++      principal: Services.scriptSecurityManager
++                         .createCodebasePrincipalFromOrigin("https://shopping.xyz.com"),
++      persisted: false
++    },
++    {
++      usage: 1024,
++      principal: Services.scriptSecurityManager
++                         .createCodebasePrincipalFromOrigin("http://cinema.bar.com"),
++      persisted: true
++    },
++    {
++      usage: 1024,
++      principal: Services.scriptSecurityManager
++                         .createCodebasePrincipalFromOrigin("http://email.bar.com"),
++      persisted: false
++    },
++  ];
++  let fakeHosts = mockSiteDataManager.fakeSites.map(site => site.principal.URI.host);
++
++  let updatePromise = promiseSiteDataManagerSitesUpdated();
++  let doc = gBrowser.selectedBrowser.contentDocument;
++  await openPreferencesViaOpenPreferencesAPI("privacy", { leaveOpen: true });
++  await updatePromise;
++  await openSiteDataSettingsDialog();
++  assertSitesListed(doc, fakeHosts.filter(host => host != "shopping.xyz.com"));
++
++  mockSiteDataManager.unregister();
++  await BrowserTestUtils.removeTab(gBrowser.selectedTab);
++});
++
++// Test grouping and listing sites across scheme, port and origin attributes by host
++add_task(async function() {
++  await SpecialPowers.pushPrefEnv({set: [["browser.storageManager.enabled", true]]});
++  const quotaUsage = 1024;
++  mockSiteDataManager.register(SiteDataManager);
++  mockSiteDataManager.fakeSites = [
++    {
++      usage: quotaUsage,
++      principal: Services.scriptSecurityManager
++                         .createCodebasePrincipalFromOrigin("https://account.xyz.com^userContextId=1"),
++      persisted: true
++    },
++    {
++      usage: quotaUsage,
++      principal: Services.scriptSecurityManager
++                         .createCodebasePrincipalFromOrigin("https://account.xyz.com"),
++      persisted: false
++    },
++    {
++      usage: quotaUsage,
++      principal: Services.scriptSecurityManager
++                         .createCodebasePrincipalFromOrigin("https://account.xyz.com:123"),
++      persisted: false
++    },
++    {
++      usage: quotaUsage,
++      principal: Services.scriptSecurityManager
++                         .createCodebasePrincipalFromOrigin("http://account.xyz.com"),
++      persisted: false
++    },
++  ];
++
++  let updatedPromise = promiseSiteDataManagerSitesUpdated();
++  await openPreferencesViaOpenPreferencesAPI("privacy", { leaveOpen: true });
++  await updatedPromise;
++  await openSiteDataSettingsDialog();
++  let win = gBrowser.selectedBrowser.contentWindow;
++  let dialogFrame = win.gSubDialog._topDialog._frame;
++  let frameDoc = dialogFrame.contentDocument;
++
++  let siteItems = frameDoc.getElementsByTagName("richlistitem");
++  is(siteItems.length, 1, "Should group sites across scheme, port and origin attributes");
++
++  let expected = "account.xyz.com";
++  let host = siteItems[0].getAttribute("host");
++  is(host, expected, "Should group and list sites by host");
++
++  let prefStrBundle = frameDoc.getElementById("bundlePreferences");
++  expected = prefStrBundle.getFormattedString("siteUsage",
++    DownloadUtils.convertByteUnits(quotaUsage * mockSiteDataManager.fakeSites.length));
++  let usage = siteItems[0].getAttribute("usage");
++  is(usage, expected, "Should sum up usages across scheme, port and origin attributes");
++
++  expected = prefStrBundle.getString("persistent");
++  let status = siteItems[0].getAttribute("status");
++  is(status, expected, "Should mark persisted status across scheme, port and origin attributes");
++
++  mockSiteDataManager.unregister();
++  await BrowserTestUtils.removeTab(gBrowser.selectedTab);
++});
+diff --git a/browser/components/preferences/in-content/tests/browser.ini b/browser/components/preferences/in-content/tests/browser.ini
+--- a/browser/components/preferences/in-content/tests/browser.ini
++++ b/browser/components/preferences/in-content/tests/browser.ini
+@@ -41,14 +41,15 @@ skip-if = e10s
+ [browser_privacypane_4.js]
+ [browser_privacypane_5.js]
+ [browser_privacypane_8.js]
+ [browser_sanitizeOnShutdown_prefLocked.js]
+ [browser_searchsuggestions.js]
+ [browser_security.js]
+ [browser_siteData.js]
+ [browser_siteData2.js]
++[browser_siteData3.js]
+ [browser_site_login_exceptions.js]
+ [browser_cookies_dialog.js]
+ [browser_subdialogs.js]
+ support-files =
+   subdialog.xul
+   subdialog2.xul
+diff --git a/browser/components/preferences/in-content/tests/browser_siteData.js b/browser/components/preferences/in-content/tests/browser_siteData.js
+--- a/browser/components/preferences/in-content/tests/browser_siteData.js
++++ b/browser/components/preferences/in-content/tests/browser_siteData.js
+@@ -88,75 +88,16 @@ function promiseCookiesCleared() {
+   });
+ }
+ 
+ registerCleanupFunction(function() {
+   delete window.sinon;
+   mockOfflineAppCacheHelper.unregister();
+ });
+ 
+-// Test grouping and listing sites across scheme, port and origin attributes by host
+-add_task(async function() {
+-  const quotaUsage = 1024;
+-  mockSiteDataManager.register();
+-  mockSiteDataManager.fakeSites = [
+-    {
+-      usage: quotaUsage,
+-      principal: Services.scriptSecurityManager
+-                         .createCodebasePrincipalFromOrigin("https://account.xyz.com^userContextId=1"),
+-      persisted: true
+-    },
+-    {
+-      usage: quotaUsage,
+-      principal: Services.scriptSecurityManager
+-                         .createCodebasePrincipalFromOrigin("https://account.xyz.com"),
+-      persisted: false
+-    },
+-    {
+-      usage: quotaUsage,
+-      principal: Services.scriptSecurityManager
+-                         .createCodebasePrincipalFromOrigin("https://account.xyz.com:123"),
+-      persisted: false
+-    },
+-    {
+-      usage: quotaUsage,
+-      principal: Services.scriptSecurityManager
+-                         .createCodebasePrincipalFromOrigin("http://account.xyz.com"),
+-      persisted: false
+-    },
+-  ];
+-
+-  let updatedPromise = promiseSitesUpdated();
+-  await openPreferencesViaOpenPreferencesAPI("advanced", "networkTab", { leaveOpen: true });
+-  await updatedPromise;
+-  await openSettingsDialog();
+-  let dialogFrame = content.gSubDialog._topDialog._frame;
+-  let frameDoc = dialogFrame.contentDocument;
+-
+-  let siteItems = frameDoc.getElementsByTagName("richlistitem");
+-  is(siteItems.length, 1, "Should group sites across scheme, port and origin attributes");
+-
+-  let expected = "account.xyz.com";
+-  let hostCol = siteItems[0].getAttribute("host");
+-  is(hostCol, expected, "Should group and list sites by host");
+-
+-  let prefStrBundle = frameDoc.getElementById("bundlePreferences");
+-  expected = prefStrBundle.getFormattedString("siteUsage",
+-    DownloadUtils.convertByteUnits(quotaUsage * mockSiteDataManager.fakeSites.length));
+-  let usageCol = siteItems[0].getAttribute("usage");
+-  is(usageCol, expected, "Should sum up usages across scheme, port and origin attributes");
+-
+-  expected = prefStrBundle.getString("persistent");
+-  let statusCol = siteItems[0].getAttribute("status");
+-  is(statusCol, expected, "Should mark persisted status across scheme, port and origin attributes");
+-
+-  mockSiteDataManager.unregister();
+-  await BrowserTestUtils.removeTab(gBrowser.selectedTab);
+-});
+-
+ // Test listing site using quota usage or site using appcache
+ add_task(async function() {
+   // Open a test site which would save into appcache
+   await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_OFFLINE_URL);
+   await BrowserTestUtils.removeTab(gBrowser.selectedTab);
+ 
+   // Open a test site which would save into quota manager
+   await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_QUOTA_USAGE_URL);
+@@ -415,65 +356,8 @@ add_task(async function() {
+       }
+     }
+   }
+ 
+   function findSiteByHost(host) {
+     return mockSiteDataManager.fakeSites.find(site => site.principal.URI.host == host);
+   }
+ });
+-
+-// Test search on the host column
+-add_task(async function() {
+-  mockSiteDataManager.register();
+-  mockSiteDataManager.fakeSites = [
+-    {
+-      usage: 1024,
+-      principal: Services.scriptSecurityManager
+-                         .createCodebasePrincipalFromOrigin("https://account.xyz.com"),
+-      persisted: true
+-    },
+-    {
+-      usage: 1024,
+-      principal: Services.scriptSecurityManager
+-                         .createCodebasePrincipalFromOrigin("https://shopping.xyz.com"),
+-      persisted: false
+-    },
+-    {
+-      usage: 1024,
+-      principal: Services.scriptSecurityManager
+-                         .createCodebasePrincipalFromOrigin("http://cinema.bar.com"),
+-      persisted: true
+-    },
+-    {
+-      usage: 1024,
+-      principal: Services.scriptSecurityManager
+-                         .createCodebasePrincipalFromOrigin("http://email.bar.com"),
+-      persisted: false
+-    },
+-  ];
+-  let fakeHosts = mockSiteDataManager.fakeSites.map(site => site.principal.URI.host);
+-
+-  let updatePromise = promiseSitesUpdated();
+-  await openPreferencesViaOpenPreferencesAPI("advanced", "networkTab", { leaveOpen: true });
+-  await updatePromise;
+-  await openSettingsDialog();
+-
+-  let win = gBrowser.selectedBrowser.contentWindow;
+-  let doc = gBrowser.selectedBrowser.contentDocument;
+-  let frameDoc = win.gSubDialog._topDialog._frame.contentDocument;
+-  let searchBox = frameDoc.getElementById("searchBox");
+-
+-  searchBox.value = "xyz";
+-  searchBox.doCommand();
+-  assertSitesListed(doc, fakeHosts.filter(host => host.includes("xyz")));
+-
+-  searchBox.value = "bar";
+-  searchBox.doCommand();
+-  assertSitesListed(doc, fakeHosts.filter(host => host.includes("bar")));
+-
+-  searchBox.value = "";
+-  searchBox.doCommand();
+-  assertSitesListed(doc, fakeHosts);
+-
+-  mockSiteDataManager.unregister();
+-  await BrowserTestUtils.removeTab(gBrowser.selectedTab);
+-});
+diff --git a/browser/components/preferences/in-content/tests/browser_siteData3.js b/browser/components/preferences/in-content/tests/browser_siteData3.js
+new file mode 100644
+--- /dev/null
++++ b/browser/components/preferences/in-content/tests/browser_siteData3.js
+@@ -0,0 +1,102 @@
++"use strict";
++
++// Test search on the host column
++add_task(async function() {
++  await SpecialPowers.pushPrefEnv({set: [["browser.storageManager.enabled", true]]});
++  mockSiteDataManager.register();
++  mockSiteDataManager.fakeSites = [
++    {
++      usage: 1024,
++      principal: Services.scriptSecurityManager
++                         .createCodebasePrincipalFromOrigin("https://account.xyz.com"),
++      persisted: true
++    },
++    {
++      usage: 1024,
++      principal: Services.scriptSecurityManager
++                         .createCodebasePrincipalFromOrigin("https://shopping.xyz.com"),
++      persisted: false
++    },
++    {
++      usage: 1024,
++      principal: Services.scriptSecurityManager
++                         .createCodebasePrincipalFromOrigin("http://cinema.bar.com"),
++      persisted: true
++    },
++    {
++      usage: 1024,
++      principal: Services.scriptSecurityManager
++                         .createCodebasePrincipalFromOrigin("http://email.bar.com"),
++      persisted: false
++    },
++  ];
++  let fakeHosts = mockSiteDataManager.fakeSites.map(site => site.principal.URI.host);
++
++  let updatePromise = promiseSitesUpdated();
++  await openPreferencesViaOpenPreferencesAPI("advanced", "networkTab", { leaveOpen: true });
++  await updatePromise;
++  await openSettingsDialog();
++
++  let win = gBrowser.selectedBrowser.contentWindow;
++  let doc = gBrowser.selectedBrowser.contentDocument;
++  let frameDoc = win.gSubDialog._topDialog._frame.contentDocument;
++  let searchBox = frameDoc.getElementById("searchBox");
++
++  searchBox.value = "xyz";
++  searchBox.doCommand();
++  assertSitesListed(doc, fakeHosts.filter(host => host.includes("xyz")));
++
++  searchBox.value = "bar";
++  searchBox.doCommand();
++  assertSitesListed(doc, fakeHosts.filter(host => host.includes("bar")));
++
++  searchBox.value = "";
++  searchBox.doCommand();
++  assertSitesListed(doc, fakeHosts);
++
++  mockSiteDataManager.unregister();
++  await BrowserTestUtils.removeTab(gBrowser.selectedTab);
++});
++
++// Test not displaying sites which store 0 byte and don't have persistent storage.
++add_task(async function() {
++  await SpecialPowers.pushPrefEnv({set: [["browser.storageManager.enabled", true]]});
++  mockSiteDataManager.register();
++  mockSiteDataManager.fakeSites = [
++    {
++      usage: 0,
++      principal: Services.scriptSecurityManager
++                         .createCodebasePrincipalFromOrigin("https://account.xyz.com"),
++      persisted: true
++    },
++    {
++      usage: 0,
++      principal: Services.scriptSecurityManager
++                         .createCodebasePrincipalFromOrigin("https://shopping.xyz.com"),
++      persisted: false
++    },
++    {
++      usage: 1024,
++      principal: Services.scriptSecurityManager
++                         .createCodebasePrincipalFromOrigin("http://cinema.bar.com"),
++      persisted: true
++    },
++    {
++      usage: 1024,
++      principal: Services.scriptSecurityManager
++                         .createCodebasePrincipalFromOrigin("http://email.bar.com"),
++      persisted: false
++    },
++  ];
++  let fakeHosts = mockSiteDataManager.fakeSites.map(site => site.principal.URI.host);
++
++  let updatePromise = promiseSitesUpdated();
++  let doc = gBrowser.selectedBrowser.contentDocument;
++  await openPreferencesViaOpenPreferencesAPI("advanced", "networkTab", { leaveOpen: true });
++  await updatePromise;
++  await openSettingsDialog();
++  assertSitesListed(doc, fakeHosts.filter(host => host != "shopping.xyz.com"));
++
++  mockSiteDataManager.unregister();
++  await BrowserTestUtils.removeTab(gBrowser.selectedTab);
++});

+ 3296 - 0
frg/mozilla-release/work-js/1382841-1-60a1.patch

@@ -0,0 +1,3296 @@
+# HG changeset patch
+# User Brian Birtles <birtles@gmail.com>
+# Date 1520838515 -32400
+# Node ID 7582d9f9c6e3f62bcbc7111e70fa152004386711
+# Parent  4f59dedbcd8b41500492a81aad2b0d21e87ec1d4
+Bug 1382841 - Remove old test_XXX openers from dom/animation/test/**; r=hiro
+
+We rename the file_XXX contents to test_XXX_to_rename so that these tests keep
+running (so long as they begin with file_ and not test_ the test harness will
+refuse to run them). If we rename these files to test_XXX in the same patch
+Mercurial will not treat this as a rename and so we will lose the history
+associated with file_XXX.
+
+Instead, we rename test_XXX_to_rename to test_XXX in the next patch in this
+series and by doing this Mercurial will treat this as two rename operations and
+`hg log -f test_XXX` will show the history file_XXX as well.
+
+One minor exception to this occurs due to the fact that we have two naming
+conventions:
+
+  (a) For tests in css-animations, css-transitions, style etc.
+
+      Most of these tests should eventually land in web-platform-tests. However,
+      in many cases that's not yet possible because, for example, CSS
+      Animations 2 does not yet specify the behavior tested by the
+      css-animations tests.
+
+      For tests in web-platform-tests we generally separate words using -.
+      However, our mochitest running machinery requires that tests begin with
+      test_. Hence we name tests: test_abc-def-ghi.html.
+
+  (b) For tests in mozilla
+
+      These tests are never intended to be part of web-platform-tests and
+      generally for mochitests we use _ to separate words (hence the test_
+      prefix). Hence we name these tests test_abc_def_ghi.html
+
+Now, there are some tests in the 'mozilla' directory that use the (a) naming
+scheme instead of (b). In this case, instead of renaming file_xxx-xxx.html to
+test_xxx-xxx_to_rename.html in this patch, and then renaming
+test_xxx-xxx_to_rename.html to test_xxx-xxx.html in a second patch, we can just
+delete test_xxx-xxx.html and rename file_xxx-xxx.html to test_xxx_xxx.html in
+the one patch and Mercurial will recognize this as a rename because the file
+names don't overlap.
+
+MozReview-Commit-ID: Etcdmyfx0zf
+
+diff --git a/dom/animation/test/css-animations/test_animation-cancel.html b/dom/animation/test/css-animations/test_animation-cancel.html
+deleted file mode 100644
+--- a/dom/animation/test/css-animations/test_animation-cancel.html
++++ /dev/null
+@@ -1,15 +0,0 @@
+-<!doctype html>
+-<meta charset=utf-8>
+-<script src="/resources/testharness.js"></script>
+-<script src="/resources/testharnessreport.js"></script>
+-<div id="log"></div>
+-<script>
+-'use strict';
+-setup({explicit_done: true});
+-SpecialPowers.pushPrefEnv(
+-  { "set": [["dom.animations-api.core.enabled", true]]},
+-  function() {
+-    window.open("file_animation-cancel.html");
+-  });
+-</script>
+-</html>
+diff --git a/dom/animation/test/css-animations/file_animation-cancel.html b/dom/animation/test/css-animations/test_animation-cancel_to_rename.html
+rename from dom/animation/test/css-animations/file_animation-cancel.html
+rename to dom/animation/test/css-animations/test_animation-cancel_to_rename.html
+--- a/dom/animation/test/css-animations/file_animation-cancel.html
++++ b/dom/animation/test/css-animations/test_animation-cancel_to_rename.html
+@@ -1,24 +1,27 @@
+ <!doctype html>
+ <meta charset=utf-8>
++<script src="/resources/testharness.js"></script>
++<script src="/resources/testharnessreport.js"></script>
+ <script src="../testcommon.js"></script>
+ <style>
+ @keyframes translateAnim {
+   to { transform: translate(100px) }
+ }
+ @keyframes marginLeftAnim {
+   to { margin-left: 100px }
+ }
+ @keyframes marginLeftAnim100To200 {
+   from { margin-left: 100px }
+   to { margin-left: 200px }
+ }
+ </style>
+ <body>
++<div id="log"></div>
+ <script>
+ 'use strict';
+ 
+ promise_test(function(t) {
+   var div = addDiv(t, { style: 'animation: translateAnim 100s' });
+   var animation = div.getAnimations()[0];
+ 
+   return animation.ready.then(function() {
+@@ -180,12 +183,11 @@ promise_test(function(t) {
+     return waitForFrame();
+   }).then(function() {
+     assert_equals(animation.playState, 'idle');
+     assert_equals(getComputedStyle(childDiv).marginLeft, '0px');
+   });
+ }, 'Setting display:none on an ancestor element cancels animations on ' +
+    'descendants');
+ 
+-done();
+ </script>
+ </body>
+ </html>
+diff --git a/dom/animation/test/css-animations/test_animation-computed-timing.html b/dom/animation/test/css-animations/test_animation-computed-timing.html
+deleted file mode 100644
+--- a/dom/animation/test/css-animations/test_animation-computed-timing.html
++++ /dev/null
+@@ -1,16 +0,0 @@
+-<!doctype html>
+-<html>
+-<meta charset=utf-8>
+-<script src="/resources/testharness.js"></script>
+-<script src="/resources/testharnessreport.js"></script>
+-<div id="log"></div>
+-<script>
+-'use strict';
+-setup({explicit_done: true});
+-SpecialPowers.pushPrefEnv(
+-  { "set": [["dom.animations-api.core.enabled", true]]},
+-  function() {
+-    window.open("file_animation-computed-timing.html");
+-  });
+-</script>
+-</html>
+diff --git a/dom/animation/test/css-animations/file_animation-computed-timing.html b/dom/animation/test/css-animations/test_animation-computed-timing_to_rename.html
+rename from dom/animation/test/css-animations/file_animation-computed-timing.html
+rename to dom/animation/test/css-animations/test_animation-computed-timing_to_rename.html
+--- a/dom/animation/test/css-animations/file_animation-computed-timing.html
++++ b/dom/animation/test/css-animations/test_animation-computed-timing_to_rename.html
+@@ -1,18 +1,21 @@
+ <!doctype html>
+ <meta charset=utf-8>
++<script src="/resources/testharness.js"></script>
++<script src="/resources/testharnessreport.js"></script>
+ <script src="../testcommon.js"></script>
+ <style>
+ @keyframes moveAnimation {
+   from { margin-left: 100px }
+   to { margin-left: 200px }
+ }
+ </style>
+ <body>
++<div id="log"></div>
+ <script>
+ 
+ 'use strict';
+ 
+ // --------------------
+ // delay
+ // --------------------
+ test(function(t) {
+@@ -556,11 +559,10 @@ test(function(t) {
+ 
+   assert_equals(effect.getComputedTiming().currentIteration, null,
+                 'currentIteration for orphaned effect');
+ }, 'currentIteration of an AnimationEffect without an Animation');
+ 
+ // TODO: If iteration duration is Infinity, currentIteration is 0.
+ // However, we cannot set iteration duration to Infinity in CSS Animation now.
+ 
+-done();
+ </script>
+ </body>
+diff --git a/dom/animation/test/css-animations/test_animation-currenttime.html b/dom/animation/test/css-animations/test_animation-currenttime.html
+deleted file mode 100644
+--- a/dom/animation/test/css-animations/test_animation-currenttime.html
++++ /dev/null
+@@ -1,15 +0,0 @@
+-<!doctype html>
+-<meta charset=utf-8>
+-<script src="/resources/testharness.js"></script>
+-<script src="/resources/testharnessreport.js"></script>
+-<div id="log"></div>
+-<script>
+-'use strict';
+-setup({explicit_done: true});
+-SpecialPowers.pushPrefEnv(
+-  { "set": [["dom.animations-api.core.enabled", true]]},
+-  function() {
+-    window.open("file_animation-currenttime.html");
+-  });
+-</script>
+-</html>
+diff --git a/dom/animation/test/css-animations/file_animation-currenttime.html b/dom/animation/test/css-animations/test_animation-currenttime_to_rename.html
+rename from dom/animation/test/css-animations/file_animation-currenttime.html
+rename to dom/animation/test/css-animations/test_animation-currenttime_to_rename.html
+--- a/dom/animation/test/css-animations/file_animation-currenttime.html
++++ b/dom/animation/test/css-animations/test_animation-currenttime_to_rename.html
+@@ -13,19 +13,22 @@
+ }
+ 
+ @keyframes anim {
+   from { margin-left: 100px; }
+   to { margin-left: 200px; }
+ }
+ 
+     </style>
++    <script src="/resources/testharness.js"></script>
++    <script src="/resources/testharnessreport.js"></script>
+     <script src="../testcommon.js"></script>
+   </head>
+   <body>
++    <div id="log"></div>
+     <script type="text/javascript">
+ 
+ 'use strict';
+ 
+ // TODO: We should separate this test(Testing for CSS Animation events /
+ // Testing for currentTime of Web Animation).
+ // e.g:
+ //  CSS Animation events test :
+@@ -334,12 +337,11 @@ promise_test(function(t) {
+   }).then(function() {
+     assert_true(animation.currentTime < 100 * 1000,
+                 'After aborting a pause when finished, the currentTime should'
+                 + ' jump back towards the start of the animation');
+   });
+ }, 'After aborting a pause when finished, the call to play() should rewind'
+    + ' the current time');
+ 
+-done();
+     </script>
+   </body>
+ </html>
+diff --git a/dom/animation/test/css-animations/test_animation-finish.html b/dom/animation/test/css-animations/test_animation-finish.html
+deleted file mode 100644
+--- a/dom/animation/test/css-animations/test_animation-finish.html
++++ /dev/null
+@@ -1,15 +0,0 @@
+-<!doctype html>
+-<meta charset=utf-8>
+-<script src="/resources/testharness.js"></script>
+-<script src="/resources/testharnessreport.js"></script>
+-<div id="log"></div>
+-<script>
+-'use strict';
+-setup({explicit_done: true});
+-SpecialPowers.pushPrefEnv(
+-  { "set": [["dom.animations-api.core.enabled", true]]},
+-  function() {
+-    window.open("file_animation-finish.html");
+-  });
+-</script>
+-</html>
+diff --git a/dom/animation/test/css-animations/file_animation-finish.html b/dom/animation/test/css-animations/test_animation-finish_to_rename.html
+rename from dom/animation/test/css-animations/file_animation-finish.html
+rename to dom/animation/test/css-animations/test_animation-finish_to_rename.html
+--- a/dom/animation/test/css-animations/file_animation-finish.html
++++ b/dom/animation/test/css-animations/test_animation-finish_to_rename.html
+@@ -1,18 +1,21 @@
+ <!doctype html>
+ <meta charset=utf-8>
++<script src="/resources/testharness.js"></script>
++<script src="/resources/testharnessreport.js"></script>
+ <script src="../testcommon.js"></script>
+ <style>
+ @keyframes anim {
+   from { margin-left: 100px; }
+   to { margin-left: 200px; }
+ }
+ </style>
+ <body>
++<div id="log"></div>
+ <script>
+ 
+ 'use strict';
+ 
+ const ANIM_PROP_VAL = 'anim 100s';
+ const ANIM_DURATION = 100000; // ms
+ 
+ test(function(t) {
+@@ -85,11 +88,10 @@ test(function(t) {
+   assert_equals(animation.playState, 'finished',
+                 'The play state of a pause-pending animation should become ' +
+                 '"finished" after finish() is called');
+   assert_equals(animation.startTime, animation.timeline.currentTime,
+                 'The start time of a pause-pending animation should be ' +
+                 'set after calling finish()');
+ }, 'Test finish() while pause-pending with negative playbackRate');
+ 
+-done();
+ </script>
+ </body>
+diff --git a/dom/animation/test/css-animations/test_animation-finished.html b/dom/animation/test/css-animations/test_animation-finished.html
+deleted file mode 100644
+--- a/dom/animation/test/css-animations/test_animation-finished.html
++++ /dev/null
+@@ -1,15 +0,0 @@
+-<!doctype html>
+-<meta charset=utf-8>
+-<script src="/resources/testharness.js"></script>
+-<script src="/resources/testharnessreport.js"></script>
+-<div id="log"></div>
+-<script>
+-'use strict';
+-setup({explicit_done: true});
+-SpecialPowers.pushPrefEnv(
+-  { "set": [["dom.animations-api.core.enabled", true]]},
+-  function() {
+-    window.open("file_animation-finished.html");
+-  });
+-</script>
+-</html>
+diff --git a/dom/animation/test/css-animations/file_animation-finished.html b/dom/animation/test/css-animations/test_animation-finished_to_rename.html
+rename from dom/animation/test/css-animations/file_animation-finished.html
+rename to dom/animation/test/css-animations/test_animation-finished_to_rename.html
+--- a/dom/animation/test/css-animations/file_animation-finished.html
++++ b/dom/animation/test/css-animations/test_animation-finished_to_rename.html
+@@ -1,18 +1,21 @@
+ <!doctype html>
+ <meta charset=utf-8>
++<script src="/resources/testharness.js"></script>
++<script src="/resources/testharnessreport.js"></script>
+ <script src="../testcommon.js"></script>
+ <style>
+ @keyframes abc {
+   to { transform: translate(10px) }
+ }
+ @keyframes def {}
+ </style>
+ <body>
++<div id="log"></div>
+ <script>
+ 'use strict';
+ 
+ const ANIM_PROP_VAL = 'abc 100s';
+ const ANIM_DURATION = 100 * MS_PER_SEC;
+ 
+ promise_test(function(t) {
+   var div = addDiv(t);
+@@ -83,11 +86,10 @@ promise_test(function(t) {
+                   'Should not replay when animation-play-state changes to ' +
+                   '"running" on finished animation');
+     assert_equals(animation.currentTime, ANIM_DURATION,
+                   'currentTime should not change when animation-play-state ' +
+                   'changes to "running" on finished animation');
+   });
+ }, 'Test finished promise changes when animationPlayState set to running');
+ 
+-done();
+ </script>
+ </body>
+diff --git a/dom/animation/test/css-animations/test_animation-id.html b/dom/animation/test/css-animations/test_animation-id.html
+deleted file mode 100644
+--- a/dom/animation/test/css-animations/test_animation-id.html
++++ /dev/null
+@@ -1,15 +0,0 @@
+-<!doctype html>
+-<meta charset=utf-8>
+-<script src="/resources/testharness.js"></script>
+-<script src="/resources/testharnessreport.js"></script>
+-<div id="log"></div>
+-<script>
+-'use strict';
+-setup({explicit_done: true});
+-SpecialPowers.pushPrefEnv(
+-  { "set": [["dom.animations-api.core.enabled", true]]},
+-  function() {
+-    window.open("file_animation-id.html");
+-  });
+-</script>
+-</html>
+diff --git a/dom/animation/test/css-animations/file_animation-id.html b/dom/animation/test/css-animations/test_animation-id_to_rename.html
+rename from dom/animation/test/css-animations/file_animation-id.html
+rename to dom/animation/test/css-animations/test_animation-id_to_rename.html
+--- a/dom/animation/test/css-animations/file_animation-id.html
++++ b/dom/animation/test/css-animations/test_animation-id_to_rename.html
+@@ -1,24 +1,26 @@
+ <!doctype html>
+ <meta charset=utf-8>
++<script src="/resources/testharness.js"></script>
++<script src="/resources/testharnessreport.js"></script>
+ <script src="../testcommon.js"></script>
+ <style>
+ @keyframes abc { }
+ </style>
+ <body>
++<div id="log"></div>
+ <script>
+ 'use strict';
+ 
+ test(function(t) {
+   var div = addDiv(t);
+   div.style.animation = 'abc 100s';
+   var animation = div.getAnimations()[0];
+   assert_equals(animation.id, '', 'id for CSS Animation is initially empty');
+   animation.id = 'anim'
+ 
+   assert_equals(animation.id, 'anim', 'animation.id reflects the value set');
+ }, 'Animation.id for CSS Animations');
+ 
+-done();
+ </script>
+ </body>
+ </html>
+diff --git a/dom/animation/test/css-animations/test_animation-pausing.html b/dom/animation/test/css-animations/test_animation-pausing.html
+deleted file mode 100644
+--- a/dom/animation/test/css-animations/test_animation-pausing.html
++++ /dev/null
+@@ -1,16 +0,0 @@
+-<!doctype html>
+-<meta charset=utf-8>
+-<script src="/resources/testharness.js"></script>
+-<script src="/resources/testharnessreport.js"></script>
+-<div id="log"></div>
+-<script>
+-'use strict';
+-setup({explicit_done: true});
+-SpecialPowers.pushPrefEnv(
+-  { "set": [["dom.animations-api.core.enabled", true],
+-            ["dom.animations-api.pending-member.enabled", true]]},
+-  function() {
+-    window.open("file_animation-pausing.html");
+-  });
+-</script>
+-</html>
+diff --git a/dom/animation/test/css-animations/file_animation-pausing.html b/dom/animation/test/css-animations/test_animation-pausing_to_rename.html
+rename from dom/animation/test/css-animations/file_animation-pausing.html
+rename to dom/animation/test/css-animations/test_animation-pausing_to_rename.html
+--- a/dom/animation/test/css-animations/file_animation-pausing.html
++++ b/dom/animation/test/css-animations/test_animation-pausing_to_rename.html
+@@ -1,18 +1,21 @@
+ <!doctype html>
+ <meta charset=utf-8>
++<script src="/resources/testharness.js"></script>
++<script src="/resources/testharnessreport.js"></script>
+ <script src="../testcommon.js"></script>
+ <style>
+-@keyframes anim { 
++@keyframes anim {
+   0% { margin-left: 0px }
+   100% { margin-left: 10000px }
+ }
+ </style>
+ <body>
++<div id="log"></div>
+ <script>
+ 'use strict';
+ 
+ function getMarginLeft(cs) {
+   return parseFloat(cs.marginLeft);
+ }
+ 
+ promise_test(function(t) {
+@@ -156,11 +159,10 @@ promise_test(function(t) {
+                   'pause operation to complete');
+ 
+     // The ready promise should now be resolved. If it's not then test will
+     // probably time out before anything else happens that causes it to resolve.
+     return animation.ready;
+   });
+ }, 'Setting the current time completes a pending pause');
+ 
+-done();
+ </script>
+ </body>
+diff --git a/dom/animation/test/css-animations/test_animation-playstate.html b/dom/animation/test/css-animations/test_animation-playstate.html
+deleted file mode 100644
+--- a/dom/animation/test/css-animations/test_animation-playstate.html
++++ /dev/null
+@@ -1,16 +0,0 @@
+-<!doctype html>
+-<meta charset=utf-8>
+-<script src="/resources/testharness.js"></script>
+-<script src="/resources/testharnessreport.js"></script>
+-<div id="log"></div>
+-<script>
+-'use strict';
+-setup({explicit_done: true});
+-SpecialPowers.pushPrefEnv(
+-  { "set": [["dom.animations-api.core.enabled", true],
+-            ["dom.animations-api.pending-member.enabled", true]]},
+-  function() {
+-    window.open("file_animation-playstate.html");
+-  });
+-</script>
+-</html>
+diff --git a/dom/animation/test/css-animations/file_animation-playstate.html b/dom/animation/test/css-animations/test_animation-playstate_to_rename.html
+rename from dom/animation/test/css-animations/file_animation-playstate.html
+rename to dom/animation/test/css-animations/test_animation-playstate_to_rename.html
+--- a/dom/animation/test/css-animations/file_animation-playstate.html
++++ b/dom/animation/test/css-animations/test_animation-playstate_to_rename.html
+@@ -1,15 +1,18 @@
+ <!doctype html>
+ <meta charset=utf-8>
++<script src="/resources/testharness.js"></script>
++<script src="/resources/testharnessreport.js"></script>
+ <script src="../testcommon.js"></script>
+ <style>
+ @keyframes anim { }
+ </style>
+ <body>
++<div id="log"></div>
+ <script>
+ 'use strict';
+ 
+ test(function(t) {
+   var div = addDiv(t);
+   var cs = getComputedStyle(div);
+   div.style.animation = 'anim 1000s';
+   var animation = div.getAnimations()[0];
+@@ -49,11 +52,10 @@ test(function(t) {
+ test(function(t) {
+   var div = addDiv(t);
+   div.style.animation = 'anim 1000s';
+   var animation = div.getAnimations()[0];
+   animation.cancel();
+   assert_equals(animation.playState, 'idle');
+ }, 'Animation returns correct playState when cancelled');
+ 
+-done();
+ </script>
+ </body>
+diff --git a/dom/animation/test/css-animations/test_animation-ready.html b/dom/animation/test/css-animations/test_animation-ready.html
+deleted file mode 100644
+--- a/dom/animation/test/css-animations/test_animation-ready.html
++++ /dev/null
+@@ -1,16 +0,0 @@
+-<!doctype html>
+-<meta charset=utf-8>
+-<script src="/resources/testharness.js"></script>
+-<script src="/resources/testharnessreport.js"></script>
+-<div id="log"></div>
+-<script>
+-'use strict';
+-setup({explicit_done: true});
+-SpecialPowers.pushPrefEnv(
+-  { "set": [["dom.animations-api.core.enabled", true],
+-            ["dom.animations-api.pending-member.enabled", true]]},
+-  function() {
+-    window.open("file_animation-ready.html");
+-  });
+-</script>
+-</html>
+diff --git a/dom/animation/test/css-animations/file_animation-ready.html b/dom/animation/test/css-animations/test_animation-ready_to_rename.html
+rename from dom/animation/test/css-animations/file_animation-ready.html
+rename to dom/animation/test/css-animations/test_animation-ready_to_rename.html
+--- a/dom/animation/test/css-animations/file_animation-ready.html
++++ b/dom/animation/test/css-animations/test_animation-ready_to_rename.html
+@@ -1,17 +1,20 @@
+ <!doctype html>
+ <meta charset=utf-8>
++<script src="/resources/testharness.js"></script>
++<script src="/resources/testharnessreport.js"></script>
+ <script src="../testcommon.js"></script>
+ <style>
+ @keyframes abc {
+   to { transform: translate(10px) }
+ }
+ </style>
+ <body>
++<div id="log"></div>
+ <script>
+ 'use strict';
+ 
+ promise_test(function(t) {
+   var div = addDiv(t);
+   div.style.animation = 'abc 100s paused';
+   var animation = div.getAnimations()[0];
+   var originalReadyPromise = animation.ready;
+@@ -137,11 +140,10 @@ promise_test(function(t) {
+   }).then(function(resolvedAnimation) {
+     assert_equals(resolvedAnimation, animation,
+                   'Promise received when ready Promise for a pause operation'
+                   + ' is completed is the animation on which the pause was'
+                   + ' performed');
+   });
+ }, 'When a pause is complete the Promise callback gets the correct animation');
+ 
+-done();
+ </script>
+ </body>
+diff --git a/dom/animation/test/css-animations/test_animation-reverse.html b/dom/animation/test/css-animations/test_animation-reverse.html
+deleted file mode 100644
+--- a/dom/animation/test/css-animations/test_animation-reverse.html
++++ /dev/null
+@@ -1,15 +0,0 @@
+-<!doctype html>
+-<meta charset=utf-8>
+-<script src="/resources/testharness.js"></script>
+-<script src="/resources/testharnessreport.js"></script>
+-<div id="log"></div>
+-<script>
+-'use strict';
+-setup({explicit_done: true});
+-SpecialPowers.pushPrefEnv(
+-  { "set": [["dom.animations-api.core.enabled", true]]},
+-  function() {
+-    window.open("file_animation-reverse.html");
+-  });
+-</script>
+-</html>
+diff --git a/dom/animation/test/css-animations/file_animation-reverse.html b/dom/animation/test/css-animations/test_animation-reverse_to_rename.html
+rename from dom/animation/test/css-animations/file_animation-reverse.html
+rename to dom/animation/test/css-animations/test_animation-reverse_to_rename.html
+--- a/dom/animation/test/css-animations/file_animation-reverse.html
++++ b/dom/animation/test/css-animations/test_animation-reverse_to_rename.html
+@@ -1,29 +1,30 @@
+ <!doctype html>
+ <meta charset=utf-8>
++<script src="/resources/testharness.js"></script>
++<script src="/resources/testharnessreport.js"></script>
+ <script src="../testcommon.js"></script>
+ <style>
+ @keyframes anim {
+   to { transform: translate(100px) }
+ }
+ </style>
+ <body>
++<div id="log"></div>
+ <script>
+ 'use strict';
+ 
+ test(function(t) {
+   var div = addDiv(t, { style: 'animation: anim 100s' });
+   var animation = div.getAnimations()[0];
+   div.style.animation = "";
+   flushComputedStyle(div);
+ 
+   assert_equals(animation.currentTime, null);
+   animation.reverse();
+ 
+   assert_equals(animation.currentTime, 100 * MS_PER_SEC,
+     'animation.currentTime should be its effect end');
+ }, 'reverse() from idle state starts playing the animation');
+ 
+-
+-done();
+ </script>
+ </body>
+diff --git a/dom/animation/test/css-animations/test_animation-starttime.html b/dom/animation/test/css-animations/test_animation-starttime.html
+deleted file mode 100644
+--- a/dom/animation/test/css-animations/test_animation-starttime.html
++++ /dev/null
+@@ -1,15 +0,0 @@
+-<!doctype html>
+-<meta charset=utf-8>
+-<script src="/resources/testharness.js"></script>
+-<script src="/resources/testharnessreport.js"></script>
+-<div id="log"></div>
+-<script>
+-'use strict';
+-setup({explicit_done: true});
+-SpecialPowers.pushPrefEnv(
+-  { "set": [["dom.animations-api.core.enabled", true]] },
+-  function() {
+-    window.open("file_animation-starttime.html");
+-  });
+-</script>
+-</html>
+diff --git a/dom/animation/test/css-animations/file_animation-starttime.html b/dom/animation/test/css-animations/test_animation-starttime_to_rename.html
+rename from dom/animation/test/css-animations/file_animation-starttime.html
+rename to dom/animation/test/css-animations/test_animation-starttime_to_rename.html
+--- a/dom/animation/test/css-animations/file_animation-starttime.html
++++ b/dom/animation/test/css-animations/test_animation-starttime_to_rename.html
+@@ -13,19 +13,22 @@
+ }
+ 
+ @keyframes anim {
+   from { margin-left: 100px; }
+   to { margin-left: 200px; }
+ }
+ 
+     </style>
++    <script src="/resources/testharness.js"></script>
++    <script src="/resources/testharnessreport.js"></script>
+     <script src="../testcommon.js"></script>
+   </head>
+   <body>
++    <div id="log"></div>
+     <script type="text/javascript">
+ 
+ 'use strict';
+ 
+ // TODO: We should separate this test(Testing for CSS Animation events /
+ // Testing for start time of Web Animation).
+ // e.g:
+ //  CSS Animation events test:
+@@ -372,12 +375,11 @@ promise_test(function(t) {
+ 
+   return animation.ready.then(function() {
+     animation.cancel();
+     assert_equals(animation.startTime, null,
+                   'The startTime of a cancelled animation should be null');
+   });
+ }, 'Animation.startTime after cancelling');
+ 
+-done();
+     </script>
+   </body>
+ </html>
+diff --git a/dom/animation/test/css-animations/test_animations-dynamic-changes.html b/dom/animation/test/css-animations/test_animations-dynamic-changes.html
+deleted file mode 100644
+--- a/dom/animation/test/css-animations/test_animations-dynamic-changes.html
++++ /dev/null
+@@ -1,15 +0,0 @@
+-<!doctype html>
+-<meta charset=utf-8>
+-<script src="/resources/testharness.js"></script>
+-<script src="/resources/testharnessreport.js"></script>
+-<div id="log"></div>
+-<script>
+-'use strict';
+-setup({explicit_done: true});
+-SpecialPowers.pushPrefEnv(
+-  { "set": [["dom.animations-api.core.enabled", true]]},
+-  function() {
+-    window.open("file_animations-dynamic-changes.html");
+-  });
+-</script>
+-</html>
+diff --git a/dom/animation/test/css-animations/file_animations-dynamic-changes.html b/dom/animation/test/css-animations/test_animations-dynamic-changes_to_rename.html
+rename from dom/animation/test/css-animations/file_animations-dynamic-changes.html
+rename to dom/animation/test/css-animations/test_animations-dynamic-changes_to_rename.html
+--- a/dom/animation/test/css-animations/file_animations-dynamic-changes.html
++++ b/dom/animation/test/css-animations/test_animations-dynamic-changes_to_rename.html
+@@ -1,18 +1,21 @@
+ <!doctype html>
+ <meta charset=utf-8>
++<script src="/resources/testharness.js"></script>
++<script src="/resources/testharnessreport.js"></script>
+ <script src="../testcommon.js"></script>
+ <style>
+ @keyframes anim1 {
+   to { left: 100px }
+ }
+ @keyframes anim2 { }
+ </style>
+ <body>
++<div id="log"></div>
+ <script>
+ 'use strict';
+ 
+ promise_test(function(t) {
+   var div = addDiv(t);
+   div.style.animation = 'anim1 100s';
+   var originalAnimation = div.getAnimations()[0];
+   var originalStartTime;
+@@ -146,11 +149,10 @@ promise_test(function(t) {
+     assert_greater_than(animations[1].startTime, animations[0].startTime,
+                         'Interleaved animation starts later than existing ' +
+                         'animations');
+     assert_greater_than(animations[0].startTime, animations[2].startTime,
+                         'Original animations retain their start time');
+   });
+ }, 'Animation state is preserved when interleaving animations in list');
+ 
+-done();
+ </script>
+ </body>
+diff --git a/dom/animation/test/css-animations/test_cssanimation-animationname.html b/dom/animation/test/css-animations/test_cssanimation-animationname.html
+deleted file mode 100644
+--- a/dom/animation/test/css-animations/test_cssanimation-animationname.html
++++ /dev/null
+@@ -1,15 +0,0 @@
+-<!doctype html>
+-<meta charset=utf-8>
+-<script src="/resources/testharness.js"></script>
+-<script src="/resources/testharnessreport.js"></script>
+-<div id="log"></div>
+-<script>
+-'use strict';
+-setup({explicit_done: true});
+-SpecialPowers.pushPrefEnv(
+-  { "set": [["dom.animations-api.core.enabled", true]]},
+-  function() {
+-    window.open("file_cssanimation-animationname.html");
+-  });
+-</script>
+-</html>
+diff --git a/dom/animation/test/css-animations/file_cssanimation-animationname.html b/dom/animation/test/css-animations/test_cssanimation-animationname_to_rename.html
+rename from dom/animation/test/css-animations/file_cssanimation-animationname.html
+rename to dom/animation/test/css-animations/test_cssanimation-animationname_to_rename.html
+--- a/dom/animation/test/css-animations/file_cssanimation-animationname.html
++++ b/dom/animation/test/css-animations/test_cssanimation-animationname_to_rename.html
+@@ -1,17 +1,20 @@
+ <!doctype html>
+ <meta charset=utf-8>
++<script src="/resources/testharness.js"></script>
++<script src="/resources/testharnessreport.js"></script>
+ <script src="../testcommon.js"></script>
+ <style>
+ @keyframes xyz {
+   to { left: 100px }
+ }
+ </style>
+ <body>
++<div id="log"></div>
+ <script>
+ 'use strict';
+ 
+ test(function(t) {
+   var div = addDiv(t);
+   div.style.animation = 'xyz 100s';
+   assert_equals(div.getAnimations()[0].animationName, 'xyz',
+                 'Animation name matches keyframes rule name');
+@@ -27,11 +30,10 @@ test(function(t) {
+ test(function(t) {
+   var div = addDiv(t);
+   div.style.animation = 'x\\79 z 100s';
+   assert_equals(div.getAnimations()[0].animationName, 'xyz',
+                 'Hex-escaped animation name matches keyframes rule'
+                 + ' name');
+ }, 'Animation name with hex-escape');
+ 
+-done();
+ </script>
+ </body>
+diff --git a/dom/animation/test/css-animations/test_document-get-animations.html b/dom/animation/test/css-animations/test_document-get-animations.html
+deleted file mode 100644
+--- a/dom/animation/test/css-animations/test_document-get-animations.html
++++ /dev/null
+@@ -1,15 +0,0 @@
+-<!doctype html>
+-<meta charset=utf-8>
+-<script src="/resources/testharness.js"></script>
+-<script src="/resources/testharnessreport.js"></script>
+-<div id="log"></div>
+-<script>
+-'use strict';
+-setup({explicit_done: true});
+-SpecialPowers.pushPrefEnv(
+-  { "set": [["dom.animations-api.core.enabled", true]]},
+-  function() {
+-    window.open("file_document-get-animations.html");
+-  });
+-</script>
+-</html>
+diff --git a/dom/animation/test/css-animations/file_document-get-animations.html b/dom/animation/test/css-animations/test_document-get-animations_to_rename.html
+rename from dom/animation/test/css-animations/file_document-get-animations.html
+rename to dom/animation/test/css-animations/test_document-get-animations_to_rename.html
+--- a/dom/animation/test/css-animations/file_document-get-animations.html
++++ b/dom/animation/test/css-animations/test_document-get-animations_to_rename.html
+@@ -1,10 +1,12 @@
+ <!doctype html>
+ <meta charset=utf-8>
++<script src="/resources/testharness.js"></script>
++<script src="/resources/testharnessreport.js"></script>
+ <script src="../testcommon.js"></script>
+ <style>
+ @keyframes animLeft {
+   to { left: 100px }
+ }
+ @keyframes animTop {
+   to { top: 100px }
+ }
+@@ -17,16 +19,17 @@
+ ::before {
+   content: ''
+ }
+ ::after {
+   content: ''
+ }
+ </style>
+ <body>
++<div id="log"></div>
+ <script>
+ 'use strict';
+ 
+ test(function(t) {
+   assert_equals(document.getAnimations().length, 0,
+     'getAnimations returns an empty sequence for a document'
+     + ' with no animations');
+ }, 'getAnimations for non-animated content');
+@@ -272,11 +275,10 @@ test(function(t) {
+                 'The animation targeting the ::before element comes second');
+   assert_equals(anims[2].effect.target.type, '::after',
+                 'The animation targeting the ::after element comes third');
+   assert_equals(anims[3].effect.target, child,
+                 'The animation targeting the child element comes last');
+ }, 'CSS Animations targetting (pseudo-)elements should have correct order ' +
+    'after sorting');
+ 
+-done();
+ </script>
+ </body>
+diff --git a/dom/animation/test/css-animations/test_effect-target.html b/dom/animation/test/css-animations/test_effect-target.html
+deleted file mode 100644
+--- a/dom/animation/test/css-animations/test_effect-target.html
++++ /dev/null
+@@ -1,15 +0,0 @@
+-<!doctype html>
+-<meta charset=utf-8>
+-<script src="/resources/testharness.js"></script>
+-<script src="/resources/testharnessreport.js"></script>
+-<div id="log"></div>
+-<script>
+-'use strict';
+-setup({explicit_done: true});
+-SpecialPowers.pushPrefEnv(
+-  { "set": [["dom.animations-api.core.enabled", true]]},
+-  function() {
+-    window.open("file_effect-target.html");
+-  });
+-</script>
+-</html>
+diff --git a/dom/animation/test/css-animations/file_effect-target.html b/dom/animation/test/css-animations/test_effect-target_to_rename.html
+rename from dom/animation/test/css-animations/file_effect-target.html
+rename to dom/animation/test/css-animations/test_effect-target_to_rename.html
+--- a/dom/animation/test/css-animations/file_effect-target.html
++++ b/dom/animation/test/css-animations/test_effect-target_to_rename.html
+@@ -1,21 +1,24 @@
+ <!doctype html>
+ <meta charset=utf-8>
++<script src="/resources/testharness.js"></script>
++<script src="/resources/testharnessreport.js"></script>
+ <script src="../testcommon.js"></script>
+ <style>
+ @keyframes anim { }
+ ::before {
+   content: ''
+ }
+ ::after {
+   content: ''
+ }
+ </style>
+ <body>
++<div id="log"></div>
+ <script>
+ 'use strict';
+ 
+ test(function(t) {
+   var div = addDiv(t);
+   div.style.animation = 'anim 100s';
+   var animation = div.getAnimations()[0];
+   assert_equals(animation.effect.target, div,
+@@ -50,11 +53,10 @@ test(function(t) {
+                 'The effect.target of the scripted-generated animation is ' +
+                 'the same as the one from the argument of ' +
+                 'KeyframeEffectReadOnly constructor');
+   assert_equals(anims[0].effect.target, newAnim.effect.target,
+                 'Both animations return the same target object');
+ }, 'effect.target from the script-generated animation should return the same ' +
+    'CSSPseudoElement object as that from the CSS generated animation');
+ 
+-done();
+ </script>
+ </body>
+diff --git a/dom/animation/test/css-animations/test_element-get-animations.html b/dom/animation/test/css-animations/test_element-get-animations.html
+deleted file mode 100644
+--- a/dom/animation/test/css-animations/test_element-get-animations.html
++++ /dev/null
+@@ -1,16 +0,0 @@
+-<!doctype html>
+-<meta charset=utf-8>
+-<script src="/resources/testharness.js"></script>
+-<script src="/resources/testharnessreport.js"></script>
+-<div id="log"></div>
+-<script>
+-'use strict';
+-setup({explicit_done: true});
+-SpecialPowers.pushPrefEnv(
+-  { "set": [["dom.animations-api.core.enabled", true],
+-            ["dom.animations-api.pending-member.enabled", true]]},
+-  function() {
+-    window.open("file_element-get-animations.html");
+-  });
+-</script>
+-</html>
+diff --git a/dom/animation/test/css-animations/file_element-get-animations.html b/dom/animation/test/css-animations/test_element-get-animations_to_rename.html
+rename from dom/animation/test/css-animations/file_element-get-animations.html
+rename to dom/animation/test/css-animations/test_element-get-animations_to_rename.html
+--- a/dom/animation/test/css-animations/file_element-get-animations.html
++++ b/dom/animation/test/css-animations/test_element-get-animations_to_rename.html
+@@ -1,10 +1,12 @@
+ <!doctype html>
+ <meta charset=utf-8>
++<script src="/resources/testharness.js"></script>
++<script src="/resources/testharnessreport.js"></script>
+ <script src="../testcommon.js"></script>
+ <style>
+ @keyframes anim1 {
+   to { left: 100px }
+ }
+ @keyframes anim2 {
+   to { top: 100px }
+ }
+@@ -15,16 +17,17 @@
+   content: ''
+ }
+ ::after {
+   content: ''
+ }
+ @keyframes empty { }
+ </style>
+ <body>
++<div id="log"></div>
+ <script>
+ 'use strict';
+ 
+ test(function(t) {
+   var div = addDiv(t);
+   assert_equals(div.getAnimations().length, 0,
+     'getAnimations returns an empty sequence for an element'
+     + ' with no animations');
+@@ -441,11 +444,10 @@ test(function(t) {
+                 'should be returned fourth');
+ 
+   assert_equals(animations[4].effect.target, child2,
+                 'The animation targeting the child2 element ' +
+                 'should be returned last');
+ 
+ }, 'Test AnimationFilter{ subtree: true } with element that has many descendant');
+ 
+-done();
+ </script>
+ </body>
+diff --git a/dom/animation/test/css-animations/test_event-dispatch.html b/dom/animation/test/css-animations/test_event-dispatch.html
+deleted file mode 100644
+--- a/dom/animation/test/css-animations/test_event-dispatch.html
++++ /dev/null
+@@ -1,15 +0,0 @@
+-<!doctype html>
+-<meta charset=utf-8>
+-<script src="/resources/testharness.js"></script>
+-<script src="/resources/testharnessreport.js"></script>
+-<div id="log"></div>
+-<script>
+-'use strict';
+-setup({explicit_done: true});
+-SpecialPowers.pushPrefEnv(
+-  { "set": [["dom.animations-api.core.enabled", true]] },
+-  function() {
+-    window.open("file_event-dispatch.html");
+-  });
+-</script>
+-</html>
+diff --git a/dom/animation/test/css-animations/file_event-dispatch.html b/dom/animation/test/css-animations/test_event-dispatch_to_rename.html
+rename from dom/animation/test/css-animations/file_event-dispatch.html
+rename to dom/animation/test/css-animations/test_event-dispatch_to_rename.html
+--- a/dom/animation/test/css-animations/file_event-dispatch.html
++++ b/dom/animation/test/css-animations/test_event-dispatch_to_rename.html
+@@ -1,20 +1,23 @@
+ <!doctype html>
+ <meta charset=utf-8>
+ <title>Tests for CSS animation event dispatch</title>
+ <link rel="help" href="https://drafts.csswg.org/css-animations-2/#event-dispatch"/>
++<script src="/resources/testharness.js"></script>
++<script src="/resources/testharnessreport.js"></script>
+ <script src="../testcommon.js"></script>
+ <style>
+   @keyframes anim {
+     from { margin-left: 0px; }
+     to { margin-left: 100px; }
+   }
+ </style>
+ <body>
++<div id="log"></div>
+ <script>
+ 'use strict';
+ 
+ /**
+  * Helper class to record the elapsedTime member of each event.
+  * The EventWatcher class in testharness.js allows us to wait on
+  * multiple events in a certain order but only records the event
+  * parameters of the most recent event.
+@@ -367,12 +370,11 @@ promise_test(t => {
+     return watcher.wait_for('animationend');
+   }).then(evt => {
+     animation.cancel();
+     // Then wait a couple of frames and check that no event was dispatched.
+     return waitForAnimationFrames(2);
+   });
+ }, 'Cancel the animation after clearing the target effect.');
+ 
+-done();
+ </script>
+ </body>
+ </html>
+diff --git a/dom/animation/test/css-animations/test_event-order.html b/dom/animation/test/css-animations/test_event-order.html
+deleted file mode 100644
+--- a/dom/animation/test/css-animations/test_event-order.html
++++ /dev/null
+@@ -1,15 +0,0 @@
+-<!doctype html>
+-<meta charset=utf-8>
+-<script src="/resources/testharness.js"></script>
+-<script src="/resources/testharnessreport.js"></script>
+-<div id="log"></div>
+-<script>
+-'use strict';
+-setup({explicit_done: true});
+-SpecialPowers.pushPrefEnv(
+-  { "set": [["dom.animations-api.core.enabled", true]]},
+-  function() {
+-    window.open("file_event-order.html");
+-  });
+-</script>
+-</html>
+diff --git a/dom/animation/test/css-animations/file_event-order.html b/dom/animation/test/css-animations/test_event-order_to_rename.html
+rename from dom/animation/test/css-animations/file_event-order.html
+rename to dom/animation/test/css-animations/test_event-order_to_rename.html
+--- a/dom/animation/test/css-animations/file_event-order.html
++++ b/dom/animation/test/css-animations/test_event-order_to_rename.html
+@@ -1,20 +1,23 @@
+ <!doctype html>
+ <meta charset=utf-8>
+ <title>Tests for CSS animation event order</title>
+ <link rel="help" href="https://drafts.csswg.org/css-animations-2/#event-dispatch"/>
++<script src="/resources/testharness.js"></script>
++<script src="/resources/testharnessreport.js"></script>
+ <script src="../testcommon.js"></script>
+ <style>
+   @keyframes anim {
+     from { margin-left: 0px; }
+     to { margin-left: 100px; }
+   }
+ </style>
+ <body>
++<div id="log"></div>
+ <script type='text/javascript'>
+ 'use strict';
+ 
+ /**
+  * Asserts that the set of actual and received events match.
+  * @param actualEvents   An array of the received AnimationEvent objects.
+  * @param expectedEvents A series of array objects representing the expected
+  *        events, each having the form:
+@@ -150,12 +153,11 @@ promise_test(t => {
+                                            'animationend' ]) ]).then(() => {
+     checkEvents(events, ['animationstart', div2, 0],
+                         ['animationstart', div1, 0],
+                         ['animationend',   div1, 100],
+                         ['animationend',   div2, 200]);
+   });
+ }, 'Test start and end events are sorted correctly when fired simultaneously');
+ 
+-done();
+ </script>
+ </body>
+ </html>
+diff --git a/dom/animation/test/css-animations/test_keyframeeffect-getkeyframes.html b/dom/animation/test/css-animations/test_keyframeeffect-getkeyframes.html
+deleted file mode 100644
+--- a/dom/animation/test/css-animations/test_keyframeeffect-getkeyframes.html
++++ /dev/null
+@@ -1,15 +0,0 @@
+-<!doctype html>
+-<meta charset=utf-8>
+-<script src="/resources/testharness.js"></script>
+-<script src="/resources/testharnessreport.js"></script>
+-<div id="log"></div>
+-<script>
+-'use strict';
+-setup({explicit_done: true});
+-SpecialPowers.pushPrefEnv(
+-  { "set": [["dom.animations-api.core.enabled", true]]},
+-  function() {
+-    window.open("file_keyframeeffect-getkeyframes.html");
+-  });
+-</script>
+-</html>
+diff --git a/dom/animation/test/css-animations/file_keyframeeffect-getkeyframes.html b/dom/animation/test/css-animations/test_keyframeeffect-getkeyframes_to_rename.html
+rename from dom/animation/test/css-animations/file_keyframeeffect-getkeyframes.html
+rename to dom/animation/test/css-animations/test_keyframeeffect-getkeyframes_to_rename.html
+--- a/dom/animation/test/css-animations/file_keyframeeffect-getkeyframes.html
++++ b/dom/animation/test/css-animations/test_keyframeeffect-getkeyframes_to_rename.html
+@@ -1,10 +1,12 @@
+ <!doctype html>
+ <meta charset=utf-8>
++<script src="/resources/testharness.js"></script>
++<script src="/resources/testharnessreport.js"></script>
+ <script src="../testcommon.js"></script>
+ <style>
+ @keyframes anim-empty { }
+ 
+ @keyframes anim-empty-frames {
+   from { }
+   to   { }
+ }
+@@ -148,16 +150,17 @@
+   to { --end-color: rgb(0, 255, 0); color: var(--end-color) }
+ }
+ @keyframes anim-only-custom-property-in-keyframe {
+   from { transform: translate(100px, 0) }
+   to { --not-used: 200px }
+ }
+ </style>
+ <body>
++<div id="log"></div>
+ <script>
+ "use strict";
+ 
+ function getKeyframes(e) {
+   return e.getAnimations()[0].effect.getKeyframes();
+ }
+ 
+ function assert_frames_equal(a, b, name) {
+@@ -744,11 +747,10 @@ test(function(t) {
+       transform: "none" },
+   ];
+   for (var i = 0; i < frames.length; i++) {
+     assert_frames_equal(frames[i], expected[i], "ComputedKeyframe #" + i);
+   }
+ }, 'KeyframeEffectReadOnly.getKeyframes() returns expected values for ' +
+    'animations with only custom property in a keyframe');
+ 
+-done();
+ </script>
+ </body>
+diff --git a/dom/animation/test/css-animations/test_pseudoElement-get-animations.html b/dom/animation/test/css-animations/test_pseudoElement-get-animations.html
+deleted file mode 100644
+--- a/dom/animation/test/css-animations/test_pseudoElement-get-animations.html
++++ /dev/null
+@@ -1,14 +0,0 @@
+-<!doctype html>
+-<meta charset=utf-8>
+-<script src="/resources/testharness.js"></script>
+-<script src="/resources/testharnessreport.js"></script>
+-<div id="log"></div>
+-<script>
+-'use strict';
+-setup({explicit_done: true});
+-SpecialPowers.pushPrefEnv(
+-  { "set": [["dom.animations-api.core.enabled", true]]},
+-  function() {
+-    window.open("file_pseudoElement-get-animations.html");
+-  });
+-</script>
+diff --git a/dom/animation/test/css-animations/file_pseudoElement-get-animations.html b/dom/animation/test/css-animations/test_pseudoElement-get-animations_to_rename.html
+rename from dom/animation/test/css-animations/file_pseudoElement-get-animations.html
+rename to dom/animation/test/css-animations/test_pseudoElement-get-animations_to_rename.html
+--- a/dom/animation/test/css-animations/file_pseudoElement-get-animations.html
++++ b/dom/animation/test/css-animations/test_pseudoElement-get-animations_to_rename.html
+@@ -1,10 +1,12 @@
+ <!doctype html>
+ <meta charset=utf-8>
++<script src="/resources/testharness.js"></script>
++<script src="/resources/testharnessreport.js"></script>
+ <script src="../testcommon.js"></script>
+ <style>
+ @keyframes anim1 { }
+ @keyframes anim2 { }
+ .before::before {
+   animation: anim1 10s;
+   content: '';
+ }
+@@ -17,16 +19,17 @@
+ }
+ .after-change::after {
+   width: 100px;
+   height: 100px;
+   content: '';
+ }
+ </style>
+ <body>
++<div id="log"></div>
+ <script>
+ 'use strict';
+ 
+ test(function(t) {
+   var div = addDiv(t, { class: 'before' });
+   var pseudoTarget = document.getAnimations()[0].effect.target;
+   assert_equals(pseudoTarget.getAnimations().length, 1,
+                 'Expected number of animations are returned');
+@@ -62,11 +65,10 @@ test(function(t) {
+                 '3rd animation is the 1st animation in animation-name list');
+   assert_equals(anims[3].animationName, 'anim2',
+                 '4rd animation is the 2nd animation in animation-name list');
+   assert_equals(anims[4].id, 'scripted-anim',
+                 'Animation added by script appears last');
+ }, 'getAnimations returns css transitions/animations, and script-generated ' +
+    'animations in the expected order');
+ 
+-done();
+ </script>
+ </body>
+diff --git a/dom/animation/test/css-animations/test_setting-effect.html b/dom/animation/test/css-animations/test_setting-effect.html
+deleted file mode 100644
+--- a/dom/animation/test/css-animations/test_setting-effect.html
++++ /dev/null
+@@ -1,15 +0,0 @@
+-<!doctype html>
+-<meta charset=utf-8>
+-<script src='/resources/testharness.js'></script>
+-<script src='/resources/testharnessreport.js'></script>
+-<div id='log'></div>
+-<script>
+-'use strict';
+-setup({explicit_done: true});
+-SpecialPowers.pushPrefEnv(
+-  { "set": [["dom.animations-api.core.enabled", true],
+-            ["dom.animations-api.pending-member.enabled", true]]},
+-  function() {
+-    window.open('file_setting-effect.html');
+-  });
+-</script>
+diff --git a/dom/animation/test/css-animations/file_setting-effect.html b/dom/animation/test/css-animations/test_setting-effect_to_rename.html
+rename from dom/animation/test/css-animations/file_setting-effect.html
+rename to dom/animation/test/css-animations/test_setting-effect_to_rename.html
+--- a/dom/animation/test/css-animations/file_setting-effect.html
++++ b/dom/animation/test/css-animations/test_setting-effect_to_rename.html
+@@ -1,22 +1,25 @@
+ <!doctype html>
+ <meta charset=utf-8>
++<script src="/resources/testharness.js"></script>
++<script src="/resources/testharnessreport.js"></script>
+ <script src='../testcommon.js'></script>
+ <style>
+   @keyframes anim {
+     from {
+       margin-left: 0px;
+     }
+     to {
+       margin-left: 100px;
+     }
+   }
+ </style>
+ <body>
++<div id="log"></div>
+ <script>
+ 'use strict';
+ 
+ promise_test(function(t) {
+   var div = addDiv(t);
+   div.style.animation = 'anim 100s';
+ 
+   var watcher = new EventWatcher(t, div, [ 'animationend',
+@@ -120,11 +123,10 @@ promise_test(function(t) {
+     animation.effect = new KeyframeEffect(div,
+                                           { left: [ '0px', '100px' ] },
+                                           200 * MS_PER_SEC);
+     return watcher.wait_for('animationstart');
+   });
+ }, 'After replacing a finished animation\'s effect with a longer one ' +
+    'it fires an animationstart event');
+ 
+-done();
+ </script>
+ </body>
+diff --git a/dom/animation/test/css-transitions/test_animation-cancel.html b/dom/animation/test/css-transitions/test_animation-cancel.html
+deleted file mode 100644
+--- a/dom/animation/test/css-transitions/test_animation-cancel.html
++++ /dev/null
+@@ -1,14 +0,0 @@
+-<!doctype html>
+-<meta charset=utf-8>
+-<script src="/resources/testharness.js"></script>
+-<script src="/resources/testharnessreport.js"></script>
+-<div id="log"></div>
+-<script>
+-'use strict';
+-setup({explicit_done: true});
+-SpecialPowers.pushPrefEnv(
+-  { "set": [["dom.animations-api.core.enabled", true]]},
+-  function() {
+-    window.open("file_animation-cancel.html");
+-  });
+-</script>
+diff --git a/dom/animation/test/css-transitions/file_animation-cancel.html b/dom/animation/test/css-transitions/test_animation-cancel_to_rename.html
+rename from dom/animation/test/css-transitions/file_animation-cancel.html
+rename to dom/animation/test/css-transitions/test_animation-cancel_to_rename.html
+--- a/dom/animation/test/css-transitions/file_animation-cancel.html
++++ b/dom/animation/test/css-transitions/test_animation-cancel_to_rename.html
+@@ -1,12 +1,15 @@
+ <!doctype html>
+ <meta charset=utf-8>
++<script src="/resources/testharness.js"></script>
++<script src="/resources/testharnessreport.js"></script>
+ <script src="../testcommon.js"></script>
+ <body>
++<div id="log"></div>
+ <script>
+ 'use strict';
+ 
+ promise_test(function(t) {
+   var div = addDiv(t, { style: 'margin-left: 0px' });
+   flushComputedStyle(div);
+ 
+   div.style.transition = 'margin-left 100s';
+@@ -223,11 +226,10 @@ promise_test(function(t) {
+     div.style.marginLeft = '0px';
+     flushComputedStyle(div);
+     return waitForFrame();
+   }).then(function() {
+     assert_equals(transition.playState, 'idle');
+   });
+ }, 'Reversing a running transition cancels the original transition');
+ 
+-done();
+ </script>
+ </body>
+diff --git a/dom/animation/test/css-transitions/test_animation-computed-timing.html b/dom/animation/test/css-transitions/test_animation-computed-timing.html
+deleted file mode 100644
+--- a/dom/animation/test/css-transitions/test_animation-computed-timing.html
++++ /dev/null
+@@ -1,16 +0,0 @@
+-<!doctype html>
+-<html>
+-<meta charset=utf-8>
+-<script src="/resources/testharness.js"></script>
+-<script src="/resources/testharnessreport.js"></script>
+-<div id="log"></div>
+-<script>
+-'use strict';
+-setup({explicit_done: true});
+-SpecialPowers.pushPrefEnv(
+-  { "set": [["dom.animations-api.core.enabled", true]]},
+-  function() {
+-    window.open("file_animation-computed-timing.html");
+-  });
+-</script>
+-</html>
+diff --git a/dom/animation/test/css-transitions/file_animation-computed-timing.html b/dom/animation/test/css-transitions/test_animation-computed-timing_to_rename.html
+rename from dom/animation/test/css-transitions/file_animation-computed-timing.html
+rename to dom/animation/test/css-transitions/test_animation-computed-timing_to_rename.html
+--- a/dom/animation/test/css-transitions/file_animation-computed-timing.html
++++ b/dom/animation/test/css-transitions/test_animation-computed-timing_to_rename.html
+@@ -1,19 +1,22 @@
+ <!doctype html>
+ <meta charset=utf-8>
++<script src="/resources/testharness.js"></script>
++<script src="/resources/testharnessreport.js"></script>
+ <script src="../testcommon.js"></script>
+ <style>
+ 
+ .animated-div {
+   margin-left: 100px;
+ }
+ 
+ </style>
+ <body>
++<div id="log"></div>
+ <script>
+ 
+ 'use strict';
+ 
+ // --------------------
+ // delay
+ // --------------------
+ test(function(t) {
+@@ -305,11 +308,10 @@ test(function(t) {
+   div.style.marginLeft = '10px';
+ 
+   var anim = div.getAnimations()[0];
+   anim.finish();
+   assert_equals(anim.effect.getComputedTiming().currentIteration, null,
+                 'finished currentIteration');
+ }, 'currentIteration of a finished transition');
+ 
+-done();
+ </script>
+ </body>
+diff --git a/dom/animation/test/css-transitions/test_animation-currenttime.html b/dom/animation/test/css-transitions/test_animation-currenttime.html
+deleted file mode 100644
+--- a/dom/animation/test/css-transitions/test_animation-currenttime.html
++++ /dev/null
+@@ -1,14 +0,0 @@
+-<!doctype html>
+-<meta charset=utf-8>
+-<script src="/resources/testharness.js"></script>
+-<script src="/resources/testharnessreport.js"></script>
+-<div id="log"></div>
+-<script>
+-'use strict';
+-setup({explicit_done: true});
+-SpecialPowers.pushPrefEnv(
+-  { "set": [["dom.animations-api.core.enabled", true]]},
+-  function() {
+-    window.open("file_animation-currenttime.html");
+-  });
+-</script>
+diff --git a/dom/animation/test/css-transitions/file_animation-currenttime.html b/dom/animation/test/css-transitions/test_animation-currenttime_to_rename.html
+rename from dom/animation/test/css-transitions/file_animation-currenttime.html
+rename to dom/animation/test/css-transitions/test_animation-currenttime_to_rename.html
+--- a/dom/animation/test/css-transitions/file_animation-currenttime.html
++++ b/dom/animation/test/css-transitions/test_animation-currenttime_to_rename.html
+@@ -7,19 +7,22 @@
+     <style>
+ 
+ .animated-div {
+   margin-left: 100px;
+   transition: margin-left 1000s linear 1000s;
+ }
+ 
+     </style>
++    <script src="/resources/testharness.js"></script>
++    <script src="/resources/testharnessreport.js"></script>
+     <script src="../testcommon.js"></script>
+   </head>
+   <body>
++    <div id="log"></div>
+     <script type="text/javascript">
+ 
+ 'use strict';
+ 
+ // TODO: Once the computedTiming property is implemented, add checks to the
+ // checker helpers to ensure that computedTiming's properties are updated as
+ // expected.
+ // See https://bugzilla.mozilla.org/show_bug.cgi?id=1108055
+@@ -296,12 +299,11 @@ async_test(function(t) {
+       'Animation.currentTime is unchanged after pausing');
+   })).catch(t.step_func(function(reason) {
+     assert_unreached(reason);
+   })).then(function() {
+     t.done();
+   });
+ }, 'Animation.currentTime after pausing');
+ 
+-done();
+     </script>
+   </body>
+ </html>
+diff --git a/dom/animation/test/css-transitions/test_animation-finished.html b/dom/animation/test/css-transitions/test_animation-finished.html
+deleted file mode 100644
+--- a/dom/animation/test/css-transitions/test_animation-finished.html
++++ /dev/null
+@@ -1,14 +0,0 @@
+-<!doctype html>
+-<meta charset=utf-8>
+-<script src="/resources/testharness.js"></script>
+-<script src="/resources/testharnessreport.js"></script>
+-<div id="log"></div>
+-<script>
+-'use strict';
+-setup({explicit_done: true});
+-SpecialPowers.pushPrefEnv(
+-  { "set": [["dom.animations-api.core.enabled", true]]},
+-  function() {
+-    window.open("file_animation-finished.html");
+-  });
+-</script>
+diff --git a/dom/animation/test/css-transitions/file_animation-finished.html b/dom/animation/test/css-transitions/test_animation-finished_to_rename.html
+rename from dom/animation/test/css-transitions/file_animation-finished.html
+rename to dom/animation/test/css-transitions/test_animation-finished_to_rename.html
+--- a/dom/animation/test/css-transitions/file_animation-finished.html
++++ b/dom/animation/test/css-transitions/test_animation-finished_to_rename.html
+@@ -1,20 +1,23 @@
+ <!doctype html>
+ <meta charset=utf-8>
++<script src="/resources/testharness.js"></script>
++<script src="/resources/testharnessreport.js"></script>
+ <script src="../testcommon.js"></script>
+ <style>
+ 
+ .animated-div {
+   margin-left: 100px;
+   transition: margin-left 1000s linear 1000s;
+ }
+ 
+ </style>
+ <body>
++<div id="log"></div>
+ <script>
+ 
+ 'use strict';
+ 
+ const ANIM_DELAY_MS = 1000000; // 1000s
+ const ANIM_DUR_MS = 1000000; // 1000s
+ 
+ async_test(function(t) {
+@@ -51,11 +54,10 @@ async_test(function(t) {
+     // 1108055) we should use that here.
+     assert_equals(animation.currentTime, ANIM_DELAY_MS + ANIM_DUR_MS,
+                   'Replaying a finished reversed transition should reset ' +
+                   'its currentTime to the end of the effect');
+     t.done();
+   }));
+ }, 'Test restarting a reversed finished transition');
+ 
+-done();
+ </script>
+ </body>
+diff --git a/dom/animation/test/css-transitions/test_animation-pausing.html b/dom/animation/test/css-transitions/test_animation-pausing.html
+deleted file mode 100644
+--- a/dom/animation/test/css-transitions/test_animation-pausing.html
++++ /dev/null
+@@ -1,14 +0,0 @@
+-<!doctype html>
+-<meta charset=utf-8>
+-<script src="/resources/testharness.js"></script>
+-<script src="/resources/testharnessreport.js"></script>
+-<div id="log"></div>
+-<script>
+-'use strict';
+-setup({explicit_done: true});
+-SpecialPowers.pushPrefEnv(
+-  { "set": [["dom.animations-api.core.enabled", true]]},
+-  function() {
+-    window.open("file_animation-pausing.html");
+-  });
+-</script>
+diff --git a/dom/animation/test/css-transitions/file_animation-pausing.html b/dom/animation/test/css-transitions/test_animation-pausing_to_rename.html
+rename from dom/animation/test/css-transitions/file_animation-pausing.html
+rename to dom/animation/test/css-transitions/test_animation-pausing_to_rename.html
+--- a/dom/animation/test/css-transitions/file_animation-pausing.html
++++ b/dom/animation/test/css-transitions/test_animation-pausing_to_rename.html
+@@ -1,12 +1,15 @@
+ <!doctype html>
+ <meta charset=utf-8>
++<script src="/resources/testharness.js"></script>
++<script src="/resources/testharnessreport.js"></script>
+ <script src="../testcommon.js"></script>
+ <body>
++<div id="log"></div>
+ <script>
+ 'use strict';
+ 
+ async_test(function(t) {
+   var div = addDiv(t);
+   var cs = getComputedStyle(div);
+ 
+   div.style.marginLeft = '0px';
+@@ -38,11 +41,10 @@ async_test(function(t) {
+   })).then(t.step_func(function() {
+     assert_greater_than(animation.effect.getComputedTiming().progress,
+                         previousProgress,
+                         'Iteration progress increases after calling play()');
+     t.done();
+   }));
+ }, 'pause() and play() a transition');
+ 
+-done();
+ </script>
+ </body>
+diff --git a/dom/animation/test/css-transitions/test_animation-ready.html b/dom/animation/test/css-transitions/test_animation-ready.html
+deleted file mode 100644
+--- a/dom/animation/test/css-transitions/test_animation-ready.html
++++ /dev/null
+@@ -1,15 +0,0 @@
+-<!doctype html>
+-<meta charset=utf-8>
+-<script src="/resources/testharness.js"></script>
+-<script src="/resources/testharnessreport.js"></script>
+-<div id="log"></div>
+-<script>
+-'use strict';
+-setup({explicit_done: true});
+-SpecialPowers.pushPrefEnv(
+-  { "set": [["dom.animations-api.core.enabled", true],
+-            ["dom.animations-api.pending-member.enabled", true]]},
+-  function() {
+-    window.open("file_animation-ready.html");
+-  });
+-</script>
+diff --git a/dom/animation/test/css-transitions/file_animation-ready.html b/dom/animation/test/css-transitions/test_animation-ready_to_rename.html
+rename from dom/animation/test/css-transitions/file_animation-ready.html
+rename to dom/animation/test/css-transitions/test_animation-ready_to_rename.html
+--- a/dom/animation/test/css-transitions/file_animation-ready.html
++++ b/dom/animation/test/css-transitions/test_animation-ready_to_rename.html
+@@ -1,12 +1,15 @@
+ <!doctype html>
+ <meta charset=utf-8>
++<script src="/resources/testharness.js"></script>
++<script src="/resources/testharnessreport.js"></script>
+ <script src="../testcommon.js"></script>
+ <body>
++<div id="log"></div>
+ <script>
+ 'use strict';
+ 
+ async_test(function(t) {
+   var div = addDiv(t);
+   div.style.transform = 'translate(0px)';
+   getComputedStyle(div).transform;
+   div.style.transition = 'transform 100s';
+@@ -86,11 +89,10 @@ async_test(function(t) {
+ 
+   // Now update the transition to animate to something not-interpolable
+   div.style.marginLeft = 'auto';
+   getComputedStyle(div).marginLeft;
+ 
+ }, 'ready promise is rejected when a transition is cancelled by changing'
+    + ' the transition property to something not interpolable');
+ 
+-done();
+ </script>
+ </body>
+diff --git a/dom/animation/test/css-transitions/test_animation-starttime.html b/dom/animation/test/css-transitions/test_animation-starttime.html
+deleted file mode 100644
+--- a/dom/animation/test/css-transitions/test_animation-starttime.html
++++ /dev/null
+@@ -1,14 +0,0 @@
+-<!doctype html>
+-<meta charset=utf-8>
+-<script src="/resources/testharness.js"></script>
+-<script src="/resources/testharnessreport.js"></script>
+-<div id="log"></div>
+-<script>
+-'use strict';
+-setup({explicit_done: true});
+-SpecialPowers.pushPrefEnv(
+-  { "set": [["dom.animations-api.core.enabled", true]]},
+-  function() {
+-    window.open("file_animation-starttime.html");
+-  });
+-</script>
+diff --git a/dom/animation/test/css-transitions/file_animation-starttime.html b/dom/animation/test/css-transitions/test_animation-starttime_to_rename.html
+rename from dom/animation/test/css-transitions/file_animation-starttime.html
+rename to dom/animation/test/css-transitions/test_animation-starttime_to_rename.html
+--- a/dom/animation/test/css-transitions/file_animation-starttime.html
++++ b/dom/animation/test/css-transitions/test_animation-starttime_to_rename.html
+@@ -7,19 +7,22 @@
+     <style>
+ 
+ .animated-div {
+   margin-left: 100px;
+   transition: margin-left 1000s linear 1000s;
+ }
+ 
+     </style>
++    <script src="/resources/testharness.js"></script>
++    <script src="/resources/testharnessreport.js"></script>
+     <script src="../testcommon.js"></script>
+   </head>
+   <body>
++    <div id="log"></div>
+     <script type="text/javascript">
+ 
+ 'use strict';
+ 
+ // TODO: Once the computedTiming property is implemented, add checks to the
+ // checker helpers to ensure that computedTiming's properties are updated as
+ // expected.
+ // See https://bugzilla.mozilla.org/show_bug.cgi?id=1108055
+@@ -273,12 +276,11 @@ async_test(function(t) {
+       'Animation.playState is "paused" after pause() call');
+   })).catch(t.step_func(function(reason) {
+     assert_unreached(reason);
+   })).then(function() {
+     t.done();
+   });
+ }, 'Animation.startTime after paused');
+ 
+-done();
+     </script>
+   </body>
+ </html>
+diff --git a/dom/animation/test/css-transitions/test_csstransition-transitionproperty.html b/dom/animation/test/css-transitions/test_csstransition-transitionproperty.html
+deleted file mode 100644
+--- a/dom/animation/test/css-transitions/test_csstransition-transitionproperty.html
++++ /dev/null
+@@ -1,14 +0,0 @@
+-<!doctype html>
+-<meta charset=utf-8>
+-<script src="/resources/testharness.js"></script>
+-<script src="/resources/testharnessreport.js"></script>
+-<div id="log"></div>
+-<script>
+-'use strict';
+-setup({explicit_done: true});
+-SpecialPowers.pushPrefEnv(
+-  { "set": [["dom.animations-api.core.enabled", true]]},
+-  function() {
+-    window.open("file_csstransition-transitionproperty.html");
+-  });
+-</script>
+diff --git a/dom/animation/test/css-transitions/file_csstransition-transitionproperty.html b/dom/animation/test/css-transitions/test_csstransition-transitionproperty_to_rename.html
+rename from dom/animation/test/css-transitions/file_csstransition-transitionproperty.html
+rename to dom/animation/test/css-transitions/test_csstransition-transitionproperty_to_rename.html
+--- a/dom/animation/test/css-transitions/file_csstransition-transitionproperty.html
++++ b/dom/animation/test/css-transitions/test_csstransition-transitionproperty_to_rename.html
+@@ -1,12 +1,15 @@
+ <!doctype html>
+ <meta charset=utf-8>
++<script src="/resources/testharness.js"></script>
++<script src="/resources/testharnessreport.js"></script>
+ <script src="../testcommon.js"></script>
+ <body>
++<div id="log"></div>
+ <script>
+ 'use strict';
+ 
+ test(function(t) {
+   var div = addDiv(t);
+ 
+   // Add a transition
+   div.style.left = '0px';
+@@ -14,11 +17,10 @@ test(function(t) {
+   div.style.transition = 'all 100s';
+   div.style.left = '100px';
+ 
+   assert_equals(div.getAnimations()[0].transitionProperty, 'left',
+                 'The transitionProperty for the corresponds to the specific ' +
+                 'property being transitioned');
+ }, 'CSSTransition.transitionProperty');
+ 
+-done();
+ </script>
+ </body>
+diff --git a/dom/animation/test/css-transitions/test_document-get-animations.html b/dom/animation/test/css-transitions/test_document-get-animations.html
+deleted file mode 100644
+--- a/dom/animation/test/css-transitions/test_document-get-animations.html
++++ /dev/null
+@@ -1,15 +0,0 @@
+-<!doctype html>
+-<meta charset=utf-8>
+-<script src="/resources/testharness.js"></script>
+-<script src="/resources/testharnessreport.js"></script>
+-<div id="log"></div>
+-<script>
+-'use strict';
+-setup({explicit_done: true});
+-SpecialPowers.pushPrefEnv(
+-  { "set": [["dom.animations-api.core.enabled", true]]},
+-  function() {
+-    window.open("file_document-get-animations.html");
+-  });
+-</script>
+-</html>
+diff --git a/dom/animation/test/css-transitions/file_document-get-animations.html b/dom/animation/test/css-transitions/test_document-get-animations_to_rename.html
+rename from dom/animation/test/css-transitions/file_document-get-animations.html
+rename to dom/animation/test/css-transitions/test_document-get-animations_to_rename.html
+--- a/dom/animation/test/css-transitions/file_document-get-animations.html
++++ b/dom/animation/test/css-transitions/test_document-get-animations_to_rename.html
+@@ -1,12 +1,15 @@
+ <!doctype html>
+ <meta charset=utf-8>
++<script src="/resources/testharness.js"></script>
++<script src="/resources/testharnessreport.js"></script>
+ <script src="../testcommon.js"></script>
+ <body>
++<div id="log"></div>
+ <script>
+ 'use strict';
+ 
+ test(function(t) {
+   assert_equals(document.getAnimations().length, 0,
+     'getAnimations returns an empty sequence for a document'
+     + ' with no animations');
+ }, 'getAnimations for non-animated content');
+@@ -83,11 +86,10 @@ async_test(function(t) {
+   assert_equals(animations.length, 1, 'Got transition');
+   animations[0].finished.then(t.step_func(function() {
+     assert_equals(document.getAnimations().length, 0,
+                   'No animations returned');
+     t.done();
+   }));
+ }, 'Transitions are not returned after they have finished');
+ 
+-done();
+ </script>
+ </body>
+diff --git a/dom/animation/test/css-transitions/test_effect-target.html b/dom/animation/test/css-transitions/test_effect-target.html
+deleted file mode 100644
+--- a/dom/animation/test/css-transitions/test_effect-target.html
++++ /dev/null
+@@ -1,14 +0,0 @@
+-<!doctype html>
+-<meta charset=utf-8>
+-<script src="/resources/testharness.js"></script>
+-<script src="/resources/testharnessreport.js"></script>
+-<div id="log"></div>
+-<script>
+-'use strict';
+-setup({explicit_done: true});
+-SpecialPowers.pushPrefEnv(
+-  { "set": [["dom.animations-api.core.enabled", true]]},
+-  function() {
+-    window.open("file_effect-target.html");
+-  });
+-</script>
+diff --git a/dom/animation/test/css-transitions/file_effect-target.html b/dom/animation/test/css-transitions/test_effect-target_to_rename.html
+rename from dom/animation/test/css-transitions/file_effect-target.html
+rename to dom/animation/test/css-transitions/test_effect-target_to_rename.html
+--- a/dom/animation/test/css-transitions/file_effect-target.html
++++ b/dom/animation/test/css-transitions/test_effect-target_to_rename.html
+@@ -1,12 +1,15 @@
+ <!doctype html>
+ <meta charset=utf-8>
++<script src="/resources/testharness.js"></script>
++<script src="/resources/testharnessreport.js"></script>
+ <script src="../testcommon.js"></script>
+ <body>
++<div id="log"></div>
+ <script>
+ 'use strict';
+ 
+ test(function(t) {
+   var div = addDiv(t);
+ 
+   div.style.left = '0px';
+   getComputedStyle(div).transitionProperty;
+@@ -56,11 +59,10 @@ test(function(t) {
+                 'the same as the one from the argument of ' +
+                 'KeyframeEffectReadOnly constructor');
+   assert_equals(anims[0].effect.target, newAnim.effect.target,
+                 'Both the transition and the scripted-generated animation ' +
+                 'return the same target object');
+ }, 'effect.target from the script-generated animation should return the same ' +
+    'CSSPseudoElement object as that from the CSS generated transition');
+ 
+-done();
+ </script>
+ </body>
+diff --git a/dom/animation/test/css-transitions/test_element-get-animations.html b/dom/animation/test/css-transitions/test_element-get-animations.html
+deleted file mode 100644
+--- a/dom/animation/test/css-transitions/test_element-get-animations.html
++++ /dev/null
+@@ -1,14 +0,0 @@
+-<!doctype html>
+-<meta charset=utf-8>
+-<script src="/resources/testharness.js"></script>
+-<script src="/resources/testharnessreport.js"></script>
+-<div id="log"></div>
+-<script>
+-'use strict';
+-setup({explicit_done: true});
+-SpecialPowers.pushPrefEnv(
+-  { "set": [["dom.animations-api.core.enabled", true]]},
+-  function() {
+-    window.open("file_element-get-animations.html");
+-  });
+-</script>
+diff --git a/dom/animation/test/css-transitions/file_element-get-animations.html b/dom/animation/test/css-transitions/test_element-get-animations_to_rename.html
+rename from dom/animation/test/css-transitions/file_element-get-animations.html
+rename to dom/animation/test/css-transitions/test_element-get-animations_to_rename.html
+--- a/dom/animation/test/css-transitions/file_element-get-animations.html
++++ b/dom/animation/test/css-transitions/test_element-get-animations_to_rename.html
+@@ -1,12 +1,15 @@
+ <!doctype html>
+ <meta charset=utf-8>
++<script src="/resources/testharness.js"></script>
++<script src="/resources/testharnessreport.js"></script>
+ <script src="../testcommon.js"></script>
+ <body>
++<div id="log"></div>
+ <script>
+ 'use strict';
+ 
+ async_test(function(t) {
+   var div = addDiv(t);
+ 
+   // FIXME: This test does too many things. It should be split up.
+ 
+@@ -137,11 +140,10 @@ test(function(t) {
+   assert_equals(div.getAnimations().length, 2,
+                 'Then a second (opacity) transition is added');
+ 
+   var animations = div.getAnimations();
+   assert_equals(animations[0].transitionProperty, 'transform');
+   assert_equals(animations[1].transitionProperty, 'opacity');
+ }, 'getAnimations sorts transitions by when they were generated');
+ 
+-done();
+ </script>
+ </body>
+diff --git a/dom/animation/test/css-transitions/test_event-dispatch.html b/dom/animation/test/css-transitions/test_event-dispatch.html
+deleted file mode 100644
+--- a/dom/animation/test/css-transitions/test_event-dispatch.html
++++ /dev/null
+@@ -1,14 +0,0 @@
+-<!doctype html>
+-<meta charset=utf-8>
+-<script src="/resources/testharness.js"></script>
+-<script src="/resources/testharnessreport.js"></script>
+-<div id="log"></div>
+-<script>
+-'use strict';
+-setup({explicit_done: true});
+-SpecialPowers.pushPrefEnv(
+-  { "set": [["dom.animations-api.core.enabled", true]]},
+-  function() {
+-    window.open("file_event-dispatch.html");
+-  });
+-</script>
+diff --git a/dom/animation/test/css-transitions/file_event-dispatch.html b/dom/animation/test/css-transitions/test_event-dispatch_to_rename.html
+rename from dom/animation/test/css-transitions/file_event-dispatch.html
+rename to dom/animation/test/css-transitions/test_event-dispatch_to_rename.html
+--- a/dom/animation/test/css-transitions/file_event-dispatch.html
++++ b/dom/animation/test/css-transitions/test_event-dispatch_to_rename.html
+@@ -1,14 +1,17 @@
+ <!doctype html>
+ <meta charset=utf-8>
+ <title>Tests for CSS-Transition events</title>
+ <link rel="help" href="https://drafts.csswg.org/css-transitions-2/#transition-events">
++<script src="/resources/testharness.js"></script>
++<script src="/resources/testharnessreport.js"></script>
+ <script src="../testcommon.js"></script>
+ <body>
++<div id="log"></div>
+ <script>
+ 'use strict';
+ 
+ /**
+  * Helper class to record the elapsedTime member of each event.
+  * The EventWatcher class in testharness.js allows us to wait on
+  * multiple events in a certain order but only records the event
+  * parameters of the most recent event.
+@@ -463,12 +466,11 @@ promise_test(t => {
+   }).then(evt => {
+     transition.cancel();
+ 
+     // Then wait a couple of frames and check that no event was dispatched
+     return waitForAnimationFrames(2);
+   });
+ }, 'Cancel the transition after clearing the target effect');
+ 
+-done();
+ </script>
+ </body>
+ </html>
+diff --git a/dom/animation/test/css-transitions/test_keyframeeffect-getkeyframes.html b/dom/animation/test/css-transitions/test_keyframeeffect-getkeyframes.html
+deleted file mode 100644
+--- a/dom/animation/test/css-transitions/test_keyframeeffect-getkeyframes.html
++++ /dev/null
+@@ -1,14 +0,0 @@
+-<!doctype html>
+-<meta charset=utf-8>
+-<script src="/resources/testharness.js"></script>
+-<script src="/resources/testharnessreport.js"></script>
+-<div id="log"></div>
+-<script>
+-'use strict';
+-setup({explicit_done: true});
+-SpecialPowers.pushPrefEnv(
+-  { "set": [["dom.animations-api.core.enabled", true]]},
+-  function() {
+-    window.open("file_keyframeeffect-getkeyframes.html");
+-  });
+-</script>
+diff --git a/dom/animation/test/css-transitions/file_keyframeeffect-getkeyframes.html b/dom/animation/test/css-transitions/test_keyframeeffect-getkeyframes_to_rename.html
+rename from dom/animation/test/css-transitions/file_keyframeeffect-getkeyframes.html
+rename to dom/animation/test/css-transitions/test_keyframeeffect-getkeyframes_to_rename.html
+--- a/dom/animation/test/css-transitions/file_keyframeeffect-getkeyframes.html
++++ b/dom/animation/test/css-transitions/test_keyframeeffect-getkeyframes_to_rename.html
+@@ -1,17 +1,20 @@
+ <!doctype html>
+ <meta charset=utf-8>
++<script src="/resources/testharness.js"></script>
++<script src="/resources/testharnessreport.js"></script>
+ <script src="../testcommon.js"></script>
+ <style>
+ :root {
+   --var-100px: 100px;
+ }
+ </style>
+ <body>
++<div id="log"></div>
+ <script>
+ 'use strict';
+ 
+ function getKeyframes(e) {
+   return e.getAnimations()[0].effect.getKeyframes();
+ }
+ 
+ function assert_frames_equal(a, b, name) {
+@@ -91,11 +94,10 @@ test(function(t) {
+       left: '100px' },
+   ];
+   for (var i = 0; i < frames.length; i++) {
+     assert_frames_equal(frames[i], expected[i], "ComputedKeyframe #" + i);
+   }
+ }, 'KeyframeEffectReadOnly.getKeyframes() returns expected frames for a'
+    + ' transition with a CSS variable endpoint');
+ 
+-done();
+ </script>
+ </body>
+diff --git a/dom/animation/test/css-transitions/test_pseudoElement-get-animations.html b/dom/animation/test/css-transitions/test_pseudoElement-get-animations.html
+deleted file mode 100644
+--- a/dom/animation/test/css-transitions/test_pseudoElement-get-animations.html
++++ /dev/null
+@@ -1,14 +0,0 @@
+-<!doctype html>
+-<meta charset=utf-8>
+-<script src="/resources/testharness.js"></script>
+-<script src="/resources/testharnessreport.js"></script>
+-<div id="log"></div>
+-<script>
+-'use strict';
+-setup({explicit_done: true});
+-SpecialPowers.pushPrefEnv(
+-  { "set": [["dom.animations-api.core.enabled", true]]},
+-  function() {
+-    window.open("file_pseudoElement-get-animations.html");
+-  });
+-</script>
+diff --git a/dom/animation/test/css-transitions/file_pseudoElement-get-animations.html b/dom/animation/test/css-transitions/test_pseudoElement-get-animations_to_rename.html
+rename from dom/animation/test/css-transitions/file_pseudoElement-get-animations.html
+rename to dom/animation/test/css-transitions/test_pseudoElement-get-animations_to_rename.html
+--- a/dom/animation/test/css-transitions/file_pseudoElement-get-animations.html
++++ b/dom/animation/test/css-transitions/test_pseudoElement-get-animations_to_rename.html
+@@ -1,26 +1,29 @@
+ <!doctype html>
+ <meta charset=utf-8>
++<script src="/resources/testharness.js"></script>
++<script src="/resources/testharnessreport.js"></script>
+ <script src="../testcommon.js"></script>
+ <style>
+ .init::before {
+   content: '';
+   height: 0px;
+   width: 0px;
+   opacity: 0;
+   transition: all 100s;
+ }
+ .change::before {
+   height: 100px;
+   width: 100px;
+   opacity: 1;
+ }
+ </style>
+ <body>
++<div id="log"></div>
+ <script>
+ 'use strict';
+ 
+ test(function(t) {
+   var div = addDiv(t, { class: 'init' });
+   flushComputedStyle(div);
+   div.classList.add('change');
+ 
+@@ -35,11 +38,10 @@ test(function(t) {
+   var anims = pseudoTarget.getAnimations();
+   assert_equals(anims.length, 3,
+                 'Got expected number of animations on pseudo-element');
+   assert_equals(anims[0].transitionProperty, 'height');
+   assert_equals(anims[1].transitionProperty, 'opacity');
+   assert_equals(anims[2].transitionProperty, 'width');
+ }, 'getAnimations sorts simultaneous transitions by name');
+ 
+-done();
+ </script>
+ </body>
+diff --git a/dom/animation/test/css-transitions/test_setting-effect.html b/dom/animation/test/css-transitions/test_setting-effect.html
+deleted file mode 100644
+--- a/dom/animation/test/css-transitions/test_setting-effect.html
++++ /dev/null
+@@ -1,15 +0,0 @@
+-<!doctype html>
+-<meta charset=utf-8>
+-<script src='/resources/testharness.js'></script>
+-<script src='/resources/testharnessreport.js'></script>
+-<div id='log'></div>
+-<script>
+-'use strict';
+-setup({explicit_done: true});
+-SpecialPowers.pushPrefEnv(
+-  { "set": [["dom.animations-api.core.enabled", true],
+-            ["dom.animations-api.pending-member.enabled", true]]},
+-  function() {
+-    window.open('file_setting-effect.html');
+-  });
+-</script>
+diff --git a/dom/animation/test/css-transitions/file_setting-effect.html b/dom/animation/test/css-transitions/test_setting-effect_to_rename.html
+rename from dom/animation/test/css-transitions/file_setting-effect.html
+rename to dom/animation/test/css-transitions/test_setting-effect_to_rename.html
+--- a/dom/animation/test/css-transitions/file_setting-effect.html
++++ b/dom/animation/test/css-transitions/test_setting-effect_to_rename.html
+@@ -1,12 +1,15 @@
+ <!doctype html>
+ <meta charset=utf-8>
++<script src="/resources/testharness.js"></script>
++<script src="/resources/testharnessreport.js"></script>
+ <script src='../testcommon.js'></script>
+ <body>
++<div id="log"></div>
+ <script>
+ 'use strict';
+ 
+ promise_test(function(t) {
+   var div = addDiv(t);
+   var watcher = new EventWatcher(t, div, [ 'transitionend',
+                                            'transitioncancel' ]);
+   div.style.left = '0px';
+@@ -90,11 +93,10 @@ promise_test(function(t) {
+   assert_equals(transition.transitionProperty, 'left');
+   assert_true(transition.pending);
+ 
+   return transition.ready.then(function() {
+     assert_false(transition.pending);
+   });
+ }, 'Test for setting a new keyframe effect to a pending transition');
+ 
+-done();
+ </script>
+ </body>
+diff --git a/dom/animation/test/document-timeline/test_document-timeline.html b/dom/animation/test/document-timeline/test_document-timeline.html
+deleted file mode 100644
+--- a/dom/animation/test/document-timeline/test_document-timeline.html
++++ /dev/null
+@@ -1,14 +0,0 @@
+-<!doctype html>
+-<meta charset=utf-8>
+-<script src="/resources/testharness.js"></script>
+-<script src="/resources/testharnessreport.js"></script>
+-<div id="log"></div>
+-<script>
+-'use strict';
+-setup({explicit_done: true});
+-SpecialPowers.pushPrefEnv(
+-  { "set": [["dom.animations-api.core.enabled", true]]},
+-  function() {
+-    window.open("file_document-timeline.html");
+-  });
+-</script>
+diff --git a/dom/animation/test/document-timeline/file_document-timeline.html b/dom/animation/test/document-timeline/test_document-timeline_to_rename.html
+rename from dom/animation/test/document-timeline/file_document-timeline.html
+rename to dom/animation/test/document-timeline/test_document-timeline_to_rename.html
+--- a/dom/animation/test/document-timeline/file_document-timeline.html
++++ b/dom/animation/test/document-timeline/test_document-timeline_to_rename.html
+@@ -1,14 +1,17 @@
+ <!doctype html>
+ <meta charset=utf-8>
+ <title>Web Animations API: DocumentTimeline tests</title>
++<script src="/resources/testharness.js"></script>
++<script src="/resources/testharnessreport.js"></script>
+ <script src="../testcommon.js"></script>
+ <iframe srcdoc='<html><meta charset=utf-8></html>' width="10" height="10" id="iframe"></iframe>
+ <iframe srcdoc='<html style="display:none"><meta charset=utf-8></html>' width="10" height="10" id="hidden-iframe"></iframe>
++<div id="log"></div>
+ <script>
+ 'use strict';
+ 
+ test(function() {
+   assert_equals(document.timeline, document.timeline,
+     'document.timeline returns the same object every time');
+   var iframe = document.getElementById('iframe');
+   assert_not_equals(document.timeline, iframe.contentDocument.timeline,
+@@ -126,10 +129,9 @@ async_test(function(t) {
+ 
+   if (hiddenIFrame.contentDocument.readyState === 'complete') {
+     testToRunOnLoad();
+   } else {
+     hiddenIFrame.addEventListener("load", testToRunOnLoad);
+   }
+ }, 'document.timeline.currentTime hidden subframe dynamic test');
+ 
+-done();
+ </script>
+diff --git a/dom/animation/test/mochitest.ini b/dom/animation/test/mochitest.ini
+--- a/dom/animation/test/mochitest.ini
++++ b/dom/animation/test/mochitest.ini
+@@ -1,131 +1,83 @@
+ [DEFAULT]
++prefs =
++  dom.animations-api.core.enabled=true
++  dom.animations-api.pending-member.enabled=true
+ # Support files for chrome tests that we want to load over HTTP need
+ # to go in here, not chrome.ini.
+ support-files =
+   chrome/file_animate_xrays.html
+-  css-animations/file_animation-cancel.html
+-  css-animations/file_animation-computed-timing.html
+-  css-animations/file_animation-currenttime.html
+-  css-animations/file_animation-finish.html
+-  css-animations/file_animation-finished.html
+-  css-animations/file_animation-id.html
+-  css-animations/file_animation-pausing.html
+-  css-animations/file_animation-playstate.html
+-  css-animations/file_animation-ready.html
+-  css-animations/file_animation-reverse.html
+-  css-animations/file_animation-starttime.html
+-  css-animations/file_animations-dynamic-changes.html
+-  css-animations/file_cssanimation-animationname.html
+-  css-animations/file_document-get-animations.html
+-  css-animations/file_effect-target.html
+-  css-animations/file_element-get-animations.html
+-  css-animations/file_event-dispatch.html
+-  css-animations/file_event-order.html
+-  css-animations/file_keyframeeffect-getkeyframes.html
+-  css-animations/file_pseudoElement-get-animations.html
+-  css-animations/file_setting-effect.html
+-  css-transitions/file_animation-cancel.html
+-  css-transitions/file_animation-computed-timing.html
+-  css-transitions/file_animation-currenttime.html
+-  css-transitions/file_animation-finished.html
+-  css-transitions/file_animation-pausing.html
+-  css-transitions/file_animation-ready.html
+-  css-transitions/file_animation-starttime.html
+-  css-transitions/file_csstransition-transitionproperty.html
+-  css-transitions/file_document-get-animations.html
+-  css-transitions/file_effect-target.html
+-  css-transitions/file_element-get-animations.html
+-  css-transitions/file_event-dispatch.html
+-  css-transitions/file_keyframeeffect-getkeyframes.html
+-  css-transitions/file_pseudoElement-get-animations.html
+-  css-transitions/file_setting-effect.html
+-  document-timeline/file_document-timeline.html
+   mozilla/xhr_doc.html
+-  mozilla/file_cubic_bezier_limits.html
+   mozilla/file_deferred_start.html
+-  mozilla/file_disabled_properties.html
+   mozilla/file_disable_animations_api_core.html
+   mozilla/file_discrete-animations.html
+-  mozilla/file_document-timeline-origin-time-range.html
+-  mozilla/file_hide_and_show.html
+   mozilla/file_restyles.html
+-  mozilla/file_restyling_xhr_doc.html
+-  mozilla/file_set-easing.html
+-  mozilla/file_transform_limits.html
+   mozilla/file_transition_finish_on_compositor.html
+-  mozilla/file_underlying-discrete-value.html
+-  style/file_animation-seeking-with-current-time.html
+-  style/file_animation-seeking-with-start-time.html
+-  style/file_animation-setting-effect.html
+-  style/file_composite.html
+-  style/file_missing-keyframe.html
+-  style/file_missing-keyframe-on-compositor.html
+   ../../../layout/style/test/property_database.js
+   testcommon.js
+ 
+-[css-animations/test_animations-dynamic-changes.html]
+-[css-animations/test_animation-cancel.html]
+-[css-animations/test_animation-computed-timing.html]
+-[css-animations/test_animation-currenttime.html]
+-[css-animations/test_animation-finish.html]
+-[css-animations/test_animation-finished.html]
+-[css-animations/test_animation-id.html]
+-[css-animations/test_animation-pausing.html]
+-[css-animations/test_animation-playstate.html]
+-[css-animations/test_animation-ready.html]
+-[css-animations/test_animation-reverse.html]
+-[css-animations/test_animation-starttime.html]
+-[css-animations/test_cssanimation-animationname.html]
+-[css-animations/test_document-get-animations.html]
+-[css-animations/test_effect-target.html]
+-[css-animations/test_element-get-animations.html]
+-[css-animations/test_event-dispatch.html]
+-[css-animations/test_event-order.html]
+-[css-animations/test_keyframeeffect-getkeyframes.html]
+-[css-animations/test_pseudoElement-get-animations.html]
+-[css-animations/test_setting-effect.html]
+-[css-transitions/test_animation-cancel.html]
+-[css-transitions/test_animation-computed-timing.html]
+-[css-transitions/test_animation-currenttime.html]
+-[css-transitions/test_animation-finished.html]
+-[css-transitions/test_animation-pausing.html]
+-[css-transitions/test_animation-ready.html]
+-[css-transitions/test_animation-starttime.html]
+-[css-transitions/test_csstransition-transitionproperty.html]
+-[css-transitions/test_document-get-animations.html]
+-[css-transitions/test_effect-target.html]
+-[css-transitions/test_element-get-animations.html]
+-[css-transitions/test_event-dispatch.html]
+-[css-transitions/test_keyframeeffect-getkeyframes.html]
+-[css-transitions/test_pseudoElement-get-animations.html]
+-[css-transitions/test_setting-effect.html]
+-[document-timeline/test_document-timeline.html]
++[css-animations/test_animations-dynamic-changes_to_rename.html]
++[css-animations/test_animation-cancel_to_rename.html]
++[css-animations/test_animation-computed-timing_to_rename.html]
++[css-animations/test_animation-currenttime_to_rename.html]
++[css-animations/test_animation-finish_to_rename.html]
++[css-animations/test_animation-finished_to_rename.html]
++[css-animations/test_animation-id_to_rename.html]
++[css-animations/test_animation-pausing_to_rename.html]
++[css-animations/test_animation-playstate_to_rename.html]
++[css-animations/test_animation-ready_to_rename.html]
++[css-animations/test_animation-reverse_to_rename.html]
++[css-animations/test_animation-starttime_to_rename.html]
++[css-animations/test_cssanimation-animationname_to_rename.html]
++[css-animations/test_document-get-animations_to_rename.html]
++[css-animations/test_effect-target_to_rename.html]
++[css-animations/test_element-get-animations_to_rename.html]
++[css-animations/test_event-dispatch_to_rename.html]
++[css-animations/test_event-order_to_rename.html]
++[css-animations/test_keyframeeffect-getkeyframes_to_rename.html]
++[css-animations/test_pseudoElement-get-animations_to_rename.html]
++[css-animations/test_setting-effect_to_rename.html]
++[css-transitions/test_animation-cancel_to_rename.html]
++[css-transitions/test_animation-computed-timing_to_rename.html]
++[css-transitions/test_animation-currenttime_to_rename.html]
++[css-transitions/test_animation-finished_to_rename.html]
++[css-transitions/test_animation-pausing_to_rename.html]
++[css-transitions/test_animation-ready_to_rename.html]
++[css-transitions/test_animation-starttime_to_rename.html]
++[css-transitions/test_csstransition-transitionproperty_to_rename.html]
++[css-transitions/test_document-get-animations_to_rename.html]
++[css-transitions/test_effect-target_to_rename.html]
++[css-transitions/test_element-get-animations_to_rename.html]
++[css-transitions/test_event-dispatch_to_rename.html]
++[css-transitions/test_keyframeeffect-getkeyframes_to_rename.html]
++[css-transitions/test_pseudoElement-get-animations_to_rename.html]
++[css-transitions/test_setting-effect_to_rename.html]
++[document-timeline/test_document-timeline_to_rename.html]
+ [document-timeline/test_request_animation_frame.html]
+ [mozilla/test_cascade.html]
+-[mozilla/test_cubic_bezier_limits.html]
++[mozilla/test_cubic_bezier_limits_to_rename.html]
+ [mozilla/test_deferred_start.html]
+ skip-if = (toolkit == 'android' && debug) || (os == 'win' && bits == 64) # Bug 1363957
+ [mozilla/test_disable_animations_api_core.html]
+-[mozilla/test_disabled_properties.html]
++[mozilla/test_disabled_properties_to_rename.html]
+ [mozilla/test_discrete-animations.html]
+ [mozilla/test_distance_of_basic_shape.html]
+ [mozilla/test_distance_of_filter.html]
+ [mozilla/test_distance_of_transform.html]
+-[mozilla/test_document-timeline-origin-time-range.html]
+-[mozilla/test_hide_and_show.html]
++[mozilla/test_document_timeline_origin_time_range.html]
++[mozilla/test_hide_and_show_to_rename.html]
+ [mozilla/test_moz-prefixed-properties.html]
+ [mozilla/test_restyles.html]
+-[mozilla/test_restyling_xhr_doc.html]
+-[mozilla/test_set-easing.html]
+-[mozilla/test_transform_limits.html]
++[mozilla/test_restyling_xhr_doc_to_rename.html]
++[mozilla/test_set_easing.html]
++[mozilla/test_transform_limits_to_rename.html]
+ [mozilla/test_transition_finish_on_compositor.html]
+ skip-if = toolkit == 'android'
+-[mozilla/test_underlying-discrete-value.html]
+-[style/test_animation-seeking-with-current-time.html]
+-[style/test_animation-seeking-with-start-time.html]
+-[style/test_animation-setting-effect.html]
+-[style/test_composite.html]
++[mozilla/test_underlying_discrete_value.html]
++[style/test_animation-seeking-with-current-time_to_rename.html]
++[style/test_animation-seeking-with-start-time_to_rename.html]
++[style/test_animation-setting-effect_to_rename.html]
++[style/test_composite_to_rename.html]
+ [style/test_interpolation-from-interpolatematrix-to-none.html]
+-[style/test_missing-keyframe.html]
+-[style/test_missing-keyframe-on-compositor.html]
++[style/test_missing-keyframe_to_rename.html]
++[style/test_missing-keyframe-on-compositor_to_rename.html]
+ [style/test_transform-non-normalizable-rotate3d.html]
+diff --git a/dom/animation/test/mozilla/test_cubic_bezier_limits.html b/dom/animation/test/mozilla/test_cubic_bezier_limits.html
+deleted file mode 100644
+--- a/dom/animation/test/mozilla/test_cubic_bezier_limits.html
++++ /dev/null
+@@ -1,14 +0,0 @@
+-<!doctype html>
+-<meta charset=utf-8>
+-<script src="/resources/testharness.js"></script>
+-<script src="/resources/testharnessreport.js"></script>
+-<div id="log"></div>
+-<script>
+-'use strict';
+-setup({explicit_done: true});
+-SpecialPowers.pushPrefEnv(
+-  { "set": [["dom.animations-api.core.enabled", true]]},
+-  function() {
+-    window.open("file_cubic_bezier_limits.html");
+-  });
+-</script>
+diff --git a/dom/animation/test/mozilla/file_cubic_bezier_limits.html b/dom/animation/test/mozilla/test_cubic_bezier_limits_to_rename.html
+rename from dom/animation/test/mozilla/file_cubic_bezier_limits.html
+rename to dom/animation/test/mozilla/test_cubic_bezier_limits_to_rename.html
+--- a/dom/animation/test/mozilla/file_cubic_bezier_limits.html
++++ b/dom/animation/test/mozilla/test_cubic_bezier_limits_to_rename.html
+@@ -1,21 +1,24 @@
+ <!doctype html>
+ <meta charset=utf-8>
++<script src="/resources/testharness.js"></script>
++<script src="/resources/testharnessreport.js"></script>
+ <script src="../testcommon.js"></script>
+ <body>
+ <style>
+ @keyframes anim {
+   to { margin-left: 100px; }
+ }
+ 
+ .transition-div {
+   margin-left: 100px;
+ }
+ </style>
++<div id="log"></div>
+ <script>
+ 'use strict';
+ 
+ // We clamp +infinity or -inifinity value in floating point to
+ // maximum floating point value or -maxinum floating point value.
+ const max_float = 3.40282e+38;
+ 
+ test(function(t) {
+@@ -156,12 +159,10 @@ test(function(t) {
+     'progress on lower edge for the lowest value of y1 and y2 control points');
+ 
+   anim.finish();
+   assert_equals(anim.effect.getComputedTiming().progress, 1.0,
+     'progress on lower edge for the lowest value of y1 and y2 control points');
+ 
+ }, 'Calculated values on both edges');
+ 
+-done();
+-
+ </script>
+ </body>
+diff --git a/dom/animation/test/mozilla/test_disabled_properties.html b/dom/animation/test/mozilla/test_disabled_properties.html
+deleted file mode 100644
+--- a/dom/animation/test/mozilla/test_disabled_properties.html
++++ /dev/null
+@@ -1,14 +0,0 @@
+-<!doctype html>
+-<meta charset=utf-8>
+-<script src="/resources/testharness.js"></script>
+-<script src="/resources/testharnessreport.js"></script>
+-<div id="log"></div>
+-<script>
+-'use strict';
+-setup({explicit_done: true});
+-SpecialPowers.pushPrefEnv(
+-  { "set": [["dom.animations-api.core.enabled", true]]},
+-  function() {
+-    window.open("file_disabled_properties.html");
+-  });
+-</script>
+diff --git a/dom/animation/test/mozilla/file_disabled_properties.html b/dom/animation/test/mozilla/test_disabled_properties_to_rename.html
+rename from dom/animation/test/mozilla/file_disabled_properties.html
+rename to dom/animation/test/mozilla/test_disabled_properties_to_rename.html
+--- a/dom/animation/test/mozilla/file_disabled_properties.html
++++ b/dom/animation/test/mozilla/test_disabled_properties_to_rename.html
+@@ -1,12 +1,15 @@
+ <!doctype html>
+ <meta charset=utf-8>
++<script src="/resources/testharness.js"></script>
++<script src="/resources/testharnessreport.js"></script>
+ <script src="../testcommon.js"></script>
+ <body>
++<div id="log"></div>
+ <script>
+ 'use strict';
+ 
+ function waitForSetPref(pref, value) {
+   return SpecialPowers.pushPrefEnv({ 'set': [[pref, value]] });
+ }
+ 
+ /*
+@@ -61,11 +64,10 @@ promise_test(function(t) {
+     var anim = createAnim();
+     assert_does_not_have_property(anim, 0, 'Initial keyframe',
+                                   'webkitTextFillColor');
+     assert_does_not_have_property(anim, 1, 'Final keyframe',
+                                   'webkitTextFillColor');
+   });
+ }, 'Specifying a disabled property using a keyframe sequence');
+ 
+-done();
+ </script>
+ </body>
+diff --git a/dom/animation/test/mozilla/test_discrete-animations.html b/dom/animation/test/mozilla/test_discrete-animations.html
+--- a/dom/animation/test/mozilla/test_discrete-animations.html
++++ b/dom/animation/test/mozilla/test_discrete-animations.html
+@@ -3,16 +3,15 @@
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <div id="log"></div>
+ <script>
+ 'use strict';
+ setup({explicit_done: true});
+ SpecialPowers.pushPrefEnv(
+   { "set": [
+-    ["dom.animations-api.core.enabled", true],
+     ["layout.css.osx-font-smoothing.enabled", true],
+     ["layout.css.prefixes.webkit", true]
+   ] },
+   function() {
+     window.open("file_discrete-animations.html");
+   });
+ </script>
+diff --git a/dom/animation/test/mozilla/test_document-timeline-origin-time-range.html b/dom/animation/test/mozilla/test_document-timeline-origin-time-range.html
+deleted file mode 100644
+--- a/dom/animation/test/mozilla/test_document-timeline-origin-time-range.html
++++ /dev/null
+@@ -1,14 +0,0 @@
+-<!doctype html>
+-<meta charset=utf-8>
+-<script src="/resources/testharness.js"></script>
+-<script src="/resources/testharnessreport.js"></script>
+-<div id="log"></div>
+-<script>
+-'use strict';
+-setup({explicit_done: true});
+-SpecialPowers.pushPrefEnv(
+-  { "set": [["dom.animations-api.core.enabled", true]]},
+-  function() {
+-    window.open("file_document-timeline-origin-time-range.html");
+-  });
+-</script>
+diff --git a/dom/animation/test/mozilla/file_document-timeline-origin-time-range.html b/dom/animation/test/mozilla/test_document_timeline_origin_time_range.html
+rename from dom/animation/test/mozilla/file_document-timeline-origin-time-range.html
+rename to dom/animation/test/mozilla/test_document_timeline_origin_time_range.html
+--- a/dom/animation/test/mozilla/file_document-timeline-origin-time-range.html
++++ b/dom/animation/test/mozilla/test_document_timeline_origin_time_range.html
+@@ -1,12 +1,15 @@
+ <!doctype html>
+ <meta charset=utf-8>
++<script src="/resources/testharness.js"></script>
++<script src="/resources/testharnessreport.js"></script>
+ <script src="../testcommon.js"></script>
+ <body>
++<div id="log"></div>
+ <script>
+ 'use strict';
+ 
+ // If the originTime parameter passed to the DocumentTimeline exceeds
+ // the range of the internal storage type (a signed 64-bit integer number
+ // of ticks--a platform-dependent unit) then we should throw.
+ // Infinity isn't allowed as an origin time value and clamping to just
+ // inside the allowed range will just mean we overflow elsewhere.
+@@ -20,11 +23,10 @@ test(function(t) {
+ 
+ test(function(t) {
+   assert_throws({ name: 'TypeError'},
+     function() {
+       new DocumentTimeline({ originTime: -1 * Number.MAX_SAFE_INTEGER });
+     });
+ }, 'Calculated current time is negative infinity');
+ 
+-done();
+ </script>
+ </body>
+diff --git a/dom/animation/test/mozilla/test_hide_and_show.html b/dom/animation/test/mozilla/test_hide_and_show.html
+deleted file mode 100644
+--- a/dom/animation/test/mozilla/test_hide_and_show.html
++++ /dev/null
+@@ -1,14 +0,0 @@
+-<!doctype html>
+-<meta charset=utf-8>
+-<script src="/resources/testharness.js"></script>
+-<script src="/resources/testharnessreport.js"></script>
+-<div id="log"></div>
+-<script>
+-'use strict';
+-setup({explicit_done: true});
+-SpecialPowers.pushPrefEnv(
+-  { "set": [["dom.animations-api.core.enabled", true]]},
+-  function() {
+-    window.open("file_hide_and_show.html");
+-  });
+-</script>
+diff --git a/dom/animation/test/mozilla/file_hide_and_show.html b/dom/animation/test/mozilla/test_hide_and_show_to_rename.html
+rename from dom/animation/test/mozilla/file_hide_and_show.html
+rename to dom/animation/test/mozilla/test_hide_and_show_to_rename.html
+--- a/dom/animation/test/mozilla/file_hide_and_show.html
++++ b/dom/animation/test/mozilla/test_hide_and_show_to_rename.html
+@@ -1,25 +1,28 @@
+ <!doctype html>
+ <meta charset=utf-8>
++<script src="/resources/testharness.js"></script>
++<script src="/resources/testharnessreport.js"></script>
+ <script src="../testcommon.js"></script>
+ <style>
+ @keyframes move {
+   100% {
+     transform: translateX(100px);
+   }
+ }
+ 
+ div.pseudo::before {
+   animation: move 0.01s;
+   content: 'content';
+ }
+ 
+ </style>
+ <body>
++<div id="log"></div>
+ <script>
+ 'use strict';
+ 
+ test(function(t) {
+   var div = addDiv(t, { style: 'animation: move 100s infinite' });
+   assert_equals(div.getAnimations().length, 1,
+                 'display:initial element has animations');
+ 
+@@ -186,11 +189,10 @@ promise_test(function(t) {
+     // Add the class again to re-generate pseudo element.
+     div.classList.add('pseudo');
+     assert_equals(document.getAnimations().length, 1,
+                   'A new CSS animation on pseudo element');
+   });
+ }, 'CSS animation on pseudo element restarts after the pseudo element that ' +
+    'had a finished CSS animation is re-generated');
+ 
+-done();
+ </script>
+ </body>
+diff --git a/dom/animation/test/mozilla/test_restyles.html b/dom/animation/test/mozilla/test_restyles.html
+--- a/dom/animation/test/mozilla/test_restyles.html
++++ b/dom/animation/test/mozilla/test_restyles.html
+@@ -3,16 +3,20 @@
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <div id='log'></div>
+ <script>
+ 'use strict';
+ SimpleTest.waitForExplicitFinish();
+ SimpleTest.expectAssertions(0, 1); // bug 1332970
+ SpecialPowers.pushPrefEnv(
+-  { 'set': [['dom.animations-api.core.enabled', true],
+-            ['layout.reflow.synthMouseMove', false],
+-            ['privacy.reduceTimerPrecision', false]] },
++  {
++    set: [
++      ['layout.reflow.synthMouseMove', false],
++      ['privacy.reduceTimerPrecision', false],
++    ],
++  },
+   function() {
+     window.open('file_restyles.html');
+-  });
++  }
++);
+ </script>
+ </html>
+diff --git a/dom/animation/test/mozilla/test_restyling_xhr_doc.html b/dom/animation/test/mozilla/test_restyling_xhr_doc.html
+deleted file mode 100644
+--- a/dom/animation/test/mozilla/test_restyling_xhr_doc.html
++++ /dev/null
+@@ -1,14 +0,0 @@
+-<!doctype html>
+-<meta charset=utf-8>
+-<script src="/resources/testharness.js"></script>
+-<script src="/resources/testharnessreport.js"></script>
+-<div id="log"></div>
+-<script>
+-'use strict';
+-setup({explicit_done: true});
+-SpecialPowers.pushPrefEnv(
+-  { "set": [["dom.animations-api.core.enabled", true]]},
+-  function() {
+-    window.open("file_restyling_xhr_doc.html");
+-  });
+-</script>
+diff --git a/dom/animation/test/mozilla/file_restyling_xhr_doc.html b/dom/animation/test/mozilla/test_restyling_xhr_doc_to_rename.html
+rename from dom/animation/test/mozilla/file_restyling_xhr_doc.html
+rename to dom/animation/test/mozilla/test_restyling_xhr_doc_to_rename.html
+--- a/dom/animation/test/mozilla/file_restyling_xhr_doc.html
++++ b/dom/animation/test/mozilla/test_restyling_xhr_doc_to_rename.html
+@@ -1,11 +1,14 @@
+ <!doctype html>
+ <meta charset=utf-8>
+ <script src="../testcommon.js"></script>
++<script src="/resources/testharness.js"></script>
++<script src="/resources/testharnessreport.js"></script>
++<div id="log"></div>
+ <script>
+ 'use strict';
+ 
+ // This test supplements the web-platform-tests in:
+ //
+ //   web-animations/interfaces/Animatable/animate-no-browsing-context.html
+ //
+ // Specifically, it covers the case where we have a running animation
+@@ -106,11 +109,9 @@ promise_test(t => {
+     if (isServo) {
+       assert_equals(getComputedStyle(div).opacity, '1',
+                     'Style should NOT be updated');
+     }
+   });
+ }, 'Moving an element with a pending animation restyle to a document without'
+    + ' a browsing context resets animation style');
+ 
+-done();
+-
+ </script>
+diff --git a/dom/animation/test/mozilla/test_set-easing.html b/dom/animation/test/mozilla/test_set-easing.html
+deleted file mode 100644
+--- a/dom/animation/test/mozilla/test_set-easing.html
++++ /dev/null
+@@ -1,14 +0,0 @@
+-<!doctype html>
+-<meta charset=utf-8>
+-<script src="/resources/testharness.js"></script>
+-<script src="/resources/testharnessreport.js"></script>
+-<div id="log"></div>
+-<script>
+-'use strict';
+-setup({explicit_done: true});
+-SpecialPowers.pushPrefEnv(
+-  { "set": [["dom.animations-api.core.enabled", true]]},
+-  function() {
+-    window.open("file_set-easing.html");
+-  });
+-</script>
+diff --git a/dom/animation/test/mozilla/file_set-easing.html b/dom/animation/test/mozilla/test_set_easing.html
+rename from dom/animation/test/mozilla/file_set-easing.html
+rename to dom/animation/test/mozilla/test_set_easing.html
+--- a/dom/animation/test/mozilla/file_set-easing.html
++++ b/dom/animation/test/mozilla/test_set_easing.html
+@@ -1,15 +1,18 @@
+ <!doctype html>
+ <head>
+ <meta charset=utf-8>
+ <title>Test setting easing in sandbox</title>
++<script src="/resources/testharness.js"></script>
++<script src="/resources/testharnessreport.js"></script>
+ <script src="../testcommon.js"></script>
+ </head>
+ <body>
++<div id="log"></div>
+ <script>
+ "use strict";
+ 
+ test(function(t) {
+   const div = document.createElement("div");
+   document.body.appendChild(div);
+   div.animate({ opacity: [0, 1] }, 100000 );
+ 
+@@ -24,11 +27,10 @@ test(function(t) {
+ 
+   const sandbox = new SpecialPowers.Cu.Sandbox(window);
+   sandbox.importFunction(document, "document");
+   sandbox.importFunction(assert_true, "assert_true");
+   sandbox.importFunction(assert_unreached, "assert_unreached");
+   SpecialPowers.Cu.evalInSandbox(`(${contentScript.toSource()})()`, sandbox);
+ }, 'Setting easing should not throw any exceptions in sandbox');
+ 
+-done();
+ </script>
+ </body>
+diff --git a/dom/animation/test/mozilla/test_transform_limits.html b/dom/animation/test/mozilla/test_transform_limits.html
+deleted file mode 100644
+--- a/dom/animation/test/mozilla/test_transform_limits.html
++++ /dev/null
+@@ -1,14 +0,0 @@
+-<!doctype html>
+-<meta charset=utf-8>
+-<script src="/resources/testharness.js"></script>
+-<script src="/resources/testharnessreport.js"></script>
+-<div id="log"></div>
+-<script>
+-'use strict';
+-setup({explicit_done: true});
+-SpecialPowers.pushPrefEnv(
+-  { "set": [["dom.animations-api.core.enabled", true]]},
+-  function() {
+-    window.open("file_transform_limits.html");
+-  });
+-</script>
+diff --git a/dom/animation/test/mozilla/file_transform_limits.html b/dom/animation/test/mozilla/test_transform_limits_to_rename.html
+rename from dom/animation/test/mozilla/file_transform_limits.html
+rename to dom/animation/test/mozilla/test_transform_limits_to_rename.html
+--- a/dom/animation/test/mozilla/file_transform_limits.html
++++ b/dom/animation/test/mozilla/test_transform_limits_to_rename.html
+@@ -1,12 +1,15 @@
+ <!doctype html>
+ <meta charset=utf-8>
++<script src="/resources/testharness.js"></script>
++<script src="/resources/testharnessreport.js"></script>
+ <script src="../testcommon.js"></script>
+ <body>
++<div id="log"></div>
+ <script>
+ 'use strict';
+ 
+ // We clamp +infinity or -inifinity value in floating point to
+ // maximum floating point value or -maximum floating point value.
+ const MAX_FLOAT = 3.40282e+38;
+ 
+ test(function(t) {
+@@ -44,12 +47,10 @@ test(function(t) {
+                          100 * MS_PER_SEC);
+ 
+   anim.pause();
+   anim.currentTime = 50 * MS_PER_SEC;
+   assert_equals(getComputedStyle(div).transform,
+                 'matrix(2, 0, 0, 2, ' + MAX_FLOAT + ', 0)');
+ }, 'Test that the parameter of transform matrix is clamped' );
+ 
+-done();
+-
+ </script>
+ </body>
+diff --git a/dom/animation/test/mozilla/test_transition_finish_on_compositor.html b/dom/animation/test/mozilla/test_transition_finish_on_compositor.html
+--- a/dom/animation/test/mozilla/test_transition_finish_on_compositor.html
++++ b/dom/animation/test/mozilla/test_transition_finish_on_compositor.html
+@@ -4,16 +4,19 @@
+ <script src="/resources/testharnessreport.js"></script>
+ <div id="log"></div>
+ <script>
+ 'use strict';
+ setup({explicit_done: true});
+ // This test appears like it might get racey and cause a timeout with too low of a
+ // precision, so we hardcode it to something reasonable.
+ SpecialPowers.pushPrefEnv(
+-  { "set":
+-      [["dom.animations-api.core.enabled", true],
+-       ["privacy.reduceTimerPrecision", true],
+-       ["privacy.resistFingerprinting.reduceTimerPrecision.microseconds", 2000]]},
++  {
++    set: [
++      ['privacy.reduceTimerPrecision', true],
++      ['privacy.resistFingerprinting.reduceTimerPrecision.microseconds', 2000],
++    ],
++  },
+   function() {
+-    window.open("file_transition_finish_on_compositor.html");
+-  });
++    window.open('file_transition_finish_on_compositor.html');
++  }
++);
+ </script>
+diff --git a/dom/animation/test/mozilla/test_underlying-discrete-value.html b/dom/animation/test/mozilla/test_underlying-discrete-value.html
+deleted file mode 100644
+--- a/dom/animation/test/mozilla/test_underlying-discrete-value.html
++++ /dev/null
+@@ -1,15 +0,0 @@
+-<!doctype html>
+-<meta charset=utf-8>
+-<script src="/resources/testharness.js"></script>
+-<script src="/resources/testharnessreport.js"></script>
+-<div id="log"></div>
+-<script>
+-'use strict';
+-setup({explicit_done: true});
+-SpecialPowers.pushPrefEnv(
+-  { "set": [["dom.animations-api.core.enabled", true]]},
+-  function() {
+-    window.open("file_underlying-discrete-value.html");
+-  });
+-</script>
+-</html>
+diff --git a/dom/animation/test/mozilla/file_underlying-discrete-value.html b/dom/animation/test/mozilla/test_underlying_discrete_value.html
+rename from dom/animation/test/mozilla/file_underlying-discrete-value.html
+rename to dom/animation/test/mozilla/test_underlying_discrete_value.html
+--- a/dom/animation/test/mozilla/file_underlying-discrete-value.html
++++ b/dom/animation/test/mozilla/test_underlying_discrete_value.html
+@@ -1,12 +1,15 @@
+ <!doctype html>
+ <meta charset=utf-8>
++<script src="/resources/testharness.js"></script>
++<script src="/resources/testharnessreport.js"></script>
+ <script src="../testcommon.js"></script>
+ <body>
++<div id="log"></div>
+ <script>
+ "use strict";
+ 
+ const isGecko = !isStyledByServo();
+ 
+ // Tests that we correctly extract the underlying value when the animation
+ // type is 'discrete'.
+ const discreteTests = [
+@@ -193,11 +196,10 @@ discreteTests.forEach(testcase => {
+                     `${ expectedKeyframe.computedOffset }`);
+       assert_equals(keyframe.alignContent, expectedKeyframe.alignContent,
+                     `alignContent of keyframes[${ index }] should be ` +
+                     `${ expectedKeyframe.alignContent }`);
+     });
+   }, testcase.explanation);
+ });
+ 
+-done();
+ </script>
+ </body>
+diff --git a/dom/animation/test/style/test_animation-seeking-with-current-time.html b/dom/animation/test/style/test_animation-seeking-with-current-time.html
+deleted file mode 100644
+--- a/dom/animation/test/style/test_animation-seeking-with-current-time.html
++++ /dev/null
+@@ -1,15 +0,0 @@
+-<!doctype html>
+-<meta charset=utf-8>
+-<script src="/resources/testharness.js"></script>
+-<script src="/resources/testharnessreport.js"></script>
+-<div id="log"></div>
+-<script>
+-'use strict';
+-setup({explicit_done: true});
+-SpecialPowers.pushPrefEnv(
+-  { "set": [["dom.animations-api.core.enabled", true]]},
+-  function() {
+-    window.open("file_animation-seeking-with-current-time.html");
+-  });
+-</script>
+-</html>
+diff --git a/dom/animation/test/style/file_animation-seeking-with-current-time.html b/dom/animation/test/style/test_animation-seeking-with-current-time_to_rename.html
+rename from dom/animation/test/style/file_animation-seeking-with-current-time.html
+rename to dom/animation/test/style/test_animation-seeking-with-current-time_to_rename.html
+--- a/dom/animation/test/style/file_animation-seeking-with-current-time.html
++++ b/dom/animation/test/style/test_animation-seeking-with-current-time_to_rename.html
+@@ -9,19 +9,22 @@
+   animation-timing-function: linear ! important;
+ }
+ 
+ @keyframes anim {
+   from { margin-left: 0px; }
+   to { margin-left: 100px; }
+ }
+     </style>
++    <script src="/resources/testharness.js"></script>
++    <script src="/resources/testharnessreport.js"></script>
+     <script src="../testcommon.js"></script>
+   </head>
+   <body>
++    <div id="log"></div>
+     <script type="text/javascript">
+ 'use strict';
+ 
+ function assert_marginLeft_equals(target, expect, description) {
+   var marginLeft = parseFloat(getComputedStyle(target).marginLeft);
+   assert_equals(marginLeft, expect, description);
+ }
+ 
+@@ -110,12 +113,11 @@ promise_test(function(t) {
+     // active -> after
+     animation.currentTime = 250 * MS_PER_SEC;
+     assert_marginLeft_equals(div, -10,
+       'Computed value is not affected after seeking forwards from ' +
+       '\'in effect\' to not \'in effect\' state');
+   });
+ }, 'Seeking to \'in effect\' from non-\'in effect\' (active -> after)');
+ 
+-done();
+     </script>
+   </body>
+ </html>
+diff --git a/dom/animation/test/style/test_animation-seeking-with-start-time.html b/dom/animation/test/style/test_animation-seeking-with-start-time.html
+deleted file mode 100644
+--- a/dom/animation/test/style/test_animation-seeking-with-start-time.html
++++ /dev/null
+@@ -1,15 +0,0 @@
+-<!doctype html>
+-<meta charset=utf-8>
+-<script src="/resources/testharness.js"></script>
+-<script src="/resources/testharnessreport.js"></script>
+-<div id="log"></div>
+-<script>
+-'use strict';
+-setup({explicit_done: true});
+-SpecialPowers.pushPrefEnv(
+-  { "set": [["dom.animations-api.core.enabled", true]]},
+-  function() {
+-    window.open("file_animation-seeking-with-start-time.html");
+-  });
+-</script>
+-</html>
+diff --git a/dom/animation/test/style/file_animation-seeking-with-start-time.html b/dom/animation/test/style/test_animation-seeking-with-start-time_to_rename.html
+rename from dom/animation/test/style/file_animation-seeking-with-start-time.html
+rename to dom/animation/test/style/test_animation-seeking-with-start-time_to_rename.html
+--- a/dom/animation/test/style/file_animation-seeking-with-start-time.html
++++ b/dom/animation/test/style/test_animation-seeking-with-start-time_to_rename.html
+@@ -9,19 +9,22 @@
+   animation-timing-function: linear ! important;
+ }
+ 
+ @keyframes anim {
+   from { margin-left: 0px; }
+   to { margin-left: 100px; }
+ }
+     </style>
++    <script src="/resources/testharness.js"></script>
++    <script src="/resources/testharnessreport.js"></script>
+     <script src="../testcommon.js"></script>
+   </head>
+   <body>
++    <div id="log"></div>
+     <script type="text/javascript">
+ 'use strict';
+ 
+ function assert_marginLeft_equals(target, expect, description) {
+   var marginLeft = parseFloat(getComputedStyle(target).marginLeft);
+   assert_equals(marginLeft, expect, description);
+ }
+ 
+@@ -110,12 +113,11 @@ promise_test(function(t) {
+     // active -> after
+     animation.startTime = animation.timeline.currentTime - 250 * MS_PER_SEC;
+     assert_marginLeft_equals(div, -10,
+       'Computed value is not affected after seeking forwards from ' +
+       '\'in effect\' to not \'in effect\' state');
+   });
+ }, 'Seeking to \'in effect\' from non-\'in effect\' (active -> after)');
+ 
+-done();
+     </script>
+   </body>
+ </html>
+diff --git a/dom/animation/test/style/test_animation-setting-effect.html b/dom/animation/test/style/test_animation-setting-effect.html
+deleted file mode 100644
+--- a/dom/animation/test/style/test_animation-setting-effect.html
++++ /dev/null
+@@ -1,15 +0,0 @@
+-<!doctype html>
+-<meta charset=utf-8>
+-<script src='/resources/testharness.js'></script>
+-<script src='/resources/testharnessreport.js'></script>
+-<div id='log'></div>
+-<script>
+-'use strict';
+-setup({explicit_done: true});
+-SpecialPowers.pushPrefEnv(
+-  { 'set': [['dom.animations-api.core.enabled', true]]},
+-  function() {
+-    window.open('file_animation-setting-effect.html');
+-  });
+-</script>
+-</html>
+diff --git a/dom/animation/test/style/file_animation-setting-effect.html b/dom/animation/test/style/test_animation-setting-effect_to_rename.html
+rename from dom/animation/test/style/file_animation-setting-effect.html
+rename to dom/animation/test/style/test_animation-setting-effect_to_rename.html
+--- a/dom/animation/test/style/file_animation-setting-effect.html
++++ b/dom/animation/test/style/test_animation-setting-effect_to_rename.html
+@@ -1,16 +1,19 @@
+ <!doctype html>
+ <html>
+   <head>
+     <meta charset=utf-8>
+     <title>Tests for setting effects by using Animation.effect</title>
++    <script src="/resources/testharness.js"></script>
++    <script src="/resources/testharnessreport.js"></script>
+     <script src='../testcommon.js'></script>
+   </head>
+   <body>
++    <div id="log"></div>
+     <script type='text/javascript'>
+ 
+ 'use strict';
+ 
+ test(function(t) {
+   var target = addDiv(t);
+   var anim = new Animation();
+   anim.effect = new KeyframeEffectReadOnly(target,
+@@ -114,12 +117,11 @@ test(function(t) {
+   var effectA = animA.effect;
+   animA.effect = animB.effect;
+   animB.effect = effectA;
+   assert_equals(getComputedStyle(target).marginLeft, '60px');
+   assert_equals(getComputedStyle(target).marginTop, '10px');
+ }, 'After swapping effects of two playing animations, both animations are ' +
+    'still running with the same current time');
+ 
+-done();
+     </script>
+   </body>
+ </html>
+diff --git a/dom/animation/test/style/test_composite.html b/dom/animation/test/style/test_composite.html
+deleted file mode 100644
+--- a/dom/animation/test/style/test_composite.html
++++ /dev/null
+@@ -1,15 +0,0 @@
+-<!doctype html>
+-<meta charset=utf-8>
+-<script src='/resources/testharness.js'></script>
+-<script src='/resources/testharnessreport.js'></script>
+-<div id='log'></div>
+-<script>
+-'use strict';
+-setup({explicit_done: true});
+-SpecialPowers.pushPrefEnv(
+-  { 'set': [['dom.animations-api.core.enabled', true]] },
+-  function() {
+-    window.open('file_composite.html');
+-  });
+-</script>
+-</html>
+diff --git a/dom/animation/test/style/file_composite.html b/dom/animation/test/style/test_composite_to_rename.html
+rename from dom/animation/test/style/file_composite.html
+rename to dom/animation/test/style/test_composite_to_rename.html
+--- a/dom/animation/test/style/file_composite.html
++++ b/dom/animation/test/style/test_composite_to_rename.html
+@@ -1,21 +1,24 @@
+ <!doctype html>
+ <meta charset=utf-8>
++<script src="/resources/testharness.js"></script>
++<script src="/resources/testharnessreport.js"></script>
+ <script src="../testcommon.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <style>
+ div {
+   /* Element needs geometry to be eligible for layerization */
+   width: 20px;
+   height: 20px;
+   background-color: white;
+ }
+ </style>
+ <body>
++<div id="log"></div>
+ <script>
+ 'use strict';
+ 
+ if (!SpecialPowers.DOMWindowUtils.layerManagerRemote ||
+     !SpecialPowers.getBoolPref(
+       'layers.offmainthreadcomposition.async-animations')) {
+   // If OMTA is disabled, nothing to run.
+   done();
+@@ -130,11 +133,10 @@ promise_test(t => {
+     var transform =
+       SpecialPowers.DOMWindowUtils.getOMTAStyle(div, 'transform');
+     assert_matrix_equals(transform, 'matrix(5, 0, 0, 5, 0, 0)',
+       // (scale(2 - 1) + scale(4 - 1) + scale(1))
+       'The accumulate scale value should be scale(5)');
+   });
+ }, 'Composite operation change');
+ 
+-done();
+ </script>
+ </body>
+diff --git a/dom/animation/test/style/test_missing-keyframe-on-compositor.html b/dom/animation/test/style/test_missing-keyframe-on-compositor.html
+deleted file mode 100644
+--- a/dom/animation/test/style/test_missing-keyframe-on-compositor.html
++++ /dev/null
+@@ -1,15 +0,0 @@
+-<!doctype html>
+-<meta charset=utf-8>
+-<script src='/resources/testharness.js'></script>
+-<script src='/resources/testharnessreport.js'></script>
+-<div id='log'></div>
+-<script>
+-'use strict';
+-setup({explicit_done: true});
+-SpecialPowers.pushPrefEnv(
+-  { 'set': [['dom.animations-api.core.enabled', true]] },
+-  function() {
+-    window.open('file_missing-keyframe-on-compositor.html');
+-  });
+-</script>
+-</html>
+diff --git a/dom/animation/test/style/file_missing-keyframe-on-compositor.html b/dom/animation/test/style/test_missing-keyframe-on-compositor_to_rename.html
+rename from dom/animation/test/style/file_missing-keyframe-on-compositor.html
+rename to dom/animation/test/style/test_missing-keyframe-on-compositor_to_rename.html
+--- a/dom/animation/test/style/file_missing-keyframe-on-compositor.html
++++ b/dom/animation/test/style/test_missing-keyframe-on-compositor_to_rename.html
+@@ -1,21 +1,24 @@
+ <!doctype html>
+ <meta charset=utf-8>
++<script src="/resources/testharness.js"></script>
++<script src="/resources/testharnessreport.js"></script>
+ <script src="../testcommon.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <style>
+ div {
+   /* Element needs geometry to be eligible for layerization */
+   width: 100px;
+   height: 100px;
+   background-color: white;
+ }
+ </style>
+ <body>
++<div id="log"></div>
+ <script>
+ 'use strict';
+ 
+ if (!SpecialPowers.DOMWindowUtils.layerManagerRemote ||
+     !SpecialPowers.getBoolPref(
+       'layers.offmainthreadcomposition.async-animations')) {
+   // If OMTA is disabled, nothing to run.
+   done();
+@@ -555,11 +558,10 @@ promise_test(t => {
+     // (150px + 300px) * (50 * MS_PER_SEC / 100 * MS_PER_SEC) = 225px.
+     assert_matrix_equals(transform, 'matrix(1, 0, 0, 1, 225, 0)',
+       'Composed transform value should be composed onto the value of ' +
+       'lower-priority animation without timeline');
+   });
+ }, 'Transform value for animation with no keyframe at offset 0 at 50% when ' +
+    'composed onto an animation without timeline');
+ 
+-done();
+ </script>
+ </body>
+diff --git a/dom/animation/test/style/test_missing-keyframe.html b/dom/animation/test/style/test_missing-keyframe.html
+deleted file mode 100644
+--- a/dom/animation/test/style/test_missing-keyframe.html
++++ /dev/null
+@@ -1,15 +0,0 @@
+-<!doctype html>
+-<meta charset=utf-8>
+-<script src='/resources/testharness.js'></script>
+-<script src='/resources/testharnessreport.js'></script>
+-<div id='log'></div>
+-<script>
+-'use strict';
+-setup({explicit_done: true});
+-SpecialPowers.pushPrefEnv(
+-  { 'set': [['dom.animations-api.core.enabled', true]] },
+-  function() {
+-    window.open('file_missing-keyframe.html');
+-  });
+-</script>
+-</html>
+diff --git a/dom/animation/test/style/file_missing-keyframe.html b/dom/animation/test/style/test_missing-keyframe_to_rename.html
+rename from dom/animation/test/style/file_missing-keyframe.html
+rename to dom/animation/test/style/test_missing-keyframe_to_rename.html
+--- a/dom/animation/test/style/file_missing-keyframe.html
++++ b/dom/animation/test/style/test_missing-keyframe_to_rename.html
+@@ -1,12 +1,15 @@
+ <!doctype html>
+ <meta charset=utf-8>
++<script src="/resources/testharness.js"></script>
++<script src="/resources/testharnessreport.js"></script>
+ <script src="../testcommon.js"></script>
+ <body>
++<div id="log"></div>
+ <script>
+ 'use strict';
+ 
+ test(t => {
+   var div = addDiv(t, { style: 'margin-left: 100px' });
+   div.animate([{ marginLeft: '200px' }], 100 * MS_PER_SEC);
+ 
+   assert_equals(getComputedStyle(div).marginLeft, '100px',
+@@ -98,11 +101,10 @@ test(t => {
+                                 iterationComposite: 'accumulate' });
+ 
+   assert_equals(getComputedStyle(div).marginLeft, '300px',
+                 'The margin-left value should be additive value of the ' +
+                 'accumulation of the initial value onto the base value ');
+ }, 'margin-left value for an animation with no keyframe at offset 1 and its ' +
+    'iterationComposite is accumulate');
+ 
+-done();
+ </script>
+ </body>

+ 282 - 0
frg/mozilla-release/work-js/1382841-2-60a1.patch

@@ -0,0 +1,282 @@
+# HG changeset patch
+# User Brian Birtles <birtles@gmail.com>
+# Date 1520838515 -32400
+# Node ID 965de8e8fccaea52fe9ec2bb93e5e8d3b1ee87b6
+# Parent  8d1b9810c5f6135b0f92c2d6b97aecf79c0de9df
+Bug 1382841 - Rename test_XXX_to_rename.html files in dom/animation/test to test_XXX.html; r=hiro
+
+See the description in this changeset's parent which describes why we do this in
+two steps.
+
+MozReview-Commit-ID: GtO0SCb0UOF
+
+diff --git a/dom/animation/test/css-animations/test_animation-cancel_to_rename.html b/dom/animation/test/css-animations/test_animation-cancel.html
+rename from dom/animation/test/css-animations/test_animation-cancel_to_rename.html
+rename to dom/animation/test/css-animations/test_animation-cancel.html
+diff --git a/dom/animation/test/css-animations/test_animation-computed-timing_to_rename.html b/dom/animation/test/css-animations/test_animation-computed-timing.html
+rename from dom/animation/test/css-animations/test_animation-computed-timing_to_rename.html
+rename to dom/animation/test/css-animations/test_animation-computed-timing.html
+diff --git a/dom/animation/test/css-animations/test_animation-currenttime_to_rename.html b/dom/animation/test/css-animations/test_animation-currenttime.html
+rename from dom/animation/test/css-animations/test_animation-currenttime_to_rename.html
+rename to dom/animation/test/css-animations/test_animation-currenttime.html
+diff --git a/dom/animation/test/css-animations/test_animation-finish_to_rename.html b/dom/animation/test/css-animations/test_animation-finish.html
+rename from dom/animation/test/css-animations/test_animation-finish_to_rename.html
+rename to dom/animation/test/css-animations/test_animation-finish.html
+diff --git a/dom/animation/test/css-animations/test_animation-finished_to_rename.html b/dom/animation/test/css-animations/test_animation-finished.html
+rename from dom/animation/test/css-animations/test_animation-finished_to_rename.html
+rename to dom/animation/test/css-animations/test_animation-finished.html
+diff --git a/dom/animation/test/css-animations/test_animation-id_to_rename.html b/dom/animation/test/css-animations/test_animation-id.html
+rename from dom/animation/test/css-animations/test_animation-id_to_rename.html
+rename to dom/animation/test/css-animations/test_animation-id.html
+diff --git a/dom/animation/test/css-animations/test_animation-pausing_to_rename.html b/dom/animation/test/css-animations/test_animation-pausing.html
+rename from dom/animation/test/css-animations/test_animation-pausing_to_rename.html
+rename to dom/animation/test/css-animations/test_animation-pausing.html
+diff --git a/dom/animation/test/css-animations/test_animation-playstate_to_rename.html b/dom/animation/test/css-animations/test_animation-playstate.html
+rename from dom/animation/test/css-animations/test_animation-playstate_to_rename.html
+rename to dom/animation/test/css-animations/test_animation-playstate.html
+diff --git a/dom/animation/test/css-animations/test_animation-ready_to_rename.html b/dom/animation/test/css-animations/test_animation-ready.html
+rename from dom/animation/test/css-animations/test_animation-ready_to_rename.html
+rename to dom/animation/test/css-animations/test_animation-ready.html
+diff --git a/dom/animation/test/css-animations/test_animation-reverse_to_rename.html b/dom/animation/test/css-animations/test_animation-reverse.html
+rename from dom/animation/test/css-animations/test_animation-reverse_to_rename.html
+rename to dom/animation/test/css-animations/test_animation-reverse.html
+diff --git a/dom/animation/test/css-animations/test_animation-starttime_to_rename.html b/dom/animation/test/css-animations/test_animation-starttime.html
+rename from dom/animation/test/css-animations/test_animation-starttime_to_rename.html
+rename to dom/animation/test/css-animations/test_animation-starttime.html
+diff --git a/dom/animation/test/css-animations/test_animations-dynamic-changes_to_rename.html b/dom/animation/test/css-animations/test_animations-dynamic-changes.html
+rename from dom/animation/test/css-animations/test_animations-dynamic-changes_to_rename.html
+rename to dom/animation/test/css-animations/test_animations-dynamic-changes.html
+diff --git a/dom/animation/test/css-animations/test_cssanimation-animationname_to_rename.html b/dom/animation/test/css-animations/test_cssanimation-animationname.html
+rename from dom/animation/test/css-animations/test_cssanimation-animationname_to_rename.html
+rename to dom/animation/test/css-animations/test_cssanimation-animationname.html
+diff --git a/dom/animation/test/css-animations/test_document-get-animations_to_rename.html b/dom/animation/test/css-animations/test_document-get-animations.html
+rename from dom/animation/test/css-animations/test_document-get-animations_to_rename.html
+rename to dom/animation/test/css-animations/test_document-get-animations.html
+diff --git a/dom/animation/test/css-animations/test_effect-target_to_rename.html b/dom/animation/test/css-animations/test_effect-target.html
+rename from dom/animation/test/css-animations/test_effect-target_to_rename.html
+rename to dom/animation/test/css-animations/test_effect-target.html
+diff --git a/dom/animation/test/css-animations/test_element-get-animations_to_rename.html b/dom/animation/test/css-animations/test_element-get-animations.html
+rename from dom/animation/test/css-animations/test_element-get-animations_to_rename.html
+rename to dom/animation/test/css-animations/test_element-get-animations.html
+diff --git a/dom/animation/test/css-animations/test_event-dispatch_to_rename.html b/dom/animation/test/css-animations/test_event-dispatch.html
+rename from dom/animation/test/css-animations/test_event-dispatch_to_rename.html
+rename to dom/animation/test/css-animations/test_event-dispatch.html
+diff --git a/dom/animation/test/css-animations/test_event-order_to_rename.html b/dom/animation/test/css-animations/test_event-order.html
+rename from dom/animation/test/css-animations/test_event-order_to_rename.html
+rename to dom/animation/test/css-animations/test_event-order.html
+diff --git a/dom/animation/test/css-animations/test_keyframeeffect-getkeyframes_to_rename.html b/dom/animation/test/css-animations/test_keyframeeffect-getkeyframes.html
+rename from dom/animation/test/css-animations/test_keyframeeffect-getkeyframes_to_rename.html
+rename to dom/animation/test/css-animations/test_keyframeeffect-getkeyframes.html
+diff --git a/dom/animation/test/css-animations/test_pseudoElement-get-animations_to_rename.html b/dom/animation/test/css-animations/test_pseudoElement-get-animations.html
+rename from dom/animation/test/css-animations/test_pseudoElement-get-animations_to_rename.html
+rename to dom/animation/test/css-animations/test_pseudoElement-get-animations.html
+diff --git a/dom/animation/test/css-animations/test_setting-effect_to_rename.html b/dom/animation/test/css-animations/test_setting-effect.html
+rename from dom/animation/test/css-animations/test_setting-effect_to_rename.html
+rename to dom/animation/test/css-animations/test_setting-effect.html
+diff --git a/dom/animation/test/css-transitions/test_animation-cancel_to_rename.html b/dom/animation/test/css-transitions/test_animation-cancel.html
+rename from dom/animation/test/css-transitions/test_animation-cancel_to_rename.html
+rename to dom/animation/test/css-transitions/test_animation-cancel.html
+diff --git a/dom/animation/test/css-transitions/test_animation-computed-timing_to_rename.html b/dom/animation/test/css-transitions/test_animation-computed-timing.html
+rename from dom/animation/test/css-transitions/test_animation-computed-timing_to_rename.html
+rename to dom/animation/test/css-transitions/test_animation-computed-timing.html
+diff --git a/dom/animation/test/css-transitions/test_animation-currenttime_to_rename.html b/dom/animation/test/css-transitions/test_animation-currenttime.html
+rename from dom/animation/test/css-transitions/test_animation-currenttime_to_rename.html
+rename to dom/animation/test/css-transitions/test_animation-currenttime.html
+diff --git a/dom/animation/test/css-transitions/test_animation-finished_to_rename.html b/dom/animation/test/css-transitions/test_animation-finished.html
+rename from dom/animation/test/css-transitions/test_animation-finished_to_rename.html
+rename to dom/animation/test/css-transitions/test_animation-finished.html
+diff --git a/dom/animation/test/css-transitions/test_animation-pausing_to_rename.html b/dom/animation/test/css-transitions/test_animation-pausing.html
+rename from dom/animation/test/css-transitions/test_animation-pausing_to_rename.html
+rename to dom/animation/test/css-transitions/test_animation-pausing.html
+diff --git a/dom/animation/test/css-transitions/test_animation-ready_to_rename.html b/dom/animation/test/css-transitions/test_animation-ready.html
+rename from dom/animation/test/css-transitions/test_animation-ready_to_rename.html
+rename to dom/animation/test/css-transitions/test_animation-ready.html
+diff --git a/dom/animation/test/css-transitions/test_animation-starttime_to_rename.html b/dom/animation/test/css-transitions/test_animation-starttime.html
+rename from dom/animation/test/css-transitions/test_animation-starttime_to_rename.html
+rename to dom/animation/test/css-transitions/test_animation-starttime.html
+diff --git a/dom/animation/test/css-transitions/test_csstransition-transitionproperty_to_rename.html b/dom/animation/test/css-transitions/test_csstransition-transitionproperty.html
+rename from dom/animation/test/css-transitions/test_csstransition-transitionproperty_to_rename.html
+rename to dom/animation/test/css-transitions/test_csstransition-transitionproperty.html
+diff --git a/dom/animation/test/css-transitions/test_document-get-animations_to_rename.html b/dom/animation/test/css-transitions/test_document-get-animations.html
+rename from dom/animation/test/css-transitions/test_document-get-animations_to_rename.html
+rename to dom/animation/test/css-transitions/test_document-get-animations.html
+diff --git a/dom/animation/test/css-transitions/test_effect-target_to_rename.html b/dom/animation/test/css-transitions/test_effect-target.html
+rename from dom/animation/test/css-transitions/test_effect-target_to_rename.html
+rename to dom/animation/test/css-transitions/test_effect-target.html
+diff --git a/dom/animation/test/css-transitions/test_element-get-animations_to_rename.html b/dom/animation/test/css-transitions/test_element-get-animations.html
+rename from dom/animation/test/css-transitions/test_element-get-animations_to_rename.html
+rename to dom/animation/test/css-transitions/test_element-get-animations.html
+diff --git a/dom/animation/test/css-transitions/test_event-dispatch_to_rename.html b/dom/animation/test/css-transitions/test_event-dispatch.html
+rename from dom/animation/test/css-transitions/test_event-dispatch_to_rename.html
+rename to dom/animation/test/css-transitions/test_event-dispatch.html
+diff --git a/dom/animation/test/css-transitions/test_keyframeeffect-getkeyframes_to_rename.html b/dom/animation/test/css-transitions/test_keyframeeffect-getkeyframes.html
+rename from dom/animation/test/css-transitions/test_keyframeeffect-getkeyframes_to_rename.html
+rename to dom/animation/test/css-transitions/test_keyframeeffect-getkeyframes.html
+diff --git a/dom/animation/test/css-transitions/test_pseudoElement-get-animations_to_rename.html b/dom/animation/test/css-transitions/test_pseudoElement-get-animations.html
+rename from dom/animation/test/css-transitions/test_pseudoElement-get-animations_to_rename.html
+rename to dom/animation/test/css-transitions/test_pseudoElement-get-animations.html
+diff --git a/dom/animation/test/css-transitions/test_setting-effect_to_rename.html b/dom/animation/test/css-transitions/test_setting-effect.html
+rename from dom/animation/test/css-transitions/test_setting-effect_to_rename.html
+rename to dom/animation/test/css-transitions/test_setting-effect.html
+diff --git a/dom/animation/test/document-timeline/test_document-timeline_to_rename.html b/dom/animation/test/document-timeline/test_document-timeline.html
+rename from dom/animation/test/document-timeline/test_document-timeline_to_rename.html
+rename to dom/animation/test/document-timeline/test_document-timeline.html
+diff --git a/dom/animation/test/mochitest.ini b/dom/animation/test/mochitest.ini
+--- a/dom/animation/test/mochitest.ini
++++ b/dom/animation/test/mochitest.ini
+@@ -10,74 +10,74 @@ support-files =
+   mozilla/file_deferred_start.html
+   mozilla/file_disable_animations_api_core.html
+   mozilla/file_discrete-animations.html
+   mozilla/file_restyles.html
+   mozilla/file_transition_finish_on_compositor.html
+   ../../../layout/style/test/property_database.js
+   testcommon.js
+ 
+-[css-animations/test_animations-dynamic-changes_to_rename.html]
+-[css-animations/test_animation-cancel_to_rename.html]
+-[css-animations/test_animation-computed-timing_to_rename.html]
+-[css-animations/test_animation-currenttime_to_rename.html]
+-[css-animations/test_animation-finish_to_rename.html]
+-[css-animations/test_animation-finished_to_rename.html]
+-[css-animations/test_animation-id_to_rename.html]
+-[css-animations/test_animation-pausing_to_rename.html]
+-[css-animations/test_animation-playstate_to_rename.html]
+-[css-animations/test_animation-ready_to_rename.html]
+-[css-animations/test_animation-reverse_to_rename.html]
+-[css-animations/test_animation-starttime_to_rename.html]
+-[css-animations/test_cssanimation-animationname_to_rename.html]
+-[css-animations/test_document-get-animations_to_rename.html]
+-[css-animations/test_effect-target_to_rename.html]
+-[css-animations/test_element-get-animations_to_rename.html]
+-[css-animations/test_event-dispatch_to_rename.html]
+-[css-animations/test_event-order_to_rename.html]
+-[css-animations/test_keyframeeffect-getkeyframes_to_rename.html]
+-[css-animations/test_pseudoElement-get-animations_to_rename.html]
+-[css-animations/test_setting-effect_to_rename.html]
+-[css-transitions/test_animation-cancel_to_rename.html]
+-[css-transitions/test_animation-computed-timing_to_rename.html]
+-[css-transitions/test_animation-currenttime_to_rename.html]
+-[css-transitions/test_animation-finished_to_rename.html]
+-[css-transitions/test_animation-pausing_to_rename.html]
+-[css-transitions/test_animation-ready_to_rename.html]
+-[css-transitions/test_animation-starttime_to_rename.html]
+-[css-transitions/test_csstransition-transitionproperty_to_rename.html]
+-[css-transitions/test_document-get-animations_to_rename.html]
+-[css-transitions/test_effect-target_to_rename.html]
+-[css-transitions/test_element-get-animations_to_rename.html]
+-[css-transitions/test_event-dispatch_to_rename.html]
+-[css-transitions/test_keyframeeffect-getkeyframes_to_rename.html]
+-[css-transitions/test_pseudoElement-get-animations_to_rename.html]
+-[css-transitions/test_setting-effect_to_rename.html]
+-[document-timeline/test_document-timeline_to_rename.html]
++[css-animations/test_animations-dynamic-changes.html]
++[css-animations/test_animation-cancel.html]
++[css-animations/test_animation-computed-timing.html]
++[css-animations/test_animation-currenttime.html]
++[css-animations/test_animation-finish.html]
++[css-animations/test_animation-finished.html]
++[css-animations/test_animation-id.html]
++[css-animations/test_animation-pausing.html]
++[css-animations/test_animation-playstate.html]
++[css-animations/test_animation-ready.html]
++[css-animations/test_animation-reverse.html]
++[css-animations/test_animation-starttime.html]
++[css-animations/test_cssanimation-animationname.html]
++[css-animations/test_document-get-animations.html]
++[css-animations/test_effect-target.html]
++[css-animations/test_element-get-animations.html]
++[css-animations/test_event-dispatch.html]
++[css-animations/test_event-order.html]
++[css-animations/test_keyframeeffect-getkeyframes.html]
++[css-animations/test_pseudoElement-get-animations.html]
++[css-animations/test_setting-effect.html]
++[css-transitions/test_animation-cancel.html]
++[css-transitions/test_animation-computed-timing.html]
++[css-transitions/test_animation-currenttime.html]
++[css-transitions/test_animation-finished.html]
++[css-transitions/test_animation-pausing.html]
++[css-transitions/test_animation-ready.html]
++[css-transitions/test_animation-starttime.html]
++[css-transitions/test_csstransition-transitionproperty.html]
++[css-transitions/test_document-get-animations.html]
++[css-transitions/test_effect-target.html]
++[css-transitions/test_element-get-animations.html]
++[css-transitions/test_event-dispatch.html]
++[css-transitions/test_keyframeeffect-getkeyframes.html]
++[css-transitions/test_pseudoElement-get-animations.html]
++[css-transitions/test_setting-effect.html]
++[document-timeline/test_document-timeline.html]
+ [document-timeline/test_request_animation_frame.html]
+ [mozilla/test_cascade.html]
+-[mozilla/test_cubic_bezier_limits_to_rename.html]
++[mozilla/test_cubic_bezier_limits.html]
+ [mozilla/test_deferred_start.html]
+ skip-if = (toolkit == 'android' && debug) || (os == 'win' && bits == 64) # Bug 1363957
+ [mozilla/test_disable_animations_api_core.html]
+-[mozilla/test_disabled_properties_to_rename.html]
++[mozilla/test_disabled_properties.html]
+ [mozilla/test_discrete-animations.html]
+ [mozilla/test_distance_of_basic_shape.html]
+ [mozilla/test_distance_of_filter.html]
+ [mozilla/test_distance_of_transform.html]
+ [mozilla/test_document_timeline_origin_time_range.html]
+-[mozilla/test_hide_and_show_to_rename.html]
++[mozilla/test_hide_and_show.html]
+ [mozilla/test_moz-prefixed-properties.html]
+ [mozilla/test_restyles.html]
+-[mozilla/test_restyling_xhr_doc_to_rename.html]
++[mozilla/test_restyling_xhr_doc.html]
+ [mozilla/test_set_easing.html]
+-[mozilla/test_transform_limits_to_rename.html]
++[mozilla/test_transform_limits.html]
+ [mozilla/test_transition_finish_on_compositor.html]
+ skip-if = toolkit == 'android'
+ [mozilla/test_underlying_discrete_value.html]
+-[style/test_animation-seeking-with-current-time_to_rename.html]
+-[style/test_animation-seeking-with-start-time_to_rename.html]
+-[style/test_animation-setting-effect_to_rename.html]
+-[style/test_composite_to_rename.html]
++[style/test_animation-seeking-with-current-time.html]
++[style/test_animation-seeking-with-start-time.html]
++[style/test_animation-setting-effect.html]
++[style/test_composite.html]
+ [style/test_interpolation-from-interpolatematrix-to-none.html]
+-[style/test_missing-keyframe_to_rename.html]
+-[style/test_missing-keyframe-on-compositor_to_rename.html]
++[style/test_missing-keyframe.html]
++[style/test_missing-keyframe-on-compositor.html]
+ [style/test_transform-non-normalizable-rotate3d.html]
+diff --git a/dom/animation/test/mozilla/test_cubic_bezier_limits_to_rename.html b/dom/animation/test/mozilla/test_cubic_bezier_limits.html
+rename from dom/animation/test/mozilla/test_cubic_bezier_limits_to_rename.html
+rename to dom/animation/test/mozilla/test_cubic_bezier_limits.html
+diff --git a/dom/animation/test/mozilla/test_disabled_properties_to_rename.html b/dom/animation/test/mozilla/test_disabled_properties.html
+rename from dom/animation/test/mozilla/test_disabled_properties_to_rename.html
+rename to dom/animation/test/mozilla/test_disabled_properties.html
+diff --git a/dom/animation/test/mozilla/test_hide_and_show_to_rename.html b/dom/animation/test/mozilla/test_hide_and_show.html
+rename from dom/animation/test/mozilla/test_hide_and_show_to_rename.html
+rename to dom/animation/test/mozilla/test_hide_and_show.html
+diff --git a/dom/animation/test/mozilla/test_restyling_xhr_doc_to_rename.html b/dom/animation/test/mozilla/test_restyling_xhr_doc.html
+rename from dom/animation/test/mozilla/test_restyling_xhr_doc_to_rename.html
+rename to dom/animation/test/mozilla/test_restyling_xhr_doc.html
+diff --git a/dom/animation/test/mozilla/test_transform_limits_to_rename.html b/dom/animation/test/mozilla/test_transform_limits.html
+rename from dom/animation/test/mozilla/test_transform_limits_to_rename.html
+rename to dom/animation/test/mozilla/test_transform_limits.html
+diff --git a/dom/animation/test/style/test_animation-seeking-with-current-time_to_rename.html b/dom/animation/test/style/test_animation-seeking-with-current-time.html
+rename from dom/animation/test/style/test_animation-seeking-with-current-time_to_rename.html
+rename to dom/animation/test/style/test_animation-seeking-with-current-time.html
+diff --git a/dom/animation/test/style/test_animation-seeking-with-start-time_to_rename.html b/dom/animation/test/style/test_animation-seeking-with-start-time.html
+rename from dom/animation/test/style/test_animation-seeking-with-start-time_to_rename.html
+rename to dom/animation/test/style/test_animation-seeking-with-start-time.html
+diff --git a/dom/animation/test/style/test_animation-setting-effect_to_rename.html b/dom/animation/test/style/test_animation-setting-effect.html
+rename from dom/animation/test/style/test_animation-setting-effect_to_rename.html
+rename to dom/animation/test/style/test_animation-setting-effect.html
+diff --git a/dom/animation/test/style/test_composite_to_rename.html b/dom/animation/test/style/test_composite.html
+rename from dom/animation/test/style/test_composite_to_rename.html
+rename to dom/animation/test/style/test_composite.html
+diff --git a/dom/animation/test/style/test_missing-keyframe-on-compositor_to_rename.html b/dom/animation/test/style/test_missing-keyframe-on-compositor.html
+rename from dom/animation/test/style/test_missing-keyframe-on-compositor_to_rename.html
+rename to dom/animation/test/style/test_missing-keyframe-on-compositor.html
+diff --git a/dom/animation/test/style/test_missing-keyframe_to_rename.html b/dom/animation/test/style/test_missing-keyframe.html
+rename from dom/animation/test/style/test_missing-keyframe_to_rename.html
+rename to dom/animation/test/style/test_missing-keyframe.html

+ 83 - 0
frg/mozilla-release/work-js/1382841-3-60a1.patch

@@ -0,0 +1,83 @@
+# HG changeset patch
+# User Brian Birtles <birtles@gmail.com>
+# Date 1520838516 -32400
+# Node ID f6e2ee0c603b171d4be938f88369ebd39bd099a9
+# Parent  f41695498d76fcb7547f682dc9a7ed2c2e652fa0
+Bug 1382841 - Rename some test files in dom/animation/test/mozilla to use _ to separate words; r=hiro
+
+This naming convention is described fully in this changeset's grandparent.
+
+MozReview-Commit-ID: 5EBpVrOdxD2
+
+diff --git a/dom/animation/test/mochitest.ini b/dom/animation/test/mochitest.ini
+--- a/dom/animation/test/mochitest.ini
++++ b/dom/animation/test/mochitest.ini
+@@ -4,17 +4,17 @@ prefs =
+   dom.animations-api.pending-member.enabled=true
+ # Support files for chrome tests that we want to load over HTTP need
+ # to go in here, not chrome.ini.
+ support-files =
+   chrome/file_animate_xrays.html
+   mozilla/xhr_doc.html
+   mozilla/file_deferred_start.html
+   mozilla/file_disable_animations_api_core.html
+-  mozilla/file_discrete-animations.html
++  mozilla/file_discrete_animations.html
+   mozilla/file_restyles.html
+   mozilla/file_transition_finish_on_compositor.html
+   ../../../layout/style/test/property_database.js
+   testcommon.js
+ 
+ [css-animations/test_animations-dynamic-changes.html]
+ [css-animations/test_animation-cancel.html]
+ [css-animations/test_animation-computed-timing.html]
+@@ -54,23 +54,23 @@ support-files =
+ [document-timeline/test_document-timeline.html]
+ [document-timeline/test_request_animation_frame.html]
+ [mozilla/test_cascade.html]
+ [mozilla/test_cubic_bezier_limits.html]
+ [mozilla/test_deferred_start.html]
+ skip-if = (toolkit == 'android' && debug) || (os == 'win' && bits == 64) # Bug 1363957
+ [mozilla/test_disable_animations_api_core.html]
+ [mozilla/test_disabled_properties.html]
+-[mozilla/test_discrete-animations.html]
++[mozilla/test_discrete_animations.html]
+ [mozilla/test_distance_of_basic_shape.html]
+ [mozilla/test_distance_of_filter.html]
+ [mozilla/test_distance_of_transform.html]
+ [mozilla/test_document_timeline_origin_time_range.html]
+ [mozilla/test_hide_and_show.html]
+-[mozilla/test_moz-prefixed-properties.html]
++[mozilla/test_moz_prefixed_properties.html]
+ [mozilla/test_restyles.html]
+ [mozilla/test_restyling_xhr_doc.html]
+ [mozilla/test_set_easing.html]
+ [mozilla/test_transform_limits.html]
+ [mozilla/test_transition_finish_on_compositor.html]
+ skip-if = toolkit == 'android'
+ [mozilla/test_underlying_discrete_value.html]
+ [style/test_animation-seeking-with-current-time.html]
+diff --git a/dom/animation/test/mozilla/file_discrete-animations.html b/dom/animation/test/mozilla/file_discrete_animations.html
+rename from dom/animation/test/mozilla/file_discrete-animations.html
+rename to dom/animation/test/mozilla/file_discrete_animations.html
+diff --git a/dom/animation/test/mozilla/test_discrete-animations.html b/dom/animation/test/mozilla/test_discrete_animations.html
+rename from dom/animation/test/mozilla/test_discrete-animations.html
+rename to dom/animation/test/mozilla/test_discrete_animations.html
+--- a/dom/animation/test/mozilla/test_discrete-animations.html
++++ b/dom/animation/test/mozilla/test_discrete_animations.html
+@@ -7,11 +7,11 @@
+ 'use strict';
+ setup({explicit_done: true});
+ SpecialPowers.pushPrefEnv(
+   { "set": [
+     ["layout.css.osx-font-smoothing.enabled", true],
+     ["layout.css.prefixes.webkit", true]
+   ] },
+   function() {
+-    window.open("file_discrete-animations.html");
++    window.open("file_discrete_animations.html");
+   });
+ </script>
+diff --git a/dom/animation/test/mozilla/test_moz-prefixed-properties.html b/dom/animation/test/mozilla/test_moz_prefixed_properties.html
+rename from dom/animation/test/mozilla/test_moz-prefixed-properties.html
+rename to dom/animation/test/mozilla/test_moz_prefixed_properties.html

+ 1008 - 0
frg/mozilla-release/work-js/1382841-4-60a1.patch

@@ -0,0 +1,1008 @@
+# HG changeset patch
+# User Brian Birtles <birtles@gmail.com>
+# Date 1520838516 -32400
+# Node ID 41d5430e77dcfacc58485ddeda550b536f3008dd
+# Parent  f320943fb4553ed1d0379cfc0f64ac609becd8cd
+Bug 1382841 - Remove old test_XXX openers that just set the dom.animations-api.core.enabled pref from layout/style/test**; r=hiro
+
+As with an earlier patch in this series, we rename the file_* test content files
+to test_*_to_rename.html in this patch, and then in a subsequent patch drop the
+_to_rename suffix so that we can trace the history of the test_* files back to
+their file_* equivalents.
+
+MozReview-Commit-ID: Jes8xSQzkCF
+
+diff --git a/layout/style/test/mochitest.ini b/layout/style/test/mochitest.ini
+--- a/layout/style/test/mochitest.ini
++++ b/layout/style/test/mochitest.ini
+@@ -1,9 +1,11 @@
+ [DEFAULT]
++prefs =
++  dom.animations-api.core.enabled=true
+ support-files =
+   animation_utils.js
+   ccd-quirks.html
+   ccd.sjs
+   ccd-standards.html
+   chrome/bug418986-2.js
+   chrome/match.png
+   chrome/mismatch.png
+@@ -45,36 +47,28 @@ support-files = additional_sheets_helper
+ [test_align_shorthand_serialization.html]
+ skip-if = stylo # bug 1339656
+ [test_all_shorthand.html]
+ [test_animations.html]
+ skip-if = (toolkit == 'android')
+ [test_animations_async_tests.html]
+ support-files = Ahem.ttf file_animations_async_tests.html
+ [test_animations_dynamic_changes.html]
+-[test_animations_effect_timing_duration.html]
+-support-files = file_animations_effect_timing_duration.html
+-[test_animations_effect_timing_enddelay.html]
+-support-files = file_animations_effect_timing_enddelay.html
+-[test_animations_effect_timing_iterations.html]
+-support-files = file_animations_effect_timing_iterations.html
++[test_animations_effect_timing_duration_to_rename.html]
++[test_animations_effect_timing_enddelay_to_rename.html]
++[test_animations_effect_timing_iterations_to_rename.html]
+ [test_animations_event_order.html]
+ [test_animations_event_handler_attribute.html]
+-[test_animations_iterationstart.html]
+-support-files = file_animations_iterationstart.html
++[test_animations_iterationstart_to_rename.html]
+ [test_animations_omta.html]
+ [test_animations_omta_start.html]
+-[test_animations_pausing.html]
+-support-files = file_animations_pausing.html
+-[test_animations_playbackrate.html]
+-support-files = file_animations_playbackrate.html
+-[test_animations_reverse.html]
+-support-files = file_animations_reverse.html
+-[test_animations_styles_on_event.html]
+-support-files = file_animations_styles_on_event.html
++[test_animations_pausing_to_rename.html]
++[test_animations_playbackrate_to_rename.html]
++[test_animations_reverse_to_rename.html]
++[test_animations_styles_on_event_to_rename.html]
+ [test_animations_variable_changes.html]
+ [test_animations_with_disabled_properties.html]
+ support-files = file_animations_with_disabled_properties.html
+ [test_any_dynamic.html]
+ [test_asyncopen2.html]
+ [test_at_rule_parse_serialize.html]
+ [test_attribute_selector_eof_behavior.html]
+ [test_background_blend_mode.html]
+@@ -322,18 +316,17 @@ skip-if = !stylo # no need to test block
+ [test_transitions_computed_value_combinations.html]
+ [test_transitions_events.html]
+ [test_transitions.html]
+ skip-if = (android_version == '18' && debug) # bug 1159532
+ [test_transitions_bug537151.html]
+ [test_transitions_dynamic_changes.html]
+ [test_transitions_per_property.html]
+ skip-if = (toolkit == 'android') # bug 775227 for android
+-[test_transitions_replacement_on_busy_frame.html]
+-support-files = file_transitions_replacement_on_busy_frame.html
++[test_transitions_replacement_on_busy_frame_to_rename.html]
+ [test_transitions_step_functions.html]
+ [test_transitions_with_disabled_properties.html]
+ support-files = file_transitions_with_disabled_properties.html
+ [test_unclosed_parentheses.html]
+ [test_unicode_range_loading.html]
+ support-files = ../../reftests/fonts/markA.woff ../../reftests/fonts/markB.woff ../../reftests/fonts/markC.woff ../../reftests/fonts/markD.woff
+ [test_units_angle.html]
+ [test_units_frequency.html]
+diff --git a/layout/style/test/test_animations_effect_timing_duration.html b/layout/style/test/test_animations_effect_timing_duration.html
+deleted file mode 100644
+--- a/layout/style/test/test_animations_effect_timing_duration.html
++++ /dev/null
+@@ -1,24 +0,0 @@
+-<!DOCTYPE HTML>
+-<html>
+-<head>
+-  <title>Test for animation.effect.timing on compositor</title>
+-  <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+-  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+-</head>
+-<body>
+-<div id="display"></div>
+-<pre id="test">
+-<script type="application/javascript">
+-"use strict";
+-
+-SimpleTest.waitForExplicitFinish();
+-
+-SpecialPowers.pushPrefEnv(
+-  { "set": [[ "dom.animations-api.core.enabled", true]] },
+-  function() {
+-    window.open("file_animations_effect_timing_duration.html");
+-  });
+-</script>
+-</pre>
+-</body>
+-</html>
+diff --git a/layout/style/test/file_animations_effect_timing_duration.html b/layout/style/test/test_animations_effect_timing_duration_to_rename.html
+rename from layout/style/test/file_animations_effect_timing_duration.html
+rename to layout/style/test/test_animations_effect_timing_duration_to_rename.html
+--- a/layout/style/test/file_animations_effect_timing_duration.html
++++ b/layout/style/test/test_animations_effect_timing_duration_to_rename.html
+@@ -1,47 +1,43 @@
+ <!DOCTYPE html>
+ <html>
+ <head>
++  <title>
++    Test for Animation.effect.timing.duration on compositor animations
++  </title>
++  <script type="application/javascript"
++    src="/tests/SimpleTest/SimpleTest.js"></script>
+   <script type="application/javascript"
+     src="/tests/SimpleTest/paint_listener.js"></script>
+   <script type="application/javascript" src="animation_utils.js"></script>
++  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css">
+   <style type="text/css">
+     @keyframes anim {
+       0% { transform: translate(0px) }
+       100% { transform: translate(100px) }
+     }
+     .target {
+       /* The animation target needs geometry in order to qualify for OMTA */
+       width: 100px;
+       height: 100px;
+       background-color: white;
+     }
+   </style>
+-  <script>
+-    var ok = opener.ok.bind(opener);
+-    var is = opener.is.bind(opener);
+-    var todo = opener.todo.bind(opener);
+-    function finish() {
+-      var o = opener;
+-      self.close();
+-      o.SimpleTest.finish();
+-    }
+-  </script>
+ </head>
+ <body>
+ <div id="display"></div>
+ <script type="application/javascript">
+ "use strict";
+ 
++SimpleTest.waitForExplicitFinish();
++
+ runOMTATest(function() {
+-  runAllAsyncAnimTests().then(function() {
+-    finish();
+-  });
+-}, finish, opener.SpecialPowers);
++  runAllAsyncAnimTests().then(SimpleTest.finish);
++}, SimpleTest.finish, SpecialPowers);
+ 
+ addAsyncAnimTest(async function() {
+   var [ div ] = new_div("");
+   var animation = div.animate(
+     [ { transform: 'translate(0px)', easing: "steps(2, start)" },
+       { transform: 'translate(100px)' } ], 4000);
+   await waitForPaints();
+ 
+diff --git a/layout/style/test/test_animations_effect_timing_enddelay.html b/layout/style/test/test_animations_effect_timing_enddelay.html
+deleted file mode 100644
+--- a/layout/style/test/test_animations_effect_timing_enddelay.html
++++ /dev/null
+@@ -1,24 +0,0 @@
+-<!DOCTYPE HTML>
+-<html>
+-<head>
+-  <title>Test for animation.effect.timing.endDelay on compositor</title>
+-  <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+-  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+-</head>
+-<body>
+-<div id="display"></div>
+-<pre id="test">
+-<script type="application/javascript">
+-"use strict";
+-
+-SimpleTest.waitForExplicitFinish();
+-
+-SpecialPowers.pushPrefEnv(
+-  { "set": [[ "dom.animations-api.core.enabled", true]] },
+-  function() {
+-    window.open("file_animations_effect_timing_enddelay.html");
+-  });
+-</script>
+-</pre>
+-</body>
+-</html>
+diff --git a/layout/style/test/file_animations_effect_timing_enddelay.html b/layout/style/test/test_animations_effect_timing_enddelay_to_rename.html
+rename from layout/style/test/file_animations_effect_timing_enddelay.html
+rename to layout/style/test/test_animations_effect_timing_enddelay_to_rename.html
+--- a/layout/style/test/file_animations_effect_timing_enddelay.html
++++ b/layout/style/test/test_animations_effect_timing_enddelay_to_rename.html
+@@ -1,43 +1,37 @@
+ <!DOCTYPE html>
+ <html>
+ <head>
++  <title>Test for Animation.effect.endDelay on compositor animations</title>
++  <script type="application/javascript"
++    src="/tests/SimpleTest/SimpleTest.js"></script>
+   <script type="application/javascript"
+     src="/tests/SimpleTest/paint_listener.js"></script>
+   <script type="application/javascript" src="animation_utils.js"></script>
++  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css">
+   <style type="text/css">
+     .target {
+       /* The animation target needs geometry in order to qualify for OMTA */
+       width: 100px;
+       height: 100px;
+       background-color: white;
+     }
+   </style>
+-  <script>
+-    var ok = opener.ok.bind(opener);
+-    var is = opener.is.bind(opener);
+-    var todo = opener.todo.bind(opener);
+-    function finish() {
+-      var o = opener;
+-      self.close();
+-      o.SimpleTest.finish();
+-    }
+-  </script>
+ </head>
+ <body>
+ <div id="display"></div>
+ <script type="application/javascript">
+ "use strict";
+ 
++SimpleTest.waitForExplicitFinish();
++
+ runOMTATest(function() {
+-  runAllAsyncAnimTests().then(function() {
+-    finish();
+-  });
+-}, finish, opener.SpecialPowers);
++  runAllAsyncAnimTests().then(SimpleTest.finish);
++}, SimpleTest.finish, SpecialPowers);
+ 
+ addAsyncAnimTest(async function() {
+   var [ div ] = new_div("");
+   var animation = div.animate(
+     [ { transform: 'translate(0px)' }, { transform: 'translate(100px)' } ],
+     { duration: 1000, fill: 'none' });
+   await waitForPaints();
+ 
+diff --git a/layout/style/test/test_animations_effect_timing_iterations.html b/layout/style/test/test_animations_effect_timing_iterations.html
+deleted file mode 100644
+--- a/layout/style/test/test_animations_effect_timing_iterations.html
++++ /dev/null
+@@ -1,24 +0,0 @@
+-<!DOCTYPE HTML>
+-<html>
+-<head>
+-  <title>Test for animation.effect.timing.iterations on compositor</title>
+-  <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+-  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+-</head>
+-<body>
+-<div id="display"></div>
+-<pre id="test">
+-<script type="application/javascript">
+-"use strict";
+-
+-SimpleTest.waitForExplicitFinish();
+-
+-SpecialPowers.pushPrefEnv(
+-  { "set": [[ "dom.animations-api.core.enabled", true]] },
+-  function() {
+-    window.open("file_animations_effect_timing_iterations.html");
+-  });
+-</script>
+-</pre>
+-</body>
+-</html>
+diff --git a/layout/style/test/file_animations_effect_timing_iterations.html b/layout/style/test/test_animations_effect_timing_iterations_to_rename.html
+rename from layout/style/test/file_animations_effect_timing_iterations.html
+rename to layout/style/test/test_animations_effect_timing_iterations_to_rename.html
+--- a/layout/style/test/file_animations_effect_timing_iterations.html
++++ b/layout/style/test/test_animations_effect_timing_iterations_to_rename.html
+@@ -1,47 +1,43 @@
+ <!DOCTYPE html>
+ <html>
+ <head>
++  <title>
++    Test for Animation.effect.timing.iterations on compositor animations
++  </title>
++  <script type="application/javascript"
++    src="/tests/SimpleTest/SimpleTest.js"></script>
+   <script type="application/javascript"
+     src="/tests/SimpleTest/paint_listener.js"></script>
+   <script type="application/javascript" src="animation_utils.js"></script>
++  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css">
+   <style type="text/css">
+     @keyframes anim {
+       0% { transform: translate(0px) }
+       100% { transform: translate(100px) }
+     }
+     .target {
+       /* The animation target needs geometry in order to qualify for OMTA */
+       width: 100px;
+       height: 100px;
+       background-color: white;
+     }
+   </style>
+-  <script>
+-    var ok = opener.ok.bind(opener);
+-    var is = opener.is.bind(opener);
+-    var todo = opener.todo.bind(opener);
+-    function finish() {
+-      var o = opener;
+-      self.close();
+-      o.SimpleTest.finish();
+-    }
+-  </script>
+ </head>
+ <body>
+ <div id="display"></div>
+ <script type="application/javascript">
+ "use strict";
+ 
++SimpleTest.waitForExplicitFinish();
++
+ runOMTATest(function() {
+-  runAllAsyncAnimTests().then(function() {
+-    finish();
+-  });
+-}, finish, opener.SpecialPowers);
++  runAllAsyncAnimTests().then(SimpleTest.finish);
++}, SimpleTest.finish, SpecialPowers);
+ 
+ addAsyncAnimTest(async function() {
+   var [ div ] = new_div("");
+   var animation = div.animate(
+     [ { transform: 'translate(0px)' },
+       { transform: 'translate(100px)' } ],
+       { duration: 4000,
+         iterations: 2
+diff --git a/layout/style/test/test_animations_iterationstart.html b/layout/style/test/test_animations_iterationstart.html
+deleted file mode 100644
+--- a/layout/style/test/test_animations_iterationstart.html
++++ /dev/null
+@@ -1,28 +0,0 @@
+-<!DOCTYPE HTML>
+-<html>
+-<!--
+-https://bugzilla.mozilla.org/show_bug.cgi?id=1248338
+--->
+-<head>
+-  <title>Test for iterationStart on compositor animations (Bug 1248338)</title>
+-  <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+-  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+-</head>
+-<body>
+-<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1248338">Mozilla Bug 1248338</a>
+-<div id="display"></div>
+-<pre id="test">
+-<script type="application/javascript">
+-"use strict";
+-
+-SimpleTest.waitForExplicitFinish();
+-
+-SpecialPowers.pushPrefEnv(
+-  { "set": [[ "dom.animations-api.core.enabled", true]] },
+-  function() {
+-    window.open("file_animations_iterationstart.html");
+-  });
+-</script>
+-</pre>
+-</body>
+-</html>
+diff --git a/layout/style/test/file_animations_iterationstart.html b/layout/style/test/test_animations_iterationstart_to_rename.html
+rename from layout/style/test/file_animations_iterationstart.html
+rename to layout/style/test/test_animations_iterationstart_to_rename.html
+--- a/layout/style/test/file_animations_iterationstart.html
++++ b/layout/style/test/test_animations_iterationstart_to_rename.html
+@@ -1,44 +1,39 @@
+ <!DOCTYPE html>
+ <html>
+ <head>
++  <title>
++    Test for Animation.effect.timing.iterationStart on compositor animations
++  </title>
++  <script type="application/javascript"
++    src="/tests/SimpleTest/SimpleTest.js"></script>
+   <script type="application/javascript"
+     src="/tests/SimpleTest/paint_listener.js"></script>
+   <script type="application/javascript" src="animation_utils.js"></script>
++  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css">
+   <style type="text/css">
+     .target {
+       /* The animation target needs geometry in order to qualify for OMTA */
+       width: 100px;
+       height: 100px;
+       background-color: white;
+     }
+   </style>
+-  <script>
+-    var ok = opener.ok.bind(opener);
+-    var is = opener.is.bind(opener);
+-    var todo = opener.todo.bind(opener);
+-    function finish() {
+-      var o = opener;
+-      self.close();
+-      o.SimpleTest.finish();
+-    }
+-  </script>
+ </head>
+ <body>
+ <div id="display"></div>
+ <script type="application/javascript">
+ "use strict";
+ 
++SimpleTest.waitForExplicitFinish();
++
+ runOMTATest(function() {
+-  runAllAsyncAnimTests().then(function() {
+-    finish();
+-  });
+-}, finish, opener.SpecialPowers);
+-
++  runAllAsyncAnimTests().then(SimpleTest.finish);
++}, SimpleTest.finish, SpecialPowers);
+ 
+ addAsyncAnimTest(async function() {
+   var [ div ] = new_div("test");
+   var animation = div.animate(
+     { transform: ["translate(0px)", "translate(100px)"] },
+     { iterationStart: 0.5, duration: 10000, fill: "both"}
+   );
+   await waitForPaints();
+diff --git a/layout/style/test/test_animations_pausing.html b/layout/style/test/test_animations_pausing.html
+deleted file mode 100644
+--- a/layout/style/test/test_animations_pausing.html
++++ /dev/null
+@@ -1,28 +0,0 @@
+-<!DOCTYPE HTML>
+-<html>
+-<!--
+-https://bugzilla.mozilla.org/show_bug.cgi?id=1070745
+--->
+-<head>
+-  <title>Test for play() and pause() on animations (Bug 1070745)</title>
+-  <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+-  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+-</head>
+-<body>
+-<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1070745">Mozilla Bug 1070745</a>
+-<div id="display"></div>
+-<pre id="test">
+-<script type="application/javascript">
+-"use strict";
+-
+-SimpleTest.waitForExplicitFinish();
+-
+-SpecialPowers.pushPrefEnv(
+-  { "set": [[ "dom.animations-api.core.enabled", true]] },
+-  function() {
+-    window.open("file_animations_pausing.html");
+-  });
+-</script>
+-</pre>
+-</body>
+-</html>
+diff --git a/layout/style/test/file_animations_pausing.html b/layout/style/test/test_animations_pausing_to_rename.html
+rename from layout/style/test/file_animations_pausing.html
+rename to layout/style/test/test_animations_pausing_to_rename.html
+--- a/layout/style/test/file_animations_pausing.html
++++ b/layout/style/test/test_animations_pausing_to_rename.html
+@@ -1,47 +1,43 @@
+ <!DOCTYPE html>
+ <html>
+ <head>
++  <title>
++    Test for Animation.play() and Animation.pause() on compositor animations
++  </title>
++  <script type="application/javascript"
++    src="/tests/SimpleTest/SimpleTest.js"></script>
+   <script type="application/javascript"
+     src="/tests/SimpleTest/paint_listener.js"></script>
+   <script type="application/javascript" src="animation_utils.js"></script>
++  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css">
+   <style type="text/css">
+     @keyframes anim {
+       0% { transform: translate(0px) }
+       100% { transform: translate(100px) }
+     }
+     .target {
+       /* The animation target needs geometry in order to qualify for OMTA */
+       width: 100px;
+       height: 100px;
+       background-color: white;
+     }
+   </style>
+-  <script>
+-    var ok = opener.ok.bind(opener);
+-    var is = opener.is.bind(opener);
+-    var todo = opener.todo.bind(opener);
+-    function finish() {
+-      var o = opener;
+-      self.close();
+-      o.SimpleTest.finish();
+-    }
+-  </script>
+ </head>
+ <body>
+ <div id="display"></div>
+ <script type="application/javascript">
+ "use strict";
+ 
++SimpleTest.waitForExplicitFinish();
++
+ runOMTATest(function() {
+-  runAllAsyncAnimTests().then(function() {
+-    finish();
+-  });
+-}, finish, opener.SpecialPowers);
++  runAllAsyncAnimTests().then(SimpleTest.finish);
++}, SimpleTest.finish, SpecialPowers);
+ 
+ addAsyncAnimTest(async function() {
+   var [ div, cs ] = new_div("animation: anim 10s 2 linear alternate");
+ 
+   // Animation is initially running on compositor
+   await waitForPaintsFlushed();
+   advance_clock(1000);
+   omta_is(div, "transform", { tx: 10 }, RunningOn.Compositor,
+diff --git a/layout/style/test/test_animations_playbackrate.html b/layout/style/test/test_animations_playbackrate.html
+deleted file mode 100644
+--- a/layout/style/test/test_animations_playbackrate.html
++++ /dev/null
+@@ -1,28 +0,0 @@
+-<!DOCTYPE HTML>
+-<html>
+-<!--
+-https://bugzilla.mozilla.org/show_bug.cgi?id=1175751
+--->
+-<head>
+-  <title>Test for Animation.playbackRate on compositor animations (Bug 1175751)</title>
+-  <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+-  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+-</head>
+-<body>
+-<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1175751">Mozilla Bug 1175751</a>
+-<div id="display"></div>
+-<pre id="test">
+-<script type="application/javascript">
+-"use strict";
+-
+-SimpleTest.waitForExplicitFinish();
+-
+-SpecialPowers.pushPrefEnv(
+-  { "set": [[ "dom.animations-api.core.enabled", true]] },
+-  function() {
+-    window.open("file_animations_playbackrate.html");
+-  });
+-</script>
+-</pre>
+-</body>
+-</html>
+diff --git a/layout/style/test/file_animations_playbackrate.html b/layout/style/test/test_animations_playbackrate_to_rename.html
+rename from layout/style/test/file_animations_playbackrate.html
+rename to layout/style/test/test_animations_playbackrate_to_rename.html
+--- a/layout/style/test/file_animations_playbackrate.html
++++ b/layout/style/test/test_animations_playbackrate_to_rename.html
+@@ -1,47 +1,41 @@
+ <!DOCTYPE html>
+ <html>
+ <head>
++  <title>Test for Animation.playbackRate on compositor animations</title>
++  <script type="application/javascript"
++    src="/tests/SimpleTest/SimpleTest.js"></script>
+   <script type="application/javascript"
+     src="/tests/SimpleTest/paint_listener.js"></script>
+   <script type="application/javascript" src="animation_utils.js"></script>
++  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css">
+   <style type="text/css">
+     @keyframes anim {
+       0% { transform: translate(0px) }
+       100% { transform: translate(100px) }
+     }
+     .target {
+       /* The animation target needs geometry in order to qualify for OMTA */
+       width: 100px;
+       height: 100px;
+       background-color: white;
+     }
+   </style>
+-  <script>
+-    var ok = opener.ok.bind(opener);
+-    var is = opener.is.bind(opener);
+-    var todo = opener.todo.bind(opener);
+-    function finish() {
+-      var o = opener;
+-      self.close();
+-      o.SimpleTest.finish();
+-    }
+-  </script>
+ </head>
+ <body>
+ <div id="display"></div>
+ <script type="application/javascript">
+ "use strict";
+ 
++SimpleTest.waitForExplicitFinish();
++
+ runOMTATest(function() {
+-  runAllAsyncAnimTests().then(function() {
+-    finish();
+-  });
+-}, finish, opener.SpecialPowers);
++  runAllAsyncAnimTests().then(SimpleTest.finish);
++}, SimpleTest.finish, SpecialPowers);
+ 
+ addAsyncAnimTest(async function() {
+   var [ div, cs ] = new_div("animation: anim 10s 1 linear forwards");
+   var animation = div.getAnimations()[0];
+   animation.playbackRate = 10;
+ 
+   advance_clock(300);
+ 
+diff --git a/layout/style/test/test_animations_reverse.html b/layout/style/test/test_animations_reverse.html
+deleted file mode 100644
+--- a/layout/style/test/test_animations_reverse.html
++++ /dev/null
+@@ -1,30 +0,0 @@
+-<!doctype html>
+-<html>
+-<!--
+-https://bugzilla.mozilla.org/show_bug.cgi?id=1343589
+--->
+-<head>
+-  <meta charset=utf-8>
+-  <title>Test for Animation.reverse() on compositor animations (Bug 1343589)</title>
+-  <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+-  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+-</head>
+-<body>
+-<a target="_blank"
+-  href="https://bugzilla.mozilla.org/show_bug.cgi?id=1343589">Mozilla Bug 1343589</a>
+-<div id="display"></div>
+-<pre id="test">
+-<script type="application/javascript">
+-"use strict";
+-
+-SimpleTest.waitForExplicitFinish();
+-
+-SpecialPowers.pushPrefEnv(
+-  { "set": [[ "dom.animations-api.core.enabled", true]] },
+-  function() {
+-    window.open("file_animations_reverse.html");
+-  });
+-</script>
+-</pre>
+-</body>
+-</html>
+diff --git a/layout/style/test/file_animations_reverse.html b/layout/style/test/test_animations_reverse_to_rename.html
+rename from layout/style/test/file_animations_reverse.html
+rename to layout/style/test/test_animations_reverse_to_rename.html
+--- a/layout/style/test/file_animations_reverse.html
++++ b/layout/style/test/test_animations_reverse_to_rename.html
+@@ -1,25 +1,19 @@
+ <!doctype html>
+ <html>
+ <head>
+   <meta charset=utf-8>
++  <title>Test for Animation.reverse() on compositor animations</title>
++  <script type="application/javascript"
++    src="/tests/SimpleTest/SimpleTest.js"></script>
+   <script type="application/javascript"
+     src="/tests/SimpleTest/paint_listener.js"></script>
+   <script type="application/javascript" src="animation_utils.js"></script>
+-  <script>
+-    var ok = opener.ok.bind(opener);
+-    var is = opener.is.bind(opener);
+-    var todo = opener.todo.bind(opener);
+-    function finish() {
+-      var o = opener;
+-      self.close();
+-      o.SimpleTest.finish();
+-    }
+-  </script>
++  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css">
+   <style type="text/css">
+     @keyframes anim {
+       0% { transform: translate(0px) }
+       100% { transform: translate(100px) }
+     }
+     .target {
+       /* The animation target needs geometry in order to qualify for OMTA */
+       width: 100px;
+@@ -28,21 +22,21 @@
+     }
+   </style>
+ </head>
+ <body>
+ <div id="display"></div>
+ <script type="application/javascript">
+ "use strict";
+ 
++SimpleTest.waitForExplicitFinish();
++
+ runOMTATest(function() {
+-  runAllAsyncAnimTests().then(function() {
+-    finish();
+-  });
+-}, finish, opener.SpecialPowers);
++  runAllAsyncAnimTests().then(SimpleTest.finish);
++}, SimpleTest.finish, SpecialPowers);
+ 
+ addAsyncAnimTest(async function() {
+   var [ div, cs ] = new_div("animation: anim 10s linear");
+   const animation = div.getAnimations()[0];
+ 
+   // Animation is initially running on compositor
+   await waitForPaintsFlushed();
+   advance_clock(5000);
+diff --git a/layout/style/test/test_animations_styles_on_event.html b/layout/style/test/test_animations_styles_on_event.html
+deleted file mode 100644
+--- a/layout/style/test/test_animations_styles_on_event.html
++++ /dev/null
+@@ -1,28 +0,0 @@
+-<!DOCTYPE HTML>
+-<html>
+-<!--
+-https://bugzilla.mozilla.org/show_bug.cgi?id=1228137
+--->
+-<head>
+-  <title>Test that mouse movement immediately after finish() should involve restyling for finished state(Bug 1228137)</title>
+-  <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+-  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+-</head>
+-<body>
+-<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1219236">Mozilla Bug 1228137</a>
+-<div id="display"></div>
+-<pre id="test">
+-<script type="application/javascript">
+-"use strict";
+-
+-SimpleTest.waitForExplicitFinish();
+-
+-SpecialPowers.pushPrefEnv(
+-  { "set": [[ "dom.animations-api.core.enabled", true]] },
+-  function() {
+-    window.open("file_animations_styles_on_event.html");
+-  });
+-</script>
+-</pre>
+-</body>
+-</html>
+diff --git a/layout/style/test/file_animations_styles_on_event.html b/layout/style/test/test_animations_styles_on_event_to_rename.html
+rename from layout/style/test/file_animations_styles_on_event.html
+rename to layout/style/test/test_animations_styles_on_event_to_rename.html
+--- a/layout/style/test/file_animations_styles_on_event.html
++++ b/layout/style/test/test_animations_styles_on_event_to_rename.html
+@@ -1,43 +1,42 @@
+ <!DOCTYPE html>
+ <html>
+ <head>
++  <title>
++    Test that mouse movement immediately after finish() should involve
++    restyling for finished state (Bug 1228137)
++  </title>
++  <script type="application/javascript"
++    src="/tests/SimpleTest/SimpleTest.js"></script>
+   <script type="application/javascript"
+     src="/tests/SimpleTest/EventUtils.js"></script>
+   <script type="application/javascript"
+     src="/tests/SimpleTest/paint_listener.js"></script>
+   <script type="application/javascript"
+     src="animation_utils.js"></script>
++  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css">
+   <style type="text/css">
+     @keyframes anim {
+       0% { transform: translateX(0px) }
+       100% { transform: translateX(100px) }
+     }
+     .target {
+       /* The animation target needs geometry in order to qualify for OMTA */
+       width: 100px;
+       height: 100px;
+       background-color: white;
+     }
+   </style>
+-  <script>
+-    var is = opener.is.bind(opener);
+-    var ok = opener.ok.bind(opener);
+-    var todo = opener.todo.bind(opener);
+-    function finish() {
+-      var o = opener;
+-      self.close();
+-      o.SimpleTest.finish();
+-    }
+-  </script>
+ </head>
+ <body>
+ <div id="display"></div>
+ <script type="application/javascript">
++SimpleTest.waitForExplicitFinish();
++
+ window.onload = function () {
+   // To avoid the effect that newly created element's styles are
+   // not updated immediately, we need to add an element without
+   // animation properties first.
+   var [ div ] = new_div("");
+   div.setAttribute("id", "bug1228137");
+ 
+   waitForPaints().then(function() {
+@@ -46,17 +45,17 @@ window.onload = function () {
+     // Now we can set animation properties.
+     div.style.animation = "anim 100s linear forwards";
+ 
+     div.addEventListener("mousemove", function(event) {
+       is(event.target.id, "bug1228137",
+          "The target of the animation should receive the mouse move event " +
+          "on the position of the animation's effect end.");
+       done_div();
+-      finish();
++      SimpleTest.finish();
+     });
+ 
+     var animation = div.getAnimations()[0];
+     animation.finish();
+ 
+     // Mouse over where the animation is positioned at finished state.
+     // We can't use synthesizeMouse here since synthesizeMouse causes
+     // layout flush. We need to check the position without explicit flushes.
+diff --git a/layout/style/test/test_animations_with_disabled_properties.html b/layout/style/test/test_animations_with_disabled_properties.html
+--- a/layout/style/test/test_animations_with_disabled_properties.html
++++ b/layout/style/test/test_animations_with_disabled_properties.html
+@@ -20,15 +20,14 @@ https://bugzilla.mozilla.org/show_bug.cg
+ SimpleTest.waitForExplicitFinish();
+ 
+ /*
+  * This test relies on the fact that the -webkit-text-fill-color property
+  * is disabled by the layout.css.prefixes.webkit pref. If we ever remove that
+  * pref we will need to substitute some other pref:property combination.
+  */
+ SpecialPowers.pushPrefEnv(
+-  { 'set': [[ 'dom.animations-api.core.enabled', true ],
+-            [ 'layout.css.prefixes.webkit', false ]] },
++  { 'set': [[ 'layout.css.prefixes.webkit', false ]] },
+   () => window.open('file_animations_with_disabled_properties.html'));
+ </script>
+ </pre>
+ </body>
+ </html>
+diff --git a/layout/style/test/test_transitions_replacement_on_busy_frame.html b/layout/style/test/test_transitions_replacement_on_busy_frame.html
+deleted file mode 100644
+--- a/layout/style/test/test_transitions_replacement_on_busy_frame.html
++++ /dev/null
+@@ -1,30 +0,0 @@
+-<!doctype html>
+-<html>
+-<!--
+-https://bugzilla.mozilla.org/show_bug.cgi?id=1167519
+--->
+-<head>
+-  <title>Test for bug 1167519</title>
+-  <script type="application/javascript"
+-    src="/tests/SimpleTest/SimpleTest.js"></script>
+-  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+-</head>
+-<body>
+-<a target="_blank"
+-  href="https://bugzilla.mozilla.org/show_bug.cgi?id=1167519">Mozilla Bug
+-  1167519</a>
+-<pre id="test">
+-<script>
+-'use strict';
+-
+-SimpleTest.waitForExplicitFinish();
+-
+-SpecialPowers.pushPrefEnv(
+-  { 'set': [[ 'dom.animations-api.core.enabled', true ]] },
+-  function() {
+-    window.open('file_transitions_replacement_on_busy_frame.html');
+-  });
+-</script>
+-</pre>
+-</body>
+-</html>
+diff --git a/layout/style/test/file_transitions_replacement_on_busy_frame.html b/layout/style/test/test_transitions_replacement_on_busy_frame_to_rename.html
+rename from layout/style/test/file_transitions_replacement_on_busy_frame.html
+rename to layout/style/test/test_transitions_replacement_on_busy_frame_to_rename.html
+--- a/layout/style/test/file_transitions_replacement_on_busy_frame.html
++++ b/layout/style/test/test_transitions_replacement_on_busy_frame_to_rename.html
+@@ -1,48 +1,45 @@
+ <!doctype html>
+ <html>
+ <!--
+ https://bugzilla.mozilla.org/show_bug.cgi?id=1167519
+ -->
+ <head>
+   <meta charset=utf-8>
++  <title>Test for bug 1167519</title>
++  <script type="application/javascript"
++    src="/tests/SimpleTest/SimpleTest.js"></script>
+   <script type="application/javascript"
+     src="/tests/SimpleTest/paint_listener.js"></script>
+   <script src="animation_utils.js"></script>
++  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css">
+   <style>
+     #target {
+       height: 100px;
+       width: 100px;
+       background: green;
+       transition: transform 100s linear;
+     }
+   </style>
+ </head>
+ <body>
+ <div id="target"></div>
+ <script>
+ 'use strict';
+ 
+-var ok = opener.ok.bind(opener);
+-var isnot = opener.isnot.bind(opener);
+-
+-function finish() {
+-  var o = opener;
+-  self.close();
+-  o.SimpleTest.finish();
+-}
++SimpleTest.waitForExplicitFinish();
+ 
+ var OMTAPrefKey = "layers.offmainthreadcomposition.async-animations";
+ var omtaEnabled = SpecialPowers.DOMWindowUtils.layerManagerRemote &&
+-                  opener.SpecialPowers.getBoolPref(OMTAPrefKey);
++                  SpecialPowers.getBoolPref(OMTAPrefKey);
+ window.addEventListener("load", function() {
+   if (!omtaEnabled) {
+     ok(true, "Skipping the test since OMTA is disabled");
+-    finish();
++    SimpleTest.finish();
+     return;
+   }
+ 
+   var div = document.getElementById("target");
+ 
+   // Start first transition
+   div.style.transform = "translateX(300px)";
+   const firstTransition = div.getAnimations()[0];
+@@ -84,17 +81,17 @@ window.addEventListener("load", function
+         var currentPropertyValue = properties[0].values[0].value;
+         isnot(currentPropertyValue, previousPropertyValue,
+               "From value of transition is updated since the moment when " +
+               "it was generated");
+         isnot(secondTransition.effect.getKeyframes()[0].transform,
+               previousKeyframeValue,
+               "Keyframe value of transition is updated since the moment when " +
+               "it was generated");
+-        finish();
++        SimpleTest.finish();
+       });
+     });
+   });
+ });
+ 
+ </script>
+ </body>
+ </html>
+diff --git a/layout/style/test/test_transitions_with_disabled_properties.html b/layout/style/test/test_transitions_with_disabled_properties.html
+--- a/layout/style/test/test_transitions_with_disabled_properties.html
++++ b/layout/style/test/test_transitions_with_disabled_properties.html
+@@ -14,15 +14,14 @@ https://bugzilla.mozilla.org/show_bug.cg
+ <a target="_blank"
+   href="https://bugzilla.mozilla.org/show_bug.cgi?id=1265611">Mozilla Bug
+   1265611</a>
+ 
+ <pre id="test">
+ <script>
+ SimpleTest.waitForExplicitFinish();
+ 
+-SpecialPowers.pushPrefEnv({'set': [['layout.css.prefixes.webkit', false],
+-                                   ['dom.animations-api.core.enabled', true]] },
++SpecialPowers.pushPrefEnv({'set': [['layout.css.prefixes.webkit', false]] },
+   () => window.open('file_transitions_with_disabled_properties.html'));
+ </script>
+ </pre>
+ </body>
+ </html>

+ 95 - 0
frg/mozilla-release/work-js/1382841-5-60a1.patch

@@ -0,0 +1,95 @@
+# HG changeset patch
+# User Brian Birtles <birtles@gmail.com>
+# Date 1520838516 -32400
+# Node ID d6ccaacd9692853a006461e883b4b9284c0e2964
+# Parent  37d348dcb52cf64d53a03c2c4fab887f2f8d8544
+Bug 1382841 - Rename test_XXX_to_rename.html files in layout/style/test to test_XXX.html; r=hiro
+
+MozReview-Commit-ID: Fes2hiyypWV
+
+diff --git a/layout/style/test/mochitest.ini b/layout/style/test/mochitest.ini
+--- a/layout/style/test/mochitest.ini
++++ b/layout/style/test/mochitest.ini
+@@ -47,28 +47,28 @@ support-files = additional_sheets_helper
+ [test_align_shorthand_serialization.html]
+ skip-if = stylo # bug 1339656
+ [test_all_shorthand.html]
+ [test_animations.html]
+ skip-if = (toolkit == 'android')
+ [test_animations_async_tests.html]
+ support-files = Ahem.ttf file_animations_async_tests.html
+ [test_animations_dynamic_changes.html]
+-[test_animations_effect_timing_duration_to_rename.html]
+-[test_animations_effect_timing_enddelay_to_rename.html]
+-[test_animations_effect_timing_iterations_to_rename.html]
++[test_animations_effect_timing_duration.html]
++[test_animations_effect_timing_enddelay.html]
++[test_animations_effect_timing_iterations.html]
+ [test_animations_event_order.html]
+ [test_animations_event_handler_attribute.html]
+-[test_animations_iterationstart_to_rename.html]
++[test_animations_iterationstart.html]
+ [test_animations_omta.html]
+ [test_animations_omta_start.html]
+-[test_animations_pausing_to_rename.html]
+-[test_animations_playbackrate_to_rename.html]
+-[test_animations_reverse_to_rename.html]
+-[test_animations_styles_on_event_to_rename.html]
++[test_animations_pausing.html]
++[test_animations_playbackrate.html]
++[test_animations_reverse.html]
++[test_animations_styles_on_event.html]
+ [test_animations_variable_changes.html]
+ [test_animations_with_disabled_properties.html]
+ support-files = file_animations_with_disabled_properties.html
+ [test_any_dynamic.html]
+ [test_asyncopen2.html]
+ [test_at_rule_parse_serialize.html]
+ [test_attribute_selector_eof_behavior.html]
+ [test_background_blend_mode.html]
+@@ -316,17 +316,17 @@ skip-if = !stylo # no need to test block
+ [test_transitions_computed_value_combinations.html]
+ [test_transitions_events.html]
+ [test_transitions.html]
+ skip-if = (android_version == '18' && debug) # bug 1159532
+ [test_transitions_bug537151.html]
+ [test_transitions_dynamic_changes.html]
+ [test_transitions_per_property.html]
+ skip-if = (toolkit == 'android') # bug 775227 for android
+-[test_transitions_replacement_on_busy_frame_to_rename.html]
++[test_transitions_replacement_on_busy_frame.html]
+ [test_transitions_step_functions.html]
+ [test_transitions_with_disabled_properties.html]
+ support-files = file_transitions_with_disabled_properties.html
+ [test_unclosed_parentheses.html]
+ [test_unicode_range_loading.html]
+ support-files = ../../reftests/fonts/markA.woff ../../reftests/fonts/markB.woff ../../reftests/fonts/markC.woff ../../reftests/fonts/markD.woff
+ [test_units_angle.html]
+ [test_units_frequency.html]
+diff --git a/layout/style/test/test_animations_effect_timing_duration_to_rename.html b/layout/style/test/test_animations_effect_timing_duration.html
+rename from layout/style/test/test_animations_effect_timing_duration_to_rename.html
+rename to layout/style/test/test_animations_effect_timing_duration.html
+diff --git a/layout/style/test/test_animations_effect_timing_enddelay_to_rename.html b/layout/style/test/test_animations_effect_timing_enddelay.html
+rename from layout/style/test/test_animations_effect_timing_enddelay_to_rename.html
+rename to layout/style/test/test_animations_effect_timing_enddelay.html
+diff --git a/layout/style/test/test_animations_effect_timing_iterations_to_rename.html b/layout/style/test/test_animations_effect_timing_iterations.html
+rename from layout/style/test/test_animations_effect_timing_iterations_to_rename.html
+rename to layout/style/test/test_animations_effect_timing_iterations.html
+diff --git a/layout/style/test/test_animations_iterationstart_to_rename.html b/layout/style/test/test_animations_iterationstart.html
+rename from layout/style/test/test_animations_iterationstart_to_rename.html
+rename to layout/style/test/test_animations_iterationstart.html
+diff --git a/layout/style/test/test_animations_pausing_to_rename.html b/layout/style/test/test_animations_pausing.html
+rename from layout/style/test/test_animations_pausing_to_rename.html
+rename to layout/style/test/test_animations_pausing.html
+diff --git a/layout/style/test/test_animations_playbackrate_to_rename.html b/layout/style/test/test_animations_playbackrate.html
+rename from layout/style/test/test_animations_playbackrate_to_rename.html
+rename to layout/style/test/test_animations_playbackrate.html
+diff --git a/layout/style/test/test_animations_reverse_to_rename.html b/layout/style/test/test_animations_reverse.html
+rename from layout/style/test/test_animations_reverse_to_rename.html
+rename to layout/style/test/test_animations_reverse.html
+diff --git a/layout/style/test/test_animations_styles_on_event_to_rename.html b/layout/style/test/test_animations_styles_on_event.html
+rename from layout/style/test/test_animations_styles_on_event_to_rename.html
+rename to layout/style/test/test_animations_styles_on_event.html
+diff --git a/layout/style/test/test_transitions_replacement_on_busy_frame_to_rename.html b/layout/style/test/test_transitions_replacement_on_busy_frame.html
+rename from layout/style/test/test_transitions_replacement_on_busy_frame_to_rename.html
+rename to layout/style/test/test_transitions_replacement_on_busy_frame.html

+ 50 - 0
frg/mozilla-release/work-js/1390675-57a1.patch

@@ -0,0 +1,50 @@
+# HG changeset patch
+# User Fischer.json <fischer.json@gmail.com>
+# Date 1503304002 -28800
+# Node ID c78673f78219da25139dd3f3d20accde51b38135
+# Parent  7bb302c9dcbddcc3535fbb23cdd27a979c8d6682
+Bug 1390675 - Preferences -> Site Data -> Settings cannot be closed with the Escape key, r=Gijs
+
+MozReview-Commit-ID: Jb2YE6QFSMN
+
+diff --git a/browser/components/preferences/siteDataSettings.js b/browser/components/preferences/siteDataSettings.js
+--- a/browser/components/preferences/siteDataSettings.js
++++ b/browser/components/preferences/siteDataSettings.js
+@@ -290,10 +290,16 @@ let gSiteDataSettings = {
+     }
+   },
+ 
+   onClickRemoveAll() {
+     let siteItems = this._list.getElementsByTagName("richlistitem");
+     if (siteItems.length > 0) {
+       this._removeSiteItems(siteItems);
+     }
++  },
++
++  onKeyPress(e) {
++    if (e.keyCode == KeyEvent.DOM_VK_ESCAPE) {
++      this.close();
++    }
+   }
+ };
+diff --git a/browser/components/preferences/siteDataSettings.xul b/browser/components/preferences/siteDataSettings.xul
+--- a/browser/components/preferences/siteDataSettings.xul
++++ b/browser/components/preferences/siteDataSettings.xul
+@@ -11,16 +11,17 @@
+ 
+ <!DOCTYPE dialog SYSTEM "chrome://browser/locale/preferences/siteDataSettings.dtd" >
+ 
+ <window id="SiteDataSettingsDialog" windowtype="Browser:SiteDataSettings"
+         class="windowDialog" title="&window.title;"
+         xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+         style="width: 45em;"
+         onload="gSiteDataSettings.init();"
++        onkeypress="gSiteDataSettings.onKeyPress(event);"
+         persist="screenX screenY width height">
+ 
+   <script src="chrome://browser/content/preferences/siteDataSettings.js"/>
+ 
+   <stringbundle id="bundlePreferences"
+                 src="chrome://browser/locale/preferences/preferences.properties"/>
+   <stringbundle id="bundle_brand" src="chrome://branding/locale/brand.properties"/>
+ 

+ 158 - 0
frg/mozilla-release/work-js/1399699-57a1.patch

@@ -0,0 +1,158 @@
+# HG changeset patch
+# User Ricky Chien <ricky060709@gmail.com>
+# Date 1505357944 -28800
+# Node ID 3907243d5b86eff477f92aa81152cbd6570ace11
+# Parent  683585e0cac75f08046aa84798f692c641a4bd9c
+Bug 1399699 - Remove redundant colon signs in cookies.dtd, siteDataSettings.dtd, translation.dtd r=jaws
+
+MozReview-Commit-ID: L3D5tRJjY3x
+
+diff --git a/browser/components/preferences/cookies.xul b/browser/components/preferences/cookies.xul
+--- a/browser/components/preferences/cookies.xul
++++ b/browser/components/preferences/cookies.xul
+@@ -34,17 +34,17 @@
+     <hbox align="center">
+       <textbox type="search" id="filter" flex="1"
+                aria-controls="cookiesList"
+                oncommand="gCookiesWindow.filter();"
+                placeholder="&searchFilter.label;"
+                accesskey="&searchFilter.accesskey;"/>
+     </hbox>
+     <separator class="thin"/>
+-    <label control="cookiesList" id="cookiesIntro">&cookiesonsystem.label;</label>
++    <label control="cookiesList" id="cookiesIntro">&cookiesonsystem2.label;</label>
+     <separator class="thin"/>
+     <tree id="cookiesList" flex="1" style="height: 10em;"
+           onkeypress="gCookiesWindow.onCookieKeyPress(event)"
+           onselect="gCookiesWindow.onCookieSelected();"
+           hidecolumnpicker="true" seltype="single">
+       <treecols>
+         <treecol id="domainCol" label="&cookiedomain.label;" flex="2" primary="true"
+                  persist="width" onclick="gCookiesWindow.sort('rawHost');"/>
+diff --git a/browser/components/preferences/siteDataRemoveSelected.xul b/browser/components/preferences/siteDataRemoveSelected.xul
+--- a/browser/components/preferences/siteDataRemoveSelected.xul
++++ b/browser/components/preferences/siteDataRemoveSelected.xul
+@@ -39,17 +39,17 @@
+         <separator class="thin"/>
+         <description id="removing-description">&removingSelected.description;</description>
+       </vbox>
+     </hbox>
+ 
+     <separator />
+ 
+     <vbox flex="1">
+-      <label>&siteTree.label;</label>
++      <label>&siteTree2.label;</label>
+       <separator class="thin"/>
+       <tree id="sitesTree" flex="1" seltype="single" hidecolumnpicker="true">
+         <treecols>
+           <treecol primary="true" flex="1" hideheader="true"/>
+         </treecols>
+         <treechildren />
+       </tree>
+     </vbox>
+diff --git a/browser/components/preferences/translation.xul b/browser/components/preferences/translation.xul
+--- a/browser/components/preferences/translation.xul
++++ b/browser/components/preferences/translation.xul
+@@ -25,17 +25,17 @@
+                 src="chrome://browser/locale/preferences/preferences.properties"/>
+ 
+   <keyset>
+     <key key="&windowClose.key;" modifiers="accel" oncommand="window.close();"/>
+   </keyset>
+ 
+   <vbox class="largeDialogContainer">
+     <vbox class="contentPane" flex="1">
+-      <label id="languagesLabel" control="permissionsTree">&noTranslationForLanguages.label;</label>
++      <label id="languagesLabel" control="permissionsTree">&noTranslationForLanguages2.label;</label>
+       <separator class="thin"/>
+       <tree id="languagesTree" flex="1" style="height: 12em;"
+             hidecolumnpicker="true"
+             onkeypress="gTranslationExceptions.onLanguageKeyPress(event)"
+             onselect="gTranslationExceptions.onLanguageSelected();">
+         <treecols>
+           <treecol id="languageCol" label="&treehead.languageName.label;" flex="1"/>
+         </treecols>
+@@ -52,17 +52,17 @@
+                 icon="clear" label="&removeAllLanguages.label;"
+                 accesskey="&removeAllLanguages.accesskey;"
+                 oncommand="gTranslationExceptions.onAllLanguagesDeleted();"/>
+         <spacer flex="1"/>
+       </hbox>
+     </hbox>
+     <separator/>
+     <vbox class="contentPane" flex="1">
+-      <label id="languagesLabel" control="permissionsTree">&noTranslationForSites.label;</label>
++      <label id="languagesLabel" control="permissionsTree">&noTranslationForSites2.label;</label>
+       <separator class="thin"/>
+       <tree id="sitesTree" flex="1" style="height: 12em;"
+             hidecolumnpicker="true"
+             onkeypress="gTranslationExceptions.onSiteKeyPress(event)"
+             onselect="gTranslationExceptions.onSiteSelected();">
+         <treecols>
+           <treecol id="siteCol" label="&treehead.siteName2.label;" flex="1"/>
+         </treecols>
+diff --git a/browser/locales/en-US/chrome/browser/preferences/cookies.dtd b/browser/locales/en-US/chrome/browser/preferences/cookies.dtd
+--- a/browser/locales/en-US/chrome/browser/preferences/cookies.dtd
++++ b/browser/locales/en-US/chrome/browser/preferences/cookies.dtd
+@@ -1,15 +1,15 @@
+ <!-- This Source Code Form is subject to the terms of the Mozilla Public
+    - License, v. 2.0. If a copy of the MPL was not distributed with this
+    - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+ 
+ <!ENTITY window.width                       "36em">
+ 
+-<!ENTITY     cookiesonsystem.label          "The following cookies are stored on your computer:">
++<!ENTITY     cookiesonsystem2.label         "The following cookies are stored on your computer">
+ <!ENTITY     cookiename.label               "Cookie Name">
+ <!ENTITY     cookiedomain.label             "Site">
+ 
+ <!ENTITY     props.name.label               "Name:">
+ <!ENTITY     props.value.label              "Content:">
+ <!ENTITY     props.domain.label             "Host:">
+ <!ENTITY     props.path.label               "Path:">
+ <!ENTITY     props.secure.label             "Send For:">
+diff --git a/browser/locales/en-US/chrome/browser/preferences/siteDataSettings.dtd b/browser/locales/en-US/chrome/browser/preferences/siteDataSettings.dtd
+--- a/browser/locales/en-US/chrome/browser/preferences/siteDataSettings.dtd
++++ b/browser/locales/en-US/chrome/browser/preferences/siteDataSettings.dtd
+@@ -11,9 +11,9 @@
+ <!ENTITY     removeSelected.label          "Remove Selected">
+ <!ENTITY     removeSelected.accesskey      "r">
+ <!ENTITY     save.label                    "Save Changes">
+ <!ENTITY     save.accesskey                "a">
+ <!ENTITY     cancel.label                  "Cancel">
+ <!ENTITY     cancel.accesskey              "C">
+ <!ENTITY     removingDialog.title          "Removing Site Data">
+ <!ENTITY     removingSelected.description  "Removing site data will also remove related cookies and offline web content. This may log you out of websites. Are you sure you want to make the changes?">
+-<!ENTITY     siteTree.label                "The following website cookies will be removed:">
++<!ENTITY     siteTree2.label               "The following website cookies will be removed">
+diff --git a/browser/locales/en-US/chrome/browser/preferences/translation.dtd b/browser/locales/en-US/chrome/browser/preferences/translation.dtd
+--- a/browser/locales/en-US/chrome/browser/preferences/translation.dtd
++++ b/browser/locales/en-US/chrome/browser/preferences/translation.dtd
+@@ -1,24 +1,24 @@
+ <!-- This Source Code Form is subject to the terms of the Mozilla Public
+    - License, v. 2.0. If a copy of the MPL was not distributed with this
+    - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+ 
+ <!ENTITY window.title                     "Exceptions - Translation">
+ <!ENTITY window.width                     "36em">
+ <!ENTITY windowClose.key                  "w">
+ 
+-<!ENTITY noTranslationForLanguages.label  "Translation will not be offered for the following languages:">
++<!ENTITY noTranslationForLanguages2.label "Translation will not be offered for the following languages">
+ <!ENTITY treehead.languageName.label      "Languages">
+ <!ENTITY removeLanguage.label             "Remove Language">
+ <!ENTITY removeLanguage.accesskey         "R">
+ <!ENTITY removeAllLanguages.label         "Remove All Languages">
+ <!ENTITY removeAllLanguages.accesskey     "e">
+ 
+-<!ENTITY noTranslationForSites.label      "Translation will not be offered for the following sites:">
++<!ENTITY noTranslationForSites2.label     "Translation will not be offered for the following sites">
+ <!ENTITY treehead.siteName2.label         "Websites">
+ <!ENTITY removeSite.label                 "Remove Site">
+ <!ENTITY removeSite.accesskey             "S">
+ <!ENTITY removeAllSites.label             "Remove All Sites">
+ <!ENTITY removeAllSites.accesskey         "i">
+ 
+ <!ENTITY button.close.label               "Close">
+ <!ENTITY button.close.accesskey           "C">

+ 202 - 0
frg/mozilla-release/work-js/1410416-1-58a1.patch

@@ -0,0 +1,202 @@
+# HG changeset patch
+# User Fischer.json <fischer.json@gmail.com>
+# Date 1508506558 -28800
+# Node ID 8ef662f3fa226a9661e3744600bef42bef98258e
+# Parent  10e1c80049cf261e49d723ea3bd8830fe86ed26d
+Bug 1410416 - Part 1: Have SiteDateManager.jsm call `propagateUnregister` to remove service workers, r=baku
+
+MozReview-Commit-ID: BNUhm6a2x1b
+
+diff --git a/browser/components/preferences/SiteDataManager.jsm b/browser/components/preferences/SiteDataManager.jsm
+--- a/browser/components/preferences/SiteDataManager.jsm
++++ b/browser/components/preferences/SiteDataManager.jsm
+@@ -42,47 +42,49 @@ var SiteDataManager = {
+   },
+ 
+   _getQuotaUsage() {
+     // Clear old data and requests first
+     this._sites.clear();
+     this._cancelGetQuotaUsage();
+     this._getQuotaUsagePromise = new Promise(resolve => {
+       let onUsageResult = request => {
+-        let items = request.result;
+-        for (let item of items) {
+-          if (!item.persisted && item.usage <= 0) {
+-            // An non-persistent-storage site with 0 byte quota usage is redundant for us so skip it.
+-            continue;
+-          }
+-          let principal =
+-            Services.scriptSecurityManager.createCodebasePrincipalFromOrigin(item.origin);
+-          let uri = principal.URI;
+-          if (uri.scheme == "http" || uri.scheme == "https") {
+-            let site = this._sites.get(uri.host);
+-            if (!site) {
+-              site = {
+-                persisted: false,
+-                quotaUsage: 0,
+-                principals: [],
+-                appCacheList: [],
+-              };
++        if (request.resultCode == Cr.NS_OK) {
++          let items = request.result;
++          for (let item of items) {
++            if (!item.persisted && item.usage <= 0) {
++              // An non-persistent-storage site with 0 byte quota usage is redundant for us so skip it.
++              continue;
+             }
+-            // Assume 3 sites:
+-            //   - Site A (not persisted): https://www.foo.com
+-            //   - Site B (not persisted): https://www.foo.com^userContextId=2
+-            //   - Site C (persisted):     https://www.foo.com:1234
+-            // Although only C is persisted, grouping by host, as a result,
+-            // we still mark as persisted here under this host group.
+-            if (item.persisted) {
+-              site.persisted = true;
++            let principal =
++              Services.scriptSecurityManager.createCodebasePrincipalFromOrigin(item.origin);
++            let uri = principal.URI;
++            if (uri.scheme == "http" || uri.scheme == "https") {
++              let site = this._sites.get(uri.host);
++              if (!site) {
++                site = {
++                  persisted: false,
++                  quotaUsage: 0,
++                  principals: [],
++                  appCacheList: [],
++                };
++              }
++              // Assume 3 sites:
++              //   - Site A (not persisted): https://www.foo.com
++              //   - Site B (not persisted): https://www.foo.com^userContextId=2
++              //   - Site C (persisted):     https://www.foo.com:1234
++              // Although only C is persisted, grouping by host, as a result,
++              // we still mark as persisted here under this host group.
++              if (item.persisted) {
++                site.persisted = true;
++              }
++              site.principals.push(principal);
++              site.quotaUsage += item.usage;
++              this._sites.set(uri.host, site);
+             }
+-            site.principals.push(principal);
+-            site.quotaUsage += item.usage;
+-            this._sites.set(uri.host, site);
+           }
+         }
+         resolve();
+       };
+       // XXX: The work of integrating localStorage into Quota Manager is in progress.
+       //      After the bug 742822 and 1286798 landed, localStorage usage will be included.
+       //      So currently only get indexedDB usage.
+       this._quotaUsageRequest = this._qms.getUsage(onUsageResult);
+@@ -215,78 +217,96 @@ var SiteDataManager = {
+             cookie.host, cookie.name, cookie.path, false, cookie.originAttributes);
+         }
+       }
+ 
+       Services.obs.notifyObservers(null, "browser:purge-domain-data", principal.URI.host);
+     }
+   },
+ 
+-  _removeServiceWorkers(site) {
++  _unregisterServiceWorker(serviceWorker) {
++    return new Promise(resolve => {
++      let unregisterCallback = {
++        unregisterSucceeded: resolve,
++        unregisterFailed: resolve, // We don't care about failures.
++        QueryInterface: XPCOMUtils.generateQI([Ci.nsIServiceWorkerUnregisterCallback])
++      };
++      serviceWorkerManager.propagateUnregister(serviceWorker.principal, unregisterCallback, serviceWorker.scope);
++    });
++  },
++
++  _removeServiceWorkersForSites(sites) {
++    let promises = [];
++    let targetHosts = sites.map(s => s.principals[0].URI.host);
+     let serviceWorkers = serviceWorkerManager.getAllRegistrations();
+     for (let i = 0; i < serviceWorkers.length; i++) {
+       let sw = serviceWorkers.queryElementAt(i, Ci.nsIServiceWorkerRegistrationInfo);
+-      for (let principal of site.principals) {
+-        if (sw.principal.equals(principal)) {
+-          serviceWorkerManager.removeAndPropagate(sw.principal.URI.host);
+-          break;
+-        }
++      // Sites are grouped and removed by host so we unregister service workers by the same host as well
++      if (targetHosts.includes(sw.principal.URI.host)) {
++        promises.push(this._unregisterServiceWorker(sw));
+       }
+     }
++    return Promise.all(promises);
+   },
+ 
+   remove(hosts) {
+-    let promises = [];
+     let unknownHost = "";
++    let targetSites = [];
+     for (let host of hosts) {
+       let site = this._sites.get(host);
+       if (site) {
+         this._removePermission(site);
+         this._removeAppCache(site);
+         this._removeCookie(site);
+-        this._removeServiceWorkers(site);
+-        promises.push(this._removeQuotaUsage(site));
++        targetSites.push(site);
+       } else {
+         unknownHost = host;
+         break;
+       }
+     }
+-    if (promises.length > 0) {
+-      Promise.all(promises).then(() => this.updateSites());
++
++    if (targetSites.length > 0) {
++      this._removeServiceWorkersForSites(targetSites)
++          .then(() => {
++            let promises = targetSites.map(s => this._removeQuotaUsage(s));
++            return Promise.all(promises);
++          })
++          .then(() => this.updateSites());
+     }
+     if (unknownHost) {
+       throw `SiteDataManager: removing unknown site of ${unknownHost}`;
+     }
+   },
+ 
+   async removeAll() {
+     Services.cache2.clear();
+     Services.cookies.removeAll();
+     OfflineAppCacheHelper.clear();
+ 
+     // Iterate through the service workers and remove them.
++    let promises = [];
+     let serviceWorkers = serviceWorkerManager.getAllRegistrations();
+     for (let i = 0; i < serviceWorkers.length; i++) {
+       let sw = serviceWorkers.queryElementAt(i, Ci.nsIServiceWorkerRegistrationInfo);
+-      let host = sw.principal.URI.host;
+-      serviceWorkerManager.removeAndPropagate(host);
++      promises.push(this._unregisterServiceWorker(sw));
+     }
++    await Promise.all(promises);
+ 
+     // Refresh sites using quota usage again.
+     // This is for the case:
+     //   1. User goes to the about:preferences Site Data section.
+     //   2. With the about:preferences opened, user visits another website.
+     //   3. The website saves to quota usage, like indexedDB.
+     //   4. User goes back to the Site Data section and commands to clear all site data.
+     // For this case, we should refresh the site list so not to miss the website in the step 3.
+     // We don't do "Clear All" on the quota manager like the cookie, appcache, http cache above
+     // because that would clear browser data as well too,
+     // see https://bugzilla.mozilla.org/show_bug.cgi?id=1312361#c9
+     await this._getQuotaUsage();
+-    let promises = [];
++    promises = [];
+     for (let site of this._sites.values()) {
+       this._removePermission(site);
+       promises.push(this._removeQuotaUsage(site));
+     }
+     return Promise.all(promises).then(() => this.updateSites());
+   },
+ 
+   isPrivateCookie(cookie) {

+ 911 - 0
frg/mozilla-release/work-js/1410416-2-58a1.patch

@@ -0,0 +1,911 @@
+# HG changeset patch
+# User Fischer.json <fischer.json@gmail.com>
+# Date 1509515038 -28800
+# Node ID f89f5660e3d74421398997e947938245069e97a5
+# Parent  4736e1754bd5c61a5f406187f4731fde1e047a1f
+Bug 1410416 - Part 2 - Add test cases about removing service workers, r=baku
+
+MozReview-Commit-ID: 6MeilGCkomc
+
+diff --git a/browser/components/preferences/in-content-new/tests/browser.ini b/browser/components/preferences/in-content-new/tests/browser.ini
+--- a/browser/components/preferences/in-content-new/tests/browser.ini
++++ b/browser/components/preferences/in-content-new/tests/browser.ini
+@@ -1,13 +1,15 @@
+ [DEFAULT]
+ support-files =
+   head.js
+   privacypane_tests_perwindow.js
+   site_data_test.html
++  service_worker_test.html
++  service_worker_test.js
+   offline/offline.html
+   offline/manifest.appcache
+ 
+ [browser_applications_selection.js]
+ skip-if = os == 'linux' # Bug 1382057
+ [browser_advanced_update.js]
+ skip-if = !updater
+ [browser_basic_rebuild_fonts_test.js]
+diff --git a/browser/components/preferences/in-content-new/tests/browser_siteData.js b/browser/components/preferences/in-content-new/tests/browser_siteData.js
+--- a/browser/components/preferences/in-content-new/tests/browser_siteData.js
++++ b/browser/components/preferences/in-content-new/tests/browser_siteData.js
+@@ -8,23 +8,26 @@ ChromeUtils.import("resource://gre/modul
+ Services.scriptloader.loadSubScript("resource://testing-common/sinon-2.3.2.js");
+ 
+ const TEST_QUOTA_USAGE_HOST = "example.com";
+ const TEST_QUOTA_USAGE_ORIGIN = "https://" + TEST_QUOTA_USAGE_HOST;
+ const TEST_QUOTA_USAGE_URL = TEST_QUOTA_USAGE_ORIGIN + "/browser/browser/components/preferences/in-content-new/tests/site_data_test.html";
+ const TEST_OFFLINE_HOST = "example.org";
+ const TEST_OFFLINE_ORIGIN = "https://" + TEST_OFFLINE_HOST;
+ const TEST_OFFLINE_URL = TEST_OFFLINE_ORIGIN + "/browser/browser/components/preferences/in-content-new/tests/offline/offline.html";
++const TEST_SERVICE_WORKER_URL = TEST_OFFLINE_ORIGIN + "/browser/browser/components/preferences/in-content-new/tests/service_worker_test.html";
+ const REMOVE_DIALOG_URL = "chrome://browser/content/preferences/siteDataRemoveSelected.xul";
+ 
+ const { NetUtil } = ChromeUtils.import("resource://gre/modules/NetUtil.jsm", {});
+ const { DownloadUtils } = ChromeUtils.import("resource://gre/modules/DownloadUtils.jsm", {});
+ const { SiteDataManager } = ChromeUtils.import("resource:///modules/SiteDataManager.jsm", {});
+ const { OfflineAppCacheHelper } = ChromeUtils.import("resource:///modules/offlineAppCache.jsm", {});
+ 
++XPCOMUtils.defineLazyServiceGetter(this, "serviceWorkerManager", "@mozilla.org/serviceworkers/manager;1", "nsIServiceWorkerManager");
++
+ const mockOfflineAppCacheHelper = {
+   clear: null,
+ 
+   originalClear: null,
+ 
+   register() {
+     this.originalClear = OfflineAppCacheHelper.clear;
+     this.clear = sinon.spy();
+@@ -80,16 +83,50 @@ const cacheUsageGetter = {
+ };
+ 
+ function promiseCookiesCleared() {
+   return TestUtils.topicObserved("cookie-changed", (subj, data) => {
+     return data === "cleared";
+   });
+ }
+ 
++async function loadServiceWorkerTestPage(url) {
++  let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, url);
++  await BrowserTestUtils.waitForCondition(() => {
++    return ContentTask.spawn(tab.linkedBrowser, {}, () =>
++      content.document.body.getAttribute("data-test-service-worker-registered") === "true");
++  }, `Fail to load service worker test ${url}`);
++  await BrowserTestUtils.removeTab(tab);
++}
++
++function promiseServiceWorkerRegisteredFor(url) {
++  return BrowserTestUtils.waitForCondition(() => {
++    try {
++      let principal = Services.scriptSecurityManager.createCodebasePrincipalFromOrigin(url);
++      let sw = serviceWorkerManager.getRegistrationByPrincipal(principal, principal.URI.spec);
++      if (sw) {
++        ok(true, `Found the service worker registered for ${url}`);
++        return true;
++      }
++    } catch (e) {}
++    return false;
++  }, `Should register service worker for ${url}`);
++}
++
++function promiseServiceWorkersCleared() {
++  return BrowserTestUtils.waitForCondition(() => {
++    let serviceWorkers = serviceWorkerManager.getAllRegistrations();
++    if (serviceWorkers.length == 0) {
++      ok(true, "Cleared all service workers");
++      return true;
++    }
++    return false;
++  }, "Should clear all service workers");
++}
++
+ registerCleanupFunction(function() {
+   delete window.sinon;
+   mockOfflineAppCacheHelper.unregister();
+ });
+ 
+ // Test listing site using quota usage or site using appcache
+ add_task(async function() {
+   // Open a test site which would save into appcache
+@@ -237,124 +274,59 @@ add_task(async function() {
+   is(cacheUsage, 0, "The cache usage should be removed");
+   is(quotaUsage, 0, "The quota usage should be removed");
+   is(totalUsage, 0, "The total usage should be removed");
+   // Test accepting "Clear All Data" ends
+ 
+   await BrowserTestUtils.removeTab(gBrowser.selectedTab);
+ });
+ 
+-// Test sorting
++// Test clearing service wroker through the "Clear All" button
+ add_task(async function() {
+-  mockSiteDataManager.register(SiteDataManager);
+-  mockSiteDataManager.fakeSites = [
+-    {
+-      usage: 1024,
+-      principal: Services.scriptSecurityManager
+-                         .createCodebasePrincipalFromOrigin("https://account.xyz.com"),
+-      persisted: true
+-    },
+-    {
+-      usage: 1024 * 2,
+-      principal: Services.scriptSecurityManager
+-                         .createCodebasePrincipalFromOrigin("https://books.foo.com"),
+-      persisted: false
+-    },
+-    {
+-      usage: 1024 * 3,
+-      principal: Services.scriptSecurityManager
+-                         .createCodebasePrincipalFromOrigin("http://cinema.bar.com"),
+-      persisted: true
+-    },
+-  ];
+-
++  await SpecialPowers.pushPrefEnv({set: [["browser.storageManager.enabled", true]]});
++  // Register a test service
++  await loadServiceWorkerTestPage(TEST_SERVICE_WORKER_URL);
++  await openPreferencesViaOpenPreferencesAPI("privacy", { leaveOpen: true });
++  // Test the initial states
++  await promiseServiceWorkerRegisteredFor(TEST_SERVICE_WORKER_URL);
++  // Click the "Clear All" button
++  let doc = gBrowser.selectedBrowser.contentDocument;
++  let clearBtn = doc.getElementById("clearSiteDataButton");
++  let acceptPromise = promiseAlertDialogOpen("accept");
+   let updatePromise = promiseSiteDataManagerSitesUpdated();
+-  await openPreferencesViaOpenPreferencesAPI("privacy", { leaveOpen: true });
++  clearBtn.doCommand();
++  await acceptPromise;
+   await updatePromise;
+-  await openSiteDataSettingsDialog();
+-
+-  let dialog = content.gSubDialog._topDialog;
+-  let dialogFrame = dialog._frame;
+-  let frameDoc = dialogFrame.contentDocument;
+-  let hostCol = frameDoc.getElementById("hostCol");
+-  let usageCol = frameDoc.getElementById("usageCol");
+-  let statusCol = frameDoc.getElementById("statusCol");
+-  let sitesList = frameDoc.getElementById("sitesList");
+-
+-  // Test default sorting
+-  assertSortByUsage("descending");
+-
+-  // Test sorting on the usage column
+-  usageCol.click();
+-  assertSortByUsage("ascending");
+-  usageCol.click();
+-  assertSortByUsage("descending");
+-
+-  // Test sorting on the host column
+-  hostCol.click();
+-  assertSortByHost("ascending");
+-  hostCol.click();
+-  assertSortByHost("descending");
+-
+-  // Test sorting on the permission status column
+-  statusCol.click();
+-  assertSortByStatus("ascending");
+-  statusCol.click();
+-  assertSortByStatus("descending");
++  await promiseServiceWorkersCleared();
++  await BrowserTestUtils.removeTab(gBrowser.selectedTab);
++});
+ 
+-  mockSiteDataManager.unregister();
+-  await BrowserTestUtils.removeTab(gBrowser.selectedTab);
+-
+-  function assertSortByHost(order) {
+-    let siteItems = sitesList.getElementsByTagName("richlistitem");
+-    for (let i = 0; i < siteItems.length - 1; ++i) {
+-      let aHost = siteItems[i].getAttribute("host");
+-      let bHost = siteItems[i + 1].getAttribute("host");
+-      let result = aHost.localeCompare(bHost);
+-      if (order == "ascending") {
+-        Assert.lessOrEqual(result, 0, "Should sort sites in the ascending order by host");
+-      } else {
+-        Assert.greaterOrEqual(result, 0, "Should sort sites in the descending order by host");
+-      }
++// Test clearing service wroker through the settings panel
++add_task(async function() {
++  // Register a test service worker
++  await loadServiceWorkerTestPage(TEST_SERVICE_WORKER_URL);
++  await openPreferencesViaOpenPreferencesAPI("privacy", { leaveOpen: true });
++  // Test the initial states
++  await promiseServiceWorkerRegisteredFor(TEST_SERVICE_WORKER_URL);
++  // Open the Site Data Settings panel and remove the site
++  await openSiteDataSettingsDialog();
++  let acceptRemovePromise = promiseAlertDialogOpen("accept");
++  let updatePromise = promiseSiteDataManagerSitesUpdated();
++  ContentTask.spawn(gBrowser.selectedBrowser, { TEST_OFFLINE_HOST }, args => {
++    let host = args.TEST_OFFLINE_HOST;
++    let frameDoc = content.gSubDialog._topDialog._frame.contentDocument;
++    let sitesList = frameDoc.getElementById("sitesList");
++    let site = sitesList.querySelector(`richlistitem[host="${host}"]`);
++    if (site) {
++      let removeBtn = frameDoc.getElementById("removeSelected");
++      let saveBtn = frameDoc.getElementById("save");
++      site.click();
++      removeBtn.doCommand();
++      saveBtn.doCommand();
++    } else {
++      ok(false, `Should have one site of ${host}`);
+     }
+-  }
+-
+-  function assertSortByStatus(order) {
+-    let siteItems = sitesList.getElementsByTagName("richlistitem");
+-    for (let i = 0; i < siteItems.length - 1; ++i) {
+-      let aHost = siteItems[i].getAttribute("host");
+-      let bHost = siteItems[i + 1].getAttribute("host");
+-      let a = findSiteByHost(aHost);
+-      let b = findSiteByHost(bHost);
+-      let result = 0;
+-      if (a.persisted && !b.persisted) {
+-        result = 1;
+-      } else if (!a.persisted && b.persisted) {
+-        result = -1;
+-      }
+-      if (order == "ascending") {
+-        Assert.lessOrEqual(result, 0, "Should sort sites in the ascending order by permission status");
+-      } else {
+-        Assert.greaterOrEqual(result, 0, "Should sort sites in the descending order by permission status");
+-      }
+-    }
+-  }
+-
+-  function assertSortByUsage(order) {
+-    let siteItems = sitesList.getElementsByTagName("richlistitem");
+-    for (let i = 0; i < siteItems.length - 1; ++i) {
+-      let aHost = siteItems[i].getAttribute("host");
+-      let bHost = siteItems[i + 1].getAttribute("host");
+-      let a = findSiteByHost(aHost);
+-      let b = findSiteByHost(bHost);
+-      let result = a.usage - b.usage;
+-      if (order == "ascending") {
+-        Assert.lessOrEqual(result, 0, "Should sort sites in the ascending order by usage");
+-      } else {
+-        Assert.greaterOrEqual(result, 0, "Should sort sites in the descending order by usage");
+-      }
+-    }
+-  }
+-
+-  function findSiteByHost(host) {
+-    return mockSiteDataManager.fakeSites.find(site => site.principal.URI.host == host);
+-  }
++  });
++  await acceptRemovePromise;
++  await updatePromise;
++  await promiseServiceWorkersCleared();
++  await BrowserTestUtils.removeTab(gBrowser.selectedTab);
+ });
+diff --git a/browser/components/preferences/in-content-new/tests/browser_siteData3.js b/browser/components/preferences/in-content-new/tests/browser_siteData3.js
+--- a/browser/components/preferences/in-content-new/tests/browser_siteData3.js
++++ b/browser/components/preferences/in-content-new/tests/browser_siteData3.js
+@@ -100,8 +100,126 @@ add_task(async function() {
+ 
+   expected = prefStrBundle.getString("persistent");
+   let status = siteItems[0].getAttribute("status");
+   is(status, expected, "Should mark persisted status across scheme, port and origin attributes");
+ 
+   mockSiteDataManager.unregister();
+   await BrowserTestUtils.removeTab(gBrowser.selectedTab);
+ });
++
++// Test sorting
++add_task(async function() {
++  await SpecialPowers.pushPrefEnv({set: [["browser.storageManager.enabled", true]]});
++  mockSiteDataManager.register(SiteDataManager);
++  mockSiteDataManager.fakeSites = [
++    {
++      usage: 1024,
++      principal: Services.scriptSecurityManager
++                         .createCodebasePrincipalFromOrigin("https://account.xyz.com"),
++      persisted: true
++    },
++    {
++      usage: 1024 * 2,
++      principal: Services.scriptSecurityManager
++                         .createCodebasePrincipalFromOrigin("https://books.foo.com"),
++      persisted: false
++    },
++    {
++      usage: 1024 * 3,
++      principal: Services.scriptSecurityManager
++                         .createCodebasePrincipalFromOrigin("http://cinema.bar.com"),
++      persisted: true
++    },
++  ];
++
++  let updatePromise = promiseSiteDataManagerSitesUpdated();
++  await openPreferencesViaOpenPreferencesAPI("privacy", { leaveOpen: true });
++  await updatePromise;
++  await openSiteDataSettingsDialog();
++
++  let dialog = content.gSubDialog._topDialog;
++  let dialogFrame = dialog._frame;
++  let frameDoc = dialogFrame.contentDocument;
++  let hostCol = frameDoc.getElementById("hostCol");
++  let usageCol = frameDoc.getElementById("usageCol");
++  let statusCol = frameDoc.getElementById("statusCol");
++  let sitesList = frameDoc.getElementById("sitesList");
++
++  // Test default sorting
++  assertSortByUsage("descending");
++
++  // Test sorting on the usage column
++  usageCol.click();
++  assertSortByUsage("ascending");
++  usageCol.click();
++  assertSortByUsage("descending");
++
++  // Test sorting on the host column
++  hostCol.click();
++  assertSortByHost("ascending");
++  hostCol.click();
++  assertSortByHost("descending");
++
++  // Test sorting on the permission status column
++  statusCol.click();
++  assertSortByStatus("ascending");
++  statusCol.click();
++  assertSortByStatus("descending");
++
++  mockSiteDataManager.unregister();
++  await BrowserTestUtils.removeTab(gBrowser.selectedTab);
++
++  function assertSortByHost(order) {
++    let siteItems = sitesList.getElementsByTagName("richlistitem");
++    for (let i = 0; i < siteItems.length - 1; ++i) {
++      let aHost = siteItems[i].getAttribute("host");
++      let bHost = siteItems[i + 1].getAttribute("host");
++      let result = aHost.localeCompare(bHost);
++      if (order == "ascending") {
++        Assert.lessOrEqual(result, 0, "Should sort sites in the ascending order by host");
++      } else {
++        Assert.greaterOrEqual(result, 0, "Should sort sites in the descending order by host");
++      }
++    }
++  }
++
++  function assertSortByStatus(order) {
++    let siteItems = sitesList.getElementsByTagName("richlistitem");
++    for (let i = 0; i < siteItems.length - 1; ++i) {
++      let aHost = siteItems[i].getAttribute("host");
++      let bHost = siteItems[i + 1].getAttribute("host");
++      let a = findSiteByHost(aHost);
++      let b = findSiteByHost(bHost);
++      let result = 0;
++      if (a.persisted && !b.persisted) {
++        result = 1;
++      } else if (!a.persisted && b.persisted) {
++        result = -1;
++      }
++      if (order == "ascending") {
++        Assert.lessOrEqual(result, 0, "Should sort sites in the ascending order by permission status");
++      } else {
++        Assert.greaterOrEqual(result, 0, "Should sort sites in the descending order by permission status");
++      }
++    }
++  }
++
++  function assertSortByUsage(order) {
++    let siteItems = sitesList.getElementsByTagName("richlistitem");
++    for (let i = 0; i < siteItems.length - 1; ++i) {
++      let aHost = siteItems[i].getAttribute("host");
++      let bHost = siteItems[i + 1].getAttribute("host");
++      let a = findSiteByHost(aHost);
++      let b = findSiteByHost(bHost);
++      let result = a.usage - b.usage;
++      if (order == "ascending") {
++        Assert.lessOrEqual(result, 0, "Should sort sites in the ascending order by usage");
++      } else {
++        Assert.greaterOrEqual(result, 0, "Should sort sites in the descending order by usage");
++      }
++    }
++  }
++
++  function findSiteByHost(host) {
++    return mockSiteDataManager.fakeSites.find(site => site.principal.URI.host == host);
++  }
++});
+diff --git a/browser/components/preferences/in-content-new/tests/head.js b/browser/components/preferences/in-content-new/tests/head.js
+--- a/browser/components/preferences/in-content-new/tests/head.js
++++ b/browser/components/preferences/in-content-new/tests/head.js
+@@ -256,17 +256,17 @@ const mockSiteDataManager = {
+   _originalRemoveQuotaUsage: null,
+ 
+   getUsage(onUsageResult) {
+     let result = this.fakeSites.map(site => ({
+       origin: site.principal.origin,
+       usage: site.usage,
+       persisted: site.persisted
+     }));
+-    onUsageResult({ result });
++    onUsageResult({ result, resultCode: Components.results.NS_OK });
+   },
+ 
+   _removeQuotaUsage(site) {
+     var target = site.principals[0].URI.host;
+     this.fakeSites = this.fakeSites.filter(fakeSite => {
+       return fakeSite.principal.URI.host != target;
+     });
+   },
+diff --git a/browser/components/preferences/in-content-new/tests/service_worker_test.html b/browser/components/preferences/in-content-new/tests/service_worker_test.html
+new file mode 100755
+--- /dev/null
++++ b/browser/components/preferences/in-content-new/tests/service_worker_test.html
+@@ -0,0 +1,19 @@
++<!DOCTYPE html>
++<html>
++  <head>
++    <meta charset="utf-8">
++    <meta http-equiv="Cache-Control" content="public" />
++    <meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1, maximum-scale=1">
++
++    <title>Service Worker Test</title>
++
++  </head>
++
++  <body>
++    <h1>Service Worker Test</h1>
++    <script type="text/javascript">
++      navigator.serviceWorker.register("service_worker_test.js")
++               .then(regis => document.body.setAttribute("data-test-service-worker-registered", "true"));
++    </script>
++  </body>
++</html>
+diff --git a/browser/components/preferences/in-content-new/tests/service_worker_test.js b/browser/components/preferences/in-content-new/tests/service_worker_test.js
+new file mode 100644
+--- /dev/null
++++ b/browser/components/preferences/in-content-new/tests/service_worker_test.js
+@@ -0,0 +1,1 @@
++// empty worker, always succeed!
+diff --git a/browser/components/preferences/in-content/tests/browser.ini b/browser/components/preferences/in-content/tests/browser.ini
+--- a/browser/components/preferences/in-content/tests/browser.ini
++++ b/browser/components/preferences/in-content/tests/browser.ini
+@@ -1,13 +1,15 @@
+ [DEFAULT]
+ support-files =
+   head.js
+   privacypane_tests_perwindow.js
+   site_data_test.html
++  service_worker_test.html
++  service_worker_test.js
+   offline/offline.html
+   offline/manifest.appcache
+ 
+ [browser_applications_selection.js]
+ [browser_advanced_update.js]
+ skip-if = !updater
+ [browser_basic_rebuild_fonts_test.js]
+ [browser_bug410900.js]
+diff --git a/browser/components/preferences/in-content/tests/browser_siteData.js b/browser/components/preferences/in-content/tests/browser_siteData.js
+--- a/browser/components/preferences/in-content/tests/browser_siteData.js
++++ b/browser/components/preferences/in-content/tests/browser_siteData.js
+@@ -8,20 +8,23 @@ ChromeUtils.import("resource://gre/modul
+ Services.scriptloader.loadSubScript("resource://testing-common/sinon-2.3.2.js");
+ 
+ const TEST_QUOTA_USAGE_HOST = "example.com";
+ const TEST_QUOTA_USAGE_ORIGIN = "https://" + TEST_QUOTA_USAGE_HOST;
+ const TEST_QUOTA_USAGE_URL = TEST_QUOTA_USAGE_ORIGIN + "/browser/browser/components/preferences/in-content/tests/site_data_test.html";
+ const TEST_OFFLINE_HOST = "example.org";
+ const TEST_OFFLINE_ORIGIN = "https://" + TEST_OFFLINE_HOST;
+ const TEST_OFFLINE_URL = TEST_OFFLINE_ORIGIN + "/browser/browser/components/preferences/in-content/tests/offline/offline.html";
++const TEST_SERVICE_WORKER_URL = TEST_OFFLINE_ORIGIN + "/browser/browser/components/preferences/in-content/tests/service_worker_test.html";
+ const { NetUtil } = ChromeUtils.import("resource://gre/modules/NetUtil.jsm", {});
+ const { DownloadUtils } = ChromeUtils.import("resource://gre/modules/DownloadUtils.jsm", {});
+ const { OfflineAppCacheHelper } = ChromeUtils.import("resource:///modules/offlineAppCache.jsm", {});
+ 
++XPCOMUtils.defineLazyServiceGetter(this, "serviceWorkerManager", "@mozilla.org/serviceworkers/manager;1", "nsIServiceWorkerManager");
++
+ const mockOfflineAppCacheHelper = {
+   clear: null,
+ 
+   originalClear: null,
+ 
+   register() {
+     this.originalClear = OfflineAppCacheHelper.clear;
+     this.clear = sinon.spy();
+@@ -83,16 +86,50 @@ const cacheUsageGetter = {
+ };
+ 
+ function promiseCookiesCleared() {
+   return TestUtils.topicObserved("cookie-changed", (subj, data) => {
+     return data === "cleared";
+   });
+ }
+ 
++async function loadServiceWorkerTestPage(url) {
++  let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, url);
++  await BrowserTestUtils.waitForCondition(() => {
++    return ContentTask.spawn(tab.linkedBrowser, {}, () =>
++      content.document.body.getAttribute("data-test-service-worker-registered") === "true");
++  }, `Fail to load service worker test ${url}`);
++  await BrowserTestUtils.removeTab(tab);
++}
++
++function promiseServiceWorkerRegisteredFor(url) {
++  return BrowserTestUtils.waitForCondition(() => {
++    try {
++      let principal = Services.scriptSecurityManager.createCodebasePrincipalFromOrigin(url);
++      let sw = serviceWorkerManager.getRegistrationByPrincipal(principal, principal.URI.spec);
++      if (sw) {
++        ok(true, `Found the service worker registered for ${url}`);
++        return true;
++      }
++    } catch (e) {}
++    return false;
++  }, `Should register service worker for ${url}`);
++}
++
++function promiseServiceWorkersCleared() {
++  return BrowserTestUtils.waitForCondition(() => {
++    let serviceWorkers = serviceWorkerManager.getAllRegistrations();
++    if (serviceWorkers.length == 0) {
++      ok(true, "Cleared all service workers");
++      return true;
++    }
++    return false;
++  }, "Should clear all service workers");
++}
++
+ registerCleanupFunction(function() {
+   delete window.sinon;
+   mockOfflineAppCacheHelper.unregister();
+ });
+ 
+ // Test listing site using quota usage or site using appcache
+ add_task(async function() {
+   // Open a test site which would save into appcache
+@@ -239,125 +276,60 @@ add_task(async function() {
+   is(cacheUsage, 0, "The cache usage should be removed");
+   is(quotaUsage, 0, "The quota usage should be removed");
+   is(totalUsage, 0, "The total usage should be removed");
+   // Test accepting "Clear All Data" ends
+ 
+   await BrowserTestUtils.removeTab(gBrowser.selectedTab);
+ });
+ 
+-// Test sorting
++// Test clearing service wroker through the "Clear All" button
+ add_task(async function() {
+-  mockSiteDataManager.register();
+-  mockSiteDataManager.fakeSites = [
+-    {
+-      usage: 1024,
+-      principal: Services.scriptSecurityManager
+-                         .createCodebasePrincipalFromOrigin("https://account.xyz.com"),
+-      persisted: true
+-    },
+-    {
+-      usage: 1024 * 2,
+-      principal: Services.scriptSecurityManager
+-                         .createCodebasePrincipalFromOrigin("https://books.foo.com"),
+-      persisted: false
+-    },
+-    {
+-      usage: 1024 * 3,
+-      principal: Services.scriptSecurityManager
+-                         .createCodebasePrincipalFromOrigin("http://cinema.bar.com"),
+-      persisted: true
+-    },
+-  ];
++  await SpecialPowers.pushPrefEnv({set: [["browser.storageManager.enabled", true]]});
++  // Register a test service
++  await loadServiceWorkerTestPage(TEST_SERVICE_WORKER_URL);
++  await openPreferencesViaOpenPreferencesAPI("privacy", { leaveOpen: true });
++  // Test the initial states
++  await promiseServiceWorkerRegisteredFor(TEST_SERVICE_WORKER_URL);
++  // Click the "Clear All" button
++  let doc = gBrowser.selectedBrowser.contentDocument;
++  let clearBtn = doc.getElementById("clearSiteDataButton");
++  let acceptPromise = promiseAlertDialogOpen("accept");
++  let updatePromise = promiseSiteDataManagerSitesUpdated();
++  clearBtn.doCommand();
++  await acceptPromise;
++  await updatePromise;
++  await promiseServiceWorkersCleared();
++  await BrowserTestUtils.removeTab(gBrowser.selectedTab);
++});
+ 
+-  let updatePromise = promiseSitesUpdated();
++// Test clearing service wroker through the settings panel
++add_task(async function() {
++  // Register a test service worker
++  await loadServiceWorkerTestPage(TEST_SERVICE_WORKER_URL);
+   await openPreferencesViaOpenPreferencesAPI("advanced", "networkTab", { leaveOpen: true });
+-  await updatePromise;
++  // Test the initial states
++  await promiseServiceWorkerRegisteredFor(TEST_SERVICE_WORKER_URL);
++  // Open the Site Data Settings panel and remove the site
+   await openSettingsDialog();
+ 
+-  let win = gBrowser.selectedBrowser.contentWindow;
+-  let dialog = win.gSubDialog._topDialog;
+-  let dialogFrame = dialog._frame;
+-  let frameDoc = dialogFrame.contentDocument;
+-  let hostCol = frameDoc.getElementById("hostCol");
+-  let usageCol = frameDoc.getElementById("usageCol");
+-  let statusCol = frameDoc.getElementById("statusCol");
+-  let sitesList = frameDoc.getElementById("sitesList");
+-
+-  // Test default sorting
+-  assertSortByUsage("descending");
+-
+-  // Test sorting on the usage column
+-  usageCol.click();
+-  assertSortByUsage("ascending");
+-  usageCol.click();
+-  assertSortByUsage("descending");
+-
+-  // Test sorting on the host column
+-  hostCol.click();
+-  assertSortByHost("ascending");
+-  hostCol.click();
+-  assertSortByHost("descending");
+-
+-  // Test sorting on the permission status column
+-  statusCol.click();
+-  assertSortByStatus("ascending");
+-  statusCol.click();
+-  assertSortByStatus("descending");
+-
+-  mockSiteDataManager.unregister();
++  let acceptRemovePromise = promiseAlertDialogOpen("accept");
++  let updatePromise = promiseSiteDataManagerSitesUpdated();
++  ContentTask.spawn(gBrowser.selectedBrowser, { TEST_OFFLINE_HOST }, args => {
++    let host = args.TEST_OFFLINE_HOST;
++    let frameDoc = content.gSubDialog._topDialog._frame.contentDocument;
++    let sitesList = frameDoc.getElementById("sitesList");
++    let site = sitesList.querySelector(`richlistitem[host="${host}"]`);
++    if (site) {
++      let removeBtn = frameDoc.getElementById("removeSelected");
++      let saveBtn = frameDoc.getElementById("save");
++      site.click();
++      removeBtn.doCommand();
++      saveBtn.doCommand();
++    } else {
++      ok(false, `Should have one site of ${host}`);
++    }
++  });
++  await acceptRemovePromise;
++  await updatePromise;
++  await promiseServiceWorkersCleared();
+   await BrowserTestUtils.removeTab(gBrowser.selectedTab);
+-
+-  function assertSortByHost(order) {
+-    let siteItems = sitesList.getElementsByTagName("richlistitem");
+-    for (let i = 0; i < siteItems.length - 1; ++i) {
+-      let aHost = siteItems[i].getAttribute("host");
+-      let bHost = siteItems[i + 1].getAttribute("host");
+-      let result = aHost.localeCompare(bHost);
+-      if (order == "ascending") {
+-        Assert.lessOrEqual(result, 0, "Should sort sites in the ascending order by host");
+-      } else {
+-        Assert.greaterOrEqual(result, 0, "Should sort sites in the descending order by host");
+-      }
+-    }
+-  }
+-
+-  function assertSortByStatus(order) {
+-    let siteItems = sitesList.getElementsByTagName("richlistitem");
+-    for (let i = 0; i < siteItems.length - 1; ++i) {
+-      let aHost = siteItems[i].getAttribute("host");
+-      let bHost = siteItems[i + 1].getAttribute("host");
+-      let a = findSiteByHost(aHost);
+-      let b = findSiteByHost(bHost);
+-      let result = 0;
+-      if (a.persisted && !b.persisted) {
+-        result = 1;
+-      } else if (!a.persisted && b.persisted) {
+-        result = -1;
+-      }
+-      if (order == "ascending") {
+-        Assert.lessOrEqual(result, 0, "Should sort sites in the ascending order by permission status");
+-      } else {
+-        Assert.greaterOrEqual(result, 0, "Should sort sites in the descending order by permission status");
+-      }
+-    }
+-  }
+-
+-  function assertSortByUsage(order) {
+-    let siteItems = sitesList.getElementsByTagName("richlistitem");
+-    for (let i = 0; i < siteItems.length - 1; ++i) {
+-      let aHost = siteItems[i].getAttribute("host");
+-      let bHost = siteItems[i + 1].getAttribute("host");
+-      let a = findSiteByHost(aHost);
+-      let b = findSiteByHost(bHost);
+-      let result = a.usage - b.usage;
+-      if (order == "ascending") {
+-        Assert.lessOrEqual(result, 0, "Should sort sites in the ascending order by usage");
+-      } else {
+-        Assert.greaterOrEqual(result, 0, "Should sort sites in the descending order by usage");
+-      }
+-    }
+-  }
+-
+-  function findSiteByHost(host) {
+-    return mockSiteDataManager.fakeSites.find(site => site.principal.URI.host == host);
+-  }
+ });
+diff --git a/browser/components/preferences/in-content/tests/browser_siteData3.js b/browser/components/preferences/in-content/tests/browser_siteData3.js
+--- a/browser/components/preferences/in-content/tests/browser_siteData3.js
++++ b/browser/components/preferences/in-content/tests/browser_siteData3.js
+@@ -95,8 +95,126 @@ add_task(async function() {
+   await openPreferencesViaOpenPreferencesAPI("advanced", "networkTab", { leaveOpen: true });
+   await updatePromise;
+   await openSettingsDialog();
+   assertSitesListed(doc, fakeHosts.filter(host => host != "shopping.xyz.com"));
+ 
+   mockSiteDataManager.unregister();
+   await BrowserTestUtils.removeTab(gBrowser.selectedTab);
+ });
++
++// Test sorting
++add_task(async function() {
++  await SpecialPowers.pushPrefEnv({set: [["browser.storageManager.enabled", true]]});
++  mockSiteDataManager.register(SiteDataManager);
++  mockSiteDataManager.fakeSites = [
++    {
++      usage: 1024,
++      principal: Services.scriptSecurityManager
++                         .createCodebasePrincipalFromOrigin("https://account.xyz.com"),
++      persisted: true
++    },
++    {
++      usage: 1024 * 2,
++      principal: Services.scriptSecurityManager
++                         .createCodebasePrincipalFromOrigin("https://books.foo.com"),
++      persisted: false
++    },
++    {
++      usage: 1024 * 3,
++      principal: Services.scriptSecurityManager
++                         .createCodebasePrincipalFromOrigin("http://cinema.bar.com"),
++      persisted: true
++    },
++  ];
++
++  let updatePromise = promiseSiteDataManagerSitesUpdated();
++  await openPreferencesViaOpenPreferencesAPI("privacy", { leaveOpen: true });
++  await updatePromise;
++  await openSiteDataSettingsDialog();
++
++  let dialog = content.gSubDialog._topDialog;
++  let dialogFrame = dialog._frame;
++  let frameDoc = dialogFrame.contentDocument;
++  let hostCol = frameDoc.getElementById("hostCol");
++  let usageCol = frameDoc.getElementById("usageCol");
++  let statusCol = frameDoc.getElementById("statusCol");
++  let sitesList = frameDoc.getElementById("sitesList");
++
++  // Test default sorting
++  assertSortByUsage("descending");
++
++  // Test sorting on the usage column
++  usageCol.click();
++  assertSortByUsage("ascending");
++  usageCol.click();
++  assertSortByUsage("descending");
++
++  // Test sorting on the host column
++  hostCol.click();
++  assertSortByHost("ascending");
++  hostCol.click();
++  assertSortByHost("descending");
++
++  // Test sorting on the permission status column
++  statusCol.click();
++  assertSortByStatus("ascending");
++  statusCol.click();
++  assertSortByStatus("descending");
++
++  mockSiteDataManager.unregister();
++  await BrowserTestUtils.removeTab(gBrowser.selectedTab);
++
++  function assertSortByHost(order) {
++    let siteItems = sitesList.getElementsByTagName("richlistitem");
++    for (let i = 0; i < siteItems.length - 1; ++i) {
++      let aHost = siteItems[i].getAttribute("host");
++      let bHost = siteItems[i + 1].getAttribute("host");
++      let result = aHost.localeCompare(bHost);
++      if (order == "ascending") {
++        Assert.lessOrEqual(result, 0, "Should sort sites in the ascending order by host");
++      } else {
++        Assert.greaterOrEqual(result, 0, "Should sort sites in the descending order by host");
++      }
++    }
++  }
++
++  function assertSortByStatus(order) {
++    let siteItems = sitesList.getElementsByTagName("richlistitem");
++    for (let i = 0; i < siteItems.length - 1; ++i) {
++      let aHost = siteItems[i].getAttribute("host");
++      let bHost = siteItems[i + 1].getAttribute("host");
++      let a = findSiteByHost(aHost);
++      let b = findSiteByHost(bHost);
++      let result = 0;
++      if (a.persisted && !b.persisted) {
++        result = 1;
++      } else if (!a.persisted && b.persisted) {
++        result = -1;
++      }
++      if (order == "ascending") {
++        Assert.lessOrEqual(result, 0, "Should sort sites in the ascending order by permission status");
++      } else {
++        Assert.greaterOrEqual(result, 0, "Should sort sites in the descending order by permission status");
++      }
++    }
++  }
++
++  function assertSortByUsage(order) {
++    let siteItems = sitesList.getElementsByTagName("richlistitem");
++    for (let i = 0; i < siteItems.length - 1; ++i) {
++      let aHost = siteItems[i].getAttribute("host");
++      let bHost = siteItems[i + 1].getAttribute("host");
++      let a = findSiteByHost(aHost);
++      let b = findSiteByHost(bHost);
++      let result = a.usage - b.usage;
++      if (order == "ascending") {
++        Assert.lessOrEqual(result, 0, "Should sort sites in the ascending order by usage");
++      } else {
++        Assert.greaterOrEqual(result, 0, "Should sort sites in the descending order by usage");
++      }
++    }
++  }
++
++  function findSiteByHost(host) {
++    return mockSiteDataManager.fakeSites.find(site => site.principal.URI.host == host);
++  }
++});
+diff --git a/browser/components/preferences/in-content/tests/head.js b/browser/components/preferences/in-content/tests/head.js
+--- a/browser/components/preferences/in-content/tests/head.js
++++ b/browser/components/preferences/in-content/tests/head.js
+@@ -22,17 +22,17 @@ const mockSiteDataManager = {
+   _originalRemoveQuotaUsage: null,
+ 
+   getUsage(onUsageResult) {
+     let result = this.fakeSites.map(site => ({
+       origin: site.principal.origin,
+       usage: site.usage,
+       persisted: site.persisted
+     }));
+-    onUsageResult({ result });
++    onUsageResult({ result, resultCode: Components.results.NS_OK });
+   },
+ 
+   _removeQuotaUsage(site) {
+     var target = site.principals[0].URI.host;
+     this.fakeSites = this.fakeSites.filter(fakeSite => {
+       return fakeSite.principal.URI.host != target;
+     });
+   },
+diff --git a/browser/components/preferences/in-content/tests/service_worker_test.html b/browser/components/preferences/in-content/tests/service_worker_test.html
+new file mode 100755
+--- /dev/null
++++ b/browser/components/preferences/in-content/tests/service_worker_test.html
+@@ -0,0 +1,19 @@
++<!DOCTYPE html>
++<html>
++  <head>
++    <meta charset="utf-8">
++    <meta http-equiv="Cache-Control" content="public" />
++    <meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1, maximum-scale=1">
++
++    <title>Service Worker Test</title>
++
++  </head>
++
++  <body>
++    <h1>Service Worker Test</h1>
++    <script type="text/javascript">
++      navigator.serviceWorker.register("service_worker_test.js")
++               .then(regis => document.body.setAttribute("data-test-service-worker-registered", "true"));
++    </script>
++  </body>
++</html>
+diff --git a/browser/components/preferences/in-content/tests/service_worker_test.js b/browser/components/preferences/in-content/tests/service_worker_test.js
+new file mode 100644
+--- /dev/null
++++ b/browser/components/preferences/in-content/tests/service_worker_test.js
+@@ -0,0 +1,1 @@
++// empty worker, always succeed!

+ 89 - 0
frg/mozilla-release/work-js/1414751-60a1.patch

@@ -0,0 +1,89 @@
+# HG changeset patch
+# User Johann Hofmann <jhofmann@mozilla.com>
+# Date 1519110836 -3600
+# Node ID 5708a33674f0a7646e4fd3624ba734e557b1bca8
+# Parent  07e20df368baef89c366d1867efe8ba4e6cfe3f4
+Bug 1414751 - Disable frequently failing task in browser_siteData.js. r=nhnt11
+
+MozReview-Commit-ID: LWPxJr6mCAl
+
+diff --git a/browser/components/preferences/in-content-new/tests/browser_siteData.js b/browser/components/preferences/in-content-new/tests/browser_siteData.js
+--- a/browser/components/preferences/in-content-new/tests/browser_siteData.js
++++ b/browser/components/preferences/in-content-new/tests/browser_siteData.js
+@@ -19,16 +19,17 @@ const { OfflineAppCacheHelper } = Chrome
+ 
+ function getPersistentStoragePermStatus(origin) {
+   let uri = NetUtil.newURI(origin);
+   let principal = Services.scriptSecurityManager.createCodebasePrincipal(uri, {});
+   return Services.perms.testExactPermissionFromPrincipal(principal, "persistent-storage");
+ }
+ 
+ // Test listing site using quota usage or site using appcache
++// This is currently disabled because of bug 1414751.
+ add_task(async function() {
+   // Open a test site which would save into appcache
+   await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_OFFLINE_URL);
+   await BrowserTestUtils.removeTab(gBrowser.selectedTab);
+ 
+   // Open a test site which would save into quota manager
+   BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_QUOTA_USAGE_URL);
+   await BrowserTestUtils.waitForContentEvent(
+@@ -56,17 +57,17 @@ add_task(async function() {
+   OfflineAppCacheHelper.clear();
+   await new Promise(resolve => {
+     let principal = Services.scriptSecurityManager
+                             .createCodebasePrincipalFromOrigin(TEST_QUOTA_USAGE_ORIGIN);
+     let request = Services.qms.clearStoragesForPrincipal(principal, null, true);
+     request.callback = resolve;
+   });
+   await BrowserTestUtils.removeTab(gBrowser.selectedTab);
+-});
++}).skip(); // Bug 1414751
+ 
+ // Test buttons are disabled and loading message shown while updating sites
+ add_task(async function() {
+   let updatedPromise = promiseSiteDataManagerSitesUpdated();
+   await openPreferencesViaOpenPreferencesAPI("privacy", { leaveOpen: true });
+   await updatedPromise;
+   let cacheSize = await SiteDataManager.getCacheSize();
+ 
+diff --git a/browser/components/preferences/in-content/tests/browser_siteData.js b/browser/components/preferences/in-content/tests/browser_siteData.js
+--- a/browser/components/preferences/in-content/tests/browser_siteData.js
++++ b/browser/components/preferences/in-content/tests/browser_siteData.js
+@@ -17,16 +17,17 @@ const { OfflineAppCacheHelper } = Chrome
+ 
+ function getPersistentStoragePermStatus(origin) {
+   let uri = NetUtil.newURI(origin);
+   let principal = Services.scriptSecurityManager.createCodebasePrincipal(uri, {});
+   return Services.perms.testExactPermissionFromPrincipal(principal, "persistent-storage");
+ }
+ 
+ // Test listing site using quota usage or site using appcache
++// This is currently disabled because of bug 1414751.
+ add_task(async function() {
+   // Open a test site which would save into appcache
+   await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_OFFLINE_URL);
+   await BrowserTestUtils.removeTab(gBrowser.selectedTab);
+ 
+   // Open a test site which would save into quota manager
+   BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_QUOTA_USAGE_URL);
+   await BrowserTestUtils.waitForContentEvent(
+@@ -53,17 +54,17 @@ add_task(async function() {
+   OfflineAppCacheHelper.clear();
+   await new Promise(resolve => {
+     let principal = Services.scriptSecurityManager
+                             .createCodebasePrincipalFromOrigin(TEST_QUOTA_USAGE_ORIGIN);
+     let request = Services.qms.clearStoragesForPrincipal(principal, null, true);
+     request.callback = resolve;
+   });
+   await BrowserTestUtils.removeTab(gBrowser.selectedTab);
+-});
++}).skip(); // Bug 1414751
+ 
+ // Test buttons are disabled and loading message shown while updating sites
+ add_task(async function() {
+   let updatedPromise = promiseSitesUpdated();
+   await openPreferencesViaOpenPreferencesAPI("advanced", "networkTab", { leaveOpen: true });
+   await updatedPromise;
+   let cacheSize = await SiteDataManager.getCacheSize();
+ 

+ 205 - 0
frg/mozilla-release/work-js/1415342-1only-61a1.patch

@@ -0,0 +1,205 @@
+# HG changeset patch
+# User Jan Henning <jh+bugzilla@buttercookie.de>
+# Date 1523553362 -7200
+# Node ID 40e13ce2180caa7e73ca3f9be1937a10dfd4a2e8
+# Parent  ee32738bd57b8e00c5e3c036c51df74964596f25
+Bug 1415342 - Part 1 - Move OfflineAppCacheHelper to Toolkit. r=standard8
+
+The OfflineAppCacheHelper was apparently introduced after the Sanitizer had been
+forked for Fennec and so far nobody bothered to use it there as well.
+
+MozReview-Commit-ID: 42Uk5hfvf9y
+
+diff --git a/browser/base/content/test/general/browser_offlineQuotaNotification.js b/browser/base/content/test/general/browser_offlineQuotaNotification.js
+--- a/browser/base/content/test/general/browser_offlineQuotaNotification.js
++++ b/browser/base/content/test/general/browser_offlineQuotaNotification.js
+@@ -11,17 +11,17 @@ const URL = "http://mochi.test:8888/brow
+ registerCleanupFunction(function() {
+   // Clean up after ourself
+   let uri = Services.io.newURI(URL);
+   let principal = Services.scriptSecurityManager.createCodebasePrincipal(uri, {});
+   Services.perms.removeFromPrincipal(principal, "offline-app");
+   Services.prefs.clearUserPref("offline-apps.quota.warn");
+   Services.prefs.clearUserPref("offline-apps.allow_by_default");
+   Services.prefs.clearUserPref("browser.preferences.useOldOrganization");
+-  let {OfflineAppCacheHelper} = ChromeUtils.import("resource:///modules/offlineAppCache.jsm", {});
++  let {OfflineAppCacheHelper} = ChromeUtils.import("resource://gre/modules/offlineAppCache.jsm", {});
+   OfflineAppCacheHelper.clear();
+ });
+ 
+ // Same as the other one, but for in-content preferences
+ function checkInContentPreferences(win) {
+   let doc = win.document;
+   let sel = doc.getElementById("categories").selectedItems[0].id;
+   is(gBrowser.currentURI.spec, "about:preferences#privacy", "about:preferences loaded");
+diff --git a/browser/components/preferences/in-content-new/tests/siteData/head.js b/browser/components/preferences/in-content-new/tests/siteData/head.js
+--- a/browser/components/preferences/in-content-new/tests/siteData/head.js
++++ b/browser/components/preferences/in-content-new/tests/siteData/head.js
+@@ -10,17 +10,17 @@ const TEST_OFFLINE_HOST = "example.org";
+ const TEST_OFFLINE_ORIGIN = "https://" + TEST_OFFLINE_HOST;
+ const TEST_OFFLINE_URL = getRootDirectory(gTestPath).replace("chrome://mochitests/content", TEST_OFFLINE_ORIGIN) + "/offline/offline.html";
+ const TEST_SERVICE_WORKER_URL = getRootDirectory(gTestPath).replace("chrome://mochitests/content", TEST_OFFLINE_ORIGIN) + "/service_worker_test.html";
+ 
+ const REMOVE_DIALOG_URL = "chrome://browser/content/preferences/siteDataRemoveSelected.xul";
+ 
+ const { DownloadUtils } = ChromeUtils.import("resource://gre/modules/DownloadUtils.jsm", {});
+ const { SiteDataManager } = ChromeUtils.import("resource:///modules/SiteDataManager.jsm", {});
+-const { OfflineAppCacheHelper } = ChromeUtils.import("resource:///modules/offlineAppCache.jsm", {});
++const { OfflineAppCacheHelper } = ChromeUtils.import("resource://gre/modules/offlineAppCache.jsm", {});
+ 
+ ChromeUtils.defineModuleGetter(this, "SiteDataTestUtils",
+                                "resource://testing-common/SiteDataTestUtils.jsm");
+ 
+ XPCOMUtils.defineLazyServiceGetter(this, "serviceWorkerManager", "@mozilla.org/serviceworkers/manager;1", "nsIServiceWorkerManager");
+ 
+ // Tests within /browser/components/preferences/in-content-new/tests/
+ // test the "new" preferences organization, after it was reorganized.
+diff --git a/browser/components/preferences/in-content/tests/siteData/head.js b/browser/components/preferences/in-content/tests/siteData/head.js
+--- a/browser/components/preferences/in-content/tests/siteData/head.js
++++ b/browser/components/preferences/in-content/tests/siteData/head.js
+@@ -10,17 +10,17 @@ const TEST_OFFLINE_HOST = "example.org";
+ const TEST_OFFLINE_ORIGIN = "https://" + TEST_OFFLINE_HOST;
+ const TEST_OFFLINE_URL = getRootDirectory(gTestPath).replace("chrome://mochitests/content", TEST_OFFLINE_ORIGIN) + "/offline/offline.html";
+ const TEST_SERVICE_WORKER_URL = getRootDirectory(gTestPath).replace("chrome://mochitests/content", TEST_OFFLINE_ORIGIN) + "/service_worker_test.html";
+ 
+ const REMOVE_DIALOG_URL = "chrome://browser/content/preferences/siteDataRemoveSelected.xul";
+ 
+ const { DownloadUtils } = ChromeUtils.import("resource://gre/modules/DownloadUtils.jsm", {});
+ const { SiteDataManager } = ChromeUtils.import("resource:///modules/SiteDataManager.jsm", {});
+-const { OfflineAppCacheHelper } = ChromeUtils.import("resource:///modules/offlineAppCache.jsm", {});
++const { OfflineAppCacheHelper } = ChromeUtils.import("resource://gre/modules/offlineAppCache.jsm", {});
+ 
+ ChromeUtils.defineModuleGetter(this, "SiteDataTestUtils",
+                                "resource://testing-common/SiteDataTestUtils.jsm");
+ 
+ XPCOMUtils.defineLazyServiceGetter(this, "serviceWorkerManager", "@mozilla.org/serviceworkers/manager;1", "nsIServiceWorkerManager");
+ 
+ // Tests within /browser/components/preferences/in-content/tests/
+ // test the "old" preferences organization, before it was reorganized.
+diff --git a/browser/modules/Sanitizer.jsm b/browser/modules/Sanitizer.jsm
+--- a/browser/modules/Sanitizer.jsm
++++ b/browser/modules/Sanitizer.jsm
+@@ -13,17 +13,17 @@ XPCOMUtils.defineLazyModuleGetters(this,
+   PlacesUtils: "resource://gre/modules/PlacesUtils.jsm",
+   FormHistory: "resource://gre/modules/FormHistory.jsm",
+   Downloads: "resource://gre/modules/Downloads.jsm",
+   DownloadsCommon: "resource:///modules/DownloadsCommon.jsm",
+   TelemetryStopwatch: "resource://gre/modules/TelemetryStopwatch.jsm",
+   console: "resource://gre/modules/Console.jsm",
+   setTimeout: "resource://gre/modules/Timer.jsm",
+   ServiceWorkerCleanUp: "resource://gre/modules/ServiceWorkerCleanUp.jsm",
+-  OfflineAppCacheHelper: "resource:///modules/offlineAppCache.jsm",
++  OfflineAppCacheHelper: "resource://gre/modules/offlineAppCache.jsm",
+ });
+ 
+ XPCOMUtils.defineLazyServiceGetter(this, "sas",
+                                    "@mozilla.org/storage/activity-service;1",
+                                    "nsIStorageActivityService");
+ XPCOMUtils.defineLazyServiceGetter(this, "quotaManagerService",
+                                    "@mozilla.org/dom/quota-manager-service;1",
+                                    "nsIQuotaManagerService");
+diff --git a/browser/modules/SiteDataManager.jsm b/browser/modules/SiteDataManager.jsm
+--- a/browser/modules/SiteDataManager.jsm
++++ b/browser/modules/SiteDataManager.jsm
+@@ -1,15 +1,15 @@
+ "use strict";
+ 
+ ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
+ ChromeUtils.import("resource://gre/modules/Services.jsm");
+ 
+ ChromeUtils.defineModuleGetter(this, "OfflineAppCacheHelper",
+-                               "resource:///modules/offlineAppCache.jsm");
++                               "resource://gre/modules/offlineAppCache.jsm");
+ ChromeUtils.defineModuleGetter(this, "ServiceWorkerCleanUp",
+                                "resource://gre/modules/ServiceWorkerCleanUp.jsm");
+ 
+ var EXPORTED_SYMBOLS = [
+   "SiteDataManager"
+ ];
+ 
+ XPCOMUtils.defineLazyGetter(this, "gStringBundle", function() {
+diff --git a/browser/modules/moz.build b/browser/modules/moz.build
+--- a/browser/modules/moz.build
++++ b/browser/modules/moz.build
+@@ -101,19 +101,16 @@ with Files("Windows8WindowFrameColor.jsm
+     BUG_COMPONENT = ("Firefox", "Theme")
+ 
+ with Files("WindowsJumpLists.jsm"):
+     BUG_COMPONENT = ("Firefox", "Shell Integration")
+ 
+ with Files("WindowsPreviewPerTab.jsm"):
+     BUG_COMPONENT = ("Core", "Widget: Win32")
+ 
+-with Files("offlineAppCache.jsm"):
+-    BUG_COMPONENT = ("Firefox", "Preferences")
+-
+ with Files("UpdateTopLevelContentWindowIDHelper.jsm"):
+     BUG_COMPONENT = ("Core", "Networking")
+ 
+ with Files("webrtcUI.jsm"):
+     BUG_COMPONENT = ("Firefox", "Device Permissions")
+ 
+ with Files("ZoomUI.jsm"):
+     BUG_COMPONENT = ("Firefox", "Toolbars and Customization")
+@@ -133,17 +130,16 @@ EXTRA_JS_MODULES += [
+     'ContentSearch.jsm',
+     'ContentWebRTC.jsm',
+     'DirectoryLinksProvider.jsm',
+     'ExtensionsUI.jsm',
+     'Feeds.jsm',
+     'FormSubmitObserver.jsm',
+     'FormValidationHandler.jsm',
+     'LaterRun.jsm',
+-    'offlineAppCache.jsm',
+     'PageActions.jsm',
+     'PermissionUI.jsm',
+     'PluginContent.jsm',
+     'ProcessHangMonitor.jsm',
+     'ReaderParent.jsm',
+     'RecentWindow.jsm',
+     'RemotePrompt.jsm',
+     'Sanitizer.jsm',
+diff --git a/toolkit/modules/moz.build b/toolkit/modules/moz.build
+--- a/toolkit/modules/moz.build
++++ b/toolkit/modules/moz.build
+@@ -104,16 +104,19 @@ with Files('NLP.jsm'):
+     BUG_COMPONENT = ('Toolkit', 'Find Toolbar')
+ 
+ with Files('NewTabUtils.jsm'):
+     BUG_COMPONENT = ('Firefox', 'Tabbed Browser')
+ 
+ with Files('ObjectUtils.jsm'):
+     BUG_COMPONENT = ('Toolkit', 'Telemetry')
+ 
++with Files("offlineAppCache.jsm"):
++    BUG_COMPONENT = ("Toolkit", "Preferences")
++
+ with Files('PageMenu.jsm'):
+     BUG_COMPONENT = ('Firefox', 'Menus')
+ 
+ with Files('PermissionsUtils.jsm'):
+     BUG_COMPONENT = ('Toolkit', 'Add-ons Manager')
+ 
+ with Files('PopupNotifications.jsm'):
+     BUG_COMPONENT = ('Toolkit', 'Password Manager')
+@@ -207,16 +210,17 @@ EXTRA_JS_MODULES += [
+     'Integration.jsm',
+     'JSONFile.jsm',
+     'LoadContextInfo.jsm',
+     'Log.jsm',
+     'Memory.jsm',
+     'NewTabUtils.jsm',
+     'NLP.jsm',
+     'ObjectUtils.jsm',
++    'offlineAppCache.jsm',
+     'PageMenu.jsm',
+     'PageMetadata.jsm',
+     'PermissionsUtils.jsm',
+     'PopupNotifications.jsm',
+     'Preferences.jsm',
+     'PrivateBrowsingUtils.jsm',
+     'ProfileAge.jsm',
+     'Promise-backend.js',
+diff --git a/browser/modules/offlineAppCache.jsm b/toolkit/modules/offlineAppCache.jsm
+rename from browser/modules/offlineAppCache.jsm
+rename to toolkit/modules/offlineAppCache.jsm

+ 85 - 0
frg/mozilla-release/work-js/1416193-59a1.patch

@@ -0,0 +1,85 @@
+# HG changeset patch
+# User Andrea Marchesini <amarchesini@mozilla.com>
+# Date 1510615374 -3600
+# Node ID d860a3b8ea299b5c59bc73556df2bda164985a30
+# Parent  922e619b506cdcf00c9b9b18e6ffacf68128cee7
+Bug 1416193 - Cloned nsHostObjectURI objects should be stored together with their BlobImpl by nsHostObjectProtocolHandler, r=valentin
+
+diff --git a/dom/file/nsHostObjectProtocolHandler.cpp b/dom/file/nsHostObjectProtocolHandler.cpp
+--- a/dom/file/nsHostObjectProtocolHandler.cpp
++++ b/dom/file/nsHostObjectProtocolHandler.cpp
+@@ -925,16 +925,28 @@ nsFontTableProtocolHandler::GetProtocolF
+ 
+ NS_IMETHODIMP
+ nsFontTableProtocolHandler::GetScheme(nsACString &result)
+ {
+   result.AssignLiteral(FONTTABLEURI_SCHEME);
+   return NS_OK;
+ }
+ 
++/* static */ void
++nsHostObjectProtocolHandler::StoreClonedURI(const nsACString& aSpec,
++                                            nsIURI* aURI)
++{
++  MOZ_ASSERT(aURI);
++
++  DataInfo* info = GetDataInfo(aSpec);
++  if (info) {
++    info->mURIs.AppendElement(do_GetWeakReference(aURI));
++  }
++}
++
+ nsresult
+ NS_GetBlobForBlobURI(nsIURI* aURI, BlobImpl** aBlob)
+ {
+   *aBlob = nullptr;
+ 
+   DataInfo* info = GetDataInfoFromURI(aURI);
+   if (!info || info->mObjectType != DataInfo::eBlobImpl) {
+     return NS_ERROR_DOM_BAD_URI;
+diff --git a/dom/file/nsHostObjectProtocolHandler.h b/dom/file/nsHostObjectProtocolHandler.h
+--- a/dom/file/nsHostObjectProtocolHandler.h
++++ b/dom/file/nsHostObjectProtocolHandler.h
+@@ -84,16 +84,19 @@ public:
+ 
+   static nsIPrincipal* GetDataEntryPrincipal(const nsACString& aUri);
+   static void Traverse(const nsACString& aUri, nsCycleCollectionTraversalCallback& aCallback);
+ 
+   static bool
+   GetAllBlobURLEntries(nsTArray<mozilla::dom::BlobURLRegistrationData>& aRegistrations,
+                        mozilla::dom::ContentParent* aCP);
+ 
++  // This is for nsHostObjectURI.
++  static void StoreClonedURI(const nsACString& aSpec, nsIURI* aURI);
++
+ protected:
+   virtual ~nsHostObjectProtocolHandler() {}
+ 
+ private:
+   static void Init();
+ };
+ 
+ class nsBlobProtocolHandler : public nsHostObjectProtocolHandler
+diff --git a/dom/file/nsHostObjectURI.cpp b/dom/file/nsHostObjectURI.cpp
+--- a/dom/file/nsHostObjectURI.cpp
++++ b/dom/file/nsHostObjectURI.cpp
+@@ -182,16 +182,19 @@ nsHostObjectURI::CloneInternal(mozilla::
+   MOZ_ASSERT(NS_SUCCEEDED(rv) && uriCheck);
+ #endif
+ 
+   nsHostObjectURI* u = static_cast<nsHostObjectURI*>(simpleClone.get());
+ 
+   u->mPrincipal = mPrincipal;
+   u->mBlobImpl = mBlobImpl;
+ 
++  nsHostObjectProtocolHandler::StoreClonedURI(newRef, simpleClone);
++
++  NS_TryToSetImmutable(simpleClone);
+   simpleClone.forget(aClone);
+   return NS_OK;
+ }
+ 
+ /* virtual */ nsresult
+ nsHostObjectURI::EqualsInternal(nsIURI* aOther,
+                                 mozilla::net::nsSimpleURI::RefHandlingEnum aRefHandlingMode,
+                                 bool* aResult)

+ 60 - 0
frg/mozilla-release/work-js/1419226-1-61a1.patch

@@ -0,0 +1,60 @@
+# HG changeset patch
+# User Mantaroh Yoshinaga <mantaroh@gmail.com>
+# Date 1520990090 -32400
+# Node ID 6b81e68d487bfac07262573dc3bbc1ba6410ef5f
+# Parent  ff62f4ca180c9a823259205a2af6780fd60974eb
+Bug 1419226 - Part 1.Change observing target window of MozAfterPaint. r=mconley
+
+Previous code, print preview waits for content window's MozAfterPaint. However
+gecko prevents send MozAfterPaint to content window[1]. So this code will not
+work correctly. However, software timer of firing MozAfterPaint ran this code.[2]
+This patch will
+  * Change the observing content window to chrome window.
+  * Add timer of MozAfterPaint event in order to ensure this event even if display
+    list invalidation doesn't invalidate. Gecko create this timer in nsPresContext
+    previously[2], but this bug will remove it
+
+[1] https://searchfox.org/mozilla-central/rev/919dce54f43356c22d6ff6b81c07ef412b1bf933/layout/base/nsPresContext.cpp#2452
+[2] https://searchfox.org/mozilla-central/rev/919dce54f43356c22d6ff6b81c07ef412b1bf933/layout/base/nsPresContext.cpp#3466-3472
+
+MozReview-Commit-ID: GcuKPjn0qhc
+
+diff --git a/toolkit/content/browser-content.js b/toolkit/content/browser-content.js
+--- a/toolkit/content/browser-content.js
++++ b/toolkit/content/browser-content.js
+@@ -622,25 +622,31 @@ var Printing = {
+     articlePromise.then(function(article) {
+       // We make use of a web progress listener in order to know when the content we inject
+       // into the DOM has finished rendering. If our layout engine is still painting, we
+       // will wait for MozAfterPaint event to be fired.
+       let webProgressListener = {
+         onStateChange(webProgress, req, flags, status) {
+           if (flags & Ci.nsIWebProgressListener.STATE_STOP) {
+             webProgress.removeProgressListener(webProgressListener);
+-            let domUtils = content.QueryInterface(Ci.nsIInterfaceRequestor)
+-                                  .getInterface(Ci.nsIDOMWindowUtils);
++            let domUtils = contentWindow.QueryInterface(Ci.nsIInterfaceRequestor)
++                                        .getInterface(Ci.nsIDOMWindowUtils);
+             // Here we tell the parent that we have parsed the document successfully
+             // using ReaderMode primitives and we are able to enter on preview mode.
+             if (domUtils.isMozAfterPaintPending) {
+-              addEventListener("MozAfterPaint", function onPaint() {
++              let onPaint = function() {
+                 removeEventListener("MozAfterPaint", onPaint);
+                 sendAsyncMessage("Printing:Preview:ReaderModeReady");
+-              });
++              };
++              contentWindow.addEventListener("MozAfterPaint", onPaint);
++              // This timer need when display list invalidation doesn't invalidate.
++              setTimeout(() => {
++                removeEventListener("MozAfterPaint", onPaint);
++                sendAsyncMessage("Printing:Preview:ReaderModeReady");
++              }, 100);
+             } else {
+               sendAsyncMessage("Printing:Preview:ReaderModeReady");
+             }
+           }
+         },
+ 
+         QueryInterface: XPCOMUtils.generateQI([
+           Ci.nsIWebProgressListener,

+ 154 - 0
frg/mozilla-release/work-js/1419226-2-61a1.patch

@@ -0,0 +1,154 @@
+# HG changeset patch
+# User Mantaroh Yoshinaga <mantaroh@gmail.com>
+# Date 1521001636 -32400
+# Node ID 3606a48f01835004c941f85fbdc5bdb88038049c
+# Parent  47af7fdd389e3e6bbc025f0ed5b70c848588cf5b
+Bug 1419226 - Part 2. Wait for MozAfterPaint instead of waiting for frames on file_deferred_start.html . r=hiro
+
+This patch will :
+ * Create test div element after waiting for document load, then wait for
+   painting after it.
+ * Change to waiting for MozAfterPaint event without workaround of waiting for
+   excessive frames.
+
+MozReview-Commit-ID: 6Ytxln3tJi4
+
+diff --git a/dom/animation/test/mozilla/file_deferred_start.html b/dom/animation/test/mozilla/file_deferred_start.html
+--- a/dom/animation/test/mozilla/file_deferred_start.html
++++ b/dom/animation/test/mozilla/file_deferred_start.html
+@@ -27,45 +27,48 @@ function waitForDocLoad() {
+ 
+ function waitForPaints() {
+   return new Promise(function(resolve, reject) {
+     waitForAllPaintsFlushed(resolve);
+   });
+ }
+ 
+ promise_test(function(t) {
+-  var div = addDiv(t);
+-  var cs = getComputedStyle(div);
+-
+   // Test that empty animations actually start.
+   //
+   // Normally we tie the start of animations to when their first frame of
+   // the animation is rendered. However, for animations that don't actually
+   // trigger a paint (e.g. because they are empty, or are animating something
+   // that doesn't render or is offscreen) we want to make sure they still
+   // start.
+   //
+-  // Before we start, wait for the document to finish loading. This is because
+-  // during loading we will have other paint events taking place which might,
+-  // by luck, happen to trigger animations that otherwise would not have been
+-  // triggered, leading to false positives.
++  // Before we start, wait for the document to finish loading, then create
++  // div element, and wait for painting. This is because during loading we will
++  // have other paint events taking place which might, by luck, happen to
++  // trigger animations that otherwise would not have been triggered, leading to
++  // false positives.
+   //
+   // As a result, it's better to wait until we have a more stable state before
+   // continuing.
++  var div;
+   var promiseCallbackDone = false;
+   return waitForDocLoad().then(function() {
++    div = addDiv(t);
++
++    return waitForPaints();
++  }).then(function() {
+     div.style.animation = 'empty 1000s';
+     var animation = div.getAnimations()[0];
+ 
+-    return animation.ready.then(function() {
++    animation.ready.then(function() {
+       promiseCallbackDone = true;
+     }).catch(function() {
+       assert_unreached('ready promise was rejected');
+     });
+-  }).then(function() {
++
+     // We need to wait for up to three frames. This is because in some
+     // cases it can take up to two frames for the initial layout
+     // to take place. Even after that happens we don't actually resolve the
+     // ready promise until the following tick.
+     return waitForAnimationFrames(3);
+   }).then(function() {
+     assert_true(promiseCallbackDone,
+                 'ready promise for an empty animation was resolved'
+@@ -87,37 +90,24 @@ promise_test(function(t) {
+   if (!isOMTAEnabled()) {
+     return;
+   }
+ 
+   const div = addDiv(t, { class: 'target' });
+ 
+   // As with the above test, any stray paints can cause this test to produce
+   // a false negative (that is, pass when it should fail). To avoid this we
+-  // first wait for the document to load, then wait until it is idle, then wait
+-  // for paints and only then do we commence the test. Even doing that, this
+-  // test can sometimes pass when it should not due to a stray paint. Most of
+-  // the time, however, it will correctly fail so hopefully even if we do
+-  // occasionally produce a false negative on one platform, another platform
+-  // will fail as expected.
+-  return waitForDocLoad().then(() => waitForIdle())
+-  .then(() => waitForPaints())
+-  .then(() => {
++  // wait for paints and only then do we commence the test.
++  return waitForPaints().then(() => {
+     div.animate({ transform: [ 'translate(0px)', 'translate(100px)' ] },
+                 { duration: 400 * MS_PER_SEC,
+                   delay: -200 * MS_PER_SEC });
+ 
+-    // TODO: Current waitForPaint() will not wait for MozAfterPaint in this
+-    // case(Bug 1341294), so this waiting code is workaround for it.
+-    // This waitForFrame() uses Promise, but bug 1193394 will be using same
+-    // handling of  microtask, so if landed bug 1193394 this test might be
+-    // failure since this promise will resolve in same tick of Element.animate.
+-    return waitForFrame();
+-  }).then(() => waitForPaints())
+-  .then(() => {
++      return waitForPaints();
++  }).then(() => {
+     const transformStr =
+       SpecialPowers.DOMWindowUtils.getOMTAStyle(div, 'transform');
+     const translateX = getTranslateXFromTransform(transformStr);
+ 
+     // If the delay has been applied we should be about half-way through
+     // the animation. However, if we applied it twice we will be at the
+     // end of the animation already so check that we are roughly half way
+     // through.
+@@ -139,31 +129,26 @@ promise_test(function(t) {
+ 
+   // This test only applies to compositor animations
+   if (!isOMTAEnabled()) {
+     return;
+   }
+ 
+   const div = addDiv(t, { class: 'target' });
+ 
+-  // Wait for idle state (see notes in previous test).
+-  return waitForDocLoad().then(() => waitForIdle())
+-  .then(() => waitForPaints())
+-  .then(() => {
++  // Wait for the document to load and painting (see notes in previous test).
++  return waitForPaints().then(() => {
+     const animation =
+       div.animate({ transform: [ 'translate(0px)', 'translate(100px)' ] },
+                   200 * MS_PER_SEC);
+     animation.currentTime = 100 * MS_PER_SEC;
+     animation.playbackRate = 0.1;
+ 
+-    // As the above test case, we should fix bug 1341294 before bug 1193394
+-    // lands.
+-    return waitForFrame();
+-  }).then(() => waitForPaints())
+-  .then(() => {
++    return waitForPaints();
++  }).then(() => {
+     const transformStr =
+       SpecialPowers.DOMWindowUtils.getOMTAStyle(div, 'transform');
+     const translateX = getTranslateXFromTransform(transformStr);
+ 
+     // We pass the playback rate to the compositor independently and we have
+     // tests to ensure that it is correctly applied there. However, if, when
+     // we resolve the start time of the pending animation, we fail to
+     // incorporate the playback rate, we will end up starting from the wrong

+ 338 - 0
frg/mozilla-release/work-js/1419382-1-59a1.patch

@@ -0,0 +1,338 @@
+# HG changeset patch
+# User Andrea Marchesini <amarchesini@mozilla.com>
+# Date 1511345898 -3600
+# Node ID 04d75d37c343992690597eddf793c9a7f15ea604
+# Parent  5fd309ebb7b2afe34119ce228a7182bff0d69e8b
+Bug 1419382 - Moving ownership of nsIInputStream when using netUtil functions - part 1 - NS_NewInputStreamChannelInternal, r=smaug
+
+diff --git a/dom/file/nsHostObjectProtocolHandler.cpp b/dom/file/nsHostObjectProtocolHandler.cpp
+--- a/dom/file/nsHostObjectProtocolHandler.cpp
++++ b/dom/file/nsHostObjectProtocolHandler.cpp
+@@ -860,17 +860,17 @@ nsHostObjectProtocolHandler::NewChannel2
+   }
+ 
+   nsAutoString contentType;
+   blobImpl->GetType(contentType);
+ 
+   nsCOMPtr<nsIChannel> channel;
+   rv = NS_NewInputStreamChannelInternal(getter_AddRefs(channel),
+                                         uri,
+-                                        stream,
++                                        stream.forget(),
+                                         NS_ConvertUTF16toUTF8(contentType),
+                                         EmptyCString(), // aContentCharset
+                                         aLoadInfo);
+   if (NS_WARN_IF(NS_FAILED(rv))) {
+     return rv;
+   }
+ 
+   if (blobImpl->IsFile()) {
+diff --git a/dom/jsurl/nsJSProtocolHandler.cpp b/dom/jsurl/nsJSProtocolHandler.cpp
+--- a/dom/jsurl/nsJSProtocolHandler.cpp
++++ b/dom/jsurl/nsJSProtocolHandler.cpp
+@@ -405,19 +405,20 @@ nsresult nsJSChannel::Init(nsIURI* aURI,
+ 
+     // Create the nsIStreamIO layer used by the nsIStreamIOChannel.
+     mIOThunk = new nsJSThunk();
+ 
+     // Create a stock input stream channel...
+     // Remember, until AsyncOpen is called, the script will not be evaluated
+     // and the underlying Input Stream will not be created...
+     nsCOMPtr<nsIChannel> channel;
++    RefPtr<nsJSThunk> thunk = mIOThunk;
+     rv = NS_NewInputStreamChannelInternal(getter_AddRefs(channel),
+                                           aURI,
+-                                          mIOThunk,
++                                          thunk.forget(),
+                                           NS_LITERAL_CSTRING("text/html"),
+                                           EmptyCString(),
+                                           aLoadInfo);
+     NS_ENSURE_SUCCESS(rv, rv);
+ 
+     rv = mIOThunk->Init(aURI);
+     if (NS_SUCCEEDED(rv)) {
+         mStreamChannel = channel;
+diff --git a/netwerk/base/nsNetUtil.cpp b/netwerk/base/nsNetUtil.cpp
+--- a/netwerk/base/nsNetUtil.cpp
++++ b/netwerk/base/nsNetUtil.cpp
+@@ -535,31 +535,33 @@ NS_GetRealPort(nsIURI *aURI)
+     nsAutoCString scheme;
+     rv = aURI->GetScheme(scheme);
+     if (NS_FAILED(rv))
+         return -1;
+ 
+     return NS_GetDefaultPort(scheme.get());
+ }
+ 
+-nsresult /* NS_NewInputStreamChannelWithLoadInfo */
+-NS_NewInputStreamChannelInternal(nsIChannel        **outChannel,
+-                                 nsIURI             *aUri,
+-                                 nsIInputStream     *aStream,
+-                                 const nsACString   &aContentType,
+-                                 const nsACString   &aContentCharset,
+-                                 nsILoadInfo        *aLoadInfo)
++nsresult
++NS_NewInputStreamChannelInternal(nsIChannel** outChannel,
++                                 nsIURI* aUri,
++                                 already_AddRefed<nsIInputStream> aStream,
++                                 const nsACString& aContentType,
++                                 const nsACString& aContentCharset,
++                                 nsILoadInfo* aLoadInfo)
+ {
+   nsresult rv;
+   nsCOMPtr<nsIInputStreamChannel> isc =
+     do_CreateInstance(NS_INPUTSTREAMCHANNEL_CONTRACTID, &rv);
+   NS_ENSURE_SUCCESS(rv, rv);
+   rv = isc->SetURI(aUri);
+   NS_ENSURE_SUCCESS(rv, rv);
+-  rv = isc->SetContentStream(aStream);
++
++  nsCOMPtr<nsIInputStream> stream = Move(aStream);
++  rv = isc->SetContentStream(stream);
+   NS_ENSURE_SUCCESS(rv, rv);
+ 
+   nsCOMPtr<nsIChannel> channel = do_QueryInterface(isc, &rv);
+   NS_ENSURE_SUCCESS(rv, rv);
+ 
+   if (!aContentType.IsEmpty()) {
+     rv = channel->SetContentType(aContentType);
+     NS_ENSURE_SUCCESS(rv, rv);
+@@ -578,57 +580,61 @@ NS_NewInputStreamChannelInternal(nsIChan
+     channel->SetOwner(nullptr);
+   }
+ 
+   channel.forget(outChannel);
+   return NS_OK;
+ }
+ 
+ nsresult
+-NS_NewInputStreamChannelInternal(nsIChannel        **outChannel,
+-                                 nsIURI             *aUri,
+-                                 nsIInputStream     *aStream,
+-                                 const nsACString   &aContentType,
+-                                 const nsACString   &aContentCharset,
+-                                 nsINode            *aLoadingNode,
+-                                 nsIPrincipal       *aLoadingPrincipal,
+-                                 nsIPrincipal       *aTriggeringPrincipal,
+-                                 nsSecurityFlags     aSecurityFlags,
++NS_NewInputStreamChannelInternal(nsIChannel** outChannel,
++                                 nsIURI* aUri,
++                                 already_AddRefed<nsIInputStream> aStream,
++                                 const nsACString& aContentType,
++                                 const nsACString& aContentCharset,
++                                 nsINode* aLoadingNode,
++                                 nsIPrincipal* aLoadingPrincipal,
++                                 nsIPrincipal* aTriggeringPrincipal,
++                                 nsSecurityFlags aSecurityFlags,
+                                  nsContentPolicyType aContentPolicyType)
+ {
+   nsCOMPtr<nsILoadInfo> loadInfo =
+     new mozilla::LoadInfo(aLoadingPrincipal,
+                           aTriggeringPrincipal,
+                           aLoadingNode,
+                           aSecurityFlags,
+                           aContentPolicyType);
+   if (!loadInfo) {
+     return NS_ERROR_UNEXPECTED;
+   }
++
++  nsCOMPtr<nsIInputStream> stream = Move(aStream);
++
+   return NS_NewInputStreamChannelInternal(outChannel,
+                                           aUri,
+-                                          aStream,
++                                          stream.forget(),
+                                           aContentType,
+                                           aContentCharset,
+                                           loadInfo);
+ }
+ 
+ nsresult /* NS_NewInputStreamChannelPrincipal */
+ NS_NewInputStreamChannel(nsIChannel        **outChannel,
+                          nsIURI             *aUri,
+                          nsIInputStream     *aStream,
+                          nsIPrincipal       *aLoadingPrincipal,
+                          nsSecurityFlags     aSecurityFlags,
+                          nsContentPolicyType aContentPolicyType,
+                          const nsACString   &aContentType    /* = EmptyCString() */,
+                          const nsACString   &aContentCharset /* = EmptyCString() */)
+ {
++  nsCOMPtr<nsIInputStream> stream = aStream;
+   return NS_NewInputStreamChannelInternal(outChannel,
+                                           aUri,
+-                                          aStream,
++                                          stream.forget(),
+                                           aContentType,
+                                           aContentCharset,
+                                           nullptr, // aLoadingNode
+                                           aLoadingPrincipal,
+                                           nullptr, // aTriggeringPrincipal
+                                           aSecurityFlags,
+                                           aContentPolicyType);
+ }
+@@ -648,17 +654,17 @@ NS_NewInputStreamChannelInternal(nsIChan
+ 
+     uint32_t len;
+     char* utf8Bytes = ToNewUTF8String(aData, &len);
+     rv = stream->AdoptData(utf8Bytes, len);
+ 
+   nsCOMPtr<nsIChannel> channel;
+   rv = NS_NewInputStreamChannelInternal(getter_AddRefs(channel),
+                                         aUri,
+-                                        stream,
++                                        stream.forget(),
+                                         aContentType,
+                                         NS_LITERAL_CSTRING("UTF-8"),
+                                         aLoadInfo);
+ 
+   NS_ENSURE_SUCCESS(rv, rv);
+ 
+   if (aIsSrcdocChannel) {
+     nsCOMPtr<nsIInputStreamChannel> inStrmChan = do_QueryInterface(channel);
+diff --git a/netwerk/base/nsNetUtil.h b/netwerk/base/nsNetUtil.h
+--- a/netwerk/base/nsNetUtil.h
++++ b/netwerk/base/nsNetUtil.h
+@@ -237,34 +237,35 @@ bool NS_StringToACE(const nsACString &id
+ 
+ /**
+  * This function is a helper function to get a protocol's default port if the
+  * URI does not specify a port explicitly. Returns -1 if this protocol has no
+  * concept of ports or if there was an error getting the port.
+  */
+ int32_t NS_GetRealPort(nsIURI *aURI);
+ 
+-nsresult /* NS_NewInputStreamChannelWithLoadInfo */
+-NS_NewInputStreamChannelInternal(nsIChannel        **outChannel,
+-                                 nsIURI             *aUri,
+-                                 nsIInputStream     *aStream,
+-                                 const nsACString   &aContentType,
+-                                 const nsACString   &aContentCharset,
+-                                 nsILoadInfo        *aLoadInfo);
++nsresult
++NS_NewInputStreamChannelInternal(nsIChannel** outChannel,
++                                 nsIURI* aUri,
++                                 already_AddRefed<nsIInputStream> aStream,
++                                 const nsACString& aContentType,
++                                 const nsACString& aContentCharset,
++                                 nsILoadInfo* aLoadInfo);
+ 
+-nsresult NS_NewInputStreamChannelInternal(nsIChannel        **outChannel,
+-                                          nsIURI             *aUri,
+-                                          nsIInputStream     *aStream,
+-                                          const nsACString   &aContentType,
+-                                          const nsACString   &aContentCharset,
+-                                          nsINode            *aLoadingNode,
+-                                          nsIPrincipal       *aLoadingPrincipal,
+-                                          nsIPrincipal       *aTriggeringPrincipal,
+-                                          nsSecurityFlags     aSecurityFlags,
+-                                          nsContentPolicyType aContentPolicyType);
++nsresult
++NS_NewInputStreamChannelInternal(nsIChannel** outChannel,
++                                 nsIURI* aUri,
++                                 already_AddRefed<nsIInputStream> aStream,
++                                 const nsACString& aContentType,
++                                 const nsACString& aContentCharset,
++                                 nsINode* aLoadingNode,
++                                 nsIPrincipal* aLoadingPrincipal,
++                                 nsIPrincipal* aTriggeringPrincipal,
++                                 nsSecurityFlags aSecurityFlags,
++                                 nsContentPolicyType aContentPolicyType);
+ 
+ 
+ nsresult /* NS_NewInputStreamChannelPrincipal */
+ NS_NewInputStreamChannel(nsIChannel        **outChannel,
+                          nsIURI             *aUri,
+                          nsIInputStream     *aStream,
+                          nsIPrincipal       *aLoadingPrincipal,
+                          nsSecurityFlags     aSecurityFlags,
+diff --git a/netwerk/protocol/about/nsAboutBlank.cpp b/netwerk/protocol/about/nsAboutBlank.cpp
+--- a/netwerk/protocol/about/nsAboutBlank.cpp
++++ b/netwerk/protocol/about/nsAboutBlank.cpp
+@@ -19,17 +19,17 @@ nsAboutBlank::NewChannel(nsIURI* aURI,
+ 
+     nsCOMPtr<nsIInputStream> in;
+     nsresult rv = NS_NewCStringInputStream(getter_AddRefs(in), EmptyCString());
+     if (NS_FAILED(rv)) return rv;
+ 
+     nsCOMPtr<nsIChannel> channel;
+     rv = NS_NewInputStreamChannelInternal(getter_AddRefs(channel),
+                                           aURI,
+-                                          in,
++                                          in.forget(),
+                                           NS_LITERAL_CSTRING("text/html"),
+                                           NS_LITERAL_CSTRING("utf-8"),
+                                           aLoadInfo);
+     if (NS_FAILED(rv)) return rv;
+ 
+     channel.forget(result);
+     return rv;
+ }
+diff --git a/netwerk/protocol/about/nsAboutCache.cpp b/netwerk/protocol/about/nsAboutCache.cpp
+--- a/netwerk/protocol/about/nsAboutCache.cpp
++++ b/netwerk/protocol/about/nsAboutCache.cpp
+@@ -75,17 +75,17 @@ nsAboutCache::Channel::Init(nsIURI* aURI
+         mStorageList.AppendElement(storageName);
+     }
+ 
+     // The entries header is added on encounter of the first entry
+     mEntriesHeaderAdded = false;
+ 
+     rv = NS_NewInputStreamChannelInternal(getter_AddRefs(mChannel),
+                                           aURI,
+-                                          inputStream,
++                                          inputStream.forget(),
+                                           NS_LITERAL_CSTRING("text/html"),
+                                           NS_LITERAL_CSTRING("utf-8"),
+                                           aLoadInfo);
+     if (NS_FAILED(rv)) return rv;
+ 
+     mBuffer.AssignLiteral(
+         "<!DOCTYPE html>\n"
+         "<html>\n"
+diff --git a/netwerk/protocol/about/nsAboutCacheEntry.cpp b/netwerk/protocol/about/nsAboutCacheEntry.cpp
+--- a/netwerk/protocol/about/nsAboutCacheEntry.cpp
++++ b/netwerk/protocol/about/nsAboutCacheEntry.cpp
+@@ -127,17 +127,17 @@ nsAboutCacheEntry::Channel::Init(nsIURI*
+     nsresult rv;
+ 
+     nsCOMPtr<nsIInputStream> stream;
+     rv = GetContentStream(uri, getter_AddRefs(stream));
+     if (NS_FAILED(rv)) return rv;
+ 
+     rv =  NS_NewInputStreamChannelInternal(getter_AddRefs(mChannel),
+                                            uri,
+-                                           stream,
++                                           stream.forget(),
+                                            NS_LITERAL_CSTRING("text/html"),
+                                            NS_LITERAL_CSTRING("utf-8"),
+                                            aLoadInfo);
+     if (NS_FAILED(rv)) return rv;
+ 
+     return NS_OK;
+ }
+ 
+diff --git a/netwerk/protocol/gio/nsGIOProtocolHandler.cpp b/netwerk/protocol/gio/nsGIOProtocolHandler.cpp
+--- a/netwerk/protocol/gio/nsGIOProtocolHandler.cpp
++++ b/netwerk/protocol/gio/nsGIOProtocolHandler.cpp
+@@ -1053,19 +1053,20 @@ nsGIOProtocolHandler::NewChannel2(nsIURI
+   if (NS_FAILED(rv))
+     return rv;
+ 
+   RefPtr<nsGIOInputStream> stream = new nsGIOInputStream(spec);
+   if (!stream) {
+     return NS_ERROR_OUT_OF_MEMORY;
+   }
+ 
++  RefPtr<nsGIOInputStream> tmpStream = stream;
+   rv = NS_NewInputStreamChannelInternal(aResult,
+                                         aURI,
+-                                        stream,
++                                        tmpStream.forget(),
+                                         NS_LITERAL_CSTRING(UNKNOWN_CONTENT_TYPE),
+                                         EmptyCString(), // aContentCharset
+                                         aLoadInfo);
+   if (NS_SUCCEEDED(rv)) {
+     stream->SetChannel(*aResult);
+   }
+   return rv;
+ }

+ 225 - 0
frg/mozilla-release/work-js/1419382-2-59a1.patch

@@ -0,0 +1,225 @@
+# HG changeset patch
+# User Andrea Marchesini <amarchesini@mozilla.com>
+# Date 1511345916 -3600
+# Node ID 6b49a0395cb47a3a18c6837aa49d5b622c953f1d
+# Parent  aa67f9ad63260902a7b5f651d4fd92d94c262150
+Bug 1419382 - Moving ownership of nsIInputStream when using netUtil functions - part 2 - NS_NewInputStreamChannel, r=smaug
+
+diff --git a/docshell/base/nsDocShell.cpp b/docshell/base/nsDocShell.cpp
+--- a/docshell/base/nsDocShell.cpp
++++ b/docshell/base/nsDocShell.cpp
+@@ -1679,19 +1679,20 @@ nsDocShell::LoadStream(nsIInputStream* a
+   mLoadType = loadType;
+ 
+   if (!triggeringPrincipal) {
+     triggeringPrincipal = nsContentUtils::GetSystemPrincipal();
+   }
+ 
+   // build up a channel for this stream.
+   nsCOMPtr<nsIChannel> channel;
++  nsCOMPtr<nsIInputStream> stream = aStream;
+   nsresult rv = NS_NewInputStreamChannel(getter_AddRefs(channel),
+                                          uri,
+-                                         aStream,
++                                         stream.forget(),
+                                          triggeringPrincipal,
+                                          nsILoadInfo::SEC_ALLOW_CROSS_ORIGIN_DATA_IS_NULL,
+                                          nsIContentPolicy::TYPE_OTHER,
+                                          aContentType,
+                                          aContentCharset);
+   NS_ENSURE_SUCCESS(rv, NS_ERROR_FAILURE);
+ 
+   nsCOMPtr<nsIURILoader> uriLoader(do_GetService(NS_URI_LOADER_CONTRACTID));
+diff --git a/dom/json/nsJSON.cpp b/dom/json/nsJSON.cpp
+--- a/dom/json/nsJSON.cpp
++++ b/dom/json/nsJSON.cpp
+@@ -207,19 +207,20 @@ nsJSON::DecodeInternal(JSContext* cx,
+   }
+ 
+   nsresult rv;
+   nsCOMPtr<nsIPrincipal> nullPrincipal = NullPrincipal::Create();
+ 
+   // The ::Decode function is deprecated [Bug 675797] and the following
+   // channel is never openend, so it does not matter what securityFlags
+   // we pass to NS_NewInputStreamChannel here.
++  nsCOMPtr<nsIInputStream> tmpStream = aStream;
+   rv = NS_NewInputStreamChannel(getter_AddRefs(jsonChannel),
+                                 mURI,
+-                                aStream,
++                                tmpStream.forget(),
+                                 nullPrincipal,
+                                 nsILoadInfo::SEC_REQUIRE_SAME_ORIGIN_DATA_IS_BLOCKED,
+                                 nsIContentPolicy::TYPE_OTHER,
+                                 NS_LITERAL_CSTRING("application/json"));
+ 
+   if (!jsonChannel || NS_FAILED(rv))
+     return NS_ERROR_FAILURE;
+ 
+diff --git a/image/decoders/icon/gtk/nsIconChannel.cpp b/image/decoders/icon/gtk/nsIconChannel.cpp
+--- a/image/decoders/icon/gtk/nsIconChannel.cpp
++++ b/image/decoders/icon/gtk/nsIconChannel.cpp
+@@ -103,17 +103,17 @@ moz_gdk_pixbuf_to_channel(GdkPixbuf* aPi
+   NS_ENSURE_SUCCESS(rv, rv);
+ 
+   // nsIconProtocolHandler::NewChannel2 will provide the correct loadInfo for
+   // this iconChannel. Use the most restrictive security settings for the
+   // temporary loadInfo to make sure the channel can not be openend.
+   nsCOMPtr<nsIPrincipal> nullPrincipal = NullPrincipal::Create();
+   return NS_NewInputStreamChannel(aChannel,
+                                   aURI,
+-                                  stream,
++                                  stream.forget(),
+                                   nullPrincipal,
+                                   nsILoadInfo::SEC_REQUIRE_SAME_ORIGIN_DATA_IS_BLOCKED,
+                                   nsIContentPolicy::TYPE_INTERNAL_IMAGE,
+                                   NS_LITERAL_CSTRING(IMAGE_ICON_MS));
+ }
+ 
+ static GtkWidget* gProtoWindow = nullptr;
+ static GtkWidget* gStockImageWidget = nullptr;
+diff --git a/layout/style/Loader.cpp b/layout/style/Loader.cpp
+--- a/layout/style/Loader.cpp
++++ b/layout/style/Loader.cpp
+@@ -1399,17 +1399,17 @@ Loader::LoadSheet(SheetLoadData* aLoadDa
+       // triggeringPrincipal should always be the systemPrincipal.
+       auto result = URLPreloader::ReadURI(aLoadData->mURI);
+       if (result.isOk()) {
+         nsCOMPtr<nsIInputStream> stream;
+         MOZ_TRY(NS_NewCStringInputStream(getter_AddRefs(stream), result.unwrap()));
+ 
+         rv = NS_NewInputStreamChannel(getter_AddRefs(channel),
+                                       aLoadData->mURI,
+-                                      stream,
++                                      stream.forget(),
+                                       nsContentUtils::GetSystemPrincipal(),
+                                       securityFlags,
+                                       contentPolicyType);
+       } else {
+         rv = NS_NewChannel(getter_AddRefs(channel),
+                            aLoadData->mURI,
+                            nsContentUtils::GetSystemPrincipal(),
+                            securityFlags,
+diff --git a/netwerk/base/nsNetUtil.cpp b/netwerk/base/nsNetUtil.cpp
+--- a/netwerk/base/nsNetUtil.cpp
++++ b/netwerk/base/nsNetUtil.cpp
+@@ -611,25 +611,25 @@ NS_NewInputStreamChannelInternal(nsIChan
+   return NS_NewInputStreamChannelInternal(outChannel,
+                                           aUri,
+                                           stream.forget(),
+                                           aContentType,
+                                           aContentCharset,
+                                           loadInfo);
+ }
+ 
+-nsresult /* NS_NewInputStreamChannelPrincipal */
+-NS_NewInputStreamChannel(nsIChannel        **outChannel,
+-                         nsIURI             *aUri,
+-                         nsIInputStream     *aStream,
+-                         nsIPrincipal       *aLoadingPrincipal,
+-                         nsSecurityFlags     aSecurityFlags,
++nsresult
++NS_NewInputStreamChannel(nsIChannel** outChannel,
++                         nsIURI* aUri,
++                         already_AddRefed<nsIInputStream> aStream,
++                         nsIPrincipal* aLoadingPrincipal,
++                         nsSecurityFlags aSecurityFlags,
+                          nsContentPolicyType aContentPolicyType,
+-                         const nsACString   &aContentType    /* = EmptyCString() */,
+-                         const nsACString   &aContentCharset /* = EmptyCString() */)
++                         const nsACString& aContentType    /* = EmptyCString() */,
++                         const nsACString& aContentCharset /* = EmptyCString() */)
+ {
+   nsCOMPtr<nsIInputStream> stream = aStream;
+   return NS_NewInputStreamChannelInternal(outChannel,
+                                           aUri,
+                                           stream.forget(),
+                                           aContentType,
+                                           aContentCharset,
+                                           nullptr, // aLoadingNode
+diff --git a/netwerk/base/nsNetUtil.h b/netwerk/base/nsNetUtil.h
+--- a/netwerk/base/nsNetUtil.h
++++ b/netwerk/base/nsNetUtil.h
+@@ -258,25 +258,25 @@ NS_NewInputStreamChannelInternal(nsIChan
+                                  const nsACString& aContentCharset,
+                                  nsINode* aLoadingNode,
+                                  nsIPrincipal* aLoadingPrincipal,
+                                  nsIPrincipal* aTriggeringPrincipal,
+                                  nsSecurityFlags aSecurityFlags,
+                                  nsContentPolicyType aContentPolicyType);
+ 
+ 
+-nsresult /* NS_NewInputStreamChannelPrincipal */
+-NS_NewInputStreamChannel(nsIChannel        **outChannel,
+-                         nsIURI             *aUri,
+-                         nsIInputStream     *aStream,
+-                         nsIPrincipal       *aLoadingPrincipal,
+-                         nsSecurityFlags     aSecurityFlags,
++nsresult
++NS_NewInputStreamChannel(nsIChannel* *outChannel,
++                         nsIURI* aUri,
++                         already_AddRefed<nsIInputStream> aStream,
++                         nsIPrincipal* aLoadingPrincipal,
++                         nsSecurityFlags aSecurityFlags,
+                          nsContentPolicyType aContentPolicyType,
+-                         const nsACString   &aContentType    = EmptyCString(),
+-                         const nsACString   &aContentCharset = EmptyCString());
++                         const nsACString& aContentType    = EmptyCString(),
++                         const nsACString& aContentCharset = EmptyCString());
+ 
+ nsresult NS_NewInputStreamChannelInternal(nsIChannel        **outChannel,
+                                           nsIURI             *aUri,
+                                           const nsAString    &aData,
+                                           const nsACString   &aContentType,
+                                           nsINode            *aLoadingNode,
+                                           nsIPrincipal       *aLoadingPrincipal,
+                                           nsIPrincipal       *aTriggeringPrincipal,
+diff --git a/parser/xml/nsSAXXMLReader.cpp b/parser/xml/nsSAXXMLReader.cpp
+--- a/parser/xml/nsSAXXMLReader.cpp
++++ b/parser/xml/nsSAXXMLReader.cpp
+@@ -498,19 +498,20 @@ nsSAXXMLReader::ParseFromStream(nsIInput
+   rv = EnsureBaseURI();
+   NS_ENSURE_SUCCESS(rv, rv);
+ 
+   nsCOMPtr<nsIPrincipal> nullPrincipal = NullPrincipal::Create();
+ 
+   // The following channel is never openend, so it does not matter what
+   // securityFlags we pass; let's follow the principle of least privilege.
+   nsCOMPtr<nsIChannel> parserChannel;
++  nsCOMPtr<nsIInputStream> tmpStream = stream;
+   rv = NS_NewInputStreamChannel(getter_AddRefs(parserChannel),
+                                 mBaseURI,
+-                                stream,
++                                tmpStream.forget(),
+                                 nullPrincipal,
+                                 nsILoadInfo::SEC_REQUIRE_SAME_ORIGIN_DATA_IS_BLOCKED,
+                                 nsIContentPolicy::TYPE_OTHER,
+                                 nsDependentCString(aContentType));
+   if (!parserChannel || NS_FAILED(rv))
+     return NS_ERROR_FAILURE;
+ 
+   if (aCharset)
+diff --git a/rdf/base/nsRDFXMLParser.cpp b/rdf/base/nsRDFXMLParser.cpp
+--- a/rdf/base/nsRDFXMLParser.cpp
++++ b/rdf/base/nsRDFXMLParser.cpp
+@@ -114,19 +114,20 @@ nsRDFXMLParser::ParseString(nsIRDFDataSo
+     rv = NS_NewCStringInputStream(getter_AddRefs(stream), aString);
+     if (NS_FAILED(rv)) return rv;
+ 
+     nsCOMPtr<nsIPrincipal> nullPrincipal = NullPrincipal::Create();
+ 
+     // The following channel is never openend, so it does not matter what
+     // securityFlags we pass; let's follow the principle of least privilege.
+     nsCOMPtr<nsIChannel> channel;
++    nsCOMPtr<nsIInputStream> tmpStream = stream;
+     rv = NS_NewInputStreamChannel(getter_AddRefs(channel),
+                                   aBaseURI,
+-                                  stream,
++                                  tmpStream.forget(),
+                                   nullPrincipal,
+                                   nsILoadInfo::SEC_REQUIRE_SAME_ORIGIN_DATA_IS_BLOCKED,
+                                   nsIContentPolicy::TYPE_OTHER,
+                                   NS_LITERAL_CSTRING("text/xml"));
+     if (NS_FAILED(rv)) return rv;
+ 
+     listener->OnStartRequest(channel, nullptr);
+     listener->OnDataAvailable(channel, nullptr, stream, 0, aString.Length());

+ 440 - 0
frg/mozilla-release/work-js/1419382-3-59a1.patch

@@ -0,0 +1,440 @@
+# HG changeset patch
+# User Andrea Marchesini <amarchesini@mozilla.com>
+# Date 1511345937 -3600
+# Node ID 53f7c44ea8555b5e95be448f4d952953a4ab3208
+# Parent  43a27f2609a8c41463af24d2b306edf44affa06a
+Bug 1419382 - Moving ownership of nsIInputStream when using netUtil functions - part 3 - NS_NewInputStreamPump, r=smaug
+
+diff --git a/dom/fetch/FetchConsumer.cpp b/dom/fetch/FetchConsumer.cpp
+--- a/dom/fetch/FetchConsumer.cpp
++++ b/dom/fetch/FetchConsumer.cpp
+@@ -468,17 +468,17 @@ FetchBodyConsumer<Derived>::BeginConsume
+   if (mShuttingDown) {
+     // We haven't started yet, but we have been terminated. AutoFailConsumeBody
+     // will dispatch a runnable to release resources.
+     return;
+   }
+ 
+   nsCOMPtr<nsIInputStreamPump> pump;
+   nsresult rv = NS_NewInputStreamPump(getter_AddRefs(pump),
+-                                      mBodyStream, 0, 0, false,
++                                      mBodyStream.forget(), 0, 0, false,
+                                       mMainThreadEventTarget);
+   if (NS_WARN_IF(NS_FAILED(rv))) {
+     return;
+   }
+ 
+   RefPtr<ConsumeBodyDoneObserver<Derived>> p =
+    new ConsumeBodyDoneObserver<Derived>(this);
+ 
+diff --git a/dom/fetch/FetchConsumer.h b/dom/fetch/FetchConsumer.h
+--- a/dom/fetch/FetchConsumer.h
++++ b/dom/fetch/FetchConsumer.h
+@@ -98,17 +98,19 @@ private:
+   nsCOMPtr<nsIThread> mTargetThread;
+   nsCOMPtr<nsIEventTarget> mMainThreadEventTarget;
+ 
+ #ifdef DEBUG
+   // This is used only to check if the body has been correctly consumed.
+   RefPtr<FetchBody<Derived>> mBody;
+ #endif
+ 
++  // This is nullified when the consuming of the body starts.
+   nsCOMPtr<nsIInputStream> mBodyStream;
++
+   MutableBlobStorage::MutableBlobStorageType mBlobStorageType;
+   nsCString mBodyMimeType;
+ 
+   // Set when consuming the body is attempted on a worker.
+   // Unset when consumption is done/aborted.
+   // This WorkerHolder keeps alive the consumer via a cycle.
+   UniquePtr<workers::WorkerHolder> mWorkerHolder;
+ 
+diff --git a/dom/workers/ScriptLoader.cpp b/dom/workers/ScriptLoader.cpp
+--- a/dom/workers/ScriptLoader.cpp
++++ b/dom/workers/ScriptLoader.cpp
+@@ -1720,17 +1720,17 @@ CacheScriptLoader::ResolvedCallback(JSCo
+                                      Move(mPrincipalInfo), mCSPHeaderValue,
+                                      mCSPReportOnlyHeaderValue,
+                                      mReferrerPolicyHeaderValue);
+     return;
+   }
+ 
+   MOZ_ASSERT(!mPump);
+   rv = NS_NewInputStreamPump(getter_AddRefs(mPump),
+-                             inputStream,
++                             inputStream.forget(),
+                              0, /* default segsize */
+                              0, /* default segcount */
+                              false, /* default closeWhenDone */
+                              mMainThreadEventTarget);
+   if (NS_WARN_IF(NS_FAILED(rv))) {
+     Fail(rv);
+     return;
+   }
+diff --git a/dom/workers/ServiceWorkerScriptCache.cpp b/dom/workers/ServiceWorkerScriptCache.cpp
+--- a/dom/workers/ServiceWorkerScriptCache.cpp
++++ b/dom/workers/ServiceWorkerScriptCache.cpp
+@@ -1155,17 +1155,17 @@ CompareCache::ManageValueResult(JSContex
+   MOZ_ASSERT(response->Ok());
+ 
+   nsCOMPtr<nsIInputStream> inputStream;
+   response->GetBody(getter_AddRefs(inputStream));
+   MOZ_ASSERT(inputStream);
+ 
+   MOZ_ASSERT(!mPump);
+   rv = NS_NewInputStreamPump(getter_AddRefs(mPump),
+-                             inputStream,
++                             inputStream.forget(),
+                              0, /* default segsize */
+                              0, /* default segcount */
+                              false, /* default closeWhenDone */
+                              SystemGroup::EventTargetFor(TaskCategory::Other));
+   if (NS_WARN_IF(NS_FAILED(rv))) {
+     Finish(rv, false);
+     return;
+   }
+diff --git a/dom/worklet/Worklet.cpp b/dom/worklet/Worklet.cpp
+--- a/dom/worklet/Worklet.cpp
++++ b/dom/worklet/Worklet.cpp
+@@ -125,17 +125,17 @@ public:
+     nsCOMPtr<nsIInputStream> inputStream;
+     response->GetBody(getter_AddRefs(inputStream));
+     if (!inputStream) {
+       RejectPromises(NS_ERROR_DOM_NETWORK_ERR);
+       return;
+     }
+ 
+     nsCOMPtr<nsIInputStreamPump> pump;
+-    rv = NS_NewInputStreamPump(getter_AddRefs(pump), inputStream);
++    rv = NS_NewInputStreamPump(getter_AddRefs(pump), inputStream.forget());
+     if (NS_WARN_IF(NS_FAILED(rv))) {
+       RejectPromises(rv);
+       return;
+     }
+ 
+     nsCOMPtr<nsIStreamLoader> loader;
+     rv = NS_NewStreamLoader(getter_AddRefs(loader), this);
+     if (NS_WARN_IF(NS_FAILED(rv))) {
+diff --git a/modules/libjar/nsJARChannel.cpp b/modules/libjar/nsJARChannel.cpp
+--- a/modules/libjar/nsJARChannel.cpp
++++ b/modules/libjar/nsJARChannel.cpp
+@@ -385,17 +385,17 @@ nsJARChannel::OpenLocalFile()
+     // Local files are always considered safe.
+     mIsUnsafe = false;
+ 
+     RefPtr<nsJARInputThunk> input;
+     nsresult rv = CreateJarInput(gJarHandler->JarCache(),
+                                  getter_AddRefs(input));
+     if (NS_SUCCEEDED(rv)) {
+         // Create input stream pump and call AsyncRead as a block.
+-        rv = NS_NewInputStreamPump(getter_AddRefs(mPump), input);
++        rv = NS_NewInputStreamPump(getter_AddRefs(mPump), input.forget());
+         if (NS_SUCCEEDED(rv))
+             rv = mPump->AsyncRead(this, nullptr);
+     }
+ 
+     return rv;
+ }
+ 
+ void
+@@ -1068,17 +1068,17 @@ nsJARChannel::OnDownloadComplete(MemoryD
+ 
+     if (NS_SUCCEEDED(status)) {
+         mTempMem = Move(aData);
+ 
+         RefPtr<nsJARInputThunk> input;
+         rv = CreateJarInput(nullptr, getter_AddRefs(input));
+         if (NS_SUCCEEDED(rv)) {
+             // create input stream pump
+-            rv = NS_NewInputStreamPump(getter_AddRefs(mPump), input);
++            rv = NS_NewInputStreamPump(getter_AddRefs(mPump), input.forget());
+             if (NS_SUCCEEDED(rv))
+                 rv = mPump->AsyncRead(this, nullptr);
+         }
+         status = rv;
+     }
+ 
+     if (NS_FAILED(status)) {
+         NotifyError(status);
+diff --git a/modules/libjar/zipwriter/nsZipWriter.cpp b/modules/libjar/zipwriter/nsZipWriter.cpp
+--- a/modules/libjar/zipwriter/nsZipWriter.cpp
++++ b/modules/libjar/zipwriter/nsZipWriter.cpp
+@@ -983,18 +983,19 @@ inline nsresult nsZipWriter::BeginProces
+ 
+         RefPtr<nsZipDataStream> stream = new nsZipDataStream();
+         NS_ENSURE_TRUE(stream, NS_ERROR_OUT_OF_MEMORY);
+         rv = stream->Init(this, mStream, header, aItem->mCompression);
+         NS_ENSURE_SUCCESS(rv, rv);
+ 
+         if (aItem->mStream) {
+             nsCOMPtr<nsIInputStreamPump> pump;
+-            rv = NS_NewInputStreamPump(getter_AddRefs(pump), aItem->mStream,
+-                                       0, 0, true);
++            nsCOMPtr<nsIInputStream> tmpStream = aItem->mStream;
++            rv = NS_NewInputStreamPump(getter_AddRefs(pump),
++                                       tmpStream.forget(), 0, 0, true);
+             NS_ENSURE_SUCCESS(rv, rv);
+ 
+             rv = pump->AsyncRead(stream, nullptr);
+             NS_ENSURE_SUCCESS(rv, rv);
+         }
+         else {
+             rv = NS_MaybeOpenChannelUsingAsyncOpen2(aItem->mChannel, stream);
+             NS_ENSURE_SUCCESS(rv, rv);
+@@ -1012,17 +1013,19 @@ inline nsresult nsZipWriter::BeginProces
+ inline nsresult nsZipWriter::BeginProcessingRemoval(int32_t aPos)
+ {
+     // Open the zip file for reading
+     nsCOMPtr<nsIInputStream> inputStream;
+     nsresult rv = NS_NewLocalFileInputStream(getter_AddRefs(inputStream),
+                                              mFile);
+     NS_ENSURE_SUCCESS(rv, rv);
+     nsCOMPtr<nsIInputStreamPump> pump;
+-    rv = NS_NewInputStreamPump(getter_AddRefs(pump), inputStream, 0, 0, true);
++    nsCOMPtr<nsIInputStream> tmpStream = inputStream;
++    rv = NS_NewInputStreamPump(getter_AddRefs(pump), tmpStream.forget(), 0, 0,
++                               true);
+     if (NS_FAILED(rv)) {
+         inputStream->Close();
+         return rv;
+     }
+     nsCOMPtr<nsIStreamListener> listener;
+     rv = NS_NewSimpleStreamListener(getter_AddRefs(listener), mStream, this);
+     if (NS_FAILED(rv)) {
+         inputStream->Close();
+diff --git a/netwerk/base/nsNetUtil.cpp b/netwerk/base/nsNetUtil.cpp
+--- a/netwerk/base/nsNetUtil.cpp
++++ b/netwerk/base/nsNetUtil.cpp
+@@ -712,32 +712,34 @@ NS_NewInputStreamChannel(nsIChannel     
+                                           aLoadingPrincipal,
+                                           nullptr, // aTriggeringPrincipal
+                                           aSecurityFlags,
+                                           aContentPolicyType,
+                                           aIsSrcdocChannel);
+ }
+ 
+ nsresult
+-NS_NewInputStreamPump(nsIInputStreamPump **result,
+-                      nsIInputStream      *stream,
+-                      uint32_t             segsize /* = 0 */,
+-                      uint32_t             segcount /* = 0 */,
+-                      bool                 closeWhenDone /* = false */,
+-                      nsIEventTarget      *mainThreadTarget /* = nullptr */)
++NS_NewInputStreamPump(nsIInputStreamPump** aResult,
++                      already_AddRefed<nsIInputStream> aStream,
++                      uint32_t aSegsize /* = 0 */,
++                      uint32_t aSegcount /* = 0 */,
++                      bool aCloseWhenDone /* = false */,
++                      nsIEventTarget* aMainThreadTarget /* = nullptr */)
+ {
++    nsCOMPtr<nsIInputStream> stream = Move(aStream);
++
+     nsresult rv;
+     nsCOMPtr<nsIInputStreamPump> pump =
+         do_CreateInstance(NS_INPUTSTREAMPUMP_CONTRACTID, &rv);
+     if (NS_SUCCEEDED(rv)) {
+-        rv = pump->Init(stream, segsize, segcount, closeWhenDone,
+-                        mainThreadTarget);
++        rv = pump->Init(stream, aSegsize, aSegcount, aCloseWhenDone,
++                        aMainThreadTarget);
+         if (NS_SUCCEEDED(rv)) {
+-            *result = nullptr;
+-            pump.swap(*result);
++            *aResult = nullptr;
++            pump.swap(*aResult);
+         }
+     }
+     return rv;
+ }
+ 
+ nsresult
+ NS_NewAsyncStreamCopier(nsIAsyncStreamCopier **result,
+                         nsIInputStream        *source,
+diff --git a/netwerk/base/nsNetUtil.h b/netwerk/base/nsNetUtil.h
+--- a/netwerk/base/nsNetUtil.h
++++ b/netwerk/base/nsNetUtil.h
+@@ -296,22 +296,23 @@ nsresult NS_NewInputStreamChannel(nsICha
+                                   nsIURI             *aUri,
+                                   const nsAString    &aData,
+                                   const nsACString   &aContentType,
+                                   nsIPrincipal       *aLoadingPrincipal,
+                                   nsSecurityFlags     aSecurityFlags,
+                                   nsContentPolicyType aContentPolicyType,
+                                   bool                aIsSrcdocChannel = false);
+ 
+-nsresult NS_NewInputStreamPump(nsIInputStreamPump **result,
+-                               nsIInputStream      *stream,
+-                               uint32_t             segsize = 0,
+-                               uint32_t             segcount = 0,
+-                               bool                 closeWhenDone = false,
+-                               nsIEventTarget      *mainThreadTarget = nullptr);
++nsresult
++NS_NewInputStreamPump(nsIInputStreamPump** aResult,
++                      already_AddRefed<nsIInputStream> aStream,
++                      uint32_t aSegsize = 0,
++                      uint32_t aSegcount = 0,
++                      bool aCloseWhenDone = false,
++                      nsIEventTarget *aMainThreadTarget = nullptr);
+ 
+ // NOTE: you will need to specify whether or not your streams are buffered
+ // (i.e., do they implement ReadSegments/WriteSegments).  the default
+ // assumption of TRUE for both streams might not be right for you!
+ nsresult NS_NewAsyncStreamCopier(nsIAsyncStreamCopier **result,
+                                  nsIInputStream        *source,
+                                  nsIOutputStream       *sink,
+                                  nsIEventTarget        *target,
+diff --git a/netwerk/protocol/res/ExtensionProtocolHandler.cpp b/netwerk/protocol/res/ExtensionProtocolHandler.cpp
+--- a/netwerk/protocol/res/ExtensionProtocolHandler.cpp
++++ b/netwerk/protocol/res/ExtensionProtocolHandler.cpp
+@@ -114,17 +114,17 @@ class ExtensionStreamGetter : public Ref
+       }
+     }
+ 
+     // Get an input stream or file descriptor from the parent asynchronously.
+     Result<Ok, nsresult> GetAsync(nsIStreamListener* aListener,
+                                   nsIChannel* aChannel);
+ 
+     // Handle an input stream being returned from the parent
+-    void OnStream(nsIInputStream* aStream);
++    void OnStream(already_AddRefed<nsIInputStream> aStream);
+ 
+     // Handle file descriptor being returned from the parent
+     void OnFD(const FileDescriptor& aFD);
+ 
+     MOZ_DECLARE_REFCOUNTED_TYPENAME(ExtensionStreamGetter)
+ 
+   private:
+     nsCOMPtr<nsIURI> mURI;
+@@ -240,17 +240,17 @@ ExtensionStreamGetter::GetAsync(nsIStrea
+   gNeckoChild->SendGetExtensionStream(uri)->Then(
+     mMainThreadEventTarget,
+     __func__,
+     [self] (const OptionalIPCStream& stream) {
+       nsCOMPtr<nsIInputStream> inputStream;
+       if (stream.type() == OptionalIPCStream::OptionalIPCStream::TIPCStream) {
+         inputStream = ipc::DeserializeIPCStream(stream);
+       }
+-      self->OnStream(inputStream);
++      self->OnStream(inputStream.forget());
+     },
+     [self] (const mozilla::ipc::ResponseRejectReason) {
+       self->OnStream(nullptr);
+     }
+   );
+   return Ok();
+ }
+ 
+@@ -264,37 +264,39 @@ CancelRequest(nsIStreamListener* aListen
+ 
+   aListener->OnStartRequest(aChannel, nullptr);
+   aListener->OnStopRequest(aChannel, nullptr, aResult);
+   aChannel->Cancel(NS_BINDING_ABORTED);
+ }
+ 
+ // Handle an input stream sent from the parent.
+ void
+-ExtensionStreamGetter::OnStream(nsIInputStream* aStream)
++ExtensionStreamGetter::OnStream(already_AddRefed<nsIInputStream> aStream)
+ {
+   MOZ_ASSERT(IsNeckoChild());
+   MOZ_ASSERT(mListener);
+   MOZ_ASSERT(mMainThreadEventTarget);
+ 
++  nsCOMPtr<nsIInputStream> stream = Move(aStream);
++
+   // We must keep an owning reference to the listener
+   // until we pass it on to AsyncRead.
+   nsCOMPtr<nsIStreamListener> listener = mListener.forget();
+ 
+   MOZ_ASSERT(mChannel);
+ 
+-  if (!aStream) {
++  if (!stream) {
+     // The parent didn't send us back a stream.
+     CancelRequest(listener, mChannel, NS_ERROR_FILE_ACCESS_DENIED);
+     return;
+   }
+ 
+   nsCOMPtr<nsIInputStreamPump> pump;
+-  nsresult rv = NS_NewInputStreamPump(getter_AddRefs(pump), aStream, 0, 0,
+-                                      false, mMainThreadEventTarget);
++  nsresult rv = NS_NewInputStreamPump(getter_AddRefs(pump), stream.forget(),
++                                      0, 0, false, mMainThreadEventTarget);
+   if (NS_FAILED(rv)) {
+     CancelRequest(listener, mChannel, rv);
+     return;
+   }
+ 
+   rv = pump->AsyncRead(listener, nullptr);
+   if (NS_FAILED(rv)) {
+     CancelRequest(listener, mChannel, rv);
+diff --git a/netwerk/protocol/wyciwyg/nsWyciwygChannel.cpp b/netwerk/protocol/wyciwyg/nsWyciwygChannel.cpp
+--- a/netwerk/protocol/wyciwyg/nsWyciwygChannel.cpp
++++ b/netwerk/protocol/wyciwyg/nsWyciwygChannel.cpp
+@@ -468,17 +468,16 @@ nsWyciwygChannel::WriteToCacheEntry(cons
+ 
+ 
+ NS_IMETHODIMP
+ nsWyciwygChannel::CloseCacheEntry(nsresult reason)
+ {
+   if (mCacheEntry) {
+     LOG(("nsWyciwygChannel::CloseCacheEntry [this=%p ]", this));
+     mCacheOutputStream = nullptr;
+-    mCacheInputStream = nullptr;
+ 
+     if (NS_FAILED(reason)) {
+       mCacheEntry->AsyncDoom(nullptr);
+     }
+ 
+     mCacheEntry = nullptr;
+   }
+   return NS_OK;
+@@ -764,22 +763,23 @@ nsWyciwygChannel::ReadFromCache()
+ 
+   nsAutoCString tmpStr;
+   rv = mCacheEntry->GetMetaDataElement("inhibit-persistent-caching",
+                                        getter_Copies(tmpStr));
+   if (NS_SUCCEEDED(rv) && tmpStr.EqualsLiteral("1"))
+     mLoadFlags |= INHIBIT_PERSISTENT_CACHING;
+ 
+   // Get a transport to the cached data...
+-  rv = mCacheEntry->OpenInputStream(0, getter_AddRefs(mCacheInputStream));
++  nsCOMPtr<nsIInputStream> inputStream;
++  rv = mCacheEntry->OpenInputStream(0, getter_AddRefs(inputStream));
+   if (NS_FAILED(rv))
+     return rv;
+-  NS_ENSURE_TRUE(mCacheInputStream, NS_ERROR_UNEXPECTED);
++  NS_ENSURE_TRUE(inputStream, NS_ERROR_UNEXPECTED);
+ 
+-  rv = NS_NewInputStreamPump(getter_AddRefs(mPump), mCacheInputStream);
++  rv = NS_NewInputStreamPump(getter_AddRefs(mPump), inputStream.forget());
+   if (NS_FAILED(rv)) return rv;
+ 
+   // Pump the cache data downstream
+   return mPump->AsyncRead(this, nullptr);
+ }
+ 
+ void
+ nsWyciwygChannel::WriteCharsetAndSourceToCache(int32_t aSource,
+diff --git a/netwerk/protocol/wyciwyg/nsWyciwygChannel.h b/netwerk/protocol/wyciwyg/nsWyciwygChannel.h
+--- a/netwerk/protocol/wyciwyg/nsWyciwygChannel.h
++++ b/netwerk/protocol/wyciwyg/nsWyciwygChannel.h
+@@ -91,17 +91,16 @@ protected:
+     nsCOMPtr<nsISupports>               mListenerContext;
+ 
+     // reuse as much of this channel implementation as we can
+     nsCOMPtr<nsIInputStreamPump>        mPump;
+ 
+     // Cache related stuff
+     nsCOMPtr<nsICacheEntry>             mCacheEntry;
+     nsCOMPtr<nsIOutputStream>           mCacheOutputStream;
+-    nsCOMPtr<nsIInputStream>            mCacheInputStream;
+ 
+     bool                                mNeedToSetSecurityInfo;
+     nsCOMPtr<nsISupports>               mSecurityInfo;
+ };
+ 
+ /**
+  * Casting nsWyciwygChannel to nsISupports is ambiguous.
+  * This method handles that.

+ 120 - 0
frg/mozilla-release/work-js/1419382-4-59a1.patch

@@ -0,0 +1,120 @@
+# HG changeset patch
+# User Andrea Marchesini <amarchesini@mozilla.com>
+# Date 1511345965 -3600
+# Node ID 6abf8547541108b5773fdf89dcedb40c76c5404e
+# Parent  e5da7bf79f46ff815d3c1bd0284f678a4e71cdd2
+Bug 1419382 - Moving ownership of nsIInputStream when using netUtil functions - part 4 - Get rid of NS_NewAsyncStreamCopier, r=smaug
+
+diff --git a/netwerk/base/nsNetUtil.cpp b/netwerk/base/nsNetUtil.cpp
+--- a/netwerk/base/nsNetUtil.cpp
++++ b/netwerk/base/nsNetUtil.cpp
+@@ -736,41 +736,16 @@ NS_NewInputStreamPump(nsIInputStreamPump
+             *aResult = nullptr;
+             pump.swap(*aResult);
+         }
+     }
+     return rv;
+ }
+ 
+ nsresult
+-NS_NewAsyncStreamCopier(nsIAsyncStreamCopier **result,
+-                        nsIInputStream        *source,
+-                        nsIOutputStream       *sink,
+-                        nsIEventTarget        *target,
+-                        bool                   sourceBuffered /* = true */,
+-                        bool                   sinkBuffered /* = true */,
+-                        uint32_t               chunkSize /* = 0 */,
+-                        bool                   closeSource /* = true */,
+-                        bool                   closeSink /* = true */)
+-{
+-    nsresult rv;
+-    nsCOMPtr<nsIAsyncStreamCopier> copier =
+-        do_CreateInstance(NS_ASYNCSTREAMCOPIER_CONTRACTID, &rv);
+-    if (NS_SUCCEEDED(rv)) {
+-        rv = copier->Init(source, sink, target, sourceBuffered, sinkBuffered,
+-                          chunkSize, closeSource, closeSink);
+-        if (NS_SUCCEEDED(rv)) {
+-            *result = nullptr;
+-            copier.swap(*result);
+-        }
+-    }
+-    return rv;
+-}
+-
+-nsresult
+ NS_NewLoadGroup(nsILoadGroup      **result,
+                 nsIRequestObserver *obs)
+ {
+     nsresult rv;
+     nsCOMPtr<nsILoadGroup> group =
+         do_CreateInstance(NS_LOADGROUP_CONTRACTID, &rv);
+     if (NS_SUCCEEDED(rv)) {
+         rv = group->SetGroupObserver(obs);
+diff --git a/netwerk/base/nsNetUtil.h b/netwerk/base/nsNetUtil.h
+--- a/netwerk/base/nsNetUtil.h
++++ b/netwerk/base/nsNetUtil.h
+@@ -304,29 +304,16 @@ nsresult NS_NewInputStreamChannel(nsICha
+ nsresult
+ NS_NewInputStreamPump(nsIInputStreamPump** aResult,
+                       already_AddRefed<nsIInputStream> aStream,
+                       uint32_t aSegsize = 0,
+                       uint32_t aSegcount = 0,
+                       bool aCloseWhenDone = false,
+                       nsIEventTarget *aMainThreadTarget = nullptr);
+ 
+-// NOTE: you will need to specify whether or not your streams are buffered
+-// (i.e., do they implement ReadSegments/WriteSegments).  the default
+-// assumption of TRUE for both streams might not be right for you!
+-nsresult NS_NewAsyncStreamCopier(nsIAsyncStreamCopier **result,
+-                                 nsIInputStream        *source,
+-                                 nsIOutputStream       *sink,
+-                                 nsIEventTarget        *target,
+-                                 bool                   sourceBuffered = true,
+-                                 bool                   sinkBuffered = true,
+-                                 uint32_t               chunkSize = 0,
+-                                 bool                   closeSource = true,
+-                                 bool                   closeSink = true);
+-
+ nsresult NS_NewLoadGroup(nsILoadGroup      **result,
+                          nsIRequestObserver *obs);
+ 
+ // Create a new nsILoadGroup that will match the given principal.
+ nsresult
+ NS_NewLoadGroup(nsILoadGroup **aResult, nsIPrincipal* aPrincipal);
+ 
+ // Determine if the given loadGroup/principal pair will produce a principal
+diff --git a/netwerk/protocol/ftp/nsFtpConnectionThread.cpp b/netwerk/protocol/ftp/nsFtpConnectionThread.cpp
+--- a/netwerk/protocol/ftp/nsFtpConnectionThread.cpp
++++ b/netwerk/protocol/ftp/nsFtpConnectionThread.cpp
+@@ -1527,23 +1527,24 @@ nsFtpState::R_pasv() {
+             // perform the data copy on the socket transport thread.  we do this
+             // because "output" is a socket output stream, so the result is that
+             // all work will be done on the socket transport thread.
+             nsCOMPtr<nsIEventTarget> stEventTarget =
+                 do_GetService(NS_SOCKETTRANSPORTSERVICE_CONTRACTID);
+             if (!stEventTarget)
+                 return FTP_ERROR;
+ 
+-            nsCOMPtr<nsIAsyncStreamCopier> copier;
+-            rv = NS_NewAsyncStreamCopier(getter_AddRefs(copier),
+-                                         mChannel->UploadStream(),
+-                                         output,
+-                                         stEventTarget,
+-                                         true,   // upload stream is buffered
+-                                         false); // output is NOT buffered
++            nsCOMPtr<nsIAsyncStreamCopier> copier =
++                do_CreateInstance(NS_ASYNCSTREAMCOPIER_CONTRACTID, &rv);
++            if (NS_SUCCEEDED(rv)) {
++                rv = copier->Init(mChannel->UploadStream(), output,
++                                  stEventTarget, true,
++                                  false /* output is NOT buffered */,
++                                  0, true, true);
++            }
+             if (NS_FAILED(rv))
+                 return FTP_ERROR;
+ 
+             rv = copier->AsyncCopy(this, nullptr);
+             if (NS_FAILED(rv))
+                 return FTP_ERROR;
+ 
+             // hold a reference to the copier so we can cancel it if necessary.

+ 390 - 0
frg/mozilla-release/work-js/1420419-1-59a1.patch

@@ -0,0 +1,390 @@
+# HG changeset patch
+# User Andrea Marchesini <amarchesini@mozilla.com>
+# Date 1511944816 -3600
+# Node ID bbb02c1fccd69fa18900c5f57ee0261c341d4749
+# Parent  c20c3a5282c6678c16a1825e1b0f5398a39bbb5d
+Bug 1420419 - Postpone the removing of BlobURL for 5 seconds in order to allow the loading of them in a remote process, r=smaug
+
+diff --git a/dom/file/nsHostObjectProtocolHandler.cpp b/dom/file/nsHostObjectProtocolHandler.cpp
+--- a/dom/file/nsHostObjectProtocolHandler.cpp
++++ b/dom/file/nsHostObjectProtocolHandler.cpp
+@@ -24,17 +24,17 @@
+ #include "nsError.h"
+ #include "nsHostObjectURI.h"
+ #include "nsIException.h" // for nsIStackFrame
+ #include "nsIMemoryReporter.h"
+ #include "nsIPrincipal.h"
+ #include "nsIUUIDGenerator.h"
+ #include "nsNetUtil.h"
+ 
+-#define RELEASING_TIMER 1000
++#define RELEASING_TIMER 5000
+ 
+ using namespace mozilla;
+ using namespace mozilla::dom;
+ using namespace mozilla::ipc;
+ 
+ // -----------------------------------------------------------------------
+ // Hash table
+ struct DataInfo
+@@ -44,47 +44,55 @@ struct DataInfo
+     eMediaStream,
+     eMediaSource
+   };
+ 
+   DataInfo(BlobImpl* aBlobImpl, nsIPrincipal* aPrincipal)
+     : mObjectType(eBlobImpl)
+     , mBlobImpl(aBlobImpl)
+     , mPrincipal(aPrincipal)
++    , mRevoked(false)
+   {}
+ 
+   DataInfo(DOMMediaStream* aMediaStream, nsIPrincipal* aPrincipal)
+     : mObjectType(eMediaStream)
+     , mMediaStream(aMediaStream)
+     , mPrincipal(aPrincipal)
++    , mRevoked(false)
+   {}
+ 
+   DataInfo(MediaSource* aMediaSource, nsIPrincipal* aPrincipal)
+     : mObjectType(eMediaSource)
+     , mMediaSource(aMediaSource)
+     , mPrincipal(aPrincipal)
++    , mRevoked(false)
+   {}
+ 
+   ObjectType mObjectType;
+ 
+   RefPtr<BlobImpl> mBlobImpl;
+   RefPtr<DOMMediaStream> mMediaStream;
+   RefPtr<MediaSource> mMediaSource;
+ 
+   nsCOMPtr<nsIPrincipal> mPrincipal;
+   nsCString mStack;
+ 
+   // WeakReferences of nsHostObjectURI objects.
+   nsTArray<nsWeakPtr> mURIs;
++
++  // When a blobURL is revoked, we keep it alive for RELEASING_TIMER
++  // milliseconds in order to support pending operations such as navigation,
++  // download and so on.
++  bool mRevoked;
+ };
+ 
+ static nsClassHashtable<nsCStringHashKey, DataInfo>* gDataTable;
+ 
+ static DataInfo*
+-GetDataInfo(const nsACString& aUri)
++GetDataInfo(const nsACString& aUri, bool aAlsoIfRevoked = false)
+ {
+   if (!gDataTable) {
+     return nullptr;
+   }
+ 
+   DataInfo* res;
+ 
+   // Let's remove any fragment and query from this URI.
+@@ -101,16 +109,20 @@ GetDataInfo(const nsACString& aUri)
+   }
+ 
+   if (pos < 0) {
+     gDataTable->Get(aUri, &res);
+   } else {
+     gDataTable->Get(StringHead(aUri, pos), &res);
+   }
+ 
++  if (!aAlsoIfRevoked && res && res->mRevoked) {
++    return nullptr;
++  }
++
+   return res;
+ }
+ 
+ static DataInfo*
+ GetDataInfoFromURI(nsIURI* aURI)
+ {
+   if (!aURI) {
+     return nullptr;
+@@ -150,29 +162,27 @@ BroadcastBlobURLRegistration(const nsACS
+     return;
+   }
+ 
+   Unused << NS_WARN_IF(!cc->SendStoreAndBroadcastBlobURLRegistration(
+     nsCString(aURI), ipcBlob, IPC::Principal(aPrincipal)));
+ }
+ 
+ void
+-BroadcastBlobURLUnregistration(const nsACString& aURI, DataInfo* aInfo)
++BroadcastBlobURLUnregistration(const nsCString& aURI)
+ {
+-  MOZ_ASSERT(aInfo);
+   MOZ_ASSERT(NS_IsMainThread());
+ 
+   if (XRE_IsParentProcess()) {
+     dom::ContentParent::BroadcastBlobURLUnregistration(aURI);
+     return;
+   }
+ 
+   dom::ContentChild* cc = dom::ContentChild::GetSingleton();
+-  Unused << NS_WARN_IF(!cc->SendUnstoreAndBroadcastBlobURLUnregistration(
+-    nsCString(aURI)));
++  Unused << NS_WARN_IF(!cc->SendUnstoreAndBroadcastBlobURLUnregistration(aURI));
+ }
+ 
+ class HostObjectURLsReporter final : public nsIMemoryReporter
+ {
+   ~HostObjectURLsReporter() {}
+ 
+  public:
+   NS_DECL_ISUPPORTS
+@@ -424,55 +434,78 @@ NS_IMPL_ISUPPORTS(BlobURLsReporter, nsIM
+ 
+ class ReleasingTimerHolder final : public nsITimerCallback
+                                  , public nsINamed
+ {
+ public:
+   NS_DECL_ISUPPORTS
+ 
+   static void
+-  Create(nsTArray<nsWeakPtr>&& aArray)
++  Create(const nsACString& aURI, bool aBroadcastToOtherProcesses)
+   {
+-    RefPtr<ReleasingTimerHolder> holder = new ReleasingTimerHolder(Move(aArray));
++    RefPtr<ReleasingTimerHolder> holder =
++      new ReleasingTimerHolder(aURI, aBroadcastToOtherProcesses);
+     nsresult rv = NS_NewTimerWithCallback(getter_AddRefs(holder->mTimer),
+                                           holder, RELEASING_TIMER,
+                                           nsITimer::TYPE_ONE_SHOT,
+                                           SystemGroup::EventTargetFor(TaskCategory::Other));
+     NS_ENSURE_SUCCESS_VOID(rv);
+   }
+ 
+   NS_IMETHOD
+   Notify(nsITimer* aTimer) override
+   {
+-    for (uint32_t i = 0; i < mURIs.Length(); ++i) {
+-      nsCOMPtr<nsIURI> uri = do_QueryReferent(mURIs[i]);
++    // If we have to broadcast the unregistration, let's do it now.
++    if (mBroadcastToOtherProcesses) {
++      BroadcastBlobURLUnregistration(mURI);
++    }
++
++    DataInfo* info = GetDataInfo(mURI, true /* We care about revoked dataInfo */);
++    if (!info) {
++      // Already gone!
++      return NS_OK;
++    }
++
++    MOZ_ASSERT(info->mRevoked);
++
++    for (uint32_t i = 0; i < info->mURIs.Length(); ++i) {
++      nsCOMPtr<nsIURI> uri = do_QueryReferent(info->mURIs[i]);
+       if (uri) {
+         static_cast<nsHostObjectURI*>(uri.get())->ForgetBlobImpl();
+       }
+     }
+ 
++    gDataTable->Remove(mURI);
++    if (gDataTable->Count() == 0) {
++      delete gDataTable;
++      gDataTable = nullptr;
++    }
++
+     return NS_OK;
+   }
+ 
+   NS_IMETHOD
+   GetName(nsACString& aName) override
+   {
+     aName.AssignLiteral("ReleasingTimerHolder");
+     return NS_OK;
+   }
+ 
+ private:
+-  explicit ReleasingTimerHolder(nsTArray<nsWeakPtr>&& aArray)
+-    : mURIs(aArray)
++  ReleasingTimerHolder(const nsACString& aURI, bool aBroadcastToOtherProcesses)
++    : mURI(aURI)
++    , mBroadcastToOtherProcesses(aBroadcastToOtherProcesses)
+   {}
+ 
+   ~ReleasingTimerHolder()
+   {}
+ 
+-  nsTArray<nsWeakPtr> mURIs;
++  nsCString mURI;
++  bool mBroadcastToOtherProcesses;
++
+   nsCOMPtr<nsITimer> mTimer;
+ };
+ 
+ NS_IMPL_ISUPPORTS(ReleasingTimerHolder, nsITimerCallback, nsINamed)
+ 
+ } // namespace mozilla
+ 
+ template<typename T>
+@@ -588,17 +621,18 @@ nsHostObjectProtocolHandler::GetAllBlobU
+ 
+     IPCBlob ipcBlob;
+     nsresult rv = IPCBlobUtils::Serialize(info->mBlobImpl, aCP, ipcBlob);
+     if (NS_WARN_IF(NS_FAILED(rv))) {
+       return false;
+     }
+ 
+     aRegistrations.AppendElement(BlobURLRegistrationData(
+-      nsCString(iter.Key()), ipcBlob, IPC::Principal(info->mPrincipal)));
++      nsCString(iter.Key()), ipcBlob, IPC::Principal(info->mPrincipal),
++                info->mRevoked));
+   }
+ 
+   return true;
+ }
+ 
+ /*static */ void
+ nsHostObjectProtocolHandler::RemoveDataEntry(const nsACString& aUri,
+                                              bool aBroadcastToOtherProcesses)
+@@ -607,29 +641,24 @@ nsHostObjectProtocolHandler::RemoveDataE
+     return;
+   }
+ 
+   DataInfo* info = GetDataInfo(aUri);
+   if (!info) {
+     return;
+   }
+ 
+-  if (aBroadcastToOtherProcesses && info->mObjectType == DataInfo::eBlobImpl) {
+-    BroadcastBlobURLUnregistration(aUri, info);
+-  }
++  info->mRevoked = true;
+ 
+-  if (!info->mURIs.IsEmpty()) {
+-    ReleasingTimerHolder::Create(Move(info->mURIs));
+-  }
+-
+-  gDataTable->Remove(aUri);
+-  if (gDataTable->Count() == 0) {
+-    delete gDataTable;
+-    gDataTable = nullptr;
+-  }
++  // The timer will take care of removing the entry for real after
++  // RELEASING_TIMER milliseconds. In the meantime, the DataInfo, marked as
++  // revoked, will not be exposed.
++  ReleasingTimerHolder::Create(aUri,
++                               aBroadcastToOtherProcesses &&
++                                 info->mObjectType == DataInfo::eBlobImpl);
+ }
+ 
+ /* static */ void
+ nsHostObjectProtocolHandler::RemoveDataEntries()
+ {
+   MOZ_ASSERT(XRE_IsContentProcess());
+ 
+   if (!gDataTable) {
+diff --git a/dom/file/nsHostObjectProtocolHandler.h b/dom/file/nsHostObjectProtocolHandler.h
+--- a/dom/file/nsHostObjectProtocolHandler.h
++++ b/dom/file/nsHostObjectProtocolHandler.h
+@@ -72,17 +72,16 @@ public:
+   // IPC only
+   static nsresult AddDataEntry(const nsACString& aURI,
+                                nsIPrincipal* aPrincipal,
+                                mozilla::dom::BlobImpl* aBlobImpl);
+ 
+   static void RemoveDataEntry(const nsACString& aUri,
+                               bool aBroadcastToOTherProcesses = true);
+ 
+-  // This is for IPC only.
+   static void RemoveDataEntries();
+ 
+   static bool HasDataEntry(const nsACString& aUri);
+ 
+   static nsIPrincipal* GetDataEntryPrincipal(const nsACString& aUri);
+   static void Traverse(const nsACString& aUri, nsCycleCollectionTraversalCallback& aCallback);
+ 
+   static bool
+diff --git a/dom/ipc/ContentChild.cpp b/dom/ipc/ContentChild.cpp
+--- a/dom/ipc/ContentChild.cpp
++++ b/dom/ipc/ContentChild.cpp
+@@ -2592,16 +2592,23 @@ ContentChild::RecvInitBlobURLs(nsTArray<
+   for (uint32_t i = 0; i < aRegistrations.Length(); ++i) {
+     BlobURLRegistrationData& registration = aRegistrations[i];
+     RefPtr<BlobImpl> blobImpl = IPCBlobUtils::Deserialize(registration.blob());
+     MOZ_ASSERT(blobImpl);
+ 
+     nsHostObjectProtocolHandler::AddDataEntry(registration.url(),
+                                               registration.principal(),
+                                               blobImpl);
++    // If we have received an already-revoked blobURL, we have to keep it alive
++    // for a while (see nsHostObjectProtocolHandler) in order to support pending
++    // operations such as navigation, download and so on.
++    if (registration.revoked()) {
++      nsHostObjectProtocolHandler::RemoveDataEntry(registration.url(),
++                                                   false);
++    }
+   }
+ 
+   return IPC_OK();
+ }
+ 
+ mozilla::ipc::IPCResult
+ ContentChild::RecvLastPrivateDocShellDestroyed()
+ {
+diff --git a/dom/ipc/PContent.ipdl b/dom/ipc/PContent.ipdl
+--- a/dom/ipc/PContent.ipdl
++++ b/dom/ipc/PContent.ipdl
+@@ -226,19 +226,20 @@ struct FileCreationErrorResult
+ union FileCreationResult
+ {
+   FileCreationSuccessResult;
+   FileCreationErrorResult;
+ };
+ 
+ struct BlobURLRegistrationData
+ {
+-    nsCString url;
+-    IPCBlob blob;
+-    Principal principal;
++  nsCString url;
++  IPCBlob blob;
++  Principal principal;
++  bool revoked;
+ };
+ 
+ struct GMPAPITags
+ {
+     nsCString api;
+     nsCString[] tags;
+ };
+ 
+diff --git a/layout/build/nsLayoutStatics.cpp b/layout/build/nsLayoutStatics.cpp
+--- a/layout/build/nsLayoutStatics.cpp
++++ b/layout/build/nsLayoutStatics.cpp
+@@ -118,16 +118,17 @@
+ #include "MediaPrefs.h"
+ #include "mozilla/ServoBindings.h"
+ #include "mozilla/StaticPresData.h"
+ #include "mozilla/StylePrefs.h"
+ #include "mozilla/dom/WebIDLGlobalNameHash.h"
+ #include "mozilla/dom/ipc/IPCBlobInputStreamStorage.h"
+ #include "mozilla/dom/U2FTokenManager.h"
+ #include "mozilla/dom/PointerEventHandler.h"
++#include "nsHostObjectProtocolHandler.h"
+ 
+ using namespace mozilla;
+ using namespace mozilla::net;
+ using namespace mozilla::dom;
+ using namespace mozilla::dom::ipc;
+ 
+ nsrefcnt nsLayoutStatics::sLayoutStaticRefcnt = 0;
+ 
+@@ -426,9 +427,11 @@ nsLayoutStatics::Shutdown()
+ 
+   ContentParent::ShutDown();
+ 
+   DisplayItemClip::Shutdown();
+ 
+   CacheObserver::Shutdown();
+ 
+   PromiseDebugging::Shutdown();
++
++  nsHostObjectProtocolHandler::RemoveDataEntries();
+ }

+ 29 - 0
frg/mozilla-release/work-js/1420419-2-59a1.patch

@@ -0,0 +1,29 @@
+# HG changeset patch
+# User Andrea Marchesini <amarchesini@mozilla.com>
+# Date 1511947952 -3600
+# Node ID 9e2680def2f9c677a88cf5d34b53cb00cbe23d73
+# Parent  c5b1f8ff6d216568985f9f5b5955fa224d17515f
+Bug 1420419 - nsHostObjectProtocolHandler::RemoveDataEntries can be called on any process, r=me CLOSED TREE
+
+diff --git a/dom/file/nsHostObjectProtocolHandler.cpp b/dom/file/nsHostObjectProtocolHandler.cpp
+--- a/dom/file/nsHostObjectProtocolHandler.cpp
++++ b/dom/file/nsHostObjectProtocolHandler.cpp
+@@ -654,18 +654,16 @@ nsHostObjectProtocolHandler::RemoveDataE
+   ReleasingTimerHolder::Create(aUri,
+                                aBroadcastToOtherProcesses &&
+                                  info->mObjectType == DataInfo::eBlobImpl);
+ }
+ 
+ /* static */ void
+ nsHostObjectProtocolHandler::RemoveDataEntries()
+ {
+-  MOZ_ASSERT(XRE_IsContentProcess());
+-
+   if (!gDataTable) {
+     return;
+   }
+ 
+   gDataTable->Clear();
+   delete gDataTable;
+   gDataTable = nullptr;
+ }

+ 246 - 0
frg/mozilla-release/work-js/1420594-1-59a1.patch

@@ -0,0 +1,246 @@
+# HG changeset patch
+# User Ben Kelly <ben@wanderview.com>
+# Date 1512399092 18000
+# Node ID 7ba9a363b3d2ae4bb0460fafc7279196754211b0
+# Parent  91afe90c9fb99829041f8fb64609a956791c4d9c
+Bug 1420594 P1 Make ClientManagerService track active ClientManagerParent actors. r=baku
+
+diff --git a/dom/clients/manager/ClientManagerActors.cpp b/dom/clients/manager/ClientManagerActors.cpp
+--- a/dom/clients/manager/ClientManagerActors.cpp
++++ b/dom/clients/manager/ClientManagerActors.cpp
+@@ -32,10 +32,17 @@ AllocClientManagerParent()
+ 
+ bool
+ DeallocClientManagerParent(PClientManagerParent* aActor)
+ {
+   delete aActor;
+   return true;
+ }
+ 
++void
++InitClientManagerParent(PClientManagerParent* aActor)
++{
++  auto actor = static_cast<ClientManagerParent*>(aActor);
++  actor->Init();
++}
++
+ } // namespace dom
+ } // namespace mozilla
+diff --git a/dom/clients/manager/ClientManagerActors.h b/dom/clients/manager/ClientManagerActors.h
+--- a/dom/clients/manager/ClientManagerActors.h
++++ b/dom/clients/manager/ClientManagerActors.h
+@@ -19,13 +19,16 @@ bool
+ DeallocClientManagerChild(PClientManagerChild* aActor);
+ 
+ PClientManagerParent*
+ AllocClientManagerParent();
+ 
+ bool
+ DeallocClientManagerParent(PClientManagerParent* aActor);
+ 
++void
++InitClientManagerParent(PClientManagerParent* aActor);
++
+ 
+ } // namespace dom
+ } // namespace mozilla
+ 
+ #endif // _mozilla_dom_ClientManagerActors_h
+diff --git a/dom/clients/manager/ClientManagerParent.cpp b/dom/clients/manager/ClientManagerParent.cpp
+--- a/dom/clients/manager/ClientManagerParent.cpp
++++ b/dom/clients/manager/ClientManagerParent.cpp
+@@ -112,12 +112,19 @@ ClientManagerParent::RecvPClientSourceCo
+ 
+ ClientManagerParent::ClientManagerParent()
+   : mService(ClientManagerService::GetOrCreateInstance())
+ {
+ }
+ 
+ ClientManagerParent::~ClientManagerParent()
+ {
++  mService->RemoveManager(this);
++}
++
++void
++ClientManagerParent::Init()
++{
++  mService->AddManager(this);
+ }
+ 
+ } // namespace dom
+ } // namespace mozilla
+diff --git a/dom/clients/manager/ClientManagerParent.h b/dom/clients/manager/ClientManagerParent.h
+--- a/dom/clients/manager/ClientManagerParent.h
++++ b/dom/clients/manager/ClientManagerParent.h
+@@ -58,14 +58,17 @@ class ClientManagerParent final : public
+ 
+   mozilla::ipc::IPCResult
+   RecvPClientSourceConstructor(PClientSourceParent* aActor,
+                                const ClientSourceConstructorArgs& aArgs) override;
+ 
+ public:
+   ClientManagerParent();
+   ~ClientManagerParent();
++
++  void
++  Init();
+ };
+ 
+ } // namespace dom
+ } // namespace mozilla
+ 
+ #endif // _mozilla_dom_ClientManagerParent_h
+diff --git a/dom/clients/manager/ClientManagerService.cpp b/dom/clients/manager/ClientManagerService.cpp
+--- a/dom/clients/manager/ClientManagerService.cpp
++++ b/dom/clients/manager/ClientManagerService.cpp
+@@ -62,16 +62,17 @@ ClientManagerService::ClientManagerServi
+ {
+   AssertIsOnBackgroundThread();
+ }
+ 
+ ClientManagerService::~ClientManagerService()
+ {
+   AssertIsOnBackgroundThread();
+   MOZ_DIAGNOSTIC_ASSERT(mSourceTable.Count() == 0);
++  MOZ_DIAGNOSTIC_ASSERT(mManagerList.IsEmpty());
+ 
+   MOZ_DIAGNOSTIC_ASSERT(sClientManagerServiceInstance == this);
+   sClientManagerServiceInstance = nullptr;
+ }
+ 
+ // static
+ already_AddRefed<ClientManagerService>
+ ClientManagerService::GetOrCreateInstance()
+@@ -128,10 +129,28 @@ ClientManagerService::FindSource(const n
+   ClientSourceParent* source = entry.Data();
+   if (!MatchPrincipalInfo(source->Info().PrincipalInfo(), aPrincipalInfo)) {
+     return nullptr;
+   }
+ 
+   return source;
+ }
+ 
++void
++ClientManagerService::AddManager(ClientManagerParent* aManager)
++{
++  AssertIsOnBackgroundThread();
++  MOZ_DIAGNOSTIC_ASSERT(aManager);
++  MOZ_ASSERT(!mManagerList.Contains(aManager));
++  mManagerList.AppendElement(aManager);
++}
++
++void
++ClientManagerService::RemoveManager(ClientManagerParent* aManager)
++{
++  AssertIsOnBackgroundThread();
++  MOZ_DIAGNOSTIC_ASSERT(aManager);
++  DebugOnly<bool> removed = mManagerList.RemoveElement(aManager);
++  MOZ_ASSERT(removed);
++}
++
+ } // namespace dom
+ } // namespace mozilla
+diff --git a/dom/clients/manager/ClientManagerService.h b/dom/clients/manager/ClientManagerService.h
+--- a/dom/clients/manager/ClientManagerService.h
++++ b/dom/clients/manager/ClientManagerService.h
+@@ -8,27 +8,30 @@
+ 
+ #include "mozilla/ipc/PBackgroundSharedTypes.h"
+ #include "nsDataHashtable.h"
+ 
+ namespace mozilla {
+ 
+ namespace dom {
+ 
++class ClientManagerParent;
+ class ClientSourceParent;
+ 
+ // Define a singleton service to manage client activity throughout the
+ // browser.  This service runs on the PBackground thread.  To interact
+ // it with it please use the ClientManager and ClientHandle classes.
+ class ClientManagerService final
+ {
+   // Store the ClientSourceParent objects in a hash table.  We want to
+   // optimize for insertion, removal, and lookup by UUID.
+   nsDataHashtable<nsIDHashKey, ClientSourceParent*> mSourceTable;
+ 
++  nsTArray<ClientManagerParent*> mManagerList;
++
+   ClientManagerService();
+   ~ClientManagerService();
+ 
+ public:
+   static already_AddRefed<ClientManagerService>
+   GetOrCreateInstance();
+ 
+   bool
+@@ -36,15 +39,21 @@ public:
+ 
+   bool
+   RemoveSource(ClientSourceParent* aSource);
+ 
+   ClientSourceParent*
+   FindSource(const nsID& aID,
+              const mozilla::ipc::PrincipalInfo& aPrincipalInfo);
+ 
++  void
++  AddManager(ClientManagerParent* aManager);
++
++  void
++  RemoveManager(ClientManagerParent* aManager);
++
+   NS_INLINE_DECL_REFCOUNTING(mozilla::dom::ClientManagerService)
+ };
+ 
+ } // namespace dom
+ } // namespace mozilla
+ 
+ #endif // _mozilla_dom_ClientManagerService_h
+diff --git a/ipc/glue/BackgroundParentImpl.cpp b/ipc/glue/BackgroundParentImpl.cpp
+--- a/ipc/glue/BackgroundParentImpl.cpp
++++ b/ipc/glue/BackgroundParentImpl.cpp
+@@ -976,16 +976,23 @@ BackgroundParentImpl::AllocPClientManage
+ }
+ 
+ bool
+ BackgroundParentImpl::DeallocPClientManagerParent(mozilla::dom::PClientManagerParent* aActor)
+ {
+   return mozilla::dom::DeallocClientManagerParent(aActor);
+ }
+ 
++mozilla::ipc::IPCResult
++BackgroundParentImpl::RecvPClientManagerConstructor(mozilla::dom::PClientManagerParent* aActor)
++{
++  mozilla::dom::InitClientManagerParent(aActor);
++  return IPC_OK();
++}
++
+ } // namespace ipc
+ } // namespace mozilla
+ 
+ void
+ TestParent::ActorDestroy(ActorDestroyReason aWhy)
+ {
+   mozilla::ipc::AssertIsInMainProcess();
+   AssertIsOnBackgroundThread();
+diff --git a/ipc/glue/BackgroundParentImpl.h b/ipc/glue/BackgroundParentImpl.h
+--- a/ipc/glue/BackgroundParentImpl.h
++++ b/ipc/glue/BackgroundParentImpl.h
+@@ -263,14 +263,17 @@ protected:
+   virtual bool
+   DeallocPHttpBackgroundChannelParent(PHttpBackgroundChannelParent *aActor) override;
+ 
+   virtual PClientManagerParent*
+   AllocPClientManagerParent() override;
+ 
+   virtual bool
+   DeallocPClientManagerParent(PClientManagerParent* aActor) override;
++
++  virtual mozilla::ipc::IPCResult
++  RecvPClientManagerConstructor(PClientManagerParent* aActor) override;
+ };
+ 
+ } // namespace ipc
+ } // namespace mozilla
+ 
+ #endif // mozilla_ipc_backgroundparentimpl_h__

+ 286 - 0
frg/mozilla-release/work-js/1420594-2-59a1.patch

@@ -0,0 +1,286 @@
+# HG changeset patch
+# User Ben Kelly <ben@wanderview.com>
+# Date 1512399092 18000
+# Node ID a3e3a096629cff5e9575cd6794702df36d3798e5
+# Parent  bdf31182d0ff46ff8c24750c9376816c38506386
+Bug 1420594 P2 Eagerly shutdown ClientManagerService. r=baku
+
+diff --git a/dom/clients/manager/ClientManagerService.cpp b/dom/clients/manager/ClientManagerService.cpp
+--- a/dom/clients/manager/ClientManagerService.cpp
++++ b/dom/clients/manager/ClientManagerService.cpp
+@@ -1,19 +1,23 @@
+ /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+ /* vim: set ts=8 sts=2 et sw=2 tw=80: */
+ /* This Source Code Form is subject to the terms of the Mozilla Public
+  * License, v. 2.0. If a copy of the MPL was not distributed with this
+  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+ 
+ #include "ClientManagerService.h"
+ 
++#include "ClientManagerParent.h"
+ #include "ClientSourceParent.h"
+ #include "mozilla/ipc/BackgroundParent.h"
+ #include "mozilla/ipc/PBackgroundSharedTypes.h"
++#include "mozilla/ClearOnShutdown.h"
++#include "mozilla/SystemGroup.h"
++#include "nsIAsyncShutdown.h"
+ 
+ namespace mozilla {
+ namespace dom {
+ 
+ using mozilla::ipc::AssertIsOnBackgroundThread;
+ using mozilla::ipc::ContentPrincipalInfo;
+ using mozilla::ipc::PrincipalInfo;
+ 
+@@ -51,47 +55,175 @@ MatchPrincipalInfo(const PrincipalInfo& 
+       break;
+     }
+   }
+ 
+   // Clients (windows/workers) should never have an expanded principal type.
+   MOZ_CRASH("unexpected principal type!");
+ }
+ 
++class ClientShutdownBlocker final : public nsIAsyncShutdownBlocker
++{
++  RefPtr<GenericPromise::Private> mPromise;
++
++  ~ClientShutdownBlocker() = default;
++
++public:
++  explicit ClientShutdownBlocker(GenericPromise::Private* aPromise)
++    : mPromise(aPromise)
++  {
++    MOZ_DIAGNOSTIC_ASSERT(mPromise);
++  }
++
++  NS_IMETHOD
++  GetName(nsAString& aNameOut) override
++  {
++    aNameOut =
++      NS_LITERAL_STRING("ClientManagerService: start destroying IPC actors early");
++    return NS_OK;
++  }
++
++  NS_IMETHOD
++  BlockShutdown(nsIAsyncShutdownClient* aClient) override
++  {
++    mPromise->Resolve(true, __func__);
++    aClient->RemoveBlocker(this);
++    return NS_OK;
++  }
++
++  NS_IMETHOD
++  GetState(nsIPropertyBag**) override
++  {
++    return NS_OK;
++  }
++
++  NS_DECL_ISUPPORTS
++};
++
++NS_IMPL_ISUPPORTS(ClientShutdownBlocker, nsIAsyncShutdownBlocker)
++
++// Helper function the resolves a MozPromise when we detect that the browser
++// has begun to shutdown.
++RefPtr<GenericPromise>
++OnShutdown()
++{
++  RefPtr<GenericPromise::Private> ref = new GenericPromise::Private(__func__);
++
++  nsCOMPtr<nsIRunnable> r = NS_NewRunnableFunction("ClientManagerServer::OnShutdown",
++  [ref] () {
++    nsCOMPtr<nsIAsyncShutdownService> svc = services::GetAsyncShutdown();
++    if (!svc) {
++      ref->Resolve(true, __func__);
++      return;
++    }
++
++    nsCOMPtr<nsIAsyncShutdownClient> phase;
++    MOZ_ALWAYS_SUCCEEDS(svc->GetXpcomWillShutdown(getter_AddRefs(phase)));
++    if (!phase) {
++      ref->Resolve(true, __func__);
++      return;
++    }
++
++    nsCOMPtr<nsIAsyncShutdownBlocker> blocker = new ClientShutdownBlocker(ref);
++    nsresult rv =
++      phase->AddBlocker(blocker, NS_LITERAL_STRING(__FILE__), __LINE__,
++                        NS_LITERAL_STRING("ClientManagerService shutdown"));
++
++    if (NS_FAILED(rv)) {
++      ref->Resolve(true, __func__);
++      return;
++    }
++  });
++
++  MOZ_ALWAYS_SUCCEEDS(
++    SystemGroup::Dispatch(TaskCategory::Other, r.forget()));
++
++  return ref.forget();
++}
++
+ } // anonymous namespace
+ 
+ ClientManagerService::ClientManagerService()
++  : mShutdown(false)
+ {
+   AssertIsOnBackgroundThread();
++
++  // While the ClientManagerService will be gracefully terminated as windows
++  // and workers are naturally killed, this can cause us to do extra work
++  // relatively late in the shutdown process.  To avoid this we eagerly begin
++  // shutdown at the first sign it has begun.  Since we handle normal shutdown
++  // gracefully we don't really need to block anything here.  We just begin
++  // destroying our IPC actors immediately.
++  OnShutdown()->Then(GetCurrentThreadSerialEventTarget(), __func__,
++    [] () {
++      RefPtr<ClientManagerService> svc = ClientManagerService::GetInstance();
++      if (svc) {
++        svc->Shutdown();
++      }
++    });
+ }
+ 
+ ClientManagerService::~ClientManagerService()
+ {
+   AssertIsOnBackgroundThread();
+   MOZ_DIAGNOSTIC_ASSERT(mSourceTable.Count() == 0);
+   MOZ_DIAGNOSTIC_ASSERT(mManagerList.IsEmpty());
+ 
+   MOZ_DIAGNOSTIC_ASSERT(sClientManagerServiceInstance == this);
+   sClientManagerServiceInstance = nullptr;
+ }
+ 
++void
++ClientManagerService::Shutdown()
++{
++  AssertIsOnBackgroundThread();
++
++  // If many ClientManagerService are created and destroyed quickly we can
++  // in theory get more than one shutdown listener calling us.
++  if (mShutdown) {
++    return;
++  }
++  mShutdown = true;
++
++  // Begin destroying our various manager actors which will in turn destroy
++  // all source, handle, and operation actors.
++  AutoTArray<ClientManagerParent*, 16> list(mManagerList);
++  for (auto actor : list) {
++    Unused << PClientManagerParent::Send__delete__(actor);
++  }
++}
++
+ // static
+ already_AddRefed<ClientManagerService>
+ ClientManagerService::GetOrCreateInstance()
+ {
+   AssertIsOnBackgroundThread();
+ 
+   if (!sClientManagerServiceInstance) {
+     sClientManagerServiceInstance = new ClientManagerService();
+   }
+ 
+   RefPtr<ClientManagerService> ref(sClientManagerServiceInstance);
+   return ref.forget();
+ }
+ 
++// static
++already_AddRefed<ClientManagerService>
++ClientManagerService::GetInstance()
++{
++  AssertIsOnBackgroundThread();
++
++  if (!sClientManagerServiceInstance) {
++    return nullptr;
++  }
++
++  RefPtr<ClientManagerService> ref(sClientManagerServiceInstance);
++  return ref.forget();
++}
++
+ bool
+ ClientManagerService::AddSource(ClientSourceParent* aSource)
+ {
+   AssertIsOnBackgroundThread();
+   MOZ_ASSERT(aSource);
+   auto entry = mSourceTable.LookupForAdd(aSource->Info().Id());
+   // Do not permit overwriting an existing ClientSource with the same
+   // UUID.  This would allow a spoofed ClientParentSource actor to
+@@ -136,16 +268,21 @@ ClientManagerService::FindSource(const n
+ 
+ void
+ ClientManagerService::AddManager(ClientManagerParent* aManager)
+ {
+   AssertIsOnBackgroundThread();
+   MOZ_DIAGNOSTIC_ASSERT(aManager);
+   MOZ_ASSERT(!mManagerList.Contains(aManager));
+   mManagerList.AppendElement(aManager);
++
++  // If shutdown has already begun then immediately destroy the actor.
++  if (mShutdown) {
++    Unused << PClientManagerParent::Send__delete__(aManager);
++  }
+ }
+ 
+ void
+ ClientManagerService::RemoveManager(ClientManagerParent* aManager)
+ {
+   AssertIsOnBackgroundThread();
+   MOZ_DIAGNOSTIC_ASSERT(aManager);
+   DebugOnly<bool> removed = mManagerList.RemoveElement(aManager);
+diff --git a/dom/clients/manager/ClientManagerService.h b/dom/clients/manager/ClientManagerService.h
+--- a/dom/clients/manager/ClientManagerService.h
++++ b/dom/clients/manager/ClientManagerService.h
+@@ -2,16 +2,17 @@
+ /* vim: set ts=8 sts=2 et sw=2 tw=80: */
+ /* This Source Code Form is subject to the terms of the Mozilla Public
+  * License, v. 2.0. If a copy of the MPL was not distributed with this
+  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+ #ifndef _mozilla_dom_ClientManagerService_h
+ #define _mozilla_dom_ClientManagerService_h
+ 
+ #include "mozilla/ipc/PBackgroundSharedTypes.h"
++#include "mozilla/MozPromise.h"
+ #include "nsDataHashtable.h"
+ 
+ namespace mozilla {
+ 
+ namespace dom {
+ 
+ class ClientManagerParent;
+ class ClientSourceParent;
+@@ -22,23 +23,32 @@ class ClientSourceParent;
+ class ClientManagerService final
+ {
+   // Store the ClientSourceParent objects in a hash table.  We want to
+   // optimize for insertion, removal, and lookup by UUID.
+   nsDataHashtable<nsIDHashKey, ClientSourceParent*> mSourceTable;
+ 
+   nsTArray<ClientManagerParent*> mManagerList;
+ 
++  bool mShutdown;
++
+   ClientManagerService();
+   ~ClientManagerService();
+ 
++  void
++  Shutdown();
++
+ public:
+   static already_AddRefed<ClientManagerService>
+   GetOrCreateInstance();
+ 
++  // Returns nullptr if the service is not already created.
++  static already_AddRefed<ClientManagerService>
++  GetInstance();
++
+   bool
+   AddSource(ClientSourceParent* aSource);
+ 
+   bool
+   RemoveSource(ClientSourceParent* aSource);
+ 
+   ClientSourceParent*
+   FindSource(const nsID& aID,

+ 21 - 0
frg/mozilla-release/work-js/1420594-3-59a1.patch

@@ -0,0 +1,21 @@
+# HG changeset patch
+# User Ben Kelly <ben@wanderview.com>
+# Date 1512399092 18000
+# Node ID b0f2a1cfbd45b53035992c5df3508b708e72eea9
+# Parent  e1061fd00b7edc837bec0026c06e95baf730ffc4
+Bug 1420594 P3 Disable test_ext_contentScripts_register.js on android since it fails without the shutdown delay bug. r=baku
+
+diff --git a/toolkit/components/extensions/test/xpcshell/xpcshell-content.ini b/toolkit/components/extensions/test/xpcshell/xpcshell-content.ini
+--- a/toolkit/components/extensions/test/xpcshell/xpcshell-content.ini
++++ b/toolkit/components/extensions/test/xpcshell/xpcshell-content.ini
+@@ -3,9 +3,10 @@ skip-if = os == "android" || (os == "win
+ [test_ext_i18n_css.js]
+ [test_ext_contentscript.js]
+ [test_ext_contentscript_scriptCreated.js]
+ skip-if = debug # Bug 1407501
+ [test_ext_contentscript_triggeringPrincipal.js]
+ skip-if = os == "android" && debug
+ [test_ext_contentscript_xrays.js]
+ [test_ext_contentScripts_register.js]
++skip-if = os == "android"
+ [test_ext_adoption_with_xrays.js]

+ 0 - 0
frg/mozilla-release/work-js/mozilla-central-push_419652.patch → frg/mozilla-release/work-js/1420894-62a1.patch


+ 407 - 0
frg/mozilla-release/work-js/1421737-1-60a1.patch

@@ -0,0 +1,407 @@
+# HG changeset patch
+# User Johann Hofmann <jhofmann@mozilla.com>
+# Date 1518205627 -3600
+# Node ID 0a00a0ef8bfcb309fc1f67e94b33d248f82f061b
+# Parent  478a7e76f684e2786d4c8f75694504b7b8c4a202
+Bug 1421737 - Part 1 - Include cookies in SiteDataManager.jsm. r=Gijs
+
+MozReview-Commit-ID: HovCREaRbgL
+
+diff --git a/browser/components/preferences/SiteDataManager.jsm b/browser/components/preferences/SiteDataManager.jsm
+--- a/browser/components/preferences/SiteDataManager.jsm
++++ b/browser/components/preferences/SiteDataManager.jsm
+@@ -1,17 +1,15 @@
+ "use strict";
+ 
+ ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
+ ChromeUtils.import("resource://gre/modules/Services.jsm");
+ 
+ ChromeUtils.defineModuleGetter(this, "OfflineAppCacheHelper",
+                                "resource:///modules/offlineAppCache.jsm");
+-ChromeUtils.defineModuleGetter(this, "ContextualIdentityService",
+-                               "resource://gre/modules/ContextualIdentityService.jsm");
+ XPCOMUtils.defineLazyServiceGetter(this, "serviceWorkerManager",
+                                    "@mozilla.org/serviceworkers/manager;1",
+                                    "nsIServiceWorkerManager");
+ 
+ var EXPORTED_SYMBOLS = [
+   "SiteDataManager"
+ ];
+ 
+@@ -43,21 +41,58 @@ var SiteDataManager = {
+   _getCacheSizePromise: null,
+ 
+   _getQuotaUsagePromise: null,
+ 
+   _quotaUsageRequest: null,
+ 
+   async updateSites() {
+     Services.obs.notifyObservers(null, "sitedatamanager:updating-sites");
++    // Clear old data and requests first
++    this._sites.clear();
++    this._getAllCookies();
+     await this._getQuotaUsage();
+     this._updateAppCache();
+     Services.obs.notifyObservers(null, "sitedatamanager:sites-updated");
+   },
+ 
++  _getBaseDomainFromHost(host) {
++    let result = host;
++    try {
++      result = Services.eTLD.getBaseDomainFromHost(host);
++    } catch (e) {
++      if (e.result == Cr.NS_ERROR_HOST_IS_IP_ADDRESS ||
++          e.result == Cr.NS_ERROR_INSUFFICIENT_DOMAIN_LEVELS) {
++        // For these 2 expected errors, just take the host as the result.
++        // - NS_ERROR_HOST_IS_IP_ADDRESS: the host is in ipv4/ipv6.
++        // - NS_ERROR_INSUFFICIENT_DOMAIN_LEVELS: not enough domain parts to extract.
++        result = host;
++      } else {
++        throw e;
++      }
++    }
++    return result;
++  },
++
++  _getOrInsertSite(host) {
++    let site = this._sites.get(host);
++    if (!site) {
++      site = {
++        baseDomain: this._getBaseDomainFromHost(host),
++        cookies: [],
++        persisted: false,
++        quotaUsage: 0,
++        principals: [],
++        appCacheList: [],
++      };
++      this._sites.set(host, site);
++    }
++    return site;
++  },
++
+   /**
+    * Retrieves the amount of space currently used by disk cache.
+    *
+    * You can use DownloadUtils.convertByteUnits to convert this to
+    * a user-understandable size/unit combination.
+    *
+    * @returns a Promise that resolves with the cache size on disk in bytes.
+    */
+@@ -89,66 +124,64 @@ var SiteDataManager = {
+         this._getCacheSizeObserver = null;
+       }
+     });
+ 
+     return this._getCacheSizePromise;
+   },
+ 
+   _getQuotaUsage() {
+-    // Clear old data and requests first
+-    this._sites.clear();
+     this._cancelGetQuotaUsage();
+     this._getQuotaUsagePromise = new Promise(resolve => {
+       let onUsageResult = request => {
+         if (request.resultCode == Cr.NS_OK) {
+           let items = request.result;
+           for (let item of items) {
+             if (!item.persisted && item.usage <= 0) {
+               // An non-persistent-storage site with 0 byte quota usage is redundant for us so skip it.
+               continue;
+             }
+             let principal =
+               Services.scriptSecurityManager.createCodebasePrincipalFromOrigin(item.origin);
+             let uri = principal.URI;
+             if (uri.scheme == "http" || uri.scheme == "https") {
+-              let site = this._sites.get(uri.host);
+-              if (!site) {
+-                site = {
+-                  persisted: false,
+-                  quotaUsage: 0,
+-                  principals: [],
+-                  appCacheList: [],
+-                };
+-              }
++              let site = this._getOrInsertSite(uri.host);
+               // Assume 3 sites:
+               //   - Site A (not persisted): https://www.foo.com
+               //   - Site B (not persisted): https://www.foo.com^userContextId=2
+               //   - Site C (persisted):     https://www.foo.com:1234
+               // Although only C is persisted, grouping by host, as a result,
+               // we still mark as persisted here under this host group.
+               if (item.persisted) {
+                 site.persisted = true;
+               }
+               site.principals.push(principal);
+               site.quotaUsage += item.usage;
+-              this._sites.set(uri.host, site);
+             }
+           }
+         }
+         resolve();
+       };
+       // XXX: The work of integrating localStorage into Quota Manager is in progress.
+       //      After the bug 742822 and 1286798 landed, localStorage usage will be included.
+       //      So currently only get indexedDB usage.
+       this._quotaUsageRequest = this._qms.getUsage(onUsageResult);
+     });
+     return this._getQuotaUsagePromise;
+   },
+ 
++  _getAllCookies() {
++    let cookiesEnum = Services.cookies.enumerator;
++    while (cookiesEnum.hasMoreElements()) {
++      let cookie = cookiesEnum.getNext().QueryInterface(Ci.nsICookie2);
++      let site = this._getOrInsertSite(cookie.rawHost);
++      site.cookies.push(cookie);
++    }
++  },
++
+   _cancelGetQuotaUsage() {
+     if (this._quotaUsageRequest) {
+       this._quotaUsageRequest.cancel();
+       this._quotaUsageRequest = null;
+     }
+   },
+ 
+   _updateAppCache() {
+@@ -156,26 +189,18 @@ var SiteDataManager = {
+     for (let group of groups) {
+       let cache = this._appCache.getActiveCache(group);
+       if (cache.usage <= 0) {
+         // A site with 0 byte appcache usage is redundant for us so skip it.
+         continue;
+       }
+       let principal = Services.scriptSecurityManager.createCodebasePrincipalFromOrigin(group);
+       let uri = principal.URI;
+-      let site = this._sites.get(uri.host);
+-      if (!site) {
+-        site = {
+-          persisted: false,
+-          quotaUsage: 0,
+-          principals: [ principal ],
+-          appCacheList: [],
+-        };
+-        this._sites.set(uri.host, site);
+-      } else if (!site.principals.some(p => p.origin == principal.origin)) {
++      let site = this._getOrInsertSite(uri.host);
++      if (!site.principals.some(p => p.origin == principal.origin)) {
+         site.principals.push(principal);
+       }
+       site.appCacheList.push(cache);
+     }
+   },
+ 
+   getTotalUsage() {
+     return this._getQuotaUsagePromise.then(() => {
+@@ -194,16 +219,18 @@ var SiteDataManager = {
+     return this._getQuotaUsagePromise.then(() => {
+       let list = [];
+       for (let [host, site] of this._sites) {
+         let usage = site.quotaUsage;
+         for (let cache of site.appCacheList) {
+           usage += cache.usage;
+         }
+         list.push({
++          baseDomain: site.baseDomain,
++          cookies: site.cookies,
+           host,
+           usage,
+           persisted: site.persisted
+         });
+       }
+       return list;
+     });
+   },
+@@ -249,81 +276,72 @@ var SiteDataManager = {
+   },
+ 
+   _removeAppCache(site) {
+     for (let cache of site.appCacheList) {
+       cache.discard();
+     }
+   },
+ 
+-  _removeCookie(site) {
+-    for (let principal of site.principals) {
+-      // Although `getCookiesFromHost` can get cookies across hosts under the same base domain, OAs matter.
+-      // We still need OAs here.
+-      let e = Services.cookies.getCookiesFromHost(principal.URI.host, principal.originAttributes);
+-      while (e.hasMoreElements()) {
+-        let cookie = e.getNext();
+-        if (cookie instanceof Ci.nsICookie) {
+-          if (this.isPrivateCookie(cookie)) {
+-            continue;
+-          }
+-          Services.cookies.remove(
+-            cookie.host, cookie.name, cookie.path, false, cookie.originAttributes);
+-        }
+-      }
+-
+-      Services.obs.notifyObservers(null, "browser:purge-domain-data", principal.URI.host);
++  _removeCookies(site) {
++    for (let cookie of site.cookies) {
++      Services.cookies.remove(
++        cookie.host, cookie.name, cookie.path, false, cookie.originAttributes);
+     }
++    site.cookies = [];
+   },
+ 
+   _unregisterServiceWorker(serviceWorker) {
+     return new Promise(resolve => {
+       let unregisterCallback = {
+         unregisterSucceeded: resolve,
+         unregisterFailed: resolve, // We don't care about failures.
+         QueryInterface: XPCOMUtils.generateQI([Ci.nsIServiceWorkerUnregisterCallback])
+       };
+       serviceWorkerManager.propagateUnregister(serviceWorker.principal, unregisterCallback, serviceWorker.scope);
+     });
+   },
+ 
+   _removeServiceWorkersForSites(sites) {
+     let promises = [];
+-    let targetHosts = sites.map(s => s.principals[0].URI.host);
+     let serviceWorkers = serviceWorkerManager.getAllRegistrations();
+     for (let i = 0; i < serviceWorkers.length; i++) {
+       let sw = serviceWorkers.queryElementAt(i, Ci.nsIServiceWorkerRegistrationInfo);
+       // Sites are grouped and removed by host so we unregister service workers by the same host as well
+-      if (targetHosts.includes(sw.principal.URI.host)) {
++      if (sites.has(sw.principal.URI.host)) {
+         promises.push(this._unregisterServiceWorker(sw));
+       }
+     }
+     return Promise.all(promises);
+   },
+ 
+   remove(hosts) {
+     let unknownHost = "";
+-    let targetSites = [];
++    let targetSites = new Map();
+     for (let host of hosts) {
+       let site = this._sites.get(host);
+       if (site) {
+         this._removePermission(site);
+         this._removeAppCache(site);
+-        this._removeCookie(site);
+-        targetSites.push(site);
++        this._removeCookies(site);
++        Services.obs.notifyObservers(null, "browser:purge-domain-data", host);
++        targetSites.set(host, site);
+       } else {
+         unknownHost = host;
+         break;
+       }
+     }
+ 
+-    if (targetSites.length > 0) {
++    if (targetSites.size > 0) {
+       this._removeServiceWorkersForSites(targetSites)
+           .then(() => {
+-            let promises = targetSites.map(s => this._removeQuotaUsage(s));
++            let promises = [];
++            for (let [, site] of targetSites) {
++              promises.push(this._removeQuotaUsage(site));
++            }
+             return Promise.all(promises);
+           })
+           .then(() => this.updateSites());
+     }
+     if (unknownHost) {
+       throw `SiteDataManager: removing unknown site of ${unknownHost}`;
+     }
+   },
+@@ -396,23 +414,18 @@ var SiteDataManager = {
+     //   1. User goes to the about:preferences Site Data section.
+     //   2. With the about:preferences opened, user visits another website.
+     //   3. The website saves to quota usage, like indexedDB.
+     //   4. User goes back to the Site Data section and commands to clear all site data.
+     // For this case, we should refresh the site list so not to miss the website in the step 3.
+     // We don't do "Clear All" on the quota manager like the cookie, appcache, http cache above
+     // because that would clear browser data as well too,
+     // see https://bugzilla.mozilla.org/show_bug.cgi?id=1312361#c9
++    this._sites.clear();
+     await this._getQuotaUsage();
+     promises = [];
+     for (let site of this._sites.values()) {
+       this._removePermission(site);
+       promises.push(this._removeQuotaUsage(site));
+     }
+     return Promise.all(promises).then(() => this.updateSites());
+   },
+-
+-  isPrivateCookie(cookie) {
+-    let { userContextId } = cookie.originAttributes;
+-    // A private cookie is when its userContextId points to a private identity.
+-    return userContextId && !ContextualIdentityService.getPublicIdentityFromId(userContextId);
+-  }
+ };
+diff --git a/browser/components/preferences/cookies.js b/browser/components/preferences/cookies.js
+--- a/browser/components/preferences/cookies.js
++++ b/browser/components/preferences/cookies.js
+@@ -5,18 +5,16 @@
+ 
+ const nsICookie = Ci.nsICookie;
+ 
+ ChromeUtils.import("resource://gre/modules/AppConstants.jsm");
+ ChromeUtils.import("resource://gre/modules/PluralForm.jsm");
+ ChromeUtils.import("resource://gre/modules/Services.jsm")
+ ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
+ 
+-ChromeUtils.defineModuleGetter(this, "SiteDataManager",
+-                               "resource:///modules/SiteDataManager.jsm");
+ ChromeUtils.defineModuleGetter(this, "ContextualIdentityService",
+                                "resource://gre/modules/ContextualIdentityService.jsm");
+ 
+ var gCookiesWindow = {
+   _hosts: {},
+   _hostOrder: [],
+   _tree: null,
+   _bundle: null,
+@@ -72,22 +70,28 @@ var gCookiesWindow = {
+   _cookieEquals(aCookieA, aCookieB, aStrippedHost) {
+     return aCookieA.rawHost == aStrippedHost &&
+            aCookieA.name == aCookieB.name &&
+            aCookieA.path == aCookieB.path &&
+            ChromeUtils.isOriginAttributesEqual(aCookieA.originAttributes,
+                                                aCookieB.originAttributes);
+   },
+ 
++  _isPrivateCookie(cookie) {
++    let { userContextId } = cookie.originAttributes;
++    // A private cookie is when its userContextId points to a private identity.
++    return userContextId && !ContextualIdentityService.getPublicIdentityFromId(userContextId);
++  },
++
+   observe(aCookie, aTopic, aData) {
+     if (aTopic != "cookie-changed")
+       return;
+ 
+     if (aCookie instanceof Ci.nsICookie) {
+-      if (SiteDataManager.isPrivateCookie(aCookie)) {
++      if (this._isPrivateCookie(aCookie)) {
+         return;
+       }
+ 
+       var strippedHost = this._makeStrippedHost(aCookie.host);
+       if (aData == "changed")
+         this._handleCookieChanged(aCookie, strippedHost);
+       else if (aData == "added")
+         this._handleCookieAdded(aCookie, strippedHost);
+@@ -471,17 +475,17 @@ var gCookiesWindow = {
+   _loadCookies() {
+     var e = Services.cookies.enumerator;
+     var hostCount = { value: 0 };
+     this._hosts = {};
+     this._hostOrder = [];
+     while (e.hasMoreElements()) {
+       var cookie = e.getNext();
+       if (cookie && cookie instanceof Ci.nsICookie) {
+-        if (SiteDataManager.isPrivateCookie(cookie)) {
++        if (this._isPrivateCookie(cookie)) {
+           continue;
+         }
+ 
+         var strippedHost = this._makeStrippedHost(cookie.host);
+         this._addCookie(strippedHost, cookie, hostCount);
+       } else
+         break;
+     }

+ 339 - 0
frg/mozilla-release/work-js/1421737-2-60a1.patch

@@ -0,0 +1,339 @@
+# HG changeset patch
+# User Johann Hofmann <jhofmann@mozilla.com>
+# Date 1518206164 -3600
+# Node ID 4a9b1b488c9426d5a9f07ca143e22486f67e2c77
+# Parent  27746596dda519e378120ad3c45062f8b120e5b4
+Bug 1421737 - Part 2 - Convert the siteListItem XBL binding to plain JS and add a cookies row. r=Gijs
+
+This commit primarily intends to add cookies to the site data manager,
+but while touching this code I transformed the siteListItem XBL binding to plain JS.
+
+This also removes the #SiteDataRemoveSelectedDialog binding rule, because it is
+unnecessary, <dialog> elements already have this binding.
+
+MozReview-Commit-ID: EpTd2E0vPN9
+
+diff --git a/browser/components/preferences/jar.mn b/browser/components/preferences/jar.mn
+--- a/browser/components/preferences/jar.mn
++++ b/browser/components/preferences/jar.mn
+@@ -30,14 +30,12 @@ browser.jar:
+     content/browser/preferences/containers.js
+     content/browser/preferences/permissions.js
+     content/browser/preferences/sanitize.xul
+     content/browser/preferences/sanitize.js
+     content/browser/preferences/selectBookmark.xul
+     content/browser/preferences/selectBookmark.js
+     content/browser/preferences/siteDataSettings.xul
+     content/browser/preferences/siteDataSettings.js
+-    content/browser/preferences/siteDataSettings.css
+ *   content/browser/preferences/siteDataRemoveSelected.xul
+     content/browser/preferences/siteDataRemoveSelected.js
+-    content/browser/preferences/siteListItem.xml
+     content/browser/preferences/translation.xul
+     content/browser/preferences/translation.js
+diff --git a/browser/components/preferences/siteDataSettings.css b/browser/components/preferences/siteDataSettings.css
+deleted file mode 100644
+--- a/browser/components/preferences/siteDataSettings.css
++++ /dev/null
+@@ -1,11 +0,0 @@
+-/* This Source Code Form is subject to the terms of the Mozilla Public
+- * License, v. 2.0. If a copy of the MPL was not distributed with this
+- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+-
+-#sitesList > richlistitem {
+-  -moz-binding: url("chrome://browser/content/preferences/siteListItem.xml#siteListItem");
+-}
+-
+-#SiteDataRemoveSelectedDialog {
+-  -moz-binding: url("chrome://global/content/bindings/dialog.xml#dialog");
+-}
+diff --git a/browser/components/preferences/siteDataSettings.js b/browser/components/preferences/siteDataSettings.js
+--- a/browser/components/preferences/siteDataSettings.js
++++ b/browser/components/preferences/siteDataSettings.js
+@@ -11,26 +11,71 @@ ChromeUtils.defineModuleGetter(this, "Do
+                                "resource://gre/modules/DownloadUtils.jsm");
+ 
+ "use strict";
+ 
+ let gSiteDataSettings = {
+ 
+   // Array of metadata of sites. Each array element is object holding:
+   // - uri: uri of site; instance of nsIURI
++  // - baseDomain: base domain of the site
++  // - cookies: array of cookies of that site
+   // - status: persistent-storage permission status
+   // - usage: disk usage which site uses
+   // - userAction: "remove" or "update-permission"; the action user wants to take.
+-  //               If not specified, means no action to take
+   _sites: null,
+ 
+   _list: null,
+   _searchBox: null,
+   _prefStrBundle: null,
+ 
++  _createSiteListItem(site) {
++    let item = document.createElement("richlistitem");
++    item.setAttribute("host", site.host);
++    let container = document.createElement("hbox");
++
++    // Creates a new column item with the specified relative width.
++    function addColumnItem(value, flexWidth) {
++      let box = document.createElement("hbox");
++      box.className = "item-box";
++      box.setAttribute("flex", flexWidth);
++      let label = document.createElement("label");
++      label.setAttribute("crop", "end");
++      if (value) {
++        box.setAttribute("tooltiptext", value);
++        label.setAttribute("value", value);
++      }
++      box.appendChild(label);
++      container.appendChild(box);
++    }
++
++    // Add "Host" column.
++    addColumnItem(site.host, "4");
++
++    // Add "Status" column
++    addColumnItem(site.persisted ?
++      this._prefStrBundle.getString("persistent") : null, "2");
++
++    // Add "Cookies" column.
++    addColumnItem(site.cookies.length, "1");
++
++    // Add "Storage" column
++    if (site.usage > 0) {
++      let size = DownloadUtils.convertByteUnits(site.usage);
++      let str = this._prefStrBundle.getFormattedString("siteUsage", size);
++      addColumnItem(str, "1");
++    } else {
++      // Pass null to avoid showing "0KB" when there is no site data stored.
++      addColumnItem(null, "1");
++    }
++
++    item.appendChild(container);
++    return item;
++  },
++
+   init() {
+     function setEventListener(id, eventType, callback) {
+       document.getElementById(id)
+               .addEventListener(eventType, callback.bind(gSiteDataSettings));
+     }
+ 
+     this._list = document.getElementById("sitesList");
+     this._searchBox = document.getElementById("searchBox");
+@@ -45,16 +90,17 @@ let gSiteDataSettings = {
+ 
+     let brandShortName = document.getElementById("bundle_brand").getString("brandShortName");
+     let settingsDescription = document.getElementById("settingsDescription");
+     settingsDescription.textContent = this._prefStrBundle.getFormattedString("siteDataSettings2.description", [brandShortName]);
+ 
+     setEventListener("sitesList", "select", this.onSelect);
+     setEventListener("hostCol", "click", this.onClickTreeCol);
+     setEventListener("usageCol", "click", this.onClickTreeCol);
++    setEventListener("cookiesCol", "click", this.onClickTreeCol);
+     setEventListener("statusCol", "click", this.onClickTreeCol);
+     setEventListener("cancel", "command", this.close);
+     setEventListener("save", "command", this.saveChanges);
+     setEventListener("searchBox", "command", this.onCommandSearch);
+     setEventListener("removeAll", "command", this.onClickRemoveAll);
+     setEventListener("removeSelected", "command", this.onClickRemoveSelected);
+   },
+ 
+@@ -86,33 +132,37 @@ let gSiteDataSettings = {
+       // Sort on the current column, flip the sorting direction
+       sortDirection = sortDirection === "ascending" ? "descending" : "ascending";
+     }
+ 
+     let sortFunc = null;
+     switch (col.id) {
+       case "hostCol":
+         sortFunc = (a, b) => {
+-          let aHost = a.host.toLowerCase();
+-          let bHost = b.host.toLowerCase();
++          let aHost = a.baseDomain.toLowerCase();
++          let bHost = b.baseDomain.toLowerCase();
+           return aHost.localeCompare(bHost);
+         };
+         break;
+ 
+       case "statusCol":
+         sortFunc = (a, b) => {
+           if (a.persisted && !b.persisted) {
+             return 1;
+           } else if (!a.persisted && b.persisted) {
+             return -1;
+           }
+           return 0;
+         };
+         break;
+ 
++      case "cookiesCol":
++        sortFunc = (a, b) => a.cookies.length - b.cookies.length;
++        break;
++
+       case "usageCol":
+         sortFunc = (a, b) => a.usage - b.usage;
+         break;
+     }
+     if (sortDirection === "descending") {
+       sites.sort((a, b) => sortFunc(b, a));
+     } else {
+       sites.sort(sortFunc);
+@@ -144,23 +194,17 @@ let gSiteDataSettings = {
+       if (keyword && !host.includes(keyword)) {
+         continue;
+       }
+ 
+       if (site.userAction === "remove") {
+         continue;
+       }
+ 
+-      let size = DownloadUtils.convertByteUnits(site.usage);
+-      let item = document.createElement("richlistitem");
+-      item.setAttribute("host", host);
+-      item.setAttribute("usage", this._prefStrBundle.getFormattedString("siteUsage", size));
+-      if (site.persisted) {
+-        item.setAttribute("status", this._prefStrBundle.getString("persistent"));
+-      }
++      let item = this._createSiteListItem(site);
+       this._list.appendChild(item);
+     }
+     this._updateButtonsState();
+   },
+ 
+   _removeSiteItems(items) {
+     for (let i = items.length - 1; i >= 0; --i) {
+       let item = items[i];
+diff --git a/browser/components/preferences/siteDataSettings.xul b/browser/components/preferences/siteDataSettings.xul
+--- a/browser/components/preferences/siteDataSettings.xul
++++ b/browser/components/preferences/siteDataSettings.xul
+@@ -7,17 +7,17 @@
+ <?xml-stylesheet href="chrome://global/skin/" type="text/css"?>
+ <?xml-stylesheet href="chrome://browser/skin/preferences/preferences.css" type="text/css"?>
+ <?xml-stylesheet href="chrome://browser/content/preferences/siteDataSettings.css" type="text/css"?>
+ <?xml-stylesheet href="chrome://browser/skin/preferences/in-content-new/siteDataSettings.css" type="text/css"?>
+ 
+ <!DOCTYPE dialog SYSTEM "chrome://browser/locale/preferences/siteDataSettings.dtd" >
+ 
+ <window id="SiteDataSettingsDialog" windowtype="Browser:SiteDataSettings"
+-        class="windowDialog" title="&window.title;"
++        class="windowDialog" title="&window1.title;"
+         xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+         style="width: 45em;"
+         onload="gSiteDataSettings.init();"
+         onkeypress="gSiteDataSettings.onKeyPress(event);"
+         persist="screenX screenY width height">
+ 
+   <script src="chrome://browser/content/preferences/siteDataSettings.js"/>
+ 
+@@ -34,16 +34,17 @@
+         placeholder="&searchTextboxPlaceHolder;" accesskey="&searchTextboxPlaceHolder.accesskey;"/>
+     </hbox>
+     <separator class="thin"/>
+ 
+     <richlistbox id="sitesList" orient="vertical" flex="1">
+       <listheader>
+         <treecol flex="4" width="50" label="&hostCol.label;" id="hostCol"/>
+         <treecol flex="2" width="50" label="&statusCol.label;" id="statusCol"/>
++        <treecol flex="1" width="50" label="&cookiesCol.label;" id="cookiesCol"/>
+         <!-- Sorted by usage so the user can quickly see which sites use the most data. -->
+         <treecol flex="1" width="50" label="&usageCol.label;" id="usageCol" data-isCurrentSortCol="true"/>
+       </listheader>
+     </richlistbox>
+   </vbox>
+ 
+   <hbox align="start">
+     <button id="removeSelected" label="&removeSelected.label;" accesskey="&removeSelected.accesskey;"/>
+diff --git a/browser/components/preferences/siteListItem.xml b/browser/components/preferences/siteListItem.xml
+deleted file mode 100644
+--- a/browser/components/preferences/siteListItem.xml
++++ /dev/null
+@@ -1,36 +0,0 @@
+-<?xml version="1.0"?>
+-
+-<!-- This Source Code Form is subject to the terms of the Mozilla Public
+-   - License, v. 2.0. If a copy of the MPL was not distributed with this
+-   - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+-<!-- import-globals-from siteDataSettings.js -->
+-
+-<!DOCTYPE overlay [
+-  <!ENTITY % brandDTD SYSTEM "chrome://branding/locale/brand.dtd">
+-  <!ENTITY % applicationsDTD SYSTEM "chrome://browser/locale/preferences/siteDataSettings.dtd">
+-  %brandDTD;
+-  %applicationsDTD;
+-]>
+-
+-<bindings id="siteListItemBindings"
+-          xmlns="http://www.mozilla.org/xbl"
+-          xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+-          xmlns:xbl="http://www.mozilla.org/xbl">
+-
+-  <binding id="siteListItem" extends="chrome://global/content/bindings/richlistbox.xml#richlistitem">
+-    <content>
+-      <xul:hbox flex="1">
+-        <xul:hbox flex="4" width="50" class="item-box" align="center" xbl:inherits="tooltiptext=host">
+-          <xul:label flex="1" crop="end" xbl:inherits="value=host"/>
+-        </xul:hbox>
+-        <xul:hbox flex="2" width="50" class="item-box" align="center" xbl:inherits="tooltiptext=status">
+-          <xul:label flex="1" crop="end" xbl:inherits="value=status"/>
+-        </xul:hbox>
+-        <xul:hbox flex="1" width="50" class="item-box" align="center" xbl:inherits="tooltiptext=usage">
+-          <xul:label flex="1" crop="end" xbl:inherits="value=usage"/>
+-        </xul:hbox>
+-      </xul:hbox>
+-    </content>
+-  </binding>
+-
+-</bindings>
+diff --git a/browser/locales/en-US/chrome/browser/preferences/siteDataSettings.dtd b/browser/locales/en-US/chrome/browser/preferences/siteDataSettings.dtd
+--- a/browser/locales/en-US/chrome/browser/preferences/siteDataSettings.dtd
++++ b/browser/locales/en-US/chrome/browser/preferences/siteDataSettings.dtd
+@@ -1,15 +1,16 @@
+ <!-- This Source Code Form is subject to the terms of the Mozilla Public
+    - License, v. 2.0. If a copy of the MPL was not distributed with this
+    - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+ 
+-<!ENTITY     window.title                  "Settings - Site Data">
++<!ENTITY     window1.title                 "Settings - Cookies and Site Data">
+ <!ENTITY     hostCol.label                 "Site">
+ <!ENTITY     statusCol.label               "Status">
++<!ENTITY     cookiesCol.label              "Cookies">
+ <!ENTITY     usageCol.label                "Storage">
+ <!ENTITY     searchTextboxPlaceHolder             "Search websites">
+ <!ENTITY     searchTextboxPlaceHolder.accesskey   "S">
+ <!ENTITY     removeSelected.label          "Remove Selected">
+ <!ENTITY     removeSelected.accesskey      "r">
+ <!ENTITY     save.label                    "Save Changes">
+ <!ENTITY     save.accesskey                "a">
+ <!ENTITY     cancel.label                  "Cancel">
+diff --git a/browser/themes/shared/incontentprefs/siteDataSettings.css b/browser/themes/shared/incontentprefs/siteDataSettings.css
+--- a/browser/themes/shared/incontentprefs/siteDataSettings.css
++++ b/browser/themes/shared/incontentprefs/siteDataSettings.css
+@@ -4,18 +4,25 @@
+ 
+ /**
+  * Site Data - Settings dialog
+  */
+ #sitesList {
+   min-height: 20em;
+ }
+ 
++#sitesList > richlistitem > hbox,
++.item-box > label {
++  -moz-box-flex: 1;
++}
++
+ .item-box {
+   padding: 5px 8px;
++  -moz-box-align: center;
++  width: 50px;
+ }
+ 
+ /**
+  * Confirmation dialog of removing sites selected
+  */
+ #SiteDataRemoveSelectedDialog {
+   padding: 16px;
+ }

+ 440 - 0
frg/mozilla-release/work-js/1421737-3-60a1.patch

@@ -0,0 +1,440 @@
+# HG changeset patch
+# User Johann Hofmann <jhofmann@mozilla.com>
+# Date 1517848974 -3600
+# Node ID d9aa01a177517a7b0bdf1fb18b3fa8b634b56584
+# Parent  20494a81a1cb7cf6c02298c8cc775b0aaff3fb89
+Bug 1421737 - Part 3 - Simplify the "remove selected sites" dialog in site data management. r=Gijs
+
+We are no longer implicitly deleting cookies when removing site data because
+cookies are now listed as part of the site data manager. We're also no longer
+deleting cookies based on the base domain, which makes most of the UI in the
+removal dialog unnecessary. Instead of a tree box with sub domains we're now
+showing a simple listbox domains to be deleted.
+
+MozReview-Commit-ID: GWv5QVxEiiy
+
+diff --git a/browser/components/preferences/siteDataRemoveSelected.js b/browser/components/preferences/siteDataRemoveSelected.js
+--- a/browser/components/preferences/siteDataRemoveSelected.js
++++ b/browser/components/preferences/siteDataRemoveSelected.js
+@@ -1,200 +1,46 @@
+ /* -*- indent-tabs-mode: nil; js-indent-level: 4 -*- */
+ /* This Source Code Form is subject to the terms of the Mozilla Public
+  * License, v. 2.0. If a copy of the MPL was not distributed with this
+  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+-ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
+-ChromeUtils.import("resource://gre/modules/Services.jsm");
+ 
+ "use strict";
+ 
+ let gSiteDataRemoveSelected = {
+ 
+-  _tree: null,
+-
+   init() {
+     let bundlePreferences = document.getElementById("bundlePreferences");
+     let acceptBtn = document.getElementById("SiteDataRemoveSelectedDialog")
+                             .getButton("accept");
+     acceptBtn.label = bundlePreferences.getString("acceptRemove");
+ 
+-    // Organize items for the tree from the argument
+-    let hostsTable = window.arguments[0].hostsTable;
+-    let visibleItems = [];
+-    let itemsTable = new Map();
+-    for (let [ baseDomain, hosts ] of hostsTable) {
+-      // In the beginning, only display base domains in the topmost level.
+-      visibleItems.push({
+-        level: 0,
+-        opened: false,
+-        host: baseDomain
+-      });
+-      // Other hosts are in the second level.
+-      let items = hosts.map(host => {
+-        return { host, level: 1 };
+-      });
+-      items.sort(sortByHost);
+-      itemsTable.set(baseDomain, items);
+-    }
+-    visibleItems.sort(sortByHost);
+-    this._view.itemsTable = itemsTable;
+-    this._view.visibleItems = visibleItems;
+-    this._tree = document.getElementById("sitesTree");
+-    this._tree.view = this._view;
+-
+-    function sortByHost(a, b) {
+-      let aHost = a.host.toLowerCase();
+-      let bHost = b.host.toLowerCase();
+-      return aHost.localeCompare(bHost);
+-    }
++    let hosts = window.arguments[0].hosts;
++    hosts.sort();
++    let tree = document.getElementById("sitesTree");
++    this._view._hosts = hosts;
++    tree.view = this._view;
+   },
+ 
+   ondialogaccept() {
+     window.arguments[0].allowed = true;
+   },
+ 
+   ondialogcancel() {
+     window.arguments[0].allowed = false;
+   },
+ 
+   _view: {
+-    _selection: null,
+-
+-    itemsTable: null,
+-
+-    visibleItems: null,
++    _hosts: null,
+ 
+     get rowCount() {
+-      return this.visibleItems.length;
++      return this._hosts.length;
+     },
+ 
+     getCellText(index, column) {
+-      let item = this.visibleItems[index];
+-      return item ? item.host : "";
+-    },
+-
+-    isContainer(index) {
+-      let item = this.visibleItems[index];
+-      if (item && item.level === 0) {
+-        return true;
+-      }
+-      return false;
+-    },
+-
+-    isContainerEmpty() {
+-      return false;
+-    },
+-
+-    isContainerOpen(index) {
+-      let item = this.visibleItems[index];
+-      if (item && item.level === 0) {
+-        return item.opened;
+-      }
+-      return false;
++      return this._hosts[index];
+     },
+ 
+     getLevel(index) {
+-      let item = this.visibleItems[index];
+-      return item ? item.level : 0;
+-    },
+-
+-    hasNextSibling(index, afterIndex) {
+-      let item = this.visibleItems[index];
+-      if (item) {
+-        let thisLV = this.getLevel(index);
+-        for (let i = afterIndex + 1; i < this.rowCount; ++i) {
+-          let nextLV = this.getLevel(i);
+-          if (nextLV == thisLV) {
+-            return true;
+-          }
+-          if (nextLV < thisLV) {
+-            break;
+-          }
+-        }
+-      }
+-      return false;
+-    },
+-
+-    getParentIndex(index) {
+-      if (!this.isContainer(index)) {
+-        for (let i = index - 1; i >= 0; --i) {
+-          if (this.isContainer(i)) {
+-            return i;
+-          }
+-        }
+-      }
+-      return -1;
++      return 0;
+     },
+-
+-    toggleOpenState(index) {
+-      let item = this.visibleItems[index];
+-      if (!this.isContainer(index)) {
+-        return;
+-      }
+-
+-      if (item.opened) {
+-        item.opened = false;
+-
+-        let deleteCount = 0;
+-        for (let i = index + 1; i < this.visibleItems.length; ++i) {
+-          if (!this.isContainer(i)) {
+-            ++deleteCount;
+-          } else {
+-            break;
+-          }
+-        }
+-
+-        if (deleteCount) {
+-          this.visibleItems.splice(index + 1, deleteCount);
+-          this.treeBox.rowCountChanged(index + 1, -deleteCount);
+-        }
+-      } else {
+-        item.opened = true;
+-
+-        let childItems = this.itemsTable.get(item.host);
+-        for (let i = 0; i < childItems.length; ++i) {
+-          this.visibleItems.splice(index + i + 1, 0, childItems[i]);
+-        }
+-        this.treeBox.rowCountChanged(index + 1, childItems.length);
+-      }
+-      this.treeBox.invalidateRow(index);
+-    },
+-
+-    get selection() {
+-      return this._selection;
+-    },
+-    set selection(v) {
+-      this._selection = v;
+-      return v;
+-    },
+-    setTree(treeBox) {
+-      this.treeBox = treeBox;
+-    },
+-    isSeparator(index) {
+-      return false;
+-    },
+-    isSorted(index) {
+-      return false;
+-    },
+-    canDrop() {
+-      return false;
+-    },
+-    drop() {},
+-    getRowProperties() {},
+-    getCellProperties() {},
+-    getColumnProperties() {},
+-    hasPreviousSibling(index) {},
+-    getImageSrc() {},
+-    getProgressMode() {},
+-    getCellValue() {},
+-    cycleHeader() {},
+-    selectionChanged() {},
+-    cycleCell() {},
+-    isEditable() {},
+-    isSelectable() {},
+-    setCellValue() {},
+-    setCellText() {},
+-    performAction() {},
+-    performActionOnRow() {},
+-    performActionOnCell() {}
+-  }
++  },
+ };
+diff --git a/browser/components/preferences/siteDataRemoveSelected.xul b/browser/components/preferences/siteDataRemoveSelected.xul
+--- a/browser/components/preferences/siteDataRemoveSelected.xul
++++ b/browser/components/preferences/siteDataRemoveSelected.xul
+@@ -8,17 +8,17 @@
+ <?xml-stylesheet href="chrome://browser/content/preferences/siteDataSettings.css" type="text/css"?>
+ <?xml-stylesheet href="chrome://browser/skin/preferences/in-content-new/siteDataSettings.css" type="text/css"?>
+ 
+ <!DOCTYPE dialog SYSTEM "chrome://browser/locale/preferences/siteDataSettings.dtd" >
+ 
+ <dialog id="SiteDataRemoveSelectedDialog"
+         windowtype="Browser:SiteDataRemoveSelected"
+         width="500"
+-        title="&removingDialog.title;"
++        title="&removingDialog1.title;"
+         onload="gSiteDataRemoveSelected.init();"
+         ondialogaccept="gSiteDataRemoveSelected.ondialogaccept(); return true;"
+         ondialogcancel="gSiteDataRemoveSelected.ondialogcancel(); return true;"
+         xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+ 
+   <script src="chrome://browser/content/preferences/siteDataRemoveSelected.js"/>
+ 
+   <stringbundle id="bundlePreferences"
+@@ -30,26 +30,26 @@
+         <image class="question-icon"/>
+       </vbox>
+       <vbox flex="1">
+         <!-- Only show this label on OS X because of no dialog title -->
+         <label id="removing-label"
+ #ifndef XP_MACOSX
+                hidden="true"
+ #endif
+-        >&removingDialog.title;</label>
++        >&removingDialog1.title;</label>
+         <separator class="thin"/>
+-        <description id="removing-description">&removingSelected.description;</description>
++        <description id="removing-description">&removingSelected1.description;</description>
+       </vbox>
+     </hbox>
+ 
+     <separator />
+ 
+     <vbox flex="1">
+-      <label>&siteTree2.label;</label>
++      <label>&siteTree3.label;</label>
+       <separator class="thin"/>
+       <tree id="sitesTree" flex="1" seltype="single" hidecolumnpicker="true">
+         <treecols>
+           <treecol primary="true" flex="1" hideheader="true"/>
+         </treecols>
+         <treechildren />
+       </tree>
+     </vbox>
+diff --git a/browser/components/preferences/siteDataSettings.js b/browser/components/preferences/siteDataSettings.js
+--- a/browser/components/preferences/siteDataSettings.js
++++ b/browser/components/preferences/siteDataSettings.js
+@@ -213,80 +213,33 @@ let gSiteDataSettings = {
+       if (siteForHost) {
+         siteForHost.userAction = "remove";
+       }
+       item.remove();
+     }
+     this._updateButtonsState();
+   },
+ 
+-  _getBaseDomainFromHost(host) {
+-    let result = host;
+-    try {
+-      result = Services.eTLD.getBaseDomainFromHost(host);
+-    } catch (e) {
+-      if (e.result == Cr.NS_ERROR_HOST_IS_IP_ADDRESS ||
+-          e.result == Cr.NS_ERROR_INSUFFICIENT_DOMAIN_LEVELS) {
+-        // For this 2 expected errors, just take the host as the result.
+-        // - NS_ERROR_HOST_IS_IP_ADDRESS: the host is in ipv4/ipv6.
+-        // - NS_ERROR_INSUFFICIENT_DOMAIN_LEVELS: not enough domain part to extract.
+-        result = host;
+-      } else {
+-        throw e;
+-      }
+-    }
+-    return result;
+-  },
++  saveChanges() {
++    // Tracks whether the user confirmed their decision.
++    let allowed = false;
+ 
+-  saveChanges() {
+-    let allowed = true;
++    let removals = this._sites
++      .filter(site => site.userAction == "remove")
++      .map(site => site.host);
+ 
+-    // Confirm user really wants to remove site data starts
+-    let removals = new Set();
+-    this._sites = this._sites.filter(site => {
+-      if (site.userAction === "remove") {
+-        removals.add(site.host);
+-        return false;
+-      }
+-      return true;
+-    });
+-
+-    if (removals.size > 0) {
+-      if (this._sites.length == 0) {
+-        if (SiteDataManager.promptSiteDataRemoval(window)) {
++    if (removals.length > 0) {
++      if (this._sites.length == removals.length) {
++        allowed = SiteDataManager.promptSiteDataRemoval(window);
++        if (allowed) {
+           SiteDataManager.removeAll();
+         }
+       } else {
+-        // User only removes partial sites.
+-        // We will remove cookies based on base domain, say, user selects "news.foo.com" to remove.
+-        // The cookies under "music.foo.com" will be removed together.
+-        // We have to prompt user about this action.
+-        let hostsTable = new Map();
+-        // Group removed sites by base domain
+-        for (let host of removals) {
+-          let baseDomain = this._getBaseDomainFromHost(host);
+-          let hosts = hostsTable.get(baseDomain);
+-          if (!hosts) {
+-            hosts = [];
+-            hostsTable.set(baseDomain, hosts);
+-          }
+-          hosts.push(host);
+-        }
+-
+-        // Pick out sites with the same base domain as removed sites
+-        for (let site of this._sites) {
+-          let baseDomain = this._getBaseDomainFromHost(site.host);
+-          let hosts = hostsTable.get(baseDomain);
+-          if (hosts) {
+-            hosts.push(site.host);
+-          }
+-        }
+-
+         let args = {
+-          hostsTable,
++          hosts: removals,
+           allowed: false
+         };
+         let features = "centerscreen,chrome,modal,resizable=no";
+         window.openDialog("chrome://browser/content/preferences/siteDataRemoveSelected.xul", "", features, args);
+         allowed = args.allowed;
+         if (allowed) {
+           try {
+             SiteDataManager.remove(removals);
+@@ -294,19 +247,22 @@ let gSiteDataSettings = {
+             // Hit error, maybe remove unknown site.
+             // Let's print out the error, then proceed to close this settings dialog.
+             // When we next open again we will once more get sites from the SiteDataManager and refresh the list.
+             Cu.reportError(e);
+           }
+         }
+       }
+     }
+-    // Confirm user really wants to remove site data ends
+ 
+-    this.close();
++    // If the user cancelled the confirm dialog keep the site data window open,
++    // they can still press cancel again to exit.
++    if (allowed) {
++      this.close();
++    }
+   },
+ 
+   close() {
+     window.close();
+   },
+ 
+   onClickTreeCol(e) {
+     this._sortSites(this._sites, e.target);
+diff --git a/browser/locales/en-US/chrome/browser/preferences/siteDataSettings.dtd b/browser/locales/en-US/chrome/browser/preferences/siteDataSettings.dtd
+--- a/browser/locales/en-US/chrome/browser/preferences/siteDataSettings.dtd
++++ b/browser/locales/en-US/chrome/browser/preferences/siteDataSettings.dtd
+@@ -10,11 +10,11 @@
+ <!ENTITY     searchTextboxPlaceHolder             "Search websites">
+ <!ENTITY     searchTextboxPlaceHolder.accesskey   "S">
+ <!ENTITY     removeSelected.label          "Remove Selected">
+ <!ENTITY     removeSelected.accesskey      "r">
+ <!ENTITY     save.label                    "Save Changes">
+ <!ENTITY     save.accesskey                "a">
+ <!ENTITY     cancel.label                  "Cancel">
+ <!ENTITY     cancel.accesskey              "C">
+-<!ENTITY     removingDialog.title          "Removing Site Data">
+-<!ENTITY     removingSelected.description  "Removing site data will also remove related cookies and offline web content. This may log you out of websites. Are you sure you want to make the changes?">
+-<!ENTITY     siteTree2.label               "The following website cookies will be removed">
++<!ENTITY     removingDialog1.title         "Removing Cookies and Site Data">
++<!ENTITY     removingSelected1.description "Removing cookies and site data may log you out of websites. Are you sure you want to make the changes?">
++<!ENTITY     siteTree3.label               "Cookies and site data for the following websites will be removed">
+diff --git a/browser/themes/shared/incontentprefs/siteDataSettings.css b/browser/themes/shared/incontentprefs/siteDataSettings.css
+--- a/browser/themes/shared/incontentprefs/siteDataSettings.css
++++ b/browser/themes/shared/incontentprefs/siteDataSettings.css
+@@ -20,16 +20,17 @@
+   width: 50px;
+ }
+ 
+ /**
+  * Confirmation dialog of removing sites selected
+  */
+ #SiteDataRemoveSelectedDialog {
+   padding: 16px;
++  min-height: 36em;
+ }
+ 
+ #contentContainer {
+   font-size: 1.2em;
+   margin-bottom: 10px;
+ }
+ 
+ .question-icon {

+ 1361 - 0
frg/mozilla-release/work-js/1421737-4-60a1.patch

@@ -0,0 +1,1361 @@
+# HG changeset patch
+# User Johann Hofmann <jhofmann@mozilla.com>
+# Date 1518131043 -3600
+# Node ID 7fe3e62d6413a53e1ee4522b48a63af799b7acdb
+# Parent  fe6c0f8a271621d1b1b196f577e2b9b040066d69
+Bug 1421737 - Part 4 - Update site data manager tests to include cookies. r=Gijs
+
+This adds a dedicated test for showing and deleting cookies in site data
+management as well as amending tests for sorting, grouping, etc.
+
+MozReview-Commit-ID: 59mN3uASwPP
+
+diff --git a/browser/components/preferences/in-content-new/tests/browser_siteData.js b/browser/components/preferences/in-content-new/tests/browser_siteData.js
+--- a/browser/components/preferences/in-content-new/tests/browser_siteData.js
++++ b/browser/components/preferences/in-content-new/tests/browser_siteData.js
+@@ -5,16 +5,17 @@
+ 
+ const TEST_QUOTA_USAGE_HOST = "example.com";
+ const TEST_QUOTA_USAGE_ORIGIN = "https://" + TEST_QUOTA_USAGE_HOST;
+ const TEST_QUOTA_USAGE_URL = TEST_QUOTA_USAGE_ORIGIN + "/browser/browser/components/preferences/in-content-new/tests/site_data_test.html";
+ const TEST_OFFLINE_HOST = "example.org";
+ const TEST_OFFLINE_ORIGIN = "https://" + TEST_OFFLINE_HOST;
+ const TEST_OFFLINE_URL = TEST_OFFLINE_ORIGIN + "/browser/browser/components/preferences/in-content-new/tests/offline/offline.html";
+ const TEST_SERVICE_WORKER_URL = TEST_OFFLINE_ORIGIN + "/browser/browser/components/preferences/in-content-new/tests/service_worker_test.html";
++const REMOVE_DIALOG_URL = "chrome://browser/content/preferences/siteDataRemoveSelected.xul";
+ 
+ const { NetUtil } = ChromeUtils.import("resource://gre/modules/NetUtil.jsm", {});
+ const { DownloadUtils } = ChromeUtils.import("resource://gre/modules/DownloadUtils.jsm", {});
+ const { SiteDataManager } = ChromeUtils.import("resource:///modules/SiteDataManager.jsm", {});
+ const { OfflineAppCacheHelper } = ChromeUtils.import("resource:///modules/offlineAppCache.jsm", {});
+ 
+ function getPersistentStoragePermStatus(origin) {
+   let uri = NetUtil.newURI(origin);
+@@ -155,8 +156,99 @@ add_task(async function() {
+     }
+   });
+   await acceptRemovePromise;
+   await updatePromise;
+   await promiseServiceWorkersCleared();
+   await BrowserTestUtils.removeTab(gBrowser.selectedTab);
+ });
+ 
++// Test showing and removing sites with cookies.
++add_task(async function() {
++  SiteDataManager.removeAll();
++
++  // Add some test cookies.
++  let uri = Services.io.newURI("https://example.com");
++  let uri2 = Services.io.newURI("https://example.org");
++  Services.cookies.add(uri.host, uri.pathQueryRef, "test1", "1",
++    false, false, false, Date.now() + 1000 * 60 * 60);
++  Services.cookies.add(uri.host, uri.pathQueryRef, "test2", "2",
++    false, false, false, Date.now() + 1000 * 60 * 60);
++  Services.cookies.add(uri2.host, uri2.pathQueryRef, "test1", "1",
++    false, false, false, Date.now() + 1000 * 60 * 60);
++
++  // Ensure that private browsing cookies are ignored.
++  Services.cookies.add(uri.host, uri.pathQueryRef, "test3", "3",
++    false, false, false, Date.now() + 1000 * 60 * 60, { privateBrowsingId: 1 });
++
++  await openPreferencesViaOpenPreferencesAPI("privacy", { leaveOpen: true });
++
++  // Open the site data manager and remove one site.
++  await openSiteDataSettingsDialog();
++  let removeDialogOpenPromise = promiseWindowDialogOpen("accept", REMOVE_DIALOG_URL);
++  ContentTask.spawn(gBrowser.selectedBrowser, null, function() {
++    let frameDoc = content.gSubDialog._topDialog._frame.contentDocument;
++
++    let siteItems = frameDoc.getElementsByTagName("richlistitem");
++    is(siteItems.length, 2, "Should list two sites with cookies");
++    let sitesList = frameDoc.getElementById("sitesList");
++    let site1 = sitesList.querySelector(`richlistitem[host="example.com"]`);
++    let site2 = sitesList.querySelector(`richlistitem[host="example.org"]`);
++
++    let columns = site1.querySelectorAll(".item-box > label");
++    is(columns[0].value, "example.com", "Should show the correct host.");
++    is(columns[2].value, "2", "Should show the correct number of cookies.");
++    is(columns[3].value, "", "Should show no site data.");
++
++    columns = site2.querySelectorAll(".item-box > label");
++    is(columns[0].value, "example.org", "Should show the correct host.");
++    is(columns[2].value, "1", "Should show the correct number of cookies.");
++    is(columns[3].value, "", "Should show no site data.");
++
++    let removeBtn = frameDoc.getElementById("removeSelected");
++    let saveBtn = frameDoc.getElementById("save");
++    site2.click();
++    removeBtn.doCommand();
++    saveBtn.doCommand();
++  });
++  await removeDialogOpenPromise;
++
++  await TestUtils.waitForCondition(() => Services.cookies.countCookiesFromHost(uri2.host) == 0, "Cookies from the first host should be cleared");
++  is(Services.cookies.countCookiesFromHost(uri.host), 2, "Cookies from the second host should not be cleared");
++
++  // Open the site data manager and remove another site.
++  await openSiteDataSettingsDialog();
++  let acceptRemovePromise = promiseAlertDialogOpen("accept");
++  ContentTask.spawn(gBrowser.selectedBrowser, null, function() {
++    let frameDoc = content.gSubDialog._topDialog._frame.contentDocument;
++
++    let siteItems = frameDoc.getElementsByTagName("richlistitem");
++    is(siteItems.length, 1, "Should list one site with cookies");
++    let sitesList = frameDoc.getElementById("sitesList");
++    let site1 = sitesList.querySelector(`richlistitem[host="example.com"]`);
++
++    let columns = site1.querySelectorAll(".item-box > label");
++    is(columns[0].value, "example.com", "Should show the correct host.");
++    is(columns[2].value, "2", "Should show the correct number of cookies.");
++    is(columns[3].value, "", "Should show no site data.");
++
++    let removeBtn = frameDoc.getElementById("removeSelected");
++    let saveBtn = frameDoc.getElementById("save");
++    site1.click();
++    removeBtn.doCommand();
++    saveBtn.doCommand();
++  });
++  await acceptRemovePromise;
++
++  await TestUtils.waitForCondition(() => Services.cookies.countCookiesFromHost(uri.host) == 0, "Cookies from the second host should be cleared");
++
++  await openSiteDataSettingsDialog();
++
++  ContentTask.spawn(gBrowser.selectedBrowser, null, function() {
++    let frameDoc = content.gSubDialog._topDialog._frame.contentDocument;
++
++    let siteItems = frameDoc.getElementsByTagName("richlistitem");
++    is(siteItems.length, 0, "Should list no sites with cookies");
++  });
++
++  await BrowserTestUtils.removeTab(gBrowser.selectedTab);
++});
++
+diff --git a/browser/components/preferences/in-content-new/tests/browser_siteData2.js b/browser/components/preferences/in-content-new/tests/browser_siteData2.js
+--- a/browser/components/preferences/in-content-new/tests/browser_siteData2.js
++++ b/browser/components/preferences/in-content-new/tests/browser_siteData2.js
+@@ -25,43 +25,38 @@ function assertAllSitesNotListed(win) {
+   let sites = sitesList.getElementsByTagName("richlistitem");
+   is(sites.length, 0, "Should not list all sites");
+   is(removeBtn.disabled, true, "Should disable the removeSelected button");
+   is(removeAllBtn.disabled, true, "Should disable the removeAllBtn button");
+ }
+ 
+ // Test selecting and removing all sites one by one
+ add_task(async function() {
+-  mockSiteDataManager.register(SiteDataManager);
+-  mockSiteDataManager.fakeSites = [
++  mockSiteDataManager.register(SiteDataManager, [
+     {
+       usage: 1024,
+-      principal: Services.scriptSecurityManager
+-                         .createCodebasePrincipalFromOrigin("https://account.xyz.com"),
++      origin: "https://account.xyz.com",
+       persisted: true
+     },
+     {
+       usage: 1024,
+-      principal: Services.scriptSecurityManager
+-                         .createCodebasePrincipalFromOrigin("https://shopping.xyz.com"),
++      origin: "https://shopping.xyz.com",
+       persisted: false
+     },
+     {
+       usage: 1024,
+-      principal: Services.scriptSecurityManager
+-                         .createCodebasePrincipalFromOrigin("http://cinema.bar.com"),
++      origin: "http://cinema.bar.com",
+       persisted: true
+     },
+     {
+       usage: 1024,
+-      principal: Services.scriptSecurityManager
+-                         .createCodebasePrincipalFromOrigin("http://email.bar.com"),
++      origin: "http://email.bar.com",
+       persisted: false
+     },
+-  ];
++  ]);
+   let fakeHosts = mockSiteDataManager.fakeSites.map(site => site.principal.URI.host);
+ 
+   let updatePromise = promiseSiteDataManagerSitesUpdated();
+   await openPreferencesViaOpenPreferencesAPI("privacy", { leaveOpen: true });
+   await updatePromise;
+   await openSiteDataSettingsDialog();
+ 
+   let win = gBrowser.selectedBrowser.contentWindow;
+@@ -85,20 +80,22 @@ add_task(async function() {
+   await openSiteDataSettingsDialog();
+   assertSitesListed(doc, fakeHosts);
+ 
+   // Test the "Save Changes" button but cancelling save
+   let cancelPromise = promiseAlertDialogOpen("cancel");
+   settingsDialogClosePromise = promiseSettingsDialogClose();
+   frameDoc = win.gSubDialog._topDialog._frame.contentDocument;
+   saveBtn = frameDoc.getElementById("save");
++  cancelBtn = frameDoc.getElementById("cancel");
+   removeAllSitesOneByOne();
+   assertAllSitesNotListed(win);
+   saveBtn.doCommand();
+   await cancelPromise;
++  cancelBtn.doCommand();
+   await settingsDialogClosePromise;
+   await openSiteDataSettingsDialog();
+   assertSitesListed(doc, fakeHosts);
+ 
+   // Test the "Save Changes" button and accepting save
+   let acceptPromise = promiseAlertDialogOpen("accept");
+   settingsDialogClosePromise = promiseSettingsDialogClose();
+   updatePromise = promiseSiteDataManagerSitesUpdated();
+@@ -125,61 +122,53 @@ add_task(async function() {
+       sites[i].click();
+       removeBtn.doCommand();
+     }
+   }
+ });
+ 
+ // Test selecting and removing partial sites
+ add_task(async function() {
+-  mockSiteDataManager.register(SiteDataManager);
+-  mockSiteDataManager.fakeSites = [
++  mockSiteDataManager.register(SiteDataManager, [
+     {
+       usage: 1024,
+-      principal: Services.scriptSecurityManager
+-                         .createCodebasePrincipalFromOrigin("https://account.xyz.com"),
++      origin: "https://account.xyz.com",
+       persisted: true
+     },
+     {
+       usage: 1024,
+-      principal: Services.scriptSecurityManager
+-                         .createCodebasePrincipalFromOrigin("https://shopping.xyz.com"),
++      origin: "https://shopping.xyz.com",
+       persisted: false
+     },
+     {
+       usage: 1024,
+-      principal: Services.scriptSecurityManager
+-                         .createCodebasePrincipalFromOrigin("http://cinema.bar.com"),
++      origin: "http://cinema.bar.com",
+       persisted: true
+     },
+     {
+       usage: 1024,
+-      principal: Services.scriptSecurityManager
+-                         .createCodebasePrincipalFromOrigin("http://email.bar.com"),
++      origin: "http://email.bar.com",
+       persisted: false
+     },
+     {
+       usage: 1024,
+-      principal: Services.scriptSecurityManager
+-                         .createCodebasePrincipalFromOrigin("https://s3-us-west-2.amazonaws.com"),
++      origin: "https://s3-us-west-2.amazonaws.com",
+       persisted: true
+     },
+     {
+       usage: 1024,
+-      principal: Services.scriptSecurityManager
+-                         .createCodebasePrincipalFromOrigin("https://127.0.0.1"),
++      origin: "https://127.0.0.1",
+       persisted: false
+     },
+     {
+       usage: 1024,
+-      principal: Services.scriptSecurityManager
+-                         .createCodebasePrincipalFromOrigin("https://[0:0:0:0:0:0:0:1]"),
++      origin: "https://[0:0:0:0:0:0:0:1]",
+       persisted: true
+     },
+-  ];
++  ]);
+   let fakeHosts = mockSiteDataManager.fakeSites.map(site => site.principal.URI.host);
+ 
+   let updatePromise = promiseSiteDataManagerSitesUpdated();
+   await openPreferencesViaOpenPreferencesAPI("privacy", { leaveOpen: true });
+   await updatePromise;
+   await openSiteDataSettingsDialog();
+ 
+   let win = gBrowser.selectedBrowser.contentWindow;
+@@ -204,20 +193,22 @@ add_task(async function() {
+   await openSiteDataSettingsDialog();
+   assertSitesListed(doc, fakeHosts);
+ 
+   // Test the "Save Changes" button but canceling save
+   removeDialogOpenPromise = promiseWindowDialogOpen("cancel", REMOVE_DIALOG_URL);
+   settingsDialogClosePromise = promiseSettingsDialogClose();
+   frameDoc = win.gSubDialog._topDialog._frame.contentDocument;
+   saveBtn = frameDoc.getElementById("save");
++  cancelBtn = frameDoc.getElementById("cancel");
+   removeSelectedSite(fakeHosts.slice(0, 2));
+   assertSitesListed(doc, fakeHosts.slice(2));
+   saveBtn.doCommand();
+   await removeDialogOpenPromise;
++  cancelBtn.doCommand();
+   await settingsDialogClosePromise;
+   await openSiteDataSettingsDialog();
+   assertSitesListed(doc, fakeHosts);
+ 
+   // Test the "Save Changes" button and accepting save
+   removeDialogOpenPromise = promiseWindowDialogOpen("accept", REMOVE_DIALOG_URL);
+   settingsDialogClosePromise = promiseSettingsDialogClose();
+   frameDoc = win.gSubDialog._topDialog._frame.contentDocument;
+@@ -248,43 +239,38 @@ add_task(async function() {
+         ok(false, `Should not select and remove inexistent site of ${host}`);
+       }
+     });
+   }
+ });
+ 
+ // Test searching and then removing only visible sites
+ add_task(async function() {
+-  mockSiteDataManager.register(SiteDataManager);
+-  mockSiteDataManager.fakeSites = [
++  mockSiteDataManager.register(SiteDataManager, [
+     {
+       usage: 1024,
+-      principal: Services.scriptSecurityManager
+-                         .createCodebasePrincipalFromOrigin("https://account.xyz.com"),
++      origin: "https://account.xyz.com",
+       persisted: true
+     },
+     {
+       usage: 1024,
+-      principal: Services.scriptSecurityManager
+-                         .createCodebasePrincipalFromOrigin("https://shopping.xyz.com"),
++      origin: "https://shopping.xyz.com",
+       persisted: false
+     },
+     {
+       usage: 1024,
+-      principal: Services.scriptSecurityManager
+-                         .createCodebasePrincipalFromOrigin("http://cinema.bar.com"),
++      origin: "http://cinema.bar.com",
+       persisted: true
+     },
+     {
+       usage: 1024,
+-      principal: Services.scriptSecurityManager
+-                         .createCodebasePrincipalFromOrigin("http://email.bar.com"),
++      origin: "http://email.bar.com",
+       persisted: false
+     },
+-  ];
++  ]);
+   let fakeHosts = mockSiteDataManager.fakeSites.map(site => site.principal.URI.host);
+ 
+   let updatePromise = promiseSiteDataManagerSitesUpdated();
+   await openPreferencesViaOpenPreferencesAPI("privacy", { leaveOpen: true });
+   await updatePromise;
+   await openSiteDataSettingsDialog();
+ 
+   // Search "foo" to only list foo.com sites
+@@ -311,31 +297,28 @@ add_task(async function() {
+   assertSitesListed(doc, fakeHosts.filter(host => !host.includes("xyz")));
+ 
+   mockSiteDataManager.unregister();
+   await BrowserTestUtils.removeTab(gBrowser.selectedTab);
+ });
+ 
+ // Test dynamically clearing all site data
+ add_task(async function() {
+-  mockSiteDataManager.register(SiteDataManager);
+-  mockSiteDataManager.fakeSites = [
++  mockSiteDataManager.register(SiteDataManager, [
+     {
+       usage: 1024,
+-      principal: Services.scriptSecurityManager
+-                         .createCodebasePrincipalFromOrigin("https://account.xyz.com"),
++      origin: "https://account.xyz.com",
+       persisted: true
+     },
+     {
+       usage: 1024,
+-      principal: Services.scriptSecurityManager
+-                         .createCodebasePrincipalFromOrigin("https://shopping.xyz.com"),
++      origin: "https://shopping.xyz.com",
+       persisted: false
+     },
+-  ];
++  ]);
+   let fakeHosts = mockSiteDataManager.fakeSites.map(site => site.principal.URI.host);
+ 
+   // Test the initial state
+   let updatePromise = promiseSiteDataManagerSitesUpdated();
+   await openPreferencesViaOpenPreferencesAPI("privacy", { leaveOpen: true });
+   await updatePromise;
+   await openSiteDataSettingsDialog();
+   let doc = gBrowser.selectedBrowser.contentDocument;
+diff --git a/browser/components/preferences/in-content-new/tests/browser_siteData3.js b/browser/components/preferences/in-content-new/tests/browser_siteData3.js
+--- a/browser/components/preferences/in-content-new/tests/browser_siteData3.js
++++ b/browser/components/preferences/in-content-new/tests/browser_siteData3.js
+@@ -1,42 +1,43 @@
+ "use strict";
+ const { SiteDataManager } = Cu.import("resource:///modules/SiteDataManager.jsm", {});
+ const { DownloadUtils } = Cu.import("resource://gre/modules/DownloadUtils.jsm", {});
+ 
+ // Test not displaying sites which store 0 byte and don't have persistent storage.
+ add_task(async function() {
+   await SpecialPowers.pushPrefEnv({set: [["browser.storageManager.enabled", true]]});
+-  mockSiteDataManager.register(SiteDataManager);
+-  mockSiteDataManager.fakeSites = [
++  mockSiteDataManager.register(SiteDataManager, [
+     {
+       usage: 0,
+-      principal: Services.scriptSecurityManager
+-                         .createCodebasePrincipalFromOrigin("https://account.xyz.com"),
++      origin: "https://account.xyz.com",
+       persisted: true
+     },
+     {
+       usage: 0,
+-      principal: Services.scriptSecurityManager
+-                         .createCodebasePrincipalFromOrigin("https://shopping.xyz.com"),
++      origin: "https://shopping.xyz.com",
+       persisted: false
+     },
+     {
+       usage: 1024,
+-      principal: Services.scriptSecurityManager
+-                         .createCodebasePrincipalFromOrigin("http://cinema.bar.com"),
++      origin: "http://cinema.bar.com",
+       persisted: true
+     },
+     {
+       usage: 1024,
+-      principal: Services.scriptSecurityManager
+-                         .createCodebasePrincipalFromOrigin("http://email.bar.com"),
++      origin: "http://email.bar.com",
+       persisted: false
+     },
+-  ];
++    {
++      usage: 0,
++      origin: "http://cookies.bar.com",
++      cookies: 5,
++      persisted: false
++    },
++  ]);
+   let fakeHosts = mockSiteDataManager.fakeSites.map(site => site.principal.URI.host);
+ 
+   let updatePromise = promiseSiteDataManagerSitesUpdated();
+   let doc = gBrowser.selectedBrowser.contentDocument;
+   await openPreferencesViaOpenPreferencesAPI("privacy", { leaveOpen: true });
+   await updatePromise;
+   await openSiteDataSettingsDialog();
+   assertSitesListed(doc, fakeHosts.filter(host => host != "shopping.xyz.com"));
+@@ -44,141 +45,149 @@ add_task(async function() {
+   mockSiteDataManager.unregister();
+   await BrowserTestUtils.removeTab(gBrowser.selectedTab);
+ });
+ 
+ // Test grouping and listing sites across scheme, port and origin attributes by host
+ add_task(async function() {
+   await SpecialPowers.pushPrefEnv({set: [["browser.storageManager.enabled", true]]});
+   const quotaUsage = 1024;
+-  mockSiteDataManager.register(SiteDataManager);
+-  mockSiteDataManager.fakeSites = [
++  mockSiteDataManager.register(SiteDataManager, [
+     {
+       usage: quotaUsage,
+-      principal: Services.scriptSecurityManager
+-                         .createCodebasePrincipalFromOrigin("https://account.xyz.com^userContextId=1"),
++      origin: "https://account.xyz.com^userContextId=1",
++      cookies: 2,
+       persisted: true
+     },
+     {
+       usage: quotaUsage,
+-      principal: Services.scriptSecurityManager
+-                         .createCodebasePrincipalFromOrigin("https://account.xyz.com"),
++      origin: "https://account.xyz.com",
++      cookies: 1,
+       persisted: false
+     },
+     {
+       usage: quotaUsage,
+-      principal: Services.scriptSecurityManager
+-                         .createCodebasePrincipalFromOrigin("https://account.xyz.com:123"),
++      origin: "https://account.xyz.com:123",
++      cookies: 1,
+       persisted: false
+     },
+     {
+       usage: quotaUsage,
+-      principal: Services.scriptSecurityManager
+-                         .createCodebasePrincipalFromOrigin("http://account.xyz.com"),
++      origin: "http://account.xyz.com",
++      cookies: 1,
+       persisted: false
+     },
+-  ];
++  ]);
+ 
+   let updatedPromise = promiseSiteDataManagerSitesUpdated();
+   await openPreferencesViaOpenPreferencesAPI("privacy", { leaveOpen: true });
+   await updatedPromise;
+   await openSiteDataSettingsDialog();
+   let win = gBrowser.selectedBrowser.contentWindow;
+   let dialogFrame = win.gSubDialog._topDialog._frame;
+   let frameDoc = dialogFrame.contentDocument;
+ 
+   let siteItems = frameDoc.getElementsByTagName("richlistitem");
+   is(siteItems.length, 1, "Should group sites across scheme, port and origin attributes");
+ 
++  let columns = siteItems[0].querySelectorAll(".item-box > label");
++
+   let expected = "account.xyz.com";
+-  let host = siteItems[0].getAttribute("host");
+-  is(host, expected, "Should group and list sites by host");
++  is(columns[0].value, expected, "Should group and list sites by host");
+ 
+   let prefStrBundle = frameDoc.getElementById("bundlePreferences");
++  expected = prefStrBundle.getString("persistent");
++  is(columns[1].value, expected, "Should mark persisted status across scheme, port and origin attributes");
++
++  is(columns[2].value, "5", "Should group cookies across scheme, port and origin attributes");
++
+   expected = prefStrBundle.getFormattedString("siteUsage",
+     DownloadUtils.convertByteUnits(quotaUsage * mockSiteDataManager.fakeSites.length));
+-  let usage = siteItems[0].getAttribute("usage");
+-  is(usage, expected, "Should sum up usages across scheme, port and origin attributes");
+-
+-  expected = prefStrBundle.getString("persistent");
+-  let status = siteItems[0].getAttribute("status");
+-  is(status, expected, "Should mark persisted status across scheme, port and origin attributes");
++  is(columns[3].value, expected, "Should sum up usages across scheme, port and origin attributes");
+ 
+   mockSiteDataManager.unregister();
+   await BrowserTestUtils.removeTab(gBrowser.selectedTab);
+ });
+ 
+ // Test sorting
+ add_task(async function() {
+   await SpecialPowers.pushPrefEnv({set: [["browser.storageManager.enabled", true]]});
+-  mockSiteDataManager.register(SiteDataManager);
+-  mockSiteDataManager.fakeSites = [
++  mockSiteDataManager.register(SiteDataManager, [
+     {
+       usage: 1024,
+-      principal: Services.scriptSecurityManager
+-                         .createCodebasePrincipalFromOrigin("https://account.xyz.com"),
++      origin: "https://account.xyz.com",
++      cookies: 6,
+       persisted: true
+     },
+     {
+       usage: 1024 * 2,
+-      principal: Services.scriptSecurityManager
+-                         .createCodebasePrincipalFromOrigin("https://books.foo.com"),
++      origin: "https://books.foo.com",
++      cookies: 0,
+       persisted: false
+     },
+     {
+       usage: 1024 * 3,
+-      principal: Services.scriptSecurityManager
+-                         .createCodebasePrincipalFromOrigin("http://cinema.bar.com"),
++      origin: "http://cinema.bar.com",
++      cookies: 3,
+       persisted: true
+     },
+-  ];
++  ]);
+ 
+   let updatePromise = promiseSiteDataManagerSitesUpdated();
+   await openPreferencesViaOpenPreferencesAPI("privacy", { leaveOpen: true });
+   await updatePromise;
+   await openSiteDataSettingsDialog();
+ 
+   let dialog = content.gSubDialog._topDialog;
+   let dialogFrame = dialog._frame;
+   let frameDoc = dialogFrame.contentDocument;
+   let hostCol = frameDoc.getElementById("hostCol");
+   let usageCol = frameDoc.getElementById("usageCol");
+   let statusCol = frameDoc.getElementById("statusCol");
++  let cookiesCol = frameDoc.getElementById("cookiesCol");
+   let sitesList = frameDoc.getElementById("sitesList");
+ 
+   // Test default sorting
+   assertSortByUsage("descending");
+ 
+   // Test sorting on the usage column
+   usageCol.click();
+   assertSortByUsage("ascending");
+   usageCol.click();
+   assertSortByUsage("descending");
+ 
+   // Test sorting on the host column
+   hostCol.click();
+-  assertSortByHost("ascending");
++  assertSortByBaseDomain("ascending");
+   hostCol.click();
+-  assertSortByHost("descending");
++  assertSortByBaseDomain("descending")
+ 
+   // Test sorting on the permission status column
++  cookiesCol.click();
++  assertSortByCookies("ascending");
++  cookiesCol.click();
++  assertSortByCookies("descending");
++
++  // Test sorting on the cookies column
+   statusCol.click();
+   assertSortByStatus("ascending");
+   statusCol.click();
+   assertSortByStatus("descending");
+ 
+   mockSiteDataManager.unregister();
+   await BrowserTestUtils.removeTab(gBrowser.selectedTab);
+ 
+-  function assertSortByHost(order) {
++  function assertSortByBaseDomain(order) {
+     let siteItems = sitesList.getElementsByTagName("richlistitem");
+     for (let i = 0; i < siteItems.length - 1; ++i) {
+       let aHost = siteItems[i].getAttribute("host");
+       let bHost = siteItems[i + 1].getAttribute("host");
+-      let result = aHost.localeCompare(bHost);
++      let a = findSiteByHost(aHost);
++      let b = findSiteByHost(bHost);
++      let result = a.baseDomain.localeCompare(b.baseDomain);
+       if (order == "ascending") {
+         Assert.lessOrEqual(result, 0, "Should sort sites in the ascending order by host");
+       } else {
+         Assert.greaterOrEqual(result, 0, "Should sort sites in the descending order by host");
+       }
+     }
+   }
+ 
+@@ -214,12 +223,28 @@ add_task(async function() {
+       if (order == "ascending") {
+         Assert.lessOrEqual(result, 0, "Should sort sites in the ascending order by usage");
+       } else {
+         Assert.greaterOrEqual(result, 0, "Should sort sites in the descending order by usage");
+       }
+     }
+   }
+ 
++  function assertSortByCookies(order) {
++    let siteItems = sitesList.getElementsByTagName("richlistitem");
++    for (let i = 0; i < siteItems.length - 1; ++i) {
++      let aHost = siteItems[i].getAttribute("host");
++      let bHost = siteItems[i + 1].getAttribute("host");
++      let a = findSiteByHost(aHost);
++      let b = findSiteByHost(bHost);
++      let result = a.cookies.length - b.cookies.length;
++      if (order == "ascending") {
++        Assert.lessOrEqual(result, 0, "Should sort sites in the ascending order by number of cookies");
++      } else {
++        Assert.greaterOrEqual(result, 0, "Should sort sites in the descending order by number of cookies");
++      }
++    }
++  }
++
+   function findSiteByHost(host) {
+     return mockSiteDataManager.fakeSites.find(site => site.principal.URI.host == host);
+   }
+ });
+diff --git a/browser/components/preferences/in-content-new/tests/head.js b/browser/components/preferences/in-content-new/tests/head.js
+--- a/browser/components/preferences/in-content-new/tests/head.js
++++ b/browser/components/preferences/in-content-new/tests/head.js
+@@ -262,26 +262,48 @@ const mockSiteDataManager = {
+ 
+   _removeQuotaUsage(site) {
+     var target = site.principals[0].URI.host;
+     this.fakeSites = this.fakeSites.filter(fakeSite => {
+       return fakeSite.principal.URI.host != target;
+     });
+   },
+ 
+-  register(SiteDataManager) {
++  register(SiteDataManager, fakeSites) {
+     this._SiteDataManager = SiteDataManager;
+     this._originalQMS = this._SiteDataManager._qms;
+     this._SiteDataManager._qms = this;
+     this._originalRemoveQuotaUsage = this._SiteDataManager._removeQuotaUsage;
+     this._SiteDataManager._removeQuotaUsage = this._removeQuotaUsage.bind(this);
+-    this.fakeSites = null;
++    // Add some fake data.
++    this.fakeSites = fakeSites;
++    for (let site of fakeSites) {
++      if (!site.principal) {
++        site.principal = Services.scriptSecurityManager
++          .createCodebasePrincipalFromOrigin(site.origin);
++      }
++
++      let uri = site.principal.URI;
++      try {
++        site.baseDomain = Services.eTLD.getBaseDomainFromHost(uri.host);
++      } catch (e) {
++        site.baseDomain = uri.host;
++      }
++
++      // Add some cookies if needed.
++      for (let i = 0; i < (site.cookies || 0); i++) {
++        Services.cookies.add(uri.host, uri.pathQueryRef, Cu.now(), i,
++          false, false, false, Date.now() + 1000 * 60 * 60);
++      }
++    }
+   },
+ 
+   unregister() {
++    this.fakeSites = null;
++    this._SiteDataManager.removeAll();
+     this._SiteDataManager._qms = this._originalQMS;
+     this._SiteDataManager._removeQuotaUsage = this._originalRemoveQuotaUsage;
+   }
+ };
+ 
+ function getQuotaUsage(origin) {
+   return new Promise(resolve => {
+     let uri = NetUtil.newURI(origin);
+diff --git a/browser/components/preferences/in-content/tests/browser_siteData.js b/browser/components/preferences/in-content/tests/browser_siteData.js
+--- a/browser/components/preferences/in-content/tests/browser_siteData.js
++++ b/browser/components/preferences/in-content/tests/browser_siteData.js
+@@ -153,8 +153,99 @@ add_task(async function() {
+       ok(false, `Should have one site of ${host}`);
+     }
+   });
+   await acceptRemovePromise;
+   await updatePromise;
+   await promiseServiceWorkersCleared();
+   await BrowserTestUtils.removeTab(gBrowser.selectedTab);
+ });
++
++// Test showing and removing sites with cookies.
++add_task(async function() {
++  SiteDataManager.removeAll();
++
++  // Add some test cookies.
++  let uri = Services.io.newURI("https://example.com");
++  let uri2 = Services.io.newURI("https://example.org");
++  Services.cookies.add(uri.host, uri.pathQueryRef, "test1", "1",
++    false, false, false, Date.now() + 1000 * 60 * 60);
++  Services.cookies.add(uri.host, uri.pathQueryRef, "test2", "2",
++    false, false, false, Date.now() + 1000 * 60 * 60);
++  Services.cookies.add(uri2.host, uri2.pathQueryRef, "test1", "1",
++    false, false, false, Date.now() + 1000 * 60 * 60);
++
++  // Ensure that private browsing cookies are ignored.
++  Services.cookies.add(uri.host, uri.pathQueryRef, "test3", "3",
++    false, false, false, Date.now() + 1000 * 60 * 60, { privateBrowsingId: 1 });
++
++  await openPreferencesViaOpenPreferencesAPI("privacy", { leaveOpen: true });
++
++  // Open the site data manager and remove one site.
++  await openSiteDataSettingsDialog();
++  let removeDialogOpenPromise = promiseWindowDialogOpen("accept", REMOVE_DIALOG_URL);
++  ContentTask.spawn(gBrowser.selectedBrowser, null, function() {
++    let frameDoc = content.gSubDialog._topDialog._frame.contentDocument;
++
++    let siteItems = frameDoc.getElementsByTagName("richlistitem");
++    is(siteItems.length, 2, "Should list two sites with cookies");
++    let sitesList = frameDoc.getElementById("sitesList");
++    let site1 = sitesList.querySelector(`richlistitem[host="example.com"]`);
++    let site2 = sitesList.querySelector(`richlistitem[host="example.org"]`);
++
++    let columns = site1.querySelectorAll(".item-box > label");
++    is(columns[0].value, "example.com", "Should show the correct host.");
++    is(columns[2].value, "2", "Should show the correct number of cookies.");
++    is(columns[3].value, "", "Should show no site data.");
++
++    columns = site2.querySelectorAll(".item-box > label");
++    is(columns[0].value, "example.org", "Should show the correct host.");
++    is(columns[2].value, "1", "Should show the correct number of cookies.");
++    is(columns[3].value, "", "Should show no site data.");
++
++    let removeBtn = frameDoc.getElementById("removeSelected");
++    let saveBtn = frameDoc.getElementById("save");
++    site2.click();
++    removeBtn.doCommand();
++    saveBtn.doCommand();
++  });
++  await removeDialogOpenPromise;
++
++  await TestUtils.waitForCondition(() => Services.cookies.countCookiesFromHost(uri2.host) == 0, "Cookies from the first host should be cleared");
++  is(Services.cookies.countCookiesFromHost(uri.host), 2, "Cookies from the second host should not be cleared");
++
++  // Open the site data manager and remove another site.
++  await openSiteDataSettingsDialog();
++  let acceptRemovePromise = promiseAlertDialogOpen("accept");
++  ContentTask.spawn(gBrowser.selectedBrowser, null, function() {
++    let frameDoc = content.gSubDialog._topDialog._frame.contentDocument;
++
++    let siteItems = frameDoc.getElementsByTagName("richlistitem");
++    is(siteItems.length, 1, "Should list one site with cookies");
++    let sitesList = frameDoc.getElementById("sitesList");
++    let site1 = sitesList.querySelector(`richlistitem[host="example.com"]`);
++
++    let columns = site1.querySelectorAll(".item-box > label");
++    is(columns[0].value, "example.com", "Should show the correct host.");
++    is(columns[2].value, "2", "Should show the correct number of cookies.");
++    is(columns[3].value, "", "Should show no site data.");
++
++    let removeBtn = frameDoc.getElementById("removeSelected");
++    let saveBtn = frameDoc.getElementById("save");
++    site1.click();
++    removeBtn.doCommand();
++    saveBtn.doCommand();
++  });
++  await acceptRemovePromise;
++
++  await TestUtils.waitForCondition(() => Services.cookies.countCookiesFromHost(uri.host) == 0, "Cookies from the second host should be cleared");
++
++  await openSiteDataSettingsDialog();
++
++  ContentTask.spawn(gBrowser.selectedBrowser, null, function() {
++    let frameDoc = content.gSubDialog._topDialog._frame.contentDocument;
++
++    let siteItems = frameDoc.getElementsByTagName("richlistitem");
++    is(siteItems.length, 0, "Should list no sites with cookies");
++  });
++
++  await BrowserTestUtils.removeTab(gBrowser.selectedTab);
++});
+diff --git a/browser/components/preferences/in-content/tests/browser_siteData2.js b/browser/components/preferences/in-content/tests/browser_siteData2.js
+--- a/browser/components/preferences/in-content/tests/browser_siteData2.js
++++ b/browser/components/preferences/in-content/tests/browser_siteData2.js
+@@ -9,43 +9,38 @@ function assertAllSitesNotListed(win) {
+   let sites = sitesList.getElementsByTagName("richlistitem");
+   is(sites.length, 0, "Should not list all sites");
+   is(removeBtn.disabled, true, "Should disable the removeSelected button");
+   is(removeAllBtn.disabled, true, "Should disable the removeAllBtn button");
+ }
+ 
+ // Test selecting and removing all sites one by one
+ add_task(async function() {
+-  mockSiteDataManager.register();
+-  mockSiteDataManager.fakeSites = [
++  mockSiteDataManager.register([
+     {
+       usage: 1024,
+-      principal: Services.scriptSecurityManager
+-                         .createCodebasePrincipalFromOrigin("https://account.xyz.com"),
++      origin: "https://account.xyz.com",
+       persisted: true
+     },
+     {
+       usage: 1024,
+-      principal: Services.scriptSecurityManager
+-                         .createCodebasePrincipalFromOrigin("https://shopping.xyz.com"),
++      origin: "https://shopping.xyz.com",
+       persisted: false
+     },
+     {
+       usage: 1024,
+-      principal: Services.scriptSecurityManager
+-                         .createCodebasePrincipalFromOrigin("http://cinema.bar.com"),
++      origin: "http://cinema.bar.com",
+       persisted: true
+     },
+     {
+       usage: 1024,
+-      principal: Services.scriptSecurityManager
+-                         .createCodebasePrincipalFromOrigin("http://email.bar.com"),
++      origin: "http://email.bar.com",
+       persisted: false
+     },
+-  ];
++  ]);
+   let fakeHosts = mockSiteDataManager.fakeSites.map(site => site.principal.URI.host);
+ 
+   let updatePromise = promiseSitesUpdated();
+   await openPreferencesViaOpenPreferencesAPI("advanced", "networkTab", { leaveOpen: true });
+   await updatePromise;
+   await openSettingsDialog();
+ 
+   let win = gBrowser.selectedBrowser.contentWindow;
+@@ -69,20 +64,22 @@ add_task(async function() {
+   await openSettingsDialog();
+   assertSitesListed(doc, fakeHosts);
+ 
+   // Test the "Save Changes" button but cancelling save
+   let cancelPromise = promiseAlertDialogOpen("cancel");
+   settingsDialogClosePromise = promiseSettingsDialogClose();
+   frameDoc = win.gSubDialog._topDialog._frame.contentDocument;
+   saveBtn = frameDoc.getElementById("save");
++  cancelBtn = frameDoc.getElementById("cancel");
+   removeAllSitesOneByOne();
+   assertAllSitesNotListed(win);
+   saveBtn.doCommand();
+   await cancelPromise;
++  cancelBtn.doCommand();
+   await settingsDialogClosePromise;
+   await openSettingsDialog();
+   assertSitesListed(doc, fakeHosts);
+ 
+   // Test the "Save Changes" button and accepting save
+   let acceptPromise = promiseAlertDialogOpen("accept");
+   settingsDialogClosePromise = promiseSettingsDialogClose();
+   updatePromise = promiseSitesUpdated();
+@@ -109,61 +106,53 @@ add_task(async function() {
+       sites[i].click();
+       removeBtn.doCommand();
+     }
+   }
+ });
+ 
+ // Test selecting and removing partial sites
+ add_task(async function() {
+-  mockSiteDataManager.register();
+-  mockSiteDataManager.fakeSites = [
++  mockSiteDataManager.register([
+     {
+       usage: 1024,
+-      principal: Services.scriptSecurityManager
+-                         .createCodebasePrincipalFromOrigin("https://account.xyz.com"),
++      origin: "https://account.xyz.com",
+       persisted: true
+     },
+     {
+       usage: 1024,
+-      principal: Services.scriptSecurityManager
+-                         .createCodebasePrincipalFromOrigin("https://shopping.xyz.com"),
++      origin: "https://shopping.xyz.com",
+       persisted: false
+     },
+     {
+       usage: 1024,
+-      principal: Services.scriptSecurityManager
+-                         .createCodebasePrincipalFromOrigin("http://cinema.bar.com"),
++      origin: "http://cinema.bar.com",
+       persisted: true
+     },
+     {
+       usage: 1024,
+-      principal: Services.scriptSecurityManager
+-                         .createCodebasePrincipalFromOrigin("http://email.bar.com"),
++      origin: "http://email.bar.com",
+       persisted: false
+     },
+     {
+       usage: 1024,
+-      principal: Services.scriptSecurityManager
+-                         .createCodebasePrincipalFromOrigin("https://s3-us-west-2.amazonaws.com"),
++      origin: "https://s3-us-west-2.amazonaws.com",
+       persisted: true
+     },
+     {
+       usage: 1024,
+-      principal: Services.scriptSecurityManager
+-                         .createCodebasePrincipalFromOrigin("https://127.0.0.1"),
++      origin: "https://127.0.0.1",
+       persisted: false
+     },
+     {
+       usage: 1024,
+-      principal: Services.scriptSecurityManager
+-                         .createCodebasePrincipalFromOrigin("https://[0:0:0:0:0:0:0:1]"),
++      origin: "https://[0:0:0:0:0:0:0:1]",
+       persisted: true
+     },
+-  ];
++  ]);
+   let fakeHosts = mockSiteDataManager.fakeSites.map(site => site.principal.URI.host);
+ 
+   let updatePromise = promiseSitesUpdated();
+   await openPreferencesViaOpenPreferencesAPI("advanced", "networkTab", { leaveOpen: true });
+   await updatePromise;
+   await openSettingsDialog();
+ 
+   let win = gBrowser.selectedBrowser.contentWindow;
+@@ -188,20 +177,22 @@ add_task(async function() {
+   await openSettingsDialog();
+   assertSitesListed(doc, fakeHosts);
+ 
+   // Test the "Save Changes" button but canceling save
+   removeDialogOpenPromise = promiseWindowDialogOpen("cancel", REMOVE_DIALOG_URL);
+   settingsDialogClosePromise = promiseSettingsDialogClose();
+   frameDoc = win.gSubDialog._topDialog._frame.contentDocument;
+   saveBtn = frameDoc.getElementById("save");
++  cancelBtn = frameDoc.getElementById("cancel");
+   removeSelectedSite(fakeHosts.slice(0, 2));
+   assertSitesListed(doc, fakeHosts.slice(2));
+   saveBtn.doCommand();
+   await removeDialogOpenPromise;
++  cancelBtn.doCommand();
+   await settingsDialogClosePromise;
+   await openSettingsDialog();
+   assertSitesListed(doc, fakeHosts);
+ 
+   // Test the "Save Changes" button and accepting save
+   removeDialogOpenPromise = promiseWindowDialogOpen("accept", REMOVE_DIALOG_URL);
+   settingsDialogClosePromise = promiseSettingsDialogClose();
+   frameDoc = win.gSubDialog._topDialog._frame.contentDocument;
+@@ -232,43 +223,38 @@ add_task(async function() {
+         ok(false, `Should not select and remove inexistent site of ${host}`);
+       }
+     });
+   }
+ });
+ 
+ // Test searching and then removing only visible sites
+ add_task(async function() {
+-  mockSiteDataManager.register(SiteDataManager);
+-  mockSiteDataManager.fakeSites = [
++  mockSiteDataManager.register([
+     {
+       usage: 1024,
+-      principal: Services.scriptSecurityManager
+-                         .createCodebasePrincipalFromOrigin("https://account.xyz.com"),
++      origin: "https://account.xyz.com",
+       persisted: true
+     },
+     {
+       usage: 1024,
+-      principal: Services.scriptSecurityManager
+-                         .createCodebasePrincipalFromOrigin("https://shopping.xyz.com"),
++      origin: "https://shopping.xyz.com",
+       persisted: false
+     },
+     {
+       usage: 1024,
+-      principal: Services.scriptSecurityManager
+-                         .createCodebasePrincipalFromOrigin("http://cinema.bar.com"),
++      origin: "http://cinema.bar.com",
+       persisted: true
+     },
+     {
+       usage: 1024,
+-      principal: Services.scriptSecurityManager
+-                         .createCodebasePrincipalFromOrigin("http://email.bar.com"),
++      origin: "http://email.bar.com",
+       persisted: false
+     },
+-  ];
++  ]);
+   let fakeHosts = mockSiteDataManager.fakeSites.map(site => site.principal.URI.host);
+ 
+   let updatePromise = promiseSitesUpdated();
+   await openPreferencesViaOpenPreferencesAPI("advanced", "networkTab", { leaveOpen: true });
+   await updatePromise;
+   await openSettingsDialog();
+ 
+   // Search "foo" to only list foo.com sites
+@@ -295,31 +281,28 @@ add_task(async function() {
+   assertSitesListed(doc, fakeHosts.filter(host => !host.includes("xyz")));
+ 
+   mockSiteDataManager.unregister();
+   await BrowserTestUtils.removeTab(gBrowser.selectedTab);
+ });
+ 
+ // Test dynamically clearing all site data
+ add_task(async function() {
+-  mockSiteDataManager.register();
+-  mockSiteDataManager.fakeSites = [
++  mockSiteDataManager.register([
+     {
+       usage: 1024,
+-      principal: Services.scriptSecurityManager
+-                         .createCodebasePrincipalFromOrigin("https://account.xyz.com"),
++      origin: "https://account.xyz.com",
+       persisted: true
+     },
+     {
+       usage: 1024,
+-      principal: Services.scriptSecurityManager
+-                         .createCodebasePrincipalFromOrigin("https://shopping.xyz.com"),
++      origin: "https://shopping.xyz.com",
+       persisted: false
+     },
+-  ];
++  ]);
+   let fakeHosts = mockSiteDataManager.fakeSites.map(site => site.principal.URI.host);
+ 
+   // Test the initial state
+   let updatePromise = promiseSitesUpdated();
+   await openPreferencesViaOpenPreferencesAPI("advanced", "networkTab", { leaveOpen: true });
+   await updatePromise;
+   await openSettingsDialog();
+   let doc = gBrowser.selectedBrowser.contentDocument;
+diff --git a/browser/components/preferences/in-content/tests/browser_siteData3.js b/browser/components/preferences/in-content/tests/browser_siteData3.js
+--- a/browser/components/preferences/in-content/tests/browser_siteData3.js
++++ b/browser/components/preferences/in-content/tests/browser_siteData3.js
+@@ -1,40 +1,35 @@
+ "use strict";
+ 
+ // Test search on the host column
+ add_task(async function() {
+   await SpecialPowers.pushPrefEnv({set: [["browser.storageManager.enabled", true]]});
+-  mockSiteDataManager.register();
+-  mockSiteDataManager.fakeSites = [
++  mockSiteDataManager.register([
+     {
+       usage: 1024,
+-      principal: Services.scriptSecurityManager
+-                         .createCodebasePrincipalFromOrigin("https://account.xyz.com"),
++      origin: "https://account.xyz.com",
+       persisted: true
+     },
+     {
+       usage: 1024,
+-      principal: Services.scriptSecurityManager
+-                         .createCodebasePrincipalFromOrigin("https://shopping.xyz.com"),
++      origin: "https://shopping.xyz.com",
+       persisted: false
+     },
+     {
+       usage: 1024,
+-      principal: Services.scriptSecurityManager
+-                         .createCodebasePrincipalFromOrigin("http://cinema.bar.com"),
++      origin: "http://cinema.bar.com",
+       persisted: true
+     },
+     {
+       usage: 1024,
+-      principal: Services.scriptSecurityManager
+-                         .createCodebasePrincipalFromOrigin("http://email.bar.com"),
++      origin: "http://email.bar.com",
+       persisted: false
+     },
+-  ];
++  ]);
+   let fakeHosts = mockSiteDataManager.fakeSites.map(site => site.principal.URI.host);
+ 
+   let updatePromise = promiseSitesUpdated();
+   await openPreferencesViaOpenPreferencesAPI("advanced", "networkTab", { leaveOpen: true });
+   await updatePromise;
+   await openSettingsDialog();
+ 
+   let win = gBrowser.selectedBrowser.contentWindow;
+@@ -56,124 +51,133 @@ add_task(async function() {
+ 
+   mockSiteDataManager.unregister();
+   await BrowserTestUtils.removeTab(gBrowser.selectedTab);
+ });
+ 
+ // Test not displaying sites which store 0 byte and don't have persistent storage.
+ add_task(async function() {
+   await SpecialPowers.pushPrefEnv({set: [["browser.storageManager.enabled", true]]});
+-  mockSiteDataManager.register();
+-  mockSiteDataManager.fakeSites = [
++  mockSiteDataManager.register([
+     {
+       usage: 0,
+-      principal: Services.scriptSecurityManager
+-                         .createCodebasePrincipalFromOrigin("https://account.xyz.com"),
++      origin: "https://account.xyz.com",
+       persisted: true
+     },
+     {
+       usage: 0,
+-      principal: Services.scriptSecurityManager
+-                         .createCodebasePrincipalFromOrigin("https://shopping.xyz.com"),
++      origin: "https://shopping.xyz.com",
+       persisted: false
+     },
+     {
+       usage: 1024,
+-      principal: Services.scriptSecurityManager
+-                         .createCodebasePrincipalFromOrigin("http://cinema.bar.com"),
++      origin: "http://cinema.bar.com",
+       persisted: true
+     },
+     {
+       usage: 1024,
+-      principal: Services.scriptSecurityManager
+-                         .createCodebasePrincipalFromOrigin("http://email.bar.com"),
++      origin: "http://email.bar.com",
+       persisted: false
+     },
+-  ];
++    {
++      usage: 0,
++      origin: "http://cookies.bar.com",
++      cookies: 5,
++      persisted: false
++    },
++  ]);
+   let fakeHosts = mockSiteDataManager.fakeSites.map(site => site.principal.URI.host);
+ 
+   let updatePromise = promiseSitesUpdated();
+   let doc = gBrowser.selectedBrowser.contentDocument;
+   await openPreferencesViaOpenPreferencesAPI("advanced", "networkTab", { leaveOpen: true });
+   await updatePromise;
+   await openSettingsDialog();
+   assertSitesListed(doc, fakeHosts.filter(host => host != "shopping.xyz.com"));
+ 
+   mockSiteDataManager.unregister();
+   await BrowserTestUtils.removeTab(gBrowser.selectedTab);
+ });
+ 
+ // Test sorting
+ add_task(async function() {
+   await SpecialPowers.pushPrefEnv({set: [["browser.storageManager.enabled", true]]});
+-  mockSiteDataManager.register(SiteDataManager);
+-  mockSiteDataManager.fakeSites = [
++  mockSiteDataManager.register([
+     {
+       usage: 1024,
+-      principal: Services.scriptSecurityManager
+-                         .createCodebasePrincipalFromOrigin("https://account.xyz.com"),
++      origin: "https://account.xyz.com",
++      cookies: 6,
+       persisted: true
+     },
+     {
+       usage: 1024 * 2,
+-      principal: Services.scriptSecurityManager
+-                         .createCodebasePrincipalFromOrigin("https://books.foo.com"),
++      origin: "https://books.foo.com",
++      cookies: 0,
+       persisted: false
+     },
+     {
+       usage: 1024 * 3,
+-      principal: Services.scriptSecurityManager
+-                         .createCodebasePrincipalFromOrigin("http://cinema.bar.com"),
++      origin: "http://cinema.bar.com",
++      cookies: 3,
+       persisted: true
+     },
+-  ];
++  ]);
+ 
+   let updatePromise = promiseSiteDataManagerSitesUpdated();
+   await openPreferencesViaOpenPreferencesAPI("privacy", { leaveOpen: true });
+   await updatePromise;
+   await openSiteDataSettingsDialog();
+ 
+   let dialog = content.gSubDialog._topDialog;
+   let dialogFrame = dialog._frame;
+   let frameDoc = dialogFrame.contentDocument;
+   let hostCol = frameDoc.getElementById("hostCol");
+   let usageCol = frameDoc.getElementById("usageCol");
+   let statusCol = frameDoc.getElementById("statusCol");
++  let cookiesCol = frameDoc.getElementById("cookiesCol");
+   let sitesList = frameDoc.getElementById("sitesList");
+ 
+   // Test default sorting
+   assertSortByUsage("descending");
+ 
+   // Test sorting on the usage column
+   usageCol.click();
+   assertSortByUsage("ascending");
+   usageCol.click();
+   assertSortByUsage("descending");
+ 
+   // Test sorting on the host column
+   hostCol.click();
+-  assertSortByHost("ascending");
++  assertSortByBaseDomain("ascending");
+   hostCol.click();
+-  assertSortByHost("descending");
++  assertSortByBaseDomain("descending");
+ 
+   // Test sorting on the permission status column
++  cookiesCol.click();
++  assertSortByCookies("ascending");
++  cookiesCol.click();
++  assertSortByCookies("descending");
++
++  // Test sorting on the cookies column
+   statusCol.click();
+   assertSortByStatus("ascending");
+   statusCol.click();
+   assertSortByStatus("descending");
+ 
+   mockSiteDataManager.unregister();
+   await BrowserTestUtils.removeTab(gBrowser.selectedTab);
+ 
+-  function assertSortByHost(order) {
++  function assertSortByBaseDomain(order) {
+     let siteItems = sitesList.getElementsByTagName("richlistitem");
+     for (let i = 0; i < siteItems.length - 1; ++i) {
+       let aHost = siteItems[i].getAttribute("host");
+       let bHost = siteItems[i + 1].getAttribute("host");
+-      let result = aHost.localeCompare(bHost);
++      let a = findSiteByHost(aHost);
++      let b = findSiteByHost(bHost);
++      let result = a.baseDomain.localeCompare(b.baseDomain);
+       if (order == "ascending") {
+         Assert.lessOrEqual(result, 0, "Should sort sites in the ascending order by host");
+       } else {
+         Assert.greaterOrEqual(result, 0, "Should sort sites in the descending order by host");
+       }
+     }
+   }
+ 
+@@ -209,12 +213,28 @@ add_task(async function() {
+       if (order == "ascending") {
+         Assert.lessOrEqual(result, 0, "Should sort sites in the ascending order by usage");
+       } else {
+         Assert.greaterOrEqual(result, 0, "Should sort sites in the descending order by usage");
+       }
+     }
+   }
+ 
++  function assertSortByCookies(order) {
++    let siteItems = sitesList.getElementsByTagName("richlistitem");
++    for (let i = 0; i < siteItems.length - 1; ++i) {
++      let aHost = siteItems[i].getAttribute("host");
++      let bHost = siteItems[i + 1].getAttribute("host");
++      let a = findSiteByHost(aHost);
++      let b = findSiteByHost(bHost);
++      let result = a.cookies.length - b.cookies.length;
++      if (order == "ascending") {
++        Assert.lessOrEqual(result, 0, "Should sort sites in the ascending order by number of cookies");
++      } else {
++        Assert.greaterOrEqual(result, 0, "Should sort sites in the descending order by number of cookies");
++      }
++    }
++  }
++
+   function findSiteByHost(host) {
+     return mockSiteDataManager.fakeSites.find(site => site.principal.URI.host == host);
+   }
+ });
+diff --git a/browser/components/preferences/in-content/tests/head.js b/browser/components/preferences/in-content/tests/head.js
+--- a/browser/components/preferences/in-content/tests/head.js
++++ b/browser/components/preferences/in-content/tests/head.js
+@@ -34,25 +34,47 @@ const mockSiteDataManager = {
+ 
+   _removeQuotaUsage(site) {
+     var target = site.principals[0].URI.host;
+     this.fakeSites = this.fakeSites.filter(fakeSite => {
+       return fakeSite.principal.URI.host != target;
+     });
+   },
+ 
+-  register() {
++  register(fakeSites) {
+     this._originalQMS = SiteDataManager._qms;
+     SiteDataManager._qms = this;
+     this._originalRemoveQuotaUsage = SiteDataManager._removeQuotaUsage;
+     SiteDataManager._removeQuotaUsage = this._removeQuotaUsage.bind(this);
+-    this.fakeSites = null;
++    // Add some fake data.
++    this.fakeSites = fakeSites;
++    for (let site of fakeSites) {
++      if (!site.principal) {
++        site.principal = Services.scriptSecurityManager
++          .createCodebasePrincipalFromOrigin(site.origin);
++      }
++
++      let uri = site.principal.URI;
++      try {
++        site.baseDomain = Services.eTLD.getBaseDomainFromHost(uri.host);
++      } catch (e) {
++        site.baseDomain = uri.host;
++      }
++
++      // Add some cookies if needed.
++      for (let i = 0; i < (site.cookies || 0); i++) {
++        Services.cookies.add(uri.host, uri.pathQueryRef, Cu.now(), i,
++          false, false, false, Date.now() + 1000 * 60 * 60);
++      }
++    }
+   },
+ 
+   unregister() {
++    this.fakeSites = null;
++    SiteDataManager.removeAll();
+     SiteDataManager._qms = this._originalQMS;
+     SiteDataManager._removeQuotaUsage = this._originalRemoveQuotaUsage;
+   }
+ };
+ 
+ function is_hidden(aElement) {
+   var style = aElement.ownerGlobal.getComputedStyle(aElement);
+   if (style.display == "none")

+ 1159 - 0
frg/mozilla-release/work-js/1422160-60a1.patch

@@ -0,0 +1,1159 @@
+# HG changeset patch
+# User Johann Hofmann <jhofmann@mozilla.com>
+# Date 1518521609 -3600
+# Node ID e8d05c7218c66151e2e30dcff2fabdf9bccae40e
+# Parent  6053af8e022c77750eae5dcf6af76ebf7e775ada
+Bug 1422160 - Merge cache and site data in about:preferences. r=jaws
+
+This commit removes most of the cache section in about:preferences,
+following the UX concept from bug 1421690. This is in the general
+interest of de-cluttering privacy preferences and giving users controls
+that are easier to understand and use.
+
+The cache size is instead shown in the site data section and the cache
+can be cleared using the "Clear Data" button in that same section.
+
+MozReview-Commit-ID: 7PDTDgllFFI
+
+diff --git a/browser/components/preferences/clearSiteData.js b/browser/components/preferences/clearSiteData.js
+--- a/browser/components/preferences/clearSiteData.js
++++ b/browser/components/preferences/clearSiteData.js
+@@ -51,28 +51,31 @@ var gClearSiteDataDialog = {
+   onCheckboxCommand(event) {
+     this._clearButton.disabled =
+       !(this._clearSiteDataCheckbox.checked || this._clearCacheCheckbox.checked);
+   },
+ 
+   onClear() {
+     let allowed = true;
+ 
++    if (this._clearCacheCheckbox.checked && allowed) {
++      SiteDataManager.removeCache();
++      // If we're not clearing site data, we need to tell the
++      // SiteDataManager to signal that it's updating.
++      if (!this._clearSiteDataCheckbox.checked) {
++        SiteDataManager.updateSites();
++      }
++    }
++
+     if (this._clearSiteDataCheckbox.checked) {
+       allowed = SiteDataManager.promptSiteDataRemoval(window);
+       if (allowed) {
+         SiteDataManager.removeSiteData();
+       }
+     }
+ 
+-    if (this._clearCacheCheckbox.checked && allowed) {
+-      SiteDataManager.removeCache();
+-      // Update cache UI in about:preferences
+-      window.opener.gPrivacyPane.updateActualCacheSize();
+-    }
+-
+     if (allowed) {
+       window.close();
+     }
+   },
+ };
+ 
+ window.addEventListener("load", () => gClearSiteDataDialog.init());
+diff --git a/browser/components/preferences/in-content-new/privacy.js b/browser/components/preferences/in-content-new/privacy.js
+--- a/browser/components/preferences/in-content-new/privacy.js
++++ b/browser/components/preferences/in-content-new/privacy.js
+@@ -144,24 +144,20 @@ var gPrivacyPane = {
+     setEventListener("showPasswords", "command",
+       gPrivacyPane.showPasswords);
+     setEventListener("addonExceptions", "command",
+       gPrivacyPane.showAddonExceptions);
+     setEventListener("viewCertificatesButton", "command",
+                      gPrivacyPane.showCertificates);
+     setEventListener("viewSecurityDevicesButton", "command",
+                      gPrivacyPane.showSecurityDevices);
+-    setEventListener("clearCacheButton", "command",
+-                     gPrivacyPane.clearCache);
+ 
+     this._pane = document.getElementById("panePrivacy");
+     this._initMasterPasswordUI();
+     this._initSafeBrowsing();
+-    this.updateCacheSizeInputField();
+-    this.updateActualCacheSize();
+ 
+     setEventListener("notificationSettingsButton", "command",
+       gPrivacyPane.showNotificationExceptions);
+     setEventListener("locationSettingsButton", "command",
+       gPrivacyPane.showLocationExceptions);
+     setEventListener("cameraSettingsButton", "command",
+       gPrivacyPane.showCameraExceptions);
+     setEventListener("microphoneSettingsButton", "command",
+@@ -177,19 +173,16 @@ var gPrivacyPane = {
+       notificationsDoNotDisturbBox.removeAttribute("hidden");
+       if (AlertsServiceDND.manualDoNotDisturb) {
+         let notificationsDoNotDisturb =
+           document.getElementById("notificationsDoNotDisturb");
+         notificationsDoNotDisturb.setAttribute("checked", true);
+       }
+     }
+ 
+-    setEventListener("cacheSize", "change",
+-                     gPrivacyPane.updateCacheSizePref);
+-
+     Services.obs.addObserver(this, "sitedatamanager:sites-updated");
+     Services.obs.addObserver(this, "sitedatamanager:updating-sites");
+     let unload = () => {
+       window.removeEventListener("unload", unload);
+       Services.obs.removeObserver(this, "sitedatamanager:sites-updated");
+       Services.obs.removeObserver(this, "sitedatamanager:updating-sites");
+     };
+     window.addEventListener("unload", unload);
+@@ -1214,126 +1207,41 @@ var gPrivacyPane = {
+ 
+   /**
+    * Displays a dialog from which the user can manage his security devices.
+    */
+   showSecurityDevices() {
+     gSubDialog.open("chrome://pippki/content/device_manager.xul");
+   },
+ 
+-  /**
+-   * Clears the cache.
+-   */
+-  clearCache() {
+-    try {
+-      var cache = Cc["@mozilla.org/netwerk/cache-storage-service;1"]
+-                    .getService(Ci.nsICacheStorageService);
+-      cache.clear();
+-    } catch (ex) {}
+-    this.updateActualCacheSize();
+-  },
+-
+   showSiteDataSettings() {
+     gSubDialog.open("chrome://browser/content/preferences/siteDataSettings.xul");
+   },
+ 
+   toggleSiteData(shouldShow) {
+     let clearButton = document.getElementById("clearSiteDataButton");
+     let settingsButton = document.getElementById("siteDataSettings");
+     clearButton.disabled = !shouldShow;
+     settingsButton.disabled = !shouldShow;
+   },
+ 
+-  updateTotalDataSizeLabel(usage) {
+-    let prefStrBundle = document.getElementById("bundlePreferences");
++  showSiteDataLoading() {
+     let totalSiteDataSizeLabel = document.getElementById("totalSiteDataSize");
+-    if (usage < 0) {
+-      totalSiteDataSizeLabel.textContent = prefStrBundle.getString("loadingSiteDataSize");
+-    } else {
+-      let size = DownloadUtils.convertByteUnits(usage);
+-      totalSiteDataSizeLabel.textContent = prefStrBundle.getFormattedString("totalSiteDataSize", size);
+-    }
+-  },
+-
+-  // Retrieves the amount of space currently used by disk cache
+-  updateActualCacheSize() {
+-    var actualSizeLabel = document.getElementById("actualDiskCacheSize");
+-    var prefStrBundle = document.getElementById("bundlePreferences");
+-
+-    // Needs to root the observer since cache service keeps only a weak reference.
+-    this.observer = {
+-      onNetworkCacheDiskConsumption(consumption) {
+-        var size = DownloadUtils.convertByteUnits(consumption);
+-        // The XBL binding for the string bundle may have been destroyed if
+-        // the page was closed before this callback was executed.
+-        if (!prefStrBundle.getFormattedString) {
+-          return;
+-        }
+-        actualSizeLabel.textContent = prefStrBundle.getFormattedString("actualDiskCacheSize", size);
+-      },
+-
+-      QueryInterface: XPCOMUtils.generateQI([
+-        Ci.nsICacheStorageConsumptionObserver,
+-        Ci.nsISupportsWeakReference
+-      ])
+-    };
+-
+-    actualSizeLabel.textContent = prefStrBundle.getString("actualDiskCacheSizeCalculated");
+-
+-    try {
+-      var cacheService =
+-        Cc["@mozilla.org/netwerk/cache-storage-service;1"]
+-          .getService(Ci.nsICacheStorageService);
+-      cacheService.asyncGetDiskConsumption(this.observer);
+-    } catch (e) {}
++    let prefStrBundle = document.getElementById("bundlePreferences");
++    totalSiteDataSizeLabel.textContent = prefStrBundle.getString("loadingSiteDataSize1");
+   },
+ 
+-  updateCacheSizeUI(smartSizeEnabled) {
+-    document.getElementById("useCacheBefore").disabled = smartSizeEnabled;
+-    document.getElementById("cacheSize").disabled = smartSizeEnabled;
+-    document.getElementById("useCacheAfter").disabled = smartSizeEnabled;
+-  },
+-
+-  readSmartSizeEnabled() {
+-    // The smart_size.enabled preference element is inverted="true", so its
+-    // value is the opposite of the actual pref value
+-    var disabled = document.getElementById("browser.cache.disk.smart_size.enabled").value;
+-    this.updateCacheSizeUI(!disabled);
+-  },
+-
+-  /**
+-   * Converts the cache size from units of KB to units of MB and stores it in
+-   * the textbox element.
+-   *
+-   * Preferences:
+-   *
+-   * browser.cache.disk.capacity
+-   * - the size of the browser cache in KB
+-   * - Only used if browser.cache.disk.smart_size.enabled is disabled
+-   */
+-  updateCacheSizeInputField() {
+-    let cacheSizeElem = document.getElementById("cacheSize");
+-    let cachePref = document.getElementById("browser.cache.disk.capacity");
+-    cacheSizeElem.value = cachePref.value / 1024;
+-    if (cachePref.locked)
+-      cacheSizeElem.disabled = true;
+-  },
+-
+-  /**
+-   * Updates the cache size preference once user enters a new value.
+-   * We intentionally do not set preference="browser.cache.disk.capacity"
+-   * onto the textbox directly, as that would update the pref at each keypress
+-   * not only after the final value is entered.
+-   */
+-  updateCacheSizePref() {
+-    let cacheSizeElem = document.getElementById("cacheSize");
+-    let cachePref = document.getElementById("browser.cache.disk.capacity");
+-    // Converts the cache size as specified in UI (in MB) to KB.
+-    let intValue = parseInt(cacheSizeElem.value, 10);
+-    cachePref.value = isNaN(intValue) ? 0 : intValue * 1024;
++  updateTotalDataSizeLabel(siteDataUsage) {
++    SiteDataManager.getCacheSize().then(function(cacheUsage) {
++      let prefStrBundle = document.getElementById("bundlePreferences");
++      let totalSiteDataSizeLabel = document.getElementById("totalSiteDataSize");
++      let totalUsage = siteDataUsage + cacheUsage;
++      let size = DownloadUtils.convertByteUnits(totalUsage);
++      totalSiteDataSizeLabel.textContent = prefStrBundle.getFormattedString("totalSiteDataSize1", size);
++    });
+   },
+ 
+   clearSiteData() {
+     gSubDialog.open("chrome://browser/content/preferences/clearSiteData.xul");
+   },
+ 
+   initDataCollection() {
+     this._setupLearnMoreLink("toolkit.datacollection.infoURL",
+@@ -1360,17 +1268,17 @@ var gPrivacyPane = {
+     }
+   },
+ 
+   observe(aSubject, aTopic, aData) {
+     switch (aTopic) {
+       case "sitedatamanager:updating-sites":
+         // While updating, we want to disable this section and display loading message until updated
+         this.toggleSiteData(false);
+-        this.updateTotalDataSizeLabel(-1);
++        this.showSiteDataLoading();
+         break;
+ 
+       case "sitedatamanager:sites-updated":
+         this.toggleSiteData(true);
+         SiteDataManager.getTotalUsage()
+           .then(this.updateTotalDataSizeLabel.bind(this));
+         break;
+     }
+diff --git a/browser/components/preferences/in-content-new/privacy.js.1422160.later b/browser/components/preferences/in-content-new/privacy.js.1422160.later
+new file mode 100644
+--- /dev/null
++++ b/browser/components/preferences/in-content-new/privacy.js.1422160.later
+@@ -0,0 +1,24 @@
++--- privacy.js
+++++ privacy.js
++@@ -106,21 +106,16 @@ Preferences.addAll([
++   { id: "browser.safebrowsing.phishing.enabled", type: "bool" },
++ 
++   { id: "browser.safebrowsing.downloads.enabled", type: "bool" },
++ 
++   { id: "urlclassifier.malwareTable", type: "string" },
++ 
++   { id: "browser.safebrowsing.downloads.remote.block_potentially_unwanted", type: "bool" },
++   { id: "browser.safebrowsing.downloads.remote.block_uncommon", type: "bool" },
++-
++-  // Network tab
++-  { id: "browser.cache.disk.capacity", type: "int" },
++-
++-  { id: "browser.cache.disk.smart_size.enabled", type: "bool", inverted: "true" },
++ ]);
++ 
++ // Data Choices tab
++ if (AppConstants.NIGHTLY_BUILD) {
++   Preferences.add({ id: "browser.chrome.errorReporter.enabled", type: "bool" });
++ }
++ if (AppConstants.MOZ_CRASHREPORTER) {
++   Preferences.add({ id: "browser.crashReports.unsubmittedCheck.autoSubmit2", type: "bool" });
+diff --git a/browser/components/preferences/in-content-new/privacy.xul b/browser/components/preferences/in-content-new/privacy.xul
+--- a/browser/components/preferences/in-content-new/privacy.xul
++++ b/browser/components/preferences/in-content-new/privacy.xul
+@@ -141,26 +141,16 @@
+ 
+   <preference id="browser.safebrowsing.downloads.remote.block_potentially_unwanted"
+               name="browser.safebrowsing.downloads.remote.block_potentially_unwanted"
+               type="bool"/>
+   <preference id="browser.safebrowsing.downloads.remote.block_uncommon"
+               name="browser.safebrowsing.downloads.remote.block_uncommon"
+               type="bool"/>
+ 
+-  <!-- Network tab -->
+-  <preference id="browser.cache.disk.capacity"
+-              name="browser.cache.disk.capacity"
+-              type="int"/>
+-
+-  <preference id="browser.cache.disk.smart_size.enabled"
+-              name="browser.cache.disk.smart_size.enabled"
+-              inverted="true"
+-              type="bool"/>
+-
+   <!-- Data Choices tab -->
+ #ifdef MOZ_CRASHREPORTER
+   <preference id="browser.crashReports.unsubmittedCheck.autoSubmit"
+               name="browser.crashReports.unsubmittedCheck.autoSubmit"
+               type="bool"/>
+ #endif
+ 
+ </preferences>
+@@ -418,43 +408,16 @@
+   <checkbox id="openpageSuggestion" label="&locbar.openpage.label;"
+             accesskey="&locbar.openpage.accesskey;"
+             preference="browser.urlbar.suggest.openpage"/>
+   <label class="text-link" onclick="gotoPref('search')">
+     &suggestionSettings2.label;
+   </label>
+ </groupbox>
+ 
+-<!-- Cache -->
+-<groupbox id="cacheGroup" data-category="panePrivacy" hidden="true">
+-  <caption><label>&httpCache.label;</label></caption>
+-
+-  <hbox align="center">
+-    <label id="actualDiskCacheSize" flex="1"/>
+-    <button id="clearCacheButton"
+-            class="accessory-button"
+-            icon="clear"
+-            label="&clearCacheNow.label;" accesskey="&clearCacheNow.accesskey;"/>
+-  </hbox>
+-  <checkbox preference="browser.cache.disk.smart_size.enabled"
+-            id="allowSmartSize"
+-            onsyncfrompreference="return gPrivacyPane.readSmartSizeEnabled();"
+-            label="&overrideSmartCacheSize.label;"
+-            accesskey="&overrideSmartCacheSize.accesskey;"/>
+-  <hbox align="center" class="indent">
+-    <label id="useCacheBefore" control="cacheSize"
+-            accesskey="&limitCacheSizeBefore.accesskey;">
+-      &limitCacheSizeBefore.label;
+-    </label>
+-    <textbox id="cacheSize" type="number" size="4" max="1024"
+-              aria-labelledby="useCacheBefore cacheSize useCacheAfter"/>
+-    <label id="useCacheAfter" flex="1">&limitCacheSizeAfter.label;</label>
+-  </hbox>
+-</groupbox>
+-
+ <!-- Site Data -->
+ <groupbox id="siteDataGroup" data-category="panePrivacy">
+   <caption><label>&siteData.label;</label></caption>
+ 
+   <hbox align="baseline">
+     <vbox flex="1">
+       <description flex="1">
+         <label id="totalSiteDataSize"></label>
+diff --git a/browser/components/preferences/in-content-new/tests/browser_bug795764_cachedisabled.js b/browser/components/preferences/in-content-new/tests/browser_bug795764_cachedisabled.js
+--- a/browser/components/preferences/in-content-new/tests/browser_bug795764_cachedisabled.js
++++ b/browser/components/preferences/in-content-new/tests/browser_bug795764_cachedisabled.js
+@@ -11,19 +11,16 @@ function test() {
+   // Otherwise, without any site then it would just return so we would end up in not testing SiteDataManager.
+   let principal = Services.scriptSecurityManager.createCodebasePrincipalFromOrigin("https://www.foo.com");
+   Services.perms.addFromPrincipal(principal, "persistent-storage", Ci.nsIPermissionManager.ALLOW_ACTION);
+   registerCleanupFunction(function() {
+     Services.perms.removeFromPrincipal(principal, "persistent-storage");
+   });
+ 
+   SpecialPowers.pushPrefEnv({set: [
+-    ["browser.cache.offline.enable", false],
+-    ["browser.cache.disk.enable", false],
+-    ["browser.cache.memory.enable", false],
+     ["privacy.userContext.ui.enabled", true]
+   ]}).then(() => open_preferences(runTest));
+ }
+ 
+ function runTest(win) {
+   is(gBrowser.currentURI.spec, "about:preferences", "about:preferences loaded");
+ 
+   let tab = win.document;
+diff --git a/browser/components/preferences/in-content-new/tests/browser_clearSiteData.js b/browser/components/preferences/in-content-new/tests/browser_clearSiteData.js
+--- a/browser/components/preferences/in-content-new/tests/browser_clearSiteData.js
++++ b/browser/components/preferences/in-content-new/tests/browser_clearSiteData.js
+@@ -11,45 +11,16 @@ const { DownloadUtils } = ChromeUtils.im
+ const TEST_QUOTA_USAGE_HOST = "example.com";
+ const TEST_QUOTA_USAGE_ORIGIN = "https://" + TEST_QUOTA_USAGE_HOST;
+ const TEST_QUOTA_USAGE_URL = TEST_QUOTA_USAGE_ORIGIN + "/browser/browser/components/preferences/in-content-new/tests/site_data_test.html";
+ const TEST_OFFLINE_HOST = "example.org";
+ const TEST_OFFLINE_ORIGIN = "https://" + TEST_OFFLINE_HOST;
+ const TEST_OFFLINE_URL = TEST_OFFLINE_ORIGIN + "/browser/browser/components/preferences/in-content-new/tests/offline/offline.html";
+ const TEST_SERVICE_WORKER_URL = TEST_OFFLINE_ORIGIN + "/browser/browser/components/preferences/in-content-new/tests/service_worker_test.html";
+ 
+-// XXX: The intermittent bug 1331851
+-// The implementation of nsICacheStorageConsumptionObserver must be passed as weak referenced,
+-// so we must hold this observer here well. If we didn't, there would be a chance that
+-// in Linux debug test run the observer was released before the operation at gecko was completed
+-// (may be because of a relatively quicker GC cycle or a relatively slower operation).
+-// As a result of that, we would never get the cache usage we want so the test would fail from timeout.
+-const cacheUsageGetter = {
+-  _promise: null,
+-  _resolve: null,
+-  get() {
+-    if (!this._promise) {
+-      this._promise = new Promise(resolve => {
+-        this._resolve = resolve;
+-        Services.cache2.asyncGetDiskConsumption(this);
+-      });
+-    }
+-    return this._promise;
+-  },
+-  // nsICacheStorageConsumptionObserver implementations
+-  onNetworkCacheDiskConsumption(usage) {
+-    cacheUsageGetter._promise = null;
+-    cacheUsageGetter._resolve(usage);
+-  },
+-  QueryInterface: XPCOMUtils.generateQI([
+-    Ci.nsICacheStorageConsumptionObserver,
+-    Ci.nsISupportsWeakReference
+-  ]),
+-};
+-
+ async function testClearData(clearSiteData, clearCache) {
+   let quotaURI = Services.io.newURI(TEST_QUOTA_USAGE_ORIGIN);
+   SitePermissions.set(quotaURI, "persistent-storage", SitePermissions.ALLOW);
+ 
+   // Open a test site which saves into appcache.
+   await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_OFFLINE_URL);
+   await BrowserTestUtils.removeTab(gBrowser.selectedTab);
+ 
+@@ -64,23 +35,28 @@ async function testClearData(clearSiteDa
+ 
+   // Register some service workers.
+   await loadServiceWorkerTestPage(TEST_SERVICE_WORKER_URL);
+   await promiseServiceWorkerRegisteredFor(TEST_SERVICE_WORKER_URL);
+ 
+   await openPreferencesViaOpenPreferencesAPI("privacy", { leaveOpen: true });
+ 
+   // Test the initial states.
+-  let cacheUsage = await cacheUsageGetter.get();
++  let cacheUsage = await SiteDataManager.getCacheSize();
+   let quotaUsage = await getQuotaUsage(TEST_QUOTA_USAGE_ORIGIN);
+   let totalUsage = await SiteDataManager.getTotalUsage();
+   Assert.greater(cacheUsage, 0, "The cache usage should not be 0");
+   Assert.greater(quotaUsage, 0, "The quota usage should not be 0");
+   Assert.greater(totalUsage, 0, "The total usage should not be 0");
+ 
++  let initialSizeLabelValue = await ContentTask.spawn(gBrowser.selectedBrowser, null, async function() {
++    let sizeLabel = content.document.getElementById("totalSiteDataSize");
++    return sizeLabel.textContent;
++  });
++
+   let doc = gBrowser.selectedBrowser.contentDocument;
+   let clearSiteDataButton = doc.getElementById("clearSiteDataButton");
+ 
+   let dialogOpened = promiseLoadSubDialog("chrome://browser/content/preferences/clearSiteData.xul");
+   clearSiteDataButton.doCommand();
+   let dialogWin = await dialogOpened;
+ 
+   // Convert the usage numbers in the same way the UI does it to assert
+@@ -140,47 +116,48 @@ async function testClearData(clearSiteDa
+   if (clearSiteData) {
+     await acceptPromise;
+   }
+ 
+   await dialogClosed;
+ 
+   if (clearCache) {
+     TestUtils.waitForCondition(async function() {
+-      let usage = await cacheUsageGetter.get();
++      let usage = await SiteDataManager.getCacheSize();
+       return usage == 0;
+     }, "The cache usage should be removed");
+   } else {
+-    Assert.greater(await cacheUsageGetter.get(), 0, "The cache usage should not be 0");
++    Assert.greater(await SiteDataManager.getCacheSize(), 0, "The cache usage should not be 0");
+   }
+ 
+   if (clearSiteData) {
+     await updatePromise;
+     await cookiesClearedPromise;
+     await promiseServiceWorkersCleared();
+ 
+     TestUtils.waitForCondition(async function() {
+       let usage = await SiteDataManager.getTotalUsage();
+       return usage == 0;
+     }, "The total usage should be removed");
+-
+-    // Check that the size label in about:preferences updates after we cleared data.
+-    await ContentTask.spawn(gBrowser.selectedBrowser, null, async function() {
+-      let sizeLabel = content.document.getElementById("totalSiteDataSize");
+-
+-      await ContentTaskUtils.waitForCondition(
+-        () => sizeLabel.textContent.includes("0"), "Site data size label should have updated.");
+-    });
+   } else {
+     quotaUsage = await getQuotaUsage(TEST_QUOTA_USAGE_ORIGIN);
+     totalUsage = await SiteDataManager.getTotalUsage();
+     Assert.greater(quotaUsage, 0, "The quota usage should not be 0");
+     Assert.greater(totalUsage, 0, "The total usage should not be 0");
+   }
+ 
++  if (clearCache || clearSiteData) {
++    // Check that the size label in about:preferences updates after we cleared data.
++    await ContentTask.spawn(gBrowser.selectedBrowser, {initialSizeLabelValue}, async function(opts) {
++      let sizeLabel = content.document.getElementById("totalSiteDataSize");
++      await ContentTaskUtils.waitForCondition(
++        () => sizeLabel.textContent != opts.initialSizeLabelValue, "Site data size label should have updated.");
++    });
++  }
++
+   let desiredPermissionState = clearSiteData ? SitePermissions.UNKNOWN : SitePermissions.ALLOW;
+   let permission = SitePermissions.get(quotaURI, "persistent-storage");
+   is(permission.state, desiredPermissionState, "Should have the correct permission state.");
+ 
+   await BrowserTestUtils.removeTab(gBrowser.selectedTab);
+   await SiteDataManager.removeAll();
+ }
+ 
+diff --git a/browser/components/preferences/in-content-new/tests/browser_siteData.js b/browser/components/preferences/in-content-new/tests/browser_siteData.js
+--- a/browser/components/preferences/in-content-new/tests/browser_siteData.js
++++ b/browser/components/preferences/in-content-new/tests/browser_siteData.js
+@@ -62,49 +62,51 @@ add_task(async function() {
+   await BrowserTestUtils.removeTab(gBrowser.selectedTab);
+ });
+ 
+ // Test buttons are disabled and loading message shown while updating sites
+ add_task(async function() {
+   let updatedPromise = promiseSiteDataManagerSitesUpdated();
+   await openPreferencesViaOpenPreferencesAPI("privacy", { leaveOpen: true });
+   await updatedPromise;
++  let cacheSize = await SiteDataManager.getCacheSize();
+ 
+   let actual = null;
+   let expected = null;
+   let doc = gBrowser.selectedBrowser.contentDocument;
+   let clearBtn = doc.getElementById("clearSiteDataButton");
+   let settingsButton = doc.getElementById("siteDataSettings");
+   let prefStrBundle = doc.getElementById("bundlePreferences");
+   let totalSiteDataSizeLabel = doc.getElementById("totalSiteDataSize");
+   is(clearBtn.disabled, false, "Should enable clear button after sites updated");
+   is(settingsButton.disabled, false, "Should enable settings button after sites updated");
+   await SiteDataManager.getTotalUsage()
+                        .then(usage => {
+                          actual = totalSiteDataSizeLabel.textContent;
+                          expected = prefStrBundle.getFormattedString(
+-                           "totalSiteDataSize", DownloadUtils.convertByteUnits(usage));
++                           "totalSiteDataSize1", DownloadUtils.convertByteUnits(usage + cacheSize));
+                           is(actual, expected, "Should show the right total site data size");
+                        });
+ 
+   Services.obs.notifyObservers(null, "sitedatamanager:updating-sites");
+   is(clearBtn.disabled, true, "Should disable clear button while updating sites");
+   is(settingsButton.disabled, true, "Should disable settings button while updating sites");
+   actual = totalSiteDataSizeLabel.textContent;
+-  expected = prefStrBundle.getString("loadingSiteDataSize");
++  expected = prefStrBundle.getString("loadingSiteDataSize1");
+   is(actual, expected, "Should show the loading message while updating");
+ 
+   Services.obs.notifyObservers(null, "sitedatamanager:sites-updated");
+   is(clearBtn.disabled, false, "Should enable clear button after sites updated");
+   is(settingsButton.disabled, false, "Should enable settings button after sites updated");
++  cacheSize = await SiteDataManager.getCacheSize();
+   await SiteDataManager.getTotalUsage()
+                        .then(usage => {
+                          actual = totalSiteDataSizeLabel.textContent;
+                          expected = prefStrBundle.getFormattedString(
+-                           "totalSiteDataSize", DownloadUtils.convertByteUnits(usage));
++                           "totalSiteDataSize1", DownloadUtils.convertByteUnits(usage + cacheSize));
+                           is(actual, expected, "Should show the right total site data size");
+                        });
+ 
+   await BrowserTestUtils.removeTab(gBrowser.selectedTab);
+ });
+ 
+ // Test clearing service wroker through the "Clear All" button
+ add_task(async function() {
+diff --git a/browser/components/preferences/in-content/advanced.js b/browser/components/preferences/in-content/advanced.js
+--- a/browser/components/preferences/in-content/advanced.js
++++ b/browser/components/preferences/in-content/advanced.js
+@@ -39,18 +39,16 @@ var gAdvancedPane = {
+       window.addEventListener("unload", onUnload);
+       Services.prefs.addObserver("app.update.", this);
+       this.updateReadPrefs();
+     }
+     if (AppConstants.MOZ_CRASHREPORTER) {
+       this.initSubmitCrashes();
+     }
+     this.updateOnScreenKeyboardVisibility();
+-    this.updateCacheSizeInputField();
+-    this.updateActualCacheSize();
+ 
+     Services.obs.addObserver(this, "sitedatamanager:sites-updated");
+     Services.obs.addObserver(this, "sitedatamanager:updating-sites");
+     let unload = () => {
+       window.removeEventListener("unload", unload);
+       Services.obs.removeObserver(this, "sitedatamanager:sites-updated");
+       Services.obs.removeObserver(this, "sitedatamanager:updating-sites");
+     };
+@@ -66,30 +64,26 @@ var gAdvancedPane = {
+ 
+     setEventListener("layers.acceleration.disabled", "change",
+                      gAdvancedPane.updateHardwareAcceleration);
+     setEventListener("advancedPrefs", "select",
+                      gAdvancedPane.tabSelectionChanged);
+ 
+     setEventListener("connectionSettings", "command",
+                      gAdvancedPane.showConnections);
+-    setEventListener("clearCacheButton", "command",
+-                     gAdvancedPane.clearCache);
+     if (AppConstants.MOZ_UPDATER) {
+       setEventListener("updateRadioGroup", "command",
+                        gAdvancedPane.updateWritePrefs);
+       setEventListener("showUpdateHistory", "command",
+                        gAdvancedPane.showUpdates);
+     }
+     setEventListener("viewCertificatesButton", "command",
+                      gAdvancedPane.showCertificates);
+     setEventListener("viewSecurityDevicesButton", "command",
+                      gAdvancedPane.showSecurityDevices);
+-    setEventListener("cacheSize", "change",
+-                     gAdvancedPane.updateCacheSizePref);
+ 
+     if (AppConstants.MOZ_WIDGET_GTK) {
+       // GTK tabbox' allow the scroll wheel to change the selected tab,
+       // but we don't want this behavior for the in-content preferences.
+       let tabsElement = document.getElementById("tabsElement");
+       tabsElement.addEventListener("DOMMouseScroll", event => {
+         event.stopPropagation();
+       }, true);
+@@ -278,109 +272,30 @@ var gAdvancedPane = {
+ 
+   toggleSiteData(shouldShow) {
+     let clearButton = document.getElementById("clearSiteDataButton");
+     let settingsButton = document.getElementById("siteDataSettings");
+     clearButton.disabled = !shouldShow;
+     settingsButton.disabled = !shouldShow;
+   },
+ 
+-  updateTotalDataSizeLabel(usage) {
+-    let prefStrBundle = document.getElementById("bundlePreferences");
++  showSiteDataLoading() {
+     let totalSiteDataSizeLabel = document.getElementById("totalSiteDataSize");
+-    if (usage < 0) {
+-      totalSiteDataSizeLabel.textContent = prefStrBundle.getString("loadingSiteDataSize");
+-    } else {
+-      let size = DownloadUtils.convertByteUnits(usage);
+-      totalSiteDataSizeLabel.textContent = prefStrBundle.getFormattedString("totalSiteDataSize", size);
+-    }
+-  },
+-
+-  // Retrieves the amount of space currently used by disk cache
+-  updateActualCacheSize() {
+-    var actualSizeLabel = document.getElementById("actualDiskCacheSize");
+-    var prefStrBundle = document.getElementById("bundlePreferences");
+-
+-    // Needs to root the observer since cache service keeps only a weak reference.
+-    this.observer = {
+-      onNetworkCacheDiskConsumption(consumption) {
+-        var size = DownloadUtils.convertByteUnits(consumption);
+-        // The XBL binding for the string bundle may have been destroyed if
+-        // the page was closed before this callback was executed.
+-        if (!prefStrBundle.getFormattedString) {
+-          return;
+-        }
+-        actualSizeLabel.value = prefStrBundle.getFormattedString("actualDiskCacheSize", size);
+-      },
+-
+-      QueryInterface: XPCOMUtils.generateQI([
+-        Ci.nsICacheStorageConsumptionObserver,
+-        Ci.nsISupportsWeakReference
+-      ])
+-    };
+-
+-    actualSizeLabel.value = prefStrBundle.getString("actualDiskCacheSizeCalculated");
+-
+-    try {
+-      var cacheService =
+-        Cc["@mozilla.org/netwerk/cache-storage-service;1"]
+-          .getService(Ci.nsICacheStorageService);
+-      cacheService.asyncGetDiskConsumption(this.observer);
+-    } catch (e) {}
++    let prefStrBundle = document.getElementById("bundlePreferences");
++    totalSiteDataSizeLabel.textContent = prefStrBundle.getString("loadingSiteDataSize1");
+   },
+ 
+-  updateCacheSizeUI(smartSizeEnabled) {
+-    document.getElementById("useCacheBefore").disabled = smartSizeEnabled;
+-    document.getElementById("cacheSize").disabled = smartSizeEnabled;
+-    document.getElementById("useCacheAfter").disabled = smartSizeEnabled;
+-  },
+-
+-  readSmartSizeEnabled() {
+-    // The smart_size.enabled preference element is inverted="true", so its
+-    // value is the opposite of the actual pref value
+-    var disabled = document.getElementById("browser.cache.disk.smart_size.enabled").value;
+-    this.updateCacheSizeUI(!disabled);
+-  },
+-
+-  /**
+-   * Converts the cache size from units of KB to units of MB and stores it in
+-   * the textbox element.
+-   */
+-  updateCacheSizeInputField() {
+-    let cacheSizeElem = document.getElementById("cacheSize");
+-    let cachePref = document.getElementById("browser.cache.disk.capacity");
+-    cacheSizeElem.value = cachePref.value / 1024;
+-    if (cachePref.locked)
+-      cacheSizeElem.disabled = true;
+-  },
+-
+-  /**
+-   * Updates the cache size preference once user enters a new value.
+-   * We intentionally do not set preference="browser.cache.disk.capacity"
+-   * onto the textbox directly, as that would update the pref at each keypress
+-   * not only after the final value is entered.
+-   */
+-  updateCacheSizePref() {
+-    let cacheSizeElem = document.getElementById("cacheSize");
+-    let cachePref = document.getElementById("browser.cache.disk.capacity");
+-    // Converts the cache size as specified in UI (in MB) to KB.
+-    let intValue = parseInt(cacheSizeElem.value, 10);
+-    cachePref.value = isNaN(intValue) ? 0 : intValue * 1024;
+-  },
+-
+-  /**
+-   * Clears the cache.
+-   */
+-  clearCache() {
+-    try {
+-      var cache = Cc["@mozilla.org/netwerk/cache-storage-service;1"]
+-                    .getService(Ci.nsICacheStorageService);
+-      cache.clear();
+-    } catch (ex) {}
+-    this.updateActualCacheSize();
++  updateTotalDataSizeLabel(siteDataUsage) {
++    SiteDataManager.getCacheSize().then(function(cacheUsage) {
++      let prefStrBundle = document.getElementById("bundlePreferences");
++      let totalSiteDataSizeLabel = document.getElementById("totalSiteDataSize");
++      let totalUsage = siteDataUsage + cacheUsage;
++      let size = DownloadUtils.convertByteUnits(totalUsage);
++      totalSiteDataSizeLabel.textContent = prefStrBundle.getFormattedString("totalSiteDataSize1", size);
++    });
+   },
+ 
+   clearSiteData() {
+     gSubDialog.open("chrome://browser/content/preferences/clearSiteData.xul");
+   },
+ 
+   // UPDATE TAB
+ 
+@@ -521,17 +436,17 @@ var gAdvancedPane = {
+     switch (aTopic) {
+       case "nsPref:changed":
+         this.updateReadPrefs();
+         break;
+ 
+       case "sitedatamanager:updating-sites":
+         // While updating, we want to disable this section and display loading message until updated
+         this.toggleSiteData(false);
+-        this.updateTotalDataSizeLabel(-1);
++        this.showSiteDataLoading();
+         break;
+ 
+       case "sitedatamanager:sites-updated":
+         this.toggleSiteData(true);
+         SiteDataManager.getTotalUsage()
+           .then(this.updateTotalDataSizeLabel.bind(this));
+         break;
+     }
+diff --git a/browser/components/preferences/in-content/advanced.xul b/browser/components/preferences/in-content/advanced.xul
+--- a/browser/components/preferences/in-content/advanced.xul
++++ b/browser/components/preferences/in-content/advanced.xul
+@@ -40,26 +40,16 @@
+ 
+   <!-- Data Choices tab -->
+ #ifdef MOZ_CRASHREPORTER
+   <preference id="browser.crashReports.unsubmittedCheck.autoSubmit2"
+               name="browser.crashReports.unsubmittedCheck.autoSubmit2"
+               type="bool"/>
+ #endif
+ 
+-  <!-- Network tab -->
+-  <preference id="browser.cache.disk.capacity"
+-              name="browser.cache.disk.capacity"
+-              type="int"/>
+-
+-  <preference id="browser.cache.disk.smart_size.enabled"
+-              name="browser.cache.disk.smart_size.enabled"
+-              inverted="true"
+-              type="bool"/>
+-
+  <!-- Update tab -->
+ #ifdef MOZ_UPDATER
+   <preference id="app.update.enabled"
+               name="app.update.enabled"
+               type="bool"/>
+   <preference id="app.update.auto"
+               name="app.update.auto"
+               type="bool"/>
+@@ -206,43 +196,16 @@
+ 
+         <hbox align="center">
+           <description flex="1" control="connectionSettings">&connectionDesc.label;</description>
+           <button id="connectionSettings" icon="network" label="&connectionSettings.label;"
+                   accesskey="&connectionSettings.accesskey;"/>
+         </hbox>
+       </groupbox>
+ 
+-      <!-- Cache -->
+-      <groupbox id="cacheGroup">
+-        <caption><label>&httpCache.label;</label></caption>
+-
+-        <hbox align="center">
+-          <label id="actualDiskCacheSize" flex="1"/>
+-          <button id="clearCacheButton" icon="clear"
+-                  label="&clearCacheNow.label;" accesskey="&clearCacheNow.accesskey;"/>
+-        </hbox>
+-        <hbox>
+-          <checkbox preference="browser.cache.disk.smart_size.enabled"
+-                    id="allowSmartSize"
+-                    onsyncfrompreference="return gAdvancedPane.readSmartSizeEnabled();"
+-                    label="&overrideSmartCacheSize.label;"
+-                    accesskey="&overrideSmartCacheSize.accesskey;"/>
+-        </hbox>
+-        <hbox align="center" class="indent">
+-          <label id="useCacheBefore" control="cacheSize"
+-                 accesskey="&limitCacheSizeBefore.accesskey;">
+-            &limitCacheSizeBefore.label;
+-          </label>
+-          <textbox id="cacheSize" type="number" size="4" max="1024"
+-                   aria-labelledby="useCacheBefore cacheSize useCacheAfter"/>
+-          <label id="useCacheAfter" flex="1">&limitCacheSizeAfter.label;</label>
+-        </hbox>
+-      </groupbox>
+-
+       <!-- Site Data -->
+       <groupbox id="siteDataGroup">
+         <caption><label>&siteData.label;</label></caption>
+ 
+         <hbox align="baseline">
+           <label id="totalSiteDataSize"></label>
+           <label id="siteDataLearnMoreLink" class="learnMore text-link" value="&siteDataLearnMoreLink.label;"></label>
+           <spacer flex="1" />
+diff --git a/browser/components/preferences/in-content/tests/browser_bug795764_cachedisabled.js b/browser/components/preferences/in-content/tests/browser_bug795764_cachedisabled.js
+--- a/browser/components/preferences/in-content/tests/browser_bug795764_cachedisabled.js
++++ b/browser/components/preferences/in-content/tests/browser_bug795764_cachedisabled.js
+@@ -11,19 +11,16 @@ function test() {
+   // Otherwise, without any site then it would just return so we would end up in not testing SiteDataManager.
+   let principal = Services.scriptSecurityManager.createCodebasePrincipalFromOrigin("https://www.foo.com");
+   Services.perms.addFromPrincipal(principal, "persistent-storage", Ci.nsIPermissionManager.ALLOW_ACTION);
+   registerCleanupFunction(function() {
+     Services.perms.removeFromPrincipal(principal, "persistent-storage");
+   });
+ 
+   SpecialPowers.pushPrefEnv({set: [
+-    ["browser.cache.offline.enable", false],
+-    ["browser.cache.disk.enable", false],
+-    ["browser.cache.memory.enable", false]
+   ]}).then(() => open_preferences(runTest));
+ }
+ 
+ function runTest(win) {
+   is(gBrowser.currentURI.spec, "about:preferences", "about:preferences loaded");
+ 
+   let tab = win.document;
+   let elements = tab.getElementById("mainPrefPane").children;
+diff --git a/browser/components/preferences/in-content/tests/browser_clearSiteData.js b/browser/components/preferences/in-content/tests/browser_clearSiteData.js
+--- a/browser/components/preferences/in-content/tests/browser_clearSiteData.js
++++ b/browser/components/preferences/in-content/tests/browser_clearSiteData.js
+@@ -11,45 +11,16 @@ const { DownloadUtils } = ChromeUtils.im
+ const TEST_QUOTA_USAGE_HOST = "example.com";
+ const TEST_QUOTA_USAGE_ORIGIN = "https://" + TEST_QUOTA_USAGE_HOST;
+ const TEST_QUOTA_USAGE_URL = TEST_QUOTA_USAGE_ORIGIN + "/browser/browser/components/preferences/in-content/tests/site_data_test.html";
+ const TEST_OFFLINE_HOST = "example.org";
+ const TEST_OFFLINE_ORIGIN = "https://" + TEST_OFFLINE_HOST;
+ const TEST_OFFLINE_URL = TEST_OFFLINE_ORIGIN + "/browser/browser/components/preferences/in-content/tests/offline/offline.html";
+ const TEST_SERVICE_WORKER_URL = TEST_OFFLINE_ORIGIN + "/browser/browser/components/preferences/in-content/tests/service_worker_test.html";
+ 
+-// XXX: The intermittent bug 1331851
+-// The implementation of nsICacheStorageConsumptionObserver must be passed as weak referenced,
+-// so we must hold this observer here well. If we didn't, there would be a chance that
+-// in Linux debug test run the observer was released before the operation at gecko was completed
+-// (may be because of a relatively quicker GC cycle or a relatively slower operation).
+-// As a result of that, we would never get the cache usage we want so the test would fail from timeout.
+-const cacheUsageGetter = {
+-  _promise: null,
+-  _resolve: null,
+-  get() {
+-    if (!this._promise) {
+-      this._promise = new Promise(resolve => {
+-        this._resolve = resolve;
+-        Services.cache2.asyncGetDiskConsumption(this);
+-      });
+-    }
+-    return this._promise;
+-  },
+-  // nsICacheStorageConsumptionObserver implementations
+-  onNetworkCacheDiskConsumption(usage) {
+-    cacheUsageGetter._promise = null;
+-    cacheUsageGetter._resolve(usage);
+-  },
+-  QueryInterface: XPCOMUtils.generateQI([
+-    Ci.nsICacheStorageConsumptionObserver,
+-    Ci.nsISupportsWeakReference
+-  ]),
+-};
+-
+ async function testClearData(clearSiteData, clearCache) {
+   let quotaURI = Services.io.newURI(TEST_QUOTA_USAGE_ORIGIN);
+   SitePermissions.set(quotaURI, "persistent-storage", SitePermissions.ALLOW);
+ 
+   // Open a test site which saves into appcache.
+   await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_OFFLINE_URL);
+   await BrowserTestUtils.removeTab(gBrowser.selectedTab);
+ 
+@@ -64,23 +35,28 @@ async function testClearData(clearSiteDa
+ 
+   // Register some service workers.
+   await loadServiceWorkerTestPage(TEST_SERVICE_WORKER_URL);
+   await promiseServiceWorkerRegisteredFor(TEST_SERVICE_WORKER_URL);
+ 
+   await openPreferencesViaOpenPreferencesAPI("advanced", "networkTab", { leaveOpen: true });
+ 
+   // Test the initial states.
+-  let cacheUsage = await cacheUsageGetter.get();
++  let cacheUsage = await SiteDataManager.getCacheSize();
+   let quotaUsage = await getQuotaUsage(TEST_QUOTA_USAGE_ORIGIN);
+   let totalUsage = await SiteDataManager.getTotalUsage();
+   Assert.greater(cacheUsage, 0, "The cache usage should not be 0");
+   Assert.greater(quotaUsage, 0, "The quota usage should not be 0");
+   Assert.greater(totalUsage, 0, "The total usage should not be 0");
+ 
++  let initialSizeLabelValue = await ContentTask.spawn(gBrowser.selectedBrowser, null, async function() {
++    let sizeLabel = content.document.getElementById("totalSiteDataSize");
++    return sizeLabel.textContent;
++  });
++
+   let doc = gBrowser.selectedBrowser.contentDocument;
+   let clearSiteDataButton = doc.getElementById("clearSiteDataButton");
+ 
+   let dialogOpened = promiseLoadSubDialog("chrome://browser/content/preferences/clearSiteData.xul");
+   clearSiteDataButton.doCommand();
+   let dialogWin = await dialogOpened;
+ 
+   // Convert the usage numbers in the same way the UI does it to assert
+@@ -140,47 +116,48 @@ async function testClearData(clearSiteDa
+   if (clearSiteData) {
+     await acceptPromise;
+   }
+ 
+   await dialogClosed;
+ 
+   if (clearCache) {
+     TestUtils.waitForCondition(async function() {
+-      let usage = await cacheUsageGetter.get();
++      let usage = await SiteDataManager.getCacheSize();
+       return usage == 0;
+     }, "The cache usage should be removed");
+   } else {
+-    Assert.greater(await cacheUsageGetter.get(), 0, "The cache usage should not be 0");
++    Assert.greater(await SiteDataManager.getCacheSize(), 0, "The cache usage should not be 0");
+   }
+ 
+   if (clearSiteData) {
+     await updatePromise;
+     await cookiesClearedPromise;
+     await promiseServiceWorkersCleared();
+ 
+     TestUtils.waitForCondition(async function() {
+       let usage = await SiteDataManager.getTotalUsage();
+       return usage == 0;
+     }, "The total usage should be removed");
+-
+-    // Check that the size label in about:preferences updates after we cleared data.
+-    await ContentTask.spawn(gBrowser.selectedBrowser, null, async function() {
+-      let sizeLabel = content.document.getElementById("totalSiteDataSize");
+-
+-      await ContentTaskUtils.waitForCondition(
+-        () => sizeLabel.textContent.includes("0"), "Site data size label should have updated.");
+-    });
+   } else {
+     quotaUsage = await getQuotaUsage(TEST_QUOTA_USAGE_ORIGIN);
+     totalUsage = await SiteDataManager.getTotalUsage();
+     Assert.greater(quotaUsage, 0, "The quota usage should not be 0");
+     Assert.greater(totalUsage, 0, "The total usage should not be 0");
+   }
+ 
++  if (clearCache || clearSiteData) {
++    // Check that the size label in about:preferences updates after we cleared data.
++    await ContentTask.spawn(gBrowser.selectedBrowser, {initialSizeLabelValue}, async function(opts) {
++      let sizeLabel = content.document.getElementById("totalSiteDataSize");
++      await ContentTaskUtils.waitForCondition(
++        () => sizeLabel.textContent != opts.initialSizeLabelValue, "Site data size label should have updated.");
++    });
++  }
++
+   let desiredPermissionState = clearSiteData ? SitePermissions.UNKNOWN : SitePermissions.ALLOW;
+   let permission = SitePermissions.get(quotaURI, "persistent-storage");
+   is(permission.state, desiredPermissionState, "Should have the correct permission state.");
+ 
+   await BrowserTestUtils.removeTab(gBrowser.selectedTab);
+   await SiteDataManager.removeAll();
+ }
+ 
+diff --git a/browser/components/preferences/in-content/tests/browser_siteData.js b/browser/components/preferences/in-content/tests/browser_siteData.js
+--- a/browser/components/preferences/in-content/tests/browser_siteData.js
++++ b/browser/components/preferences/in-content/tests/browser_siteData.js
+@@ -60,49 +60,51 @@ add_task(async function() {
+   await BrowserTestUtils.removeTab(gBrowser.selectedTab);
+ });
+ 
+ // Test buttons are disabled and loading message shown while updating sites
+ add_task(async function() {
+   let updatedPromise = promiseSitesUpdated();
+   await openPreferencesViaOpenPreferencesAPI("advanced", "networkTab", { leaveOpen: true });
+   await updatedPromise;
++  let cacheSize = await SiteDataManager.getCacheSize();
+ 
+   let actual = null;
+   let expected = null;
+   let doc = gBrowser.selectedBrowser.contentDocument;
+   let clearBtn = doc.getElementById("clearSiteDataButton");
+   let settingsButton = doc.getElementById("siteDataSettings");
+   let prefStrBundle = doc.getElementById("bundlePreferences");
+   let totalSiteDataSizeLabel = doc.getElementById("totalSiteDataSize");
+   is(clearBtn.disabled, false, "Should enable clear button after sites updated");
+   is(settingsButton.disabled, false, "Should enable settings button after sites updated");
+   await SiteDataManager.getTotalUsage()
+                        .then(usage => {
+                          actual = totalSiteDataSizeLabel.textContent;
+                          expected = prefStrBundle.getFormattedString(
+-                           "totalSiteDataSize", DownloadUtils.convertByteUnits(usage));
++                           "totalSiteDataSize1", DownloadUtils.convertByteUnits(usage + cacheSize));
+                           is(actual, expected, "Should show the right total site data size");
+                        });
+ 
+   Services.obs.notifyObservers(null, "sitedatamanager:updating-sites");
+   is(clearBtn.disabled, true, "Should disable clear button while updating sites");
+   is(settingsButton.disabled, true, "Should disable settings button while updating sites");
+   actual = totalSiteDataSizeLabel.textContent;
+-  expected = prefStrBundle.getString("loadingSiteDataSize");
++  expected = prefStrBundle.getString("loadingSiteDataSize1");
+   is(actual, expected, "Should show the loading message while updating");
+ 
+   Services.obs.notifyObservers(null, "sitedatamanager:sites-updated");
+   is(clearBtn.disabled, false, "Should enable clear button after sites updated");
+   is(settingsButton.disabled, false, "Should enable settings button after sites updated");
++  cacheSize = await SiteDataManager.getCacheSize();
+   await SiteDataManager.getTotalUsage()
+                        .then(usage => {
+                           actual = totalSiteDataSizeLabel.textContent;
+                           expected = prefStrBundle.getFormattedString(
+-                           "totalSiteDataSize", DownloadUtils.convertByteUnits(usage));
++                           "totalSiteDataSize1", DownloadUtils.convertByteUnits(usage + cacheSize));
+                           is(actual, expected, "Should show the right total site data size");
+                        });
+ 
+   await BrowserTestUtils.removeTab(gBrowser.selectedTab);
+ });
+ 
+ // Test clearing service wroker through the "Clear All" button
+ add_task(async function() {
+diff --git a/browser/locales/en-US/chrome/browser/preferences/advanced.dtd b/browser/locales/en-US/chrome/browser/preferences/advanced.dtd
+--- a/browser/locales/en-US/chrome/browser/preferences/advanced.dtd
++++ b/browser/locales/en-US/chrome/browser/preferences/advanced.dtd
+@@ -35,40 +35,24 @@
+ <!ENTITY networkTab.label                "Network">
+ 
+ <!ENTITY networkProxy.label              "Network Proxy">
+ 
+ <!ENTITY connectionDesc.label            "Configure how &brandShortName; connects to the Internet">
+ <!ENTITY connectionSettings.label        "Settings…">
+ <!ENTITY connectionSettings.accesskey    "e">
+ 
+-<!ENTITY httpCache.label                 "Cached Web Content">
+-
+ <!--  Site Data section manages sites using Storage API and is under Network -->
+ <!ENTITY siteData.label                  "Site Data">
+ <!ENTITY clearSiteData.label             "Clear All Data">
+ <!ENTITY clearSiteData.accesskey         "l">
+ <!ENTITY siteDataSettings.label          "Settings…">
+ <!ENTITY siteDataSettings.accesskey      "i">
+ <!ENTITY siteDataLearnMoreLink.label     "Learn more">
+ 
+-<!-- LOCALIZATION NOTE:
+-  The entities limitCacheSizeBefore.label and limitCacheSizeAfter.label appear on a single
+-  line in preferences as follows:
+-
+-  &limitCacheSizeBefore.label [textbox for cache size in MB] &limitCacheSizeAfter.label;
+--->
+-<!ENTITY limitCacheSizeBefore.label      "Limit cache to">
+-<!ENTITY limitCacheSizeBefore.accesskey  "L">
+-<!ENTITY limitCacheSizeAfter.label       "MB of space">
+-<!ENTITY clearCacheNow.label             "Clear Now">
+-<!ENTITY clearCacheNow.accesskey         "C">
+-<!ENTITY overrideSmartCacheSize.label    "Override automatic cache management">
+-<!ENTITY overrideSmartCacheSize.accesskey "O">
+-
+ <!ENTITY updateTab.label                 "Update">
+ 
+ <!-- LOCALIZATION NOTE (updateApplication.label):
+   Strings from aboutDialog.dtd are displayed in this section of the preferences.
+   Please check for possible accesskey conflicts.
+ -->
+ <!ENTITY updateApplication.label         "&brandShortName; Updates">
+ <!-- LOCALIZATION NOTE (updateApplication.version.*): updateApplication.version.pre
+diff --git a/browser/locales/en-US/chrome/browser/preferences/preferences.properties b/browser/locales/en-US/chrome/browser/preferences/preferences.properties
+--- a/browser/locales/en-US/chrome/browser/preferences/preferences.properties
++++ b/browser/locales/en-US/chrome/browser/preferences/preferences.properties
+@@ -150,38 +150,22 @@ removeAllShownCookies.accesskey=A
+ # you can use #1 in your localization as a placeholder for the number.
+ # For example this is the English string with numbers:
+ # removeSelectedCookied=Remove #1 Selected;Remove #1 Selected
+ removeSelectedCookies.label=Remove Selected;Remove Selected
+ removeSelectedCookies.accesskey=R
+ 
+ defaultUserContextLabel=None
+ 
+-####Preferences::Advanced::Network
+-#LOCALIZATION NOTE: The next string is for the disk usage of the web content cache.
+-#   e.g., "Your web content cache is currently using 200 MB"
+-#   %1$S = size
+-#   %2$S = unit (MB, KB, etc.)
+-actualDiskCacheSize=Your web content cache is currently using %1$S %2$S of disk space
+-actualDiskCacheSizeCalculated=Calculating web content cache size…
+-
+-####Preferences::Advanced::Network
+-#LOCALIZATION NOTE: The next string is for the disk usage of the application cache.
+-#   e.g., "Your application cache is currently using 200 MB"
+-#   %1$S = size
+-#   %2$S = unit (MB, KB, etc.)
+-actualAppCacheSize=Your application cache is currently using %1$S %2$S of disk space
+-
+-####Preferences::Advanced::Network
+ #LOCALIZATION NOTE: The next string is for the total usage of site data.
+ #   e.g., "The total usage is currently using 200 MB"
+ #   %1$S = size
+ #   %2$S = unit (MB, KB, etc.)
+-totalSiteDataSize=Your stored site data is currently using %1$S %2$S of disk space
+-loadingSiteDataSize=Calculating site data size…
++totalSiteDataSize1=Your stored site data and cache are currently using %1$S %2$S of disk space
++loadingSiteDataSize1=Calculating site data and cache size…
+ persistent=Persistent
+ siteUsage=%1$S %2$S
+ acceptRemove=Remove
+ # LOCALIZATION NOTE (siteDataSettings2.description): %S = brandShortName
+ siteDataSettings2.description=The following websites store site data on your computer. %S keeps data from websites with persistent storage until you delete it, and deletes data from websites with non-persistent storage as space is needed.
+ # LOCALIZATION NOTE (removeAllSiteData, removeAllSiteDataShown):
+ # removeAllSiteData and removeAllSiteDataShown are both used on the same one button,
+ # never displayed together and can share the same accesskey.

+ 597 - 0
frg/mozilla-release/work-js/1422163-1-60a1.patch

@@ -0,0 +1,597 @@
+# HG changeset patch
+# User Johann Hofmann <jhofmann@mozilla.com>
+# Date 1515583480 -3600
+# Node ID fd5e712f297cfc164bc163bc366b9145065b6ecb
+# Parent  cf0b8c0f8a4908e04c3d4f22543c478062e94344
+Bug 1422163 - Part 1 - Make a new confirm dialog for clearing all site data that allows you to clear cache. r=Gijs
+
+MozReview-Commit-ID: G9xQXlfT9Ay
+
+diff --git a/browser/components/preferences/SiteDataManager.jsm b/browser/components/preferences/SiteDataManager.jsm
+--- a/browser/components/preferences/SiteDataManager.jsm
++++ b/browser/components/preferences/SiteDataManager.jsm
+@@ -10,42 +10,94 @@ ChromeUtils.defineModuleGetter(this, "Co
+ XPCOMUtils.defineLazyServiceGetter(this, "serviceWorkerManager",
+                                    "@mozilla.org/serviceworkers/manager;1",
+                                    "nsIServiceWorkerManager");
+ 
+ var EXPORTED_SYMBOLS = [
+   "SiteDataManager"
+ ];
+ 
++XPCOMUtils.defineLazyGetter(this, "gStringBundle", function() {
++  return Services.strings.createBundle("chrome://browser/locale/siteData.properties");
++});
++
++XPCOMUtils.defineLazyGetter(this, "gBrandBundle", function() {
++  return Services.strings.createBundle("chrome://branding/locale/brand.properties");
++});
++
+ var SiteDataManager = {
+ 
+   _qms: Services.qms,
+ 
+   _appCache: Cc["@mozilla.org/network/application-cache-service;1"].getService(Ci.nsIApplicationCacheService),
+ 
+   // A Map of sites and their disk usage according to Quota Manager and appcache
+   // Key is host (group sites based on host across scheme, port, origin atttributes).
+   // Value is one object holding:
+   //   - principals: instances of nsIPrincipal.
+   //   - persisted: the persistent-storage status.
+   //   - quotaUsage: the usage of indexedDB and localStorage.
+   //   - appCacheList: an array of app cache; instances of nsIApplicationCache
+   _sites: new Map(),
+ 
++  _getCacheSizeObserver: null,
++
++  _getCacheSizePromise: null,
++
+   _getQuotaUsagePromise: null,
+ 
+   _quotaUsageRequest: null,
+ 
+   async updateSites() {
+     Services.obs.notifyObservers(null, "sitedatamanager:updating-sites");
+     await this._getQuotaUsage();
+     this._updateAppCache();
+     Services.obs.notifyObservers(null, "sitedatamanager:sites-updated");
+   },
+ 
++  /**
++   * Retrieves the amount of space currently used by disk cache.
++   *
++   * You can use DownloadUtils.convertByteUnits to convert this to
++   * a user-understandable size/unit combination.
++   *
++   * @returns a Promise that resolves with the cache size on disk in bytes.
++   */
++  getCacheSize() {
++    if (this._getCacheSizePromise) {
++      return this._getCacheSizePromise;
++    }
++
++    this._getCacheSizePromise = new Promise((resolve, reject) => {
++      // Needs to root the observer since cache service keeps only a weak reference.
++      this._getCacheSizeObserver = {
++        onNetworkCacheDiskConsumption: consumption => {
++          resolve(consumption);
++          this._getCacheSizePromise = null;
++          this._getCacheSizeObserver = null;
++        },
++
++        QueryInterface: XPCOMUtils.generateQI([
++          Ci.nsICacheStorageConsumptionObserver,
++          Ci.nsISupportsWeakReference
++        ])
++      };
++
++      try {
++        Services.cache2.asyncGetDiskConsumption(this._getCacheSizeObserver);
++      } catch (e) {
++        reject(e);
++        this._getCacheSizePromise = null;
++        this._getCacheSizeObserver = null;
++      }
++    });
++
++    return this._getCacheSizePromise;
++  },
++
+   _getQuotaUsage() {
+     // Clear old data and requests first
+     this._sites.clear();
+     this._cancelGetQuotaUsage();
+     this._getQuotaUsagePromise = new Promise(resolve => {
+       let onUsageResult = request => {
+         if (request.resultCode == Cr.NS_OK) {
+           let items = request.result;
+@@ -271,18 +323,67 @@ var SiteDataManager = {
+           })
+           .then(() => this.updateSites());
+     }
+     if (unknownHost) {
+       throw `SiteDataManager: removing unknown site of ${unknownHost}`;
+     }
+   },
+ 
++  /**
++   * In the specified window, shows a prompt for removing
++   * all site data, warning the user that this may log them
++   * out of websites.
++   *
++   * @param {mozIDOMWindowProxy} a parent DOM window to host the dialog.
++   * @returns a boolean whether the user confirmed the prompt.
++   */
++  promptSiteDataRemoval(win) {
++    let brandName = gBrandBundle.GetStringFromName("brandShortName");
++    let flags =
++      Services.prompt.BUTTON_TITLE_IS_STRING * Services.prompt.BUTTON_POS_0 +
++      Services.prompt.BUTTON_TITLE_CANCEL * Services.prompt.BUTTON_POS_1 +
++      Services.prompt.BUTTON_POS_0_DEFAULT;
++    let title = gStringBundle.GetStringFromName("clearSiteDataPromptTitle");
++    let text = gStringBundle.formatStringFromName("clearSiteDataPromptText", [brandName], 1);
++    let btn0Label = gStringBundle.GetStringFromName("clearSiteDataNow");
++
++    let result = Services.prompt.confirmEx(
++      win, title, text, flags, btn0Label, null, null, null, {});
++    return result == 0;
++  },
++
++  /**
++   * Clears all site data and cache
++   *
++   * @returns a Promise that resolves when the data is cleared.
++   */
+   async removeAll() {
++    this.removeCache();
++    return this.removeSiteData();
++  },
++
++  /**
++   * Clears the entire network cache.
++   */
++  removeCache() {
+     Services.cache2.clear();
++  },
++
++  /**
++   * Clears all site data, which currently means
++   *   - Cookies
++   *   - AppCache
++   *   - ServiceWorkers
++   *   - Quota Managed Storage
++   *   - persistent-storage permissions
++   *
++   * @returns a Promise that resolves with the cache size on disk in bytes
++   */
++  async removeSiteData() {
+     Services.cookies.removeAll();
+     OfflineAppCacheHelper.clear();
+ 
+     // Iterate through the service workers and remove them.
+     let promises = [];
+     let serviceWorkers = serviceWorkerManager.getAllRegistrations();
+     for (let i = 0; i < serviceWorkers.length; i++) {
+       let sw = serviceWorkers.queryElementAt(i, Ci.nsIServiceWorkerRegistrationInfo);
+diff --git a/browser/components/preferences/clearSiteData.css b/browser/components/preferences/clearSiteData.css
+new file mode 100644
+--- /dev/null
++++ b/browser/components/preferences/clearSiteData.css
+@@ -0,0 +1,19 @@
++/* This Source Code Form is subject to the terms of the Mozilla Public
++ * License, v. 2.0. If a copy of the MPL was not distributed with this
++ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
++
++.options-container {
++  background-color: var(--in-content-box-background);
++  border: 1px solid var(--in-content-box-border-color);
++  border-radius: 2px;
++  color: var(--in-content-text-color);
++  padding: 0.5em;
++}
++
++.option {
++  padding-bottom: 16px;
++}
++
++.option-description {
++  color: #737373;
++}
+diff --git a/browser/components/preferences/clearSiteData.js b/browser/components/preferences/clearSiteData.js
+new file mode 100644
+--- /dev/null
++++ b/browser/components/preferences/clearSiteData.js
+@@ -0,0 +1,78 @@
++/* This Source Code Form is subject to the terms of the Mozilla Public
++ * License, v. 2.0. If a copy of the MPL was not distributed with this
++ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
++
++ChromeUtils.import("resource://gre/modules/Services.jsm");
++ChromeUtils.import("resource://gre/modules/AppConstants.jsm");
++ChromeUtils.import("resource://gre/modules/DownloadUtils.jsm");
++ChromeUtils.import("resource:///modules/SiteDataManager.jsm");
++
++var gClearSiteDataDialog = {
++  _clearSiteDataCheckbox: null,
++  _clearCacheCheckbox: null,
++  _clearButton: null,
++
++  init() {
++    this._bundle = Services.strings
++      .createBundle("chrome://browser/locale/preferences/clearSiteData.properties");
++
++    SiteDataManager.getTotalUsage().then(bytes => {
++      // Size is an array of amount and unit, e.g. [20, "MB"].
++      let size = DownloadUtils.convertByteUnits(bytes);
++      document.getElementById("clearSiteDataLabel").value =
++        this._bundle.formatStringFromName("clearSiteDataWithEstimates.label", size, 2);
++    });
++    SiteDataManager.getCacheSize().then(bytes => {
++      // Size is an array of amount and unit, e.g. [20, "MB"].
++      let size = DownloadUtils.convertByteUnits(bytes);
++      document.getElementById("clearCacheLabel").value =
++        this._bundle.formatStringFromName("clearCacheWithEstimates.label", size, 2);
++    });
++
++    this._clearButton = document.getElementById("clearButton");
++    this._cancelButton = document.getElementById("cancelButton");
++    this._clearSiteDataCheckbox = document.getElementById("clearSiteData");
++    this._clearCacheCheckbox = document.getElementById("clearCache");
++
++    window.addEventListener("keypress", this.onWindowKeyPress);
++
++    this._cancelButton.addEventListener("command", window.close);
++    this._clearButton.addEventListener("command", () => this.onClear());
++
++    this._clearSiteDataCheckbox.addEventListener("command", e => this.onCheckboxCommand(e));
++    this._clearCacheCheckbox.addEventListener("command", e => this.onCheckboxCommand(e));
++  },
++
++  onWindowKeyPress(event) {
++    if (event.keyCode == KeyEvent.DOM_VK_ESCAPE)
++      window.close();
++  },
++
++  onCheckboxCommand(event) {
++    this._clearButton.disabled =
++      !(this._clearSiteDataCheckbox.checked || this._clearCacheCheckbox.checked);
++  },
++
++  onClear() {
++    let allowed = true;
++
++    if (this._clearSiteDataCheckbox.checked) {
++      allowed = SiteDataManager.promptSiteDataRemoval(window);
++      if (allowed) {
++        SiteDataManager.removeSiteData();
++      }
++    }
++
++    if (this._clearCacheCheckbox.checked && allowed) {
++      SiteDataManager.removeCache();
++      // Update cache UI in about:preferences
++      window.opener.gPrivacyPane.updateActualCacheSize();
++    }
++
++    if (allowed) {
++      window.close();
++    }
++  },
++};
++
++window.addEventListener("load", () => gClearSiteDataDialog.init());
+diff --git a/browser/components/preferences/clearSiteData.xul b/browser/components/preferences/clearSiteData.xul
+new file mode 100644
+--- /dev/null
++++ b/browser/components/preferences/clearSiteData.xul
+@@ -0,0 +1,64 @@
++<?xml version="1.0"?>
++
++<!-- This Source Code Form is subject to the terms of the Mozilla Public
++   - License, v. 2.0. If a copy of the MPL was not distributed with this
++   - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
++
++<?xml-stylesheet href="chrome://global/skin/" type="text/css"?>
++<?xml-stylesheet href="chrome://browser/skin/preferences/preferences.css" type="text/css"?>
++<?xml-stylesheet href="chrome://browser/content/preferences/clearSiteData.css" type="text/css"?>
++
++<!DOCTYPE dialog [
++ <!ENTITY % brandDTD SYSTEM "chrome://branding/locale/brand.dtd">
++ <!ENTITY % clearSiteDataDTD SYSTEM "chrome://browser/locale/preferences/clearSiteData.dtd">
++ %brandDTD;
++ %clearSiteDataDTD;
++]>
++
++<window id="ClearSiteDataDialog" class="windowDialog"
++        windowtype="Browser:ClearSiteData"
++        xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
++        style="width: &window.width;;"
++        title="&window.title;"
++        persist="screenX screenY width height">
++
++  <script src="chrome://browser/content/preferences/clearSiteData.js"/>
++
++  <stringbundle id="bundlePreferences"
++                src="chrome://browser/locale/preferences/preferences.properties"/>
++
++  <keyset>
++    <key key="&windowClose.key;" modifiers="accel" oncommand="window.close();"/>
++  </keyset>
++
++  <vbox class="contentPane largeDialogContainer" flex="1">
++    <description control="url">&window.description;</description>
++    <separator class="thin"/>
++    <vbox class="options-container">
++      <hbox class="option">
++        <checkbox id="clearSiteData" checked="true"
++                  accesskey="&clearSiteData.accesskey;" />
++        <vbox>
++          <label for="clearSiteData" id="clearSiteDataLabel" value="&clearSiteData.label;" />
++          <description class="option-description">&clearSiteData.description;</description>
++        </vbox>
++      </hbox>
++      <hbox class="option">
++        <checkbox id="clearCache" checked="true"
++                  accesskey="&clearCache.accesskey;" />
++        <vbox>
++          <label for="clearCache" id="clearCacheLabel" value="&clearCache.label;" />
++          <description class="option-description">&clearCache.description;</description>
++        </vbox>
++      </hbox>
++    </vbox>
++  </vbox>
++  <vbox>
++    <hbox class="actionButtons" align="right" flex="1">
++      <button id="cancelButton" icon="close"
++              label="&button.cancel.label;" accesskey="&button.cancel.accesskey;" />
++      <button id="clearButton" icon="save"
++              label="&button.clear.label;" accesskey="&button.clear.accesskey;"/>
++    </hbox>
++  </vbox>
++</window>
+diff --git a/browser/components/preferences/in-content-new/privacy.js b/browser/components/preferences/in-content-new/privacy.js
+--- a/browser/components/preferences/in-content-new/privacy.js
++++ b/browser/components/preferences/in-content-new/privacy.js
+@@ -1327,30 +1327,17 @@ var gPrivacyPane = {
+     let cacheSizeElem = document.getElementById("cacheSize");
+     let cachePref = document.getElementById("browser.cache.disk.capacity");
+     // Converts the cache size as specified in UI (in MB) to KB.
+     let intValue = parseInt(cacheSizeElem.value, 10);
+     cachePref.value = isNaN(intValue) ? 0 : intValue * 1024;
+   },
+ 
+   clearSiteData() {
+-    let flags =
+-      Services.prompt.BUTTON_TITLE_IS_STRING * Services.prompt.BUTTON_POS_0 +
+-      Services.prompt.BUTTON_TITLE_CANCEL * Services.prompt.BUTTON_POS_1 +
+-      Services.prompt.BUTTON_POS_0_DEFAULT;
+-    let prefStrBundle = document.getElementById("bundlePreferences");
+-    let title = prefStrBundle.getString("clearSiteDataPromptTitle");
+-    let text = prefStrBundle.getString("clearSiteDataPromptText");
+-    let btn0Label = prefStrBundle.getString("clearSiteDataNow");
+-
+-    let result = Services.prompt.confirmEx(
+-      window, title, text, flags, btn0Label, null, null, null, {});
+-    if (result == 0) {
+-      SiteDataManager.removeAll();
+-    }
++    gSubDialog.open("chrome://browser/content/preferences/clearSiteData.xul");
+   },
+ 
+   initDataCollection() {
+     this._setupLearnMoreLink("toolkit.datacollection.infoURL",
+                              "dataCollectionPrivacyNotice");
+   },
+ 
+   initSubmitCrashes() {
+diff --git a/browser/components/preferences/in-content/advanced.js b/browser/components/preferences/in-content/advanced.js
+--- a/browser/components/preferences/in-content/advanced.js
++++ b/browser/components/preferences/in-content/advanced.js
+@@ -374,30 +374,17 @@ var gAdvancedPane = {
+       var cache = Cc["@mozilla.org/netwerk/cache-storage-service;1"]
+                     .getService(Ci.nsICacheStorageService);
+       cache.clear();
+     } catch (ex) {}
+     this.updateActualCacheSize();
+   },
+ 
+   clearSiteData() {
+-    let flags =
+-      Services.prompt.BUTTON_TITLE_IS_STRING * Services.prompt.BUTTON_POS_0 +
+-      Services.prompt.BUTTON_TITLE_CANCEL * Services.prompt.BUTTON_POS_1 +
+-      Services.prompt.BUTTON_POS_0_DEFAULT;
+-    let prefStrBundle = document.getElementById("bundlePreferences");
+-    let title = prefStrBundle.getString("clearSiteDataPromptTitle");
+-    let text = prefStrBundle.getString("clearSiteDataPromptText");
+-    let btn0Label = prefStrBundle.getString("clearSiteDataNow");
+-
+-    let result = Services.prompt.confirmEx(
+-      window, title, text, flags, btn0Label, null, null, null, {});
+-    if (result == 0) {
+-      SiteDataManager.removeAll();
+-    }
++    gSubDialog.open("chrome://browser/content/preferences/clearSiteData.xul");
+   },
+ 
+   // UPDATE TAB
+ 
+   /*
+    * Preferences:
+    *
+    * app.update.enabled
+diff --git a/browser/components/preferences/jar.mn b/browser/components/preferences/jar.mn
+--- a/browser/components/preferences/jar.mn
++++ b/browser/components/preferences/jar.mn
+@@ -2,16 +2,19 @@
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ 
+ browser.jar:
+     content/browser/preferences/applicationManager.xul
+     content/browser/preferences/applicationManager.js
+     content/browser/preferences/blocklists.xul
+     content/browser/preferences/blocklists.js
++    content/browser/preferences/clearSiteData.css
++    content/browser/preferences/clearSiteData.js
++    content/browser/preferences/clearSiteData.xul
+ *   content/browser/preferences/colors.xul
+ *   content/browser/preferences/cookies.xul
+     content/browser/preferences/cookies.js
+ *   content/browser/preferences/connection.xul
+     content/browser/preferences/connection.js
+     content/browser/preferences/donottrack.xul
+ *   content/browser/preferences/fonts.xul
+     content/browser/preferences/fonts.js
+diff --git a/browser/components/preferences/siteDataSettings.js b/browser/components/preferences/siteDataSettings.js
+--- a/browser/components/preferences/siteDataSettings.js
++++ b/browser/components/preferences/siteDataSettings.js
+@@ -201,28 +201,17 @@ let gSiteDataSettings = {
+         removals.add(site.host);
+         return false;
+       }
+       return true;
+     });
+ 
+     if (removals.size > 0) {
+       if (this._sites.length == 0) {
+-        // User selects all sites so equivalent to clearing all data
+-        let flags =
+-          Services.prompt.BUTTON_TITLE_IS_STRING * Services.prompt.BUTTON_POS_0 +
+-          Services.prompt.BUTTON_TITLE_CANCEL * Services.prompt.BUTTON_POS_1 +
+-          Services.prompt.BUTTON_POS_0_DEFAULT;
+-        let prefStrBundle = document.getElementById("bundlePreferences");
+-        let title = prefStrBundle.getString("clearSiteDataPromptTitle");
+-        let text = prefStrBundle.getString("clearSiteDataPromptText");
+-        let btn0Label = prefStrBundle.getString("clearSiteDataNow");
+-        let result = Services.prompt.confirmEx(window, title, text, flags, btn0Label, null, null, null, {});
+-        allowed = result == 0;
+-        if (allowed) {
++        if (SiteDataManager.promptSiteDataRemoval(window)) {
+           SiteDataManager.removeAll();
+         }
+       } else {
+         // User only removes partial sites.
+         // We will remove cookies based on base domain, say, user selects "news.foo.com" to remove.
+         // The cookies under "music.foo.com" will be removed together.
+         // We have to prompt user about this action.
+         let hostsTable = new Map();
+diff --git a/browser/locales/en-US/chrome/browser/preferences/clearSiteData.dtd b/browser/locales/en-US/chrome/browser/preferences/clearSiteData.dtd
+new file mode 100644
+--- /dev/null
++++ b/browser/locales/en-US/chrome/browser/preferences/clearSiteData.dtd
+@@ -0,0 +1,22 @@
++<!-- This Source Code Form is subject to the terms of the Mozilla Public
++   - License, v. 2.0. If a copy of the MPL was not distributed with this
++   - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
++
++<!ENTITY window.title                 "Clear Data">
++<!ENTITY window.width                 "35em">
++
++<!ENTITY window.description           "Clearing all cookies and site data stored by &brandShortName; may sign you out of websites and remove offline web content. Clearing cache data will not affect your logins.">
++<!ENTITY windowClose.key              "w">
++
++<!ENTITY clearSiteData.label          "Cookies and Site Data">
++<!ENTITY clearSiteData.accesskey      "S">
++<!ENTITY clearSiteData.description    "You may get signed out of websites if cleared">
++
++<!ENTITY clearCache.label             "Cached Web Content">
++<!ENTITY clearCache.accesskey         "W">
++<!ENTITY clearCache.description       "Will require websites to reload images and data">
++
++<!ENTITY button.cancel.label          "Cancel">
++<!ENTITY button.cancel.accesskey      "C">
++<!ENTITY button.clear.label           "Clear">
++<!ENTITY button.clear.accesskey       "l">
+diff --git a/browser/locales/en-US/chrome/browser/preferences/clearSiteData.properties b/browser/locales/en-US/chrome/browser/preferences/clearSiteData.properties
+new file mode 100644
+--- /dev/null
++++ b/browser/locales/en-US/chrome/browser/preferences/clearSiteData.properties
+@@ -0,0 +1,12 @@
++# This Source Code Form is subject to the terms of the Mozilla Public
++# License, v. 2.0. If a copy of the MPL was not distributed with this
++# file, You can obtain one at http://mozilla.org/MPL/2.0/.
++
++# LOCALIZATION NOTE (clearSiteDataWithEstimates.label, clearCacheWithEstimates.label):
++#   The parameters in parentheses in these strings describe disk usage
++#   in the format (size unit), e.g. "Cookies and Site Data (24 KB)"
++#   %1$S = size
++#   %2$S = unit (MB, KB, etc.)
++clearSiteDataWithEstimates.label = Cookies and Site Data (%1$S %2$S)
++clearCacheWithEstimates.label = Cached Web Content (%1$S %2$S)
++
+diff --git a/browser/locales/en-US/chrome/browser/preferences/preferences.properties b/browser/locales/en-US/chrome/browser/preferences/preferences.properties
+--- a/browser/locales/en-US/chrome/browser/preferences/preferences.properties
++++ b/browser/locales/en-US/chrome/browser/preferences/preferences.properties
+@@ -172,19 +172,16 @@ actualAppCacheSize=Your application cach
+ 
+ ####Preferences::Advanced::Network
+ #LOCALIZATION NOTE: The next string is for the total usage of site data.
+ #   e.g., "The total usage is currently using 200 MB"
+ #   %1$S = size
+ #   %2$S = unit (MB, KB, etc.)
+ totalSiteDataSize=Your stored site data is currently using %1$S %2$S of disk space
+ loadingSiteDataSize=Calculating site data size…
+-clearSiteDataPromptTitle=Clear all cookies and site data
+-clearSiteDataPromptText=Selecting ‘Clear Now’ will clear all cookies and site data stored by Firefox. This may sign you out of websites and remove offline web content.
+-clearSiteDataNow=Clear Now
+ persistent=Persistent
+ siteUsage=%1$S %2$S
+ acceptRemove=Remove
+ # LOCALIZATION NOTE (siteDataSettings2.description): %S = brandShortName
+ siteDataSettings2.description=The following websites store site data on your computer. %S keeps data from websites with persistent storage until you delete it, and deletes data from websites with non-persistent storage as space is needed.
+ # LOCALIZATION NOTE (removeAllSiteData, removeAllSiteDataShown):
+ # removeAllSiteData and removeAllSiteDataShown are both used on the same one button,
+ # never displayed together and can share the same accesskey.
+diff --git a/browser/locales/en-US/chrome/browser/siteData.properties b/browser/locales/en-US/chrome/browser/siteData.properties
+new file mode 100644
+--- /dev/null
++++ b/browser/locales/en-US/chrome/browser/siteData.properties
+@@ -0,0 +1,8 @@
++# This Source Code Form is subject to the terms of the Mozilla Public
++# License, v. 2.0. If a copy of the MPL was not distributed with this
++# file, You can obtain one at http://mozilla.org/MPL/2.0/.
++
++clearSiteDataPromptTitle=Clear all cookies and site data
++# LOCALIZATION NOTE (clearSiteDataPromptText): %S = brandShortName
++clearSiteDataPromptText=Selecting ‘Clear Now’ will clear all cookies and site data stored by %S. This may sign you out of websites and remove offline web content.
++clearSiteDataNow=Clear Now
+diff --git a/browser/locales/jar.mn b/browser/locales/jar.mn
+--- a/browser/locales/jar.mn
++++ b/browser/locales/jar.mn
+@@ -32,16 +32,17 @@
+     locale/browser/newTab.dtd                      (%chrome/browser/newTab.dtd)
+     locale/browser/newTab.properties               (%chrome/browser/newTab.properties)
+     locale/browser/pageInfo.dtd                    (%chrome/browser/pageInfo.dtd)
+     locale/browser/pageInfo.properties             (%chrome/browser/pageInfo.properties)
+     locale/browser/quitDialog.properties           (%chrome/browser/quitDialog.properties)
+     locale/browser/safeMode.dtd                    (%chrome/browser/safeMode.dtd)
+     locale/browser/sanitize.dtd                    (%chrome/browser/sanitize.dtd)
+     locale/browser/search.properties               (%chrome/browser/search.properties)
++    locale/browser/siteData.properties             (%chrome/browser/siteData.properties)
+     locale/browser/sitePermissions.properties      (%chrome/browser/sitePermissions.properties)
+     locale/browser/engineManager.properties        (%chrome/browser/engineManager.properties)
+     locale/browser/setDesktopBackground.dtd        (%chrome/browser/setDesktopBackground.dtd)
+     locale/browser/shellservice.properties         (%chrome/browser/shellservice.properties)
+     locale/browser/tabbrowser.properties           (%chrome/browser/tabbrowser.properties)
+     locale/browser/taskbar.properties              (%chrome/browser/taskbar.properties)
+     locale/browser/translation.dtd                 (%chrome/browser/translation.dtd)
+     locale/browser/translation.properties          (%chrome/browser/translation.properties)
+@@ -62,16 +63,18 @@
+     locale/browser/migration/migration.properties  (%chrome/browser/migration/migration.properties)
+     locale/browser/preferences-old/advanced.dtd       (%chrome/browser/preferences-old/advanced.dtd)
+     locale/browser/preferences/advanced.dtd           (%chrome/browser/preferences/advanced.dtd)
+     locale/browser/preferences/applicationManager.dtd     (%chrome/browser/preferences/applicationManager.dtd)
+     locale/browser/preferences/applicationManager.properties     (%chrome/browser/preferences/applicationManager.properties)
+     locale/browser/preferences-old/applications.dtd   (%chrome/browser/preferences-old/applications.dtd)
+     locale/browser/preferences/applications.dtd       (%chrome/browser/preferences/applications.dtd)
+     locale/browser/preferences/blocklists.dtd         (%chrome/browser/preferences/blocklists.dtd)
++    locale/browser/preferences/clearSiteData.dtd      (%chrome/browser/preferences/clearSiteData.dtd)
++    locale/browser/preferences/clearSiteData.properties     (%chrome/browser/preferences/clearSiteData.properties)
+     locale/browser/preferences/colors.dtd             (%chrome/browser/preferences/colors.dtd)
+     locale/browser/preferences/connection.dtd         (%chrome/browser/preferences/connection.dtd)
+     locale/browser/preferences-old/containers.dtd     (%chrome/browser/preferences-old/containers.dtd)
+     locale/browser/preferences/containers.dtd         (%chrome/browser/preferences/containers.dtd)
+     locale/browser/preferences-old/containers.properties (%chrome/browser/preferences-old/containers.properties)
+     locale/browser/preferences/containers.properties     (%chrome/browser/preferences/containers.properties)
+     locale/browser/preferences-old/content.dtd        (%chrome/browser/preferences-old/content.dtd)
+     locale/browser/preferences/content.dtd            (%chrome/browser/preferences/content.dtd)

+ 1668 - 0
frg/mozilla-release/work-js/1422163-2-60a1.patch

@@ -0,0 +1,1668 @@
+# HG changeset patch
+# User Johann Hofmann <jhofmann@mozilla.com>
+# Date 1515583504 -3600
+# Node ID 9904deec37b3dce9d5fbc89a91d380ba1bb0ae6f
+# Parent  55fd3f890a6e84b1e0b191d027190101b249fa9f
+Bug 1422163 - Part 2 - Make a new test for clearing all site data with the new dialog. r=Gijs
+
+MozReview-Commit-ID: 6Lt3hPZ1YiO
+
+diff --git a/browser/components/preferences/in-content-new/tests/browser.ini b/browser/components/preferences/in-content-new/tests/browser.ini
+--- a/browser/components/preferences/in-content-new/tests/browser.ini
++++ b/browser/components/preferences/in-content-new/tests/browser.ini
+@@ -34,16 +34,17 @@ skip-if = !updater
+ [browser_engines.js]
+ support-files =
+   browser_bug1184989_prevent_scrolling_when_preferences_flipped.xul
+ [browser_change_app_handler.js]
+ skip-if = os != "win" || (os == "win" && os_version == "6.1")
+ # This test tests the windows-specific app selection dialog, so can't run on non-Windows.
+ # Skip the test on Window 7, see the detail at Bug 1381706.
+ [browser_checkspelling.js]
++[browser_clearSiteData.js]
+ [browser_connection.js]
+ [browser_connection_bug388287.js]
+ [browser_cookies_exceptions.js]
+ [browser_defaultbrowser_alwayscheck.js]
+ [browser_homepages_filter_aboutpreferences.js]
+ [browser_layersacceleration.js]
+ [browser_masterpassword.js]
+ [browser_notifications_do_not_disturb.js]
+diff --git a/browser/components/preferences/in-content-new/tests/browser_siteData.js b/browser/components/preferences/in-content-new/tests/browser_clearSiteData.js
+copy from browser/components/preferences/in-content-new/tests/browser_siteData.js
+copy to browser/components/preferences/in-content-new/tests/browser_clearSiteData.js
+--- a/browser/components/preferences/in-content-new/tests/browser_siteData.js
++++ b/browser/components/preferences/in-content-new/tests/browser_clearSiteData.js
+@@ -1,62 +1,25 @@
+ /* Any copyright is dedicated to the Public Domain.
+  * http://creativecommons.org/publicdomain/zero/1.0/ */
+ 
+ "use strict";
+ 
+-ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
+-/* global sinon */
+-Services.scriptloader.loadSubScript("resource://testing-common/sinon-2.3.2.js");
++ChromeUtils.import("resource:///modules/SitePermissions.jsm");
++
++const { SiteDataManager } = ChromeUtils.import("resource:///modules/SiteDataManager.jsm", {});
++const { DownloadUtils } = ChromeUtils.import("resource://gre/modules/DownloadUtils.jsm", {});
+ 
+ const TEST_QUOTA_USAGE_HOST = "example.com";
+ const TEST_QUOTA_USAGE_ORIGIN = "https://" + TEST_QUOTA_USAGE_HOST;
+ const TEST_QUOTA_USAGE_URL = TEST_QUOTA_USAGE_ORIGIN + "/browser/browser/components/preferences/in-content-new/tests/site_data_test.html";
+ const TEST_OFFLINE_HOST = "example.org";
+ const TEST_OFFLINE_ORIGIN = "https://" + TEST_OFFLINE_HOST;
+ const TEST_OFFLINE_URL = TEST_OFFLINE_ORIGIN + "/browser/browser/components/preferences/in-content-new/tests/offline/offline.html";
+ const TEST_SERVICE_WORKER_URL = TEST_OFFLINE_ORIGIN + "/browser/browser/components/preferences/in-content-new/tests/service_worker_test.html";
+-const REMOVE_DIALOG_URL = "chrome://browser/content/preferences/siteDataRemoveSelected.xul";
+-
+-const { NetUtil } = ChromeUtils.import("resource://gre/modules/NetUtil.jsm", {});
+-const { DownloadUtils } = ChromeUtils.import("resource://gre/modules/DownloadUtils.jsm", {});
+-const { SiteDataManager } = ChromeUtils.import("resource:///modules/SiteDataManager.jsm", {});
+-const { OfflineAppCacheHelper } = ChromeUtils.import("resource:///modules/offlineAppCache.jsm", {});
+-
+-XPCOMUtils.defineLazyServiceGetter(this, "serviceWorkerManager", "@mozilla.org/serviceworkers/manager;1", "nsIServiceWorkerManager");
+-
+-const mockOfflineAppCacheHelper = {
+-  clear: null,
+-
+-  originalClear: null,
+-
+-  register() {
+-    this.originalClear = OfflineAppCacheHelper.clear;
+-    this.clear = sinon.spy();
+-    OfflineAppCacheHelper.clear = this.clear;
+-  },
+-
+-  unregister() {
+-    OfflineAppCacheHelper.clear = this.originalClear;
+-  }
+-};
+-
+-function getPersistentStoragePermStatus(origin) {
+-  let uri = NetUtil.newURI(origin);
+-  let principal = Services.scriptSecurityManager.createCodebasePrincipal(uri, {});
+-  return Services.perms.testExactPermissionFromPrincipal(principal, "persistent-storage");
+-}
+-
+-function getQuotaUsage(origin) {
+-  return new Promise(resolve => {
+-    let uri = NetUtil.newURI(origin);
+-    let principal = Services.scriptSecurityManager.createCodebasePrincipal(uri, {});
+-    Services.qms.getUsageForPrincipal(principal, request => resolve(request.result.usage));
+-  });
+-}
+ 
+ // XXX: The intermittent bug 1331851
+ // The implementation of nsICacheStorageConsumptionObserver must be passed as weak referenced,
+ // so we must hold this observer here well. If we didn't, there would be a chance that
+ // in Linux debug test run the observer was released before the operation at gecko was completed
+ // (may be because of a relatively quicker GC cycle or a relatively slower operation).
+ // As a result of that, we would never get the cache usage we want so the test would fail from timeout.
+ const cacheUsageGetter = {
+@@ -77,256 +40,166 @@ const cacheUsageGetter = {
+     cacheUsageGetter._resolve(usage);
+   },
+   QueryInterface: XPCOMUtils.generateQI([
+     Ci.nsICacheStorageConsumptionObserver,
+     Ci.nsISupportsWeakReference
+   ]),
+ };
+ 
+-function promiseCookiesCleared() {
+-  return TestUtils.topicObserved("cookie-changed", (subj, data) => {
+-    return data === "cleared";
+-  });
+-}
+-
+-async function loadServiceWorkerTestPage(url) {
+-  let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, url);
+-  await BrowserTestUtils.waitForCondition(() => {
+-    return ContentTask.spawn(tab.linkedBrowser, {}, () =>
+-      content.document.body.getAttribute("data-test-service-worker-registered") === "true");
+-  }, `Fail to load service worker test ${url}`);
+-  await BrowserTestUtils.removeTab(tab);
+-}
++async function testClearData(clearSiteData, clearCache) {
++  let quotaURI = Services.io.newURI(TEST_QUOTA_USAGE_ORIGIN);
++  SitePermissions.set(quotaURI, "persistent-storage", SitePermissions.ALLOW);
+ 
+-function promiseServiceWorkerRegisteredFor(url) {
+-  return BrowserTestUtils.waitForCondition(() => {
+-    try {
+-      let principal = Services.scriptSecurityManager.createCodebasePrincipalFromOrigin(url);
+-      let sw = serviceWorkerManager.getRegistrationByPrincipal(principal, principal.URI.spec);
+-      if (sw) {
+-        ok(true, `Found the service worker registered for ${url}`);
+-        return true;
+-      }
+-    } catch (e) {}
+-    return false;
+-  }, `Should register service worker for ${url}`);
+-}
+-
+-function promiseServiceWorkersCleared() {
+-  return BrowserTestUtils.waitForCondition(() => {
+-    let serviceWorkers = serviceWorkerManager.getAllRegistrations();
+-    if (serviceWorkers.length == 0) {
+-      ok(true, "Cleared all service workers");
+-      return true;
+-    }
+-    return false;
+-  }, "Should clear all service workers");
+-}
+-
+-registerCleanupFunction(function() {
+-  delete window.sinon;
+-  mockOfflineAppCacheHelper.unregister();
+-});
+-
+-// Test listing site using quota usage or site using appcache
+-add_task(async function() {
+-  // Open a test site which would save into appcache
++  // Open a test site which saves into appcache.
+   await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_OFFLINE_URL);
+   await BrowserTestUtils.removeTab(gBrowser.selectedTab);
+ 
+-  // Open a test site which would save into quota manager
+-  await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_QUOTA_USAGE_URL);
+-  await waitForEvent(gBrowser.selectedBrowser.contentWindow, "test-indexedDB-done");
++  // Fill indexedDB with test data.
++  // Don't wait for the page to load, to register the content event handler as quickly as possible.
++  // If this test goes intermittent, we might have to tell the page to wait longer before
++  // firing the event.
++  BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_QUOTA_USAGE_URL, false);
++  await BrowserTestUtils.waitForContentEvent(
++    gBrowser.selectedBrowser, "test-indexedDB-done", false, null, true);
+   await BrowserTestUtils.removeTab(gBrowser.selectedTab);
+ 
+-  let updatedPromise = promiseSiteDataManagerSitesUpdated();
+-  await openPreferencesViaOpenPreferencesAPI("privacy", { leaveOpen: true });
+-  await updatedPromise;
+-  await openSiteDataSettingsDialog();
+-  let dialog = content.gSubDialog._topDialog;
+-  let dialogFrame = dialog._frame;
+-  let frameDoc = dialogFrame.contentDocument;
+-
+-  let siteItems = frameDoc.getElementsByTagName("richlistitem");
+-  is(siteItems.length, 2, "Should list sites using quota usage or appcache");
+-
+-  let appcacheSite = frameDoc.querySelector(`richlistitem[host="${TEST_OFFLINE_HOST}"]`);
+-  ok(appcacheSite, "Should list site using appcache");
+-
+-  let qoutaUsageSite = frameDoc.querySelector(`richlistitem[host="${TEST_QUOTA_USAGE_HOST}"]`);
+-  ok(qoutaUsageSite, "Should list site using quota usage");
+-
+-  // Always remember to clean up
+-  OfflineAppCacheHelper.clear();
+-  await new Promise(resolve => {
+-    let principal = Services.scriptSecurityManager
+-                            .createCodebasePrincipalFromOrigin(TEST_QUOTA_USAGE_ORIGIN);
+-    let request = Services.qms.clearStoragesForPrincipal(principal, null, true);
+-    request.callback = resolve;
+-  });
+-  await BrowserTestUtils.removeTab(gBrowser.selectedTab);
+-});
+-
+-// Test buttons are disabled and loading message shown while updating sites
+-add_task(async function() {
+-  let updatedPromise = promiseSiteDataManagerSitesUpdated();
+-  await openPreferencesViaOpenPreferencesAPI("privacy", { leaveOpen: true });
+-  await updatedPromise;
+-
+-  let actual = null;
+-  let expected = null;
+-  let doc = gBrowser.selectedBrowser.contentDocument;
+-  let clearBtn = doc.getElementById("clearSiteDataButton");
+-  let settingsButton = doc.getElementById("siteDataSettings");
+-  let prefStrBundle = doc.getElementById("bundlePreferences");
+-  let totalSiteDataSizeLabel = doc.getElementById("totalSiteDataSize");
+-  is(clearBtn.disabled, false, "Should enable clear button after sites updated");
+-  is(settingsButton.disabled, false, "Should enable settings button after sites updated");
+-  await SiteDataManager.getTotalUsage()
+-                       .then(usage => {
+-                         actual = totalSiteDataSizeLabel.textContent;
+-                         expected = prefStrBundle.getFormattedString(
+-                           "totalSiteDataSize", DownloadUtils.convertByteUnits(usage));
+-                          is(actual, expected, "Should show the right total site data size");
+-                       });
+-
+-  Services.obs.notifyObservers(null, "sitedatamanager:updating-sites");
+-  is(clearBtn.disabled, true, "Should disable clear button while updating sites");
+-  is(settingsButton.disabled, true, "Should disable settings button while updating sites");
+-  actual = totalSiteDataSizeLabel.textContent;
+-  expected = prefStrBundle.getString("loadingSiteDataSize");
+-  is(actual, expected, "Should show the loading message while updating");
+-
+-  Services.obs.notifyObservers(null, "sitedatamanager:sites-updated");
+-  is(clearBtn.disabled, false, "Should enable clear button after sites updated");
+-  is(settingsButton.disabled, false, "Should enable settings button after sites updated");
+-  await SiteDataManager.getTotalUsage()
+-                       .then(usage => {
+-                         actual = totalSiteDataSizeLabel.textContent;
+-                         expected = prefStrBundle.getFormattedString(
+-                           "totalSiteDataSize", DownloadUtils.convertByteUnits(usage));
+-                          is(actual, expected, "Should show the right total site data size");
+-                       });
+-
+-  await BrowserTestUtils.removeTab(gBrowser.selectedTab);
+-});
+-
+-// Test the function of the "Clear All Data" button
+-add_task(async function() {
+-  addPersistentStoragePerm(TEST_QUOTA_USAGE_ORIGIN);
+-
+-  await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_QUOTA_USAGE_URL);
+-  await waitForEvent(gBrowser.selectedBrowser.contentWindow, "test-indexedDB-done");
+-  await BrowserTestUtils.removeTab(gBrowser.selectedTab);
++  // Register some service workers.
++  await loadServiceWorkerTestPage(TEST_SERVICE_WORKER_URL);
++  await promiseServiceWorkerRegisteredFor(TEST_SERVICE_WORKER_URL);
+ 
+   await openPreferencesViaOpenPreferencesAPI("privacy", { leaveOpen: true });
+ 
+-  // Test the initial states
++  // Test the initial states.
+   let cacheUsage = await cacheUsageGetter.get();
+   let quotaUsage = await getQuotaUsage(TEST_QUOTA_USAGE_ORIGIN);
+   let totalUsage = await SiteDataManager.getTotalUsage();
+   Assert.greater(cacheUsage, 0, "The cache usage should not be 0");
+   Assert.greater(quotaUsage, 0, "The quota usage should not be 0");
+   Assert.greater(totalUsage, 0, "The total usage should not be 0");
+ 
+-  // Test cancelling "Clear All Data"
+-  // Click "Clear All Data" button and then cancel
+   let doc = gBrowser.selectedBrowser.contentDocument;
+-  let cancelPromise = promiseAlertDialogOpen("cancel");
+-  let clearBtn = doc.getElementById("clearSiteDataButton");
+-  clearBtn.doCommand();
+-  await cancelPromise;
++  let clearSiteDataButton = doc.getElementById("clearSiteDataButton");
++
++  let dialogOpened = promiseLoadSubDialog("chrome://browser/content/preferences/clearSiteData.xul");
++  clearSiteDataButton.doCommand();
++  let dialogWin = await dialogOpened;
++
++  // Convert the usage numbers in the same way the UI does it to assert
++  // that they're displayed in the dialog.
++  let [convertedTotalUsage] = DownloadUtils.convertByteUnits(totalUsage);
++  // For cache we just assert that the right unit (KB, probably) is displayed,
++  // since we've had cache intermittently changing under our feet.
++  let [, convertedCacheUnit] = DownloadUtils.convertByteUnits(cacheUsage);
+ 
+-  // Test the items are not removed
+-  let status = getPersistentStoragePermStatus(TEST_QUOTA_USAGE_ORIGIN);
+-  is(status, Ci.nsIPermissionManager.ALLOW_ACTION, "Should not remove permission");
++  let clearSiteDataLabel = dialogWin.document.getElementById("clearSiteDataLabel");
++  let clearCacheLabel = dialogWin.document.getElementById("clearCacheLabel");
++  // The usage details are filled asynchronously, so we assert that they're present by
++  // waiting for them to be filled in.
++  await Promise.all([
++    TestUtils.waitForCondition(
++      () => clearSiteDataLabel.value && clearSiteDataLabel.value.includes(convertedTotalUsage), "Should show the quota usage"),
++    TestUtils.waitForCondition(
++      () => clearCacheLabel.value && clearCacheLabel.value.includes(convertedCacheUnit), "Should show the cache usage")
++  ]);
+ 
+-  cacheUsage = await cacheUsageGetter.get();
+-  quotaUsage = await getQuotaUsage(TEST_QUOTA_USAGE_ORIGIN);
+-  totalUsage = await SiteDataManager.getTotalUsage();
+-  Assert.greater(cacheUsage, 0, "The cache usage should not be 0");
+-  Assert.greater(quotaUsage, 0, "The quota usage should not be 0");
+-  Assert.greater(totalUsage, 0, "The total usage should not be 0");
+-  // Test cancelling "Clear All Data" ends
++  // Check the boxes according to our test input.
++  let clearSiteDataCheckbox = dialogWin.document.getElementById("clearSiteData");
++  let clearCacheCheckbox = dialogWin.document.getElementById("clearCache");
++  clearSiteDataCheckbox.checked = clearSiteData;
++  clearCacheCheckbox.checked = clearCache;
++
++  // Some additional promises/assertions to wait for
++  // when deleting site data.
++  let acceptPromise;
++  let updatePromise;
++  let cookiesClearedPromise;
++  if (clearSiteData) {
++    acceptPromise = promiseAlertDialogOpen("accept");
++    updatePromise = promiseSiteDataManagerSitesUpdated();
++    cookiesClearedPromise = promiseCookiesCleared();
++  }
++
++  let dialogClosed = BrowserTestUtils.waitForEvent(dialogWin, "unload");
+ 
+-  // Test accepting "Clear All Data"
+-  // Click "Clear All Data" button and then accept
+-  let acceptPromise = promiseAlertDialogOpen("accept");
+-  let updatePromise = promiseSiteDataManagerSitesUpdated();
+-  let cookiesClearedPromise = promiseCookiesCleared();
++  let clearButton = dialogWin.document.getElementById("clearButton");
++  if (!clearSiteData && !clearCache) {
++    // Simulate user input on one of the checkboxes to trigger the event listener for
++    // disabling the clearButton.
++    clearCacheCheckbox.doCommand();
++    // Check that the clearButton gets disabled by unchecking both options.
++    await TestUtils.waitForCondition(() => clearButton.disabled, "Clear button should be disabled");
++    let cancelButton = dialogWin.document.getElementById("cancelButton");
++    // Cancel, since we can't delete anything.
++    cancelButton.click();
++  } else {
++    // Delete stuff!
++    clearButton.click();
++  }
+ 
+-  mockOfflineAppCacheHelper.register();
+-  clearBtn.doCommand();
+-  await acceptPromise;
+-  await updatePromise;
+-  mockOfflineAppCacheHelper.unregister();
++  // For site data we display an extra warning dialog, make sure
++  // to accept it.
++  if (clearSiteData) {
++    await acceptPromise;
++  }
++
++  await dialogClosed;
+ 
+-  // Test all the items are removed
+-  await cookiesClearedPromise;
++  if (clearCache) {
++    TestUtils.waitForCondition(async function() {
++      let usage = await cacheUsageGetter.get();
++      return usage == 0;
++    }, "The cache usage should be removed");
++  } else {
++    Assert.greater(await cacheUsageGetter.get(), 0, "The cache usage should not be 0");
++  }
+ 
+-  ok(mockOfflineAppCacheHelper.clear.calledOnce, "Should clear app cache");
+-
+-  status = getPersistentStoragePermStatus(TEST_QUOTA_USAGE_ORIGIN);
+-  is(status, Ci.nsIPermissionManager.UNKNOWN_ACTION, "Should remove permission");
++  if (clearSiteData) {
++    await updatePromise;
++    await cookiesClearedPromise;
++    await promiseServiceWorkersCleared();
+ 
+-  cacheUsage = await cacheUsageGetter.get();
+-  quotaUsage = await getQuotaUsage(TEST_QUOTA_USAGE_ORIGIN);
+-  totalUsage = await SiteDataManager.getTotalUsage();
+-  is(cacheUsage, 0, "The cache usage should be removed");
+-  is(quotaUsage, 0, "The quota usage should be removed");
+-  is(totalUsage, 0, "The total usage should be removed");
+-  // Test accepting "Clear All Data" ends
++    TestUtils.waitForCondition(async function() {
++      let usage = await SiteDataManager.getTotalUsage();
++      return usage == 0;
++    }, "The total usage should be removed");
++
++    // Check that the size label in about:preferences updates after we cleared data.
++    await ContentTask.spawn(gBrowser.selectedBrowser, null, async function() {
++      let sizeLabel = content.document.getElementById("totalSiteDataSize");
++
++      await ContentTaskUtils.waitForCondition(
++        () => sizeLabel.textContent.includes("0"), "Site data size label should have updated.");
++    });
++  } else {
++    quotaUsage = await getQuotaUsage(TEST_QUOTA_USAGE_ORIGIN);
++    totalUsage = await SiteDataManager.getTotalUsage();
++    Assert.greater(quotaUsage, 0, "The quota usage should not be 0");
++    Assert.greater(totalUsage, 0, "The total usage should not be 0");
++  }
++
++  let desiredPermissionState = clearSiteData ? SitePermissions.UNKNOWN : SitePermissions.ALLOW;
++  let permission = SitePermissions.get(quotaURI, "persistent-storage");
++  is(permission.state, desiredPermissionState, "Should have the correct permission state.");
+ 
+   await BrowserTestUtils.removeTab(gBrowser.selectedTab);
++  await SiteDataManager.removeAll();
++}
++
++// Test opening the "Clear All Data" dialog and cancelling.
++add_task(async function() {
++  await testClearData(false, false);
+ });
+ 
+-// Test clearing service wroker through the "Clear All" button
++// Test opening the "Clear All Data" dialog and removing all site data.
+ add_task(async function() {
+-  await SpecialPowers.pushPrefEnv({set: [["browser.storageManager.enabled", true]]});
+-  // Register a test service
+-  await loadServiceWorkerTestPage(TEST_SERVICE_WORKER_URL);
+-  await openPreferencesViaOpenPreferencesAPI("privacy", { leaveOpen: true });
+-  // Test the initial states
+-  await promiseServiceWorkerRegisteredFor(TEST_SERVICE_WORKER_URL);
+-  // Click the "Clear All" button
+-  let doc = gBrowser.selectedBrowser.contentDocument;
+-  let clearBtn = doc.getElementById("clearSiteDataButton");
+-  let acceptPromise = promiseAlertDialogOpen("accept");
+-  let updatePromise = promiseSiteDataManagerSitesUpdated();
+-  clearBtn.doCommand();
+-  await acceptPromise;
+-  await updatePromise;
+-  await promiseServiceWorkersCleared();
+-  await BrowserTestUtils.removeTab(gBrowser.selectedTab);
++  await testClearData(true, false);
+ });
+ 
+-// Test clearing service wroker through the settings panel
++// Test opening the "Clear All Data" dialog and removing all cache.
+ add_task(async function() {
+-  // Register a test service worker
+-  await loadServiceWorkerTestPage(TEST_SERVICE_WORKER_URL);
+-  await openPreferencesViaOpenPreferencesAPI("privacy", { leaveOpen: true });
+-  // Test the initial states
+-  await promiseServiceWorkerRegisteredFor(TEST_SERVICE_WORKER_URL);
+-  // Open the Site Data Settings panel and remove the site
+-  await openSiteDataSettingsDialog();
+-  let acceptRemovePromise = promiseAlertDialogOpen("accept");
+-  let updatePromise = promiseSiteDataManagerSitesUpdated();
+-  ContentTask.spawn(gBrowser.selectedBrowser, { TEST_OFFLINE_HOST }, args => {
+-    let host = args.TEST_OFFLINE_HOST;
+-    let frameDoc = content.gSubDialog._topDialog._frame.contentDocument;
+-    let sitesList = frameDoc.getElementById("sitesList");
+-    let site = sitesList.querySelector(`richlistitem[host="${host}"]`);
+-    if (site) {
+-      let removeBtn = frameDoc.getElementById("removeSelected");
+-      let saveBtn = frameDoc.getElementById("save");
+-      site.click();
+-      removeBtn.doCommand();
+-      saveBtn.doCommand();
+-    } else {
+-      ok(false, `Should have one site of ${host}`);
+-    }
+-  });
+-  await acceptRemovePromise;
+-  await updatePromise;
+-  await promiseServiceWorkersCleared();
+-  await BrowserTestUtils.removeTab(gBrowser.selectedTab);
++  await testClearData(false, true);
+ });
++
++// Test opening the "Clear All Data" dialog and removing everything.
++add_task(async function() {
++  await testClearData(true, true);
++});
+diff --git a/browser/components/preferences/in-content-new/tests/browser_siteData.js b/browser/components/preferences/in-content-new/tests/browser_siteData.js
+--- a/browser/components/preferences/in-content-new/tests/browser_siteData.js
++++ b/browser/components/preferences/in-content-new/tests/browser_siteData.js
+@@ -1,146 +1,42 @@
+ /* Any copyright is dedicated to the Public Domain.
+  * http://creativecommons.org/publicdomain/zero/1.0/ */
+ 
+ "use strict";
+ 
+-ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
+-/* global sinon */
+-Services.scriptloader.loadSubScript("resource://testing-common/sinon-2.3.2.js");
+-
+ const TEST_QUOTA_USAGE_HOST = "example.com";
+ const TEST_QUOTA_USAGE_ORIGIN = "https://" + TEST_QUOTA_USAGE_HOST;
+ const TEST_QUOTA_USAGE_URL = TEST_QUOTA_USAGE_ORIGIN + "/browser/browser/components/preferences/in-content-new/tests/site_data_test.html";
+ const TEST_OFFLINE_HOST = "example.org";
+ const TEST_OFFLINE_ORIGIN = "https://" + TEST_OFFLINE_HOST;
+ const TEST_OFFLINE_URL = TEST_OFFLINE_ORIGIN + "/browser/browser/components/preferences/in-content-new/tests/offline/offline.html";
+ const TEST_SERVICE_WORKER_URL = TEST_OFFLINE_ORIGIN + "/browser/browser/components/preferences/in-content-new/tests/service_worker_test.html";
+-const REMOVE_DIALOG_URL = "chrome://browser/content/preferences/siteDataRemoveSelected.xul";
+ 
+ const { NetUtil } = ChromeUtils.import("resource://gre/modules/NetUtil.jsm", {});
+ const { DownloadUtils } = ChromeUtils.import("resource://gre/modules/DownloadUtils.jsm", {});
+ const { SiteDataManager } = ChromeUtils.import("resource:///modules/SiteDataManager.jsm", {});
+ const { OfflineAppCacheHelper } = ChromeUtils.import("resource:///modules/offlineAppCache.jsm", {});
+ 
+-XPCOMUtils.defineLazyServiceGetter(this, "serviceWorkerManager", "@mozilla.org/serviceworkers/manager;1", "nsIServiceWorkerManager");
+-
+-const mockOfflineAppCacheHelper = {
+-  clear: null,
+-
+-  originalClear: null,
+-
+-  register() {
+-    this.originalClear = OfflineAppCacheHelper.clear;
+-    this.clear = sinon.spy();
+-    OfflineAppCacheHelper.clear = this.clear;
+-  },
+-
+-  unregister() {
+-    OfflineAppCacheHelper.clear = this.originalClear;
+-  }
+-};
+-
+ function getPersistentStoragePermStatus(origin) {
+   let uri = NetUtil.newURI(origin);
+   let principal = Services.scriptSecurityManager.createCodebasePrincipal(uri, {});
+   return Services.perms.testExactPermissionFromPrincipal(principal, "persistent-storage");
+ }
+ 
+-function getQuotaUsage(origin) {
+-  return new Promise(resolve => {
+-    let uri = NetUtil.newURI(origin);
+-    let principal = Services.scriptSecurityManager.createCodebasePrincipal(uri, {});
+-    Services.qms.getUsageForPrincipal(principal, request => resolve(request.result.usage));
+-  });
+-}
+-
+-// XXX: The intermittent bug 1331851
+-// The implementation of nsICacheStorageConsumptionObserver must be passed as weak referenced,
+-// so we must hold this observer here well. If we didn't, there would be a chance that
+-// in Linux debug test run the observer was released before the operation at gecko was completed
+-// (may be because of a relatively quicker GC cycle or a relatively slower operation).
+-// As a result of that, we would never get the cache usage we want so the test would fail from timeout.
+-const cacheUsageGetter = {
+-  _promise: null,
+-  _resolve: null,
+-  get() {
+-    if (!this._promise) {
+-      this._promise = new Promise(resolve => {
+-        this._resolve = resolve;
+-        Services.cache2.asyncGetDiskConsumption(this);
+-      });
+-    }
+-    return this._promise;
+-  },
+-  // nsICacheStorageConsumptionObserver implementations
+-  onNetworkCacheDiskConsumption(usage) {
+-    cacheUsageGetter._promise = null;
+-    cacheUsageGetter._resolve(usage);
+-  },
+-  QueryInterface: XPCOMUtils.generateQI([
+-    Ci.nsICacheStorageConsumptionObserver,
+-    Ci.nsISupportsWeakReference
+-  ]),
+-};
+-
+-function promiseCookiesCleared() {
+-  return TestUtils.topicObserved("cookie-changed", (subj, data) => {
+-    return data === "cleared";
+-  });
+-}
+-
+-async function loadServiceWorkerTestPage(url) {
+-  let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, url);
+-  await BrowserTestUtils.waitForCondition(() => {
+-    return ContentTask.spawn(tab.linkedBrowser, {}, () =>
+-      content.document.body.getAttribute("data-test-service-worker-registered") === "true");
+-  }, `Fail to load service worker test ${url}`);
+-  await BrowserTestUtils.removeTab(tab);
+-}
+-
+-function promiseServiceWorkerRegisteredFor(url) {
+-  return BrowserTestUtils.waitForCondition(() => {
+-    try {
+-      let principal = Services.scriptSecurityManager.createCodebasePrincipalFromOrigin(url);
+-      let sw = serviceWorkerManager.getRegistrationByPrincipal(principal, principal.URI.spec);
+-      if (sw) {
+-        ok(true, `Found the service worker registered for ${url}`);
+-        return true;
+-      }
+-    } catch (e) {}
+-    return false;
+-  }, `Should register service worker for ${url}`);
+-}
+-
+-function promiseServiceWorkersCleared() {
+-  return BrowserTestUtils.waitForCondition(() => {
+-    let serviceWorkers = serviceWorkerManager.getAllRegistrations();
+-    if (serviceWorkers.length == 0) {
+-      ok(true, "Cleared all service workers");
+-      return true;
+-    }
+-    return false;
+-  }, "Should clear all service workers");
+-}
+-
+-registerCleanupFunction(function() {
+-  delete window.sinon;
+-  mockOfflineAppCacheHelper.unregister();
+-});
+-
+ // Test listing site using quota usage or site using appcache
+ add_task(async function() {
+   // Open a test site which would save into appcache
+   await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_OFFLINE_URL);
+   await BrowserTestUtils.removeTab(gBrowser.selectedTab);
+ 
+   // Open a test site which would save into quota manager
+-  await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_QUOTA_USAGE_URL);
+-  await waitForEvent(gBrowser.selectedBrowser.contentWindow, "test-indexedDB-done");
++  BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_QUOTA_USAGE_URL);
++  await BrowserTestUtils.waitForContentEvent(
++    gBrowser.selectedBrowser, "test-indexedDB-done", false, null, true);
+   await BrowserTestUtils.removeTab(gBrowser.selectedTab);
+ 
+   let updatedPromise = promiseSiteDataManagerSitesUpdated();
+   await openPreferencesViaOpenPreferencesAPI("privacy", { leaveOpen: true });
+   await updatedPromise;
+   await openSiteDataSettingsDialog();
+   let dialog = content.gSubDialog._topDialog;
+   let dialogFrame = dialog._frame;
+@@ -205,85 +101,16 @@ add_task(async function() {
+                          expected = prefStrBundle.getFormattedString(
+                            "totalSiteDataSize", DownloadUtils.convertByteUnits(usage));
+                           is(actual, expected, "Should show the right total site data size");
+                        });
+ 
+   await BrowserTestUtils.removeTab(gBrowser.selectedTab);
+ });
+ 
+-// Test the function of the "Clear All Data" button
+-add_task(async function() {
+-  addPersistentStoragePerm(TEST_QUOTA_USAGE_ORIGIN);
+-
+-  await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_QUOTA_USAGE_URL);
+-  await waitForEvent(gBrowser.selectedBrowser.contentWindow, "test-indexedDB-done");
+-  await BrowserTestUtils.removeTab(gBrowser.selectedTab);
+-
+-  await openPreferencesViaOpenPreferencesAPI("privacy", { leaveOpen: true });
+-
+-  // Test the initial states
+-  let cacheUsage = await cacheUsageGetter.get();
+-  let quotaUsage = await getQuotaUsage(TEST_QUOTA_USAGE_ORIGIN);
+-  let totalUsage = await SiteDataManager.getTotalUsage();
+-  Assert.greater(cacheUsage, 0, "The cache usage should not be 0");
+-  Assert.greater(quotaUsage, 0, "The quota usage should not be 0");
+-  Assert.greater(totalUsage, 0, "The total usage should not be 0");
+-
+-  // Test cancelling "Clear All Data"
+-  // Click "Clear All Data" button and then cancel
+-  let doc = gBrowser.selectedBrowser.contentDocument;
+-  let cancelPromise = promiseAlertDialogOpen("cancel");
+-  let clearBtn = doc.getElementById("clearSiteDataButton");
+-  clearBtn.doCommand();
+-  await cancelPromise;
+-
+-  // Test the items are not removed
+-  let status = getPersistentStoragePermStatus(TEST_QUOTA_USAGE_ORIGIN);
+-  is(status, Ci.nsIPermissionManager.ALLOW_ACTION, "Should not remove permission");
+-
+-  cacheUsage = await cacheUsageGetter.get();
+-  quotaUsage = await getQuotaUsage(TEST_QUOTA_USAGE_ORIGIN);
+-  totalUsage = await SiteDataManager.getTotalUsage();
+-  Assert.greater(cacheUsage, 0, "The cache usage should not be 0");
+-  Assert.greater(quotaUsage, 0, "The quota usage should not be 0");
+-  Assert.greater(totalUsage, 0, "The total usage should not be 0");
+-  // Test cancelling "Clear All Data" ends
+-
+-  // Test accepting "Clear All Data"
+-  // Click "Clear All Data" button and then accept
+-  let acceptPromise = promiseAlertDialogOpen("accept");
+-  let updatePromise = promiseSiteDataManagerSitesUpdated();
+-  let cookiesClearedPromise = promiseCookiesCleared();
+-
+-  mockOfflineAppCacheHelper.register();
+-  clearBtn.doCommand();
+-  await acceptPromise;
+-  await updatePromise;
+-  mockOfflineAppCacheHelper.unregister();
+-
+-  // Test all the items are removed
+-  await cookiesClearedPromise;
+-
+-  ok(mockOfflineAppCacheHelper.clear.calledOnce, "Should clear app cache");
+-
+-  status = getPersistentStoragePermStatus(TEST_QUOTA_USAGE_ORIGIN);
+-  is(status, Ci.nsIPermissionManager.UNKNOWN_ACTION, "Should remove permission");
+-
+-  cacheUsage = await cacheUsageGetter.get();
+-  quotaUsage = await getQuotaUsage(TEST_QUOTA_USAGE_ORIGIN);
+-  totalUsage = await SiteDataManager.getTotalUsage();
+-  is(cacheUsage, 0, "The cache usage should be removed");
+-  is(quotaUsage, 0, "The quota usage should be removed");
+-  is(totalUsage, 0, "The total usage should be removed");
+-  // Test accepting "Clear All Data" ends
+-
+-  await BrowserTestUtils.removeTab(gBrowser.selectedTab);
+-});
+-
+ // Test clearing service wroker through the "Clear All" button
+ add_task(async function() {
+   await SpecialPowers.pushPrefEnv({set: [["browser.storageManager.enabled", true]]});
+   // Register a test service
+   await loadServiceWorkerTestPage(TEST_SERVICE_WORKER_URL);
+   await openPreferencesViaOpenPreferencesAPI("privacy", { leaveOpen: true });
+   // Test the initial states
+   await promiseServiceWorkerRegisteredFor(TEST_SERVICE_WORKER_URL);
+@@ -325,8 +152,9 @@ add_task(async function() {
+       ok(false, `Should have one site of ${host}`);
+     }
+   });
+   await acceptRemovePromise;
+   await updatePromise;
+   await promiseServiceWorkersCleared();
+   await BrowserTestUtils.removeTab(gBrowser.selectedTab);
+ });
++
+diff --git a/browser/components/preferences/in-content-new/tests/head.js b/browser/components/preferences/in-content-new/tests/head.js
+--- a/browser/components/preferences/in-content-new/tests/head.js
++++ b/browser/components/preferences/in-content-new/tests/head.js
+@@ -1,13 +1,15 @@
+ /* Any copyright is dedicated to the Public Domain.
+  * http://creativecommons.org/publicdomain/zero/1.0/ */
+ 
+ ChromeUtils.import("resource://gre/modules/Promise.jsm");
+ 
++XPCOMUtils.defineLazyServiceGetter(this, "serviceWorkerManager", "@mozilla.org/serviceworkers/manager;1", "nsIServiceWorkerManager");
++
+ // Tests within /browser/components/preferences/in-content-new/tests/
+ // test the "new" preferences organization, after it was reorganized.
+ // Thus, all of these tests should set the "oldOrganization" to false
+ // before running.
+ Services.prefs.setBoolPref("browser.preferences.useOldOrganization", false);
+ registerCleanupFunction(function() {
+   Services.prefs.clearUserPref("browser.preferences.useOldOrganization");
+ });
+@@ -184,22 +186,16 @@ function promiseWindowDialogOpen(buttonA
+     });
+   });
+ }
+ 
+ function promiseAlertDialogOpen(buttonAction) {
+   return promiseWindowDialogOpen(buttonAction, "chrome://global/content/commonDialog.xul");
+ }
+ 
+-function addPersistentStoragePerm(origin) {
+-  let uri = NetUtil.newURI(origin);
+-  let principal = Services.scriptSecurityManager.createCodebasePrincipal(uri, {});
+-  Services.perms.addFromPrincipal(principal, "persistent-storage", Ci.nsIPermissionManager.ALLOW_ACTION);
+-}
+-
+ function promiseSiteDataManagerSitesUpdated() {
+   return TestUtils.topicObserved("sitedatamanager:sites-updated", () => true);
+ }
+ 
+ function openSiteDataSettingsDialog() {
+   let doc = gBrowser.selectedBrowser.contentDocument;
+   let settingsBtn = doc.getElementById("siteDataSettings");
+   let dialogOverlay = content.gSubDialog._preloadDialog._overlay;
+@@ -280,8 +276,57 @@ const mockSiteDataManager = {
+     this.fakeSites = null;
+   },
+ 
+   unregister() {
+     this._SiteDataManager._qms = this._originalQMS;
+     this._SiteDataManager._removeQuotaUsage = this._originalRemoveQuotaUsage;
+   }
+ };
++
++function getQuotaUsage(origin) {
++  return new Promise(resolve => {
++    let uri = NetUtil.newURI(origin);
++    let principal = Services.scriptSecurityManager.createCodebasePrincipal(uri, {});
++    Services.qms.getUsageForPrincipal(principal, request => resolve(request.result.usage));
++  });
++}
++
++function promiseCookiesCleared() {
++  return TestUtils.topicObserved("cookie-changed", (subj, data) => {
++    return data === "cleared";
++  });
++}
++
++async function loadServiceWorkerTestPage(url) {
++  let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, url);
++  await BrowserTestUtils.waitForCondition(() => {
++    return ContentTask.spawn(tab.linkedBrowser, {}, () =>
++      content.document.body.getAttribute("data-test-service-worker-registered") === "true");
++  }, `Fail to load service worker test ${url}`);
++  await BrowserTestUtils.removeTab(tab);
++}
++
++function promiseServiceWorkersCleared() {
++  return BrowserTestUtils.waitForCondition(() => {
++    let serviceWorkers = serviceWorkerManager.getAllRegistrations();
++    if (serviceWorkers.length == 0) {
++      ok(true, "Cleared all service workers");
++      return true;
++    }
++    return false;
++  }, "Should clear all service workers");
++}
++
++function promiseServiceWorkerRegisteredFor(url) {
++  return BrowserTestUtils.waitForCondition(() => {
++    try {
++      let principal = Services.scriptSecurityManager.createCodebasePrincipalFromOrigin(url);
++      let sw = serviceWorkerManager.getRegistrationByPrincipal(principal, principal.URI.spec);
++      if (sw) {
++        ok(true, `Found the service worker registered for ${url}`);
++        return true;
++      }
++    } catch (e) {}
++    return false;
++  }, `Should register service worker for ${url}`);
++}
++
+diff --git a/browser/components/preferences/in-content-new/tests/site_data_test.html b/browser/components/preferences/in-content-new/tests/site_data_test.html
+--- a/browser/components/preferences/in-content-new/tests/site_data_test.html
++++ b/browser/components/preferences/in-content-new/tests/site_data_test.html
+@@ -16,14 +16,14 @@
+       request.onupgradeneeded = function(e) {
+         let db = e.target.result;
+         db.createObjectStore("TestStore", { keyPath: "id" });
+       };
+       request.onsuccess = function(e) {
+         let db = e.target.result;
+         let tx = db.transaction("TestStore", "readwrite");
+         let store = tx.objectStore("TestStore");
+-        tx.oncomplete = () => window.dispatchEvent(new Event("test-indexedDB-done"));
++        tx.oncomplete = () => document.dispatchEvent(new CustomEvent("test-indexedDB-done", {bubbles: true, cancelable: false}));
+         store.put({ id: "test_id", description: "Site Data Test"});
+       }
+     </script>
+   </body>
+ </html>
+diff --git a/browser/components/preferences/in-content/tests/browser.ini b/browser/components/preferences/in-content/tests/browser.ini
+--- a/browser/components/preferences/in-content/tests/browser.ini
++++ b/browser/components/preferences/in-content/tests/browser.ini
+@@ -18,16 +18,17 @@ skip-if = !updater
+ [browser_bug795764_cachedisabled.js]
+ [browser_bug1018066_resetScrollPosition.js]
+ [browser_bug1020245_openPreferences_to_paneContent.js]
+ [browser_bug1184989_prevent_scrolling_when_preferences_flipped.js]
+ support-files =
+   browser_bug1184989_prevent_scrolling_when_preferences_flipped.xul
+ [browser_change_app_handler.js]
+ skip-if = os != "win" # This test tests the windows-specific app selection dialog, so can't run on non-Windows
++[browser_clearSiteData.js]
+ [browser_connection.js]
+ [browser_connection_bug388287.js]
+ [browser_cookies_exceptions.js]
+ [browser_defaultbrowser_alwayscheck.js]
+ [browser_homepages_filter_aboutpreferences.js]
+ [browser_notifications_do_not_disturb.js]
+ [browser_password_management.js]
+ [browser_performance.js]
+diff --git a/browser/components/preferences/in-content/tests/browser_siteData.js b/browser/components/preferences/in-content/tests/browser_clearSiteData.js
+copy from browser/components/preferences/in-content/tests/browser_siteData.js
+copy to browser/components/preferences/in-content/tests/browser_clearSiteData.js
+--- a/browser/components/preferences/in-content/tests/browser_siteData.js
++++ b/browser/components/preferences/in-content/tests/browser_clearSiteData.js
+@@ -1,65 +1,25 @@
+ /* Any copyright is dedicated to the Public Domain.
+  * http://creativecommons.org/publicdomain/zero/1.0/ */
+ 
+ "use strict";
+ 
+-ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
+-/* global sinon */
+-Services.scriptloader.loadSubScript("resource://testing-common/sinon-2.3.2.js");
++ChromeUtils.import("resource:///modules/SitePermissions.jsm");
++
++const { SiteDataManager } = ChromeUtils.import("resource:///modules/SiteDataManager.jsm", {});
++const { DownloadUtils } = ChromeUtils.import("resource://gre/modules/DownloadUtils.jsm", {});
+ 
+ const TEST_QUOTA_USAGE_HOST = "example.com";
+ const TEST_QUOTA_USAGE_ORIGIN = "https://" + TEST_QUOTA_USAGE_HOST;
+ const TEST_QUOTA_USAGE_URL = TEST_QUOTA_USAGE_ORIGIN + "/browser/browser/components/preferences/in-content/tests/site_data_test.html";
+ const TEST_OFFLINE_HOST = "example.org";
+ const TEST_OFFLINE_ORIGIN = "https://" + TEST_OFFLINE_HOST;
+ const TEST_OFFLINE_URL = TEST_OFFLINE_ORIGIN + "/browser/browser/components/preferences/in-content/tests/offline/offline.html";
+ const TEST_SERVICE_WORKER_URL = TEST_OFFLINE_ORIGIN + "/browser/browser/components/preferences/in-content/tests/service_worker_test.html";
+-const { NetUtil } = ChromeUtils.import("resource://gre/modules/NetUtil.jsm", {});
+-const { DownloadUtils } = ChromeUtils.import("resource://gre/modules/DownloadUtils.jsm", {});
+-const { OfflineAppCacheHelper } = ChromeUtils.import("resource:///modules/offlineAppCache.jsm", {});
+-
+-XPCOMUtils.defineLazyServiceGetter(this, "serviceWorkerManager", "@mozilla.org/serviceworkers/manager;1", "nsIServiceWorkerManager");
+-
+-const mockOfflineAppCacheHelper = {
+-  clear: null,
+-
+-  originalClear: null,
+-
+-  register() {
+-    this.originalClear = OfflineAppCacheHelper.clear;
+-    this.clear = sinon.spy();
+-    OfflineAppCacheHelper.clear = this.clear;
+-  },
+-
+-  unregister() {
+-    OfflineAppCacheHelper.clear = this.originalClear;
+-  }
+-};
+-
+-function addPersistentStoragePerm(origin) {
+-  let uri = NetUtil.newURI(origin);
+-  let principal = Services.scriptSecurityManager.createCodebasePrincipal(uri, {});
+-  Services.perms.addFromPrincipal(principal, "persistent-storage", Ci.nsIPermissionManager.ALLOW_ACTION);
+-}
+-
+-function getPersistentStoragePermStatus(origin) {
+-  let uri = NetUtil.newURI(origin);
+-  let principal = Services.scriptSecurityManager.createCodebasePrincipal(uri, {});
+-  return Services.perms.testExactPermissionFromPrincipal(principal, "persistent-storage");
+-}
+-
+-function getQuotaUsage(origin) {
+-  return new Promise(resolve => {
+-    let uri = NetUtil.newURI(origin);
+-    let principal = Services.scriptSecurityManager.createCodebasePrincipal(uri, {});
+-    Services.qms.getUsageForPrincipal(principal, request => resolve(request.result.usage));
+-  });
+-}
+ 
+ // XXX: The intermittent bug 1331851
+ // The implementation of nsICacheStorageConsumptionObserver must be passed as weak referenced,
+ // so we must hold this observer here well. If we didn't, there would be a chance that
+ // in Linux debug test run the observer was released before the operation at gecko was completed
+ // (may be because of a relatively quicker GC cycle or a relatively slower operation).
+ // As a result of that, we would never get the cache usage we want so the test would fail from timeout.
+ const cacheUsageGetter = {
+@@ -80,256 +40,166 @@ const cacheUsageGetter = {
+     cacheUsageGetter._resolve(usage);
+   },
+   QueryInterface: XPCOMUtils.generateQI([
+     Ci.nsICacheStorageConsumptionObserver,
+     Ci.nsISupportsWeakReference
+   ]),
+ };
+ 
+-function promiseCookiesCleared() {
+-  return TestUtils.topicObserved("cookie-changed", (subj, data) => {
+-    return data === "cleared";
+-  });
+-}
+-
+-async function loadServiceWorkerTestPage(url) {
+-  let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, url);
+-  await BrowserTestUtils.waitForCondition(() => {
+-    return ContentTask.spawn(tab.linkedBrowser, {}, () =>
+-      content.document.body.getAttribute("data-test-service-worker-registered") === "true");
+-  }, `Fail to load service worker test ${url}`);
+-  await BrowserTestUtils.removeTab(tab);
+-}
++async function testClearData(clearSiteData, clearCache) {
++  let quotaURI = Services.io.newURI(TEST_QUOTA_USAGE_ORIGIN);
++  SitePermissions.set(quotaURI, "persistent-storage", SitePermissions.ALLOW);
+ 
+-function promiseServiceWorkerRegisteredFor(url) {
+-  return BrowserTestUtils.waitForCondition(() => {
+-    try {
+-      let principal = Services.scriptSecurityManager.createCodebasePrincipalFromOrigin(url);
+-      let sw = serviceWorkerManager.getRegistrationByPrincipal(principal, principal.URI.spec);
+-      if (sw) {
+-        ok(true, `Found the service worker registered for ${url}`);
+-        return true;
+-      }
+-    } catch (e) {}
+-    return false;
+-  }, `Should register service worker for ${url}`);
+-}
+-
+-function promiseServiceWorkersCleared() {
+-  return BrowserTestUtils.waitForCondition(() => {
+-    let serviceWorkers = serviceWorkerManager.getAllRegistrations();
+-    if (serviceWorkers.length == 0) {
+-      ok(true, "Cleared all service workers");
+-      return true;
+-    }
+-    return false;
+-  }, "Should clear all service workers");
+-}
+-
+-registerCleanupFunction(function() {
+-  delete window.sinon;
+-  mockOfflineAppCacheHelper.unregister();
+-});
+-
+-// Test listing site using quota usage or site using appcache
+-add_task(async function() {
+-  // Open a test site which would save into appcache
++  // Open a test site which saves into appcache.
+   await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_OFFLINE_URL);
+   await BrowserTestUtils.removeTab(gBrowser.selectedTab);
+ 
+-  // Open a test site which would save into quota manager
+-  await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_QUOTA_USAGE_URL);
+-  await waitForEvent(gBrowser.selectedBrowser.contentWindow, "test-indexedDB-done");
++  // Fill indexedDB with test data.
++  // Don't wait for the page to load, to register the content event handler as quickly as possible.
++  // If this test goes intermittent, we might have to tell the page to wait longer before
++  // firing the event.
++  BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_QUOTA_USAGE_URL, false);
++  await BrowserTestUtils.waitForContentEvent(
++    gBrowser.selectedBrowser, "test-indexedDB-done", false, null, true);
+   await BrowserTestUtils.removeTab(gBrowser.selectedTab);
+ 
+-  let updatedPromise = promiseSitesUpdated();
+-  await openPreferencesViaOpenPreferencesAPI("advanced", "networkTab", { leaveOpen: true });
+-  await updatedPromise;
+-  await openSettingsDialog();
+-  let dialogFrame = content.gSubDialog._topDialog._frame;
+-  let frameDoc = dialogFrame.contentDocument;
+-
+-  let siteItems = frameDoc.getElementsByTagName("richlistitem");
+-  is(siteItems.length, 2, "Should list sites using quota usage or appcache");
+-
+-  let appcacheSite = frameDoc.querySelector(`richlistitem[host="${TEST_OFFLINE_HOST}"]`);
+-  ok(appcacheSite, "Should list site using appcache");
+-
+-  let qoutaUsageSite = frameDoc.querySelector(`richlistitem[host="${TEST_QUOTA_USAGE_HOST}"]`);
+-  ok(qoutaUsageSite, "Should list site using quota usage");
+-
+-  // Always remember to clean up
+-  OfflineAppCacheHelper.clear();
+-  await new Promise(resolve => {
+-    let principal = Services.scriptSecurityManager
+-                            .createCodebasePrincipalFromOrigin(TEST_QUOTA_USAGE_ORIGIN);
+-    let request = Services.qms.clearStoragesForPrincipal(principal, null, true);
+-    request.callback = resolve;
+-  });
+-  await BrowserTestUtils.removeTab(gBrowser.selectedTab);
+-});
+-
+-// Test buttons are disabled and loading message shown while updating sites
+-add_task(async function() {
+-  let updatedPromise = promiseSitesUpdated();
+-  await openPreferencesViaOpenPreferencesAPI("advanced", "networkTab", { leaveOpen: true });
+-  await updatedPromise;
+-
+-  let actual = null;
+-  let expected = null;
+-  let doc = gBrowser.selectedBrowser.contentDocument;
+-  let clearBtn = doc.getElementById("clearSiteDataButton");
+-  let settingsButton = doc.getElementById("siteDataSettings");
+-  let prefStrBundle = doc.getElementById("bundlePreferences");
+-  let totalSiteDataSizeLabel = doc.getElementById("totalSiteDataSize");
+-  is(clearBtn.disabled, false, "Should enable clear button after sites updated");
+-  is(settingsButton.disabled, false, "Should enable settings button after sites updated");
+-  await SiteDataManager.getTotalUsage()
+-                       .then(usage => {
+-                         actual = totalSiteDataSizeLabel.textContent;
+-                         expected = prefStrBundle.getFormattedString(
+-                           "totalSiteDataSize", DownloadUtils.convertByteUnits(usage));
+-                          is(actual, expected, "Should show the right total site data size");
+-                       });
+-
+-  Services.obs.notifyObservers(null, "sitedatamanager:updating-sites");
+-  is(clearBtn.disabled, true, "Should disable clear button while updating sites");
+-  is(settingsButton.disabled, true, "Should disable settings button while updating sites");
+-  actual = totalSiteDataSizeLabel.textContent;
+-  expected = prefStrBundle.getString("loadingSiteDataSize");
+-  is(actual, expected, "Should show the loading message while updating");
+-
+-  Services.obs.notifyObservers(null, "sitedatamanager:sites-updated");
+-  is(clearBtn.disabled, false, "Should enable clear button after sites updated");
+-  is(settingsButton.disabled, false, "Should enable settings button after sites updated");
+-  await SiteDataManager.getTotalUsage()
+-                       .then(usage => {
+-                          actual = totalSiteDataSizeLabel.textContent;
+-                          expected = prefStrBundle.getFormattedString(
+-                           "totalSiteDataSize", DownloadUtils.convertByteUnits(usage));
+-                          is(actual, expected, "Should show the right total site data size");
+-                       });
+-
+-  await BrowserTestUtils.removeTab(gBrowser.selectedTab);
+-});
+-
+-// Test the function of the "Clear All Data" button
+-add_task(async function() {
+-  addPersistentStoragePerm(TEST_QUOTA_USAGE_ORIGIN);
+-
+-  await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_QUOTA_USAGE_URL);
+-  await waitForEvent(gBrowser.selectedBrowser.contentWindow, "test-indexedDB-done");
+-  await BrowserTestUtils.removeTab(gBrowser.selectedTab);
++  // Register some service workers.
++  await loadServiceWorkerTestPage(TEST_SERVICE_WORKER_URL);
++  await promiseServiceWorkerRegisteredFor(TEST_SERVICE_WORKER_URL);
+ 
+   await openPreferencesViaOpenPreferencesAPI("advanced", "networkTab", { leaveOpen: true });
+ 
+-  // Test the initial states
++  // Test the initial states.
+   let cacheUsage = await cacheUsageGetter.get();
+   let quotaUsage = await getQuotaUsage(TEST_QUOTA_USAGE_ORIGIN);
+   let totalUsage = await SiteDataManager.getTotalUsage();
+   Assert.greater(cacheUsage, 0, "The cache usage should not be 0");
+   Assert.greater(quotaUsage, 0, "The quota usage should not be 0");
+   Assert.greater(totalUsage, 0, "The total usage should not be 0");
+ 
+-  // Test cancelling "Clear All Data"
+-  // Click "Clear All Data" button and then cancel
+   let doc = gBrowser.selectedBrowser.contentDocument;
+-  let cancelPromise = promiseAlertDialogOpen("cancel");
+-  let clearBtn = doc.getElementById("clearSiteDataButton");
+-  clearBtn.doCommand();
+-  await cancelPromise;
++  let clearSiteDataButton = doc.getElementById("clearSiteDataButton");
++
++  let dialogOpened = promiseLoadSubDialog("chrome://browser/content/preferences/clearSiteData.xul");
++  clearSiteDataButton.doCommand();
++  let dialogWin = await dialogOpened;
++
++  // Convert the usage numbers in the same way the UI does it to assert
++  // that they're displayed in the dialog.
++  let [convertedTotalUsage] = DownloadUtils.convertByteUnits(totalUsage);
++  // For cache we just assert that the right unit (KB, probably) is displayed,
++  // since we've had cache intermittently changing under our feet.
++  let [, convertedCacheUnit] = DownloadUtils.convertByteUnits(cacheUsage);
+ 
+-  // Test the items are not removed
+-  let status = getPersistentStoragePermStatus(TEST_QUOTA_USAGE_ORIGIN);
+-  is(status, Ci.nsIPermissionManager.ALLOW_ACTION, "Should not remove permission");
++  let clearSiteDataLabel = dialogWin.document.getElementById("clearSiteDataLabel");
++  let clearCacheLabel = dialogWin.document.getElementById("clearCacheLabel");
++  // The usage details are filled asynchronously, so we assert that they're present by
++  // waiting for them to be filled in.
++  await Promise.all([
++    TestUtils.waitForCondition(
++      () => clearSiteDataLabel.value && clearSiteDataLabel.value.includes(convertedTotalUsage), "Should show the quota usage"),
++    TestUtils.waitForCondition(
++      () => clearCacheLabel.value && clearCacheLabel.value.includes(convertedCacheUnit), "Should show the cache usage")
++  ]);
+ 
+-  cacheUsage = await cacheUsageGetter.get();
+-  quotaUsage = await getQuotaUsage(TEST_QUOTA_USAGE_ORIGIN);
+-  totalUsage = await SiteDataManager.getTotalUsage();
+-  Assert.greater(cacheUsage, 0, "The cache usage should not be 0");
+-  Assert.greater(quotaUsage, 0, "The quota usage should not be 0");
+-  Assert.greater(totalUsage, 0, "The total usage should not be 0");
+-  // Test cancelling "Clear All Data" ends
++  // Check the boxes according to our test input.
++  let clearSiteDataCheckbox = dialogWin.document.getElementById("clearSiteData");
++  let clearCacheCheckbox = dialogWin.document.getElementById("clearCache");
++  clearSiteDataCheckbox.checked = clearSiteData;
++  clearCacheCheckbox.checked = clearCache;
++
++  // Some additional promises/assertions to wait for
++  // when deleting site data.
++  let acceptPromise;
++  let updatePromise;
++  let cookiesClearedPromise;
++  if (clearSiteData) {
++    acceptPromise = promiseAlertDialogOpen("accept");
++    updatePromise = promiseSiteDataManagerSitesUpdated();
++    cookiesClearedPromise = promiseCookiesCleared();
++  }
++
++  let dialogClosed = BrowserTestUtils.waitForEvent(dialogWin, "unload");
+ 
+-  // Test accepting "Clear All Data"
+-  // Click "Clear All Data" button and then accept
+-  let acceptPromise = promiseAlertDialogOpen("accept");
+-  let updatePromise = promiseSitesUpdated();
+-  let cookiesClearedPromise = promiseCookiesCleared();
++  let clearButton = dialogWin.document.getElementById("clearButton");
++  if (!clearSiteData && !clearCache) {
++    // Simulate user input on one of the checkboxes to trigger the event listener for
++    // disabling the clearButton.
++    clearCacheCheckbox.doCommand();
++    // Check that the clearButton gets disabled by unchecking both options.
++    await TestUtils.waitForCondition(() => clearButton.disabled, "Clear button should be disabled");
++    let cancelButton = dialogWin.document.getElementById("cancelButton");
++    // Cancel, since we can't delete anything.
++    cancelButton.click();
++  } else {
++    // Delete stuff!
++    clearButton.click();
++  }
+ 
+-  mockOfflineAppCacheHelper.register();
+-  clearBtn.doCommand();
+-  await acceptPromise;
+-  await updatePromise;
+-  mockOfflineAppCacheHelper.unregister();
++  // For site data we display an extra warning dialog, make sure
++  // to accept it.
++  if (clearSiteData) {
++    await acceptPromise;
++  }
++
++  await dialogClosed;
+ 
+-  // Test all the items are removed
+-  await cookiesClearedPromise;
++  if (clearCache) {
++    TestUtils.waitForCondition(async function() {
++      let usage = await cacheUsageGetter.get();
++      return usage == 0;
++    }, "The cache usage should be removed");
++  } else {
++    Assert.greater(await cacheUsageGetter.get(), 0, "The cache usage should not be 0");
++  }
+ 
+-  ok(mockOfflineAppCacheHelper.clear.calledOnce, "Should clear app cache");
+-
+-  status = getPersistentStoragePermStatus(TEST_QUOTA_USAGE_ORIGIN);
+-  is(status, Ci.nsIPermissionManager.UNKNOWN_ACTION, "Should remove permission");
++  if (clearSiteData) {
++    await updatePromise;
++    await cookiesClearedPromise;
++    await promiseServiceWorkersCleared();
+ 
+-  cacheUsage = await cacheUsageGetter.get();
+-  quotaUsage = await getQuotaUsage(TEST_QUOTA_USAGE_ORIGIN);
+-  totalUsage = await SiteDataManager.getTotalUsage();
+-  is(cacheUsage, 0, "The cache usage should be removed");
+-  is(quotaUsage, 0, "The quota usage should be removed");
+-  is(totalUsage, 0, "The total usage should be removed");
+-  // Test accepting "Clear All Data" ends
++    TestUtils.waitForCondition(async function() {
++      let usage = await SiteDataManager.getTotalUsage();
++      return usage == 0;
++    }, "The total usage should be removed");
++
++    // Check that the size label in about:preferences updates after we cleared data.
++    await ContentTask.spawn(gBrowser.selectedBrowser, null, async function() {
++      let sizeLabel = content.document.getElementById("totalSiteDataSize");
++
++      await ContentTaskUtils.waitForCondition(
++        () => sizeLabel.textContent.includes("0"), "Site data size label should have updated.");
++    });
++  } else {
++    quotaUsage = await getQuotaUsage(TEST_QUOTA_USAGE_ORIGIN);
++    totalUsage = await SiteDataManager.getTotalUsage();
++    Assert.greater(quotaUsage, 0, "The quota usage should not be 0");
++    Assert.greater(totalUsage, 0, "The total usage should not be 0");
++  }
++
++  let desiredPermissionState = clearSiteData ? SitePermissions.UNKNOWN : SitePermissions.ALLOW;
++  let permission = SitePermissions.get(quotaURI, "persistent-storage");
++  is(permission.state, desiredPermissionState, "Should have the correct permission state.");
+ 
+   await BrowserTestUtils.removeTab(gBrowser.selectedTab);
++  await SiteDataManager.removeAll();
++}
++
++// Test opening the "Clear All Data" dialog and cancelling.
++add_task(async function() {
++  await testClearData(false, false);
+ });
+ 
+-// Test clearing service wroker through the "Clear All" button
++// Test opening the "Clear All Data" dialog and removing all site data.
+ add_task(async function() {
+-  await SpecialPowers.pushPrefEnv({set: [["browser.storageManager.enabled", true]]});
+-  // Register a test service
+-  await loadServiceWorkerTestPage(TEST_SERVICE_WORKER_URL);
+-  await openPreferencesViaOpenPreferencesAPI("privacy", { leaveOpen: true });
+-  // Test the initial states
+-  await promiseServiceWorkerRegisteredFor(TEST_SERVICE_WORKER_URL);
+-  // Click the "Clear All" button
+-  let doc = gBrowser.selectedBrowser.contentDocument;
+-  let clearBtn = doc.getElementById("clearSiteDataButton");
+-  let acceptPromise = promiseAlertDialogOpen("accept");
+-  let updatePromise = promiseSiteDataManagerSitesUpdated();
+-  clearBtn.doCommand();
+-  await acceptPromise;
+-  await updatePromise;
+-  await promiseServiceWorkersCleared();
+-  await BrowserTestUtils.removeTab(gBrowser.selectedTab);
++  await testClearData(true, false);
+ });
+ 
+-// Test clearing service wroker through the settings panel
++// Test opening the "Clear All Data" dialog and removing all cache.
+ add_task(async function() {
+-  // Register a test service worker
+-  await loadServiceWorkerTestPage(TEST_SERVICE_WORKER_URL);
+-  await openPreferencesViaOpenPreferencesAPI("advanced", "networkTab", { leaveOpen: true });
+-  // Test the initial states
+-  await promiseServiceWorkerRegisteredFor(TEST_SERVICE_WORKER_URL);
+-  // Open the Site Data Settings panel and remove the site
+-  await openSettingsDialog();
++  await testClearData(false, true);
++});
+ 
+-  let acceptRemovePromise = promiseAlertDialogOpen("accept");
+-  let updatePromise = promiseSiteDataManagerSitesUpdated();
+-  ContentTask.spawn(gBrowser.selectedBrowser, { TEST_OFFLINE_HOST }, args => {
+-    let host = args.TEST_OFFLINE_HOST;
+-    let frameDoc = content.gSubDialog._topDialog._frame.contentDocument;
+-    let sitesList = frameDoc.getElementById("sitesList");
+-    let site = sitesList.querySelector(`richlistitem[host="${host}"]`);
+-    if (site) {
+-      let removeBtn = frameDoc.getElementById("removeSelected");
+-      let saveBtn = frameDoc.getElementById("save");
+-      site.click();
+-      removeBtn.doCommand();
+-      saveBtn.doCommand();
+-    } else {
+-      ok(false, `Should have one site of ${host}`);
+-    }
+-  });
+-  await acceptRemovePromise;
+-  await updatePromise;
+-  await promiseServiceWorkersCleared();
+-  await BrowserTestUtils.removeTab(gBrowser.selectedTab);
++// Test opening the "Clear All Data" dialog and removing everything.
++add_task(async function() {
++  await testClearData(true, true);
+ });
+diff --git a/browser/components/preferences/in-content/tests/browser_siteData.js b/browser/components/preferences/in-content/tests/browser_siteData.js
+--- a/browser/components/preferences/in-content/tests/browser_siteData.js
++++ b/browser/components/preferences/in-content/tests/browser_siteData.js
+@@ -1,149 +1,41 @@
+ /* Any copyright is dedicated to the Public Domain.
+  * http://creativecommons.org/publicdomain/zero/1.0/ */
+ 
+ "use strict";
+ 
+-ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
+-/* global sinon */
+-Services.scriptloader.loadSubScript("resource://testing-common/sinon-2.3.2.js");
+-
+ const TEST_QUOTA_USAGE_HOST = "example.com";
+ const TEST_QUOTA_USAGE_ORIGIN = "https://" + TEST_QUOTA_USAGE_HOST;
+ const TEST_QUOTA_USAGE_URL = TEST_QUOTA_USAGE_ORIGIN + "/browser/browser/components/preferences/in-content/tests/site_data_test.html";
+ const TEST_OFFLINE_HOST = "example.org";
+ const TEST_OFFLINE_ORIGIN = "https://" + TEST_OFFLINE_HOST;
+ const TEST_OFFLINE_URL = TEST_OFFLINE_ORIGIN + "/browser/browser/components/preferences/in-content/tests/offline/offline.html";
+ const TEST_SERVICE_WORKER_URL = TEST_OFFLINE_ORIGIN + "/browser/browser/components/preferences/in-content/tests/service_worker_test.html";
++
+ const { NetUtil } = ChromeUtils.import("resource://gre/modules/NetUtil.jsm", {});
+ const { DownloadUtils } = ChromeUtils.import("resource://gre/modules/DownloadUtils.jsm", {});
+ const { OfflineAppCacheHelper } = ChromeUtils.import("resource:///modules/offlineAppCache.jsm", {});
+ 
+-XPCOMUtils.defineLazyServiceGetter(this, "serviceWorkerManager", "@mozilla.org/serviceworkers/manager;1", "nsIServiceWorkerManager");
+-
+-const mockOfflineAppCacheHelper = {
+-  clear: null,
+-
+-  originalClear: null,
+-
+-  register() {
+-    this.originalClear = OfflineAppCacheHelper.clear;
+-    this.clear = sinon.spy();
+-    OfflineAppCacheHelper.clear = this.clear;
+-  },
+-
+-  unregister() {
+-    OfflineAppCacheHelper.clear = this.originalClear;
+-  }
+-};
+-
+-function addPersistentStoragePerm(origin) {
+-  let uri = NetUtil.newURI(origin);
+-  let principal = Services.scriptSecurityManager.createCodebasePrincipal(uri, {});
+-  Services.perms.addFromPrincipal(principal, "persistent-storage", Ci.nsIPermissionManager.ALLOW_ACTION);
+-}
+-
+ function getPersistentStoragePermStatus(origin) {
+   let uri = NetUtil.newURI(origin);
+   let principal = Services.scriptSecurityManager.createCodebasePrincipal(uri, {});
+   return Services.perms.testExactPermissionFromPrincipal(principal, "persistent-storage");
+ }
+ 
+-function getQuotaUsage(origin) {
+-  return new Promise(resolve => {
+-    let uri = NetUtil.newURI(origin);
+-    let principal = Services.scriptSecurityManager.createCodebasePrincipal(uri, {});
+-    Services.qms.getUsageForPrincipal(principal, request => resolve(request.result.usage));
+-  });
+-}
+-
+-// XXX: The intermittent bug 1331851
+-// The implementation of nsICacheStorageConsumptionObserver must be passed as weak referenced,
+-// so we must hold this observer here well. If we didn't, there would be a chance that
+-// in Linux debug test run the observer was released before the operation at gecko was completed
+-// (may be because of a relatively quicker GC cycle or a relatively slower operation).
+-// As a result of that, we would never get the cache usage we want so the test would fail from timeout.
+-const cacheUsageGetter = {
+-  _promise: null,
+-  _resolve: null,
+-  get() {
+-    if (!this._promise) {
+-      this._promise = new Promise(resolve => {
+-        this._resolve = resolve;
+-        Services.cache2.asyncGetDiskConsumption(this);
+-      });
+-    }
+-    return this._promise;
+-  },
+-  // nsICacheStorageConsumptionObserver implementations
+-  onNetworkCacheDiskConsumption(usage) {
+-    cacheUsageGetter._promise = null;
+-    cacheUsageGetter._resolve(usage);
+-  },
+-  QueryInterface: XPCOMUtils.generateQI([
+-    Ci.nsICacheStorageConsumptionObserver,
+-    Ci.nsISupportsWeakReference
+-  ]),
+-};
+-
+-function promiseCookiesCleared() {
+-  return TestUtils.topicObserved("cookie-changed", (subj, data) => {
+-    return data === "cleared";
+-  });
+-}
+-
+-async function loadServiceWorkerTestPage(url) {
+-  let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, url);
+-  await BrowserTestUtils.waitForCondition(() => {
+-    return ContentTask.spawn(tab.linkedBrowser, {}, () =>
+-      content.document.body.getAttribute("data-test-service-worker-registered") === "true");
+-  }, `Fail to load service worker test ${url}`);
+-  await BrowserTestUtils.removeTab(tab);
+-}
+-
+-function promiseServiceWorkerRegisteredFor(url) {
+-  return BrowserTestUtils.waitForCondition(() => {
+-    try {
+-      let principal = Services.scriptSecurityManager.createCodebasePrincipalFromOrigin(url);
+-      let sw = serviceWorkerManager.getRegistrationByPrincipal(principal, principal.URI.spec);
+-      if (sw) {
+-        ok(true, `Found the service worker registered for ${url}`);
+-        return true;
+-      }
+-    } catch (e) {}
+-    return false;
+-  }, `Should register service worker for ${url}`);
+-}
+-
+-function promiseServiceWorkersCleared() {
+-  return BrowserTestUtils.waitForCondition(() => {
+-    let serviceWorkers = serviceWorkerManager.getAllRegistrations();
+-    if (serviceWorkers.length == 0) {
+-      ok(true, "Cleared all service workers");
+-      return true;
+-    }
+-    return false;
+-  }, "Should clear all service workers");
+-}
+-
+-registerCleanupFunction(function() {
+-  delete window.sinon;
+-  mockOfflineAppCacheHelper.unregister();
+-});
+-
+ // Test listing site using quota usage or site using appcache
+ add_task(async function() {
+   // Open a test site which would save into appcache
+   await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_OFFLINE_URL);
+   await BrowserTestUtils.removeTab(gBrowser.selectedTab);
+ 
+   // Open a test site which would save into quota manager
+-  await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_QUOTA_USAGE_URL);
+-  await waitForEvent(gBrowser.selectedBrowser.contentWindow, "test-indexedDB-done");
++  BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_QUOTA_USAGE_URL);
++  await BrowserTestUtils.waitForContentEvent(
++    gBrowser.selectedBrowser, "test-indexedDB-done", false, null, true);
+   await BrowserTestUtils.removeTab(gBrowser.selectedTab);
+ 
+   let updatedPromise = promiseSitesUpdated();
+   await openPreferencesViaOpenPreferencesAPI("advanced", "networkTab", { leaveOpen: true });
+   await updatedPromise;
+   await openSettingsDialog();
+   let dialogFrame = content.gSubDialog._topDialog._frame;
+   let frameDoc = dialogFrame.contentDocument;
+@@ -207,85 +99,16 @@ add_task(async function() {
+                           expected = prefStrBundle.getFormattedString(
+                            "totalSiteDataSize", DownloadUtils.convertByteUnits(usage));
+                           is(actual, expected, "Should show the right total site data size");
+                        });
+ 
+   await BrowserTestUtils.removeTab(gBrowser.selectedTab);
+ });
+ 
+-// Test the function of the "Clear All Data" button
+-add_task(async function() {
+-  addPersistentStoragePerm(TEST_QUOTA_USAGE_ORIGIN);
+-
+-  await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_QUOTA_USAGE_URL);
+-  await waitForEvent(gBrowser.selectedBrowser.contentWindow, "test-indexedDB-done");
+-  await BrowserTestUtils.removeTab(gBrowser.selectedTab);
+-
+-  await openPreferencesViaOpenPreferencesAPI("advanced", "networkTab", { leaveOpen: true });
+-
+-  // Test the initial states
+-  let cacheUsage = await cacheUsageGetter.get();
+-  let quotaUsage = await getQuotaUsage(TEST_QUOTA_USAGE_ORIGIN);
+-  let totalUsage = await SiteDataManager.getTotalUsage();
+-  Assert.greater(cacheUsage, 0, "The cache usage should not be 0");
+-  Assert.greater(quotaUsage, 0, "The quota usage should not be 0");
+-  Assert.greater(totalUsage, 0, "The total usage should not be 0");
+-
+-  // Test cancelling "Clear All Data"
+-  // Click "Clear All Data" button and then cancel
+-  let doc = gBrowser.selectedBrowser.contentDocument;
+-  let cancelPromise = promiseAlertDialogOpen("cancel");
+-  let clearBtn = doc.getElementById("clearSiteDataButton");
+-  clearBtn.doCommand();
+-  await cancelPromise;
+-
+-  // Test the items are not removed
+-  let status = getPersistentStoragePermStatus(TEST_QUOTA_USAGE_ORIGIN);
+-  is(status, Ci.nsIPermissionManager.ALLOW_ACTION, "Should not remove permission");
+-
+-  cacheUsage = await cacheUsageGetter.get();
+-  quotaUsage = await getQuotaUsage(TEST_QUOTA_USAGE_ORIGIN);
+-  totalUsage = await SiteDataManager.getTotalUsage();
+-  Assert.greater(cacheUsage, 0, "The cache usage should not be 0");
+-  Assert.greater(quotaUsage, 0, "The quota usage should not be 0");
+-  Assert.greater(totalUsage, 0, "The total usage should not be 0");
+-  // Test cancelling "Clear All Data" ends
+-
+-  // Test accepting "Clear All Data"
+-  // Click "Clear All Data" button and then accept
+-  let acceptPromise = promiseAlertDialogOpen("accept");
+-  let updatePromise = promiseSitesUpdated();
+-  let cookiesClearedPromise = promiseCookiesCleared();
+-
+-  mockOfflineAppCacheHelper.register();
+-  clearBtn.doCommand();
+-  await acceptPromise;
+-  await updatePromise;
+-  mockOfflineAppCacheHelper.unregister();
+-
+-  // Test all the items are removed
+-  await cookiesClearedPromise;
+-
+-  ok(mockOfflineAppCacheHelper.clear.calledOnce, "Should clear app cache");
+-
+-  status = getPersistentStoragePermStatus(TEST_QUOTA_USAGE_ORIGIN);
+-  is(status, Ci.nsIPermissionManager.UNKNOWN_ACTION, "Should remove permission");
+-
+-  cacheUsage = await cacheUsageGetter.get();
+-  quotaUsage = await getQuotaUsage(TEST_QUOTA_USAGE_ORIGIN);
+-  totalUsage = await SiteDataManager.getTotalUsage();
+-  is(cacheUsage, 0, "The cache usage should be removed");
+-  is(quotaUsage, 0, "The quota usage should be removed");
+-  is(totalUsage, 0, "The total usage should be removed");
+-  // Test accepting "Clear All Data" ends
+-
+-  await BrowserTestUtils.removeTab(gBrowser.selectedTab);
+-});
+-
+ // Test clearing service wroker through the "Clear All" button
+ add_task(async function() {
+   await SpecialPowers.pushPrefEnv({set: [["browser.storageManager.enabled", true]]});
+   // Register a test service
+   await loadServiceWorkerTestPage(TEST_SERVICE_WORKER_URL);
+   await openPreferencesViaOpenPreferencesAPI("privacy", { leaveOpen: true });
+   // Test the initial states
+   await promiseServiceWorkerRegisteredFor(TEST_SERVICE_WORKER_URL);
+diff --git a/browser/components/preferences/in-content/tests/head.js b/browser/components/preferences/in-content/tests/head.js
+--- a/browser/components/preferences/in-content/tests/head.js
++++ b/browser/components/preferences/in-content/tests/head.js
+@@ -1,13 +1,15 @@
+ /* Any copyright is dedicated to the Public Domain.
+  * http://creativecommons.org/publicdomain/zero/1.0/ */
+ 
+ ChromeUtils.import("resource://gre/modules/Promise.jsm", this);
+ 
++XPCOMUtils.defineLazyServiceGetter(this, "serviceWorkerManager", "@mozilla.org/serviceworkers/manager;1", "nsIServiceWorkerManager");
++
+ // Tests within /browser/components/preferences/in-content/tests/
+ // test the "old" preferences organization, before it was reorganized.
+ // Thus, all of these tests should revert back to the "oldOrganization"
+ // before running.
+ Services.prefs.setBoolPref("browser.preferences.useOldOrganization", true);
+ registerCleanupFunction(function() {
+   Services.prefs.clearUserPref("browser.preferences.useOldOrganization");
+ });
+@@ -270,8 +272,57 @@ function promiseSettingsDialogClose() {
+     dialogWin.addEventListener("unload", function unload() {
+       if (dialogWin.document.documentURI === "chrome://browser/content/preferences/siteDataSettings.xul") {
+         isnot(dialogOverlay.style.visibility, "visible", "The Settings dialog should be hidden");
+         resolve();
+       }
+     }, { once: true });
+   });
+ }
++
++function getQuotaUsage(origin) {
++  return new Promise(resolve => {
++    let uri = NetUtil.newURI(origin);
++    let principal = Services.scriptSecurityManager.createCodebasePrincipal(uri, {});
++    Services.qms.getUsageForPrincipal(principal, request => resolve(request.result.usage));
++  });
++}
++
++function promiseCookiesCleared() {
++  return TestUtils.topicObserved("cookie-changed", (subj, data) => {
++    return data === "cleared";
++  });
++}
++
++async function loadServiceWorkerTestPage(url) {
++  let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, url);
++  await BrowserTestUtils.waitForCondition(() => {
++    return ContentTask.spawn(tab.linkedBrowser, {}, () =>
++      content.document.body.getAttribute("data-test-service-worker-registered") === "true");
++  }, `Fail to load service worker test ${url}`);
++  await BrowserTestUtils.removeTab(tab);
++}
++
++function promiseServiceWorkersCleared() {
++  return BrowserTestUtils.waitForCondition(() => {
++    let serviceWorkers = serviceWorkerManager.getAllRegistrations();
++    if (serviceWorkers.length == 0) {
++      ok(true, "Cleared all service workers");
++      return true;
++    }
++    return false;
++  }, "Should clear all service workers");
++}
++
++function promiseServiceWorkerRegisteredFor(url) {
++  return BrowserTestUtils.waitForCondition(() => {
++    try {
++      let principal = Services.scriptSecurityManager.createCodebasePrincipalFromOrigin(url);
++      let sw = serviceWorkerManager.getRegistrationByPrincipal(principal, principal.URI.spec);
++      if (sw) {
++        ok(true, `Found the service worker registered for ${url}`);
++        return true;
++      }
++    } catch (e) {}
++    return false;
++  }, `Should register service worker for ${url}`);
++}
++
+diff --git a/browser/components/preferences/in-content/tests/site_data_test.html b/browser/components/preferences/in-content/tests/site_data_test.html
+--- a/browser/components/preferences/in-content/tests/site_data_test.html
++++ b/browser/components/preferences/in-content/tests/site_data_test.html
+@@ -16,14 +16,14 @@
+       request.onupgradeneeded = function(e) {
+         let db = e.target.result;
+         db.createObjectStore("TestStore", { keyPath: "id" });
+       };
+       request.onsuccess = function(e) {
+         let db = e.target.result;
+         let tx = db.transaction("TestStore", "readwrite");
+         let store = tx.objectStore("TestStore");
+-        tx.oncomplete = () => window.dispatchEvent(new Event("test-indexedDB-done"));
++        tx.oncomplete = () => document.dispatchEvent(new CustomEvent("test-indexedDB-done", {bubbles: true, cancelable: false}));
+         store.put({ id: "test_id", description: "Site Data Test"});
+       };
+     </script>
+   </body>
+ </html>

+ 211 - 0
frg/mozilla-release/work-js/1422314-59a1.patch

@@ -0,0 +1,211 @@
+# HG changeset patch
+# User Andrea Marchesini <amarchesini@mozilla.com>
+# Date 1512399092 18000
+# Node ID 9f96be0a67d03d0f27d510ee5067f99731e11dbe
+# Parent  d3c6f9edc73d7e0bf1ad25f4fed71e766e590b79
+Bug 1422314 - BlobURLs should be immediately released when shutting down, r=bkelly
+
+diff --git a/dom/file/nsHostObjectProtocolHandler.cpp b/dom/file/nsHostObjectProtocolHandler.cpp
+--- a/dom/file/nsHostObjectProtocolHandler.cpp
++++ b/dom/file/nsHostObjectProtocolHandler.cpp
+@@ -18,16 +18,17 @@
+ #include "mozilla/LoadInfo.h"
+ #include "mozilla/ModuleUtils.h"
+ #include "mozilla/Preferences.h"
+ #include "mozilla/SystemGroup.h"
+ #include "nsClassHashtable.h"
+ #include "nsContentUtils.h"
+ #include "nsError.h"
+ #include "nsHostObjectURI.h"
++#include "nsIAsyncShutdown.h"
+ #include "nsIException.h" // for nsIStackFrame
+ #include "nsIMemoryReporter.h"
+ #include "nsIPrincipal.h"
+ #include "nsIUUIDGenerator.h"
+ #include "nsNetUtil.h"
+ 
+ #define RELEASING_TIMER 5000
+ 
+@@ -429,87 +430,166 @@ class BlobURLsReporter final : public ns
+     }
+   }
+ };
+ 
+ NS_IMPL_ISUPPORTS(BlobURLsReporter, nsIMemoryReporter)
+ 
+ class ReleasingTimerHolder final : public nsITimerCallback
+                                  , public nsINamed
++                                 , public nsIAsyncShutdownBlocker
+ {
+ public:
+   NS_DECL_ISUPPORTS
+ 
+   static void
+   Create(const nsACString& aURI, bool aBroadcastToOtherProcesses)
+   {
++    MOZ_ASSERT(NS_IsMainThread());
++
+     RefPtr<ReleasingTimerHolder> holder =
+       new ReleasingTimerHolder(aURI, aBroadcastToOtherProcesses);
++
++    auto raii = mozilla::MakeScopeExit([&] {
++      holder->CancelTimerAndRevokeURI();
++    });
++
+     nsresult rv = NS_NewTimerWithCallback(getter_AddRefs(holder->mTimer),
+                                           holder, RELEASING_TIMER,
+                                           nsITimer::TYPE_ONE_SHOT,
+                                           SystemGroup::EventTargetFor(TaskCategory::Other));
+     NS_ENSURE_SUCCESS_VOID(rv);
++
++    nsCOMPtr<nsIAsyncShutdownClient> phase = GetShutdownPhase();
++    NS_ENSURE_TRUE_VOID(!!phase);
++
++    rv = phase->AddBlocker(holder, NS_LITERAL_STRING(__FILE__), __LINE__,
++                           NS_LITERAL_STRING("ReleasingTimerHolder shutdown"));
++    NS_ENSURE_SUCCESS_VOID(rv);
++
++    raii.release();
+   }
+ 
++  // nsITimerCallback interface
++
+   NS_IMETHOD
+   Notify(nsITimer* aTimer) override
+   {
++    RevokeURI(mBroadcastToOtherProcesses);
++    return NS_OK;
++  }
++
++  // nsINamed interface
++
++  NS_IMETHOD
++  GetName(nsACString& aName) override
++  {
++    aName.AssignLiteral("ReleasingTimerHolder");
++    return NS_OK;
++  }
++
++  // nsIAsyncShutdownBlocker interface
++
++  NS_IMETHOD
++  GetName(nsAString& aName) override
++  {
++    aName.AssignLiteral("ReleasingTimerHolder");
++    return NS_OK;
++  }
++
++  NS_IMETHOD
++  BlockShutdown(nsIAsyncShutdownClient* aClient) override
++  {
++    CancelTimerAndRevokeURI();
++    return NS_OK;
++  }
++
++  NS_IMETHOD
++  GetState(nsIPropertyBag**) override
++  {
++    return NS_OK;
++  }
++
++private:
++  ReleasingTimerHolder(const nsACString& aURI, bool aBroadcastToOtherProcesses)
++    : mURI(aURI)
++    , mBroadcastToOtherProcesses(aBroadcastToOtherProcesses)
++  {}
++
++  ~ReleasingTimerHolder()
++  {}
++
++  void
++  RevokeURI(bool aBroadcastToOtherProcesses)
++  {
++    // Remove the shutting down blocker
++    nsCOMPtr<nsIAsyncShutdownClient> phase = GetShutdownPhase();
++    if (phase) {
++      phase->RemoveBlocker(this);
++    }
++
+     // If we have to broadcast the unregistration, let's do it now.
+-    if (mBroadcastToOtherProcesses) {
++    if (aBroadcastToOtherProcesses) {
+       BroadcastBlobURLUnregistration(mURI);
+     }
+ 
+     DataInfo* info = GetDataInfo(mURI, true /* We care about revoked dataInfo */);
+     if (!info) {
+       // Already gone!
+-      return NS_OK;
++      return;
+     }
+ 
+     MOZ_ASSERT(info->mRevoked);
+ 
+     for (uint32_t i = 0; i < info->mURIs.Length(); ++i) {
+       nsCOMPtr<nsIURI> uri = do_QueryReferent(info->mURIs[i]);
+       if (uri) {
+         static_cast<nsHostObjectURI*>(uri.get())->ForgetBlobImpl();
+       }
+     }
+ 
+     gDataTable->Remove(mURI);
+     if (gDataTable->Count() == 0) {
+       delete gDataTable;
+       gDataTable = nullptr;
+     }
++  }
+ 
+-    return NS_OK;
++  void
++  CancelTimerAndRevokeURI()
++  {
++    if (mTimer) {
++      mTimer->Cancel();
++      mTimer = nullptr;
++    }
++
++    RevokeURI(false /* aBroadcastToOtherProcesses */);
+   }
+ 
+-  NS_IMETHOD
+-  GetName(nsACString& aName) override
++  static nsCOMPtr<nsIAsyncShutdownClient>
++  GetShutdownPhase()
+   {
+-    aName.AssignLiteral("ReleasingTimerHolder");
+-    return NS_OK;
+-  }
++    nsCOMPtr<nsIAsyncShutdownService> svc = services::GetAsyncShutdown();
++    NS_ENSURE_TRUE(!!svc, nullptr);
+ 
+-private:
+-  ReleasingTimerHolder(const nsACString& aURI, bool aBroadcastToOtherProcesses)
+-    : mURI(aURI)
+-    , mBroadcastToOtherProcesses(aBroadcastToOtherProcesses)
+-  {}
++    nsCOMPtr<nsIAsyncShutdownClient> phase;
++    nsresult rv = svc->GetXpcomWillShutdown(getter_AddRefs(phase));
++    NS_ENSURE_SUCCESS(rv, nullptr);
+ 
+-  ~ReleasingTimerHolder()
+-  {}
++    return Move(phase);
++  }
+ 
+   nsCString mURI;
+   bool mBroadcastToOtherProcesses;
+ 
+   nsCOMPtr<nsITimer> mTimer;
+ };
+ 
+-NS_IMPL_ISUPPORTS(ReleasingTimerHolder, nsITimerCallback, nsINamed)
++NS_IMPL_ISUPPORTS(ReleasingTimerHolder, nsITimerCallback, nsINamed,
++                  nsIAsyncShutdownBlocker)
+ 
+ } // namespace mozilla
+ 
+ template<typename T>
+ static nsresult
+ AddDataEntryInternal(const nsACString& aURI, T aObject,
+                      nsIPrincipal* aPrincipal)
+ {

+ 79 - 0
frg/mozilla-release/work-js/1424949-1-59a1.patch

@@ -0,0 +1,79 @@
+# HG changeset patch
+# User Hiroyuki Ikezoe <hikezoe@mozilla.com>
+# Date 1513268697 -32400
+# Node ID a467c6237e0cac82110f1ce8ffcf794c82ae50fd
+# Parent  47a48cb45989a7b8d7f6a25b7e8b2d960bcac9be
+Bug 1424949 - Use assert_greater_than or assert_less_than in file_animations-dynamic-changes.html. r=birtles
+
+MozReview-Commit-ID: I24yopOv51d
+
+diff --git a/dom/animation/test/css-animations/file_animations-dynamic-changes.html b/dom/animation/test/css-animations/file_animations-dynamic-changes.html
+--- a/dom/animation/test/css-animations/file_animations-dynamic-changes.html
++++ b/dom/animation/test/css-animations/file_animations-dynamic-changes.html
+@@ -84,18 +84,18 @@ promise_test(function(t) {
+                   'First Animation is in second position after update');
+     assert_equals(animations[2], animation2,
+                   'Second Animation is in third position after update');
+     assert_equals(animations[1].startTime, animations[2].startTime,
+                   'Old Animations have the same start time');
+     // TODO: Check that animations[0].startTime === null
+     return animations[0].ready;
+   }).then(function() {
+-    assert_true(animations[0].startTime > animations[1].startTime,
+-                'New Animation has later start time');
++    assert_greater_than(animations[0].startTime, animations[1].startTime,
++                        'New Animation has later start time');
+   });
+ }, 'Only the startTimes of existing animations are preserved');
+ 
+ promise_test(function(t) {
+   var div = addDiv(t);
+   div.style.animation = 'anim1 100s, anim1 100s';
+   var secondAnimation = div.getAnimations()[1];
+ 
+@@ -104,18 +104,19 @@ promise_test(function(t) {
+     // Trim list of animations
+     div.style.animationName = 'anim1';
+     var animations = div.getAnimations();
+     assert_equals(animations.length, 1, 'List of Animations was trimmed');
+     assert_equals(animations[0], secondAnimation,
+                   'Remaining Animation is the second one in the list');
+     assert_equals(typeof(animations[0].startTime), 'number',
+                   'Remaining Animation has resolved startTime');
+-    assert_true(animations[0].startTime < animations[0].timeline.currentTime,
+-                'Remaining Animation preserves startTime');
++    assert_less_than(animations[0].startTime,
++                     animations[0].timeline.currentTime,
++                     'Remaining Animation preserves startTime');
+   });
+ }, 'Animations are removed from the start of the list while preserving'
+    + ' the state of existing Animations');
+ 
+ promise_test(function(t) {
+   var div = addDiv(t);
+   div.style.animation = 'anim1 100s';
+   var firstAddedAnimation = div.getAnimations()[0],
+@@ -137,18 +138,19 @@ promise_test(function(t) {
+     assert_equals(animations[0], secondAddedAnimation,
+                   'Second Animation remains in same position after'
+                   + ' interleaving');
+     assert_equals(animations[2], firstAddedAnimation,
+                   'First Animation remains in same position after'
+                   + ' interleaving');
+     return animations[1].ready;
+   }).then(function() {
+-    assert_true(animations[1].startTime > animations[0].startTime,
+-                'Interleaved animation starts later than existing animations');
+-    assert_true(animations[0].startTime > animations[2].startTime,
+-                'Original animations retain their start time');
++    assert_greater_than(animations[1].startTime, animations[0].startTime,
++                        'Interleaved animation starts later than existing ' +
++                        'animations');
++    assert_greater_than(animations[0].startTime, animations[2].startTime,
++                        'Original animations retain their start time');
+   });
+ }, 'Animation state is preserved when interleaving animations in list');
+ 
+ done();
+ </script>
+ </body>

+ 32 - 0
frg/mozilla-release/work-js/1424949-2-59a1.patch

@@ -0,0 +1,32 @@
+# HG changeset patch
+# User Hiroyuki Ikezoe <hikezoe@mozilla.com>
+# Date 1513269364 -32400
+# Node ID b5f52a71f9a1d0c352d0efaa466db13d33605a85
+# Parent  a467c6237e0cac82110f1ce8ffcf794c82ae50fd
+Bug 1424949 - Wait for one more frame before checking animation start time to avoid the situation that the animation just begins at the time when it's ready. r=birtles
+
+MozReview-Commit-ID: 6bNfl10TiT6
+
+diff --git a/dom/animation/test/css-animations/file_animations-dynamic-changes.html b/dom/animation/test/css-animations/file_animations-dynamic-changes.html
+--- a/dom/animation/test/css-animations/file_animations-dynamic-changes.html
++++ b/dom/animation/test/css-animations/file_animations-dynamic-changes.html
+@@ -95,17 +95,17 @@ promise_test(function(t) {
+ }, 'Only the startTimes of existing animations are preserved');
+ 
+ promise_test(function(t) {
+   var div = addDiv(t);
+   div.style.animation = 'anim1 100s, anim1 100s';
+   var secondAnimation = div.getAnimations()[1];
+ 
+   // Wait before continuing so we can compare start times
+-  return secondAnimation.ready.then(waitForFrame).then(function() {
++  return secondAnimation.ready.then(waitForNextFrame).then(function() {
+     // Trim list of animations
+     div.style.animationName = 'anim1';
+     var animations = div.getAnimations();
+     assert_equals(animations.length, 1, 'List of Animations was trimmed');
+     assert_equals(animations[0], secondAnimation,
+                   'Remaining Animation is the second one in the list');
+     assert_equals(typeof(animations[0].startTime), 'number',
+                   'Remaining Animation has resolved startTime');
+

+ 61 - 0
frg/mozilla-release/work-js/1425270-59a1.patch

@@ -0,0 +1,61 @@
+# HG changeset patch
+# User Gijs Kruitbosch <gijskruitbosch@gmail.com>
+# Date 1513272413 21600
+# Node ID 9b7b38c7c0a6043dff29a8957be64c64197b8b16
+# Parent  922e619b506cdcf00c9b9b18e6ffacf68128cee7
+Bug 1425270 - update readability from github commit fa9d8bda48ee574bcffbc19d68b4ca39e1f9036a, rs=already-reviewed
+
+diff --git a/toolkit/components/reader/Readability.js b/toolkit/components/reader/Readability.js
+--- a/toolkit/components/reader/Readability.js
++++ b/toolkit/components/reader/Readability.js
+@@ -103,20 +103,20 @@ Readability.prototype = {
+   DEFAULT_TAGS_TO_SCORE: "section,h2,h3,h4,h5,h6,p,td,pre".toUpperCase().split(","),
+ 
+   // The default number of words an article must have in order to return a result
+   DEFAULT_WORD_THRESHOLD: 500,
+ 
+   // All of the regular expressions in use within readability.
+   // Defined up here so we don't instantiate them repeatedly in loops.
+   REGEXPS: {
+-    unlikelyCandidates: /banner|breadcrumbs|combx|comment|community|cover-wrap|disqus|extra|foot|header|legends|menu|modal|related|remark|replies|rss|shoutbox|sidebar|skyscraper|social|sponsor|supplemental|ad-break|agegate|pagination|pager|popup|yom-remote/i,
++    unlikelyCandidates: /banner|breadcrumbs|combx|comment|community|cover-wrap|disqus|extra|foot|header|legends|menu|related|remark|replies|rss|shoutbox|sidebar|skyscraper|social|sponsor|supplemental|ad-break|agegate|pagination|pager|popup|yom-remote/i,
+     okMaybeItsACandidate: /and|article|body|column|main|shadow/i,
+     positive: /article|body|content|entry|hentry|h-entry|main|page|pagination|post|text|blog|story/i,
+-    negative: /hidden|^hid$| hid$| hid |^hid |banner|combx|comment|com-|contact|foot|footer|footnote|masthead|media|meta|modal|outbrain|promo|related|scroll|share|shoutbox|sidebar|skyscraper|sponsor|shopping|tags|tool|widget/i,
++    negative: /hidden|^hid$| hid$| hid |^hid |banner|combx|comment|com-|contact|foot|footer|footnote|masthead|media|meta|outbrain|promo|related|scroll|share|shoutbox|sidebar|skyscraper|sponsor|shopping|tags|tool|widget/i,
+     extraneous: /print|archive|comment|discuss|e[\-]?mail|share|reply|all|login|sign|single|utility/i,
+     byline: /byline|author|dateline|writtenby|p-author/i,
+     replaceFonts: /<(\/?)font[^>]*>/gi,
+     normalize: /\s{2,}/g,
+     videos: /\/\/(www\.)?(dailymotion|youtube|youtube-nocookie|player\.vimeo)\.com/i,
+     nextLink: /(next|weiter|continue|>([^\|]|$)|»([^\|]|$))/i,
+     prevLink: /(prev|earl|old|new|<|«)/i,
+     whitespace: /^\s*$/,
+@@ -244,25 +244,25 @@ Readability.prototype = {
+    * subtree, except those that match CLASSES_TO_PRESERVE and
+    * the classesToPreserve array from the options object.
+    *
+    * @param Element
+    * @return void
+    */
+   _cleanClasses: function(node) {
+     var classesToPreserve = this._classesToPreserve;
+-    var className = node.className
++    var className = (node.getAttribute("class") || "")
+       .split(/\s+/)
+       .filter(function(cls) {
+         return classesToPreserve.indexOf(cls) != -1;
+       })
+       .join(" ");
+ 
+     if (className) {
+-      node.className = className;
++      node.setAttribute("class", className);
+     } else {
+       node.removeAttribute("class");
+     }
+ 
+     for (node = node.firstElementChild; node; node = node.nextElementSibling) {
+       this._cleanClasses(node);
+     }
+   },

+ 100 - 0
frg/mozilla-release/work-js/1430654-1-60a1.patch

@@ -0,0 +1,100 @@
+# HG changeset patch
+# User Hiroyuki Ikezoe <hikezoe@mozilla.com>
+# Date 1516600516 -32400
+# Node ID edbac2302f907df4fca6aea3843bd033a1a1bb89
+# Parent  c6b46ea3cea8b035e8df89afb69cafaf10e65ede
+Bug 1430654 - Introduce assert_time_equals_literal and use it. r=birtles
+
+This assertion is supposed to be used where the first argument has a tolerance
+but the second argument doesn't have such tolerance.  Whereas
+assert_times_equal() is supposed to be used for the case both arguments have
+the same tolerance, actually it hasn't, it will be fixed in a subsequent patch
+in this patch series.
+
+MozReview-Commit-ID: FEDHilbX2rm
+
+diff --git a/dom/animation/test/css-animations/file_animation-currenttime.html b/dom/animation/test/css-animations/file_animation-currenttime.html
+--- a/dom/animation/test/css-animations/file_animation-currenttime.html
++++ b/dom/animation/test/css-animations/file_animation-currenttime.html
+@@ -52,17 +52,17 @@ test(function(t)
+   assert_equals(animation.currentTime, 0,
+     'Animation.currentTime should be zero when an animation ' +
+     'is initially created');
+ 
+   // Make sure the animation is running before we set the current time.
+   animation.startTime = animation.timeline.currentTime;
+ 
+   animation.currentTime = 50 * MS_PER_SEC;
+-  assert_times_equal(animation.currentTime, 50 * MS_PER_SEC,
++  assert_time_equals_literal(animation.currentTime, 50 * MS_PER_SEC,
+     'Check setting of currentTime actually works');
+ }, 'Sanity test to check round-tripping assigning to new animation\'s ' +
+    'currentTime');
+ 
+ promise_test(function(t) {
+   var div = addDiv(t, {'class': 'animated-div'});
+   var eventWatcher = new EventWatcher(t, div, CSS_ANIM_EVENTS);
+   div.style.animation = "anim 100s 100s";
+diff --git a/dom/animation/test/css-animations/file_event-dispatch.html b/dom/animation/test/css-animations/file_event-dispatch.html
+--- a/dom/animation/test/css-animations/file_event-dispatch.html
++++ b/dom/animation/test/css-animations/file_event-dispatch.html
+@@ -120,30 +120,30 @@ promise_test(t => {
+   const { animation, watcher, div } = setupAnimation(t, 'anim 100s');
+ 
+   return watcher.wait_for('animationstart').then(evt => {
+     animation.currentTime = 100.0;
+     // Make idle
+     animation.timeline = null;
+     return watcher.wait_for('animationcancel');
+   }).then(evt => {
+-    assert_times_equal(evt.elapsedTime, 0.1);
++    assert_time_equals_literal(evt.elapsedTime, 0.1);
+   });
+ }, 'Active -> Idle, setting Animation.timeline = null');
+ 
+ promise_test(t => {
+   // we should NOT pause animation since calling cancel synchronously.
+   const { animation, watcher, div } = setupAnimation(t, 'anim 100s');
+ 
+   return watcher.wait_for('animationstart').then(evt => {
+     animation.currentTime = 50.0;
+     animation.cancel();
+     return watcher.wait_for('animationcancel');
+   }).then(evt => {
+-    assert_times_equal(evt.elapsedTime, 0.05);
++    assert_time_equals_literal(evt.elapsedTime, 0.05);
+   });
+ }, 'Active -> Idle, calling Animation.cancel()');
+ 
+ promise_test(t => {
+   const { animation, watcher } =
+     setupAnimation(t, 'anim 100s 100s paused');
+ 
+   // Seek to Active phase.
+diff --git a/dom/animation/test/testcommon.js b/dom/animation/test/testcommon.js
+--- a/dom/animation/test/testcommon.js
++++ b/dom/animation/test/testcommon.js
+@@ -25,16 +25,23 @@ var TIME_PRECISION = 0.0005; // ms
+  * Allow implementations to substitute an alternative method for comparing
+  * times based on their precision requirements.
+  */
+ function assert_times_equal(actual, expected, description) {
+   assert_approx_equals(actual, expected, TIME_PRECISION, description);
+ }
+ 
+ /*
++ * Compare a time value based on its precision requirements with a fixed value.
++ */
++function assert_time_equals_literal(actual, expected, description) {
++  assert_approx_equals(actual, expected, TIME_PRECISION, description);
++}
++
++/*
+  * Compare matrix string like 'matrix(1, 0, 0, 1, 100, 0)'.
+  * This function allows error, 0.01, because on Android when we are scaling down
+  * the document, it results in some errors.
+  */
+ function assert_matrix_equals(actual, expected, description) {
+   var matrixRegExp = /^matrix\((.+),(.+),(.+),(.+),(.+),(.+)\)/;
+   assert_regexp_match(actual, matrixRegExp,
+     'Actual value should be a matrix')

+ 34 - 0
frg/mozilla-release/work-js/1430654-2-60a1.patch

@@ -0,0 +1,34 @@
+# HG changeset patch
+# User Hiroyuki Ikezoe <hikezoe@mozilla.com>
+# Date 1516600677 -32400
+# Node ID a226a272784c3ac44a6b93136111fb78f2f312e5
+# Parent  595a1289338a250f5eb6d4eee608a32b5df3b4d1
+Bug 1430654 - Double epsilon value for assert_times_equal. r=birtles
+
+Since the function assumes that both of actual and expected values
+have the same precision requirements.
+
+MozReview-Commit-ID: 4C3TAH6mUVg
+
+diff --git a/dom/animation/test/testcommon.js b/dom/animation/test/testcommon.js
+--- a/dom/animation/test/testcommon.js
++++ b/dom/animation/test/testcommon.js
+@@ -21,17 +21,17 @@ const MS_PER_SEC = 1000;
+  */
+ var TIME_PRECISION = 0.0005; // ms
+ 
+ /*
+  * Allow implementations to substitute an alternative method for comparing
+  * times based on their precision requirements.
+  */
+ function assert_times_equal(actual, expected, description) {
+-  assert_approx_equals(actual, expected, TIME_PRECISION, description);
++  assert_approx_equals(actual, expected, TIME_PRECISION * 2, description);
+ }
+ 
+ /*
+  * Compare a time value based on its precision requirements with a fixed value.
+  */
+ function assert_time_equals_literal(actual, expected, description) {
+   assert_approx_equals(actual, expected, TIME_PRECISION, description);
+ }

+ 86 - 0
frg/mozilla-release/work-js/1430654-3-60a1.patch

@@ -0,0 +1,86 @@
+# HG changeset patch
+# User Hiroyuki Ikezoe <hikezoe@mozilla.com>
+# Date 1516600677 -32400
+# Node ID 235797f7083a060ada86a217dc7c4e83d238f252
+# Parent  f2ccfb973766eac572d8b506d81a19512490cfe0
+Bug 1430654 - Use assert_times_equal for comparing timing values. r=birtles
+
+MozReview-Commit-ID: CUn1f8M0jo5
+
+diff --git a/dom/animation/test/css-animations/file_animation-finish.html b/dom/animation/test/css-animations/file_animation-finish.html
+--- a/dom/animation/test/css-animations/file_animation-finish.html
++++ b/dom/animation/test/css-animations/file_animation-finish.html
+@@ -40,21 +40,20 @@ async_test(function(t) {
+   div.style.animation = ANIM_PROP_VAL + ' paused';
+   var animation = div.getAnimations()[0];
+ 
+   animation.ready.then(t.step_func(function() {
+     animation.finish();
+     assert_equals(animation.playState, 'finished',
+                   'The play state of a paused animation should become ' +
+                   '"finished" after finish() is called');
+-    assert_approx_equals(animation.startTime,
+-                         animation.timeline.currentTime - ANIM_DURATION,
+-                         0.0001,
+-                         'The start time of a paused animation should be set ' +
+-                         'after calling finish()');
++    assert_times_equal(animation.startTime,
++                       animation.timeline.currentTime - ANIM_DURATION,
++                       'The start time of a paused animation should be set ' +
++                       'after calling finish()');
+     t.done();
+   }));
+ }, 'Test finish() while paused');
+ 
+ test(function(t) {
+   var div = addDiv(t);
+   div.style.animation = ANIM_PROP_VAL + ' paused';
+   var animation = div.getAnimations()[0];
+@@ -64,21 +63,20 @@ test(function(t) {
+   animation.playbackRate = 2;
+ 
+   // While animation is still pause-pending call finish()
+   animation.finish();
+ 
+   assert_equals(animation.playState, 'finished',
+                 'The play state of a pause-pending animation should become ' +
+                 '"finished" after finish() is called');
+-  assert_approx_equals(animation.startTime,
+-                       animation.timeline.currentTime - ANIM_DURATION / 2,
+-                       0.0001,
+-                       'The start time of a pause-pending animation should ' +
+-                       'be set after calling finish()');
++  assert_times_equal(animation.startTime,
++                     animation.timeline.currentTime - ANIM_DURATION / 2,
++                     'The start time of a pause-pending animation should ' +
++                     'be set after calling finish()');
+ }, 'Test finish() while pause-pending with positive playbackRate');
+ 
+ test(function(t) {
+   var div = addDiv(t);
+   div.style.animation = ANIM_PROP_VAL + ' paused';
+   var animation = div.getAnimations()[0];
+ 
+   animation.playbackRate = -2;
+diff --git a/dom/animation/test/css-transitions/file_animation-starttime.html b/dom/animation/test/css-transitions/file_animation-starttime.html
+--- a/dom/animation/test/css-transitions/file_animation-starttime.html
++++ b/dom/animation/test/css-transitions/file_animation-starttime.html
+@@ -149,17 +149,17 @@ test(function(t)
+ {
+   var div = addDiv(t, {'class': 'animated-div'});
+   flushComputedStyle(div);
+   div.style.marginLeft = '200px'; // initiate transition
+ 
+   var animation = div.getAnimations()[0];
+   var currentTime = animation.timeline.currentTime;
+   animation.startTime = currentTime;
+-  assert_approx_equals(animation.startTime, currentTime, 0.0001, // rounding error
++  assert_times_equal(animation.startTime, currentTime,
+     'Check setting of startTime actually works');
+ }, 'Sanity test to check round-tripping assigning to new animation\'s ' +
+    'startTime');
+ 
+ 
+ async_test(function(t) {
+   var div = addDiv(t, {'class': 'animated-div'});
+   var eventWatcher = new EventWatcher(t, div, 'transitionend');

+ 36 - 0
frg/mozilla-release/work-js/1430745-59a1.patch

@@ -0,0 +1,36 @@
+# HG changeset patch
+# User Heiher <r@hev.cc>
+# Date 1516202248 -28800
+# Node ID f9019fd30399c75c7a93b5171f604800c17149ac
+# Parent  a9862959056544376806b65182cc5f55e19a7578
+Bug 1430745 - IPC: Fix unaligned accesses in DirReaderLinux. r=froydnj
+
+---
+ ipc/chromium/src/base/dir_reader_linux.h | 5 ++++-
+ 1 file changed, 4 insertions(+), 1 deletion(-)
+
+diff --git a/ipc/chromium/src/base/dir_reader_linux.h b/ipc/chromium/src/base/dir_reader_linux.h
+--- a/ipc/chromium/src/base/dir_reader_linux.h
++++ b/ipc/chromium/src/base/dir_reader_linux.h
+@@ -85,17 +85,20 @@ class DirReaderLinux {
+   }
+ 
+   static bool IsFallback() {
+     return false;
+   }
+ 
+  private:
+   const int fd_;
+-  unsigned char buf_[512];
++  union {
++    linux_dirent dirent_;
++    unsigned char buf_[512];
++  };
+   size_t offset_, size_;
+ 
+   DISALLOW_COPY_AND_ASSIGN(DirReaderLinux);
+ };
+ 
+ }  // namespace base
+ 
+ #endif // BASE_DIR_READER_LINUX_H_

+ 178 - 0
frg/mozilla-release/work-js/1431029-1-60a1.patch

@@ -0,0 +1,178 @@
+# HG changeset patch
+# User Johann Hofmann <jhofmann@mozilla.com>
+# Date 1519134857 -3600
+# Node ID 963a227648afe5d8d4544b8d3878f7a3bf062acf
+# Parent  cf46f500960643afb541426a1c51a54e8efedc5e
+Bug 1431029 - Expose timestamp for quota managed storage. r=asuth
+
+MozReview-Commit-ID: 7MNd2m2Jp46
+
+diff --git a/dom/quota/ActorsChild.cpp b/dom/quota/ActorsChild.cpp
+--- a/dom/quota/ActorsChild.cpp
++++ b/dom/quota/ActorsChild.cpp
+@@ -158,17 +158,18 @@ QuotaUsageRequestChild::HandleResponse(c
+ 
+     usageResults.SetCapacity(count);
+ 
+     for (uint32_t index = 0; index < count; index++) {
+       auto& originUsage = aResponse[index];
+ 
+       RefPtr<UsageResult> usageResult = new UsageResult(originUsage.origin(),
+                                                         originUsage.persisted(),
+-                                                        originUsage.usage());
++                                                        originUsage.usage(),
++                                                        originUsage.lastAccessed());
+ 
+       usageResults.AppendElement(usageResult.forget());
+     }
+ 
+     variant->SetAsArray(nsIDataType::VTYPE_INTERFACE_IS,
+                         &NS_GET_IID(nsIQuotaUsageResult),
+                         usageResults.Length(),
+                         static_cast<void*>(usageResults.Elements()));
+diff --git a/dom/quota/ActorsParent.cpp b/dom/quota/ActorsParent.cpp
+--- a/dom/quota/ActorsParent.cpp
++++ b/dom/quota/ActorsParent.cpp
+@@ -6973,16 +6973,18 @@ GetUsageOp::TraverseRepository(QuotaMana
+ 
+       mOriginUsagesIndex.Put(origin, index);
+     }
+ 
+     if (aPersistenceType == PERSISTENCE_TYPE_DEFAULT) {
+       originUsage->persisted() = persisted;
+     }
+ 
++    originUsage->lastAccessed() = timestamp;
++
+     UsageInfo usageInfo;
+     rv = GetUsageForOrigin(aQuotaManager,
+                            aPersistenceType,
+                            group,
+                            origin,
+                            &usageInfo);
+     if (NS_WARN_IF(NS_FAILED(rv))) {
+       return rv;
+diff --git a/dom/quota/PQuotaUsageRequest.ipdl b/dom/quota/PQuotaUsageRequest.ipdl
+--- a/dom/quota/PQuotaUsageRequest.ipdl
++++ b/dom/quota/PQuotaUsageRequest.ipdl
+@@ -8,16 +8,17 @@ namespace mozilla {
+ namespace dom {
+ namespace quota {
+ 
+ struct OriginUsage
+ {
+   nsCString origin;
+   bool persisted;
+   uint64_t usage;
++  uint64_t lastAccessed;
+ };
+ 
+ struct AllUsageResponse
+ {
+   OriginUsage[] originUsages;
+ };
+ 
+ struct OriginUsageResponse
+diff --git a/dom/quota/QuotaResults.cpp b/dom/quota/QuotaResults.cpp
+--- a/dom/quota/QuotaResults.cpp
++++ b/dom/quota/QuotaResults.cpp
+@@ -7,20 +7,22 @@
+ #include "QuotaResults.h"
+ 
+ namespace mozilla {
+ namespace dom {
+ namespace quota {
+ 
+ UsageResult::UsageResult(const nsACString& aOrigin,
+                          bool aPersisted,
+-                         uint64_t aUsage)
++                         uint64_t aUsage,
++                         uint64_t aLastAccessed)
+   : mOrigin(aOrigin)
+   , mUsage(aUsage)
+   , mPersisted(aPersisted)
++  , mLastAccessed(aLastAccessed)
+ {
+ }
+ 
+ NS_IMPL_ISUPPORTS(UsageResult,
+                   nsIQuotaUsageResult)
+ 
+ NS_IMETHODIMP
+ UsageResult::GetOrigin(nsACString& aOrigin)
+@@ -42,16 +44,25 @@ NS_IMETHODIMP
+ UsageResult::GetUsage(uint64_t* aUsage)
+ {
+   MOZ_ASSERT(aUsage);
+ 
+   *aUsage = mUsage;
+   return NS_OK;
+ }
+ 
++NS_IMETHODIMP
++UsageResult::GetLastAccessed(uint64_t* aLastAccessed)
++{
++  MOZ_ASSERT(aLastAccessed);
++
++  *aLastAccessed = mLastAccessed;
++  return NS_OK;
++}
++
+ OriginUsageResult::OriginUsageResult(uint64_t aUsage,
+                                      uint64_t aFileUsage,
+                                      uint64_t aLimit)
+   : mUsage(aUsage)
+   , mFileUsage(aFileUsage)
+   , mLimit(aLimit)
+ {
+ }
+diff --git a/dom/quota/QuotaResults.h b/dom/quota/QuotaResults.h
+--- a/dom/quota/QuotaResults.h
++++ b/dom/quota/QuotaResults.h
+@@ -14,21 +14,23 @@ namespace dom {
+ namespace quota {
+ 
+ class UsageResult
+   : public nsIQuotaUsageResult
+ {
+   nsCString mOrigin;
+   uint64_t mUsage;
+   bool mPersisted;
++  uint64_t mLastAccessed;
+ 
+ public:
+   UsageResult(const nsACString& aOrigin,
+               bool aPersisted,
+-              uint64_t aUsage);
++              uint64_t aUsage,
++              uint64_t aLastAccessed);
+ 
+ private:
+   virtual ~UsageResult()
+   { }
+ 
+   NS_DECL_ISUPPORTS
+   NS_DECL_NSIQUOTAUSAGERESULT
+ };
+diff --git a/dom/quota/nsIQuotaResults.idl b/dom/quota/nsIQuotaResults.idl
+--- a/dom/quota/nsIQuotaResults.idl
++++ b/dom/quota/nsIQuotaResults.idl
+@@ -9,16 +9,18 @@
+ [scriptable, function, uuid(d8c9328b-9aa8-4f5d-90e6-482de4a6d5b8)]
+ interface nsIQuotaUsageResult : nsISupports
+ {
+   readonly attribute ACString origin;
+ 
+   readonly attribute boolean persisted;
+ 
+   readonly attribute unsigned long long usage;
++
++  readonly attribute unsigned long long lastAccessed;
+ };
+ 
+ [scriptable, function, uuid(96df03d2-116a-493f-bb0b-118c212a6b32)]
+ interface nsIQuotaOriginUsageResult : nsISupports
+ {
+   readonly attribute unsigned long long usage;
+ 
+   readonly attribute unsigned long long fileUsage;

+ 1033 - 0
frg/mozilla-release/work-js/1431029-2-60a1.patch

@@ -0,0 +1,1033 @@
+# HG changeset patch
+# User Johann Hofmann <jhofmann@mozilla.com>
+# Date 1519134883 -3600
+# Node ID dced0221e5df52a19ee5886296ee61e147c1fd86
+# Parent  b3090be69c1cfeee3c8d9458a023e0b7961fe80e
+Bug 1431029 - Show a "last accessed" column in the site data manager. r=Gijs
+
+MozReview-Commit-ID: LidkPQ6kLfX
+
+diff --git a/browser/components/preferences/SiteDataManager.jsm b/browser/components/preferences/SiteDataManager.jsm
+--- a/browser/components/preferences/SiteDataManager.jsm
++++ b/browser/components/preferences/SiteDataManager.jsm
+@@ -75,16 +75,17 @@ var SiteDataManager = {
+   _getOrInsertSite(host) {
+     let site = this._sites.get(host);
+     if (!site) {
+       site = {
+         baseDomain: this._getBaseDomainFromHost(host),
+         cookies: [],
+         persisted: false,
+         quotaUsage: 0,
++        lastAccessed: 0,
+         principals: [],
+         appCacheList: [],
+       };
+       this._sites.set(host, site);
+     }
+     return site;
+   },
+ 
+@@ -148,16 +149,19 @@ var SiteDataManager = {
+               //   - Site A (not persisted): https://www.foo.com
+               //   - Site B (not persisted): https://www.foo.com^userContextId=2
+               //   - Site C (persisted):     https://www.foo.com:1234
+               // Although only C is persisted, grouping by host, as a result,
+               // we still mark as persisted here under this host group.
+               if (item.persisted) {
+                 site.persisted = true;
+               }
++              if (site.lastAccessed < item.lastAccessed) {
++                site.lastAccessed = item.lastAccessed;
++              }
+               site.principals.push(principal);
+               site.quotaUsage += item.usage;
+             }
+           }
+         }
+         resolve();
+       };
+       // XXX: The work of integrating localStorage into Quota Manager is in progress.
+@@ -169,16 +173,19 @@ var SiteDataManager = {
+   },
+ 
+   _getAllCookies() {
+     let cookiesEnum = Services.cookies.enumerator;
+     while (cookiesEnum.hasMoreElements()) {
+       let cookie = cookiesEnum.getNext().QueryInterface(Ci.nsICookie2);
+       let site = this._getOrInsertSite(cookie.rawHost);
+       site.cookies.push(cookie);
++      if (site.lastAccessed < cookie.lastAccessed) {
++        site.lastAccessed = cookie.lastAccessed;
++      }
+     }
+   },
+ 
+   _cancelGetQuotaUsage() {
+     if (this._quotaUsageRequest) {
+       this._quotaUsageRequest.cancel();
+       this._quotaUsageRequest = null;
+     }
+@@ -223,17 +230,18 @@ var SiteDataManager = {
+         for (let cache of site.appCacheList) {
+           usage += cache.usage;
+         }
+         list.push({
+           baseDomain: site.baseDomain,
+           cookies: site.cookies,
+           host,
+           usage,
+-          persisted: site.persisted
++          persisted: site.persisted,
++          lastAccessed: new Date(site.lastAccessed / 1000),
+         });
+       }
+       return list;
+     });
+   },
+ 
+   _removePermission(site) {
+     let removals = new Set();
+diff --git a/browser/components/preferences/in-content-new/tests/browser_siteData.js b/browser/components/preferences/in-content-new/tests/browser_siteData.js
+--- a/browser/components/preferences/in-content-new/tests/browser_siteData.js
++++ b/browser/components/preferences/in-content-new/tests/browser_siteData.js
+@@ -56,16 +56,18 @@ add_task(async function() {
+   // Always remember to clean up
+   OfflineAppCacheHelper.clear();
+   await new Promise(resolve => {
+     let principal = Services.scriptSecurityManager
+                             .createCodebasePrincipalFromOrigin(TEST_QUOTA_USAGE_ORIGIN);
+     let request = Services.qms.clearStoragesForPrincipal(principal, null, true);
+     request.callback = resolve;
+   });
++
++  await SiteDataManager.removeAll();
+   await BrowserTestUtils.removeTab(gBrowser.selectedTab);
+ }).skip(); // Bug 1414751
+ 
+ // Test buttons are disabled and loading message shown while updating sites
+ add_task(async function() {
+   let updatedPromise = promiseSiteDataManagerSitesUpdated();
+   await openPreferencesViaOpenPreferencesAPI("privacy", { leaveOpen: true });
+   await updatedPromise;
+@@ -154,87 +156,105 @@ add_task(async function() {
+       saveBtn.doCommand();
+     } else {
+       ok(false, `Should have one site of ${host}`);
+     }
+   });
+   await acceptRemovePromise;
+   await updatePromise;
+   await promiseServiceWorkersCleared();
++  await SiteDataManager.removeAll();
+   await BrowserTestUtils.removeTab(gBrowser.selectedTab);
+ });
+ 
+ // Test showing and removing sites with cookies.
+ add_task(async function() {
+-  SiteDataManager.removeAll();
+-
+   // Add some test cookies.
+   let uri = Services.io.newURI("https://example.com");
+   let uri2 = Services.io.newURI("https://example.org");
+   Services.cookies.add(uri.host, uri.pathQueryRef, "test1", "1",
+     false, false, false, Date.now() + 1000 * 60 * 60);
+   Services.cookies.add(uri.host, uri.pathQueryRef, "test2", "2",
+     false, false, false, Date.now() + 1000 * 60 * 60);
+   Services.cookies.add(uri2.host, uri2.pathQueryRef, "test1", "1",
+     false, false, false, Date.now() + 1000 * 60 * 60);
+ 
+   // Ensure that private browsing cookies are ignored.
+   Services.cookies.add(uri.host, uri.pathQueryRef, "test3", "3",
+     false, false, false, Date.now() + 1000 * 60 * 60, { privateBrowsingId: 1 });
+ 
++  // Get the exact creation date from the cookies (to avoid intermittents
++  // from minimal time differences, since we round up to minutes).
++  let cookiesEnum1 = Services.cookies.getCookiesFromHost(uri.host);
++  // We made two valid cookies for example.com.
++  cookiesEnum1.getNext();
++  let cookiesEnum2 = Services.cookies.getCookiesFromHost(uri2.host);
++  let cookie1 = cookiesEnum1.getNext().QueryInterface(Ci.nsICookie2);
++  let cookie2 = cookiesEnum2.getNext().QueryInterface(Ci.nsICookie2);
++
++  let formatter = new Services.intl.DateTimeFormat(undefined, {
++    dateStyle: "short", timeStyle: "short",
++  });
++
++  let creationDate1 = formatter.format(new Date(cookie1.lastAccessed / 1000));
++  let creationDate2 = formatter.format(new Date(cookie2.lastAccessed / 1000));
++
+   await openPreferencesViaOpenPreferencesAPI("privacy", { leaveOpen: true });
+ 
+   // Open the site data manager and remove one site.
+   await openSiteDataSettingsDialog();
+   let removeDialogOpenPromise = promiseWindowDialogOpen("accept", REMOVE_DIALOG_URL);
+-  ContentTask.spawn(gBrowser.selectedBrowser, null, function() {
++  await ContentTask.spawn(gBrowser.selectedBrowser, {creationDate1, creationDate2}, function(args) {
+     let frameDoc = content.gSubDialog._topDialog._frame.contentDocument;
+ 
+     let siteItems = frameDoc.getElementsByTagName("richlistitem");
+     is(siteItems.length, 2, "Should list two sites with cookies");
+     let sitesList = frameDoc.getElementById("sitesList");
+     let site1 = sitesList.querySelector(`richlistitem[host="example.com"]`);
+     let site2 = sitesList.querySelector(`richlistitem[host="example.org"]`);
+ 
+     let columns = site1.querySelectorAll(".item-box > label");
+     is(columns[0].value, "example.com", "Should show the correct host.");
+     is(columns[2].value, "2", "Should show the correct number of cookies.");
+     is(columns[3].value, "", "Should show no site data.");
++    is(columns[4].value, args.creationDate1, "Should show the correct date.");
+ 
+     columns = site2.querySelectorAll(".item-box > label");
+     is(columns[0].value, "example.org", "Should show the correct host.");
+     is(columns[2].value, "1", "Should show the correct number of cookies.");
+     is(columns[3].value, "", "Should show no site data.");
++    is(columns[4].value, args.creationDate2, "Should show the correct date.");
+ 
+     let removeBtn = frameDoc.getElementById("removeSelected");
+     let saveBtn = frameDoc.getElementById("save");
+     site2.click();
+     removeBtn.doCommand();
+     saveBtn.doCommand();
+   });
+   await removeDialogOpenPromise;
+ 
+   await TestUtils.waitForCondition(() => Services.cookies.countCookiesFromHost(uri2.host) == 0, "Cookies from the first host should be cleared");
+   is(Services.cookies.countCookiesFromHost(uri.host), 2, "Cookies from the second host should not be cleared");
+ 
+   // Open the site data manager and remove another site.
+   await openSiteDataSettingsDialog();
+   let acceptRemovePromise = promiseAlertDialogOpen("accept");
+-  ContentTask.spawn(gBrowser.selectedBrowser, null, function() {
++  await ContentTask.spawn(gBrowser.selectedBrowser, {creationDate1}, function(args) {
+     let frameDoc = content.gSubDialog._topDialog._frame.contentDocument;
+ 
+     let siteItems = frameDoc.getElementsByTagName("richlistitem");
+     is(siteItems.length, 1, "Should list one site with cookies");
+     let sitesList = frameDoc.getElementById("sitesList");
+     let site1 = sitesList.querySelector(`richlistitem[host="example.com"]`);
+ 
+     let columns = site1.querySelectorAll(".item-box > label");
+     is(columns[0].value, "example.com", "Should show the correct host.");
+     is(columns[2].value, "2", "Should show the correct number of cookies.");
+     is(columns[3].value, "", "Should show no site data.");
++    is(columns[4].value, args.creationDate1, "Should show the correct date.");
+ 
+     let removeBtn = frameDoc.getElementById("removeSelected");
+     let saveBtn = frameDoc.getElementById("save");
+     site1.click();
+     removeBtn.doCommand();
+     saveBtn.doCommand();
+   });
+   await acceptRemovePromise;
+diff --git a/browser/components/preferences/in-content-new/tests/browser_siteData2.js b/browser/components/preferences/in-content-new/tests/browser_siteData2.js
+--- a/browser/components/preferences/in-content-new/tests/browser_siteData2.js
++++ b/browser/components/preferences/in-content-new/tests/browser_siteData2.js
+@@ -105,17 +105,17 @@ add_task(async function() {
+   assertAllSitesNotListed(win);
+   saveBtn.doCommand();
+   await acceptPromise;
+   await settingsDialogClosePromise;
+   await updatePromise;
+   await openSiteDataSettingsDialog();
+   assertAllSitesNotListed(win);
+ 
+-  mockSiteDataManager.unregister();
++  await mockSiteDataManager.unregister();
+   await BrowserTestUtils.removeTab(gBrowser.selectedTab);
+ 
+   function removeAllSitesOneByOne() {
+     frameDoc = win.gSubDialog._topDialog._frame.contentDocument;
+     let removeBtn = frameDoc.getElementById("removeSelected");
+     let sitesList = frameDoc.getElementById("sitesList");
+     let sites = sitesList.getElementsByTagName("richlistitem");
+     for (let i = sites.length - 1; i >= 0; --i) {
+@@ -216,17 +216,17 @@ add_task(async function() {
+   removeSelectedSite(fakeHosts.slice(0, 2));
+   assertSitesListed(doc, fakeHosts.slice(2));
+   saveBtn.doCommand();
+   await removeDialogOpenPromise;
+   await settingsDialogClosePromise;
+   await openSiteDataSettingsDialog();
+   assertSitesListed(doc, fakeHosts.slice(2));
+ 
+-  mockSiteDataManager.unregister();
++  await mockSiteDataManager.unregister();
+   await BrowserTestUtils.removeTab(gBrowser.selectedTab);
+ 
+   function removeSelectedSite(hosts) {
+     frameDoc = win.gSubDialog._topDialog._frame.contentDocument;
+     let removeBtn = frameDoc.getElementById("removeSelected");
+     let sitesList = frameDoc.getElementById("sitesList");
+     hosts.forEach(host => {
+       let site = sitesList.querySelector(`richlistitem[host="${host}"]`);
+@@ -291,17 +291,17 @@ add_task(async function() {
+   removeAllBtn.doCommand();
+   saveBtn.doCommand();
+   await acceptRemovePromise;
+   await settingsDialogClosePromise;
+   await updatePromise;
+   await openSiteDataSettingsDialog();
+   assertSitesListed(doc, fakeHosts.filter(host => !host.includes("xyz")));
+ 
+-  mockSiteDataManager.unregister();
++  await mockSiteDataManager.unregister();
+   await BrowserTestUtils.removeTab(gBrowser.selectedTab);
+ });
+ 
+ // Test dynamically clearing all site data
+ add_task(async function() {
+   mockSiteDataManager.register(SiteDataManager, [
+     {
+       usage: 1024,
+@@ -348,11 +348,11 @@ add_task(async function() {
+   removeAllBtn.doCommand();
+   saveBtn.doCommand();
+   await acceptRemovePromise;
+   await settingsDialogClosePromise;
+   await updatePromise;
+   await openSiteDataSettingsDialog();
+   assertAllSitesNotListed(win);
+ 
+-  mockSiteDataManager.unregister();
++  await mockSiteDataManager.unregister();
+   await BrowserTestUtils.removeTab(gBrowser.selectedTab);
+ });
+diff --git a/browser/components/preferences/in-content-new/tests/browser_siteData3.js b/browser/components/preferences/in-content-new/tests/browser_siteData3.js
+--- a/browser/components/preferences/in-content-new/tests/browser_siteData3.js
++++ b/browser/components/preferences/in-content-new/tests/browser_siteData3.js
+@@ -37,17 +37,17 @@ add_task(async function() {
+ 
+   let updatePromise = promiseSiteDataManagerSitesUpdated();
+   let doc = gBrowser.selectedBrowser.contentDocument;
+   await openPreferencesViaOpenPreferencesAPI("privacy", { leaveOpen: true });
+   await updatePromise;
+   await openSiteDataSettingsDialog();
+   assertSitesListed(doc, fakeHosts.filter(host => host != "shopping.xyz.com"));
+ 
+-  mockSiteDataManager.unregister();
++  await mockSiteDataManager.unregister();
+   await BrowserTestUtils.removeTab(gBrowser.selectedTab);
+ });
+ 
+ // Test grouping and listing sites across scheme, port and origin attributes by host
+ add_task(async function() {
+   await SpecialPowers.pushPrefEnv({set: [["browser.storageManager.enabled", true]]});
+   const quotaUsage = 1024;
+   mockSiteDataManager.register(SiteDataManager, [
+@@ -98,41 +98,41 @@ add_task(async function() {
+   is(columns[1].value, expected, "Should mark persisted status across scheme, port and origin attributes");
+ 
+   is(columns[2].value, "5", "Should group cookies across scheme, port and origin attributes");
+ 
+   expected = prefStrBundle.getFormattedString("siteUsage",
+     DownloadUtils.convertByteUnits(quotaUsage * mockSiteDataManager.fakeSites.length));
+   is(columns[3].value, expected, "Should sum up usages across scheme, port and origin attributes");
+ 
+-  mockSiteDataManager.unregister();
++  await mockSiteDataManager.unregister();
+   await BrowserTestUtils.removeTab(gBrowser.selectedTab);
+ });
+ 
+ // Test sorting
+ add_task(async function() {
+   await SpecialPowers.pushPrefEnv({set: [["browser.storageManager.enabled", true]]});
+   mockSiteDataManager.register(SiteDataManager, [
+     {
+       usage: 1024,
+       origin: "https://account.xyz.com",
+       cookies: 6,
+-      persisted: true
++      persisted: true,
+     },
+     {
+       usage: 1024 * 2,
+       origin: "https://books.foo.com",
+       cookies: 0,
+-      persisted: false
++      persisted: false,
+     },
+     {
+       usage: 1024 * 3,
+       origin: "http://cinema.bar.com",
+       cookies: 3,
+-      persisted: true
++      persisted: true,
+     },
+   ]);
+ 
+   let updatePromise = promiseSiteDataManagerSitesUpdated();
+   await openPreferencesViaOpenPreferencesAPI("privacy", { leaveOpen: true });
+   await updatePromise;
+   await openSiteDataSettingsDialog();
+ 
+@@ -167,17 +167,17 @@ add_task(async function() {
+   assertSortByCookies("descending");
+ 
+   // Test sorting on the cookies column
+   statusCol.click();
+   assertSortByStatus("ascending");
+   statusCol.click();
+   assertSortByStatus("descending");
+ 
+-  mockSiteDataManager.unregister();
++  await mockSiteDataManager.unregister();
+   await BrowserTestUtils.removeTab(gBrowser.selectedTab);
+ 
+   function assertSortByBaseDomain(order) {
+     let siteItems = sitesList.getElementsByTagName("richlistitem");
+     for (let i = 0; i < siteItems.length - 1; ++i) {
+       let aHost = siteItems[i].getAttribute("host");
+       let bHost = siteItems[i + 1].getAttribute("host");
+       let a = findSiteByHost(aHost);
+@@ -243,8 +243,74 @@ add_task(async function() {
+       }
+     }
+   }
+ 
+   function findSiteByHost(host) {
+     return mockSiteDataManager.fakeSites.find(site => site.principal.URI.host == host);
+   }
+ });
++
++// Test sorting based on access date (separate from cookies for simplicity,
++// since cookies access date affects this as well, but we don't mock our cookies)
++add_task(async function() {
++  mockSiteDataManager.register(SiteDataManager, [
++    {
++      usage: 1024,
++      origin: "https://account.xyz.com",
++      persisted: true,
++      lastAccessed: (Date.now() - 120 * 1000) * 1000,
++    },
++    {
++      usage: 1024 * 2,
++      origin: "https://books.foo.com",
++      persisted: false,
++      lastAccessed: (Date.now() - 240 * 1000) * 1000,
++    },
++    {
++      usage: 1024 * 3,
++      origin: "http://cinema.bar.com",
++      persisted: true,
++      lastAccessed: Date.now() * 1000,
++    },
++  ]);
++
++  let updatePromise = promiseSiteDataManagerSitesUpdated();
++  await openPreferencesViaOpenPreferencesAPI("privacy", { leaveOpen: true });
++  await updatePromise;
++  await openSiteDataSettingsDialog();
++
++  // eslint-disable-next-line mozilla/no-cpows-in-tests
++  let dialog = content.gSubDialog._topDialog;
++  let dialogFrame = dialog._frame;
++  let frameDoc = dialogFrame.contentDocument;
++  let lastAccessedCol = frameDoc.getElementById("lastAccessedCol");
++  let sitesList = frameDoc.getElementById("sitesList");
++
++  // Test sorting on the date column
++  lastAccessedCol.click();
++  assertSortByDate("ascending");
++  lastAccessedCol.click();
++  assertSortByDate("descending");
++
++  await mockSiteDataManager.unregister();
++  await BrowserTestUtils.removeTab(gBrowser.selectedTab);
++
++  function assertSortByDate(order) {
++    let siteItems = sitesList.getElementsByTagName("richlistitem");
++    for (let i = 0; i < siteItems.length - 1; ++i) {
++      let aHost = siteItems[i].getAttribute("host");
++      let bHost = siteItems[i + 1].getAttribute("host");
++      let a = findSiteByHost(aHost);
++      let b = findSiteByHost(bHost);
++      let result = a.lastAccessed - b.lastAccessed;
++      if (order == "ascending") {
++        Assert.lessOrEqual(result, 0, "Should sort sites in the ascending order by date");
++      } else {
++        Assert.greaterOrEqual(result, 0, "Should sort sites in the descending order date");
++      }
++    }
++  }
++
++  function findSiteByHost(host) {
++    return mockSiteDataManager.fakeSites.find(site => site.principal.URI.host == host);
++  }
++});
+diff --git a/browser/components/preferences/in-content-new/tests/head.js b/browser/components/preferences/in-content-new/tests/head.js
+--- a/browser/components/preferences/in-content-new/tests/head.js
++++ b/browser/components/preferences/in-content-new/tests/head.js
+@@ -250,17 +250,18 @@ const mockSiteDataManager = {
+   _SiteDataManager: null,
+   _originalQMS: null,
+   _originalRemoveQuotaUsage: null,
+ 
+   getUsage(onUsageResult) {
+     let result = this.fakeSites.map(site => ({
+       origin: site.principal.origin,
+       usage: site.usage,
+-      persisted: site.persisted
++      persisted: site.persisted,
++      lastAccessed: site.lastAccessed,
+     }));
+     onUsageResult({ result, resultCode: Components.results.NS_OK });
+   },
+ 
+   _removeQuotaUsage(site) {
+     var target = site.principals[0].URI.host;
+     this.fakeSites = this.fakeSites.filter(fakeSite => {
+       return fakeSite.principal.URI.host != target;
+@@ -291,19 +292,19 @@ const mockSiteDataManager = {
+       // Add some cookies if needed.
+       for (let i = 0; i < (site.cookies || 0); i++) {
+         Services.cookies.add(uri.host, uri.pathQueryRef, Cu.now(), i,
+           false, false, false, Date.now() + 1000 * 60 * 60);
+       }
+     }
+   },
+ 
+-  unregister() {
++  async unregister() {
++    await this._SiteDataManager.removeAll();
+     this.fakeSites = null;
+-    this._SiteDataManager.removeAll();
+     this._SiteDataManager._qms = this._originalQMS;
+     this._SiteDataManager._removeQuotaUsage = this._originalRemoveQuotaUsage;
+   }
+ };
+ 
+ function getQuotaUsage(origin) {
+   return new Promise(resolve => {
+     let uri = NetUtil.newURI(origin);
+diff --git a/browser/components/preferences/in-content/tests/browser_siteData.js b/browser/components/preferences/in-content/tests/browser_siteData.js
+--- a/browser/components/preferences/in-content/tests/browser_siteData.js
++++ b/browser/components/preferences/in-content/tests/browser_siteData.js
+@@ -53,16 +53,18 @@ add_task(async function() {
+   // Always remember to clean up
+   OfflineAppCacheHelper.clear();
+   await new Promise(resolve => {
+     let principal = Services.scriptSecurityManager
+                             .createCodebasePrincipalFromOrigin(TEST_QUOTA_USAGE_ORIGIN);
+     let request = Services.qms.clearStoragesForPrincipal(principal, null, true);
+     request.callback = resolve;
+   });
++
++  await SiteDataManager.removeAll();
+   await BrowserTestUtils.removeTab(gBrowser.selectedTab);
+ }).skip(); // Bug 1414751
+ 
+ // Test buttons are disabled and loading message shown while updating sites
+ add_task(async function() {
+   let updatedPromise = promiseSitesUpdated();
+   await openPreferencesViaOpenPreferencesAPI("advanced", "networkTab", { leaveOpen: true });
+   await updatedPromise;
+@@ -152,87 +154,105 @@ add_task(async function() {
+       saveBtn.doCommand();
+     } else {
+       ok(false, `Should have one site of ${host}`);
+     }
+   });
+   await acceptRemovePromise;
+   await updatePromise;
+   await promiseServiceWorkersCleared();
++  await SiteDataManager.removeAll();
+   await BrowserTestUtils.removeTab(gBrowser.selectedTab);
+ });
+ 
+ // Test showing and removing sites with cookies.
+ add_task(async function() {
+-  SiteDataManager.removeAll();
+-
+   // Add some test cookies.
+   let uri = Services.io.newURI("https://example.com");
+   let uri2 = Services.io.newURI("https://example.org");
+   Services.cookies.add(uri.host, uri.pathQueryRef, "test1", "1",
+     false, false, false, Date.now() + 1000 * 60 * 60);
+   Services.cookies.add(uri.host, uri.pathQueryRef, "test2", "2",
+     false, false, false, Date.now() + 1000 * 60 * 60);
+   Services.cookies.add(uri2.host, uri2.pathQueryRef, "test1", "1",
+     false, false, false, Date.now() + 1000 * 60 * 60);
+ 
+   // Ensure that private browsing cookies are ignored.
+   Services.cookies.add(uri.host, uri.pathQueryRef, "test3", "3",
+     false, false, false, Date.now() + 1000 * 60 * 60, { privateBrowsingId: 1 });
+ 
++  // Get the exact creation date from the cookies (to avoid intermittents
++  // from minimal time differences, since we round up to minutes).
++  let cookiesEnum1 = Services.cookies.getCookiesFromHost(uri.host);
++  // We made two valid cookies for example.com.
++  cookiesEnum1.getNext();
++  let cookiesEnum2 = Services.cookies.getCookiesFromHost(uri2.host);
++  let cookie1 = cookiesEnum1.getNext().QueryInterface(Ci.nsICookie2);
++  let cookie2 = cookiesEnum2.getNext().QueryInterface(Ci.nsICookie2);
++
++  let formatter = new Services.intl.DateTimeFormat(undefined, {
++    dateStyle: "short", timeStyle: "short",
++  });
++
++  let creationDate1 = formatter.format(new Date(cookie1.lastAccessed / 1000));
++  let creationDate2 = formatter.format(new Date(cookie2.lastAccessed / 1000));
++
+   await openPreferencesViaOpenPreferencesAPI("privacy", { leaveOpen: true });
+ 
+   // Open the site data manager and remove one site.
+   await openSiteDataSettingsDialog();
+   let removeDialogOpenPromise = promiseWindowDialogOpen("accept", REMOVE_DIALOG_URL);
+-  ContentTask.spawn(gBrowser.selectedBrowser, null, function() {
++  await ContentTask.spawn(gBrowser.selectedBrowser, {creationDate1, creationDate2}, function(args) {
+     let frameDoc = content.gSubDialog._topDialog._frame.contentDocument;
+ 
+     let siteItems = frameDoc.getElementsByTagName("richlistitem");
+     is(siteItems.length, 2, "Should list two sites with cookies");
+     let sitesList = frameDoc.getElementById("sitesList");
+     let site1 = sitesList.querySelector(`richlistitem[host="example.com"]`);
+     let site2 = sitesList.querySelector(`richlistitem[host="example.org"]`);
+ 
+     let columns = site1.querySelectorAll(".item-box > label");
+     is(columns[0].value, "example.com", "Should show the correct host.");
+     is(columns[2].value, "2", "Should show the correct number of cookies.");
+     is(columns[3].value, "", "Should show no site data.");
++    is(columns[4].value, args.creationDate1, "Should show the correct date.");
+ 
+     columns = site2.querySelectorAll(".item-box > label");
+     is(columns[0].value, "example.org", "Should show the correct host.");
+     is(columns[2].value, "1", "Should show the correct number of cookies.");
+     is(columns[3].value, "", "Should show no site data.");
++    is(columns[4].value, args.creationDate2, "Should show the correct date.");
+ 
+     let removeBtn = frameDoc.getElementById("removeSelected");
+     let saveBtn = frameDoc.getElementById("save");
+     site2.click();
+     removeBtn.doCommand();
+     saveBtn.doCommand();
+   });
+   await removeDialogOpenPromise;
+ 
+   await TestUtils.waitForCondition(() => Services.cookies.countCookiesFromHost(uri2.host) == 0, "Cookies from the first host should be cleared");
+   is(Services.cookies.countCookiesFromHost(uri.host), 2, "Cookies from the second host should not be cleared");
+ 
+   // Open the site data manager and remove another site.
+   await openSiteDataSettingsDialog();
+   let acceptRemovePromise = promiseAlertDialogOpen("accept");
+-  ContentTask.spawn(gBrowser.selectedBrowser, null, function() {
++  await ContentTask.spawn(gBrowser.selectedBrowser, {creationDate1}, function(args) {
+     let frameDoc = content.gSubDialog._topDialog._frame.contentDocument;
+ 
+     let siteItems = frameDoc.getElementsByTagName("richlistitem");
+     is(siteItems.length, 1, "Should list one site with cookies");
+     let sitesList = frameDoc.getElementById("sitesList");
+     let site1 = sitesList.querySelector(`richlistitem[host="example.com"]`);
+ 
+     let columns = site1.querySelectorAll(".item-box > label");
+     is(columns[0].value, "example.com", "Should show the correct host.");
+     is(columns[2].value, "2", "Should show the correct number of cookies.");
+     is(columns[3].value, "", "Should show no site data.");
++    is(columns[4].value, args.creationDate1, "Should show the correct date.");
+ 
+     let removeBtn = frameDoc.getElementById("removeSelected");
+     let saveBtn = frameDoc.getElementById("save");
+     site1.click();
+     removeBtn.doCommand();
+     saveBtn.doCommand();
+   });
+   await acceptRemovePromise;
+diff --git a/browser/components/preferences/in-content/tests/browser_siteData2.js b/browser/components/preferences/in-content/tests/browser_siteData2.js
+--- a/browser/components/preferences/in-content/tests/browser_siteData2.js
++++ b/browser/components/preferences/in-content/tests/browser_siteData2.js
+@@ -89,17 +89,17 @@ add_task(async function() {
+   assertAllSitesNotListed(win);
+   saveBtn.doCommand();
+   await acceptPromise;
+   await settingsDialogClosePromise;
+   await updatePromise;
+   await openSettingsDialog();
+   assertAllSitesNotListed(win);
+ 
+-  mockSiteDataManager.unregister();
++  await mockSiteDataManager.unregister();
+   await BrowserTestUtils.removeTab(gBrowser.selectedTab);
+ 
+   function removeAllSitesOneByOne() {
+     frameDoc = win.gSubDialog._topDialog._frame.contentDocument;
+     let removeBtn = frameDoc.getElementById("removeSelected");
+     let sitesList = frameDoc.getElementById("sitesList");
+     let sites = sitesList.getElementsByTagName("richlistitem");
+     for (let i = sites.length - 1; i >= 0; --i) {
+@@ -200,17 +200,17 @@ add_task(async function() {
+   removeSelectedSite(fakeHosts.slice(0, 2));
+   assertSitesListed(doc, fakeHosts.slice(2));
+   saveBtn.doCommand();
+   await removeDialogOpenPromise;
+   await settingsDialogClosePromise;
+   await openSettingsDialog();
+   assertSitesListed(doc, fakeHosts.slice(2));
+ 
+-  mockSiteDataManager.unregister();
++  await mockSiteDataManager.unregister();
+   await BrowserTestUtils.removeTab(gBrowser.selectedTab);
+ 
+   function removeSelectedSite(hosts) {
+     frameDoc = win.gSubDialog._topDialog._frame.contentDocument;
+     let removeBtn = frameDoc.getElementById("removeSelected");
+     let sitesList = frameDoc.getElementById("sitesList");
+     hosts.forEach(host => {
+       let site = sitesList.querySelector(`richlistitem[host="${host}"]`);
+@@ -275,17 +275,17 @@ add_task(async function() {
+   removeAllBtn.doCommand();
+   saveBtn.doCommand();
+   await acceptRemovePromise;
+   await settingsDialogClosePromise;
+   await updatePromise;
+   await openSettingsDialog();
+   assertSitesListed(doc, fakeHosts.filter(host => !host.includes("xyz")));
+ 
+-  mockSiteDataManager.unregister();
++  await mockSiteDataManager.unregister();
+   await BrowserTestUtils.removeTab(gBrowser.selectedTab);
+ });
+ 
+ // Test dynamically clearing all site data
+ add_task(async function() {
+   mockSiteDataManager.register([
+     {
+       usage: 1024,
+@@ -332,11 +332,11 @@ add_task(async function() {
+   removeAllBtn.doCommand();
+   saveBtn.doCommand();
+   await acceptRemovePromise;
+   await settingsDialogClosePromise;
+   await updatePromise;
+   await openSettingsDialog();
+   assertAllSitesNotListed(win);
+ 
+-  mockSiteDataManager.unregister();
++  await mockSiteDataManager.unregister();
+   await BrowserTestUtils.removeTab(gBrowser.selectedTab);
+ });
+diff --git a/browser/components/preferences/in-content/tests/browser_siteData3.js b/browser/components/preferences/in-content/tests/browser_siteData3.js
+--- a/browser/components/preferences/in-content/tests/browser_siteData3.js
++++ b/browser/components/preferences/in-content/tests/browser_siteData3.js
+@@ -44,17 +44,17 @@ add_task(async function() {
+   searchBox.value = "bar";
+   searchBox.doCommand();
+   assertSitesListed(doc, fakeHosts.filter(host => host.includes("bar")));
+ 
+   searchBox.value = "";
+   searchBox.doCommand();
+   assertSitesListed(doc, fakeHosts);
+ 
+-  mockSiteDataManager.unregister();
++  await mockSiteDataManager.unregister();
+   await BrowserTestUtils.removeTab(gBrowser.selectedTab);
+ });
+ 
+ // Test not displaying sites which store 0 byte and don't have persistent storage.
+ add_task(async function() {
+   await SpecialPowers.pushPrefEnv({set: [["browser.storageManager.enabled", true]]});
+   mockSiteDataManager.register([
+     {
+@@ -88,41 +88,41 @@ add_task(async function() {
+ 
+   let updatePromise = promiseSitesUpdated();
+   let doc = gBrowser.selectedBrowser.contentDocument;
+   await openPreferencesViaOpenPreferencesAPI("advanced", "networkTab", { leaveOpen: true });
+   await updatePromise;
+   await openSettingsDialog();
+   assertSitesListed(doc, fakeHosts.filter(host => host != "shopping.xyz.com"));
+ 
+-  mockSiteDataManager.unregister();
++  await mockSiteDataManager.unregister();
+   await BrowserTestUtils.removeTab(gBrowser.selectedTab);
+ });
+ 
+ // Test sorting
+ add_task(async function() {
+   await SpecialPowers.pushPrefEnv({set: [["browser.storageManager.enabled", true]]});
+   mockSiteDataManager.register([
+     {
+       usage: 1024,
+       origin: "https://account.xyz.com",
+       cookies: 6,
+-      persisted: true
++      persisted: true,
+     },
+     {
+       usage: 1024 * 2,
+       origin: "https://books.foo.com",
+       cookies: 0,
+-      persisted: false
++      persisted: false,
+     },
+     {
+       usage: 1024 * 3,
+       origin: "http://cinema.bar.com",
+       cookies: 3,
+-      persisted: true
++      persisted: true,
+     },
+   ]);
+ 
+   let updatePromise = promiseSiteDataManagerSitesUpdated();
+   await openPreferencesViaOpenPreferencesAPI("privacy", { leaveOpen: true });
+   await updatePromise;
+   await openSiteDataSettingsDialog();
+ 
+@@ -157,17 +157,17 @@ add_task(async function() {
+   assertSortByCookies("descending");
+ 
+   // Test sorting on the cookies column
+   statusCol.click();
+   assertSortByStatus("ascending");
+   statusCol.click();
+   assertSortByStatus("descending");
+ 
+-  mockSiteDataManager.unregister();
++  await mockSiteDataManager.unregister();
+   await BrowserTestUtils.removeTab(gBrowser.selectedTab);
+ 
+   function assertSortByBaseDomain(order) {
+     let siteItems = sitesList.getElementsByTagName("richlistitem");
+     for (let i = 0; i < siteItems.length - 1; ++i) {
+       let aHost = siteItems[i].getAttribute("host");
+       let bHost = siteItems[i + 1].getAttribute("host");
+       let a = findSiteByHost(aHost);
+@@ -233,8 +233,74 @@ add_task(async function() {
+       }
+     }
+   }
+ 
+   function findSiteByHost(host) {
+     return mockSiteDataManager.fakeSites.find(site => site.principal.URI.host == host);
+   }
+ });
++
++// Test sorting based on access date (separate from cookies for simplicity,
++// since cookies access date affects this as well, but we don't mock our cookies)
++add_task(async function() {
++  mockSiteDataManager.register([
++    {
++      usage: 1024,
++      origin: "https://account.xyz.com",
++      persisted: true,
++      lastAccessed: (Date.now() - 120 * 1000) * 1000,
++    },
++    {
++      usage: 1024 * 2,
++      origin: "https://books.foo.com",
++      persisted: false,
++      lastAccessed: (Date.now() - 240 * 1000) * 1000,
++    },
++    {
++      usage: 1024 * 3,
++      origin: "http://cinema.bar.com",
++      persisted: true,
++      lastAccessed: Date.now() * 1000,
++    },
++  ]);
++
++  let updatePromise = promiseSiteDataManagerSitesUpdated();
++  await openPreferencesViaOpenPreferencesAPI("privacy", { leaveOpen: true });
++  await updatePromise;
++  await openSiteDataSettingsDialog();
++
++  // eslint-disable-next-line mozilla/no-cpows-in-tests
++  let dialog = content.gSubDialog._topDialog;
++  let dialogFrame = dialog._frame;
++  let frameDoc = dialogFrame.contentDocument;
++  let lastAccessedCol = frameDoc.getElementById("lastAccessedCol");
++  let sitesList = frameDoc.getElementById("sitesList");
++
++  // Test sorting on the date column
++  lastAccessedCol.click();
++  assertSortByDate("ascending");
++  lastAccessedCol.click();
++  assertSortByDate("descending");
++
++  await mockSiteDataManager.unregister();
++  await BrowserTestUtils.removeTab(gBrowser.selectedTab);
++
++  function assertSortByDate(order) {
++    let siteItems = sitesList.getElementsByTagName("richlistitem");
++    for (let i = 0; i < siteItems.length - 1; ++i) {
++      let aHost = siteItems[i].getAttribute("host");
++      let bHost = siteItems[i + 1].getAttribute("host");
++      let a = findSiteByHost(aHost);
++      let b = findSiteByHost(bHost);
++      let result = a.lastAccessed - b.lastAccessed;
++      if (order == "ascending") {
++        Assert.lessOrEqual(result, 0, "Should sort sites in the ascending order by date");
++      } else {
++        Assert.greaterOrEqual(result, 0, "Should sort sites in the descending order date");
++      }
++    }
++  }
++
++  function findSiteByHost(host) {
++    return mockSiteDataManager.fakeSites.find(site => site.principal.URI.host == host);
++  }
++});
+diff --git a/browser/components/preferences/in-content/tests/head.js b/browser/components/preferences/in-content/tests/head.js
+--- a/browser/components/preferences/in-content/tests/head.js
++++ b/browser/components/preferences/in-content/tests/head.js
+@@ -22,17 +22,18 @@ const mockSiteDataManager = {
+   _originalGetQuotaUsage: null,
+   _originalQMS: null,
+   _originalRemoveQuotaUsage: null,
+ 
+   getUsage(onUsageResult) {
+     let result = this.fakeSites.map(site => ({
+       origin: site.principal.origin,
+       usage: site.usage,
+-      persisted: site.persisted
++      persisted: site.persisted,
++      lastAccessed: site.lastAccessed,
+     }));
+     onUsageResult({ result, resultCode: Components.results.NS_OK });
+   },
+ 
+   _removeQuotaUsage(site) {
+     var target = site.principals[0].URI.host;
+     this.fakeSites = this.fakeSites.filter(fakeSite => {
+       return fakeSite.principal.URI.host != target;
+@@ -62,19 +63,19 @@ const mockSiteDataManager = {
+       // Add some cookies if needed.
+       for (let i = 0; i < (site.cookies || 0); i++) {
+         Services.cookies.add(uri.host, uri.pathQueryRef, Cu.now(), i,
+           false, false, false, Date.now() + 1000 * 60 * 60);
+       }
+     }
+   },
+ 
+-  unregister() {
++  async unregister() {
++    await SiteDataManager.removeAll();
+     this.fakeSites = null;
+-    SiteDataManager.removeAll();
+     SiteDataManager._qms = this._originalQMS;
+     SiteDataManager._removeQuotaUsage = this._originalRemoveQuotaUsage;
+   }
+ };
+ 
+ function is_hidden(aElement) {
+   var style = aElement.ownerGlobal.getComputedStyle(aElement);
+   if (style.display == "none")
+diff --git a/browser/components/preferences/siteDataSettings.js b/browser/components/preferences/siteDataSettings.js
+--- a/browser/components/preferences/siteDataSettings.js
++++ b/browser/components/preferences/siteDataSettings.js
+@@ -62,26 +62,34 @@ let gSiteDataSettings = {
+       let size = DownloadUtils.convertByteUnits(site.usage);
+       let str = this._prefStrBundle.getFormattedString("siteUsage", size);
+       addColumnItem(str, "1");
+     } else {
+       // Pass null to avoid showing "0KB" when there is no site data stored.
+       addColumnItem(null, "1");
+     }
+ 
++    // Add "Last Used" column.
++    addColumnItem(site.lastAccessed > 0 ?
++      this._formatter.format(site.lastAccessed) : null, "2");
++
+     item.appendChild(container);
+     return item;
+   },
+ 
+   init() {
+     function setEventListener(id, eventType, callback) {
+       document.getElementById(id)
+               .addEventListener(eventType, callback.bind(gSiteDataSettings));
+     }
+ 
++    this._formatter = new Services.intl.DateTimeFormat(undefined, {
++      dateStyle: "short", timeStyle: "short",
++    });
++
+     this._list = document.getElementById("sitesList");
+     this._searchBox = document.getElementById("searchBox");
+     this._prefStrBundle = document.getElementById("bundlePreferences");
+     SiteDataManager.getSites().then(sites => {
+       this._sites = sites;
+       let sortCol = document.querySelector("treecol[data-isCurrentSortCol=true]");
+       this._sortSites(this._sites, sortCol);
+       this._buildSitesList(this._sites);
+@@ -90,16 +98,17 @@ let gSiteDataSettings = {
+ 
+     let brandShortName = document.getElementById("bundle_brand").getString("brandShortName");
+     let settingsDescription = document.getElementById("settingsDescription");
+     settingsDescription.textContent = this._prefStrBundle.getFormattedString("siteDataSettings2.description", [brandShortName]);
+ 
+     setEventListener("sitesList", "select", this.onSelect);
+     setEventListener("hostCol", "click", this.onClickTreeCol);
+     setEventListener("usageCol", "click", this.onClickTreeCol);
++    setEventListener("lastAccessedCol", "click", this.onClickTreeCol);
+     setEventListener("cookiesCol", "click", this.onClickTreeCol);
+     setEventListener("statusCol", "click", this.onClickTreeCol);
+     setEventListener("cancel", "command", this.close);
+     setEventListener("save", "command", this.saveChanges);
+     setEventListener("searchBox", "command", this.onCommandSearch);
+     setEventListener("removeAll", "command", this.onClickRemoveAll);
+     setEventListener("removeSelected", "command", this.onClickRemoveSelected);
+   },
+@@ -156,16 +165,20 @@ let gSiteDataSettings = {
+ 
+       case "cookiesCol":
+         sortFunc = (a, b) => a.cookies.length - b.cookies.length;
+         break;
+ 
+       case "usageCol":
+         sortFunc = (a, b) => a.usage - b.usage;
+         break;
++
++      case "lastAccessedCol":
++        sortFunc = (a, b) => a.lastAccessed - b.lastAccessed;
++        break;
+     }
+     if (sortDirection === "descending") {
+       sites.sort((a, b) => sortFunc(b, a));
+     } else {
+       sites.sort(sortFunc);
+     }
+ 
+     let cols = this._list.querySelectorAll("treecol");
+diff --git a/browser/components/preferences/siteDataSettings.xul b/browser/components/preferences/siteDataSettings.xul
+--- a/browser/components/preferences/siteDataSettings.xul
++++ b/browser/components/preferences/siteDataSettings.xul
+@@ -37,16 +37,17 @@
+ 
+     <richlistbox id="sitesList" orient="vertical" flex="1">
+       <listheader>
+         <treecol flex="4" width="50" label="&hostCol.label;" id="hostCol"/>
+         <treecol flex="2" width="50" label="&statusCol.label;" id="statusCol"/>
+         <treecol flex="1" width="50" label="&cookiesCol.label;" id="cookiesCol"/>
+         <!-- Sorted by usage so the user can quickly see which sites use the most data. -->
+         <treecol flex="1" width="50" label="&usageCol.label;" id="usageCol" data-isCurrentSortCol="true"/>
++        <treecol flex="2" width="50" label="&lastAccessedCol.label;" id="lastAccessedCol" />
+       </listheader>
+     </richlistbox>
+   </vbox>
+ 
+   <hbox align="start">
+     <button id="removeSelected" label="&removeSelected.label;" accesskey="&removeSelected.accesskey;"/>
+     <button id="removeAll"/>
+   </hbox>
+diff --git a/browser/locales/en-US/chrome/browser/preferences/siteDataSettings.dtd b/browser/locales/en-US/chrome/browser/preferences/siteDataSettings.dtd
+--- a/browser/locales/en-US/chrome/browser/preferences/siteDataSettings.dtd
++++ b/browser/locales/en-US/chrome/browser/preferences/siteDataSettings.dtd
+@@ -2,16 +2,17 @@
+    - License, v. 2.0. If a copy of the MPL was not distributed with this
+    - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+ 
+ <!ENTITY     window1.title                 "Settings - Cookies and Site Data">
+ <!ENTITY     hostCol.label                 "Site">
+ <!ENTITY     statusCol.label               "Status">
+ <!ENTITY     cookiesCol.label              "Cookies">
+ <!ENTITY     usageCol.label                "Storage">
++<!ENTITY     lastAccessedCol.label                "Last Used">
+ <!ENTITY     searchTextboxPlaceHolder             "Search websites">
+ <!ENTITY     searchTextboxPlaceHolder.accesskey   "S">
+ <!ENTITY     removeSelected.label          "Remove Selected">
+ <!ENTITY     removeSelected.accesskey      "r">
+ <!ENTITY     save.label                    "Save Changes">
+ <!ENTITY     save.accesskey                "a">
+ <!ENTITY     cancel.label                  "Cancel">
+ <!ENTITY     cancel.accesskey              "C">

+ 495 - 0
frg/mozilla-release/work-js/1432745-60a1.patch

@@ -0,0 +1,495 @@
+# HG changeset patch
+# User Johann Hofmann <jhofmann@mozilla.com>
+# Date 1519207589 -3600
+# Node ID 57a398ca3a4db155ffbe90b5293e731936cf2b22
+# Parent  278d965eb75f50ca057fef4c11ee1f28df1fa85e
+Bug 1432745 - Merge the "persistent" column in the site data manager into the "usage" column. r=nhnt11
+
+MozReview-Commit-ID: 6a2ZOhC7p3a
+
+diff --git a/browser/components/preferences/in-content-new/tests/browser_siteData.js b/browser/components/preferences/in-content-new/tests/browser_siteData.js
+--- a/browser/components/preferences/in-content-new/tests/browser_siteData.js
++++ b/browser/components/preferences/in-content-new/tests/browser_siteData.js
+@@ -208,25 +208,25 @@ add_task(async function() {
+     let siteItems = frameDoc.getElementsByTagName("richlistitem");
+     is(siteItems.length, 2, "Should list two sites with cookies");
+     let sitesList = frameDoc.getElementById("sitesList");
+     let site1 = sitesList.querySelector(`richlistitem[host="example.com"]`);
+     let site2 = sitesList.querySelector(`richlistitem[host="example.org"]`);
+ 
+     let columns = site1.querySelectorAll(".item-box > label");
+     is(columns[0].value, "example.com", "Should show the correct host.");
+-    is(columns[2].value, "2", "Should show the correct number of cookies.");
+-    is(columns[3].value, "", "Should show no site data.");
+-    is(columns[4].value, args.creationDate1, "Should show the correct date.");
++    is(columns[1].value, "2", "Should show the correct number of cookies.");
++    is(columns[2].value, "", "Should show no site data.");
++    is(columns[3].value, args.creationDate1, "Should show the correct date.");
+ 
+     columns = site2.querySelectorAll(".item-box > label");
+     is(columns[0].value, "example.org", "Should show the correct host.");
+-    is(columns[2].value, "1", "Should show the correct number of cookies.");
+-    is(columns[3].value, "", "Should show no site data.");
+-    is(columns[4].value, args.creationDate2, "Should show the correct date.");
++    is(columns[1].value, "1", "Should show the correct number of cookies.");
++    is(columns[2].value, "", "Should show no site data.");
++    is(columns[3].value, args.creationDate2, "Should show the correct date.");
+ 
+     let removeBtn = frameDoc.getElementById("removeSelected");
+     let saveBtn = frameDoc.getElementById("save");
+     site2.click();
+     removeBtn.doCommand();
+     saveBtn.doCommand();
+   });
+   await removeDialogOpenPromise;
+@@ -242,19 +242,19 @@ add_task(async function() {
+ 
+     let siteItems = frameDoc.getElementsByTagName("richlistitem");
+     is(siteItems.length, 1, "Should list one site with cookies");
+     let sitesList = frameDoc.getElementById("sitesList");
+     let site1 = sitesList.querySelector(`richlistitem[host="example.com"]`);
+ 
+     let columns = site1.querySelectorAll(".item-box > label");
+     is(columns[0].value, "example.com", "Should show the correct host.");
+-    is(columns[2].value, "2", "Should show the correct number of cookies.");
+-    is(columns[3].value, "", "Should show no site data.");
+-    is(columns[4].value, args.creationDate1, "Should show the correct date.");
++    is(columns[1].value, "2", "Should show the correct number of cookies.");
++    is(columns[2].value, "", "Should show no site data.");
++    is(columns[3].value, args.creationDate1, "Should show the correct date.");
+ 
+     let removeBtn = frameDoc.getElementById("removeSelected");
+     let saveBtn = frameDoc.getElementById("save");
+     site1.click();
+     removeBtn.doCommand();
+     saveBtn.doCommand();
+   });
+   await acceptRemovePromise;
+diff --git a/browser/components/preferences/in-content-new/tests/browser_siteData3.js b/browser/components/preferences/in-content-new/tests/browser_siteData3.js
+--- a/browser/components/preferences/in-content-new/tests/browser_siteData3.js
++++ b/browser/components/preferences/in-content-new/tests/browser_siteData3.js
+@@ -88,25 +88,22 @@ add_task(async function() {
+   let siteItems = frameDoc.getElementsByTagName("richlistitem");
+   is(siteItems.length, 1, "Should group sites across scheme, port and origin attributes");
+ 
+   let columns = siteItems[0].querySelectorAll(".item-box > label");
+ 
+   let expected = "account.xyz.com";
+   is(columns[0].value, expected, "Should group and list sites by host");
+ 
+-  let prefStrBundle = frameDoc.getElementById("bundlePreferences");
+-  expected = prefStrBundle.getString("persistent");
+-  is(columns[1].value, expected, "Should mark persisted status across scheme, port and origin attributes");
++  is(columns[1].value, "5", "Should group cookies across scheme, port and origin attributes");
+ 
+-  is(columns[2].value, "5", "Should group cookies across scheme, port and origin attributes");
+-
+-  expected = prefStrBundle.getFormattedString("siteUsage",
++  let prefStrBundle = frameDoc.getElementById("bundlePreferences");
++  expected = prefStrBundle.getFormattedString("siteUsagePersistent",
+     DownloadUtils.convertByteUnits(quotaUsage * mockSiteDataManager.fakeSites.length));
+-  is(columns[3].value, expected, "Should sum up usages across scheme, port and origin attributes");
++  is(columns[2].value, expected, "Should sum up usages across scheme, port, origin attributes and persistent status");
+ 
+   await mockSiteDataManager.unregister();
+   await BrowserTestUtils.removeTab(gBrowser.selectedTab);
+ });
+ 
+ // Test sorting
+ add_task(async function() {
+   await SpecialPowers.pushPrefEnv({set: [["browser.storageManager.enabled", true]]});
+@@ -136,17 +133,16 @@ add_task(async function() {
+   await updatePromise;
+   await openSiteDataSettingsDialog();
+ 
+   let dialog = content.gSubDialog._topDialog;
+   let dialogFrame = dialog._frame;
+   let frameDoc = dialogFrame.contentDocument;
+   let hostCol = frameDoc.getElementById("hostCol");
+   let usageCol = frameDoc.getElementById("usageCol");
+-  let statusCol = frameDoc.getElementById("statusCol");
+   let cookiesCol = frameDoc.getElementById("cookiesCol");
+   let sitesList = frameDoc.getElementById("sitesList");
+ 
+   // Test default sorting
+   assertSortByUsage("descending");
+ 
+   // Test sorting on the usage column
+   usageCol.click();
+@@ -155,28 +151,22 @@ add_task(async function() {
+   assertSortByUsage("descending");
+ 
+   // Test sorting on the host column
+   hostCol.click();
+   assertSortByBaseDomain("ascending");
+   hostCol.click();
+   assertSortByBaseDomain("descending")
+ 
+-  // Test sorting on the permission status column
++  // Test sorting on the cookies column
+   cookiesCol.click();
+   assertSortByCookies("ascending");
+   cookiesCol.click();
+   assertSortByCookies("descending");
+ 
+-  // Test sorting on the cookies column
+-  statusCol.click();
+-  assertSortByStatus("ascending");
+-  statusCol.click();
+-  assertSortByStatus("descending");
+-
+   await mockSiteDataManager.unregister();
+   await BrowserTestUtils.removeTab(gBrowser.selectedTab);
+ 
+   function assertSortByBaseDomain(order) {
+     let siteItems = sitesList.getElementsByTagName("richlistitem");
+     for (let i = 0; i < siteItems.length - 1; ++i) {
+       let aHost = siteItems[i].getAttribute("host");
+       let bHost = siteItems[i + 1].getAttribute("host");
+@@ -186,37 +176,16 @@ add_task(async function() {
+       if (order == "ascending") {
+         Assert.lessOrEqual(result, 0, "Should sort sites in the ascending order by host");
+       } else {
+         Assert.greaterOrEqual(result, 0, "Should sort sites in the descending order by host");
+       }
+     }
+   }
+ 
+-  function assertSortByStatus(order) {
+-    let siteItems = sitesList.getElementsByTagName("richlistitem");
+-    for (let i = 0; i < siteItems.length - 1; ++i) {
+-      let aHost = siteItems[i].getAttribute("host");
+-      let bHost = siteItems[i + 1].getAttribute("host");
+-      let a = findSiteByHost(aHost);
+-      let b = findSiteByHost(bHost);
+-      let result = 0;
+-      if (a.persisted && !b.persisted) {
+-        result = 1;
+-      } else if (!a.persisted && b.persisted) {
+-        result = -1;
+-      }
+-      if (order == "ascending") {
+-        Assert.lessOrEqual(result, 0, "Should sort sites in the ascending order by permission status");
+-      } else {
+-        Assert.greaterOrEqual(result, 0, "Should sort sites in the descending order by permission status");
+-      }
+-    }
+-  }
+-
+   function assertSortByUsage(order) {
+     let siteItems = sitesList.getElementsByTagName("richlistitem");
+     for (let i = 0; i < siteItems.length - 1; ++i) {
+       let aHost = siteItems[i].getAttribute("host");
+       let bHost = siteItems[i + 1].getAttribute("host");
+       let a = findSiteByHost(aHost);
+       let b = findSiteByHost(bHost);
+       let result = a.usage - b.usage;
+diff --git a/browser/components/preferences/in-content/tests/browser_siteData.js b/browser/components/preferences/in-content/tests/browser_siteData.js
+--- a/browser/components/preferences/in-content/tests/browser_siteData.js
++++ b/browser/components/preferences/in-content/tests/browser_siteData.js
+@@ -206,25 +206,25 @@ add_task(async function() {
+     let siteItems = frameDoc.getElementsByTagName("richlistitem");
+     is(siteItems.length, 2, "Should list two sites with cookies");
+     let sitesList = frameDoc.getElementById("sitesList");
+     let site1 = sitesList.querySelector(`richlistitem[host="example.com"]`);
+     let site2 = sitesList.querySelector(`richlistitem[host="example.org"]`);
+ 
+     let columns = site1.querySelectorAll(".item-box > label");
+     is(columns[0].value, "example.com", "Should show the correct host.");
+-    is(columns[2].value, "2", "Should show the correct number of cookies.");
+-    is(columns[3].value, "", "Should show no site data.");
+-    is(columns[4].value, args.creationDate1, "Should show the correct date.");
++    is(columns[1].value, "2", "Should show the correct number of cookies.");
++    is(columns[2].value, "", "Should show no site data.");
++    is(columns[3].value, args.creationDate1, "Should show the correct date.");
+ 
+     columns = site2.querySelectorAll(".item-box > label");
+     is(columns[0].value, "example.org", "Should show the correct host.");
+-    is(columns[2].value, "1", "Should show the correct number of cookies.");
+-    is(columns[3].value, "", "Should show no site data.");
+-    is(columns[4].value, args.creationDate2, "Should show the correct date.");
++    is(columns[1].value, "1", "Should show the correct number of cookies.");
++    is(columns[2].value, "", "Should show no site data.");
++    is(columns[3].value, args.creationDate2, "Should show the correct date.");
+ 
+     let removeBtn = frameDoc.getElementById("removeSelected");
+     let saveBtn = frameDoc.getElementById("save");
+     site2.click();
+     removeBtn.doCommand();
+     saveBtn.doCommand();
+   });
+   await removeDialogOpenPromise;
+@@ -240,19 +240,19 @@ add_task(async function() {
+ 
+     let siteItems = frameDoc.getElementsByTagName("richlistitem");
+     is(siteItems.length, 1, "Should list one site with cookies");
+     let sitesList = frameDoc.getElementById("sitesList");
+     let site1 = sitesList.querySelector(`richlistitem[host="example.com"]`);
+ 
+     let columns = site1.querySelectorAll(".item-box > label");
+     is(columns[0].value, "example.com", "Should show the correct host.");
+-    is(columns[2].value, "2", "Should show the correct number of cookies.");
+-    is(columns[3].value, "", "Should show no site data.");
+-    is(columns[4].value, args.creationDate1, "Should show the correct date.");
++    is(columns[1].value, "2", "Should show the correct number of cookies.");
++    is(columns[2].value, "", "Should show no site data.");
++    is(columns[3].value, args.creationDate1, "Should show the correct date.");
+ 
+     let removeBtn = frameDoc.getElementById("removeSelected");
+     let saveBtn = frameDoc.getElementById("save");
+     site1.click();
+     removeBtn.doCommand();
+     saveBtn.doCommand();
+   });
+   await acceptRemovePromise;
+diff --git a/browser/components/preferences/in-content/tests/browser_siteData3.js b/browser/components/preferences/in-content/tests/browser_siteData3.js
+--- a/browser/components/preferences/in-content/tests/browser_siteData3.js
++++ b/browser/components/preferences/in-content/tests/browser_siteData3.js
+@@ -126,17 +126,16 @@ add_task(async function() {
+   await updatePromise;
+   await openSiteDataSettingsDialog();
+ 
+   let dialog = content.gSubDialog._topDialog;
+   let dialogFrame = dialog._frame;
+   let frameDoc = dialogFrame.contentDocument;
+   let hostCol = frameDoc.getElementById("hostCol");
+   let usageCol = frameDoc.getElementById("usageCol");
+-  let statusCol = frameDoc.getElementById("statusCol");
+   let cookiesCol = frameDoc.getElementById("cookiesCol");
+   let sitesList = frameDoc.getElementById("sitesList");
+ 
+   // Test default sorting
+   assertSortByUsage("descending");
+ 
+   // Test sorting on the usage column
+   usageCol.click();
+@@ -145,28 +144,22 @@ add_task(async function() {
+   assertSortByUsage("descending");
+ 
+   // Test sorting on the host column
+   hostCol.click();
+   assertSortByBaseDomain("ascending");
+   hostCol.click();
+   assertSortByBaseDomain("descending");
+ 
+-  // Test sorting on the permission status column
++  // Test sorting on the cookies column
+   cookiesCol.click();
+   assertSortByCookies("ascending");
+   cookiesCol.click();
+   assertSortByCookies("descending");
+ 
+-  // Test sorting on the cookies column
+-  statusCol.click();
+-  assertSortByStatus("ascending");
+-  statusCol.click();
+-  assertSortByStatus("descending");
+-
+   await mockSiteDataManager.unregister();
+   await BrowserTestUtils.removeTab(gBrowser.selectedTab);
+ 
+   function assertSortByBaseDomain(order) {
+     let siteItems = sitesList.getElementsByTagName("richlistitem");
+     for (let i = 0; i < siteItems.length - 1; ++i) {
+       let aHost = siteItems[i].getAttribute("host");
+       let bHost = siteItems[i + 1].getAttribute("host");
+@@ -176,37 +169,16 @@ add_task(async function() {
+       if (order == "ascending") {
+         Assert.lessOrEqual(result, 0, "Should sort sites in the ascending order by host");
+       } else {
+         Assert.greaterOrEqual(result, 0, "Should sort sites in the descending order by host");
+       }
+     }
+   }
+ 
+-  function assertSortByStatus(order) {
+-    let siteItems = sitesList.getElementsByTagName("richlistitem");
+-    for (let i = 0; i < siteItems.length - 1; ++i) {
+-      let aHost = siteItems[i].getAttribute("host");
+-      let bHost = siteItems[i + 1].getAttribute("host");
+-      let a = findSiteByHost(aHost);
+-      let b = findSiteByHost(bHost);
+-      let result = 0;
+-      if (a.persisted && !b.persisted) {
+-        result = 1;
+-      } else if (!a.persisted && b.persisted) {
+-        result = -1;
+-      }
+-      if (order == "ascending") {
+-        Assert.lessOrEqual(result, 0, "Should sort sites in the ascending order by permission status");
+-      } else {
+-        Assert.greaterOrEqual(result, 0, "Should sort sites in the descending order by permission status");
+-      }
+-    }
+-  }
+-
+   function assertSortByUsage(order) {
+     let siteItems = sitesList.getElementsByTagName("richlistitem");
+     for (let i = 0; i < siteItems.length - 1; ++i) {
+       let aHost = siteItems[i].getAttribute("host");
+       let bHost = siteItems[i + 1].getAttribute("host");
+       let a = findSiteByHost(aHost);
+       let b = findSiteByHost(bHost);
+       let result = a.usage - b.usage;
+diff --git a/browser/components/preferences/siteDataSettings.js b/browser/components/preferences/siteDataSettings.js
+--- a/browser/components/preferences/siteDataSettings.js
++++ b/browser/components/preferences/siteDataSettings.js
+@@ -13,17 +13,16 @@ ChromeUtils.defineModuleGetter(this, "Do
+ "use strict";
+ 
+ let gSiteDataSettings = {
+ 
+   // Array of metadata of sites. Each array element is object holding:
+   // - uri: uri of site; instance of nsIURI
+   // - baseDomain: base domain of the site
+   // - cookies: array of cookies of that site
+-  // - status: persistent-storage permission status
+   // - usage: disk usage which site uses
+   // - userAction: "remove" or "update-permission"; the action user wants to take.
+   _sites: null,
+ 
+   _list: null,
+   _searchBox: null,
+   _prefStrBundle: null,
+ 
+@@ -45,31 +44,27 @@ let gSiteDataSettings = {
+       }
+       box.appendChild(label);
+       container.appendChild(box);
+     }
+ 
+     // Add "Host" column.
+     addColumnItem(site.host, "4");
+ 
+-    // Add "Status" column
+-    addColumnItem(site.persisted ?
+-      this._prefStrBundle.getString("persistent") : null, "2");
+-
+     // Add "Cookies" column.
+     addColumnItem(site.cookies.length, "1");
+ 
+     // Add "Storage" column
+-    if (site.usage > 0) {
++    if (site.usage > 0 || site.persisted) {
+       let size = DownloadUtils.convertByteUnits(site.usage);
+-      let str = this._prefStrBundle.getFormattedString("siteUsage", size);
+-      addColumnItem(str, "1");
++      let strName = site.persisted ? "siteUsagePersistent" : "siteUsage";
++      addColumnItem(this._prefStrBundle.getFormattedString(strName, size), "2");
+     } else {
+       // Pass null to avoid showing "0KB" when there is no site data stored.
+-      addColumnItem(null, "1");
++      addColumnItem(null, "2");
+     }
+ 
+     // Add "Last Used" column.
+     addColumnItem(site.lastAccessed > 0 ?
+       this._formatter.format(site.lastAccessed) : null, "2");
+ 
+     item.appendChild(container);
+     return item;
+@@ -100,17 +95,16 @@ let gSiteDataSettings = {
+     let settingsDescription = document.getElementById("settingsDescription");
+     settingsDescription.textContent = this._prefStrBundle.getFormattedString("siteDataSettings2.description", [brandShortName]);
+ 
+     setEventListener("sitesList", "select", this.onSelect);
+     setEventListener("hostCol", "click", this.onClickTreeCol);
+     setEventListener("usageCol", "click", this.onClickTreeCol);
+     setEventListener("lastAccessedCol", "click", this.onClickTreeCol);
+     setEventListener("cookiesCol", "click", this.onClickTreeCol);
+-    setEventListener("statusCol", "click", this.onClickTreeCol);
+     setEventListener("cancel", "command", this.close);
+     setEventListener("save", "command", this.saveChanges);
+     setEventListener("searchBox", "command", this.onCommandSearch);
+     setEventListener("removeAll", "command", this.onClickRemoveAll);
+     setEventListener("removeSelected", "command", this.onClickRemoveSelected);
+   },
+ 
+   _updateButtonsState() {
+@@ -147,27 +141,16 @@ let gSiteDataSettings = {
+       case "hostCol":
+         sortFunc = (a, b) => {
+           let aHost = a.baseDomain.toLowerCase();
+           let bHost = b.baseDomain.toLowerCase();
+           return aHost.localeCompare(bHost);
+         };
+         break;
+ 
+-      case "statusCol":
+-        sortFunc = (a, b) => {
+-          if (a.persisted && !b.persisted) {
+-            return 1;
+-          } else if (!a.persisted && b.persisted) {
+-            return -1;
+-          }
+-          return 0;
+-        };
+-        break;
+-
+       case "cookiesCol":
+         sortFunc = (a, b) => a.cookies.length - b.cookies.length;
+         break;
+ 
+       case "usageCol":
+         sortFunc = (a, b) => a.usage - b.usage;
+         break;
+ 
+diff --git a/browser/components/preferences/siteDataSettings.xul b/browser/components/preferences/siteDataSettings.xul
+--- a/browser/components/preferences/siteDataSettings.xul
++++ b/browser/components/preferences/siteDataSettings.xul
+@@ -33,20 +33,19 @@
+       <textbox id="searchBox" type="search" flex="1"
+         placeholder="&searchTextboxPlaceHolder;" accesskey="&searchTextboxPlaceHolder.accesskey;"/>
+     </hbox>
+     <separator class="thin"/>
+ 
+     <richlistbox id="sitesList" orient="vertical" flex="1">
+       <listheader>
+         <treecol flex="4" width="50" label="&hostCol.label;" id="hostCol"/>
+-        <treecol flex="2" width="50" label="&statusCol.label;" id="statusCol"/>
+         <treecol flex="1" width="50" label="&cookiesCol.label;" id="cookiesCol"/>
+         <!-- Sorted by usage so the user can quickly see which sites use the most data. -->
+-        <treecol flex="1" width="50" label="&usageCol.label;" id="usageCol" data-isCurrentSortCol="true"/>
++        <treecol flex="2" width="50" label="&usageCol.label;" id="usageCol" data-isCurrentSortCol="true"/>
+         <treecol flex="2" width="50" label="&lastAccessedCol.label;" id="lastAccessedCol" />
+       </listheader>
+     </richlistbox>
+   </vbox>
+ 
+   <hbox align="start">
+     <button id="removeSelected" label="&removeSelected.label;" accesskey="&removeSelected.accesskey;"/>
+     <button id="removeAll"/>
+diff --git a/browser/locales/en-US/chrome/browser/preferences/preferences.properties b/browser/locales/en-US/chrome/browser/preferences/preferences.properties
+--- a/browser/locales/en-US/chrome/browser/preferences/preferences.properties
++++ b/browser/locales/en-US/chrome/browser/preferences/preferences.properties
+@@ -150,24 +150,26 @@ removeAllShownCookies.accesskey=A
+ # you can use #1 in your localization as a placeholder for the number.
+ # For example this is the English string with numbers:
+ # removeSelectedCookied=Remove #1 Selected;Remove #1 Selected
+ removeSelectedCookies.label=Remove Selected;Remove Selected
+ removeSelectedCookies.accesskey=R
+ 
+ defaultUserContextLabel=None
+ 
+-#LOCALIZATION NOTE: The next string is for the total usage of site data.
+-#   e.g., "The total usage is currently using 200 MB"
++# LOCALIZATION NOTE (totalSiteDataSize1, siteUsage, siteUsagePersistent):
++#   This is the total usage of site data, where we insert storage size and unit.
++#   e.g., "The total usage is currently 200 MB"
+ #   %1$S = size
+ #   %2$S = unit (MB, KB, etc.)
+ totalSiteDataSize1=Your stored site data and cache are currently using %1$S %2$S of disk space
++siteUsage=%1$S %2$S
++siteUsagePersistent=%1$S %2$S (Persistent)
+ loadingSiteDataSize1=Calculating site data and cache size…
+-persistent=Persistent
+-siteUsage=%1$S %2$S
++
+ acceptRemove=Remove
+ # LOCALIZATION NOTE (siteDataSettings2.description): %S = brandShortName
+ siteDataSettings2.description=The following websites store site data on your computer. %S keeps data from websites with persistent storage until you delete it, and deletes data from websites with non-persistent storage as space is needed.
+ # LOCALIZATION NOTE (removeAllSiteData, removeAllSiteDataShown):
+ # removeAllSiteData and removeAllSiteDataShown are both used on the same one button,
+ # never displayed together and can share the same accesskey.
+ # When only partial sites are shown as a result of keyword search,
+ # removeAllShown is displayed as button label.

+ 175 - 0
frg/mozilla-release/work-js/1432759-60a1.patch

@@ -0,0 +1,175 @@
+# HG changeset patch
+# User Michael Kohler <me@michaelkohler.info>
+# Date 1517859568 -3600
+# Node ID f37162b6183944f2e38ab82651377cd83b038288
+# Parent  03842b5710673b081036d1081f0c2cf0cb742173
+Bug 1432759 - 'Remove selected' should be greyed out if no item is selected in the site data manager r=johannh
+
+MozReview-Commit-ID: 5MejSmaVJeA
+
+diff --git a/browser/components/preferences/in-content-new/tests/browser_siteData2.js b/browser/components/preferences/in-content-new/tests/browser_siteData2.js
+--- a/browser/components/preferences/in-content-new/tests/browser_siteData2.js
++++ b/browser/components/preferences/in-content-new/tests/browser_siteData2.js
+@@ -236,17 +236,19 @@ add_task(async function() {
+   function removeSelectedSite(hosts) {
+     frameDoc = win.gSubDialog._topDialog._frame.contentDocument;
+     let removeBtn = frameDoc.getElementById("removeSelected");
+     let sitesList = frameDoc.getElementById("sitesList");
+     hosts.forEach(host => {
+       let site = sitesList.querySelector(`richlistitem[host="${host}"]`);
+       if (site) {
+         site.click();
++        is(removeBtn.disabled, false, "Should enable the removeSelected button");
+         removeBtn.doCommand();
++        is(removeBtn.disabled, true, "Should disable the removeSelected button");
+       } else {
+         ok(false, `Should not select and remove inexistent site of ${host}`);
+       }
+     });
+   }
+ });
+ 
+ // Test searching and then removing only visible sites
+diff --git a/browser/components/preferences/in-content-new/tests/head.js b/browser/components/preferences/in-content-new/tests/head.js
+--- a/browser/components/preferences/in-content-new/tests/head.js
++++ b/browser/components/preferences/in-content-new/tests/head.js
+@@ -214,17 +214,17 @@ function assertSitesListed(doc, hosts) {
+   let removeAllBtn = frameDoc.getElementById("removeAll");
+   let sitesList = frameDoc.getElementById("sitesList");
+   let totalSitesNumber = sitesList.getElementsByTagName("richlistitem").length;
+   is(totalSitesNumber, hosts.length, "Should list the right sites number");
+   hosts.forEach(host => {
+     let site = sitesList.querySelector(`richlistitem[host="${host}"]`);
+     ok(site, `Should list the site of ${host}`);
+   });
+-  is(removeBtn.disabled, false, "Should enable the removeSelected button");
++  is(removeBtn.disabled, true, "Should disable the removeSelected button");
+   is(removeAllBtn.disabled, false, "Should enable the removeAllBtn button");
+ }
+ 
+ async function evaluateSearchResults(keyword, searchReults) {
+   searchReults = Array.isArray(searchReults) ? searchReults : [searchReults];
+   searchReults.push("header-searchResults");
+ 
+   let searchInput = gBrowser.contentDocument.getElementById("searchInput");
+diff --git a/browser/components/preferences/in-content/tests/browser_siteData2.js b/browser/components/preferences/in-content/tests/browser_siteData2.js
+--- a/browser/components/preferences/in-content/tests/browser_siteData2.js
++++ b/browser/components/preferences/in-content/tests/browser_siteData2.js
+@@ -220,17 +220,19 @@ add_task(async function() {
+   function removeSelectedSite(hosts) {
+     frameDoc = win.gSubDialog._topDialog._frame.contentDocument;
+     let removeBtn = frameDoc.getElementById("removeSelected");
+     let sitesList = frameDoc.getElementById("sitesList");
+     hosts.forEach(host => {
+       let site = sitesList.querySelector(`richlistitem[host="${host}"]`);
+       if (site) {
+         site.click();
++        is(removeBtn.disabled, false, "Should enable the removeSelected button");
+         removeBtn.doCommand();
++        is(removeBtn.disabled, true, "Should disable the removeSelected button");
+       } else {
+         ok(false, `Should not select and remove inexistent site of ${host}`);
+       }
+     });
+   }
+ });
+ 
+ // Test searching and then removing only visible sites
+diff --git a/browser/components/preferences/in-content/tests/head.js b/browser/components/preferences/in-content/tests/head.js
+--- a/browser/components/preferences/in-content/tests/head.js
++++ b/browser/components/preferences/in-content/tests/head.js
+@@ -251,17 +251,17 @@ function assertSitesListed(doc, hosts) {
+   let removeAllBtn = frameDoc.getElementById("removeAll");
+   let sitesList = frameDoc.getElementById("sitesList");
+   let totalSitesNumber = sitesList.getElementsByTagName("richlistitem").length;
+   is(totalSitesNumber, hosts.length, "Should list the right sites number");
+   hosts.forEach(host => {
+     let site = sitesList.querySelector(`richlistitem[host="${host}"]`);
+     ok(site, `Should list the site of ${host}`);
+   });
+-  is(removeBtn.disabled, false, "Should enable the removeSelected button");
++  is(removeBtn.disabled, true, "Should disable the removeSelected button");
+   is(removeAllBtn.disabled, false, "Should enable the removeAllBtn button");
+ }
+ 
+ function promiseSitesUpdated() {
+   return TestUtils.topicObserved("sitedatamanager:sites-updated", () => true);
+ }
+ 
+ function promiseSettingsDialogClose() {
+diff --git a/browser/components/preferences/siteDataSettings.js b/browser/components/preferences/siteDataSettings.js
+--- a/browser/components/preferences/siteDataSettings.js
++++ b/browser/components/preferences/siteDataSettings.js
+@@ -42,32 +42,33 @@ let gSiteDataSettings = {
+       this._buildSitesList(this._sites);
+       Services.obs.notifyObservers(null, "sitedata-settings-init");
+     });
+ 
+     let brandShortName = document.getElementById("bundle_brand").getString("brandShortName");
+     let settingsDescription = document.getElementById("settingsDescription");
+     settingsDescription.textContent = this._prefStrBundle.getFormattedString("siteDataSettings2.description", [brandShortName]);
+ 
++    setEventListener("sitesList", "select", this.onSelect);
+     setEventListener("hostCol", "click", this.onClickTreeCol);
+     setEventListener("usageCol", "click", this.onClickTreeCol);
+     setEventListener("statusCol", "click", this.onClickTreeCol);
+     setEventListener("cancel", "command", this.close);
+     setEventListener("save", "command", this.saveChanges);
+     setEventListener("searchBox", "command", this.onCommandSearch);
+     setEventListener("removeAll", "command", this.onClickRemoveAll);
+     setEventListener("removeSelected", "command", this.onClickRemoveSelected);
+   },
+ 
+   _updateButtonsState() {
+     let items = this._list.getElementsByTagName("richlistitem");
+     let removeSelectedBtn = document.getElementById("removeSelected");
+     let removeAllBtn = document.getElementById("removeAll");
+-    removeSelectedBtn.disabled = items.length == 0;
+-    removeAllBtn.disabled = removeSelectedBtn.disabled;
++    removeSelectedBtn.disabled = this._list.selectedItems.length == 0;
++    removeAllBtn.disabled = items.length == 0;
+ 
+     let removeAllBtnLabelStringID = "removeAllSiteData.label";
+     let removeAllBtnAccesskeyStringID = "removeAllSiteData.accesskey";
+     if (this._searchBox.value) {
+       removeAllBtnLabelStringID = "removeAllSiteDataShown.label";
+       removeAllBtnAccesskeyStringID = "removeAllSiteDataShown.accesskey";
+     }
+     removeAllBtn.setAttribute("label", this._prefStrBundle.getString(removeAllBtnLabelStringID));
+@@ -265,30 +266,36 @@ let gSiteDataSettings = {
+ 
+   onClickTreeCol(e) {
+     this._sortSites(this._sites, e.target);
+     this._buildSitesList(this._sites);
+   },
+ 
+   onCommandSearch() {
+     this._buildSitesList(this._sites);
++    this._list.clearSelection();
+   },
+ 
+   onClickRemoveSelected() {
+     let selected = this._list.selectedItem;
+     if (selected) {
+       this._removeSiteItems([selected]);
+     }
++    this._list.clearSelection();
+   },
+ 
+   onClickRemoveAll() {
+     let siteItems = this._list.getElementsByTagName("richlistitem");
+     if (siteItems.length > 0) {
+       this._removeSiteItems(siteItems);
+     }
+   },
+ 
+   onKeyPress(e) {
+     if (e.keyCode == KeyEvent.DOM_VK_ESCAPE) {
+       this.close();
+     }
++  },
++
++  onSelect() {
++    this._updateButtonsState();
+   }
+ };

+ 34 - 0
frg/mozilla-release/work-js/1433492-1-60a1.patch

@@ -0,0 +1,34 @@
+# HG changeset patch
+# User Mark Banner <standard8@mozilla.com>
+# Date 1516983784 0
+# Node ID ee3ef79bf57b4789b71c6ee1ee6ed11c5f23f4e7
+# Parent  ecc00d184fd20aa4c36ffce07b533cf56adc2e13
+Bug 1433492 - Fix a missed callback to .then conversion causing RSS feeds to not show their read/unread status properly after a restart. r=mak
+
+MozReview-Commit-ID: 3iG8JbyGJul
+
+diff --git a/toolkit/components/places/nsLivemarkService.js b/toolkit/components/places/nsLivemarkService.js
+--- a/toolkit/components/places/nsLivemarkService.js
++++ b/toolkit/components/places/nsLivemarkService.js
+@@ -556,19 +556,19 @@ Livemark.prototype = {
+ 
+     // Discard the previous cached nodes, new ones should be generated.
+     for (let container of this._resultObservers.keys()) {
+       this._nodes.delete(container);
+     }
+ 
+     // Update visited status for each entry.
+     for (let child of this._children) {
+-      history.hasVisits(child.uri, isVisited => {
++      history.hasVisits(child.uri).then(isVisited => {
+         this.updateURIVisitedStatus(child.uri, isVisited);
+-      });
++      }).catch(Cu.reportError);
+     }
+ 
+     return this._children;
+   },
+ 
+   _isURIVisited(aURI) {
+     return this.children.some(child => child.uri.equals(aURI) && child.visited);
+   },

+ 212 - 0
frg/mozilla-release/work-js/1433492-2-60a1.patch

@@ -0,0 +1,212 @@
+# HG changeset patch
+# User Mark Banner <standard8@mozilla.com>
+# Date 1516984825 0
+# Node ID f1f8869f631a70d01239df3530fa42a0e95c2c5f
+# Parent  a93c6b2933d812ef282a009086f9587ab641df23
+Bug 1433492 - Remove remaining instances of PlacesUtils.asyncHistory.isURIVisited being called directly. r=mak
+
+MozReview-Commit-ID: B1sqEtJcq4L
+
+diff --git a/browser/base/content/test/general/head.js b/browser/base/content/test/general/head.js
+--- a/browser/base/content/test/general/head.js
++++ b/browser/base/content/test/general/head.js
+@@ -272,32 +272,22 @@ function promiseTabLoaded(aTab) {
+ /**
+  * Ensures that the specified URIs are either cleared or not.
+  *
+  * @param aURIs
+  *        Array of page URIs
+  * @param aShouldBeCleared
+  *        True if each visit to the URI should be cleared, false otherwise
+  */
+-function promiseHistoryClearedState(aURIs, aShouldBeCleared) {
+-  return new Promise(resolve => {
+-    let callbackCount = 0;
+-    let niceStr = aShouldBeCleared ? "no longer" : "still";
+-    function callbackDone() {
+-      if (++callbackCount == aURIs.length)
+-        resolve();
+-    }
+-    aURIs.forEach(function(aURI) {
+-      PlacesUtils.asyncHistory.isURIVisited(aURI, function(uri, isVisited) {
+-        is(isVisited, !aShouldBeCleared,
+-           "history visit " + uri.spec + " should " + niceStr + " exist");
+-        callbackDone();
+-      });
+-    });
+-  });
++async function promiseHistoryClearedState(aURIs, aShouldBeCleared) {
++  for (let uri of aURIs) {
++    let visited = await PlacesUtils.history.hasVisits(uri);
++    Assert.equal(visited, !aShouldBeCleared,
++      `history visit ${uri.spec} should ${aShouldBeCleared ? "no longer" : "still"} exist`);
++  }
+ }
+ 
+ var FullZoomHelper = {
+ 
+   selectTabAndWaitForLocationChange: function selectTabAndWaitForLocationChange(tab) {
+     if (!tab)
+       throw new Error("tab must be given.");
+     if (gBrowser.selectedTab == tab)
+diff --git a/browser/components/places/tests/browser/head.js b/browser/components/places/tests/browser/head.js
+--- a/browser/components/places/tests/browser/head.js
++++ b/browser/components/places/tests/browser/head.js
+@@ -130,28 +130,16 @@ function synthesizeClickOnSelectedTreeCe
+   var rect = tbo.getCoordsForCellItem(rowID, aTree.columns[0], "text");
+   var x = rect.x + rect.width / 2;
+   var y = rect.y + rect.height / 2;
+   // Simulate the click.
+   EventUtils.synthesizeMouse(aTree.body, x, y, aOptions || {},
+                              aTree.ownerGlobal);
+ }
+ 
+-/**
+- * Asynchronously check a url is visited.
+- *
+- * @param aURI The URI.
+- * @return {Promise}
+- * @resolves When the check has been added successfully.
+- * @rejects JavaScript exception.
+- */
+-function promiseIsURIVisited(aURI) {
+-  return PlacesUtils.history.hasVisits(aURI);
+-}
+-
+ function promiseBookmarksNotification(notification, conditionFn) {
+   info(`promiseBookmarksNotification: waiting for ${notification}`);
+   return new Promise((resolve) => {
+     let proxifiedObserver = new Proxy({}, {
+       get: (target, name) => {
+         if (name == "QueryInterface")
+           return XPCOMUtils.generateQI([ Ci.nsINavBookmarkObserver ]);
+         info(`promiseBookmarksNotification: got ${name} notification`);
+diff --git a/toolkit/components/places/tests/history/test_removeVisits.js b/toolkit/components/places/tests/history/test_removeVisits.js
+--- a/toolkit/components/places/tests/history/test_removeVisits.js
++++ b/toolkit/components/places/tests/history/test_removeVisits.js
+@@ -39,18 +39,18 @@ add_task(async function remove_visits_ou
+   root.containerOpen = true;
+   Assert.equal(root.childCount, 10);
+   for (let i = 0; i < root.childCount; i++) {
+     let visitTime = root.getChild(i).time;
+     Assert.equal(visitTime, DB_NOW - 100000 - (i * 1000));
+   }
+   root.containerOpen = false;
+ 
+-  info("PlacesUtils.history.hasVisits should return true.");
+-  Assert.ok(await PlacesUtils.history.hasVisits(TEST_URI));
++  Assert.ok(await PlacesUtils.history.hasVisits(TEST_URI),
++    "visit should exist");
+ 
+   await PlacesTestUtils.promiseAsyncUpdates();
+   info("Frecency should be positive.");
+   Assert.ok(frecencyForUrl(TEST_URI) > 0);
+ 
+   await cleanup();
+ });
+ 
+@@ -90,18 +90,18 @@ add_task(async function remove_visits_ou
+   root.containerOpen = true;
+   Assert.equal(root.childCount, 10);
+   for (let i = 0; i < root.childCount; i++) {
+     let visitTime = root.getChild(i).time;
+     Assert.equal(visitTime, DB_NOW - 100000 - (i * 1000));
+   }
+   root.containerOpen = false;
+ 
+-  info("asyncHistory.isURIVisited should return true.");
+-  Assert.ok(await PlacesUtils.history.hasVisits(TEST_URI));
++  Assert.ok(await PlacesUtils.history.hasVisits(TEST_URI),
++    "visit should exist");
+   await PlacesTestUtils.promiseAsyncUpdates();
+ 
+   info("Frecency should be positive.");
+   Assert.ok(frecencyForUrl(TEST_URI) > 0);
+ 
+   await cleanup();
+ });
+ 
+@@ -135,18 +135,18 @@ add_task(async function remove_visits_un
+   root.containerOpen = true;
+   Assert.equal(root.childCount, 5);
+   for (let i = 0; i < root.childCount; i++) {
+     let visitTime = root.getChild(i).time;
+     Assert.equal(visitTime, DB_NOW - (i * 1000) - 5000);
+   }
+   root.containerOpen = false;
+ 
+-  info("asyncHistory.isURIVisited should return true.");
+-  Assert.ok(await PlacesUtils.history.hasVisits(TEST_URI));
++  Assert.ok(await PlacesUtils.history.hasVisits(TEST_URI),
++    "visit should exist");
+   await PlacesTestUtils.promiseAsyncUpdates();
+ 
+   info("Frecency should be positive.");
+   Assert.ok(frecencyForUrl(TEST_URI) > 0);
+ 
+   await cleanup();
+ });
+ 
+@@ -186,18 +186,18 @@ add_task(async function remove_visits_bo
+   root.containerOpen = true;
+   Assert.equal(root.childCount, 5);
+   for (let i = 0; i < root.childCount; i++) {
+     let visitTime = root.getChild(i).time;
+     Assert.equal(visitTime, DB_NOW - (i * 1000) - 5000);
+   }
+   root.containerOpen = false;
+ 
+-  info("asyncHistory.isURIVisited should return true.");
+-  Assert.ok(await PlacesUtils.history.hasVisits(TEST_URI));
++  Assert.ok(await PlacesUtils.history.hasVisits(TEST_URI),
++    "visit should exist");
+   await PlacesTestUtils.promiseAsyncUpdates();
+ 
+   info("Frecency should be positive.");
+   Assert.ok(frecencyForUrl(TEST_URI) > 0);
+ 
+   await cleanup();
+ });
+ 
+@@ -227,18 +227,18 @@ add_task(async function remove_all_visit
+   let opts = PlacesUtils.history.getNewQueryOptions();
+   opts.resultType = opts.RESULTS_AS_VISIT;
+   opts.sortingMode = opts.SORT_BY_DATE_DESCENDING;
+   let root = PlacesUtils.history.executeQuery(query, opts).root;
+   root.containerOpen = true;
+   Assert.equal(root.childCount, 0);
+   root.containerOpen = false;
+ 
+-  info("asyncHistory.isURIVisited should return false.");
+-  Assert.equal(false, await PlacesUtils.history.hasVisits(TEST_URI));
++  Assert.equal(false, await PlacesUtils.history.hasVisits(TEST_URI),
++    "visit should not exist");
+ 
+   await cleanup();
+ });
+ 
+ add_task(async function remove_all_visits_bookmarked_uri() {
+   info("*** TEST: Remove all visits from a bookmarked URI");
+ 
+   info("Add some visits for the URI.");
+@@ -271,18 +271,18 @@ add_task(async function remove_all_visit
+   let opts = PlacesUtils.history.getNewQueryOptions();
+   opts.resultType = opts.RESULTS_AS_VISIT;
+   opts.sortingMode = opts.SORT_BY_DATE_DESCENDING;
+   let root = PlacesUtils.history.executeQuery(query, opts).root;
+   root.containerOpen = true;
+   Assert.equal(root.childCount, 0);
+   root.containerOpen = false;
+ 
+-  info("asyncHistory.isURIVisited should return false.");
+-  Assert.equal(false, await PlacesUtils.history.hasVisits(TEST_URI));
++  Assert.equal(false, await PlacesUtils.history.hasVisits(TEST_URI),
++    "visit should not exist");
+ 
+   info("URI should be bookmarked");
+   Assert.ok(await PlacesUtils.bookmarks.fetch({url: TEST_URI}));
+   await PlacesTestUtils.promiseAsyncUpdates();
+ 
+   info("Frecency should be smaller.")
+   Assert.ok(frecencyForUrl(TEST_URI) < initialFrecency);
+ 

+ 603 - 0
frg/mozilla-release/work-js/1434414-60a1.patch

@@ -0,0 +1,603 @@
+# HG changeset patch
+# User Marco Bonardo <mbonardo@mozilla.com>
+# Date 1518651778 -3600
+# Node ID 56ebb683fa91a3e2a7253a28f093d1f24592568e
+# Parent  3031fe13ca9454edd1c6f7c05b674ad0502db660
+Bug 1434414 - Locking the 'sanitize on shutdown' pref causes sanitization to happen at every startup. r=johannh
+
+MozReview-Commit-ID: 6PvRFmaZsBC
+
+diff --git a/browser/base/content/sanitize.xul b/browser/base/content/sanitize.xul
+--- a/browser/base/content/sanitize.xul
++++ b/browser/base/content/sanitize.xul
+@@ -26,20 +26,16 @@
+             style="width: &sanitizeDialog2.width;;"
+             ondialogaccept="return gSanitizePromptDialog.sanitize();">
+ 
+   <prefpane id="SanitizeDialogPane" onpaneload="gSanitizePromptDialog.init();">
+     <stringbundle id="bundleBrowser"
+                   src="chrome://browser/locale/browser.properties"/>
+ 
+     <script type="application/javascript"
+-            src="chrome://browser/content/sanitize.js"/>
+-
+-
+-    <script type="application/javascript"
+             src="chrome://browser/content/sanitizeDialog.js"/>
+ 
+     <preferences id="sanitizePreferences">
+       <preference id="privacy.cpd.history"               name="privacy.cpd.history"               type="bool"/>
+       <preference id="privacy.cpd.formdata"              name="privacy.cpd.formdata"              type="bool"/>
+       <preference id="privacy.cpd.downloads"             name="privacy.cpd.downloads"             type="bool" disabled="true"/>
+       <preference id="privacy.cpd.cookies"               name="privacy.cpd.cookies"               type="bool"/>
+       <preference id="privacy.cpd.cache"                 name="privacy.cpd.cache"                 type="bool"/>
+diff --git a/browser/base/content/sanitizeDialog.js b/browser/base/content/sanitizeDialog.js
+--- a/browser/base/content/sanitizeDialog.js
++++ b/browser/base/content/sanitizeDialog.js
+@@ -88,17 +88,16 @@ var gSanitizePromptDialog = {
+     acceptButton.disabled = true;
+     acceptButton.setAttribute("label",
+                               this.bundleBrowser.getString("sanitizeButtonClearing"));
+     docElt.getButton("cancel").disabled = true;
+ 
+     try {
+       let range = Sanitizer.getClearRange(this.selectedTimespan);
+       let options = {
+-        prefDomain: "privacy.cpd.",
+         ignoreTimespan: !range,
+         range,
+       };
+       Sanitizer.sanitize(null, options)
+         .catch(Cu.reportError)
+         .then(() => window.close())
+         .catch(Cu.reportError);
+       return false;
+diff --git a/browser/components/nsBrowserGlue.js b/browser/components/nsBrowserGlue.js
+--- a/browser/components/nsBrowserGlue.js
++++ b/browser/components/nsBrowserGlue.js
+@@ -454,19 +454,16 @@ BrowserGlue.prototype = {
+           for (let addon of addons) {
+             if (addon.type != "experiment") {
+               this._notifyUnsignedAddonsDisabled();
+               break;
+             }
+           }
+         });
+         break;
+-      case "test-initialize-sanitizer":
+-        Sanitizer.onStartup();
+-        break;
+       case "sync-ui-state:update":
+         this._updateFxaBadges();
+         break;
+       case "handlersvc-store-initialized":
+         // Initialize PdfJs when running in-process and remote. This only
+         // happens once since PdfJs registers global hooks. If the PdfJs
+         // extension is installed the init method below will be overridden
+         // leaving initialization to the extension.
+diff --git a/browser/components/places/tests/unit/test_clearHistory_shutdown.js b/browser/components/places/tests/unit/test_clearHistory_shutdown.js
+--- a/browser/components/places/tests/unit/test_clearHistory_shutdown.js
++++ b/browser/components/places/tests/unit/test_clearHistory_shutdown.js
+@@ -24,16 +24,18 @@ var EXPECTED_NOTIFICATIONS = [
+ ];
+ 
+ const UNEXPECTED_NOTIFICATIONS = [
+   "xpcom-shutdown"
+ ];
+ 
+ const FTP_URL = "ftp://localhost/clearHistoryOnShutdown/";
+ 
++ChromeUtils.import("resource:///modules/Sanitizer.jsm");
++
+ // Send the profile-after-change notification to the form history component to ensure
+ // that it has been initialized.
+ var formHistoryStartup = Cc["@mozilla.org/satchel/form-history-startup;1"].
+                          getService(Ci.nsIObserver);
+ formHistoryStartup.observe(null, "profile-after-change", null);
+ ChromeUtils.defineModuleGetter(this, "FormHistory",
+                                "resource://gre/modules/FormHistory.jsm");
+ 
+@@ -41,30 +43,29 @@ var timeInMicroseconds = Date.now() * 10
+ 
+ add_task(async function test_execute() {
+   info("Initialize browserglue before Places");
+ 
+   // Avoid default bookmarks import.
+   let glue = Cc["@mozilla.org/browser/browserglue;1"].
+              getService(Ci.nsIObserver);
+   glue.observe(null, "initial-migration-will-import-default-bookmarks", null);
+-  glue.observe(null, "test-initialize-sanitizer", null);
+-
++  Sanitizer.onStartup();
+ 
+-  Services.prefs.setBoolPref("privacy.clearOnShutdown.cache", true);
+-  Services.prefs.setBoolPref("privacy.clearOnShutdown.cookies", true);
+-  Services.prefs.setBoolPref("privacy.clearOnShutdown.offlineApps", true);
+-  Services.prefs.setBoolPref("privacy.clearOnShutdown.history", true);
+-  Services.prefs.setBoolPref("privacy.clearOnShutdown.downloads", true);
+-  Services.prefs.setBoolPref("privacy.clearOnShutdown.cookies", true);
+-  Services.prefs.setBoolPref("privacy.clearOnShutdown.formData", true);
+-  Services.prefs.setBoolPref("privacy.clearOnShutdown.sessions", true);
+-  Services.prefs.setBoolPref("privacy.clearOnShutdown.siteSettings", true);
++  Services.prefs.setBoolPref(Sanitizer.PREF_SHUTDOWN_BRANCH + "cache", true);
++  Services.prefs.setBoolPref(Sanitizer.PREF_SHUTDOWN_BRANCH + "cookies", true);
++  Services.prefs.setBoolPref(Sanitizer.PREF_SHUTDOWN_BRANCH + "offlineApps", true);
++  Services.prefs.setBoolPref(Sanitizer.PREF_SHUTDOWN_BRANCH + "history", true);
++  Services.prefs.setBoolPref(Sanitizer.PREF_SHUTDOWN_BRANCH + "downloads", true);
++  Services.prefs.setBoolPref(Sanitizer.PREF_SHUTDOWN_BRANCH + "cookies", true);
++  Services.prefs.setBoolPref(Sanitizer.PREF_SHUTDOWN_BRANCH + "formData", true);
++  Services.prefs.setBoolPref(Sanitizer.PREF_SHUTDOWN_BRANCH + "sessions", true);
++  Services.prefs.setBoolPref(Sanitizer.PREF_SHUTDOWN_BRANCH + "siteSettings", true);
+ 
+-  Services.prefs.setBoolPref("privacy.sanitize.sanitizeOnShutdown", true);
++  Services.prefs.setBoolPref(Sanitizer.PREF_SANITIZE_ON_SHUTDOWN, true);
+ 
+   info("Add visits.");
+   for (let aUrl of URIS) {
+     await PlacesTestUtils.addVisits({
+       uri: uri(aUrl), visitDate: timeInMicroseconds++,
+       transition: PlacesUtils.history.TRANSITION_TYPED
+     });
+   }
+diff --git a/browser/modules/Sanitizer.jsm b/browser/modules/Sanitizer.jsm
+--- a/browser/modules/Sanitizer.jsm
++++ b/browser/modules/Sanitizer.jsm
+@@ -21,43 +21,44 @@ XPCOMUtils.defineLazyModuleGetters(this,
+ 
+ XPCOMUtils.defineLazyServiceGetter(this, "serviceWorkerManager",
+                                    "@mozilla.org/serviceworkers/manager;1",
+                                    "nsIServiceWorkerManager");
+ XPCOMUtils.defineLazyServiceGetter(this, "quotaManagerService",
+                                    "@mozilla.org/dom/quota-manager-service;1",
+                                    "nsIQuotaManagerService");
+ 
++// Used as unique id for pending sanitizations.
++var gPendingSanitizationSerial = 0;
++
+ /**
+  * A number of iterations after which to yield time back
+  * to the system.
+  */
+ const YIELD_PERIOD = 10;
+ 
+ var Sanitizer = {
+   /**
+    * Whether we should sanitize on shutdown.
+    */
+   PREF_SANITIZE_ON_SHUTDOWN: "privacy.sanitize.sanitizeOnShutdown",
+ 
+   /**
+-   * During a sanitization this is set to a json containing the array of items
+-   * being sanitized, then cleared once the sanitization is complete.
+-   * This allows to retry a sanitization on startup in case it was interrupted
+-   * by a crash.
++   * During a sanitization this is set to a JSON containing an array of the
++   * pending sanitizations. This allows to retry sanitizations on startup in
++   * case they dind't run or were interrupted by a crash.
++   * Use addPendingSanitization and removePendingSanitization to manage it.
+    */
+-  PREF_SANITIZE_IN_PROGRESS: "privacy.sanitize.sanitizeInProgress",
++  PREF_PENDING_SANITIZATIONS: "privacy.sanitize.pending",
+ 
+   /**
+-   * Whether the previous shutdown sanitization completed successfully.
+-   * This is used to detect cases where we were supposed to sanitize on shutdown
+-   * but due to a crash we were unable to.  In such cases there may not be any
+-   * sanitization in progress, cause we didn't have a chance to start it yet.
++   * Pref branches to fetch sanitization options from.
+    */
+-  PREF_SANITIZE_DID_SHUTDOWN: "privacy.sanitize.didShutdownSanitize",
++  PREF_CPD_BRANCH: "privacy.cpd.",
++  PREF_SHUTDOWN_BRANCH: "privacy.clearOnShutdown.",
+ 
+   /**
+    * The fallback timestamp used when no argument is given to
+    * Sanitizer.getClearRange.
+    */
+   PREF_TIMESPAN: "privacy.sanitize.timeSpan",
+ 
+   /**
+@@ -68,16 +69,24 @@ var Sanitizer = {
+   TIMESPAN_HOUR:       1,
+   TIMESPAN_2HOURS:     2,
+   TIMESPAN_4HOURS:     3,
+   TIMESPAN_TODAY:      4,
+   TIMESPAN_5MIN:       5,
+   TIMESPAN_24HOURS:    6,
+ 
+   /**
++   * Whether we should sanitize on shutdown.
++   * When this is set, a pending sanitization should also be added and removed
++   * when shutdown sanitization is complete. This allows to retry incomplete
++   * sanitizations on startup.
++   */
++  shouldSanitizeOnShutdown: false,
++
++  /**
+    * Shows a sanitization dialog to the user.
+    *
+    * @param [optional] parentWindow the window to use as
+    *                   parent for the created dialog.
+    */
+   showUI(parentWindow) {
+     let win = AppConstants.platform == "macosx" ?
+       null : // make this an app-modal window on Mac
+@@ -86,59 +95,59 @@ var Sanitizer = {
+                            "chrome://browser/content/sanitize.xul",
+                            "Sanitize",
+                            "chrome,titlebar,dialog,centerscreen,modal",
+                            null);
+   },
+ 
+   /**
+    * Performs startup tasks:
+-   *  - Checks if sanitization was interrupted during last shutdown.
++   *  - Checks if sanitizations were not completed during the last session.
+    *  - Registers sanitize-on-shutdown.
+    */
+   async onStartup() {
+-    // Check if we were interrupted during the last shutdown sanitization.
+-    let shutdownSanitizationWasInterrupted =
+-      Services.prefs.getBoolPref(Sanitizer.PREF_SANITIZE_ON_SHUTDOWN, false) &&
+-      Services.prefs.getPrefType(Sanitizer.PREF_SANITIZE_DID_SHUTDOWN) == Ci.nsIPrefBranch.PREF_INVALID;
++    // First, collect pending sanitizations from the last session, before we
++    // add pending sanitizations for this session.
++    let pendingSanitizations = getAndClearPendingSanitizations();
+ 
+-    if (Services.prefs.prefHasUserValue(Sanitizer.PREF_SANITIZE_DID_SHUTDOWN)) {
+-      // Reset the pref, so that if we crash before having a chance to
+-      // sanitize on shutdown, we will do at the next startup.
+-      // Flushing prefs has a cost, so do this only if necessary.
+-      Services.prefs.clearUserPref(Sanitizer.PREF_SANITIZE_DID_SHUTDOWN);
+-      Services.prefs.savePrefFile(null);
++    // Check if we should sanitize on shutdown.
++    this.shouldSanitizeOnShutdown =
++      Services.prefs.getBoolPref(Sanitizer.PREF_SANITIZE_ON_SHUTDOWN, false);
++    Services.prefs.addObserver(Sanitizer.PREF_SANITIZE_ON_SHUTDOWN, this, true);
++    // Add a pending shutdown sanitization, if necessary.
++    if (this.shouldSanitizeOnShutdown) {
++      let itemsToClear = getItemsToClearFromPrefBranch(Sanitizer.PREF_SHUTDOWN_BRANCH);
++      addPendingSanitization("shutdown", itemsToClear, {});
+     }
++    // Shutdown sanitization is always pending, but the user may change the
++    // sanitize on shutdown prefs during the session. Then the pending
++    // sanitization would become stale and must be updated.
++    Services.prefs.addObserver(Sanitizer.PREF_SHUTDOWN_BRANCH, this, true);
+ 
+     // Make sure that we are triggered during shutdown.
+     let shutdownClient = PlacesUtils.history.shutdownClient.jsclient;
+     // We need to pass to sanitize() (through sanitizeOnShutdown) a state object
+     // that tracks the status of the shutdown blocker. This `progress` object
+     // will be updated during sanitization and reported with the crash in case of
+     // a shutdown timeout.
+     // We use the `options` argument to pass the `progress` object to sanitize().
+     let progress = { isShutdown: true };
+     shutdownClient.addBlocker("sanitize.js: Sanitize on shutdown",
+-      () => sanitizeOnShutdown({ progress }),
+-      {
+-        fetchState: () => ({ progress })
+-      }
++      () => sanitizeOnShutdown(progress),
++      {fetchState: () => ({ progress })}
+     );
+ 
+-    // Check if Firefox crashed during a sanitization.
+-    let lastInterruptedSanitization = Services.prefs.getStringPref(Sanitizer.PREF_SANITIZE_IN_PROGRESS, "");
+-    if (lastInterruptedSanitization) {
+-      // If the json is invalid this will just throw and reject the Task.
+-      let {itemsToClear, options} = JSON.parse(lastInterruptedSanitization);
+-      await this.sanitize(itemsToClear, options);
+-    } else if (shutdownSanitizationWasInterrupted) {
+-      // Otherwise, could be we were supposed to sanitize on shutdown but we
+-      // didn't have a chance, due to an earlier crash.
+-      // In such a case, just redo a shutdown sanitize now, during startup.
+-      await sanitizeOnShutdown();
++    // Finally, run the sanitizations that were left pending, because we crashed
++    // before completing them.
++    for (let {itemsToClear, options} of pendingSanitizations) {
++      try {
++        await this.sanitize(itemsToClear, options);
++      } catch (ex) {
++        Cu.reportError("A previously pending sanitization failed: " + itemsToClear + "\n" + ex);
++      }
+     }
+   },
+ 
+   /**
+    * Returns a 2 element array representing the start and end times,
+    * in the uSec-since-epoch format that PRTime likes. If we should
+    * clear everything, this function returns null.
+    *
+@@ -201,23 +210,23 @@ var Sanitizer = {
+    *        Object whose properties are options for this sanitization:
+    *         - ignoreTimespan (default: true): Time span only makes sense in
+    *           certain cases.  Consumers who want to only clear some private
+    *           data can opt in by setting this to false, and can optionally
+    *           specify a specific range.
+    *           If timespan is not ignored, and range is not set, sanitize() will
+    *           use the value of the timespan pref to determine a range.
+    *         - range (default: null)
+-   *         - prefDomain (default: "privacy.cpd."): indicates the preferences
+-   *           branch to collect the list of items to sanitize from.
+    *         - privateStateForNewWindow (default: "non-private"): when clearing
+    *           open windows, defines the private state for the newly opened window.
+    */
+   async sanitize(itemsToClear = null, options = {}) {
+     let progress = options.progress || {};
++    if (!itemsToClear)
++      itemsToClear = getItemsToClearFromPrefBranch(this.PREF_CPD_BRANCH);
+     let promise = sanitizeInternal(this.items, itemsToClear, progress, options);
+ 
+     // Depending on preferences, the sanitizer may perform asynchronous
+     // work before it starts cleaning up the Places database (e.g. closing
+     // windows). We need to make sure that the connection to that database
+     // hasn't been closed by the time we use it.
+     // Though, if this is a sanitize on shutdown, we already have a blocker.
+     if (!progress.isShutdown) {
+@@ -235,16 +244,41 @@ var Sanitizer = {
+ 
+     try {
+       await promise;
+     } finally {
+       Services.obs.notifyObservers(null, "sanitizer-sanitization-complete");
+     }
+   },
+ 
++  observe(subject, topic, data) {
++    if (topic == "nsPref:changed") {
++      if (data.startsWith(this.PREF_SHUTDOWN_BRANCH) &&
++          this.shouldSanitizeOnShutdown) {
++        // Update the pending shutdown sanitization.
++        removePendingSanitization("shutdown");
++        let itemsToClear = getItemsToClearFromPrefBranch(Sanitizer.PREF_SHUTDOWN_BRANCH);
++        addPendingSanitization("shutdown", itemsToClear, {});
++      } else if (data == this.PREF_SANITIZE_ON_SHUTDOWN) {
++        this.shouldSanitizeOnShutdown =
++          Services.prefs.getBoolPref(Sanitizer.PREF_SANITIZE_ON_SHUTDOWN, false);
++        removePendingSanitization("shutdown");
++        if (this.shouldSanitizeOnShutdown) {
++          let itemsToClear = getItemsToClearFromPrefBranch(Sanitizer.PREF_SHUTDOWN_BRANCH);
++          addPendingSanitization("shutdown", itemsToClear, {});
++        }
++      }
++    }
++  },
++
++  QueryInterface: XPCOMUtils.generateQI([
++    Ci.nsiObserver,
++    Ci.nsISupportsWeakReference
++  ]),
++
+   items: {
+     cache: {
+       async clear(range) {
+         let seenException;
+         let refObj = {};
+         TelemetryStopwatch.start("FX_SANITIZE_CACHE", refObj);
+ 
+         try {
+@@ -724,38 +758,29 @@ var Sanitizer = {
+         newWindow.focus();
+         await promiseReady;
+       }
+     },
+   },
+ };
+ 
+ async function sanitizeInternal(items, aItemsToClear, progress, options = {}) {
+-  let { prefDomain = "privacy.cpd.", ignoreTimespan = true, range } = options;
++  let { ignoreTimespan = true, range } = options;
+   let seenError = false;
+-  let itemsToClear;
+-  if (Array.isArray(aItemsToClear)) {
+-    // Shallow copy the array, as we are going to modify
+-    // it in place later.
+-    itemsToClear = [...aItemsToClear];
+-  } else {
+-    let branch = Services.prefs.getBranch(prefDomain);
+-    itemsToClear = Object.keys(items).filter(itemName => {
+-      try {
+-        return branch.getBoolPref(itemName);
+-      } catch (ex) {
+-        return false;
+-      }
+-    });
+-  }
++  // Shallow copy the array, as we are going to modify it in place later.
++  if (!Array.isArray(aItemsToClear))
++    throw new Error("Must pass an array of items to clear.");
++  let itemsToClear = [...aItemsToClear];
+ 
+   // Store the list of items to clear, in case we are killed before we
+   // get a chance to complete.
+-  Services.prefs.setStringPref(Sanitizer.PREF_SANITIZE_IN_PROGRESS,
+-                               JSON.stringify({itemsToClear, options}));
++  let uid = gPendingSanitizationSerial++;
++  // Shutdown sanitization is managed outside.
++  if (!progress.isShutdown)
++    addPendingSanitization(uid, itemsToClear, options);
+ 
+   // Store the list of items to clear, for debugging/forensics purposes
+   for (let k of itemsToClear) {
+     progress[k] = "ready";
+   }
+ 
+   // Ensure open windows get cleared first, if they're in our list, so that
+   // they don't stick around in the recently closed windows list, and so we
+@@ -808,29 +833,81 @@ async function sanitizeInternal(items, a
+   }
+   for (let handle of handles) {
+     progress[handle.name] = "blocking";
+     await handle.promise;
+   }
+ 
+   // Sanitization is complete.
+   TelemetryStopwatch.finish("FX_SANITIZE_TOTAL", refObj);
+-  // Reset the inProgress preference since we were not killed during
+-  // sanitization.
+-  Services.prefs.clearUserPref(Sanitizer.PREF_SANITIZE_IN_PROGRESS);
++  if (!progress.isShutdown)
++    removePendingSanitization(uid);
+   progress = {};
+   if (seenError) {
+     throw new Error("Error sanitizing");
+   }
+ }
+ 
+-async function sanitizeOnShutdown(options = {}) {
+-  if (!Services.prefs.getBoolPref(Sanitizer.PREF_SANITIZE_ON_SHUTDOWN)) {
++async function sanitizeOnShutdown(progress) {
++  if (!Sanitizer.shouldSanitizeOnShutdown) {
+     return;
+   }
+   // Need to sanitize upon shutdown
+-  options.prefDomain = "privacy.clearOnShutdown.";
+-  await Sanitizer.sanitize(null, options);
++  let itemsToClear = getItemsToClearFromPrefBranch(Sanitizer.PREF_SHUTDOWN_BRANCH);
++  await Sanitizer.sanitize(itemsToClear, { progress });
+   // We didn't crash during shutdown sanitization, so annotate it to avoid
+   // sanitizing again on startup.
+-  Services.prefs.setBoolPref(Sanitizer.PREF_SANITIZE_DID_SHUTDOWN, true);
++  removePendingSanitization("shutdown");
+   Services.prefs.savePrefFile(null);
+ }
++
++/**
++ * Gets an array of items to clear from the given pref branch.
++ * @param branch The pref branch to fetch.
++ * @return Array of items to clear
++ */
++function getItemsToClearFromPrefBranch(branch) {
++  branch = Services.prefs.getBranch(branch);
++  return Object.keys(Sanitizer.items).filter(itemName => {
++    try {
++      return branch.getBoolPref(itemName);
++    } catch (ex) {
++      return false;
++    }
++  });
++}
++
++/**
++ * These functions are used to track pending sanitization on the next startup
++ * in case of a crash before a sanitization could happen.
++ * @param id A unique id identifying the sanitization
++ * @param itemsToClear The items to clear
++ * @param options The Sanitize options
++ */
++function addPendingSanitization(id, itemsToClear, options) {
++  let pendingSanitizations = safeGetPendingSanitizations();
++  pendingSanitizations.push({id, itemsToClear, options});
++  Services.prefs.setStringPref(Sanitizer.PREF_PENDING_SANITIZATIONS,
++                               JSON.stringify(pendingSanitizations));
++}
++function removePendingSanitization(id) {
++  let pendingSanitizations = safeGetPendingSanitizations();
++  let i = pendingSanitizations.findIndex(s => s.id == id);
++  let [s] = pendingSanitizations.splice(i, 1);
++  Services.prefs.setStringPref(Sanitizer.PREF_PENDING_SANITIZATIONS,
++    JSON.stringify(pendingSanitizations));
++  return s;
++}
++function getAndClearPendingSanitizations() {
++  let pendingSanitizations = safeGetPendingSanitizations();
++  if (pendingSanitizations.length)
++    Services.prefs.clearUserPref(Sanitizer.PREF_PENDING_SANITIZATIONS);
++  return pendingSanitizations;
++}
++function safeGetPendingSanitizations() {
++  try {
++    return JSON.parse(
++      Services.prefs.getStringPref(Sanitizer.PREF_PENDING_SANITIZATIONS, "[]"));
++  } catch (ex) {
++    Cu.reportError("Invalid JSON value for pending sanitizations: " + ex);
++    return [];
++  }
++}
+diff --git a/browser/modules/test/unit/test_Sanitizer_interrupted.js b/browser/modules/test/unit/test_Sanitizer_interrupted.js
+new file mode 100644
+--- /dev/null
++++ b/browser/modules/test/unit/test_Sanitizer_interrupted.js
+@@ -0,0 +1,67 @@
++/* Any copyright is dedicated to the Public Domain.
++ * http://creativecommons.org/publicdomain/zero/1.0/ */
++
++"use strict";
++
++ChromeUtils.import("resource://gre/modules/Services.jsm");
++
++do_get_profile();
++
++// Test that interrupted sanitizations are properly tracked.
++
++add_task(async function() {
++  ChromeUtils.import("resource:///modules/Sanitizer.jsm");
++  registerCleanupFunction(() => {
++    Services.prefs.clearUserPref(Sanitizer.PREF_SANITIZE_ON_SHUTDOWN);
++    Services.prefs.clearUserPref(Sanitizer.PREF_SHUTDOWN_BRANCH + "formdata");
++  });
++  Services.prefs.setBoolPref(Sanitizer.PREF_SANITIZE_ON_SHUTDOWN, true);
++  Services.prefs.setBoolPref(Sanitizer.PREF_SHUTDOWN_BRANCH + "formdata", true);
++
++  await Sanitizer.onStartup();
++  Assert.ok(Sanitizer.shouldSanitizeOnShutdown, "Should sanitize on shutdown");
++
++  let pendingSanitizations = JSON.parse(
++    Services.prefs.getStringPref(Sanitizer.PREF_PENDING_SANITIZATIONS, "[]"));
++  Assert.equal(pendingSanitizations.length, 1, "Should have 1 pending sanitization");
++  Assert.equal(pendingSanitizations[0].id, "shutdown", "Should be the shutdown sanitization");
++  Assert.ok(pendingSanitizations[0].itemsToClear.includes("formdata"), "Pref has been setup");
++  Assert.ok(!pendingSanitizations[0].options.isShutdown, "Shutdown option is not present");
++
++  // Check the preference listeners.
++  Services.prefs.setBoolPref(Sanitizer.PREF_SANITIZE_ON_SHUTDOWN, false);
++  pendingSanitizations = JSON.parse(
++    Services.prefs.getStringPref(Sanitizer.PREF_PENDING_SANITIZATIONS, "[]"));
++  Assert.equal(pendingSanitizations.length, 0, "Should not have pending sanitizations");
++  Assert.ok(!Sanitizer.shouldSanitizeOnShutdown, "Should not sanitize on shutdown");
++  Services.prefs.setBoolPref(Sanitizer.PREF_SANITIZE_ON_SHUTDOWN, true);
++  pendingSanitizations = JSON.parse(
++    Services.prefs.getStringPref(Sanitizer.PREF_PENDING_SANITIZATIONS, "[]"));
++  Assert.equal(pendingSanitizations.length, 1, "Should have 1 pending sanitization");
++  Assert.equal(pendingSanitizations[0].id, "shutdown", "Should be the shutdown sanitization");
++
++  Assert.ok(pendingSanitizations[0].itemsToClear.includes("formdata"),
++            "Pending sanitizations should include formdata");
++  Services.prefs.setBoolPref(Sanitizer.PREF_SHUTDOWN_BRANCH + "formdata", false);
++  pendingSanitizations = JSON.parse(
++    Services.prefs.getStringPref(Sanitizer.PREF_PENDING_SANITIZATIONS, "[]"));
++  Assert.equal(pendingSanitizations.length, 1, "Should have 1 pending sanitization");
++  Assert.ok(!pendingSanitizations[0].itemsToClear.includes("formdata"),
++            "Pending sanitizations should have been updated");
++
++  // Check a sanitization properly rebuilds the pref.
++  await Sanitizer.sanitize(["formdata"]);
++  pendingSanitizations = JSON.parse(
++    Services.prefs.getStringPref(Sanitizer.PREF_PENDING_SANITIZATIONS, "[]"));
++  Assert.equal(pendingSanitizations.length, 1, "Should have 1 pending sanitization");
++  Assert.equal(pendingSanitizations[0].id, "shutdown", "Should be the shutdown sanitization");
++
++  // Startup should run the pending one and setup a new shutdown sanitization.
++  Services.prefs.setBoolPref(Sanitizer.PREF_SHUTDOWN_BRANCH + "formdata", false);
++  await Sanitizer.onStartup();
++  pendingSanitizations = JSON.parse(
++    Services.prefs.getStringPref(Sanitizer.PREF_PENDING_SANITIZATIONS, "[]"));
++  Assert.equal(pendingSanitizations.length, 1, "Should have 1 pending sanitization");
++  Assert.equal(pendingSanitizations[0].id, "shutdown", "Should be the shutdown sanitization");
++  Assert.ok(!pendingSanitizations[0].itemsToClear.includes("formdata"), "Pref has been setup");
++});
+diff --git a/browser/modules/test/unit/xpcshell.ini b/browser/modules/test/unit/xpcshell.ini
+--- a/browser/modules/test/unit/xpcshell.ini
++++ b/browser/modules/test/unit/xpcshell.ini
+@@ -2,10 +2,11 @@
+ head =
+ firefox-appdir = browser
+ skip-if = toolkit == 'android'
+ 
+ [test_AttributionCode.js]
+ skip-if = os != 'win'
+ [test_DirectoryLinksProvider.js]
+ [test_E10SUtils_nested_URIs.js]
++[test_Sanitizer_interrupted.js]
+ [test_SitePermissions.js]
+ [test_LaterRun.js]

+ 331 - 0
frg/mozilla-release/work-js/1435910-0_2-60a1.patch

@@ -0,0 +1,331 @@
+# HG changeset patch
+# User Tooru Fujisawa <arai_a@mac.com>
+# Date 1519781802 -32400
+# Node ID 6331c25d7425cc1f1fdf72aa96376d26e503e3e5
+# Parent  278d965eb75f50ca057fef4c11ee1f28df1fa85e
+Bug 1435910 - Part 0.2: Add BrowserTestUtils.{promiseAlertDialogOpen,promiseAlertDialog}. r=Gijs
+
+diff --git a/browser/components/downloads/test/browser/browser_confirm_unblock_download.js b/browser/components/downloads/test/browser/browser_confirm_unblock_download.js
+--- a/browser/components/downloads/test/browser/browser_confirm_unblock_download.js
++++ b/browser/components/downloads/test/browser/browser_confirm_unblock_download.js
+@@ -3,17 +3,17 @@
+ 
+ "use strict";
+ 
+ // Tests the dialog which allows the user to unblock a downloaded file.
+ 
+ registerCleanupFunction(() => {});
+ 
+ async function assertDialogResult({ args, buttonToClick, expectedResult }) {
+-  promiseAlertDialogOpen(buttonToClick);
++  BrowserTestUtils.promiseAlertDialogOpen(buttonToClick);
+   is(await DownloadsCommon.confirmUnblockDownload(args), expectedResult);
+ }
+ 
+ /**
+  * Tests the "unblock" dialog, for each of the possible verdicts.
+  */
+ add_task(async function test_unblock_dialog_unblock() {
+   for (let verdict of [Downloads.Error.BLOCK_VERDICT_MALWARE,
+diff --git a/browser/components/downloads/test/browser/head.js b/browser/components/downloads/test/browser/head.js
+--- a/browser/components/downloads/test/browser/head.js
++++ b/browser/components/downloads/test/browser/head.js
+@@ -167,34 +167,8 @@ function openLibrary(aLeftPaneRoot) {
+   let library = window.openDialog("chrome://browser/content/places/places.xul",
+                                   "", "chrome,toolbar=yes,dialog=no,resizable",
+                                   aLeftPaneRoot);
+ 
+   return new Promise(resolve => {
+     waitForFocus(resolve, library);
+   });
+ }
+-
+-function promiseAlertDialogOpen(buttonAction) {
+-  return new Promise(resolve => {
+-    Services.ww.registerNotification(function onOpen(subj, topic, data) {
+-      if (topic == "domwindowopened" && subj instanceof Ci.nsIDOMWindow) {
+-        // The test listens for the "load" event which guarantees that the alert
+-        // class has already been added (it is added when "DOMContentLoaded" is
+-        // fired).
+-        subj.addEventListener("load", function() {
+-          if (subj.document.documentURI ==
+-              "chrome://global/content/commonDialog.xul") {
+-            Services.ww.unregisterNotification(onOpen);
+-
+-            let dialog = subj.document.getElementById("commonDialog");
+-            ok(dialog.classList.contains("alert-dialog"),
+-               "The dialog element should contain an alert class.");
+-
+-            let doc = subj.document.documentElement;
+-            doc.getButton(buttonAction).click();
+-            resolve();
+-          }
+-        }, {once: true});
+-      }
+-    });
+-  });
+-}
+diff --git a/browser/components/preferences/in-content-new/tests/head.js b/browser/components/preferences/in-content-new/tests/head.js
+--- a/browser/components/preferences/in-content-new/tests/head.js
++++ b/browser/components/preferences/in-content-new/tests/head.js
+@@ -166,34 +166,21 @@ function waitForCondition(aConditionFn, 
+       setTimeout(tryNow, aCheckInterval);
+     }
+     let tries = 0;
+     tryAgain();
+   });
+ }
+ 
+ function promiseWindowDialogOpen(buttonAction, url) {
+-  return new Promise(resolve => {
+-    Services.ww.registerNotification(function onOpen(subj, topic, data) {
+-      if (topic == "domwindowopened" && subj instanceof Ci.nsIDOMWindow) {
+-        subj.addEventListener("load", function onLoad() {
+-          if (subj.document.documentURI == url) {
+-            Services.ww.unregisterNotification(onOpen);
+-            let doc = subj.document.documentElement;
+-            doc.getButton(buttonAction).click();
+-            resolve();
+-          }
+-        }, {once: true});
+-      }
+-    });
+-  });
++  return BrowserTestUtils.promiseAlertDialogOpen(buttonAction, url);
+ }
+ 
+ function promiseAlertDialogOpen(buttonAction) {
+-  return promiseWindowDialogOpen(buttonAction, "chrome://global/content/commonDialog.xul");
++  return BrowserTestUtils.promiseAlertDialogOpen(buttonAction);
+ }
+ 
+ function promiseSiteDataManagerSitesUpdated() {
+   return TestUtils.topicObserved("sitedatamanager:sites-updated", () => true);
+ }
+ 
+ function openSiteDataSettingsDialog() {
+   let doc = gBrowser.selectedBrowser.contentDocument;
+diff --git a/browser/components/preferences/in-content/tests/head.js b/browser/components/preferences/in-content/tests/head.js
+--- a/browser/components/preferences/in-content/tests/head.js
++++ b/browser/components/preferences/in-content/tests/head.js
+@@ -228,34 +228,21 @@ function waitForCondition(aConditionFn, 
+       setTimeout(tryNow, aCheckInterval);
+     }
+     let tries = 0;
+     tryAgain();
+   });
+ }
+ 
+ function promiseWindowDialogOpen(buttonAction, url) {
+-  return new Promise(resolve => {
+-    Services.ww.registerNotification(function onOpen(subj, topic, data) {
+-      if (topic == "domwindowopened" && subj instanceof Ci.nsIDOMWindow) {
+-        subj.addEventListener("load", function onLoad() {
+-          if (subj.document.documentURI == url) {
+-            Services.ww.unregisterNotification(onOpen);
+-            let doc = subj.document.documentElement;
+-            doc.getButton(buttonAction).click();
+-            resolve();
+-          }
+-        }, {once: true});
+-      }
+-    });
+-  });
++  return BrowserTestUtils.promiseAlertDialogOpen(buttonAction, url);
+ }
+ 
+ function promiseAlertDialogOpen(buttonAction) {
+-  return promiseWindowDialogOpen(buttonAction, "chrome://global/content/commonDialog.xul");
++  return BrowserTestUtils.promiseAlertDialogOpen(buttonAction);
+ }
+ 
+ function openSettingsDialog() {
+   let win = gBrowser.selectedBrowser.contentWindow;
+   let doc = gBrowser.selectedBrowser.contentDocument;
+   let settingsBtn = doc.getElementById("siteDataSettings");
+   let dialogOverlay = win.gSubDialog._preloadDialog._overlay;
+   let dialogLoadPromise = promiseLoadSubDialog("chrome://browser/content/preferences/siteDataSettings.xul");
+diff --git a/dom/workers/test/serviceworkers/browser_download_canceled.js b/dom/workers/test/serviceworkers/browser_download_canceled.js
+--- a/dom/workers/test/serviceworkers/browser_download_canceled.js
++++ b/dom/workers/test/serviceworkers/browser_download_canceled.js
+@@ -28,54 +28,35 @@ const { Downloads } = Cu.import("resourc
+ async function clearDownloads() {
+   const downloads = await Downloads.getList(Downloads.ALL);
+   downloads.removeFinished();
+ }
+ 
+ /**
+  * Returns a Promise that will be resolved once the download dialog shows up and
+  * we have clicked the given button.
+- *
+- * Derived from browser/components/downloads/test/browser/head.js's
+- * self-contained promiseAlertDialogOpen helper, but modified to work on the
+- * download dialog instead of commonDialog.xul.
+  */
+ function promiseClickDownloadDialogButton(buttonAction) {
+-  return new Promise(resolve => {
+-    Services.ww.registerNotification(function onOpen(win, topic, data) {
+-      if (topic === "domwindowopened" && win instanceof Ci.nsIDOMWindow) {
+-        // The test listens for the "load" event which guarantees that the alert
+-        // class has already been added (it is added when "DOMContentLoaded" is
+-        // fired).
+-        win.addEventListener("load", function() {
+-          info(`found window of type: ${win.document.documentURI}`);
+-          if (win.document.documentURI ===
+-                "chrome://mozapps/content/downloads/unknownContentType.xul") {
+-            Services.ww.unregisterNotification(onOpen);
++  const uri = "chrome://mozapps/content/downloads/unknownContentType.xul";
++  BrowserTestUtils.promiseAlertDialogOpen(buttonAction, uri, async win => {
++    // nsHelperAppDlg.js currently uses an eval-based setTimeout(0) to invoke
++    // its postShowCallback that results in a misleading error to the console
++    // if we close the dialog before it gets a chance to run.  Just a
++    // setTimeout is not sufficient because it appears we get our "load"
++    // listener before the document's, so we use TestUtils.waitForTick() to
++    // defer until after its load handler runs, then use setTimeout(0) to end
++    // up after its eval.
++    await TestUtils.waitForTick();
+ 
+-            // nsHelperAppDlg.js currently uses an eval-based setTimeout(0) to
+-            // invoke its postShowCallback that results in a misleading error to
+-            // the console if we close the dialog before it gets a chance to
+-            // run.  Just a setTimeout is not sufficient because it appears we
+-            // get our "load" listener before the document's, so we use
+-            // executeSoon to defer until after its load handler runs, then
+-            // use setTimeout(0) to end up after its eval.
+-            executeSoon(function() {
+-              setTimeout(function() {
+-                const button = win.document.documentElement.getButton(buttonAction);
+-                button.disabled = false;
+-                info(`clicking ${buttonAction} button`);
+-                button.click();
+-                resolve();
+-              }, 0);
+-            });
+-          }
+-        }, {once: true});
+-      }
+-    });
++    await new Promise(resolve => setTimeout(resolve, 0));
++
++    const button = win.document.documentElement.getButton(buttonAction);
++    button.disabled = false;
++    info(`clicking ${buttonAction} button`);
++    button.click();
+   });
+ }
+ 
+ async function performCanceledDownload(tab, path) {
+   // Start waiting for the download dialog before triggering the download.
+   info("watching for download popup");
+   const cancelDownload = promiseClickDownloadDialogButton("cancel");
+ 
+@@ -158,9 +139,9 @@ add_task(async function interruptedDownl
+   await ContentTask.spawn(
+     tab.linkedBrowser,
+     null,
+     function() {
+       return content.wrappedJSObject.registration.unregister();
+     });
+   await BrowserTestUtils.removeTab(tab);
+   await clearDownloads();
+-});
+\ No newline at end of file
++});
+diff --git a/testing/mochitest/BrowserTestUtils/BrowserTestUtils.jsm b/testing/mochitest/BrowserTestUtils/BrowserTestUtils.jsm
+--- a/testing/mochitest/BrowserTestUtils/BrowserTestUtils.jsm
++++ b/testing/mochitest/BrowserTestUtils/BrowserTestUtils.jsm
+@@ -555,20 +555,20 @@ var BrowserTestUtils = {
+    *        and observing should continue. If not specified, the first window
+    *        resolves the returned promise.
+    * @return {Promise}
+    *         A Promise which resolves when a "domwindowopened" notification
+    *         has been fired by the window watcher.
+    */
+   domWindowOpened(win, checkFn) {
+     return new Promise(resolve => {
+-      function observer(subject, topic, data) {
++      async function observer(subject, topic, data) {
+         if (topic == "domwindowopened" && (!win || subject === win)) {
+           let observedWindow = subject.QueryInterface(Ci.nsIDOMWindow);
+-          if (checkFn && !checkFn(observedWindow)) {
++          if (checkFn && !await checkFn(observedWindow)) {
+             return;
+           }
+           Services.ww.unregisterNotification(observer);
+           resolve(observedWindow);
+         }
+       }
+       Services.ww.registerNotification(observer);
+     });
+@@ -1538,16 +1538,71 @@ var BrowserTestUtils = {
+   async _removeAboutPageRegistrations() {
+     for (let aboutModule of this._knownAboutPages) {
+       await this.unregisterAboutPage(aboutModule);
+     }
+     Services.ppmm.removeDelayedProcessScript(kAboutPageRegistrationContentScript);
+   },
+ 
+   /**
++   * Waits for the dialog to open, and clicks the specified button.
++   *
++   * @param {string} buttonAction
++   *        The ID of the button to click ("accept", "cancel", etc).
++   * @param {string} uri
++   *        The URI of the dialog to wait for.  Defaults to the common dialog.
++   * @return {Promise}
++   *         A Promise which resolves when a "domwindowopened" notification
++   *         for a dialog has been fired by the window watcher and the
++   *         specified button is clicked.
++   */
++  async promiseAlertDialogOpen(buttonAction,
++                               uri="chrome://global/content/commonDialog.xul",
++                               func) {
++    let win = await this.domWindowOpened(null, async win => {
++      // The test listens for the "load" event which guarantees that the alert
++      // class has already been added (it is added when "DOMContentLoaded" is
++      // fired).
++      await this.waitForEvent(win, "load");
++
++      return win.document.documentURI === uri;
++    });
++
++    if (func) {
++      await func(win);
++      return win;
++    }
++
++    let doc = win.document.documentElement;
++    doc.getButton(buttonAction).click();
++
++    return win;
++  },
++
++  /**
++   * Waits for the dialog to open, and clicks the specified button, and waits
++   * for the dialog to close.
++   *
++   * @param {string} buttonAction
++   *        The ID of the button to click ("accept", "cancel", etc).
++   * @param {string} uri
++   *        The URI of the dialog to wait for.  Defaults to the common dialog.
++   * @return {Promise}
++   *         A Promise which resolves when a "domwindowopened" notification
++   *         for a dialog has been fired by the window watcher and the
++   *         specified button is clicked, and the dialog has been fully closed.
++   */
++  async promiseAlertDialog(buttonAction,
++                           uri="chrome://global/content/commonDialog.xul",
++                           func) {
++    let win = await this.promiseAlertDialogOpen(buttonAction, uri, func);
++    return this.windowClosed(win);
++  },
++
++  /**
+    * Opens a tab with a given uri and params object. If the params object is not set
+    * or the params parameter does not include a triggeringPricnipal then this function
+    * provides a params object using the systemPrincipal as the default triggeringPrincipal.
+    */
+   addTab(browser, uri, params = {}) {
+     if (!params.triggeringPrincipal) {
+       params.triggeringPrincipal = Services.scriptSecurityManager.getSystemPrincipal();
+     }

+ 32 - 0
frg/mozilla-release/work-js/1437248-60a1.patch

@@ -0,0 +1,32 @@
+# HG changeset patch
+# User Hiroyuki Ikezoe <hikezoe@mozilla.com>
+# Date 1518244265 -32400
+# Node ID ac305af315a9738439a82fe88239a30f0e7c8258
+# Parent  2a5f4e2ed7561ce20ed61029cdc3161721e6f1e8
+Bug 1437248 - Disable privacy.reduceTimerPrecision on test_restyles.html. r=arai
+
+In this test we need to know precise time for checking that we unthrottle
+throttled transform animations periodically.
+
+MozReview-Commit-ID: ICLf448KFLr
+
+diff --git a/dom/animation/test/mozilla/test_restyles.html b/dom/animation/test/mozilla/test_restyles.html
+--- a/dom/animation/test/mozilla/test_restyles.html
++++ b/dom/animation/test/mozilla/test_restyles.html
+@@ -4,14 +4,15 @@
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <div id='log'></div>
+ <script>
+ 'use strict';
+ SimpleTest.waitForExplicitFinish();
+ SimpleTest.expectAssertions(0, 1); // bug 1332970
+ SpecialPowers.pushPrefEnv(
+   { 'set': [['dom.animations-api.core.enabled', true],
+-            ['layout.reflow.synthMouseMove', false]] },
++            ['layout.reflow.synthMouseMove', false],
++            ['privacy.reduceTimerPrecision', false]] },
+   function() {
+     window.open('file_restyles.html');
+   });
+ </script>
+ </html>

+ 30 - 0
frg/mozilla-release/work-js/1437880-61a1.patch

@@ -0,0 +1,30 @@
+# HG changeset patch
+# User accakks <aakanksha.jain8@gmail.com>
+# Date 1521815674 -19800
+# Node ID e7de3b21524c2d56a5c82ecde470b43f3ddb2d87
+# Parent  fe0ebf2dfdaaba9d03504eaba592f4ff7aa99fba
+Bug 1437880 -Disable "Remove Selected"  button in the site data manager dialog if a website is deselected. r=prathiksha
+
+MozReview-Commit-ID: 7YhXjg84QSx
+
+diff --git a/browser/components/preferences/siteDataSettings.js b/browser/components/preferences/siteDataSettings.js
+--- a/browser/components/preferences/siteDataSettings.js
++++ b/browser/components/preferences/siteDataSettings.js
+@@ -258,16 +258,17 @@ let gSiteDataSettings = {
+ 
+   close() {
+     window.close();
+   },
+ 
+   onClickTreeCol(e) {
+     this._sortSites(this._sites, e.target);
+     this._buildSitesList(this._sites);
++    this._list.clearSelection();
+   },
+ 
+   onCommandSearch() {
+     this._buildSitesList(this._sites);
+     this._list.clearSelection();
+   },
+ 
+   onClickRemoveSelected() {

+ 29 - 0
frg/mozilla-release/work-js/1438556-2-fix-whitespace-62a1.patch

@@ -0,0 +1,29 @@
+# HG changeset patch
+# User Matthew Gaudet <mgaudet@mozilla.com>
+# Date 1525097675 14400
+#      Mon Apr 30 10:14:35 2018 -0400
+# Node ID 0302149bc1691dab204128aa3ec57a150f81a91e
+# Parent  918e651c5db0844bd2cd2d4c1501059c3103a73e
+Bug 1438556: [Part 2] Verify global wrapper not nuked r=tcampbell
+
+diff --git a/js/src/jit/IonCacheIRCompiler.cpp b/js/src/jit/IonCacheIRCompiler.cpp
+--- a/js/src/jit/IonCacheIRCompiler.cpp
++++ b/js/src/jit/IonCacheIRCompiler.cpp
+@@ -676,17 +676,16 @@ IonCacheIRCompiler::emitGuardProto()
+ }
+ 
+ bool
+ IonCacheIRCompiler::emitGuardCompartment()
+ {
+     Register obj = allocator.useRegister(masm, reader.objOperandId());
+     JSObject* globalWrapper = objectStubField(reader.stubOffset());
+     JSCompartment* compartment = compartmentStubField(reader.stubOffset());
+-
+     AutoScratchRegister scratch(allocator, masm);
+ 
+     FailurePath* failure;
+     if (!addFailurePath(&failure))
+         return false;
+ 
+     // Verify that the global wrapper is still valid, as
+     // it is pre-requisite for doing the compartment check.

+ 277 - 0
frg/mozilla-release/work-js/1440638-61a1.patch

@@ -0,0 +1,277 @@
+# HG changeset patch
+# User Matheus Longaray <mlongaray@hp.com>
+# Date 1520429756 10800
+# Node ID ea46c0d873829f9270e55baec11ef0ec979d9985
+# Parent  fc65f96fde05696f1ceab42233adee37f85b35e0
+Bug 1440638 - Render error message for the Simplify Page document. r=mconley
+
+This patch renders error message for the Simplify Page document when
+Reader Mode fails to parse original document. This patch also adds a test
+to reliably produce a readable document that returns a parsing error when
+run through the readability library.
+
+MozReview-Commit-ID: 686mBkU9eVM
+
+diff --git a/toolkit/components/printing/tests/browser.ini b/toolkit/components/printing/tests/browser.ini
+--- a/toolkit/components/printing/tests/browser.ini
++++ b/toolkit/components/printing/tests/browser.ini
+@@ -1,10 +1,15 @@
+ [browser_page_change_print_original.js]
+ support-files =
+   file_page_change_print_original_1.html
+   file_page_change_print_original_2.html
+ skip-if = os == "mac"
+ 
++[browser_preview_print_simplify_non_article.js]
++support-files =
++    simplifyNonArticleSample.html
++skip-if = os == "mac"
++
+ [browser_preview_switch_print_selected.js]
+ support-files =
+     simplifyArticleSample.html
+ skip-if = os == "mac"
+\ No newline at end of file
+diff --git a/toolkit/components/printing/tests/browser_preview_print_simplify_non_article.js b/toolkit/components/printing/tests/browser_preview_print_simplify_non_article.js
+new file mode 100644
+--- /dev/null
++++ b/toolkit/components/printing/tests/browser_preview_print_simplify_non_article.js
+@@ -0,0 +1,80 @@
++/**
++ * Verify if we recover from parsing errors of Reader Mode when
++ * Simplify Page checkbox is checked.
++ */
++
++const TEST_PATH = getRootDirectory(gTestPath)
++                    .replace("chrome://mochitests/content", "http://example.com");
++
++add_task(async function set_simplify_and_reader_pref() {
++  // Ensure we have the simplify page preference set
++  await SpecialPowers.pushPrefEnv({
++    set: [
++      ["print.use_simplify_page", true],
++      ["reader.parse-on-load.enabled", true]
++    ]
++  });
++});
++
++add_task(async function switch_print_preview_browsers() {
++  let url = TEST_PATH + "simplifyNonArticleSample.html";
++
++  // Can only do something if we have a print preview UI:
++  if (AppConstants.platform != "win" && AppConstants.platform != "linux") {
++    ok(false, "Can't test if there's no print preview.");
++    return;
++  }
++
++  // Ensure we get a browserStopped for this browser
++  let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, url, false, true);
++
++  // Trick browser to think loaded tab has isArticle property set as true
++  tab.linkedBrowser.isArticle = true;
++
++  // Enter print preview
++  let defaultPPBrowser = PrintPreviewListener.getPrintPreviewBrowser();
++  let defaultPPEntered = BrowserTestUtils
++                          .waitForMessage(defaultPPBrowser.messageManager,
++                                          "Printing:Preview:Entered");
++  document.getElementById("cmd_printPreview").doCommand();
++  await defaultPPEntered;
++
++  // Assert that we are showing the initial content on default print preview browser
++  await ContentTask.spawn(defaultPPBrowser, null, async function() {
++    is(content.document.title, "Non article title", "Should have initial content.");
++  });
++
++  // Here we call simplified mode
++  let simplifiedPPBrowser = PrintPreviewListener.getSimplifiedPrintPreviewBrowser();
++  let simplifiedPPEntered = BrowserTestUtils
++                              .waitForMessage(simplifiedPPBrowser.messageManager,
++                                              "Printing:Preview:Entered");
++  let printPreviewToolbar = document.getElementById("print-preview-toolbar");
++
++  // Wait for simplify page option enablement
++  await BrowserTestUtils.waitForCondition(() => {
++    return !printPreviewToolbar.mSimplifyPageCheckbox.disabled;
++  });
++
++  printPreviewToolbar.mSimplifyPageCheckbox.click();
++  await simplifiedPPEntered;
++
++  // Assert that simplify page option is checked
++  is(printPreviewToolbar.mSimplifyPageCheckbox.checked, true,
++     "Should have simplify page option checked");
++
++  // Assert that we are showing recovery content on simplified print preview browser
++  await ContentTask.spawn(simplifiedPPBrowser, null, async function() {
++    is(content.document.title, "Failed to load article from page", "Should have recovery content.");
++  });
++
++  // Assert that we are selecting simplified print preview browser, and not default one
++  is(gBrowser.selectedTab.linkedBrowser, simplifiedPPBrowser,
++     "Should have simplified print preview browser selected");
++  isnot(gBrowser.selectedTab.linkedBrowser, defaultPPBrowser,
++        "Should not have default print preview browser selected");
++
++  PrintUtils.exitPrintPreview();
++
++  await BrowserTestUtils.removeTab(tab);
++});
+diff --git a/toolkit/components/printing/tests/simplifyNonArticleSample.html b/toolkit/components/printing/tests/simplifyNonArticleSample.html
+new file mode 100644
+--- /dev/null
++++ b/toolkit/components/printing/tests/simplifyNonArticleSample.html
+@@ -0,0 +1,9 @@
++<!DOCTYPE html>
++<html>
++<head>
++<title>Non article title</title>
++<meta name="description" content="This is the non-article description." />
++</head>
++<body>
++</body>
++</html>
+\ No newline at end of file
+diff --git a/toolkit/content/browser-content.js b/toolkit/content/browser-content.js
+--- a/toolkit/content/browser-content.js
++++ b/toolkit/content/browser-content.js
+@@ -651,25 +651,22 @@ var Printing = {
+         QueryInterface: XPCOMUtils.generateQI([
+           Ci.nsIWebProgressListener,
+           Ci.nsISupportsWeakReference,
+           Ci.nsIObserver,
+         ]),
+       };
+ 
+       // Here we QI the docShell into a nsIWebProgress passing our web progress listener in.
+-      let webProgress =  docShell.QueryInterface(Ci.nsIInterfaceRequestor)
+-                                 .getInterface(Ci.nsIWebProgress);
++      let webProgress = docShell.QueryInterface(Ci.nsIInterfaceRequestor)
++                                .getInterface(Ci.nsIWebProgress);
+       webProgress.addProgressListener(webProgressListener, Ci.nsIWebProgress.NOTIFY_STATE_REQUEST);
+ 
+       content.document.head.innerHTML = "";
+ 
+-      // Set title of document
+-      content.document.title = article.title;
+-
+       // Set base URI of document. Print preview code will read this value to
+       // populate the URL field in print settings so that it doesn't show
+       // "about:blank" as its URI.
+       let headBaseElement = content.document.createElement("base");
+       headBaseElement.setAttribute("href", URL);
+       content.document.head.appendChild(headBaseElement);
+ 
+       // Create link element referencing aboutReader.css and append it to head
+@@ -688,57 +685,78 @@ var Printing = {
+ 
+       content.document.body.innerHTML = "";
+ 
+       // Create container div (main element) and append it to body
+       let containerElement = content.document.createElement("div");
+       containerElement.setAttribute("id", "container");
+       content.document.body.appendChild(containerElement);
+ 
+-      // Create header div and append it to container
+-      let headerElement = content.document.createElement("div");
+-      headerElement.setAttribute("id", "reader-header");
+-      headerElement.setAttribute("class", "header");
+-      containerElement.appendChild(headerElement);
++      // Reader Mode might return null if there's a failure when parsing the document.
++      // We'll render the error message for the Simplify Page document when that happens.
++      if (article) {
++        // Set title of document
++        content.document.title = article.title;
++
++        // Create header div and append it to container
++        let headerElement = content.document.createElement("div");
++        headerElement.setAttribute("id", "reader-header");
++        headerElement.setAttribute("class", "header");
++        containerElement.appendChild(headerElement);
+ 
+-      // Jam the article's title and byline into header div
+-      let titleElement = content.document.createElement("h1");
+-      titleElement.setAttribute("id", "reader-title");
+-      titleElement.textContent = article.title;
+-      headerElement.appendChild(titleElement);
++        // Jam the article's title and byline into header div
++        let titleElement = content.document.createElement("h1");
++        titleElement.setAttribute("id", "reader-title");
++        titleElement.textContent = article.title;
++        headerElement.appendChild(titleElement);
+ 
+-      let bylineElement = content.document.createElement("div");
+-      bylineElement.setAttribute("id", "reader-credits");
+-      bylineElement.setAttribute("class", "credits");
+-      bylineElement.textContent = article.byline;
+-      headerElement.appendChild(bylineElement);
++        let bylineElement = content.document.createElement("div");
++        bylineElement.setAttribute("id", "reader-credits");
++        bylineElement.setAttribute("class", "credits");
++        bylineElement.textContent = article.byline;
++        headerElement.appendChild(bylineElement);
+ 
+-      // Display header element
+-      headerElement.style.display = "block";
++        // Display header element
++        headerElement.style.display = "block";
+ 
+-      // Create content div and append it to container
+-      let contentElement = content.document.createElement("div");
+-      contentElement.setAttribute("class", "content");
+-      containerElement.appendChild(contentElement);
++        // Create content div and append it to container
++        let contentElement = content.document.createElement("div");
++        contentElement.setAttribute("class", "content");
++        containerElement.appendChild(contentElement);
+ 
+-      // Jam the article's content into content div
+-      let readerContent = content.document.createElement("div");
+-      readerContent.setAttribute("id", "moz-reader-content");
+-      contentElement.appendChild(readerContent);
++        // Jam the article's content into content div
++        let readerContent = content.document.createElement("div");
++        readerContent.setAttribute("id", "moz-reader-content");
++        contentElement.appendChild(readerContent);
++
++        let articleUri = Services.io.newURI(article.url);
++        let parserUtils = Cc["@mozilla.org/parserutils;1"].getService(Ci.nsIParserUtils);
++        let contentFragment = parserUtils.parseFragment(article.content,
++          Ci.nsIParserUtils.SanitizerDropForms | Ci.nsIParserUtils.SanitizerAllowStyle,
++          false, articleUri, readerContent);
++
++        readerContent.appendChild(contentFragment);
+ 
+-      let articleUri = Services.io.newURI(article.url);
+-      let parserUtils = Cc["@mozilla.org/parserutils;1"].getService(Ci.nsIParserUtils);
+-      let contentFragment = parserUtils.parseFragment(article.content,
+-        Ci.nsIParserUtils.SanitizerDropForms | Ci.nsIParserUtils.SanitizerAllowStyle,
+-        false, articleUri, readerContent);
++        // Display reader content element
++        readerContent.style.display = "block";
++      } else {
++        let aboutReaderStrings = Services.strings.createBundle("chrome://global/locale/aboutReader.properties");
++        let errorMessage = aboutReaderStrings.GetStringFromName("aboutReader.loadError");
++
++        content.document.title = errorMessage;
+ 
+-      readerContent.appendChild(contentFragment);
++        // Create reader message div and append it to body
++        let readerMessageElement = content.document.createElement("div");
++        readerMessageElement.setAttribute("class", "reader-message");
++        readerMessageElement.textContent = errorMessage;
++        containerElement.appendChild(readerMessageElement);
+ 
+-      // Display reader content element
+-      readerContent.style.display = "block";
++        // Display reader message element
++        readerMessageElement.style.display = "block";
++      }
+     });
+   },
+ 
+   enterPrintPreview(contentWindow, simplifiedMode, changingBrowsers, defaultPrinterName) {
+     try {
+       let printSettings = this.getPrintSettings(defaultPrinterName);
+ 
+       // If we happen to be on simplified mode, we need to set docURL in order

+ 387 - 0
frg/mozilla-release/work-js/1442183-61a1.patch

@@ -0,0 +1,387 @@
+# HG changeset patch
+# User Johann Hofmann <jhofmann@mozilla.com>
+# Date 1523429989 -7200
+# Node ID 99b6961d14a25843b84aab592a7f0c0a7d86e45f
+# Parent  839ee7878d1f0c94cc4680f9494609df208245b8
+Bug 1442183 - Allow multiple selection in the site data manager list. r=prathiksha
+
+MozReview-Commit-ID: D1u3963xC6C
+
+diff --git a/browser/components/preferences/in-content-new/tests/siteData/browser.ini b/browser/components/preferences/in-content-new/tests/siteData/browser.ini
+--- a/browser/components/preferences/in-content-new/tests/siteData/browser.ini
++++ b/browser/components/preferences/in-content-new/tests/siteData/browser.ini
+@@ -7,8 +7,9 @@ support-files =
+   offline/offline.html
+   offline/manifest.appcache
+ 
+ [browser_clearSiteData.js]
+ [browser_siteData.js]
+ skip-if = (os == 'linux' && debug) # Bug 1439332
+ [browser_siteData2.js]
+ [browser_siteData3.js]
++[browser_siteData_multi_select.js]
+diff --git a/browser/components/preferences/in-content-new/tests/siteData/browser_siteData2.js b/browser/components/preferences/in-content-new/tests/siteData/browser_siteData2.js
+--- a/browser/components/preferences/in-content-new/tests/siteData/browser_siteData2.js
++++ b/browser/components/preferences/in-content-new/tests/siteData/browser_siteData2.js
+@@ -1,24 +1,10 @@
+ "use strict";
+ 
+-function promiseSettingsDialogClose() {
+-  return new Promise(resolve => {
+-    let win = gBrowser.selectedBrowser.contentWindow;
+-    let dialogOverlay = win.gSubDialog._topDialog._overlay;
+-    let dialogWin = win.gSubDialog._topDialog._frame.contentWindow;
+-    dialogWin.addEventListener("unload", function unload() {
+-      if (dialogWin.document.documentURI === "chrome://browser/content/preferences/siteDataSettings.xul") {
+-        isnot(dialogOverlay.style.visibility, "visible", "The Settings dialog should be hidden");
+-        resolve();
+-      }
+-    }, { once: true });
+-  });
+-}
+-
+ function assertAllSitesNotListed(win) {
+   let frameDoc = win.gSubDialog._topDialog._frame.contentDocument;
+   let removeBtn = frameDoc.getElementById("removeSelected");
+   let removeAllBtn = frameDoc.getElementById("removeAll");
+   let sitesList = frameDoc.getElementById("sitesList");
+   let sites = sitesList.getElementsByTagName("richlistitem");
+   is(sites.length, 0, "Should not list all sites");
+   is(removeBtn.disabled, true, "Should disable the removeSelected button");
+diff --git a/browser/components/preferences/in-content-new/tests/siteData/browser_siteData_multi_select.js b/browser/components/preferences/in-content-new/tests/siteData/browser_siteData_multi_select.js
+new file mode 100644
+--- /dev/null
++++ b/browser/components/preferences/in-content-new/tests/siteData/browser_siteData_multi_select.js
+@@ -0,0 +1,84 @@
++"use strict";
++
++// Test selecting and removing partial sites
++add_task(async function() {
++  mockSiteDataManager.register(SiteDataManager, [
++    {
++      usage: 1024,
++      origin: "https://account.xyz.com",
++      persisted: true
++    },
++    {
++      usage: 1024,
++      origin: "https://shopping.xyz.com",
++      persisted: false
++    },
++    {
++      usage: 1024,
++      origin: "http://cinema.bar.com",
++      persisted: true
++    },
++    {
++      usage: 1024,
++      origin: "http://email.bar.com",
++      persisted: false
++    },
++    {
++      usage: 1024,
++      origin: "https://s3-us-west-2.amazonaws.com",
++      persisted: true
++    },
++    {
++      usage: 1024,
++      origin: "https://127.0.0.1",
++      persisted: false
++    },
++    {
++      usage: 1024,
++      origin: "https://[0:0:0:0:0:0:0:1]",
++      persisted: true
++    },
++  ]);
++  let fakeHosts = mockSiteDataManager.fakeSites.map(site => site.principal.URI.host);
++
++  let updatePromise = promiseSiteDataManagerSitesUpdated();
++  await openPreferencesViaOpenPreferencesAPI("privacy", { leaveOpen: true });
++  await updatePromise;
++  await openSiteDataSettingsDialog();
++
++  let doc = gBrowser.selectedBrowser.contentDocument;
++
++  // Test the initial state
++  assertSitesListed(doc, fakeHosts);
++
++  let removeDialogOpenPromise = BrowserTestUtils.promiseAlertDialogOpen("accept", REMOVE_DIALOG_URL);
++  let settingsDialogClosePromise = promiseSettingsDialogClose();
++
++  let win = gBrowser.selectedBrowser.contentWindow;
++  let frameDoc = win.gSubDialog._topDialog._frame.contentDocument;
++
++  // Select some sites to remove.
++  let sitesList = frameDoc.getElementById("sitesList");
++  fakeHosts.slice(0, 2).forEach(host => {
++    let site = sitesList.querySelector(`richlistitem[host="${host}"]`);
++    sitesList.addItemToSelection(site);
++  });
++
++  let removeBtn = frameDoc.getElementById("removeSelected");
++  is(removeBtn.disabled, false, "Should enable the removeSelected button");
++  removeBtn.doCommand();
++  is(removeBtn.disabled, true, "Should disable the removeSelected button");
++
++  let saveBtn = frameDoc.getElementById("save");
++  assertSitesListed(doc, fakeHosts.slice(2));
++  saveBtn.doCommand();
++
++  await removeDialogOpenPromise;
++  await settingsDialogClosePromise;
++  await openSiteDataSettingsDialog();
++
++  assertSitesListed(doc, fakeHosts.slice(2));
++
++  await mockSiteDataManager.unregister();
++  BrowserTestUtils.removeTab(gBrowser.selectedTab);
++});
+diff --git a/browser/components/preferences/in-content-new/tests/siteData/head.js b/browser/components/preferences/in-content-new/tests/siteData/head.js
+--- a/browser/components/preferences/in-content-new/tests/siteData/head.js
++++ b/browser/components/preferences/in-content-new/tests/siteData/head.js
+@@ -136,16 +136,30 @@ function openSiteDataSettingsDialog() {
+   let dialogInitPromise = TestUtils.topicObserved("sitedata-settings-init", () => true);
+   let fullyLoadPromise = Promise.all([ dialogLoadPromise, dialogInitPromise ]).then(() => {
+     is(dialogOverlay.style.visibility, "visible", "The Settings dialog should be visible");
+   });
+   settingsBtn.doCommand();
+   return fullyLoadPromise;
+ }
+ 
++function promiseSettingsDialogClose() {
++  return new Promise(resolve => {
++    let win = gBrowser.selectedBrowser.contentWindow;
++    let dialogOverlay = win.gSubDialog._topDialog._overlay;
++    let dialogWin = win.gSubDialog._topDialog._frame.contentWindow;
++    dialogWin.addEventListener("unload", function unload() {
++      if (dialogWin.document.documentURI === "chrome://browser/content/preferences/siteDataSettings.xul") {
++        isnot(dialogOverlay.style.visibility, "visible", "The Settings dialog should be hidden");
++        resolve();
++      }
++    }, { once: true });
++  });
++}
++
+ function assertSitesListed(doc, hosts) {
+   let frameDoc = content.gSubDialog._topDialog._frame.contentDocument;
+   let removeBtn = frameDoc.getElementById("removeSelected");
+   let removeAllBtn = frameDoc.getElementById("removeAll");
+   let sitesList = frameDoc.getElementById("sitesList");
+   let totalSitesNumber = sitesList.getElementsByTagName("richlistitem").length;
+   is(totalSitesNumber, hosts.length, "Should list the right sites number");
+   hosts.forEach(host => {
+diff --git a/browser/components/preferences/in-content/tests/siteData/browser.ini b/browser/components/preferences/in-content/tests/siteData/browser.ini
+--- a/browser/components/preferences/in-content/tests/siteData/browser.ini
++++ b/browser/components/preferences/in-content/tests/siteData/browser.ini
+@@ -7,8 +7,9 @@ support-files =
+   offline/offline.html
+   offline/manifest.appcache
+ 
+ [browser_clearSiteData.js]
+ [browser_siteData.js]
+ skip-if = (os == 'linux' && debug) # Bug 1439332
+ [browser_siteData2.js]
+ [browser_siteData3.js]
++[browser_siteData_multi_select.js]
+diff --git a/browser/components/preferences/in-content/tests/siteData/browser_siteData_multi_select.js b/browser/components/preferences/in-content/tests/siteData/browser_siteData_multi_select.js
+new file mode 100644
+--- /dev/null
++++ b/browser/components/preferences/in-content/tests/siteData/browser_siteData_multi_select.js
+@@ -0,0 +1,84 @@
++"use strict";
++
++// Test selecting and removing partial sites
++add_task(async function() {
++  mockSiteDataManager.register(SiteDataManager, [
++    {
++      usage: 1024,
++      origin: "https://account.xyz.com",
++      persisted: true
++    },
++    {
++      usage: 1024,
++      origin: "https://shopping.xyz.com",
++      persisted: false
++    },
++    {
++      usage: 1024,
++      origin: "http://cinema.bar.com",
++      persisted: true
++    },
++    {
++      usage: 1024,
++      origin: "http://email.bar.com",
++      persisted: false
++    },
++    {
++      usage: 1024,
++      origin: "https://s3-us-west-2.amazonaws.com",
++      persisted: true
++    },
++    {
++      usage: 1024,
++      origin: "https://127.0.0.1",
++      persisted: false
++    },
++    {
++      usage: 1024,
++      origin: "https://[0:0:0:0:0:0:0:1]",
++      persisted: true
++    },
++  ]);
++  let fakeHosts = mockSiteDataManager.fakeSites.map(site => site.principal.URI.host);
++
++  let updatePromise = promiseSiteDataManagerSitesUpdated();
++  await openPreferencesViaOpenPreferencesAPI("privacy", { leaveOpen: true });
++  await updatePromise;
++  await openSiteDataSettingsDialog();
++
++  let doc = gBrowser.selectedBrowser.contentDocument;
++
++  // Test the initial state
++  assertSitesListed(doc, fakeHosts);
++
++  let removeDialogOpenPromise = BrowserTestUtils.promiseAlertDialogOpen("accept", REMOVE_DIALOG_URL);
++  let settingsDialogClosePromise = promiseSettingsDialogClose();
++
++  let win = gBrowser.selectedBrowser.contentWindow;
++  let frameDoc = win.gSubDialog._topDialog._frame.contentDocument;
++
++  // Select some sites to remove.
++  let sitesList = frameDoc.getElementById("sitesList");
++  fakeHosts.slice(0, 2).forEach(host => {
++    let site = sitesList.querySelector(`richlistitem[host="${host}"]`);
++    sitesList.addItemToSelection(site);
++  });
++
++  let removeBtn = frameDoc.getElementById("removeSelected");
++  is(removeBtn.disabled, false, "Should enable the removeSelected button");
++  removeBtn.doCommand();
++  is(removeBtn.disabled, true, "Should disable the removeSelected button");
++
++  let saveBtn = frameDoc.getElementById("save");
++  assertSitesListed(doc, fakeHosts.slice(2));
++  saveBtn.doCommand();
++
++  await removeDialogOpenPromise;
++  await settingsDialogClosePromise;
++  await openSiteDataSettingsDialog();
++
++  assertSitesListed(doc, fakeHosts.slice(2));
++
++  await mockSiteDataManager.unregister();
++  BrowserTestUtils.removeTab(gBrowser.selectedTab);
++});
+diff --git a/browser/components/preferences/in-content/tests/siteData/head.js b/browser/components/preferences/in-content/tests/siteData/head.js
+--- a/browser/components/preferences/in-content/tests/siteData/head.js
++++ b/browser/components/preferences/in-content/tests/siteData/head.js
+@@ -194,16 +194,30 @@ function openSettingsDialog() {
+   let dialogInitPromise = TestUtils.topicObserved("sitedata-settings-init", () => true);
+   let fullyLoadPromise = Promise.all([ dialogLoadPromise, dialogInitPromise ]).then(() => {
+     is(dialogOverlay.style.visibility, "visible", "The Settings dialog should be visible");
+   });
+   settingsBtn.doCommand();
+   return fullyLoadPromise;
+ }
+ 
++function promiseSettingsDialogClose() {
++  return new Promise(resolve => {
++    let win = gBrowser.selectedBrowser.contentWindow;
++    let dialogOverlay = win.gSubDialog._topDialog._overlay;
++    let dialogWin = win.gSubDialog._topDialog._frame.contentWindow;
++    dialogWin.addEventListener("unload", function unload() {
++      if (dialogWin.document.documentURI === "chrome://browser/content/preferences/siteDataSettings.xul") {
++        isnot(dialogOverlay.style.visibility, "visible", "The Settings dialog should be hidden");
++        resolve();
++      }
++    }, { once: true });
++  }); 
++}
++
+ function assertSitesListed(doc, hosts) {
+   let win = gBrowser.selectedBrowser.contentWindow;
+   let frameDoc = win.gSubDialog._topDialog._frame.contentDocument;
+   let removeBtn = frameDoc.getElementById("removeSelected");
+   let removeAllBtn = frameDoc.getElementById("removeAll");
+   let sitesList = frameDoc.getElementById("sitesList");
+   let totalSitesNumber = sitesList.getElementsByTagName("richlistitem").length;
+   is(totalSitesNumber, hosts.length, "Should list the right sites number");
+@@ -214,30 +228,16 @@ function assertSitesListed(doc, hosts) {
+   is(removeBtn.disabled, true, "Should disable the removeSelected button");
+   is(removeAllBtn.disabled, false, "Should enable the removeAllBtn button");
+ }
+ 
+ function promiseSitesUpdated() {
+   return TestUtils.topicObserved("sitedatamanager:sites-updated", () => true);
+ }
+ 
+-function promiseSettingsDialogClose() {
+-  return new Promise(resolve => {
+-    let win = gBrowser.selectedBrowser.contentWindow;
+-    let dialogOverlay = win.gSubDialog._topDialog._overlay;
+-    let dialogWin = win.gSubDialog._topDialog._frame.contentWindow;
+-    dialogWin.addEventListener("unload", function unload() {
+-      if (dialogWin.document.documentURI === "chrome://browser/content/preferences/siteDataSettings.xul") {
+-        isnot(dialogOverlay.style.visibility, "visible", "The Settings dialog should be hidden");
+-        resolve();
+-      }
+-    }, { once: true });
+-  });
+-}
+-
+ function promiseCookiesCleared() {
+   return TestUtils.topicObserved("cookie-changed", (subj, data) => {
+     return data === "cleared";
+   });
+ }
+ 
+ async function loadServiceWorkerTestPage(url) {
+   let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, url);
+diff --git a/browser/components/preferences/siteDataSettings.js b/browser/components/preferences/siteDataSettings.js
+--- a/browser/components/preferences/siteDataSettings.js
++++ b/browser/components/preferences/siteDataSettings.js
+@@ -254,20 +254,17 @@ let gSiteDataSettings = {
+   },
+ 
+   onCommandSearch() {
+     this._buildSitesList(this._sites);
+     this._list.clearSelection();
+   },
+ 
+   onClickRemoveSelected() {
+-    let selected = this._list.selectedItem;
+-    if (selected) {
+-      this._removeSiteItems([selected]);
+-    }
++    this._removeSiteItems(this._list.selectedItems);
+     this._list.clearSelection();
+   },
+ 
+   onClickRemoveAll() {
+     let siteItems = this._list.getElementsByTagName("richlistitem");
+     if (siteItems.length > 0) {
+       this._removeSiteItems(siteItems);
+     }
+diff --git a/browser/components/preferences/siteDataSettings.xul b/browser/components/preferences/siteDataSettings.xul
+--- a/browser/components/preferences/siteDataSettings.xul
++++ b/browser/components/preferences/siteDataSettings.xul
+@@ -30,17 +30,17 @@
+     <separator class="thin"/>
+ 
+     <hbox id="searchBoxContainer">
+       <textbox id="searchBox" type="search" flex="1"
+         placeholder="&searchTextboxPlaceHolder;" accesskey="&searchTextboxPlaceHolder.accesskey;"/>
+     </hbox>
+     <separator class="thin"/>
+ 
+-    <richlistbox id="sitesList" orient="vertical" flex="1">
++    <richlistbox seltype="multiple" id="sitesList" orient="vertical" flex="1">
+       <listheader>
+         <treecol flex="4" width="50" label="&hostCol.label;" id="hostCol"/>
+         <treecol flex="1" width="50" label="&cookiesCol.label;" id="cookiesCol"/>
+         <!-- Sorted by usage so the user can quickly see which sites use the most data. -->
+         <treecol flex="2" width="50" label="&usageCol.label;" id="usageCol" data-isCurrentSortCol="true"/>
+         <treecol flex="2" width="50" label="&lastAccessedCol.label;" id="lastAccessedCol" />
+       </listheader>
+     </richlistbox>

+ 275 - 0
frg/mozilla-release/work-js/1444082-60a1.patch

@@ -0,0 +1,275 @@
+# HG changeset patch
+# User Gijs Kruitbosch <gijskruitbosch@gmail.com>
+# Date 1520519702 0
+# Node ID 733da5dbad32b2de372687527367596f1f7aa4a2
+# Parent  3bbc4cfa64e1b7bfe534e7352aa6e2dbb660c495
+Bug 1444082 - sync reader mode to github tip ( 8525c6af36d3badbe27c4672a6f2dd99ddb4097f ), r=johannh
+
+MozReview-Commit-ID: LZLFf9kyUR5
+
+diff --git a/toolkit/components/reader/JSDOMParser.js b/toolkit/components/reader/JSDOMParser.js
+--- a/toolkit/components/reader/JSDOMParser.js
++++ b/toolkit/components/reader/JSDOMParser.js
+@@ -555,17 +555,18 @@
+       delete this._textContent;
+     },
+     set textContent(newText) {
+       this._textContent = newText;
+       delete this._innerHTML;
+     },
+   };
+ 
+-  var Document = function () {
++  var Document = function (url) {
++    this.documentURI = url;
+     this.styleSheets = [];
+     this.childNodes = [];
+     this.children = [];
+   };
+ 
+   Document.prototype = {
+     __proto__: Node.prototype,
+ 
+@@ -595,16 +596,30 @@
+       return node;
+     },
+ 
+     createTextNode: function (text) {
+       var node = new Text();
+       node.textContent = text;
+       return node;
+     },
++
++    get baseURI() {
++      if (!this.hasOwnProperty("_baseURI")) {
++        this._baseURI = this.documentURI;
++        var baseElements = this.getElementsByTagName("base");
++        var href = baseElements[0] && baseElements[0].getAttribute("href");
++        if (href) {
++          try {
++            this._baseURI = (new URL(href, this._baseURI)).href;
++          } catch (ex) {/* Just fall back to documentURI */}
++        }
++      }
++      return this._baseURI;
++    },
+   };
+ 
+   var Element = function (tag) {
+     this.attributes = [];
+     this.childNodes = [];
+     this.children = [];
+     this.nextElementSibling = this.previousElementSibling = null;
+     this.localName = tag.toLowerCase();
+@@ -1113,19 +1128,19 @@
+       }
+ 
+       return node;
+     },
+ 
+     /**
+      * Parses an HTML string and returns a JS implementation of the Document.
+      */
+-    parse: function (html) {
++    parse: function (html, url) {
+       this.html = html;
+-      var doc = this.doc = new Document();
++      var doc = this.doc = new Document(url);
+       this.readChildren(doc);
+ 
+       // If this is an HTML document, remove root-level children except for the
+       // <html> node
+       if (doc.documentElement) {
+         for (var i = doc.childNodes.length; --i >= 0;) {
+           var child = doc.childNodes[i];
+           if (child !== doc.documentElement) {
+diff --git a/toolkit/components/reader/Readability.js b/toolkit/components/reader/Readability.js
+--- a/toolkit/components/reader/Readability.js
++++ b/toolkit/components/reader/Readability.js
+@@ -36,16 +36,17 @@
+ function Readability(uri, doc, options) {
+   options = options || {};
+ 
+   this._uri = uri;
+   this._doc = doc;
+   this._articleTitle = null;
+   this._articleByline = null;
+   this._articleDir = null;
++  this._attempts = [];
+ 
+   // Configurable options
+   this._debug = !!options.debug;
+   this._maxElemsToParse = options.maxElemsToParse || this.DEFAULT_MAX_ELEMS_TO_PARSE;
+   this._nbTopCandidates = options.nbTopCandidates || this.DEFAULT_N_TOP_CANDIDATES;
+   this._wordThreshold = options.wordThreshold || this.DEFAULT_WORD_THRESHOLD;
+   this._classesToPreserve = this.CLASSES_TO_PRESERVE.concat(options.classesToPreserve || []);
+ 
+@@ -270,44 +271,30 @@ Readability.prototype = {
+   /**
+    * Converts each <a> and <img> uri in the given element to an absolute URI,
+    * ignoring #ref URIs.
+    *
+    * @param Element
+    * @return void
+    */
+   _fixRelativeUris: function(articleContent) {
+-    var scheme = this._uri.scheme;
+-    var prePath = this._uri.prePath;
+-    var pathBase = this._uri.pathBase;
+-
++    var baseURI = this._doc.baseURI;
++    var documentURI = this._doc.documentURI;
+     function toAbsoluteURI(uri) {
+-      // If this is already an absolute URI, return it.
+-      if (/^[a-zA-Z][a-zA-Z0-9\+\-\.]*:/.test(uri))
++      // Leave hash links alone if the base URI matches the document URI:
++      if (baseURI == documentURI && uri.charAt(0) == "#") {
+         return uri;
+-
+-      // Scheme-rooted relative URI.
+-      if (uri.substr(0, 2) == "//")
+-        return scheme + "://" + uri.substr(2);
+-
+-      // Prepath-rooted relative URI.
+-      if (uri[0] == "/")
+-        return prePath + uri;
+-
+-      // Dotslash relative URI.
+-      if (uri.indexOf("./") === 0)
+-        return pathBase + uri.slice(2);
+-
+-      // Ignore hash URIs:
+-      if (uri[0] == "#")
+-        return uri;
+-
+-      // Standard relative URI; add entire path. pathBase already includes a
+-      // trailing "/".
+-      return pathBase + uri;
++      }
++      // Otherwise, resolve against base URI:
++      try {
++        return new URL(uri, baseURI).href;
++      } catch (ex) {
++        // Something went wrong, just return the original:
++      }
++      return uri;
+     }
+ 
+     var links = articleContent.getElementsByTagName("a");
+     this._forEachNode(links, function(link) {
+       var href = link.getAttribute("href");
+       if (href) {
+         // Replace links with javascript: URIs with text content, since
+         // they won't work after scripts have been removed from the page.
+@@ -530,16 +517,17 @@ Readability.prototype = {
+ 
+     // Clean out junk from the article content
+     this._cleanConditionally(articleContent, "form");
+     this._cleanConditionally(articleContent, "fieldset");
+     this._clean(articleContent, "object");
+     this._clean(articleContent, "embed");
+     this._clean(articleContent, "h1");
+     this._clean(articleContent, "footer");
++    this._clean(articleContent, "link");
+ 
+     // Clean out elements have "share" in their id/class combinations from final top candidates,
+     // which means we don't remove the top candidates even they have "share".
+     this._forEachNode(articleContent.children, function(topCandidate) {
+       this._cleanMatchedNodes(topCandidate, /share/);
+     });
+ 
+     // If there is only one h2 and its text content substantially equals article title,
+@@ -1084,34 +1072,55 @@ Readability.prototype = {
+           div.appendChild(children[0]);
+         }
+         articleContent.appendChild(div);
+       }
+ 
+       if (this._debug)
+         this.log("Article content after paging: " + articleContent.innerHTML);
+ 
++      var parseSuccessful = true;
++
+       // Now that we've gone through the full algorithm, check to see if
+       // we got any meaningful content. If we didn't, we may need to re-run
+       // grabArticle with different flags set. This gives us a higher likelihood of
+       // finding the content, and the sieve approach gives us a higher likelihood of
+       // finding the -right- content.
+-      if (this._getInnerText(articleContent, true).length < this._wordThreshold) {
++      var textLength = this._getInnerText(articleContent, true).length;
++      if (textLength < this._wordThreshold) {
++        parseSuccessful = false;
+         page.innerHTML = pageCacheHtml;
+ 
+         if (this._flagIsActive(this.FLAG_STRIP_UNLIKELYS)) {
+           this._removeFlag(this.FLAG_STRIP_UNLIKELYS);
++          this._attempts.push({articleContent: articleContent, textLength: textLength});
+         } else if (this._flagIsActive(this.FLAG_WEIGHT_CLASSES)) {
+           this._removeFlag(this.FLAG_WEIGHT_CLASSES);
++          this._attempts.push({articleContent: articleContent, textLength: textLength});
+         } else if (this._flagIsActive(this.FLAG_CLEAN_CONDITIONALLY)) {
+           this._removeFlag(this.FLAG_CLEAN_CONDITIONALLY);
++          this._attempts.push({articleContent: articleContent, textLength: textLength});
+         } else {
+-          return null;
++          this._attempts.push({articleContent: articleContent, textLength: textLength});
++          // No luck after removing flags, just return the longest text we found during the different loops
++          this._attempts.sort(function (a, b) {
++            return a.textLength < b.textLength;
++          });
++
++          // But first check if we actually have something
++          if (!this._attempts[0].textLength) {
++            return null;
++          }
++
++          articleContent = this._attempts[0].articleContent;
++          parseSuccessful = true;
+         }
+-      } else {
++      }
++
++      if (parseSuccessful) {
+         // Find out text direction from ancestors of final top candidate.
+         var ancestors = [parentOfTopCandidate, topCandidate].concat(this._getNodeAncestors(parentOfTopCandidate));
+         this._someNode(ancestors, function(ancestor) {
+           if (!ancestor.tagName)
+             return false;
+           var articleDir = ancestor.getAttribute("dir");
+           if (articleDir) {
+             this._articleDir = articleDir;
+diff --git a/toolkit/components/reader/ReaderWorker.js b/toolkit/components/reader/ReaderWorker.js
+--- a/toolkit/components/reader/ReaderWorker.js
++++ b/toolkit/components/reader/ReaderWorker.js
+@@ -42,12 +42,12 @@ var Agent = {
+    *
+    * @param {object} uri URI data for the document.
+    * @param {string} serializedDoc The serialized document.
+    * @param {object} options Options object to pass to Readability.
+    *
+    * @return {object} Article object returned from Readability.
+    */
+   parseDocument(uri, serializedDoc, options) {
+-    let doc = new JSDOMParser().parse(serializedDoc);
++    let doc = new JSDOMParser().parse(serializedDoc, uri.spec);
+     return new Readability(uri, doc, options).parse();
+   },
+ };
+diff --git a/toolkit/components/reader/test/readerModeNonArticle.html b/toolkit/components/reader/test/readerModeNonArticle.html
+--- a/toolkit/components/reader/test/readerModeNonArticle.html
++++ b/toolkit/components/reader/test/readerModeNonArticle.html
+@@ -1,14 +1,9 @@
+ <!DOCTYPE html>
+ <html>
+ <head>
+ <title>Non article title</title>
+ <meta name="description" content="This is the non-article description." />
+ </head>
+ <body>
+-<header>Site header</header>
+-<div>
+-<h1>Non article title</h1>
+-<p>Woot!</p>
+-</div>
+ </body>
+ </html>

+ 62 - 0
frg/mozilla-release/work-js/1444679-1-60a1.patch

@@ -0,0 +1,62 @@
+# HG changeset patch
+# User Emilio Cobos Alvarez <emilio@crisal.io>
+# Date 1520732206 -3600
+# Node ID c5e091682eb164417dea678551c968cb0f6da792
+# Parent  e293e585bf67d4c4bdf49837b6be02922fa28b32
+Bug 1444679: Remove unused nsXPCWrappedJSClass::IsWrappedJS. r=bholley
+
+MozReview-Commit-ID: KnNOjaEpa0g
+
+diff --git a/js/xpconnect/src/XPCWrappedJSClass.cpp b/js/xpconnect/src/XPCWrappedJSClass.cpp
+--- a/js/xpconnect/src/XPCWrappedJSClass.cpp
++++ b/js/xpconnect/src/XPCWrappedJSClass.cpp
+@@ -535,27 +535,16 @@ GetFunctionName(JSContext* cx, HandleObj
+     displayName.Append(']');
+     return displayName;
+ }
+ 
+ } // anonymous namespace
+ 
+ /***************************************************************************/
+ 
+-// static
+-bool
+-nsXPCWrappedJSClass::IsWrappedJS(nsISupports* aPtr)
+-{
+-    void* result;
+-    NS_PRECONDITION(aPtr, "null pointer");
+-    return aPtr &&
+-           NS_OK == aPtr->QueryInterface(NS_GET_IID(WrappedJSIdentity), &result) &&
+-           result == WrappedJSIdentity::GetSingleton();
+-}
+-
+ NS_IMETHODIMP
+ nsXPCWrappedJSClass::DelegatedQueryInterface(nsXPCWrappedJS* self,
+                                              REFNSIID aIID,
+                                              void** aInstancePtr)
+ {
+     if (aIID.Equals(NS_GET_IID(nsIXPConnectJSObjectHolder))) {
+         NS_ADDREF(self);
+         *aInstancePtr = (void*) static_cast<nsIXPConnectJSObjectHolder*>(self);
+diff --git a/js/xpconnect/src/xpcprivate.h b/js/xpconnect/src/xpcprivate.h
+--- a/js/xpconnect/src/xpcprivate.h
++++ b/js/xpconnect/src/xpcprivate.h
+@@ -1830,18 +1830,16 @@ public:
+                  REFNSIID aIID,
+                  bool allowNonScriptable = false);
+ 
+     REFNSIID GetIID() const {return mIID;}
+     XPCJSRuntime* GetRuntime() const {return mRuntime;}
+     nsIInterfaceInfo* GetInterfaceInfo() const {return mInfo;}
+     const char* GetInterfaceName();
+ 
+-    static bool IsWrappedJS(nsISupports* aPtr);
+-
+     NS_IMETHOD DelegatedQueryInterface(nsXPCWrappedJS* self, REFNSIID aIID,
+                                        void** aInstancePtr);
+ 
+     JSObject* GetRootJSObject(JSContext* cx, JSObject* aJSObj);
+ 
+     NS_IMETHOD CallMethod(nsXPCWrappedJS* wrapper, uint16_t methodIndex,
+                           const nsXPTMethodInfo* info,
+                           nsXPTCMiniVariant* params);

+ 88 - 0
frg/mozilla-release/work-js/1444679-2-60a1.patch

@@ -0,0 +1,88 @@
+# HG changeset patch
+# User Emilio Cobos Alvarez <emilio@crisal.io>
+# Date 1520734427 -3600
+# Node ID 2f58f0b2f939f93f2c7188b06edd925f16488d75
+# Parent  9e19eed3e4e5125e850b3c287cd55cdd129dc05d
+Bug 1444679: Remove WrappedJSIdentity. r=bholley
+
+Seemed to only be used for the method that was removed in the previous patch.
+
+MozReview-Commit-ID: 1cKpVBlxa7r
+
+diff --git a/js/xpconnect/src/XPCWrappedJSClass.cpp b/js/xpconnect/src/XPCWrappedJSClass.cpp
+--- a/js/xpconnect/src/XPCWrappedJSClass.cpp
++++ b/js/xpconnect/src/XPCWrappedJSClass.cpp
+@@ -411,48 +411,16 @@ NS_IMETHODIMP xpcProperty::GetName(nsASt
+ NS_IMETHODIMP xpcProperty::GetValue(nsIVariant * *aValue)
+ {
+     nsCOMPtr<nsIVariant> rval = mValue;
+     rval.forget(aValue);
+     return NS_OK;
+ }
+ 
+ /***************************************************************************/
+-// This 'WrappedJSIdentity' class and singleton allow us to figure out if
+-// any given nsISupports* is implemented by a WrappedJS object. This is done
+-// using a QueryInterface call on the interface pointer with our ID. If
+-// that call returns NS_OK and the pointer is to our singleton, then the
+-// interface must be implemented by a WrappedJS object. NOTE: the
+-// 'WrappedJSIdentity' object is not a real XPCOM object and should not be
+-// used for anything else (hence it is declared in this implementation file).
+-
+-// {5C5C3BB0-A9BA-11d2-BA64-00805F8A5DD7}
+-#define NS_IXPCONNECT_WRAPPED_JS_IDENTITY_CLASS_IID                           \
+-{ 0x5c5c3bb0, 0xa9ba, 0x11d2,                                                 \
+-  { 0xba, 0x64, 0x0, 0x80, 0x5f, 0x8a, 0x5d, 0xd7 } }
+-
+-class WrappedJSIdentity
+-{
+-    // no instance methods...
+-public:
+-    NS_DECLARE_STATIC_IID_ACCESSOR(NS_IXPCONNECT_WRAPPED_JS_IDENTITY_CLASS_IID)
+-
+-    static void* GetSingleton()
+-    {
+-        static WrappedJSIdentity* singleton = nullptr;
+-        if (!singleton)
+-            singleton = new WrappedJSIdentity();
+-        return (void*) singleton;
+-    }
+-};
+-
+-NS_DEFINE_STATIC_IID_ACCESSOR(WrappedJSIdentity,
+-                              NS_IXPCONNECT_WRAPPED_JS_IDENTITY_CLASS_IID)
+-
+-/***************************************************************************/
+ 
+ namespace {
+ 
+ class WrappedJSNamed final : public nsINamed
+ {
+     nsCString mName;
+ 
+     ~WrappedJSNamed() {}
+@@ -546,24 +514,16 @@ nsXPCWrappedJSClass::DelegatedQueryInter
+                                              void** aInstancePtr)
+ {
+     if (aIID.Equals(NS_GET_IID(nsIXPConnectJSObjectHolder))) {
+         NS_ADDREF(self);
+         *aInstancePtr = (void*) static_cast<nsIXPConnectJSObjectHolder*>(self);
+         return NS_OK;
+     }
+ 
+-    // Objects internal to xpconnect are the only objects that even know *how*
+-    // to ask for this iid. And none of them bother refcounting the thing.
+-    if (aIID.Equals(NS_GET_IID(WrappedJSIdentity))) {
+-        // asking to find out if this is a wrapper object
+-        *aInstancePtr = WrappedJSIdentity::GetSingleton();
+-        return NS_OK;
+-    }
+-
+     if (aIID.Equals(NS_GET_IID(nsIPropertyBag))) {
+         // We only want to expose one implementation from our aggregate.
+         nsXPCWrappedJS* root = self->GetRootWrapper();
+ 
+         if (!root->IsValid()) {
+             *aInstancePtr = nullptr;
+             return NS_NOINTERFACE;
+         }

+ 1677 - 0
frg/mozilla-release/work-js/1445188-61a1.patch

@@ -0,0 +1,1677 @@
+# HG changeset patch
+# User Johann Hofmann <jhofmann@mozilla.com>
+# Date 1520934098 -3600
+# Node ID 23192d47861cfc5f93a5a97d076b1354241a3774
+# Parent  6f67590504824c194c18f8a8c9e935923d93c85c
+Bug 1445188 - Move site data tests into their own subdirectory. r=jaws
+
+This commit is mostly about moving files but also contains a few test cleanups.
+
+MozReview-Commit-ID: AkDhFxlGdCT
+
+diff --git a/browser/components/preferences/in-content-new/tests/browser.ini b/browser/components/preferences/in-content-new/tests/browser.ini
+--- a/browser/components/preferences/in-content-new/tests/browser.ini
++++ b/browser/components/preferences/in-content-new/tests/browser.ini
+@@ -1,17 +1,12 @@
+ [DEFAULT]
+ support-files =
+   head.js
+   privacypane_tests_perwindow.js
+-  site_data_test.html
+-  service_worker_test.html
+-  service_worker_test.js
+-  offline/offline.html
+-  offline/manifest.appcache
+ 
+ [browser_applications_selection.js]
+ skip-if = os == 'linux' # Bug 1382057
+ [browser_advanced_update.js]
+ skip-if = !updater
+ [browser_basic_rebuild_fonts_test.js]
+ [browser_bug410900.js]
+ [browser_bug705422.js]
+@@ -34,17 +29,16 @@ skip-if = !updater
+ [browser_engines.js]
+ support-files =
+   browser_bug1184989_prevent_scrolling_when_preferences_flipped.xul
+ [browser_change_app_handler.js]
+ skip-if = os != "win" || (os == "win" && os_version == "6.1")
+ # This test tests the windows-specific app selection dialog, so can't run on non-Windows.
+ # Skip the test on Window 7, see the detail at Bug 1381706.
+ [browser_checkspelling.js]
+-[browser_clearSiteData.js]
+ [browser_connection.js]
+ [browser_connection_bug388287.js]
+ [browser_cookies_exceptions.js]
+ [browser_defaultbrowser_alwayscheck.js]
+ [browser_homepages_filter_aboutpreferences.js]
+ [browser_layersacceleration.js]
+ [browser_masterpassword.js]
+ [browser_notifications_do_not_disturb.js]
+@@ -61,18 +55,15 @@ skip-if = e10s
+ [browser_privacypane_3.js]
+ [browser_privacypane_4.js]
+ [browser_privacypane_5.js]
+ [browser_privacypane_8.js]
+ [browser_sanitizeOnShutdown_prefLocked.js]
+ [browser_searchsuggestions.js]
+ [browser_security-1.js]
+ [browser_security-2.js]
+-[browser_siteData.js]
+-[browser_siteData2.js]
+-[browser_siteData3.js]
+ [browser_site_login_exceptions.js]
+ [browser_permissions_dialog.js]
+ [browser_cookies_dialog.js]
+ [browser_subdialogs.js]
+ support-files =
+   subdialog.xul
+   subdialog2.xul
+diff --git a/browser/components/preferences/in-content-new/tests/head.js b/browser/components/preferences/in-content-new/tests/head.js
+--- a/browser/components/preferences/in-content-new/tests/head.js
++++ b/browser/components/preferences/in-content-new/tests/head.js
+@@ -1,15 +1,13 @@
+ /* Any copyright is dedicated to the Public Domain.
+  * http://creativecommons.org/publicdomain/zero/1.0/ */
+ 
+ ChromeUtils.import("resource://gre/modules/Promise.jsm");
+ 
+-XPCOMUtils.defineLazyServiceGetter(this, "serviceWorkerManager", "@mozilla.org/serviceworkers/manager;1", "nsIServiceWorkerManager");
+-
+ // Tests within /browser/components/preferences/in-content-new/tests/
+ // test the "new" preferences organization, after it was reorganized.
+ // Thus, all of these tests should set the "oldOrganization" to false
+ // before running.
+ Services.prefs.setBoolPref("browser.preferences.useOldOrganization", false);
+ registerCleanupFunction(function() {
+   Services.prefs.clearUserPref("browser.preferences.useOldOrganization");
+ });
+@@ -165,56 +163,16 @@ function waitForCondition(aConditionFn, 
+     function tryAgain() {
+       setTimeout(tryNow, aCheckInterval);
+     }
+     let tries = 0;
+     tryAgain();
+   });
+ }
+ 
+-function promiseWindowDialogOpen(buttonAction, url) {
+-  return BrowserTestUtils.promiseAlertDialogOpen(buttonAction, url);
+-}
+-
+-function promiseAlertDialogOpen(buttonAction) {
+-  return BrowserTestUtils.promiseAlertDialogOpen(buttonAction);
+-}
+-
+-function promiseSiteDataManagerSitesUpdated() {
+-  return TestUtils.topicObserved("sitedatamanager:sites-updated", () => true);
+-}
+-
+-function openSiteDataSettingsDialog() {
+-  let doc = gBrowser.selectedBrowser.contentDocument;
+-  let settingsBtn = doc.getElementById("siteDataSettings");
+-  let dialogOverlay = content.gSubDialog._preloadDialog._overlay;
+-  let dialogLoadPromise = promiseLoadSubDialog("chrome://browser/content/preferences/siteDataSettings.xul");
+-  let dialogInitPromise = TestUtils.topicObserved("sitedata-settings-init", () => true);
+-  let fullyLoadPromise = Promise.all([ dialogLoadPromise, dialogInitPromise ]).then(() => {
+-    is(dialogOverlay.style.visibility, "visible", "The Settings dialog should be visible");
+-  });
+-  settingsBtn.doCommand();
+-  return fullyLoadPromise;
+-}
+-
+-function assertSitesListed(doc, hosts) {
+-  let frameDoc = content.gSubDialog._topDialog._frame.contentDocument;
+-  let removeBtn = frameDoc.getElementById("removeSelected");
+-  let removeAllBtn = frameDoc.getElementById("removeAll");
+-  let sitesList = frameDoc.getElementById("sitesList");
+-  let totalSitesNumber = sitesList.getElementsByTagName("richlistitem").length;
+-  is(totalSitesNumber, hosts.length, "Should list the right sites number");
+-  hosts.forEach(host => {
+-    let site = sitesList.querySelector(`richlistitem[host="${host}"]`);
+-    ok(site, `Should list the site of ${host}`);
+-  });
+-  is(removeBtn.disabled, true, "Should disable the removeSelected button");
+-  is(removeAllBtn.disabled, false, "Should enable the removeAllBtn button");
+-}
+-
+ async function evaluateSearchResults(keyword, searchReults) {
+   searchReults = Array.isArray(searchReults) ? searchReults : [searchReults];
+   searchReults.push("header-searchResults");
+ 
+   let searchInput = gBrowser.contentDocument.getElementById("searchInput");
+   searchInput.focus();
+   let searchCompletedPromise = BrowserTestUtils.waitForEvent(
+       gBrowser.contentWindow, "PreferencesSearchCompleted", evt => evt.detail == keyword);
+@@ -227,116 +185,8 @@ async function evaluateSearchResults(key
+     if (searchReults.includes(child.id)) {
+       is_element_visible(child, "Should be in search results");
+     } else if (child.id) {
+       is_element_hidden(child, "Should not be in search results");
+     }
+   }
+ }
+ 
+-const mockSiteDataManager = {
+-
+-  _SiteDataManager: null,
+-  _originalQMS: null,
+-  _originalRemoveQuotaUsage: null,
+-
+-  getUsage(onUsageResult) {
+-    let result = this.fakeSites.map(site => ({
+-      origin: site.principal.origin,
+-      usage: site.usage,
+-      persisted: site.persisted,
+-      lastAccessed: site.lastAccessed,
+-    }));
+-    onUsageResult({ result, resultCode: Components.results.NS_OK });
+-  },
+-
+-  _removeQuotaUsage(site) {
+-    var target = site.principals[0].URI.host;
+-    this.fakeSites = this.fakeSites.filter(fakeSite => {
+-      return fakeSite.principal.URI.host != target;
+-    });
+-  },
+-
+-  register(SiteDataManager, fakeSites) {
+-    this._SiteDataManager = SiteDataManager;
+-    this._originalQMS = this._SiteDataManager._qms;
+-    this._SiteDataManager._qms = this;
+-    this._originalRemoveQuotaUsage = this._SiteDataManager._removeQuotaUsage;
+-    this._SiteDataManager._removeQuotaUsage = this._removeQuotaUsage.bind(this);
+-    // Add some fake data.
+-    this.fakeSites = fakeSites;
+-    for (let site of fakeSites) {
+-      if (!site.principal) {
+-        site.principal = Services.scriptSecurityManager
+-          .createCodebasePrincipalFromOrigin(site.origin);
+-      }
+-
+-      let uri = site.principal.URI;
+-      try {
+-        site.baseDomain = Services.eTLD.getBaseDomainFromHost(uri.host);
+-      } catch (e) {
+-        site.baseDomain = uri.host;
+-      }
+-
+-      // Add some cookies if needed.
+-      for (let i = 0; i < (site.cookies || 0); i++) {
+-        Services.cookies.add(uri.host, uri.pathQueryRef, Cu.now(), i,
+-          false, false, false, Date.now() + 1000 * 60 * 60);
+-      }
+-    }
+-  },
+-
+-  async unregister() {
+-    await this._SiteDataManager.removeAll();
+-    this.fakeSites = null;
+-    this._SiteDataManager._qms = this._originalQMS;
+-    this._SiteDataManager._removeQuotaUsage = this._originalRemoveQuotaUsage;
+-  }
+-};
+-
+-function getQuotaUsage(origin) {
+-  return new Promise(resolve => {
+-    let uri = NetUtil.newURI(origin);
+-    let principal = Services.scriptSecurityManager.createCodebasePrincipal(uri, {});
+-    Services.qms.getUsageForPrincipal(principal, request => resolve(request.result.usage));
+-  });
+-}
+-
+-function promiseCookiesCleared() {
+-  return TestUtils.topicObserved("cookie-changed", (subj, data) => {
+-    return data === "cleared";
+-  });
+-}
+-
+-async function loadServiceWorkerTestPage(url) {
+-  let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, url);
+-  await BrowserTestUtils.waitForCondition(() => {
+-    return ContentTask.spawn(tab.linkedBrowser, {}, () =>
+-      content.document.body.getAttribute("data-test-service-worker-registered") === "true");
+-  }, `Fail to load service worker test ${url}`);
+-  await BrowserTestUtils.removeTab(tab);
+-}
+-
+-function promiseServiceWorkersCleared() {
+-  return BrowserTestUtils.waitForCondition(() => {
+-    let serviceWorkers = serviceWorkerManager.getAllRegistrations();
+-    if (serviceWorkers.length == 0) {
+-      ok(true, "Cleared all service workers");
+-      return true;
+-    }
+-    return false;
+-  }, "Should clear all service workers");
+-}
+-
+-function promiseServiceWorkerRegisteredFor(url) {
+-  return BrowserTestUtils.waitForCondition(() => {
+-    try {
+-      let principal = Services.scriptSecurityManager.createCodebasePrincipalFromOrigin(url);
+-      let sw = serviceWorkerManager.getRegistrationByPrincipal(principal, principal.URI.spec);
+-      if (sw) {
+-        ok(true, `Found the service worker registered for ${url}`);
+-        return true;
+-      }
+-    } catch (e) {}
+-    return false;
+-  }, `Should register service worker for ${url}`);
+-}
+-
+diff --git a/browser/components/preferences/in-content-new/tests/siteData/browser.ini b/browser/components/preferences/in-content-new/tests/siteData/browser.ini
+new file mode 100644
+--- /dev/null
++++ b/browser/components/preferences/in-content-new/tests/siteData/browser.ini
+@@ -0,0 +1,14 @@
++[DEFAULT]
++support-files =
++  head.js
++  site_data_test.html
++  service_worker_test.html
++  service_worker_test.js
++  offline/offline.html
++  offline/manifest.appcache
++
++[browser_clearSiteData.js]
++[browser_siteData.js]
++skip-if = (os == 'linux' && debug) # Bug 1439332
++[browser_siteData2.js]
++[browser_siteData3.js]
+diff --git a/browser/components/preferences/in-content-new/tests/browser_clearSiteData.js b/browser/components/preferences/in-content-new/tests/siteData/browser_clearSiteData.js
+rename from browser/components/preferences/in-content-new/tests/browser_clearSiteData.js
+rename to browser/components/preferences/in-content-new/tests/siteData/browser_clearSiteData.js
+--- a/browser/components/preferences/in-content-new/tests/browser_clearSiteData.js
++++ b/browser/components/preferences/in-content-new/tests/siteData/browser_clearSiteData.js
+@@ -1,26 +1,15 @@
+ /* Any copyright is dedicated to the Public Domain.
+  * http://creativecommons.org/publicdomain/zero/1.0/ */
+ 
+ "use strict";
+ 
+ ChromeUtils.import("resource:///modules/SitePermissions.jsm");
+ 
+-const { SiteDataManager } = ChromeUtils.import("resource:///modules/SiteDataManager.jsm", {});
+-const { DownloadUtils } = ChromeUtils.import("resource://gre/modules/DownloadUtils.jsm", {});
+-
+-const TEST_QUOTA_USAGE_HOST = "example.com";
+-const TEST_QUOTA_USAGE_ORIGIN = "https://" + TEST_QUOTA_USAGE_HOST;
+-const TEST_QUOTA_USAGE_URL = TEST_QUOTA_USAGE_ORIGIN + "/browser/browser/components/preferences/in-content-new/tests/site_data_test.html";
+-const TEST_OFFLINE_HOST = "example.org";
+-const TEST_OFFLINE_ORIGIN = "https://" + TEST_OFFLINE_HOST;
+-const TEST_OFFLINE_URL = TEST_OFFLINE_ORIGIN + "/browser/browser/components/preferences/in-content-new/tests/offline/offline.html";
+-const TEST_SERVICE_WORKER_URL = TEST_OFFLINE_ORIGIN + "/browser/browser/components/preferences/in-content-new/tests/service_worker_test.html";
+-
+ async function testClearData(clearSiteData, clearCache) {
+   let quotaURI = Services.io.newURI(TEST_QUOTA_USAGE_ORIGIN);
+   SitePermissions.set(quotaURI, "persistent-storage", SitePermissions.ALLOW);
+ 
+   // Open a test site which saves into appcache.
+   await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_OFFLINE_URL);
+   await BrowserTestUtils.removeTab(gBrowser.selectedTab);
+ 
+@@ -84,17 +73,17 @@ async function testClearData(clearSiteDa
+   clearCacheCheckbox.checked = clearCache;
+ 
+   // Some additional promises/assertions to wait for
+   // when deleting site data.
+   let acceptPromise;
+   let updatePromise;
+   let cookiesClearedPromise;
+   if (clearSiteData) {
+-    acceptPromise = promiseAlertDialogOpen("accept");
++    acceptPromise = BrowserTestUtils.promiseAlertDialogOpen("accept");
+     updatePromise = promiseSiteDataManagerSitesUpdated();
+     cookiesClearedPromise = promiseCookiesCleared();
+   }
+ 
+   let dialogClosed = BrowserTestUtils.waitForEvent(dialogWin, "unload");
+ 
+   let clearButton = dialogWin.document.getElementById("clearButton");
+   if (!clearSiteData && !clearCache) {
+diff --git a/browser/components/preferences/in-content-new/tests/browser_siteData.js b/browser/components/preferences/in-content-new/tests/siteData/browser_siteData.js
+rename from browser/components/preferences/in-content-new/tests/browser_siteData.js
+rename to browser/components/preferences/in-content-new/tests/siteData/browser_siteData.js
+--- a/browser/components/preferences/in-content-new/tests/browser_siteData.js
++++ b/browser/components/preferences/in-content-new/tests/siteData/browser_siteData.js
+@@ -1,29 +1,15 @@
+ /* Any copyright is dedicated to the Public Domain.
+  * http://creativecommons.org/publicdomain/zero/1.0/ */
+ 
+ "use strict";
+ 
+-const TEST_QUOTA_USAGE_HOST = "example.com";
+-const TEST_QUOTA_USAGE_ORIGIN = "https://" + TEST_QUOTA_USAGE_HOST;
+-const TEST_QUOTA_USAGE_URL = TEST_QUOTA_USAGE_ORIGIN + "/browser/browser/components/preferences/in-content-new/tests/site_data_test.html";
+-const TEST_OFFLINE_HOST = "example.org";
+-const TEST_OFFLINE_ORIGIN = "https://" + TEST_OFFLINE_HOST;
+-const TEST_OFFLINE_URL = TEST_OFFLINE_ORIGIN + "/browser/browser/components/preferences/in-content-new/tests/offline/offline.html";
+-const TEST_SERVICE_WORKER_URL = TEST_OFFLINE_ORIGIN + "/browser/browser/components/preferences/in-content-new/tests/service_worker_test.html";
+-const REMOVE_DIALOG_URL = "chrome://browser/content/preferences/siteDataRemoveSelected.xul";
+-
+-const { NetUtil } = ChromeUtils.import("resource://gre/modules/NetUtil.jsm", {});
+-const { DownloadUtils } = ChromeUtils.import("resource://gre/modules/DownloadUtils.jsm", {});
+-const { SiteDataManager } = ChromeUtils.import("resource:///modules/SiteDataManager.jsm", {});
+-const { OfflineAppCacheHelper } = ChromeUtils.import("resource:///modules/offlineAppCache.jsm", {});
+-
+ function getPersistentStoragePermStatus(origin) {
+-  let uri = NetUtil.newURI(origin);
++  let uri = Services.io.newURI(origin);
+   let principal = Services.scriptSecurityManager.createCodebasePrincipal(uri, {});
+   return Services.perms.testExactPermissionFromPrincipal(principal, "persistent-storage");
+ }
+ 
+ // Test listing site using quota usage or site using appcache
+ // This is currently disabled because of bug 1414751.
+ add_task(async function() {
+   // Open a test site which would save into appcache
+@@ -136,17 +122,17 @@ add_task(async function() {
+ add_task(async function() {
+   // Register a test service worker
+   await loadServiceWorkerTestPage(TEST_SERVICE_WORKER_URL);
+   await openPreferencesViaOpenPreferencesAPI("privacy", { leaveOpen: true });
+   // Test the initial states
+   await promiseServiceWorkerRegisteredFor(TEST_SERVICE_WORKER_URL);
+   // Open the Site Data Settings panel and remove the site
+   await openSiteDataSettingsDialog();
+-  let acceptRemovePromise = promiseAlertDialogOpen("accept");
++  let acceptRemovePromise = BrowserTestUtils.promiseAlertDialogOpen("accept");
+   let updatePromise = promiseSiteDataManagerSitesUpdated();
+   ContentTask.spawn(gBrowser.selectedBrowser, { TEST_OFFLINE_HOST }, args => {
+     let host = args.TEST_OFFLINE_HOST;
+     let frameDoc = content.gSubDialog._topDialog._frame.contentDocument;
+     let sitesList = frameDoc.getElementById("sitesList");
+     let site = sitesList.querySelector(`richlistitem[host="${host}"]`);
+     if (site) {
+       let removeBtn = frameDoc.getElementById("removeSelected");
+@@ -196,17 +182,17 @@ add_task(async function() {
+ 
+   let creationDate1 = formatter.format(new Date(cookie1.lastAccessed / 1000));
+   let creationDate2 = formatter.format(new Date(cookie2.lastAccessed / 1000));
+ 
+   await openPreferencesViaOpenPreferencesAPI("privacy", { leaveOpen: true });
+ 
+   // Open the site data manager and remove one site.
+   await openSiteDataSettingsDialog();
+-  let removeDialogOpenPromise = promiseWindowDialogOpen("accept", REMOVE_DIALOG_URL);
++  let removeDialogOpenPromise = BrowserTestUtils.promiseAlertDialogOpen("accept", REMOVE_DIALOG_URL);
+   await ContentTask.spawn(gBrowser.selectedBrowser, {creationDate1, creationDate2}, function(args) {
+     let frameDoc = content.gSubDialog._topDialog._frame.contentDocument;
+ 
+     let siteItems = frameDoc.getElementsByTagName("richlistitem");
+     is(siteItems.length, 2, "Should list two sites with cookies");
+     let sitesList = frameDoc.getElementById("sitesList");
+     let site1 = sitesList.querySelector(`richlistitem[host="example.com"]`);
+     let site2 = sitesList.querySelector(`richlistitem[host="example.org"]`);
+@@ -231,17 +217,17 @@ add_task(async function() {
+   });
+   await removeDialogOpenPromise;
+ 
+   await TestUtils.waitForCondition(() => Services.cookies.countCookiesFromHost(uri2.host) == 0, "Cookies from the first host should be cleared");
+   is(Services.cookies.countCookiesFromHost(uri.host), 2, "Cookies from the second host should not be cleared");
+ 
+   // Open the site data manager and remove another site.
+   await openSiteDataSettingsDialog();
+-  let acceptRemovePromise = promiseAlertDialogOpen("accept");
++  let acceptRemovePromise = BrowserTestUtils.promiseAlertDialogOpen("accept");
+   await ContentTask.spawn(gBrowser.selectedBrowser, {creationDate1}, function(args) {
+     let frameDoc = content.gSubDialog._topDialog._frame.contentDocument;
+ 
+     let siteItems = frameDoc.getElementsByTagName("richlistitem");
+     is(siteItems.length, 1, "Should list one site with cookies");
+     let sitesList = frameDoc.getElementById("sitesList");
+     let site1 = sitesList.querySelector(`richlistitem[host="example.com"]`);
+ 
+diff --git a/browser/components/preferences/in-content-new/tests/browser_siteData2.js b/browser/components/preferences/in-content-new/tests/siteData/browser_siteData2.js
+rename from browser/components/preferences/in-content-new/tests/browser_siteData2.js
+rename to browser/components/preferences/in-content-new/tests/siteData/browser_siteData2.js
+--- a/browser/components/preferences/in-content-new/tests/browser_siteData2.js
++++ b/browser/components/preferences/in-content-new/tests/siteData/browser_siteData2.js
+@@ -1,12 +1,9 @@
+ "use strict";
+-const { SiteDataManager } = ChromeUtils.import("resource:///modules/SiteDataManager.jsm", {});
+-
+-const REMOVE_DIALOG_URL = "chrome://browser/content/preferences/siteDataRemoveSelected.xul";
+ 
+ function promiseSettingsDialogClose() {
+   return new Promise(resolve => {
+     let win = gBrowser.selectedBrowser.contentWindow;
+     let dialogOverlay = win.gSubDialog._topDialog._overlay;
+     let dialogWin = win.gSubDialog._topDialog._frame.contentWindow;
+     dialogWin.addEventListener("unload", function unload() {
+       if (dialogWin.document.documentURI === "chrome://browser/content/preferences/siteDataSettings.xul") {
+@@ -76,32 +73,32 @@ add_task(async function() {
+   removeAllSitesOneByOne();
+   assertAllSitesNotListed(win);
+   cancelBtn.doCommand();
+   await settingsDialogClosePromise;
+   await openSiteDataSettingsDialog();
+   assertSitesListed(doc, fakeHosts);
+ 
+   // Test the "Save Changes" button but cancelling save
+-  let cancelPromise = promiseAlertDialogOpen("cancel");
++  let cancelPromise = BrowserTestUtils.promiseAlertDialogOpen("cancel");
+   settingsDialogClosePromise = promiseSettingsDialogClose();
+   frameDoc = win.gSubDialog._topDialog._frame.contentDocument;
+   saveBtn = frameDoc.getElementById("save");
+   cancelBtn = frameDoc.getElementById("cancel");
+   removeAllSitesOneByOne();
+   assertAllSitesNotListed(win);
+   saveBtn.doCommand();
+   await cancelPromise;
+   cancelBtn.doCommand();
+   await settingsDialogClosePromise;
+   await openSiteDataSettingsDialog();
+   assertSitesListed(doc, fakeHosts);
+ 
+   // Test the "Save Changes" button and accepting save
+-  let acceptPromise = promiseAlertDialogOpen("accept");
++  let acceptPromise = BrowserTestUtils.promiseAlertDialogOpen("accept");
+   settingsDialogClosePromise = promiseSettingsDialogClose();
+   updatePromise = promiseSiteDataManagerSitesUpdated();
+   frameDoc = win.gSubDialog._topDialog._frame.contentDocument;
+   saveBtn = frameDoc.getElementById("save");
+   removeAllSitesOneByOne();
+   assertAllSitesNotListed(win);
+   saveBtn.doCommand();
+   await acceptPromise;
+@@ -189,32 +186,32 @@ add_task(async function() {
+   removeSelectedSite(fakeHosts.slice(0, 2));
+   assertSitesListed(doc, fakeHosts.slice(2));
+   cancelBtn.doCommand();
+   await settingsDialogClosePromise;
+   await openSiteDataSettingsDialog();
+   assertSitesListed(doc, fakeHosts);
+ 
+   // Test the "Save Changes" button but canceling save
+-  removeDialogOpenPromise = promiseWindowDialogOpen("cancel", REMOVE_DIALOG_URL);
++  removeDialogOpenPromise = BrowserTestUtils.promiseAlertDialogOpen("cancel", REMOVE_DIALOG_URL);
+   settingsDialogClosePromise = promiseSettingsDialogClose();
+   frameDoc = win.gSubDialog._topDialog._frame.contentDocument;
+   saveBtn = frameDoc.getElementById("save");
+   cancelBtn = frameDoc.getElementById("cancel");
+   removeSelectedSite(fakeHosts.slice(0, 2));
+   assertSitesListed(doc, fakeHosts.slice(2));
+   saveBtn.doCommand();
+   await removeDialogOpenPromise;
+   cancelBtn.doCommand();
+   await settingsDialogClosePromise;
+   await openSiteDataSettingsDialog();
+   assertSitesListed(doc, fakeHosts);
+ 
+   // Test the "Save Changes" button and accepting save
+-  removeDialogOpenPromise = promiseWindowDialogOpen("accept", REMOVE_DIALOG_URL);
++  removeDialogOpenPromise = BrowserTestUtils.promiseAlertDialogOpen("accept", REMOVE_DIALOG_URL);
+   settingsDialogClosePromise = promiseSettingsDialogClose();
+   frameDoc = win.gSubDialog._topDialog._frame.contentDocument;
+   saveBtn = frameDoc.getElementById("save");
+   removeSelectedSite(fakeHosts.slice(0, 2));
+   assertSitesListed(doc, fakeHosts.slice(2));
+   saveBtn.doCommand();
+   await removeDialogOpenPromise;
+   await settingsDialogClosePromise;
+@@ -279,17 +276,17 @@ add_task(async function() {
+   let frameDoc = win.gSubDialog._topDialog._frame.contentDocument;
+   let searchBox = frameDoc.getElementById("searchBox");
+   searchBox.value = "xyz";
+   searchBox.doCommand();
+   assertSitesListed(doc, fakeHosts.filter(host => host.includes("xyz")));
+ 
+   // Test only removing all visible sites listed
+   updatePromise = promiseSiteDataManagerSitesUpdated();
+-  let acceptRemovePromise = promiseWindowDialogOpen("accept", REMOVE_DIALOG_URL);
++  let acceptRemovePromise = BrowserTestUtils.promiseAlertDialogOpen("accept", REMOVE_DIALOG_URL);
+   let settingsDialogClosePromise = promiseSettingsDialogClose();
+   let removeAllBtn = frameDoc.getElementById("removeAll");
+   let saveBtn = frameDoc.getElementById("save");
+   removeAllBtn.doCommand();
+   saveBtn.doCommand();
+   await acceptRemovePromise;
+   await settingsDialogClosePromise;
+   await updatePromise;
+@@ -336,17 +333,17 @@ add_task(async function() {
+                        .createCodebasePrincipalFromOrigin("http://email.bar.com"),
+     persisted: false
+   });
+ 
+   // Test clearing all site data dynamically
+   let win = gBrowser.selectedBrowser.contentWindow;
+   let frameDoc = win.gSubDialog._topDialog._frame.contentDocument;
+   updatePromise = promiseSiteDataManagerSitesUpdated();
+-  let acceptRemovePromise = promiseAlertDialogOpen("accept");
++  let acceptRemovePromise = BrowserTestUtils.promiseAlertDialogOpen("accept");
+   let settingsDialogClosePromise = promiseSettingsDialogClose();
+   let removeAllBtn = frameDoc.getElementById("removeAll");
+   let saveBtn = frameDoc.getElementById("save");
+   removeAllBtn.doCommand();
+   saveBtn.doCommand();
+   await acceptRemovePromise;
+   await settingsDialogClosePromise;
+   await updatePromise;
+diff --git a/browser/components/preferences/in-content-new/tests/browser_siteData3.js b/browser/components/preferences/in-content-new/tests/siteData/browser_siteData3.js
+rename from browser/components/preferences/in-content-new/tests/browser_siteData3.js
+rename to browser/components/preferences/in-content-new/tests/siteData/browser_siteData3.js
+--- a/browser/components/preferences/in-content-new/tests/browser_siteData3.js
++++ b/browser/components/preferences/in-content-new/tests/siteData/browser_siteData3.js
+@@ -1,15 +1,12 @@
+ "use strict";
+-const { SiteDataManager } = Cu.import("resource:///modules/SiteDataManager.jsm", {});
+-const { DownloadUtils } = Cu.import("resource://gre/modules/DownloadUtils.jsm", {});
+ 
+ // Test not displaying sites which store 0 byte and don't have persistent storage.
+ add_task(async function() {
+-  await SpecialPowers.pushPrefEnv({set: [["browser.storageManager.enabled", true]]});
+   mockSiteDataManager.register(SiteDataManager, [
+     {
+       usage: 0,
+       origin: "https://account.xyz.com",
+       persisted: true
+     },
+     {
+       usage: 0,
+@@ -43,17 +40,16 @@ add_task(async function() {
+   assertSitesListed(doc, fakeHosts.filter(host => host != "shopping.xyz.com"));
+ 
+   await mockSiteDataManager.unregister();
+   await BrowserTestUtils.removeTab(gBrowser.selectedTab);
+ });
+ 
+ // Test grouping and listing sites across scheme, port and origin attributes by host
+ add_task(async function() {
+-  await SpecialPowers.pushPrefEnv({set: [["browser.storageManager.enabled", true]]});
+   const quotaUsage = 1024;
+   mockSiteDataManager.register(SiteDataManager, [
+     {
+       usage: quotaUsage,
+       origin: "https://account.xyz.com^userContextId=1",
+       cookies: 2,
+       persisted: true
+     },
+@@ -101,17 +97,16 @@ add_task(async function() {
+   is(columns[2].value, expected, "Should sum up usages across scheme, port, origin attributes and persistent status");
+ 
+   await mockSiteDataManager.unregister();
+   await BrowserTestUtils.removeTab(gBrowser.selectedTab);
+ });
+ 
+ // Test sorting
+ add_task(async function() {
+-  await SpecialPowers.pushPrefEnv({set: [["browser.storageManager.enabled", true]]});
+   mockSiteDataManager.register(SiteDataManager, [
+     {
+       usage: 1024,
+       origin: "https://account.xyz.com",
+       cookies: 6,
+       persisted: true,
+     },
+     {
+diff --git a/browser/components/preferences/in-content-new/tests/head.js b/browser/components/preferences/in-content-new/tests/siteData/head.js
+copy from browser/components/preferences/in-content-new/tests/head.js
+copy to browser/components/preferences/in-content-new/tests/siteData/head.js
+--- a/browser/components/preferences/in-content-new/tests/head.js
++++ b/browser/components/preferences/in-content-new/tests/siteData/head.js
+@@ -1,25 +1,41 @@
+ /* Any copyright is dedicated to the Public Domain.
+  * http://creativecommons.org/publicdomain/zero/1.0/ */
+ 
+-ChromeUtils.import("resource://gre/modules/Promise.jsm");
++"use strict";
++
++const TEST_QUOTA_USAGE_HOST = "example.com";
++const TEST_QUOTA_USAGE_ORIGIN = "https://" + TEST_QUOTA_USAGE_HOST;
++const TEST_QUOTA_USAGE_URL = getRootDirectory(gTestPath).replace("chrome://mochitests/content", TEST_QUOTA_USAGE_ORIGIN) + "/site_data_test.html";
++const TEST_OFFLINE_HOST = "example.org";
++const TEST_OFFLINE_ORIGIN = "https://" + TEST_OFFLINE_HOST;
++const TEST_OFFLINE_URL = getRootDirectory(gTestPath).replace("chrome://mochitests/content", TEST_OFFLINE_ORIGIN) + "/offline/offline.html";
++const TEST_SERVICE_WORKER_URL = getRootDirectory(gTestPath).replace("chrome://mochitests/content", TEST_OFFLINE_ORIGIN) + "/service_worker_test.html";
++
++const REMOVE_DIALOG_URL = "chrome://browser/content/preferences/siteDataRemoveSelected.xul";
++
++const { DownloadUtils } = ChromeUtils.import("resource://gre/modules/DownloadUtils.jsm", {});
++const { SiteDataManager } = ChromeUtils.import("resource:///modules/SiteDataManager.jsm", {});
++const { OfflineAppCacheHelper } = ChromeUtils.import("resource:///modules/offlineAppCache.jsm", {});
+ 
+ XPCOMUtils.defineLazyServiceGetter(this, "serviceWorkerManager", "@mozilla.org/serviceworkers/manager;1", "nsIServiceWorkerManager");
+ 
+ // Tests within /browser/components/preferences/in-content-new/tests/
+ // test the "new" preferences organization, after it was reorganized.
+ // Thus, all of these tests should set the "oldOrganization" to false
+ // before running.
+ Services.prefs.setBoolPref("browser.preferences.useOldOrganization", false);
+ registerCleanupFunction(function() {
+   Services.prefs.clearUserPref("browser.preferences.useOldOrganization");
+ });
+ 
+-const kDefaultWait = 2000;
++function promiseSiteDataManagerSitesUpdated() {
++  return TestUtils.topicObserved("sitedatamanager:sites-updated", () => true);
++}
+ 
+ function is_hidden(aElement) {
+   var style = aElement.ownerGlobal.getComputedStyle(aElement);
+   if (style.display == "none")
+     return true;
+   if (style.visibility != "visible")
+     return true;
+ 
+@@ -35,30 +51,16 @@ function is_element_visible(aElement, aM
+   ok(!is_hidden(aElement), aMsg);
+ }
+ 
+ function is_element_hidden(aElement, aMsg) {
+   isnot(aElement, null, "Element should not be null, when checking visibility");
+   ok(is_hidden(aElement), aMsg);
+ }
+ 
+-function open_preferences(aCallback) {
+-  gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser, "about:preferences");
+-  let newTabBrowser = gBrowser.getBrowserForTab(gBrowser.selectedTab);
+-  newTabBrowser.addEventListener("Initialized", function() {
+-    aCallback(gBrowser.contentWindow);
+-  }, {capture: true, once: true});
+-}
+-
+-function openAndLoadSubDialog(aURL, aFeatures = null, aParams = null, aClosingCallback = null) {
+-  let promise = promiseLoadSubDialog(aURL);
+-  content.gSubDialog.open(aURL, aFeatures, aParams, aClosingCallback);
+-  return promise;
+-}
+-
+ function promiseLoadSubDialog(aURL) {
+   return new Promise((resolve, reject) => {
+     content.gSubDialog._dialogStack.addEventListener("dialogopen", function dialogopen(aEvent) {
+       if (aEvent.detail.dialog._frame.contentWindow.location == "about:blank")
+         return;
+       content.gSubDialog._dialogStack.removeEventListener("dialogopen", dialogopen);
+ 
+       is(aEvent.detail.dialog._frame.contentWindow.location.toString(), aURL,
+@@ -78,63 +80,16 @@ function promiseLoadSubDialog(aURL) {
+       }
+       is(expectedStyleSheetURLs.length, 0, "All expectedStyleSheetURLs should have been found");
+ 
+       resolve(aEvent.detail.dialog._frame.contentWindow);
+     });
+   });
+ }
+ 
+-/**
+- * Waits a specified number of miliseconds for a specified event to be
+- * fired on a specified element.
+- *
+- * Usage:
+- *    let receivedEvent = waitForEvent(element, "eventName");
+- *    // Do some processing here that will cause the event to be fired
+- *    // ...
+- *    // Now yield until the Promise is fulfilled
+- *    yield receivedEvent;
+- *    if (receivedEvent && !(receivedEvent instanceof Error)) {
+- *      receivedEvent.msg == "eventName";
+- *      // ...
+- *    }
+- *
+- * @param aSubject the element that should receive the event
+- * @param aEventName the event to wait for
+- * @param aTimeoutMs the number of miliseconds to wait before giving up
+- * @returns a Promise that resolves to the received event, or to an Error
+- */
+-function waitForEvent(aSubject, aEventName, aTimeoutMs, aTarget) {
+-  let eventDeferred = Promise.defer();
+-  let timeoutMs = aTimeoutMs || kDefaultWait;
+-  let stack = new Error().stack;
+-  let timerID = setTimeout(function wfe_canceller() {
+-    aSubject.removeEventListener(aEventName, listener);
+-    eventDeferred.reject(new Error(aEventName + " event timeout at " + stack));
+-  }, timeoutMs);
+-
+-  var listener = function(aEvent) {
+-    if (aTarget && aTarget !== aEvent.target)
+-        return;
+-
+-    // stop the timeout clock and resume
+-    clearTimeout(timerID);
+-    eventDeferred.resolve(aEvent);
+-  };
+-
+-  function cleanup(aEventOrError) {
+-    // unhook listener in case of success or failure
+-    aSubject.removeEventListener(aEventName, listener);
+-    return aEventOrError;
+-  }
+-  aSubject.addEventListener(aEventName, listener);
+-  return eventDeferred.promise.then(cleanup, cleanup);
+-}
+-
+ function openPreferencesViaOpenPreferencesAPI(aPane, aOptions) {
+   return new Promise(resolve => {
+     gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser, "about:blank");
+     openPreferences(aPane);
+     let newTabBrowser = gBrowser.selectedBrowser;
+ 
+     newTabBrowser.addEventListener("Initialized", function() {
+       newTabBrowser.contentWindow.addEventListener("load", function() {
+@@ -165,28 +120,16 @@ function waitForCondition(aConditionFn, 
+     function tryAgain() {
+       setTimeout(tryNow, aCheckInterval);
+     }
+     let tries = 0;
+     tryAgain();
+   });
+ }
+ 
+-function promiseWindowDialogOpen(buttonAction, url) {
+-  return BrowserTestUtils.promiseAlertDialogOpen(buttonAction, url);
+-}
+-
+-function promiseAlertDialogOpen(buttonAction) {
+-  return BrowserTestUtils.promiseAlertDialogOpen(buttonAction);
+-}
+-
+-function promiseSiteDataManagerSitesUpdated() {
+-  return TestUtils.topicObserved("sitedatamanager:sites-updated", () => true);
+-}
+-
+ function openSiteDataSettingsDialog() {
+   let doc = gBrowser.selectedBrowser.contentDocument;
+   let settingsBtn = doc.getElementById("siteDataSettings");
+   let dialogOverlay = content.gSubDialog._preloadDialog._overlay;
+   let dialogLoadPromise = promiseLoadSubDialog("chrome://browser/content/preferences/siteDataSettings.xul");
+   let dialogInitPromise = TestUtils.topicObserved("sitedata-settings-init", () => true);
+   let fullyLoadPromise = Promise.all([ dialogLoadPromise, dialogInitPromise ]).then(() => {
+     is(dialogOverlay.style.visibility, "visible", "The Settings dialog should be visible");
+@@ -205,38 +148,16 @@ function assertSitesListed(doc, hosts) {
+   hosts.forEach(host => {
+     let site = sitesList.querySelector(`richlistitem[host="${host}"]`);
+     ok(site, `Should list the site of ${host}`);
+   });
+   is(removeBtn.disabled, true, "Should disable the removeSelected button");
+   is(removeAllBtn.disabled, false, "Should enable the removeAllBtn button");
+ }
+ 
+-async function evaluateSearchResults(keyword, searchReults) {
+-  searchReults = Array.isArray(searchReults) ? searchReults : [searchReults];
+-  searchReults.push("header-searchResults");
+-
+-  let searchInput = gBrowser.contentDocument.getElementById("searchInput");
+-  searchInput.focus();
+-  let searchCompletedPromise = BrowserTestUtils.waitForEvent(
+-      gBrowser.contentWindow, "PreferencesSearchCompleted", evt => evt.detail == keyword);
+-  EventUtils.sendString(keyword);
+-  await searchCompletedPromise;
+-
+-  let mainPrefTag = gBrowser.contentDocument.getElementById("mainPrefPane");
+-  for (let i = 0; i < mainPrefTag.childElementCount; i++) {
+-    let child = mainPrefTag.children[i];
+-    if (searchReults.includes(child.id)) {
+-      is_element_visible(child, "Should be in search results");
+-    } else if (child.id) {
+-      is_element_hidden(child, "Should not be in search results");
+-    }
+-  }
+-}
+-
+ const mockSiteDataManager = {
+ 
+   _SiteDataManager: null,
+   _originalQMS: null,
+   _originalRemoveQuotaUsage: null,
+ 
+   getUsage(onUsageResult) {
+     let result = this.fakeSites.map(site => ({
+@@ -250,18 +171,18 @@ const mockSiteDataManager = {
+ 
+   _removeQuotaUsage(site) {
+     var target = site.principals[0].URI.host;
+     this.fakeSites = this.fakeSites.filter(fakeSite => {
+       return fakeSite.principal.URI.host != target;
+     });
+   },
+ 
+-  register(SiteDataManager, fakeSites) {
+-    this._SiteDataManager = SiteDataManager;
++  register(siteDataManager, fakeSites) {
++    this._SiteDataManager = siteDataManager;
+     this._originalQMS = this._SiteDataManager._qms;
+     this._SiteDataManager._qms = this;
+     this._originalRemoveQuotaUsage = this._SiteDataManager._removeQuotaUsage;
+     this._SiteDataManager._removeQuotaUsage = this._removeQuotaUsage.bind(this);
+     // Add some fake data.
+     this.fakeSites = fakeSites;
+     for (let site of fakeSites) {
+       if (!site.principal) {
+diff --git a/browser/components/preferences/in-content-new/tests/offline/manifest.appcache b/browser/components/preferences/in-content-new/tests/siteData/offline/manifest.appcache
+rename from browser/components/preferences/in-content-new/tests/offline/manifest.appcache
+rename to browser/components/preferences/in-content-new/tests/siteData/offline/manifest.appcache
+diff --git a/browser/components/preferences/in-content-new/tests/offline/offline.html b/browser/components/preferences/in-content-new/tests/siteData/offline/offline.html
+rename from browser/components/preferences/in-content-new/tests/offline/offline.html
+rename to browser/components/preferences/in-content-new/tests/siteData/offline/offline.html
+diff --git a/browser/components/preferences/in-content-new/tests/service_worker_test.html b/browser/components/preferences/in-content-new/tests/siteData/service_worker_test.html
+rename from browser/components/preferences/in-content-new/tests/service_worker_test.html
+rename to browser/components/preferences/in-content-new/tests/siteData/service_worker_test.html
+diff --git a/browser/components/preferences/in-content-new/tests/service_worker_test.js b/browser/components/preferences/in-content-new/tests/siteData/service_worker_test.js
+rename from browser/components/preferences/in-content-new/tests/service_worker_test.js
+rename to browser/components/preferences/in-content-new/tests/siteData/service_worker_test.js
+diff --git a/browser/components/preferences/in-content-new/tests/site_data_test.html b/browser/components/preferences/in-content-new/tests/siteData/site_data_test.html
+rename from browser/components/preferences/in-content-new/tests/site_data_test.html
+rename to browser/components/preferences/in-content-new/tests/siteData/site_data_test.html
+diff --git a/browser/components/preferences/in-content/tests/browser.ini b/browser/components/preferences/in-content/tests/browser.ini
+--- a/browser/components/preferences/in-content/tests/browser.ini
++++ b/browser/components/preferences/in-content/tests/browser.ini
+@@ -1,34 +1,28 @@
+ [DEFAULT]
+ support-files =
+   head.js
+   privacypane_tests_perwindow.js
+-  site_data_test.html
+-  service_worker_test.html
+-  service_worker_test.js
+-  offline/offline.html
+-  offline/manifest.appcache
+ 
+ [browser_applications_selection.js]
+ [browser_advanced_update.js]
+ skip-if = !updater
+ [browser_basic_rebuild_fonts_test.js]
+ [browser_bug410900.js]
+ [browser_bug705422.js]
+ [browser_bug731866.js]
+ [browser_bug795764_cachedisabled.js]
+ [browser_bug1018066_resetScrollPosition.js]
+ [browser_bug1020245_openPreferences_to_paneContent.js]
+ [browser_bug1184989_prevent_scrolling_when_preferences_flipped.js]
+ support-files =
+   browser_bug1184989_prevent_scrolling_when_preferences_flipped.xul
+ [browser_change_app_handler.js]
+ skip-if = os != "win" # This test tests the windows-specific app selection dialog, so can't run on non-Windows
+-[browser_clearSiteData.js]
+ [browser_connection.js]
+ [browser_connection_bug388287.js]
+ [browser_cookies_exceptions.js]
+ [browser_defaultbrowser_alwayscheck.js]
+ [browser_homepages_filter_aboutpreferences.js]
+ [browser_notifications_do_not_disturb.js]
+ [browser_password_management.js]
+ [browser_performance.js]
+@@ -42,17 +36,14 @@ skip-if = e10s
+ [browser_privacypane_1.js]
+ [browser_privacypane_3.js]
+ [browser_privacypane_4.js]
+ [browser_privacypane_5.js]
+ [browser_privacypane_8.js]
+ [browser_sanitizeOnShutdown_prefLocked.js]
+ [browser_searchsuggestions.js]
+ [browser_security.js]
+-[browser_siteData.js]
+-[browser_siteData2.js]
+-[browser_siteData3.js]
+ [browser_site_login_exceptions.js]
+ [browser_cookies_dialog.js]
+ [browser_subdialogs.js]
+ support-files =
+   subdialog.xul
+   subdialog2.xul
+diff --git a/browser/components/preferences/in-content/tests/head.js b/browser/components/preferences/in-content/tests/head.js
+--- a/browser/components/preferences/in-content/tests/head.js
++++ b/browser/components/preferences/in-content/tests/head.js
+@@ -1,85 +1,23 @@
+ /* Any copyright is dedicated to the Public Domain.
+  * http://creativecommons.org/publicdomain/zero/1.0/ */
+ 
+ ChromeUtils.import("resource://gre/modules/Promise.jsm", this);
+ 
+-XPCOMUtils.defineLazyServiceGetter(this, "serviceWorkerManager", "@mozilla.org/serviceworkers/manager;1", "nsIServiceWorkerManager");
+-
+ // Tests within /browser/components/preferences/in-content/tests/
+ // test the "old" preferences organization, before it was reorganized.
+ // Thus, all of these tests should revert back to the "oldOrganization"
+ // before running.
+ Services.prefs.setBoolPref("browser.preferences.useOldOrganization", true);
+ registerCleanupFunction(function() {
+   Services.prefs.clearUserPref("browser.preferences.useOldOrganization");
+ });
+ 
+ const kDefaultWait = 2000;
+-const REMOVE_DIALOG_URL = "chrome://browser/content/preferences/siteDataRemoveSelected.xul";
+-const { SiteDataManager } = ChromeUtils.import("resource:///modules/SiteDataManager.jsm", {});
+-const mockSiteDataManager = {
+-
+-  _originalGetQuotaUsage: null,
+-  _originalQMS: null,
+-  _originalRemoveQuotaUsage: null,
+-
+-  getUsage(onUsageResult) {
+-    let result = this.fakeSites.map(site => ({
+-      origin: site.principal.origin,
+-      usage: site.usage,
+-      persisted: site.persisted,
+-      lastAccessed: site.lastAccessed,
+-    }));
+-    onUsageResult({ result, resultCode: Components.results.NS_OK });
+-  },
+-
+-  _removeQuotaUsage(site) {
+-    var target = site.principals[0].URI.host;
+-    this.fakeSites = this.fakeSites.filter(fakeSite => {
+-      return fakeSite.principal.URI.host != target;
+-    });
+-  },
+-
+-  register(fakeSites) {
+-    this._originalQMS = SiteDataManager._qms;
+-    SiteDataManager._qms = this;
+-    this._originalRemoveQuotaUsage = SiteDataManager._removeQuotaUsage;
+-    SiteDataManager._removeQuotaUsage = this._removeQuotaUsage.bind(this);
+-    // Add some fake data.
+-    this.fakeSites = fakeSites;
+-    for (let site of fakeSites) {
+-      if (!site.principal) {
+-        site.principal = Services.scriptSecurityManager
+-          .createCodebasePrincipalFromOrigin(site.origin);
+-      }
+-
+-      let uri = site.principal.URI;
+-      try {
+-        site.baseDomain = Services.eTLD.getBaseDomainFromHost(uri.host);
+-      } catch (e) {
+-        site.baseDomain = uri.host;
+-      }
+-
+-      // Add some cookies if needed.
+-      for (let i = 0; i < (site.cookies || 0); i++) {
+-        Services.cookies.add(uri.host, uri.pathQueryRef, Cu.now(), i,
+-          false, false, false, Date.now() + 1000 * 60 * 60);
+-      }
+-    }
+-  },
+-
+-  async unregister() {
+-    await SiteDataManager.removeAll();
+-    this.fakeSites = null;
+-    SiteDataManager._qms = this._originalQMS;
+-    SiteDataManager._removeQuotaUsage = this._originalRemoveQuotaUsage;
+-  }
+-};
+ 
+ function is_hidden(aElement) {
+   var style = aElement.ownerGlobal.getComputedStyle(aElement);
+   if (style.display == "none")
+     return true;
+   if (style.visibility != "visible")
+     return true;
+ 
+@@ -227,112 +165,8 @@ function waitForCondition(aConditionFn, 
+     function tryAgain() {
+       setTimeout(tryNow, aCheckInterval);
+     }
+     let tries = 0;
+     tryAgain();
+   });
+ }
+ 
+-function promiseWindowDialogOpen(buttonAction, url) {
+-  return BrowserTestUtils.promiseAlertDialogOpen(buttonAction, url);
+-}
+-
+-function promiseAlertDialogOpen(buttonAction) {
+-  return BrowserTestUtils.promiseAlertDialogOpen(buttonAction);
+-}
+-
+-function openSettingsDialog() {
+-  let win = gBrowser.selectedBrowser.contentWindow;
+-  let doc = gBrowser.selectedBrowser.contentDocument;
+-  let settingsBtn = doc.getElementById("siteDataSettings");
+-  let dialogOverlay = win.gSubDialog._preloadDialog._overlay;
+-  let dialogLoadPromise = promiseLoadSubDialog("chrome://browser/content/preferences/siteDataSettings.xul");
+-  let dialogInitPromise = TestUtils.topicObserved("sitedata-settings-init", () => true);
+-  let fullyLoadPromise = Promise.all([ dialogLoadPromise, dialogInitPromise ]).then(() => {
+-    is(dialogOverlay.style.visibility, "visible", "The Settings dialog should be visible");
+-  });
+-  settingsBtn.doCommand();
+-  return fullyLoadPromise;
+-}
+-
+-function assertSitesListed(doc, hosts) {
+-  let win = gBrowser.selectedBrowser.contentWindow;
+-  let frameDoc = win.gSubDialog._topDialog._frame.contentDocument;
+-  let removeBtn = frameDoc.getElementById("removeSelected");
+-  let removeAllBtn = frameDoc.getElementById("removeAll");
+-  let sitesList = frameDoc.getElementById("sitesList");
+-  let totalSitesNumber = sitesList.getElementsByTagName("richlistitem").length;
+-  is(totalSitesNumber, hosts.length, "Should list the right sites number");
+-  hosts.forEach(host => {
+-    let site = sitesList.querySelector(`richlistitem[host="${host}"]`);
+-    ok(site, `Should list the site of ${host}`);
+-  });
+-  is(removeBtn.disabled, true, "Should disable the removeSelected button");
+-  is(removeAllBtn.disabled, false, "Should enable the removeAllBtn button");
+-}
+-
+-function promiseSitesUpdated() {
+-  return TestUtils.topicObserved("sitedatamanager:sites-updated", () => true);
+-}
+-
+-function promiseSettingsDialogClose() {
+-  return new Promise(resolve => {
+-    let win = gBrowser.selectedBrowser.contentWindow;
+-    let dialogOverlay = win.gSubDialog._topDialog._overlay;
+-    let dialogWin = win.gSubDialog._topDialog._frame.contentWindow;
+-    dialogWin.addEventListener("unload", function unload() {
+-      if (dialogWin.document.documentURI === "chrome://browser/content/preferences/siteDataSettings.xul") {
+-        isnot(dialogOverlay.style.visibility, "visible", "The Settings dialog should be hidden");
+-        resolve();
+-      }
+-    }, { once: true });
+-  });
+-}
+-
+-function getQuotaUsage(origin) {
+-  return new Promise(resolve => {
+-    let uri = NetUtil.newURI(origin);
+-    let principal = Services.scriptSecurityManager.createCodebasePrincipal(uri, {});
+-    Services.qms.getUsageForPrincipal(principal, request => resolve(request.result.usage));
+-  });
+-}
+-
+-function promiseCookiesCleared() {
+-  return TestUtils.topicObserved("cookie-changed", (subj, data) => {
+-    return data === "cleared";
+-  });
+-}
+-
+-async function loadServiceWorkerTestPage(url) {
+-  let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, url);
+-  await BrowserTestUtils.waitForCondition(() => {
+-    return ContentTask.spawn(tab.linkedBrowser, {}, () =>
+-      content.document.body.getAttribute("data-test-service-worker-registered") === "true");
+-  }, `Fail to load service worker test ${url}`);
+-  await BrowserTestUtils.removeTab(tab);
+-}
+-
+-function promiseServiceWorkersCleared() {
+-  return BrowserTestUtils.waitForCondition(() => {
+-    let serviceWorkers = serviceWorkerManager.getAllRegistrations();
+-    if (serviceWorkers.length == 0) {
+-      ok(true, "Cleared all service workers");
+-      return true;
+-    }
+-    return false;
+-  }, "Should clear all service workers");
+-}
+-
+-function promiseServiceWorkerRegisteredFor(url) {
+-  return BrowserTestUtils.waitForCondition(() => {
+-    try {
+-      let principal = Services.scriptSecurityManager.createCodebasePrincipalFromOrigin(url);
+-      let sw = serviceWorkerManager.getRegistrationByPrincipal(principal, principal.URI.spec);
+-      if (sw) {
+-        ok(true, `Found the service worker registered for ${url}`);
+-        return true;
+-      }
+-    } catch (e) {}
+-    return false;
+-  }, `Should register service worker for ${url}`);
+-}
+-
+diff --git a/browser/components/preferences/in-content/tests/siteData/browser.ini b/browser/components/preferences/in-content/tests/siteData/browser.ini
+new file mode 100644
+--- /dev/null
++++ b/browser/components/preferences/in-content/tests/siteData/browser.ini
+@@ -0,0 +1,14 @@
++[DEFAULT]
++support-files =
++  head.js
++  site_data_test.html
++  service_worker_test.html
++  service_worker_test.js
++  offline/offline.html
++  offline/manifest.appcache
++
++[browser_clearSiteData.js]
++[browser_siteData.js]
++skip-if = (os == 'linux' && debug) # Bug 1439332
++[browser_siteData2.js]
++[browser_siteData3.js]
+diff --git a/browser/components/preferences/in-content/tests/browser_clearSiteData.js b/browser/components/preferences/in-content/tests/siteData/browser_clearSiteData.js
+rename from browser/components/preferences/in-content/tests/browser_clearSiteData.js
+rename to browser/components/preferences/in-content/tests/siteData/browser_clearSiteData.js
+--- a/browser/components/preferences/in-content/tests/browser_clearSiteData.js
++++ b/browser/components/preferences/in-content/tests/siteData/browser_clearSiteData.js
+@@ -1,26 +1,15 @@
+ /* Any copyright is dedicated to the Public Domain.
+  * http://creativecommons.org/publicdomain/zero/1.0/ */
+ 
+ "use strict";
+ 
+ ChromeUtils.import("resource:///modules/SitePermissions.jsm");
+ 
+-const { SiteDataManager } = ChromeUtils.import("resource:///modules/SiteDataManager.jsm", {});
+-const { DownloadUtils } = ChromeUtils.import("resource://gre/modules/DownloadUtils.jsm", {});
+-
+-const TEST_QUOTA_USAGE_HOST = "example.com";
+-const TEST_QUOTA_USAGE_ORIGIN = "https://" + TEST_QUOTA_USAGE_HOST;
+-const TEST_QUOTA_USAGE_URL = TEST_QUOTA_USAGE_ORIGIN + "/browser/browser/components/preferences/in-content/tests/site_data_test.html";
+-const TEST_OFFLINE_HOST = "example.org";
+-const TEST_OFFLINE_ORIGIN = "https://" + TEST_OFFLINE_HOST;
+-const TEST_OFFLINE_URL = TEST_OFFLINE_ORIGIN + "/browser/browser/components/preferences/in-content/tests/offline/offline.html";
+-const TEST_SERVICE_WORKER_URL = TEST_OFFLINE_ORIGIN + "/browser/browser/components/preferences/in-content/tests/service_worker_test.html";
+-
+ async function testClearData(clearSiteData, clearCache) {
+   let quotaURI = Services.io.newURI(TEST_QUOTA_USAGE_ORIGIN);
+   SitePermissions.set(quotaURI, "persistent-storage", SitePermissions.ALLOW);
+ 
+   // Open a test site which saves into appcache.
+   await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_OFFLINE_URL);
+   await BrowserTestUtils.removeTab(gBrowser.selectedTab);
+ 
+@@ -84,17 +73,17 @@ async function testClearData(clearSiteDa
+   clearCacheCheckbox.checked = clearCache;
+ 
+   // Some additional promises/assertions to wait for
+   // when deleting site data.
+   let acceptPromise;
+   let updatePromise;
+   let cookiesClearedPromise;
+   if (clearSiteData) {
+-    acceptPromise = promiseAlertDialogOpen("accept");
++    acceptPromise = BrowserTestUtils.promiseAlertDialogOpen("accept");
+     updatePromise = promiseSiteDataManagerSitesUpdated();
+     cookiesClearedPromise = promiseCookiesCleared();
+   }
+ 
+   let dialogClosed = BrowserTestUtils.waitForEvent(dialogWin, "unload");
+ 
+   let clearButton = dialogWin.document.getElementById("clearButton");
+   if (!clearSiteData && !clearCache) {
+diff --git a/browser/components/preferences/in-content/tests/browser_siteData.js b/browser/components/preferences/in-content/tests/siteData/browser_siteData.js
+rename from browser/components/preferences/in-content/tests/browser_siteData.js
+rename to browser/components/preferences/in-content/tests/siteData/browser_siteData.js
+--- a/browser/components/preferences/in-content/tests/browser_siteData.js
++++ b/browser/components/preferences/in-content/tests/siteData/browser_siteData.js
+@@ -1,27 +1,15 @@
+ /* Any copyright is dedicated to the Public Domain.
+  * http://creativecommons.org/publicdomain/zero/1.0/ */
+ 
+ "use strict";
+ 
+-const TEST_QUOTA_USAGE_HOST = "example.com";
+-const TEST_QUOTA_USAGE_ORIGIN = "https://" + TEST_QUOTA_USAGE_HOST;
+-const TEST_QUOTA_USAGE_URL = TEST_QUOTA_USAGE_ORIGIN + "/browser/browser/components/preferences/in-content/tests/site_data_test.html";
+-const TEST_OFFLINE_HOST = "example.org";
+-const TEST_OFFLINE_ORIGIN = "https://" + TEST_OFFLINE_HOST;
+-const TEST_OFFLINE_URL = TEST_OFFLINE_ORIGIN + "/browser/browser/components/preferences/in-content/tests/offline/offline.html";
+-const TEST_SERVICE_WORKER_URL = TEST_OFFLINE_ORIGIN + "/browser/browser/components/preferences/in-content/tests/service_worker_test.html";
+-
+-const { NetUtil } = ChromeUtils.import("resource://gre/modules/NetUtil.jsm", {});
+-const { DownloadUtils } = ChromeUtils.import("resource://gre/modules/DownloadUtils.jsm", {});
+-const { OfflineAppCacheHelper } = ChromeUtils.import("resource:///modules/offlineAppCache.jsm", {});
+-
+ function getPersistentStoragePermStatus(origin) {
+-  let uri = NetUtil.newURI(origin);
++  let uri = Services.io.newURI(origin);
+   let principal = Services.scriptSecurityManager.createCodebasePrincipal(uri, {});
+   return Services.perms.testExactPermissionFromPrincipal(principal, "persistent-storage");
+ }
+ 
+ // Test listing site using quota usage or site using appcache
+ // This is currently disabled because of bug 1414751.
+ add_task(async function() {
+   // Open a test site which would save into appcache
+@@ -106,17 +94,16 @@ add_task(async function() {
+                           is(actual, expected, "Should show the right total site data size");
+                        });
+ 
+   await BrowserTestUtils.removeTab(gBrowser.selectedTab);
+ });
+ 
+ // Test clearing service wroker through the "Clear All" button
+ add_task(async function() {
+-  await SpecialPowers.pushPrefEnv({set: [["browser.storageManager.enabled", true]]});
+   // Register a test service
+   await loadServiceWorkerTestPage(TEST_SERVICE_WORKER_URL);
+   await openPreferencesViaOpenPreferencesAPI("privacy", { leaveOpen: true });
+   // Test the initial states
+   await promiseServiceWorkerRegisteredFor(TEST_SERVICE_WORKER_URL);
+   // Click the "Clear All" button
+   let doc = gBrowser.selectedBrowser.contentDocument;
+   let clearBtn = doc.getElementById("clearSiteDataButton");
+@@ -134,17 +121,17 @@ add_task(async function() {
+   // Register a test service worker
+   await loadServiceWorkerTestPage(TEST_SERVICE_WORKER_URL);
+   await openPreferencesViaOpenPreferencesAPI("advanced", "networkTab", { leaveOpen: true });
+   // Test the initial states
+   await promiseServiceWorkerRegisteredFor(TEST_SERVICE_WORKER_URL);
+   // Open the Site Data Settings panel and remove the site
+   await openSettingsDialog();
+ 
+-  let acceptRemovePromise = promiseAlertDialogOpen("accept");
++  let acceptRemovePromise = BrowserTestUtils.promiseAlertDialogOpen("accept");
+   let updatePromise = promiseSiteDataManagerSitesUpdated();
+   ContentTask.spawn(gBrowser.selectedBrowser, { TEST_OFFLINE_HOST }, args => {
+     let host = args.TEST_OFFLINE_HOST;
+     let frameDoc = content.gSubDialog._topDialog._frame.contentDocument;
+     let sitesList = frameDoc.getElementById("sitesList");
+     let site = sitesList.querySelector(`richlistitem[host="${host}"]`);
+     if (site) {
+       let removeBtn = frameDoc.getElementById("removeSelected");
+@@ -194,17 +181,17 @@ add_task(async function() {
+ 
+   let creationDate1 = formatter.format(new Date(cookie1.lastAccessed / 1000));
+   let creationDate2 = formatter.format(new Date(cookie2.lastAccessed / 1000));
+ 
+   await openPreferencesViaOpenPreferencesAPI("privacy", { leaveOpen: true });
+ 
+   // Open the site data manager and remove one site.
+   await openSiteDataSettingsDialog();
+-  let removeDialogOpenPromise = promiseWindowDialogOpen("accept", REMOVE_DIALOG_URL);
++  let removeDialogOpenPromise = BrowserTestUtils.promiseAlertDialogOpen("accept", REMOVE_DIALOG_URL);
+   await ContentTask.spawn(gBrowser.selectedBrowser, {creationDate1, creationDate2}, function(args) {
+     let frameDoc = content.gSubDialog._topDialog._frame.contentDocument;
+ 
+     let siteItems = frameDoc.getElementsByTagName("richlistitem");
+     is(siteItems.length, 2, "Should list two sites with cookies");
+     let sitesList = frameDoc.getElementById("sitesList");
+     let site1 = sitesList.querySelector(`richlistitem[host="example.com"]`);
+     let site2 = sitesList.querySelector(`richlistitem[host="example.org"]`);
+@@ -229,17 +216,17 @@ add_task(async function() {
+   });
+   await removeDialogOpenPromise;
+ 
+   await TestUtils.waitForCondition(() => Services.cookies.countCookiesFromHost(uri2.host) == 0, "Cookies from the first host should be cleared");
+   is(Services.cookies.countCookiesFromHost(uri.host), 2, "Cookies from the second host should not be cleared");
+ 
+   // Open the site data manager and remove another site.
+   await openSiteDataSettingsDialog();
+-  let acceptRemovePromise = promiseAlertDialogOpen("accept");
++  let acceptRemovePromise = BrowserTestUtils.promiseAlertDialogOpen("accept");
+   await ContentTask.spawn(gBrowser.selectedBrowser, {creationDate1}, function(args) {
+     let frameDoc = content.gSubDialog._topDialog._frame.contentDocument;
+ 
+     let siteItems = frameDoc.getElementsByTagName("richlistitem");
+     is(siteItems.length, 1, "Should list one site with cookies");
+     let sitesList = frameDoc.getElementById("sitesList");
+     let site1 = sitesList.querySelector(`richlistitem[host="example.com"]`);
+ 
+diff --git a/browser/components/preferences/in-content/tests/browser_siteData2.js b/browser/components/preferences/in-content/tests/siteData/browser_siteData2.js
+rename from browser/components/preferences/in-content/tests/browser_siteData2.js
+rename to browser/components/preferences/in-content/tests/siteData/browser_siteData2.js
+--- a/browser/components/preferences/in-content/tests/browser_siteData2.js
++++ b/browser/components/preferences/in-content/tests/siteData/browser_siteData2.js
+@@ -1,11 +1,10 @@
+ "use strict";
+ 
+-
+ function assertAllSitesNotListed(win) {
+   let frameDoc = win.gSubDialog._topDialog._frame.contentDocument;
+   let removeBtn = frameDoc.getElementById("removeSelected");
+   let removeAllBtn = frameDoc.getElementById("removeAll");
+   let sitesList = frameDoc.getElementById("sitesList");
+   let sites = sitesList.getElementsByTagName("richlistitem");
+   is(sites.length, 0, "Should not list all sites");
+   is(removeBtn.disabled, true, "Should disable the removeSelected button");
+@@ -60,32 +59,32 @@ add_task(async function() {
+   removeAllSitesOneByOne();
+   assertAllSitesNotListed(win);
+   cancelBtn.doCommand();
+   await settingsDialogClosePromise;
+   await openSettingsDialog();
+   assertSitesListed(doc, fakeHosts);
+ 
+   // Test the "Save Changes" button but cancelling save
+-  let cancelPromise = promiseAlertDialogOpen("cancel");
++  let cancelPromise = BrowserTestUtils.promiseAlertDialogOpen("cancel");
+   settingsDialogClosePromise = promiseSettingsDialogClose();
+   frameDoc = win.gSubDialog._topDialog._frame.contentDocument;
+   saveBtn = frameDoc.getElementById("save");
+   cancelBtn = frameDoc.getElementById("cancel");
+   removeAllSitesOneByOne();
+   assertAllSitesNotListed(win);
+   saveBtn.doCommand();
+   await cancelPromise;
+   cancelBtn.doCommand();
+   await settingsDialogClosePromise;
+   await openSettingsDialog();
+   assertSitesListed(doc, fakeHosts);
+ 
+   // Test the "Save Changes" button and accepting save
+-  let acceptPromise = promiseAlertDialogOpen("accept");
++  let acceptPromise = BrowserTestUtils.promiseAlertDialogOpen("accept");
+   settingsDialogClosePromise = promiseSettingsDialogClose();
+   updatePromise = promiseSitesUpdated();
+   frameDoc = win.gSubDialog._topDialog._frame.contentDocument;
+   saveBtn = frameDoc.getElementById("save");
+   removeAllSitesOneByOne();
+   assertAllSitesNotListed(win);
+   saveBtn.doCommand();
+   await acceptPromise;
+@@ -173,32 +172,32 @@ add_task(async function() {
+   removeSelectedSite(fakeHosts.slice(0, 2));
+   assertSitesListed(doc, fakeHosts.slice(2));
+   cancelBtn.doCommand();
+   await settingsDialogClosePromise;
+   await openSettingsDialog();
+   assertSitesListed(doc, fakeHosts);
+ 
+   // Test the "Save Changes" button but canceling save
+-  removeDialogOpenPromise = promiseWindowDialogOpen("cancel", REMOVE_DIALOG_URL);
++  removeDialogOpenPromise = BrowserTestUtils.promiseAlertDialogOpen("cancel", REMOVE_DIALOG_URL);
+   settingsDialogClosePromise = promiseSettingsDialogClose();
+   frameDoc = win.gSubDialog._topDialog._frame.contentDocument;
+   saveBtn = frameDoc.getElementById("save");
+   cancelBtn = frameDoc.getElementById("cancel");
+   removeSelectedSite(fakeHosts.slice(0, 2));
+   assertSitesListed(doc, fakeHosts.slice(2));
+   saveBtn.doCommand();
+   await removeDialogOpenPromise;
+   cancelBtn.doCommand();
+   await settingsDialogClosePromise;
+   await openSettingsDialog();
+   assertSitesListed(doc, fakeHosts);
+ 
+   // Test the "Save Changes" button and accepting save
+-  removeDialogOpenPromise = promiseWindowDialogOpen("accept", REMOVE_DIALOG_URL);
++  removeDialogOpenPromise = BrowserTestUtils.promiseAlertDialogOpen("accept", REMOVE_DIALOG_URL);
+   settingsDialogClosePromise = promiseSettingsDialogClose();
+   frameDoc = win.gSubDialog._topDialog._frame.contentDocument;
+   saveBtn = frameDoc.getElementById("save");
+   removeSelectedSite(fakeHosts.slice(0, 2));
+   assertSitesListed(doc, fakeHosts.slice(2));
+   saveBtn.doCommand();
+   await removeDialogOpenPromise;
+   await settingsDialogClosePromise;
+@@ -263,17 +262,17 @@ add_task(async function() {
+   let frameDoc = win.gSubDialog._topDialog._frame.contentDocument;
+   let searchBox = frameDoc.getElementById("searchBox");
+   searchBox.value = "xyz";
+   searchBox.doCommand();
+   assertSitesListed(doc, fakeHosts.filter(host => host.includes("xyz")));
+ 
+   // Test only removing all visible sites listed
+   updatePromise = promiseSitesUpdated();
+-  let acceptRemovePromise = promiseWindowDialogOpen("accept", REMOVE_DIALOG_URL);
++  let acceptRemovePromise = BrowserTestUtils.promiseAlertDialogOpen("accept", REMOVE_DIALOG_URL);
+   let settingsDialogClosePromise = promiseSettingsDialogClose();
+   let removeAllBtn = frameDoc.getElementById("removeAll");
+   let saveBtn = frameDoc.getElementById("save");
+   removeAllBtn.doCommand();
+   saveBtn.doCommand();
+   await acceptRemovePromise;
+   await settingsDialogClosePromise;
+   await updatePromise;
+@@ -320,17 +319,17 @@ add_task(async function() {
+                        .createCodebasePrincipalFromOrigin("http://email.bar.com"),
+     persisted: false
+   });
+ 
+   // Test clearing all site data dynamically
+   let win = gBrowser.selectedBrowser.contentWindow;
+   let frameDoc = win.gSubDialog._topDialog._frame.contentDocument;
+   updatePromise = promiseSitesUpdated();
+-  let acceptRemovePromise = promiseAlertDialogOpen("accept");
++  let acceptRemovePromise = BrowserTestUtils.promiseAlertDialogOpen("accept");
+   let settingsDialogClosePromise = promiseSettingsDialogClose();
+   let removeAllBtn = frameDoc.getElementById("removeAll");
+   let saveBtn = frameDoc.getElementById("save");
+   removeAllBtn.doCommand();
+   saveBtn.doCommand();
+   await acceptRemovePromise;
+   await settingsDialogClosePromise;
+   await updatePromise;
+diff --git a/browser/components/preferences/in-content/tests/browser_siteData3.js b/browser/components/preferences/in-content/tests/siteData/browser_siteData3.js
+rename from browser/components/preferences/in-content/tests/browser_siteData3.js
+rename to browser/components/preferences/in-content/tests/siteData/browser_siteData3.js
+--- a/browser/components/preferences/in-content/tests/browser_siteData3.js
++++ b/browser/components/preferences/in-content/tests/siteData/browser_siteData3.js
+@@ -1,13 +1,12 @@
+ "use strict";
+ 
+ // Test search on the host column
+ add_task(async function() {
+-  await SpecialPowers.pushPrefEnv({set: [["browser.storageManager.enabled", true]]});
+   mockSiteDataManager.register([
+     {
+       usage: 1024,
+       origin: "https://account.xyz.com",
+       persisted: true
+     },
+     {
+       usage: 1024,
+@@ -50,17 +49,16 @@ add_task(async function() {
+   assertSitesListed(doc, fakeHosts);
+ 
+   await mockSiteDataManager.unregister();
+   await BrowserTestUtils.removeTab(gBrowser.selectedTab);
+ });
+ 
+ // Test not displaying sites which store 0 byte and don't have persistent storage.
+ add_task(async function() {
+-  await SpecialPowers.pushPrefEnv({set: [["browser.storageManager.enabled", true]]});
+   mockSiteDataManager.register([
+     {
+       usage: 0,
+       origin: "https://account.xyz.com",
+       persisted: true
+     },
+     {
+       usage: 0,
+@@ -94,17 +92,16 @@ add_task(async function() {
+   assertSitesListed(doc, fakeHosts.filter(host => host != "shopping.xyz.com"));
+ 
+   await mockSiteDataManager.unregister();
+   await BrowserTestUtils.removeTab(gBrowser.selectedTab);
+ });
+ 
+ // Test sorting
+ add_task(async function() {
+-  await SpecialPowers.pushPrefEnv({set: [["browser.storageManager.enabled", true]]});
+   mockSiteDataManager.register([
+     {
+       usage: 1024,
+       origin: "https://account.xyz.com",
+       cookies: 6,
+       persisted: true,
+     },
+     {
+diff --git a/browser/components/preferences/in-content/tests/head.js b/browser/components/preferences/in-content/tests/siteData/head.js
+copy from browser/components/preferences/in-content/tests/head.js
+copy to browser/components/preferences/in-content/tests/siteData/head.js
+--- a/browser/components/preferences/in-content/tests/head.js
++++ b/browser/components/preferences/in-content/tests/siteData/head.js
+@@ -1,27 +1,38 @@
+ /* Any copyright is dedicated to the Public Domain.
+  * http://creativecommons.org/publicdomain/zero/1.0/ */
+ 
+-ChromeUtils.import("resource://gre/modules/Promise.jsm", this);
++"use strict";
++
++const TEST_QUOTA_USAGE_HOST = "example.com";
++const TEST_QUOTA_USAGE_ORIGIN = "https://" + TEST_QUOTA_USAGE_HOST;
++const TEST_QUOTA_USAGE_URL = getRootDirectory(gTestPath).replace("chrome://mochitests/content", TEST_QUOTA_USAGE_ORIGIN) + "/site_data_test.html";
++const TEST_OFFLINE_HOST = "example.org";
++const TEST_OFFLINE_ORIGIN = "https://" + TEST_OFFLINE_HOST;
++const TEST_OFFLINE_URL = getRootDirectory(gTestPath).replace("chrome://mochitests/content", TEST_OFFLINE_ORIGIN) + "/offline/offline.html";
++const TEST_SERVICE_WORKER_URL = getRootDirectory(gTestPath).replace("chrome://mochitests/content", TEST_OFFLINE_ORIGIN) + "/service_worker_test.html";
++
++const REMOVE_DIALOG_URL = "chrome://browser/content/preferences/siteDataRemoveSelected.xul";
++
++const { DownloadUtils } = ChromeUtils.import("resource://gre/modules/DownloadUtils.jsm", {});
++const { SiteDataManager } = ChromeUtils.import("resource:///modules/SiteDataManager.jsm", {});
++const { OfflineAppCacheHelper } = ChromeUtils.import("resource:///modules/offlineAppCache.jsm", {});
+ 
+ XPCOMUtils.defineLazyServiceGetter(this, "serviceWorkerManager", "@mozilla.org/serviceworkers/manager;1", "nsIServiceWorkerManager");
+ 
+ // Tests within /browser/components/preferences/in-content/tests/
+ // test the "old" preferences organization, before it was reorganized.
+ // Thus, all of these tests should revert back to the "oldOrganization"
+ // before running.
+ Services.prefs.setBoolPref("browser.preferences.useOldOrganization", true);
+ registerCleanupFunction(function() {
+   Services.prefs.clearUserPref("browser.preferences.useOldOrganization");
+ });
+ 
+-const kDefaultWait = 2000;
+-const REMOVE_DIALOG_URL = "chrome://browser/content/preferences/siteDataRemoveSelected.xul";
+-const { SiteDataManager } = ChromeUtils.import("resource:///modules/SiteDataManager.jsm", {});
+ const mockSiteDataManager = {
+ 
+   _originalGetQuotaUsage: null,
+   _originalQMS: null,
+   _originalRemoveQuotaUsage: null,
+ 
+   getUsage(onUsageResult) {
+     let result = this.fakeSites.map(site => ({
+@@ -95,30 +106,16 @@ function is_element_visible(aElement, aM
+   ok(!is_hidden(aElement), aMsg);
+ }
+ 
+ function is_element_hidden(aElement, aMsg) {
+   isnot(aElement, null, "Element should not be null, when checking visibility");
+   ok(is_hidden(aElement), aMsg);
+ }
+ 
+-function open_preferences(aCallback) {
+-  gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser, "about:preferences");
+-  let newTabBrowser = gBrowser.getBrowserForTab(gBrowser.selectedTab);
+-  newTabBrowser.addEventListener("Initialized", function() {
+-    aCallback(gBrowser.contentWindow);
+-  }, {capture: true, once: true});
+-}
+-
+-function openAndLoadSubDialog(aURL, aFeatures = null, aParams = null, aClosingCallback = null) {
+-  let promise = promiseLoadSubDialog(aURL);
+-  content.gSubDialog.open(aURL, aFeatures, aParams, aClosingCallback);
+-  return promise;
+-}
+-
+ function promiseLoadSubDialog(aURL) {
+   return new Promise((resolve, reject) => {
+     content.gSubDialog._dialogStack.addEventListener("dialogopen", function dialogopen(aEvent) {
+       if (aEvent.detail.dialog._frame.contentWindow.location == "about:blank")
+         return;
+       content.gSubDialog._dialogStack.removeEventListener("dialogopen", dialogopen);
+ 
+       is(aEvent.detail.dialog._frame.contentWindow.location.toString(), aURL,
+@@ -138,63 +135,16 @@ function promiseLoadSubDialog(aURL) {
+       }
+       is(expectedStyleSheetURLs.length, 0, "All expectedStyleSheetURLs should have been found");
+ 
+       resolve(aEvent.detail.dialog._frame.contentWindow);
+     });
+   });
+ }
+ 
+-/**
+- * Waits a specified number of miliseconds for a specified event to be
+- * fired on a specified element.
+- *
+- * Usage:
+- *    let receivedEvent = waitForEvent(element, "eventName");
+- *    // Do some processing here that will cause the event to be fired
+- *    // ...
+- *    // Now yield until the Promise is fulfilled
+- *    yield receivedEvent;
+- *    if (receivedEvent && !(receivedEvent instanceof Error)) {
+- *      receivedEvent.msg == "eventName";
+- *      // ...
+- *    }
+- *
+- * @param aSubject the element that should receive the event
+- * @param aEventName the event to wait for
+- * @param aTimeoutMs the number of miliseconds to wait before giving up
+- * @returns a Promise that resolves to the received event, or to an Error
+- */
+-function waitForEvent(aSubject, aEventName, aTimeoutMs, aTarget) {
+-  let eventDeferred = Promise.defer();
+-  let timeoutMs = aTimeoutMs || kDefaultWait;
+-  let stack = new Error().stack;
+-  let timerID = setTimeout(function wfe_canceller() {
+-    aSubject.removeEventListener(aEventName, listener);
+-    eventDeferred.reject(new Error(aEventName + " event timeout at " + stack));
+-  }, timeoutMs);
+-
+-  var listener = function(aEvent) {
+-    if (aTarget && aTarget !== aEvent.target)
+-        return;
+-
+-    // stop the timeout clock and resume
+-    clearTimeout(timerID);
+-    eventDeferred.resolve(aEvent);
+-  };
+-
+-  function cleanup(aEventOrError) {
+-    // unhook listener in case of success or failure
+-    aSubject.removeEventListener(aEventName, listener);
+-    return aEventOrError;
+-  }
+-  aSubject.addEventListener(aEventName, listener);
+-  return eventDeferred.promise.then(cleanup, cleanup);
+-}
+-
+ function openPreferencesViaOpenPreferencesAPI(aPane, aAdvancedTab, aOptions) {
+   return new Promise(resolve => {
+     gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser, "about:blank");
+     openPreferences(aPane, aAdvancedTab ? {advancedTab: aAdvancedTab} : undefined);
+     let newTabBrowser = gBrowser.selectedBrowser;
+ 
+     newTabBrowser.addEventListener("Initialized", function() {
+       newTabBrowser.contentWindow.addEventListener("load", function() {
+@@ -227,24 +177,16 @@ function waitForCondition(aConditionFn, 
+     function tryAgain() {
+       setTimeout(tryNow, aCheckInterval);
+     }
+     let tries = 0;
+     tryAgain();
+   });
+ }
+ 
+-function promiseWindowDialogOpen(buttonAction, url) {
+-  return BrowserTestUtils.promiseAlertDialogOpen(buttonAction, url);
+-}
+-
+-function promiseAlertDialogOpen(buttonAction) {
+-  return BrowserTestUtils.promiseAlertDialogOpen(buttonAction);
+-}
+-
+ function openSettingsDialog() {
+   let win = gBrowser.selectedBrowser.contentWindow;
+   let doc = gBrowser.selectedBrowser.contentDocument;
+   let settingsBtn = doc.getElementById("siteDataSettings");
+   let dialogOverlay = win.gSubDialog._preloadDialog._overlay;
+   let dialogLoadPromise = promiseLoadSubDialog("chrome://browser/content/preferences/siteDataSettings.xul");
+   let dialogInitPromise = TestUtils.topicObserved("sitedata-settings-init", () => true);
+   let fullyLoadPromise = Promise.all([ dialogLoadPromise, dialogInitPromise ]).then(() => {
+diff --git a/browser/components/preferences/in-content/tests/offline/manifest.appcache b/browser/components/preferences/in-content/tests/siteData/offline/manifest.appcache
+rename from browser/components/preferences/in-content/tests/offline/manifest.appcache
+rename to browser/components/preferences/in-content/tests/siteData/offline/manifest.appcache
+diff --git a/browser/components/preferences/in-content/tests/offline/offline.html b/browser/components/preferences/in-content/tests/siteData/offline/offline.html
+rename from browser/components/preferences/in-content/tests/offline/offline.html
+rename to browser/components/preferences/in-content/tests/siteData/offline/offline.html
+diff --git a/browser/components/preferences/in-content/tests/service_worker_test.html b/browser/components/preferences/in-content/tests/siteData/service_worker_test.html
+rename from browser/components/preferences/in-content/tests/service_worker_test.html
+rename to browser/components/preferences/in-content/tests/siteData/service_worker_test.html
+diff --git a/browser/components/preferences/in-content/tests/service_worker_test.js b/browser/components/preferences/in-content/tests/siteData/service_worker_test.js
+rename from browser/components/preferences/in-content/tests/service_worker_test.js
+rename to browser/components/preferences/in-content/tests/siteData/service_worker_test.js
+diff --git a/browser/components/preferences/moz.build b/browser/components/preferences/moz.build
+--- a/browser/components/preferences/moz.build
++++ b/browser/components/preferences/moz.build
+@@ -6,17 +6,19 @@
+ 
+ DIRS += [
+ 	'in-content-new',
+ 	'in-content'
+ ]
+ 
+ BROWSER_CHROME_MANIFESTS += [
+     'in-content-new/tests/browser.ini',
+-    'in-content/tests/browser.ini'
++    'in-content-new/tests/siteData/browser.ini',
++    'in-content/tests/browser.ini',
++    'in-content/tests/siteData/browser.ini'
+ ]
+ 
+ for var in ('MOZ_APP_NAME', 'MOZ_MACBUNDLE_NAME'):
+     DEFINES[var] = CONFIG[var]
+ 
+ if CONFIG['MOZ_WIDGET_TOOLKIT'] in ('windows', 'gtk', 'cocoa'):
+     DEFINES['HAVE_SHELL_SERVICE'] = 1
+ 

+ 196 - 0
frg/mozilla-release/work-js/1445207-61a1.patch

@@ -0,0 +1,196 @@
+# HG changeset patch
+# User Olli Pettay <Olli.Pettay@helsinki.fi>
+# Date 1521828361 -7200
+# Node ID 342633799d0850f79cf27db8c826cdc4149a2cd9
+# Parent  3813d165f09467f72ff2b820b20a0b88b83df26d
+Bug 1445207, populate input type=date's .value when changing the value in the UI, r=mconley
+
+diff --git a/dom/html/test/forms/test_input_date_key_events.html b/dom/html/test/forms/test_input_date_key_events.html
+--- a/dom/html/test/forms/test_input_date_key_events.html
++++ b/dom/html/test/forms/test_input_date_key_events.html
+@@ -190,16 +190,22 @@ var testData = [
+     expectedVal: "2016-01-01"
+   },
+   {
+     // End key should have no effect on year field.
+     keys: ["VK_TAB", "VK_TAB", "VK_END"],
+     initialVal: "2016-01-01",
+     expectedVal: "2016-01-01"
+   },
++  {
++    // Incomplete value maps to empty .value.
++    keys: ["1111"],
++    initialVal: "",
++    expectedVal: ""
++  },
+ ];
+ 
+ function sendKeys(aKeys) {
+   for (let i = 0; i < aKeys.length; i++) {
+     let key = aKeys[i];
+     if (key.startsWith("VK")) {
+       synthesizeKey(key, {});
+     } else {
+@@ -210,20 +216,20 @@ function sendKeys(aKeys) {
+ 
+ function test() {
+   var elem = document.getElementById("input");
+ 
+   for (let { keys, initialVal, expectedVal } of testData) {
+     elem.focus();
+     elem.value = initialVal;
+     sendKeys(keys);
+-    elem.blur();
+     is(elem.value, expectedVal,
+        "Test with " + keys + ", result should be " + expectedVal);
+     elem.value = "";
++    elem.blur();
+   }
+ 
+   function chromeListener(e) {
+     ok(false, "Picker should not be opened when dispatching untrusted click.");
+   }
+   SpecialPowers.addChromeEventListener("MozOpenDateTimePicker",
+     chromeListener);
+   input.click();
+diff --git a/dom/html/test/forms/test_input_datetime_input_change_events.html b/dom/html/test/forms/test_input_datetime_input_change_events.html
+--- a/dom/html/test/forms/test_input_datetime_input_change_events.html
++++ b/dom/html/test/forms/test_input_datetime_input_change_events.html
+@@ -66,28 +66,29 @@ function test() {
+     is(input.value, expectedValues[i][0], "Check that value was set correctly (1).");
+     is(changeEvents[i], 1, "Change event should be dispatched (1).");
+     is(inputEvents[i], 1, "Input event should ne dispatched (1).");
+ 
+     // Test that change and input events are fired when changing the value with
+     // the keyboard.
+     synthesizeKey("0", {});
+     synthesizeKey("1", {});
++    // We get event per character.
+     is(input.value, expectedValues[i][1], "Check that value was set correctly (2).");
+-    is(changeEvents[i], 2, "Change event should be dispatched (2).");
+-    is(inputEvents[i], 2, "Input event should be dispatched (2).");
++    is(changeEvents[i], 3, "Change event should be dispatched (2).");
++    is(inputEvents[i], 3, "Input event should be dispatched (2).");
+ 
+     // Reset button is desktop only.
+     if (isDesktop) {
+       // Test that change and input events are fired when clearing the value using
+       // the reset button.
+       synthesizeMouse(input, resetButton_X, resetButton_Y, {});
+       is(input.value, "", "Check that value was set correctly (3).");
+-      is(changeEvents[i], 3, "Change event should be dispatched (3).");
+-      is(inputEvents[i], 3, "Input event should be dispatched (3).");
++      is(changeEvents[i], 4, "Change event should be dispatched (3).");
++      is(inputEvents[i], 4, "Input event should be dispatched (3).");
+     }
+   }
+ }
+ 
+ </script>
+ </pre>
+ </body>
+ </html>
+diff --git a/dom/html/test/forms/test_input_time_key_events.html b/dom/html/test/forms/test_input_time_key_events.html
+--- a/dom/html/test/forms/test_input_time_key_events.html
++++ b/dom/html/test/forms/test_input_time_key_events.html
+@@ -159,16 +159,22 @@ var testData = [
+     expectedVal: "16:00:00"
+   },
+   {
+     // End key on second field sets it to the minimum second, which is 59.
+     keys: ["VK_TAB", "VK_TAB", "VK_END"],
+     initialVal: "16:00:30",
+     expectedVal: "16:00:59"
+   },
++  {
++    // Incomplete value maps to empty .value.
++    keys: ["1"],
++    initialVal: "",
++    expectedVal: ""
++  },
+ ];
+ 
+ function sendKeys(aKeys) {
+   for (let i = 0; i < aKeys.length; i++) {
+     let key = aKeys[i];
+     if (key.startsWith("VK")) {
+       synthesizeKey(key, {});
+     } else {
+@@ -179,19 +185,19 @@ function sendKeys(aKeys) {
+ 
+ function test() {
+   var elem = document.getElementById("input");
+ 
+   for (let { keys, initialVal, expectedVal } of testData) {
+     elem.focus();
+     elem.value = initialVal;
+     sendKeys(keys);
+-    elem.blur();
+     is(elem.value, expectedVal,
+        "Test with " + keys + ", result should be " + expectedVal);
+     elem.value = "";
++    elem.blur();
+   }
+ }
+ 
+ </script>
+ </pre>
+ </body>
+ </html>
+diff --git a/toolkit/content/widgets/datetimebox.xml b/toolkit/content/widgets/datetimebox.xml
+--- a/toolkit/content/widgets/datetimebox.xml
++++ b/toolkit/content/widgets/datetimebox.xml
+@@ -248,16 +248,17 @@
+             let n = Number(buffer);
+             let max = targetField.getAttribute("max");
+             let maxLength = targetField.getAttribute("maxlength");
+             if (buffer.length >= maxLength || n * 10 > max) {
+               buffer = "";
+               this.advanceToNextField();
+             }
+             targetField.setAttribute("typeBuffer", buffer);
++            this.setInputValueFromFields();
+           }
+         ]]>
+         </body>
+       </method>
+ 
+       <method name="incrementFieldValue">
+         <parameter name="aTargetField"/>
+         <parameter name="aTimes"/>
+@@ -1024,16 +1025,17 @@
+ 
+           if (this.hasDayPeriodField() &&
+               targetField == this.mDayPeriodField) {
+             if (key == "a" || key == "A") {
+               this.setDayPeriodValue(this.mAMIndicator);
+             } else if (key == "p" || key == "P") {
+               this.setDayPeriodValue(this.mPMIndicator);
+             }
++            this.setInputValueFromFields();
+             return;
+           }
+ 
+           if (targetField.classList.contains("numeric") && key.match(/[0-9]/)) {
+             let buffer = targetField.getAttribute("typeBuffer") || "";
+ 
+             buffer = buffer.concat(key);
+             this.setFieldValue(targetField, buffer);
+@@ -1041,16 +1043,17 @@
+             let n = Number(buffer);
+             let max = targetField.getAttribute("max");
+             let maxLength = targetField.getAttribute("maxlength");
+             if (buffer.length >= maxLength || n * 10 > max) {
+               buffer = "";
+               this.advanceToNextField();
+             }
+             targetField.setAttribute("typeBuffer", buffer);
++            this.setInputValueFromFields();
+           }
+         ]]>
+         </body>
+       </method>
+ 
+       <method name="setFieldValue">
+        <parameter name="aField"/>
+        <parameter name="aValue"/>

+ 35 - 0
frg/mozilla-release/work-js/1445837-61a1.patch

@@ -0,0 +1,35 @@
+# HG changeset patch
+# User Matheus Longaray <mlongaray@hp.com>
+# Date 1521476828 10800
+# Node ID d2c963c5022507cb99c74c4bcba9d2c596b4339e
+# Parent  7dfc9e876adf41bc3aa0fef273cfc82fd4d0b2ff
+Bug 1445837 - Fix race condition in toolkit/components/printing/tests/browser_preview_print_simplify_non_article.js. r=mconley
+
+This patch address a race condition issue for toolkit/components/printing/tests/
+browser_preview_print_simplify_non_article.js test added in bug 1440638.
+
+MozReview-Commit-ID: 4X17u2CP4ea
+
+diff --git a/toolkit/components/printing/tests/browser_preview_print_simplify_non_article.js b/toolkit/components/printing/tests/browser_preview_print_simplify_non_article.js
+--- a/toolkit/components/printing/tests/browser_preview_print_simplify_non_article.js
++++ b/toolkit/components/printing/tests/browser_preview_print_simplify_non_article.js
+@@ -60,17 +60,18 @@ add_task(async function switch_print_pre
+   await simplifiedPPEntered;
+ 
+   // Assert that simplify page option is checked
+   is(printPreviewToolbar.mSimplifyPageCheckbox.checked, true,
+      "Should have simplify page option checked");
+ 
+   // Assert that we are showing recovery content on simplified print preview browser
+   await ContentTask.spawn(simplifiedPPBrowser, null, async function() {
+-    is(content.document.title, "Failed to load article from page", "Should have recovery content.");
++    await ContentTaskUtils.waitForCondition(() => content.document.title === "Failed to load article from page",
++      "Simplified document title should be updated with recovery title.");
+   });
+ 
+   // Assert that we are selecting simplified print preview browser, and not default one
+   is(gBrowser.selectedTab.linkedBrowser, simplifiedPPBrowser,
+      "Should have simplified print preview browser selected");
+   isnot(gBrowser.selectedTab.linkedBrowser, defaultPPBrowser,
+         "Should not have default print preview browser selected");
+ 

+ 689 - 0
frg/mozilla-release/work-js/1446342-61a1.patch

@@ -0,0 +1,689 @@
+# HG changeset patch
+# User Emilio Cobos Alvarez <emilio@crisal.io>
+# Date 1521412907 -3600
+# Node ID 05270b574f0085299f8d0f3477a8ad7ba939196c
+# Parent  1fe192e4d736c7de3e3c5df56b7c57e1a58419ef
+Bug 1446342: Don't include forms in the scope chain for XBL datetime bindings. r=smaug
+
+Reviewers: smaug
+
+Bug #: 1446342
+
+Differential Revision: https://phabricator.services.mozilla.com/D769
+
+MozReview-Commit-ID: HK7nChYf0X6
+
+diff --git a/dom/base/nsGkAtomList.h b/dom/base/nsGkAtomList.h
+--- a/dom/base/nsGkAtomList.h
++++ b/dom/base/nsGkAtomList.h
+@@ -1662,16 +1662,17 @@ GK_ATOM(requiredFeatures, "requiredFeatu
+ GK_ATOM(rotate, "rotate")
+ GK_ATOM(rx, "rx")
+ GK_ATOM(ry, "ry")
+ GK_ATOM(saturate, "saturate")
+ GK_ATOM(saturation, "saturation")
+ //GK_ATOM(set, "set")  # "set" is present below
+ GK_ATOM(seed, "seed")
+ GK_ATOM(shape_rendering, "shape-rendering")
++GK_ATOM(simpleScopeChain, "simpleScopeChain")
+ GK_ATOM(skewX, "skewX")
+ GK_ATOM(skewY, "skewY")
+ GK_ATOM(slope, "slope")
+ GK_ATOM(slot, "slot")
+ GK_ATOM(softLight, "soft-light")
+ GK_ATOM(spacing, "spacing")
+ GK_ATOM(spacingAndGlyphs, "spacingAndGlyphs")
+ GK_ATOM(specularConstant, "specularConstant")
+diff --git a/dom/base/nsJSUtils.cpp b/dom/base/nsJSUtils.cpp
+--- a/dom/base/nsJSUtils.cpp
++++ b/dom/base/nsJSUtils.cpp
+@@ -21,16 +21,17 @@
+ #include "nsCOMPtr.h"
+ #include "nsIScriptSecurityManager.h"
+ #include "nsPIDOMWindow.h"
+ #include "GeckoProfiler.h"
+ #include "nsJSPrincipals.h"
+ #include "xpcpublic.h"
+ #include "nsContentUtils.h"
+ #include "nsGlobalWindow.h"
++#include "nsXBLPrototypeBinding.h"
+ #include "mozilla/CycleCollectedJSContext.h"
+ #include "mozilla/dom/BindingUtils.h"
+ #include "mozilla/dom/Date.h"
+ #include "mozilla/dom/Element.h"
+ #include "mozilla/dom/ScriptSettings.h"
+ 
+ using namespace mozilla;
+ using namespace mozilla::dom;
+@@ -458,37 +459,74 @@ nsJSUtils::ModuleEvaluate(JSContext* aCx
+ 
+   if (!JS::ModuleEvaluate(aCx, aModule)) {
+     return NS_ERROR_FAILURE;
+   }
+ 
+   return NS_OK;
+ }
+ 
++static bool
++AddScopeChainItem(JSContext* aCx,
++                  nsINode* aNode,
++                  JS::AutoObjectVector& aScopeChain)
++{
++  JS::RootedValue val(aCx);
++  if (!GetOrCreateDOMReflector(aCx, aNode, &val)) {
++    return false;
++  }
++
++  if (!aScopeChain.append(&val.toObject())) {
++    return false;
++  }
++
++  return true;
++}
++
+ /* static */
+ bool
+ nsJSUtils::GetScopeChainForElement(JSContext* aCx,
+-                                   mozilla::dom::Element* aElement,
++                                   Element* aElement,
+                                    JS::AutoObjectVector& aScopeChain)
+ {
+   for (nsINode* cur = aElement; cur; cur = cur->GetScopeChainParent()) {
+-    JS::RootedValue val(aCx);
+-    if (!GetOrCreateDOMReflector(aCx, cur, &val)) {
+-      return false;
+-    }
+-
+-    if (!aScopeChain.append(&val.toObject())) {
++    if (!AddScopeChainItem(aCx, cur, aScopeChain)) {
+       return false;
+     }
+   }
+ 
+   return true;
+ }
+ 
+ /* static */
++bool
++nsJSUtils::GetScopeChainForXBL(JSContext* aCx,
++                               Element* aElement,
++                               const nsXBLPrototypeBinding& aProtoBinding,
++                               JS::AutoObjectVector& aScopeChain)
++{
++  if (!aElement) {
++    return true;
++  }
++
++  if (!aProtoBinding.SimpleScopeChain()) {
++    return GetScopeChainForElement(aCx, aElement, aScopeChain);
++  }
++
++  if (!AddScopeChainItem(aCx, aElement, aScopeChain)) {
++    return false;
++  }
++
++  if (!AddScopeChainItem(aCx, aElement->OwnerDoc(), aScopeChain)) {
++    return false;
++  }
++  return true;
++}
++
++/* static */
+ void
+ nsJSUtils::ResetTimeZone()
+ {
+   JS::ResetTimeZone();
+ }
+ 
+ //
+ // nsDOMJSUtils.h
+diff --git a/dom/base/nsJSUtils.h b/dom/base/nsJSUtils.h
+--- a/dom/base/nsJSUtils.h
++++ b/dom/base/nsJSUtils.h
+@@ -20,16 +20,17 @@
+ #include "jsapi.h"
+ #include "jsfriendapi.h"
+ #include "js/Conversions.h"
+ #include "nsString.h"
+ 
+ class nsIScriptContext;
+ class nsIScriptElement;
+ class nsIScriptGlobalObject;
++class nsXBLPrototypeBinding;
+ 
+ namespace mozilla {
+ namespace dom {
+ class AutoJSAPI;
+ class Element;
+ } // namespace dom
+ } // namespace mozilla
+ 
+@@ -197,16 +198,27 @@ public:
+                                  JS::Handle<JSObject*> aModule);
+ 
+   // Returns false if an exception got thrown on aCx.  Passing a null
+   // aElement is allowed; that wil produce an empty aScopeChain.
+   static bool GetScopeChainForElement(JSContext* aCx,
+                                       mozilla::dom::Element* aElement,
+                                       JS::AutoObjectVector& aScopeChain);
+ 
++  // Returns a scope chain suitable for XBL execution.
++  //
++  // This is by default GetScopeChainForElemenet, but will be different if the
++  // <binding> element had the simpleScopeChain attribute.
++  //
++  // This is to prevent footguns like bug 1446342.
++  static bool GetScopeChainForXBL(JSContext* aCx,
++                                  mozilla::dom::Element* aBoundElement,
++                                  const nsXBLPrototypeBinding& aProtoBinding,
++                                  JS::AutoObjectVector& aScopeChain);
++
+   static void ResetTimeZone();
+ };
+ 
+ template<typename T>
+ inline bool
+ AssignJSString(JSContext *cx, T &dest, JSString *s)
+ {
+   size_t len = js::GetStringLength(s);
+diff --git a/dom/xbl/nsXBLProtoImplField.cpp b/dom/xbl/nsXBLProtoImplField.cpp
+--- a/dom/xbl/nsXBLProtoImplField.cpp
++++ b/dom/xbl/nsXBLProtoImplField.cpp
+@@ -203,17 +203,17 @@ InstallXBLField(JSContext* cx,
+     JS::Value slotVal = ::JS_GetReservedSlot(xblProto, 0);
+     protoBinding = static_cast<nsXBLPrototypeBinding*>(slotVal.toPrivate());
+     MOZ_ASSERT(protoBinding);
+   }
+ 
+   nsXBLProtoImplField* field = protoBinding->FindField(fieldName);
+   MOZ_ASSERT(field);
+ 
+-  nsresult rv = field->InstallField(thisObj, protoBinding->DocURI(), installed);
++  nsresult rv = field->InstallField(thisObj, *protoBinding, installed);
+   if (NS_SUCCEEDED(rv)) {
+     return true;
+   }
+ 
+   if (!::JS_IsExceptionPending(cx)) {
+     xpc::Throw(cx, rv);
+   }
+   return false;
+@@ -368,34 +368,34 @@ nsXBLProtoImplField::InstallAccessors(JS
+     return NS_ERROR_OUT_OF_MEMORY;
+   }
+ 
+   return NS_OK;
+ }
+ 
+ nsresult
+ nsXBLProtoImplField::InstallField(JS::Handle<JSObject*> aBoundNode,
+-                                  nsIURI* aBindingDocURI,
++                                  const nsXBLPrototypeBinding& aProtoBinding,
+                                   bool* aDidInstall) const
+ {
+   NS_PRECONDITION(aBoundNode,
+                   "uh-oh, bound node should NOT be null or bad things will "
+                   "happen");
+ 
+   *aDidInstall = false;
+ 
+   // Empty fields are treated as not actually present.
+   if (IsEmpty()) {
+     return NS_OK;
+   }
+ 
+   nsAutoMicroTask mt;
+ 
+   nsAutoCString uriSpec;
+-  nsresult rv = aBindingDocURI->GetSpec(uriSpec);
++  nsresult rv = aProtoBinding.DocURI()->GetSpec(uriSpec);
+   if (NS_WARN_IF(NS_FAILED(rv))) {
+     return rv;
+   }
+ 
+   nsIGlobalObject* globalObject = xpc::WindowGlobalOrNull(aBoundNode);
+   if (!globalObject) {
+     return NS_OK;
+   }
+@@ -407,17 +407,17 @@ nsXBLProtoImplField::InstallField(JS::Ha
+   // to be our entry global.
+   AutoJSAPI jsapi;
+   if (!jsapi.Init(globalObject)) {
+     return NS_ERROR_UNEXPECTED;
+   }
+   MOZ_ASSERT(!::JS_IsExceptionPending(jsapi.cx()),
+              "Shouldn't get here when an exception is pending!");
+ 
+-  JSAddonId* addonId = MapURIToAddonID(aBindingDocURI);
++  JSAddonId* addonId = MapURIToAddonID(aProtoBinding.DocURI());
+ 
+   // Note: the UNWRAP_OBJECT may mutate boundNode; don't use it after that call.
+   JS::Rooted<JSObject*> boundNode(jsapi.cx(), aBoundNode);
+   Element* boundElement = nullptr;
+   rv = UNWRAP_OBJECT(Element, &boundNode, boundElement);
+   if (NS_WARN_IF(NS_FAILED(rv))) {
+     return rv;
+   }
+@@ -430,17 +430,17 @@ nsXBLProtoImplField::InstallField(JS::Ha
+ 
+   AutoEntryScript aes(scopeObject, "XBL <field> initialization", true);
+   JSContext* cx = aes.cx();
+ 
+   JS::Rooted<JS::Value> result(cx);
+   JS::CompileOptions options(cx);
+   options.setFileAndLine(uriSpec.get(), mLineNumber);
+   JS::AutoObjectVector scopeChain(cx);
+-  if (!nsJSUtils::GetScopeChainForElement(cx, boundElement, scopeChain)) {
++  if (!nsJSUtils::GetScopeChainForXBL(cx, boundElement, aProtoBinding, scopeChain)) {
+     return NS_ERROR_OUT_OF_MEMORY;
+   }
+   rv = NS_OK;
+   {
+     nsJSUtils::ExecutionContext exec(cx, scopeObject);
+     exec.SetScopeChain(scopeChain);
+     exec.CompileAndExec(options, nsDependentString(mFieldText,
+                                                    mFieldTextLength));
+diff --git a/dom/xbl/nsXBLProtoImplField.h b/dom/xbl/nsXBLProtoImplField.h
+--- a/dom/xbl/nsXBLProtoImplField.h
++++ b/dom/xbl/nsXBLProtoImplField.h
+@@ -28,17 +28,17 @@ public:
+   void SetLineNumber(uint32_t aLineNumber) {
+     mLineNumber = aLineNumber;
+   }
+ 
+   nsXBLProtoImplField* GetNext() const { return mNext; }
+   void SetNext(nsXBLProtoImplField* aNext) { mNext = aNext; }
+ 
+   nsresult InstallField(JS::Handle<JSObject*> aBoundNode,
+-                        nsIURI* aBindingDocURI,
++                        const nsXBLPrototypeBinding& aProtoBinding,
+                         bool* aDidInstall) const;
+ 
+   nsresult InstallAccessors(JSContext* aCx,
+                             JS::Handle<JSObject*> aTargetClassObject);
+ 
+   nsresult Read(nsIObjectInputStream* aStream);
+   nsresult Write(nsIObjectOutputStream* aStream);
+ 
+diff --git a/dom/xbl/nsXBLProtoImplMethod.cpp b/dom/xbl/nsXBLProtoImplMethod.cpp
+--- a/dom/xbl/nsXBLProtoImplMethod.cpp
++++ b/dom/xbl/nsXBLProtoImplMethod.cpp
+@@ -255,17 +255,17 @@ nsXBLProtoImplMethod::Write(nsIObjectOut
+     JS::Rooted<JSObject*> method(RootingCx(), GetCompiledMethod());
+     return XBL_SerializeFunction(aStream, method);
+   }
+ 
+   return NS_OK;
+ }
+ 
+ nsresult
+-nsXBLProtoImplAnonymousMethod::Execute(nsIContent* aBoundElement, JSAddonId* aAddonId)
++nsXBLProtoImplAnonymousMethod::Execute(nsIContent* aBoundElement, JSAddonId* aAddonId, const nsXBLPrototypeBinding& aProtoBinding)
+ {
+   MOZ_ASSERT(aBoundElement->IsElement());
+   NS_PRECONDITION(IsCompiled(), "Can't execute uncompiled method");
+ 
+   if (!GetCompiledMethod()) {
+     // Nothing to do here
+     return NS_OK;
+   }
+@@ -298,18 +298,19 @@ nsXBLProtoImplAnonymousMethod::Execute(n
+     xpc::GetScopeForXBLExecution(jsapi.cx(), globalObject, aAddonId));
+   NS_ENSURE_TRUE(scopeObject, NS_ERROR_OUT_OF_MEMORY);
+ 
+   dom::AutoEntryScript aes(scopeObject,
+                            "XBL <constructor>/<destructor> invocation",
+                            true);
+   JSContext* cx = aes.cx();
+   JS::AutoObjectVector scopeChain(cx);
+-  if (!nsJSUtils::GetScopeChainForElement(cx, aBoundElement->AsElement(),
+-                                          scopeChain)) {
++  if (!nsJSUtils::GetScopeChainForXBL(cx, aBoundElement->AsElement(),
++                                      aProtoBinding,
++                                      scopeChain)) {
+     return NS_ERROR_OUT_OF_MEMORY;
+   }
+   MOZ_ASSERT(scopeChain.length() != 0);
+ 
+   // Clone the function object, using our scope chain (for backwards
+   // compat to the days when this was an event handler).
+   JS::Rooted<JSObject*> jsMethodObject(cx, GetCompiledMethod());
+   JS::Rooted<JSObject*> method(cx, JS::CloneFunctionObject(cx, jsMethodObject,
+diff --git a/dom/xbl/nsXBLProtoImplMethod.h b/dom/xbl/nsXBLProtoImplMethod.h
+--- a/dom/xbl/nsXBLProtoImplMethod.h
++++ b/dom/xbl/nsXBLProtoImplMethod.h
+@@ -133,17 +133,17 @@ protected:
+ };
+ 
+ class nsXBLProtoImplAnonymousMethod : public nsXBLProtoImplMethod {
+ public:
+   explicit nsXBLProtoImplAnonymousMethod(const char16_t* aName) :
+     nsXBLProtoImplMethod(aName)
+   {}
+ 
+-  nsresult Execute(nsIContent* aBoundElement, JSAddonId* aAddonId);
++  nsresult Execute(nsIContent* aBoundElement, JSAddonId* aAddonId, const nsXBLPrototypeBinding&);
+ 
+   // Override InstallMember; these methods never get installed as members on
+   // binding instantiations (though they may hang out in mMembers on the
+   // prototype implementation).
+   virtual nsresult InstallMember(JSContext* aCx,
+                                  JS::Handle<JSObject*> aTargetClassObject) override {
+     return NS_OK;
+   }
+diff --git a/dom/xbl/nsXBLPrototypeBinding.cpp b/dom/xbl/nsXBLPrototypeBinding.cpp
+--- a/dom/xbl/nsXBLPrototypeBinding.cpp
++++ b/dom/xbl/nsXBLPrototypeBinding.cpp
+@@ -101,16 +101,17 @@ protected:
+ nsXBLPrototypeBinding::nsXBLPrototypeBinding()
+ : mImplementation(nullptr),
+   mBaseBinding(nullptr),
+   mInheritStyle(true),
+   mCheckedBaseProto(false),
+   mKeyHandlersRegistered(false),
+   mChromeOnlyContent(false),
+   mBindToUntrustedContent(false),
++  mSimpleScopeChain(false),
+   mResources(nullptr),
+   mBaseNameSpaceID(kNameSpaceID_None)
+ {
+   MOZ_COUNT_CTOR(nsXBLPrototypeBinding);
+ }
+ 
+ nsresult
+ nsXBLPrototypeBinding::Init(const nsACString& aID,
+@@ -217,16 +218,21 @@ nsXBLPrototypeBinding::SetBindingElement
+ 
+   mChromeOnlyContent = mBinding->AttrValueIs(kNameSpaceID_None,
+                                              nsGkAtoms::chromeOnlyContent,
+                                              nsGkAtoms::_true, eCaseMatters);
+ 
+   mBindToUntrustedContent = mBinding->AttrValueIs(kNameSpaceID_None,
+                                                   nsGkAtoms::bindToUntrustedContent,
+                                                   nsGkAtoms::_true, eCaseMatters);
++
++  // TODO(emilio): Should we imply mBindToUntrustedContent -> mSimpleScopeChain?
++  mSimpleScopeChain = mBinding->AttrValueIs(kNameSpaceID_None,
++                                            nsGkAtoms::simpleScopeChain,
++                                            nsGkAtoms::_true, eCaseMatters);
+ }
+ 
+ bool
+ nsXBLPrototypeBinding::GetAllowScripts() const
+ {
+   return mXBLDocInfoWeak->GetScriptAccess();
+ }
+ 
+@@ -257,26 +263,26 @@ nsXBLPrototypeBinding::FlushSkinSheets()
+   return NS_OK;
+ }
+ 
+ nsresult
+ nsXBLPrototypeBinding::BindingAttached(nsIContent* aBoundElement)
+ {
+   if (mImplementation && mImplementation->CompiledMembers() &&
+       mImplementation->mConstructor)
+-    return mImplementation->mConstructor->Execute(aBoundElement, MapURIToAddonID(mBindingURI));
++    return mImplementation->mConstructor->Execute(aBoundElement, MapURIToAddonID(mBindingURI), *this);
+   return NS_OK;
+ }
+ 
+ nsresult
+ nsXBLPrototypeBinding::BindingDetached(nsIContent* aBoundElement)
+ {
+   if (mImplementation && mImplementation->CompiledMembers() &&
+       mImplementation->mDestructor)
+-    return mImplementation->mDestructor->Execute(aBoundElement, MapURIToAddonID(mBindingURI));
++    return mImplementation->mDestructor->Execute(aBoundElement, MapURIToAddonID(mBindingURI), *this);
+   return NS_OK;
+ }
+ 
+ nsXBLProtoImplAnonymousMethod*
+ nsXBLPrototypeBinding::GetConstructor()
+ {
+   if (mImplementation)
+     return mImplementation->mConstructor;
+@@ -842,16 +848,18 @@ nsXBLPrototypeBinding::Read(nsIObjectInp
+                             nsIDocument* aDocument,
+                             uint8_t aFlags)
+ {
+   mInheritStyle = (aFlags & XBLBinding_Serialize_InheritStyle) ? true : false;
+   mChromeOnlyContent =
+     (aFlags & XBLBinding_Serialize_ChromeOnlyContent) ? true : false;
+   mBindToUntrustedContent =
+     (aFlags & XBLBinding_Serialize_BindToUntrustedContent) ? true : false;
++  mSimpleScopeChain =
++    (aFlags & XBLBinding_Serialize_SimpleScopeChain) ? true : false;
+ 
+   // nsXBLContentSink::ConstructBinding doesn't create a binding with an empty
+   // id, so we don't here either.
+   nsAutoCString id;
+   nsresult rv = aStream->ReadCString(id);
+ 
+   NS_ENSURE_SUCCESS(rv, rv);
+   NS_ENSURE_TRUE(!id.IsEmpty(), NS_ERROR_FAILURE);
+@@ -1065,16 +1073,20 @@ nsXBLPrototypeBinding::Write(nsIObjectOu
+   if (mChromeOnlyContent) {
+     flags |= XBLBinding_Serialize_ChromeOnlyContent;
+   }
+ 
+   if (mBindToUntrustedContent) {
+     flags |= XBLBinding_Serialize_BindToUntrustedContent;
+   }
+ 
++  if (mSimpleScopeChain) {
++    flags |= XBLBinding_Serialize_SimpleScopeChain;
++  }
++
+   nsresult rv = aStream->Write8(flags);
+   NS_ENSURE_SUCCESS(rv, rv);
+ 
+   nsAutoCString id;
+   mBindingURI->GetRef(id);
+   rv = aStream->WriteStringZ(id.get());
+   NS_ENSURE_SUCCESS(rv, rv);
+ 
+diff --git a/dom/xbl/nsXBLPrototypeBinding.h b/dom/xbl/nsXBLPrototypeBinding.h
+--- a/dom/xbl/nsXBLPrototypeBinding.h
++++ b/dom/xbl/nsXBLPrototypeBinding.h
+@@ -264,18 +264,19 @@ public:
+    * has the localname given by aTag and is in the XBL namespace.
+    */
+   mozilla::dom::Element* GetImmediateChild(nsIAtom* aTag);
+   mozilla::dom::Element* LocateInstance(mozilla::dom::Element* aBoundElt,
+                                         nsIContent* aTemplRoot,
+                                         nsIContent* aCopyRoot,
+                                         mozilla::dom::Element* aTemplChild);
+ 
+-  bool ChromeOnlyContent() { return mChromeOnlyContent; }
+-  bool BindToUntrustedContent() { return mBindToUntrustedContent; }
++  bool ChromeOnlyContent() const { return mChromeOnlyContent; }
++  bool SimpleScopeChain() const { return mSimpleScopeChain; }
++  bool BindToUntrustedContent() const { return mBindToUntrustedContent; }
+ 
+   typedef nsClassHashtable<nsISupportsHashKey, nsXBLAttributeEntry> InnerAttributeTable;
+ 
+ protected:
+   // Ensure that mAttributeTable has been created.
+   void EnsureAttributeTable();
+   // Ad an entry to the attribute table
+   void AddToAttributeTable(int32_t aSourceNamespaceID, nsIAtom* aSourceTag,
+@@ -302,16 +303,19 @@ protected:
+ 
+   // Weak.  The docinfo will own our base binding.
+   mozilla::WeakPtr<nsXBLPrototypeBinding> mBaseBinding;
+   bool mInheritStyle;
+   bool mCheckedBaseProto;
+   bool mKeyHandlersRegistered;
+   bool mChromeOnlyContent;
+   bool mBindToUntrustedContent;
++  // True if constructors, handlers, etc for this binding would skip the scope
++  // chain for parent elements and go directly to the document.
++  bool mSimpleScopeChain;
+ 
+   nsAutoPtr<nsXBLPrototypeResources> mResources; // If we have any resources, this will be non-null.
+ 
+   nsXBLDocumentInfo* mXBLDocInfoWeak; // A pointer back to our doc info.  Weak, since it owns us.
+ 
+   // A table for attribute containers. Namespace IDs are used as
+   // keys in the table. Containers are InnerAttributeTables.
+   // This table is used to efficiently handle attribute changes.
+diff --git a/dom/xbl/nsXBLPrototypeHandler.cpp b/dom/xbl/nsXBLPrototypeHandler.cpp
+--- a/dom/xbl/nsXBLPrototypeHandler.cpp
++++ b/dom/xbl/nsXBLPrototypeHandler.cpp
+@@ -363,17 +363,17 @@ nsXBLPrototypeHandler::ExecuteHandler(Ev
+   JS::Rooted<JSObject*> genericHandler(cx, handler.get());
+   bool ok = JS_WrapObject(cx, &genericHandler);
+   NS_ENSURE_TRUE(ok, NS_ERROR_OUT_OF_MEMORY);
+   MOZ_ASSERT(!js::IsCrossCompartmentWrapper(genericHandler));
+ 
+   // Build a scope chain in the XBL scope.
+   RefPtr<Element> targetElement = do_QueryObject(scriptTarget);
+   JS::AutoObjectVector scopeChain(cx);
+-  ok = nsJSUtils::GetScopeChainForElement(cx, targetElement, scopeChain);
++  ok = nsJSUtils::GetScopeChainForXBL(cx, targetElement, *mPrototypeBinding, scopeChain);
+   NS_ENSURE_TRUE(ok, NS_ERROR_OUT_OF_MEMORY);
+ 
+   // Next, clone the generic handler with our desired scope chain.
+   JS::Rooted<JSObject*> bound(cx, JS::CloneFunctionObject(cx, genericHandler,
+                                                           scopeChain));
+   NS_ENSURE_TRUE(bound, NS_ERROR_FAILURE);
+ 
+   RefPtr<EventHandlerNonNull> handlerCallback =
+diff --git a/dom/xbl/nsXBLSerialize.h b/dom/xbl/nsXBLSerialize.h
+--- a/dom/xbl/nsXBLSerialize.h
++++ b/dom/xbl/nsXBLSerialize.h
+@@ -11,29 +11,32 @@
+ #include "nsIObjectOutputStream.h"
+ #include "mozilla/dom/NameSpaceConstants.h"
+ #include "js/TypeDecls.h"
+ 
+ typedef uint8_t XBLBindingSerializeDetails;
+ 
+ // A version number to ensure we don't load cached data in a different
+ // file format.
+-#define XBLBinding_Serialize_Version 0x00000004
++#define XBLBinding_Serialize_Version 0x00000005
+ 
+ // Set for the first binding in a document
+-#define XBLBinding_Serialize_IsFirstBinding 1
++#define XBLBinding_Serialize_IsFirstBinding (1 << 0)
+ 
+ // Set to indicate that nsXBLPrototypeBinding::mInheritStyle should be true
+-#define XBLBinding_Serialize_InheritStyle 2
++#define XBLBinding_Serialize_InheritStyle (1 << 1)
+ 
+ // Set to indicate that nsXBLPrototypeBinding::mChromeOnlyContent should be true
+-#define XBLBinding_Serialize_ChromeOnlyContent 4
++#define XBLBinding_Serialize_ChromeOnlyContent (1 << 2)
+ 
+ // Set to indicate that nsXBLPrototypeBinding::mBindToUntrustedContent should be true
+-#define XBLBinding_Serialize_BindToUntrustedContent 8
++#define XBLBinding_Serialize_BindToUntrustedContent (1 << 3)
++
++// Set to indicate that nsXBLPrototypeBinding::mSimpleScopeChain should be true
++#define XBLBinding_Serialize_SimpleScopeChain (1 << 4)
+ 
+ // Appears at the end of the serialized data to indicate that no more bindings
+ // are present for this document.
+ #define XBLBinding_Serialize_NoMoreBindings 0x80
+ 
+ // Implementation member types. The serialized value for each member contains one
+ // of these values, combined with the read-only flag XBLBinding_Serialize_ReadOnly.
+ // Use XBLBinding_Serialize_Mask to filter out the read-only flag and check for
+diff --git a/dom/xbl/test/file_fieldScopeChain.xml b/dom/xbl/test/file_fieldScopeChain.xml
+--- a/dom/xbl/test/file_fieldScopeChain.xml
++++ b/dom/xbl/test/file_fieldScopeChain.xml
+@@ -1,8 +1,13 @@
+ <bindings xmlns="http://www.mozilla.org/xbl"
+           xmlns:html="http://www.w3.org/1999/xhtml">
+-  <binding id="foo">
++  <binding id="foo" simpleScopeChain="true">
+     <implementation>
+       <field name="bar">baz</field>
++      <constructor>
++      <![CDATA[
++        is(document, this.ownerDocument, "Shouldn't have forms in the scope chain");
++      ]]>
++      </constructor>
+     </implementation>
+   </binding>
+ </bindings>
+diff --git a/dom/xbl/test/test_fieldScopeChain.html b/dom/xbl/test/test_fieldScopeChain.html
+--- a/dom/xbl/test/test_fieldScopeChain.html
++++ b/dom/xbl/test/test_fieldScopeChain.html
+@@ -24,11 +24,15 @@ https://bugzilla.mozilla.org/show_bug.cg
+ </head>
+ <body>
+ <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1095660">Mozilla Bug </a>
+ <p id="display"></p>
+ <div id="content" style="display: none">
+ 
+ </div>
+ <pre id="test" style="-moz-binding: url(file_fieldScopeChain.xml#foo)">
++<form>
++  <input name="document">
++  <input type="text" id="input-test" style="-moz-binding: url(file_fieldScopeChain.xml#foo)">
++</form>
+ </pre>
+ </body>
+ </html>
+diff --git a/toolkit/content/widgets/datetimebox.xml b/toolkit/content/widgets/datetimebox.xml
+--- a/toolkit/content/widgets/datetimebox.xml
++++ b/toolkit/content/widgets/datetimebox.xml
+@@ -11,16 +11,17 @@
+ 
+ <bindings id="datetimeboxBindings"
+    xmlns="http://www.mozilla.org/xbl"
+    xmlns:html="http://www.w3.org/1999/xhtml"
+    xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+    xmlns:xbl="http://www.mozilla.org/xbl">
+ 
+   <binding id="date-input"
++           simpleScopeChain="true"
+            extends="chrome://global/content/bindings/datetimebox.xml#datetime-input-base">
+     <resources>
+       <stylesheet src="chrome://global/content/textbox.css"/>
+       <stylesheet src="chrome://global/skin/textbox.css"/>
+       <stylesheet src="chrome://global/content/bindings/datetimebox.css"/>
+     </resources>
+ 
+     <implementation>
+@@ -423,16 +424,17 @@
+         ]]>
+         </body>
+       </method>
+ 
+     </implementation>
+   </binding>
+ 
+   <binding id="time-input"
++           simpleScopeChain="true"
+            extends="chrome://global/content/bindings/datetimebox.xml#datetime-input-base">
+     <resources>
+       <stylesheet src="chrome://global/content/textbox.css"/>
+       <stylesheet src="chrome://global/skin/textbox.css"/>
+       <stylesheet src="chrome://global/content/bindings/datetimebox.css"/>
+     </resources>
+ 
+     <implementation>
+@@ -1187,17 +1189,18 @@
+           this.log("getCurrentValue: " + JSON.stringify(time));
+           return time;
+         ]]>
+         </body>
+       </method>
+     </implementation>
+   </binding>
+ 
+-  <binding id="datetime-input-base">
++  <binding id="datetime-input-base"
++           simpleScopeChain="true">
+     <resources>
+       <stylesheet src="chrome://global/content/textbox.css"/>
+       <stylesheet src="chrome://global/skin/textbox.css"/>
+       <stylesheet src="chrome://global/content/bindings/datetimebox.css"/>
+     </resources>
+ 
+     <content>
+       <html:div class="datetime-input-box-wrapper" anonid="input-box-wrapper"

+ 49 - 0
frg/mozilla-release/work-js/1447262-61a1.patch

@@ -0,0 +1,49 @@
+# HG changeset patch
+# User Andrea Marchesini <amarchesini@mozilla.com>
+# Date 1521613446 -3600
+# Node ID c4d938fa8c8bc7cd2538dee686516004f0ffbf24
+# Parent  3f12a3dafb3e67dbac4ae719e7e51c985297322e
+Bug 1447262 - BlobURLs must be mutable, r=valentin
+
+diff --git a/dom/file/nsHostObjectProtocolHandler.cpp b/dom/file/nsHostObjectProtocolHandler.cpp
+--- a/dom/file/nsHostObjectProtocolHandler.cpp
++++ b/dom/file/nsHostObjectProtocolHandler.cpp
+@@ -894,17 +894,16 @@ nsHostObjectProtocolHandler::NewURI(cons
+     uri = new nsHostObjectURI(info->mPrincipal, info->mBlobImpl);
+   } else {
+     uri = new nsHostObjectURI(nullptr, nullptr);
+   }
+ 
+   rv = uri->SetSpec(aSpec);
+   NS_ENSURE_SUCCESS(rv, rv);
+ 
+-  NS_TryToSetImmutable(uri);
+   uri.forget(aResult);
+ 
+   if (info && info->mObjectType == DataInfo::eBlobImpl) {
+     info->mURIs.AppendElement(do_GetWeakReference(*aResult));
+   }
+ 
+   return NS_OK;
+ }
+diff --git a/dom/file/nsHostObjectURI.cpp b/dom/file/nsHostObjectURI.cpp
+--- a/dom/file/nsHostObjectURI.cpp
++++ b/dom/file/nsHostObjectURI.cpp
+@@ -184,17 +184,16 @@ nsHostObjectURI::CloneInternal(mozilla::
+ 
+   nsHostObjectURI* u = static_cast<nsHostObjectURI*>(simpleClone.get());
+ 
+   u->mPrincipal = mPrincipal;
+   u->mBlobImpl = mBlobImpl;
+ 
+   nsHostObjectProtocolHandler::StoreClonedURI(newRef, simpleClone);
+ 
+-  NS_TryToSetImmutable(simpleClone);
+   simpleClone.forget(aClone);
+   return NS_OK;
+ }
+ 
+ /* virtual */ nsresult
+ nsHostObjectURI::EqualsInternal(nsIURI* aOther,
+                                 mozilla::net::nsSimpleURI::RefHandlingEnum aRefHandlingMode,
+                                 bool* aResult)

+ 396 - 0
frg/mozilla-release/work-js/1450242-61a1.patch

@@ -0,0 +1,396 @@
+# HG changeset patch
+# User Florian Quèze <florian@queze.net>
+# Date 1522763045 -7200
+# Node ID fe0e01b08506fe803fa12aa8b84fff9a5c59d1e8
+# Parent  87cb146103e95cedf62f963efe3c75f07e18ba51
+Bug 1450242 - Remove useless constants at the top of nsBrowserContentHandler.js, r=Standard8.
+
+diff --git a/browser/components/nsBrowserContentHandler.js b/browser/components/nsBrowserContentHandler.js
+--- a/browser/components/nsBrowserContentHandler.js
++++ b/browser/components/nsBrowserContentHandler.js
+@@ -14,55 +14,30 @@ ChromeUtils.defineModuleGetter(this, "Pr
+                                "resource://gre/modules/PrivateBrowsingUtils.jsm");
+ ChromeUtils.defineModuleGetter(this, "RecentWindow",
+                                "resource:///modules/RecentWindow.jsm");
+ ChromeUtils.defineModuleGetter(this, "ShellService",
+                                "resource:///modules/ShellService.jsm");
+ XPCOMUtils.defineLazyServiceGetter(this, "WindowsUIUtils",
+                                    "@mozilla.org/windows-ui-utils;1", "nsIWindowsUIUtils");
+ 
+-const nsISupports            = Ci.nsISupports;
+-
+-const nsIBrowserDOMWindow    = Ci.nsIBrowserDOMWindow;
+-const nsIBrowserHandler      = Ci.nsIBrowserHandler;
+-const nsIBrowserHistory      = Ci.nsIBrowserHistory;
+-const nsIChannel             = Ci.nsIChannel;
+-const nsICommandLine         = Ci.nsICommandLine;
+-const nsICommandLineHandler  = Ci.nsICommandLineHandler;
+-const nsIContentHandler      = Ci.nsIContentHandler;
+-const nsIDocShellTreeItem    = Ci.nsIDocShellTreeItem;
+-const nsIDOMChromeWindow     = Ci.nsIDOMChromeWindow;
+-const nsIDOMWindow           = Ci.nsIDOMWindow;
+-const nsIFileURL             = Ci.nsIFileURL;
+-const nsIInterfaceRequestor  = Ci.nsIInterfaceRequestor;
+-const nsINetUtil             = Ci.nsINetUtil;
+-const nsIPrefLocalizedString = Ci.nsIPrefLocalizedString;
+-const nsISupportsString      = Ci.nsISupportsString;
+-const nsIWebNavigation       = Ci.nsIWebNavigation;
+-const nsIWebNavigationInfo   = Ci.nsIWebNavigationInfo;
+-const nsICommandLineValidator = Ci.nsICommandLineValidator;
+-
+-const NS_BINDING_ABORTED = Cr.NS_BINDING_ABORTED;
+-const NS_ERROR_WONT_HANDLE_CONTENT = 0x805d0001;
+-const NS_ERROR_ABORT = Cr.NS_ERROR_ABORT;
+-
+ function shouldLoadURI(aURI) {
+   if (aURI && !aURI.schemeIs("chrome"))
+     return true;
+ 
+   dump("*** Preventing external load of chrome: URI into browser window\n");
+   dump("    Use --chrome <uri> instead\n");
+   return false;
+ }
+ 
+ function resolveURIInternal(aCmdLine, aArgument) {
+   var uri = aCmdLine.resolveURI(aArgument);
+   var uriFixup = Services.uriFixup;
+ 
+-  if (!(uri instanceof nsIFileURL)) {
++  if (!(uri instanceof Ci.nsIFileURL)) {
+     return uriFixup.createFixupURI(aArgument,
+                                    uriFixup.FIXUP_FLAG_FIX_SCHEME_TYPOS);
+   }
+ 
+   try {
+     if (uri.file.exists())
+       return uri;
+   } catch (e) {
+@@ -175,17 +150,17 @@ function getPostUpdateOverridePage(defau
+ const NO_EXTERNAL_URIS = 1;
+ 
+ function openWindow(parent, url, target, features, args, noExternalArgs) {
+   if (noExternalArgs == NO_EXTERNAL_URIS) {
+     // Just pass in the defaultArgs directly
+     var argstring;
+     if (args) {
+       argstring = Cc["@mozilla.org/supports-string;1"]
+-                    .createInstance(nsISupportsString);
++                    .createInstance(Ci.nsISupportsString);
+       argstring.data = args;
+     }
+ 
+     return Services.ww.openWindow(parent, url, target, features, argstring);
+   }
+ 
+   // Pass an array to avoid the browser "|"-splitting behavior.
+   var argArray = Cc["@mozilla.org/array;1"]
+@@ -199,17 +174,17 @@ function openWindow(parent, url, target,
+     stringArgs = [args];
+ 
+   if (stringArgs) {
+     // put the URIs into argArray
+     var uriArray = Cc["@mozilla.org/array;1"]
+                        .createInstance(Ci.nsIMutableArray);
+     stringArgs.forEach(function(uri) {
+       var sstring = Cc["@mozilla.org/supports-string;1"]
+-                      .createInstance(nsISupportsString);
++                      .createInstance(Ci.nsISupportsString);
+       sstring.data = uri;
+       uriArray.appendElement(sstring);
+     });
+     argArray.appendElement(uriArray);
+   } else {
+     argArray.appendElement(null);
+   }
+ 
+@@ -240,25 +215,21 @@ function openPreferences(extraArgs) {
+   args.appendElement(wuri);
+ 
+   Services.ww.openWindow(null, gBrowserContentHandler.chromeURL,
+                          "_blank",
+                          "chrome,dialog=no,all",
+                          args);
+ }
+ 
+-function logSystemBasedSearch(engine) {
++function doSearch(searchTerm, cmdLine) {
++  var engine = Services.search.defaultEngine;
+   var countId = (engine.identifier || ("other-" + engine.name)) + ".system";
+   var count = Services.telemetry.getKeyedHistogramById("SEARCH_COUNTS");
+   count.add(countId);
+-}
+-
+-function doSearch(searchTerm, cmdLine) {
+-  var engine = Services.search.defaultEngine;
+-  logSystemBasedSearch(engine);
+ 
+   var submission = engine.getSubmission(searchTerm, null, "system");
+ 
+   // fill our nsIMutableArray with uri-as-wstring, null, null, postData
+   var args = Cc["@mozilla.org/array;1"]
+                      .createInstance(Ci.nsIMutableArray);
+ 
+   var wuri = Cc["@mozilla.org/supports-string;1"]
+@@ -303,20 +274,20 @@ nsBrowserContentHandler.prototype = {
+     }
+ 
+     this.mChromeURL = Services.prefs.getCharPref("browser.chromeURL");
+ 
+     return this.mChromeURL;
+   },
+ 
+   /* nsISupports */
+-  QueryInterface: XPCOMUtils.generateQI([nsICommandLineHandler,
+-                                         nsIBrowserHandler,
+-                                         nsIContentHandler,
+-                                         nsICommandLineValidator]),
++  QueryInterface: XPCOMUtils.generateQI([Ci.nsICommandLineHandler,
++                                         Ci.nsIBrowserHandler,
++                                         Ci.nsIContentHandler,
++                                         Ci.nsICommandLineValidator]),
+ 
+   /* nsICommandLineHandler */
+   handle: function bch_handle(cmdLine) {
+     if (cmdLine.handleFlag("browser", false)) {
+       // Passing defaultArgs, so use NO_EXTERNAL_URIS
+       openWindow(null, this.chromeURL, "_blank",
+                  "chrome,dialog=no,all" + this.getFeatures(cmdLine),
+                  this.defaultArgs, NO_EXTERNAL_URIS);
+@@ -326,17 +297,17 @@ nsBrowserContentHandler.prototype = {
+     // In the past, when an instance was not already running, the -remote
+     // option returned an error code. Any script or application invoking the
+     // -remote option is expected to be handling this case, otherwise they
+     // wouldn't be doing anything when there is no Firefox already running.
+     // Making the -remote option always return an error code makes those
+     // scripts or applications handle the situation as if Firefox was not
+     // already running.
+     if (cmdLine.handleFlag("remote", true)) {
+-      throw NS_ERROR_ABORT;
++      throw Cr.NS_ERROR_ABORT;
+     }
+ 
+     var uriparam;
+     try {
+       while ((uriparam = cmdLine.handleFlagWithParam("new-window", false))) {
+         let uri = resolveURIInternal(cmdLine, uriparam);
+         if (!shouldLoadURI(uri))
+           continue;
+@@ -347,17 +318,18 @@ nsBrowserContentHandler.prototype = {
+       }
+     } catch (e) {
+       Cu.reportError(e);
+     }
+ 
+     try {
+       while ((uriparam = cmdLine.handleFlagWithParam("new-tab", false))) {
+         let uri = resolveURIInternal(cmdLine, uriparam);
+-        handURIToExistingBrowser(uri, nsIBrowserDOMWindow.OPEN_NEWTAB, cmdLine, false,
++        handURIToExistingBrowser(uri, Ci.nsIBrowserDOMWindow.OPEN_NEWTAB,
++                                 cmdLine, false,
+                                  Services.scriptSecurityManager.getSystemPrincipal());
+         cmdLine.preventDefault = true;
+       }
+     } catch (e) {
+       Cu.reportError(e);
+     }
+ 
+     var chromeParam = cmdLine.handleFlagWithParam("chrome", false);
+@@ -396,17 +368,18 @@ nsBrowserContentHandler.prototype = {
+     }
+     if (cmdLine.handleFlag("silent", false))
+       cmdLine.preventDefault = true;
+ 
+     try {
+       var privateWindowParam = cmdLine.handleFlagWithParam("private-window", false);
+       if (privateWindowParam) {
+         let resolvedURI = resolveURIInternal(cmdLine, privateWindowParam);
+-        handURIToExistingBrowser(resolvedURI, nsIBrowserDOMWindow.OPEN_NEWTAB, cmdLine, true,
++        handURIToExistingBrowser(resolvedURI, Ci.nsIBrowserDOMWindow.OPEN_NEWTAB,
++                                 cmdLine, true,
+                                  Services.scriptSecurityManager.getSystemPrincipal());
+         cmdLine.preventDefault = true;
+       }
+     } catch (e) {
+       if (e.result != Cr.NS_ERROR_INVALID_ARG) {
+         throw e;
+       }
+       // NS_ERROR_INVALID_ARG is thrown when flag exists, but has no param.
+@@ -562,21 +535,21 @@ nsBrowserContentHandler.prototype = {
+     if (overridePage && startPage && !willRestoreSession && !skipStartPage)
+       return overridePage + "|" + startPage;
+ 
+     return overridePage || startPage || "about:blank";
+   },
+ 
+   get startPage() {
+     var uri = Services.prefs.getComplexValue("browser.startup.homepage",
+-                                             nsIPrefLocalizedString).data;
++                                             Ci.nsIPrefLocalizedString).data;
+     if (!uri) {
+       Services.prefs.clearUserPref("browser.startup.homepage");
+       uri = Services.prefs.getComplexValue("browser.startup.homepage",
+-                                           nsIPrefLocalizedString).data;
++                                           Ci.nsIPrefLocalizedString).data;
+     }
+     return uri;
+   },
+ 
+   mFeatures: null,
+ 
+   getFeatures: function bch_features(cmdLine) {
+     if (this.mFeatures === null) {
+@@ -606,55 +579,57 @@ nsBrowserContentHandler.prototype = {
+     }
+ 
+     return this.mFeatures;
+   },
+ 
+   /* nsIContentHandler */
+ 
+   handleContent: function bch_handleContent(contentType, context, request) {
++    const NS_ERROR_WONT_HANDLE_CONTENT = 0x805d0001;
++
+     try {
+       var webNavInfo = Cc["@mozilla.org/webnavigation-info;1"]
+-                         .getService(nsIWebNavigationInfo);
++                         .getService(Ci.nsIWebNavigationInfo);
+       if (!webNavInfo.isTypeSupported(contentType, null)) {
+         throw NS_ERROR_WONT_HANDLE_CONTENT;
+       }
+     } catch (e) {
+       throw NS_ERROR_WONT_HANDLE_CONTENT;
+     }
+ 
+-    request.QueryInterface(nsIChannel);
+-    handURIToExistingBrowser(request.URI, nsIBrowserDOMWindow.OPEN_DEFAULTWINDOW, null, false,
+-                             request.loadInfo.triggeringPrincipal);
+-    request.cancel(NS_BINDING_ABORTED);
++    request.QueryInterface(Ci.nsIChannel);
++    handURIToExistingBrowser(request.URI, Ci.nsIBrowserDOMWindow.OPEN_DEFAULTWINDOW,
++                             null, false, request.loadInfo.triggeringPrincipal);
++    request.cancel(Cr.NS_BINDING_ABORTED);
+   },
+ 
+   /* nsICommandLineValidator */
+   validate: function bch_validate(cmdLine) {
+     // Other handlers may use osint so only handle the osint flag if the url
+     // flag is also present and the command line is valid.
+     var osintFlagIdx = cmdLine.findFlag("osint", false);
+     var urlFlagIdx = cmdLine.findFlag("url", false);
+     if (urlFlagIdx > -1 && (osintFlagIdx > -1 ||
+-        cmdLine.state == nsICommandLine.STATE_REMOTE_EXPLICIT)) {
++        cmdLine.state == Ci.nsICommandLine.STATE_REMOTE_EXPLICIT)) {
+       var urlParam = cmdLine.getArgument(urlFlagIdx + 1);
+       if (cmdLine.length != urlFlagIdx + 2 || /firefoxurl:/.test(urlParam))
+-        throw NS_ERROR_ABORT;
++        throw Cr.NS_ERROR_ABORT;
+       var isDefault = false;
+       try {
+         var url = Services.urlFormatter.formatURLPref("app.support.baseURL") +
+                   "win10-default-browser";
+         if (urlParam == url) {
+           isDefault = ShellService.isDefaultBrowser(false, false);
+         }
+       } catch (ex) {}
+       if (isDefault) {
+         // Firefox is already the default HTTP handler.
+         // We don't have to show the instruction page.
+-        throw NS_ERROR_ABORT;
++        throw Cr.NS_ERROR_ABORT;
+       }
+       cmdLine.handleFlag("osint", false);
+     }
+   },
+ };
+ var gBrowserContentHandler = new nsBrowserContentHandler();
+ 
+ function handURIToExistingBrowser(uri, location, cmdLine, forcePrivate, triggeringPrincipal) {
+@@ -670,36 +645,36 @@ function handURIToExistingBrowser(uri, l
+     var features = "chrome,dialog=no,all" + gBrowserContentHandler.getFeatures(cmdLine);
+     if (forcePrivate) {
+       features += ",private";
+     }
+     openWindow(null, gBrowserContentHandler.chromeURL, "_blank", features, uri.spec);
+     return;
+   }
+ 
+-  var navNav = navWin.QueryInterface(nsIInterfaceRequestor)
+-                     .getInterface(nsIWebNavigation);
+-  var rootItem = navNav.QueryInterface(nsIDocShellTreeItem).rootTreeItem;
+-  var rootWin = rootItem.QueryInterface(nsIInterfaceRequestor)
+-                        .getInterface(nsIDOMWindow);
+-  var bwin = rootWin.QueryInterface(nsIDOMChromeWindow).browserDOMWindow;
++  var navNav = navWin.QueryInterface(Ci.nsIInterfaceRequestor)
++                     .getInterface(Ci.nsIWebNavigation);
++  var rootItem = navNav.QueryInterface(Ci.nsIDocShellTreeItem).rootTreeItem;
++  var rootWin = rootItem.QueryInterface(Ci.nsIInterfaceRequestor)
++                        .getInterface(Ci.nsIDOMWindow);
++  var bwin = rootWin.QueryInterface(Ci.nsIDOMChromeWindow).browserDOMWindow;
+   bwin.openURI(uri, null, location,
+-               nsIBrowserDOMWindow.OPEN_EXTERNAL, triggeringPrincipal);
++               Ci.nsIBrowserDOMWindow.OPEN_EXTERNAL, triggeringPrincipal);
+ }
+ 
+ function nsDefaultCommandLineHandler() {
+ }
+ 
+ nsDefaultCommandLineHandler.prototype = {
+   classID: Components.ID("{47cd0651-b1be-4a0f-b5c4-10e5a573ef71}"),
+ 
+   /* nsISupports */
+   QueryInterface: function dch_QI(iid) {
+-    if (!iid.equals(nsISupports) &&
+-        !iid.equals(nsICommandLineHandler))
++    if (!iid.equals(Ci.nsISupports) &&
++        !iid.equals(Ci.nsICommandLineHandler))
+       throw Cr.NS_ERROR_NO_INTERFACE;
+ 
+     return this;
+   },
+ 
+   _haveProfile: false,
+ 
+   /* nsICommandLineHandler */
+@@ -752,38 +727,39 @@ nsDefaultCommandLineHandler.prototype = 
+           urilist.push(resolveURIInternal(cmdLine, curarg));
+         } catch (e) {
+           Cu.reportError("Error opening URI '" + curarg + "' from the command line: " + e + "\n");
+         }
+       }
+     }
+ 
+     if (urilist.length) {
+-      if (cmdLine.state != nsICommandLine.STATE_INITIAL_LAUNCH &&
++      if (cmdLine.state != Ci.nsICommandLine.STATE_INITIAL_LAUNCH &&
+           urilist.length == 1) {
+         // Try to find an existing window and load our URI into the
+         // current tab, new tab, or new window as prefs determine.
+         try {
+-          handURIToExistingBrowser(urilist[0], nsIBrowserDOMWindow.OPEN_DEFAULTWINDOW, cmdLine, false,
++          handURIToExistingBrowser(urilist[0], Ci.nsIBrowserDOMWindow.OPEN_DEFAULTWINDOW,
++                                   cmdLine, false,
+                                    Services.scriptSecurityManager.getSystemPrincipal());
+           return;
+         } catch (e) {
+         }
+       }
+ 
+       var URLlist = urilist.filter(shouldLoadURI).map(u => u.spec);
+       if (URLlist.length) {
+         openWindow(null, gBrowserContentHandler.chromeURL, "_blank",
+                    "chrome,dialog=no,all" + gBrowserContentHandler.getFeatures(cmdLine),
+                    URLlist);
+       }
+ 
+     } else if (!cmdLine.preventDefault) {
+       if (AppConstants.isPlatformAndVersionAtLeast("win", "10") &&
+-          cmdLine.state != nsICommandLine.STATE_INITIAL_LAUNCH &&
++          cmdLine.state != Ci.nsICommandLine.STATE_INITIAL_LAUNCH &&
+           WindowsUIUtils.inTabletMode) {
+         // In windows 10 tablet mode, do not create a new window, but reuse the existing one.
+         let win = RecentWindow.getMostRecentBrowserWindow();
+         if (win) {
+           win.focus();
+           return;
+         }
+       }

+ 254 - 0
frg/mozilla-release/work-js/1453589-61a1.patch

@@ -0,0 +1,254 @@
+# HG changeset patch
+# User Michael Kohler <me@michaelkohler.info>
+# Date 1524159371 14400
+# Node ID 7c588027cbbb6274f47a2000bfe055a3163977c4
+# Parent  ee32738bd57b8e00c5e3c036c51df74964596f25
+Bug 1453589 - Select next item in list when removing items in Site Data Manager. r=johannh
+
+Select next item in list when removing items in Site Data Manager. When there
+are multiple selected sites, it will select the next item after the last
+previously selected item.
+
+Differential Revision: https://phabricator.services.mozilla.com/D965
+
+diff --git a/browser/components/preferences/in-content-new/tests/siteData/browser_siteData2.js b/browser/components/preferences/in-content-new/tests/siteData/browser_siteData2.js
+--- a/browser/components/preferences/in-content-new/tests/siteData/browser_siteData2.js
++++ b/browser/components/preferences/in-content-new/tests/siteData/browser_siteData2.js
+@@ -205,24 +205,26 @@ add_task(async function() {
+   assertSitesListed(doc, fakeHosts.slice(2));
+ 
+   await mockSiteDataManager.unregister();
+   await BrowserTestUtils.removeTab(gBrowser.selectedTab);
+ 
+   function removeSelectedSite(hosts) {
+     frameDoc = win.gSubDialog._topDialog._frame.contentDocument;
+     let removeBtn = frameDoc.getElementById("removeSelected");
++    is(removeBtn.disabled, true, "Should start with disabled removeSelected button");
+     let sitesList = frameDoc.getElementById("sitesList");
+     hosts.forEach(host => {
+       let site = sitesList.querySelector(`richlistitem[host="${host}"]`);
+       if (site) {
+         site.click();
++        let currentSelectedIndex = sitesList.selectedIndex;
+         is(removeBtn.disabled, false, "Should enable the removeSelected button");
+         removeBtn.doCommand();
+-        is(removeBtn.disabled, true, "Should disable the removeSelected button");
++        is(sitesList.selectedIndex, currentSelectedIndex);
+       } else {
+         ok(false, `Should not select and remove inexistent site of ${host}`);
+       }
+     });
+   }
+ });
+ 
+ // Test searching and then removing only visible sites
+diff --git a/browser/components/preferences/in-content-new/tests/siteData/browser_siteData_multi_select.js b/browser/components/preferences/in-content-new/tests/siteData/browser_siteData_multi_select.js
+--- a/browser/components/preferences/in-content-new/tests/siteData/browser_siteData_multi_select.js
++++ b/browser/components/preferences/in-content-new/tests/siteData/browser_siteData_multi_select.js
+@@ -45,34 +45,34 @@ add_task(async function() {
+   await openPreferencesViaOpenPreferencesAPI("privacy", { leaveOpen: true });
+   await updatePromise;
+   await openSiteDataSettingsDialog();
+ 
+   let doc = gBrowser.selectedBrowser.contentDocument;
+ 
+   // Test the initial state
+   assertSitesListed(doc, fakeHosts);
++  let win = gBrowser.selectedBrowser.contentWindow;
++  let frameDoc = win.gSubDialog._topDialog._frame.contentDocument;
++  let removeBtn = frameDoc.getElementById("removeSelected");
++  is(removeBtn.disabled, true, "Should start with disabled removeSelected button");
+ 
+   let removeDialogOpenPromise = BrowserTestUtils.promiseAlertDialogOpen("accept", REMOVE_DIALOG_URL);
+   let settingsDialogClosePromise = promiseSettingsDialogClose();
+ 
+-  let win = gBrowser.selectedBrowser.contentWindow;
+-  let frameDoc = win.gSubDialog._topDialog._frame.contentDocument;
+-
+   // Select some sites to remove.
+   let sitesList = frameDoc.getElementById("sitesList");
+   fakeHosts.slice(0, 2).forEach(host => {
+     let site = sitesList.querySelector(`richlistitem[host="${host}"]`);
+     sitesList.addItemToSelection(site);
+   });
+ 
+-  let removeBtn = frameDoc.getElementById("removeSelected");
+   is(removeBtn.disabled, false, "Should enable the removeSelected button");
+   removeBtn.doCommand();
+-  is(removeBtn.disabled, true, "Should disable the removeSelected button");
++  is(sitesList.selectedIndex, 0, "Should select next item");
+ 
+   let saveBtn = frameDoc.getElementById("save");
+   assertSitesListed(doc, fakeHosts.slice(2));
+   saveBtn.doCommand();
+ 
+   await removeDialogOpenPromise;
+   await settingsDialogClosePromise;
+   await openSiteDataSettingsDialog();
+diff --git a/browser/components/preferences/in-content-new/tests/siteData/head.js b/browser/components/preferences/in-content-new/tests/siteData/head.js
+--- a/browser/components/preferences/in-content-new/tests/siteData/head.js
++++ b/browser/components/preferences/in-content-new/tests/siteData/head.js
+@@ -152,26 +152,24 @@ function promiseSettingsDialogClose() {
+         resolve();
+       }
+     }, { once: true });
+   });
+ }
+ 
+ function assertSitesListed(doc, hosts) {
+   let frameDoc = content.gSubDialog._topDialog._frame.contentDocument;
+-  let removeBtn = frameDoc.getElementById("removeSelected");
+   let removeAllBtn = frameDoc.getElementById("removeAll");
+   let sitesList = frameDoc.getElementById("sitesList");
+   let totalSitesNumber = sitesList.getElementsByTagName("richlistitem").length;
+   is(totalSitesNumber, hosts.length, "Should list the right sites number");
+   hosts.forEach(host => {
+     let site = sitesList.querySelector(`richlistitem[host="${host}"]`);
+     ok(site, `Should list the site of ${host}`);
+   });
+-  is(removeBtn.disabled, true, "Should disable the removeSelected button");
+   is(removeAllBtn.disabled, false, "Should enable the removeAllBtn button");
+ }
+ 
+ const mockSiteDataManager = {
+ 
+   _SiteDataManager: null,
+   _originalQMS: null,
+   _originalRemoveQuotaUsage: null,
+diff --git a/browser/components/preferences/in-content/tests/siteData/browser_siteData2.js b/browser/components/preferences/in-content/tests/siteData/browser_siteData2.js
+--- a/browser/components/preferences/in-content/tests/siteData/browser_siteData2.js
++++ b/browser/components/preferences/in-content/tests/siteData/browser_siteData2.js
+@@ -205,24 +205,26 @@ add_task(async function() {
+   assertSitesListed(doc, fakeHosts.slice(2));
+ 
+   await mockSiteDataManager.unregister();
+   await BrowserTestUtils.removeTab(gBrowser.selectedTab);
+ 
+   function removeSelectedSite(hosts) {
+     frameDoc = win.gSubDialog._topDialog._frame.contentDocument;
+     let removeBtn = frameDoc.getElementById("removeSelected");
++    is(removeBtn.disabled, true, "Should start with disabled removeSelected button");
+     let sitesList = frameDoc.getElementById("sitesList");
+     hosts.forEach(host => {
+       let site = sitesList.querySelector(`richlistitem[host="${host}"]`);
+       if (site) {
+         site.click();
++        let currentSelectedIndex = sitesList.selectedIndex;
+         is(removeBtn.disabled, false, "Should enable the removeSelected button");
+         removeBtn.doCommand();
+-        is(removeBtn.disabled, true, "Should disable the removeSelected button");
++        is(sitesList.selectedIndex, currentSelectedIndex);
+       } else {
+         ok(false, `Should not select and remove inexistent site of ${host}`);
+       }
+     });
+   }
+ });
+ 
+ // Test searching and then removing only visible sites
+diff --git a/browser/components/preferences/in-content/tests/siteData/browser_siteData_multi_select.js b/browser/components/preferences/in-content/tests/siteData/browser_siteData_multi_select.js
+--- a/browser/components/preferences/in-content/tests/siteData/browser_siteData_multi_select.js
++++ b/browser/components/preferences/in-content/tests/siteData/browser_siteData_multi_select.js
+@@ -45,34 +45,34 @@ add_task(async function() {
+   await openPreferencesViaOpenPreferencesAPI("privacy", { leaveOpen: true });
+   await updatePromise;
+   await openSiteDataSettingsDialog();
+ 
+   let doc = gBrowser.selectedBrowser.contentDocument;
+ 
+   // Test the initial state
+   assertSitesListed(doc, fakeHosts);
++  let win = gBrowser.selectedBrowser.contentWindow;
++  let frameDoc = win.gSubDialog._topDialog._frame.contentDocument;
++  let removeBtn = frameDoc.getElementById("removeSelected");
++  is(removeBtn.disabled, true, "Should start with disabled removeSelected button");
+ 
+   let removeDialogOpenPromise = BrowserTestUtils.promiseAlertDialogOpen("accept", REMOVE_DIALOG_URL);
+   let settingsDialogClosePromise = promiseSettingsDialogClose();
+ 
+-  let win = gBrowser.selectedBrowser.contentWindow;
+-  let frameDoc = win.gSubDialog._topDialog._frame.contentDocument;
+-
+   // Select some sites to remove.
+   let sitesList = frameDoc.getElementById("sitesList");
+   fakeHosts.slice(0, 2).forEach(host => {
+     let site = sitesList.querySelector(`richlistitem[host="${host}"]`);
+     sitesList.addItemToSelection(site);
+   });
+ 
+-  let removeBtn = frameDoc.getElementById("removeSelected");
+   is(removeBtn.disabled, false, "Should enable the removeSelected button");
+   removeBtn.doCommand();
+-  is(removeBtn.disabled, true, "Should disable the removeSelected button");
++  is(sitesList.selectedIndex, 0, "Should select next item");
+ 
+   let saveBtn = frameDoc.getElementById("save");
+   assertSitesListed(doc, fakeHosts.slice(2));
+   saveBtn.doCommand();
+ 
+   await removeDialogOpenPromise;
+   await settingsDialogClosePromise;
+   await openSiteDataSettingsDialog();
+diff --git a/browser/components/preferences/in-content/tests/siteData/head.js b/browser/components/preferences/in-content/tests/siteData/head.js
+--- a/browser/components/preferences/in-content/tests/siteData/head.js
++++ b/browser/components/preferences/in-content/tests/siteData/head.js
+@@ -211,26 +211,24 @@ function promiseSettingsDialogClose() {
+       }
+     }, { once: true });
+   }); 
+ }
+ 
+ function assertSitesListed(doc, hosts) {
+   let win = gBrowser.selectedBrowser.contentWindow;
+   let frameDoc = win.gSubDialog._topDialog._frame.contentDocument;
+-  let removeBtn = frameDoc.getElementById("removeSelected");
+   let removeAllBtn = frameDoc.getElementById("removeAll");
+   let sitesList = frameDoc.getElementById("sitesList");
+   let totalSitesNumber = sitesList.getElementsByTagName("richlistitem").length;
+   is(totalSitesNumber, hosts.length, "Should list the right sites number");
+   hosts.forEach(host => {
+     let site = sitesList.querySelector(`richlistitem[host="${host}"]`);
+     ok(site, `Should list the site of ${host}`);
+   });
+-  is(removeBtn.disabled, true, "Should disable the removeSelected button");
+   is(removeAllBtn.disabled, false, "Should enable the removeAllBtn button");
+ }
+ 
+ function promiseSitesUpdated() {
+   return TestUtils.topicObserved("sitedatamanager:sites-updated", () => true);
+ }
+ 
+ function promiseCookiesCleared() {
+diff --git a/browser/components/preferences/siteDataSettings.js b/browser/components/preferences/siteDataSettings.js
+--- a/browser/components/preferences/siteDataSettings.js
++++ b/browser/components/preferences/siteDataSettings.js
+@@ -254,18 +254,29 @@ let gSiteDataSettings = {
+   },
+ 
+   onCommandSearch() {
+     this._buildSitesList(this._sites);
+     this._list.clearSelection();
+   },
+ 
+   onClickRemoveSelected() {
++    let lastIndex = this._list.selectedItems.length - 1;
++    let lastSelectedItem = this._list.selectedItems[lastIndex];
++    let lastSelectedItemPosition = this._list.getIndexOfItem(lastSelectedItem);
++    let nextSelectedItem = this._list.getItemAtIndex(lastSelectedItemPosition + 1);
++
+     this._removeSiteItems(this._list.selectedItems);
+     this._list.clearSelection();
++
++    if (nextSelectedItem) {
++      this._list.selectedItem = nextSelectedItem;
++    } else {
++      this._list.selectedIndex = this._list.itemCount - 1;
++    }
+   },
+ 
+   onClickRemoveAll() {
+     let siteItems = this._list.getElementsByTagName("richlistitem");
+     if (siteItems.length > 0) {
+       this._removeSiteItems(siteItems);
+     }
+   },

+ 0 - 0
frg/mozilla-release/work-js/mozilla-central-push_419583.patch → frg/mozilla-release/work-js/1457059-62a1.patch


+ 4 - 4
frg/mozilla-release/work-js/1461938-22-62a1.patch

@@ -3,7 +3,7 @@
 # Date 1527081920 -7200
 # Date 1527081920 -7200
 #      Wed May 23 15:25:20 2018 +0200
 #      Wed May 23 15:25:20 2018 +0200
 # Node ID d23c763dfa4ba0012e7b306b2f489175603232bf
 # Node ID d23c763dfa4ba0012e7b306b2f489175603232bf
-# Parent  e12f367e03e02521b51f7c4eea3857c885cdfcf4
+# Parent  04fc3723850ab17040c1a27d983dc5f264efd868
 Bug 1461938 part 22 - Move template objects from JSCompartment to JS::Realm. r=anba
 Bug 1461938 part 22 - Move template objects from JSCompartment to JS::Realm. r=anba
 
 
 diff --git a/js/src/jit/IonBuilder.cpp b/js/src/jit/IonBuilder.cpp
 diff --git a/js/src/jit/IonBuilder.cpp b/js/src/jit/IonBuilder.cpp
@@ -283,9 +283,9 @@ diff --git a/js/src/vm/JSCompartment.h b/js/src/vm/JSCompartment.h
  
  
      void fixupScriptMapsAfterMovingGC();
      void fixupScriptMapsAfterMovingGC();
  
  
-@@ -1226,16 +1218,23 @@ class JS::Realm : public JSCompartment
-         performanceMonitoring.unlink();
-         isSystem_ = isSystem;
+@@ -1234,16 +1226,23 @@ class JS::Realm : public JSCompartment
+ 
+         return isSystem_;
      }
      }
  
  
      // Used to approximate non-content code when reporting telemetry.
      // Used to approximate non-content code when reporting telemetry.

+ 18 - 18
frg/mozilla-release/work-js/1461938-23-62a1.patch

@@ -3,7 +3,7 @@
 # Date 1527081960 -7200
 # Date 1527081960 -7200
 #      Wed May 23 15:26:00 2018 +0200
 #      Wed May 23 15:26:00 2018 +0200
 # Node ID 20512f4a1de5b456de577985631f0e2ab6037ff6
 # Node ID 20512f4a1de5b456de577985631f0e2ab6037ff6
-# Parent  d23c763dfa4ba0012e7b306b2f489175603232bf
+# Parent  9d9f4e94259c33f113aced6327e9b93dd8884654
 Bug 1461938 part 23 - Move debugModeBits from JSCompartment to JS::Realm. r=luke
 Bug 1461938 part 23 - Move debugModeBits from JSCompartment to JS::Realm. r=luke
 
 
 diff --git a/js/src/builtin/Promise.cpp b/js/src/builtin/Promise.cpp
 diff --git a/js/src/builtin/Promise.cpp b/js/src/builtin/Promise.cpp
@@ -52,7 +52,7 @@ diff --git a/js/src/builtin/Promise.cpp b/js/src/builtin/Promise.cpp
 diff --git a/js/src/gc/GC.cpp b/js/src/gc/GC.cpp
 diff --git a/js/src/gc/GC.cpp b/js/src/gc/GC.cpp
 --- a/js/src/gc/GC.cpp
 --- a/js/src/gc/GC.cpp
 +++ b/js/src/gc/GC.cpp
 +++ b/js/src/gc/GC.cpp
-@@ -8009,23 +8009,23 @@ GCRuntime::mergeCompartments(JSCompartme
+@@ -8112,23 +8112,23 @@ GCRuntime::mergeCompartments(JSCompartme
  
  
      AutoTraceSession session(rt);
      AutoTraceSession session(rt);
  
  
@@ -147,7 +147,7 @@ diff --git a/js/src/jit/JitFrames.cpp b/js/src/jit/JitFrames.cpp
          if (!shouldBail) {
          if (!shouldBail) {
              JitActivation* act = cx->activation()->asJit();
              JitActivation* act = cx->activation()->asJit();
              rematFrame = act->lookupRematerializedFrame(frame.frame().fp(), frame.frameNo());
              rematFrame = act->lookupRematerializedFrame(frame.frame().fp(), frame.frameNo());
-@@ -647,17 +647,17 @@ HandleException(ResumeFromException* rfe
+@@ -677,17 +677,17 @@ HandleException(ResumeFromException* rfe
              // them.
              // them.
              InlineFrameIterator frames(cx, &frame);
              InlineFrameIterator frames(cx, &frame);
  
  
@@ -210,12 +210,12 @@ diff --git a/js/src/jit/VMFunctions.cpp b/js/src/jit/VMFunctions.cpp
 diff --git a/js/src/jsapi.cpp b/js/src/jsapi.cpp
 diff --git a/js/src/jsapi.cpp b/js/src/jsapi.cpp
 --- a/js/src/jsapi.cpp
 --- a/js/src/jsapi.cpp
 +++ b/js/src/jsapi.cpp
 +++ b/js/src/jsapi.cpp
-@@ -4064,17 +4064,17 @@ JS::CompileOptions::CompileOptions(JSCon
+@@ -4052,17 +4052,17 @@ JS::CompileOptions::CompileOptions(JSCon
        introductionScriptRoot(cx)
        introductionScriptRoot(cx)
  {
  {
      strictOption = cx->options().strictMode();
      strictOption = cx->options().strictMode();
      extraWarningsOption = cx->realm()->behaviors().extraWarnings(cx);
      extraWarningsOption = cx->realm()->behaviors().extraWarnings(cx);
-     isProbablySystemCode = cx->realm()->isProbablySystemCode();
+     isProbablySystemOrAddonCode = cx->realm()->isProbablySystemOrAddonCode();
      werrorOption = cx->options().werror();
      werrorOption = cx->options().werror();
      if (!cx->options().asmJS())
      if (!cx->options().asmJS())
          asmJSOption = AsmJSOption::Disabled;
          asmJSOption = AsmJSOption::Disabled;
@@ -602,7 +602,7 @@ diff --git a/js/src/vm/Debugger.cpp b/js/src/vm/Debugger.cpp
  
  
  /* static */ bool
  /* static */ bool
  Debugger::cannotTrackAllocations(const GlobalObject& global)
  Debugger::cannotTrackAllocations(const GlobalObject& global)
-@@ -3589,18 +3591,18 @@ Debugger::setAllowUnobservedAsmJS(JSCont
+@@ -3602,18 +3604,18 @@ Debugger::setAllowUnobservedAsmJS(JSCont
  {
  {
      THIS_DEBUGGER(cx, argc, vp, "set allowUnobservedAsmJS", args, dbg);
      THIS_DEBUGGER(cx, argc, vp, "set allowUnobservedAsmJS", args, dbg);
      if (!args.requireAtLeast(cx, "Debugger.set allowUnobservedAsmJS", 1))
      if (!args.requireAtLeast(cx, "Debugger.set allowUnobservedAsmJS", 1))
@@ -623,7 +623,7 @@ diff --git a/js/src/vm/Debugger.cpp b/js/src/vm/Debugger.cpp
  
  
  /* static */ bool
  /* static */ bool
  Debugger::getAllowWasmBinarySource(JSContext* cx, unsigned argc, Value* vp)
  Debugger::getAllowWasmBinarySource(JSContext* cx, unsigned argc, Value* vp)
-@@ -3615,18 +3617,18 @@ Debugger::setAllowWasmBinarySource(JSCon
+@@ -3628,18 +3630,18 @@ Debugger::setAllowWasmBinarySource(JSCon
  {
  {
      THIS_DEBUGGER(cx, argc, vp, "set allowWasmBinarySource", args, dbg);
      THIS_DEBUGGER(cx, argc, vp, "set allowWasmBinarySource", args, dbg);
      if (!args.requireAtLeast(cx, "Debugger.set allowWasmBinarySource", 1))
      if (!args.requireAtLeast(cx, "Debugger.set allowWasmBinarySource", 1))
@@ -644,7 +644,7 @@ diff --git a/js/src/vm/Debugger.cpp b/js/src/vm/Debugger.cpp
  
  
  /* static */ bool
  /* static */ bool
  Debugger::getCollectCoverageInfo(JSContext* cx, unsigned argc, Value* vp)
  Debugger::getCollectCoverageInfo(JSContext* cx, unsigned argc, Value* vp)
-@@ -3764,51 +3766,51 @@ Debugger::removeDebuggee(JSContext* cx, 
+@@ -3777,51 +3779,51 @@ Debugger::removeDebuggee(JSContext* cx, 
      THIS_DEBUGGER(cx, argc, vp, "removeDebuggee", args, dbg);
      THIS_DEBUGGER(cx, argc, vp, "removeDebuggee", args, dbg);
  
  
      if (!args.requireAtLeast(cx, "Debugger.removeDebuggee", 1))
      if (!args.requireAtLeast(cx, "Debugger.removeDebuggee", 1))
@@ -703,7 +703,7 @@ diff --git a/js/src/vm/Debugger.cpp b/js/src/vm/Debugger.cpp
  
  
      args.rval().setUndefined();
      args.rval().setUndefined();
      return true;
      return true;
-@@ -3883,18 +3885,17 @@ Debugger::getNewestFrame(JSContext* cx, 
+@@ -3896,18 +3898,17 @@ Debugger::getNewestFrame(JSContext* cx, 
      return true;
      return true;
  }
  }
  
  
@@ -723,7 +723,7 @@ diff --git a/js/src/vm/Debugger.cpp b/js/src/vm/Debugger.cpp
  {
  {
      CallArgs args = CallArgsFromVp(argc, vp);
      CallArgs args = CallArgsFromVp(argc, vp);
  
  
-@@ -3970,51 +3971,50 @@ Debugger::addDebuggeeGlobal(JSContext* c
+@@ -3983,51 +3984,50 @@ Debugger::addDebuggeeGlobal(JSContext* c
      }
      }
  
  
      /*
      /*
@@ -785,7 +785,7 @@ diff --git a/js/src/vm/Debugger.cpp b/js/src/vm/Debugger.cpp
      Zone* zone = global->zone();
      Zone* zone = global->zone();
  
  
      // (1)
      // (1)
-@@ -4068,17 +4068,17 @@ Debugger::addDebuggeeGlobal(JSContext* c
+@@ -4081,17 +4081,17 @@ Debugger::addDebuggeeGlobal(JSContext* c
          return false;
          return false;
  
  
      auto allocationsTrackingGuard = MakeScopeExit([&] {
      auto allocationsTrackingGuard = MakeScopeExit([&] {
@@ -804,7 +804,7 @@ diff --git a/js/src/vm/Debugger.cpp b/js/src/vm/Debugger.cpp
          return false;
          return false;
  
  
      globalDebuggersGuard.release();
      globalDebuggersGuard.release();
-@@ -4173,41 +4173,41 @@ Debugger::removeDebuggeeGlobal(FreeOp* f
+@@ -4186,41 +4186,41 @@ Debugger::removeDebuggeeGlobal(FreeOp* f
          zoneDebuggersVector->erase(findDebuggerInVector(this, zoneDebuggersVector));
          zoneDebuggersVector->erase(findDebuggerInVector(this, zoneDebuggersVector));
  
  
      /* Remove all breakpoints for the debuggee. */
      /* Remove all breakpoints for the debuggee. */
@@ -854,7 +854,7 @@ diff --git a/js/src/vm/Debugger.cpp b/js/src/vm/Debugger.cpp
  
  
  /*
  /*
   * A class for parsing 'findScripts' query arguments and searching for
   * A class for parsing 'findScripts' query arguments and searching for
-@@ -4579,17 +4579,18 @@ class MOZ_STACK_CLASS Debugger::ScriptQu
+@@ -4592,17 +4592,18 @@ class MOZ_STACK_CLASS Debugger::ScriptQu
          return true;
          return true;
      }
      }
  
  
@@ -981,7 +981,7 @@ diff --git a/js/src/vm/Interpreter.cpp b/js/src/vm/Interpreter.cpp
        case ScopeKind::Global:
        case ScopeKind::Global:
        case ScopeKind::NonSyntactic:
        case ScopeKind::NonSyntactic:
        case ScopeKind::Module:
        case ScopeKind::Module:
-@@ -1810,17 +1810,17 @@ Interpret(JSContext* cx, RunState& state
+@@ -1839,17 +1839,17 @@ Interpret(JSContext* cx, RunState& state
      JS_END_MACRO
      JS_END_MACRO
  
  
      /*
      /*
@@ -1000,7 +1000,7 @@ diff --git a/js/src/vm/Interpreter.cpp b/js/src/vm/Interpreter.cpp
  
  
      /*
      /*
       * Increment the code coverage counter associated with the given pc.
       * Increment the code coverage counter associated with the given pc.
-@@ -1930,17 +1930,17 @@ Interpret(JSContext* cx, RunState& state
+@@ -1959,17 +1959,17 @@ Interpret(JSContext* cx, RunState& state
  
  
  INTERPRETER_LOOP() {
  INTERPRETER_LOOP() {
  
  
@@ -1019,7 +1019,7 @@ diff --git a/js/src/vm/Interpreter.cpp b/js/src/vm/Interpreter.cpp
          if (script->stepModeEnabled()) {
          if (script->stepModeEnabled()) {
              RootedValue rval(cx);
              RootedValue rval(cx);
              ResumeMode mode = Debugger::onSingleStep(cx, &rval);
              ResumeMode mode = Debugger::onSingleStep(cx, &rval);
-@@ -3975,51 +3975,51 @@ CASE(JSOP_POPLEXICALENV)
+@@ -4004,51 +4004,51 @@ CASE(JSOP_POPLEXICALENV)
  #ifdef DEBUG
  #ifdef DEBUG
      // Pop block from scope chain.
      // Pop block from scope chain.
      Scope* scope = script->lookupScope(REGS.pc);
      Scope* scope = script->lookupScope(REGS.pc);
@@ -1075,7 +1075,7 @@ diff --git a/js/src/vm/Interpreter.cpp b/js/src/vm/Interpreter.cpp
  END_CASE(JSOP_RECREATELEXICALENV)
  END_CASE(JSOP_RECREATELEXICALENV)
  
  
  CASE(JSOP_PUSHVARENV)
  CASE(JSOP_PUSHVARENV)
-@@ -4035,17 +4035,17 @@ CASE(JSOP_POPVARENV)
+@@ -4064,17 +4064,17 @@ CASE(JSOP_POPVARENV)
  {
  {
  #ifdef DEBUG
  #ifdef DEBUG
      Scope* scope = script->lookupScope(REGS.pc);
      Scope* scope = script->lookupScope(REGS.pc);
@@ -1568,7 +1568,7 @@ diff --git a/js/src/vm/JSCompartment.h b/js/src/vm/JSCompartment.h
      void clearTables();
      void clearTables();
  
  
      void addSizeOfIncludingThis(mozilla::MallocSizeOf mallocSizeOf,
      void addSizeOfIncludingThis(mozilla::MallocSizeOf mallocSizeOf,
-@@ -1225,16 +1114,121 @@ class JS::Realm : public JSCompartment
+@@ -1233,16 +1122,121 @@ class JS::Realm : public JSCompartment
      }
      }
  
  
      static const size_t IterResultObjectValueSlot = 0;
      static const size_t IterResultObjectValueSlot = 0;

+ 17 - 8
frg/mozilla-release/work-js/1461938-28-62a1.patch

@@ -3,7 +3,7 @@
 # Date 1527097473 -7200
 # Date 1527097473 -7200
 #      Wed May 23 19:44:33 2018 +0200
 #      Wed May 23 19:44:33 2018 +0200
 # Node ID 84f6e67dcd1c7ad4b1acfeca4670ba5437e65ee3
 # Node ID 84f6e67dcd1c7ad4b1acfeca4670ba5437e65ee3
-# Parent  7d6335bbd6c3d2d7559c0cbdc6a3bdb797d75bad
+# Parent  12df5f3b0a4747eb44c331a278c442f46a9e657d
 Bug 1461938 part 28 - Rename LCovCompartment to LCovRealm and move to JS::Realm. r=luke
 Bug 1461938 part 28 - Rename LCovCompartment to LCovRealm and move to JS::Realm. r=luke
 
 
 diff --git a/js/src/vm/BytecodeUtil.cpp b/js/src/vm/BytecodeUtil.cpp
 diff --git a/js/src/vm/BytecodeUtil.cpp b/js/src/vm/BytecodeUtil.cpp
@@ -112,7 +112,7 @@ diff --git a/js/src/vm/BytecodeUtil.cpp b/js/src/vm/BytecodeUtil.cpp
 diff --git a/js/src/vm/CodeCoverage.cpp b/js/src/vm/CodeCoverage.cpp
 diff --git a/js/src/vm/CodeCoverage.cpp b/js/src/vm/CodeCoverage.cpp
 --- a/js/src/vm/CodeCoverage.cpp
 --- a/js/src/vm/CodeCoverage.cpp
 +++ b/js/src/vm/CodeCoverage.cpp
 +++ b/js/src/vm/CodeCoverage.cpp
-@@ -450,53 +450,53 @@ LCovSource::writeScript(JSScript* script
+@@ -450,59 +450,59 @@ LCovSource::writeScript(JSScript* script
      // assume that the code coverage report is complete, as this script has
      // assume that the code coverage report is complete, as this script has
      // references on all inner scripts.
      // references on all inner scripts.
      if (script->isTopLevel())
      if (script->isTopLevel())
@@ -130,6 +130,13 @@ diff --git a/js/src/vm/CodeCoverage.cpp b/js/src/vm/CodeCoverage.cpp
      MOZ_ASSERT(alloc_.isEmpty());
      MOZ_ASSERT(alloc_.isEmpty());
  }
  }
  
  
+-LCovCompartment::~LCovCompartment()
++LCovRealm::~LCovRealm()
+ {
+     if (sources_)
+         sources_->~LCovSourceVector();
+ }
+ 
  void
  void
 -LCovCompartment::collectCodeCoverageInfo(JSCompartment* comp, JSScript* script, const char* name)
 -LCovCompartment::collectCodeCoverageInfo(JSCompartment* comp, JSScript* script, const char* name)
 +LCovRealm::collectCodeCoverageInfo(JS::Realm* realm, JSScript* script, const char* name)
 +LCovRealm::collectCodeCoverageInfo(JS::Realm* realm, JSScript* script, const char* name)
@@ -172,7 +179,7 @@ diff --git a/js/src/vm/CodeCoverage.cpp b/js/src/vm/CodeCoverage.cpp
              return nullptr;
              return nullptr;
          }
          }
  
  
-@@ -520,17 +520,17 @@ LCovCompartment::lookupOrAdd(JSCompartme
+@@ -526,17 +526,17 @@ LCovCompartment::lookupOrAdd(JSCompartme
          outTN_.reportOutOfMemory();
          outTN_.reportOutOfMemory();
          return nullptr;
          return nullptr;
      }
      }
@@ -191,7 +198,7 @@ diff --git a/js/src/vm/CodeCoverage.cpp b/js/src/vm/CodeCoverage.cpp
      bool someComplete = false;
      bool someComplete = false;
      for (const LCovSource& sc : *sources_) {
      for (const LCovSource& sc : *sources_) {
          if (sc.isComplete()) {
          if (sc.isComplete()) {
-@@ -546,47 +546,48 @@ LCovCompartment::exportInto(GenericPrint
+@@ -552,47 +552,48 @@ LCovCompartment::exportInto(GenericPrint
      outTN_.exportInto(out);
      outTN_.exportInto(out);
      for (const LCovSource& sc : *sources_) {
      for (const LCovSource& sc : *sources_) {
          if (sc.isComplete())
          if (sc.isComplete())
@@ -243,7 +250,7 @@ diff --git a/js/src/vm/CodeCoverage.cpp b/js/src/vm/CodeCoverage.cpp
  LCovRuntime::LCovRuntime()
  LCovRuntime::LCovRuntime()
    : out_(),
    : out_(),
      pid_(getpid()),
      pid_(getpid()),
-@@ -644,28 +645,28 @@ LCovRuntime::finishFile()
+@@ -650,28 +651,28 @@ LCovRuntime::finishFile()
          char name[1024];
          char name[1024];
          if (!fillWithFilename(name, sizeof(name)))
          if (!fillWithFilename(name, sizeof(name)))
              return;
              return;
@@ -277,7 +284,7 @@ diff --git a/js/src/vm/CodeCoverage.cpp b/js/src/vm/CodeCoverage.cpp
 diff --git a/js/src/vm/CodeCoverage.h b/js/src/vm/CodeCoverage.h
 diff --git a/js/src/vm/CodeCoverage.h b/js/src/vm/CodeCoverage.h
 --- a/js/src/vm/CodeCoverage.h
 --- a/js/src/vm/CodeCoverage.h
 +++ b/js/src/vm/CodeCoverage.h
 +++ b/js/src/vm/CodeCoverage.h
-@@ -17,18 +17,16 @@
+@@ -18,18 +18,16 @@
  #include "vm/Printer.h"
  #include "vm/Printer.h"
  
  
  namespace js {
  namespace js {
@@ -296,7 +303,7 @@ diff --git a/js/src/vm/CodeCoverage.h b/js/src/vm/CodeCoverage.h
      ~LCovSource();
      ~LCovSource();
  
  
      // Whether the given script name matches this LCovSource.
      // Whether the given script name matches this LCovSource.
-@@ -77,46 +75,46 @@ class LCovSource
+@@ -78,47 +76,47 @@ class LCovSource
      size_t numLinesInstrumented_;
      size_t numLinesInstrumented_;
      size_t numLinesHit_;
      size_t numLinesHit_;
      size_t maxLineHit_;
      size_t maxLineHit_;
@@ -310,7 +317,9 @@ diff --git a/js/src/vm/CodeCoverage.h b/js/src/vm/CodeCoverage.h
  {
  {
    public:
    public:
 -    LCovCompartment();
 -    LCovCompartment();
+-    ~LCovCompartment();
 +    LCovRealm();
 +    LCovRealm();
++    ~LCovRealm();
  
  
      // Collect code coverage information for the given source.
      // Collect code coverage information for the given source.
 -    void collectCodeCoverageInfo(JSCompartment* comp, JSScript* topLevel, const char* name);
 -    void collectCodeCoverageInfo(JSCompartment* comp, JSScript* topLevel, const char* name);
@@ -350,7 +359,7 @@ diff --git a/js/src/vm/CodeCoverage.h b/js/src/vm/CodeCoverage.h
    public:
    public:
      LCovRuntime();
      LCovRuntime();
      ~LCovRuntime();
      ~LCovRuntime();
-@@ -128,19 +126,19 @@ class LCovRuntime
+@@ -130,19 +128,19 @@ class LCovRuntime
      //
      //
      // At the end of the execution, this file should contains the LCOV output of
      // At the end of the execution, this file should contains the LCOV output of
      // all the scripts executed in the current JSRuntime.
      // all the scripts executed in the current JSRuntime.

+ 20 - 20
frg/mozilla-release/work-js/1461938-32-62a1.patch

@@ -3,13 +3,13 @@
 # Date 1527156173 -7200
 # Date 1527156173 -7200
 #      Thu May 24 12:02:53 2018 +0200
 #      Thu May 24 12:02:53 2018 +0200
 # Node ID a7c669b99bd15f2b45561aa6bc649c847e9ae0d2
 # Node ID a7c669b99bd15f2b45561aa6bc649c847e9ae0d2
-# Parent  19b350681b94b7c9ba1ac517b0e1ac7068bec4b1
+# Parent  f2776a447a18fbe7f97d2efb9a239cc4d6b71a78
 Bug 1461938 part 32 - Rename JitCompartment to JitRealm and move to JS::Realm. r=luke
 Bug 1461938 part 32 - Rename JitCompartment to JitRealm and move to JS::Realm. r=luke
 
 
 diff --git a/js/src/builtin/TestingFunctions.cpp b/js/src/builtin/TestingFunctions.cpp
 diff --git a/js/src/builtin/TestingFunctions.cpp b/js/src/builtin/TestingFunctions.cpp
 --- a/js/src/builtin/TestingFunctions.cpp
 --- a/js/src/builtin/TestingFunctions.cpp
 +++ b/js/src/builtin/TestingFunctions.cpp
 +++ b/js/src/builtin/TestingFunctions.cpp
-@@ -5207,17 +5207,17 @@ BaselineCompile(JSContext* cx, unsigned 
+@@ -5181,17 +5181,17 @@ BaselineCompile(JSContext* cx, unsigned 
          if (!jit::IsBaselineEnabled(cx)) {
          if (!jit::IsBaselineEnabled(cx)) {
              returnedStr = "baseline disabled";
              returnedStr = "baseline disabled";
              break;
              break;
@@ -53,7 +53,7 @@ diff --git a/js/src/gc/Allocator.cpp b/js/src/gc/Allocator.cpp
 diff --git a/js/src/gc/GC.cpp b/js/src/gc/GC.cpp
 diff --git a/js/src/gc/GC.cpp b/js/src/gc/GC.cpp
 --- a/js/src/gc/GC.cpp
 --- a/js/src/gc/GC.cpp
 +++ b/js/src/gc/GC.cpp
 +++ b/js/src/gc/GC.cpp
-@@ -2573,17 +2573,17 @@ GCRuntime::sweepZoneAfterCompacting(Zone
+@@ -2608,17 +2608,17 @@ GCRuntime::sweepZoneAfterCompacting(Zone
      for (RealmsInZoneIter r(zone); !r.done(); r.next()) {
      for (RealmsInZoneIter r(zone); !r.done(); r.next()) {
          r->objectGroups.sweep();
          r->objectGroups.sweep();
          r->sweepRegExps();
          r->sweepRegExps();
@@ -70,9 +70,9 @@ diff --git a/js/src/gc/GC.cpp b/js/src/gc/GC.cpp
  }
  }
  
  
  template <typename T>
  template <typename T>
- static inline void
- UpdateCellPointers(MovingTracer* trc, T* cell)
-@@ -5547,18 +5547,18 @@ GCRuntime::sweepJitDataOnMainThread(Free
+ static inline void UpdateCellPointers(MovingTracer* trc, T* cell) {
+   // We only update unmoved GC things or the new copy of moved GC things, never
+@@ -5602,18 +5602,18 @@ GCRuntime::sweepJitDataOnMainThread(Free
  
  
          if (initialState != State::NotActive) {
          if (initialState != State::NotActive) {
              // Cancel any active or pending off thread compilations. We also did
              // Cancel any active or pending off thread compilations. We also did
@@ -96,7 +96,7 @@ diff --git a/js/src/gc/GC.cpp b/js/src/gc/GC.cpp
 diff --git a/js/src/gc/Nursery.cpp b/js/src/gc/Nursery.cpp
 diff --git a/js/src/gc/Nursery.cpp b/js/src/gc/Nursery.cpp
 --- a/js/src/gc/Nursery.cpp
 --- a/js/src/gc/Nursery.cpp
 +++ b/js/src/gc/Nursery.cpp
 +++ b/js/src/gc/Nursery.cpp
-@@ -777,20 +777,20 @@ js::Nursery::collect(JS::gcreason::Reaso
+@@ -778,20 +778,20 @@ js::Nursery::collect(JS::gcreason::Reaso
          if (shouldPretenure && zone->allocNurseryStrings && zone->tenuredStrings >= 30 * 1000) {
          if (shouldPretenure && zone->allocNurseryStrings && zone->tenuredStrings >= 30 * 1000) {
              if (!session.isSome())
              if (!session.isSome())
                  session.emplace(rt, JS::HeapState::MinorCollecting);
                  session.emplace(rt, JS::HeapState::MinorCollecting);
@@ -377,7 +377,7 @@ diff --git a/js/src/jit/CodeGenerator.cpp b/js/src/jit/CodeGenerator.cpp
      js_delete(scriptCounts_);
      js_delete(scriptCounts_);
  }
  }
  
  
-@@ -1889,17 +1889,17 @@ CreateMatchResultFallback(MacroAssembler
+@@ -1882,17 +1882,17 @@ CreateMatchResultFallback(MacroAssembler
  
  
      masm.branchPtr(Assembler::Equal, object, ImmWord(0), fail);
      masm.branchPtr(Assembler::Equal, object, ImmWord(0), fail);
  
  
@@ -396,7 +396,7 @@ diff --git a/js/src/jit/CodeGenerator.cpp b/js/src/jit/CodeGenerator.cpp
  
  
      // We are free to clobber all registers, as LRegExpMatcher is a call instruction.
      // We are free to clobber all registers, as LRegExpMatcher is a call instruction.
      AllocatableGeneralRegisterSet regs(GeneralRegisterSet::All());
      AllocatableGeneralRegisterSet regs(GeneralRegisterSet::All());
-@@ -2212,30 +2212,30 @@ CodeGenerator::visitRegExpMatcher(LRegEx
+@@ -2205,30 +2205,30 @@ CodeGenerator::visitRegExpMatcher(LRegEx
      MOZ_ASSERT(RegExpMatcherLastIndexReg != JSReturnReg);
      MOZ_ASSERT(RegExpMatcherLastIndexReg != JSReturnReg);
  #endif
  #endif
  
  
@@ -430,7 +430,7 @@ diff --git a/js/src/jit/CodeGenerator.cpp b/js/src/jit/CodeGenerator.cpp
  
  
      // We are free to clobber all registers, as LRegExpSearcher is a call instruction.
      // We are free to clobber all registers, as LRegExpSearcher is a call instruction.
      AllocatableGeneralRegisterSet regs(GeneralRegisterSet::All());
      AllocatableGeneralRegisterSet regs(GeneralRegisterSet::All());
-@@ -2362,30 +2362,30 @@ CodeGenerator::visitRegExpSearcher(LRegE
+@@ -2355,30 +2355,30 @@ CodeGenerator::visitRegExpSearcher(LRegE
      MOZ_ASSERT(RegExpTesterStringReg != ReturnReg);
      MOZ_ASSERT(RegExpTesterStringReg != ReturnReg);
      MOZ_ASSERT(RegExpTesterLastIndexReg != ReturnReg);
      MOZ_ASSERT(RegExpTesterLastIndexReg != ReturnReg);
  
  
@@ -464,7 +464,7 @@ diff --git a/js/src/jit/CodeGenerator.cpp b/js/src/jit/CodeGenerator.cpp
  
  
      StackMacroAssembler masm(cx);
      StackMacroAssembler masm(cx);
  
  
-@@ -2499,18 +2499,18 @@ CodeGenerator::visitRegExpTester(LRegExp
+@@ -2492,18 +2492,18 @@ CodeGenerator::visitRegExpTester(LRegExp
  
  
      MOZ_ASSERT(RegExpTesterRegExpReg != ReturnReg);
      MOZ_ASSERT(RegExpTesterRegExpReg != ReturnReg);
      MOZ_ASSERT(RegExpTesterStringReg != ReturnReg);
      MOZ_ASSERT(RegExpTesterStringReg != ReturnReg);
@@ -485,7 +485,7 @@ diff --git a/js/src/jit/CodeGenerator.cpp b/js/src/jit/CodeGenerator.cpp
  
  
  class OutOfLineRegExpPrototypeOptimizable : public OutOfLineCodeBase<CodeGenerator>
  class OutOfLineRegExpPrototypeOptimizable : public OutOfLineCodeBase<CodeGenerator>
  {
  {
-@@ -8197,18 +8197,18 @@ static const VMFunction ConcatStringsInf
+@@ -8190,18 +8190,18 @@ static const VMFunction ConcatStringsInf
      FunctionInfo<ConcatStringsFn>(ConcatStrings<CanGC>, "ConcatStrings");
      FunctionInfo<ConcatStringsFn>(ConcatStrings<CanGC>, "ConcatStrings");
  
  
  void
  void
@@ -506,7 +506,7 @@ diff --git a/js/src/jit/CodeGenerator.cpp b/js/src/jit/CodeGenerator.cpp
  
  
  void
  void
  CodeGenerator::visitConcat(LConcat* lir)
  CodeGenerator::visitConcat(LConcat* lir)
-@@ -8478,17 +8478,17 @@ CodeGenerator::visitSubstr(LSubstr* lir)
+@@ -8471,17 +8471,17 @@ CodeGenerator::visitSubstr(LSubstr* lir)
          masm.storeNonInlineStringChars(temp, output);
          masm.storeNonInlineStringChars(temp, output);
          masm.jump(done);
          masm.jump(done);
      }
      }
@@ -525,7 +525,7 @@ diff --git a/js/src/jit/CodeGenerator.cpp b/js/src/jit/CodeGenerator.cpp
      Register temp1 = CallTempReg2;
      Register temp1 = CallTempReg2;
      Register temp2 = CallTempReg3;
      Register temp2 = CallTempReg3;
      Register temp3 = CallTempReg4;
      Register temp3 = CallTempReg4;
-@@ -10288,19 +10288,19 @@ CodeGenerator::link(JSContext* cx, Compi
+@@ -10282,19 +10282,19 @@ CodeGenerator::link(JSContext* cx, Compi
      // removed from the relevant lists by this point. Don't allow GC here.
      // removed from the relevant lists by this point. Don't allow GC here.
      JS::AutoAssertNoGC nogc(cx);
      JS::AutoAssertNoGC nogc(cx);
  
  
@@ -987,7 +987,7 @@ diff --git a/js/src/jit/Ion.cpp b/js/src/jit/Ion.cpp
 diff --git a/js/src/jit/IonAnalysis.cpp b/js/src/jit/IonAnalysis.cpp
 diff --git a/js/src/jit/IonAnalysis.cpp b/js/src/jit/IonAnalysis.cpp
 --- a/js/src/jit/IonAnalysis.cpp
 --- a/js/src/jit/IonAnalysis.cpp
 +++ b/js/src/jit/IonAnalysis.cpp
 +++ b/js/src/jit/IonAnalysis.cpp
-@@ -4207,17 +4207,17 @@ jit::AnalyzeNewScriptDefiniteProperties(
+@@ -4217,17 +4217,17 @@ jit::AnalyzeNewScriptDefiniteProperties(
  
  
      LifoAlloc alloc(TempAllocator::PreferredLifoChunkSize);
      LifoAlloc alloc(TempAllocator::PreferredLifoChunkSize);
      TempAllocator temp(&alloc);
      TempAllocator temp(&alloc);
@@ -1006,7 +1006,7 @@ diff --git a/js/src/jit/IonAnalysis.cpp b/js/src/jit/IonAnalysis.cpp
              return false;
              return false;
          if (status != Method_Compiled)
          if (status != Method_Compiled)
              return true;
              return true;
-@@ -4461,17 +4461,17 @@ jit::AnalyzeArgumentsUsage(JSContext* cx
+@@ -4471,17 +4471,17 @@ jit::AnalyzeArgumentsUsage(JSContext* cx
  
  
      LifoAlloc alloc(TempAllocator::PreferredLifoChunkSize);
      LifoAlloc alloc(TempAllocator::PreferredLifoChunkSize);
      TempAllocator temp(&alloc);
      TempAllocator temp(&alloc);
@@ -1221,7 +1221,7 @@ diff --git a/js/src/jit/Linker.h b/js/src/jit/Linker.h
 diff --git a/js/src/jit/MCallOptimize.cpp b/js/src/jit/MCallOptimize.cpp
 diff --git a/js/src/jit/MCallOptimize.cpp b/js/src/jit/MCallOptimize.cpp
 --- a/js/src/jit/MCallOptimize.cpp
 --- a/js/src/jit/MCallOptimize.cpp
 +++ b/js/src/jit/MCallOptimize.cpp
 +++ b/js/src/jit/MCallOptimize.cpp
-@@ -2147,17 +2147,17 @@ IonBuilder::inlineRegExpMatcher(CallInfo
+@@ -2187,17 +2187,17 @@ IonBuilder::inlineRegExpMatcher(CallInfo
  
  
      if (strArg->type() != MIRType::String && strArg->type() != MIRType::Value)
      if (strArg->type() != MIRType::String && strArg->type() != MIRType::Value)
          return InliningStatus_NotInlined;
          return InliningStatus_NotInlined;
@@ -1240,7 +1240,7 @@ diff --git a/js/src/jit/MCallOptimize.cpp b/js/src/jit/MCallOptimize.cpp
  
  
      MInstruction* matcher = MRegExpMatcher::New(alloc(), rxArg, strArg, lastIndexArg);
      MInstruction* matcher = MRegExpMatcher::New(alloc(), rxArg, strArg, lastIndexArg);
      current->add(matcher);
      current->add(matcher);
-@@ -2191,17 +2191,17 @@ IonBuilder::inlineRegExpSearcher(CallInf
+@@ -2231,17 +2231,17 @@ IonBuilder::inlineRegExpSearcher(CallInf
  
  
      if (strArg->type() != MIRType::String && strArg->type() != MIRType::Value)
      if (strArg->type() != MIRType::String && strArg->type() != MIRType::Value)
          return InliningStatus_NotInlined;
          return InliningStatus_NotInlined;
@@ -1259,7 +1259,7 @@ diff --git a/js/src/jit/MCallOptimize.cpp b/js/src/jit/MCallOptimize.cpp
  
  
      MInstruction* searcher = MRegExpSearcher::New(alloc(), rxArg, strArg, lastIndexArg);
      MInstruction* searcher = MRegExpSearcher::New(alloc(), rxArg, strArg, lastIndexArg);
      current->add(searcher);
      current->add(searcher);
-@@ -2235,17 +2235,17 @@ IonBuilder::inlineRegExpTester(CallInfo&
+@@ -2275,17 +2275,17 @@ IonBuilder::inlineRegExpTester(CallInfo&
  
  
      if (strArg->type() != MIRType::String && strArg->type() != MIRType::Value)
      if (strArg->type() != MIRType::String && strArg->type() != MIRType::Value)
          return InliningStatus_NotInlined;
          return InliningStatus_NotInlined;
@@ -2288,7 +2288,7 @@ diff --git a/js/src/vm/JSCompartment.h b/js/src/vm/JSCompartment.h
      const JS::RealmBehaviors& behaviors() const { return behaviors_; }
      const JS::RealmBehaviors& behaviors() const { return behaviors_; }
  
  
      /* Whether to preserve JIT code on non-shrinking GCs. */
      /* Whether to preserve JIT code on non-shrinking GCs. */
-@@ -1223,16 +1215,23 @@ class JS::Realm : public JSCompartment
+@@ -1231,16 +1223,23 @@ class JS::Realm : public JSCompartment
      mozilla::HashCodeScrambler randomHashCodeScrambler();
      mozilla::HashCodeScrambler randomHashCodeScrambler();
  
  
      bool isAccessValid() const {
      bool isAccessValid() const {

+ 12 - 12
frg/mozilla-release/work-js/1461938-33-62a1.patch

@@ -3,7 +3,7 @@
 # Date 1527156173 -7200
 # Date 1527156173 -7200
 #      Thu May 24 12:02:53 2018 +0200
 #      Thu May 24 12:02:53 2018 +0200
 # Node ID 6a363dbae27396fd891c955e0dbe42957b89d10a
 # Node ID 6a363dbae27396fd891c955e0dbe42957b89d10a
-# Parent  a7c669b99bd15f2b45561aa6bc649c847e9ae0d2
+# Parent  d82cda1c0ace2725d8321bb397d2951454116e10
 Bug 1461938 part 33 - Introduce ObjectRealm and use it for some fields. r=jonco
 Bug 1461938 part 33 - Introduce ObjectRealm and use it for some fields. r=jonco
 
 
 diff --git a/js/src/builtin/Array.cpp b/js/src/builtin/Array.cpp
 diff --git a/js/src/builtin/Array.cpp b/js/src/builtin/Array.cpp
@@ -96,7 +96,7 @@ diff --git a/js/src/builtin/Eval.cpp b/js/src/builtin/Eval.cpp
 diff --git a/js/src/builtin/TypedObject.cpp b/js/src/builtin/TypedObject.cpp
 diff --git a/js/src/builtin/TypedObject.cpp b/js/src/builtin/TypedObject.cpp
 --- a/js/src/builtin/TypedObject.cpp
 --- a/js/src/builtin/TypedObject.cpp
 +++ b/js/src/builtin/TypedObject.cpp
 +++ b/js/src/builtin/TypedObject.cpp
-@@ -1344,17 +1344,17 @@ TypedObject::typedMemBase() const
+@@ -1342,17 +1342,17 @@ TypedObject::typedMemBase() const
          return owner.as<ArrayBufferObject>().dataPointer();
          return owner.as<ArrayBufferObject>().dataPointer();
      return owner.as<InlineTypedObject>().inlineTypedMem();
      return owner.as<InlineTypedObject>().inlineTypedMem();
  }
  }
@@ -115,7 +115,7 @@ diff --git a/js/src/builtin/TypedObject.cpp b/js/src/builtin/TypedObject.cpp
          return true;
          return true;
      }
      }
      if (is<InlineOpaqueTypedObject>())
      if (is<InlineOpaqueTypedObject>())
-@@ -2138,25 +2138,26 @@ InlineTypedObject::obj_moved(JSObject* d
+@@ -2136,25 +2136,26 @@ InlineTypedObject::obj_moved(JSObject* d
      }
      }
  
  
      return 0;
      return 0;
@@ -148,7 +148,7 @@ diff --git a/js/src/builtin/TypedObject.cpp b/js/src/builtin/TypedObject.cpp
 diff --git a/js/src/gc/GC.cpp b/js/src/gc/GC.cpp
 diff --git a/js/src/gc/GC.cpp b/js/src/gc/GC.cpp
 --- a/js/src/gc/GC.cpp
 --- a/js/src/gc/GC.cpp
 +++ b/js/src/gc/GC.cpp
 +++ b/js/src/gc/GC.cpp
-@@ -2574,17 +2574,17 @@ GCRuntime::sweepZoneAfterCompacting(Zone
+@@ -2609,17 +2609,17 @@ GCRuntime::sweepZoneAfterCompacting(Zone
          r->objectGroups.sweep();
          r->objectGroups.sweep();
          r->sweepRegExps();
          r->sweepRegExps();
          r->sweepSavedStacks();
          r->sweepSavedStacks();
@@ -164,10 +164,10 @@ diff --git a/js/src/gc/GC.cpp b/js/src/gc/GC.cpp
  }
  }
  
  
  template <typename T>
  template <typename T>
- static inline void
- UpdateCellPointers(MovingTracer* trc, T* cell)
- {
-@@ -5426,17 +5426,17 @@ static void
+ static inline void UpdateCellPointers(MovingTracer* trc, T* cell) {
+   // We only update unmoved GC things or the new copy of moved GC things, never
+   // the old copy. If this happened it could clear the forwarded flag which
+@@ -5481,17 +5481,17 @@ static void
  SweepMisc(GCParallelTask* task)
  SweepMisc(GCParallelTask* task)
  {
  {
      JSRuntime* runtime = task->runtime();
      JSRuntime* runtime = task->runtime();
@@ -189,7 +189,7 @@ diff --git a/js/src/gc/GC.cpp b/js/src/gc/GC.cpp
 diff --git a/js/src/jit/CacheIR.cpp b/js/src/jit/CacheIR.cpp
 diff --git a/js/src/jit/CacheIR.cpp b/js/src/jit/CacheIR.cpp
 --- a/js/src/jit/CacheIR.cpp
 --- a/js/src/jit/CacheIR.cpp
 +++ b/js/src/jit/CacheIR.cpp
 +++ b/js/src/jit/CacheIR.cpp
-@@ -4367,17 +4367,17 @@ GetIteratorIRGenerator::tryAttachNativeI
+@@ -4366,17 +4366,17 @@ GetIteratorIRGenerator::tryAttachNativeI
          writer.guardNoDenseElements(objId);
          writer.guardNoDenseElements(objId);
      else if (expandoId)
      else if (expandoId)
          writer.guardNoDenseElements(*expandoId);
          writer.guardNoDenseElements(*expandoId);
@@ -211,7 +211,7 @@ diff --git a/js/src/jit/CacheIR.cpp b/js/src/jit/CacheIR.cpp
 diff --git a/js/src/jsapi.cpp b/js/src/jsapi.cpp
 diff --git a/js/src/jsapi.cpp b/js/src/jsapi.cpp
 --- a/js/src/jsapi.cpp
 --- a/js/src/jsapi.cpp
 +++ b/js/src/jsapi.cpp
 +++ b/js/src/jsapi.cpp
-@@ -1289,27 +1289,27 @@ extern JS_PUBLIC_API(JSObject*)
+@@ -1278,27 +1278,27 @@ extern JS_PUBLIC_API(JSObject*)
  JS_GlobalLexicalEnvironment(JSObject* obj)
  JS_GlobalLexicalEnvironment(JSObject* obj)
  {
  {
      return &obj->as<GlobalObject>().lexicalEnvironment();
      return &obj->as<GlobalObject>().lexicalEnvironment();
@@ -241,7 +241,7 @@ diff --git a/js/src/jsapi.cpp b/js/src/jsapi.cpp
  JS::CurrentGlobalOrNull(JSContext* cx)
  JS::CurrentGlobalOrNull(JSContext* cx)
  {
  {
      AssertHeapIsIdleOrIterating();
      AssertHeapIsIdleOrIterating();
-@@ -3625,17 +3625,17 @@ CreateNonSyntacticEnvironmentChain(JSCon
+@@ -3613,17 +3613,17 @@ CreateNonSyntacticEnvironmentChain(JSCon
          // Also get a non-syntactic lexical environment to capture 'let' and
          // Also get a non-syntactic lexical environment to capture 'let' and
          // 'const' bindings. To persist lexical bindings, we have a 1-1
          // 'const' bindings. To persist lexical bindings, we have a 1-1
          // mapping with the final unwrapped environment object (the
          // mapping with the final unwrapped environment object (the
@@ -263,7 +263,7 @@ diff --git a/js/src/jsapi.cpp b/js/src/jsapi.cpp
 diff --git a/js/src/jsfriendapi.cpp b/js/src/jsfriendapi.cpp
 diff --git a/js/src/jsfriendapi.cpp b/js/src/jsfriendapi.cpp
 --- a/js/src/jsfriendapi.cpp
 --- a/js/src/jsfriendapi.cpp
 +++ b/js/src/jsfriendapi.cpp
 +++ b/js/src/jsfriendapi.cpp
-@@ -1420,17 +1420,17 @@ JS_FRIEND_API(void)
+@@ -1426,17 +1426,17 @@ JS_FRIEND_API(void)
  js::SetAllocationMetadataBuilder(JSContext* cx, const AllocationMetadataBuilder* callback)
  js::SetAllocationMetadataBuilder(JSContext* cx, const AllocationMetadataBuilder* callback)
  {
  {
      cx->realm()->setAllocationMetadataBuilder(callback);
      cx->realm()->setAllocationMetadataBuilder(callback);

+ 0 - 0
frg/mozilla-release/work-js/mozilla-central-push_419649.patch → frg/mozilla-release/work-js/1462939-1-62a1.patch


+ 0 - 0
frg/mozilla-release/work-js/mozilla-central-push_419676.patch → frg/mozilla-release/work-js/1463717-62a1.patch


+ 0 - 0
frg/mozilla-release/work-js/mozilla-central-push_419677.patch → frg/mozilla-release/work-js/1463723-62a1.patch


+ 0 - 0
frg/mozilla-release/work-js/mozilla-central-push_419730.patch → frg/mozilla-release/work-js/1463847-1-62a1.patch


+ 0 - 0
frg/mozilla-release/work-js/mozilla-central-push_419731.patch → frg/mozilla-release/work-js/1463847-2-62a1.patch


+ 0 - 0
frg/mozilla-release/work-js/mozilla-central-push_419788.patch → frg/mozilla-release/work-js/1463939-1-62a1.patch


+ 0 - 0
frg/mozilla-release/work-js/mozilla-central-push_419789.patch → frg/mozilla-release/work-js/1463939-2-62a1.patch


+ 0 - 0
frg/mozilla-release/work-js/mozilla-central-push_419790.patch → frg/mozilla-release/work-js/1463939-3-62a1.patch


Some files were not shown because too many files changed in this diff