Merge mozilla-central to autoland. a=merge on a CLOSED TREE
authorDaniel Varga <dvarga@mozilla.com>
Tue, 27 Nov 2018 07:36:22 +0200
changeset 504618 e321cef882b8caf0063cf9d17eec89a8b3170606
parent 504617 d5a15c64bd401768c581663b1b13f45e66080616 (current diff)
parent 504505 510f4bccd603a6a64b514e174c5d9e52d7afd185 (diff)
child 504619 b0e69b1368832a3846c0940a6fb8bb834bdc62a1
push id10290
push userffxbld-merge
push dateMon, 03 Dec 2018 16:23:23 +0000
treeherdermozilla-beta@700bed2445e6 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmerge
milestone65.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Merge mozilla-central to autoland. a=merge on a CLOSED TREE
--- a/browser/components/sessionstore/test/browser.ini
+++ b/browser/components/sessionstore/test/browser.ini
@@ -227,17 +227,17 @@ skip-if = verify
 [browser_694378.js]
 [browser_701377.js]
 skip-if = (verify && debug && (os == 'mac' || os == 'win'))
 [browser_705597.js]
 [browser_707862.js]
 [browser_739531.js]
 [browser_739805.js]
 [browser_819510_perwindowpb.js]
-skip-if = (os == 'win' && bits == 64) || (os == "mac") || (os == "linux") # Win: Bug 1284312, Mac: Bug 1341980, Linux: bug 1381451
+skip-if = true # Bug 1284312, Bug 1341980, bug 1381451
 [browser_not_collect_when_idle.js]
 
 # Disabled for frequent intermittent failures
 [browser_464620_a.js]
 skip-if = true
 [browser_464620_b.js]
 skip-if = true
 
--- a/browser/extensions/formautofill/content/autofillEditForms.js
+++ b/browser/extensions/formautofill/content/autofillEditForms.js
@@ -375,27 +375,51 @@ class EditCreditCard extends EditAutofil
   loadRecord(record, addresses, preserveFieldValues) {
     // _record must be updated before generateYears and generateBillingAddressOptions are called.
     this._record = record;
     this._addresses = addresses;
     this.generateBillingAddressOptions(preserveFieldValues);
     if (!preserveFieldValues) {
       // Re-populating the networks will reset the selected option.
       this.populateNetworks();
+      // Re-generating the months will reset the selected option.
+      this.generateMonths();
       // Re-generating the years will reset the selected option.
       this.generateYears();
       super.loadRecord(record);
 
       // Resetting the form in the super.loadRecord won't clear custom validity
       // state so reset it here. Since the cc-number field is disabled upon editing
       // we don't need to recaclulate its validity here.
       this._elements.ccNumber.setCustomValidity("");
     }
   }
 
+  generateMonths() {
+    const count = 12;
+
+    // Clear the list
+    this._elements.month.textContent = "";
+
+    // Empty month option
+    this._elements.month.appendChild(new Option());
+
+    // Populate month list. Format: "month number - month name"
+    let dateFormat = new Intl.DateTimeFormat(navigator.language, {month: "long"}).format;
+    for (let i = 0; i < count; i++) {
+      let monthNumber = (i + 1).toString();
+      let monthName = dateFormat(new Date(Date.UTC(1970, i, 1)));
+      let option = new Option();
+      option.value = monthNumber;
+      // XXX: Bug 1446164 - Localize this string.
+      option.textContent = `${monthNumber.padStart(2, "0")} - ${monthName}`;
+      this._elements.month.appendChild(option);
+    }
+  }
+
   generateYears() {
     const count = 11;
     const currentYear = new Date().getFullYear();
     const ccExpYear = this._record && this._record["cc-exp-year"];
 
     // Clear the list
     this._elements.year.textContent = "";
 
--- a/browser/extensions/formautofill/content/editCreditCard.xhtml
+++ b/browser/extensions/formautofill/content/editCreditCard.xhtml
@@ -25,28 +25,16 @@
     <label id="cc-number-container" class="container">
       <span id="invalidCardNumberString" hidden="hidden" data-localization="invalidCardNumber"></span>
       <input id="cc-number" type="text" required="required" minlength="9" pattern="[- 0-9]+"/>
       <span data-localization="cardNumber" class="label-text"/>
     </label>
     <label id="cc-exp-month-container" class="container">
       <select id="cc-exp-month" required="required">
         <option/>
-        <option value="1">01</option>
-        <option value="2">02</option>
-        <option value="3">03</option>
-        <option value="4">04</option>
-        <option value="5">05</option>
-        <option value="6">06</option>
-        <option value="7">07</option>
-        <option value="8">08</option>
-        <option value="9">09</option>
-        <option value="10">10</option>
-        <option value="11">11</option>
-        <option value="12">12</option>
       </select>
       <span data-localization="cardExpiresMonth" class="label-text"/>
     </label>
     <label id="cc-exp-year-container" class="container">
       <select id="cc-exp-year" required="required">
         <option/>
       </select>
       <span data-localization="cardExpiresYear" class="label-text"/>
--- a/intl/locale/MozLocale.cpp
+++ b/intl/locale/MozLocale.cpp
@@ -13,25 +13,23 @@
 using namespace mozilla::intl;
 
 /**
  * Note: The file name is `MozLocale` to avoid compilation problems on case-insensitive
  * Windows. The class name is `Locale`.
  */
 Locale::Locale(const nsACString& aLocale)
 {
-  MOZ_ASSERT(!aLocale.IsEmpty(), "Locale string cannot be empty");
-
-  int32_t position = 0;
-
-  if (!IsASCII(aLocale)) {
+  if (aLocale.IsEmpty() || !IsASCII(aLocale)) {
     mIsWellFormed = false;
     return;
   }
 
+  int32_t position = 0;
+
   nsAutoCString normLocale(aLocale);
   normLocale.ReplaceChar('_', '-');
 
   /**
    * BCP47 language tag:
    *
    * langtag = language            2*3ALPHA
    *           ["-" extlang]       3ALPHA *2("-" 3ALPHA)
--- a/intl/locale/nsLanguageAtomService.cpp
+++ b/intl/locale/nsLanguageAtomService.cpp
@@ -178,17 +178,17 @@ nsLanguageAtomService::GetUncachedLangua
     if (BinarySearchIf(kLangGroups, 0, ArrayLength(kLangGroups),
                        [&langStr](const char* tag) -> int {
                          return langStr.Compare(tag);
                        },
                        &unused)) {
       langGroup = NS_Atomize(langStr);
       return langGroup.forget();
     }
-  } else if (!langStr.IsEmpty()) {
+  } else {
     // If the lang code can be parsed as BCP47, look up its (likely) script
     Locale loc(langStr);
     if (loc.IsWellFormed()) {
       if (loc.GetScript().IsEmpty()) {
         loc.AddLikelySubtags();
       }
       if (loc.GetScript().EqualsLiteral("Hant")) {
         if (loc.GetRegion().EqualsLiteral("HK")) {
--- a/layout/svg/nsSVGUtils.cpp
+++ b/layout/svg/nsSVGUtils.cpp
@@ -869,21 +869,20 @@ nsSVGUtils::PaintFrameWithEffects(nsIFra
     MOZ_ASSERT(target != &aContext);
     blender.BlendToTarget();
   }
 }
 
 bool
 nsSVGUtils::HitTestClip(nsIFrame *aFrame, const gfxPoint &aPoint)
 {
+  // If the clip-path property references non-existent or invalid clipPath
+  // element(s) we ignore it.
   nsSVGClipPathFrame* clipPathFrame;
-  if (SVGObserverUtils::GetAndObserveClipPath(aFrame, &clipPathFrame) ==
-        SVGObserverUtils::eHasRefsSomeInvalid) {
-    return false; // everything clipped away if clip path is invalid
-  }
+  SVGObserverUtils::GetAndObserveClipPath(aFrame, &clipPathFrame);
   if (clipPathFrame) {
     return clipPathFrame->PointIsInsideClipPath(aFrame, aPoint);
   }
   if (aFrame->StyleSVGReset()->HasClipPath()) {
     return nsCSSClipPathInstance::HitTestBasicShapeOrPathClip(aFrame, aPoint);
   }
   return true;
 }
--- a/mfbt/HashTable.h
+++ b/mfbt/HashTable.h
@@ -79,16 +79,17 @@
 #include "mozilla/Attributes.h"
 #include "mozilla/Casting.h"
 #include "mozilla/HashFunctions.h"
 #include "mozilla/MathAlgorithms.h"
 #include "mozilla/MemoryChecking.h"
 #include "mozilla/MemoryReporting.h"
 #include "mozilla/Move.h"
 #include "mozilla/Opaque.h"
+#include "mozilla/OperatorNewExtensions.h"
 #include "mozilla/PodOperations.h"
 #include "mozilla/ReentrancyGuard.h"
 #include "mozilla/TypeTraits.h"
 #include "mozilla/UniquePtr.h"
 
 namespace mozilla {
 
 template<class>
@@ -1163,17 +1164,17 @@ public:
 
   HashNumber getKeyHash() const { return mKeyHash & ~sCollisionBit; }
 
   template<typename... Args>
   void setLive(HashNumber aHashNumber, Args&&... aArgs)
   {
     MOZ_ASSERT(!isLive());
     mKeyHash = aHashNumber;
-    new (valuePtr()) T(std::forward<Args>(aArgs)...);
+    new (KnownNotNull, valuePtr()) T(std::forward<Args>(aArgs)...);
     MOZ_ASSERT(isLive());
   }
 };
 
 template<class T, class HashPolicy, class AllocPolicy>
 class HashTable : private AllocPolicy
 {
   friend class mozilla::ReentrancyGuard;
@@ -1650,17 +1651,17 @@ public:
                             uint32_t aCapacity,
                             FailureBehavior aReportFailure = ReportFailure)
   {
     Entry* table = aReportFailure
                      ? aAllocPolicy.template pod_malloc<Entry>(aCapacity)
                      : aAllocPolicy.template maybe_pod_malloc<Entry>(aCapacity);
     if (table) {
       for (uint32_t i = 0; i < aCapacity; i++) {
-        new (&table[i]) Entry();
+        new (KnownNotNull, &table[i]) Entry();
       }
     }
     return table;
   }
 
   static void destroyTable(AllocPolicy& aAllocPolicy,
                            Entry* aOldTable,
                            uint32_t aCapacity)
--- a/mfbt/SegmentedVector.h
+++ b/mfbt/SegmentedVector.h
@@ -21,16 +21,17 @@
 #define mozilla_SegmentedVector_h
 
 #include "mozilla/AllocPolicy.h"
 #include "mozilla/Array.h"
 #include "mozilla/Attributes.h"
 #include "mozilla/LinkedList.h"
 #include "mozilla/MemoryReporting.h"
 #include "mozilla/Move.h"
+#include "mozilla/OperatorNewExtensions.h"
 #include "mozilla/TypeTraits.h"
 
 #include <new>  // for placement new
 
 namespace mozilla {
 
 // |IdealSegmentSize| specifies how big each segment will be in bytes (or as
 // close as is possible). Use the following guidelines to choose a size.
@@ -95,17 +96,17 @@ class SegmentedVector : private AllocPol
 
     template<typename U>
     void Append(U&& aU)
     {
       MOZ_ASSERT(mLength < SegmentCapacity);
       // Pre-increment mLength so that the bounds-check in operator[] passes.
       mLength++;
       T* elem = &(*this)[mLength - 1];
-      new (elem) T(std::forward<U>(aU));
+      new (KnownNotNull, elem) T(std::forward<U>(aU));
     }
 
     void PopLast()
     {
       MOZ_ASSERT(mLength > 0);
       (*this)[mLength - 1].~T();
       mLength--;
     }
@@ -166,17 +167,17 @@ public:
   MOZ_MUST_USE bool Append(U&& aU)
   {
     Segment* last = mSegments.getLast();
     if (!last || last->Length() == kSegmentCapacity) {
       last = this->template pod_malloc<Segment>(1);
       if (!last) {
         return false;
       }
-      new (last) Segment();
+      new (KnownNotNull, last) Segment();
       mSegments.insertBack(last);
     }
     last->Append(std::forward<U>(aU));
     return true;
   }
 
   // You should probably only use this instead of Append() if you are using an
   // infallible allocation policy. It will crash if the allocation fails.
--- a/mobile/android/tests/browser/chrome/test_awsy_lite.html
+++ b/mobile/android/tests/browser/chrome/test_awsy_lite.html
@@ -38,16 +38,31 @@
   SimpleTest.waitForExplicitFinish();
   SimpleTest.requestLongerTimeout(3); // several long waits and GCs make for a long-running test
   SimpleTest.requestCompleteLog(); // so that "PERFHERDER_DATA" can be scraped from the log
 
   function checkpoint(aName) {
     var mrm = Cc["@mozilla.org/memory-reporter-manager;1"].getService(Ci.nsIMemoryReporterManager);
     gResults.push( { name: aName, resident: mrm.resident } );
     info(`${aName} | Resident Memory: ${mrm.resident}`);
+    var env = Cc["@mozilla.org/process/environment;1"].getService(Ci.nsIEnvironment);
+    var upload = env.get("MOZ_UPLOAD_DIR");
+    if (upload) {
+        var path = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile);
+        path.initWithPath(upload);
+        if (!path.exists()) {
+            path.create(Ci.nsIFile.DIRECTORY_TYPE, 0o755);
+        }
+        var fileName = aName.replace(/ /g, "_").replace(/\W/g, "") + ".json.gz";
+        path.appendRelativePath(fileName);
+        var dumper = Cc["@mozilla.org/memory-info-dumper;1"].getService(Ci.nsIMemoryInfoDumper);
+        dumper.dumpMemoryReportsToNamedFile(path.path, function() {
+            info("finished dump to " + path.path);
+        }, null, /* anonymize = */ false);
+    }
   }
 
   var browserListener = {
     onOpenWindow: function(aXulWin) {
         var win = aXulWin.docShell.domWindow;
         win.addEventListener("UIReady", function(aEvent) {
             attachTo(win);
         }, {once: true});
--- a/taskcluster/ci/test/compiled.yml
+++ b/taskcluster/ci/test/compiled.yml
@@ -57,16 +57,17 @@ jittest:
     treeherder-symbol: Jit
     instance-size:
         by-test-platform:
             android-em.*: xlarge
             default: default
     run-on-projects:
         by-test-platform:
             android-hw.*: ['try']
+            linux.*: []  # redundant with SM(...)
             default: built-projects
     chunks:
         by-test-platform:
             windows.*: 1
             windows10-64-ccov/debug: 6
             macosx.*: 1
             macosx64-ccov/debug: 4
             android-em-4.3-arm7-api-15/debug: 20
--- a/testing/mochitest/runtestsremote.py
+++ b/testing/mochitest/runtestsremote.py
@@ -313,16 +313,21 @@ class MochiRemote(MochitestDesktop):
             del browserEnv["XPCOM_MEM_BLOAT_LOG"]
         # override mozLogs to avoid processing in MochitestDesktop base class
         self.mozLogs = None
         browserEnv["MOZ_LOG_FILE"] = os.path.join(
             self.remoteMozLog,
             self.mozLogName)
         if options.dmd:
             browserEnv['DMD'] = '1'
+        # Contents of remoteMozLog will be pulled from device and copied to the
+        # host MOZ_UPLOAD_DIR, to be made available as test artifacts. Make
+        # MOZ_UPLOAD_DIR available to the browser environment so that tests
+        # can use it as though they were running on the host.
+        browserEnv["MOZ_UPLOAD_DIR"] = self.remoteMozLog
         return browserEnv
 
     def runApp(self, *args, **kwargs):
         """front-end automation.py's `runApp` functionality until FennecRunner is written"""
 
         # automation.py/remoteautomation `runApp` takes the profile path,
         # whereas runtest.py's `runApp` takes a mozprofile object.
         if 'profileDir' not in kwargs and 'profile' in kwargs:
new file mode 100644
--- /dev/null
+++ b/testing/web-platform/meta/css/css-grid/grid-model/grid-container-sizing-constraints-001.html.ini
@@ -0,0 +1,19 @@
+[grid-container-sizing-constraints-001.html]
+  [.grid 2]
+    expected: FAIL
+
+  [.grid 4]
+    expected: FAIL
+
+  [.grid 7]
+    expected: FAIL
+
+  [.grid 9]
+    expected: FAIL
+
+  [.grid 12]
+    expected: FAIL
+
+  [.grid 14]
+    expected: FAIL
+
new file mode 100644
--- /dev/null
+++ b/testing/web-platform/meta/css/css-masking/clip/clip-filter-order.html.ini
@@ -0,0 +1,4 @@
+[clip-filter-order.html]
+  expected:
+    if debug and webrender and e10s and (os == "linux") and (version == "Ubuntu 16.04") and (processor == "x86_64") and (bits == 64): FAIL
+    if not debug and webrender and e10s and (os == "linux") and (version == "Ubuntu 16.04") and (processor == "x86_64") and (bits == 64): FAIL
new file mode 100644
--- /dev/null
+++ b/testing/web-platform/meta/css/css-tables/percent-width-ignored-002.tentative.html.ini
@@ -0,0 +1,4 @@
+[percent-width-ignored-002.tentative.html]
+  [#stf 1]
+    expected: FAIL
+
new file mode 100644
--- /dev/null
+++ b/testing/web-platform/meta/fetch/api/response/response-init-001.html.ini
@@ -0,0 +1,4 @@
+[response-init-001.html]
+  [Check default value for statusText attribute]
+    expected: FAIL
+
--- a/testing/web-platform/meta/mediacapture-streams/MediaDevices-enumerateDevices.https.html.ini
+++ b/testing/web-platform/meta/mediacapture-streams/MediaDevices-enumerateDevices.https.html.ini
@@ -1,4 +1,7 @@
 [MediaDevices-enumerateDevices.https.html]
   [mediaDevices.enumerateDevices() is present and working on navigator]
     expected: FAIL
 
+  [InputDeviceInfo is supported]
+    expected: FAIL
+
--- a/testing/web-platform/meta/mediacapture-streams/MediaDevices-getUserMedia.https.html.ini
+++ b/testing/web-platform/meta/mediacapture-streams/MediaDevices-getUserMedia.https.html.ini
@@ -1,7 +1,19 @@
 [MediaDevices-getUserMedia.https.html]
   [groupId is correctly supported by getUserMedia() for video devices]
     expected: FAIL
 
   [groupId is correctly supported by getUserMedia() for audio devices]
     expected: FAIL
 
+  [getUserMedia() supports resizeMode.]
+    expected: FAIL
+
+  [getUserMedia() fails with exact invalid resizeMode.]
+    expected: FAIL
+
+  [getUserMedia() supports setting none as resizeMode.]
+    expected: FAIL
+
+  [getUserMedia() supports setting crop-and-scale as resizeMode.]
+    expected: FAIL
+
--- a/testing/web-platform/meta/mediacapture-streams/MediaStreamTrack-applyConstraints.https.html.ini
+++ b/testing/web-platform/meta/mediacapture-streams/MediaStreamTrack-applyConstraints.https.html.ini
@@ -1,7 +1,10 @@
 [MediaStreamTrack-applyConstraints.https.html]
   [applyConstraints rejects invalid groupID]
     expected: FAIL
 
   [applyConstraints rejects attempt to switch device using groupId]
     expected: FAIL
 
+  [applyConstraints rejects invalid resizeMode]
+    expected: FAIL
+
--- a/testing/web-platform/meta/mediacapture-streams/MediaStreamTrack-getCapabilities.https.html.ini
+++ b/testing/web-platform/meta/mediacapture-streams/MediaStreamTrack-getCapabilities.https.html.ini
@@ -1,4 +1,121 @@
 [MediaStreamTrack-getCapabilities.https.html]
   [MediaStreamTrack GetCapabilities]
     expected: FAIL
 
+  [Video device getCapabilities() method present.]
+    expected: FAIL
+
+  [MediaStreamTrack getCapabilities().]
+    expected: FAIL
+
+  [Audio device getCapabilities() method present.]
+    expected: FAIL
+
+  [Setup video MediaStreamTrack getCapabilities() test for frameRate]
+    expected: FAIL
+
+  [Setup video MediaStreamTrack getCapabilities() test for facingMode]
+    expected: FAIL
+
+  [Setup audio InputDeviceInfo getCapabilities() test for sampleSize]
+    expected: FAIL
+
+  [Setup video InputDeviceInfo getCapabilities() test for height]
+    expected: FAIL
+
+  [Setup audio InputDeviceInfo getCapabilities() test for latency]
+    expected: FAIL
+
+  [Setup video InputDeviceInfo getCapabilities() test for resizeMode]
+    expected: FAIL
+
+  [Setup video InputDeviceInfo getCapabilities() test for frameRate]
+    expected: FAIL
+
+  [Setup audio MediaStreamTrack getCapabilities() test for groupId]
+    expected: FAIL
+
+  [Setup audio MediaStreamTrack getCapabilities() test for sampleSize]
+    expected: FAIL
+
+  [Setup audio MediaStreamTrack getCapabilities() test for volume]
+    expected: FAIL
+
+  [Setup video MediaStreamTrack getCapabilities() test for groupId]
+    expected: FAIL
+
+  [Setup video MediaStreamTrack getCapabilities() test for height]
+    expected: FAIL
+
+  [Setup audio InputDeviceInfo getCapabilities() test for sampleRate]
+    expected: FAIL
+
+  [Setup audio InputDeviceInfo getCapabilities() test for channelCount]
+    expected: FAIL
+
+  [Setup video MediaStreamTrack getCapabilities() test for aspectRatio]
+    expected: FAIL
+
+  [Setup audio MediaStreamTrack getCapabilities() test for echoCancellation]
+    expected: FAIL
+
+  [Setup audio InputDeviceInfo getCapabilities() test for autoGainControl]
+    expected: FAIL
+
+  [Setup video InputDeviceInfo getCapabilities() test for facingMode]
+    expected: FAIL
+
+  [Setup audio MediaStreamTrack getCapabilities() test for noiseSuppression]
+    expected: FAIL
+
+  [Setup video InputDeviceInfo getCapabilities() test for deviceId]
+    expected: FAIL
+
+  [Setup video InputDeviceInfo getCapabilities() test for groupId]
+    expected: FAIL
+
+  [Setup video MediaStreamTrack getCapabilities() test for resizeMode]
+    expected: FAIL
+
+  [Setup audio InputDeviceInfo getCapabilities() test for volume]
+    expected: FAIL
+
+  [Setup audio InputDeviceInfo getCapabilities() test for echoCancellation]
+    expected: FAIL
+
+  [Setup audio InputDeviceInfo getCapabilities() test for noiseSuppression]
+    expected: FAIL
+
+  [Setup audio MediaStreamTrack getCapabilities() test for deviceId]
+    expected: FAIL
+
+  [Setup video MediaStreamTrack getCapabilities() test for width]
+    expected: FAIL
+
+  [Setup video MediaStreamTrack getCapabilities() test for deviceId]
+    expected: FAIL
+
+  [Setup audio MediaStreamTrack getCapabilities() test for sampleRate]
+    expected: FAIL
+
+  [Setup audio MediaStreamTrack getCapabilities() test for channelCount]
+    expected: FAIL
+
+  [Setup audio MediaStreamTrack getCapabilities() test for latency]
+    expected: FAIL
+
+  [Setup audio InputDeviceInfo getCapabilities() test for groupId]
+    expected: FAIL
+
+  [Setup video InputDeviceInfo getCapabilities() test for width]
+    expected: FAIL
+
+  [Setup audio InputDeviceInfo getCapabilities() test for deviceId]
+    expected: FAIL
+
+  [Setup audio MediaStreamTrack getCapabilities() test for autoGainControl]
+    expected: FAIL
+
+  [Setup video InputDeviceInfo getCapabilities() test for aspectRatio]
+    expected: FAIL
+
--- a/testing/web-platform/meta/mozilla-sync
+++ b/testing/web-platform/meta/mozilla-sync
@@ -1,2 +1,2 @@
-local: 2ddea9a53594e1fc30a9f6366b99be95911716ff
-upstream: 0944ee217e239e6b7cb0d532c828e728ff9b60e1
+local: 2d41baf691dc7f46f11c88e5f59ae4bf73ea82e8
+upstream: c00fd5dec24aac426acb570e9d253ec609b109bd
new file mode 100644
--- /dev/null
+++ b/testing/web-platform/meta/svg/embedded/image-embedding-svg-with-viewport-units-inline-style.svg.ini
@@ -0,0 +1,2 @@
+[image-embedding-svg-with-viewport-units-inline-style.svg]
+  expected: FAIL
--- a/testing/web-platform/tests/.azure-pipelines.yml
+++ b/testing/web-platform/tests/.azure-pipelines.yml
@@ -1,85 +1,93 @@
 # This is the configuration file for Azure Pipelines, used to run tests on
 # macOS. Documentation to help understand this setup:
 # https://docs.microsoft.com/en-us/azure/devops/pipelines/yaml-schema
+# https://docs.microsoft.com/en-us/azure/devops/pipelines/process/multiple-phases
+# https://docs.microsoft.com/en-us/azure/devops/pipelines/process/templates
 # https://docs.microsoft.com/en-us/azure/devops/pipelines/process/variables
+# https://docs.microsoft.com/en-us/azure/devops/pipelines/tasks/index
 #
 # In addition to this configuration file, the "Build pull requests from forks
 # of this repository" setting must also be enabled in the Azure DevOps project:
 # https://docs.microsoft.com/en-us/azure/devops/pipelines/repos/github#validate-contributions-from-forks
 
 trigger: none # disable builds for branches
 
 jobs:
-- job: macOS
-
+- job: root
+  displayName: './wpt test-jobs'
   pool:
-    vmImage: 'macOS-10.13'
-
+    vmImage: 'ubuntu-16.04'
   steps:
-  - checkout: self
-    fetchDepth: 50
-    submodules: false
-
+  - template: tools/ci/azure/checkout.yml
   - script: |
-      echo "Test jobs:"
       ./wpt test-jobs | while read job; do
         echo "$job"
-        echo "##vso[task.setvariable variable=run_$job]true";
+        echo "##vso[task.setvariable variable=$job;isOutput=true]true";
       done
-    displayName: 'List test jobs'
-
-  - script: |
-      sudo easy_install pip
-      sudo pip install -U virtualenv
-    displayName: 'Install Python packages'
-    condition: variables.run_wptrunner_infrastructure
+    name: test_jobs
+    displayName: 'Run ./wpt test-jobs'
 
-  # Installig Ahem in /Library/Fonts instead of using --install-fonts is a
-  # workaround for https://github.com/web-platform-tests/wpt/issues/13803.
-  - script: sudo cp fonts/Ahem.ttf /Library/Fonts
-    displayName: 'Install Ahem font'
-    condition: variables.run_wptrunner_infrastructure
-
-  - script: |
-      # https://github.com/web-platform-tests/results-collection/blob/master/src/scripts/trust-root-ca.sh
-      sudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain tools/certs/cacert.pem
-    displayName: 'Install web-platform.test certificate'
-    condition: variables.run_wptrunner_infrastructure
-
-  - script: HOMEBREW_NO_AUTO_UPDATE=1 brew cask install Homebrew/homebrew-cask-versions/google-chrome-dev
-    displayName: 'Install Chrome Dev'
-    condition: variables.run_wptrunner_infrastructure
-
-  - script: HOMEBREW_NO_AUTO_UPDATE=1 brew cask install Homebrew/homebrew-cask-versions/firefox-nightly
-    displayName: 'Install Firefox Nightly'
-    condition: variables.run_wptrunner_infrastructure
+- job: infrastructure_macOS
+  displayName: 'infrastructure/ tests (macOS)'
+  dependsOn: root
+  condition: dependencies.root.outputs['test_jobs.wptrunner_infrastructure']
+  pool:
+    vmImage: 'macOS-10.13'
+  steps:
+  - template: tools/ci/azure/checkout.yml
+  - template: tools/ci/azure/pip_install.yml
+    parameters:
+      packages: virtualenv
+  - template: tools/ci/azure/install_fonts.yml
+  - template: tools/ci/azure/install_certs.yml
+  - template: tools/ci/azure/install_chrome.yml
+  - template: tools/ci/azure/install_firefox.yml
+  - template: tools/ci/azure/install_safari.yml
+  - template: tools/ci/azure/update_hosts.yml
+  - template: tools/ci/azure/update_manifest.yml
+  - script: no_proxy='*' ./wpt run --yes --no-manifest-update --manifest MANIFEST.json --metadata infrastructure/metadata/ --channel=dev chrome infrastructure/
+    displayName: 'Run tests (Chrome Dev)'
+  - script: no_proxy='*' ./wpt run --yes --no-manifest-update --manifest MANIFEST.json --metadata infrastructure/metadata/ --channel=nightly firefox infrastructure/
+    displayName: 'Run tests (Firefox Nightly)'
+  - script: no_proxy='*' ./wpt run --yes --no-manifest-update --manifest MANIFEST.json --metadata infrastructure/metadata/ --channel=preview safari_webdriver infrastructure/
+    displayName: 'Run tests (Safari Technology Preview)'
 
-  - script: |
-      # Pin to STP 67, as SafariDriver isn't working in 68:
-      # https://github.com/web-platform-tests/wpt/issues/13800
-      HOMEBREW_NO_AUTO_UPDATE=1 brew cask install https://raw.githubusercontent.com/Homebrew/homebrew-cask-versions/23fae0a88868911913c2ee7d527c89164b6d5720/Casks/safari-technology-preview.rb
-      # https://web-platform-tests.org/running-tests/safari.html
-      sudo "/Applications/Safari Technology Preview.app/Contents/MacOS/safaridriver" --enable
-      defaults write com.apple.Safari WebKitJavaScriptCanOpenWindowsAutomatically 1
-    displayName: 'Install Safari Technology Preview'
-    condition: variables.run_wptrunner_infrastructure
-
-  - script: ./wpt make-hosts-file | sudo tee -a /etc/hosts
-    displayName: 'Update /etc/hosts'
-    condition: variables.run_wptrunner_infrastructure
+- job: tools_unittest_macOS
+  displayName: 'tools/ unittests (macOS)'
+  dependsOn: root
+  condition: dependencies.root.outputs['test_jobs.tools_unittest']
+  pool:
+    vmImage: 'macOS-10.13'
+  steps:
+  - template: tools/ci/azure/checkout.yml
+  - template: tools/ci/azure/tox_pytest.yml
+    parameters:
+      directory: tools/
+      toxenv: py27
 
-  - script: ./wpt manifest
-    displayName: 'Update manifest'
-    condition: variables.run_wptrunner_infrastructure
-
-  - script: no_proxy='*' ./wpt run --yes --no-manifest-update --manifest MANIFEST.json --metadata infrastructure/metadata/ --channel=dev chrome infrastructure/
-    displayName: 'Run infrastructure/ tests (Chrome Dev)'
-    condition: variables.run_wptrunner_infrastructure
+- job: wptrunner_unittest_macOS
+  displayName: 'tools/wptrunner/ unittests (macOS)'
+  dependsOn: root
+  condition: dependencies.root.outputs['test_jobs.wptrunner_unittest']
+  pool:
+    vmImage: 'macOS-10.13'
+  steps:
+  - template: tools/ci/azure/checkout.yml
+  - template: tools/ci/azure/tox_pytest.yml
+    parameters:
+      directory: tools/wptrunner/
 
-  - script: no_proxy='*' ./wpt run --yes --no-manifest-update --manifest MANIFEST.json --metadata infrastructure/metadata/ --channel=nightly firefox infrastructure/
-    displayName: 'Run infrastructure/ tests (Firefox Nightly)'
-    condition: variables.run_wptrunner_infrastructure
-
-  - script: no_proxy='*' ./wpt run --yes --no-manifest-update --manifest MANIFEST.json --metadata infrastructure/metadata/ --channel=preview safari_webdriver infrastructure/
-    displayName: 'Run infrastructure/ tests (Safari Technology Preview)'
-    condition: variables.run_wptrunner_infrastructure
+- job: wpt_integration_macOS
+  displayName: 'tools/wpt/ tests (macOS)'
+  dependsOn: root
+  condition: dependencies.root.outputs['test_jobs.wpt_integration']
+  pool:
+    vmImage: 'macOS-10.13'
+  steps:
+  # full checkout required
+  - template: tools/ci/azure/install_chrome.yml
+  - template: tools/ci/azure/install_firefox.yml
+  - template: tools/ci/azure/update_hosts.yml
+  - template: tools/ci/azure/tox_pytest.yml
+    parameters:
+      directory: tools/wpt/
new file mode 100644
--- /dev/null
+++ b/testing/web-platform/tests/css/css-grid/grid-model/grid-container-sizing-constraints-001.html
@@ -0,0 +1,110 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>CSS Grid Layout Test: min|max-content sizing constraints on grid containers</title>
+<link rel="author" title="Manuel Rego Casasnovas" href="mailto:rego@igalia.com">
+<link rel="help" href="https://www.w3.org/TR/css-grid-1/#intrinsic-sizes">
+<link rel="help" href="https://www.w3.org/TR/css-sizing-3/#sizing-values">
+<meta name="assert" content="The test checks the intrinsic size of a grid container when sized under different constraints. In inline axis min|max-content have some effect, however in block axis they behave as auto.">
+<style>
+  .grid {
+    display: grid;
+    float: left;
+    background: lime;
+  }
+
+  .grid-columns-minmax-50-100 {
+    grid-template-columns: minmax(50px, 100px);
+  }
+
+  .grid-columns-minmax-100-200 {
+    grid-template-columns: minmax(100px, 200px);
+  }
+
+  .grid-rows-minmax-50-100 {
+    grid-template-rows: minmax(50px, 100px);
+  }
+
+  .min-content {
+    width: min-content;
+    height: min-content;
+  }
+
+  .max-content {
+    width: max-content;
+    height: max-content;
+  }
+</style>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/check-layout-th.js"></script>
+
+<body onload="checkLayout('.grid');">
+
+  <div id="log"></div>
+
+  <div class="grid grid-columns-minmax-50-100 grid-rows-minmax-50-100"
+    data-expected-width="100" data-expected-height="100"></div>
+
+  <div class="grid min-content grid-columns-minmax-100-200 grid-rows-minmax-50-100"
+    data-expected-width="100" data-expected-height="100"></div>
+
+  <div class="grid max-content grid-columns-minmax-50-100 grid-rows-minmax-50-100"
+    data-expected-width="100" data-expected-height="100"></div>
+
+  <div class="min-content">
+    <div class="grid grid-columns-minmax-100-200 grid-rows-minmax-50-100"
+      data-expected-width="100" data-expected-height="100"></div>
+  </div>
+
+  <div class="max-content">
+    <div class="grid grid-columns-minmax-50-100 grid-rows-minmax-50-100"
+      data-expected-width="100" data-expected-height="100"></div>
+  </div>
+
+  <div style="writing-mode: vertical-lr;">
+
+    <div class="grid grid-columns-minmax-50-100 grid-rows-minmax-50-100"
+      data-expected-width="100" data-expected-height="100"></div>
+
+    <div class="grid min-content grid-columns-minmax-100-200 grid-rows-minmax-50-100"
+      data-expected-width="100" data-expected-height="100"></div>
+
+    <div class="grid max-content grid-columns-minmax-50-100 grid-rows-minmax-50-100"
+      data-expected-width="100" data-expected-height="100"></div>
+
+    <div class="min-content">
+      <div class="grid grid-columns-minmax-100-200 grid-rows-minmax-50-100"
+        data-expected-width="100" data-expected-height="100"></div>
+    </div>
+
+    <div class="max-content">
+      <div class="grid grid-columns-minmax-50-100 grid-rows-minmax-50-100"
+        data-expected-width="100" data-expected-height="100"></div>
+    </div>
+
+  </div>
+
+  <div style="writing-mode: vertical-rl;">
+
+    <div class="grid grid-columns-minmax-50-100 grid-rows-minmax-50-100"
+      data-expected-width="100" data-expected-height="100"></div>
+
+    <div class="grid min-content grid-columns-minmax-100-200 grid-rows-minmax-50-100"
+      data-expected-width="100" data-expected-height="100"></div>
+
+    <div class="grid max-content grid-columns-minmax-50-100 grid-rows-minmax-50-100"
+      data-expected-width="100" data-expected-height="100"></div>
+
+    <div class="min-content">
+      <div class="grid grid-columns-minmax-100-200 grid-rows-minmax-50-100"
+        data-expected-width="100" data-expected-height="100"></div>
+    </div>
+
+    <div class="max-content">
+      <div class="grid grid-columns-minmax-50-100 grid-rows-minmax-50-100"
+        data-expected-width="100" data-expected-height="100"></div>
+    </div>
+
+  </div>
+
+</body>
new file mode 100644
--- /dev/null
+++ b/testing/web-platform/tests/css/css-masking/clip-path/clip-path-filter-order-ref.html
@@ -0,0 +1,21 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Clip-path should be applied after filtering</title>
+<link rel="author" title="Philip Rogers" href="mailto:pdr@chromium.org">
+
+<div>
+  <p>Expected: A green box.<br>
+  There should be no red visible.<br>
+  There should be a crisp, clipped edge around the green box (no blurring).</p>
+</div>
+
+<style>
+  #testcase {
+    position: absolute;
+    width: 200px;
+    height: 200px;
+    background: green;
+    will-change: transform;
+  }
+</style>
+<div id="testcase"></div>
new file mode 100644
--- /dev/null
+++ b/testing/web-platform/tests/css/css-masking/clip-path/clip-path-filter-order.html
@@ -0,0 +1,25 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Clip-path should be applied after filtering</title>
+<link rel="author" title="Philip Rogers" href="mailto:pdr@chromium.org">
+<link rel="help" href="https://drafts.fxtf.org/css-masking-1/#placement">
+<link rel="match" href="clip-path-filter-order-ref.html">
+
+<div>
+  <p>Expected: A green box.<br>
+  There should be no red visible.<br>
+  There should be a crisp, clipped edge around the green box (no blurring).</p>
+</div>
+
+<style>
+  #testcase {
+    position: absolute;
+    width: 400px;
+    height: 400px;
+    background: green;
+    will-change: transform;
+    filter: drop-shadow(16px 16px 20px red);
+    clip-path: inset(0px 200px 200px 0px);
+  }
+</style>
+<div id="testcase"></div>
new file mode 100644
--- /dev/null
+++ b/testing/web-platform/tests/css/css-masking/clip/clip-filter-order-ref.html
@@ -0,0 +1,21 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Clip should be applied after filtering</title>
+<link rel="author" title="Philip Rogers" href="mailto:pdr@chromium.org">
+
+<div>
+  <p>Expected: A green box.<br>
+  There should be no red visible.<br>
+  There should be a crisp, clipped edge around the green box (no blurring).</p>
+</div>
+
+<style>
+  #testcase {
+    position: absolute;
+    width: 200px;
+    height: 200px;
+    background: green;
+    will-change: transform;
+  }
+</style>
+<div id="testcase"></div>
new file mode 100644
--- /dev/null
+++ b/testing/web-platform/tests/css/css-masking/clip/clip-filter-order.html
@@ -0,0 +1,25 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Clip should be applied after filtering</title>
+<link rel="author" title="Philip Rogers" href="mailto:pdr@chromium.org">
+<link rel="help" href="https://drafts.fxtf.org/css-masking-1/#placement">
+<link rel="match" href="clip-filter-order-ref.html">
+
+<div>
+  <p>Expected: A green box.<br>
+  There should be no red visible.<br>
+  There should be a crisp, clipped edge around the green box (no blurring).</p>
+</div>
+
+<style>
+  #testcase {
+    position: absolute;
+    width: 400px;
+    height: 400px;
+    background: green;
+    will-change: transform;
+    filter: drop-shadow(16px 16px 20px red);
+    clip: rect(0px, 200px, 200px, 0px);
+  }
+</style>
+<div id="testcase"></div>
new file mode 100644
--- /dev/null
+++ b/testing/web-platform/tests/css/css-tables/percent-width-ignored-001.tentative.html
@@ -0,0 +1,26 @@
+<!DOCTYPE html>
+<script src='/resources/testharness.js'></script>
+<script src='/resources/testharnessreport.js'></script>
+<script src='/resources/check-layout-th.js'></script>
+<link rel="author" title="David Grogan" href="dgrogan@chromium.org">
+<link rel="help" href="https://github.com/w3c/csswg-drafts/issues/3336">
+<meta name="flags" content="" />
+<meta name="assert" content="A cell's percent width is ignored when its table is nested in another cell" />
+No red should show. Yellow and blue rectangles are in proportion to their
+contents' intrinsic widths, not affected by yellow's percent-width bloating the
+inner table.
+<table id="outerTable" style="width: 300px" cellspacing="0" cellpadding="0">
+    <td style="background:red;">
+        <table cellspacing="0" cellpadding="0">
+            <td style="width:1%; background:yellow;" data-expected-width="100">
+                <div style="width:20px; height:150px;"></div>
+            </td>
+        </table>
+    </td>
+    <td style="background:lightblue;" data-expected-width="200">
+        <div style="width:40px; height:150px;"></div>
+    </td>
+</table>
+<script>
+checkLayout('#outerTable');
+</script>
new file mode 100644
--- /dev/null
+++ b/testing/web-platform/tests/css/css-tables/percent-width-ignored-002.tentative.html
@@ -0,0 +1,19 @@
+<!DOCTYPE html>
+<script src='/resources/testharness.js'></script>
+<script src='/resources/testharnessreport.js'></script>
+<script src='/resources/check-layout-th.js'></script>
+<link rel="author" title="David Grogan" href="dgrogan@chromium.org">
+<link rel="help" href="https://github.com/w3c/csswg-drafts/issues/3336">
+<meta name="flags" content="" />
+<meta name="assert" content="Should cell's percent width be ignored when its table is nested in a shrink to fit block?" />
+<p>Edge 44.17763 and Chrome 70 make this 300px wide. FF makes it 150px wide.</p>
+<div id="stf" style="position:absolute; background:blue;" data-expected-width=300>
+    <table cellspacing="0" cellpadding="0">
+        <td style="width:50%;">
+            <div style="width:150px; height:150px;"></div>
+        </td>
+    </table>
+</div>
+<script>
+checkLayout('#stf');
+</script>
new file mode 100644
--- /dev/null
+++ b/testing/web-platform/tests/css/css-tables/percent-width-ignored-003.tentative.html
@@ -0,0 +1,26 @@
+<!DOCTYPE html>
+<script src='/resources/testharness.js'></script>
+<script src='/resources/testharnessreport.js'></script>
+<script src='/resources/check-layout-th.js'></script>
+<link rel="author" title="David Grogan" href="dgrogan@chromium.org">
+<link rel="help" href="https://github.com/w3c/csswg-drafts/issues/3336">
+<meta name="flags" content="" />
+<meta name="assert" content="A cell's percent width is ignored when its table is nested in another cell, even if there's an intermediate block." />
+Engines render this same as without the intermediate background:blue block -- yellow 1% width is ignored.
+<table id="outerTable" style="width: 300px" cellspacing="0" cellpadding="0">
+    <td style="background:red;">
+      <div style="background:blue">
+        <table cellspacing="0" cellpadding="0">
+            <td style="width:1%; background:yellow;" data-expected-width="100">
+                <div style="width:20px; height:150px;"></div>
+            </td>
+        </table>
+      </div>
+    </td>
+    <td style="background:lightblue;" data-expected-width="200">
+        <div style="width:40px; height:150px;"></div>
+    </td>
+</table>
+<script>
+checkLayout('#outerTable');
+</script>
new file mode 100644
--- /dev/null
+++ b/testing/web-platform/tests/css/css-text/text-indent/percentage-value-intrinsic-size.html
@@ -0,0 +1,19 @@
+<!DOCTYPE html>
+<link rel="author" title="Morten Stenshorne" href="mstensho@chromium.org">
+<link rel="help" href="https://www.w3.org/TR/css-text-3/#text-indent-property">
+<meta name="assert" content="Percentages should be ignored when calculating min/max intrinsic sizes.">
+<p>Test passes if there is a filled green square.</p>
+<div id="container" data-expected-width="50" style="position:relative; float:left; height:100px; background:green;">
+  <div id="foo">
+    <div data-offset-x="50" data-expected-width="50" style="display:inline-block; width:50px; height:100px; background:green;"></div>
+    <div style="width:50px;"></div>
+  </div>
+</div>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/check-layout-th.js"></script>
+<script>
+  document.body.offsetTop;
+  foo.style.textIndent = "100%";
+  checkLayout("#container");
+</script>
new file mode 100644
--- /dev/null
+++ b/testing/web-platform/tests/css/selectors/invalidation/attribute.html
@@ -0,0 +1,234 @@
+<!DOCTYPE html>
+<html>
+  <head>
+    <title>CSS Selectors Invalidation: attribute</title>
+    <link rel="help" href="https://drafts.csswg.org/selectors-4/#attribute-selectors">
+    <meta name="assert" content="This tests that the attribute selectors are effective">
+    <script src="/resources/testharness.js"></script>
+    <script src="/resources/testharnessreport.js"></script>
+    <style>
+      div {
+        color: gray;
+      }
+
+      #a1[style] {
+        color: blue;
+      }
+      #a1[style] > #b1 {
+        color: green;
+      }
+      #a1[style] #c1 {
+        color: red;
+      }
+      #a1[style] + #d1 {
+        color: yellow;
+      }
+
+      [id=a2] {
+        color: blue;
+      }
+      [id=a2] > #b2 {
+        color: green;
+      }
+      [id=a2] #c2 {
+        color: red;
+      }
+      [id=a2] + #d2 {
+        color: yellow;
+      }
+
+      #a3[class~=q] {
+        color: blue;
+      }
+      #a3[class~=q] > #b3 {
+        color: green;
+      }
+      #a3[class~=q] #c3 {
+        color: red;
+      }
+      #a3[class~=q] + #d3 {
+        color: yellow;
+      }
+
+      #a4[run|=one] {
+        color: blue;
+      }
+      #a4[run|=one] > #b4 {
+        color: green;
+      }
+      #a4[run|=one] #c4 {
+        color: red;
+      }
+      #a4[run|=one] + #d4 {
+        color: yellow;
+      }
+
+      #a5 {
+        color: blue;
+      }
+      #a5 > #b5 {
+        color: green;
+      }
+      #a5 #c5 {
+        color: red;
+      }
+      #a5 + #d5 {
+        color: yellow;
+      }
+
+      #a6.q {
+        color: blue;
+      }
+      #a6.q > #b6 {
+        color: green;
+      }
+      #a6.q #c6 {
+        color: red;
+      }
+      #a6.q + #d6 {
+        color: yellow;
+      }
+
+  </style>
+  </head>
+  <body>
+
+    <div id="a1">
+      <div id="b1">
+        <div id="c1">
+        </div>
+      </div>
+    </div>
+    <div id="d1">
+    </div>
+
+    <div>
+      <div id="b2">
+        <div id="c2">
+        </div>
+      </div>
+    </div>
+    <div id="d2">
+    </div>
+
+    <div id="a3">
+      <div id="b3">
+        <div id="c3">
+        </div>
+      </div>
+    </div>
+    <div id="d3">
+    </div>
+
+    <div id="a4">
+      <div id="b4">
+        <div id="c4">
+        </div>
+      </div>
+    </div>
+    <div id="d4">
+    </div>
+
+    <div>
+      <div id="b5">
+        <div id="c5">
+        </div>
+      </div>
+    </div>
+    <div id="d5">
+    </div>
+
+    <div id="a6">
+      <div id="b6">
+        <div id="c6">
+        </div>
+      </div>
+    </div>
+    <div id="d6">
+    </div>
+
+    <script>
+      const gray = "rgb(128, 128, 128)";
+      const blue = "rgb(0, 0, 255)";
+      const green = "rgb(0, 128, 0)";
+      const red = "rgb(255, 0, 0)";
+      const yellow = "rgb(255, 255, 0)";
+
+      function assertGray(a, b, c, d) {
+        assert_equals(getComputedStyle(a).color, gray);
+        assert_equals(getComputedStyle(b).color, gray);
+        assert_equals(getComputedStyle(c).color, gray);
+        assert_equals(getComputedStyle(d).color, gray);
+      }
+
+      function assertColorful(a, b, c, d) {
+        assert_equals(getComputedStyle(a).color, blue);
+        assert_equals(getComputedStyle(b).color, green);
+        assert_equals(getComputedStyle(c).color, red);
+        assert_equals(getComputedStyle(d).color, yellow);
+      }
+
+      test(() => {
+        assertGray(a1, b1, c1, d1);
+        a1.style.visibility = "visible";
+        assertColorful(a1, b1, c1, d1);
+        a1.removeAttribute('style');
+        assertGray(a1, b1, c1, d1);
+      }, "[att] selector is effective");
+
+      test(() => {
+        const a2 = b2.parentElement;
+        assertGray(a2, b2, c2, d2);
+        a2.id = 'x-a2';
+        assertGray(a2, b2, c2, d2);
+        a2.id = 'a2';
+        assertColorful(a2, b2, c2, d2);
+        a2.id = 'a2-y';
+        assertGray(a2, b2, c2, d2);
+      }, "[att=val] selector is effective");
+
+      test(() => {
+        assertGray(a3, b3, c3, d3);
+        a3.setAttribute('class', 'p q r');
+        assertColorful(a3, b3, c3, d3);
+        a3.setAttribute('class', 'q-r');
+        assertGray(a3, b3, c3, d3);
+      }, "[att~=val] selector is effective");
+
+      test(() => {
+        assertGray(a4, b4, c4, d4);
+        a4.setAttribute('run', 'one');
+        assertColorful(a4, b4, c4, d4);
+        a4.setAttribute('run', 'one two three');
+        assertGray(a4, b4, c4, d4);
+        a4.setAttribute('run', 'one-two-three');
+        assertColorful(a4, b4, c4, d4);
+        a4.setAttribute('run', 'zero-one');
+        assertGray(a4, b4, c4, d4);
+      }, "[att|=val] selector is effective");
+
+      test(() => {
+        const a5 = b5.parentElement;
+        assertGray(a5, b5, c5, d5);
+        a5.setAttribute('id', 'x-a5');
+        assertGray(a5, b5, c5, d5);
+        a5.setAttribute('id', 'a5');
+        assertColorful(a5, b5, c5, d5);
+        a5.setAttribute('id', 'a5-y');
+        assertGray(a5, b5, c5, d5);
+      }, "#id selector is effective");
+
+      test(() => {
+        assertGray(a6, b6, c6, d6);
+        a6.classList.add('p');
+        a6.classList.add('q');
+        a6.classList.add('r');
+        assertColorful(a6, b6, c6, d6);
+        a6.classList.remove('q');
+        a6.classList.add('q-r');
+        assertGray(a6, b6, c6, d6);
+      }, ".class selector is effective");
+
+    </script>
+  </body>
+</html>
new file mode 100644
--- /dev/null
+++ b/testing/web-platform/tests/css/selectors/invalidation/sibling.html
@@ -0,0 +1,139 @@
+<!DOCTYPE html>
+<html>
+  <head>
+    <title>CSS Selectors Invalidation: sibling</title>
+    <link rel="help" href="https://drafts.csswg.org/selectors-4/#adjacent-sibling-combinators">
+    <link rel="help" href="https://drafts.csswg.org/selectors-4/#general-sibling-combinators">
+    <meta name="assert" content="This tests that the + next-sibling selector is effective">
+    <meta name="assert" content="This tests that the ~ subsequent-sibling selector is effective">
+    <script src="/resources/testharness.js"></script>
+    <script src="/resources/testharnessreport.js"></script>
+    <style>
+
+* { background-color: inherit; }
+
+body { background-color: rgba(0, 0, 0, 0); }
+
+.t1 .sibling + *,
+.t2 .sibling ~ *,
+.t3 + .sibling + *,
+.t4 + .sibling,
+.t5 + *,
+.t6 ~ .sibling,
+.t7 + * + * .child { background-color: rgb(0, 128, 0); }
+
+    </style>
+  </head>
+  <body>
+
+<div>
+    <div id="t1">
+        <div class="sibling"></div>
+        <div id="r1"></div>
+        <div id="u1"></div>
+    </div>
+</div>
+<div>
+    <div id="t2">
+        <div class="sibling"></div>
+        <div></div>
+        <div id="r2"></div>
+    </div>
+</div>
+<div>
+    <div id="t3"></div>
+    <div class="sibling"></div>
+    <div id="r3"></div>
+</div>
+<div>
+    <div id="t4"></div>
+    <div id="r4" class="sibling"></div>
+    <div id="u4" class="sibling"></div>
+</div>
+<div>
+    <div id="t5"></div>
+    <div id="r5"></div>
+    <div id="u5"></div>
+</div>
+<div>
+    <div id="t6"></div>
+    <div></div>
+    <div id="r6" class="sibling">
+        <div id="r6b"></div>
+    </div>
+    <div id="u6"></div>
+</div>
+<div>
+    <div id="t7">
+        <div class="child"></div>
+    </div>
+    <div></div>
+    <div>
+        <div id="r7" class="child"></div>
+    </div>
+    <div>
+        <div id="u7" class="child"></div>
+    </div>
+</div>
+
+    <script>
+
+test(function() {
+    assert_equals(getComputedStyle(r1).backgroundColor, "rgba(0, 0, 0, 0)", "Background color should initially be transparent");
+
+    t1.className = "t1";
+    assert_equals(getComputedStyle(r1).backgroundColor, "rgb(0, 128, 0)", "Background color is green after class change");
+    assert_equals(getComputedStyle(u1).backgroundColor, "rgba(0, 0, 0, 0)", "Background color remains transparent");
+}, "Adjacent with universal selector");
+
+test(function() {
+    assert_equals(getComputedStyle(r2).backgroundColor, "rgba(0, 0, 0, 0)", "Background color should initially be transparent");
+
+    t2.className = "t2";
+    assert_equals(getComputedStyle(r2).backgroundColor, "rgb(0, 128, 0)", "Background color is green after class change");
+}, "Indirect adjacent with universal selector");
+
+test(function() {
+    assert_equals(getComputedStyle(r3).backgroundColor, "rgba(0, 0, 0, 0)", "Background color should initially be transparent");
+
+    t3.className = "t3";
+    assert_equals(getComputedStyle(r3).backgroundColor, "rgb(0, 128, 0)", "Background color is green after class change");
+}, "Indirect adjacent with two adjacent selectors");
+
+test(function() {
+    assert_equals(getComputedStyle(r4).backgroundColor, "rgba(0, 0, 0, 0)", "Background color should initially be transparent");
+
+    t4.className = "t4";
+    assert_equals(getComputedStyle(r4).backgroundColor, "rgb(0, 128, 0)", "Background color is green after class change");
+    assert_equals(getComputedStyle(u4).backgroundColor, "rgba(0, 0, 0, 0)", "Background color remains transparent");
+}, "Adjacent class");
+
+test(function() {
+    assert_equals(getComputedStyle(r5).backgroundColor, "rgba(0, 0, 0, 0)", "Background color should initially be transparent");
+
+    t5.className = "t5";
+    assert_equals(getComputedStyle(r5).backgroundColor, "rgb(0, 128, 0)", "Background color is green after class change");
+    assert_equals(getComputedStyle(u5).backgroundColor, "rgba(0, 0, 0, 0)", "Background color remains transparent");
+}, "Adjacent universal");
+
+test(function() {
+    assert_equals(getComputedStyle(r6).backgroundColor, "rgba(0, 0, 0, 0)", "Background color should initially be transparent");
+    assert_equals(getComputedStyle(r6b).backgroundColor, "rgba(0, 0, 0, 0)", "Child's background color should initially be transparent");
+
+    t6.className = "t6";
+    assert_equals(getComputedStyle(r6).backgroundColor, "rgb(0, 128, 0)", "Background color is green after class change");
+    assert_equals(getComputedStyle(r6b).backgroundColor, "rgb(0, 128, 0)", "Child's background color is green after class change");
+    assert_equals(getComputedStyle(u6).backgroundColor, "rgba(0, 0, 0, 0)", "Background color remains transparent");
+}, "Sibling subtree through an indirect adjacent combinator");
+
+test(function() {
+    assert_equals(getComputedStyle(r7).backgroundColor, "rgba(0, 0, 0, 0)", "Background color should initially be transparent");
+
+    t7.className = "t7";
+    assert_equals(getComputedStyle(r7).backgroundColor, "rgb(0, 128, 0)", "Background color is green after class change");
+    assert_equals(getComputedStyle(u7).backgroundColor, "rgba(0, 0, 0, 0)", "Background color remains transparent");
+}, "Sibling descendant through a universal selector");
+
+    </script>
+  </body>
+</html>
--- a/testing/web-platform/tests/css/vendor-imports/mozilla/mozilla-central-reftests/ui3/reftest.list
+++ b/testing/web-platform/tests/css/vendor-imports/mozilla/mozilla-central-reftests/ui3/reftest.list
@@ -2,9 +2,9 @@
 == box-sizing-border-box-002.xht box-sizing-border-box-002-ref.xht
 == box-sizing-border-box-003.xht box-sizing-border-box-003-ref.xht
 == box-sizing-border-box-004.xht box-sizing-border-box-004-ref.xht
 == box-sizing-content-box-001.xht box-sizing-content-box-001-ref.xht
 == box-sizing-content-box-002.xht box-sizing-content-box-002-ref.xht
 == box-sizing-content-box-003.xht box-sizing-content-box-003-ref.xht
 == box-sizing-replaced-001.xht box-sizing-replaced-001-ref.xht
 == box-sizing-replaced-002.xht box-sizing-replaced-002-ref.xht
-skip-if(cocoaWidget) == box-sizing-replaced-003.xht box-sizing-replaced-003-ref.xht # Bug 1383454
+== box-sizing-replaced-003.xht box-sizing-replaced-003-ref.xht
--- a/testing/web-platform/tests/fetch/api/response/response-init-001.html
+++ b/testing/web-platform/tests/fetch/api/response/response-init-001.html
@@ -10,25 +10,25 @@
     <script src="/resources/testharnessreport.js"></script>
   </head>
   <body>
     <script>
       var defaultValues = { "type" : "default",
                             "url" : "",
                             "ok" : true,
                             "status" : 200,
-                            "statusText" : "OK",
+                            "statusText" : "",
                             "body" : null
       };
 
       var statusCodes = { "givenValues" : [200, 300, 400, 500, 599],
-                       "expectedValues" : [200, 300, 400, 500, 599]
+                          "expectedValues" : [200, 300, 400, 500, 599]
       };
-      var statusTexts = { "givenValues" : ["OK", "with space", String.fromCharCode(0x80)],
-                       "expectedValues" : ["OK", "with space", String.fromCharCode(0x80)]
+      var statusTexts = { "givenValues" : ["", "OK", "with space", String.fromCharCode(0x80)],
+                          "expectedValues" : ["", "OK", "with space", String.fromCharCode(0x80)]
       };
       var initValuesDict = { "status" : statusCodes,
                              "statusText" : statusTexts
       };
 
       function isOkStatus(status) {
         return 200 <= status &&  299 >= status;
       }
@@ -37,40 +37,41 @@
       for (var attributeName in defaultValues) {
         test(function() {
           var expectedValue = defaultValues[attributeName];
           assert_equals(response[attributeName], expectedValue,
             "Expect default response." + attributeName + " is " + expectedValue);
         }, "Check default value for " + attributeName + " attribute");
       }
 
-      for (var attributeName in initValuesDict)
+      for (var attributeName in initValuesDict) {
         test(function() {
           var valuesToTest = initValuesDict[attributeName];
           for (var valueIdx in valuesToTest["givenValues"]) {
             var givenValue = valuesToTest["givenValues"][valueIdx];
             var expectedValue = valuesToTest["expectedValues"][valueIdx];
             var responseInit = {};
             responseInit[attributeName] = givenValue;
             var response = new Response("", responseInit);
             assert_equals(response[attributeName], expectedValue,
               "Expect response." + attributeName + " is " + expectedValue +
               " when initialized with " + givenValue);
             assert_equals(response.ok, isOkStatus(response.status),
               "Expect response.ok is " + isOkStatus(response.status));
           }
         }, "Check " + attributeName + " init values and associated getter");
+      }
 
-        test(function() {
-          const response1 = new Response("");
-          assert_equals(response1.headers, response1.headers);
+      test(function() {
+        const response1 = new Response("");
+        assert_equals(response1.headers, response1.headers);
 
-          const response2 = new Response("", {"headers": {"X-Foo": "bar"}});
-          assert_equals(response2.headers, response2.headers);
-          const headers = response2.headers;
-          response2.headers.set("X-Foo", "quux");
-          assert_equals(headers, response2.headers);
-          headers.set("X-Other-Header", "baz");
-          assert_equals(headers, response2.headers);
-        }, "Test that Response.headers has the [SameObject] extended attribute");
+        const response2 = new Response("", {"headers": {"X-Foo": "bar"}});
+        assert_equals(response2.headers, response2.headers);
+        const headers = response2.headers;
+        response2.headers.set("X-Foo", "quux");
+        assert_equals(headers, response2.headers);
+        headers.set("X-Other-Header", "baz");
+        assert_equals(headers, response2.headers);
+      }, "Test that Response.headers has the [SameObject] extended attribute");
     </script>
   </body>
 </html>
--- a/testing/web-platform/tests/mediacapture-streams/MediaDevices-enumerateDevices.https.html
+++ b/testing/web-platform/tests/mediacapture-streams/MediaDevices-enumerateDevices.https.html
@@ -11,70 +11,35 @@
 <p class="instructions">This test checks for the presence of the
 <code>navigator.mediaDevices.enumerateDevices()</code> method.</p>
 <div id='log'></div>
 <script src=/resources/testharness.js></script>
 <script src=/resources/testharnessreport.js></script>
 <script>
 "use strict";
 //NOTE ALEX: for completion, a test for ondevicechange event is missing.
-promise_test(function() {
+promise_test(async () => {
   assert_true(undefined !== navigator.mediaDevices.enumerateDevices, "navigator.mediaDevices.enumerateDevices exists");
-  return navigator.mediaDevices.enumerateDevices().then(function(list) {
-    for (let mediainfo of list) {
-      assert_true(undefined !== mediainfo.deviceId, "mediaInfo's deviceId should exist.");
-      assert_true(undefined !== mediainfo.kind,     "mediaInfo's kind     should exist.");
-      assert_true(undefined !== mediainfo.label,    "mediaInfo's label    should exist.");
-      assert_true(undefined !== mediainfo.groupId,  "mediaInfo's groupId  should exist.");
-      // TODO the values of some of those fields should be empty string by default if no permission has been requested.
-      if ( mediainfo.kind == "audioinput" || mediainfo.kind == "videoinput") {
-        assert_true(mediainfo instanceof InputDeviceInfo);
-        var capabilities = mediainfo.getCapabilities();
-        assert_equals(typeof capabilities, "object", "capabilities must be an object.");
-        assert_equals(typeof capabilities.deviceId, "string", "deviceId must be a string.");
-        assert_equals(typeof capabilities.groupId, "string", "groupId must be a string.");
-        if (mediainfo.kind == "audioinput") {
-          assert_equals(typeof capabilities.echoCancellation, "object", "echoCancellation must be an object.");
-          assert_equals(typeof capabilities.autoGainControl, "object", "autoGainControl must be an object.");
-          assert_equals(typeof capabilities.noiseSuppression, "object", "noiseSuppression must be an object.");
-        }
-        if (mediainfo.kind == "videoinput") {
-          assert_equals(typeof capabilities.facingMode, "object", "facingMode must be an object.");
-          verifyVideoRangeProperties(capabilities);
-        }
-      } else if ( mediainfo.kind == "audiooutput" ) {
-        assert_true(mediainfo instanceof MediaDeviceInfo);
-      } else {
-        assert_unreached("mediainfo.kind should be one of 'audioinput', 'videoinput', or 'audiooutput'.")
-      }
+  const device_list =  await navigator.mediaDevices.enumerateDevices();
+  for (const mediainfo of device_list) {
+    assert_true(undefined !== mediainfo.deviceId, "mediaInfo's deviceId should exist.");
+    assert_true(undefined !== mediainfo.kind,     "mediaInfo's kind     should exist.");
+    assert_in_array(mediainfo.kind, ["videoinput", "audioinput", "audiooutput"]);
+    assert_true(undefined !== mediainfo.label,    "mediaInfo's label    should exist.");
+    assert_true(undefined !== mediainfo.groupId,  "mediaInfo's groupId  should exist.");
+  }
+}, "mediaDevices.enumerateDevices() is present and working");
+
+promise_test(async () => {
+  const device_list =  await navigator.mediaDevices.enumerateDevices();
+  for (const mediainfo of device_list) {
+    if (mediainfo.kind == "audioinput" || mediainfo.kind == "videoinput") {
+      assert_true(mediainfo instanceof InputDeviceInfo);
+    } else if ( mediainfo.kind == "audiooutput" ) {
+      assert_true(mediainfo instanceof MediaDeviceInfo);
+    } else {
+      assert_unreached("mediainfo.kind should be one of 'audioinput', 'videoinput', or 'audiooutput'.")
     }
-  });
-}, "mediaDevices.enumerateDevices() is present and working on navigator");
-
-function verifyVideoRangeProperties(capabilities) {
-  if (capabilities.hasOwnProperty('width')) {
-      assert_equals(Object.keys(capabilities.width).length, 2);
-      assert_true(capabilities.width.hasOwnProperty('min'));
-      assert_true(capabilities.width.hasOwnProperty('max'));
-      assert_less_than_equal(capabilities.width.min, capabilities.width.max);
   }
-  if (capabilities.hasOwnProperty('height')) {
-    assert_equals(Object.keys(capabilities.height).length, 2);
-    assert_true(capabilities.height.hasOwnProperty('min'));
-    assert_true(capabilities.height.hasOwnProperty('max'));
-    assert_less_than_equal(capabilities.height.min, capabilities.height.max);
-  }
-  if (capabilities.hasOwnProperty('aspectRatio')) {
-    assert_equals(Object.keys(capabilities.aspectRatio).length, 2);
-    assert_true(capabilities.aspectRatio.hasOwnProperty('min'));
-    assert_true(capabilities.aspectRatio.hasOwnProperty('max'));
-    assert_less_than_equal(capabilities.aspectRatio.min, capabilities.aspectRatio.max);
-  }
-  if (capabilities.hasOwnProperty('frameRate')) {
-    assert_equals(Object.keys(capabilities.frameRate).length, 2);
-    assert_true(capabilities.frameRate.hasOwnProperty('min'));
-    assert_true(capabilities.frameRate.hasOwnProperty('max'));
-    assert_less_than_equal(capabilities.frameRate.min, capabilities.frameRate.max);
-  }
-}
+}, "InputDeviceInfo is supported");
 </script>
 </body>
 </html>
--- a/testing/web-platform/tests/mediacapture-streams/MediaDevices-getUserMedia.https.html
+++ b/testing/web-platform/tests/mediacapture-streams/MediaDevices-getUserMedia.https.html
@@ -87,11 +87,39 @@ promise_test(t => {
                 var found_device = devices.find(element => {
                   return element.kind == "audioinput" &&
                         element.groupId == devices[i].groupId});
                 assert_true(undefined === found_device);
               }));
       }
     }));
 }, 'groupId is correctly supported by getUserMedia() for audio devices');
+
+promise_test(async t => {
+  const stream = await navigator.mediaDevices.getUserMedia(
+      { video: {resizeMode: {exact: 'none'}}});
+  const [track] = stream.getVideoTracks();
+  t.add_cleanup(() => track.stop());
+  assert_equals(track.getSettings().resizeMode, 'none');
+}, 'getUserMedia() supports setting none as resizeMode.');
+
+promise_test(async t => {
+  const stream = await navigator.mediaDevices.getUserMedia(
+      { video: {resizeMode: {exact: 'crop-and-scale'}}});
+  const [track] = stream.getVideoTracks();
+  t.add_cleanup(() => track.stop());
+  assert_equals(track.getSettings().resizeMode, 'crop-and-scale');
+}, 'getUserMedia() supports setting crop-and-scale as resizeMode.');
+
+promise_test(async t => {
+  try {
+    let stream = await navigator.mediaDevices.getUserMedia(
+        { video: {resizeMode: {exact: 'INVALID'}}});
+    t.add_cleanup(() => stream.getVideoTracks()[0].stop());
+    t.unreached_func('getUserMedia() should fail with invalid resizeMode')();
+  } catch (e) {
+    assert_equals(e.name, 'OverconstrainedError');
+    assert_equals(e.constraint, 'resizeMode');
+  }
+}, 'getUserMedia() fails with exact invalid resizeMode.');
 </script>
 </body>
 </html>
--- a/testing/web-platform/tests/mediacapture-streams/MediaStreamTrack-applyConstraints.https.html
+++ b/testing/web-platform/tests/mediacapture-streams/MediaStreamTrack-applyConstraints.https.html
@@ -49,9 +49,31 @@
                 t.step_func(e => {
                   assert_equals(e.name, 'OverconstrainedError');
                   assert_equals(e.constraint, 'groupId');
                 }));
           }
         });
       }));
   }, 'applyConstraints rejects attempt to switch device using groupId');
+
+  promise_test(async t => {
+    const stream = await navigator.mediaDevices.getUserMedia({ video: true });
+    const [track] = stream.getVideoTracks();
+    t.add_cleanup(() => track.stop());
+    try {
+      await track.applyConstraints({ resizeMode: { exact: "INVALID" } });
+      t.unreached_func('applyConstraints() must fail with invalid resizeMode')();
+    } catch (e) {
+      assert_equals(e.name, 'OverconstrainedError');
+      assert_equals(e.constraint, 'resizeMode');
+    }
+  }, 'applyConstraints rejects invalid resizeMode');
+
+  promise_test(async t => {
+    const stream = await navigator.mediaDevices.getUserMedia({ video: true });
+    const [track] = stream.getVideoTracks();
+    t.add_cleanup(() => track.stop());
+    const resizeMode = track.getSettings().resizeMode;
+    await track.applyConstraints({ resizeMode: "INVALID" });
+    assert_equals(track.getSettings().resizeMode, resizeMode);
+  }, 'applyConstraints accepts invalid ideal resizeMode, does not change setting');
 </script>
--- a/testing/web-platform/tests/mediacapture-streams/MediaStreamTrack-getCapabilities.https.html
+++ b/testing/web-platform/tests/mediacapture-streams/MediaStreamTrack-getCapabilities.https.html
@@ -1,22 +1,150 @@
 <!doctype html>
-<title>MediaStreamTrack GetCapabilities</title>
-<p class="instructions">This test checks for the presence of audio and video properties in
-<code>MediaStreamTrack.getCapabilities()</code> method.</p>
+<title>MediaStreamTrack and InputDeviceInfo GetCapabilities</title>
 <script src=/resources/testharness.js></script>
 <script src=/resources/testharnessreport.js></script>
 <script>
-  promise_test(() => {
-  return navigator.mediaDevices.getUserMedia({audio: true, video: true})
-    .then(stream => {
-      var audioCapabilities = stream.getAudioTracks()[0].getCapabilities();
-      var videoCapabilities = stream.getVideoTracks()[0].getCapabilities();
-      assert_true(undefined !== audioCapabilities.deviceId, "MediaTrackCapabilities's deviceId should exist for an audio track.");
-      assert_true(undefined !== audioCapabilities.groupId, "MediaTrackCapabilities's groupId should exist for an audio track.");
-      assert_true(undefined !== audioCapabilities.echoCancellation, "MediaTrackCapabilities's echoCancellation should exist for an audio track.");
-      assert_true(undefined !== audioCapabilities.autoGainControl, "MediaTrackCapabilities's autoGainControl should exist for an audio track.");
-      assert_true(undefined !== audioCapabilities.noiseSuppression, "MediaTrackCapabilities's noiseSuppression should exist for an audio track.");
-      assert_true(undefined !== videoCapabilities.deviceId, "MediaTrackCapabilities's deviceId should exist for a video track.");
-      assert_true(undefined !== videoCapabilities.groupId, "MediaTrackCapabilities's groupId should exist for a video track.");
-    });
+
+const audioProperties = [
+  {name: "volume", type: "number"},
+  {name: "sampleRate", type: "number"},
+  {name: "sampleSize", type: "number"},
+  {name: "echoCancellation", type: "boolean"},
+  {name: "autoGainControl", type: "boolean"},
+  {name: "noiseSuppression", type: "boolean"},
+  {name: "latency", type: "number"},
+  {name: "channelCount", type: "number"},
+  {name: "deviceId", type: "string"},
+  {name: "groupId", type: "string"}
+];
+
+const videoProperties = [
+  {name: "width", type: "number"},
+  {name: "height", type: "number"},
+  {name: "aspectRatio", type: "number"},
+  {name: "frameRate", type: "number"},
+  {name: "facingMode", type: "enum-any", validValues: ["user", "environment", "left", "right"]},
+  {name: "resizeMode", type: "enum-all", validValues: ["none", "crop-and-scale"]},
+  {name: "deviceId", type: "string"},
+  {name: "groupId", type: "string"},
+];
+
+function verifyBooleanCapability(capability) {
+  assert_less_than_equal(capability.length, 2);
+  capability.forEach(c => assert_equals(typeof c, "boolean"));
+}
+
+function verifyNumberCapability(capability) {
+    assert_equals(typeof capability, "object");
+    assert_equals(Object.keys(capability).length, 2);
+    assert_true(capability.hasOwnProperty('min'));
+    assert_true(capability.hasOwnProperty('max'));
+    assert_less_than_equal(capability.min, capability.max);
+}
+
+// Verify that any value provided by an enum capability is in the set of valid
+// values.
+function verifyEnumAnyCapability(capability, enumMembers) {
+  capability.forEach(c => {
+    assert_equals(typeof c, "string");
+    assert_in_array(c, enumMembers);
+  });
+}
+
+// Verify that all required values are supported by a capability.
+function verifyEnumAllCapability(capability, enumMembers, testNamePrefix) {
+  enumMembers.forEach(member => {
+    test(() => {
+      assert_in_array(member, capability);
+    }, testNamePrefix + " Value: " + member);
   });
+}
+
+function testCapabilities(capabilities, property, testNamePrefix) {
+  let testName = testNamePrefix + " " + property.name;
+  test(() => {
+    assert_true(capabilities.hasOwnProperty(property.name));
+  }, testName + " property present.");
+
+  const capability = capabilities[property.name];
+  testName += " properly supported.";
+  if (property.type == "string") {
+    test(() => {
+      assert_equals(typeof capability, "string");
+    }, testName);
+  }
+
+  if (property.type == "boolean") {
+    test(() => {
+      verifyBooleanCapability(capability);
+    }, testName);
+  }
+
+  if (property.type == "number") {
+    test(() => {
+      verifyNumberCapability(capability);
+    }, testName);
+  }
+
+  if (property.type.startsWith("enum")) {
+    test(() => {
+      verifyEnumAnyCapability(capability, property.validValues);
+    }, testName);
+
+    if (property.type == "enum-all") {
+      verifyEnumAllCapability(capability, property.validValues, testName);
+    }
+  }
+}
+
+{
+  audioProperties.forEach(property => {
+    promise_test(async t => {
+      const stream = await navigator.mediaDevices.getUserMedia({audio: true});
+      t.add_cleanup(() => stream.getAudioTracks()[0].stop());
+      const audioCapabilities = stream.getAudioTracks()[0].getCapabilities();
+      testCapabilities(audioCapabilities, property, "Audio track getCapabilities()");
+    }, "Setup audio MediaStreamTrack getCapabilities() test for " + property.name);
+  });
+
+  videoProperties.forEach(property => {
+    promise_test(async t => {
+      const stream = await navigator.mediaDevices.getUserMedia({video: true});
+      t.add_cleanup(() => stream.getVideoTracks()[0].stop());
+      const audioCapabilities = stream.getVideoTracks()[0].getCapabilities();
+      testCapabilities(audioCapabilities, property, "Video track getCapabilities()");
+    }, "Setup video MediaStreamTrack getCapabilities() test for " + property.name);
+  });
+}
+
+{
+  audioProperties.forEach(property => {
+    promise_test(async t => {
+      const devices = await navigator.mediaDevices.enumerateDevices();
+      for (const device of devices) {
+        // Test only one device.
+        if (device.kind == "audioinput") {
+          assert_inherits(device, "getCapabilities");
+          const capabilities = device.getCapabilities();
+          testCapabilities(capabilities, property, "Audio device getCapabilities()");
+          break;
+        }
+      }
+    }, "Setup audio InputDeviceInfo getCapabilities() test for " + property.name);
+  });
+
+  videoProperties.forEach(property => {
+    promise_test(async t => {
+      const devices = await navigator.mediaDevices.enumerateDevices();
+      for (const device of devices) {
+        // Test only one device.
+        if (device.kind == "videoinput") {
+          assert_inherits(device, "getCapabilities");
+          const capabilities = device.getCapabilities();
+          testCapabilities(capabilities, property, "Video device getCapabilities()");
+          break;
+        }
+      }
+    }, "Setup video InputDeviceInfo getCapabilities() test for " + property.name);
+  });
+}
 </script>
--- a/testing/web-platform/tests/payment-request/payment-response/retry-method-manual.https.html
+++ b/testing/web-platform/tests/payment-request/payment-response/retry-method-manual.https.html
@@ -51,17 +51,17 @@ function repeatedCallsToRetry(button) {
 }
 
 function callCompleteWhileRetrying(button) {
   button.disabled = true;
   promise_test(async t => {
     const { response } = await getPaymentRequestResponse();
     const retryPromise = response.retry();
     const completePromise1 = response.complete("success");
-    const completePromise2 = response.complete("failure");
+    const completePromise2 = response.complete("fail");
     assert_not_equals(
       completePromise1,
       completePromise2,
       "complete() must return unique promises"
     );
     await promise_rejects(
       t,
       "InvalidStateError",
--- a/testing/web-platform/tests/picture-in-picture/mediastream.html
+++ b/testing/web-platform/tests/picture-in-picture/mediastream.html
@@ -6,16 +6,17 @@
 <script src="/resources/testdriver-vendor.js"></script>
 <script src="resources/picture-in-picture-helpers.js"></script>
 <body></body>
 <script>
 promise_test(async t => {
   const canvas = document.createElement('canvas');
   const video = document.createElement('video');
   const mediastreamVideoLoadedPromise = new Promise((resolve, reject) => {
+    canvas.getContext('2d').fillRect(0, 0, canvas.width, canvas.height);
     video.autoplay = true;
     video.srcObject = canvas.captureStream(60 /* fps */);
     video.onloadedmetadata = () => {
       resolve(video);
     };
     video.onerror = error => {
       reject(error);
     };
--- a/testing/web-platform/tests/service-workers/service-worker/clients-get-client-types.https.html
+++ b/testing/web-platform/tests/service-workers/service-worker/clients-get-client-types.https.html
@@ -4,21 +4,23 @@
 <script src="/resources/testharnessreport.js"></script>
 <script src="resources/test-helpers.sub.js"></script>
 <script>
 var scope = 'resources/clients-get-client-types';
 var frame_url = scope + '-frame.html';
 var shared_worker_url = scope + '-shared-worker.js';
 var worker_url = scope + '-worker.js';
 var client_ids = [];
+var registration;
 var frame;
 promise_test(function(t) {
     return service_worker_unregister_and_register(
         t, 'resources/clients-get-worker.js', scope)
-      .then(function(registration) {
+      .then(function(r) {
+          registration = r;
           add_completion_callback(function() { registration.unregister(); });
           return wait_for_state(t, registration.installing, 'activated');
         })
       .then(function() {
           return with_iframe(frame_url);
         })
       .then(function(f) {
           frame = f;
@@ -54,22 +56,20 @@ promise_test(function(t) {
           return new Promise(function(resolve) {
               channel.port1.onmessage = function(e) {
                 resolve(e.data.clientId);
               };
             });
         })
       .then(function(client_id) {
           client_ids.push(client_id);
-          var channel = new MessageChannel();
           var saw_message = new Promise(function(resolve) {
-              channel.port1.onmessage = resolve;
+              navigator.serviceWorker.onmessage = resolve;
             });
-          frame.contentWindow.navigator.serviceWorker.controller.postMessage(
-              {port: channel.port2, clientIds: client_ids}, [channel.port2]);
+          registration.active.postMessage({clientIds: client_ids});
           return saw_message;
         })
       .then(function(e) {
           assert_equals(e.data.length, expected.length);
           // We use these assert_not_equals because assert_array_equals doesn't
           // print the error description when passed an undefined value.
           assert_not_equals(e.data[0], undefined,
               'Window client should not be undefined');
--- a/testing/web-platform/tests/service-workers/service-worker/clients-get.https.html
+++ b/testing/web-platform/tests/service-workers/service-worker/clients-get.https.html
@@ -1,224 +1,154 @@
 <!DOCTYPE html>
 <title>Service Worker: Clients.get</title>
 <script src="/resources/testharness.js"></script>
 <script src="/resources/testharnessreport.js"></script>
 <script src="resources/test-helpers.sub.js"></script>
 <script>
-var scope = 'resources/clients-get-frame.html';
-var client_ids = [];
-var frame;
-promise_test(function(t) {
-    return service_worker_unregister_and_register(
-        t, 'resources/clients-get-worker.js', scope)
-      .then(function(registration) {
-          add_completion_callback(function() { registration.unregister(); });
-          return wait_for_state(t, registration.installing, 'activated');
-        })
-      .then(function() {
-          return with_iframe(scope + '#1');
-        })
-      .then(function(frame1) {
-          add_completion_callback(function() { frame1.remove(); });
-          frame1.focus();
-          return wait_for_clientId();
-        })
-      .then(function(client_id) {
-          client_ids.push(client_id);
-          return with_iframe(scope + '#2');
-        })
-      .then(function(frame2) {
-          frame = frame2;
-          add_completion_callback(function() { frame2.remove(); });
-          return wait_for_clientId();
-        })
-      .then(function(client_id) {
-          client_ids.push(client_id, 'invalid-id');
-          var channel = new MessageChannel();
-          var saw_message = new Promise(function(resolve) {
-              channel.port1.onmessage = resolve;
-            });
-          frame.contentWindow.navigator.serviceWorker.controller.postMessage(
-              {port: channel.port2, clientIds: client_ids}, [channel.port2]);
-          return saw_message;
-        })
-      .then(function(e) {
-          assert_equals(e.data.length, 3);
-          assert_array_equals(e.data[0], expected[0]);
-          assert_array_equals(e.data[1], expected[1]);
-          assert_equals(e.data[2], expected[2]);
-        });
-  }, 'Test Clients.get()');
-
-promise_test((t) => {
-  let frame = null;
-  const scope = 'resources/simple.html';
-  const outerSwContainer = navigator.serviceWorker;
-  let innerSwReg = null;
-  let innerSw = null;
-
-  return service_worker_unregister_and_register(
-    t, 'resources/clients-get-resultingClientId-worker.js', scope)
-    .then((registration) => {
-        innerSwReg = registration;
-        add_completion_callback(function() { registration.unregister(); });
-        return wait_for_state(t, registration.installing, 'activated');
-    })
-    .then(() => {
-        // load frame and get resulting client id
-      let channel = new MessageChannel();
-      innerSw = innerSwReg.active;
-
-      let p = new Promise(resolve => {
-        function getResultingClientId(e) {
-          if (e.data.msg == 'getResultingClientId') {
-            const { resultingClientId } = e.data;
-
-            channel.port1.removeEventListener('message', getResultingClientId);
-
-            resolve({ resultingClientId, port: channel.port1 });
-          }
-        }
-
-        channel.port1.onmessage = getResultingClientId;
-      });
-
-
-      return with_iframe(scope).then((iframe) => {
-        innerSw.postMessage(
-          { port: channel.port2, msg: 'getResultingClientId' },
-          [channel.port2],
-        );
-
-        frame = iframe;
-        frame.focus();
-        add_completion_callback(() => iframe.remove());
-
-        return p;
-      });
-    })
-    .then(({ resultingClientId, port }) => {
-      // query service worker for clients.get(resultingClientId)
-      let channel = new MessageChannel();
-
-      let p = new Promise(resolve => {
-        function getIsResultingClientUndefined(e) {
-          if (e.data.msg == 'getIsResultingClientUndefined') {
-            let { isResultingClientUndefined } = e.data;
-
-            port.removeEventListener('message', getIsResultingClientUndefined);
-
-            resolve(isResultingClientUndefined);
-          }
-        }
-
-        port.onmessage = getIsResultingClientUndefined;
-      });
-
-      innerSw.postMessage(
-        { port: channel.port2, msg: 'getIsResultingClientUndefined', resultingClientId },
-        [channel.port2],
-      );
-
-      return p;
-    })
-    .then((isResultingClientUndefined) => {
-      assert_false(isResultingClientUndefined, 'Clients.get(FetchEvent.resultingClientId) resolved with a Client');
-    });
-}, 'Test successful Clients.get(FetchEvent.resultingClientId)');
-
-promise_test((t) => {
-  const scope = 'resources/simple.html?fail';
-  const outerSwContainer = navigator.serviceWorker;
-  let innerSwReg = null;
-  let innerSw = null;
-
-  return service_worker_unregister_and_register(
-    t, 'resources/clients-get-resultingClientId-worker.js', scope)
-    .then((registration) => {
-        innerSwReg = registration;
-        add_completion_callback(function() { registration.unregister(); });
-        return wait_for_state(t, registration.installing, 'activated');
-    })
-    .then(() => {
-        // load frame, destroying it while loading, and get resulting client id
-        innerSw = innerSwReg.active;
-
-        let iframe = document.createElement('iframe');
-        iframe.className = 'test-iframe';
-        iframe.src = scope;
-
-        function destroyIframe(e) {
-          if (e.data.msg == 'destroyResultingClient') {
-            iframe.remove();
-            iframe = null;
-
-            innerSw.postMessage({ msg: 'resultingClientDestroyed' });
-          }
-        }
-
-        outerSwContainer.addEventListener('message', destroyIframe);
-
-        let p = new Promise(resolve => {
-          function resultingClientDestroyedAck(e) {
-            if (e.data.msg == 'resultingClientDestroyedAck') {
-              let { resultingDestroyedClientId } = e.data;
-
-              outerSwContainer.removeEventListener('message', resultingClientDestroyedAck);
-              resolve(resultingDestroyedClientId);
-            }
-          }
-
-          outerSwContainer.addEventListener('message', resultingClientDestroyedAck);
-        });
-
-        document.body.appendChild(iframe);
-
-        return p;
-    })
-    .then((resultingDestroyedClientId) => {
-        // query service worker for clients.get(resultingDestroyedClientId)
-        let channel = new MessageChannel();
-
-        let p = new Promise((resolve, reject) => {
-          function getIsResultingClientUndefined(e) {
-            if (e.data.msg == 'getIsResultingClientUndefined') {
-              let { isResultingClientUndefined } = e.data;
-
-              channel.port1.removeEventListener('message', getIsResultingClientUndefined);
-
-              resolve(isResultingClientUndefined);
-            }
-          }
-
-          channel.port1.onmessage = getIsResultingClientUndefined;
-        });
-
-        innerSw.postMessage(
-          { port: channel.port2, msg: 'getIsResultingClientUndefined', resultingClientId: resultingDestroyedClientId },
-          [channel.port2],
-        );
-
-        return p;
-    })
-    .then((isResultingClientUndefined) => {
-      assert_true(isResultingClientUndefined, 'Clients.get(FetchEvent.resultingClientId) resolved with `undefined`');
-    });
-}, 'Test unsuccessful Clients.get(FetchEvent.resultingClientId)');
-
 function wait_for_clientId() {
   return new Promise(function(resolve, reject) {
-      function get_client_id(e) {
-        window.removeEventListener('message', get_client_id);
-        resolve(e.data.clientId);
-      }
-      window.addEventListener('message', get_client_id, false);
-    });
+    window.onmessage = e => {
+      resolve(e.data.clientId);
+    };
+  });
 }
 
-var expected = [
+promise_test(async t => {
+  // Register service worker.
+  const scope = 'resources/clients-get-frame.html';
+  const client_ids = [];
+  const registration = await service_worker_unregister_and_register(
+    t, 'resources/clients-get-worker.js', scope);
+  t.add_cleanup(() => registration.unregister());
+  await wait_for_state(t, registration.installing, 'activated');
+
+  // Prepare for test cases.
+  // Case 1: frame1 which is focused.
+  const frame1 = await with_iframe(scope + '#1');
+  t.add_cleanup(() => frame1.remove());
+  frame1.focus();
+  client_ids.push(await wait_for_clientId());
+  // Case 2: frame2 which is not focused.
+  const frame2 = await with_iframe(scope + '#2');
+  t.add_cleanup(() =>  frame2.remove());
+  client_ids.push(await wait_for_clientId());
+  // Case 3: invalid id.
+  client_ids.push('invalid-id');
+
+  // Call clients.get() for each id on the service worker.
+  const message_event = await new Promise(resolve => {
+    navigator.serviceWorker.onmessage = resolve;
+    registration.active.postMessage({clientIds: client_ids});
+  });
+
+  const expected = [
     // visibilityState, focused, url, type, frameType
     ['visible', true, normalizeURL(scope) + '#1', 'window', 'nested'],
     ['visible', false, normalizeURL(scope) + '#2', 'window', 'nested'],
     undefined
-];
+  ];
+  assert_equals(message_event.data.length, 3);
+  assert_array_equals(message_event.data[0], expected[0]);
+  assert_array_equals(message_event.data[1], expected[1]);
+  assert_equals(message_event.data[2], expected[2]);
+}, 'Test Clients.get()');
+
+promise_test(async t => {
+  // Register service worker.
+  const scope = 'resources/simple.html';
+  const registration = await service_worker_unregister_and_register(
+    t, 'resources/clients-get-resultingClientId-worker.js', scope)
+  t.add_cleanup(() =>  registration.unregister());
+  await wait_for_state(t, registration.installing, 'activated');
+  const worker = registration.active;
+
+  // Load frame within the scope.
+  const frame = await with_iframe(scope);
+  t.add_cleanup(() => frame.remove());
+  frame.focus();
+
+  // Get resulting client id.
+  const resultingClientId = await new Promise(resolve => {
+    navigator.serviceWorker.onmessage = e => {
+      if (e.data.msg == 'getResultingClientId') {
+        resolve(e.data.resultingClientId);
+      }
+    };
+    worker.postMessage({msg: 'getResultingClientId'});
+  });
+
+  // Query service worker for clients.get(resultingClientId).
+  const isResultingClientUndefined = await new Promise(resolve => {
+    navigator.serviceWorker.onmessage = e => {
+      if (e.data.msg == 'getIsResultingClientUndefined') {
+        resolve(e.data.isResultingClientUndefined);
+      }
+    };
+    worker.postMessage({msg: 'getIsResultingClientUndefined',
+                        resultingClientId});
+  });
+
+  assert_false(
+    isResultingClientUndefined,
+    'Clients.get(FetchEvent.resultingClientId) resolved with a Client');
+}, 'Test successful Clients.get(FetchEvent.resultingClientId)');
+
+promise_test(async t => {
+  // Register service worker.
+  const scope = 'resources/simple.html?fail';
+  const registration = await service_worker_unregister_and_register(
+    t, 'resources/clients-get-resultingClientId-worker.js', scope);
+  t.add_cleanup(() =>  registration.unregister());
+  await wait_for_state(t, registration.installing, 'activated');
+
+  // Load frame, and destroy it while loading.
+  const worker = registration.active;
+  let frame = document.createElement('iframe');
+  frame.src = scope;
+  t.add_cleanup(() => {
+    if (frame) {
+      frame.remove();
+    }
+  });
+
+  await new Promise(resolve => {
+    navigator.serviceWorker.onmessage = e => {
+      // The service worker posts a message to remove the iframe during fetch
+      // event.
+      if (e.data.msg == 'destroyResultingClient') {
+        frame.remove();
+        frame = null;
+        worker.postMessage({msg: 'resultingClientDestroyed'});
+        resolve();
+      }
+    };
+    document.body.appendChild(frame);
+  });
+
+  resultingDestroyedClientId = await new Promise(resolve => {
+    navigator.serviceWorker.onmessage = e => {
+      // The worker sends a message back when it receives the message
+      // 'resultingClientDestroyed' with the resultingClientId.
+      if (e.data.msg == 'resultingClientDestroyedAck') {
+        assert_equals(frame, null, 'Frame should be destroyed at this point.');
+        resolve(e.data.resultingDestroyedClientId);
+      }
+    };
+  });
+
+  // Query service worker for clients.get(resultingDestroyedClientId).
+  const isResultingClientUndefined = await new Promise(resolve => {
+    navigator.serviceWorker.onmessage = e => {
+      if (e.data.msg == 'getIsResultingClientUndefined') {
+        resolve(e.data.isResultingClientUndefined);
+      }
+    };
+    worker.postMessage({msg: 'getIsResultingClientUndefined',
+                        resultingClientId: resultingDestroyedClientId });
+  });
+
+  assert_true(
+    isResultingClientUndefined,
+    'Clients.get(FetchEvent.resultingClientId) resolved with `undefined`');
+}, 'Test unsuccessful Clients.get(FetchEvent.resultingClientId)');
+
 </script>
--- a/testing/web-platform/tests/service-workers/service-worker/resources/clients-get-cross-origin-frame.html
+++ b/testing/web-platform/tests/service-workers/service-worker/resources/clients-get-cross-origin-frame.html
@@ -33,21 +33,18 @@ window.addEventListener('message', funct
   var cross_origin_client_ids = [];
   cross_origin_client_ids.push(e.data.clientId);
   wait_for_worker_promise
     .then(function() {
         return with_iframe(scope);
       })
     .then(function(iframe) {
         add_completion_callback(function() { iframe.remove(); });
-        var channel = new MessageChannel();
-        channel.port1.onmessage = function(e) {
+        navigator.serviceWorker.onmessage = function(e) {
           registration.unregister();
           window.parent.postMessage(
             { type: 'clientId', value: e.data }, host_info['HTTPS_ORIGIN']
           );
         };
-        iframe.contentWindow.navigator.serviceWorker.controller.postMessage(
-            {port:channel.port2, clientIds: cross_origin_client_ids},
-            [channel.port2]);
+        registration.active.postMessage({clientIds: cross_origin_client_ids});
       });
 });
 </script>
--- a/testing/web-platform/tests/service-workers/service-worker/resources/clients-get-resultingClientId-worker.js
+++ b/testing/web-platform/tests/service-workers/service-worker/resources/clients-get-resultingClientId-worker.js
@@ -1,64 +1,60 @@
 let savedPort = null;
 let savedResultingClientId = null;
 
-async function destroyResultingClient(e) {
-  const outer = await self.clients.matchAll({ type: 'window', includeUncontrolled: true })
-    .then((clientList) => {
-    for (let c of clientList) {
-      if (c.url.endsWith('clients-get.https.html')) {
-        c.focus();
-        return c;
-      }
+async function getTestingPage() {
+  const clientList = await self.clients.matchAll({ type: 'window', includeUncontrolled: true });
+  for (let c of clientList) {
+    if (c.url.endsWith('clients-get.https.html')) {
+      c.focus();
+      return c;
     }
-  });
+  }
+  return null;
+}
 
-  const p = new Promise(resolve => {
-    function resultingClientDestroyed(evt) {
-      if (evt.data.msg == 'resultingClientDestroyed') {
-        self.removeEventListener('message', resultingClientDestroyed);
-        resolve(outer);
+async function destroyResultingClient(testingPage) {
+  const destroyedPromise = new Promise(resolve => {
+    self.addEventListener('message', e => {
+      if (e.data.msg == 'resultingClientDestroyed') {
+        resolve();
       }
-    }
-
-    self.addEventListener('message', resultingClientDestroyed);
+    }, {once: true});
   });
-
-  outer.postMessage({ msg: 'destroyResultingClient' });
-
-  return await p;
+  testingPage.postMessage({ msg: 'destroyResultingClient' });
+  return destroyedPromise;
 }
 
 self.addEventListener('fetch', async (e) => {
   let { resultingClientId } = e;
   savedResultingClientId = resultingClientId;
 
   if (e.request.url.endsWith('simple.html?fail')) {
-    e.waitUntil(new Promise(async (resolve) => {
-        let outer = await destroyResultingClient(e);
+    e.waitUntil((async () => {
+      const testingPage = await getTestingPage();
+      await destroyResultingClient(testingPage);
+      testingPage.postMessage({ msg: 'resultingClientDestroyedAck',
+                                resultingDestroyedClientId: savedResultingClientId });
+    })());
+    return;
+  }
 
-        outer.postMessage({ msg: 'resultingClientDestroyedAck',
-                            resultingDestroyedClientId: savedResultingClientId });
-        resolve();
-    }));
-  } else {
-    e.respondWith(fetch(e.request));
-  }
+  e.respondWith(fetch(e.request));
 });
 
 self.addEventListener('message', (e) => {
-  let { msg, port, resultingClientId } = e.data;
-  savedPort = savedPort || port;
-
-  if (msg == 'getIsResultingClientUndefined') {
-    self.clients.get(resultingClientId).then((client) => {
+  let { msg, resultingClientId } = e.data;
+  e.waitUntil((async () => {
+    if (msg == 'getIsResultingClientUndefined') {
+      const client = await self.clients.get(resultingClientId);
       let isUndefined = typeof client == 'undefined';
-      savedPort.postMessage({ msg: 'getIsResultingClientUndefined',
+      e.source.postMessage({ msg: 'getIsResultingClientUndefined',
         isResultingClientUndefined: isUndefined });
-    });
-  }
-
-  if (msg == 'getResultingClientId') {
-    savedPort.postMessage({ msg: 'getResultingClientId',
-      resultingClientId: savedResultingClientId });
-  }
+      return;
+    }
+    if (msg == 'getResultingClientId') {
+      e.source.postMessage({ msg: 'getResultingClientId',
+                             resultingClientId: savedResultingClientId });
+      return;
+    }
+  })());
 });
--- a/testing/web-platform/tests/service-workers/service-worker/resources/clients-get-worker.js
+++ b/testing/web-platform/tests/service-workers/service-worker/resources/clients-get-worker.js
@@ -6,37 +6,36 @@
 self.onfetch = function(e) {
   if (/\/clientId$/.test(e.request.url)) {
     e.respondWith(new Response(e.clientId));
     return;
   }
 };
 
 self.onmessage = function(e) {
-  var port = e.data.port;
   var client_ids = e.data.clientIds;
   var message = [];
 
   e.waitUntil(Promise.all(
       client_ids.map(function(client_id) {
           return self.clients.get(client_id);
         }))
       .then(function(clients) {
           // No matching client for a given id or a matched client is off-origin
           // from the service worker.
           if (clients.length == 1 && clients[0] == undefined) {
-            port.postMessage(clients[0]);
+            e.source.postMessage(clients[0]);
           } else {
             clients.forEach(function(client) {
                 if (client instanceof Client) {
                   message.push([client.visibilityState,
                                 client.focused,
                                 client.url,
                                 client.type,
                                 client.frameType]);
                 } else {
                   message.push(client);
                 }
               });
-            port.postMessage(message);
+            e.source.postMessage(message);
           }
         }));
 };
new file mode 100644
--- /dev/null
+++ b/testing/web-platform/tests/svg/embedded/image-embedding-svg-with-viewport-units-inline-style.svg
@@ -0,0 +1,10 @@
+<svg xmlns="http://www.w3.org/2000/svg"
+     xmlns:xlink="http://www.w3.org/1999/xlink"
+     xmlns:h="http://www.w3.org/1999/xhtml">
+  <metadata>
+    <title>&lt;image&gt; embedding SVG image with viewport units</title>
+    <h:link rel="match" href="support/green-rect-100x100.svg"/>
+  </metadata>
+  <image xlink:href="data:image/svg+xml,&lt;svg xmlns='http://www.w3.org/2000/svg'&gt;&lt;rect style='width: 50vw; height: 50vh' fill='green'/&gt;&lt;/svg&gt;"
+         width="200" height="200"/>
+</svg>
new file mode 100644
--- /dev/null
+++ b/testing/web-platform/tests/svg/rendering/order/clip-path-filter-order-ref.svg
@@ -0,0 +1,6 @@
+<svg xmlns="http://www.w3.org/2000/svg"
+     xmlns:h="http://www.w3.org/1999/xhtml"
+     xmlns:xlink="http://www.w3.org/1999/xlink"
+     width="800" height="600" viewBox="0 0 800 600">
+  <rect x="100" y="100" width="200" height="200" fill="green" />
+</svg>
new file mode 100644
--- /dev/null
+++ b/testing/web-platform/tests/svg/rendering/order/clip-path-filter-order.svg
@@ -0,0 +1,26 @@
+<svg xmlns="http://www.w3.org/2000/svg"
+     xmlns:h="http://www.w3.org/1999/xhtml"
+     xmlns:xlink="http://www.w3.org/1999/xlink"
+     width="800" height="600" viewBox="0 0 800 600">
+  <metadata>
+    <h:link rel="author" title="Philip Rogers" href="mailto:pdr@chromium.org"/>
+    <h:link rel="help" href="https://www.w3.org/TR/SVG11/single-page.html#render-RenderingOrder"/>
+    <h:link rel="match" href="clip-path-filter-order-ref.svg"/>
+    <h:meta name="assert" content="Clip path should apply after filtering."/>
+  </metadata>
+
+  <defs>
+    <filter id="redDropShadowFilter">
+      <feOffset result="offsetOut" in="SourceGraphic" dx="10" dy="10" />
+      <feColorMatrix result="colorMatrixOut" in="offsetOut" type="matrix"
+          values="1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0" />
+      <feGaussianBlur result="blurOut" in="colorMatrixOut" stdDeviation="15" />
+      <feBlend in="SourceGraphic" in2="blurOut" mode="normal" />
+    </filter>
+    <clipPath id="clipPath">
+      <rect x="100" y="100" width="200" height="200" />
+    </clipPath>
+  </defs>
+  <rect x="100" y="100" width="400" height="400" fill="green"
+      filter="url(#redDropShadowFilter)" clip-path="url(#clipPath)" />
+</svg>
new file mode 100644
--- /dev/null
+++ b/testing/web-platform/tests/tools/ci/azure/README.md
@@ -0,0 +1,2 @@
+These are step templates for Azure Pipelines, used in `.azure-pipelines.yml`
+in the root of the repository.
new file mode 100644
--- /dev/null
+++ b/testing/web-platform/tests/tools/ci/azure/checkout.yml
@@ -0,0 +1,4 @@
+steps:
+- checkout: self
+  fetchDepth: 50
+  submodules: false
new file mode 100644
--- /dev/null
+++ b/testing/web-platform/tests/tools/ci/azure/install_certs.yml
@@ -0,0 +1,5 @@
+steps:
+- script: |
+    # https://github.com/web-platform-tests/results-collection/blob/master/src/scripts/trust-root-ca.sh
+    sudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain tools/certs/cacert.pem
+  displayName: 'Install web-platform.test certificate'
new file mode 100644
--- /dev/null
+++ b/testing/web-platform/tests/tools/ci/azure/install_chrome.yml
@@ -0,0 +1,3 @@
+steps:
+- script: HOMEBREW_NO_AUTO_UPDATE=1 brew cask install Homebrew/homebrew-cask-versions/google-chrome-dev
+  displayName: 'Install Chrome Dev'
new file mode 100644
--- /dev/null
+++ b/testing/web-platform/tests/tools/ci/azure/install_firefox.yml
@@ -0,0 +1,3 @@
+steps:
+- script: HOMEBREW_NO_AUTO_UPDATE=1 brew cask install Homebrew/homebrew-cask-versions/firefox-nightly
+  displayName: 'Install Firefox Nightly'
new file mode 100644
--- /dev/null
+++ b/testing/web-platform/tests/tools/ci/azure/install_fonts.yml
@@ -0,0 +1,5 @@
+steps:
+# Installig Ahem in /Library/Fonts instead of using --install-fonts is a
+# workaround for https://github.com/web-platform-tests/wpt/issues/13803.
+- script: sudo cp fonts/Ahem.ttf /Library/Fonts
+  displayName: 'Install Ahem font'
new file mode 100644
--- /dev/null
+++ b/testing/web-platform/tests/tools/ci/azure/install_safari.yml
@@ -0,0 +1,9 @@
+steps:
+- script: |
+    # Pin to STP 67, as SafariDriver isn't working in 68:
+    # https://github.com/web-platform-tests/wpt/issues/13800
+    HOMEBREW_NO_AUTO_UPDATE=1 brew cask install https://raw.githubusercontent.com/Homebrew/homebrew-cask-versions/23fae0a88868911913c2ee7d527c89164b6d5720/Casks/safari-technology-preview.rb
+    # https://web-platform-tests.org/running-tests/safari.html
+    sudo "/Applications/Safari Technology Preview.app/Contents/MacOS/safaridriver" --enable
+    defaults write com.apple.Safari WebKitJavaScriptCanOpenWindowsAutomatically 1
+  displayName: 'Install Safari Technology Preview'
new file mode 100644
--- /dev/null
+++ b/testing/web-platform/tests/tools/ci/azure/pip_install.yml
@@ -0,0 +1,12 @@
+parameters:
+  packages: ''
+
+steps:
+- script: |
+    sudo easy_install pip
+    # `sudo pip install` is not used because some packages (e.g. tox) depend on
+    # system packages (e.g. setuptools) which cannot be upgraded due to System
+    # Integrity Protection, see https://stackoverflow.com/a/33004920.
+    pip install --user ${{ parameters.packages }}
+    echo "##vso[task.prependpath]$HOME/Library/Python/2.7/bin"
+  displayName: 'Install Python packages'
new file mode 100644
--- /dev/null
+++ b/testing/web-platform/tests/tools/ci/azure/tox_pytest.yml
@@ -0,0 +1,18 @@
+parameters:
+  directory: ''
+  toxenv: 'ALL'
+
+steps:
+- template: pip_install.yml
+  parameters:
+    packages: tox
+
+- script: tox -c ${{ parameters.directory }} -e ${{ parameters.toxenv }} -- --junitxml=results.xml
+  displayName: 'Run tests'
+
+- task: PublishTestResults@2
+  inputs:
+    testResultsFiles: '${{ parameters.directory }}/results.xml'
+    testRunTitle: '${{ parameters.directory }}'
+  displayName: 'Publish results'
+  condition: succeededOrFailed()
new file mode 100644
--- /dev/null
+++ b/testing/web-platform/tests/tools/ci/azure/update_hosts.yml
@@ -0,0 +1,3 @@
+steps:
+- script: ./wpt make-hosts-file | sudo tee -a /etc/hosts
+  displayName: 'Update /etc/hosts'
new file mode 100644
--- /dev/null
+++ b/testing/web-platform/tests/tools/ci/azure/update_manifest.yml
@@ -0,0 +1,3 @@
+steps:
+- script: ./wpt manifest
+  displayName: 'Update manifest'
--- a/testing/web-platform/tests/webrtc/RTCPeerConnection-helper.js
+++ b/testing/web-platform/tests/webrtc/RTCPeerConnection-helper.js
@@ -246,24 +246,29 @@ function createDataChannelPair(
     pc2.addEventListener('datachannel', onDataChannel);
 
     doSignalingHandshake(pc1, pc2);
   });
 }
 
 // Wait for RTP and RTCP stats to arrive
 async function waitForRtpAndRtcpStats(pc) {
+  // If remote stats are never reported, return after 5 seconds.
+  const startTime = performance.now();
   while (true) {
     const report = await pc.getStats();
     const stats = [...report.values()].filter(({type}) => type.endsWith("bound-rtp"));
     // Each RTP and RTCP stat has a reference
     // to the matching stat in the other direction
     if (stats.length && stats.every(({localId, remoteId}) => localId || remoteId)) {
       break;
     }
+    if (performance.now() > startTime + 5000) {
+      break;
+    }
   }
 }
 
 // Wait for a single message event and return
 // a promise that resolve when the event fires
 function awaitMessage(channel) {
   return new Promise((resolve, reject) => {
     channel.addEventListener('message',
--- a/testing/web-platform/tests/webrtc/RTCPeerConnection-onnegotiationneeded.html
+++ b/testing/web-platform/tests/webrtc/RTCPeerConnection-onnegotiationneeded.html
@@ -159,20 +159,23 @@
 
   /*
     4.7.3.  Updating the Negotiation-Needed flag
       To update the negotiation-needed flag
       2.  If connection's signaling state is not "stable", abort these steps.
    */
   test_never_resolve(t => {
     const pc = new RTCPeerConnection();
-    const negotiated = awaitNegotiation(pc);
+    let negotiated;
 
     return generateAudioReceiveOnlyOffer(pc)
-    .then(offer => pc.setLocalDescription(offer))
+    .then(offer => {
+      pc.setLocalDescription(offer);
+      negotiated = awaitNegotiation(pc);
+    })
     .then(() => negotiated)
     .then(({nextPromise}) => {
       assert_equals(pc.signalingState, 'have-local-offer');
       pc.createDataChannel('test');
       return nextPromise;
     });
   }, 'negotiationneeded event should not fire if signaling state is not stable');
 
--- a/testing/web-platform/tests/websockets/binary/002.html
+++ b/testing/web-platform/tests/websockets/binary/002.html
@@ -18,10 +18,10 @@ async_test(function(t){
    ws.send(data);
   });
   ws.onmessage = t.step_func(function(e) {
     assert_true(e.data instanceof Blob);
     assert_equals(e.data.size, datasize);
     t.done();
   });
 
-}, null, {timeout:20000});
+});
 </script>
--- a/testing/web-platform/tests/websockets/binary/004.html
+++ b/testing/web-platform/tests/websockets/binary/004.html
@@ -17,10 +17,10 @@ async_test(function(t){
    data = new ArrayBuffer(datasize);
    ws.send(data);
   })
   ws.onmessage = t.step_func(function(e) {
     assert_equals(e.data.byteLength, datasize);
     t.done();
   })
 
-}, null, {timeout:20000});
+});
 </script>
--- a/testing/web-platform/tests/websockets/constructor/013.html
+++ b/testing/web-platform/tests/websockets/constructor/013.html
@@ -31,11 +31,11 @@ async_test(function(t) {
       events++;
       if (events == 75) {
         t.done();
       }
       this.onclose = t.step_func(function() {assert_unreached()});
     }, ws[i]);
     ws[i].onerror = t.step_func(function() {assert_unreached()});
   }
-}, null, {timeout:25000});
+});
 </script>
 
--- a/testing/web-platform/tests/websockets/cookies/003.html
+++ b/testing/web-platform/tests/websockets/cookies/003.html
@@ -24,10 +24,10 @@ var t = async_test(function(t) {
       ws.close();
       ws.onclose = null;
       assert_regexp_match(e.data, new RegExp('ws_test_'+cookie_id+'=test'));
       t.done();
     });
     ws.onerror = ws.onclose = t.step_func(function(e) {assert_unreached(e.type)});
   });
   document.body.appendChild(iframe);
-}, null, {timeout:9900});
+});
 </script>
--- a/testing/web-platform/tests/websockets/cookies/004.html
+++ b/testing/web-platform/tests/websockets/cookies/004.html
@@ -22,10 +22,10 @@ var t = async_test(function(t) {
   ws.onopen = t.step_func(function(e) {
     ws.close();
     ws.onclose = null;
     assert_false(new RegExp('ws_test_'+cookie_id+'=test').test(document.cookie));
     t.done();
   });
   ws.onerror = ws.onclose = t.step_func(function(e) {assert_unreached(e.type)});
   document.body.appendChild(iframe);
-}, null, {timeout:9900})
+});
 </script>
--- a/testing/web-platform/tests/websockets/cookies/007.html
+++ b/testing/web-platform/tests/websockets/cookies/007.html
@@ -26,10 +26,10 @@ async_test(function(t) {
   // sleep for 2 seconds with sync xhr
   var sleep = new XMLHttpRequest();
   sleep.open('GET', '/common/blank.html?pipe=trickle(d2)', false);
   sleep.send(null);
 
   if (new RegExp('ws_test_'+cookie_id+'=test').test(document.cookie)) {
     assert_unreached('cookie was set during script execution');
   }
-}, null, {timeout:12000});
+});
 </script>
--- a/testing/web-platform/tests/websockets/extended-payload-length.html
+++ b/testing/web-platform/tests/websockets/extended-payload-length.html
@@ -15,53 +15,53 @@ async_test(function(t){
     ws.onopen = t.step_func(function(e) {
         data = new Array(datasize + 1).join('a');
         ws.send(data);
     });
     ws.onmessage = t.step_func(function(e) {
         assert_equals(e.data, data);
         t.done();
     });
-}, "Application data is 125 byte which means any 'Extended payload length' field isn't used at all.", {timeout:20000});
+}, "Application data is 125 byte which means any 'Extended payload length' field isn't used at all.");
 
 async_test(function(t){
     var ws = new WebSocket(SCHEME_DOMAIN_PORT+'/echo');
     var datasize = 126;
     var data = null;
     ws.onopen = t.step_func(function(e) {
         data = new Array(datasize + 1).join('a');
         ws.send(data);
     });
     ws.onmessage = t.step_func(function(e) {
         assert_equals(e.data, data);
         t.done();
     });
-}, "Application data is 126 byte which starts to use the 16 bit 'Extended payload length' field.", {timeout:20000});
+}, "Application data is 126 byte which starts to use the 16 bit 'Extended payload length' field.");
 
 async_test(function(t){
     var ws = new WebSocket(SCHEME_DOMAIN_PORT+'/echo');
     var datasize = 0xFFFF;
     var data = null;
     ws.onopen = t.step_func(function(e) {
         data = new Array(datasize + 1).join('a');
         ws.send(data);
     });
     ws.onmessage = t.step_func(function(e) {
         assert_equals(e.data, data);
         t.done();
     });
-}, "Application data is 0xFFFF byte which means the upper bound of the 16 bit 'Extended payload length' field.", {timeout:20000});
+}, "Application data is 0xFFFF byte which means the upper bound of the 16 bit 'Extended payload length' field.");
 
 async_test(function(t){
     var ws = new WebSocket(SCHEME_DOMAIN_PORT+'/echo');
     var datasize = 0xFFFF + 1;
     var data = null;
     ws.onopen = t.step_func(function(e) {
         data = new Array(datasize + 1).join('a');
         ws.send(data);
     });
     ws.onmessage = t.step_func(function(e) {
         assert_equals(e.data, data);
         t.done();
     });
-}, "Application data is (0xFFFF + 1) byte which starts to use the 64 bit 'Extended payload length' field", {timeout:20000});
+}, "Application data is (0xFFFF + 1) byte which starts to use the 64 bit 'Extended payload length' field");
 
 </script>
--- a/testing/web-platform/tests/websockets/interfaces/CloseEvent/clean-close.html
+++ b/testing/web-platform/tests/websockets/interfaces/CloseEvent/clean-close.html
@@ -14,10 +14,10 @@ async_test(function(t) {
   });
   ws.onmessage = t.step_func(function(e) {
     ws.close();
   });
   ws.onclose = t.step_func(function(e) {
     assert_equals(e.wasClean,true);
     t.done();
   });
-}, null, {timeout:2000});
+});
 </script>
--- a/testing/web-platform/tests/websockets/interfaces/WebSocket/bufferedAmount/bufferedAmount-large.html
+++ b/testing/web-platform/tests/websockets/interfaces/WebSocket/bufferedAmount/bufferedAmount-large.html
@@ -18,10 +18,10 @@ async_test(function(t) {
     }
     ws.send(data);
     assert_equals(data.length, ws.bufferedAmount);
   });
   ws.onmessage = t.step_func(function(e) {
     assert_equals(e.data, data);
     t.done();
   })
-}, null, {timeout:20000});
+});
 </script>
--- a/testing/web-platform/tests/websockets/interfaces/WebSocket/close/close-basic.html
+++ b/testing/web-platform/tests/websockets/interfaces/WebSocket/close/close-basic.html
@@ -17,10 +17,10 @@ async_test(function(t) {
     delete e.wasClean;
     assert_equals(e.wasClean, false, 'delete e.wasClean');
     delete CloseEvent.prototype.wasClean;
     assert_equals(e.wasClean, undefined, 'delete CloseEvent.prototype.wasClean');
     t.done();
   });
   ws.close();
   assert_equals(ws.readyState, ws.CLOSING);
-}, undefined, {timeout:9900});
+});
 </script>
--- a/testing/web-platform/tests/websockets/interfaces/WebSocket/close/close-connecting.html
+++ b/testing/web-platform/tests/websockets/interfaces/WebSocket/close/close-connecting.html
@@ -16,10 +16,10 @@ async_test(function(t) {
     assert_equals(ws.readyState, ws.CLOSING);
     ws.onclose = t.step_func(function(e) {
       assert_equals(ws.readyState, ws.CLOSED);
       assert_equals(e.wasClean, false);
       t.done();
     });
   }, 1000);
   ws.onopen = ws.onclose = t.unreached_func();
-}, undefined, {timeout:12000});
+});
 </script>
--- a/testing/web-platform/tests/websockets/interfaces/WebSocket/close/close-nested.html
+++ b/testing/web-platform/tests/websockets/interfaces/WebSocket/close/close-nested.html
@@ -19,10 +19,10 @@ async_test(function(t) {
     }
     t.step_timeout(function() {
       assert_equals(i, 1);
       t.done();
     }, 50);
   });
   ws.close();
   assert_equals(ws.readyState, ws.CLOSING);
-}, undefined, {timeout:9900});
+});
 </script>
--- a/testing/web-platform/tests/websockets/interfaces/WebSocket/events/003.html
+++ b/testing/web-platform/tests/websockets/interfaces/WebSocket/events/003.html
@@ -12,10 +12,10 @@ async_test(function(t) {
   var foo = t.step_func(function (e) {
     if (e.detail == 5)
       t.done();
   })
   ws.onopen = foo;
   var ev = document.createEvent('UIEvents');
   ev.initUIEvent('open', false, false, window, 5);
   ws.dispatchEvent(ev);
-}, null, {timeout:2000});
+});
 </script>
--- a/testing/web-platform/tests/websockets/interfaces/WebSocket/events/007.html
+++ b/testing/web-platform/tests/websockets/interfaces/WebSocket/events/007.html
@@ -12,10 +12,10 @@ async_test(function(t) {
   var foo = t.step_func(function (e) {
     if (e.detail == 5)
       t.done();
   })
   ws.onmessage = foo;
   var ev = document.createEvent('UIEvents');
   ev.initUIEvent('message', false, false, window, 5);
   ws.dispatchEvent(ev);
-}, null, {timeout:2000});
+});
 </script>
--- a/testing/web-platform/tests/websockets/interfaces/WebSocket/events/009.html
+++ b/testing/web-platform/tests/websockets/interfaces/WebSocket/events/009.html
@@ -12,10 +12,10 @@ async_test(function(t) {
   var foo = t.step_func(function (e) {
     if (e.detail == 5)
       t.done();
   });
   ws.onclose = foo;
   var ev = document.createEvent('UIEvents');
   ev.initUIEvent('close', false, false, window, 5);
   ws.dispatchEvent(ev);
-}, null, {timeout:2000});
+});
 </script>
--- a/testing/web-platform/tests/websockets/keeping-connection-open/001.html
+++ b/testing/web-platform/tests/websockets/keeping-connection-open/001.html
@@ -19,10 +19,10 @@ async_test(function(t) {
         assert_equals(e.data, 'test');
         ws.onclose = t.step_func(function(e) {
           t.step_timeout(() => t.done(), 50);
         });
         ws.close();
       });
     }, 20000);
   })
-}, null, {timeout:30000});
+});
 </script>
--- a/testing/web-platform/tests/websockets/opening-handshake/001.html
+++ b/testing/web-platform/tests/websockets/opening-handshake/001.html
@@ -10,10 +10,10 @@
 async_test(function(t) {
   var ws = new WebSocket(SCHEME_DOMAIN_PORT+'/invalid');
   ws.onclose = t.step_func(function(e) {
     assert_false(e.wasClean);
     ws.onclose = t.unreached_func();
     t.step_timeout(() => t.done(), 50);
   });
    ws.onmessage = ws.onopen = t.unreached_func();
-}, null, {timeout:9900});
+});
 </script>
--- a/testing/web-platform/tests/websockets/opening-handshake/002.html
+++ b/testing/web-platform/tests/websockets/opening-handshake/002.html
@@ -14,10 +14,10 @@ async_test(function(t) {
     ws.onclose = t.step_func(function(e) {
       assert_equals(e.wasClean, true);
       ws.onclose = t.unreached_func();
       t.step_timeout(() => t.done(), 50);
     })
     ws.close();
   });
   ws.onerror = ws.onmessage = ws.onclose = t.unreached_func();
-}, null, {timeout:9900});
+});
 </script>
--- a/testing/web-platform/tests/websockets/unload-a-document/002.html
+++ b/testing/web-platform/tests/websockets/unload-a-document/002.html
@@ -5,17 +5,17 @@
 <script src=/resources/testharnessreport.js></script>
 <script src=/common/utils.js></script>
 <script src=../constants.js?pipe=sub></script>
 <meta name="variant" content="">
 <meta name="variant" content="?wss">
 <p>Test requires popup blocker disabled</p>
 <div id=log></div>
 <script>
-var t = async_test(null, {timeout:15000});
+var t = async_test();
 var w;
 var uuid;
 t.step(function() {
   uuid = token()
   w = window.open("002-1.html");
   add_result_callback(function() {
     w.close();
   });
--- a/testing/web-platform/tests/websockets/unload-a-document/004.html
+++ b/testing/web-platform/tests/websockets/unload-a-document/004.html
@@ -2,15 +2,15 @@
 <title>WebSockets: navigating nested browsing context with closed websocket</title>
 <meta name=timeout content=long>
 <script src=/resources/testharness.js></script>
 <script src=/resources/testharnessreport.js></script>
 <script src=/common/utils.js></script>
 <div id=log></div>
 <script>
 var uuid;
-var t = async_test(null, {timeout:15000})
+var t = async_test();
 t.step(function() {uuid = token()});
 var navigate = t.step_func(function() {
   document.getElementsByTagName("iframe")[0].src = 'data:text/html,<body onload="history.back()">';
 });
 </script>
 <iframe src=002-1.html></iframe>
--- a/testing/web-platform/tests/websockets/unload-a-document/005.html
+++ b/testing/web-platform/tests/websockets/unload-a-document/005.html
@@ -5,16 +5,16 @@
 <script src=/resources/testharnessreport.js></script>
 <script src=../constants.js?pipe=sub></script>
 <meta name="variant" content="">
 <meta name="variant" content="?wss">
 <div id=log></div>
 <p>Test requires popup blocker disabled</p>
 <div id=log></div>
 <script>
-var t = async_test(null, {timeout:15000});
+var t = async_test();
 t.step(function() {
   var w = window.open("005-1.html");
   add_result_callback(function() {
     w.close();
   });
 });
 </script>
--- a/third_party/python/gdbpp/gdbpp/thashtable.py
+++ b/third_party/python/gdbpp/gdbpp/thashtable.py
@@ -111,25 +111,30 @@ class thashtable_printer(object):
         capacity = 1 << (hashBits - table['mHashShift'])
 
         # Pierce generation-tracking EntryStore class to get at buffer.  The
         # class instance always exists, but this char* may be null.
         store = table['mEntryStore']['mEntryStore']
 
         key_field_name = self.key_field_name
 
+        # The entry store is laid out with hashes for all possible entries
+        # first, followed by all the entries.
+        pHashes = store.cast(hashType.pointer())
+        pEntries = pHashes + capacity
+        pEntries = pEntries.cast(self.entry_type.pointer())
         seenCount = 0
-        pEntry = store.cast(self.entry_type.pointer())
         for i in range(0, int(capacity)):
-            entry = (pEntry + i).dereference()
-            # An mKeyHash of 0 means empty, 1 means deleted sentinel, so skip
+            entryHash = (pHashes + i).dereference()
+            # An entry hash of 0 means empty, 1 means deleted sentinel, so skip
             # if that's the case.
-            if entry['mKeyHash'] <= 1:
+            if entryHash <= 1:
                 continue
 
+            entry = (pEntries + i).dereference()
             yield ('%d' % i, entry[key_field_name])
             if self.is_table:
                 yield ('%d' % i, entry['mData'])
 
             # Stop iterating if we know there are no more occupied slots.
             seenCount += 1
             if seenCount >= entryCount:
                 break
--- a/xpcom/ds/PLDHashTable.cpp
+++ b/xpcom/ds/PLDHashTable.cpp
@@ -11,16 +11,17 @@
 #include "PLDHashTable.h"
 #include "mozilla/HashFunctions.h"
 #include "mozilla/MathAlgorithms.h"
 #include "mozilla/OperatorNewExtensions.h"
 #include "nsAlgorithm.h"
 #include "nsPointerHashKeys.h"
 #include "mozilla/Likely.h"
 #include "mozilla/MemoryReporting.h"
+#include "mozilla/Maybe.h"
 #include "mozilla/ChaosMode.h"
 
 using namespace mozilla;
 
 #ifdef DEBUG
 
 class AutoReadOp
 {
@@ -121,18 +122,19 @@ static const PLDHashTableOps gStubOps = 
 PLDHashTable::StubOps()
 {
   return &gStubOps;
 }
 
 static bool
 SizeOfEntryStore(uint32_t aCapacity, uint32_t aEntrySize, uint32_t* aNbytes)
 {
-  uint64_t nbytes64 = uint64_t(aCapacity) * uint64_t(aEntrySize);
-  *aNbytes = aCapacity * aEntrySize;
+  uint32_t slotSize = aEntrySize + sizeof(PLDHashNumber);
+  uint64_t nbytes64 = uint64_t(aCapacity) * uint64_t(slotSize);
+  *aNbytes = aCapacity * slotSize;
   return uint64_t(*aNbytes) == nbytes64;   // returns false on overflow
 }
 
 // Compute max and min load numbers (entry counts). We have a secondary max
 // that allows us to overload a table reasonably if it cannot be grown further
 // (i.e. if ChangeTable() fails). The table slows down drastically if the
 // secondary max is too close to 1, but 0.96875 gives only a slight slowdown
 // while allowing 1.3x more elements.
@@ -295,52 +297,45 @@ PLDHashTable::Hash2(PLDHashNumber aHash0
 // that a removed-entry sentinel need be stored only if the removed entry had
 // a colliding entry added after it. Therefore we can use 1 as the collision
 // flag in addition to the removed-entry sentinel value. Multiplicative hash
 // uses the high order bits of mKeyHash, so this least-significant reservation
 // should not hurt the hash function's effectiveness much.
 
 // Match an entry's mKeyHash against an unstored one computed from a key.
 /* static */ bool
-PLDHashTable::MatchEntryKeyhash(const PLDHashEntryHdr* aEntry,
-                                const PLDHashNumber aKeyHash)
+PLDHashTable::MatchSlotKeyhash(Slot& aSlot, const PLDHashNumber aKeyHash)
 {
-  return (aEntry->mKeyHash & ~kCollisionFlag) == aKeyHash;
+  return (aSlot.KeyHash() & ~kCollisionFlag) == aKeyHash;
 }
 
 // Compute the address of the indexed entry in table.
-PLDHashEntryHdr*
-PLDHashTable::AddressEntry(uint32_t aIndex) const
+auto
+PLDHashTable::SlotForIndex(uint32_t aIndex) const -> Slot
 {
-  return const_cast<PLDHashEntryHdr*>(
-    reinterpret_cast<const PLDHashEntryHdr*>(
-      mEntryStore.Get() + aIndex * mEntrySize));
+  return mEntryStore.SlotForIndex(aIndex, mEntrySize, CapacityFromHashShift());
 }
 
 PLDHashTable::~PLDHashTable()
 {
 #ifdef DEBUG
   AutoDestructorOp op(mChecker);
 #endif
 
   if (!mEntryStore.Get()) {
     recordreplay::DestroyPLDHashTableCallbacks(mOps);
     return;
   }
 
   // Clear any remaining live entries.
-  char* entryAddr = mEntryStore.Get();
-  char* entryLimit = entryAddr + Capacity() * mEntrySize;
-  while (entryAddr < entryLimit) {
-    PLDHashEntryHdr* entry = (PLDHashEntryHdr*)entryAddr;
-    if (EntryIsLive(entry)) {
-      mOps->clearEntry(this, entry);
+  mEntryStore.ForEachSlot(Capacity(), mEntrySize, [&](const Slot& aSlot) {
+    if (aSlot.IsLive()) {
+      mOps->clearEntry(this, aSlot.ToEntry());
     }
-    entryAddr += mEntrySize;
-  }
+  });
 
   recordreplay::DestroyPLDHashTableCallbacks(mOps);
 
   // Entry storage is freed last, by ~EntryStore().
 }
 
 void
 PLDHashTable::ClearAndPrepareForLength(uint32_t aLength)
@@ -360,116 +355,123 @@ PLDHashTable::Clear()
 }
 
 // If |Reason| is |ForAdd|, the return value is always non-null and it may be
 // a previously-removed entry. If |Reason| is |ForSearchOrRemove|, the return
 // value is null on a miss, and will never be a previously-removed entry on a
 // hit. This distinction is a bit grotty but this function is hot enough that
 // these differences are worthwhile. (It's also hot enough that
 // MOZ_ALWAYS_INLINE makes a significant difference.)
-template <PLDHashTable::SearchReason Reason>
-MOZ_ALWAYS_INLINE PLDHashEntryHdr*
-PLDHashTable::SearchTable(const void* aKey, PLDHashNumber aKeyHash) const
+template <PLDHashTable::SearchReason Reason, typename Success, typename Failure>
+MOZ_ALWAYS_INLINE
+auto
+PLDHashTable::SearchTable(const void* aKey, PLDHashNumber aKeyHash,
+                          Success&& aSuccess, Failure&& aFailure) const
 {
   MOZ_ASSERT(mEntryStore.Get());
   NS_ASSERTION(!(aKeyHash & kCollisionFlag),
                "!(aKeyHash & kCollisionFlag)");
 
   // Compute the primary hash address.
   PLDHashNumber hash1 = Hash1(aKeyHash);
-  PLDHashEntryHdr* entry = AddressEntry(hash1);
+  Slot slot = SlotForIndex(hash1);
 
   // Miss: return space for a new entry.
-  if (EntryIsFree(entry)) {
-    return (Reason == ForAdd) ? entry : nullptr;
+  if (slot.IsFree()) {
+    return (Reason == ForAdd) ? aSuccess(slot) : aFailure();
   }
 
   // Hit: return entry.
   PLDHashMatchEntry matchEntry = mOps->matchEntry;
-  if (MatchEntryKeyhash(entry, aKeyHash) &&
-      matchEntry(entry, aKey)) {
-    return entry;
+  if (MatchSlotKeyhash(slot, aKeyHash)) {
+    PLDHashEntryHdr* e = slot.ToEntry();
+    if (matchEntry(e, aKey)) {
+      return aSuccess(slot);
+    }
   }
 
   // Collision: double hash.
   PLDHashNumber hash2;
   uint32_t sizeMask;
   Hash2(aKeyHash, hash2, sizeMask);
 
-  // Save the first removed entry pointer so Add() can recycle it. (Only used
+  // Save the first removed entry slot so Add() can recycle it. (Only used
   // if Reason==ForAdd.)
-  PLDHashEntryHdr* firstRemoved = nullptr;
+  Maybe<Slot> firstRemoved;
 
   for (;;) {
     if (Reason == ForAdd && !firstRemoved) {
-      if (MOZ_UNLIKELY(EntryIsRemoved(entry))) {
-        firstRemoved = entry;
+      if (MOZ_UNLIKELY(slot.IsRemoved())) {
+        firstRemoved.emplace(slot);
       } else {
-        entry->mKeyHash |= kCollisionFlag;
+        slot.MarkColliding();
       }
     }
 
     hash1 -= hash2;
     hash1 &= sizeMask;
 
-    entry = AddressEntry(hash1);
-    if (EntryIsFree(entry)) {
-      return (Reason == ForAdd) ? (firstRemoved ? firstRemoved : entry)
-                                : nullptr;
+    slot = SlotForIndex(hash1);
+    if (slot.IsFree()) {
+      if (Reason != ForAdd) {
+        return aFailure();
+      }
+      return aSuccess(firstRemoved.refOr(slot));
     }
 
-    if (MatchEntryKeyhash(entry, aKeyHash) &&
-        matchEntry(entry, aKey)) {
-      return entry;
+    if (MatchSlotKeyhash(slot, aKeyHash)) {
+      PLDHashEntryHdr* e = slot.ToEntry();
+      if (matchEntry(e, aKey)) {
+        return aSuccess(slot);
+      }
     }
   }
 
   // NOTREACHED
-  return nullptr;
+  return aFailure();
 }
 
 // This is a copy of SearchTable(), used by ChangeTable(), hardcoded to
 //   1. assume |Reason| is |ForAdd|,
 //   2. assume that |aKey| will never match an existing entry, and
 //   3. assume that no entries have been removed from the current table
 //      structure.
 // Avoiding the need for |aKey| means we can avoid needing a way to map entries
 // to keys, which means callers can use complex key types more easily.
-MOZ_ALWAYS_INLINE PLDHashEntryHdr*
-PLDHashTable::FindFreeEntry(PLDHashNumber aKeyHash) const
+MOZ_ALWAYS_INLINE auto
+PLDHashTable::FindFreeSlot(PLDHashNumber aKeyHash) const -> Slot
 {
   MOZ_ASSERT(mEntryStore.Get());
   NS_ASSERTION(!(aKeyHash & kCollisionFlag),
                "!(aKeyHash & kCollisionFlag)");
 
   // Compute the primary hash address.
   PLDHashNumber hash1 = Hash1(aKeyHash);
-  PLDHashEntryHdr* entry = AddressEntry(hash1);
+  Slot slot = SlotForIndex(hash1);
 
   // Miss: return space for a new entry.
-  if (EntryIsFree(entry)) {
-    return entry;
+  if (slot.IsFree()) {
+    return slot;
   }
 
   // Collision: double hash.
   PLDHashNumber hash2;
   uint32_t sizeMask;
   Hash2(aKeyHash, hash2, sizeMask);
 
   for (;;) {
-    NS_ASSERTION(!EntryIsRemoved(entry),
-                 "!EntryIsRemoved(entry)");
-    entry->mKeyHash |= kCollisionFlag;
+    MOZ_ASSERT(!slot.IsRemoved());
+    slot.MarkColliding();
 
     hash1 -= hash2;
     hash1 &= sizeMask;
 
-    entry = AddressEntry(hash1);
-    if (EntryIsFree(entry)) {
-      return entry;
+    slot = SlotForIndex(hash1);
+    if (slot.IsFree()) {
+      return slot;
     }
   }
 
   // NOTREACHED
 }
 
 bool
 PLDHashTable::ChangeTable(int32_t aDeltaLog2)
@@ -494,35 +496,31 @@ PLDHashTable::ChangeTable(int32_t aDelta
     return false;
   }
 
   // We can't fail from here on, so update table parameters.
   mHashShift = kPLDHashNumberBits - newLog2;
   mRemovedCount = 0;
 
   // Assign the new entry store to table.
-  char* oldEntryStore;
-  char* oldEntryAddr;
-  oldEntryAddr = oldEntryStore = mEntryStore.Get();
+  char* oldEntryStore = mEntryStore.Get();
   mEntryStore.Set(newEntryStore, &mGeneration);
   PLDHashMoveEntry moveEntry = mOps->moveEntry;
 
   // Copy only live entries, leaving removed ones behind.
   uint32_t oldCapacity = 1u << oldLog2;
-  for (uint32_t i = 0; i < oldCapacity; ++i) {
-    PLDHashEntryHdr* oldEntry = (PLDHashEntryHdr*)oldEntryAddr;
-    if (EntryIsLive(oldEntry)) {
-      const PLDHashNumber key = oldEntry->mKeyHash & ~kCollisionFlag;
-      PLDHashEntryHdr* newEntry = FindFreeEntry(key);
-      NS_ASSERTION(EntryIsFree(newEntry), "EntryIsFree(newEntry)");
-      moveEntry(this, oldEntry, newEntry);
-      newEntry->mKeyHash = key;
+  EntryStore::ForEachSlot(oldEntryStore, oldCapacity, mEntrySize, [&](const Slot& slot) {
+    if (slot.IsLive()) {
+      const PLDHashNumber key = slot.KeyHash() & ~kCollisionFlag;
+      Slot newSlot = FindFreeSlot(key);
+      MOZ_ASSERT(newSlot.IsFree());
+      moveEntry(this, slot.ToEntry(), newSlot.ToEntry());
+      newSlot.SetKeyHash(key);
     }
-    oldEntryAddr += mEntrySize;
-  }
+  });
 
   free(oldEntryStore);
   return true;
 }
 
 MOZ_ALWAYS_INLINE PLDHashNumber
 PLDHashTable::ComputeKeyHash(const void* aKey) const
 {
@@ -541,21 +539,28 @@ PLDHashTable::ComputeKeyHash(const void*
 
 PLDHashEntryHdr*
 PLDHashTable::Search(const void* aKey) const
 {
 #ifdef DEBUG
   AutoReadOp op(mChecker);
 #endif
 
-  PLDHashEntryHdr* entry = mEntryStore.Get()
-                         ? SearchTable<ForSearchOrRemove>(aKey,
-                                                          ComputeKeyHash(aKey))
-                         : nullptr;
-  return entry;
+  if (!mEntryStore.Get()) {
+    return nullptr;
+  }
+
+  return SearchTable<ForSearchOrRemove>(aKey,
+                                        ComputeKeyHash(aKey),
+                                        [&](Slot& slot) -> PLDHashEntryHdr* {
+                                          return slot.ToEntry();
+                                        },
+                                        [&]() -> PLDHashEntryHdr* {
+                                          return nullptr;
+                                        });
 }
 
 PLDHashEntryHdr*
 PLDHashTable::Add(const void* aKey, const mozilla::fallible_t&)
 {
 #ifdef DEBUG
   AutoWriteOp op(mChecker);
 #endif
@@ -591,31 +596,36 @@ PLDHashTable::Add(const void* aKey, cons
         mEntryCount + mRemovedCount >= MaxLoadOnGrowthFailure(capacity)) {
       return nullptr;
     }
   }
 
   // Look for entry after possibly growing, so we don't have to add it,
   // then skip it while growing the table and re-add it after.
   PLDHashNumber keyHash = ComputeKeyHash(aKey);
-  PLDHashEntryHdr* entry = SearchTable<ForAdd>(aKey, keyHash);
-  if (!EntryIsLive(entry)) {
-    // Initialize the entry, indicating that it's no longer free.
-    if (EntryIsRemoved(entry)) {
+  Slot slot = SearchTable<ForAdd>(aKey, keyHash,
+                                  [&](Slot& found) -> Slot { return found; },
+                                  [&]() -> Slot {
+                                    MOZ_CRASH("Nope");
+                                    return Slot(nullptr, nullptr);
+                                  });
+  if (!slot.IsLive()) {
+    // Initialize the slot, indicating that it's no longer free.
+    if (slot.IsRemoved()) {
       mRemovedCount--;
       keyHash |= kCollisionFlag;
     }
     if (mOps->initEntry) {
-      mOps->initEntry(entry, aKey);
+      mOps->initEntry(slot.ToEntry(), aKey);
     }
-    entry->mKeyHash = keyHash;
+    slot.SetKeyHash(keyHash);
     mEntryCount++;
   }
 
-  return entry;
+  return slot.ToEntry();
 }
 
 PLDHashEntryHdr*
 PLDHashTable::Add(const void* aKey)
 {
   PLDHashEntryHdr* entry = Add(aKey, fallible);
   if (!entry) {
     if (!mEntryStore.Get()) {
@@ -636,57 +646,70 @@ PLDHashTable::Add(const void* aKey)
 
 void
 PLDHashTable::Remove(const void* aKey)
 {
 #ifdef DEBUG
   AutoWriteOp op(mChecker);
 #endif
 
-  PLDHashEntryHdr* entry = mEntryStore.Get()
-                         ? SearchTable<ForSearchOrRemove>(aKey,
-                                                          ComputeKeyHash(aKey))
-                         : nullptr;
-  if (entry) {
-    RawRemove(entry);
-    ShrinkIfAppropriate();
+  if (!mEntryStore.Get()) {
+    return;
   }
+
+  PLDHashNumber keyHash = ComputeKeyHash(aKey);
+  SearchTable<ForSearchOrRemove>(aKey, keyHash,
+                                 [&](Slot& slot) {
+                                   RawRemove(slot);
+                                   ShrinkIfAppropriate();
+                                 },
+                                 [&]() {
+                                   // Do nothing.
+                                 });
 }
 
 void
 PLDHashTable::RemoveEntry(PLDHashEntryHdr* aEntry)
 {
 #ifdef DEBUG
   AutoWriteOp op(mChecker);
 #endif
 
   RawRemove(aEntry);
   ShrinkIfAppropriate();
 }
 
 void
 PLDHashTable::RawRemove(PLDHashEntryHdr* aEntry)
 {
+  Slot slot(mEntryStore.SlotForPLDHashEntry(aEntry, Capacity(), mEntrySize));
+  RawRemove(slot);
+}
+
+void
+PLDHashTable::RawRemove(Slot& aSlot)
+{
   // Unfortunately, we can only do weak checking here. That's because
   // RawRemove() can be called legitimately while an Enumerate() call is
   // active, which doesn't fit well into how Checker's mState variable works.
   MOZ_ASSERT(mChecker.IsWritable());
 
   MOZ_ASSERT(mEntryStore.Get());
 
-  MOZ_ASSERT(EntryIsLive(aEntry), "EntryIsLive(aEntry)");
+  MOZ_ASSERT(aSlot.IsLive());
 
   // Load keyHash first in case clearEntry() goofs it.
-  PLDHashNumber keyHash = aEntry->mKeyHash;
-  mOps->clearEntry(this, aEntry);
+  PLDHashEntryHdr* entry = aSlot.ToEntry();
+  PLDHashNumber keyHash = aSlot.KeyHash();
+  mOps->clearEntry(this, entry);
   if (keyHash & kCollisionFlag) {
-    MarkEntryRemoved(aEntry);
+    aSlot.MarkRemoved();
     mRemovedCount++;
   } else {
-    MarkEntryFree(aEntry);
+    aSlot.MarkFree();
   }
   mEntryCount--;
 }
 
 // Shrink or compress if a quarter or more of all entries are removed, or if the
 // table is underloaded according to the minimum alpha, and is not minimal-size
 // already.
 void
@@ -718,56 +741,56 @@ PLDHashTable::ShallowSizeOfExcludingThis
 size_t
 PLDHashTable::ShallowSizeOfIncludingThis(MallocSizeOf aMallocSizeOf) const
 {
   return aMallocSizeOf(this) + ShallowSizeOfExcludingThis(aMallocSizeOf);
 }
 
 PLDHashTable::Iterator::Iterator(Iterator&& aOther)
   : mTable(aOther.mTable)
-  , mLimit(aOther.mLimit)
   , mCurrent(aOther.mCurrent)
   , mNexts(aOther.mNexts)
   , mNextsLimit(aOther.mNextsLimit)
   , mHaveRemoved(aOther.mHaveRemoved)
+  , mEntrySize(aOther.mEntrySize)
 {
   // No need to change |mChecker| here.
   aOther.mTable = nullptr;
-  aOther.mLimit = nullptr;
-  aOther.mCurrent = nullptr;
+  // We don't really have the concept of a null slot, so leave mCurrent.
   aOther.mNexts = 0;
   aOther.mNextsLimit = 0;
   aOther.mHaveRemoved = false;
+  aOther.mEntrySize = 0;
 }
 
 PLDHashTable::Iterator::Iterator(PLDHashTable* aTable)
   : mTable(aTable)
-  , mLimit(mTable->mEntryStore.Get() + mTable->Capacity() * mTable->mEntrySize)
-  , mCurrent(mTable->mEntryStore.Get())
+  , mCurrent(mTable->mEntryStore.SlotForIndex(0, mTable->mEntrySize,
+                                              mTable->Capacity()))
   , mNexts(0)
   , mNextsLimit(mTable->EntryCount())
   , mHaveRemoved(false)
+  , mEntrySize(aTable->mEntrySize)
 {
 #ifdef DEBUG
   mTable->mChecker.StartReadOp();
 #endif
 
   if (ChaosMode::isActive(ChaosFeature::HashTableIteration) &&
       mTable->Capacity() > 0) {
     // Start iterating at a random entry. It would be even more chaotic to
     // iterate in fully random order, but that's harder.
-    mCurrent += ChaosMode::randomUint32LessThan(mTable->Capacity()) *
-                mTable->mEntrySize;
+    uint32_t capacity = mTable->CapacityFromHashShift();
+    uint32_t i = ChaosMode::randomUint32LessThan(capacity);
+    mCurrent = mTable->mEntryStore.SlotForIndex(i, mTable->mEntrySize, capacity);
   }
 
   // Advance to the first live entry, if there is one.
-  if (!Done()) {
-    while (IsOnNonLiveEntry()) {
-      MoveToNextEntry();
-    }
+  if (!Done() && IsOnNonLiveEntry()) {
+    MoveToNextLiveEntry();
   }
 }
 
 PLDHashTable::Iterator::~Iterator()
 {
   if (mTable) {
     if (mHaveRemoved) {
       mTable->ShrinkIfAppropriate();
@@ -777,48 +800,72 @@ PLDHashTable::Iterator::~Iterator()
 #endif
   }
 }
 
 MOZ_ALWAYS_INLINE bool
 PLDHashTable::Iterator::IsOnNonLiveEntry() const
 {
   MOZ_ASSERT(!Done());
-  return !EntryIsLive(reinterpret_cast<PLDHashEntryHdr*>(mCurrent));
-}
-
-MOZ_ALWAYS_INLINE void
-PLDHashTable::Iterator::MoveToNextEntry()
-{
-  mCurrent += mTable->mEntrySize;
-  if (mCurrent == mLimit) {
-    mCurrent = mTable->mEntryStore.Get();  // Wrap-around. Possible due to Chaos Mode.
-  }
+  return !mCurrent.IsLive();
 }
 
 void
 PLDHashTable::Iterator::Next()
 {
   MOZ_ASSERT(!Done());
 
   mNexts++;
 
   // Advance to the next live entry, if there is one.
   if (!Done()) {
-    do {
-      MoveToNextEntry();
-    } while (IsOnNonLiveEntry());
+    MoveToNextLiveEntry();
   }
 }
 
+MOZ_ALWAYS_INLINE void
+PLDHashTable::Iterator::MoveToNextLiveEntry()
+{
+  // Chaos mode requires wraparound to cover all possible entries, so we can't
+  // simply move to the next live entry and stop when we hit the end of the
+  // entry store. But we don't want to introduce extra branches into our inner
+  // loop. So we are going to exploit the structure of the entry store in this
+  // method to implement an efficient inner loop.
+  //
+  // The idea is that since we are really only iterating through the stored
+  // hashes and because we know that there are a power-of-two number of
+  // hashes, we can use masking to implement the wraparound for us. This
+  // method does have the downside of needing to recalculate where the
+  // associated entry is once we've found it, but that seems OK.
+
+  // Our current slot and its associated hash.
+  Slot slot = mCurrent;
+  PLDHashNumber* p = slot.HashPtr();
+  const uint32_t capacity = mTable->CapacityFromHashShift();
+  const uint32_t mask = capacity - 1;
+  auto hashes = reinterpret_cast<PLDHashNumber*>(mTable->mEntryStore.Get());
+  uint32_t slotIndex = p - hashes;
+
+  do {
+    slotIndex = (slotIndex + 1) & mask;
+  } while (!Slot::IsLiveHash(hashes[slotIndex]));
+
+  // slotIndex now indicates where a live slot is. Rematerialize the entry
+  // and the slot.
+  auto entries = reinterpret_cast<char*>(&hashes[capacity]);
+  char* entryPtr = entries + slotIndex * mEntrySize;
+  auto entry = reinterpret_cast<PLDHashEntryHdr*>(entryPtr);
+
+  mCurrent = Slot(entry, &hashes[slotIndex]);
+}
+
 void
 PLDHashTable::Iterator::Remove()
 {
-  // This cast is needed for the same reason as the one in the destructor.
-  mTable->RawRemove(Get());
+  mTable->RawRemove(mCurrent);
   mHaveRemoved = true;
 }
 
 #ifdef DEBUG
 void
 PLDHashTable::MarkImmutable()
 {
   mChecker.SetNonWritable();
--- a/xpcom/ds/PLDHashTable.h
+++ b/xpcom/ds/PLDHashTable.h
@@ -8,16 +8,17 @@
 // PLDHashTable and mozilla::HashTable.
 
 #ifndef PLDHashTable_h
 #define PLDHashTable_h
 
 #include "mozilla/Atomics.h"
 #include "mozilla/Attributes.h" // for MOZ_ALWAYS_INLINE
 #include "mozilla/fallible.h"
+#include "mozilla/FunctionTypeTraits.h"
 #include "mozilla/HashFunctions.h"
 #include "mozilla/MemoryReporting.h"
 #include "mozilla/Move.h"
 #include "mozilla/Types.h"
 #include "nscore.h"
 
 using PLDHashNumber = mozilla::HashNumber;
 static const uint32_t kPLDHashNumberBits = mozilla::kHashNumberBits;
@@ -31,33 +32,30 @@ struct PLDHashTableOps;
 // either here. Instead, the API uses const void *key as a formal parameter.
 // The key need not be stored in the entry; it may be part of the value, but
 // need not be stored at all.
 //
 // Callback types are defined below and grouped into the PLDHashTableOps
 // structure, for single static initialization per hash table sub-type.
 //
 // Each hash table sub-type should make its entry type a subclass of
-// PLDHashEntryHdr. The mKeyHash member contains the result of suitably
-// scrambling the hash code returned from the hashKey callback (see below),
-// then constraining the result to avoid the magic 0 and 1 values. The stored
-// mKeyHash value is table size invariant, and it is maintained automatically
-// -- users need never access it.
+// PLDHashEntryHdr. PLDHashEntryHdr is merely a common superclass to present a
+// uniform interface to PLDHashTable clients. The zero-sized base class
+// optimization, employed by all of our supported C++ compilers, will ensure
+// that this abstraction does not make objects needlessly larger.
 struct PLDHashEntryHdr
 {
   PLDHashEntryHdr() = default;
   PLDHashEntryHdr(const PLDHashEntryHdr&) = delete;
   PLDHashEntryHdr& operator=(const PLDHashEntryHdr&) = delete;
   PLDHashEntryHdr(PLDHashEntryHdr&&) = default;
   PLDHashEntryHdr& operator=(PLDHashEntryHdr&&) = default;
 
 private:
   friend class PLDHashTable;
-
-  PLDHashNumber mKeyHash;
 };
 
 #ifdef DEBUG
 
 // This class does three kinds of checking:
 //
 // - that calls to one of |mOps| or to an enumerator do not cause re-entry into
 //   the table in an unsafe way;
@@ -217,39 +215,174 @@ private:
 // case you should keep using double hashing but switch to using pointer
 // elements). Also, with double hashing, you can't safely hold an entry pointer
 // and use it after an add or remove operation, unless you sample Generation()
 // before adding or removing, and compare the sample after, dereferencing the
 // entry pointer only if Generation() has not changed.
 class PLDHashTable
 {
 private:
+  // A slot represents a cached hash value and its associated entry stored in
+  // the hash table. The hash value and the entry are not stored contiguously.
+  struct Slot
+  {
+    Slot(PLDHashEntryHdr* aEntry, PLDHashNumber* aKeyHash)
+      : mEntry(aEntry)
+      , mKeyHash(aKeyHash)
+    {}
+
+    Slot(const Slot&) = default;
+    Slot(Slot&& aOther) = default;
+
+    Slot& operator=(Slot&& aOther) {
+      this->~Slot();
+      new (this) Slot(std::move(aOther));
+      return *this;
+    }
+
+    bool operator==(const Slot& aOther) { return mEntry == aOther.mEntry; }
+
+    PLDHashNumber KeyHash() const { return *HashPtr(); }
+    void SetKeyHash(PLDHashNumber aHash) { *HashPtr() = aHash; }
+
+    PLDHashEntryHdr* ToEntry() const { return mEntry; }
+
+    bool IsFree() const { return KeyHash() == 0; }
+    bool IsRemoved() const { return KeyHash() == 1; }
+    bool IsLive() const { return IsLiveHash(KeyHash()); }
+    static bool IsLiveHash(uint32_t aHash) { return aHash >= 2; }
+
+    void MarkFree() { *HashPtr() = 0; }
+    void MarkRemoved() { *HashPtr() = 1; }
+    void MarkColliding() { *HashPtr() |= kCollisionFlag; }
+
+    void Next(uint32_t aEntrySize) {
+      char* p = reinterpret_cast<char*>(mEntry);
+      p += aEntrySize;
+      mEntry = reinterpret_cast<PLDHashEntryHdr*>(p);
+      mKeyHash++;
+    }
+    PLDHashNumber* HashPtr() const { return mKeyHash; }
+
+  private:
+    PLDHashEntryHdr* mEntry;
+    PLDHashNumber* mKeyHash;
+  };
+
   // This class maintains the invariant that every time the entry store is
   // changed, the generation is updated.
   //
+  // The data layout separates the cached hashes of entries and the entries
+  // themselves to save space. We could store the entries thusly:
+  //
+  // +--------+--------+---------+
+  // | entry0 | entry1 | ...     |
+  // +--------+--------+---------+
+  //
+  // where the entries themselves contain the cached hash stored as their
+  // first member. PLDHashTable did this for a long time, with entries looking
+  // like:
+  //
+  // class PLDHashEntryHdr
+  // {
+  //   PLDHashNumber mKeyHash;
+  // };
+  //
+  // class MyEntry : public PLDHashEntryHdr
+  // {
+  //   ...
+  // };
+  //
+  // The problem with this setup is that, depending on the layout of
+  // `MyEntry`, there may be platform ABI-mandated padding between `mKeyHash`
+  // and the first member of `MyEntry`. This ABI-mandated padding is wasted
+  // space, and was surprisingly common, e.g. when MyEntry contained a single
+  // pointer on 64-bit platforms.
+  //
+  // As previously alluded to, the current setup stores things thusly:
+  //
+  // +-------+-------+-------+-------+--------+--------+---------+
+  // | hash0 | hash1 | ..... | hashN | entry0 | entry1 | ...     |
+  // +-------+-------+-------+-------+--------+--------+---------+
+  //
+  // which contains no wasted space between the hashes themselves, and no
+  // wasted space between the entries themselves. malloc is guaranteed to
+  // return blocks of memory with at least word alignment on all of our major
+  // platforms. PLDHashTable mandates that the size of the hash table is
+  // always a power of two, so the alignment of the memory containing the
+  // first entry is always at least the alignment of the entire entry store.
+  // That means the alignment of `entry0` should be its natural alignment.
+  // Entries may have problems if they contain over-aligned members such as
+  // SIMD vector types, but this has not been a problem in practice.
+  //
   // Note: It would be natural to store the generation within this class, but
   // we can't do that without bloating sizeof(PLDHashTable) on 64-bit machines.
   // So instead we store it outside this class, and Set() takes a pointer to it
   // and ensures it is updated as necessary.
   class EntryStore
   {
   private:
     char* mEntryStore;
 
+    static char* Entries(char* aStore, uint32_t aCapacity)
+    {
+      return aStore + aCapacity * sizeof(PLDHashNumber);
+    }
+
+    char* Entries(uint32_t aCapacity) const
+    {
+      return Entries(Get(), aCapacity);
+    }
+
   public:
     EntryStore() : mEntryStore(nullptr) {}
 
     ~EntryStore()
     {
       free(mEntryStore);
       mEntryStore = nullptr;
     }
 
-    char* Get() { return mEntryStore; }
-    const char* Get() const { return mEntryStore; }
+    char* Get() const { return mEntryStore; }
+
+    Slot SlotForIndex(uint32_t aIndex, uint32_t aEntrySize,
+                      uint32_t aCapacity) const
+    {
+      char* entries = Entries(aCapacity);
+      auto entry = reinterpret_cast<PLDHashEntryHdr*>(entries + aIndex * aEntrySize);
+      auto hashes = reinterpret_cast<PLDHashNumber*>(Get());
+      return Slot(entry, &hashes[aIndex]);
+    }
+
+    Slot SlotForPLDHashEntry(PLDHashEntryHdr* aEntry,
+                             uint32_t aCapacity, uint32_t aEntrySize)
+    {
+      char* entries = Entries(aCapacity);
+      char* entry = reinterpret_cast<char*>(aEntry);
+      uint32_t entryOffset = entry - entries;
+      uint32_t slotIndex = entryOffset / aEntrySize;
+      return SlotForIndex(slotIndex, aEntrySize, aCapacity);
+    }
+
+    template<typename F>
+    void ForEachSlot(uint32_t aCapacity, uint32_t aEntrySize, F&& aFunc) {
+      ForEachSlot(Get(), aCapacity, aEntrySize, std::move(aFunc));
+    }
+
+    template<typename F>
+    static void ForEachSlot(char* aStore, uint32_t aCapacity, uint32_t aEntrySize,
+                            F&& aFunc) {
+      char* entries = Entries(aStore, aCapacity);
+      Slot slot(reinterpret_cast<PLDHashEntryHdr*>(entries),
+                reinterpret_cast<PLDHashNumber*>(aStore));
+      for (size_t i = 0; i < aCapacity; ++i) {
+        aFunc(slot);
+        slot.Next(aEntrySize);
+      }
+    }
 
     void Set(char* aEntryStore, uint16_t* aGeneration)
     {
       mEntryStore = aEntryStore;
       *aGeneration += 1;
     }
   };
 
@@ -343,21 +476,19 @@ public:
 
   // To add an entry identified by |key| to table, call:
   //
   //   entry = table.Add(key, mozilla::fallible);
   //
   // If |entry| is null upon return, then the table is severely overloaded and
   // memory can't be allocated for entry storage.
   //
-  // Otherwise, |aEntry->mKeyHash| has been set so that
-  // PLDHashTable::EntryIsFree(entry) is false, and it is up to the caller to
-  // initialize the key and value parts of the entry sub-type, if they have not
-  // been set already (i.e. if entry was not already in the table, and if the
-  // optional initEntry hook was not used).
+  // Otherwise, if the initEntry hook was provided, |entry| will be
+  // initialized.  If the initEntry hook was not provided, the caller
+  // should initialize |entry| as appropriate.
   PLDHashEntryHdr* Add(const void* aKey, const mozilla::fallible_t&);
 
   // This is like the other Add() function, but infallible, and so never
   // returns null.
   PLDHashEntryHdr* Add(const void* aKey);
 
   // To remove an entry identified by |key| from table, call:
   //
@@ -463,42 +594,41 @@ public:
 
     // Have we finished?
     bool Done() const { return mNexts == mNextsLimit; }
 
     // Get the current entry.
     PLDHashEntryHdr* Get() const
     {
       MOZ_ASSERT(!Done());
-
-      PLDHashEntryHdr* entry = reinterpret_cast<PLDHashEntryHdr*>(mCurrent);
-      MOZ_ASSERT(EntryIsLive(entry));
-      return entry;
+      MOZ_ASSERT(mCurrent.IsLive());
+      return mCurrent.ToEntry();
     }
 
     // Advance to the next entry.
     void Next();
 
     // Remove the current entry. Must only be called once per entry, and Get()
     // must not be called on that entry afterwards.
     void Remove();
 
   protected:
     PLDHashTable* mTable;             // Main table pointer.
 
   private:
-    char* mLimit;                     // One past the last entry.
-    char* mCurrent;                   // Pointer to the current entry.
+    Slot mCurrent;                    // Pointer to the current entry.
     uint32_t mNexts;                  // Number of Next() calls.
     uint32_t mNextsLimit;             // Next() call limit.
 
     bool mHaveRemoved;                // Have any elements been removed?
+    uint8_t mEntrySize;               // Size of entries.
 
     bool IsOnNonLiveEntry() const;
-    void MoveToNextEntry();
+
+    void MoveToNextLiveEntry();
 
     Iterator() = delete;
     Iterator(const Iterator&) = delete;
     Iterator& operator=(const Iterator&) = delete;
     Iterator& operator=(const Iterator&&) = delete;
   };
 
   Iterator Iter() { return Iterator(this); }
@@ -510,65 +640,46 @@ public:
     return Iterator(const_cast<PLDHashTable*>(this));
   }
 
 private:
   static uint32_t HashShift(uint32_t aEntrySize, uint32_t aLength);
 
   static const PLDHashNumber kCollisionFlag = 1;
 
-  static bool EntryIsFree(const PLDHashEntryHdr* aEntry)
-  {
-    return aEntry->mKeyHash == 0;
-  }
-  static bool EntryIsRemoved(const PLDHashEntryHdr* aEntry)
-  {
-    return aEntry->mKeyHash == 1;
-  }
-  static bool EntryIsLive(const PLDHashEntryHdr* aEntry)
-  {
-    return aEntry->mKeyHash >= 2;
-  }
-
-  static void MarkEntryFree(PLDHashEntryHdr* aEntry)
-  {
-    aEntry->mKeyHash = 0;
-  }
-  static void MarkEntryRemoved(PLDHashEntryHdr* aEntry)
-  {
-    aEntry->mKeyHash = 1;
-  }
-
   PLDHashNumber Hash1(PLDHashNumber aHash0) const;
   void Hash2(PLDHashNumber aHash,
              uint32_t& aHash2Out, uint32_t& aSizeMaskOut) const;
 
-  static bool MatchEntryKeyhash(const PLDHashEntryHdr* aEntry,
-                                const PLDHashNumber aHash);
-  PLDHashEntryHdr* AddressEntry(uint32_t aIndex) const;
+  static bool MatchSlotKeyhash(Slot& aSlot, const PLDHashNumber aHash);
+  Slot SlotForIndex(uint32_t aIndex) const;
 
   // We store mHashShift rather than sizeLog2 to optimize the collision-free
   // case in SearchTable.
   uint32_t CapacityFromHashShift() const
   {
     return ((uint32_t)1 << (kPLDHashNumberBits - mHashShift));
   }
 
   PLDHashNumber ComputeKeyHash(const void* aKey) const;
 
   enum SearchReason { ForSearchOrRemove, ForAdd };
 
-  template <SearchReason Reason>
-  PLDHashEntryHdr* NS_FASTCALL
-    SearchTable(const void* aKey, PLDHashNumber aKeyHash) const;
+  // Avoid using bare `Success` and `Failure`, as those names are commonly
+  // defined as macros.
+  template <SearchReason Reason, typename PLDSuccess, typename PLDFailure>
+  auto
+  SearchTable(const void* aKey, PLDHashNumber aKeyHash,
+              PLDSuccess&& aSucess, PLDFailure&& aFailure) const;
 
-  PLDHashEntryHdr* FindFreeEntry(PLDHashNumber aKeyHash) const;
+  Slot FindFreeSlot(PLDHashNumber aKeyHash) const;
 
   bool ChangeTable(int aDeltaLog2);
 
+  void RawRemove(Slot& aSlot);
   void ShrinkIfAppropriate();
 
   PLDHashTable(const PLDHashTable& aOther) = delete;
   PLDHashTable& operator=(const PLDHashTable& aOther) = delete;
 };
 
 // Compute the hash code for a given key to be looked up, added, or removed.
 // A hash code may have any PLDHashNumber value.
@@ -587,40 +698,37 @@ typedef void (*PLDHashMoveEntry)(PLDHash
                                  const PLDHashEntryHdr* aFrom,
                                  PLDHashEntryHdr* aTo);
 
 // Clear the entry and drop any strong references it holds. This callback is
 // invoked by Remove(), but only if the given key is found in the table.
 typedef void (*PLDHashClearEntry)(PLDHashTable* aTable,
                                   PLDHashEntryHdr* aEntry);
 
-// Initialize a new entry, apart from mKeyHash. This function is called when
-// Add() finds no existing entry for the given key, and must add a new one. At
-// that point, |aEntry->mKeyHash| is not set yet, to avoid claiming the last
-// free entry in a severely overloaded table.
+// Initialize a new entry. This function is called when
+// Add() finds no existing entry for the given key, and must add a new one.
 typedef void (*PLDHashInitEntry)(PLDHashEntryHdr* aEntry, const void* aKey);
 
 // Finally, the "vtable" structure for PLDHashTable. The first four hooks
 // must be provided by implementations; they're called unconditionally by the
 // generic PLDHashTable.cpp code. Hooks after these may be null.
 //
 // Summary of allocation-related hook usage with C++ placement new emphasis:
 //  initEntry           Call placement new using default key-based ctor.
 //  moveEntry           Call placement new using copy ctor, run dtor on old
 //                      entry storage.
 //  clearEntry          Run dtor on entry.
 //
 // Note the reason why initEntry is optional: the default hooks (stubs) clear
-// entry storage:  On successful Add(tbl, key), the returned entry pointer
-// addresses an entry struct whose mKeyHash member has been set non-zero, but
-// all other entry members are still clear (null). Add() callers can test such
-// members to see whether the entry was newly created by the Add() call that
-// just succeeded. If placement new or similar initialization is required,
-// define an |initEntry| hook. Of course, the |clearEntry| hook must zero or
-// null appropriately.
+// entry storage. On a successful Add(tbl, key), the returned entry pointer
+// addresses an entry struct whose entry members are still clear (null). Add()
+// callers can test such members to see whether the entry was newly created by
+// the Add() call that just succeeded. If placement new or similar
+// initialization is required, define an |initEntry| hook. Of course, the
+// |clearEntry| hook must zero or null appropriately.
 //
 // XXX assumes 0 is null for pointer types.
 struct PLDHashTableOps
 {
   // Mandatory hooks. All implementations must provide these.
   PLDHashHashKey      hashKey;
   PLDHashMatchEntry   matchEntry;
   PLDHashMoveEntry    moveEntry;