Merge mozilla-central to inbound. a=merge CLOSED TREE
authorshindli <shindli@mozilla.com>
Thu, 06 Dec 2018 06:02:52 +0200
changeset 505943 92080f90bad8
parent 505942 0bbffdf069f1 (current diff)
parent 505935 d932537fec3b (diff)
child 505944 b7b76a72997d
push id10301
push userarchaeopteryx@coole-files.de
push dateThu, 06 Dec 2018 16:36:14 +0000
treeherdermozilla-beta@7d2f3c71997c [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 inbound. a=merge CLOSED TREE
--- a/accessible/android/AccessibleWrap.cpp
+++ b/accessible/android/AccessibleWrap.cpp
@@ -282,17 +282,19 @@ uint32_t AccessibleWrap::GetFlags(role a
 
   if (aRole == roles::PASSWORD_TEXT) {
     flags |= java::SessionAccessibility::FLAG_PASSWORD;
   }
 
   return flags;
 }
 
-void AccessibleWrap::GetRoleDescription(role aRole, nsAString& aGeckoRole,
+void AccessibleWrap::GetRoleDescription(role aRole,
+                                        nsIPersistentProperties* aAttributes,
+                                        nsAString& aGeckoRole,
                                         nsAString& aRoleDescription) {
   nsresult rv = NS_OK;
 
   nsCOMPtr<nsIStringBundleService> sbs =
       do_GetService(NS_STRINGBUNDLE_CONTRACTID, &rv);
   if (NS_FAILED(rv)) {
     NS_WARNING("Failed to get string bundle service");
     return;
@@ -300,16 +302,29 @@ void AccessibleWrap::GetRoleDescription(
 
   nsCOMPtr<nsIStringBundle> bundle;
   rv = sbs->CreateBundle(ROLE_STRINGS_URL, getter_AddRefs(bundle));
   if (NS_FAILED(rv)) {
     NS_WARNING("Failed to get string bundle");
     return;
   }
 
+  if (aRole == roles::HEADING) {
+    nsString level;
+    rv = aAttributes->GetStringProperty(NS_LITERAL_CSTRING("level"), level);
+    if (NS_SUCCEEDED(rv)) {
+      const char16_t* formatString[] = {level.get()};
+      rv = bundle->FormatStringFromName("headingLevel", formatString, 1,
+                                        aRoleDescription);
+      if (NS_SUCCEEDED(rv)) {
+        return;
+      }
+    }
+  }
+
   GetAccService()->GetStringRole(aRole, aGeckoRole);
   rv = bundle->GetStringFromName(NS_ConvertUTF16toUTF8(aGeckoRole).get(),
                                  aRoleDescription);
   if (NS_FAILED(rv)) {
     aRoleDescription.AssignLiteral("");
   }
 }
 
@@ -445,17 +460,17 @@ mozilla::java::GeckoBundle::LocalRef Acc
     GECKOBUNDLE_PUT(nodeInfo, "text", jni::StringParam(aTextValue));
   } else {
     GECKOBUNDLE_PUT(nodeInfo, "text", jni::StringParam(aName));
   }
 
   nsAutoString geckoRole;
   nsAutoString roleDescription;
   if (VirtualViewID() != kNoID) {
-    GetRoleDescription(role, geckoRole, roleDescription);
+    GetRoleDescription(role, aAttributes, geckoRole, roleDescription);
   }
 
   GECKOBUNDLE_PUT(nodeInfo, "roleDescription",
                   jni::StringParam(roleDescription));
   GECKOBUNDLE_PUT(nodeInfo, "geckoRole", jni::StringParam(geckoRole));
 
   GECKOBUNDLE_PUT(nodeInfo, "roleDescription",
                   jni::StringParam(roleDescription));
--- a/accessible/android/AccessibleWrap.h
+++ b/accessible/android/AccessibleWrap.h
@@ -76,18 +76,21 @@ class AccessibleWrap : public Accessible
     return static_cast<AccessibleWrap*>(Parent());
   }
 
   virtual bool WrapperRangeInfo(double* aCurVal, double* aMinVal,
                                 double* aMaxVal, double* aStep);
 
   virtual role WrapperRole() { return Role(); }
 
-  static void GetRoleDescription(role aRole, nsAString& aGeckoRole,
+  static void GetRoleDescription(role aRole,
+                                 nsIPersistentProperties* aAttributes,
+                                 nsAString& aGeckoRole,
                                  nsAString& aRoleDescription);
+
   static uint32_t GetFlags(role aRole, uint64_t aState, uint8_t aActionCount);
 };
 
 static inline AccessibleWrap* WrapperFor(const ProxyAccessible* aProxy) {
   return reinterpret_cast<AccessibleWrap*>(aProxy->GetWrapper());
 }
 
 }  // namespace a11y
--- a/accessible/android/DocAccessibleWrap.cpp
+++ b/accessible/android/DocAccessibleWrap.cpp
@@ -1,14 +1,15 @@
 /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
 /* vim: set ts=2 et sw=2 tw=80: */
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
+#include "Accessible-inl.h"
 #include "DocAccessibleWrap.h"
 #include "nsIDocShell.h"
 #include "nsLayoutUtils.h"
 #include "DocAccessibleChild.h"
 #include "nsAccessibilityService.h"
 #include "nsAccUtils.h"
 #include "nsIPersistentProperties2.h"
 #include "SessionAccessibility.h"
@@ -224,16 +225,21 @@ void DocAccessibleWrap::UpdateFocusPathB
     return;
   }
 
   if (IPCAccessibilityActive()) {
     DocAccessibleChild* ipcDoc = IPCDoc();
     nsTArray<BatchData> boundsData(mFocusPath.Count());
     for (auto iter = mFocusPath.Iter(); !iter.Done(); iter.Next()) {
       Accessible* accessible = iter.Data();
+      if (!accessible || accessible->IsDefunct()) {
+        MOZ_ASSERT_UNREACHABLE("Focus path cached accessible is gone.");
+        continue;
+      }
+
       auto uid = accessible->IsDoc() && accessible->AsDoc()->IPCDoc()
                      ? 0
                      : reinterpret_cast<uint64_t>(accessible->UniqueID());
       boundsData.AppendElement(BatchData(
           accessible->Document()->IPCDoc(), uid, 0, accessible->Bounds(), 0,
           nsString(), nsString(), nsString(), UnspecifiedNaN<double>(),
           UnspecifiedNaN<double>(), UnspecifiedNaN<double>(),
           UnspecifiedNaN<double>(), nsTArray<Attribute>()));
--- a/browser/app/profile/firefox.js
+++ b/browser/app/profile/firefox.js
@@ -1468,19 +1468,19 @@ pref("media.gmp.trial-create.enabled", t
 pref("media.gmp-widevinecdm.visible", true);
 pref("media.gmp-widevinecdm.enabled", true);
 #endif
 
 #ifdef NIGHTLY_BUILD
 // Switch block autoplay logic to v2, and enable UI.
 pref("media.autoplay.enabled.user-gestures-needed", true);
 // Allow asking for permission to autoplay to appear in UI.
-pref("media.autoplay.ask-permission", true);
+pref("media.autoplay.ask-permission", false);
 // Set Firefox to block autoplay, asking for permission by default.
-pref("media.autoplay.default", 2); // 0=Allowed, 1=Blocked, 2=Prompt
+pref("media.autoplay.default", 1); // 0=Allowed, 1=Blocked, 2=Prompt
 #else
 pref("media.autoplay.default", 0); // 0=Allowed, 1=Blocked, 2=Prompt
 pref("media.autoplay.enabled.user-gestures-needed", false);
 pref("media.autoplay.ask-permission", false);
 #endif
 
 
 // Play with different values of the decay time and get telemetry,
@@ -1534,16 +1534,18 @@ pref("dom.storage_access.enabled", true)
 
 pref("dom.storage_access.auto_grants", true);
 pref("dom.storage_access.max_concurrent_auto_grants", 5);
 
 // Define a set of default features for the Content Blocking UI.
 pref("browser.contentblocking.trackingprotection.control-center.ui.enabled", true);
 pref("browser.contentblocking.rejecttrackers.control-center.ui.enabled", true);
 
+pref("browser.contentblocking.control-center.ui.showAllowedLabels", false);
+
 // Enable the Report Breakage UI on Nightly and Beta but not on Release yet.
 #ifdef EARLY_BETA_OR_EARLIER
 pref("browser.contentblocking.reportBreakage.enabled", true);
 #else
 pref("browser.contentblocking.reportBreakage.enabled", false);
 #endif
 // Show report breakage for tracking cookies in all channels.
 pref("browser.contentblocking.rejecttrackers.reportBreakage.enabled", true);
--- a/browser/base/content/browser-contentblocking.js
+++ b/browser/base/content/browser-contentblocking.js
@@ -75,19 +75,19 @@ var TrackingProtection = {
     this.updateCategoryLabel();
   },
 
   updateCategoryLabel() {
     let label;
     if (this.enabled) {
       label = "contentBlocking.trackers.blocked.label";
     } else {
-      label = "contentBlocking.trackers.allowed.label";
+      label = ContentBlocking.showAllowedLabels ? "contentBlocking.trackers.allowed.label" : null;
     }
-    this.categoryLabel.textContent = gNavigatorBundle.getString(label);
+    this.categoryLabel.textContent = label ? gNavigatorBundle.getString(label) : "";
   },
 
   isBlocking(state) {
     return (state & Ci.nsIWebProgressListener.STATE_BLOCKED_TRACKING_CONTENT) != 0;
   },
 
   isAllowing(state) {
     return (state & Ci.nsIWebProgressListener.STATE_LOADED_TRACKING_CONTENT) != 0;
@@ -261,20 +261,20 @@ var ThirdPartyCookies = {
       break;
     case Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER:
       label = "contentBlocking.cookies.trackersBlocked.label";
       break;
     default:
       Cu.reportError(`Error: Unknown cookieBehavior pref observed: ${this.behaviorPref}`);
       // fall through
     case Ci.nsICookieService.BEHAVIOR_ACCEPT:
-      label = "contentBlocking.cookies.allowed.label";
+      label = ContentBlocking.showAllowedLabels ? "contentBlocking.cookies.allowed.label" : null;
       break;
     }
-    this.categoryLabel.textContent = gNavigatorBundle.getString(label);
+    this.categoryLabel.textContent = label ? gNavigatorBundle.getString(label) : "";
   },
 
   init() {
     XPCOMUtils.defineLazyPreferenceGetter(this, "behaviorPref", this.PREF_ENABLED,
       Ci.nsICookieService.BEHAVIOR_ACCEPT, this.updateCategoryLabel.bind(this));
     XPCOMUtils.defineLazyPreferenceGetter(this, "reportBreakageEnabled",
       this.PREF_REPORT_BREAKAGE_ENABLED, false);
     this.updateCategoryLabel();
@@ -483,16 +483,17 @@ var ThirdPartyCookies = {
 var ContentBlocking = {
   // If the user ignores the doorhanger, we stop showing it after some time.
   MAX_INTROS: 20,
   PREF_ANIMATIONS_ENABLED: "toolkit.cosmeticAnimations.enabled",
   PREF_REPORT_BREAKAGE_ENABLED: "browser.contentblocking.reportBreakage.enabled",
   PREF_REPORT_BREAKAGE_URL: "browser.contentblocking.reportBreakage.url",
   PREF_INTRO_COUNT_CB: "browser.contentblocking.introCount",
   PREF_CB_CATEGORY: "browser.contentblocking.category",
+  PREF_SHOW_ALLOWED_LABELS: "browser.contentblocking.control-center.ui.showAllowedLabels",
   content: null,
   icon: null,
   activeTooltipText: null,
   disabledTooltipText: null,
 
   get prefIntroCount() {
     return this.PREF_INTRO_COUNT_CB;
   },
@@ -570,16 +571,22 @@ var ContentBlocking = {
         blocker.init();
       }
     }
 
     this.updateAnimationsEnabled();
 
     Services.prefs.addObserver(this.PREF_ANIMATIONS_ENABLED, this.updateAnimationsEnabled);
 
+    XPCOMUtils.defineLazyPreferenceGetter(this, "showAllowedLabels",
+      this.PREF_SHOW_ALLOWED_LABELS, false, () => {
+        for (let blocker of this.blockers) {
+          blocker.updateCategoryLabel();
+        }
+    });
     XPCOMUtils.defineLazyPreferenceGetter(this, "reportBreakageEnabled",
       this.PREF_REPORT_BREAKAGE_ENABLED, false);
 
     this.appMenuLabel.setAttribute("value", this.strings.appMenuTitle);
     this.appMenuLabel.setAttribute("tooltiptext", this.strings.appMenuTooltip);
 
     this.activeTooltipText =
       gNavigatorBundle.getString("trackingProtection.icon.activeTooltip");
--- a/browser/base/content/test/favicons/browser_oversized.js
+++ b/browser/base/content/test/favicons/browser_oversized.js
@@ -1,16 +1,16 @@
 /* Any copyright is dedicated to the Public Domain.
  * http://creativecommons.org/publicdomain/zero/1.0/ */
 
 const ROOT = "http://mochi.test:8888/browser/browser/base/content/test/favicons/";
 
 add_task(async () => {
   await BrowserTestUtils.withNewTab({ gBrowser, url: "about:blank" }, async (browser) => {
-    let faviconPromise = waitForFaviconMessage();
+    let faviconPromise = waitForFaviconMessage(true, `${ROOT}large.png`);
 
     BrowserTestUtils.loadURI(browser, ROOT + "large_favicon.html");
     await BrowserTestUtils.browserLoaded(browser);
 
     await Assert.rejects(faviconPromise, result => {
       return result.iconURL == `${ROOT}large.png`;
     }, "Should have failed to load the large icon.");
   });
--- a/browser/base/content/test/trackingUI/browser_trackingUI_animation_2.js
+++ b/browser/base/content/test/trackingUI/browser_trackingUI_animation_2.js
@@ -17,33 +17,16 @@ requestLongerTimeout(2);
 registerCleanupFunction(function() {
   UrlClassifierTestUtils.cleanupTestTrackers();
   Services.prefs.clearUserPref(TP_PREF);
   Services.prefs.clearUserPref(TP_PB_PREF);
   Services.prefs.clearUserPref(NCB_PREF);
   Services.prefs.clearUserPref(ContentBlocking.prefIntroCount);
 });
 
-function waitForSecurityChange(tabbrowser, numChanges = 1) {
-  return new Promise(resolve => {
-    let n = 0;
-    let listener = {
-      onSecurityChange() {
-        n = n + 1;
-        info("Received onSecurityChange event " + n + " of " + numChanges);
-        if (n >= numChanges) {
-          tabbrowser.removeProgressListener(listener);
-          resolve(n);
-        }
-      },
-    };
-    tabbrowser.addProgressListener(listener);
-  });
-}
-
 async function testTrackingProtectionAnimation(tabbrowser) {
   info("Load a test page not containing tracking elements");
   let benignTab = await BrowserTestUtils.openNewForegroundTab(tabbrowser, BENIGN_PAGE);
   let ContentBlocking = tabbrowser.ownerGlobal.ContentBlocking;
 
   ok(!ContentBlocking.iconBox.hasAttribute("active"), "iconBox not active");
   ok(!ContentBlocking.iconBox.hasAttribute("animate"), "iconBox not animating");
 
@@ -57,101 +40,101 @@ async function testTrackingProtectionAni
   info("Load a test page containing tracking cookies");
   let trackingCookiesTab = await BrowserTestUtils.openNewForegroundTab(tabbrowser, COOKIE_PAGE);
 
   ok(ContentBlocking.iconBox.hasAttribute("active"), "iconBox active");
   ok(ContentBlocking.iconBox.hasAttribute("animate"), "iconBox animating");
   await BrowserTestUtils.waitForEvent(ContentBlocking.animatedIcon, "animationend");
 
   info("Switch from tracking cookie -> benign tab");
-  let securityChanged = waitForSecurityChange(tabbrowser);
+  let securityChanged = waitForSecurityChange(1, tabbrowser.ownerGlobal);
   tabbrowser.selectedTab = benignTab;
   await securityChanged;
 
   ok(!ContentBlocking.iconBox.hasAttribute("active"), "iconBox not active");
   ok(!ContentBlocking.iconBox.hasAttribute("animate"), "iconBox not animating");
 
   info("Switch from benign -> tracking tab");
-  securityChanged = waitForSecurityChange(tabbrowser);
+  securityChanged = waitForSecurityChange(1, tabbrowser.ownerGlobal);
   tabbrowser.selectedTab = trackingTab;
   await securityChanged;
 
   ok(ContentBlocking.iconBox.hasAttribute("active"), "iconBox active");
   ok(!ContentBlocking.iconBox.hasAttribute("animate"), "iconBox not animating");
 
   info("Switch from tracking -> tracking cookies tab");
-  securityChanged = waitForSecurityChange(tabbrowser);
+  securityChanged = waitForSecurityChange(1, tabbrowser.ownerGlobal);
   tabbrowser.selectedTab = trackingCookiesTab;
   await securityChanged;
 
   ok(ContentBlocking.iconBox.hasAttribute("active"), "iconBox active");
   ok(!ContentBlocking.iconBox.hasAttribute("animate"), "iconBox not animating");
 
   info("Reload tracking cookies tab");
-  securityChanged = waitForSecurityChange(tabbrowser, 3);
+  securityChanged = waitForSecurityChange(3, tabbrowser.ownerGlobal);
   tabbrowser.reload();
   await securityChanged;
 
   ok(ContentBlocking.iconBox.hasAttribute("active"), "iconBox active");
   ok(ContentBlocking.iconBox.hasAttribute("animate"), "iconBox animating");
   await BrowserTestUtils.waitForEvent(ContentBlocking.animatedIcon, "animationend");
 
   info("Reload tracking tab");
-  securityChanged = waitForSecurityChange(tabbrowser, 4);
+  securityChanged = waitForSecurityChange(4, tabbrowser.ownerGlobal);
   tabbrowser.selectedTab = trackingTab;
   tabbrowser.reload();
   await securityChanged;
 
   ok(ContentBlocking.iconBox.hasAttribute("active"), "iconBox active");
   ok(ContentBlocking.iconBox.hasAttribute("animate"), "iconBox animating");
   await BrowserTestUtils.waitForEvent(ContentBlocking.animatedIcon, "animationend");
 
   info("Inject tracking cookie inside tracking tab");
-  securityChanged = waitForSecurityChange(tabbrowser);
+  securityChanged = waitForSecurityChange(1, tabbrowser.ownerGlobal);
   let timeoutPromise = new Promise(resolve => setTimeout(resolve, 500));
   await ContentTask.spawn(tabbrowser.selectedBrowser, {},
                           function() {
     content.postMessage("cookie", "*");
   });
   let result = await Promise.race([securityChanged, timeoutPromise]);
   is(result, undefined, "No securityChange events should be received");
 
   ok(ContentBlocking.iconBox.hasAttribute("active"), "iconBox active");
   ok(!ContentBlocking.iconBox.hasAttribute("animate"), "iconBox not animating");
 
   info("Inject tracking element inside tracking tab");
-  securityChanged = waitForSecurityChange(tabbrowser);
+  securityChanged = waitForSecurityChange(1, tabbrowser.ownerGlobal);
   timeoutPromise = new Promise(resolve => setTimeout(resolve, 500));
   await ContentTask.spawn(tabbrowser.selectedBrowser, {},
                           function() {
     content.postMessage("tracking", "*");
   });
   result = await Promise.race([securityChanged, timeoutPromise]);
   is(result, undefined, "No securityChange events should be received");
 
   ok(ContentBlocking.iconBox.hasAttribute("active"), "iconBox active");
   ok(!ContentBlocking.iconBox.hasAttribute("animate"), "iconBox not animating");
 
   tabbrowser.selectedTab = trackingCookiesTab;
 
   info("Inject tracking cookie inside tracking cookies tab");
-  securityChanged = waitForSecurityChange(tabbrowser);
+  securityChanged = waitForSecurityChange(1, tabbrowser.ownerGlobal);
   timeoutPromise = new Promise(resolve => setTimeout(resolve, 500));
   await ContentTask.spawn(tabbrowser.selectedBrowser, {},
                           function() {
     content.postMessage("cookie", "*");
   });
   result = await Promise.race([securityChanged, timeoutPromise]);
   is(result, undefined, "No securityChange events should be received");
 
   ok(ContentBlocking.iconBox.hasAttribute("active"), "iconBox active");
   ok(!ContentBlocking.iconBox.hasAttribute("animate"), "iconBox not animating");
 
   info("Inject tracking element inside tracking cookies tab");
-  securityChanged = waitForSecurityChange(tabbrowser);
+  securityChanged = waitForSecurityChange(1, tabbrowser.ownerGlobal);
   timeoutPromise = new Promise(resolve => setTimeout(resolve, 500));
   await ContentTask.spawn(tabbrowser.selectedBrowser, {},
                           function() {
     content.postMessage("tracking", "*");
   });
   result = await Promise.race([securityChanged, timeoutPromise]);
   is(result, undefined, "No securityChange events should be received");
 
--- a/browser/base/content/test/trackingUI/browser_trackingUI_categories.js
+++ b/browser/base/content/test/trackingUI/browser_trackingUI_categories.js
@@ -65,16 +65,18 @@ add_task(async function testCategoryLabe
     await TestUtils.waitForCondition(() => appMenuCategoryLabel.value ==
       gNavigatorBundle.getString("contentBlocking.category.custom"));
     is(appMenuCategoryLabel.value, gNavigatorBundle.getString("contentBlocking.category.custom"),
       "The appMenuCategory label has been changed to custom");
   });
 });
 
 add_task(async function testSubcategoryLabels() {
+  SpecialPowers.pushPrefEnv({set: [["browser.contentblocking.control-center.ui.showAllowedLabels", true]]});
+
   await BrowserTestUtils.withNewTab("http://www.example.com", async function() {
     let categoryLabel =
       document.getElementById("identity-popup-content-blocking-tracking-protection-state-label");
 
     Services.prefs.setBoolPref(TP_PREF, true);
     await TestUtils.waitForCondition(() => categoryLabel.textContent ==
       gNavigatorBundle.getString("contentBlocking.trackers.blocked.label"),
       "The category label has updated correctly");
--- a/browser/base/content/test/trackingUI/head.js
+++ b/browser/base/content/test/trackingUI/head.js
@@ -35,24 +35,27 @@ function promiseTabLoadEvent(tab, url) {
 
 function openIdentityPopup() {
   let mainView = document.getElementById("identity-popup-mainView");
   let viewShown = BrowserTestUtils.waitForEvent(mainView, "ViewShown");
   gIdentityHandler._identityBox.click();
   return viewShown;
 }
 
-function waitForSecurityChange(numChanges = 1) {
+function waitForSecurityChange(numChanges = 1, win = null) {
+  if (!win) {
+    win = window;
+  }
   return new Promise(resolve => {
     let n = 0;
     let listener = {
       onSecurityChange() {
         n = n + 1;
         info("Received onSecurityChange event " + n + " of " + numChanges);
         if (n >= numChanges) {
-          gBrowser.removeProgressListener(listener);
+          win.gBrowser.removeProgressListener(listener);
           resolve(n);
         }
       },
     };
-    gBrowser.addProgressListener(listener);
+    win.gBrowser.addProgressListener(listener);
   });
 }
--- a/browser/components/extensions/parent/ext-pageAction.js
+++ b/browser/components/extensions/parent/ext-pageAction.js
@@ -50,18 +50,19 @@ this.pageAction = class extends Extensio
     let show, showMatches, hideMatches;
     let show_matches = options.show_matches || [];
     let hide_matches = options.hide_matches || [];
     if (!show_matches.length) {
       // Always hide by default. No need to do any pattern matching.
       show = false;
     } else {
       // Might show or hide depending on the URL. Enable pattern matching.
-      showMatches = new MatchPatternSet(show_matches);
-      hideMatches = new MatchPatternSet(hide_matches);
+      const {restrictSchemes} = extension;
+      showMatches = new MatchPatternSet(show_matches, {restrictSchemes});
+      hideMatches = new MatchPatternSet(hide_matches, {restrictSchemes});
     }
 
     this.defaults = {
       show,
       showMatches,
       hideMatches,
       title: options.default_title || extension.name,
       popup: options.default_popup || "",
--- a/browser/components/extensions/test/browser/browser_ext_pageAction_show_matches.js
+++ b/browser/components/extensions/test/browser/browser_ext_pageAction_show_matches.js
@@ -170,8 +170,68 @@ add_task(async function test_pageAction_
   info("Check <all_urls> is not allowed in hide_matches");
   let extension = getExtension({
     "show_matches": ["*://mochi.test/*"],
     "hide_matches": ["<all_urls>"],
   });
   let rejects = await extension.startup().then(() => false, () => true);
   is(rejects, true, "startup failed");
 });
+
+add_task(async function test_pageAction_restrictScheme_false() {
+  info("Check restricted origins are allowed in show_matches for privileged extensions");
+  let extension = ExtensionTestUtils.loadExtension({
+    isPrivileged: true,
+    manifest: {
+      permissions: ["mozillaAddons", "tabs"],
+      page_action: {
+        "show_matches": ["about:reader*"],
+        "hide_matches": ["*://*/*"],
+      },
+    },
+    background: function() {
+      browser.tabs.onUpdated.addListener(async (tabId, changeInfo) => {
+        if (changeInfo.url && changeInfo.url.startsWith("about:reader")) {
+          browser.test.sendMessage("readerModeEntered");
+        }
+      });
+
+      browser.test.onMessage.addListener(async (msg) => {
+        if (msg !== "enterReaderMode") {
+          browser.test.fail(`Received unexpected test message: ${msg}`);
+          return;
+        }
+
+        browser.tabs.toggleReaderMode();
+      });
+    },
+  });
+
+  async function expectPageAction(extension, tab, isShown) {
+    await promiseAnimationFrame();
+    let widgetId = makeWidgetId(extension.id);
+    let pageActionId = BrowserPageActions.urlbarButtonNodeIDForActionID(widgetId);
+    let iconEl = document.getElementById(pageActionId);
+
+    if (isShown) {
+      ok(iconEl && !iconEl.hasAttribute("disabled"), "pageAction is shown");
+    } else {
+      ok(iconEl == null || iconEl.getAttribute("disabled") == "true", "pageAction is hidden");
+    }
+  }
+
+  const baseUrl = getRootDirectory(gTestPath).replace("chrome://mochitests/content", "http://example.com");
+  const url = `${baseUrl}/readerModeArticle.html`;
+  let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, url, true, true);
+
+  await extension.startup();
+
+  await expectPageAction(extension, tab, false);
+
+  extension.sendMessage("enterReaderMode");
+  await extension.awaitMessage("readerModeEntered");
+
+  await expectPageAction(extension, tab, true);
+
+  BrowserTestUtils.removeTab(tab);
+
+  await extension.unload();
+});
--- a/browser/extensions/screenshots/manifest.json
+++ b/browser/extensions/screenshots/manifest.json
@@ -31,17 +31,17 @@
     }
   ],
   "page_action": {
     "browser_style": true,
     "default_icon" : {
       "32": "icons/icon-v2.svg"
     },
     "default_title": "__MSG_contextMenuLabel__",
-    "show_matches": ["<all_urls>"],
+    "show_matches": ["<all_urls>", "about:reader*"],
     "pinned": false
   },
   "icons": {
     "32": "icons/icon-v2.svg"
   },
   "web_accessible_resources": [
     "blank.html",
     "icons/cancel.svg",
--- a/devtools/client/inspector/shared/highlighters-overlay.js
+++ b/devtools/client/inspector/shared/highlighters-overlay.js
@@ -65,16 +65,17 @@ class HighlightersOverlay {
     // Name of the highlighter shown on mouse hover.
     this.hoveredHighlighterShown = null;
     // Name of the selector highlighter shown.
     this.selectorHighlighterShown = null;
     // NodeFront of the shape that is highlighted
     this.shapesHighlighterShown = null;
 
     this.onClick = this.onClick.bind(this);
+    this.onDisplayChange = this.onDisplayChange.bind(this);
     this.onMarkupMutation = this.onMarkupMutation.bind(this);
     this.onMouseMove = this.onMouseMove.bind(this);
     this.onMouseOut = this.onMouseOut.bind(this);
     this.onWillNavigate = this.onWillNavigate.bind(this);
     this.hideFlexboxHighlighter = this.hideFlexboxHighlighter.bind(this);
     this.hideFlexItemHighlighter = this.hideFlexItemHighlighter.bind(this);
     this.hideGridHighlighter = this.hideGridHighlighter.bind(this);
     this.hideShapesHighlighter = this.hideShapesHighlighter.bind(this);
@@ -83,16 +84,17 @@ class HighlightersOverlay {
     this.showGridHighlighter = this.showGridHighlighter.bind(this);
     this.showShapesHighlighter = this.showShapesHighlighter.bind(this);
     this._handleRejection = this._handleRejection.bind(this);
     this.onShapesHighlighterShown = this.onShapesHighlighterShown.bind(this);
     this.onShapesHighlighterHidden = this.onShapesHighlighterHidden.bind(this);
 
     // Add inspector events, not specific to a given view.
     this.inspector.on("markupmutation", this.onMarkupMutation);
+    this.inspector.walker.on("display-change", this.onDisplayChange);
     this.inspector.target.on("will-navigate", this.onWillNavigate);
 
     EventEmitter.decorate(this);
   }
 
   /**
    * Returns true if the grid highlighter can be toggled on/off for the given node, and
    * false otherwise. A grid container can be toggled on if the max grid highlighters
@@ -1033,16 +1035,45 @@ class HighlightersOverlay {
 
       this.toggleShapesHighlighter(this.inspector.selection.nodeFront, {
         mode: event.target.dataset.mode,
         transformMode: event.metaKey || event.ctrlKey,
       }, nodeInfo.value.textProperty);
     }
   }
 
+  /**
+   * Handler for "display-change" events from the walker. Hides the flexbox or
+   * grid highlighter if their respective node is no longer a flex container or
+   * grid container.
+   *
+   * @param  {Array} nodes
+   *         An array of nodeFronts
+   */
+  async onDisplayChange(nodes) {
+    for (const node of nodes) {
+      const display = node.displayType;
+
+      // Hide the flexbox highlighter if the node is no longer a flexbox
+      // container.
+      if (display !== "flex" && display !== "inline-flex" &&
+          node == this.flexboxHighlighterShown) {
+        await this.hideFlexboxHighlighter(node);
+        return;
+      }
+
+      // Hide the grid highlighter if the node is no longer a grid container.
+      if (display !== "grid" && display !== "inline-grid" &&
+          this.gridHighlighters.has(node)) {
+        await this.hideGridHighlighter(node);
+        return;
+      }
+    }
+  }
+
   onMouseMove(event) {
     // Bail out if the target is the same as for the last mousemove.
     if (event.target === this._lastHovered) {
       return;
     }
 
     // Only one highlighter can be displayed at a time, hide the currently shown.
     this._hideHoveredHighlighter();
--- a/devtools/client/preferences/devtools-client.js
+++ b/devtools/client/preferences/devtools-client.js
@@ -272,22 +272,18 @@ pref("devtools.webconsole.timestampMessa
 pref("devtools.webconsole.sidebarToggle", true);
 #else
 pref("devtools.webconsole.sidebarToggle", false);
 #endif
 
 // Enable CodeMirror in the JsTerm
 pref("devtools.webconsole.jsterm.codeMirror", true);
 
-// Enable console input reverse-search in Nightly builds
-#if defined(NIGHTLY_BUILD)
+// Enable console input reverse-search everywhere
 pref("devtools.webconsole.jsterm.reverse-search", true);
-#else
-pref("devtools.webconsole.jsterm.reverse-search", false);
-#endif
 
 // Disable the new performance recording panel by default
 pref("devtools.performance.new-panel-enabled", false);
 
 // Enable client-side mapping service for source maps
 pref("devtools.source-map.client-service.enabled", true);
 
 // The number of lines that are displayed in the web console.
--- a/devtools/server/actors/highlighters/flexbox.js
+++ b/devtools/server/actors/highlighters/flexbox.js
@@ -752,28 +752,16 @@ class FlexboxHighlighter extends AutoRef
           break;
       }
     }
 
     this.ctx.restore();
   }
 
   _update() {
-    // If this.currentNode is not a flex container we have nothing to highlight.
-    // We can't simply use getAsFlexContainer() here because this fails for
-    // text fields. This will be removed by https://bugzil.la/1509460.
-    if (!this.computedStyle) {
-      this.computedStyle = getComputedStyle(this.currentNode);
-    }
-
-    if (this.computedStyle.display !== "flex" &&
-        this.computedStyle.display !== "inline-flex") {
-      return false;
-    }
-
     setIgnoreLayoutChanges(true);
 
     const root = this.getElement("root");
 
     // Hide the root element and force the reflow in order to get the proper window's
     // dimensions without increasing them.
     root.setAttribute("style", "display: none");
     this.win.document.documentElement.offsetWidth;
--- a/dom/base/nsDOMWindowUtils.cpp
+++ b/dom/base/nsDOMWindowUtils.cpp
@@ -555,17 +555,17 @@ nsDOMWindowUtils::SetResolution(float aR
 
 NS_IMETHODIMP
 nsDOMWindowUtils::SetResolutionAndScaleTo(float aResolution) {
   nsIPresShell* presShell = GetPresShell();
   if (!presShell) {
     return NS_ERROR_FAILURE;
   }
 
-  presShell->SetResolutionAndScaleTo(aResolution);
+  presShell->SetResolutionAndScaleTo(aResolution, nsGkAtoms::other);
 
   return NS_OK;
 }
 
 NS_IMETHODIMP
 nsDOMWindowUtils::SetRestoreResolution(float aResolution,
                                        uint32_t aDisplayWidth,
                                        uint32_t aDisplayHeight) {
--- a/dom/base/nsDocument.cpp
+++ b/dom/base/nsDocument.cpp
@@ -10183,17 +10183,17 @@ void nsIDocument::CleanupFullscreenState
     }
   }
   mFullscreenStack.Clear();
   mFullscreenRoot = nullptr;
 
   // Restore the zoom level that was in place prior to entering fullscreen.
   if (nsIPresShell* shell = GetShell()) {
     if (shell->GetMobileViewportManager()) {
-      shell->SetResolutionAndScaleTo(mSavedResolution);
+      shell->SetResolutionAndScaleTo(mSavedResolution, nsGkAtoms::restore);
     }
   }
 
   UpdateViewportScrollbarOverrideForFullscreen(this);
 }
 
 bool nsIDocument::FullscreenStackPush(Element* aElement) {
   NS_ASSERTION(aElement, "Must pass non-null to FullscreenStackPush()");
@@ -10580,17 +10580,18 @@ bool nsIDocument::ApplyFullscreen(Unique
     // fixed elements are sized to the layout viewport).
     // This also ensures that things like video controls aren't zoomed in
     // when in fullscreen mode.
     if (nsIPresShell* shell = child->GetShell()) {
       if (RefPtr<MobileViewportManager> manager =
               shell->GetMobileViewportManager()) {
         // Save the previous resolution so it can be restored.
         child->mSavedResolution = shell->GetResolution();
-        shell->SetResolutionAndScaleTo(manager->ComputeIntrinsicResolution());
+        shell->SetResolutionAndScaleTo(manager->ComputeIntrinsicResolution(),
+                                       nsGkAtoms::other);
       }
     }
 
     NS_ASSERTION(child->GetFullscreenRoot() == fullScreenRootDoc,
                  "Fullscreen root should be set!");
     if (child == fullScreenRootDoc) {
       break;
     }
--- a/dom/media/MediaManager.cpp
+++ b/dom/media/MediaManager.cpp
@@ -1118,18 +1118,24 @@ class GetUserMediaStreamRunnable : publi
           mWindowID(aWindowID),
           mGraph(aTrack->GraphImpl()),
           mStream(new nsMainThreadPtrHolder<DOMMediaStream>(
               "TracksCreatedListener::mStream", aStream)),
           mTrack(new nsMainThreadPtrHolder<MediaStreamTrack>(
               "TracksCreatedListener::mTrack", aTrack)) {}
 
     ~TracksCreatedListener() {
-      mHolder.RejectIfExists(
-          MakeRefPtr<MediaMgrError>(MediaMgrError::Name::AbortError), __func__);
+      RejectIfExists(MakeRefPtr<MediaMgrError>(MediaMgrError::Name::AbortError),
+                     __func__);
+    }
+
+    // TODO: The need for this should be removed by an upcoming refactor.
+    void RejectIfExists(RefPtr<MediaMgrError>&& aError,
+                        const char* aMethodName) {
+      mHolder.RejectIfExists(std::move(aError), aMethodName);
     }
 
     void NotifyOutput(MediaStreamGraph* aGraph,
                       StreamTime aCurrentTrackTime) override {
       // It's enough to know that one of the tracks have output, as both tracks
       // are guaranteed to be created in the graph at this point.
 
       if (mDispatchedTracksCreated) {
@@ -1379,30 +1385,30 @@ class GetUserMediaStreamRunnable : publi
               ("GetUserMediaStreamRunnable::Run: starting success callback "
                "following InitializeAsync()"));
           // Initiating and starting devices succeeded.
           track->AddListener(tracksCreatedListener);
           windowListener->ChromeAffectingStateChanged();
           manager->SendPendingGUMRequest();
         },
         [manager = mManager, windowID = mWindowID,
-         holder = std::move(mHolder)](RefPtr<MediaMgrError>&& aError) mutable {
+         tracksCreatedListener](RefPtr<MediaMgrError>&& aError) {
           MOZ_ASSERT(NS_IsMainThread());
           LOG(
               ("GetUserMediaStreamRunnable::Run: starting failure callback "
                "following InitializeAsync()"));
           // Initiating and starting devices failed.
 
           // Only run if the window is still active for our window listener.
           if (!(manager->IsWindowStillActive(windowID))) {
             return;
           }
           // This is safe since we're on main-thread, and the windowlist can
           // only be invalidated from the main-thread (see OnNavigation)
-          holder.Reject(std::move(aError), __func__);
+          tracksCreatedListener->RejectIfExists(std::move(aError), __func__);
         });
 
     if (!IsPincipalInfoPrivate(mPrincipalInfo)) {
       // Call GetPrincipalKey again, this time w/persist = true, to promote
       // deviceIds to persistent, in case they're not already. Fire'n'forget.
       media::GetPrincipalKey(mPrincipalInfo, true)
           ->Then(GetCurrentThreadSerialEventTarget(), __func__,
                  [](const PrincipalKeyPromise::ResolveOrRejectValue& aValue) {
--- a/gfx/layers/FrameMetrics.h
+++ b/gfx/layers/FrameMetrics.h
@@ -807,31 +807,33 @@ struct ScrollMetadata {
         mLineScrollAmount(0, 0),
         mPageScrollAmount(0, 0),
         mScrollClip(),
         mHasScrollgrab(false),
         mIsLayersIdRoot(false),
         mIsAutoDirRootContentRTL(false),
         mUsesContainerScrolling(false),
         mForceDisableApz(false),
+        mResolutionUpdated(false),
         mOverscrollBehavior() {}
 
   bool operator==(const ScrollMetadata& aOther) const {
     return mMetrics == aOther.mMetrics && mSnapInfo == aOther.mSnapInfo &&
            mScrollParentId == aOther.mScrollParentId &&
            mBackgroundColor == aOther.mBackgroundColor &&
            // don't compare mContentDescription
            mLineScrollAmount == aOther.mLineScrollAmount &&
            mPageScrollAmount == aOther.mPageScrollAmount &&
            mScrollClip == aOther.mScrollClip &&
            mHasScrollgrab == aOther.mHasScrollgrab &&
            mIsLayersIdRoot == aOther.mIsLayersIdRoot &&
            mIsAutoDirRootContentRTL == aOther.mIsAutoDirRootContentRTL &&
            mUsesContainerScrolling == aOther.mUsesContainerScrolling &&
            mForceDisableApz == aOther.mForceDisableApz &&
+           mResolutionUpdated == aOther.mResolutionUpdated &&
            mDisregardedDirection == aOther.mDisregardedDirection &&
            mOverscrollBehavior == aOther.mOverscrollBehavior;
   }
 
   bool operator!=(const ScrollMetadata& aOther) const {
     return !operator==(aOther);
   }
 
@@ -902,16 +904,18 @@ struct ScrollMetadata {
   // Implemented out of line because the implementation needs gfxPrefs.h
   // and we don't want to include that from FrameMetrics.h.
   void SetUsesContainerScrolling(bool aValue);
   bool UsesContainerScrolling() const { return mUsesContainerScrolling; }
   void SetForceDisableApz(bool aForceDisable) {
     mForceDisableApz = aForceDisable;
   }
   bool IsApzForceDisabled() const { return mForceDisableApz; }
+  void SetResolutionUpdated(bool aUpdated) { mResolutionUpdated = aUpdated; }
+  bool IsResolutionUpdated() const { return mResolutionUpdated; }
 
   // For more details about the concept of a disregarded direction, refer to the
   // code which defines mDisregardedDirection.
   Maybe<ScrollDirection> GetDisregardedDirection() const {
     return mDisregardedDirection;
   }
   void SetDisregardedDirection(const Maybe<ScrollDirection>& aValue) {
     mDisregardedDirection = aValue;
@@ -978,16 +982,21 @@ struct ScrollMetadata {
   // True if scrolling using containers, false otherwise. This can be removed
   // when containerful scrolling is eliminated.
   bool mUsesContainerScrolling : 1;
 
   // Whether or not the compositor should actually do APZ-scrolling on this
   // scrollframe.
   bool mForceDisableApz : 1;
 
+  // Whether the pres shell resolution stored in mMetrics reflects a change
+  // originated by the main thread. Plays a similar role for the resolution as
+  // FrameMetrics::mScrollUpdateType) does for the scroll offset.
+  bool mResolutionUpdated : 1;
+
   // The disregarded direction means the direction which is disregarded anyway,
   // even if the scroll frame overflows in that direction and the direction is
   // specified as scrollable. This could happen in some scenarios, for instance,
   // a single-line text control frame should disregard wheel scroll in
   // its block-flow direction even if it overflows in that direction.
   Maybe<ScrollDirection> mDisregardedDirection;
 
   // The overscroll behavior for this scroll frame.
--- a/gfx/layers/apz/src/AsyncPanZoomController.cpp
+++ b/gfx/layers/apz/src/AsyncPanZoomController.cpp
@@ -4413,21 +4413,23 @@ void AsyncPanZoomController::NotifyLayer
                this);
       needContentRepaint = true;
     }
   } else {
     // If we're not taking the aLayerMetrics wholesale we still need to pull
     // in some things into our local Metrics() because these things are
     // determined by Gecko and our copy in Metrics() may be stale.
 
+    // TODO: Rely entirely on |aScrollMetadata.IsResolutionUpdated()| to
+    //       determine which branch to take, and drop the other conditions.
     if (FuzzyEqualsAdditive(Metrics().GetCompositionBounds().Width(),
                             aLayerMetrics.GetCompositionBounds().Width()) &&
         Metrics().GetDevPixelsPerCSSPixel() ==
             aLayerMetrics.GetDevPixelsPerCSSPixel() &&
-        !viewportUpdated) {
+        !viewportUpdated && !aScrollMetadata.IsResolutionUpdated()) {
       // Any change to the pres shell resolution was requested by APZ and is
       // already included in our zoom; however, other components of the
       // cumulative resolution (a parent document's pres-shell resolution, or
       // the css-driven resolution) may have changed, and we need to update
       // our zoom to reflect that. Note that we can't just take
       // aLayerMetrics.mZoom because the APZ may have additional async zoom
       // since the repaint request.
       gfxSize totalResolutionChange = aLayerMetrics.GetCumulativeResolution() /
--- a/gfx/layers/apz/util/APZCCallbackHelper.cpp
+++ b/gfx/layers/apz/util/APZCCallbackHelper.cpp
@@ -312,17 +312,17 @@ void APZCCallbackHelper::UpdateRootFrame
                                    aRequest.GetPresShellResolution())) {
       return;
     }
 
     // The pres shell resolution is updated by the the async zoom since the
     // last paint.
     presShellResolution =
         aRequest.GetPresShellResolution() * aRequest.GetAsyncZoom().scale;
-    shell->SetResolutionAndScaleTo(presShellResolution);
+    shell->SetResolutionAndScaleTo(presShellResolution, nsGkAtoms::apz);
   }
 
   // Do this as late as possible since scrolling can flush layout. It also
   // adjusts the display port margins, so do it before we set those.
   ScreenMargin displayPortMargins = ScrollFrame(content, aRequest);
 
   SetDisplayPortMargins(shell, content, displayPortMargins,
                         aRequest.CalculateCompositedSizeInCssPixels());
--- a/gfx/layers/ipc/LayersMessageUtils.h
+++ b/gfx/layers/ipc/LayersMessageUtils.h
@@ -334,16 +334,17 @@ struct ParamTraits<mozilla::layers::Scro
     WriteParam(aMsg, aParam.mLineScrollAmount);
     WriteParam(aMsg, aParam.mPageScrollAmount);
     WriteParam(aMsg, aParam.mScrollClip);
     WriteParam(aMsg, aParam.mHasScrollgrab);
     WriteParam(aMsg, aParam.mIsLayersIdRoot);
     WriteParam(aMsg, aParam.mIsAutoDirRootContentRTL);
     WriteParam(aMsg, aParam.mUsesContainerScrolling);
     WriteParam(aMsg, aParam.mForceDisableApz);
+    WriteParam(aMsg, aParam.mResolutionUpdated);
     WriteParam(aMsg, aParam.mDisregardedDirection);
     WriteParam(aMsg, aParam.mOverscrollBehavior);
   }
 
   static bool ReadContentDescription(const Message* aMsg, PickleIterator* aIter,
                                      paramType* aResult) {
     nsCString str;
     if (!ReadParam(aMsg, aIter, &str)) {
@@ -368,16 +369,18 @@ struct ParamTraits<mozilla::layers::Scro
             ReadBoolForBitfield(aMsg, aIter, aResult,
                                 &paramType::SetIsLayersIdRoot) &&
             ReadBoolForBitfield(aMsg, aIter, aResult,
                                 &paramType::SetIsAutoDirRootContentRTL) &&
             ReadBoolForBitfield(aMsg, aIter, aResult,
                                 &paramType::SetUsesContainerScrolling) &&
             ReadBoolForBitfield(aMsg, aIter, aResult,
                                 &paramType::SetForceDisableApz) &&
+            ReadBoolForBitfield(aMsg, aIter, aResult,
+                                &paramType::SetResolutionUpdated) &&
             ReadParam(aMsg, aIter, &aResult->mDisregardedDirection) &&
             ReadParam(aMsg, aIter, &aResult->mOverscrollBehavior));
   }
 };
 
 template <>
 struct ParamTraits<mozilla::layers::TextureFactoryIdentifier> {
   typedef mozilla::layers::TextureFactoryIdentifier paramType;
--- a/layout/base/MobileViewportManager.cpp
+++ b/layout/base/MobileViewportManager.cpp
@@ -324,17 +324,17 @@ void MobileViewportManager::UpdateResolu
       }
     }
   }
 
   // If the zoom has changed, update the pres shell resolution accordingly.
   if (newZoom) {
     LayoutDeviceToLayerScale resolution = ZoomToResolution(*newZoom, cssToDev);
     MVM_LOG("%p: setting resolution %f\n", this, resolution.scale);
-    mPresShell->SetResolutionAndScaleTo(resolution.scale);
+    mPresShell->SetResolutionAndScaleTo(resolution.scale, nsGkAtoms::other);
 
     MVM_LOG("%p: New zoom is %f\n", this, newZoom->scale);
   }
 
   // The visual viewport size depends on both the zoom and the display size,
   // and needs to be updated if either might have changed.
   if (newZoom || aType == UpdateType::ViewportSize) {
     UpdateVisualViewportSize(aDisplaySize, newZoom ? *newZoom : zoom);
--- a/layout/base/PresShell.cpp
+++ b/layout/base/PresShell.cpp
@@ -7,17 +7,16 @@
 /* a presentation of a document, part 2 */
 
 #include "mozilla/PresShell.h"
 
 #include "mozilla/dom/FontFaceSet.h"
 #include "mozilla/ArrayUtils.h"
 #include "mozilla/Attributes.h"
 #include "mozilla/AutoRestore.h"
-#include "mozilla/StyleSheetInlines.h"
 #include "mozilla/EventDispatcher.h"
 #include "mozilla/EventStateManager.h"
 #include "mozilla/EventStates.h"
 #include "mozilla/IMEStateManager.h"
 #include "mozilla/MemoryReporting.h"
 #include "mozilla/dom/TabChild.h"
 #include "mozilla/Likely.h"
 #include "mozilla/Logging.h"
@@ -776,16 +775,17 @@ PresShell::PresShell()
       mApproximateFrameVisibilityVisited(false),
       mNextPaintCompressed(false),
       mHasCSSBackgroundColor(false),
       mScaleToResolution(false),
       mIsLastChromeOnlyEscapeKeyConsumed(false),
       mHasReceivedPaintMessage(false),
       mIsLastKeyDownCanceled(false),
       mHasHandledUserInput(false),
+      mResolutionUpdated(false),
       mForceDispatchKeyPressEventsForNonPrintableKeys(false),
       mForceUseLegacyKeyCodeAndCharCodeValues(false),
       mInitializedWithKeyPressEventDispatchingBlacklist(false) {
   MOZ_LOG(gLog, LogLevel::Debug, ("PresShell::PresShell this=%p", this));
 
 #ifdef MOZ_REFLOW_PERF
   mReflowCountMgr = MakeUnique<ReflowCountMgr>();
   mReflowCountMgr->SetPresContext(mPresContext);
@@ -5101,31 +5101,35 @@ void PresShell::SetIgnoreViewportScrolli
   }
   RenderingState state(this);
   state.mRenderFlags = ChangeFlag(state.mRenderFlags, aIgnore,
                                   STATE_IGNORING_VIEWPORT_SCROLLING);
   SetRenderingState(state);
 }
 
 nsresult PresShell::SetResolutionImpl(float aResolution,
-                                      bool aScaleToResolution) {
+                                      bool aScaleToResolution,
+                                      nsAtom* aOrigin) {
   if (!(aResolution > 0.0)) {
     return NS_ERROR_ILLEGAL_VALUE;
   }
   if (aResolution == mResolution.valueOr(0.0)) {
     MOZ_ASSERT(mResolution.isSome());
     return NS_OK;
   }
   RenderingState state(this);
   state.mResolution = Some(aResolution);
   SetRenderingState(state);
   mScaleToResolution = aScaleToResolution;
   if (mMobileViewportManager) {
     mMobileViewportManager->ResolutionUpdated();
   }
+  if (aOrigin != nsGkAtoms::apz) {
+    mResolutionUpdated = true;
+  }
 
   return NS_OK;
 }
 
 bool PresShell::ScaleToResolution() const { return mScaleToResolution; }
 
 float PresShell::GetCumulativeResolution() {
   float resolution = GetResolution();
--- a/layout/base/PresShell.h
+++ b/layout/base/PresShell.h
@@ -190,22 +190,29 @@ class PresShell final : public nsIPresSh
 
   LayerManager* GetLayerManager() override;
 
   bool AsyncPanZoomEnabled() override;
 
   void SetIgnoreViewportScrolling(bool aIgnore) override;
 
   nsresult SetResolution(float aResolution) override {
-    return SetResolutionImpl(aResolution, /* aScaleToResolution = */ false);
+    return SetResolutionImpl(aResolution, /* aScaleToResolution = */ false,
+                             nsGkAtoms::other);
   }
-  nsresult SetResolutionAndScaleTo(float aResolution) override {
-    return SetResolutionImpl(aResolution, /* aScaleToResolution = */ true);
+  nsresult SetResolutionAndScaleTo(float aResolution,
+                                   nsAtom* aOrigin) override {
+    return SetResolutionImpl(aResolution, /* aScaleToResolution = */ true,
+                             aOrigin);
   }
   bool ScaleToResolution() const override;
+  bool IsResolutionUpdated() const override { return mResolutionUpdated; }
+  void SetResolutionUpdated(bool aUpdated) override {
+    mResolutionUpdated = aUpdated;
+  }
   float GetCumulativeResolution() override;
   float GetCumulativeNonRootScaleResolution() override;
   void SetRestoreResolution(float aResolution,
                             LayoutDeviceIntSize aDisplaySize) override;
 
   // nsIViewObserver interface
 
   void Paint(nsView* aViewToPaint, const nsRegion& aDirtyRegion,
@@ -706,17 +713,18 @@ class PresShell final : public nsIPresSh
 
   nsRevocableEventPtr<nsRunnableMethod<PresShell>>
       mUpdateApproximateFrameVisibilityEvent;
 
   // A set of frames that were visible or could be visible soon at the time
   // that we last did an approximate frame visibility update.
   VisibleFrames mApproximatelyVisibleFrames;
 
-  nsresult SetResolutionImpl(float aResolution, bool aScaleToResolution);
+  nsresult SetResolutionImpl(float aResolution, bool aScaleToResolution,
+                             nsAtom* aOrigin);
 
   nsIContent* GetOverrideClickTarget(WidgetGUIEvent* aEvent, nsIFrame* aFrame);
 #ifdef DEBUG
   // The reflow root under which we're currently reflowing.  Null when
   // not in reflow.
   nsIFrame* mCurrentReflowRoot;
 #endif
 
@@ -823,16 +831,20 @@ class PresShell final : public nsIPresSh
   // Whether the widget has received a paint message yet.
   bool mHasReceivedPaintMessage : 1;
 
   bool mIsLastKeyDownCanceled : 1;
 
   // Whether we have ever handled a user input event
   bool mHasHandledUserInput : 1;
 
+  // Whether the most recent change to the pres shell resolution was
+  // originated by the main thread.
+  bool mResolutionUpdated : 1;
+
   // Whether we should dispatch keypress events even for non-printable keys
   // for keeping backward compatibility.
   bool mForceDispatchKeyPressEventsForNonPrintableKeys : 1;
   // Whether we should set keyCode or charCode value of keypress events whose
   // value is zero to the other value or not.  When this is set to true, we
   // should keep using legacy keyCode and charCode values (i.e., one of them
   // is always 0).
   bool mForceUseLegacyKeyCodeAndCharCodeValues : 1;
--- a/layout/base/RestyleManager.cpp
+++ b/layout/base/RestyleManager.cpp
@@ -18,17 +18,16 @@
 #include "mozilla/ServoStyleSetInlines.h"
 #include "mozilla/Unused.h"
 #include "mozilla/ViewportFrame.h"
 #include "mozilla/dom/ChildIterator.h"
 #include "mozilla/dom/ElementInlines.h"
 #include "mozilla/dom/HTMLBodyElement.h"
 
 #include "Layers.h"
-#include "LayerAnimationInfo.h"  // For LayerAnimationInfo::sRecords
 #include "nsAnimationManager.h"
 #include "nsBlockFrame.h"
 #include "nsBulletFrame.h"
 #include "nsContentUtils.h"
 #include "nsCSSFrameConstructor.h"
 #include "nsCSSRendering.h"
 #include "nsIDocumentInlines.h"
 #include "nsIFrame.h"
@@ -39,17 +38,16 @@
 #include "nsPrintfCString.h"
 #include "nsRefreshDriver.h"
 #include "nsStyleChangeList.h"
 #include "nsStyleUtil.h"
 #include "nsTransitionManager.h"
 #include "StickyScrollContainer.h"
 #include "mozilla/EffectSet.h"
 #include "mozilla/IntegerRange.h"
-#include "mozilla/ViewportFrame.h"
 #include "SVGObserverUtils.h"
 #include "SVGTextFrame.h"
 #include "ActiveLayerTracker.h"
 #include "nsSVGIntegrationUtils.h"
 
 #ifdef ACCESSIBILITY
 #include "nsAccessibilityService.h"
 #endif
--- a/layout/base/nsCSSFrameConstructor.cpp
+++ b/layout/base/nsCSSFrameConstructor.cpp
@@ -99,17 +99,16 @@
 #include "nsGfxScrollFrame.h"
 #include "nsPageFrame.h"
 #include "nsSimplePageSequenceFrame.h"
 #include "nsTableWrapperFrame.h"
 #include "nsIScrollableFrame.h"
 #include "nsBackdropFrame.h"
 #include "nsTransitionManager.h"
 #include "DetailsFrame.h"
-#include "nsStyleConsts.h"
 
 #ifdef MOZ_XUL
 #include "nsIPopupContainer.h"
 #endif
 #ifdef ACCESSIBILITY
 #include "nsAccessibilityService.h"
 #endif
 
--- a/layout/base/nsDocumentViewer.cpp
+++ b/layout/base/nsDocumentViewer.cpp
@@ -44,17 +44,16 @@
 #include "mozilla/StyleSheetInlines.h"
 
 #include "nsViewManager.h"
 #include "nsView.h"
 
 #include "nsIPageSequenceFrame.h"
 #include "nsNetUtil.h"
 #include "nsIContentViewerEdit.h"
-#include "mozilla/StyleSheetInlines.h"
 #include "mozilla/css/Loader.h"
 #include "nsIInterfaceRequestor.h"
 #include "nsIInterfaceRequestorUtils.h"
 #include "nsDocShell.h"
 #include "nsIBaseWindow.h"
 #include "nsILayoutHistoryState.h"
 #include "nsCharsetSource.h"
 #include "mozilla/ReflowInput.h"
--- a/layout/base/nsIPresShell.h
+++ b/layout/base/nsIPresShell.h
@@ -1367,31 +1367,43 @@ class nsIPresShell : public nsStubDocume
    *
    * The resolution defaults to 1.0.
    */
   virtual nsresult SetResolution(float aResolution) = 0;
   float GetResolution() const { return mResolution.valueOr(1.0); }
   virtual float GetCumulativeResolution() = 0;
 
   /**
+   * Accessors for a flag that tracks whether the most recent change to
+   * the pres shell's resolution was originated by the main thread.
+   */
+  virtual bool IsResolutionUpdated() const = 0;
+  virtual void SetResolutionUpdated(bool aUpdated) = 0;
+
+  /**
    * Calculate the cumulative scale resolution from this document up to
    * but not including the root document.
    */
   virtual float GetCumulativeNonRootScaleResolution() = 0;
 
   /**
    * Was the current resolution set by the user or just default initialized?
    */
   bool IsResolutionSet() { return mResolution.isSome(); }
 
   /**
    * Similar to SetResolution() but also increases the scale of the content
    * by the same amount.
+   * |aOrigin| specifies who originated the resolution change. For changes
+   * sent by APZ, pass nsGkAtoms::apz. For changes sent by the main thread,
+   * use pass nsGkAtoms::other or nsGkAtoms::restore (similar to the |aOrigin|
+   * parameter of nsIScrollableFrame::ScrollToCSSPixels()).
    */
-  virtual nsresult SetResolutionAndScaleTo(float aResolution) = 0;
+  virtual nsresult SetResolutionAndScaleTo(float aResolution,
+                                           nsAtom* aOrigin) = 0;
 
   /**
    * Return whether we are scaling to the set resolution.
    * This is initially false; it's set to true by a call to
    * SetResolutionAndScaleTo(), and set to false by a call to SetResolution().
    */
   virtual bool ScaleToResolution() const = 0;
 
--- a/layout/base/nsLayoutUtils.cpp
+++ b/layout/base/nsLayoutUtils.cpp
@@ -46,17 +46,16 @@
 #include "nsCSSFrameConstructor.h"
 #include "nsBlockFrame.h"
 #include "nsBidiPresUtils.h"
 #include "imgIContainer.h"
 #include "ImageOps.h"
 #include "ImageRegion.h"
 #include "gfxRect.h"
 #include "gfxContext.h"
-#include "gfxContext.h"
 #include "nsIInterfaceRequestorUtils.h"
 #include "nsCSSRendering.h"
 #include "nsTextFragment.h"
 #include "nsStyleConsts.h"
 #include "nsPIDOMWindow.h"
 #include "nsIDocShell.h"
 #include "nsIWidget.h"
 #include "gfxMatrix.h"
@@ -112,17 +111,16 @@
 #include "nsIContentViewer.h"
 #include "LayersLogging.h"
 #include "mozilla/Preferences.h"
 #include "nsFrameSelection.h"
 #include "FrameLayerBuilder.h"
 #include "mozilla/layers/APZUtils.h"  // for apz::CalculatePendingDisplayPort
 #include "mozilla/layers/CompositorBridgeChild.h"
 #include "mozilla/Telemetry.h"
-#include "mozilla/EventDispatcher.h"
 #include "mozilla/StyleAnimationValue.h"
 #include "mozilla/ServoStyleSet.h"
 #include "mozilla/WheelHandlingHelper.h"  // for WheelHandlingUtils
 #include "RegionBuilder.h"
 #include "SVGViewportElement.h"
 #include "DisplayItemClip.h"
 #include "mozilla/layers/StackingContextHelper.h"
 #include "mozilla/layers/WebRenderLayerManager.h"
@@ -8815,16 +8813,23 @@ static void MaybeReflowForInflationScree
 
   // Only the root scrollable frame for a given presShell should pick up
   // the presShell's resolution. All the other frames are 1.0.
   if (isRootScrollFrame) {
     metrics.SetPresShellResolution(presShell->GetResolution());
   } else {
     metrics.SetPresShellResolution(1.0f);
   }
+
+  if (presShell->IsResolutionUpdated()) {
+    metadata.SetResolutionUpdated(true);
+    // We only need to tell APZ about the resolution update once.
+    presShell->SetResolutionUpdated(false);
+  }
+
   // The cumulative resolution is the resolution at which the scroll frame's
   // content is actually rendered. It includes the pres shell resolutions of
   // all the pres shells from here up to the root, as well as any css-driven
   // resolution. We don't need to compute it as it's already stored in the
   // container parameters... except if we're in WebRender in which case we
   // don't have a aContainerParameters. In that case we're also not rasterizing
   // in Gecko anyway, so the only resolution we care about here is the presShell
   // resolution which we need to propagate to WebRender.
--- a/layout/base/nsLayoutUtils.h
+++ b/layout/base/nsLayoutUtils.h
@@ -27,17 +27,16 @@
 #include "nsGkAtoms.h"
 #include "imgIContainer.h"
 #include "mozilla/gfx/2D.h"
 #include "Units.h"
 #include "mozilla/ToString.h"
 #include "mozilla/ReflowOutput.h"
 #include "ImageContainer.h"
 #include "gfx2DGlue.h"
-#include "nsStyleConsts.h"
 #include "SVGImageContext.h"
 #include <limits>
 #include <algorithm>
 #include "gfxPoint.h"
 #include "nsClassHashtable.h"
 
 class gfxContext;
 class nsPresContext;
--- a/layout/base/nsRefreshDriver.h
+++ b/layout/base/nsRefreshDriver.h
@@ -15,17 +15,16 @@
 #include "mozilla/FlushType.h"
 #include "mozilla/TimeStamp.h"
 #include "mozilla/UniquePtr.h"
 #include "mozilla/Vector.h"
 #include "mozilla/WeakPtr.h"
 #include "nsTObserverArray.h"
 #include "nsTArray.h"
 #include "nsTHashtable.h"
-#include "nsTObserverArray.h"
 #include "nsClassHashtable.h"
 #include "nsHashKeys.h"
 #include "mozilla/Attributes.h"
 #include "mozilla/Maybe.h"
 #include "mozilla/layers/TransactionIdAllocator.h"
 
 class nsPresContext;
 class nsIPresShell;
--- a/layout/forms/nsDateTimeControlFrame.cpp
+++ b/layout/forms/nsDateTimeControlFrame.cpp
@@ -9,17 +9,16 @@
  * datetime-local.
  */
 
 #include "nsDateTimeControlFrame.h"
 
 #include "nsContentUtils.h"
 #include "nsCheckboxRadioFrame.h"
 #include "nsGkAtoms.h"
-#include "nsContentUtils.h"
 #include "nsContentCreatorFunctions.h"
 #include "mozilla/AsyncEventDispatcher.h"
 #include "mozilla/dom/HTMLInputElement.h"
 #include "mozilla/dom/MutationEventBinding.h"
 #include "nsDOMTokenList.h"
 #include "nsNodeInfoManager.h"
 #include "nsIDateTimeInputArea.h"
 #include "nsIObserverService.h"
--- a/layout/generic/ColumnSetWrapperFrame.cpp
+++ b/layout/generic/ColumnSetWrapperFrame.cpp
@@ -2,16 +2,18 @@
 /* vim: set ts=8 sts=2 et sw=2 tw=80: */
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
 
 #include "ColumnSetWrapperFrame.h"
 
 #include "nsContentUtils.h"
+#include "nsIFrame.h"
+#include "nsIFrameInlines.h"
 
 using namespace mozilla;
 
 nsBlockFrame* NS_NewColumnSetWrapperFrame(nsIPresShell* aPresShell,
                                           ComputedStyle* aStyle,
                                           nsFrameState aStateFlags) {
   ColumnSetWrapperFrame* frame = new (aPresShell) ColumnSetWrapperFrame(aStyle);
 
--- a/layout/generic/nsBlockFrame.cpp
+++ b/layout/generic/nsBlockFrame.cpp
@@ -30,17 +30,16 @@
 #include "nsLineLayout.h"
 #include "nsPlaceholderFrame.h"
 #include "nsStyleConsts.h"
 #include "nsFrameManager.h"
 #include "nsPresContext.h"
 #include "nsIPresShell.h"
 #include "nsHTMLParts.h"
 #include "nsGkAtoms.h"
-#include "nsGenericHTMLElement.h"
 #include "nsAttrValueInlines.h"
 #include "mozilla/Sprintf.h"
 #include "nsFloatManager.h"
 #include "prenv.h"
 #include "plstr.h"
 #include "nsError.h"
 #include "nsIScrollableFrame.h"
 #include <algorithm>
--- a/layout/generic/nsFrame.cpp
+++ b/layout/generic/nsFrame.cpp
@@ -42,17 +42,16 @@
 #include "nsTableWrapperFrame.h"
 #include "nsView.h"
 #include "nsViewManager.h"
 #include "nsIScrollableFrame.h"
 #include "nsPresContext.h"
 #include "nsStyleConsts.h"
 #include "nsIPresShell.h"
 #include "mozilla/Logging.h"
-#include "mozilla/Sprintf.h"
 #include "nsLayoutUtils.h"
 #include "LayoutLogging.h"
 #include "mozilla/RestyleManager.h"
 #include "nsInlineFrame.h"
 #include "nsFrameSelection.h"
 #include "nsGkAtoms.h"
 #include "nsCSSAnonBoxes.h"
 #include "nsCSSClipPathInstance.h"
@@ -111,19 +110,16 @@
 #include "mozilla/css/ImageLoader.h"
 #include "mozilla/dom/TouchEvent.h"
 #include "mozilla/gfx/Tools.h"
 #include "mozilla/layers/WebRenderUserData.h"
 #include "nsPrintfCString.h"
 #include "ActiveLayerTracker.h"
 
 #include "nsITheme.h"
-#include "nsStyleConsts.h"
-
-#include "ImageLoader.h"
 
 using namespace mozilla;
 using namespace mozilla::css;
 using namespace mozilla::dom;
 using namespace mozilla::gfx;
 using namespace mozilla::layers;
 using namespace mozilla::layout;
 typedef nsAbsoluteContainingBlock::AbsPosReflowFlags AbsPosReflowFlags;
--- a/layout/generic/nsFrameSelection.cpp
+++ b/layout/generic/nsFrameSelection.cpp
@@ -39,17 +39,16 @@
 #include "nsGkAtoms.h"
 #include "nsIFrameTraversal.h"
 #include "nsLayoutUtils.h"
 #include "nsLayoutCID.h"
 #include "nsBidiPresUtils.h"
 static NS_DEFINE_CID(kFrameTraversalCID, NS_FRAMETRAVERSAL_CID);
 #include "nsTextFrame.h"
 
-#include "nsContentUtils.h"
 #include "nsThreadUtils.h"
 #include "mozilla/Preferences.h"
 
 #include "nsPresContext.h"
 #include "nsIPresShell.h"
 #include "nsCaret.h"
 
 #include "mozilla/MouseEvents.h"
--- a/layout/generic/nsGfxScrollFrame.cpp
+++ b/layout/generic/nsGfxScrollFrame.cpp
@@ -6140,17 +6140,18 @@ void ScrollFrameHelper::RestoreState(Pre
 
   // Resolution properties should only exist on root scroll frames.
   MOZ_ASSERT(mIsRoot ||
              (!aState->scaleToResolution() && aState->resolution() == 1.0));
 
   if (mIsRoot) {
     nsIPresShell* presShell = mOuter->PresShell();
     if (aState->scaleToResolution()) {
-      presShell->SetResolutionAndScaleTo(aState->resolution());
+      presShell->SetResolutionAndScaleTo(aState->resolution(),
+                                         nsGkAtoms::restore);
     } else {
       presShell->SetResolution(aState->resolution());
     }
   }
 }
 
 void ScrollFrameHelper::PostScrolledAreaEvent() {
   if (mScrolledAreaEvent.IsPending()) {
--- a/layout/generic/nsImageFrame.cpp
+++ b/layout/generic/nsImageFrame.cpp
@@ -51,17 +51,16 @@
 #include "nsNameSpaceManager.h"
 #include <algorithm>
 #ifdef ACCESSIBILITY
 #include "nsAccessibilityService.h"
 #endif
 #include "nsLayoutUtils.h"
 #include "nsDisplayList.h"
 #include "nsIContent.h"
-#include "nsIDocument.h"
 #include "FrameLayerBuilder.h"
 #include "mozilla/dom/Selection.h"
 #include "nsIURIMutator.h"
 
 #include "imgIContainer.h"
 #include "imgLoader.h"
 #include "imgRequestProxy.h"
 
--- a/layout/generic/nsPageFrame.cpp
+++ b/layout/generic/nsPageFrame.cpp
@@ -11,17 +11,16 @@
 #include "nsDeviceContext.h"
 #include "nsFontMetrics.h"
 #include "nsLayoutUtils.h"
 #include "nsPresContext.h"
 #include "nsGkAtoms.h"
 #include "nsIPresShell.h"
 #include "nsPageContentFrame.h"
 #include "nsDisplayList.h"
-#include "nsLayoutUtils.h"              // for function BinarySearchForPosition
 #include "nsSimplePageSequenceFrame.h"  // for nsSharedPageData
 #include "nsTextFormatter.h"  // for page number localization formatting
 #include "nsBidiUtils.h"
 #include "nsIPrintSettings.h"
 
 #include "mozilla/Logging.h"
 extern mozilla::LazyLogModule gLayoutPrintingLog;
 #define PR_PL(_p1) MOZ_LOG(gLayoutPrintingLog, mozilla::LogLevel::Debug, _p1)
--- a/layout/generic/nsTextFrame.cpp
+++ b/layout/generic/nsTextFrame.cpp
@@ -77,17 +77,16 @@
 #include <algorithm>
 #include <limits>
 #ifdef ACCESSIBILITY
 #include "nsAccessibilityService.h"
 #endif
 
 #include "nsPrintfCString.h"
 
-#include "gfxContext.h"
 #include "mozilla/gfx/DrawTargetRecording.h"
 
 #include "mozilla/UniquePtr.h"
 #include "mozilla/dom/Element.h"
 #include "mozilla/LookAndFeel.h"
 
 #include "GeckoProfiler.h"
 
--- a/layout/inspector/inDeepTreeWalker.cpp
+++ b/layout/inspector/inDeepTreeWalker.cpp
@@ -3,17 +3,16 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 #include "inDeepTreeWalker.h"
 #include "inLayoutUtils.h"
 
 #include "BindingStyleRule.h"
-#include "InspectorUtils.h"
 #include "nsString.h"
 #include "nsIDocument.h"
 #include "nsServiceManagerUtils.h"
 #include "nsIContent.h"
 #include "ChildIterator.h"
 #include "mozilla/dom/Element.h"
 #include "mozilla/dom/InspectorUtils.h"
 #include "mozilla/dom/NodeFilterBinding.h"
--- a/layout/painting/nsCSSRendering.cpp
+++ b/layout/painting/nsCSSRendering.cpp
@@ -34,17 +34,16 @@
 #include "nsIDocumentInlines.h"
 #include "nsIScrollableFrame.h"
 #include "imgIRequest.h"
 #include "imgIContainer.h"
 #include "ImageOps.h"
 #include "nsCSSRendering.h"
 #include "nsCSSColorUtils.h"
 #include "nsITheme.h"
-#include "nsStyleConsts.h"
 #include "nsLayoutUtils.h"
 #include "nsBlockFrame.h"
 #include "nsStyleStructInlines.h"
 #include "nsCSSFrameConstructor.h"
 #include "nsCSSProps.h"
 #include "nsContentUtils.h"
 #include "SVGObserverUtils.h"
 #include "nsSVGIntegrationUtils.h"
--- a/layout/painting/nsCSSRenderingBorders.cpp
+++ b/layout/painting/nsCSSRenderingBorders.cpp
@@ -22,17 +22,16 @@
 #include "nsCSSRenderingGradients.h"
 #include "nsDisplayList.h"
 #include "GeckoProfiler.h"
 #include "nsExpirationTracker.h"
 #include "nsIScriptError.h"
 #include "nsClassHashtable.h"
 #include "nsPresContext.h"
 #include "nsStyleStruct.h"
-#include "mozilla/gfx/2D.h"
 #include "gfx2DGlue.h"
 #include "gfxGradientCache.h"
 #include "mozilla/layers/StackingContextHelper.h"
 #include "mozilla/layers/WebRenderLayerManager.h"
 #include "mozilla/Range.h"
 #include <algorithm>
 
 using namespace mozilla;
--- a/layout/painting/nsDisplayList.h
+++ b/layout/painting/nsDisplayList.h
@@ -12,17 +12,16 @@
 
 #ifndef NSDISPLAYLIST_H_
 #define NSDISPLAYLIST_H_
 
 #include "mozilla/Attributes.h"
 #include "gfxContext.h"
 #include "mozilla/ArenaAllocator.h"
 #include "mozilla/Assertions.h"
-#include "mozilla/Attributes.h"
 #include "mozilla/Array.h"
 #include "mozilla/DebugOnly.h"
 #include "mozilla/EnumSet.h"
 #include "mozilla/Maybe.h"
 #include "mozilla/RefPtr.h"
 #include "mozilla/TemplateLib.h"  // mozilla::tl::Max
 #include "nsCOMPtr.h"
 #include "nsContainerFrame.h"
@@ -32,17 +31,16 @@
 #include "nsDisplayListInvalidation.h"
 #include "DisplayItemClipChain.h"
 #include "DisplayListClipState.h"
 #include "LayerState.h"
 #include "FrameMetrics.h"
 #include "ImgDrawResult.h"
 #include "mozilla/EffectCompositor.h"
 #include "mozilla/EnumeratedArray.h"
-#include "mozilla/Maybe.h"
 #include "mozilla/UniquePtr.h"
 #include "mozilla/TimeStamp.h"
 #include "mozilla/gfx/UserData.h"
 #include "mozilla/layers/LayerAttributes.h"
 #include "mozilla/layers/ScrollableLayerGuid.h"
 #include "nsCSSRenderingBorders.h"
 #include "nsPresArena.h"
 #include "nsAutoLayoutPhase.h"
--- a/layout/painting/nsImageRenderer.cpp
+++ b/layout/painting/nsImageRenderer.cpp
@@ -20,17 +20,16 @@
 #include "nsCSSRendering.h"
 #include "nsCSSRenderingGradients.h"
 #include "nsDeviceContext.h"
 #include "nsIFrame.h"
 #include "nsStyleStructInlines.h"
 #include "nsSVGDisplayableFrame.h"
 #include "SVGObserverUtils.h"
 #include "nsSVGIntegrationUtils.h"
-#include "mozilla/layers/WebRenderLayerManager.h"
 
 using namespace mozilla;
 using namespace mozilla::gfx;
 using namespace mozilla::image;
 using namespace mozilla::layers;
 
 nsSize CSSSizeOrRatio::ComputeConcreteSize() const {
   NS_ASSERTION(CanComputeConcreteSize(), "Cannot compute");
--- a/layout/printing/nsPrintJob.cpp
+++ b/layout/printing/nsPrintJob.cpp
@@ -42,17 +42,16 @@ static const char sPrintSettingsServiceC
 #include "nsPrintPreviewListener.h"
 #include "nsThreadUtils.h"
 
 // Printing
 #include "nsIWebBrowserPrint.h"
 
 // Print Preview
 #include "imgIContainer.h"       // image animation mode constants
-#include "nsIWebBrowserPrint.h"  // needed for PrintPreview Navigation constants
 
 // Print Progress
 #include "nsIPrintProgress.h"
 #include "nsIPrintProgressParams.h"
 #include "nsIObserver.h"
 
 // Print error dialog
 #include "nsIPrompt.h"
@@ -89,17 +88,16 @@ static const char kPrintingPromptService
 #include "nsLayoutUtils.h"
 #include "mozilla/Preferences.h"
 #include "Text.h"
 
 #include "nsWidgetsCID.h"
 #include "nsIDeviceContextSpec.h"
 #include "nsDeviceContextSpecProxy.h"
 #include "nsViewManager.h"
-#include "nsView.h"
 
 #include "nsIPageSequenceFrame.h"
 #include "nsIURL.h"
 #include "nsIContentViewerEdit.h"
 #include "nsIInterfaceRequestor.h"
 #include "nsIInterfaceRequestorUtils.h"
 #include "nsIDocShellTreeOwner.h"
 #include "nsIWebBrowserChrome.h"
--- a/layout/style/Loader.cpp
+++ b/layout/style/Loader.cpp
@@ -10,17 +10,16 @@
 
 #include "mozilla/ArrayUtils.h"
 #include "mozilla/dom/DocGroup.h"
 #include "mozilla/dom/SRILogHelper.h"
 #include "mozilla/IntegerPrintfMacros.h"
 #include "mozilla/LoadInfo.h"
 #include "mozilla/Logging.h"
 #include "mozilla/MemoryReporting.h"
-#include "mozilla/StyleSheetInlines.h"
 #include "mozilla/SystemGroup.h"
 #include "mozilla/ResultExtensions.h"
 #include "mozilla/URLPreloader.h"
 #include "nsIRunnable.h"
 #include "nsITimedChannel.h"
 #include "nsSyncLoadService.h"
 #include "nsCOMPtr.h"
 #include "nsString.h"
@@ -60,17 +59,16 @@
 #endif
 
 #include "nsError.h"
 
 #include "nsIContentSecurityPolicy.h"
 #include "mozilla/dom/SRICheck.h"
 
 #include "mozilla/Encoding.h"
-#include "mozilla/Logging.h"
 
 using namespace mozilla::dom;
 
 // 1024 bytes is specified in https://drafts.csswg.org/css-syntax/
 #define SNIFFING_BUFFER_SIZE 1024
 
 /**
  * OVERALL ARCHITECTURE
--- a/layout/style/MappedDeclarations.cpp
+++ b/layout/style/MappedDeclarations.cpp
@@ -2,16 +2,17 @@
 /* vim: set ts=8 sts=2 et sw=2 tw=80: */
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 #include "MappedDeclarations.h"
 
 #include "nsAttrValue.h"
+#include "nsAttrValueInlines.h"
 #include "nsIDocument.h"
 #include "nsPresContext.h"
 
 namespace mozilla {
 
 void MappedDeclarations::SetIdentAtomValue(nsCSSPropertyID aId,
                                            nsAtom* aValue) {
   Servo_DeclarationBlock_SetIdentStringValue(mDecl, aId, aValue);
--- a/layout/style/StyleAnimationValue.cpp
+++ b/layout/style/StyleAnimationValue.cpp
@@ -5,17 +5,16 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 /* Utilities for animation of computed style values */
 
 #include "mozilla/StyleAnimationValue.h"
 
 #include "mozilla/ArrayUtils.h"
 #include "mozilla/MathAlgorithms.h"
-#include "mozilla/ServoBindings.h"
 #include "mozilla/ServoStyleSet.h"
 #include "mozilla/Tuple.h"
 #include "mozilla/UniquePtr.h"
 #include "nsAutoPtr.h"
 #include "nsCOMArray.h"
 #include "nsString.h"
 #include "mozilla/ComputedStyle.h"
 #include "nsComputedDOMStyle.h"
new file mode 100644
--- /dev/null
+++ b/layout/style/UserAgentStyleSheetList.h
@@ -0,0 +1,35 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/* list of user agent style sheets that nsLayoutStylesheetCache manages */
+
+/*
+ * STYLE_SHEET(identifier_, url_, lazy_)
+ *
+ * identifier_
+ *   An identifier for the style sheet, suitable for use as an enum class value.
+ *
+ * url_
+ *   The URL of the style sheet.
+ *
+ * lazy_
+ *   A boolean indicating whether the sheet is loaded lazily.
+ */
+
+STYLE_SHEET(ContentEditable, "resource://gre/res/contenteditable.css", true)
+STYLE_SHEET(CounterStyles, "resource://gre-resources/counterstyles.css", false)
+STYLE_SHEET(DesignMode, "resource://gre/res/designmode.css", true)
+STYLE_SHEET(Forms, "resource://gre-resources/forms.css", true)
+STYLE_SHEET(HTML, "resource://gre-resources/html.css", false)
+STYLE_SHEET(MathML, "resource://gre-resources/mathml.css", true)
+STYLE_SHEET(MinimalXUL, "chrome://global/content/minimal-xul.css", false)
+STYLE_SHEET(NoFrames, "resource://gre-resources/noframes.css", true)
+STYLE_SHEET(NoScript, "resource://gre-resources/noscript.css", true)
+STYLE_SHEET(Quirk, "resource://gre-resources/quirk.css", false)
+STYLE_SHEET(Scrollbars, "chrome://global/skin/scrollbars.css", true)
+STYLE_SHEET(SVG, "resource://gre/res/svg.css", false)
+STYLE_SHEET(UA, "resource://gre-resources/ua.css", true)
+STYLE_SHEET(XUL, "chrome://global/content/xul.css", true)
--- a/layout/style/moz.build
+++ b/layout/style/moz.build
@@ -107,16 +107,17 @@ EXPORTS.mozilla += [
     'ServoUtils.h',
     'SheetType.h',
     'StyleAnimationValue.h',
     'StyleComplexColor.h',
     'StyleSheet.h',
     'StyleSheetInfo.h',
     'StyleSheetInlines.h',
     'URLExtraData.h',
+    'UserAgentStyleSheetList.h',
 ]
 
 EXPORTS.mozilla.dom += [
     'CSS.h',
     'CSSCounterStyleRule.h',
     'CSSFontFaceRule.h',
     'CSSFontFeatureValuesRule.h',
     'CSSImportRule.h',
--- a/layout/style/nsCSSProps.cpp
+++ b/layout/style/nsCSSProps.cpp
@@ -11,17 +11,16 @@
 
 #include "nsCSSProps.h"
 
 #include "mozilla/ArrayUtils.h"
 #include "mozilla/Casting.h"
 
 #include "nsCSSKeywords.h"
 #include "nsLayoutUtils.h"
-#include "nsStyleConsts.h"
 #include "nsIWidget.h"
 #include "nsStyleConsts.h"  // For system widget appearance types
 
 #include "mozilla/dom/Animation.h"
 #include "mozilla/dom/AnimationEffectBinding.h"  // for PlaybackDirection
 #include "mozilla/LookAndFeel.h"                 // for system colors
 
 #include "nsString.h"
--- a/layout/style/nsLayoutStylesheetCache.cpp
+++ b/layout/style/nsLayoutStylesheetCache.cpp
@@ -2,17 +2,16 @@
 /* vim: set ts=8 sts=2 et sw=2 tw=80: */
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 #include "nsLayoutStylesheetCache.h"
 
 #include "nsAppDirectoryServiceDefs.h"
-#include "mozilla/StyleSheetInlines.h"
 #include "mozilla/MemoryReporting.h"
 #include "mozilla/Omnijar.h"
 #include "mozilla/Preferences.h"
 #include "mozilla/StyleSheet.h"
 #include "mozilla/StyleSheetInlines.h"
 #include "mozilla/Telemetry.h"
 #include "mozilla/css/Loader.h"
 #include "mozilla/dom/SRIMetadata.h"
@@ -55,103 +54,34 @@ nsresult nsLayoutStylesheetCache::Observ
     mScrollbarsSheet = nullptr;
     mFormsSheet = nullptr;
   } else {
     MOZ_ASSERT_UNREACHABLE("Unexpected observer topic.");
   }
   return NS_OK;
 }
 
-StyleSheet* nsLayoutStylesheetCache::ScrollbarsSheet() {
-  if (!mScrollbarsSheet) {
-    // Scrollbars don't need access to unsafe rules
-    LoadSheetURL("chrome://global/skin/scrollbars.css", &mScrollbarsSheet,
-                 eSafeAgentSheetFeatures, eCrash);
+#define STYLE_SHEET(identifier_, url_, lazy_)                                  \
+  StyleSheet* nsLayoutStylesheetCache::identifier_##Sheet() {                  \
+    if (lazy_ && !m##identifier_##Sheet) {                                     \
+      LoadSheetURL(url_, &m##identifier_##Sheet, eAgentSheetFeatures, eCrash); \
+    }                                                                          \
+    return m##identifier_##Sheet;                                              \
   }
-
-  return mScrollbarsSheet;
-}
-
-StyleSheet* nsLayoutStylesheetCache::FormsSheet() {
-  if (!mFormsSheet) {
-    // forms.css needs access to unsafe rules
-    LoadSheetURL("resource://gre-resources/forms.css", &mFormsSheet,
-                 eAgentSheetFeatures, eCrash);
-  }
-
-  return mFormsSheet;
-}
+#include "mozilla/UserAgentStyleSheetList.h"
+#undef STYLE_SHEET
 
 StyleSheet* nsLayoutStylesheetCache::UserContentSheet() {
   return mUserContentSheet;
 }
 
 StyleSheet* nsLayoutStylesheetCache::UserChromeSheet() {
   return mUserChromeSheet;
 }
 
-StyleSheet* nsLayoutStylesheetCache::UASheet() {
-  if (!mUASheet) {
-    LoadSheetURL("resource://gre-resources/ua.css", &mUASheet,
-                 eAgentSheetFeatures, eCrash);
-  }
-
-  return mUASheet;
-}
-
-StyleSheet* nsLayoutStylesheetCache::HTMLSheet() { return mHTMLSheet; }
-
-StyleSheet* nsLayoutStylesheetCache::MinimalXULSheet() {
-  return mMinimalXULSheet;
-}
-
-StyleSheet* nsLayoutStylesheetCache::XULSheet() {
-  if (!mXULSheet) {
-    LoadSheetURL("chrome://global/content/xul.css", &mXULSheet,
-                 eAgentSheetFeatures, eCrash);
-  }
-
-  return mXULSheet;
-}
-
-StyleSheet* nsLayoutStylesheetCache::QuirkSheet() { return mQuirkSheet; }
-
-StyleSheet* nsLayoutStylesheetCache::SVGSheet() { return mSVGSheet; }
-
-StyleSheet* nsLayoutStylesheetCache::MathMLSheet() {
-  if (!mMathMLSheet) {
-    LoadSheetURL("resource://gre-resources/mathml.css", &mMathMLSheet,
-                 eAgentSheetFeatures, eCrash);
-  }
-
-  return mMathMLSheet;
-}
-
-StyleSheet* nsLayoutStylesheetCache::CounterStylesSheet() {
-  return mCounterStylesSheet;
-}
-
-StyleSheet* nsLayoutStylesheetCache::NoScriptSheet() {
-  if (!mNoScriptSheet) {
-    LoadSheetURL("resource://gre-resources/noscript.css", &mNoScriptSheet,
-                 eAgentSheetFeatures, eCrash);
-  }
-
-  return mNoScriptSheet;
-}
-
-StyleSheet* nsLayoutStylesheetCache::NoFramesSheet() {
-  if (!mNoFramesSheet) {
-    LoadSheetURL("resource://gre-resources/noframes.css", &mNoFramesSheet,
-                 eAgentSheetFeatures, eCrash);
-  }
-
-  return mNoFramesSheet;
-}
-
 StyleSheet* nsLayoutStylesheetCache::ChromePreferenceSheet(
     nsPresContext* aPresContext) {
   if (!mChromePreferenceSheet) {
     BuildPreferenceSheet(&mChromePreferenceSheet, aPresContext);
   }
 
   return mChromePreferenceSheet;
 }
@@ -160,34 +90,16 @@ StyleSheet* nsLayoutStylesheetCache::Con
     nsPresContext* aPresContext) {
   if (!mContentPreferenceSheet) {
     BuildPreferenceSheet(&mContentPreferenceSheet, aPresContext);
   }
 
   return mContentPreferenceSheet;
 }
 
-StyleSheet* nsLayoutStylesheetCache::ContentEditableSheet() {
-  if (!mContentEditableSheet) {
-    LoadSheetURL("resource://gre/res/contenteditable.css",
-                 &mContentEditableSheet, eAgentSheetFeatures, eCrash);
-  }
-
-  return mContentEditableSheet;
-}
-
-StyleSheet* nsLayoutStylesheetCache::DesignModeSheet() {
-  if (!mDesignModeSheet) {
-    LoadSheetURL("resource://gre/res/designmode.css", &mDesignModeSheet,
-                 eAgentSheetFeatures, eCrash);
-  }
-
-  return mDesignModeSheet;
-}
-
 void nsLayoutStylesheetCache::Shutdown() {
   gCSSLoader = nullptr;
   NS_WARNING_ASSERTION(!gStyleCache || !gUserContentSheetURL,
                        "Got the URL but never used?");
   gStyleCache = nullptr;
   gUserContentSheetURL = nullptr;
 }
 
@@ -210,34 +122,24 @@ nsLayoutStylesheetCache::CollectReports(
 }
 
 size_t nsLayoutStylesheetCache::SizeOfIncludingThis(
     mozilla::MallocSizeOf aMallocSizeOf) const {
   size_t n = aMallocSizeOf(this);
 
 #define MEASURE(s) n += s ? s->SizeOfIncludingThis(aMallocSizeOf) : 0;
 
+#define STYLE_SHEET(identifier_, url_, lazy_) MEASURE(m##identifier_##Sheet);
+#include "mozilla/UserAgentStyleSheetList.h"
+#undef STYLE_SHEET
+
   MEASURE(mChromePreferenceSheet);
-  MEASURE(mContentEditableSheet);
   MEASURE(mContentPreferenceSheet);
-  MEASURE(mCounterStylesSheet);
-  MEASURE(mDesignModeSheet);
-  MEASURE(mFormsSheet);
-  MEASURE(mHTMLSheet);
-  MEASURE(mMathMLSheet);
-  MEASURE(mMinimalXULSheet);
-  MEASURE(mNoFramesSheet);
-  MEASURE(mNoScriptSheet);
-  MEASURE(mQuirkSheet);
-  MEASURE(mSVGSheet);
-  MEASURE(mScrollbarsSheet);
-  MEASURE(mUASheet);
   MEASURE(mUserChromeSheet);
   MEASURE(mUserContentSheet);
-  MEASURE(mXULSheet);
 
   // Measurement of the following members may be added later if DMD finds it is
   // worthwhile:
   // - gCSSLoader
 
   return n;
 }
 
@@ -251,26 +153,23 @@ nsLayoutStylesheetCache::nsLayoutStylesh
     obsSvc->AddObserver(this, "chrome-flush-skin-caches", false);
     obsSvc->AddObserver(this, "chrome-flush-caches", false);
   }
 
   InitFromProfile();
 
   // And make sure that we load our UA sheets.  No need to do this
   // per-profile, since they're profile-invariant.
-  LoadSheetURL("resource://gre-resources/counterstyles.css",
-               &mCounterStylesSheet, eAgentSheetFeatures, eCrash);
-  LoadSheetURL("resource://gre-resources/html.css", &mHTMLSheet,
-               eAgentSheetFeatures, eCrash);
-  LoadSheetURL("chrome://global/content/minimal-xul.css", &mMinimalXULSheet,
-               eAgentSheetFeatures, eCrash);
-  LoadSheetURL("resource://gre-resources/quirk.css", &mQuirkSheet,
-               eAgentSheetFeatures, eCrash);
-  LoadSheetURL("resource://gre/res/svg.css", &mSVGSheet, eAgentSheetFeatures,
-               eCrash);
+#define STYLE_SHEET(identifier_, url_, lazy_)                                \
+  if (!lazy_) {                                                              \
+    LoadSheetURL(url_, &m##identifier_##Sheet, eAgentSheetFeatures, eCrash); \
+  }
+#include "mozilla/UserAgentStyleSheetList.h"
+#undef STYLE_SHEET
+
   if (XRE_IsParentProcess()) {
     // We know we need xul.css for the UI, so load that now too:
     XULSheet();
   }
 
   if (gUserContentSheetURL) {
     MOZ_ASSERT(XRE_IsContentProcess(), "Only used in content processes.");
     LoadSheet(gUserContentSheetURL, &mUserContentSheet, eUserSheetFeatures,
--- a/layout/style/nsLayoutStylesheetCache.h
+++ b/layout/style/nsLayoutStylesheetCache.h
@@ -33,34 +33,25 @@ enum FailureAction { eCrash = 0, eLogToC
 class nsLayoutStylesheetCache final : public nsIObserver,
                                       public nsIMemoryReporter {
   NS_DECL_ISUPPORTS
   NS_DECL_NSIOBSERVER
   NS_DECL_NSIMEMORYREPORTER
 
   static nsLayoutStylesheetCache* Singleton();
 
-  mozilla::StyleSheet* ScrollbarsSheet();
-  mozilla::StyleSheet* FormsSheet();
+#define STYLE_SHEET(identifier_, url_, lazy_) \
+  mozilla::StyleSheet* identifier_##Sheet();
+#include "mozilla/UserAgentStyleSheetList.h"
+#undef STYLE_SHEET
+
   mozilla::StyleSheet* UserContentSheet();
   mozilla::StyleSheet* UserChromeSheet();
-  mozilla::StyleSheet* UASheet();
-  mozilla::StyleSheet* HTMLSheet();
-  mozilla::StyleSheet* MinimalXULSheet();
-  mozilla::StyleSheet* XULSheet();
-  mozilla::StyleSheet* QuirkSheet();
-  mozilla::StyleSheet* SVGSheet();
-  mozilla::StyleSheet* MathMLSheet();
-  mozilla::StyleSheet* CounterStylesSheet();
-  mozilla::StyleSheet* NoScriptSheet();
-  mozilla::StyleSheet* NoFramesSheet();
   mozilla::StyleSheet* ChromePreferenceSheet(nsPresContext* aPresContext);
   mozilla::StyleSheet* ContentPreferenceSheet(nsPresContext* aPresContext);
-  mozilla::StyleSheet* ContentEditableSheet();
-  mozilla::StyleSheet* DesignModeSheet();
 
   static void InvalidatePreferenceSheets();
 
   static void Shutdown();
 
   static void SetUserContentCSSURL(nsIURI* aURI);
 
   size_t SizeOfIncludingThis(mozilla::MallocSizeOf aMallocSizeOf) const;
--- a/layout/style/nsStyleStruct.cpp
+++ b/layout/style/nsStyleStruct.cpp
@@ -7,17 +7,16 @@
 /*
  * structs that contain the data provided by ComputedStyle, the
  * internal API for computed style data for an element
  */
 
 #include "nsStyleStruct.h"
 #include "nsStyleStructInlines.h"
 #include "nsStyleConsts.h"
-#include "nsStyleConsts.h"
 #include "nsString.h"
 #include "nsPresContext.h"
 #include "nsIAppShellService.h"
 #include "nsIWidget.h"
 #include "nsCRTGlue.h"
 #include "nsCSSProps.h"
 #include "nsDeviceContext.h"
 #include "nsStyleUtil.h"
--- a/layout/svg/SVGContextPaint.cpp
+++ b/layout/svg/SVGContextPaint.cpp
@@ -9,17 +9,16 @@
 #include "gfxContext.h"
 #include "gfxUtils.h"
 #include "mozilla/gfx/2D.h"
 #include "mozilla/dom/SVGDocument.h"
 #include "mozilla/Preferences.h"
 #include "nsIDocument.h"
 #include "nsSVGPaintServerFrame.h"
 #include "SVGObserverUtils.h"
-#include "nsSVGPaintServerFrame.h"
 
 using namespace mozilla::gfx;
 using namespace mozilla::image;
 
 namespace mozilla {
 
 using image::imgDrawingParams;
 
--- a/layout/svg/SVGObserverUtils.cpp
+++ b/layout/svg/SVGObserverUtils.cpp
@@ -3,32 +3,34 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 // Main header first:
 #include "SVGObserverUtils.h"
 
 // Keep others in (case-insensitive) order:
+#include "mozilla/css/ImageLoader.h"
 #include "mozilla/dom/CanvasRenderingContext2D.h"
+#include "mozilla/net/ReferrerPolicy.h"
 #include "mozilla/RestyleManager.h"
 #include "nsCSSFrameConstructor.h"
+#include "nsCycleCollectionParticipant.h"
+#include "nsIContent.h"
+#include "nsIContentInlines.h"
+#include "nsIReflowCallback.h"
 #include "nsISupportsImpl.h"
 #include "nsSVGClipPathFrame.h"
+#include "nsSVGFilterFrame.h"
 #include "nsSVGMarkerFrame.h"
+#include "nsSVGMaskFrame.h"
 #include "nsSVGPaintServerFrame.h"
-#include "nsSVGFilterFrame.h"
-#include "nsSVGMaskFrame.h"
-#include "nsIReflowCallback.h"
-#include "nsCycleCollectionParticipant.h"
 #include "SVGGeometryElement.h"
 #include "SVGTextPathElement.h"
 #include "SVGUseElement.h"
-#include "ImageLoader.h"
-#include "mozilla/net/ReferrerPolicy.h"
 
 using namespace mozilla;
 using namespace mozilla::dom;
 
 static already_AddRefed<URLAndReferrerInfo> ResolveURLUsingLocalRef(
     nsIFrame* aFrame, const css::URLValue* aURL) {
   MOZ_ASSERT(aFrame);
 
--- a/layout/svg/nsFilterInstance.cpp
+++ b/layout/svg/nsFilterInstance.cpp
@@ -20,17 +20,16 @@
 #include "mozilla/gfx/PatternHelpers.h"
 #include "nsSVGDisplayableFrame.h"
 #include "nsCSSFilterInstance.h"
 #include "nsSVGFilterInstance.h"
 #include "nsSVGFilterPaintCallback.h"
 #include "nsSVGUtils.h"
 #include "SVGContentUtils.h"
 #include "FilterSupport.h"
-#include "gfx2DGlue.h"
 #include "mozilla/Unused.h"
 
 using namespace mozilla;
 using namespace mozilla::dom;
 using namespace mozilla::gfx;
 using namespace mozilla::image;
 
 FilterDescription nsFilterInstance::GetFilterDescription(
--- a/layout/tables/nsCellMap.h
+++ b/layout/tables/nsCellMap.h
@@ -3,17 +3,16 @@
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 #ifndef nsCellMap_h__
 #define nsCellMap_h__
 
 #include "nscore.h"
 #include "celldata.h"
 #include "nsTArray.h"
-#include "nsTArray.h"
 #include "nsCOMPtr.h"
 #include "nsAlgorithm.h"
 #include "nsRect.h"
 #include <algorithm>
 #include "TableArea.h"
 
 #undef DEBUG_TABLE_CELLMAP
 
--- a/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/AccessibilityTest.kt
+++ b/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/AccessibilityTest.kt
@@ -432,16 +432,74 @@ class AccessibilityTest : BaseSessionTes
         waitUntilTextTraversed(18, 28) // "sit amet, "
 
         provider.performAction(nodeId,
                 AccessibilityNodeInfo.ACTION_PREVIOUS_AT_MOVEMENT_GRANULARITY,
                 moveByGranularityArguments(AccessibilityNodeInfo.MOVEMENT_GRANULARITY_LINE))
         waitUntilTextTraversed(0, 18) // "Lorem ipsum dolor "
     }
 
+    @Test fun testHeadings() {
+        var nodeId = AccessibilityNodeProvider.HOST_VIEW_ID;
+        sessionRule.session.loadString("""
+            <a href=\"%23\">preamble</a>
+            <h1>Fried cheese</h1><p>with club sauce.</p>
+            <h2>Popcorn shrimp</h2><button>with club sauce.</button>
+            <h3>Chicken fingers</h3><p>with spicy club sauce.</p>""".trimIndent(), "text/html")
+        waitForInitialFocus()
+
+        val bundle = Bundle()
+        bundle.putString(AccessibilityNodeInfo.ACTION_ARGUMENT_HTML_ELEMENT_STRING, "HEADING")
+
+        provider.performAction(nodeId, AccessibilityNodeInfo.ACTION_NEXT_HTML_ELEMENT, bundle)
+        sessionRule.waitUntilCalled(object : EventDelegate {
+            @AssertCalled(count = 1)
+            override fun onAccessibilityFocused(event: AccessibilityEvent) {
+                nodeId = getSourceId(event)
+                val node = createNodeInfo(nodeId)
+                assertThat("Accessibility focus on first heading", node.text as String, startsWith("Fried cheese"))
+                if (Build.VERSION.SDK_INT >= 19) {
+                    assertThat("First heading is level 1",
+                            node.extras.getCharSequence("AccessibilityNodeInfo.roleDescription").toString(),
+                            equalTo("heading level 1"))
+                }
+            }
+        })
+
+        provider.performAction(nodeId, AccessibilityNodeInfo.ACTION_NEXT_HTML_ELEMENT, bundle)
+        sessionRule.waitUntilCalled(object : EventDelegate {
+            @AssertCalled(count = 1)
+            override fun onAccessibilityFocused(event: AccessibilityEvent) {
+                nodeId = getSourceId(event)
+                val node = createNodeInfo(nodeId)
+                assertThat("Accessibility focus on second heading", node.text as String, startsWith("Popcorn shrimp"))
+                if (Build.VERSION.SDK_INT >= 19) {
+                    assertThat("Second heading is level 2",
+                            node.extras.getCharSequence("AccessibilityNodeInfo.roleDescription").toString(),
+                            equalTo("heading level 2"))
+                }
+            }
+        })
+
+        provider.performAction(nodeId, AccessibilityNodeInfo.ACTION_NEXT_HTML_ELEMENT, bundle)
+        sessionRule.waitUntilCalled(object : EventDelegate {
+            @AssertCalled(count = 1)
+            override fun onAccessibilityFocused(event: AccessibilityEvent) {
+                nodeId = getSourceId(event)
+                val node = createNodeInfo(nodeId)
+                assertThat("Accessibility focus on second heading", node.text as String, startsWith("Chicken fingers"))
+                if (Build.VERSION.SDK_INT >= 19) {
+                    assertThat("Third heading is level 3",
+                            node.extras.getCharSequence("AccessibilityNodeInfo.roleDescription").toString(),
+                            equalTo("heading level 3"))
+                }
+            }
+        })
+    }
+
     @Test fun testCheckbox() {
         var nodeId = AccessibilityNodeProvider.HOST_VIEW_ID;
         sessionRule.session.loadString("<label><input type='checkbox'>many option</label>", "text/html")
         waitForInitialFocus(true)
 
         sessionRule.waitUntilCalled(object : EventDelegate {
             @AssertCalled(count = 1)
             override fun onAccessibilityFocused(event: AccessibilityEvent) {
--- a/python/mozboot/mozboot/bootstrap.py
+++ b/python/mozboot/mozboot/bootstrap.py
@@ -35,17 +35,17 @@ from mozboot.mozillabuild import Mozilla
 from mozboot.util import (
     get_state_dir,
 )
 
 APPLICATION_CHOICE = '''
 Note on Artifact Mode:
 
 Artifact builds download prebuilt C++ components rather than building
-them locally.
+them locally. Artifact builds are faster!
 
 Artifact builds are recommended for people working on Firefox or
 Firefox for Android frontends. They are unsuitable for those working
 on C++ code. For more information see:
 https://developer.mozilla.org/en-US/docs/Artifact_builds.
 
 Please choose the version of Firefox you want to build:
 %s
--- a/python/mozlint/test/python.ini
+++ b/python/mozlint/test/python.ini
@@ -1,10 +1,10 @@
 [DEFAULT]
-subsuite = mozlint, os == "linux"
+subsuite = mozlint
 skip-if = python == 3
 
 [test_cli.py]
 [test_editor.py]
 [test_formatters.py]
 [test_parser.py]
 [test_pathutils.py]
 [test_result.py]
--- a/taskcluster/ci/source-test/python.yml
+++ b/taskcluster/ci/source-test/python.yml
@@ -1,19 +1,22 @@
 job-defaults:
     platform: linux64/opt
     always-target: true
     worker-type:
         by-platform:
             linux64.*: aws-provisioner-v1/gecko-t-linux-xlarge
+            windows10-64.*: aws-provisioner-v1/gecko-t-win10-64
     worker:
         by-platform:
             linux64.*:
                 docker-image: {in-tree: "lint"}
                 max-run-time: 3600
+            default:
+                max-run-time: 3600
     treeherder:
         kind: test
         tier: 2
     run:
         using: mach
     when:
         files-changed:
             - 'config/mozunit/**'
@@ -29,16 +32,19 @@ taskgraph-tests:
         subsuite: taskgraph
     when:
         files-changed:
             - 'taskcluster/**/*.py'
             - 'python/mach/**/*.py'
 
 marionette-harness:
     description: testing/marionette/harness unit tests
+    platform:
+        - linux64/opt
+        - windows10-64/opt
     python-version: [2]
     treeherder:
         symbol: mnh
     run:
         using: python-test
         subsuite: marionette-harness
     when:
         files-changed:
@@ -84,16 +90,19 @@ mochitest-harness:
             - 'testing/mozbase/moztest/moztest/selftest/**'
             - 'testing/mozharness/mozharness/base/log.py'
             - 'testing/mozharness/mozharness/mozilla/structuredlog.py'
             - 'testing/mozharness/mozharness/mozilla/testing/errors.py'
             - 'testing/profiles/**'
 
 mozbase:
     description: testing/mozbase unit tests
+    platform:
+        - linux64/opt
+        - windows10-64/opt
     python-version: [2, 3]
     treeherder:
         symbol: mb
     run:
         using: python-test
         subsuite: mozbase
     when:
         files-changed:
@@ -110,16 +119,19 @@ mozharness:
             cd /builds/worker/checkouts/gecko/testing/mozharness &&
             /usr/local/bin/tox -e py27-hg4.3
     when:
         files-changed:
             - 'testing/mozharness/**'
 
 mozlint:
     description: python/mozlint unit tests
+    platform:
+        - linux64/opt
+        - windows10-64/opt
     python-version: [2]
     treeherder:
         symbol: ml
     run:
         using: python-test
         subsuite: mozlint
     when:
         files-changed:
@@ -135,16 +147,19 @@ mozrelease:
         using: python-test
         subsuite: mozrelease
     when:
         files-changed:
             - 'python/mozrelease/**'
 
 mozterm:
     description: python/mozterm unit tests
+    platform:
+        - linux64/opt
+        - windows10-64/opt
     python-version: [2, 3]
     treeherder:
         symbol: term
     run:
         using: python-test
         subsuite: mozterm
     when:
         files-changed:
@@ -159,16 +174,19 @@ mozversioncontrol:
         using: python-test
         subsuite: mozversioncontrol
     when:
         files-changed:
             - 'python/mozversioncontrol/**'
 
 raptor:
     description: testing/raptor unit tests
+    platform:
+        - linux64/opt
+        - windows10-64/opt
     python-version: [2]
     treeherder:
         symbol: rap
     run:
         using: python-test
         subsuite: raptor
     when:
         files-changed:
@@ -210,16 +228,19 @@ reftest-harness:
             - 'layout/tools/reftest/**'
             - 'testing/mozbase/moztest/moztest/selftest/**'
             - 'testing/mozharness/mozharness/base/log.py'
             - 'testing/mozharness/mozharness/mozilla/structuredlog.py'
             - 'testing/mozharness/mozharness/mozilla/testing/errors.py'
 
 tryselect:
     description: tools/tryselect unit tests
+    platform:
+        - linux64/opt
+        - windows10-64/opt
     python-version: [2]
     treeherder:
         symbol: try
     run:
         using: python-test
         subsuite: try
     when:
         files-changed:
--- a/taskcluster/scripts/run-task
+++ b/taskcluster/scripts/run-task
@@ -443,23 +443,24 @@ def vcs_checkout(source_repo, dest, stor
             '--config', 'hostsecurity.hg.mozilla.org:fingerprints=%s' % hgmo_fingerprint,
         ])
 
     if base_repo:
         args.extend(['--upstream', base_repo])
     if sparse_profile:
         args.extend(['--sparseprofile', sparse_profile])
 
+    dest = os.path.abspath(dest)
     args.extend([
         revision_flag, revision_value,
         source_repo, dest,
     ])
 
     res = run_and_prefix_output(b'vcs', args,
-                                extra_env={b'PYTHONUNBUFFERED': b'1'})
+                                extra_env={'PYTHONUNBUFFERED': '1'})
     if res:
         sys.exit(res)
 
     # Update the current revision hash and ensure that it is well formed.
     revision = subprocess.check_output(
         [hg_bin, 'log',
          '--rev', '.',
          '--template', '{node}'],
@@ -736,16 +737,19 @@ def main(args):
             branch=os.environ.get('COMM_HEAD_REF'))
 
     elif not os.environ.get('COMM_HEAD_REV') and \
             os.environ.get('COMM_HEAD_REF'):
         print('task should be defined in terms of non-symbolic revision')
         return 1
 
     try:
+        if 'GECKO_PATH' in os.environ:
+            os.environ['GECKO_PATH'] = os.path.abspath(os.environ['GECKO_PATH'])
+
         if 'MOZ_FETCHES' in os.environ:
             fetch_artifacts()
 
         return run_and_prefix_output(b'task', task_args)
     finally:
         fetches_dir = os.environ.get('MOZ_FETCHES_DIR')
         if fetches_dir and os.path.isdir(fetches_dir):
             print_line(b'fetches', b'removing %s\n' % fetches_dir.encode('utf-8'))
--- a/taskcluster/taskgraph/transforms/bouncer_submission.py
+++ b/taskcluster/taskgraph/transforms/bouncer_submission.py
@@ -97,18 +97,18 @@ CONFIG_PER_BOUNCER_PRODUCT = {
             'win': '{pretty_product}%20Installer.exe',
             'win64': '{pretty_product}%20Installer.exe',
         },
     },
     'msi': {
         'name_postfix': '-msi-SSL',
         'path_template': RELEASES_PATH_TEMPLATE,
         'file_names': {
-            'win': '{pretty_product}%20Installer.msi',
-            'win64': '{pretty_product}%20Installer.msi',
+            'win': '{pretty_product}%20Setup%20{version}.msi',
+            'win64': '{pretty_product}%20Setup%20{version}.msi',
         }
     }
 }
 CONFIG_PER_BOUNCER_PRODUCT['installer-ssl'] = copy.deepcopy(
     CONFIG_PER_BOUNCER_PRODUCT['installer'])
 CONFIG_PER_BOUNCER_PRODUCT['installer-ssl']['name_postfix'] = '-SSL'
 
 transforms = TransformSequence()
--- a/taskcluster/taskgraph/transforms/job/common.py
+++ b/taskcluster/taskgraph/transforms/job/common.py
@@ -59,45 +59,55 @@ def generic_worker_add_artifacts(config,
 
 
 def support_vcs_checkout(config, job, taskdesc, sparse=False):
     """Update a job/task with parameters to enable a VCS checkout.
 
     This can only be used with ``run-task`` tasks, as the cache name is
     reserved for ``run-task`` tasks.
     """
-    level = config.params['level']
+    is_win = job['worker']['os'] == 'windows'
 
-    # native-engine does not support caches (yet), so we just do a full clone
-    # every time :(
+    if is_win:
+        checkoutdir = './build'
+        geckodir = '{}/src'.format(checkoutdir)
+        hgstore = 'y:/hg-shared'
+    else:
+        checkoutdir = '{workdir}/checkouts'.format(**job['run'])
+        geckodir = '{}/gecko'.format(checkoutdir)
+        hgstore = '{}/hg-store'.format(checkoutdir)
+
+    level = config.params['level']
+    # native-engine and generic-worker do not support caches (yet), so we just
+    # do a full clone every time :(
     if job['worker']['implementation'] in ('docker-worker', 'docker-engine'):
         name = 'level-%s-checkouts' % level
 
         # comm-central checkouts need their own cache, because clobber won't
         # remove the comm-central checkout
         if job['run'].get('comm-checkout', False):
             name += '-comm'
 
         # Sparse checkouts need their own cache because they can interfere
         # with clients that aren't sparse aware.
         if sparse:
             name += '-sparse'
 
         taskdesc['worker'].setdefault('caches', []).append({
             'type': 'persistent',
             'name': name,
-            'mount-point': '{workdir}/checkouts'.format(**job['run']),
+            'mount-point': checkoutdir,
         })
 
     taskdesc['worker'].setdefault('env', {}).update({
         'GECKO_BASE_REPOSITORY': config.params['base_repository'],
         'GECKO_HEAD_REPOSITORY': config.params['head_repository'],
         'GECKO_HEAD_REV': config.params['head_rev'],
-        'GECKO_PATH': '{workdir}/checkouts/gecko'.format(**job['run']),
-        'HG_STORE_PATH': '{workdir}/checkouts/hg-store'.format(**job['run']),
+        'GECKO_PATH': geckodir,
+        'HG_STORE_PATH': hgstore,
     })
 
     if 'comm_base_repository' in config.params:
         taskdesc['worker']['env'].update({
             'COMM_BASE_REPOSITORY': config.params['comm_base_repository'],
             'COMM_HEAD_REPOSITORY': config.params['comm_head_repository'],
             'COMM_HEAD_REV': config.params['comm_head_rev'],
         })
--- a/taskcluster/taskgraph/transforms/job/mach.py
+++ b/taskcluster/taskgraph/transforms/job/mach.py
@@ -25,19 +25,24 @@ mach_schema = Schema({
     # gecko checkout
     Required('comm-checkout'): bool,
 
     # Base work directory used to set up the task.
     Required('workdir'): basestring,
 })
 
 
-@run_job_using("docker-worker", "mach", schema=mach_schema, defaults={'comm-checkout': False})
-@run_job_using("native-engine", "mach", schema=mach_schema, defaults={'comm-checkout': False})
-@run_job_using("generic-worker", "mach", schema=mach_schema, defaults={'comm-checkout': False})
-def docker_worker_mach(config, job, taskdesc):
+defaults = {
+    'comm-checkout': False,
+}
+
+
+@run_job_using("docker-worker", "mach", schema=mach_schema, defaults=defaults)
+@run_job_using("native-engine", "mach", schema=mach_schema, defaults=defaults)
+@run_job_using("generic-worker", "mach", schema=mach_schema, defaults=defaults)
+def configure_mach(config, job, taskdesc):
     run = job['run']
 
     # defer to the run_task implementation
-    run['command'] = 'cd {workdir}/checkouts/gecko && ./mach {mach}'.format(**run)
+    run['command'] = 'cd $GECKO_PATH && ./mach {mach}'.format(**run)
     run['using'] = 'run-task'
     del run['mach']
     configure_taskdesc_for_run(config, job, taskdesc, job['worker']['implementation'])
--- a/taskcluster/taskgraph/transforms/job/python_test.py
+++ b/taskcluster/taskgraph/transforms/job/python_test.py
@@ -20,22 +20,25 @@ python_test_schema = Schema({
     # The subsuite to run
     Required('subsuite'): basestring,
 
     # Base work directory used to set up the task.
     Required('workdir'): basestring,
 })
 
 
-@run_job_using(
-    'docker-worker',
-    'python-test',
-    schema=python_test_schema,
-    defaults={'python-version': 2, 'subsuite': 'default'})
-def docker_worker_python_test(config, job, taskdesc):
+defaults = {
+    'python-version': 2,
+    'subsuite': 'default',
+}
+
+
+@run_job_using('docker-worker', 'python-test', schema=python_test_schema, defaults=defaults)
+@run_job_using('generic-worker', 'python-test', schema=python_test_schema, defaults=defaults)
+def configure_python_test(config, job, taskdesc):
     run = job['run']
 
     # defer to the mach implementation
     run['mach'] = 'python-test --python {python-version} --subsuite {subsuite}'.format(**run)
     run['using'] = 'mach'
     del run['python-version']
     del run['subsuite']
     configure_taskdesc_for_run(config, job, taskdesc, job['worker']['implementation'])
--- a/taskcluster/taskgraph/transforms/job/run_task.py
+++ b/taskcluster/taskgraph/transforms/job/run_task.py
@@ -36,22 +36,22 @@ run_task_schema = Schema({
     # it will be included in a single argument to `bash -cx`.
     Required('command'): Any([taskref_or_string], taskref_or_string),
 
     # Base work directory used to set up the task.
     Required('workdir'): basestring,
 })
 
 
-def common_setup(config, job, taskdesc, command, checkoutdir):
+def common_setup(config, job, taskdesc, command, geckodir):
     run = job['run']
     if run['checkout']:
         support_vcs_checkout(config, job, taskdesc,
                              sparse=bool(run['sparse-profile']))
-        command.append('--vcs-checkout={}/gecko'.format(checkoutdir))
+        command.append('--vcs-checkout={}'.format(geckodir))
 
     if run['sparse-profile']:
         command.append('--sparse-profile=build/sparse-profiles/%s' %
                        run['sparse-profile'])
 
     taskdesc['worker'].setdefault('env', {})['MOZ_SCM_LEVEL'] = config.params['level']
 
 
@@ -68,17 +68,18 @@ def run_task_url(config):
                 config.params['head_repository'], config.params['head_rev'])
 
 
 @run_job_using("docker-worker", "run-task", schema=run_task_schema, defaults=worker_defaults)
 def docker_worker_run_task(config, job, taskdesc):
     run = job['run']
     worker = taskdesc['worker'] = job['worker']
     command = ['/builds/worker/bin/run-task']
-    common_setup(config, job, taskdesc, command, checkoutdir='{workdir}/checkouts'.format(**run))
+    common_setup(config, job, taskdesc, command,
+                 geckodir='{workdir}/checkouts/gecko'.format(**run))
 
     if run.get('cache-dotcache'):
         worker['caches'].append({
             'type': 'persistent',
             'name': 'level-{level}-{project}-dotcache'.format(**config.params),
             'mount-point': '{workdir}/.cache'.format(**run),
             'skip-untrusted': True,
         })
@@ -95,17 +96,18 @@ def docker_worker_run_task(config, job, 
     worker['command'] = command
 
 
 @run_job_using("native-engine", "run-task", schema=run_task_schema, defaults=worker_defaults)
 def native_engine_run_task(config, job, taskdesc):
     run = job['run']
     worker = taskdesc['worker'] = job['worker']
     command = ['./run-task']
-    common_setup(config, job, taskdesc, command, checkoutdir='{workdir}/checkouts'.format(**run))
+    common_setup(config, job, taskdesc, command,
+                 geckodir='{workdir}/checkouts/gecko'.format(**run))
 
     worker['context'] = run_task_url(config)
 
     if run.get('cache-dotcache'):
         raise Exception("No cache support on native-worker; can't use cache-dotcache")
 
     run_command = run['command']
     if isinstance(run_command, basestring):
@@ -114,30 +116,48 @@ def native_engine_run_task(config, job, 
     command.extend(run_command)
     worker['command'] = command
 
 
 @run_job_using("generic-worker", "run-task", schema=run_task_schema, defaults=worker_defaults)
 def generic_worker_run_task(config, job, taskdesc):
     run = job['run']
     worker = taskdesc['worker'] = job['worker']
-    command = ['./run-task']
-    common_setup(config, job, taskdesc, command, checkoutdir='{workdir}/checkouts'.format(**run))
+    is_win = worker['os'] == 'windows'
+
+    if is_win:
+        command = ['C:/mozilla-build/python3/python3.exe', 'run-task']
+        geckodir = './build/src'
+    else:
+        command = ['./run-task']
+        geckodir = '{workdir}/checkouts/gecko'.format(**run)
+
+    common_setup(config, job, taskdesc, command, geckodir=geckodir)
 
     worker.setdefault('mounts', [])
     if run.get('cache-dotcache'):
         worker['mounts'].append({
             'cache-name': 'level-{level}-{project}-dotcache'.format(**config.params),
             'directory': '{workdir}/.cache'.format(**run),
         })
     worker['mounts'].append({
         'content': {
             'url': run_task_url(config),
         },
         'file': './run-task',
     })
 
     run_command = run['command']
     if isinstance(run_command, basestring):
+        if is_win:
+            run_command = '"{}"'.format(run_command)
         run_command = ['bash', '-cx', run_command]
+
     command.append('--')
     command.extend(run_command)
-    worker['command'] = [['chmod', '+x', 'run-task'], command]
+
+    if is_win:
+        worker['command'] = [' '.join(command)]
+    else:
+        worker['command'] = [
+            ['chmod', '+x', 'run-task'],
+            command,
+        ]
--- a/taskcluster/taskgraph/transforms/task.py
+++ b/taskcluster/taskgraph/transforms/task.py
@@ -1875,18 +1875,18 @@ def check_caches_are_volumes(task):
     volumes = set(task['worker']['volumes'])
     paths = set(c['mount-point'] for c in task['worker'].get('caches', []))
     missing = paths - volumes
 
     if not missing:
         return
 
     raise Exception('task %s (image %s) has caches that are not declared as '
-                    'Docker volumes: %s'
-                    'Have you added them as VOLUMEs in the Dockerfile?'
+                    'Docker volumes: %s '
+                    '(have you added them as VOLUMEs in the Dockerfile?)'
                     % (task['label'], task['worker']['docker-image'],
                        ', '.join(sorted(missing))))
 
 
 @transforms.add
 def check_run_task_caches(config, tasks):
     """Audit for caches requiring run-task.
 
--- a/testing/marionette/browser.js
+++ b/testing/marionette/browser.js
@@ -7,17 +7,18 @@
 
 const {WebElementEventTarget} = ChromeUtils.import("chrome://marionette/content/dom.js", {});
 ChromeUtils.import("chrome://marionette/content/element.js");
 const {
   NoSuchWindowError,
   UnsupportedOperationError,
 } = ChromeUtils.import("chrome://marionette/content/error.js", {});
 const {
-  MessageManagerDestroyedPromise,
+  waitForEvent,
+  waitForObserverTopic,
 } = ChromeUtils.import("chrome://marionette/content/sync.js", {});
 
 this.EXPORTED_SYMBOLS = ["browser", "Context", "WindowState"];
 
 /** @namespace */
 this.browser = {};
 
 const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
@@ -65,21 +66,21 @@ this.Context = Context;
  * @param {Tab} tab
  *     The tab whose browser needs to be returned.
  *
  * @return {Browser}
  *     The linked browser for the tab or null if no browser can be found.
  */
 browser.getBrowserForTab = function(tab) {
   // Fennec
-  if ("browser" in tab) {
+  if (tab && "browser" in tab) {
     return tab.browser;
 
   // Firefox
-  } else if ("linkedBrowser" in tab) {
+  } else if (tab && "linkedBrowser" in tab) {
     return tab.linkedBrowser;
   }
 
   return null;
 };
 
 /**
  * Return the tab browser for the specified chrome window.
@@ -282,27 +283,69 @@ browser.Context = class {
 
   /**
    * Close the current window.
    *
    * @return {Promise}
    *     A promise which is resolved when the current window has been closed.
    */
   closeWindow() {
-    return new Promise(resolve => {
-      // Wait for the window message manager to be destroyed
-      let destroyed = new MessageManagerDestroyedPromise(
-          this.window.messageManager);
+    // Create a copy of the messageManager before it is disconnected
+    let messageManager = this.window.messageManager;
+    let disconnected = waitForObserverTopic("message-manager-disconnect",
+        subject => subject === messageManager);
+    let unloaded = waitForEvent(this.window, "unload");
+
+    this.window.close();
+
+    return Promise.all([disconnected, unloaded]);
+  }
+
+  /**
+   * Open a new browser window.
+   *
+   * @return {Promise}
+   *     A promise resolving to the newly created chrome window.
+   */
+  async openBrowserWindow(focus = false) {
+    switch (this.driver.appName) {
+      case "firefox":
+        // Open new browser window, and wait until it is fully loaded.
+        // Also wait for the window to be focused and activated to prevent a
+        // race condition when promptly focusing to the original window again.
+        let win = this.window.OpenBrowserWindow();
 
-      this.window.addEventListener("unload", async () => {
-        await destroyed;
-        resolve();
-      }, {once: true});
-      this.window.close();
-    });
+        // Bug 1509380 - Missing focus/activate event when Firefox is not
+        // the top-most application. As such wait for the next tick, and
+        // manually focus the newly opened window.
+        win.setTimeout(() => win.focus(), 0);
+
+        let activated = waitForEvent(win, "activate");
+        let focused = waitForEvent(win, "focus", {capture: true});
+        let startup = waitForObserverTopic("browser-delayed-startup-finished",
+            subject => subject == win);
+        await Promise.all([activated, focused, startup]);
+
+        if (!focus) {
+          // The new window shouldn't get focused. As such set the
+          // focus back to the currently selected window.
+          activated = waitForEvent(this.window, "activate");
+          focused = waitForEvent(this.window, "focus", {capture: true});
+
+          this.window.focus();
+
+          await Promise.all([activated, focused]);
+        }
+
+        return win;
+
+      default:
+        throw new UnsupportedOperationError(
+            `openWindow() not supported in ${this.driver.appName}`);
+    }
   }
 
   /**
    * Close the current tab.
    *
    * @return {Promise}
    *     A promise which is resolved when the current tab has been closed.
    *
@@ -314,50 +357,75 @@ browser.Context = class {
     // same if only one remaining tab is open, or no tab selected at all.
     if (!this.tabBrowser ||
         !this.tabBrowser.tabs ||
         this.tabBrowser.tabs.length === 1 ||
         !this.tab) {
       return this.closeWindow();
     }
 
-    return new Promise((resolve, reject) => {
-      // Wait for the browser message manager to be destroyed
-      let browserDetached = async () => {
-        await new MessageManagerDestroyedPromise(this.messageManager);
-        resolve();
-      };
+    // Create a copy of the messageManager before it is disconnected
+    let messageManager = this.messageManager;
+    let disconnected = waitForObserverTopic("message-manager-disconnect",
+        subject => subject === messageManager);
 
-      if (this.tabBrowser.closeTab) {
+    let tabClosed;
+
+    switch (this.driver.appName) {
+      case "fennec":
         // Fennec
-        this.tabBrowser.deck.addEventListener(
-            "TabClose", browserDetached, {once: true});
+        tabClosed = waitForEvent(this.tabBrowser.deck, "TabClose");
         this.tabBrowser.closeTab(this.tab);
+        break;
 
-      } else if (this.tabBrowser.removeTab) {
-        // Firefox
-        this.tab.addEventListener(
-            "TabClose", browserDetached, {once: true});
+      case "firefox":
+        tabClosed = waitForEvent(this.tab, "TabClose");
         this.tabBrowser.removeTab(this.tab);
+        break;
 
-      } else {
-        reject(new UnsupportedOperationError(
-            `closeTab() not supported in ${this.driver.appName}`));
-      }
-    });
+      default:
+        throw new UnsupportedOperationError(
+          `closeTab() not supported in ${this.driver.appName}`);
+    }
+
+    return Promise.all([disconnected, tabClosed]);
   }
 
   /**
-   * Opens a tab with given URI.
-   *
-   * @param {string} uri
-   *      URI to open.
+   * Open a new tab in the currently selected chrome window.
    */
-  addTab(uri) {
-    return this.tabBrowser.addTab(uri, true);
+  async openTab(focus = false) {
+    let tab = null;
+    let tabOpened = waitForEvent(this.window, "TabOpen");
+
+    switch (this.driver.appName) {
+      case "fennec":
+        tab = this.tabBrowser.addTab(null, {selected: focus});
+        break;
+
+      case "firefox":
+        this.window.BrowserOpenTab();
+        tab = this.tabBrowser.selectedTab;
+
+        // The new tab is always selected by default. If focus is not wanted,
+        // the previously tab needs to be selected again.
+        if (!focus) {
+          this.tabBrowser.selectedTab = this.tab;
+        }
+
+        break;
+
+      default:
+        throw new UnsupportedOperationError(
+          `openTab() not supported in ${this.driver.appName}`);
+    }
+
+    await tabOpened;
+
+    return tab;
   }
 
   /**
    * Set the current tab.
    *
    * @param {number=} index
    *     Tab index to switch to. If the parameter is undefined,
    *     the currently selected tab will be used.
@@ -381,26 +449,28 @@ browser.Context = class {
     }
 
     if (typeof index == "undefined") {
       this.tab = this.tabBrowser.selectedTab;
     } else {
       this.tab = this.tabBrowser.tabs[index];
 
       if (focus) {
-        if (this.tabBrowser.selectTab) {
-          // Fennec
-          this.tabBrowser.selectTab(this.tab);
+        switch (this.driver.appName) {
+          case "fennec":
+            this.tabBrowser.selectTab(this.tab);
+            break;
 
-        } else if ("selectedTab" in this.tabBrowser) {
-          // Firefox
-          this.tabBrowser.selectedTab = this.tab;
+          case "firefox":
+            this.tabBrowser.selectedTab = this.tab;
+            break;
 
-        } else {
-          throw new UnsupportedOperationError("switchToTab() not supported");
+          default:
+            throw new UnsupportedOperationError(
+              `switchToTab() not supported in ${this.driver.appName}`);
         }
       }
     }
 
     // TODO(ato): Currently tied to curBrowser, but should be moved to
     // WebElement when introduced by https://bugzil.la/1400256.
     this.eventObserver = new WebElementEventTarget(this.messageManager);
   }
--- a/testing/marionette/client/marionette_driver/marionette.py
+++ b/testing/marionette/client/marionette_driver/marionette.py
@@ -1423,16 +1423,30 @@ class Marionette(object):
         return self._send_message("WebDriver:GetChromeWindowHandles")
 
     @property
     def page_source(self):
         """A string representation of the DOM."""
         return self._send_message("WebDriver:GetPageSource",
                                   key="value")
 
+    def open(self, type=None, focus=False):
+        """Open a new window, or tab based on the specified context type.
+
+        If no context type is given the application will choose the best
+        option based on tab and window support.
+
+        :param type: Type of window to be opened. Can be one of "tab" or "window"
+        :param focus: If true, the opened window will be focused
+
+        :returns: Dict with new window handle, and type of opened window
+        """
+        body = {"type": type, "focus": focus}
+        return self._send_message("WebDriver:NewWindow", body)
+
     def close(self):
         """Close the current window, ending the session if it's the last
         window currently open.
 
         :returns: Unordered list of remaining unique window handles as strings
         """
         return self._send_message("WebDriver:CloseWindow")
 
--- a/testing/marionette/doc/internals/sync.rst
+++ b/testing/marionette/doc/internals/sync.rst
@@ -1,16 +1,21 @@
 sync module
 ===========
 
 Provides an assortment of synchronisation primitives.
 
-.. js:autoclass:: MessageManagerDestroyedPromise
-  :members:
+.. js:autofunction:: executeSoon
 
 .. js:autoclass:: PollPromise
   :members:
 
 .. js:autoclass:: Sleep
   :members:
 
 .. js:autoclass:: TimedPromise
   :members:
+
+.. js:autofunction:: waitForEvent
+
+.. js:autofunction:: waitForMessage
+
+.. js:autofunction:: waitForObserverTopic
--- a/testing/marionette/driver.js
+++ b/testing/marionette/driver.js
@@ -57,16 +57,18 @@ ChromeUtils.import("chrome://marionette/
 const {MarionettePrefs} = ChromeUtils.import("chrome://marionette/content/prefs.js", {});
 ChromeUtils.import("chrome://marionette/content/proxy.js");
 ChromeUtils.import("chrome://marionette/content/reftest.js");
 const {
   DebounceCallback,
   IdlePromise,
   PollPromise,
   TimedPromise,
+  waitForEvent,
+  waitForObserverTopic,
 } = ChromeUtils.import("chrome://marionette/content/sync.js", {});
 
 XPCOMUtils.defineLazyGetter(this, "logger", Log.get);
 XPCOMUtils.defineLazyGlobalGetters(this, ["URL"]);
 
 this.EXPORTED_SYMBOLS = ["GeckoDriver"];
 
 const APP_ID_FIREFOX = "{ec8030f7-c20a-464f-9b0e-13a3a9e97384}";
@@ -101,23 +103,22 @@ const globalMessageManager = Services.mm
  * browsing context's content frame message listener via ListenerProxy.
  *
  * Throughout this prototype, functions with the argument <var>cmd</var>'s
  * documentation refers to the contents of the <code>cmd.parameter</code>
  * object.
  *
  * @class GeckoDriver
  *
- * @param {string} appId
- *     Unique identifier of the application.
  * @param {MarionetteServer} server
  *     The instance of Marionette server.
  */
-this.GeckoDriver = function(appId, server) {
-  this.appId = appId;
+this.GeckoDriver = function(server) {
+  this.appId = Services.appinfo.ID;
+  this.appName = Services.appinfo.name.toLowerCase();
   this._server = server;
 
   this.sessionID = null;
   this.wins = new browser.Windows();
   this.browsers = {};
   // points to current browser
   this.curBrowser = null;
   // top-most chrome window
@@ -1302,16 +1303,17 @@ GeckoDriver.prototype.updateIdForBrowser
  * Retrieves a listener id for the given xul browser element. In case
  * the browser is not known, an attempt is made to retrieve the id from
  * a CPOW, and null is returned if this fails.
  */
 GeckoDriver.prototype.getIdForBrowser = function(browser) {
   if (browser === null) {
     return null;
   }
+
   let permKey = browser.permanentKey;
   if (this._browserIds.has(permKey)) {
     return this._browserIds.get(permKey);
   }
 
   let winId = browser.outerWindowID;
   if (winId) {
     this._browserIds.set(permKey, winId);
@@ -2718,16 +2720,83 @@ GeckoDriver.prototype.deleteCookie = asy
   for (let c of cookie.iter(hostname, pathname)) {
     if (c.name === name) {
       cookie.remove(c);
     }
   }
 };
 
 /**
+ * Open a new top-level browsing context.
+ *
+ * @param {string=} type
+ *     Optional type of the new top-level browsing context. Can be one of
+ *     `tab` or `window`.
+ * @param {boolean=} focus
+ *     Optional flag if the new top-level browsing context should be opened
+ *     in foreground (focused) or background (not focused).
+ *
+ * @return {Object.<string, string>}
+ *     Handle and type of the new browsing context.
+ */
+GeckoDriver.prototype.newWindow = async function(cmd) {
+  assert.open(this.getCurrentWindow(Context.Content));
+  await this._handleUserPrompts();
+
+  let focus = false;
+  if (typeof cmd.parameters.focus != "undefined") {
+    focus = assert.boolean(cmd.parameters.focus,
+        pprint`Expected "focus" to be a boolean, got ${cmd.parameters.focus}`);
+  }
+
+  let type;
+  if (typeof cmd.parameters.type != "undefined") {
+    type = assert.string(cmd.parameters.type,
+        pprint`Expected "type" to be a string, got ${cmd.parameters.type}`);
+  }
+
+  let types = ["tab", "window"];
+  switch (this.appName) {
+    case "firefox":
+      if (typeof type == "undefined" || !types.includes(type)) {
+        type = "window";
+      }
+      break;
+    case "fennec":
+      if (typeof type == "undefined" || !types.includes(type)) {
+        type = "tab";
+      }
+      break;
+  }
+
+  let contentBrowser;
+
+  switch (type) {
+    case "tab":
+      let tab = await this.curBrowser.openTab(focus);
+      contentBrowser = browser.getBrowserForTab(tab);
+      break;
+
+    default:
+      let win = await this.curBrowser.openBrowserWindow(focus);
+      contentBrowser = browser.getTabBrowser(win).selectedBrowser;
+  }
+
+  // Even with the framescript registered, the browser might not be known to
+  // the parent process yet. Wait until it is available.
+  // TODO: Fix by using `Browser:Init` or equivalent on bug 1311041
+  let windowId = await new PollPromise((resolve, reject) => {
+    let id = this.getIdForBrowser(contentBrowser);
+    this.windowHandles.includes(id) ? resolve(id) : reject();
+  });
+
+  return {"handle": windowId.toString(), type};
+};
+
+/**
  * Close the currently selected tab/window.
  *
  * With multiple open tabs present the currently selected tab will
  * be closed.  Otherwise the window itself will be closed. If it is the
  * last window currently open, the window will not be closed to prevent
  * a shutdown of the application. Instead the returned list of window
  * handles is empty.
  *
@@ -3096,46 +3165,42 @@ GeckoDriver.prototype.fullscreenWindow =
 /**
  * Dismisses a currently displayed tab modal, or returns no such alert if
  * no modal is displayed.
  */
 GeckoDriver.prototype.dismissDialog = async function() {
   let win = assert.open(this.getCurrentWindow());
   this._checkIfAlertIsPresent();
 
-  await new Promise(resolve => {
-    win.addEventListener("DOMModalDialogClosed", async () => {
-      await new IdlePromise(win);
-      this.dialog = null;
-      resolve();
-    }, {once: true});
-
-    let {button0, button1} = this.dialog.ui;
-    (button1 ? button1 : button0).click();
-  });
+  let dialogClosed = waitForEvent(win, "DOMModalDialogClosed");
+
+  let {button0, button1} = this.dialog.ui;
+  (button1 ? button1 : button0).click();
+
+  await dialogClosed;
+
+  this.dialog = null;
 };
 
 /**
  * Accepts a currently displayed tab modal, or returns no such alert if
  * no modal is displayed.
  */
 GeckoDriver.prototype.acceptDialog = async function() {
   let win = assert.open(this.getCurrentWindow());
   this._checkIfAlertIsPresent();
 
-  await new Promise(resolve => {
-    win.addEventListener("DOMModalDialogClosed", async () => {
-      await new IdlePromise(win);
-      this.dialog = null;
-      resolve();
-    }, {once: true});
-
-    let {button0} = this.dialog.ui;
-    button0.click();
-  });
+  let dialogClosed = waitForEvent(win, "DOMModalDialogClosed");
+
+  let {button0} = this.dialog.ui;
+  button0.click();
+
+  await dialogClosed;
+
+  this.dialog = null;
 };
 
 /**
  * Returns the message shown in a currently displayed modal, or returns
  * a no such alert error if no modal is currently displayed.
  */
 GeckoDriver.prototype.getTextFromDialog = function() {
   assert.open(this.getCurrentWindow());
@@ -3296,25 +3361,20 @@ GeckoDriver.prototype.quit = async funct
   } else {
     mode = Ci.nsIAppStartup.eAttemptQuit;
   }
 
   this._server.acceptConnections = false;
   this.deleteSession();
 
   // delay response until the application is about to quit
-  let quitApplication = new Promise(resolve => {
-    Services.obs.addObserver(
-        (subject, topic, data) => resolve(data),
-        "quit-application");
-  });
-
+  let quitApplication = waitForObserverTopic("quit-application");
   Services.startup.quit(mode);
 
-  return {cause: await quitApplication};
+  return {cause: (await quitApplication).data};
 };
 
 GeckoDriver.prototype.installAddon = function(cmd) {
   assert.desktop();
 
   let path = cmd.parameters.path;
   let temp = cmd.parameters.temporary || false;
   if (typeof path == "undefined" || typeof path != "string" ||
@@ -3566,16 +3626,17 @@ GeckoDriver.prototype.commands = {
   "WebDriver:GetWindowRect": GeckoDriver.prototype.getWindowRect,
   "WebDriver:IsElementDisplayed": GeckoDriver.prototype.isElementDisplayed,
   "WebDriver:IsElementEnabled": GeckoDriver.prototype.isElementEnabled,
   "WebDriver:IsElementSelected": GeckoDriver.prototype.isElementSelected,
   "WebDriver:MinimizeWindow": GeckoDriver.prototype.minimizeWindow,
   "WebDriver:MaximizeWindow": GeckoDriver.prototype.maximizeWindow,
   "WebDriver:Navigate": GeckoDriver.prototype.get,
   "WebDriver:NewSession": GeckoDriver.prototype.newSession,
+  "WebDriver:NewWindow": GeckoDriver.prototype.newWindow,
   "WebDriver:PerformActions": GeckoDriver.prototype.performActions,
   "WebDriver:Refresh":  GeckoDriver.prototype.refresh,
   "WebDriver:ReleaseActions": GeckoDriver.prototype.releaseActions,
   "WebDriver:SendAlertText": GeckoDriver.prototype.sendKeysToDialog,
   "WebDriver:SetTimeouts": GeckoDriver.prototype.setTimeouts,
   "WebDriver:SetWindowRect": GeckoDriver.prototype.setWindowRect,
   "WebDriver:SwitchToFrame": GeckoDriver.prototype.switchToFrame,
   "WebDriver:SwitchToParentFrame": GeckoDriver.prototype.switchToParentFrame,
--- a/testing/marionette/harness/marionette_harness/runner/mixins/window_manager.py
+++ b/testing/marionette/harness/marionette_harness/runner/mixins/window_manager.py
@@ -1,24 +1,22 @@
 # This Source Code Form is subject to the terms of the Mozilla Public
 # License, v. 2.0. If a copy of the MPL was not distributed with this
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 from __future__ import absolute_import
 
 import sys
 
-from marionette_driver import By, Wait
+from marionette_driver import Wait
 from six import reraise
 
 
 class WindowManagerMixin(object):
 
-    _menu_item_new_tab = (By.ID, "menu_newNavigatorTab")
-
     def setUp(self):
         super(WindowManagerMixin, self).setUp()
 
         self.start_window = self.marionette.current_chrome_window_handle
         self.start_windows = self.marionette.chrome_window_handles
 
         self.start_tab = self.marionette.current_window_handle
         self.start_tabs = self.marionette.window_handles
@@ -55,75 +53,89 @@ class WindowManagerMixin(object):
         current_chrome_window_handles.remove(self.start_window)
 
         for handle in current_chrome_window_handles:
             self.marionette.switch_to_window(handle)
             self.marionette.close_chrome_window()
 
         self.marionette.switch_to_window(self.start_window)
 
-    def open_tab(self, trigger="menu"):
+    def open_tab(self, callback=None, focus=False):
         current_tabs = self.marionette.window_handles
 
         try:
-            if callable(trigger):
-                trigger()
-            elif trigger == 'menu':
-                with self.marionette.using_context("chrome"):
-                    self.marionette.find_element(*self._menu_item_new_tab).click()
+            if callable(callback):
+                callback()
+            else:
+                result = self.marionette.open(type="tab", focus=focus)
+                if result["type"] != "tab":
+                    raise Exception(
+                        "Newly opened browsing context is of type {} and not tab.".format(
+                            result["type"]))
         except Exception:
             exc, val, tb = sys.exc_info()
             reraise(exc, 'Failed to trigger opening a new tab: {}'.format(val), tb)
         else:
             Wait(self.marionette).until(
                 lambda mn: len(mn.window_handles) == len(current_tabs) + 1,
                 message="No new tab has been opened"
             )
 
             [new_tab] = list(set(self.marionette.window_handles) - set(current_tabs))
 
             return new_tab
 
-    def open_window(self, trigger=None):
+    def open_window(self, callback=None, focus=False):
         current_windows = self.marionette.chrome_window_handles
+        current_tabs = self.marionette.window_handles
 
         def loaded(handle):
             with self.marionette.using_context("chrome"):
                 return self.marionette.execute_script("""
                   Components.utils.import("resource://gre/modules/Services.jsm");
 
                   let win = Services.wm.getOuterWindowWithId(Number(arguments[0]));
                   return win.document.readyState == "complete";
                 """, script_args=[handle])
 
         try:
-            if callable(trigger):
-                trigger()
+            if callable(callback):
+                callback()
             else:
-                with self.marionette.using_context("chrome"):
-                    self.marionette.execute_script("OpenBrowserWindow();")
+                result = self.marionette.open(type="window", focus=focus)
+                if result["type"] != "window":
+                    raise Exception(
+                        "Newly opened browsing context is of type {} and not window.".format(
+                            result["type"]))
         except Exception:
             exc, val, tb = sys.exc_info()
             reraise(exc, 'Failed to trigger opening a new window: {}'.format(val), tb)
         else:
             Wait(self.marionette).until(
                 lambda mn: len(mn.chrome_window_handles) == len(current_windows) + 1,
                 message="No new window has been opened"
             )
 
             [new_window] = list(set(self.marionette.chrome_window_handles) - set(current_windows))
 
             # Before continuing ensure the window has been completed loading
             Wait(self.marionette).until(
                 lambda _: loaded(new_window),
                 message="Window with handle '{}'' did not finish loading".format(new_window))
 
-            return new_window
+            # Bug 1507771 - Return the correct handle based on the currently selected context
+            # as long as "WebDriver:NewWindow" is not handled separtely in chrome context
+            context = self.marionette._send_message("Marionette:GetContext", key="value")
+            if context == "chrome":
+                return new_window
+            elif context == "content":
+                [new_tab] = list(set(self.marionette.window_handles) - set(current_tabs))
+                return new_tab
 
-    def open_chrome_window(self, url):
+    def open_chrome_window(self, url, focus=False):
         """Open a new chrome window with the specified chrome URL.
 
         Can be replaced with "WebDriver:NewWindow" once the command
         supports opening generic chrome windows beside browsers (bug 1507771).
         """
         def open_with_js():
             with self.marionette.using_context("chrome"):
                 self.marionette.execute_async_script("""
@@ -161,9 +173,10 @@ class WindowManagerMixin(object):
                     let focused = waitForFocus(window);
                     window.focus();
                     await focused;
 
                     resolve();
                   })();
                 """, script_args=(url,))
 
-        return self.open_window(trigger=open_with_js)
+        with self.marionette.using_context("chrome"):
+            return self.open_window(callback=open_with_js, focus=focus)
--- a/testing/marionette/harness/marionette_harness/tests/harness_unit/test_marionette_runner.py
+++ b/testing/marionette/harness/marionette_harness/tests/harness_unit/test_marionette_runner.py
@@ -1,14 +1,16 @@
 # This Source Code Form is subject to the terms of the Mozilla Public
 # License, v. 2.0. If a copy of the MPL was not distributed with this
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 from __future__ import absolute_import
 
+import os
+
 import manifestparser
 import mozunit
 import pytest
 
 from mock import Mock, patch, mock_open, sentinel, DEFAULT
 
 from marionette_harness.runtests import MarionetteTestRunner
 
@@ -291,17 +293,17 @@ def test_add_test_directory(runner):
     tests = list(dir_contents[0][2] + dir_contents[1][2])
     assert len(runner.tests) == 0
     # Need to use side effect to make isdir return True for test_dir and False for tests
     with patch('os.path.isdir', side_effect=[True] + [False for t in tests]) as isdir:
         with patch('os.walk', return_value=dir_contents) as walk:
             runner.add_test(test_dir)
     assert isdir.called and walk.called
     for test in runner.tests:
-        assert test_dir in test['filepath']
+        assert os.path.normpath(test_dir) in test['filepath']
     assert len(runner.tests) == 2
 
 
 @pytest.mark.parametrize("test_files_exist", [True, False])
 def test_add_test_manifest(mock_runner, manifest_with_tests, monkeypatch, test_files_exist):
     monkeypatch.setattr('marionette_harness.runner.base.TestManifest',
                         manifest_with_tests.manifest_class)
     mock_runner.marionette = mock_runner.driverclass()
--- a/testing/marionette/harness/marionette_harness/tests/unit/test_chrome.py
+++ b/testing/marionette/harness/marionette_harness/tests/unit/test_chrome.py
@@ -1,53 +1,32 @@
-#Copyright 2007-2009 WebDriver committers
-#Copyright 2007-2009 Google Inc.
-#
-#Licensed under the Apache License, Version 2.0 (the "License");
-#you may not use this file except in compliance with the License.
-#You may obtain a copy of the License at
-#
-#     http://www.apache.org/licenses/LICENSE-2.0
-#
-#Unless required by applicable law or agreed to in writing, software
-#distributed under the License is distributed on an "AS IS" BASIS,
-#WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-#See the License for the specific language governing permissions and
-#limitations under the License.
-
 from __future__ import absolute_import
 
-from marionette_driver import By
-
 from marionette_harness import MarionetteTestCase, WindowManagerMixin
 
 
 class ChromeTests(WindowManagerMixin, MarionetteTestCase):
 
     def setUp(self):
         super(ChromeTests, self).setUp()
 
-        self.marionette.set_context('chrome')
-
     def tearDown(self):
         self.close_all_windows()
         super(ChromeTests, self).tearDown()
 
     def test_hang_until_timeout(self):
-        def open_with_menu():
-            menu = self.marionette.find_element(By.ID, 'aboutName')
-            menu.click()
-
-        new_window = self.open_window(trigger=open_with_menu)
+        with self.marionette.using_context("chrome"):
+            new_window = self.open_window()
         self.marionette.switch_to_window(new_window)
 
         try:
             try:
                 # Raise an exception type which should not be thrown by Marionette
                 # while running this test. Otherwise it would mask eg. IOError as
                 # thrown for a socket timeout.
                 raise NotImplementedError('Exception should not cause a hang when '
-                                          'closing the chrome window')
+                                          'closing the chrome window in content '
+                                          'context')
             finally:
                 self.marionette.close_chrome_window()
                 self.marionette.switch_to_window(self.start_window)
         except NotImplementedError:
             pass
--- a/testing/marionette/harness/marionette_harness/tests/unit/test_click.py
+++ b/testing/marionette/harness/marionette_harness/tests/unit/test_click.py
@@ -444,25 +444,21 @@ class TestClickCloseContext(WindowManage
         self.test_page = self.marionette.absolute_url("clicks.html")
 
     def tearDown(self):
         self.close_all_tabs()
 
         super(TestClickCloseContext, self).tearDown()
 
     def test_click_close_tab(self):
-        self.marionette.navigate(self.marionette.absolute_url("windowHandles.html"))
-        tab = self.open_tab(
-            lambda: self.marionette.find_element(By.ID, "new-tab").click())
-        self.marionette.switch_to_window(tab)
+        new_tab = self.open_tab()
+        self.marionette.switch_to_window(new_tab)
 
         self.marionette.navigate(self.test_page)
         self.marionette.find_element(By.ID, "close-window").click()
 
     @skip_if_mobile("Fennec doesn't support other chrome windows")
     def test_click_close_window(self):
-        self.marionette.navigate(self.marionette.absolute_url("windowHandles.html"))
-        win = self.open_window(
-            lambda: self.marionette.find_element(By.ID, "new-window").click())
-        self.marionette.switch_to_window(win)
+        new_tab = self.open_window()
+        self.marionette.switch_to_window(new_tab)
 
         self.marionette.navigate(self.test_page)
         self.marionette.find_element(By.ID, "close-window").click()
--- a/testing/marionette/harness/marionette_harness/tests/unit/test_key_actions.py
+++ b/testing/marionette/harness/marionette_harness/tests/unit/test_key_actions.py
@@ -72,27 +72,8 @@ class TestKeyActions(WindowManagerMixin,
     def test_input_with_wait(self):
         self.key_action.key_down("a").key_down("b").key_down("c").perform()
         (self.key_action.key_down(self.mod_key)
                         .key_down("a")
                         .wait(.5)
                         .key_down("x")
                         .perform())
         self.assertEqual(self.key_reporter_value, "")
-
-    @skip_if_mobile("Interacting with chrome windows not available for Fennec")
-    def test_open_in_new_window_shortcut(self):
-
-        def open_window_with_action():
-            el = self.marionette.find_element(By.TAG_NAME, "a")
-            (self.key_action.key_down(Keys.SHIFT)
-                            .press(el)
-                            .release()
-                            .key_up(Keys.SHIFT)
-                            .perform())
-
-        self.marionette.navigate(inline("<a href='#'>Click</a>"))
-        new_window = self.open_window(trigger=open_window_with_action)
-
-        self.marionette.switch_to_window(new_window)
-        self.marionette.close_chrome_window()
-
-        self.marionette.switch_to_window(self.start_window)
--- a/testing/marionette/harness/marionette_harness/tests/unit/test_navigation.py
+++ b/testing/marionette/harness/marionette_harness/tests/unit/test_navigation.py
@@ -50,23 +50,18 @@ class BaseNavigationTestCase(WindowManag
         self.test_page_remote = self.marionette.absolute_url("test.html")
         self.test_page_slow_resource = self.marionette.absolute_url("slow_resource.html")
 
         if self.marionette.session_capabilities["platformName"] == "mac":
             self.mod_key = Keys.META
         else:
             self.mod_key = Keys.CONTROL
 
-        def open_with_link():
-            link = self.marionette.find_element(By.ID, "new-blank-tab")
-            link.click()
-
         # Always use a blank new tab for an empty history
-        self.marionette.navigate(self.marionette.absolute_url("windowHandles.html"))
-        self.new_tab = self.open_tab(open_with_link)
+        self.new_tab = self.open_tab()
         self.marionette.switch_to_window(self.new_tab)
         Wait(self.marionette, timeout=self.marionette.timeout.page_load).until(
             lambda _: self.history_length == 1,
             message="The newly opened tab doesn't have a browser history length of 1")
 
     def tearDown(self):
         self.marionette.timeout.reset()
         self.marionette.switch_to_parent_frame()
@@ -293,17 +288,16 @@ class TestNavigate(BaseNavigationTestCas
     @skip_if_mobile("Bug 1322993 - Missing temporary folder")
     def test_focus_after_navigation(self):
         self.marionette.restart()
 
         self.marionette.navigate(inline("<input autofocus>"))
         focus_el = self.marionette.find_element(By.CSS_SELECTOR, ":focus")
         self.assertEqual(self.marionette.get_active_element(), focus_el)
 
-    @skip_if_mobile("Needs application independent method to open a new tab")
     def test_no_hang_when_navigating_after_closing_original_tab(self):
         # Close the start tab
         self.marionette.switch_to_window(self.start_tab)
         self.marionette.close()
 
         self.marionette.switch_to_window(self.new_tab)
         self.marionette.navigate(self.test_page_remote)
 
@@ -335,32 +329,16 @@ class TestNavigate(BaseNavigationTestCas
             urlbar.send_keys(self.mod_key + "x")
             urlbar.send_keys(self.test_page_remote + Keys.ENTER)
 
         Wait(self.marionette, timeout=self.marionette.timeout.page_load).until(
             lambda mn: mn.get_url() == self.test_page_remote,
             message="'{}' hasn't been loaded".format(self.test_page_remote))
         self.assertTrue(self.is_remote_tab)
 
-    @skip_if_mobile("On Android no shortcuts are available")
-    def test_navigate_shortcut_key(self):
-
-        def open_with_shortcut():
-            self.marionette.navigate(self.test_page_remote)
-            with self.marionette.using_context("chrome"):
-                main_win = self.marionette.find_element(By.ID, "main-window")
-                main_win.send_keys(self.mod_key, Keys.SHIFT, "a")
-
-        new_tab = self.open_tab(trigger=open_with_shortcut)
-        self.marionette.switch_to_window(new_tab)
-
-        Wait(self.marionette, timeout=self.marionette.timeout.page_load).until(
-            lambda mn: mn.get_url() == "about:addons",
-            message="'about:addons' hasn't been loaded")
-
 
 class TestBackForwardNavigation(BaseNavigationTestCase):
 
     def run_bfcache_test(self, test_pages):
         # Helper method to run simple back and forward testcases.
 
         def check_page_status(page, expected_history_length):
             if "alert_text" in page:
@@ -818,17 +796,17 @@ class TestPageLoadStrategy(BaseNavigatio
         self.marionette.navigate(self.test_page_slow_resource)
         self.assertEqual(self.test_page_slow_resource, self.marionette.get_url())
         self.assertEqual("complete", self.ready_state)
         self.marionette.find_element(By.ID, "slow")
 
     @skip("Bug 1422741 - Causes following tests to fail in loading remote browser")
     @run_if_e10s("Requires e10s mode enabled")
     def test_strategy_after_remoteness_change(self):
-        """Bug 1378191 - Reset of capabilities after listener reload"""
+        """Bug 1378191 - Reset of capabilities after listener reload."""
         self.marionette.delete_session()
         self.marionette.start_session({"pageLoadStrategy": "eager"})
 
         # Trigger a remoteness change which will reload the listener script
         self.assertTrue(self.is_remote_tab, "Initial tab doesn't have remoteness flag set")
         self.marionette.navigate("about:robots")
         self.assertFalse(self.is_remote_tab, "Tab has remoteness flag set")
         self.marionette.navigate(self.test_page_slow_resource)
--- a/testing/marionette/harness/marionette_harness/tests/unit/test_screenshot.py
+++ b/testing/marionette/harness/marionette_harness/tests/unit/test_screenshot.py
@@ -248,17 +248,18 @@ class TestScreenCaptureChrome(WindowMana
             self.marionette.navigate(box)
             content_element = self.marionette.find_element(By.ID, "green")
 
         self.assertRaisesRegexp(NoSuchElementException, "Web element reference not seen before",
                                 self.marionette.screenshot, highlights=[content_element])
 
         chrome_document_element = self.document_element
         with self.marionette.using_context('content'):
-            self.assertRaisesRegexp(NoSuchElementException, "Web element reference not seen before",
+            self.assertRaisesRegexp(NoSuchElementException,
+                                    "Web element reference not seen before",
                                     self.marionette.screenshot,
                                     highlights=[chrome_document_element])
 
 
 class TestScreenCaptureContent(WindowManagerMixin, ScreenCaptureTestCase):
 
     def setUp(self):
         super(TestScreenCaptureContent, self).setUp()
@@ -269,20 +270,19 @@ class TestScreenCaptureContent(WindowMan
         super(TestScreenCaptureContent, self).tearDown()
 
     @property
     def scroll_dimensions(self):
         return tuple(self.marionette.execute_script("""
             return [document.body.scrollWidth, document.body.scrollHeight]
             """))
 
-    @skip_if_mobile("Needs application independent method to open a new tab")
     def test_capture_tab_already_closed(self):
-        tab = self.open_tab()
-        self.marionette.switch_to_window(tab)
+        new_tab = self.open_tab()
+        self.marionette.switch_to_window(new_tab)
         self.marionette.close()
 
         self.assertRaises(NoSuchWindowException, self.marionette.screenshot)
         self.marionette.switch_to_window(self.start_tab)
 
     @skip_if_mobile("Bug 1487124 - Android need its own maximum allowed dimensions")
     def test_capture_vertical_bounds(self):
         self.marionette.navigate(inline("<body style='margin-top: 32768px'>foo"))
--- a/testing/marionette/harness/marionette_harness/tests/unit/test_switch_window_chrome.py
+++ b/testing/marionette/harness/marionette_harness/tests/unit/test_switch_window_chrome.py
@@ -1,20 +1,19 @@
 # This Source Code Form is subject to the terms of the Mozilla Public
 # License, v. 2.0. If a copy of the MPL was not distributed with this file,
 # You can obtain one at http://mozilla.org/MPL/2.0/.
 
 from __future__ import absolute_import
 
 import os
 import sys
+
 from unittest import skipIf
 
-from marionette_driver import By
-
 # add this directory to the path
 sys.path.append(os.path.dirname(__file__))
 
 from test_switch_window_content import TestSwitchToWindowContent
 
 
 class TestSwitchWindowChrome(TestSwitchToWindowContent):
 
@@ -23,124 +22,75 @@ class TestSwitchWindowChrome(TestSwitchT
 
         self.marionette.set_context("chrome")
 
     def tearDown(self):
         self.close_all_windows()
 
         super(TestSwitchWindowChrome, self).tearDown()
 
-    def open_window_in_background(self):
-        with self.marionette.using_context("chrome"):
-            self.marionette.execute_async_script("""
-              let callback = arguments[0];
-              (async function() {
-                function promiseEvent(target, type, args) {
-                  return new Promise(r => {
-                    let params = Object.assign({once: true}, args);
-                    target.addEventListener(type, r, params);
-                  });
-                }
-                function promiseWindowFocus(w) {
-                  return Promise.all([
-                    promiseEvent(w, "focus", {capture: true}),
-                    promiseEvent(w, "activate"),
-                  ]);
-                }
-                // Open a window, wait for it to receive focus
-                let win = OpenBrowserWindow();
-                await promiseWindowFocus(win);
-
-                // Now refocus our original window and wait for that to happen.
-                let windowFocusPromise = promiseWindowFocus(window);
-                window.focus();
-                return windowFocusPromise;
-              })().then(() => {
-                // can't just pass `callback`, as we can't JSON-ify the events it'd get passed.
-                callback()
-              });
-            """)
-
-    def open_window_in_foreground(self):
-        with self.marionette.using_context("content"):
-            self.marionette.navigate(self.test_page)
-            link = self.marionette.find_element(By.ID, "new-window")
-            link.click()
-
+    @skipIf(sys.platform.startswith("linux"),
+            "Bug 1511970 - New window isn't moved to the background on Linux")
     def test_switch_tabs_for_new_background_window_without_focus_change(self):
-        # Open an addition tab in the original window so we can better check
+        # Open an additional tab in the original window so we can better check
         # the selected index in thew new window to be opened.
-        second_tab = self.open_tab(trigger=self.open_tab_in_foreground)
+        second_tab = self.open_tab(focus=True)
         self.marionette.switch_to_window(second_tab, focus=True)
         second_tab_index = self.get_selected_tab_index()
         self.assertNotEqual(second_tab_index, self.selected_tab_index)
 
-        # Opens a new background window, but we are interested in the tab
-        tab_in_new_window = self.open_tab(trigger=self.open_window_in_background)
+        # Open a new background window, but we are interested in the tab
+        with self.marionette.using_context("content"):
+            tab_in_new_window = self.open_window()
         self.assertEqual(self.marionette.current_window_handle, second_tab)
         self.assertEqual(self.marionette.current_chrome_window_handle, self.start_window)
         self.assertEqual(self.get_selected_tab_index(), second_tab_index)
-        with self.marionette.using_context("content"):
-            self.assertEqual(self.marionette.get_url(), self.empty_page)
 
         # Switch to the tab in the new window but don't focus it
         self.marionette.switch_to_window(tab_in_new_window, focus=False)
         self.assertEqual(self.marionette.current_window_handle, tab_in_new_window)
         self.assertNotEqual(self.marionette.current_chrome_window_handle, self.start_window)
         self.assertEqual(self.get_selected_tab_index(), second_tab_index)
-        with self.marionette.using_context("content"):
-            self.assertEqual(self.marionette.get_url(), "about:blank")
 
     def test_switch_tabs_for_new_foreground_window_with_focus_change(self):
         # Open an addition tab in the original window so we can better check
         # the selected index in thew new window to be opened.
-        second_tab = self.open_tab(trigger=self.open_tab_in_foreground)
+        second_tab = self.open_tab()
         self.marionette.switch_to_window(second_tab, focus=True)
         second_tab_index = self.get_selected_tab_index()
         self.assertNotEqual(second_tab_index, self.selected_tab_index)
 
         # Opens a new window, but we are interested in the tab
-        tab_in_new_window = self.open_tab(trigger=self.open_window_in_foreground)
+        with self.marionette.using_context("content"):
+            tab_in_new_window = self.open_window(focus=True)
         self.assertEqual(self.marionette.current_window_handle, second_tab)
         self.assertEqual(self.marionette.current_chrome_window_handle, self.start_window)
         self.assertNotEqual(self.get_selected_tab_index(), second_tab_index)
-        with self.marionette.using_context("content"):
-            self.assertEqual(self.marionette.get_url(), self.test_page)
 
         self.marionette.switch_to_window(tab_in_new_window)
         self.assertEqual(self.marionette.current_window_handle, tab_in_new_window)
         self.assertNotEqual(self.marionette.current_chrome_window_handle, self.start_window)
         self.assertNotEqual(self.get_selected_tab_index(), second_tab_index)
-        with self.marionette.using_context("content"):
-            self.assertEqual(self.marionette.get_url(), self.empty_page)
 
         self.marionette.switch_to_window(second_tab, focus=True)
         self.assertEqual(self.marionette.current_window_handle, second_tab)
         self.assertEqual(self.marionette.current_chrome_window_handle, self.start_window)
         # Bug 1335085 - The focus doesn't change even as requested so.
         # self.assertEqual(self.get_selected_tab_index(), second_tab_index)
-        with self.marionette.using_context("content"):
-            self.assertEqual(self.marionette.get_url(), self.test_page)
 
     def test_switch_tabs_for_new_foreground_window_without_focus_change(self):
         # Open an addition tab in the original window so we can better check
         # the selected index in thew new window to be opened.
-        second_tab = self.open_tab(trigger=self.open_tab_in_foreground)
+        second_tab = self.open_tab()
         self.marionette.switch_to_window(second_tab, focus=True)
         second_tab_index = self.get_selected_tab_index()
         self.assertNotEqual(second_tab_index, self.selected_tab_index)
 
-        # Opens a new window, but we are interested in the tab which automatically
-        # gets the focus.
-        self.open_tab(trigger=self.open_window_in_foreground)
+        self.open_window(focus=True)
         self.assertEqual(self.marionette.current_window_handle, second_tab)
         self.assertEqual(self.marionette.current_chrome_window_handle, self.start_window)
         self.assertNotEqual(self.get_selected_tab_index(), second_tab_index)
-        with self.marionette.using_context("content"):
-            self.assertEqual(self.marionette.get_url(), self.test_page)
 
         # Switch to the second tab in the first window, but don't focus it.
         self.marionette.switch_to_window(second_tab, focus=False)
         self.assertEqual(self.marionette.current_window_handle, second_tab)
         self.assertEqual(self.marionette.current_chrome_window_handle, self.start_window)
         self.assertNotEqual(self.get_selected_tab_index(), second_tab_index)
-        with self.marionette.using_context("content"):
-            self.assertEqual(self.marionette.get_url(), self.test_page)
--- a/testing/marionette/harness/marionette_harness/tests/unit/test_switch_window_content.py
+++ b/testing/marionette/harness/marionette_harness/tests/unit/test_switch_window_content.py
@@ -1,38 +1,32 @@
 # This Source Code Form is subject to the terms of the Mozilla ublic
 # License, v. 2.0. If a copy of the MPL was not distributed with this file,
 # You can obtain one at http://mozilla.org/MPL/2.0/.
 
 from __future__ import absolute_import
 
-from marionette_driver import Actions, By, Wait
+from marionette_driver import By
 from marionette_driver.keys import Keys
 
-from marionette_harness import MarionetteTestCase, skip_if_mobile, WindowManagerMixin
+from marionette_harness import MarionetteTestCase, WindowManagerMixin
 
 
 class TestSwitchToWindowContent(WindowManagerMixin, MarionetteTestCase):
 
     def setUp(self):
         super(TestSwitchToWindowContent, self).setUp()
 
         if self.marionette.session_capabilities["platformName"] == "mac":
             self.mod_key = Keys.META
         else:
             self.mod_key = Keys.CONTROL
 
-        self.empty_page = self.marionette.absolute_url("empty.html")
-        self.test_page = self.marionette.absolute_url("windowHandles.html")
-
         self.selected_tab_index = self.get_selected_tab_index()
 
-        with self.marionette.using_context("content"):
-            self.marionette.navigate(self.test_page)
-
     def tearDown(self):
         self.close_all_tabs()
 
         super(TestSwitchToWindowContent, self).tearDown()
 
     def get_selected_tab_index(self):
         with self.marionette.using_context("chrome"):
             return self.marionette.execute_script("""
@@ -64,110 +58,90 @@ class TestSwitchToWindowContent(WindowMa
 
                 for (let i = 0; i < tabBrowser.tabs.length; i++) {
                   if (tabBrowser.tabs[i] == tabBrowser.selectedTab) {
                     return i;
                   }
                 }
             """)
 
-    def open_tab_in_background(self):
-        with self.marionette.using_context("content"):
-            link = self.marionette.find_element(By.ID, "new-tab")
-
-            action = Actions(self.marionette)
-            action.key_down(self.mod_key).click(link).perform()
-
-    def open_tab_in_foreground(self):
-        with self.marionette.using_context("content"):
-            link = self.marionette.find_element(By.ID, "new-tab")
-            link.click()
-
     def test_switch_tabs_with_focus_change(self):
-        new_tab = self.open_tab(self.open_tab_in_foreground)
+        new_tab = self.open_tab(focus=True)
         self.assertEqual(self.marionette.current_window_handle, self.start_tab)
         self.assertNotEqual(self.get_selected_tab_index(), self.selected_tab_index)
-        with self.marionette.using_context("content"):
-            self.assertEqual(self.marionette.get_url(), self.test_page)
 
+        # Switch to new tab first because it is already selected
         self.marionette.switch_to_window(new_tab)
         self.assertEqual(self.marionette.current_window_handle, new_tab)
         self.assertNotEqual(self.get_selected_tab_index(), self.selected_tab_index)
 
-        with self.marionette.using_context("content"):
-            Wait(self.marionette, timeout=self.marionette.timeout.page_load).until(
-                lambda _: self.marionette.get_url() == self.empty_page,
-                message="{} has been loaded in the newly opened tab.".format(self.empty_page))
-
+        # Switch to original tab by explicitely setting the focus
         self.marionette.switch_to_window(self.start_tab, focus=True)
         self.assertEqual(self.marionette.current_window_handle, self.start_tab)
         self.assertEqual(self.get_selected_tab_index(), self.selected_tab_index)
-        with self.marionette.using_context("content"):
-            self.assertEqual(self.marionette.get_url(), self.test_page)
-
-        self.marionette.switch_to_window(new_tab)
-        self.marionette.close()
-        self.marionette.switch_to_window(self.start_tab)
-
-        self.assertEqual(self.marionette.current_window_handle, self.start_tab)
-        self.assertEqual(self.get_selected_tab_index(), self.selected_tab_index)
-        with self.marionette.using_context("content"):
-            self.assertEqual(self.marionette.get_url(), self.test_page)
-
-    def test_switch_tabs_without_focus_change(self):
-        new_tab = self.open_tab(self.open_tab_in_foreground)
-        self.assertEqual(self.marionette.current_window_handle, self.start_tab)
-        self.assertNotEqual(self.get_selected_tab_index(), self.selected_tab_index)
-        with self.marionette.using_context("content"):
-            self.assertEqual(self.marionette.get_url(), self.test_page)
-
-        # Switch to new tab first because it is already selected
-        self.marionette.switch_to_window(new_tab)
-        self.assertEqual(self.marionette.current_window_handle, new_tab)
-
-        self.marionette.switch_to_window(self.start_tab, focus=False)
-        self.assertEqual(self.marionette.current_window_handle, self.start_tab)
-        self.assertNotEqual(self.get_selected_tab_index(), self.selected_tab_index)
-
-        with self.marionette.using_context("content"):
-            self.assertEqual(self.marionette.get_url(), self.test_page)
 
         self.marionette.switch_to_window(new_tab)
         self.marionette.close()
 
         self.marionette.switch_to_window(self.start_tab)
         self.assertEqual(self.marionette.current_window_handle, self.start_tab)
         self.assertEqual(self.get_selected_tab_index(), self.selected_tab_index)
-        with self.marionette.using_context("content"):
-            self.assertEqual(self.marionette.get_url(), self.test_page)
+
+    def test_switch_tabs_without_focus_change(self):
+        new_tab = self.open_tab(focus=True)
+        self.assertEqual(self.marionette.current_window_handle, self.start_tab)
+        self.assertNotEqual(self.get_selected_tab_index(), self.selected_tab_index)
+
+        # Switch to new tab first because it is already selected
+        self.marionette.switch_to_window(new_tab)
+        self.assertEqual(self.marionette.current_window_handle, new_tab)
+
+        # Switch to original tab by explicitely not setting the focus
+        self.marionette.switch_to_window(self.start_tab, focus=False)
+        self.assertEqual(self.marionette.current_window_handle, self.start_tab)
+        self.assertNotEqual(self.get_selected_tab_index(), self.selected_tab_index)
+
+        self.marionette.switch_to_window(new_tab)
+        self.marionette.close()
+
+        self.marionette.switch_to_window(self.start_tab)
+        self.assertEqual(self.marionette.current_window_handle, self.start_tab)
+        self.assertEqual(self.get_selected_tab_index(), self.selected_tab_index)
 
     def test_switch_from_content_to_chrome_window_should_not_change_selected_tab(self):
-        new_tab = self.open_tab(self.open_tab_in_foreground)
+        new_tab = self.open_tab(focus=True)
 
         self.marionette.switch_to_window(new_tab)
         self.assertEqual(self.marionette.current_window_handle, new_tab)
         new_tab_index = self.get_selected_tab_index()
 
         self.marionette.switch_to_window(self.start_window)
         self.assertEqual(self.marionette.current_window_handle, new_tab)
         self.assertEqual(self.get_selected_tab_index(), new_tab_index)
 
-    @skip_if_mobile("New windows not supported in Fennec")
-    def test_switch_to_new_private_browsing_window_has_to_register_browsers(self):
+    def test_switch_to_new_private_browsing_tab(self):
         # Test that tabs (browsers) are correctly registered for a newly opened
-        # private browsing window. This has to also happen without explicitely
+        # private browsing window/tab. This has to also happen without explicitely
         # switching to the tab itself before using any commands in content scope.
         #
         # Note: Not sure why this only affects private browsing windows only.
+        new_tab = self.open_tab(focus=True)
+        self.marionette.switch_to_window(new_tab)
 
-        def open_private_browsing_window():
+        def open_private_browsing_window_firefox():
             with self.marionette.using_context("content"):
-                self.marionette.navigate("about:privatebrowsing")
-                button = self.marionette.find_element(By.ID, "startPrivateBrowsing")
-                button.click()
+                self.marionette.find_element(By.ID, "startPrivateBrowsing").click()
 
-        new_window = self.open_window(open_private_browsing_window)
-        self.marionette.switch_to_window(new_window)
-        self.assertEqual(self.marionette.current_chrome_window_handle, new_window)
-        self.assertNotEqual(self.marionette.current_window_handle, self.start_tab)
+        def open_private_browsing_tab_fennec():
+            with self.marionette.using_context("content"):
+                self.marionette.find_element(By.ID, "newPrivateTabLink").click()
 
         with self.marionette.using_context("content"):
-            self.marionette.execute_script(" return true; ")
+            self.marionette.navigate("about:privatebrowsing")
+            if self.marionette.session_capabilities["browserName"] == "fennec":
+                new_pb_tab = self.open_tab(open_private_browsing_tab_fennec)
+            else:
+                new_pb_tab = self.open_tab(open_private_browsing_window_firefox)
+
+        self.marionette.switch_to_window(new_pb_tab)
+        self.assertEqual(self.marionette.current_window_handle, new_pb_tab)
+
+        self.marionette.execute_script(" return true; ")
--- a/testing/marionette/harness/marionette_harness/tests/unit/test_window_close_chrome.py
+++ b/testing/marionette/harness/marionette_harness/tests/unit/test_window_close_chrome.py
@@ -16,24 +16,24 @@ class TestCloseWindow(WindowManagerMixin
 
     def tearDown(self):
         self.close_all_windows()
         self.close_all_tabs()
 
         super(TestCloseWindow, self).tearDown()
 
     def test_close_chrome_window_for_browser_window(self):
-        win = self.open_window()
-        self.marionette.switch_to_window(win)
+        new_window = self.open_window()
+        self.marionette.switch_to_window(new_window)
 
-        self.assertNotIn(win, self.marionette.window_handles)
+        self.assertNotIn(new_window, self.marionette.window_handles)
         chrome_window_handles = self.marionette.close_chrome_window()
-        self.assertNotIn(win, chrome_window_handles)
+        self.assertNotIn(new_window, chrome_window_handles)
         self.assertListEqual(self.start_windows, chrome_window_handles)
-        self.assertNotIn(win, self.marionette.window_handles)
+        self.assertNotIn(new_window, self.marionette.window_handles)
 
     def test_close_chrome_window_for_non_browser_window(self):
         win = self.open_chrome_window("chrome://marionette/content/test.xul")
         self.marionette.switch_to_window(win)
 
         self.assertIn(win, self.marionette.chrome_window_handles)
         self.assertNotIn(win, self.marionette.window_handles)
         chrome_window_handles = self.marionette.close_chrome_window()
@@ -45,30 +45,30 @@ class TestCloseWindow(WindowManagerMixin
         self.close_all_windows()
 
         self.assertListEqual([], self.marionette.close_chrome_window())
         self.assertListEqual([self.start_tab], self.marionette.window_handles)
         self.assertListEqual([self.start_window], self.marionette.chrome_window_handles)
         self.assertIsNotNone(self.marionette.session)
 
     def test_close_window_for_browser_tab(self):
-        tab = self.open_tab()
-        self.marionette.switch_to_window(tab)
+        new_tab = self.open_tab()
+        self.marionette.switch_to_window(new_tab)
 
         window_handles = self.marionette.close()
-        self.assertNotIn(tab, window_handles)
+        self.assertNotIn(new_tab, window_handles)
         self.assertListEqual(self.start_tabs, window_handles)
 
     def test_close_window_for_browser_window_with_single_tab(self):
-        win = self.open_window()
-        self.marionette.switch_to_window(win)
+        new_window = self.open_window()
+        self.marionette.switch_to_window(new_window)
 
         self.assertEqual(len(self.start_tabs) + 1, len(self.marionette.window_handles))
         window_handles = self.marionette.close()
-        self.assertNotIn(win, window_handles)
+        self.assertNotIn(new_window, window_handles)
         self.assertListEqual(self.start_tabs, window_handles)
         self.assertListEqual(self.start_windows, self.marionette.chrome_window_handles)
 
     def test_close_window_for_last_open_tab(self):
         self.close_all_tabs()
 
         self.assertListEqual([], self.marionette.close())
         self.assertListEqual([self.start_tab], self.marionette.window_handles)
--- a/testing/marionette/harness/marionette_harness/tests/unit/test_window_close_content.py
+++ b/testing/marionette/harness/marionette_harness/tests/unit/test_window_close_content.py
@@ -19,98 +19,97 @@ class TestCloseWindow(WindowManagerMixin
     def tearDown(self):
         self.close_all_windows()
         self.close_all_tabs()
 
         super(TestCloseWindow, self).tearDown()
 
     @skip_if_mobile("Interacting with chrome windows not available for Fennec")
     def test_close_chrome_window_for_browser_window(self):
-        win = self.open_window()
-        self.marionette.switch_to_window(win)
+        with self.marionette.using_context("chrome"):
+            new_window = self.open_window()
+        self.marionette.switch_to_window(new_window)
 
-        self.assertNotIn(win, self.marionette.window_handles)
+        self.assertIn(new_window, self.marionette.chrome_window_handles)
         chrome_window_handles = self.marionette.close_chrome_window()
-        self.assertNotIn(win, chrome_window_handles)
+        self.assertNotIn(new_window, chrome_window_handles)
         self.assertListEqual(self.start_windows, chrome_window_handles)
-        self.assertNotIn(win, self.marionette.window_handles)
+        self.assertNotIn(new_window, self.marionette.window_handles)
 
     @skip_if_mobile("Interacting with chrome windows not available for Fennec")
     def test_close_chrome_window_for_non_browser_window(self):
-        win = self.open_chrome_window("chrome://marionette/content/test.xul")
-        self.marionette.switch_to_window(win)
+        new_window = self.open_chrome_window("chrome://marionette/content/test.xul")
+        self.marionette.switch_to_window(new_window)
 
-        self.assertIn(win, self.marionette.chrome_window_handles)
-        self.assertNotIn(win, self.marionette.window_handles)
+        self.assertIn(new_window, self.marionette.chrome_window_handles)
+        self.assertNotIn(new_window, self.marionette.window_handles)
         chrome_window_handles = self.marionette.close_chrome_window()
-        self.assertNotIn(win, chrome_window_handles)
+        self.assertNotIn(new_window, chrome_window_handles)
         self.assertListEqual(self.start_windows, chrome_window_handles)
-        self.assertNotIn(win, self.marionette.window_handles)
+        self.assertNotIn(new_window, self.marionette.window_handles)
 
     @skip_if_mobile("Interacting with chrome windows not available for Fennec")
     def test_close_chrome_window_for_last_open_window(self):
         self.close_all_windows()
 
         self.assertListEqual([], self.marionette.close_chrome_window())
         self.assertListEqual([self.start_tab], self.marionette.window_handles)
         self.assertListEqual([self.start_window], self.marionette.chrome_window_handles)
         self.assertIsNotNone(self.marionette.session)
 
-    @skip_if_mobile("Needs application independent method to open a new tab")
     def test_close_window_for_browser_tab(self):
-        tab = self.open_tab()
-        self.marionette.switch_to_window(tab)
+        new_tab = self.open_tab()
+        self.marionette.switch_to_window(new_tab)
 
         window_handles = self.marionette.close()
-        self.assertNotIn(tab, window_handles)
+        self.assertNotIn(new_tab, window_handles)
         self.assertListEqual(self.start_tabs, window_handles)
 
-    @skip_if_mobile("Needs application independent method to open a new tab")
     def test_close_window_with_dismissed_beforeunload_prompt(self):
-        tab = self.open_tab()
-        self.marionette.switch_to_window(tab)
+        new_tab = self.open_tab()
+        self.marionette.switch_to_window(new_tab)
 
         self.marionette.navigate(inline("""
           <input type="text">
           <script>
             window.addEventListener("beforeunload", function (event) {
               event.preventDefault();
             });
           </script>
         """))
 
         self.marionette.find_element(By.TAG_NAME, "input").send_keys("foo")
         self.marionette.close()
 
     @skip_if_mobile("Interacting with chrome windows not available for Fennec")
     def test_close_window_for_browser_window_with_single_tab(self):
-        win = self.open_window()
-        self.marionette.switch_to_window(win)
+        new_tab = self.open_window()
+        self.marionette.switch_to_window(new_tab)
 
-        self.assertEqual(len(self.start_tabs) + 1, len(self.marionette.window_handles))
+        self.assertEqual(len(self.marionette.window_handles), len(self.start_tabs) + 1)
         window_handles = self.marionette.close()
-        self.assertNotIn(win, window_handles)
+        self.assertNotIn(new_tab, window_handles)
         self.assertListEqual(self.start_tabs, window_handles)
         self.assertListEqual(self.start_windows, self.marionette.chrome_window_handles)
 
     def test_close_window_for_last_open_tab(self):
         self.close_all_tabs()
 
         self.assertListEqual([], self.marionette.close())
         self.assertListEqual([self.start_tab], self.marionette.window_handles)
         self.assertListEqual([self.start_window], self.marionette.chrome_window_handles)
         self.assertIsNotNone(self.marionette.session)
 
     @skip_if_mobile("discardBrowser is only available in Firefox")
     def test_close_browserless_tab(self):
         self.close_all_tabs()
 
         test_page = self.marionette.absolute_url("windowHandles.html")
-        tab = self.open_tab()
-        self.marionette.switch_to_window(tab)
+        new_tab = self.open_tab()
+        self.marionette.switch_to_window(new_tab)
         self.marionette.navigate(test_page)
         self.marionette.switch_to_window(self.start_tab)
 
         with self.marionette.using_context("chrome"):
             self.marionette.execute_async_script("""
               Components.utils.import("resource:///modules/BrowserWindowTracker.jsm");
 
               let win = BrowserWindowTracker.getTopWindow();
--- a/testing/marionette/harness/marionette_harness/tests/unit/test_window_handles_chrome.py
+++ b/testing/marionette/harness/marionette_harness/tests/unit/test_window_handles_chrome.py
@@ -1,29 +1,27 @@
 # This Source Code Form is subject to the terms of the Mozilla Public
 # License, v. 2.0. If a copy of the MPL was not distributed with this
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 from __future__ import absolute_import
 
 import types
 
-from marionette_driver import By, errors, Wait
+from marionette_driver import errors
 
 from marionette_harness import MarionetteTestCase, WindowManagerMixin
 
 
 class TestWindowHandles(WindowManagerMixin, MarionetteTestCase):
 
     def setUp(self):
         super(TestWindowHandles, self).setUp()
 
-        self.empty_page = self.marionette.absolute_url("empty.html")
-        self.test_page = self.marionette.absolute_url("windowHandles.html")
-        self.marionette.navigate(self.test_page)
+        self.xul_dialog = "chrome://marionette/content/test_dialog.xul"
 
         self.marionette.set_context("chrome")
 
     def tearDown(self):
         self.close_all_windows()
         self.close_all_tabs()
 
         super(TestWindowHandles, self).tearDown()
@@ -37,265 +35,171 @@ class TestWindowHandles(WindowManagerMix
 
         for handle in self.marionette.chrome_window_handles:
             self.assertIsInstance(handle, types.StringTypes)
 
         for handle in self.marionette.window_handles:
             self.assertIsInstance(handle, types.StringTypes)
 
     def test_chrome_window_handles_with_scopes(self):
-        # Open a browser and a non-browser (about window) chrome window
-        self.open_window(
-            trigger=lambda: self.marionette.execute_script("OpenBrowserWindow();"))
+        new_browser = self.open_window()
         self.assert_window_handles()
         self.assertEqual(len(self.marionette.chrome_window_handles), len(self.start_windows) + 1)
+        self.assertIn(new_browser, self.marionette.chrome_window_handles)
         self.assertEqual(self.marionette.current_chrome_window_handle, self.start_window)
 
-        self.open_window(
-            trigger=lambda: self.marionette.find_element(By.ID, "aboutName").click())
+        new_dialog = self.open_chrome_window(self.xul_dialog)
         self.assert_window_handles()
         self.assertEqual(len(self.marionette.chrome_window_handles), len(self.start_windows) + 2)
+        self.assertIn(new_dialog, self.marionette.chrome_window_handles)
         self.assertEqual(self.marionette.current_chrome_window_handle, self.start_window)
 
         chrome_window_handles_in_chrome_scope = self.marionette.chrome_window_handles
         window_handles_in_chrome_scope = self.marionette.window_handles
 
         with self.marionette.using_context("content"):
             self.assertEqual(self.marionette.chrome_window_handles,
                              chrome_window_handles_in_chrome_scope)
             self.assertEqual(self.marionette.window_handles,
                              window_handles_in_chrome_scope)
 
-    def test_chrome_window_handles_after_opening_new_dialog(self):
-        xul_dialog = "chrome://marionette/content/test_dialog.xul"
-        new_win = self.open_chrome_window(xul_dialog)
+    def test_chrome_window_handles_after_opening_new_chrome_window(self):
+        new_window = self.open_chrome_window(self.xul_dialog)
         self.assert_window_handles()
         self.assertEqual(len(self.marionette.chrome_window_handles), len(self.start_windows) + 1)
+        self.assertIn(new_window, self.marionette.chrome_window_handles)
         self.assertEqual(self.marionette.current_chrome_window_handle, self.start_window)
 
-        # Check that the new tab has the correct page loaded
-        self.marionette.switch_to_window(new_win)
+        # Check that the new chrome window has the correct URL loaded
+        self.marionette.switch_to_window(new_window)
         self.assert_window_handles()
-        self.assertEqual(self.marionette.current_chrome_window_handle, new_win)
-        self.assertEqual(self.marionette.get_url(), xul_dialog)
+        self.assertEqual(self.marionette.current_chrome_window_handle, new_window)
+        self.assertEqual(self.marionette.get_url(), self.xul_dialog)
 
-        # Close the opened dialog and carry on in our original tab.
+        # Close the chrome window, and carry on in our original window.
         self.marionette.close_chrome_window()
         self.assert_window_handles()
         self.assertEqual(len(self.marionette.chrome_window_handles), len(self.start_windows))
+        self.assertNotIn(new_window, self.marionette.chrome_window_handles)
 
         self.marionette.switch_to_window(self.start_window)
         self.assert_window_handles()
         self.assertEqual(self.marionette.current_chrome_window_handle, self.start_window)
-        with self.marionette.using_context("content"):
-            self.assertEqual(self.marionette.get_url(), self.test_page)
 
     def test_chrome_window_handles_after_opening_new_window(self):
-        def open_with_link():
-            with self.marionette.using_context("content"):
-                link = self.marionette.find_element(By.ID, "new-window")
-                link.click()
-
-        # We open a new window but are actually interested in the new tab
-        new_win = self.open_window(trigger=open_with_link)
+        new_window = self.open_window()
         self.assert_window_handles()
         self.assertEqual(len(self.marionette.chrome_window_handles), len(self.start_windows) + 1)
+        self.assertIn(new_window, self.marionette.chrome_window_handles)
         self.assertEqual(self.marionette.current_chrome_window_handle, self.start_window)
 
-        # Check that the new tab has the correct page loaded
-        self.marionette.switch_to_window(new_win)
+        self.marionette.switch_to_window(new_window)
         self.assert_window_handles()
-        self.assertEqual(self.marionette.current_chrome_window_handle, new_win)
-        with self.marionette.using_context("content"):
-            Wait(self.marionette, timeout=self.marionette.timeout.page_load).until(
-                lambda mn: mn.get_url() == self.empty_page,
-                message="{} did not load after opening a new tab".format(self.empty_page))
+        self.assertEqual(self.marionette.current_chrome_window_handle, new_window)
 
-        # Ensure navigate works in our current window
-        other_page = self.marionette.absolute_url("test.html")
-        with self.marionette.using_context("content"):
-            self.marionette.navigate(other_page)
-            self.assertEqual(self.marionette.get_url(), other_page)
-
-        # Close the opened window and carry on in our original tab.
+        # Close the opened window and carry on in our original window.
         self.marionette.close()
         self.assert_window_handles()
         self.assertEqual(len(self.marionette.chrome_window_handles), len(self.start_windows))
+        self.assertNotIn(new_window, self.marionette.chrome_window_handles)
 
         self.marionette.switch_to_window(self.start_window)
         self.assert_window_handles()
         self.assertEqual(self.marionette.current_chrome_window_handle, self.start_window)
-        with self.marionette.using_context("content"):
-            self.assertEqual(self.marionette.get_url(), self.test_page)
 
     def test_window_handles_after_opening_new_tab(self):
-        def open_with_link():
-            with self.marionette.using_context("content"):
-                link = self.marionette.find_element(By.ID, "new-tab")
-                link.click()
-
-        new_tab = self.open_tab(trigger=open_with_link)
+        with self.marionette.using_context("content"):
+            new_tab = self.open_tab()
         self.assert_window_handles()
         self.assertEqual(len(self.marionette.window_handles), len(self.start_tabs) + 1)
+        self.assertIn(new_tab, self.marionette.window_handles)
         self.assertEqual(self.marionette.current_window_handle, self.start_tab)
 
         self.marionette.switch_to_window(new_tab)
         self.assert_window_handles()
         self.assertEqual(self.marionette.current_window_handle, new_tab)
-        with self.marionette.using_context("content"):
-            Wait(self.marionette, timeout=self.marionette.timeout.page_load).until(
-                lambda mn: mn.get_url() == self.empty_page,
-                message="{} did not load after opening a new tab".format(self.empty_page))
-
-        # Ensure navigate works in our current tab
-        other_page = self.marionette.absolute_url("test.html")
-        with self.marionette.using_context("content"):
-            self.marionette.navigate(other_page)
-            self.assertEqual(self.marionette.get_url(), other_page)
 
         self.marionette.switch_to_window(self.start_tab)
         self.assert_window_handles()
         self.assertEqual(self.marionette.current_window_handle, self.start_tab)
-        with self.marionette.using_context("content"):
-            self.assertEqual(self.marionette.get_url(), self.test_page)
 
         self.marionette.switch_to_window(new_tab)
         self.marionette.close()
         self.assert_window_handles()
         self.assertEqual(len(self.marionette.window_handles), len(self.start_tabs))
+        self.assertNotIn(new_tab, self.marionette.window_handles)
 
         self.marionette.switch_to_window(self.start_tab)
+        self.assert_window_handles()
+        self.assertEqual(self.marionette.current_window_handle, self.start_tab)
+
+    def test_window_handles_after_opening_new_foreground_tab(self):
+        with self.marionette.using_context("content"):
+            new_tab = self.open_tab(focus=True)
+        self.assert_window_handles()
+        self.assertEqual(len(self.marionette.window_handles), len(self.start_tabs) + 1)
+        self.assertIn(new_tab, self.marionette.window_handles)
         self.assertEqual(self.marionette.current_window_handle, self.start_tab)
 
-    def test_window_handles_after_opening_new_dialog(self):
-        xul_dialog = "chrome://marionette/content/test_dialog.xul"
-        new_win = self.open_chrome_window(xul_dialog)
+        # We still have the default tab set as our window handle. This
+        # get_url command should be sent immediately, and not be forever-queued.
+        with self.marionette.using_context("content"):
+            self.marionette.get_url()
+
+        self.marionette.switch_to_window(new_tab)
+        self.assert_window_handles()
+        self.assertEqual(self.marionette.current_window_handle, new_tab)
+
+        self.marionette.close()
         self.assert_window_handles()
         self.assertEqual(len(self.marionette.window_handles), len(self.start_tabs))
+        self.assertNotIn(new_tab, self.marionette.window_handles)
+
+        self.marionette.switch_to_window(self.start_tab)
+        self.assert_window_handles()
         self.assertEqual(self.marionette.current_window_handle, self.start_tab)
 
-        self.marionette.switch_to_window(new_win)
+    def test_window_handles_after_opening_new_chrome_window(self):
+        new_window = self.open_chrome_window(self.xul_dialog)
         self.assert_window_handles()
-        self.assertEqual(self.marionette.get_url(), xul_dialog)
+        self.assertEqual(len(self.marionette.window_handles), len(self.start_tabs))
+        self.assertNotIn(new_window, self.marionette.window_handles)
+        self.assertEqual(self.marionette.current_window_handle, self.start_tab)
+
+        self.marionette.switch_to_window(new_window)
+        self.assert_window_handles()
+        self.assertEqual(self.marionette.get_url(), self.xul_dialog)
 
         # Check that the opened dialog is not accessible via window handles
         with self.assertRaises(errors.NoSuchWindowException):
             self.marionette.current_window_handle
         with self.assertRaises(errors.NoSuchWindowException):
             self.marionette.close()
 
         # Close the dialog and carry on in our original tab.
         self.marionette.close_chrome_window()
         self.assert_window_handles()
         self.assertEqual(len(self.marionette.window_handles), len(self.start_tabs))
 
         self.marionette.switch_to_window(self.start_tab)
         self.assert_window_handles()
         self.assertEqual(self.marionette.current_window_handle, self.start_tab)
+
+    def test_window_handles_after_closing_original_tab(self):
         with self.marionette.using_context("content"):
-            self.assertEqual(self.marionette.get_url(), self.test_page)
-
-    def test_window_handles_after_opening_new_window(self):
-        def open_with_link():
-            with self.marionette.using_context("content"):
-                link = self.marionette.find_element(By.ID, "new-window")
-                link.click()
-
-        # We open a new window but are actually interested in the new tab
-        new_tab = self.open_tab(trigger=open_with_link)
+            new_tab = self.open_tab()
         self.assert_window_handles()
         self.assertEqual(len(self.marionette.window_handles), len(self.start_tabs) + 1)
-        self.assertEqual(self.marionette.current_window_handle, self.start_tab)
-
-        # Check that the new tab has the correct page loaded
-        self.marionette.switch_to_window(new_tab)
-        self.assert_window_handles()
-        self.assertEqual(self.marionette.current_window_handle, new_tab)
-        with self.marionette.using_context("content"):
-            Wait(self.marionette, timeout=self.marionette.timeout.page_load).until(
-                lambda mn: mn.get_url() == self.empty_page,
-                message="{} did not load after opening a new tab".format(self.empty_page))
-
-        # Ensure navigate works in our current window
-        other_page = self.marionette.absolute_url("test.html")
-        with self.marionette.using_context("content"):
-            self.marionette.navigate(other_page)
-            self.assertEqual(self.marionette.get_url(), other_page)
-
-        # Close the opened window and carry on in our original tab.
-        self.marionette.close()
-        self.assert_window_handles()
-        self.assertEqual(len(self.marionette.window_handles), len(self.start_tabs))
-
-        self.marionette.switch_to_window(self.start_tab)
-        self.assert_window_handles()
-        self.assertEqual(self.marionette.current_window_handle, self.start_tab)
-        with self.marionette.using_context("content"):
-            self.assertEqual(self.marionette.get_url(), self.test_page)
-
-    def test_window_handles_after_closing_original_tab(self):
-        def open_with_link():
-            with self.marionette.using_context("content"):
-                link = self.marionette.find_element(By.ID, "new-tab")
-                link.click()
-
-        new_tab = self.open_tab(trigger=open_with_link)
-        self.assert_window_handles()
-        self.assertEqual(len(self.marionette.window_handles), len(self.start_tabs) + 1)
+        self.assertIn(new_tab, self.marionette.window_handles)
         self.assertEqual(self.marionette.current_window_handle, self.start_tab)
 
         self.marionette.close()
         self.assert_window_handles()
         self.assertEqual(len(self.marionette.window_handles), len(self.start_tabs))
-
-        self.marionette.switch_to_window(new_tab)
-        self.assert_window_handles()
-        self.assertEqual(self.marionette.current_window_handle, new_tab)
-        with self.marionette.using_context("content"):
-            Wait(self.marionette, timeout=self.marionette.timeout.page_load).until(
-                lambda mn: mn.get_url() == self.empty_page,
-                message="{} did not load after opening a new tab".format(self.empty_page))
-
-    def test_window_handles_no_switch(self):
-        """Regression test for bug 1294456.
-        This test is testing the case where Marionette attempts to send a
-        command to a window handle when the browser has opened and selected
-        a new tab. Before bug 1294456 landed, the Marionette driver was getting
-        confused about which window handle the client cared about, and assumed
-        it was the window handle for the newly opened and selected tab.
-
-        This caused Marionette to think that the browser needed to do a remoteness
-        flip in the e10s case, since the tab opened by menu_newNavigatorTab is
-        about:newtab (which is currently non-remote). This meant that commands
-        sent to what should have been the original window handle would be
-        queued and never sent, since the remoteness flip in the new tab was
-        never going to happen.
-        """
-        def open_with_menu():
-            menu_new_tab = self.marionette.find_element(By.ID, 'menu_newNavigatorTab')
-            menu_new_tab.click()
-
-        new_tab = self.open_tab(trigger=open_with_menu)
-        self.assert_window_handles()
-
-        # We still have the default tab set as our window handle. This
-        # get_url command should be sent immediately, and not be forever-queued.
-        with self.marionette.using_context("content"):
-            self.assertEqual(self.marionette.get_url(), self.test_page)
-
-        self.assertEqual(len(self.marionette.window_handles), len(self.start_tabs) + 1)
-        self.assertEqual(self.marionette.current_window_handle, self.start_tab)
+        self.assertIn(new_tab, self.marionette.window_handles)
 
         self.marionette.switch_to_window(new_tab)
         self.assert_window_handles()
         self.assertEqual(self.marionette.current_window_handle, new_tab)
 
-        self.marionette.close()
-        self.assert_window_handles()
-        self.assertEqual(len(self.marionette.window_handles), len(self.start_tabs))
-
-        self.marionette.switch_to_window(self.start_tab)
-        self.assert_window_handles()
-        self.assertEqual(self.marionette.current_window_handle, self.start_tab)
-
     def test_window_handles_after_closing_last_window(self):
         self.close_all_windows()
         self.assertEqual(self.marionette.close_chrome_window(), [])
--- a/testing/marionette/harness/marionette_harness/tests/unit/test_window_handles_content.py
+++ b/testing/marionette/harness/marionette_harness/tests/unit/test_window_handles_content.py
@@ -2,135 +2,96 @@
 # License, v. 2.0. If a copy of the MPL was not distributed with this
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 from __future__ import absolute_import
 
 import types
 import urllib
 
-from marionette_driver import By, errors, Wait
+from marionette_driver import errors
 
 from marionette_harness import MarionetteTestCase, skip_if_mobile, WindowManagerMixin
 
 
 def inline(doc):
     return "data:text/html;charset=utf-8,{}".format(urllib.quote(doc))
 
 
 class TestWindowHandles(WindowManagerMixin, MarionetteTestCase):
 
     def setUp(self):
         super(TestWindowHandles, self).setUp()
 
-        self.empty_page = self.marionette.absolute_url("empty.html")
-        self.test_page = self.marionette.absolute_url("windowHandles.html")
-        self.marionette.navigate(self.test_page)
+        self.xul_dialog = "chrome://marionette/content/test_dialog.xul"
 
     def tearDown(self):
         self.close_all_tabs()
 
         super(TestWindowHandles, self).tearDown()
 
     def assert_window_handles(self):
         try:
             self.assertIsInstance(self.marionette.current_window_handle, types.StringTypes)
         except errors.NoSuchWindowException:
             pass
 
         for handle in self.marionette.window_handles:
             self.assertIsInstance(handle, types.StringTypes)
 
-    def test_window_handles_after_opening_new_tab(self):
-        def open_with_link():
-            link = self.marionette.find_element(By.ID, "new-tab")
-            link.click()
-
-        new_tab = self.open_tab(trigger=open_with_link)
+    def tst_window_handles_after_opening_new_tab(self):
+        new_tab = self.open_tab()
         self.assert_window_handles()
         self.assertEqual(len(self.marionette.window_handles), len(self.start_tabs) + 1)
         self.assertEqual(self.marionette.current_window_handle, self.start_tab)
 
         self.marionette.switch_to_window(new_tab)
         self.assert_window_handles()
         self.assertEqual(self.marionette.current_window_handle, new_tab)
-        Wait(self.marionette, timeout=self.marionette.timeout.page_load).until(
-            lambda mn: mn.get_url() == self.empty_page,
-            message="{} did not load after opening a new tab".format(self.empty_page))
 
         self.marionette.switch_to_window(self.start_tab)
         self.assertEqual(self.marionette.current_window_handle, self.start_tab)
-        self.assertEqual(self.marionette.get_url(), self.test_page)
 
         self.marionette.switch_to_window(new_tab)
         self.marionette.close()
         self.assert_window_handles()
         self.assertEqual(len(self.marionette.window_handles), len(self.start_tabs))
 
         self.marionette.switch_to_window(self.start_tab)
         self.assert_window_handles()
         self.assertEqual(self.marionette.current_window_handle, self.start_tab)
 
-    def test_window_handles_after_opening_new_browser_window(self):
-        def open_with_link():
-            link = self.marionette.find_element(By.ID, "new-window")
-            link.click()
-
-        # We open a new window but are actually interested in the new tab
-        new_tab = self.open_tab(trigger=open_with_link)
+    def tst_window_handles_after_opening_new_browser_window(self):
+        new_tab = self.open_window()
         self.assert_window_handles()
         self.assertEqual(len(self.marionette.window_handles), len(self.start_tabs) + 1)
         self.assertEqual(self.marionette.current_window_handle, self.start_tab)
 
-        # Check that the new tab has the correct page loaded
         self.marionette.switch_to_window(new_tab)
         self.assert_window_handles()
         self.assertEqual(self.marionette.current_window_handle, new_tab)
-        Wait(self.marionette, self.marionette.timeout.page_load).until(
-            lambda _: self.marionette.get_url() == self.empty_page,
-            message="The expected page '{}' has not been loaded".format(self.empty_page))
-
-        # Ensure navigate works in our current window
-        other_page = self.marionette.absolute_url("test.html")
-        self.marionette.navigate(other_page)
-        self.assertEqual(self.marionette.get_url(), other_page)
 
         # Close the opened window and carry on in our original tab.
         self.marionette.close()
         self.assert_window_handles()
         self.assertEqual(len(self.marionette.window_handles), len(self.start_tabs))
 
         self.marionette.switch_to_window(self.start_tab)
         self.assert_window_handles()
         self.assertEqual(self.marionette.current_window_handle, self.start_tab)
-        self.assertEqual(self.marionette.get_url(), self.test_page)
 
     @skip_if_mobile("Fennec doesn't support other chrome windows")
-    def test_window_handles_after_opening_new_non_browser_window(self):
-        def open_with_link():
-            self.marionette.navigate(inline("""
-              <a id="blob-download" download="foo.html">Download</a>
-
-              <script>
-                const string = "test";
-                const blob = new Blob([string], { type: "text/html" });
-
-                const link = document.getElementById("blob-download");
-                link.href = URL.createObjectURL(blob);
-              </script>
-            """))
-            link = self.marionette.find_element(By.ID, "blob-download")
-            link.click()
-
-        new_win = self.open_window(trigger=open_with_link)
+    def tst_window_handles_after_opening_new_non_browser_window(self):
+        new_window = self.open_chrome_window(self.xul_dialog)
         self.assert_window_handles()
         self.assertEqual(len(self.marionette.window_handles), len(self.start_tabs))
         self.assertEqual(self.marionette.current_window_handle, self.start_tab)
+        self.assertNotIn(new_window, self.marionette.window_handles)
 
-        self.marionette.switch_to_window(new_win)
+        self.marionette.switch_to_window(new_window)
         self.assert_window_handles()
 
         # Check that the opened window is not accessible via window handles
         with self.assertRaises(errors.NoSuchWindowException):
             self.marionette.current_window_handle
         with self.assertRaises(errors.NoSuchWindowException):
             self.marionette.close()
 
@@ -139,31 +100,26 @@ class TestWindowHandles(WindowManagerMix
         self.assert_window_handles()
         self.assertEqual(len(self.marionette.window_handles), len(self.start_tabs))
 
         self.marionette.switch_to_window(self.start_tab)
         self.assert_window_handles()
         self.assertEqual(self.marionette.current_window_handle, self.start_tab)
 
     def test_window_handles_after_closing_original_tab(self):
-        def open_with_link():
-            link = self.marionette.find_element(By.ID, "new-tab")
-            link.click()
-
-        new_tab = self.open_tab(trigger=open_with_link)
+        new_tab = self.open_tab()
         self.assert_window_handles()
         self.assertEqual(len(self.marionette.window_handles), len(self.start_tabs) + 1)
         self.assertEqual(self.marionette.current_window_handle, self.start_tab)
+        self.assertIn(new_tab, self.marionette.window_handles)
 
         self.marionette.close()
         self.assert_window_handles()
         self.assertEqual(len(self.marionette.window_handles), len(self.start_tabs))
+        self.assertNotIn(self.start_tab, self.marionette.window_handles)
 
         self.marionette.switch_to_window(new_tab)
         self.assert_window_handles()
         self.assertEqual(self.marionette.current_window_handle, new_tab)
-        Wait(self.marionette, self.marionette.timeout.page_load).until(
-            lambda _: self.marionette.get_url() == self.empty_page,
-            message="The expected page '{}' has not been loaded".format(self.empty_page))
 
-    def test_window_handles_after_closing_last_tab(self):
+    def tst_window_handles_after_closing_last_tab(self):
         self.close_all_tabs()
         self.assertEqual(self.marionette.close(), [])
--- a/testing/marionette/harness/marionette_harness/tests/unit/test_window_management.py
+++ b/testing/marionette/harness/marionette_harness/tests/unit/test_window_management.py
@@ -16,93 +16,87 @@ class TestNoSuchWindowContent(WindowMana
         super(TestNoSuchWindowContent, self).setUp()
 
     def tearDown(self):
         self.close_all_tabs()
         super(TestNoSuchWindowContent, self).tearDown()
 
     @skip_if_mobile("Fennec doesn't support other chrome windows")
     def test_closed_chrome_window(self):
-
-        def open_with_link():
-            with self.marionette.using_context("content"):
-                test_page = self.marionette.absolute_url("windowHandles.html")
-                self.marionette.navigate(test_page)
-                self.marionette.find_element(By.ID, "new-window").click()
-
-        win = self.open_window(open_with_link)
-        self.marionette.switch_to_window(win)
+        with self.marionette.using_context("chrome"):
+            new_window = self.open_window()
+        self.marionette.switch_to_window(new_window)
         self.marionette.close_chrome_window()
 
         # When closing a browser window both handles are not available
         for context in ("chrome", "content"):
             with self.marionette.using_context(context):
                 with self.assertRaises(NoSuchWindowException):
                     self.marionette.current_chrome_window_handle
                 with self.assertRaises(NoSuchWindowException):
                     self.marionette.current_window_handle
 
         self.marionette.switch_to_window(self.start_window)
 
         with self.assertRaises(NoSuchWindowException):
-            self.marionette.switch_to_window(win)
+            self.marionette.switch_to_window(new_window)
 
     @skip_if_mobile("Fennec doesn't support other chrome windows")
     def test_closed_chrome_window_while_in_frame(self):
-        win = self.open_chrome_window("chrome://marionette/content/test.xul")
-        self.marionette.switch_to_window(win)
+        new_window = self.open_chrome_window("chrome://marionette/content/test.xul")
+        self.marionette.switch_to_window(new_window)
         with self.marionette.using_context("chrome"):
             self.marionette.switch_to_frame("iframe")
         self.marionette.close_chrome_window()
 
         with self.assertRaises(NoSuchWindowException):
             self.marionette.current_window_handle
         with self.assertRaises(NoSuchWindowException):
             self.marionette.current_chrome_window_handle
 
         self.marionette.switch_to_window(self.start_window)
 
         with self.assertRaises(NoSuchWindowException):
-            self.marionette.switch_to_window(win)
+            self.marionette.switch_to_window(new_window)
 
     def test_closed_tab(self):
-        with self.marionette.using_context("content"):
-            tab = self.open_tab()
-            self.marionette.switch_to_window(tab)
-            self.marionette.close()
+        new_tab = self.open_tab()
+        self.marionette.switch_to_window(new_tab)
+        self.marionette.close()
 
         # Check that only the content window is not available in both contexts
         for context in ("chrome", "content"):
             with self.marionette.using_context(context):
                 with self.assertRaises(NoSuchWindowException):
                     self.marionette.current_window_handle
                 self.marionette.current_chrome_window_handle
 
         self.marionette.switch_to_window(self.start_tab)
 
         with self.assertRaises(NoSuchWindowException):
-            self.marionette.switch_to_window(tab)
+            self.marionette.switch_to_window(new_tab)
 
     def test_closed_tab_while_in_frame(self):
+        new_tab = self.open_tab()
+        self.marionette.switch_to_window(new_tab)
+
         with self.marionette.using_context("content"):
-            tab = self.open_tab()
-            self.marionette.switch_to_window(tab)
             self.marionette.navigate(self.marionette.absolute_url("test_iframe.html"))
             frame = self.marionette.find_element(By.ID, "test_iframe")
             self.marionette.switch_to_frame(frame)
-            self.marionette.close()
+        self.marionette.close()
 
-            with self.assertRaises(NoSuchWindowException):
-                self.marionette.current_window_handle
-            self.marionette.current_chrome_window_handle
+        with self.assertRaises(NoSuchWindowException):
+            self.marionette.current_window_handle
+        self.marionette.current_chrome_window_handle
 
         self.marionette.switch_to_window(self.start_tab)
 
         with self.assertRaises(NoSuchWindowException):
-            self.marionette.switch_to_window(tab)
+            self.marionette.switch_to_window(new_tab)
 
 
 class TestNoSuchWindowChrome(TestNoSuchWindowContent):
 
     def setUp(self):
         super(TestNoSuchWindowChrome, self).setUp()
         self.marionette.set_context("chrome")
 
@@ -116,47 +110,27 @@ class TestSwitchWindow(WindowManagerMixi
     def setUp(self):
         super(TestSwitchWindow, self).setUp()
         self.marionette.set_context("chrome")
 
     def tearDown(self):
         self.close_all_windows()
         super(TestSwitchWindow, self).tearDown()
 
-    def test_windows(self):
-        def open_browser_with_js():
-            self.marionette.execute_script(" window.open(); ")
-
-        new_window = self.open_window(trigger=open_browser_with_js)
+    def test_switch_window_after_open_and_close(self):
+        with self.marionette.using_context("chrome"):
+            new_window = self.open_window()
+        self.assertEqual(len(self.marionette.chrome_window_handles), len(self.start_windows) + 1)
+        self.assertIn(new_window, self.marionette.chrome_window_handles)
         self.assertEqual(self.marionette.current_chrome_window_handle, self.start_window)
 
-        # switch to the other window
+        # switch to the new chrome window and close it
         self.marionette.switch_to_window(new_window)
         self.assertEqual(self.marionette.current_chrome_window_handle, new_window)
         self.assertNotEqual(self.marionette.current_chrome_window_handle, self.start_window)
 
-        # switch back and close original window
+        self.marionette.close_chrome_window()
+        self.assertEqual(len(self.marionette.chrome_window_handles), len(self.start_windows))
+        self.assertNotIn(new_window, self.marionette.chrome_window_handles)
+
+        # switch back to the original chrome window
         self.marionette.switch_to_window(self.start_window)
         self.assertEqual(self.marionette.current_chrome_window_handle, self.start_window)
-        self.marionette.close_chrome_window()
-
-        self.assertNotIn(self.start_window, self.marionette.chrome_window_handles)
-        self.assertEqual(len(self.marionette.chrome_window_handles), len(self.start_windows))
-
-    def test_should_load_and_close_a_window(self):
-        def open_window_with_link():
-            test_html = self.marionette.absolute_url("test_windows.html")
-            with self.marionette.using_context("content"):
-                self.marionette.navigate(test_html)
-                self.marionette.find_element(By.LINK_TEXT, "Open new window").click()
-
-        new_window = self.open_window(trigger=open_window_with_link)
-        self.marionette.switch_to_window(new_window)
-        self.assertEqual(self.marionette.current_chrome_window_handle, new_window)
-        self.assertEqual(len(self.marionette.chrome_window_handles), 2)
-
-        with self.marionette.using_context('content'):
-            self.assertEqual(self.marionette.title, "We Arrive Here")
-
-        # Let's close and check
-        self.marionette.close_chrome_window()
-        self.marionette.switch_to_window(self.start_window)
-        self.assertEqual(len(self.marionette.chrome_window_handles), 1)
--- a/testing/marionette/harness/marionette_harness/tests/unit/test_window_status_content.py
+++ b/testing/marionette/harness/marionette_harness/tests/unit/test_window_status_content.py
@@ -10,100 +10,87 @@ from marionette_driver.errors import NoS
 from marionette_harness import MarionetteTestCase, WindowManagerMixin, skip_if_mobile
 
 
 class TestNoSuchWindowContent(WindowManagerMixin, MarionetteTestCase):
 
     def setUp(self):
         super(TestNoSuchWindowContent, self).setUp()
 
-        self.test_page = self.marionette.absolute_url("windowHandles.html")
-        with self.marionette.using_context("content"):
-            self.marionette.navigate(self.test_page)
-
     def tearDown(self):
         self.close_all_windows()
         super(TestNoSuchWindowContent, self).tearDown()
 
-    def open_tab_in_foreground(self):
-        with self.marionette.using_context("content"):
-            link = self.marionette.find_element(By.ID, "new-tab")
-            link.click()
-
     @skip_if_mobile("Fennec doesn't support other chrome windows")
     def test_closed_chrome_window(self):
-
-        def open_with_link():
-            with self.marionette.using_context("content"):
-                test_page = self.marionette.absolute_url("windowHandles.html")
-                self.marionette.navigate(test_page)
-                self.marionette.find_element(By.ID, "new-window").click()
-
-        win = self.open_window(open_with_link)
-        self.marionette.switch_to_window(win)
+        with self.marionette.using_context("chrome"):
+            new_window = self.open_window()
+        self.marionette.switch_to_window(new_window)
         self.marionette.close_chrome_window()
 
         # When closing a browser window both handles are not available
         for context in ("chrome", "content"):
             with self.marionette.using_context(context):
                 with self.assertRaises(NoSuchWindowException):
                     self.marionette.current_chrome_window_handle
                 with self.assertRaises(NoSuchWindowException):
                     self.marionette.current_window_handle
 
         self.marionette.switch_to_window(self.start_window)
 
         with self.assertRaises(NoSuchWindowException):
-            self.marionette.switch_to_window(win)
+            self.marionette.switch_to_window(new_window)
 
     @skip_if_mobile("Fennec doesn't support other chrome windows")
     def test_closed_chrome_window_while_in_frame(self):
-        win = self.open_chrome_window("chrome://marionette/content/test.xul")
-        self.marionette.switch_to_window(win)
+        new_window = self.open_chrome_window("chrome://marionette/content/test.xul")
+        self.marionette.switch_to_window(new_window)
+
         with self.marionette.using_context("chrome"):
             self.marionette.switch_to_frame("iframe")
         self.marionette.close_chrome_window()
 
         with self.assertRaises(NoSuchWindowException):
             self.marionette.current_window_handle
         with self.assertRaises(NoSuchWindowException):
             self.marionette.current_chrome_window_handle
 
         self.marionette.switch_to_window(self.start_window)
 
         with self.assertRaises(NoSuchWindowException):
-            self.marionette.switch_to_window(win)
+            self.marionette.switch_to_window(new_window)
 
     def test_closed_tab(self):
-        with self.marionette.using_context("content"):
-            tab = self.open_tab(self.open_tab_in_foreground)
-            self.marionette.switch_to_window(tab)
-            self.marionette.close()
+        new_tab = self.open_tab(focus=True)
+        self.marionette.switch_to_window(new_tab)
+        self.marionette.close()
 
         # Check that only the content window is not available in both contexts
         for context in ("chrome", "content"):
             with self.marionette.using_context(context):
                 with self.assertRaises(NoSuchWindowException):
                     self.marionette.current_window_handle
                 self.marionette.current_chrome_window_handle
 
         self.marionette.switch_to_window(self.start_tab)
 
         with self.assertRaises(NoSuchWindowException):
-            self.marionette.switch_to_window(tab)
+            self.marionette.switch_to_window(new_tab)
 
     def test_closed_tab_while_in_frame(self):
+        new_tab = self.open_tab()
+        self.marionette.switch_to_window(new_tab)
+
         with self.marionette.using_context("content"):
-            tab = self.open_tab(self.open_tab_in_foreground)
-            self.marionette.switch_to_window(tab)
             self.marionette.navigate(self.marionette.absolute_url("test_iframe.html"))
             frame = self.marionette.find_element(By.ID, "test_iframe")
             self.marionette.switch_to_frame(frame)
-            self.marionette.close()
+
+        self.marionette.close()
 
-            with self.assertRaises(NoSuchWindowException):
-                self.marionette.current_window_handle
-            self.marionette.current_chrome_window_handle
+        with self.assertRaises(NoSuchWindowException):
+            self.marionette.current_window_handle
+        self.marionette.current_chrome_window_handle
 
         self.marionette.switch_to_window(self.start_tab)
 
         with self.assertRaises(NoSuchWindowException):
-            self.marionette.switch_to_window(tab)
+            self.marionette.switch_to_window(new_tab)
--- a/testing/marionette/proxy.js
+++ b/testing/marionette/proxy.js
@@ -10,17 +10,17 @@ ChromeUtils.import("resource://gre/modul
 const {
   error,
   WebDriverError,
 } = ChromeUtils.import("chrome://marionette/content/error.js", {});
 ChromeUtils.import("chrome://marionette/content/evaluate.js");
 const {Log} = ChromeUtils.import("chrome://marionette/content/log.js", {});
 ChromeUtils.import("chrome://marionette/content/modal.js");
 const {
-  MessageManagerDestroyedPromise,
+  waitForObserverTopic,
 } = ChromeUtils.import("chrome://marionette/content/sync.js", {});
 
 this.EXPORTED_SYMBOLS = ["proxy"];
 
 XPCOMUtils.defineLazyGetter(this, "log", Log.get);
 XPCOMUtils.defineLazyServiceGetter(
     this, "uuidgen", "@mozilla.org/uuid-generator;1", "nsIUUIDGenerator");
 
@@ -151,17 +151,19 @@ proxy.AsyncMessageChannel = class {
           case "unload":
             messageManager = this.browser.window.messageManager;
             break;
           case "TabClose":
             messageManager = this.browser.messageManager;
             break;
         }
 
-        await new MessageManagerDestroyedPromise(messageManager);
+        await waitForObserverTopic("message-manager-disconnect",
+            subject => subject === messageManager);
+
         this.removeHandlers();
         resolve();
       };
 
       // A modal or tab modal dialog has been opened. To be able to handle it,
       // the active command has to be aborted. Therefore remove all handlers,
       // and cancel any ongoing requests in the listener.
       this.dialogueObserver_ = (subject, topic) => {
--- a/testing/marionette/server.js
+++ b/testing/marionette/server.js
@@ -6,17 +6,16 @@
 
 const CC = Components.Constructor;
 
 const ServerSocket = CC(
     "@mozilla.org/network/server-socket;1",
     "nsIServerSocket",
     "initSpecialConnection");
 
-ChromeUtils.import("resource://gre/modules/Services.jsm");
 ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
 
 ChromeUtils.import("chrome://marionette/content/assert.js");
 const {GeckoDriver} = ChromeUtils.import("chrome://marionette/content/driver.js", {});
 const {WebElement} = ChromeUtils.import("chrome://marionette/content/element.js", {});
 const {
   error,
   UnknownCommandError,
@@ -69,17 +68,17 @@ class TCPListener {
    *
    * Determines the application to initialise the driver with.
    *
    * @return {GeckoDriver}
    *     A driver instance.
    */
   driverFactory() {
     MarionettePrefs.contentListener = false;
-    return new GeckoDriver(Services.appinfo.ID, this);
+    return new GeckoDriver(this);
   }
 
   set acceptConnections(value) {
     if (value) {
       if (!this.socket) {
         try {
           const flags = KeepWhenOffline | LoopbackOnly;
           const backlog = 1;
--- a/testing/marionette/sync.js
+++ b/testing/marionette/sync.js
@@ -8,33 +8,52 @@ ChromeUtils.import("resource://gre/modul
 ChromeUtils.import("resource://gre/modules/Services.jsm");
 ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
 
 const {
   error,
   stack,
   TimeoutError,
 } = ChromeUtils.import("chrome://marionette/content/error.js", {});
+const {truncate} = ChromeUtils.import("chrome://marionette/content/format.js", {});
 const {Log} = ChromeUtils.import("chrome://marionette/content/log.js", {});
 
 XPCOMUtils.defineLazyGetter(this, "log", Log.get);
 
 this.EXPORTED_SYMBOLS = [
+  "executeSoon",
   "DebounceCallback",
   "IdlePromise",
-  "MessageManagerDestroyedPromise",
   "PollPromise",
   "Sleep",
   "TimedPromise",
+  "waitForEvent",
+  "waitForMessage",
+  "waitForObserverTopic",
 ];
 
 const {TYPE_ONE_SHOT, TYPE_REPEATING_SLACK} = Ci.nsITimer;
 
 const PROMISE_TIMEOUT = AppConstants.DEBUG ? 4500 : 1500;
 
+
+/**
+ * Dispatch a function to be executed on the main thread.
+ *
+ * @param {function} func
+ *     Function to be executed.
+ */
+function executeSoon(func) {
+  if (typeof func != "function") {
+    throw new TypeError();
+  }
+
+  Services.tm.dispatchToMainThread(func);
+}
+
 /**
  * @callback Condition
  *
  * @param {function(*)} resolve
  *     To be called when the condition has been met.  Will return the
  *     resolved value.
  * @param {function} reject
  *     To be called when the condition has not been met.  Will cause
@@ -232,56 +251,16 @@ function TimedPromise(fn,
 function Sleep(timeout) {
   if (typeof timeout != "number") {
     throw new TypeError();
   }
   return new TimedPromise(() => {}, {timeout, throws: null});
 }
 
 /**
- * Detects when the specified message manager has been destroyed.
- *
- * One can observe the removal and detachment of a content browser
- * (`<xul:browser>`) or a chrome window by its message manager
- * disconnecting.
- *
- * When a browser is associated with a tab, this is safer than only
- * relying on the event `TabClose` which signalises the _intent to_
- * remove a tab and consequently would lead to the destruction of
- * the content browser and its browser message manager.
- *
- * When closing a chrome window it is safer than only relying on
- * the event 'unload' which signalises the _intent to_ close the
- * chrome window and consequently would lead to the destruction of
- * the window and its window message manager.
- *
- * @param {MessageListenerManager} messageManager
- *     The message manager to observe for its disconnect state.
- *     Use the browser message manager when closing a content browser,
- *     and the window message manager when closing a chrome window.
- *
- * @return {Promise}
- *     A promise that resolves when the message manager has been destroyed.
- */
-function MessageManagerDestroyedPromise(messageManager) {
-  return new Promise(resolve => {
-    function observe(subject, topic) {
-      log.trace(`Received observer notification ${topic}`);
-
-      if (subject == messageManager) {
-        Services.obs.removeObserver(this, "message-manager-disconnect");
-        resolve();
-      }
-    }
-
-    Services.obs.addObserver(observe, "message-manager-disconnect");
-  });
-}
-
-/**
  * Throttle until the main thread is idle and `window` has performed
  * an animation frame (in that order).
  *
  * @param {ChromeWindow} win
  *     Window to request the animation frame from.
  *
  * @return Promise
  */
@@ -346,8 +325,197 @@ class DebounceCallback {
     this.timer.cancel();
     this.timer.initWithCallback(() => {
       this.timer.cancel();
       this.fn(ev);
     }, this.timeout, TYPE_ONE_SHOT);
   }
 }
 this.DebounceCallback = DebounceCallback;
+
+/**
+ * Wait for an event to be fired on a specified element.
+ *
+ * This method has been duplicated from BrowserTestUtils.jsm.
+ *
+ * Because this function is intended for testing, any error in checkFn
+ * will cause the returned promise to be rejected instead of waiting for
+ * the next event, since this is probably a bug in the test.
+ *
+ * Usage::
+ *
+ *    let promiseEvent = waitForEvent(element, "eventName");
+ *    // Do some processing here that will cause the event to be fired
+ *    // ...
+ *    // Now wait until the Promise is fulfilled
+ *    let receivedEvent = await promiseEvent;
+ *
+ * The promise resolution/rejection handler for the returned promise is
+ * guaranteed not to be called until the next event tick after the event
+ * listener gets called, so that all other event listeners for the element
+ * are executed before the handler is executed::
+ *
+ *    let promiseEvent = waitForEvent(element, "eventName");
+ *    // Same event tick here.
+ *    await promiseEvent;
+ *    // Next event tick here.
+ *
+ * If some code, such like adding yet another event listener, needs to be
+ * executed in the same event tick, use raw addEventListener instead and
+ * place the code inside the event listener::
+ *
+ *    element.addEventListener("load", () => {
+ *      // Add yet another event listener in the same event tick as the load
+ *      // event listener.
+ *      p = waitForEvent(element, "ready");
+ *    }, { once: true });
+ *
+ * @param {Element} subject
+ *     The element that should receive the event.
+ * @param {string} eventName
+ *     Name of the event to listen to.
+ * @param {Object=} options
+ *     Extra options.
+ * @param {boolean=} options.capture
+ *     True to use a capturing listener.
+ * @param {function(Event)=} options.checkFn
+ *     Called with the ``Event`` object as argument, should return ``true``
+ *     if the event is the expected one, or ``false`` if it should be
+ *     ignored and listening should continue. If not specified, the first
+ *     event with the specified name resolves the returned promise.
+ * @param {boolean=} options.wantsUntrusted
+ *     True to receive synthetic events dispatched by web content.
+ *
+ * @return {Promise.<Event>}
+ *     Promise which resolves to the received ``Event`` object, or rejects
+ *     in case of a failure.
+ */
+function waitForEvent(subject, eventName,
+    {capture = false, checkFn = null, wantsUntrusted = false} = {}) {
+  if (subject == null || !("addEventListener" in subject)) {
+    throw new TypeError();
+  }
+  if (typeof eventName != "string") {
+    throw new TypeError();
+  }
+  if (capture != null && typeof capture != "boolean") {
+    throw new TypeError();
+  }
+  if (checkFn != null && typeof checkFn != "function") {
+    throw new TypeError();
+  }
+  if (wantsUntrusted != null && typeof wantsUntrusted != "boolean") {
+    throw new TypeError();
+  }
+
+  return new Promise((resolve, reject) => {
+    subject.addEventListener(eventName, function listener(event) {
+      log.trace(`Received DOM event ${event.type} for ${event.target}`);
+      try {
+        if (checkFn && !checkFn(event)) {
+          return;
+        }
+        subject.removeEventListener(eventName, listener, capture);
+        executeSoon(() => resolve(event));
+      } catch (ex) {
+        try {
+          subject.removeEventListener(eventName, listener, capture);
+        } catch (ex2) {
+          // Maybe the provided object does not support removeEventListener.
+        }
+        executeSoon(() => reject(ex));
+      }
+    }, capture, wantsUntrusted);
+  });
+}
+
+/**
+ * Wait for a message to be fired from a particular message manager.
+ *
+ * This method has been duplicated from BrowserTestUtils.jsm.
+ *
+ * @param {nsIMessageManager} messageManager
+ *     The message manager that should be used.
+ * @param {string} messageName
+ *     The message to wait for.
+ * @param {Object=} options
+ *     Extra options.
+ * @param {function(Message)=} options.checkFn
+ *     Called with the ``Message`` object as argument, should return ``true``
+ *     if the message is the expected one, or ``false`` if it should be
+ *     ignored and listening should continue. If not specified, the first
+ *     message with the specified name resolves the returned promise.
+ *
+ * @return {Promise.<Object>}
+ *     Promise which resolves to the data property of the received
+ *     ``Message``.
+ */
+function waitForMessage(messageManager, messageName,
+    {checkFn = undefined} = {}) {
+  if (messageManager == null || !("addMessageListener" in messageManager)) {
+    throw new TypeError();
+  }
+  if (typeof messageName != "string") {
+    throw new TypeError();
+  }
+  if (checkFn && typeof checkFn != "function") {
+    throw new TypeError();
+  }
+
+  return new Promise(resolve => {
+    messageManager.addMessageListener(messageName, function onMessage(msg) {
+      log.trace(`Received ${messageName} for ${msg.target}`);
+      if (checkFn && !checkFn(msg)) {
+        return;
+      }
+      messageManager.removeMessageListener(messageName, onMessage);
+      resolve(msg.data);
+    });
+  });
+}
+
+/**
+ * Wait for the specified observer topic to be observed.
+ *
+ * This method has been duplicated from TestUtils.jsm.
+ *
+ * Because this function is intended for testing, any error in checkFn
+ * will cause the returned promise to be rejected instead of waiting for
+ * the next notification, since this is probably a bug in the test.
+ *
+ * @param {string} topic
+ *     The topic to observe.
+ * @param {Object=} options
+ *     Extra options.
+ * @param {function(String,Object)=} options.checkFn
+ *     Called with ``subject``, and ``data`` as arguments, should return true
+ *     if the notification is the expected one, or false if it should be
+ *     ignored and listening should continue. If not specified, the first
+ *     notification for the specified topic resolves the returned promise.
+ *
+ * @return {Promise.<Array<String, Object>>}
+ *     Promise which resolves to an array of ``subject``, and ``data`` from
+ *     the observed notification.
+ */
+function waitForObserverTopic(topic, {checkFn = null} = {}) {
+  if (typeof topic != "string") {
+    throw new TypeError();
+  }
+  if (checkFn != null && typeof checkFn != "function") {
+    throw new TypeError();
+  }
+
+  return new Promise((resolve, reject) => {
+    Services.obs.addObserver(function observer(subject, topic, data) {
+      log.trace(`Received observer notification ${topic}`);
+      try {
+        if (checkFn && !checkFn(subject, data)) {
+          return;
+        }
+        Services.obs.removeObserver(observer, topic);
+        resolve({subject, data});
+      } catch (ex) {
+        Services.obs.removeObserver(observer, topic);
+        reject(ex);
+      }
+    }, topic);
+  });
+}
--- a/testing/marionette/test/unit/test_sync.js
+++ b/testing/marionette/test/unit/test_sync.js
@@ -1,23 +1,100 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this file,
  * You can obtain one at http://mozilla.org/MPL/2.0/. */
 
+ChromeUtils.import("resource://gre/modules/Services.jsm");
+
 const {
   DebounceCallback,
   IdlePromise,
   PollPromise,
   Sleep,
   TimedPromise,
+  waitForEvent,
+  waitForMessage,
+  waitForObserverTopic,
 } = ChromeUtils.import("chrome://marionette/content/sync.js", {});
 
 const DEFAULT_TIMEOUT = 2000;
 
 /**
+ * Mimic a DOM node for listening for events.
+ */
+class MockElement {
+  constructor() {
+    this.capture = false;
+    this.func = null;
+    this.eventName = null;
+    this.untrusted = false;
+  }
+
+  addEventListener(name, func, capture, untrusted) {
+    this.eventName = name;
+    this.func = func;
+    if (capture != null) {
+      this.capture = capture;
+    }
+    if (untrusted != null) {
+      this.untrusted = untrusted;
+    }
+  }
+
+  click() {
+    if (this.func) {
+      let details = {
+        capture: this.capture,
+        target: this,
+        type: this.eventName,
+        untrusted: this.untrusted,
+      };
+      this.func(details);
+    }
+  }
+
+  removeEventListener(name, func) {
+    this.capture = false;
+    this.func = null;
+    this.eventName = null;
+    this.untrusted = false;
+  }
+}
+
+/**
+ * Mimic a message manager for sending messages.
+ */
+class MessageManager {
+  constructor() {
+    this.func = null;
+    this.message = null;
+  }
+
+  addMessageListener(message, func) {
+    this.func = func;
+    this.message = message;
+  }
+
+  removeMessageListener(message) {
+    this.func = null;
+    this.message = null;
+  }
+
+  send(message, data) {
+    if (this.func) {
+      this.func({
+        data,
+        message,
+        target: this,
+      });
+    }
+  }
+}
+
+/**
  * Mimics nsITimer, but instead of using a system clock you can
  * preprogram it to invoke the callback after a given number of ticks.
  */
 class MockTimer {
   constructor(ticksBeforeFiring) {
     this.goal = ticksBeforeFiring;
     this.ticks = 0;
     this.cancelled = false;
@@ -30,16 +107,33 @@ class MockTimer {
     }
   }
 
   cancel() {
     this.cancelled = true;
   }
 }
 
+add_test(function test_executeSoon_callback() {
+  // executeSoon() is already defined for xpcshell in head.js. As such import
+  // our implementation into a custom namespace.
+  let sync = {};
+  ChromeUtils.import("chrome://marionette/content/sync.js", sync);
+
+  for (let func of ["foo", null, true, [], {}]) {
+    Assert.throws(() => sync.executeSoon(func), /TypeError/);
+  }
+
+  let a;
+  sync.executeSoon(() => { a = 1; });
+  executeSoon(() => equal(1, a));
+
+  run_next_test();
+});
+
 add_test(function test_PollPromise_funcTypes() {
   for (let type of ["foo", 42, null, undefined, true, [], {}]) {
     Assert.throws(() => new PollPromise(type), /TypeError/);
   }
   new PollPromise(() => {});
   new PollPromise(function() {});
 
   run_next_test();
@@ -208,8 +302,160 @@ add_task(async function test_DebounceCal
   // we only expect the last one to fire
   debouncer.handleEvent(uniqueEvent);
   debouncer.handleEvent(uniqueEvent);
   debouncer.handleEvent(uniqueEvent);
 
   equal(ncalls, 1);
   ok(debouncer.timer.cancelled);
 });
+
+add_task(async function test_waitForEvent_subjectAndEventNameTypes() {
+  let element = new MockElement();
+
+  for (let subject of ["foo", 42, null, undefined, true, [], {}]) {
+    Assert.throws(() => waitForEvent(subject, "click"), /TypeError/);
+  }
+
+  for (let eventName of [42, null, undefined, true, [], {}]) {
+    Assert.throws(() => waitForEvent(element, eventName), /TypeError/);
+  }
+
+  let clicked = waitForEvent(element, "click");
+  element.click();
+  let event = await clicked;
+  equal(element, event.target);
+});
+
+add_task(async function test_waitForEvent_captureTypes() {
+  let element = new MockElement();
+
+  for (let capture of ["foo", 42, [], {}]) {
+    Assert.throws(() => waitForEvent(
+        element, "click", {capture}), /TypeError/);
+  }
+
+  for (let capture of [null, undefined, false, true]) {
+    let expected_capture = (capture == null) ? false : capture;
+
+    element = new MockElement();
+    let clicked = waitForEvent(element, "click", {capture});
+    element.click();
+    let event = await clicked;
+    equal(element, event.target);
+    equal(expected_capture, event.capture);
+  }
+});
+
+add_task(async function test_waitForEvent_checkFnTypes() {
+  let element = new MockElement();
+
+  for (let checkFn of ["foo", 42, true, [], {}]) {
+    Assert.throws(() => waitForEvent(
+        element, "click", {checkFn}), /TypeError/);
+  }
+
+  let count;
+  for (let checkFn of [null, undefined, event => count++ > 0]) {
+    let expected_count = (checkFn == null) ? 0 : 2;
+    count = 0;
+
+    element = new MockElement();
+    let clicked = waitForEvent(element, "click", {checkFn});
+    element.click();
+    element.click();
+    let event = await clicked;
+    equal(element, event.target);
+    equal(expected_count, count);
+  }
+});
+
+add_task(async function test_waitForEvent_wantsUntrustedTypes() {
+  let element = new MockElement();
+
+  for (let wantsUntrusted of ["foo", 42, [], {}]) {
+    Assert.throws(() => waitForEvent(
+        element, "click", {wantsUntrusted}), /TypeError/);
+  }
+
+  for (let wantsUntrusted of [null, undefined, false, true]) {
+    let expected_untrusted = (wantsUntrusted == null) ? false : wantsUntrusted;
+
+    element = new MockElement();
+    let clicked = waitForEvent(element, "click", {wantsUntrusted});
+    element.click();
+    let event = await clicked;
+    equal(element, event.target);
+    equal(expected_untrusted, event.untrusted);
+  }
+});
+
+add_task(async function test_waitForMessage_messageManagerAndMessageTypes() {
+  let messageManager = new MessageManager();
+
+  for (let manager of ["foo", 42, null, undefined, true, [], {}]) {
+    Assert.throws(() => waitForMessage(manager, "message"), /TypeError/);
+  }
+
+  for (let message of [42, null, undefined, true, [], {}]) {
+    Assert.throws(() => waitForEvent(messageManager, message), /TypeError/);
+  }
+
+  let data = {"foo": "bar"};
+  let sent = waitForMessage(messageManager, "message");
+  messageManager.send("message", data);
+  equal(data, await sent);
+});
+
+add_task(async function test_waitForMessage_checkFnTypes() {
+  let messageManager = new MessageManager();
+
+  for (let checkFn of ["foo", 42, true, [], {}]) {
+    Assert.throws(() => waitForMessage(
+        messageManager, "message", {checkFn}), /TypeError/);
+  }
+
+  let data1 = {"fo": "bar"};
+  let data2 = {"foo": "bar"};
+
+  for (let checkFn of [null, undefined, msg => "foo" in msg.data]) {
+    let expected_data = (checkFn == null) ? data1 : data2;
+
+    messageManager = new MessageManager();
+    let sent = waitForMessage(messageManager, "message", {checkFn});
+    messageManager.send("message", data1);
+    messageManager.send("message", data2);
+    equal(expected_data, await sent);
+  }
+});
+
+add_task(async function test_waitForObserverTopic_topicTypes() {
+  for (let topic of [42, null, undefined, true, [], {}]) {
+    Assert.throws(() => waitForObserverTopic(topic), /TypeError/);
+  }
+
+  let data = {"foo": "bar"};
+  let sent = waitForObserverTopic("message");
+  Services.obs.notifyObservers(this, "message", data);
+  let result = await sent;
+  equal(this, result.subject);
+  equal(data, result.data);
+});
+
+add_task(async function test_waitForObserverTopic_checkFnTypes() {
+  for (let checkFn of ["foo", 42, true, [], {}]) {
+    Assert.throws(() => waitForObserverTopic(
+        "message", {checkFn}), /TypeError/);
+  }
+
+  let data1 = {"fo": "bar"};
+  let data2 = {"foo": "bar"};
+
+  for (let checkFn of [null, undefined, (subject, data) => data == data2]) {
+    let expected_data = (checkFn == null) ? data1 : data2;
+
+    let sent = waitForObserverTopic("message");
+    Services.obs.notifyObservers(this, "message", data1);
+    Services.obs.notifyObservers(this, "message", data2);
+    let result = await sent;
+    equal(expected_data, result.data);
+  }
+});
--- a/testing/marionette/transport.js
+++ b/testing/marionette/transport.js
@@ -5,24 +5,27 @@
 "use strict";
 
 /* global Pipe, ScriptableInputStream */
 
 const CC = Components.Constructor;
 
 ChromeUtils.import("resource://gre/modules/Services.jsm");
 ChromeUtils.import("resource://gre/modules/EventEmitter.jsm");
-const {StreamUtils} =
-    ChromeUtils.import("chrome://marionette/content/stream-utils.js", {});
-const {Packet, JSONPacket, BulkPacket} =
-    ChromeUtils.import("chrome://marionette/content/packets.js", {});
-
-const executeSoon = function(func) {
-  Services.tm.dispatchToMainThread(func);
-};
+const {
+  StreamUtils,
+} = ChromeUtils.import("chrome://marionette/content/stream-utils.js", {});
+const {
+  BulkPacket,
+  JSONPacket,
+  Packet,
+} = ChromeUtils.import("chrome://marionette/content/packets.js", {});
+const {
+  executeSoon,
+} = ChromeUtils.import("chrome://marionette/content/sync.js", {});
 
 const flags = {wantVerbose: false, wantLogging: false};
 
 const dumpv =
   flags.wantVerbose ?
   function(msg) { dump(msg + "\n"); } :
   function() {};
 
--- a/testing/mozbase/manifestparser/tests/manifest.ini
+++ b/testing/mozbase/manifestparser/tests/manifest.ini
@@ -1,10 +1,10 @@
 [DEFAULT]
-subsuite = mozbase, os == "linux"
+subsuite = mozbase
 skip-if = python == 3
 [test_expressionparser.py]
 [test_manifestparser.py]
 [test_testmanifest.py]
 [test_read_ini.py]
 [test_convert_directory.py]
 [test_filters.py]
 [test_chunking.py]
--- a/testing/mozbase/mozcrash/tests/manifest.ini
+++ b/testing/mozbase/mozcrash/tests/manifest.ini
@@ -1,8 +1,8 @@
 [DEFAULT]
-subsuite = mozbase, os == "linux"
+subsuite = mozbase
 skip-if = python == 3
 [test_basic.py]
 [test_java_exception.py]
 [test_save_path.py]
 [test_stackwalk.py]
 [test_symbols_path.py]
--- a/testing/mozbase/mozdebug/tests/manifest.ini
+++ b/testing/mozbase/mozdebug/tests/manifest.ini
@@ -1,3 +1,3 @@
 [DEFAULT]
-subsuite = mozbase, os == "linux"
+subsuite = mozbase
 [test.py]
--- a/testing/mozbase/mozdevice/tests/manifest.ini
+++ b/testing/mozbase/mozdevice/tests/manifest.ini
@@ -1,7 +1,7 @@
 [DEFAULT]
-subsuite = mozbase, os == "linux"
+subsuite = mozbase
 skip-if = python == 3
 [test_socket_connection.py]
 [test_is_app_installed.py]
 [test_chown.py]
 [test_escape_command_line.py]
--- a/testing/mozbase/mozfile/tests/manifest.ini
+++ b/testing/mozbase/mozfile/tests/manifest.ini
@@ -1,9 +1,9 @@
 [DEFAULT]
-subsuite = mozbase, os == "linux"
+subsuite = mozbase
 [test_extract.py]
 [test_load.py]
 [test_move_remove.py]
 [test_tempdir.py]
 [test_tempfile.py]
 [test_tree.py]
 [test_url.py]
--- a/testing/mozbase/mozhttpd/tests/manifest.ini
+++ b/testing/mozbase/mozhttpd/tests/manifest.ini
@@ -1,10 +1,10 @@
 [DEFAULT]
-subsuite = mozbase, os == "linux"
+subsuite = mozbase
 [api.py]
 skip-if = python == 3
 [baseurl.py]
 [basic.py]
 [filelisting.py]
 skip-if = python == 3
 [paths.py]
 [requestlog.py]
--- a/testing/mozbase/mozinfo/tests/manifest.ini
+++ b/testing/mozbase/mozinfo/tests/manifest.ini
@@ -1,3 +1,3 @@
 [DEFAULT]
-subsuite = mozbase, os == "linux"
+subsuite = mozbase
 [test.py]
--- a/testing/mozbase/mozinstall/tests/manifest.ini
+++ b/testing/mozbase/mozinstall/tests/manifest.ini
@@ -1,7 +1,7 @@
 [DEFAULT]
-subsuite = mozbase, os == "linux"
+subsuite = mozbase
 skip-if = python == 3
 [test_binary.py]
 [test_install.py]
 [test_is_installer.py]
 [test_uninstall.py]
--- a/testing/mozbase/mozlog/tests/manifest.ini
+++ b/testing/mozbase/mozlog/tests/manifest.ini
@@ -1,9 +1,9 @@
 [DEFAULT]
-subsuite = mozbase, os == "linux"
+subsuite = mozbase
 [test_logger.py]
 skip-if = python == 3
 [test_logtypes.py]
 [test_formatters.py]
 skip-if = python == 3
 [test_structured.py]
 skip-if = python == 3
--- a/testing/mozbase/moznetwork/tests/manifest.ini
+++ b/testing/mozbase/moznetwork/tests/manifest.ini
@@ -1,4 +1,4 @@
 [DEFAULT]
-subsuite = mozbase, os == "linux"
+subsuite = mozbase
 skip-if = python == 3
 [test.py]
--- a/testing/mozbase/mozprocess/tests/manifest.ini
+++ b/testing/mozbase/mozprocess/tests/manifest.ini
@@ -1,10 +1,10 @@
 [DEFAULT]
-subsuite = mozbase, os == "linux"
+subsuite = mozbase
 skip-if = python == 3
 [test_detached.py]
 skip-if = os == "win"  # Bug 1493796
 [test_kill.py]
 [test_misc.py]
 [test_pid.py]
 [test_poll.py]
 [test_wait.py]
--- a/testing/mozbase/mozprofile/tests/manifest.ini
+++ b/testing/mozbase/mozprofile/tests/manifest.ini
@@ -1,10 +1,10 @@
 [DEFAULT]
-subsuite = mozbase, os == "linux"
+subsuite = mozbase
 [test_addonid.py]
 [test_server_locations.py]
 [test_preferences.py]
 [test_permissions.py]
 [test_bug758250.py]
 [test_nonce.py]
 [test_clone_cleanup.py]
 [test_profile.py]
--- a/testing/mozbase/mozrunner/tests/manifest.ini
+++ b/testing/mozbase/mozrunner/tests/manifest.ini
@@ -1,10 +1,10 @@
 [DEFAULT]
-subsuite = mozbase, os == "linux"
+subsuite = mozbase
 # We skip these tests in automated Windows builds because they trigger crashes
 # in sh.exe; see bug 1489277.
 skip-if = python == 3 || (automation && os == "win")
 [test_crash.py]
 [test_interactive.py]
 [test_start.py]
 [test_states.py]
 [test_stop.py]
--- a/testing/mozbase/mozsystemmonitor/tests/manifest.ini
+++ b/testing/mozbase/mozsystemmonitor/tests/manifest.ini
@@ -1,3 +1,3 @@
 [DEFAULT]
-subsuite = mozbase, os == "linux"
+subsuite = mozbase
 [test_resource_monitor.py]
--- a/testing/mozbase/moztest/tests/manifest.ini
+++ b/testing/mozbase/moztest/tests/manifest.ini
@@ -1,5 +1,5 @@
 [DEFAULT]
-subsuite = mozbase, os == "linux"
+subsuite = mozbase
 skip-if = python == 3
 [test.py]
 [test_resolve.py]
--- a/testing/mozbase/mozversion/tests/manifest.ini
+++ b/testing/mozbase/mozversion/tests/manifest.ini
@@ -1,6 +1,6 @@
 [DEFAULT]
-subsuite = mozbase, os == "linux"
+subsuite = mozbase
 
 [test_binary.py]
 [test_apk.py]
 skip-if = python == 3
--- a/testing/web-platform/meta/webdriver/tests/execute_script/promise.py.ini
+++ b/testing/web-platform/meta/webdriver/tests/execute_script/promise.py.ini
@@ -1,10 +1,9 @@
 [promise.py]
-  expected: TIMEOUT
   [test_promise_timeout]
     expected: FAIL
 
   [test_promise_reject_timeout]
     expected: FAIL
 
   [test_promise_resolve_timeout]
     expected: FAIL
--- a/toolkit/components/extensions/Extension.jsm
+++ b/toolkit/components/extensions/Extension.jsm
@@ -462,31 +462,38 @@ class ExtensionData {
           resolve(JSON.parse(text));
         } catch (e) {
           reject(e);
         }
       });
     });
   }
 
+  get restrictSchemes() {
+    // ExtensionData can't check the signature (as it is not yet passed to its constructor
+    // as it is for the Extension class, where this getter is overridden to check both the
+    // signature and the permissions).
+    return !this.hasPermission("mozillaAddons");
+  }
+
   /**
    * Returns an object representing any capabilities that the extension
    * has access to based on fixed properties in the manifest.  The result
    * includes the contents of the "permissions" property as well as other
    * capabilities that are derived from manifest fields that users should
    * be informed of (e.g., origins where content scripts are injected).
    */
   get manifestPermissions() {
     if (this.type !== "extension") {
       return null;
     }
 
     let permissions = new Set();
     let origins = new Set();
-    let restrictSchemes = !this.hasPermission("mozillaAddons");
+    let {restrictSchemes} = this;
     for (let perm of this.manifest.permissions || []) {
       let type = classifyPermission(perm, restrictSchemes);
       if (type.origin) {
         origins.add(perm);
       } else if (type.permission) {
         permissions.add(perm);
       }
     }
@@ -853,17 +860,19 @@ class ExtensionData {
     this.type = manifestData.type;
 
     this.modules = manifestData.modules;
 
     this.apiManager = this.getAPIManager();
     await this.apiManager.lazyInit();
 
     this.webAccessibleResources = manifestData.webAccessibleResources.map(res => new MatchGlob(res));
-    this.whiteListedHosts = new MatchPatternSet(manifestData.originPermissions, {restrictSchemes: !this.hasPermission("mozillaAddons")});
+    this.whiteListedHosts = new MatchPatternSet(manifestData.originPermissions, {
+      restrictSchemes: this.restrictSchemes,
+    });
 
     return this.manifest;
   }
 
   hasPermission(perm, includeOptional = false) {
     // If the permission is a "manifest property" permission, we check if the extension
     // does have the required property in its manifest.
     let manifest_ = "manifest:";
@@ -1371,18 +1380,20 @@ class Extension extends ExtensionData {
     this.on("add-permissions", (ignoreEvent, permissions) => {
       for (let perm of permissions.permissions) {
         this.permissions.add(perm);
       }
 
       if (permissions.origins.length > 0) {
         let patterns = this.whiteListedHosts.patterns.map(host => host.pattern);
 
-        this.whiteListedHosts = new MatchPatternSet(new Set([...patterns, ...permissions.origins]),
-                                                    {restrictSchemes: !this.hasPermission("mozillaAddons"), ignorePath: true});
+        this.whiteListedHosts = new MatchPatternSet(new Set([...patterns, ...permissions.origins]), {
+          restrictSchemes: this.restrictSchemes,
+          ignorePath: true,
+        });
       }
 
       this.policy.permissions = Array.from(this.permissions);
       this.policy.allowedOrigins = this.whiteListedHosts;
 
       this.cachePermissions();
     });
 
@@ -1401,16 +1412,20 @@ class Extension extends ExtensionData {
       this.policy.permissions = Array.from(this.permissions);
       this.policy.allowedOrigins = this.whiteListedHosts;
 
       this.cachePermissions();
     });
     /* eslint-enable mozilla/balanced-listeners */
   }
 
+  get restrictSchemes() {
+    return !(this.isPrivileged && this.hasPermission("mozillaAddons"));
+  }
+
   // Some helpful properties added elsewhere:
   /**
    * An object used to map between extension-visible tab ids and
    * native Tab object
    * @property {TabManager} tabManager
    */
 
   static getBootstrapScope(id, file) {
@@ -2010,17 +2025,17 @@ class Extension extends ExtensionData {
   }
 
   get name() {
     return this.manifest.name;
   }
 
   get optionalOrigins() {
     if (this._optionalOrigins == null) {
-      let restrictSchemes = !this.hasPermission("mozillaAddons");
+      let {restrictSchemes} = this;
       let origins = this.manifest.optional_permissions.filter(perm => classifyPermission(perm, restrictSchemes).origin);
       this._optionalOrigins = new MatchPatternSet(origins, {restrictSchemes, ignorePath: true});
     }
     return this._optionalOrigins;
   }
 }
 
 class Dictionary extends ExtensionData {
--- a/toolkit/mozapps/extensions/content/extensions.js
+++ b/toolkit/mozapps/extensions/content/extensions.js
@@ -285,36 +285,16 @@ function setSearchLabel(type) {
       .textContent = gStrings.ext.GetStringFromName(`searchLabel.${type}`);
     searchLabel.hidden = false;
   } else {
     searchLabel.textContent = "";
     searchLabel.hidden = true;
   }
 }
 
-function setThemeScreenshot(addon, node) {
-  let findElement = () => node.querySelector(".theme-screenshot")
-    || document.getAnonymousElementByAttribute(node, "anonid", "theme-screenshot");
-  let screenshot = findElement();
-  if (!screenshot) {
-    // Force a layout since screenshot might not exist yet on Windows.
-    node.clientTop;
-    screenshot = findElement();
-  }
-  // There's a test that doesn't have this for some reason, but it's doing weird things.
-  if (!screenshot)
-    return;
-  if (addon.type == "theme" && addon.screenshots && addon.screenshots.length > 0) {
-    screenshot.setAttribute("src", addon.screenshots[0].url);
-    screenshot.hidden = false;
-  } else {
-    screenshot.hidden = true;
-  }
-}
-
 /**
  * Obtain the main DOMWindow for the current context.
  */
 function getMainWindow() {
   return window.docShell.rootTreeItem.domWindow;
 }
 
 function getBrowserElement() {
@@ -1583,16 +1563,25 @@ function sortElements(aElements, aSortBy
     // If we're in descending order, swap a and b, because
     // we don't ever want to have descending uiStates
     if (!aAscending)
       [a, b] = [b, a];
 
     return (UISTATE_ORDER.indexOf(a) - UISTATE_ORDER.indexOf(b));
   }
 
+  // Prioritize themes that have screenshots.
+  function hasPreview(aHasStr, bHasStr) {
+    let aHas = aHasStr == "true";
+    let bHas = bHasStr == "true";
+    if (aHas == bHas)
+      return 0;
+    return aHas ? -1 : 1;
+  }
+
   function getValue(aObj, aKey) {
     if (!aObj)
       return null;
 
     if (aObj.hasAttribute(aKey))
       return aObj.getAttribute(aKey);
 
     var addon = aObj.mAddon || aObj.mInstall;
@@ -1629,16 +1618,18 @@ function sortElements(aElements, aSortBy
     aSortFuncs[i] = stringCompare;
 
     if (sortBy == "uiState")
       aSortFuncs[i] = uiStateCompare;
     else if (DATE_FIELDS.includes(sortBy))
       aSortFuncs[i] = dateCompare;
     else if (NUMERIC_FIELDS.includes(sortBy))
       aSortFuncs[i] = numberCompare;
+    else if (sortBy == "hasPreview")
+      aSortFuncs[i] = hasPreview;
   }
 
 
   aElements.sort(function(a, b) {
     if (!aAscending)
       [a, b] = [b, a];
 
     for (let i = 0; i < aSortFuncs.length; i++) {
@@ -1650,18 +1641,19 @@ function sortElements(aElements, aSortBy
         return 0;
       if (!aValue)
         return -1;
       if (!bValue)
         return 1;
       if (aValue != bValue) {
         var result = aSortFuncs[i](aValue, bValue);
 
-        if (result != 0)
+        if (result != 0) {
           return result;
+        }
       }
     }
 
     // If we got here, then all values of a and b
     // must have been equal.
     return 0;
 
   });
@@ -2439,20 +2431,25 @@ var gListView = {
       for (let addonItem of aAddonsList)
         elements.push(createItem(addonItem));
 
       for (let installItem of aInstallsList)
         elements.push(createItem(installItem, true));
 
       this.showEmptyNotice(elements.length == 0);
       if (elements.length > 0) {
-        sortElements(elements, ["uiState", "name"], true);
+        let sortBy;
+        if (aType == "theme") {
+          sortBy = ["uiState", "hasPreview", "name"];
+        } else {
+          sortBy = ["uiState", "name"];
+        }
+        sortElements(elements, sortBy, true);
         for (let element of elements) {
           this._listBox.appendChild(element);
-          setThemeScreenshot(element.mAddon, element);
         }
       }
 
       this.filterDisabledUnsigned(showOnlyDisabledUnsigned);
       let legacyNotice = document.getElementById("legacy-extensions-notice");
       if (showLegacyInfo) {
         let el = document.getElementById("legacy-extensions-description");
         if (el.childNodes[0].nodeName == "#text") {
@@ -2610,17 +2607,24 @@ var gDetailView = {
   },
 
   onUpdateModeChanged() {
     this.onPropertyChanged(["applyBackgroundUpdates"]);
   },
 
   _updateView(aAddon, aIsRemote, aScrollToPreferences) {
     setSearchLabel(aAddon.type);
-    setThemeScreenshot(aAddon, this.node);
+
+    // Set the preview image for themes, if available.
+    if (aAddon.type == "theme") {
+      let previewURL = aAddon.screenshots && aAddon.screenshots[0] && aAddon.screenshots[0].url;
+      if (previewURL) {
+        this.node.querySelector(".card-heading-image").src = previewURL;
+      }
+    }
 
     AddonManager.addManagerListener(this);
     this.clearLoading();
 
     this._addon = aAddon;
     gEventManager.registerAddonListener(this, aAddon.id);
     gEventManager.registerInstallListener(this);
 
--- a/toolkit/mozapps/extensions/content/extensions.xml
+++ b/toolkit/mozapps/extensions/content/extensions.xml
@@ -586,17 +586,17 @@
         <xul:label anonid="pending" flex="1"/>
         <xul:button anonid="undo-btn" class="button-link"
                     label="&addon.undoAction.label;"
                     tooltipText="&addon.undoAction.tooltip;"
                     oncommand="document.getBindingParent(this).undo();"/>
         <xul:spacer flex="5000"/> <!-- Necessary to allow the message to wrap -->
       </xul:hbox>
 
-      <xul:image class="card-heading-image" anonid="theme-screenshot" hidden="true"/>
+      <xul:image class="card-heading-image" anonid="theme-screenshot" xbl:inherits="src=previewURL"/>
 
       <xul:hbox class="content-container" align="center">
         <xul:vbox class="icon-container">
           <xul:image anonid="icon" class="icon"/>
         </xul:vbox>
         <xul:vbox class="content-inner-container" flex="1">
           <xul:hbox class="basicinfo-container">
               <xul:hbox class="name-container">
@@ -904,16 +904,24 @@
           else
             this._icon.src = "";
 
           if (this.mAddon.description)
             this._description.value = this.mAddon.description;
           else
             this._description.hidden = true;
 
+          // Set a previewURL for themes if one exists.
+          let previewURL = this.mAddon.type == "theme" &&
+            this.mAddon.screenshots &&
+            this.mAddon.screenshots[0] &&
+            this.mAddon.screenshots[0].url;
+          this.setAttribute("previewURL", previewURL ? previewURL : "");
+          this.setAttribute("hasPreview", previewURL ? "true" : "fase");
+
           let legacyWarning = legacyExtensionsEnabled && !this.mAddon.install &&
             isLegacyExtension(this.mAddon);
           this.setAttribute("legacy", legacyWarning);
           document.getAnonymousElementByAttribute(this, "anonid", "legacy").href = SUPPORT_URL + "webextensions";
 
           if (!("applyBackgroundUpdates" in this.mAddon) ||
               (this.mAddon.applyBackgroundUpdates == AddonManager.AUTOUPDATE_DISABLE ||
                (this.mAddon.applyBackgroundUpdates == AddonManager.AUTOUPDATE_DEFAULT &&
--- a/toolkit/mozapps/extensions/content/extensions.xul
+++ b/toolkit/mozapps/extensions/content/extensions.xul
@@ -477,17 +477,17 @@
                               label="&addon.undoAction.label;"
                               tooltipText="&addon.undoAction.tooltip;"
                               command="cmd_cancelOperation"/>
                       <spacer flex="5000"/> <!-- Necessary to allow the message to wrap -->
                     </hbox>
                   </vbox>
                   <hbox class="card addon-detail" align="start">
                     <vbox flex="1">
-                      <image class="card-heading-image theme-screenshot" hidden="true"/>
+                      <image class="card-heading-image theme-screenshot"/>
                       <hbox align="start">
                         <vbox id="detail-icon-container" align="end">
                           <image id="detail-icon" class="icon"/>
                         </vbox>
                         <vbox id="detail-summary">
                           <hbox id="detail-name-container" class="name-container"
                                 align="start">
                             <label id="detail-name" flex="1"/>
--- a/toolkit/mozapps/extensions/test/browser/browser.ini
+++ b/toolkit/mozapps/extensions/test/browser/browser.ini
@@ -76,16 +76,17 @@ skip-if = os == 'linux' && !debug # Bug 
 [browser_inlinesettings_browser.js]
 skip-if = os == 'mac' || os == 'linux' # Bug 1483347
 [browser_installssl.js]
 skip-if = verify
 [browser_langpack_signing.js]
 [browser_legacy.js]
 [browser_legacy_pre57.js]
 [browser_list.js]
+[browser_theme_previews.js]
 [browser_manualupdates.js]
 [browser_pluginprefs.js]
 [browser_pluginprefs_is_not_disabled.js]
 [browser_plugin_enabled_state_locked.js]
 [browser_recentupdates.js]
 [browser_reinstall.js]
 [browser_sorting.js]
 [browser_sorting_plugins.js]
new file mode 100644
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/browser_theme_previews.js
@@ -0,0 +1,113 @@
+const {AddonTestUtils} = ChromeUtils.import("resource://testing-common/AddonTestUtils.jsm", {});
+
+let gManagerWindow;
+let gCategoryUtilities;
+
+registerCleanupFunction(() => {
+  // AddonTestUtils with open_manager cause this reference to be maintained and creates a leak.
+  gManagerWindow = null;
+});
+
+function imageBufferFromDataURI(encodedImageData) {
+  let decodedImageData = atob(encodedImageData);
+  return Uint8Array.from(decodedImageData, byte => byte.charCodeAt(0)).buffer;
+}
+const img = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVQImWNgYGBgAAAABQABh6FO1AAAAABJRU5ErkJggg==";
+const imageBuffer = imageBufferFromDataURI(img);
+
+const id = "theme@mochi.test";
+
+AddonTestUtils.initMochitest(this);
+
+function getThemeData(_id = id, manifest = {}, files = {}) {
+  return {
+    "manifest.json": {
+      applications: {
+        gecko: {id: _id},
+      },
+      manifest_version: 2,
+      name: "atheme",
+      description: "wow. such theme.",
+      author: "Pixel Pusher",
+      version: "1",
+      theme: {},
+      ...manifest,
+    },
+    "preview.png": imageBuffer,
+    ...files,
+  };
+}
+
+async function init(startPage) {
+  gManagerWindow = await open_manager(null);
+  gCategoryUtilities = new CategoryUtilities(gManagerWindow);
+  return gCategoryUtilities.openType(startPage);
+}
+
+add_task(async function testThemePreviewShown() {
+  await init("theme");
+
+  await AddonTestUtils.promiseInstallXPI(getThemeData());
+  let addon = await AddonManager.getAddonByID(id);
+
+  ok(addon.screenshots[0].url, "The add-on has a preview URL");
+  let previewURL = addon.screenshots[0].url;
+
+  let doc = gManagerWindow.document;
+  let item = doc.querySelector(`richlistitem[value="${id}"]`);
+
+  await BrowserTestUtils.waitForCondition(
+    () => item.getAttribute("status") == "installed",
+    "Wait for the item to update to installed");
+
+  is(item.getAttribute("previewURL"), previewURL, "The previewURL is set on the item");
+  let image = doc.getAnonymousElementByAttribute(item, "anonid", "theme-screenshot");
+  is(image.src, previewURL, "The previewURL is set on the image src");
+
+  item.click();
+  await wait_for_view_load(gManagerWindow);
+
+  image = doc.querySelector(".theme-screenshot");
+  is(image.src, previewURL, "The previewURL is set on the detail image src");
+
+  await close_manager(gManagerWindow);
+  await addon.uninstall();
+});
+
+add_task(async function testThemeOrdering() {
+  // Install themes before loading the manager, if it's open they'll sort by install date.
+  let themeId = id => id + "@mochi.test";
+  let themeIds = [themeId(5), themeId(6), themeId(7), themeId(8)];
+  await AddonTestUtils.promiseInstallXPI(getThemeData(themeId(6), {name: "BBB"}));
+  await AddonTestUtils.promiseInstallXPI(getThemeData(themeId(7), {name: "CCC"}));
+  await AddonTestUtils.promiseInstallXPI(getThemeData(themeId(5), {name: "AAA"}, {previewURL: ""}));
+  await AddonTestUtils.promiseInstallXPI(getThemeData(themeId(8), {name: "DDD"}));
+
+  // Enable a theme to make sure it's first.
+  let addon = await AddonManager.getAddonByID(themeId(8));
+  addon.enable();
+
+  // Load themes now that the extensions are setup.
+  await init("theme");
+
+  // Find the order of ids for the ones we installed.
+  let list = gManagerWindow.document.getElementById("addon-list");
+  let idOrder = list.itemChildren
+    .map(row => row.getAttribute("value"))
+    .filter(id => themeIds.includes(id));
+
+  // Check the order.
+  Assert.deepEqual(
+    idOrder,
+    [
+      themeId(8), // The active theme first.
+      themeId(6), themeId(7), // With previews, ordered by name.
+      themeId(5), // The theme without a preview last.
+    ],
+    "Themes are ordered by enabled, previews, then name");
+
+  await close_manager(gManagerWindow);
+  for (let addon of await promiseAddonsByIDs(themeIds)) {
+    await addon.uninstall();
+  }
+});
--- a/tools/lint/test/python.ini
+++ b/tools/lint/test/python.ini
@@ -1,7 +1,7 @@
 [DEFAULT]
-subsuite=mozlint, os == "linux"
+subsuite = mozlint
 skip-if = python == 3
 
 [test_eslint.py]
 skip-if = os == "win"  # node not installed on worker
 [test_flake8.py]
--- a/widget/gtk/nsGtkKeyUtils.cpp
+++ b/widget/gtk/nsGtkKeyUtils.cpp
@@ -538,64 +538,74 @@ static void keyboard_handle_keymap(void*
   static auto sXkbContextUnref =
       (void (*)(struct xkb_context*))dlsym(RTLD_DEFAULT, "xkb_context_unref");
   sXkbContextUnref(xkb_context);
 }
 
 static void keyboard_handle_enter(void* data, struct wl_keyboard* keyboard,
                                   uint32_t serial, struct wl_surface* surface,
                                   struct wl_array* keys) {}
-
 static void keyboard_handle_leave(void* data, struct wl_keyboard* keyboard,
                                   uint32_t serial, struct wl_surface* surface) {
 }
-
 static void keyboard_handle_key(void* data, struct wl_keyboard* keyboard,
                                 uint32_t serial, uint32_t time, uint32_t key,
                                 uint32_t state) {}
-
 static void keyboard_handle_modifiers(void* data, struct wl_keyboard* keyboard,
                                       uint32_t serial, uint32_t mods_depressed,
                                       uint32_t mods_latched,
                                       uint32_t mods_locked, uint32_t group) {}
 
 static const struct wl_keyboard_listener keyboard_listener = {
     keyboard_handle_keymap, keyboard_handle_enter,     keyboard_handle_leave,
     keyboard_handle_key,    keyboard_handle_modifiers,
 };
 
-void KeymapWrapper::InitBySystemSettingsWayland() {
-  GdkDeviceManager* manager =
-      gdk_display_get_device_manager(gdk_display_get_default());
-  GList* devices =
-      gdk_device_manager_list_devices(manager, GDK_DEVICE_TYPE_MASTER);
-  GdkDevice* device = nullptr;
+static void seat_handle_capabilities(void* data, struct wl_seat* seat,
+                                     unsigned int caps) {
+  static wl_keyboard* keyboard = nullptr;
 
-  GList* list = devices;
-  while (devices) {
-    device = static_cast<GdkDevice*>(devices->data);
-    if (gdk_device_get_source(device) == GDK_SOURCE_KEYBOARD) {
-      break;
-    }
-    devices = devices->next;
+  if ((caps & WL_SEAT_CAPABILITY_KEYBOARD) && !keyboard) {
+    keyboard = wl_seat_get_keyboard(seat);
+    wl_keyboard_add_listener(keyboard, &keyboard_listener, nullptr);
+  } else if (!(caps & WL_SEAT_CAPABILITY_KEYBOARD) && keyboard) {
+    wl_keyboard_destroy(keyboard);
+    keyboard = nullptr;
   }
+}
 
-  if (list) {
-    g_list_free(list);
+static const struct wl_seat_listener seat_listener = {
+    seat_handle_capabilities,
+};
+
+static void gdk_registry_handle_global(void* data, struct wl_registry* registry,
+                                       uint32_t id, const char* interface,
+                                       uint32_t version) {
+  if (strcmp(interface, "wl_seat") == 0) {
+    wl_seat* seat =
+        (wl_seat*)wl_registry_bind(registry, id, &wl_seat_interface, 1);
+    wl_seat_add_listener(seat, &seat_listener, data);
   }
+}
 
-  if (device) {
-    // Present in Gtk+ 3.10
-    static auto sGdkWaylandDeviceGetWlKeyboard =
-        (struct wl_keyboard * (*)(GdkDevice * device))
-            dlsym(RTLD_DEFAULT, "gdk_wayland_device_get_wl_keyboard");
+static void gdk_registry_handle_global_remove(void* data,
+                                              struct wl_registry* registry,
+                                              uint32_t id) {}
+
+static const struct wl_registry_listener keyboard_registry_listener = {
+    gdk_registry_handle_global, gdk_registry_handle_global_remove};
 
-    wl_keyboard_add_listener(sGdkWaylandDeviceGetWlKeyboard(device),
-                             &keyboard_listener, nullptr);
-  }
+void KeymapWrapper::InitBySystemSettingsWayland() {
+  // Available as of GTK 3.8+
+  static auto sGdkWaylandDisplayGetWlDisplay = (wl_display * (*)(GdkDisplay*))
+      dlsym(RTLD_DEFAULT, "gdk_wayland_display_get_wl_display");
+  wl_display* display =
+      sGdkWaylandDisplayGetWlDisplay(gdk_display_get_default());
+  wl_registry_add_listener(wl_display_get_registry(display),
+                           &keyboard_registry_listener, this);
 }
 #endif
 
 KeymapWrapper::~KeymapWrapper() {
   gdk_window_remove_filter(nullptr, FilterEvents, this);
   g_signal_handlers_disconnect_by_func(mGdkKeymap,
                                        FuncToGpointer(OnKeysChanged), this);
   g_signal_handlers_disconnect_by_func(