Merge mozilla-central to autoland
authorCarsten "Tomcat" Book <cbook@mozilla.com>
Fri, 29 Jul 2016 12:36:04 +0200
changeset 349492 4907812dbb41d1a4f88a5770c4410dce5f4a37cd
parent 349491 b2c393a4ffdb333fee14ca64978d4307537b6436 (current diff)
parent 349315 2ea3d51ba1bb9f5c3b6921c43ea63f70b4fdf5d2 (diff)
child 349493 0f6396641036e51528865fd2a216482270c1ff35
push id1230
push userjlund@mozilla.com
push dateMon, 31 Oct 2016 18:13:35 +0000
treeherdermozilla-release@5e06e3766db2 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
milestone50.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Merge mozilla-central to autoland
browser/components/extensions/test/browser/browser_ext_history.js
toolkit/components/extensions/test/mochitest/file_download.html
toolkit/components/extensions/test/mochitest/file_download.txt
toolkit/components/extensions/test/mochitest/interruptible.sjs
toolkit/components/extensions/test/mochitest/test_chrome_ext_downloads_download.html
toolkit/components/extensions/test/mochitest/test_chrome_ext_downloads_misc.html
toolkit/components/extensions/test/mochitest/test_chrome_ext_downloads_search.html
toolkit/components/extensions/test/mochitest/test_ext_alarms.html
toolkit/components/extensions/test/mochitest/test_ext_background_generated_load_events.html
toolkit/components/extensions/test/mochitest/test_ext_background_generated_reload.html
toolkit/components/extensions/test/mochitest/test_ext_background_runtime_connect_params.html
toolkit/components/extensions/test/mochitest/test_ext_background_sub_windows.html
toolkit/components/extensions/test/mochitest/test_ext_background_window_properties.html
toolkit/components/extensions/test/mochitest/test_ext_bookmarks.html
toolkit/components/extensions/test/mochitest/test_ext_downloads.html
toolkit/components/extensions/test/mochitest/test_ext_extension.html
toolkit/components/extensions/test/mochitest/test_ext_idle.html
toolkit/components/extensions/test/mochitest/test_ext_localStorage.html
toolkit/components/extensions/test/mochitest/test_ext_onmessage_removelistener.html
toolkit/components/extensions/test/mochitest/test_ext_runtime_getPlatformInfo.html
toolkit/components/extensions/test/mochitest/test_ext_runtime_sendMessage.html
toolkit/components/extensions/test/mochitest/test_ext_simple.html
toolkit/components/extensions/test/mochitest/test_ext_storage.html
--- a/.eslintignore
+++ b/.eslintignore
@@ -107,26 +107,31 @@ devtools/client/shared/widgets/*.jsm
 devtools/client/sourceeditor/**
 devtools/client/webaudioeditor/**
 devtools/client/webconsole/**
 !devtools/client/webconsole/panel.js
 !devtools/client/webconsole/jsterm.js
 !devtools/client/webconsole/console-commands.js
 devtools/client/webide/**
 !devtools/client/webide/components/webideCli.js
-devtools/server/**
+devtools/server/*.js
+devtools/server/*.jsm
 !devtools/server/child.js
 !devtools/server/css-logic.js
 !devtools/server/main.js
+devtools/server/actors/**
 !devtools/server/actors/inspector.js
 !devtools/server/actors/highlighters/eye-dropper.js
 !devtools/server/actors/webbrowser.js
+!devtools/server/actors/webextension.js
 !devtools/server/actors/styles.js
 !devtools/server/actors/string.js
 !devtools/server/actors/csscoverage.js
+devtools/server/performance/**
+devtools/server/tests/**
 devtools/shared/*.js
 !devtools/shared/css-lexer.js
 !devtools/shared/defer.js
 !devtools/shared/event-emitter.js
 !devtools/shared/task.js
 devtools/shared/*.jsm
 !devtools/shared/Loader.jsm
 devtools/shared/apps/**
--- a/browser/app/profile/firefox.js
+++ b/browser/app/profile/firefox.js
@@ -1030,16 +1030,17 @@ pref("services.sync.prefs.sync.accessibi
 pref("services.sync.prefs.sync.accessibility.typeaheadfind", true);
 pref("services.sync.prefs.sync.accessibility.typeaheadfind.linksonly", true);
 pref("services.sync.prefs.sync.addons.ignoreUserEnabledChanges", true);
 // The addons prefs related to repository verification are intentionally
 // not synced for security reasons. If a system is compromised, a user
 // could weaken the pref locally, install an add-on from an untrusted
 // source, and this would propagate automatically to other,
 // uncompromised Sync-connected devices.
+pref("services.sync.prefs.sync.browser.ctrlTab.previews", true);
 pref("services.sync.prefs.sync.browser.download.useDownloadDir", true);
 pref("services.sync.prefs.sync.browser.formfill.enable", true);
 pref("services.sync.prefs.sync.browser.link.open_newwindow", true);
 pref("services.sync.prefs.sync.browser.newtabpage.enabled", true);
 pref("services.sync.prefs.sync.browser.newtabpage.enhanced", true);
 pref("services.sync.prefs.sync.browser.newtabpage.pinned", true);
 pref("services.sync.prefs.sync.browser.offline-apps.notify", true);
 pref("services.sync.prefs.sync.browser.safebrowsing.phishing.enabled", true);
--- a/browser/base/content/browser.js
+++ b/browser/base/content/browser.js
@@ -7283,60 +7283,49 @@ var gIdentityHandler = {
     let uri = gBrowser.currentURI;
 
     for (let permission of SitePermissions.getPermissionDetailsByURI(uri)) {
       let item = this._createPermissionItem(permission);
       this._permissionList.appendChild(item);
     }
   },
 
-  setPermission: function (aPermission, aState) {
-    if (aState == SitePermissions.getDefault(aPermission))
-      SitePermissions.remove(gBrowser.currentURI, aPermission);
-    else
-      SitePermissions.set(gBrowser.currentURI, aPermission, aState);
-  },
-
   _createPermissionItem: function (aPermission) {
-    let menulist = document.createElement("menulist");
-    let menupopup = document.createElement("menupopup");
-    for (let state of aPermission.availableStates) {
-      let menuitem = document.createElement("menuitem");
-      menuitem.setAttribute("value", state.id);
-      menuitem.setAttribute("label", state.label);
-      menupopup.appendChild(menuitem);
-    }
-    menulist.appendChild(menupopup);
-    menulist.setAttribute("value", aPermission.state);
-    menulist.setAttribute("oncommand", "gIdentityHandler.setPermission('" +
-                                       aPermission.id + "', this.value)");
-    menulist.setAttribute("id", "identity-popup-permission:" + aPermission.id);
-
-    let label = document.createElement("label");
-    label.setAttribute("flex", "1");
-    label.setAttribute("class", "identity-popup-permission-label");
-    label.setAttribute("control", menulist.getAttribute("id"));
-    label.textContent = aPermission.label;
+    let container = document.createElement("hbox");
+    container.setAttribute("class", "identity-popup-permission-item");
+    container.setAttribute("align", "center");
 
     let img = document.createElement("image");
     let isBlocked = (aPermission.state == SitePermissions.BLOCK) ? " blocked" : "";
     img.setAttribute("class",
       "identity-popup-permission-icon " + aPermission.id + "-icon" + isBlocked);
 
-    let container = document.createElement("hbox");
-    container.setAttribute("align", "center");
+    let nameLabel = document.createElement("label");
+    nameLabel.setAttribute("flex", "1");
+    nameLabel.setAttribute("class", "identity-popup-permission-label");
+    nameLabel.textContent = SitePermissions.getPermissionLabel(aPermission.id);
+
+    let stateLabel = document.createElement("label");
+    stateLabel.setAttribute("flex", "1");
+    stateLabel.setAttribute("class", "identity-popup-permission-state-label");
+    stateLabel.textContent = SitePermissions.getStateLabel(
+      aPermission.id, aPermission.state);
+
+    let button = document.createElement("button");
+    button.setAttribute("class", "identity-popup-permission-remove-button");
+    button.addEventListener("command", () => {
+      this._permissionList.removeChild(container);
+      this._identityPopupMultiView.setHeightToFit();
+      SitePermissions.remove(gBrowser.currentURI, aPermission.id);
+    });
+
     container.appendChild(img);
-    container.appendChild(label);
-    container.appendChild(menulist);
-
-    // The menuitem text can be long and we don't want the dropdown
-    // to expand to the width of unselected labels.
-    // Need to set this attribute after it's appended, otherwise it gets
-    // overridden with sizetopopup="pref".
-    menulist.setAttribute("sizetopopup", "none");
+    container.appendChild(nameLabel);
+    container.appendChild(stateLabel);
+    container.appendChild(button);
 
     return container;
   }
 };
 
 function getNotificationBox(aWindow) {
   var foundBrowser = gBrowser.getBrowserForDocument(aWindow.document);
   if (foundBrowser)
--- a/browser/base/content/browser.xul
+++ b/browser/base/content/browser.xul
@@ -720,17 +720,17 @@
                    align="center"
                    aria-label="&urlbar.viewSiteInfo.label;"
                    onclick="gIdentityHandler.handleIdentityButtonEvent(event);"
                    onkeypress="gIdentityHandler.handleIdentityButtonEvent(event);"
                    ondragstart="gIdentityHandler.onDragStart(event);">
                 <image id="identity-icon"
                        consumeanchor="identity-box"
                        onclick="PageProxyClickHandler(event);"/>
-                <box id="blocked-permissions-container" align="center">
+                <box id="blocked-permissions-container" align="center" tooltiptext="">
                   <image data-permission-id="geo" class="notification-anchor-icon geo-icon blocked" role="button"
                          aria-label="&urlbar.geolocationNotificationAnchor.label;"/>
                   <image data-permission-id="desktop-notification" class="notification-anchor-icon desktop-notification-icon blocked" role="button"
                          aria-label="&urlbar.webNotsNotificationAnchor3.label;"/>
                   <image data-permission-id="camera" class="notification-anchor-icon camera-icon blocked" role="button"
                          aria-label="&urlbar.webRTCShareDevicesNotificationAnchor.label;"/>
                   <image data-permission-id="indexedDB" class="notification-anchor-icon indexedDB-icon blocked" role="button"
                          aria-label="&urlbar.indexedDBNotificationAnchor.label;"/>
--- a/browser/base/content/sanitize.js
+++ b/browser/base/content/sanitize.js
@@ -229,17 +229,16 @@ Sanitizer.prototype = {
       })
     },
 
     cookies: {
       clear: Task.async(function* (range) {
         let seenException;
         let yieldCounter = 0;
         let refObj = {};
-        TelemetryStopwatch.start("FX_SANITIZE_COOKIES", refObj);
 
         // Clear cookies.
         TelemetryStopwatch.start("FX_SANITIZE_COOKIES_2", refObj);
         try {
           let cookieMgr = Components.classes["@mozilla.org/cookiemanager;1"]
                                     .getService(Ci.nsICookieManager);
           if (range) {
             // Iterate through the cookies and delete any created after our cutoff.
@@ -282,17 +281,16 @@ Sanitizer.prototype = {
         // As evidenced in bug 1253204, clearing plugin data can sometimes be
         // very, very long, for mysterious reasons. Unfortunately, this is not
         // something actionable by Mozilla, so crashing here serves no purpose.
         //
         // For this reason, instead of waiting for sanitization to always
         // complete, we introduce a soft timeout. Once this timeout has
         // elapsed, we proceed with the shutdown of Firefox.
         let promiseClearPluginCookies;
-        TelemetryStopwatch.start("FX_SANITIZE_PLUGINS", refObj);
         try {
           // We don't want to wait for this operation to complete...
           promiseClearPluginCookies = this.promiseClearPluginCookies(range);
 
           //... at least, not for more than 10 seconds.
           yield Promise.race([
             promiseClearPluginCookies,
             new Promise(resolve => setTimeout(resolve, 10000 /* 10 seconds */))
@@ -301,20 +299,16 @@ Sanitizer.prototype = {
           seenException = ex;
         }
 
         // Detach waiting for plugin cookies to be cleared.
         promiseClearPluginCookies.catch(() => {
           // If this exception is raised before the soft timeout, it
           // will appear in `seenException`. Otherwise, it's too late
           // to do anything about it.
-        }).then(() => {
-          // Finally, update statistics.
-          TelemetryStopwatch.finish("FX_SANITIZE_PLUGINS", refObj);
-          TelemetryStopwatch.finish("FX_SANITIZE_COOKIES", refObj);
         });
 
         if (seenException) {
           throw seenException;
         }
       }),
 
       promiseClearPluginCookies: Task.async(function* (range) {
@@ -356,25 +350,19 @@ Sanitizer.prototype = {
             }
           }
         }
       })
     },
 
     offlineApps: {
       clear: Task.async(function* (range) {
-        let refObj = {};
-        TelemetryStopwatch.start("FX_SANITIZE_OFFLINEAPPS", refObj);
-        try {
-          Components.utils.import("resource:///modules/offlineAppCache.jsm");
-          // This doesn't wait for the cleanup to be complete.
-          OfflineAppCacheHelper.clear();
-        } finally {
-          TelemetryStopwatch.finish("FX_SANITIZE_OFFLINEAPPS", refObj);
-        }
+        Components.utils.import("resource:///modules/offlineAppCache.jsm");
+        // This doesn't wait for the cleanup to be complete.
+        OfflineAppCacheHelper.clear();
       })
     },
 
     history: {
       clear: Task.async(function* (range) {
         let seenException;
         let refObj = {};
         TelemetryStopwatch.start("FX_SANITIZE_HISTORY", refObj);
--- a/browser/base/content/test/general/browser_misused_characters_in_strings.js
+++ b/browser/base/content/test/general/browser_misused_characters_in_strings.js
@@ -95,20 +95,16 @@ let gWhitelist = [{
   }, {
     file: "netError.dtd",
     key: "inadequateSecurityError.longDesc",
     type: "single-quote"
   }, {
     file: "netErrorApp.dtd",
     key: "securityOverride.warningContent",
     type: "single-quote"
-  }, {
-    file: "sync.properties",
-    key: "client.name2",
-    type: "apostrophe"
   }
 ];
 
 var moduleLocation = gTestPath.replace(/\/[^\/]*$/i, "/parsingTestHelpers.jsm");
 var {generateURIsFromDirTree} = Cu.import(moduleLocation, {});
 
 /**
  * Check if an error should be ignored due to matching one of the whitelist
--- a/browser/base/content/test/general/browser_permissions.js
+++ b/browser/base/content/test/general/browser_permissions.js
@@ -2,17 +2,16 @@
  * Test the Permissions section in the Control Center.
  */
 
 var {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
 const PERMISSIONS_PAGE = "http://example.com/browser/browser/base/content/test/general/permissions.html";
 var {SitePermissions} = Cu.import("resource:///modules/SitePermissions.jsm", {});
 
 registerCleanupFunction(function() {
-  SitePermissions.remove(gBrowser.currentURI, "install");
   SitePermissions.remove(gBrowser.currentURI, "cookie");
   SitePermissions.remove(gBrowser.currentURI, "geo");
   SitePermissions.remove(gBrowser.currentURI, "camera");
   SitePermissions.remove(gBrowser.currentURI, "microphone");
 
   while (gBrowser.tabs.length > 1) {
     gBrowser.removeCurrentTab();
   }
@@ -25,88 +24,114 @@ add_task(function* testMainViewVisible()
 
   let permissionsList = document.getElementById("identity-popup-permission-list");
   let emptyLabel = permissionsList.nextSibling;
 
   gIdentityHandler._identityBox.click();
   ok(!is_hidden(emptyLabel), "List of permissions is empty");
   gIdentityHandler._identityPopup.hidden = true;
 
-  gIdentityHandler.setPermission("install", SitePermissions.ALLOW);
+  SitePermissions.set(gBrowser.currentURI, "camera", SitePermissions.ALLOW);
 
   gIdentityHandler._identityBox.click();
   ok(is_hidden(emptyLabel), "List of permissions is not empty");
 
-  let labelText = SitePermissions.getPermissionLabel("install");
+  let labelText = SitePermissions.getPermissionLabel("camera");
   let labels = permissionsList.querySelectorAll(".identity-popup-permission-label");
   is(labels.length, 1, "One permission visible in main view");
   is(labels[0].textContent, labelText, "Correct value");
 
-  let menulists = permissionsList.querySelectorAll("menulist");
-  is(menulists.length, 1, "One permission visible in main view");
-  is(menulists[0].id, "identity-popup-permission:install", "Install permission visible");
-  is(menulists[0].value, "1", "Correct value on install menulist");
-  gIdentityHandler._identityPopup.hidden = true;
+  let img = permissionsList.querySelector("image.identity-popup-permission-icon");
+  ok(img, "There is an image for the permissions");
+  ok(img.classList.contains("camera-icon"), "proper class is in image class");
 
-  let img = menulists[0].parentNode.querySelector("image");
-  ok(img, "There is an image for the permissions");
-  ok(img.classList.contains("install-icon"), "proper class is in image class");
-
-  gIdentityHandler.setPermission("install", SitePermissions.getDefault("install"));
+  SitePermissions.remove(gBrowser.currentURI, "camera");
 
   gIdentityHandler._identityBox.click();
   ok(!is_hidden(emptyLabel), "List of permissions is empty");
   gIdentityHandler._identityPopup.hidden = true;
 });
 
 add_task(function* testIdentityIcon() {
   let {gIdentityHandler} = gBrowser.ownerGlobal;
   let tab = gBrowser.selectedTab = gBrowser.addTab();
   yield promiseTabLoadEvent(tab, PERMISSIONS_PAGE);
 
-  gIdentityHandler.setPermission("geo", SitePermissions.ALLOW);
+  SitePermissions.set(gBrowser.currentURI, "geo", SitePermissions.ALLOW);
 
   ok(gIdentityHandler._identityBox.classList.contains("grantedPermissions"),
     "identity-box signals granted permissions");
 
-  gIdentityHandler.setPermission("geo", SitePermissions.getDefault("geo"));
+  SitePermissions.remove(gBrowser.currentURI, "geo");
+
+  ok(!gIdentityHandler._identityBox.classList.contains("grantedPermissions"),
+    "identity-box doesn't signal granted permissions");
+
+  SitePermissions.set(gBrowser.currentURI, "camera", SitePermissions.BLOCK);
 
   ok(!gIdentityHandler._identityBox.classList.contains("grantedPermissions"),
     "identity-box doesn't signal granted permissions");
 
-  gIdentityHandler.setPermission("camera", SitePermissions.BLOCK);
-
-  ok(!gIdentityHandler._identityBox.classList.contains("grantedPermissions"),
-    "identity-box doesn't signal granted permissions");
-
-  gIdentityHandler.setPermission("cookie", SitePermissions.SESSION);
+  SitePermissions.set(gBrowser.currentURI, "cookie", SitePermissions.SESSION);
 
   ok(gIdentityHandler._identityBox.classList.contains("grantedPermissions"),
     "identity-box signals granted permissions");
+
+  SitePermissions.remove(gBrowser.currentURI, "geo");
+  SitePermissions.remove(gBrowser.currentURI, "camera");
+  SitePermissions.remove(gBrowser.currentURI, "cookie");
+});
+
+add_task(function* testCancelPermission() {
+  let {gIdentityHandler} = gBrowser.ownerGlobal;
+  let tab = gBrowser.selectedTab = gBrowser.addTab();
+  yield promiseTabLoadEvent(tab, PERMISSIONS_PAGE);
+
+  let permissionsList = document.getElementById("identity-popup-permission-list");
+  let emptyLabel = permissionsList.nextSibling;
+
+  SitePermissions.set(gBrowser.currentURI, "geo", SitePermissions.ALLOW);
+  SitePermissions.set(gBrowser.currentURI, "camera", SitePermissions.BLOCK);
+
+  gIdentityHandler._identityBox.click();
+
+  ok(is_hidden(emptyLabel), "List of permissions is not empty");
+
+  let cancelButtons = permissionsList
+    .querySelectorAll(".identity-popup-permission-remove-button");
+
+  cancelButtons[0].click();
+  let labels = permissionsList.querySelectorAll(".identity-popup-permission-label");
+  is(labels.length, 1, "One permission should be removed");
+  cancelButtons[1].click();
+  labels = permissionsList.querySelectorAll(".identity-popup-permission-label");
+  is(labels.length, 0, "One permission should be removed");
+
+  gIdentityHandler._identityPopup.hidden = true;
 });
 
 add_task(function* testPermissionIcons() {
   let {gIdentityHandler} = gBrowser.ownerGlobal;
   let tab = gBrowser.selectedTab = gBrowser.addTab();
   yield promiseTabLoadEvent(tab, PERMISSIONS_PAGE);
 
-  gIdentityHandler.setPermission("camera", SitePermissions.ALLOW);
-  gIdentityHandler.setPermission("geo", SitePermissions.BLOCK);
-  gIdentityHandler.setPermission("microphone", SitePermissions.SESSION);
+  SitePermissions.set(gBrowser.currentURI, "camera", SitePermissions.ALLOW);
+  SitePermissions.set(gBrowser.currentURI, "geo", SitePermissions.BLOCK);
+  SitePermissions.set(gBrowser.currentURI, "microphone", SitePermissions.SESSION);
 
   let geoIcon = gIdentityHandler._identityBox.querySelector("[data-permission-id='geo']");
   ok(geoIcon.hasAttribute("showing"), "blocked permission icon is shown");
   ok(geoIcon.classList.contains("blocked"),
     "blocked permission icon is shown as blocked");
 
   let cameraIcon = gIdentityHandler._identityBox.querySelector("[data-permission-id='camera']");
   ok(!cameraIcon.hasAttribute("showing"),
     "allowed permission icon is not shown");
 
   let microphoneIcon  = gIdentityHandler._identityBox.querySelector("[data-permission-id='microphone']");
   ok(!microphoneIcon.hasAttribute("showing"),
     "allowed permission icon is not shown");
 
-  gIdentityHandler.setPermission("geo", SitePermissions.getDefault("geo"));
+  SitePermissions.remove(gBrowser.currentURI, "geo");
 
   ok(!geoIcon.hasAttribute("showing"),
     "blocked permission icon is not shown after reset");
 });
--- a/browser/components/contextualidentity/test/browser/browser_broadcastchannel.js
+++ b/browser/components/contextualidentity/test/browser/browser_broadcastchannel.js
@@ -58,24 +58,23 @@ add_task(function* test() {
         sender.browser,
         { name: channelName, message: sender.message },
         function (opts) {
           let bc = new content.window.BroadcastChannel(opts.name);
           bc.postMessage(opts.message);
         });
   }
 
-  // make sure we have received a message
-  yield ContentTask.spawn(receiver.browser, channelName,
-    function* (name) {
-      yield content.window.testPromise.then(function() {});
+  // Since sender1 sends before sender2, if the title is exactly
+  // sender2's message, sender1's message must've been blocked
+  yield ContentTask.spawn(receiver.browser, sender2.message,
+    function* (message) {
+      yield content.window.testPromise.then(function() {
+        is(content.document.title, message,
+           "should only receive messages from the same user context");
+      });
     }
   );
 
-  // Since sender1 sends before sender2, if the title is exactly
-  // sender2's message, sender1's message must've been blocked
-  is(receiver.browser.contentDocument.title, sender2.message,
-      "should only receive messages from the same user context");
-
   gBrowser.removeTab(sender1.tab);
   gBrowser.removeTab(sender2.tab);
   gBrowser.removeTab(receiver.tab);
 });
--- a/browser/components/customizableui/CustomizableWidgets.jsm
+++ b/browser/components/customizableui/CustomizableWidgets.jsm
@@ -500,30 +500,26 @@ const CustomizableWidgets = [
       return item;
     },
   }, {
     id: "privatebrowsing-button",
     shortcutId: "key_privatebrowsing",
     defaultArea: CustomizableUI.AREA_PANEL,
     onCommand: function(e) {
       let win = e.target.ownerGlobal;
-      if (typeof win.OpenBrowserWindow == "function") {
-        win.OpenBrowserWindow({private: true});
-      }
+      win.OpenBrowserWindow({private: true});
     }
   }, {
     id: "save-page-button",
     shortcutId: "key_savePage",
     tooltiptext: "save-page-button.tooltiptext3",
     defaultArea: CustomizableUI.AREA_PANEL,
     onCommand: function(aEvent) {
       let win = aEvent.target.ownerGlobal;
-      if (typeof win.saveBrowser == "function") {
-        win.saveBrowser(win.gBrowser.selectedBrowser);
-      }
+      win.saveBrowser(win.gBrowser.selectedBrowser);
     }
   }, {
     id: "find-button",
     shortcutId: "key_find",
     tooltiptext: "find-button.tooltiptext3",
     defaultArea: CustomizableUI.AREA_PANEL,
     onCommand: function(aEvent) {
       let win = aEvent.target.ownerGlobal;
@@ -533,19 +529,17 @@ const CustomizableWidgets = [
     }
   }, {
     id: "open-file-button",
     shortcutId: "openFileKb",
     tooltiptext: "open-file-button.tooltiptext3",
     defaultArea: CustomizableUI.AREA_PANEL,
     onCommand: function(aEvent) {
       let win = aEvent.target.ownerGlobal;
-      if (typeof win.BrowserOpenFileWindow == "function") {
-        win.BrowserOpenFileWindow();
-      }
+      win.BrowserOpenFileWindow();
     }
   }, {
     id: "sidebar-button",
     type: "view",
     viewId: "PanelUI-sidebar",
     tooltiptext: "sidebar-button.tooltiptext2",
     onViewShowing: function(aEvent) {
       // Populate the subview with whatever menuitems are in the
@@ -610,19 +604,17 @@ const CustomizableWidgets = [
     }
   }, {
     id: "add-ons-button",
     shortcutId: "key_openAddons",
     tooltiptext: "add-ons-button.tooltiptext3",
     defaultArea: CustomizableUI.AREA_PANEL,
     onCommand: function(aEvent) {
       let win = aEvent.target.ownerGlobal;
-      if (typeof win.BrowserOpenAddonsMgr == "function") {
-        win.BrowserOpenAddonsMgr();
-      }
+      win.BrowserOpenAddonsMgr();
     }
   }, {
     id: "zoom-controls",
     type: "custom",
     tooltiptext: "zoom-controls.tooltiptext2",
     defaultArea: CustomizableUI.AREA_PANEL,
     onBuild: function(aDocument) {
       const kPanelId = "PanelUI-popup";
@@ -1181,19 +1173,17 @@ const CustomizableWidgets = [
     ]),
   }];
 
 let preferencesButton = {
   id: "preferences-button",
   defaultArea: CustomizableUI.AREA_PANEL,
   onCommand: function(aEvent) {
     let win = aEvent.target.ownerGlobal;
-    if (typeof win.openPreferences == "function") {
-      win.openPreferences();
-    }
+    win.openPreferences();
   }
 };
 if (AppConstants.platform == "win") {
   preferencesButton.label = "preferences-button.labelWin";
   preferencesButton.tooltiptext = "preferences-button.tooltipWin2";
 } else if (AppConstants.platform == "macosx") {
   preferencesButton.tooltiptext = "preferences-button.tooltiptext.withshortcut";
   preferencesButton.shortcutId = "key_preferencesCmdMac";
@@ -1272,15 +1262,13 @@ if (AppConstants.E10S_TESTING_ONLY) {
       id: "e10s-button",
       defaultArea: CustomizableUI.AREA_PANEL,
       onBuild: function(aDocument) {
           node.setAttribute("label", CustomizableUI.getLocalizedProperty(this, "label"));
           node.setAttribute("tooltiptext", CustomizableUI.getLocalizedProperty(this, "tooltiptext"));
       },
       onCommand: function(aEvent) {
         let win = aEvent.view;
-        if (win && typeof win.OpenBrowserWindow == "function") {
-          win.OpenBrowserWindow({remote: false});
-        }
+        win.OpenBrowserWindow({remote: false});
       },
     });
   }
 }
--- a/browser/components/customizableui/content/panelUI.xml
+++ b/browser/components/customizableui/content/panelUI.xml
@@ -389,16 +389,37 @@
           }
 
           if (this._shouldSetHeight()) {
             this._adjustContainerHeight();
           }
         ]]></body>
       </method>
 
+      <!-- Call this when the height of one of your views (the main view or a
+           subview) changes and you want the heights of the multiview and panel
+           to be the same as the view's height. -->
+      <method name="setHeightToFit">
+        <body><![CDATA[
+          // Set the max-height to zero, wait until the height is actually
+          // updated, and then remove it.  If it's not removed, weird things can
+          // happen, like widgets in the panel won't respond to clicks even
+          // though they're visible.
+          let count = 5;
+          let height = getComputedStyle(this).height;
+          this.style.maxHeight = "0";
+          let interval = setInterval(() => {
+            if (height != getComputedStyle(this).height || --count == 0) {
+              clearInterval(interval);
+              this.style.removeProperty("max-height");
+            }
+          }, 0);
+        ]]></body>
+      </method>
+
       <method name="_heightOfSubview">
         <parameter name="aSubview"/>
         <parameter name="aContainerToCheck"/>
         <body><![CDATA[
           function getFullHeight(element) {
             //XXXgijs: unfortunately, scrollHeight rounds values, and there's no alternative
             // that works with overflow: auto elements. Fortunately for us,
             // we have exactly 1 (potentially) scrolling element in here (the subview body),
--- a/browser/components/downloads/content/downloads.css
+++ b/browser/components/downloads/content/downloads.css
@@ -155,23 +155,16 @@ richlistitem.download button {
 
 #downloadsPanel[hasdownloads] #emptyDownloads,
 #downloadsPanel:not([hasdownloads]) #downloadsListBox {
   display: none;
 }
 
 /*** Downloads panel multiview (main view and blocked-downloads subview) ***/
 
-#downloadsPanel,
-#downloadsPanel .panel-viewstack[viewtype=main]:not([transitioning]) #downloadsPanel-mainView {
-  /* Tiny hack to ensure the panel shrinks back to its original
-     size after closing a subview that is bigger than the main view. */
-  max-height: 0;
-}
-
 /* Hide all the usual buttons. */
 #downloadsPanel-mainView .download-state[state="8"] .downloadCancel,
 #downloadsPanel-mainView .download-state[state="8"] .downloadConfirmBlock,
 #downloadsPanel-mainView .download-state[state="8"] .downloadChooseUnblock,
 #downloadsPanel-mainView .download-state[state="8"] .downloadChooseOpen,
 #downloadsPanel-mainView .download-state[state="8"] .downloadRetry,
 #downloadsPanel-mainView .download-state[state="8"] .downloadShow {
   display: none;
--- a/browser/components/downloads/content/downloads.js
+++ b/browser/components/downloads/content/downloads.js
@@ -704,16 +704,18 @@ const DownloadsView = {
     if (count > 0) {
       DownloadsCommon.log("Setting the panel's hasdownloads attribute to true.");
       DownloadsPanel.panel.setAttribute("hasdownloads", "true");
     } else {
       DownloadsCommon.log("Removing the panel's hasdownloads attribute.");
       DownloadsPanel.panel.removeAttribute("hasdownloads");
     }
 
+    DownloadsBlockedSubview.view.setHeightToFit();
+
     // If we've got some hidden downloads, we should activate the
     // DownloadsSummary. The DownloadsSummary will determine whether or not
     // it's appropriate to actually display the summary.
     DownloadsSummary.active = hiddenCount > 0;
   },
 
   /**
    * Element corresponding to the list of downloads.
@@ -1189,17 +1191,27 @@ const DownloadsViewController = {
     // Firstly, determine if this is a command that we can handle.
     if (!DownloadsViewUI.isCommandName(aCommand)) {
       return false;
     }
     if (!(aCommand in this) &&
         !(aCommand in DownloadsViewItem.prototype)) {
       return false;
     }
-    // Secondly, determine if focus is on a control in the downloads list.
+    // The currently supported commands depend on whether the blocked subview is
+    // showing.  If it is, then take the following path.
+    if (DownloadsBlockedSubview.view.showingSubView) {
+      let blockedSubviewCmds = [
+        "downloadsCmd_chooseOpen",
+        "cmd_delete",
+      ];
+      return blockedSubviewCmds.indexOf(aCommand) >= 0;
+    }
+    // If the blocked subview is not showing, then determine if focus is on a
+    // control in the downloads list.
     let element = document.commandDispatcher.focusedElement;
     while (element && element != DownloadsView.richListBox) {
       element = element.parentNode;
     }
     // We should handle the command only if the downloads list is among the
     // ancestors of the focused element.
     return !!element;
   },
@@ -1599,16 +1611,19 @@ const DownloadsBlockedSubview = {
     }
   },
 
   /**
    * Slides out the blocked subview and shows the main view.
    */
   hide() {
     this.view.showMainView();
+    // The point of this is to focus the proper element in the panel now that
+    // the main view is showing again.  showPanel handles that.
+    DownloadsPanel.showPanel();
   },
 
   /**
    * Deletes the download and hides the entire panel.
    */
   confirmBlock() {
     goDoCommand("cmd_delete");
     DownloadsPanel.hidePanel();
--- a/browser/components/downloads/test/browser/browser_downloads_panel_block.js
+++ b/browser/components/downloads/test/browser/browser_downloads_panel_block.js
@@ -34,31 +34,33 @@ add_task(function* mainTest() {
 
     // Show the subview again.
     EventUtils.sendMouseEvent({ type: "click" }, item);
     yield promiseSubviewShown(true);
 
     // Click the Open button.  The alert blocked-download dialog should be
     // shown.
     let dialogPromise = promiseAlertDialogOpen("cancel");
-    DownloadsBlockedSubview.elements.openButton.click();
+    EventUtils.synthesizeMouse(DownloadsBlockedSubview.elements.openButton,
+                               10, 10, {}, window);
     yield dialogPromise;
 
     window.focus();
     yield SimpleTest.promiseFocus(window);
 
     // Reopen the panel and show the subview again.
     yield openPanel();
 
     EventUtils.sendMouseEvent({ type: "click" }, item);
     yield promiseSubviewShown(true);
 
     // Click the Remove button.  The panel should close and the item should be
     // removed from it.
-    DownloadsBlockedSubview.elements.deleteButton.click();
+    EventUtils.synthesizeMouse(DownloadsBlockedSubview.elements.deleteButton,
+                               10, 10, {}, window);
     yield promisePanelHidden();
     yield openPanel();
 
     Assert.ok(!item.parentNode);
     DownloadsPanel.hidePanel();
     yield promisePanelHidden();
   }
 
@@ -145,21 +147,21 @@ function makeDownload(verdict) {
       becauseBlocked: true,
       becauseBlockedByReputationCheck: true,
       reputationCheckVerdict:  verdict,
     },
   };
 }
 
 function promiseSubviewShown(shown) {
+  // More terribleness, but I'm tired of fighting intermittent timeouts on try.
+  // Just poll for the subview and wait a second before resolving the promise.
   return new Promise(resolve => {
-    if (shown == DownloadsBlockedSubview.view.showingSubView) {
-      resolve();
-      return;
-    }
-    let event = shown ? "ViewShowing" : "ViewHiding";
-    let subview = DownloadsBlockedSubview.subview;
-    subview.addEventListener(event, function showing() {
-      subview.removeEventListener(event, showing);
-      resolve();
-    });
+    let interval = setInterval(() => {
+      if (shown == DownloadsBlockedSubview.view.showingSubView &&
+          !DownloadsBlockedSubview.view._transitioning) {
+        clearInterval(interval);
+        setTimeout(resolve, 1000);
+        return;
+      }
+    }, 0);
   });
 }
--- a/browser/components/extensions/ext-utils.js
+++ b/browser/components/extensions/ext-utils.js
@@ -70,16 +70,21 @@ class BasePopup {
       Services.scriptSecurityManager.DISALLOW_SCRIPT);
 
     this.extension = extension;
     this.popupURI = popupURI;
     this.viewNode = viewNode;
     this.browserStyle = browserStyle;
     this.window = viewNode.ownerGlobal;
 
+    this.panel = this.viewNode;
+    while (this.panel.localName != "panel") {
+      this.panel = this.panel.parentNode;
+    }
+
     this.contentReady = new Promise(resolve => {
       this._resolveContentReady = resolve;
     });
 
     this.viewNode.addEventListener(this.DESTROY_EVENT, this);
 
     this.browser = null;
     this.browserReady = this.createBrowser(viewNode, popupURI);
@@ -88,29 +93,34 @@ class BasePopup {
   destroy() {
     this.browserReady.then(() => {
       this.browser.removeEventListener("DOMWindowCreated", this, true);
       this.browser.removeEventListener("load", this, true);
       this.browser.removeEventListener("DOMTitleChanged", this, true);
       this.browser.removeEventListener("DOMWindowClose", this, true);
       this.browser.removeEventListener("MozScrolledAreaChanged", this, true);
       this.viewNode.removeEventListener(this.DESTROY_EVENT, this);
+      this.viewNode.style.maxHeight = "";
       this.browser.remove();
 
       this.browser = null;
       this.viewNode = null;
     });
   }
 
   // Returns the name of the event fired on `viewNode` when the popup is being
   // destroyed. This must be implemented by every subclass.
   get DESTROY_EVENT() {
     throw new Error("Not implemented");
   }
 
+  get fixedWidth() {
+    return false;
+  }
+
   handleEvent(event) {
     switch (event.type) {
       case this.DESTROY_EVENT:
         this.destroy();
         break;
 
       case "DOMWindowCreated":
         if (this.browserStyle && event.target === this.browser.contentDocument) {
@@ -161,25 +171,26 @@ class BasePopup {
 
   createBrowser(viewNode, popupURI) {
     let document = viewNode.ownerDocument;
     this.browser = document.createElementNS(XUL_NS, "browser");
     this.browser.setAttribute("type", "content");
     this.browser.setAttribute("disableglobalhistory", "true");
     this.browser.setAttribute("webextension-view-type", "popup");
 
+    // We only need flex sizing for the sake of the slide-in sub-views of the
+    // main menu panel, so that the browser occupies the full width of the view,
+    // and also takes up any extra height that's available to it.
+    this.browser.setAttribute("flex", "1");
+
     // Note: When using noautohide panels, the popup manager will add width and
     // height attributes to the panel, breaking our resize code, if the browser
     // starts out smaller than 30px by 10px. This isn't an issue now, but it
     // will be if and when we popup debugging.
 
-    // This overrides the content's preferred size when displayed in a
-    // fixed-size, slide-in panel.
-    this.browser.setAttribute("flex", "1");
-
     viewNode.appendChild(this.browser);
 
     return new Promise(resolve => {
       // The first load event is for about:blank.
       // We can't finish setting up the browser until the binding has fully
       // initialized. Waiting for the first load event guarantees that it has.
       let loadListener = event => {
         this.browser.removeEventListener("load", loadListener, true);
@@ -212,38 +223,86 @@ class BasePopup {
 
   _resizeBrowser() {
     this.resizeTimeout = null;
 
     if (!this.browser) {
       return;
     }
 
-    let width, height;
-    try {
-      let w = {}, h = {};
-      this.browser.docShell.contentViewer.getContentSize(w, h);
+    if (this.fixedWidth) {
+      // If we're in a fixed-width area (namely a slide-in subview of the main
+      // menu panel), we need to calculate the view height based on the
+      // preferred height of the content document's root scrollable element at the
+      // current width, rather than the complete preferred dimensions of the
+      // content window.
+
+      let doc = this.browser.contentDocument;
+      if (!doc || !doc.documentElement) {
+        return;
+      }
 
-      width = w.value / this.window.devicePixelRatio;
-      height = h.value / this.window.devicePixelRatio;
+      let root = doc.documentElement;
+      let body = doc.body;
+      if (!body || doc.compatMode == "BackCompat") {
+        // In quirks mode, the root element is used as the scroll frame, and the
+        // body lies about its scroll geometry, and returns the values for the
+        // root instead.
+        body = root;
+      }
+
+      // Compensate for any offsets (margin, padding, ...) between the scroll
+      // area of the body and the outer height of the document.
+      let getHeight = elem => elem.getBoundingClientRect(elem).height;
+      let bodyPadding = getHeight(root) - getHeight(body);
+
+      let height = Math.ceil(body.scrollHeight + bodyPadding);
+
+      // Figure out how much extra space we have on the side of the panel
+      // opposite the arrow.
+      let side = this.panel.getAttribute("side") == "top" ? "bottom" : "top";
+      let maxHeight = this.viewHeight + this.extraHeight[side];
 
-      // The width calculation is imperfect, and is often a fraction of a pixel
-      // too narrow, even after taking the ceiling, which causes lines of text
-      // to wrap.
-      width += 1;
-    } catch (e) {
-      // getContentSize can throw
-      [width, height] = [400, 400];
+      height = Math.min(height, maxHeight);
+      this.browser.style.height = `${height}px`;
+
+      // Set a maximum height on the <panelview> element to our preferred
+      // maximum height, so that the PanelUI resizing code can make an accurate
+      // calculation. If we don't do this, the flex sizing logic will prevent us
+      // from ever reporting a preferred size smaller than the height currently
+      // available to us in the panel.
+      height = Math.max(height, this.viewHeight);
+      this.viewNode.style.maxHeight = `${height}px`;
+    } else {
+      let width, height;
+      try {
+        let w = {}, h = {};
+        this.browser.docShell.contentViewer.getContentSize(w, h);
+
+        width = w.value / this.window.devicePixelRatio;
+        height = h.value / this.window.devicePixelRatio;
+
+        // The width calculation is imperfect, and is often a fraction of a pixel
+        // too narrow, even after taking the ceiling, which causes lines of text
+        // to wrap.
+        width += 1;
+      } catch (e) {
+        // getContentSize can throw
+        [width, height] = [400, 400];
+      }
+
+      width = Math.ceil(Math.min(width, 800));
+      height = Math.ceil(Math.min(height, 600));
+
+      this.browser.style.width = `${width}px`;
+      this.browser.style.height = `${height}px`;
     }
 
-    width = Math.ceil(Math.min(width, 800));
-    height = Math.ceil(Math.min(height, 600));
-
-    this.browser.style.width = `${width}px`;
-    this.browser.style.height = `${height}px`;
+    let event = new this.window.CustomEvent("WebExtPopupResized");
+    this.browser.dispatchEvent(event);
 
     this._resolveContentReady();
   }
 }
 
 global.PanelPopup = class PanelPopup extends BasePopup {
   constructor(extension, imageNode, popupURL, browserStyle) {
     let document = imageNode.ownerDocument;
@@ -278,32 +337,59 @@ global.PanelPopup = class PanelPopup ext
       if (this.viewNode) {
         this.viewNode.hidePopup();
       }
     });
   }
 };
 
 global.ViewPopup = class ViewPopup extends BasePopup {
+  constructor(...args) {
+    super(...args);
+
+    // Store the initial height of the view, so that we never resize menu panel
+    // sub-views smaller than the initial height of the menu.
+    this.viewHeight = this.viewNode.boxObject.height;
+
+    // Calculate the extra height available on the screen above and below the
+    // menu panel. Use that to calculate the how much the sub-view may grow.
+    let popupRect = this.panel.getBoundingClientRect();
+
+    let win = this.window;
+    let popupBottom = win.mozInnerScreenY + popupRect.bottom;
+    let popupTop = win.mozInnerScreenY + popupRect.top;
+
+    let screenBottom = win.screen.availTop + win.screen.availHeight;
+    this.extraHeight = {
+      bottom: Math.max(0, screenBottom - popupBottom),
+      top:  Math.max(0, popupTop - win.screen.availTop),
+    };
+  }
+
   get DESTROY_EVENT() {
     return "ViewHiding";
   }
 
+  get fixedWidth() {
+    return !this.viewNode.classList.contains("cui-widget-panelview");
+  }
+
   closePopup() {
     CustomizableUI.hidePanelForNode(this.viewNode);
   }
 };
 
 // Manages tab-specific context data, and dispatching tab select events
 // across all windows.
 global.TabContext = function TabContext(getDefaults, extension) {
   this.extension = extension;
   this.getDefaults = getDefaults;
 
   this.tabData = new WeakMap();
+  this.lastLocation = new WeakMap();
 
   AllWindowEvents.addListener("progress", this);
   AllWindowEvents.addListener("TabSelect", this);
 
   EventEmitter.decorate(this);
 };
 
 TabContext.prototype = {
@@ -322,22 +408,34 @@ TabContext.prototype = {
   handleEvent(event) {
     if (event.type == "TabSelect") {
       let tab = event.target;
       this.emit("tab-select", tab);
       this.emit("location-change", tab);
     }
   },
 
+  onStateChange(browser, webProgress, request, stateFlags, statusCode) {
+    let flags = Ci.nsIWebProgressListener;
+
+    if (!(~stateFlags & (flags.STATE_IS_WINDOW | flags.STATE_START) ||
+          this.lastLocation.has(browser))) {
+      this.lastLocation.set(browser, request.URI);
+    }
+  },
+
   onLocationChange(browser, webProgress, request, locationURI, flags) {
     let gBrowser = browser.ownerGlobal.gBrowser;
-    if (browser === gBrowser.selectedBrowser) {
+    let lastLocation = this.lastLocation.get(browser);
+    if (browser === gBrowser.selectedBrowser &&
+        !(lastLocation && lastLocation.equalsExceptRef(browser.currentURI))) {
       let tab = gBrowser.getTabForBrowser(browser);
       this.emit("location-change", tab, true);
     }
+    this.lastLocation.set(browser, browser.currentURI);
   },
 
   shutdown() {
     AllWindowEvents.removeListener("progress", this);
     AllWindowEvents.removeListener("TabSelect", this);
   },
 };
 
--- a/browser/components/extensions/test/browser/browser.ini
+++ b/browser/components/extensions/test/browser/browser.ini
@@ -30,17 +30,16 @@ support-files =
 [browser_ext_contextMenus.js]
 [browser_ext_contextMenus_checkboxes.js]
 [browser_ext_contextMenus_icons.js]
 [browser_ext_contextMenus_radioGroups.js]
 [browser_ext_contextMenus_uninstall.js]
 [browser_ext_contextMenus_urlPatterns.js]
 [browser_ext_currentWindow.js]
 [browser_ext_getViews.js]
-[browser_ext_history.js]
 [browser_ext_incognito_popup.js]
 [browser_ext_lastError.js]
 [browser_ext_optionsPage_privileges.js]
 [browser_ext_pageAction_context.js]
 [browser_ext_pageAction_popup.js]
 [browser_ext_pageAction_popup_resize.js]
 [browser_ext_pageAction_simple.js]
 [browser_ext_popup_api_injection.js]
--- a/browser/components/extensions/test/browser/browser_ext_browserAction_popup_resize.js
+++ b/browser/components/extensions/test/browser/browser_ext_browserAction_popup_resize.js
@@ -1,37 +1,57 @@
 /* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
 /* vim: set sts=2 sw=2 et tw=80: */
 "use strict";
 
-add_task(function* testPageActionPopupResize() {
+function* openPanel(extension, win = window) {
+  clickBrowserAction(extension, win);
+
+  let {target} = yield BrowserTestUtils.waitForEvent(win.document, "load", true, (event) => {
+    return event.target.location && event.target.location.href.endsWith("popup.html");
+  });
+
+  return target.defaultView
+               .QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDocShell)
+               .chromeEventHandler;
+}
+
+function* awaitResize(browser) {
+  // Debouncing code makes this a bit racy.
+  // Try to skip the first, early resize, and catch the resize event we're
+  // looking for, but don't wait longer than a few seconds.
+
+  return Promise.race([
+    BrowserTestUtils.waitForEvent(browser, "WebExtPopupResized")
+      .then(() => BrowserTestUtils.waitForEvent(browser, "WebExtPopupResized")),
+    new Promise(resolve => setTimeout(resolve, 5000)),
+  ]);
+}
+
+add_task(function* testBrowserActionPopupResize() {
   let extension = ExtensionTestUtils.loadExtension({
     manifest: {
       "browser_action": {
         "default_popup": "popup.html",
         "browser_style": true,
       },
     },
 
     files: {
-      "popup.html": "<html><head><meta charset=\"utf-8\"></head></html>",
+      "popup.html": '<html><head><meta charset="utf-8"></head></html>',
     },
   });
 
   yield extension.startup();
 
   clickBrowserAction(extension, window);
 
-  let {target: panelDocument} = yield BrowserTestUtils.waitForEvent(document, "load", true, (event) => {
-    info(`Loaded ${event.target.location}`);
-    return event.target.location && event.target.location.href.endsWith("popup.html");
-  });
-
-  let panelWindow = panelDocument.defaultView;
-  let panelBody = panelDocument.body;
+  let browser = yield openPanel(extension);
+  let panelWindow = browser.contentWindow;
+  let panelBody = panelWindow.document.body;
 
   function checkSize(expected) {
     is(panelWindow.innerHeight, expected, `Panel window should be ${expected}px tall`);
     is(panelBody.clientHeight, panelBody.scrollHeight,
       "Panel body should be tall enough to fit its contents");
 
     // Tolerate if it is 1px too wide, as that may happen with the current resizing method.
     ok(Math.abs(panelWindow.innerWidth - expected) <= 1, `Panel window should be ${expected}px wide`);
@@ -47,15 +67,245 @@ add_task(function* testPageActionPopupRe
   let sizes = [
     200,
     400,
     300,
   ];
 
   for (let size of sizes) {
     setSize(size);
-    yield BrowserTestUtils.waitForEvent(panelWindow, "resize");
+    yield awaitResize(browser);
     checkSize(size);
   }
 
   yield closeBrowserAction(extension);
   yield extension.unload();
 });
+
+function* testPopupSize(standardsMode, browserWin = window, arrowSide = "top") {
+  let docType = standardsMode ? "<!DOCTYPE html>" : "";
+
+  let extension = ExtensionTestUtils.loadExtension({
+    manifest: {
+      "browser_action": {
+        "default_popup": "popup.html",
+        "browser_style": false,
+      },
+    },
+
+    files: {
+      "popup.html": `${docType}
+        <html>
+          <head>
+            <meta charset="utf-8">
+            <style type="text/css">
+              body > span {
+                display: inline-block;
+                width: 10px;
+                height: 150px;
+                border: 2px solid black;
+              }
+              .big > span {
+                width: 300px;
+                height: 100px;
+              }
+              .bigger > span {
+                width: 150px;
+                height: 150px;
+              }
+              .huge > span {
+                height: ${2 * screen.height}px;
+              }
+            </style>
+          </head>
+          <body>
+            <span></span>
+            <span></span>
+            <span></span>
+            <span></span>
+          </body>
+        </html>`,
+    },
+  });
+
+  yield extension.startup();
+
+  if (arrowSide == "top") {
+    // Test the standalone panel for a toolbar button.
+    let browser = yield openPanel(extension, browserWin);
+    let win = browser.contentWindow;
+    let body = win.document.body;
+
+    let isStandards = win.document.compatMode != "BackCompat";
+    is(isStandards, standardsMode, "Document has the expected compat mode");
+
+    let {innerWidth, innerHeight} = win;
+
+    body.classList.add("bigger");
+    yield awaitResize(browser);
+
+    is(win.innerHeight, innerHeight, "Window height should not change");
+    ok(win.innerWidth > innerWidth, `Window width should increase (${win.innerWidth} > ${innerWidth})`);
+
+
+    body.classList.remove("bigger");
+    yield awaitResize(browser);
+
+    is(win.innerHeight, innerHeight, "Window height should not change");
+
+    // The getContentSize calculation is not always reliable to single-pixel
+    // precision.
+    ok(Math.abs(win.innerWidth - innerWidth) <= 1,
+       `Window width should return to approximately its original value (${win.innerWidth} ~= ${innerWidth})`);
+
+    yield closeBrowserAction(extension, browserWin);
+  }
+
+
+  // Test the PanelUI panel for a menu panel button.
+  let widget = getBrowserActionWidget(extension);
+  CustomizableUI.addWidgetToArea(widget.id, CustomizableUI.AREA_PANEL);
+
+  let browser = yield openPanel(extension, browserWin);
+  let win = browser.contentWindow;
+  let body = win.document.body;
+
+  let {panel} = browserWin.PanelUI;
+  let origPanelRect = panel.getBoundingClientRect();
+
+  // Check that the panel is still positioned as expected.
+  let checkPanelPosition = () => {
+    is(panel.getAttribute("side"), arrowSide, "Panel arrow is positioned as expected");
+
+    let panelRect = panel.getBoundingClientRect();
+    if (arrowSide == "top") {
+      ok(panelRect.top, origPanelRect.top, "Panel has not moved downwards");
+      ok(panelRect.bottom >= origPanelRect.bottom, `Panel has not shrunk from original size (${panelRect.bottom} >= ${origPanelRect.bottom})`);
+
+      let screenBottom = browserWin.screen.availTop + win.screen.availHeight;
+      let panelBottom = browserWin.mozInnerScreenY + panelRect.bottom;
+      ok(panelBottom <= screenBottom, `Bottom of popup should be on-screen. (${panelBottom} <= ${screenBottom})`);
+    } else {
+      ok(panelRect.bottom, origPanelRect.bottom, "Panel has not moved upwards");
+      ok(panelRect.top <= origPanelRect.top, `Panel has not shrunk from original size (${panelRect.top} <= ${origPanelRect.top})`);
+
+      let panelTop = browserWin.mozInnerScreenY + panelRect.top;
+      ok(panelTop >= browserWin.screen.availTop, `Top of popup should be on-screen. (${panelTop} >= ${browserWin.screen.availTop})`);
+    }
+  };
+
+
+  let isStandards = win.document.compatMode != "BackCompat";
+  is(isStandards, standardsMode, "Document has the expected compat mode");
+
+  // Wait long enough to make sure the initial resize debouncing timer has
+  // expired.
+  yield new Promise(resolve => setTimeout(resolve, 100));
+
+  // If the browser's preferred height is smaller than the initial height of the
+  // panel, then it will still take up the full available vertical space. Even
+  // so, we need to check that we've gotten the preferred height calculation
+  // correct, so check that explicitly.
+  let getHeight = () => parseFloat(browser.style.height);
+
+  let {innerWidth, innerHeight} = win;
+  let height = getHeight();
+
+
+  info("Increase body children's width. " +
+       "Expect them to wrap, and the frame to grow vertically rather than widen.");
+  body.className = "big";
+  yield awaitResize(browser);
+
+  ok(getHeight() > height, `Browser height should increase (${getHeight()} > ${height})`);
+
+  is(win.innerWidth, innerWidth, "Window width should not change");
+  ok(win.innerHeight >= innerHeight, `Window height should increase (${win.innerHeight} >= ${innerHeight})`);
+  is(win.scrollMaxY, 0, "Document should not be vertically scrollable");
+
+  checkPanelPosition();
+
+
+  info("Increase body children's width and height. " +
+       "Expect them to wrap, and the frame to grow vertically rather than widen.");
+  body.className = "bigger";
+  yield awaitResize(browser);
+
+  ok(getHeight() > height, `Browser height should increase (${getHeight()} > ${height})`);
+
+  is(win.innerWidth, innerWidth, "Window width should not change");
+  ok(win.innerHeight >= innerHeight, `Window height should increase (${win.innerHeight} >= ${innerHeight})`);
+  is(win.scrollMaxY, 0, "Document should not be vertically scrollable");
+
+  checkPanelPosition();
+
+
+  info("Increase body height beyond the height of the screen. " +
+       "Expect the panel to grow to accommodate, but not larger than the height of the screen.");
+  body.className = "huge";
+  yield awaitResize(browser);
+
+  ok(getHeight() > height, `Browser height should increase (${getHeight()} > ${height})`);
+
+  is(win.innerWidth, innerWidth, "Window width should not change");
+  ok(win.innerHeight > innerHeight, `Window height should increase (${win.innerHeight} > ${innerHeight})`);
+  ok(win.innerHeight < screen.height, `Window height be less than the screen height (${win.innerHeight} < ${screen.height})`);
+  ok(win.scrollMaxY > 0, `Document should be vertically scrollable (${win.scrollMaxY} > 0)`);
+
+  checkPanelPosition();
+
+
+  info("Restore original styling. Expect original dimensions.");
+  body.className = "";
+  yield awaitResize(browser);
+
+  is(getHeight(), height, "Browser height should return to its original value");
+
+  is(win.innerWidth, innerWidth, "Window width should not change");
+  is(win.innerHeight, innerHeight, "Window height should return to its original value");
+  is(win.scrollMaxY, 0, "Document should not be vertically scrollable");
+
+  checkPanelPosition();
+
+  yield closeBrowserAction(extension, browserWin);
+
+  yield extension.unload();
+}
+
+add_task(function* testBrowserActionMenuResizeStandards() {
+  yield testPopupSize(true);
+});
+
+add_task(function* testBrowserActionMenuResizeQuirks() {
+  yield testPopupSize(false);
+});
+
+// Test that we still make reasonable maximum size calculations when the window
+// is close enough to the bottom of the screen that the menu panel opens above,
+// rather than below, its button.
+add_task(function* testBrowserActionMenuResizeBottomArrow() {
+  const WIDTH = 800;
+  const HEIGHT = 300;
+
+  let left = screen.availLeft + screen.availWidth - WIDTH;
+  let top = screen.availTop + screen.availHeight - HEIGHT;
+
+  let win = yield BrowserTestUtils.openNewBrowserWindow();
+
+  win.resizeTo(WIDTH, HEIGHT);
+
+  // Sometimes we run into problems on Linux with resizing being asynchronous
+  // and window managers not allowing us to move the window so that any part of
+  // it is off-screen, so we need to try more than once.
+  for (let i = 0; i < 20; i++) {
+    win.moveTo(left, top);
+
+    if (win.screenX == left && win.screenY == top) {
+      break;
+    }
+
+    yield new Promise(resolve => setTimeout(resolve, 100));
+  }
+
+  yield testPopupSize(true, win, "bottom");
+
+  yield BrowserTestUtils.closeWindow(win);
+});
--- a/browser/components/extensions/test/browser/browser_ext_pageAction_context.js
+++ b/browser/components/extensions/test/browser/browser_ext_pageAction_context.js
@@ -243,16 +243,25 @@ add_task(function* testTabSwitchContext(
             browser.pageAction.setIcon({tabId, path: "2.png"});
             browser.pageAction.setPopup({tabId, popup: "2.html"});
             browser.pageAction.setTitle({tabId, title: "Title 2"});
 
             expect(details[2]);
           });
         },
         expect => {
+          browser.test.log("Change the hash. Expect same properties.");
+
+          promiseTabLoad({id: tabs[1], url: "about:blank?0#ref"}).then(() => {
+            expect(details[2]);
+          });
+
+          browser.tabs.update(tabs[1], {url: "about:blank?0#ref"});
+        },
+        expect => {
           browser.test.log("Clear the title. Expect default title.");
           browser.pageAction.setTitle({tabId: tabs[1], title: ""});
 
           expect(details[3]);
         },
         expect => {
           browser.test.log("Navigate to a new page. Expect icon hidden.");
 
--- a/browser/components/extensions/test/browser/browser_ext_tabs_executeScript.js
+++ b/browser/components/extensions/test/browser/browser_ext_tabs_executeScript.js
@@ -81,16 +81,46 @@ add_task(function* testExecuteScript() {
         }).then(result => {
           browser.test.fail("Expected error when returning non-structured-clonable object");
         }, error => {
           browser.test.assertEq("Script returned non-structured-clonable data",
                                 error.message, "Got expected error");
         }),
 
         browser.tabs.executeScript({
+          frameId: Number.MAX_SAFE_INTEGER,
+          code: "42",
+        }).then(result => {
+          browser.test.fail("Expected error when specifying invalid frame ID");
+        }, error => {
+          let details = {
+            frame_id: Number.MAX_SAFE_INTEGER,
+            matchesHost: ["http://mochi.test/", "http://example.com/"],
+          };
+          browser.test.assertEq(`No window matching ${JSON.stringify(details)}`,
+                                error.message, "Got expected error");
+        }),
+
+        browser.tabs.create({url: "http://example.net/", active: false}).then(tab => {
+          return browser.tabs.executeScript(tab.id, {
+            code: "42",
+          }).then(result => {
+            browser.test.fail("Expected error when trying to execute on invalid domain");
+          }, error => {
+            let details = {
+              matchesHost: ["http://mochi.test/", "http://example.com/"],
+            };
+            browser.test.assertEq(`No window matching ${JSON.stringify(details)}`,
+                                  error.message, "Got expected error");
+          }).then(() => {
+            return browser.tabs.remove(tab.id);
+          });
+        }),
+
+        browser.tabs.executeScript({
           code: "Promise.resolve(42)",
         }).then(result => {
           browser.test.assertEq(42, result, "Got expected promise resolution value as result");
         }),
 
         browser.tabs.executeScript({
           code: "location.href;",
           runAt: "document_end",
--- a/browser/components/extensions/test/xpcshell/.eslintrc
+++ b/browser/components/extensions/test/xpcshell/.eslintrc
@@ -1,3 +1,7 @@
 {
   "extends": "../../../../../testing/xpcshell/xpcshell.eslintrc",
+
+  "globals": {
+    "browser": false,
+  },
 }
--- a/browser/components/extensions/test/xpcshell/head.js
+++ b/browser/components/extensions/test/xpcshell/head.js
@@ -1,50 +1,55 @@
 "use strict";
 
 const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
 
+/* exported createHttpServer */
+
 Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
 
+XPCOMUtils.defineLazyModuleGetter(this, "AppConstants",
+                                  "resource://gre/modules/AppConstants.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "Extension",
                                   "resource://gre/modules/Extension.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "ExtensionData",
                                   "resource://gre/modules/Extension.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "ExtensionManagement",
+                                  "resource://gre/modules/ExtensionManagement.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "ExtensionTestUtils",
+                                  "resource://testing-common/ExtensionXPCShellUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "FileUtils",
+                                  "resource://gre/modules/FileUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "HttpServer",
+                                  "resource://testing-common/httpd.js");
 XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
                                   "resource://gre/modules/NetUtil.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "Schemas",
                                   "resource://gre/modules/Schemas.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "Services",
                                   "resource://gre/modules/Services.jsm");
 
-/* exported normalizeManifest */
-
-let BASE_MANIFEST = {
-  "applications": {"gecko": {"id": "test@web.ext"}},
-
-  "manifest_version": 2,
+ExtensionTestUtils.init(this);
 
-  "name": "name",
-  "version": "0",
-};
-
-function* normalizeManifest(manifest, baseManifest = BASE_MANIFEST) {
-  const {Management} = Cu.import("resource://gre/modules/Extension.jsm", {});
-  yield Management.lazyInit();
 
-  let errors = [];
-  let context = {
-    url: null,
-
-    logError: error => {
-      errors.push(error);
-    },
+/**
+ * Creates a new HttpServer for testing, and begins listening on the
+ * specified port. Automatically shuts down the server when the test
+ * unit ends.
+ *
+ * @param {integer} [port]
+ *        The port to listen on. If omitted, listen on a random
+ *        port. The latter is the preferred behavior.
+ *
+ * @returns {HttpServer}
+ */
+function createHttpServer(port = -1) {
+  let server = new HttpServer();
+  server.start(port);
 
-    preprocessors: {},
-  };
-
-  manifest = Object.assign({}, baseManifest, manifest);
+  do_register_cleanup(() => {
+    return new Promise(resolve => {
+      server.stop(resolve);
+    });
+  });
 
-  let normalized = Schemas.normalize(manifest, "manifest.WebExtensionManifest", context);
-  normalized.errors = errors;
-
-  return normalized;
+  return server;
 }
rename from toolkit/components/extensions/test/mochitest/test_ext_bookmarks.html
rename to browser/components/extensions/test/xpcshell/test_ext_bookmarks.js
--- a/toolkit/components/extensions/test/mochitest/test_ext_bookmarks.html
+++ b/browser/components/extensions/test/xpcshell/test_ext_bookmarks.js
@@ -1,21 +1,10 @@
-<!DOCTYPE HTML>
-<html>
-<head>
-  <title>WebExtension test</title>
-  <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
-  <script type="text/javascript" src="/tests/SimpleTest/SpawnTask.js"></script>
-  <script type="text/javascript" src="/tests/SimpleTest/ExtensionTestUtils.js"></script>
-  <script type="text/javascript" src="head.js"></script>
-  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
-</head>
-<body>
-
-<script type="text/javascript">
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
 "use strict";
 
 function backgroundScript() {
   let unsortedId, ourId;
   let initialBookmarkCount = 0;
   let createdBookmarks = new Set();
   const nonExistentId = "000000000000";
   const bookmarkGuids = {
@@ -271,23 +260,23 @@ function backgroundScript() {
   }).then(results => {
     browser.test.assertEq(1, results.length, "Expected number of results returned for title search");
     checkBookmark({title: "EFF", url: "http://eff.org/", index: 0, parentId: bookmarkGuids.unfiledGuid}, results[0]);
 
     // finds menu items
     return browser.bookmarks.search("Menu Item");
   }).then(results => {
     browser.test.assertEq(1, results.length, "Expected number of results returned for menu item search");
-    checkBookmark({title: "Menu Item", url: "http://menu.org/", index: 3, parentId: bookmarkGuids.menuGuid}, results[0]);
+    checkBookmark({title: "Menu Item", url: "http://menu.org/", index: 0, parentId: bookmarkGuids.menuGuid}, results[0]);
 
     // finds toolbar items
     return browser.bookmarks.search("Toolbar Item");
   }).then(results => {
     browser.test.assertEq(1, results.length, "Expected number of results returned for toolbar item search");
-    checkBookmark({title: "Toolbar Item", url: "http://toolbar.org/", index: 2, parentId: bookmarkGuids.toolbarGuid}, results[0]);
+    checkBookmark({title: "Toolbar Item", url: "http://toolbar.org/", index: 0, parentId: bookmarkGuids.toolbarGuid}, results[0]);
 
     // finds folders
     return browser.bookmarks.search("Mozilla Folder");
   }).then(results => {
     browser.test.assertEq(1, results.length, "Expected number of folders returned");
     browser.test.assertEq("Mozilla Folder", results[0].title, "Folder has the expected title");
 
     // is case-insensitive
@@ -384,17 +373,17 @@ function backgroundScript() {
     browser.test.assertEq(2, corporationBookmark.index, "Bookmark has the expected index");
 
     return browser.bookmarks.move(corporationBookmark.id, {index: 0}).then(result => {
       browser.test.assertEq(0, result.index, "Bookmark has the expected index");
 
       return browser.bookmarks.move(corporationBookmark.id, {parentId: bookmarkGuids.menuGuid});
     }).then(result => {
       browser.test.assertEq(bookmarkGuids.menuGuid, result.parentId, "Bookmark has the expected parent");
-      browser.test.assertEq(childCount + 1, result.index, "Bookmark has the expected index");
+      browser.test.assertEq(childCount, result.index, "Bookmark has the expected index");
 
       return browser.bookmarks.move(corporationBookmark.id, {index: 1});
     }).then(result => {
       browser.test.assertEq(bookmarkGuids.menuGuid, result.parentId, "Bookmark has the expected parent");
       browser.test.assertEq(1, result.index, "Bookmark has the expected index");
 
       return browser.bookmarks.move(corporationBookmark.id, {parentId: bookmarkGuids.toolbarGuid, index: 1});
     }).then(result => {
@@ -485,13 +474,8 @@ let extensionData = {
 };
 
 add_task(function* test_contentscript() {
   let extension = ExtensionTestUtils.loadExtension(extensionData);
   yield extension.startup();
   yield extension.awaitFinish("bookmarks");
   yield extension.unload();
 });
-
-</script>
-
-</body>
-</html>
rename from browser/components/extensions/test/browser/browser_ext_history.js
rename to browser/components/extensions/test/xpcshell/test_ext_history.js
--- a/browser/components/extensions/test/browser/browser_ext_history.js
+++ b/browser/components/extensions/test/xpcshell/test_ext_history.js
@@ -80,58 +80,58 @@ add_task(function* test_delete() {
       }
     } else {
       pushVisit(visit.visits);
     }
     visits.push(visit);
   }
 
   yield PlacesUtils.history.insertMany(visits);
-  is(yield PlacesTestUtils.visitsInDB(visits[0].url), 5, "5 visits for uri found in history database");
+  equal((yield PlacesTestUtils.visitsInDB(visits[0].url)), 5, "5 visits for uri found in history database");
 
   let testUrl = visits[2].url;
-  ok(yield PlacesTestUtils.isPageInDB(testUrl), "expected url found in history database");
+  ok((yield PlacesTestUtils.isPageInDB(testUrl)), "expected url found in history database");
 
   extension.sendMessage("delete-url", testUrl);
   yield extension.awaitMessage("url-deleted");
-  is(yield PlacesTestUtils.isPageInDB(testUrl), false, "expected url not found in history database");
+  equal((yield PlacesTestUtils.isPageInDB(testUrl)), false, "expected url not found in history database");
 
   // delete 3 of the 5 visits for url 1
   let filter = {
     startTime: visits[0].visits[0].date,
     endTime: visits[0].visits[2].date,
   };
 
   extension.sendMessage("delete-range", filter);
   let removedUrls = yield extension.awaitMessage("range-deleted");
   ok(!removedUrls.includes(visits[0].url), `${visits[0].url} not received by onVisitRemoved`);
-  ok(yield PlacesTestUtils.isPageInDB(visits[0].url), "expected uri found in history database");
-  is(yield PlacesTestUtils.visitsInDB(visits[0].url), 2, "2 visits for uri found in history database");
-  ok(yield PlacesTestUtils.isPageInDB(visits[1].url), "expected uri found in history database");
-  is(yield PlacesTestUtils.visitsInDB(visits[1].url), 1, "1 visit for uri found in history database");
+  ok((yield PlacesTestUtils.isPageInDB(visits[0].url)), "expected uri found in history database");
+  equal((yield PlacesTestUtils.visitsInDB(visits[0].url)), 2, "2 visits for uri found in history database");
+  ok((yield PlacesTestUtils.isPageInDB(visits[1].url)), "expected uri found in history database");
+  equal((yield PlacesTestUtils.visitsInDB(visits[1].url)), 1, "1 visit for uri found in history database");
 
   // delete the rest of the visits for url 1, and the visit for url 2
   filter.startTime = visits[0].visits[0].date;
   filter.endTime = visits[1].visits[0].date;
 
   extension.sendMessage("delete-range", filter);
   yield extension.awaitMessage("range-deleted");
 
-  is(yield PlacesTestUtils.isPageInDB(visits[0].url), false, "expected uri not found in history database");
-  is(yield PlacesTestUtils.visitsInDB(visits[0].url), 0, "0 visits for uri found in history database");
-  is(yield PlacesTestUtils.isPageInDB(visits[1].url), false, "expected uri not found in history database");
-  is(yield PlacesTestUtils.visitsInDB(visits[1].url), 0, "0 visits for uri found in history database");
+  equal((yield PlacesTestUtils.isPageInDB(visits[0].url)), false, "expected uri not found in history database");
+  equal((yield PlacesTestUtils.visitsInDB(visits[0].url)), 0, "0 visits for uri found in history database");
+  equal((yield PlacesTestUtils.isPageInDB(visits[1].url)), false, "expected uri not found in history database");
+  equal((yield PlacesTestUtils.visitsInDB(visits[1].url)), 0, "0 visits for uri found in history database");
 
-  ok(yield PlacesTestUtils.isPageInDB(visits[3].url), "expected uri found in history database");
+  ok((yield PlacesTestUtils.isPageInDB(visits[3].url)), "expected uri found in history database");
 
   extension.sendMessage("delete-all");
   [historyClearedCount, removedUrls] = yield extension.awaitMessage("history-cleared");
-  is(PlacesUtils.history.hasHistoryEntries, false, "history is empty");
-  is(historyClearedCount, 2, "onVisitRemoved called for each clearing of history");
-  is(removedUrls.length, 3, "onVisitRemoved called the expected number of times");
+  equal(PlacesUtils.history.hasHistoryEntries, false, "history is empty");
+  equal(historyClearedCount, 2, "onVisitRemoved called for each clearing of history");
+  equal(removedUrls.length, 3, "onVisitRemoved called the expected number of times");
   for (let i = 1; i < 3; ++i) {
     let url = visits[i].url;
     ok(removedUrls.includes(url), `${url} received by onVisitRemoved`);
   }
   yield extension.unload();
 });
 
 add_task(function* test_search() {
@@ -210,45 +210,45 @@ add_task(function* test_search() {
   });
 
   function findResult(url, results) {
     return results.find(r => r.url === url);
   }
 
   function checkResult(results, url, expectedCount) {
     let result = findResult(url, results);
-    isnot(result, null, `history.search result was found for ${url}`);
-    is(result.visitCount, expectedCount, `history.search reports ${expectedCount} visit(s)`);
-    is(result.title, `test visit for ${url}`, "title for search result is correct");
+    notEqual(result, null, `history.search result was found for ${url}`);
+    equal(result.visitCount, expectedCount, `history.search reports ${expectedCount} visit(s)`);
+    equal(result.title, `test visit for ${url}`, "title for search result is correct");
   }
 
   yield extension.startup();
   yield extension.awaitMessage("ready");
   yield PlacesTestUtils.clearHistory();
 
   yield PlacesUtils.history.insertMany(PAGE_INFOS);
 
   extension.sendMessage("check-history");
 
   let results = yield extension.awaitMessage("empty-search");
-  is(results.length, 3, "history.search with empty text returned 3 results");
+  equal(results.length, 3, "history.search with empty text returned 3 results");
   checkResult(results, SINGLE_VISIT_URL, 1);
   checkResult(results, DOUBLE_VISIT_URL, 2);
   checkResult(results, MOZILLA_VISIT_URL, 1);
 
   results = yield extension.awaitMessage("text-search");
-  is(results.length, 1, "history.search with specific text returned 1 result");
+  equal(results.length, 1, "history.search with specific text returned 1 result");
   checkResult(results, MOZILLA_VISIT_URL, 1);
 
   results = yield extension.awaitMessage("max-results-search");
-  is(results.length, 1, "history.search with maxResults returned 1 result");
+  equal(results.length, 1, "history.search with maxResults returned 1 result");
   checkResult(results, DOUBLE_VISIT_URL, 2);
 
   results = yield extension.awaitMessage("date-range-search");
-  is(results.length, 2, "history.search with a date range returned 2 result");
+  equal(results.length, 2, "history.search with a date range returned 2 result");
   checkResult(results, DOUBLE_VISIT_URL, 2);
   checkResult(results, SINGLE_VISIT_URL, 1);
 
   yield extension.awaitFinish("search");
   yield extension.unload();
   yield PlacesTestUtils.clearHistory();
 });
 
@@ -295,21 +295,21 @@ add_task(function* test_add_url() {
 
   let failTestData = [
     [{transition: "generated"}, "an invalid transition", "|generated| is not a supported transition for history"],
     [{visitTime: Date.now() + 1000000}, "a future date", "cannot be a future date"],
     [{url: "about.config"}, "an invalid url", "about.config is not a valid URL"],
   ];
 
   function* checkUrl(results) {
-    ok(yield PlacesTestUtils.isPageInDB(results.details.url), `${results.details.url} found in history database`);
+    ok((yield PlacesTestUtils.isPageInDB(results.details.url)), `${results.details.url} found in history database`);
     ok(PlacesUtils.isValidGuid(results.result.id), "URL was added with a valid id");
-    is(results.result.title, results.details.title, "URL was added with the correct title");
+    equal(results.result.title, results.details.title, "URL was added with the correct title");
     if (results.details.visitTime) {
-      is(results.result.lastVisitTime,
+      equal(results.result.lastVisitTime,
          Number(ExtensionUtils.normalizeTime(results.details.visitTime)),
          "URL was added with the correct date");
     }
   }
 
   let extension = ExtensionTestUtils.loadExtension({
     manifest: {
       permissions: ["history"],
@@ -453,22 +453,22 @@ add_task(function* test_on_visited() {
 
   yield PlacesUtils.history.insertMany(PAGE_INFOS);
 
   let onVisitedData = yield extension.awaitMessage("on-visited-data");
 
   function checkOnVisitedData(index, expected) {
     let onVisited = onVisitedData[index];
     ok(PlacesUtils.isValidGuid(onVisited.id), "onVisited received a valid id");
-    is(onVisited.url, expected.url, "onVisited received the expected url");
+    equal(onVisited.url, expected.url, "onVisited received the expected url");
     // Title will be blank until bug 1287928 lands
     // https://bugzilla.mozilla.org/show_bug.cgi?id=1287928
-    is(onVisited.title, "", "onVisited received a blank title");
-    is(onVisited.lastVisitTime, expected.time, "onVisited received the expected time");
-    is(onVisited.visitCount, expected.visitCount, "onVisited received the expected visitCount");
+    equal(onVisited.title, "", "onVisited received a blank title");
+    equal(onVisited.lastVisitTime, expected.time, "onVisited received the expected time");
+    equal(onVisited.visitCount, expected.visitCount, "onVisited received the expected visitCount");
   }
 
   let expected = {
     url: PAGE_INFOS[0].url,
     title: PAGE_INFOS[0].title,
     time: PAGE_INFOS[0].visits[0].date.getTime(),
     visitCount: 1,
   };
--- a/browser/components/extensions/test/xpcshell/test_ext_manifest_commands.js
+++ b/browser/components/extensions/test/xpcshell/test_ext_manifest_commands.js
@@ -1,15 +1,15 @@
 /* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
 /* vim: set sts=2 sw=2 et tw=80: */
 "use strict";
 
 
 add_task(function* test_manifest_commands() {
-  let normalized = yield normalizeManifest({
+  let normalized = yield ExtensionTestUtils.normalizeManifest({
     "commands": {
       "toggle-feature": {
         "suggested_key": {"default": "Shifty+Y"},
         "description": "Send a 'toggle-feature' event to the extension",
       },
     },
   });
 
--- a/browser/components/extensions/test/xpcshell/xpcshell.ini
+++ b/browser/components/extensions/test/xpcshell/xpcshell.ini
@@ -1,6 +1,8 @@
 [DEFAULT]
 head = head.js
 tail =
 firefox-appdir = browser
 
+[test_ext_bookmarks.js]
+[test_ext_history.js]
 [test_ext_manifest_commands.js]
--- a/browser/components/preferences/in-content/main.xul
+++ b/browser/components/preferences/in-content/main.xul
@@ -100,16 +100,19 @@
     <preference id="browser.sessionstore.restore_on_demand"
                 name="browser.sessionstore.restore_on_demand"
                 type="bool"/>
 #ifdef XP_WIN
     <preference id="browser.taskbar.previews.enable"
                 name="browser.taskbar.previews.enable"
                 type="bool"/>
 #endif
+    <preference id="browser.ctrlTab.previews"
+                name="browser.ctrlTab.previews"
+                type="bool"/>
 </preferences>
 
 <hbox id="header-general"
       class="header"
       hidden="true"
       data-category="paneGeneral">
   <label class="header-name" flex="1">&paneGeneral.title;</label>
   <button class="help-button"
@@ -263,16 +266,21 @@
     </hbox>
   </radiogroup>
 </groupbox>
 
 <!-- Tab preferences -->
 <groupbox data-category="paneGeneral"
           hidden="true" align="start">
     <caption><label>&tabsGroup.label;</label></caption>
+
+    <checkbox id="ctrlTabRecentlyUsedOrder" label="&ctrlTabRecentlyUsedOrder.label;"
+              accesskey="&ctrlTabRecentlyUsedOrder.accesskey;"
+              preference="browser.ctrlTab.previews"/>
+
     <checkbox id="linkTargeting" label="&newWindowsAsTabs.label;"
               accesskey="&newWindowsAsTabs.accesskey;"
               preference="browser.link.open_newwindow"
               onsyncfrompreference="return gMainPane.readLinkTarget();"
               onsynctopreference="return gMainPane.writeLinkTarget();"/>
 
     <checkbox id="warnCloseMultiple" label="&warnCloseMultipleTabs.label;"
               accesskey="&warnCloseMultipleTabs.accesskey;"
--- a/browser/components/preferences/in-content/search.js
+++ b/browser/components/preferences/in-content/search.js
@@ -101,16 +101,28 @@ var gSearchPane = {
       if (e.name == currentEngine)
         list.selectedItem = item;
     });
   },
 
   handleEvent: function(aEvent) {
     switch (aEvent.type) {
       case "click":
+        if (aEvent.target.id != "engineChildren" && aEvent.target.id != "removeEngineButton") {
+          let engineList = document.getElementById("engineList");
+          // We don't want to toggle off selection while editing keyword
+          // so proceed only when the input field is hidden
+          if (engineList.inputField.hidden) {
+            let selection = engineList.view.selection;
+            if (selection.count > 0) {
+              selection.toggleSelect(selection.currentIndex);
+            }
+            engineList.blur();
+          }
+        }
         if (aEvent.target.id == "addEngines" && aEvent.button == 0) {
           Services.wm.getMostRecentWindow('navigator:browser')
                      .BrowserSearch.loadAddEngines();
         }
         break;
       case "command":
         switch (aEvent.target.id) {
           case "":
--- a/browser/extensions/pdfjs/README.mozilla
+++ b/browser/extensions/pdfjs/README.mozilla
@@ -1,3 +1,3 @@
 This is the pdf.js project output, https://github.com/mozilla/pdf.js
 
-Current extension version is: 1.5.345
+Current extension version is: 1.5.365
--- a/browser/extensions/pdfjs/content/build/pdf.js
+++ b/browser/extensions/pdfjs/content/build/pdf.js
@@ -23,18 +23,18 @@ define('pdfjs-dist/build/pdf', ['exports
     factory(exports);
   } else {
 factory((root.pdfjsDistBuildPdf = {}));
   }
 }(this, function (exports) {
   // Use strict in our context only - users might not want it
   'use strict';
 
-var pdfjsVersion = '1.5.345';
-var pdfjsBuild = '10f9f11';
+var pdfjsVersion = '1.5.365';
+var pdfjsBuild = '19105f0';
 
   var pdfjsFilePath =
     typeof document !== 'undefined' && document.currentScript ?
       document.currentScript.src : null;
 
   var pdfjsLibs = {};
 
   (function pdfjsWrapper() {
@@ -6766,16 +6766,18 @@ var PDFDocumentProxy = (function PDFDocu
 })();
 
 /**
  * Page getTextContent parameters.
  *
  * @typedef {Object} getTextContentParameters
  * @param {boolean} normalizeWhitespace - replaces all occurrences of
  *   whitespace with standard spaces (0x20). The default value is `false`.
+ * @param {boolean} disableCombineTextItems - do not attempt to combine
+ *   same line {@link TextItem}'s. The default value is `false`.
  */
 
 /**
  * Page text content.
  *
  * @typedef {Object} TextContent
  * @property {array} items - array of {@link TextItem}
  * @property {Object} styles - {@link TextStyles} objects, indexed by font
@@ -7057,21 +7059,22 @@ var PDFPageProxy = (function PDFPageProx
     },
 
     /**
      * @param {getTextContentParameters} params - getTextContent parameters.
      * @return {Promise} That is resolved a {@link TextContent}
      * object that represent the page text content.
      */
     getTextContent: function PDFPageProxy_getTextContent(params) {
-      var normalizeWhitespace = (params && params.normalizeWhitespace) || false;
-
       return this.transport.messageHandler.sendWithPromise('GetTextContent', {
         pageIndex: this.pageNumber - 1,
-        normalizeWhitespace: normalizeWhitespace,
+        normalizeWhitespace: (params && params.normalizeWhitespace === true ?
+                              true : /* Default */ false),
+        combineTextItems: (params && params.disableCombineTextItems === true ?
+                           false : /* Default */ true),
       });
     },
 
     /**
      * Destroys page object.
      */
     _destroy: function PDFPageProxy_destroy() {
       this.destroyed = true;
--- a/browser/extensions/pdfjs/content/build/pdf.worker.js
+++ b/browser/extensions/pdfjs/content/build/pdf.worker.js
@@ -23,18 +23,18 @@ define('pdfjs-dist/build/pdf.worker', ['
     factory(exports);
   } else {
 factory((root.pdfjsDistBuildPdfWorker = {}));
   }
 }(this, function (exports) {
   // Use strict in our context only - users might not want it
   'use strict';
 
-var pdfjsVersion = '1.5.345';
-var pdfjsBuild = '10f9f11';
+var pdfjsVersion = '1.5.365';
+var pdfjsBuild = '19105f0';
 
   var pdfjsFilePath =
     typeof document !== 'undefined' && document.currentScript ?
       document.currentScript.src : null;
 
   var pdfjsLibs = {};
 
   (function pdfjsWrapper() {
@@ -36501,66 +36501,88 @@ var PartialEvaluator = (function Partial
         return this.fontCache.get(fontRef);
       }
 
       font = xref.fetchIfRef(fontRef);
       if (!isDict(font)) {
         return errorFont();
       }
 
-      // We are holding font.translated references just for fontRef that are not
-      // dictionaries (Dict). See explanation below.
+      // We are holding `font.translated` references just for `fontRef`s that
+      // are not actually `Ref`s, but rather `Dict`s. See explanation below.
       if (font.translated) {
         return font.translated;
       }
 
       var fontCapability = createPromiseCapability();
 
       var preEvaluatedFont = this.preEvaluateFont(font, xref);
       var descriptor = preEvaluatedFont.descriptor;
-      var fontID = fontRef.num + '_' + fontRef.gen;
+
+      var fontRefIsRef = isRef(fontRef), fontID;
+      if (fontRefIsRef) {
+        fontID = fontRef.toString();
+      }
+
       if (isDict(descriptor)) {
         if (!descriptor.fontAliases) {
           descriptor.fontAliases = Object.create(null);
         }
 
         var fontAliases = descriptor.fontAliases;
         var hash = preEvaluatedFont.hash;
         if (fontAliases[hash]) {
           var aliasFontRef = fontAliases[hash].aliasRef;
-          if (aliasFontRef && this.fontCache.has(aliasFontRef)) {
+          if (fontRefIsRef && aliasFontRef &&
+              this.fontCache.has(aliasFontRef)) {
             this.fontCache.putAlias(fontRef, aliasFontRef);
             return this.fontCache.get(fontRef);
           }
-        }
-
-        if (!fontAliases[hash]) {
+        } else {
           fontAliases[hash] = {
             fontID: Font.getFontID()
           };
         }
 
-        fontAliases[hash].aliasRef = fontRef;
+        if (fontRefIsRef) {
+          fontAliases[hash].aliasRef = fontRef;
+        }
         fontID = fontAliases[hash].fontID;
       }
 
-      // Workaround for bad PDF generators that don't reference fonts
-      // properly, i.e. by not using an object identifier.
-      // Check if the fontRef is a Dict (as opposed to a standard object),
-      // in which case we don't cache the font and instead reference it by
-      // fontName in font.loadedName below.
-      var fontRefIsDict = isDict(fontRef);
-      if (!fontRefIsDict) {
+      // Workaround for bad PDF generators that reference fonts incorrectly,
+      // where `fontRef` is a `Dict` rather than a `Ref` (fixes bug946506.pdf).
+      // In this case we should not put the font into `this.fontCache` (which is
+      // a `RefSetCache`), since it's not meaningful to use a `Dict` as a key.
+      //
+      // However, if we don't cache the font it's not possible to remove it
+      // when `cleanup` is triggered from the API, which causes issues on
+      // subsequent rendering operations (see issue7403.pdf).
+      // A simple workaround would be to just not hold `font.translated`
+      // references in this case, but this would force us to unnecessarily load
+      // the same fonts over and over.
+      //
+      // Instead, we cheat a bit by attempting to use a modified `fontID` as a
+      // key in `this.fontCache`, to allow the font to be cached.
+      // NOTE: This works because `RefSetCache` calls `toString()` on provided
+      //       keys. Also, since `fontRef` is used when getting cached fonts,
+      //       we'll not accidentally match fonts cached with the `fontID`.
+      if (fontRefIsRef) {
         this.fontCache.put(fontRef, fontCapability.promise);
-      }
+      } else {
+        if (!fontID) {
+          fontID = (this.uniquePrefix || 'F_') + (++this.idCounters.obj);
+        }
+        this.fontCache.put('id_' + fontID, fontCapability.promise);
+      }
+      assert(fontID, 'The "fontID" must be defined.');
 
       // Keep track of each font we translated so the caller can
       // load them asynchronously before calling display on a page.
-      font.loadedName = 'g_' + this.pdfManager.docId + '_f' + (fontRefIsDict ?
-        fontName.replace(/\W/g, '') : fontID);
+      font.loadedName = 'g_' + this.pdfManager.docId + '_f' + fontID;
 
       font.translated = fontCapability.promise;
 
       // TODO move promises into translate font
       var translatedPromise;
       try {
         translatedPromise = this.translateFont(preEvaluatedFont, xref);
       } catch (e) {
@@ -36949,17 +36971,18 @@ var PartialEvaluator = (function Partial
         }
         resolve();
       });
     },
 
     getTextContent:
         function PartialEvaluator_getTextContent(stream, task, resources,
                                                  stateManager,
-                                                 normalizeWhitespace) {
+                                                 normalizeWhitespace,
+                                                 combineTextItems) {
 
       stateManager = (stateManager || new StateManager(new TextState()));
 
       var WhitespaceRegexp = /\s/g;
 
       var textContent = {
         items: [],
         styles: Object.create(null)
@@ -37260,17 +37283,18 @@ var PartialEvaluator = (function Partial
               flushTextContentItem();
               textState.leading = args[0];
               break;
             case OPS.moveText:
               // Optimization to treat same line movement as advance
               var isSameTextLine = !textState.font ? false :
                 ((textState.font.vertical ? args[0] : args[1]) === 0);
               advance = args[0] - args[1];
-              if (isSameTextLine && textContentItem.initialized &&
+              if (combineTextItems &&
+                  isSameTextLine && textContentItem.initialized &&
                   advance > 0 &&
                   advance <= textContentItem.fakeMultiSpaceMax) {
                 textState.translateTextLineMatrix(args[0], args[1]);
                 textContentItem.width +=
                   (args[0] - textContentItem.lastAdvanceWidth);
                 textContentItem.height +=
                   (args[1] - textContentItem.lastAdvanceHeight);
                 diff = (args[0] - textContentItem.lastAdvanceWidth) -
@@ -37292,17 +37316,18 @@ var PartialEvaluator = (function Partial
             case OPS.nextLine:
               flushTextContentItem();
               textState.carriageReturn();
               break;
             case OPS.setTextMatrix:
               // Optimization to treat same line movement as advance.
               advance = textState.calcTextLineMatrixAdvance(
                 args[0], args[1], args[2], args[3], args[4], args[5]);
-              if (advance !== null && textContentItem.initialized &&
+              if (combineTextItems &&
+                  advance !== null && textContentItem.initialized &&
                   advance.value > 0 &&
                   advance.value <= textContentItem.fakeMultiSpaceMax) {
                 textState.translateTextLineMatrix(advance.width,
                                                   advance.height);
                 textContentItem.width +=
                   (advance.width - textContentItem.lastAdvanceWidth);
                 textContentItem.height +=
                   (advance.height - textContentItem.lastAdvanceHeight);
@@ -37433,17 +37458,18 @@ var PartialEvaluator = (function Partial
               stateManager.save();
               var matrix = xobj.dict.getArray('Matrix');
               if (isArray(matrix) && matrix.length === 6) {
                 stateManager.transform(matrix);
               }
 
               next(self.getTextContent(xobj, task,
                    xobj.dict.get('Resources') || resources, stateManager,
-                   normalizeWhitespace).then(function (formTextContent) {
+                   normalizeWhitespace, combineTextItems).then(
+                function (formTextContent) {
                   Util.appendToArray(textContent.items, formTextContent.items);
                   Util.extendObj(textContent.styles, formTextContent.styles);
                   stateManager.restore();
 
                   xobjsCache.key = name;
                   xobjsCache.texts = formTextContent;
                 }));
               return;
@@ -37589,30 +37615,36 @@ var PartialEvaluator = (function Partial
       if (properties.toUnicode && properties.toUnicode.length !== 0) {
         return Promise.resolve(properties.toUnicode);
       }
       // According to the spec if the font is a simple font we should only map
       // to unicode if the base encoding is MacRoman, MacExpert, or WinAnsi or
       // the differences array only contains adobe standard or symbol set names,
       // in pratice it seems better to always try to create a toUnicode
       // map based of the default encoding.
-      var toUnicode, charcode;
+      var toUnicode, charcode, glyphName;
       if (!properties.composite /* is simple font */) {
         toUnicode = [];
         var encoding = properties.defaultEncoding.slice();
         var baseEncodingName = properties.baseEncodingName;
         // Merge in the differences array.
         var differences = properties.differences;
         for (charcode in differences) {
-          encoding[charcode] = differences[charcode];
+          glyphName = differences[charcode];
+          if (glyphName === '.notdef') {
+            // Skip .notdef to prevent rendering errors, e.g. boxes appearing
+            // where there should be spaces (fixes issue5256.pdf).
+            continue;
+          }
+          encoding[charcode] = glyphName;
         }
         var glyphsUnicodeMap = getGlyphsUnicode();
         for (charcode in encoding) {
           // a) Map the character code to a character name.
-          var glyphName = encoding[charcode];
+          glyphName = encoding[charcode];
           // b) Look up the character name in the Adobe Glyph List (see the
           //    Bibliography) to obtain the corresponding Unicode value.
           if (glyphName === '') {
             continue;
           } else if (glyphsUnicodeMap[glyphName] === undefined) {
             // (undocumented) c) Few heuristics to recognize unknown glyphs
             // NOTE: Adobe Reader does not do this step, but OSX Preview does
             var code = 0;
@@ -37954,25 +37986,25 @@ var PartialEvaluator = (function Partial
 
       var descriptor = dict.get('FontDescriptor');
       if (descriptor) {
         var hash = new MurmurHash3_64();
         var encoding = baseDict.getRaw('Encoding');
         if (isName(encoding)) {
           hash.update(encoding.name);
         } else if (isRef(encoding)) {
-          hash.update(encoding.num + '_' + encoding.gen);
+          hash.update(encoding.toString());
         } else if (isDict(encoding)) {
           var keys = encoding.getKeys();
           for (var i = 0, ii = keys.length; i < ii; i++) {
             var entry = encoding.getRaw(keys[i]);
             if (isName(entry)) {
               hash.update(entry.name);
             } else if (isRef(entry)) {
-              hash.update(entry.num + '_' + entry.gen);
+              hash.update(entry.toString());
             } else if (isArray(entry)) { // 'Differences' entry.
               // Ideally we should check the contents of the array, but to avoid
               // parsing it here and then again in |extractDataStructures|,
               // we only use the array length for now (fixes bug1157493.pdf).
               hash.update(entry.length.toString());
             }
           }
         }
@@ -39160,23 +39192,24 @@ AnnotationFactory.prototype = /** @lends
   create: function AnnotationFactory_create(xref, ref) {
     var dict = xref.fetchIfRef(ref);
     if (!isDict(dict)) {
       return;
     }
 
     // Determine the annotation's subtype.
     var subtype = dict.get('Subtype');
-    subtype = isName(subtype) ? subtype.name : '';
+    subtype = isName(subtype) ? subtype.name : null;
 
     // Return the right annotation object based on the subtype and field type.
     var parameters = {
       xref: xref,
       dict: dict,
-      ref: ref
+      ref: ref,
+      subtype: subtype,
     };
 
     switch (subtype) {
       case 'Link':
         return new LinkAnnotation(parameters);
 
       case 'Text':
         return new TextAnnotation(parameters);
@@ -39202,18 +39235,22 @@ AnnotationFactory.prototype = /** @lends
 
       case 'StrikeOut':
         return new StrikeOutAnnotation(parameters);
 
       case 'FileAttachment':
         return new FileAttachmentAnnotation(parameters);
 
       default:
-        warn('Unimplemented annotation type "' + subtype + '", ' +
-             'falling back to base annotation');
+        if (!subtype) {
+          warn('Annotation is missing the required /Subtype.');
+        } else {
+          warn('Unimplemented annotation type "' + subtype + '", ' +
+               'falling back to base annotation.');
+        }
         return new Annotation(parameters);
     }
   }
 };
 
 var Annotation = (function AnnotationClosure() {
   // 12.5.5: Algorithm: Appearance streams
   function getTransformMatrix(rect, bbox, matrix) {
@@ -39267,17 +39304,17 @@ var Annotation = (function AnnotationClo
     this.setRectangle(dict.getArray('Rect'));
     this.setColor(dict.getArray('C'));
     this.setBorderStyle(dict);
     this.appearance = getDefaultAppearance(dict);
 
     // Expose public properties using a data object.
     this.data = {};
     this.data.id = params.ref.toString();
-    this.data.subtype = dict.get('Subtype').name;
+    this.data.subtype = params.subtype;
     this.data.annotationFlags = this.flags;
     this.data.rect = this.rectangle;
     this.data.color = this.color;
     this.data.borderStyle = this.borderStyle;
     this.data.hasAppearance = !!this.appearance;
   }
 
   Annotation.prototype = {
@@ -40275,17 +40312,18 @@ var Page = (function PageClosure() {
         return annotationsReadyPromise.then(function () {
           pageOpList.flush(true);
           return pageOpList;
         });
       });
     },
 
     extractTextContent: function Page_extractTextContent(task,
-                                                         normalizeWhitespace) {
+                                                         normalizeWhitespace,
+                                                         combineTextItems) {
       var handler = {
         on: function nullHandlerOn() {},
         send: function nullHandlerSend() {}
       };
 
       var self = this;
 
       var pdfManager = this.pdfManager;
@@ -40308,17 +40346,18 @@ var Page = (function PageClosure() {
                                                     self.idCounters,
                                                     self.fontCache,
                                                     self.evaluatorOptions);
 
         return partialEvaluator.getTextContent(contentStream,
                                                task,
                                                self.resources,
                                                /* stateManager = */ null,
-                                               normalizeWhitespace);
+                                               normalizeWhitespace,
+                                               combineTextItems);
       });
     },
 
     getAnnotationsData: function Page_getAnnotationsData(intent) {
       var annotations = this.annotations;
       var annotationsData = [];
       for (var i = 0, n = annotations.length; i < n; ++i) {
         if (intent) {
@@ -41572,22 +41611,24 @@ var WorkerMessageHandler = {
           });
         });
       });
     }, this);
 
     handler.on('GetTextContent', function wphExtractText(data) {
       var pageIndex = data.pageIndex;
       var normalizeWhitespace = data.normalizeWhitespace;
+      var combineTextItems = data.combineTextItems;
       return pdfManager.getPage(pageIndex).then(function(page) {
         var task = new WorkerTask('GetTextContent: page ' + pageIndex);
         startWorkerTask(task);
         var pageNum = pageIndex + 1;
         var start = Date.now();
-        return page.extractTextContent(task, normalizeWhitespace).then(
+        return page.extractTextContent(task, normalizeWhitespace,
+                                       combineTextItems).then(
             function(textContent) {
           finishWorkerTask(task);
           info('text indexing: page=' + pageNum + ' - time=' +
                (Date.now() - start) + 'ms');
           return textContent;
         }, function (reason) {
           finishWorkerTask(task);
           if (task.terminated) {
--- a/browser/extensions/pdfjs/content/web/viewer.js
+++ b/browser/extensions/pdfjs/content/web/viewer.js
@@ -2521,17 +2521,16 @@ exports.binarySearchFirstItem = binarySe
         pageNumber: e.pageNumber
       });
       e.source.textLayerDiv.dispatchEvent(event);
     });
     eventBus.on('pagechange', function (e) {
       var event = document.createEvent('UIEvents');
       event.initUIEvent('pagechange', true, true, window, 0);
       event.pageNumber = e.pageNumber;
-      event.previousPageNumber = e.previousPageNumber;
       e.source.container.dispatchEvent(event);
     });
     eventBus.on('pagesinit', function (e) {
       var event = document.createEvent('CustomEvent');
       event.initCustomEvent('pagesinit', true, true, null);
       e.source.container.dispatchEvent(event);
     });
     eventBus.on('pagesloaded', function (e) {
@@ -5469,22 +5468,22 @@ var PDFPageView = (function PDFPageViewC
       };
       var renderTask = this.renderTask = this.pdfPage.render(renderContext);
       renderTask.onContinue = renderContinueCallback;
 
       this.renderTask.promise.then(
         function pdfPageRenderCallback() {
           pageViewDrawCallback(null);
           if (textLayer) {
-            self.pdfPage.getTextContent({ normalizeWhitespace: true }).then(
-              function textContentResolved(textContent) {
-                textLayer.setTextContent(textContent);
-                textLayer.render(TEXT_LAYER_RENDER_DELAY);
-              }
-            );
+            self.pdfPage.getTextContent({
+              normalizeWhitespace: true,
+            }).then(function textContentResolved(textContent) {
+              textLayer.setTextContent(textContent);
+              textLayer.render(TEXT_LAYER_RENDER_DELAY);
+            });
           }
         },
         function pdfPageRenderError(error) {
           pageViewDrawCallback(error);
         }
       );
 
       if (this.annotationLayerFactory) {
@@ -6352,16 +6351,19 @@ var PDFViewer = (function pdfViewer() {
     get currentPageNumber() {
       return this._currentPageNumber;
     },
 
     /**
      * @param {number} val - The page number.
      */
     set currentPageNumber(val) {
+      if ((val | 0) !== val) { // Ensure that `val` is an integer.
+        throw new Error('Invalid page number.');
+      }
       if (!this.pdfDocument) {
         this._currentPageNumber = val;
         return;
       }
       // The intent can be to just reset a scroll position and/or scale.
       this._setCurrentPageNumber(val, /* resetCurrentPageView = */ true);
     },
 
@@ -6371,32 +6373,24 @@ var PDFViewer = (function pdfViewer() {
     _setCurrentPageNumber:
         function pdfViewer_setCurrentPageNumber(val, resetCurrentPageView) {
       if (this._currentPageNumber === val) {
         if (resetCurrentPageView) {
           this._resetCurrentPageView();
         }
         return;
       }
-      var arg;
+
       if (!(0 < val && val <= this.pagesCount)) {
-        arg = {
-          source: this,
-          pageNumber: this._currentPageNumber,
-          previousPageNumber: val
-        };
-        this.eventBus.dispatch('pagechanging', arg);
-        this.eventBus.dispatch('pagechange', arg);
         return;
       }
 
-      arg = {
+      var arg = {
         source: this,
         pageNumber: val,
-        previousPageNumber: this._currentPageNumber
       };
       this._currentPageNumber = val;
       this.eventBus.dispatch('pagechanging', arg);
       this.eventBus.dispatch('pagechange', arg);
 
       if (resetCurrentPageView) {
         this._resetCurrentPageView();
       }
@@ -6409,17 +6403,17 @@ var PDFViewer = (function pdfViewer() {
       return this._currentScale !== UNKNOWN_SCALE ? this._currentScale :
                                                     DEFAULT_SCALE;
     },
 
     /**
      * @param {number} val - Scale of the pages in percents.
      */
     set currentScale(val) {
-      if (isNaN(val))  {
+      if (isNaN(val)) {
         throw new Error('Invalid numeric scale');
       }
       if (!this.pdfDocument) {
         this._currentScale = val;
         this._currentScaleValue = val !== UNKNOWN_SCALE ? val.toString() : null;
         return;
       }
       this._setScale(val, false);
@@ -6970,17 +6964,19 @@ var PDFViewer = (function pdfViewer() {
         }.bind(this));
         return true;
       }
       return false;
     },
 
     getPageTextContent: function (pageIndex) {
       return this.pdfDocument.getPage(pageIndex + 1).then(function (page) {
-        return page.getTextContent({ normalizeWhitespace: true });
+        return page.getTextContent({
+          normalizeWhitespace: true,
+        });
       });
     },
 
     /**
      * @param {HTMLDivElement} textLayerDiv
      * @param {number} pageIndex
      * @param {PageViewport} viewport
      * @returns {TextLayerBuilder}
@@ -8328,21 +8324,22 @@ function webViewerInitialized() {
       PDFViewerApplication.zoomOut();
     });
 
   appConfig.toolbar.pageNumber.addEventListener('click', function() {
     this.select();
   });
 
   appConfig.toolbar.pageNumber.addEventListener('change', function() {
-    // Handle the user inputting a floating point number.
     PDFViewerApplication.page = (this.value | 0);
 
-    if (this.value !== (this.value | 0).toString()) {
-      this.value = PDFViewerApplication.page;
+    // Ensure that the page number input displays the correct value, even if the
+    // value entered by the user was invalid (e.g. a floating point number).
+    if (this.value !== PDFViewerApplication.page.toString()) {
+      PDFViewerApplication._updateUIToolbar({});
     }
   });
 
   appConfig.toolbar.scaleSelect.addEventListener('change', function() {
     if (this.value === 'custom') {
       return;
     }
     PDFViewerApplication.pdfViewer.currentScaleValue = this.value;
@@ -8688,18 +8685,18 @@ function webViewerScaleChanging(e) {
 }
 
 function webViewerPageChanging(e) {
   var page = e.pageNumber;
 
   PDFViewerApplication._updateUIToolbar({
     pageNumber: page,
   });
-  if (e.previousPageNumber !== page &&
-      PDFViewerApplication.pdfSidebar.isThumbnailViewVisible) {
+
+  if (PDFViewerApplication.pdfSidebar.isThumbnailViewVisible) {
     PDFViewerApplication.pdfThumbnailViewer.scrollThumbnailIntoView(page);
   }
 
   // we need to update stats
   if (pdfjsLib.PDFJS.pdfBug && Stats.enabled) {
     var pageView = PDFViewerApplication.pdfViewer.getPageView(page - 1);
     if (pageView.stats) {
       Stats.add(page, pageView.stats);
--- a/browser/locales/en-US/chrome/browser/preferences/main.dtd
+++ b/browser/locales/en-US/chrome/browser/preferences/main.dtd
@@ -27,17 +27,17 @@
 <!ENTITY chooseFolderWin.label        "Browse…">
 <!ENTITY chooseFolderWin.accesskey    "o">
 <!ENTITY chooseFolderMac.label        "Choose…">
 <!ENTITY chooseFolderMac.accesskey    "e">
 <!ENTITY alwaysAsk.label "Always ask me where to save files">
 <!ENTITY alwaysAsk.accesskey "A">
 
 <!ENTITY alwaysCheckDefault2.label        "Always check if &brandShortName; is your default browser">
-<!ENTITY alwaysCheckDefault2.accesskey    "w">
+<!ENTITY alwaysCheckDefault2.accesskey    "y">
 <!ENTITY setAsMyDefaultBrowser2.label     "Make Default">
 <!ENTITY setAsMyDefaultBrowser2.accesskey "D">
 <!ENTITY isDefault.label                  "&brandShortName; is currently your default browser">
 <!ENTITY isNotDefault.label               "&brandShortName; is not your default browser">
 
 <!ENTITY separateProfileMode.label        "Allow &brandShortName; and Firefox to run at the same time">
 <!ENTITY useFirefoxSync.label             "Tip: This uses separate profiles. Use Sync to share data between them.">
 <!ENTITY getStarted.label                 "Start using Sync…">
--- a/browser/locales/en-US/chrome/browser/preferences/tabs.dtd
+++ b/browser/locales/en-US/chrome/browser/preferences/tabs.dtd
@@ -1,14 +1,17 @@
 <!-- This Source Code Form is subject to the terms of the Mozilla Public
    - License, v. 2.0. If a copy of the MPL was not distributed with this
    - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
 
+<!ENTITY ctrlTabRecentlyUsedOrder.label       "Ctrl+Tab cycles through tabs in recently used order">
+<!ENTITY ctrlTabRecentlyUsedOrder.accesskey   "T">
+
 <!ENTITY newWindowsAsTabs.label       "Open new windows in a new tab instead">
-<!ENTITY newWindowsAsTabs.accesskey   "t">
+<!ENTITY newWindowsAsTabs.accesskey   "w">
 
 <!ENTITY warnCloseMultipleTabs.label  "Warn me when closing multiple tabs">
 <!ENTITY warnCloseMultipleTabs.accesskey  "m">
 
 <!ENTITY warnOpenManyTabs.label       "Warn me when opening multiple tabs might slow down &brandShortName;">
 <!ENTITY warnOpenManyTabs.accesskey   "d">
 
 <!ENTITY switchToNewTabs.label        "When I open a link in a new tab, switch to it immediately">
--- a/browser/modules/ContentLinkHandler.jsm
+++ b/browser/modules/ContentLinkHandler.jsm
@@ -103,47 +103,19 @@ this.ContentLinkHandler = {
                 }
               }
             }
           } else {
             sizesType = SIZES_TELEMETRY_ENUM.NO_SIZES;
           }
           sizeHistogramTypes.add(sizesType);
 
-	  if (uri.scheme == 'blob') {
-            // Blob URLs don't work cross process, work around this by sending as a data uri
-            let channel = NetUtil.newChannel({
-              uri: uri,
-              contentPolicyType: Ci.nsIContentPolicy.TYPE_INTERNAL_IMAGE,
-              loadUsingSystemPrincipal: true
-            });
-            let listener = {
-              encoded: "",
-              bis: null,
-              onStartRequest: function(aRequest, aContext) {
-                this.bis = Components.classes["@mozilla.org/binaryinputstream;1"]
-                    .createInstance(Components.interfaces.nsIBinaryInputStream);
-              },
-              onStopRequest: function(aRequest, aContext, aStatusCode) {
-                let spec = "data:" + channel.contentType + ";base64," + this.encoded;
-                chromeGlobal.sendAsyncMessage(
-                  "Link:SetIcon",
-                  {url: spec, loadingPrincipal: link.ownerDocument.nodePrincipal});
-              },
-              onDataAvailable: function(request, context, inputStream, offset, count) {
-                this.bis.setInputStream(inputStream);
-                this.encoded += btoa(this.bis.readBytes(this.bis.available()));
-              }
-            }
-            channel.asyncOpen2(listener);
-          } else {
-            chromeGlobal.sendAsyncMessage(
-              "Link:SetIcon",
-              {url: uri.spec, loadingPrincipal: link.ownerDocument.nodePrincipal});
-          }
+          chromeGlobal.sendAsyncMessage(
+            "Link:SetIcon",
+            {url: uri.spec, loadingPrincipal: link.ownerDocument.nodePrincipal});
           iconAdded = true;
           break;
         case "search":
           if (!searchAdded && event.type == "DOMLinkAdded") {
             var type = link.type && link.type.toLowerCase();
             type = type.replace(/^\s+|\s*(?:;.*)?$/g, "");
 
             let re = /^(?:https?|ftp):/i;
--- a/browser/themes/osx/devedition.css
+++ b/browser/themes/osx/devedition.css
@@ -86,20 +86,16 @@
 }
 
 #back-button:hover:active:not([disabled="true"]) {
   -moz-image-region: rect(18px, 54px, 36px, 36px);
 }
 
 /* Use smaller back button icon */
 @media (min-resolution: 2dppx) {
-  #back-button {
-    -moz-image-region: rect(0, 108px, 36px, 72px);
-  }
-
   #back-button:hover:active:not([disabled="true"]) {
     -moz-image-region: rect(36px, 108px, 72px, 72px);
   }
 }
 
 #forward-button:hover:active:not(:-moz-lwtheme) {
   background-image: none;
   box-shadow: none;
--- a/browser/themes/shared/controlcenter/panel.inc.css
+++ b/browser/themes/shared/controlcenter/panel.inc.css
@@ -357,18 +357,18 @@ description#identity-popup-content-verif
 
 #identity-popup-permission-list {
   /* Offset the padding set on #identity-popup-permissions-content so that it
      shows up just below the section. The permission icons are 16px wide and
      should be right aligned with the section icon. */
   margin-inline-start: calc(-1em - 16px);
 }
 
-#identity-popup-permission-list menulist {
-  min-width: 60px;
+.identity-popup-permission-item {
+  min-height: 24px;
 }
 
 #identity-popup-permission-list:not(:empty) {
   margin-top: 5px;
 }
 
 #identity-popup-permission-list:not(:empty) + description {
   display: none;
@@ -376,10 +376,53 @@ description#identity-popup-content-verif
 
 .identity-popup-permission-icon {
   width: 16px;
   height: 16px;
 }
 
 .identity-popup-permission-label {
   margin-inline-start: 1em;
-  word-wrap: break-word;
+}
+
+.identity-popup-permission-state-label {
+  text-align: end;
+  opacity: 0.6;
+}
+
+.identity-popup-permission-remove-button {
+  -moz-appearance: none;
+  margin: 0;
+  border-width: 0;
+  border-radius: 50%;
+  min-width: 0;
+  padding: 2px;
+}
+
+.identity-popup-permission-remove-button > .button-box {
+  border-width: 0;
+  padding: 0;
 }
+
+.identity-popup-permission-remove-button > .button-box > .button-icon {
+  margin: 0;
+  width: 16px;
+  height: 16px;
+  list-style-image: url(chrome://browser/skin/panel-icons.svg#cancel);
+  filter: url(chrome://browser/skin/filters.svg#fill);
+  fill: #999;
+}
+
+.identity-popup-permission-remove-button > .button-box > .button-text {
+  display: none;
+}
+
+.identity-popup-permission-remove-button:hover {
+  background-color: #999;
+}
+
+.identity-popup-permission-remove-button:hover > .button-box > .button-icon {
+  fill: #fff;
+}
+
+.identity-popup-permission-remove-button:hover:active {
+  background-color: #808080;
+}
--- a/browser/themes/shared/customizableui/panelUI.inc.css
+++ b/browser/themes/shared/customizableui/panelUI.inc.css
@@ -272,16 +272,20 @@ panelmultiview[nosubviews=true] > .panel
 .cui-widget-panel[viewId^=PanelUI-webext-] > .panel-arrowcontainer > .panel-arrowcontent {
   padding: 0;
 }
 
 .cui-widget-panelview[id^=PanelUI-webext-] {
   border-radius: 3.5px;
 }
 
+panelview[id^=PanelUI-webext-] {
+  overflow: hidden;
+}
+
 panelview:not([mainview]) .toolbarbutton-text,
 .cui-widget-panel toolbarbutton > .toolbarbutton-text {
   text-align: start;
   display: -moz-box;
 }
 
 .cui-widget-panel > .panel-arrowcontainer > .panel-arrowcontent {
   padding: 4px 0;
--- a/browser/themes/shared/devedition.inc.css
+++ b/browser/themes/shared/devedition.inc.css
@@ -223,16 +223,22 @@ window:not([chromehidden~="toolbar"]) #u
   padding-bottom: 0;
 }
 
 /* Use smaller back button icon */
 #back-button {
   -moz-image-region: rect(0, 54px, 18px, 36px);
 }
 
+@media (min-resolution: 1.1dppx) {
+  #back-button {
+    -moz-image-region: rect(0, 108px, 36px, 72px);
+  }
+}
+
 .tab-background {
   visibility: hidden;
 }
 
 /* Tab separators */
 .tabbrowser-tab::after,
 .tabbrowser-tab::before {
   background: currentColor;
--- a/browser/themes/shared/jar.inc.mn
+++ b/browser/themes/shared/jar.inc.mn
@@ -67,16 +67,17 @@
   skin/classic/browser/identity-mixed-active-loaded.svg        (../shared/identity-block/identity-mixed-active-loaded.svg)
   skin/classic/browser/info.svg                                (../shared/info.svg)
   skin/classic/browser/notification-icons.svg                  (../shared/notification-icons.svg)
   skin/classic/browser/tracking-protection-16.svg              (../shared/identity-block/tracking-protection-16.svg)
   skin/classic/browser/tracking-protection-disabled-16.svg     (../shared/identity-block/tracking-protection-disabled-16.svg)
   skin/classic/browser/newtab/close.png                        (../shared/newtab/close.png)
   skin/classic/browser/newtab/controls.svg                     (../shared/newtab/controls.svg)
   skin/classic/browser/newtab/whimsycorn.png                   (../shared/newtab/whimsycorn.png)
+  skin/classic/browser/panel-icons.svg                         (../shared/panel-icons.svg)
   skin/classic/browser/preferences/in-content/favicon.ico      (../shared/incontentprefs/favicon.ico)
   skin/classic/browser/preferences/in-content/icons.svg        (../shared/incontentprefs/icons.svg)
   skin/classic/browser/preferences/in-content/search.css       (../shared/incontentprefs/search.css)
   skin/classic/browser/fxa/default-avatar.svg                  (../shared/fxa/default-avatar.svg)
   skin/classic/browser/fxa/logo.png                            (../shared/fxa/logo.png)
   skin/classic/browser/fxa/logo@2x.png                         (../shared/fxa/logo@2x.png)
   skin/classic/browser/fxa/sync-illustration.png               (../shared/fxa/sync-illustration.png)
   skin/classic/browser/fxa/sync-illustration@2x.png            (../shared/fxa/sync-illustration@2x.png)
--- a/browser/themes/shared/notification-icons.inc.css
+++ b/browser/themes/shared/notification-icons.inc.css
@@ -36,17 +36,17 @@
 }
 
 .popup-notification-icon {
   width: 64px;
   height: 64px;
   margin-inline-end: 10px;
 }
 
-#notification-popup-box > .notification-anchor-icon:hover {
+#notification-popup-box > .notification-anchor-icon:not(.in-use):hover {
   fill: #606060;
 }
 
 /* INDIVIDUAL NOTIFICATIONS */
 
 /* For the moment we apply the color filter only on the icons listed here.
    The first two selectors are used by socialchat.xml (bug 1275558). */
 .webRTC-sharingDevices-notification-icon,
@@ -269,17 +269,17 @@
 /* PLUGINS */
 
 .plugin-icon {
   list-style-image: url(chrome://browser/skin/notification-icons.svg#plugin);
 }
 
 .plugin-icon.plugin-blocked {
   list-style-image: url(chrome://browser/skin/notification-icons.svg#plugin-blocked);
-  fill: #d92215;
+  fill: #d92215 !important; /* important! to override the default hover color */
 }
 
 #notification-popup-box[hidden] {
   /* Override display:none to make the pluginBlockedNotification animation work
      when showing the notification repeatedly. */
   display: -moz-box;
   visibility: collapse;
 }
new file mode 100644
--- /dev/null
+++ b/browser/themes/shared/panel-icons.svg
@@ -0,0 +1,8 @@
+<?xml version="1.0"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+   - License, v. 2.0. If a copy of the MPL was not distributed with this
+   - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<svg xmlns="http://www.w3.org/2000/svg"
+     width="32" height="32" viewBox="0 0 32 32">
+  <path id="cancel" d="m 6,9.5 6.5,6.5 -6.5,6.5 3.5,3.5 6.5,-6.5 6.5,6.5 3.5,-3.5 -6.5,-6.5 6.5,-6.5 -3.5,-3.5 -6.5,6.5 -6.5,-6.5 z" />
+</svg>
--- a/browser/themes/windows/devedition.css
+++ b/browser/themes/windows/devedition.css
@@ -38,48 +38,38 @@
 #TabsToolbar::after {
   display: none;
 }
 
 #back-button > .toolbarbutton-icon,
 #forward-button > .toolbarbutton-icon {
   background: var(--chrome-nav-buttons-background) !important;
   border-radius: 0 !important;
-  width: auto !important;
   height: auto !important;
   padding: var(--toolbarbutton-vertical-inner-padding) 5px !important;
   margin: 0 !important;
   border: 1px solid var(--chrome-nav-bar-controls-border-color) !important;
   box-shadow: none !important;
 }
 
+#back-button > .toolbarbutton-icon {
+  /* 18px icon + 2 * 5px padding + 2 * 1px border */
+  width: 30px !important;
+}
+
+#forward-button > .toolbarbutton-icon {
+  /* 18px icon + 2 * 5px padding + 1 * 1px border */
+  width: 29px !important;
+}
+
 /* the normal theme adds box-shadow: <stuff> !important when the back-button is [open]. Fix: */
 #back-button[open="true"] > .toolbarbutton-icon {
   box-shadow: none !important;
 }
 
-/* Force 1x image for back/forward button for now, otherwise it breaks the
-   layout - Bug 1165360. */
-@media (min-resolution: 1.1dppx) {
-  #back-button,
-  #forward-button {
-    list-style-image: url("chrome://browser/skin/Toolbar.png");
-  }
-
-  toolbar[brighttext] #back-button,
-  toolbar[brighttext] #forward-button {
-    list-style-image: url("chrome://browser/skin/Toolbar-inverted.png");
-  }
-
-  /* The back button region is already set in devedition.inc.css */
-  #forward-button {
-    -moz-image-region: rect(0px, 72px, 18px, 54px);
-  }
-}
-
 #forward-button > .toolbarbutton-icon {
   border-inline-start: none !important;
 }
 
 /* Override a box shadow for disabled back button */
 #main-window:not([customizing]) #back-button[disabled] > .toolbarbutton-icon {
   box-shadow: none !important;
 }
new file mode 100644
--- /dev/null
+++ b/devtools/client/aboutdebugging/test/addons/test-devtools-webextension-nobg/manifest.json
@@ -0,0 +1,10 @@
+{
+  "manifest_version": 2,
+  "name": "test-devtools-webextension-nobg",
+  "version": "1.0",
+  "applications": {
+    "gecko": {
+      "id": "test-devtools-webextension-nobg@mozilla.org"
+    }
+  }
+}
new file mode 100644
--- /dev/null
+++ b/devtools/client/aboutdebugging/test/addons/test-devtools-webextension/bg.js
@@ -0,0 +1,20 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/* eslint-env browser */
+/* global browser */
+
+"use strict";
+
+document.body.innerText = "Background Page Body Test Content";
+
+// This function are called from the webconsole test:
+// browser_addons_debug_webextension.js
+
+function myWebExtensionAddonFunction() {  // eslint-disable-line no-unused-vars
+  console.log("Background page function called", browser.runtime.getManifest());
+}
+
+function myWebExtensionShowPopup() {  // eslint-disable-line no-unused-vars
+  console.log("readyForOpenPopup");
+}
new file mode 100644
--- /dev/null
+++ b/devtools/client/aboutdebugging/test/addons/test-devtools-webextension/manifest.json
@@ -0,0 +1,17 @@
+{
+  "manifest_version": 2,
+  "name": "test-devtools-webextension",
+  "version": "1.0",
+  "applications": {
+    "gecko": {
+      "id": "test-devtools-webextension@mozilla.org"
+    }
+  },
+  "background": {
+    "scripts": ["bg.js"]
+  },
+  "browser_action": {
+    "default_title": "WebExtension Popup Debugging",
+    "default_popup": "popup.html"
+  }
+}
new file mode 100644
--- /dev/null
+++ b/devtools/client/aboutdebugging/test/addons/test-devtools-webextension/popup.html
@@ -0,0 +1,10 @@
+<!DOCTYPE html>
+<html>
+  <head>
+    <meta charset="utf-8">
+    <script src="popup.js"></script>
+  </head>
+  <body>
+    Background Page Body Test Content
+  </body>
+</html>
new file mode 100644
--- /dev/null
+++ b/devtools/client/aboutdebugging/test/addons/test-devtools-webextension/popup.js
@@ -0,0 +1,13 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/* eslint-env browser */
+/* global browser */
+
+"use strict";
+
+// This function is called from the webconsole test:
+// browser_addons_debug_webextension.js
+function myWebExtensionPopupAddonFunction() {  // eslint-disable-line no-unused-vars
+  console.log("Popup page function called", browser.runtime.getManifest());
+}
--- a/devtools/client/aboutdebugging/test/browser.ini
+++ b/devtools/client/aboutdebugging/test/browser.ini
@@ -2,23 +2,29 @@
 tags = devtools
 subsuite = devtools
 support-files =
   head.js
   addons/unpacked/bootstrap.js
   addons/unpacked/install.rdf
   addons/bad/manifest.json
   addons/bug1273184.xpi
+  addons/test-devtools-webextension/*
+  addons/test-devtools-webextension-nobg/*
   service-workers/empty-sw.html
   service-workers/empty-sw.js
   service-workers/push-sw.html
   service-workers/push-sw.js
   !/devtools/client/framework/test/shared-head.js
 
 [browser_addons_debug_bootstrapped.js]
+[browser_addons_debug_webextension.js]
+[browser_addons_debug_webextension_inspector.js]
+[browser_addons_debug_webextension_nobg.js]
+[browser_addons_debug_webextension_popup.js]
 [browser_addons_debugging_initial_state.js]
 [browser_addons_install.js]
 [browser_addons_reload.js]
 [browser_addons_toggle_debug.js]
 [browser_page_not_found.js]
 [browser_service_workers.js]
 [browser_service_workers_not_compatible.js]
 [browser_service_workers_push.js]
--- a/devtools/client/aboutdebugging/test/browser_addons_debug_bootstrapped.js
+++ b/devtools/client/aboutdebugging/test/browser_addons_debug_bootstrapped.js
@@ -21,18 +21,21 @@ add_task(function* () {
       // Enable Browser toolbox test script execution via env variable
       ["devtools.browser-toolbox.allow-unsafe-script", true],
     ]};
     SpecialPowers.pushPrefEnv(options, resolve);
   });
 
   let { tab, document } = yield openAboutDebugging("addons");
   yield waitForInitialAddonList(document);
-  yield installAddon(document, "addons/unpacked/install.rdf", ADDON_NAME,
-                     "test-devtools");
+  yield installAddon({
+    document,
+    path: "addons/unpacked/install.rdf",
+    name: ADDON_NAME,
+  });
 
   // Retrieve the DEBUG button for the addon
   let names = [...document.querySelectorAll("#addons .target-name")];
   let name = names.filter(element => element.textContent === ADDON_NAME)[0];
   ok(name, "Found the addon in the list");
   let targetElement = name.parentNode.parentNode;
   let debugBtn = targetElement.querySelector(".debug-button");
   ok(debugBtn, "Found its debug button");
@@ -70,11 +73,11 @@ add_task(function* () {
   debugBtn.click();
 
   yield onCustomMessage;
   ok(true, "Received the notification message from the bootstrap.js function");
 
   yield onToolboxClose;
   ok(true, "Addon toolbox closed");
 
-  yield uninstallAddon(document, ADDON_ID, ADDON_NAME);
+  yield uninstallAddon({document, id: ADDON_ID, name: ADDON_NAME});
   yield closeAboutDebugging(tab);
 });
copy from devtools/client/aboutdebugging/test/browser_addons_debug_bootstrapped.js
copy to devtools/client/aboutdebugging/test/browser_addons_debug_webextension.js
--- a/devtools/client/aboutdebugging/test/browser_addons_debug_bootstrapped.js
+++ b/devtools/client/aboutdebugging/test/browser_addons_debug_webextension.js
@@ -1,80 +1,74 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 "use strict";
 
 // Avoid test timeouts that can occur while waiting for the "addon-console-works" message.
 requestLongerTimeout(2);
 
-const ADDON_ID = "test-devtools@mozilla.org";
-const ADDON_NAME = "test-devtools";
+const ADDON_ID = "test-devtools-webextension@mozilla.org";
+const ADDON_NAME = "test-devtools-webextension";
+const ADDON_MANIFEST_PATH = "addons/test-devtools-webextension/manifest.json";
 
-const { BrowserToolboxProcess } = Cu.import("resource://devtools/client/framework/ToolboxProcess.jsm", {});
+const {
+  BrowserToolboxProcess
+} = Cu.import("resource://devtools/client/framework/ToolboxProcess.jsm", {});
 
-add_task(function* () {
-  yield new Promise(resolve => {
-    let options = {"set": [
-      // Force enabling of addons debugging
-      ["devtools.chrome.enabled", true],
-      ["devtools.debugger.remote-enabled", true],
-      // Disable security prompt
-      ["devtools.debugger.prompt-connection", false],
-      // Enable Browser toolbox test script execution via env variable
-      ["devtools.browser-toolbox.allow-unsafe-script", true],
-    ]};
-    SpecialPowers.pushPrefEnv(options, resolve);
-  });
-
-  let { tab, document } = yield openAboutDebugging("addons");
-  yield waitForInitialAddonList(document);
-  yield installAddon(document, "addons/unpacked/install.rdf", ADDON_NAME,
-                     "test-devtools");
-
-  // Retrieve the DEBUG button for the addon
-  let names = [...document.querySelectorAll("#addons .target-name")];
-  let name = names.filter(element => element.textContent === ADDON_NAME)[0];
-  ok(name, "Found the addon in the list");
-  let targetElement = name.parentNode.parentNode;
-  let debugBtn = targetElement.querySelector(".debug-button");
-  ok(debugBtn, "Found its debug button");
+/**
+ * This test file ensures that the webextension addon developer toolbox:
+ * - when the debug button is clicked on a webextension, the opened toolbox
+ *   has a working webconsole with the background page as default target;
+ */
+add_task(function* testWebExtensionsToolboxWebConsole() {
+  let {
+    tab, document, debugBtn,
+  } = yield setupTestAboutDebuggingWebExtension(ADDON_NAME, ADDON_MANIFEST_PATH);
 
   // Wait for a notification sent by a script evaluated the test addon via
   // the web console.
   let onCustomMessage = new Promise(done => {
-    Services.obs.addObserver(function listener() {
-      Services.obs.removeObserver(listener, "addon-console-works");
-      done();
-    }, "addon-console-works", false);
+    Services.obs.addObserver(function listener(message, topic) {
+      let apiMessage = message.wrappedJSObject;
+      if (!apiMessage.originAttributes ||
+          apiMessage.originAttributes.addonId != ADDON_ID) {
+        return;
+      }
+      Services.obs.removeObserver(listener, "console-api-log-event");
+      done(apiMessage.arguments);
+    }, "console-api-log-event", false);
   });
 
   // Be careful, this JS function is going to be executed in the addon toolbox,
   // which lives in another process. So do not try to use any scope variable!
   let env = Cc["@mozilla.org/process/environment;1"]
               .getService(Ci.nsIEnvironment);
   let testScript = function () {
     /* eslint-disable no-undef */
     toolbox.selectTool("webconsole")
       .then(console => {
         let { jsterm } = console.hud;
-        return jsterm.execute("myBootstrapAddonFunction()");
+        return jsterm.execute("myWebExtensionAddonFunction()");
       })
       .then(() => toolbox.destroy());
     /* eslint-enable no-undef */
   };
   env.set("MOZ_TOOLBOX_TEST_SCRIPT", "new " + testScript);
   registerCleanupFunction(() => {
     env.set("MOZ_TOOLBOX_TEST_SCRIPT", "");
   });
 
   let onToolboxClose = BrowserToolboxProcess.once("close");
 
   debugBtn.click();
 
-  yield onCustomMessage;
-  ok(true, "Received the notification message from the bootstrap.js function");
+  let args = yield onCustomMessage;
+  ok(true, "Received console message from the background page function as expected");
+  is(args[0], "Background page function called", "Got the expected console message");
+  is(args[1] && args[1].name, ADDON_NAME,
+     "Got the expected manifest from WebExtension API");
 
   yield onToolboxClose;
   ok(true, "Addon toolbox closed");
 
-  yield uninstallAddon(document, ADDON_ID, ADDON_NAME);
+  yield uninstallAddon({document, id: ADDON_ID, name: ADDON_NAME});
   yield closeAboutDebugging(tab);
 });
new file mode 100644
--- /dev/null
+++ b/devtools/client/aboutdebugging/test/browser_addons_debug_webextension_inspector.js
@@ -0,0 +1,82 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// Avoid test timeouts that can occur while waiting for the "addon-console-works" message.
+requestLongerTimeout(2);
+
+const ADDON_ID = "test-devtools-webextension@mozilla.org";
+const ADDON_NAME = "test-devtools-webextension";
+const ADDON_PATH = "addons/test-devtools-webextension/manifest.json";
+
+const {
+  BrowserToolboxProcess
+} = Cu.import("resource://devtools/client/framework/ToolboxProcess.jsm", {});
+
+/**
+ * This test file ensures that the webextension addon developer toolbox:
+ * - the webextension developer toolbox has a working Inspector panel, with the
+ *   background page as default target;
+ */
+add_task(function* testWebExtensionsToolboxInspector() {
+  let {
+    tab, document, debugBtn,
+  } = yield setupTestAboutDebuggingWebExtension(ADDON_NAME, ADDON_PATH);
+
+  // Be careful, this JS function is going to be executed in the addon toolbox,
+  // which lives in another process. So do not try to use any scope variable!
+  let env = Cc["@mozilla.org/process/environment;1"]
+        .getService(Ci.nsIEnvironment);
+  let testScript = function () {
+    /* eslint-disable no-undef */
+    toolbox.selectTool("inspector")
+      .then(inspector => {
+        return inspector.walker.querySelector(inspector.walker.rootNode, "body");
+      })
+      .then((nodeActor) => {
+        if (!nodeActor) {
+          throw new Error("nodeActor not found");
+        }
+
+        dump("Got a nodeActor\n");
+
+        if (!(nodeActor.inlineTextChild)) {
+          throw new Error("inlineTextChild not found");
+        }
+
+        dump("Got a nodeActor with an inline text child\n");
+
+        let expectedValue = "Background Page Body Test Content";
+        let actualValue = nodeActor.inlineTextChild._form.nodeValue;
+
+        if (String(actualValue).trim() !== String(expectedValue).trim()) {
+          throw new Error(
+            `mismatched inlineTextchild value: "${actualValue}" !== "${expectedValue}"`
+          );
+        }
+
+        dump("Got the expected inline text content in the selected node\n");
+        return Promise.resolve();
+      })
+      .then(() => toolbox.destroy())
+      .catch((error) => {
+        dump("Error while running code in the browser toolbox process:\n");
+        dump(error + "\n");
+        dump("stack:\n" + error.stack + "\n");
+      });
+    /* eslint-enable no-undef */
+  };
+  env.set("MOZ_TOOLBOX_TEST_SCRIPT", "new " + testScript);
+  registerCleanupFunction(() => {
+    env.set("MOZ_TOOLBOX_TEST_SCRIPT", "");
+  });
+
+  let onToolboxClose = BrowserToolboxProcess.once("close");
+  debugBtn.click();
+  yield onToolboxClose;
+
+  ok(true, "Addon toolbox closed");
+
+  yield uninstallAddon({document, id: ADDON_ID, name: ADDON_NAME});
+  yield closeAboutDebugging(tab);
+});
new file mode 100644
--- /dev/null
+++ b/devtools/client/aboutdebugging/test/browser_addons_debug_webextension_nobg.js
@@ -0,0 +1,84 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// Avoid test timeouts that can occur while waiting for the "addon-console-works" message.
+requestLongerTimeout(2);
+
+const ADDON_NOBG_ID = "test-devtools-webextension-nobg@mozilla.org";
+const ADDON_NOBG_NAME = "test-devtools-webextension-nobg";
+const ADDON_NOBG_PATH = "addons/test-devtools-webextension-nobg/manifest.json";
+
+const {
+  BrowserToolboxProcess
+} = Cu.import("resource://devtools/client/framework/ToolboxProcess.jsm", {});
+
+/**
+ * This test file ensures that the webextension addon developer toolbox:
+ * - the webextension developer toolbox is connected to a fallback page when the
+ *   background page is not available (and in the fallback page document body contains
+ *   the expected message, which warns the user that the current page is not a real
+ *   webextension context);
+ */
+add_task(function* testWebExtensionsToolboxNoBackgroundPage() {
+  let {
+    tab, document, debugBtn,
+  } = yield setupTestAboutDebuggingWebExtension(ADDON_NOBG_NAME, ADDON_NOBG_PATH);
+
+  // Be careful, this JS function is going to be executed in the addon toolbox,
+  // which lives in another process. So do not try to use any scope variable!
+  let env = Cc["@mozilla.org/process/environment;1"]
+        .getService(Ci.nsIEnvironment);
+  let testScript = function () {
+    /* eslint-disable no-undef */
+    toolbox.selectTool("inspector")
+      .then(inspector => {
+        return inspector.walker.querySelector(inspector.walker.rootNode, "body");
+      })
+      .then((nodeActor) => {
+        if (!nodeActor) {
+          throw new Error("nodeActor not found");
+        }
+
+        dump("Got a nodeActor\n");
+
+        if (!(nodeActor.inlineTextChild)) {
+          throw new Error("inlineTextChild not found");
+        }
+
+        dump("Got a nodeActor with an inline text child\n");
+
+        let expectedValue = "Your addon does not have any document opened yet.";
+        let actualValue = nodeActor.inlineTextChild._form.nodeValue;
+
+        if (actualValue !== expectedValue) {
+          throw new Error(
+            `mismatched inlineTextchild value: "${actualValue}" !== "${expectedValue}"`
+          );
+        }
+
+        dump("Got the expected inline text content in the selected node\n");
+        return Promise.resolve();
+      })
+      .then(() => toolbox.destroy())
+      .catch((error) => {
+        dump("Error while running code in the browser toolbox process:\n");
+        dump(error + "\n");
+        dump("stack:\n" + error.stack + "\n");
+      });
+    /* eslint-enable no-undef */
+  };
+  env.set("MOZ_TOOLBOX_TEST_SCRIPT", "new " + testScript);
+  registerCleanupFunction(() => {
+    env.set("MOZ_TOOLBOX_TEST_SCRIPT", "");
+  });
+
+  let onToolboxClose = BrowserToolboxProcess.once("close");
+  debugBtn.click();
+  yield onToolboxClose;
+
+  ok(true, "Addon toolbox closed");
+
+  yield uninstallAddon({document, id: ADDON_NOBG_ID, name: ADDON_NOBG_NAME});
+  yield closeAboutDebugging(tab);
+});
new file mode 100644
--- /dev/null
+++ b/devtools/client/aboutdebugging/test/browser_addons_debug_webextension_popup.js
@@ -0,0 +1,180 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// Avoid test timeouts that can occur while waiting for the "addon-console-works" message.
+requestLongerTimeout(2);
+
+const ADDON_ID = "test-devtools-webextension@mozilla.org";
+const ADDON_NAME = "test-devtools-webextension";
+const ADDON_MANIFEST_PATH = "addons/test-devtools-webextension/manifest.json";
+
+const {
+  BrowserToolboxProcess
+} = Cu.import("resource://devtools/client/framework/ToolboxProcess.jsm", {});
+
+/**
+ * This test file ensures that the webextension addon developer toolbox:
+ * - when the debug button is clicked on a webextension, the opened toolbox
+ *   has a working webconsole with the background page as default target;
+ * - the webextension developer toolbox has a working Inspector panel, with the
+ *   background page as default target;
+ * - the webextension developer toolbox is connected to a fallback page when the
+ *   background page is not available (and in the fallback page document body contains
+ *   the expected message, which warns the user that the current page is not a real
+ *   webextension context);
+ * - the webextension developer toolbox has a frame list menu and the noautohide toolbar
+ *   toggle button, and they can be used to switch the current target to the extension
+ *   popup page.
+ */
+
+/**
+ * Returns the widget id for an extension with the passed id.
+ */
+function makeWidgetId(id) {
+  id = id.toLowerCase();
+  return id.replace(/[^a-z0-9_-]/g, "_");
+}
+
+add_task(function* testWebExtensionsToolboxSwitchToPopup() {
+  let {
+    tab, document, debugBtn,
+  } = yield setupTestAboutDebuggingWebExtension(ADDON_NAME, ADDON_MANIFEST_PATH);
+
+  let onReadyForOpenPopup = new Promise(done => {
+    Services.obs.addObserver(function listener(message, topic) {
+      let apiMessage = message.wrappedJSObject;
+      if (!apiMessage.originAttributes ||
+          apiMessage.originAttributes.addonId != ADDON_ID) {
+        return;
+      }
+
+      if (apiMessage.arguments[0] == "readyForOpenPopup") {
+        Services.obs.removeObserver(listener, "console-api-log-event");
+        done();
+      }
+    }, "console-api-log-event", false);
+  });
+
+  // Be careful, this JS function is going to be executed in the addon toolbox,
+  // which lives in another process. So do not try to use any scope variable!
+  let env = Cc["@mozilla.org/process/environment;1"]
+        .getService(Ci.nsIEnvironment);
+  let testScript = function () {
+    /* eslint-disable no-undef */
+
+    let jsterm;
+
+    toolbox.selectTool("webconsole")
+      .then(console => {
+        dump(`Clicking the noautohide button\n`);
+        toolbox.doc.getElementById("command-button-noautohide").click();
+        dump(`Clicked the noautohide button\n`);
+
+        let waitForFrameListUpdate = new Promise((done) => {
+          toolbox.target.once("frame-update", () => {
+            done(console);
+          });
+        });
+
+        jsterm = console.hud.jsterm;
+        jsterm.execute("myWebExtensionShowPopup()");
+
+        // Wait the initial frame update (which list the background page).
+        return waitForFrameListUpdate;
+      })
+      .then((console) => {
+        // Wait the new frame update (once the extension popup has been opened).
+        return new Promise((done) => {
+          toolbox.target.once("frame-update", done);
+        });
+      })
+      .then(() => {
+        dump(`Clicking the frame list button\n`);
+        let btn = toolbox.doc.getElementById("command-button-frames");
+        let menu = toolbox.showFramesMenu({target: btn});
+        dump(`Clicked the frame list button\n`);
+        return menu.once("open").then(() => {
+          return menu;
+        });
+      })
+      .then(frameMenu => {
+        let frames = frameMenu.items;
+
+        if (frames.length != 2) {
+          throw Error(`Number of frames found is wrong: ${frames.length} != 2`);
+        }
+
+        let popupFrameBtn = frames.filter((frame) => {
+          return frame.label.endsWith("popup.html");
+        }).pop();
+
+        if (!popupFrameBtn) {
+          throw Error("Extension Popup frame not found in the listed frames");
+        }
+
+        let waitForNavigated = toolbox.target.once("navigate");
+
+        popupFrameBtn.click();
+
+        return waitForNavigated;
+      })
+      .then(() => {
+        return jsterm.execute("myWebExtensionPopupAddonFunction()");
+      })
+      .then(() => toolbox.destroy())
+      .catch((error) => {
+        dump("Error while running code in the browser toolbox process:\n");
+        dump(error + "\n");
+        dump("stack:\n" + error.stack + "\n");
+      });
+    /* eslint-enable no-undef */
+  };
+  env.set("MOZ_TOOLBOX_TEST_SCRIPT", "new " + testScript);
+  registerCleanupFunction(() => {
+    env.set("MOZ_TOOLBOX_TEST_SCRIPT", "");
+  });
+
+  // Wait for a notification sent by a script evaluated the test addon via
+  // the web console.
+  let onPopupCustomMessage = new Promise(done => {
+    Services.obs.addObserver(function listener(message, topic) {
+      let apiMessage = message.wrappedJSObject;
+      if (!apiMessage.originAttributes ||
+          apiMessage.originAttributes.addonId != ADDON_ID) {
+        return;
+      }
+
+      if (apiMessage.arguments[0] == "Popup page function called") {
+        Services.obs.removeObserver(listener, "console-api-log-event");
+        done(apiMessage.arguments);
+      }
+    }, "console-api-log-event", false);
+  });
+
+  let onToolboxClose = BrowserToolboxProcess.once("close");
+
+  debugBtn.click();
+
+  yield onReadyForOpenPopup;
+
+  let browserActionId = makeWidgetId(ADDON_ID) + "-browser-action";
+  let browserActionEl = window.document.getElementById(browserActionId);
+
+  ok(browserActionEl, "Got the browserAction button from the browser UI");
+  browserActionEl.click();
+  info("Clicked on the browserAction button");
+
+  let args = yield onPopupCustomMessage;
+  ok(true, "Received console message from the popup page function as expected");
+  is(args[0], "Popup page function called", "Got the expected console message");
+  is(args[1] && args[1].name, ADDON_NAME,
+     "Got the expected manifest from WebExtension API");
+
+  yield onToolboxClose;
+
+  ok(true, "Addon toolbox closed");
+
+  yield uninstallAddon({document, id: ADDON_ID, name: ADDON_NAME});
+  yield closeAboutDebugging(tab);
+});
--- a/devtools/client/aboutdebugging/test/browser_addons_debugging_initial_state.js
+++ b/devtools/client/aboutdebugging/test/browser_addons_debugging_initial_state.js
@@ -45,26 +45,29 @@ function* testCheckboxState(testData) {
     ]};
     SpecialPowers.pushPrefEnv(options, resolve);
   });
 
   let { tab, document } = yield openAboutDebugging("addons");
   yield waitForInitialAddonList(document);
 
   info("Install a test addon.");
-  yield installAddon(document, "addons/unpacked/install.rdf", ADDON_NAME,
-                     "test-devtools");
+  yield installAddon({
+    document,
+    path: "addons/unpacked/install.rdf",
+    name: ADDON_NAME,
+  });
 
   info("Test checkbox checked state.");
   let addonDebugCheckbox = document.querySelector("#enable-addon-debugging");
   is(addonDebugCheckbox.checked, testData.expected,
     "Addons debugging checkbox should be in expected state.");
 
   info("Test debug buttons disabled state.");
   let debugButtons = [...document.querySelectorAll("#addons .debug-button")];
   ok(debugButtons.every(b => b.disabled != testData.expected),
     "Debug buttons should be in the expected state");
 
   info("Uninstall test addon installed earlier.");
-  yield uninstallAddon(document, ADDON_ID, ADDON_NAME);
+  yield uninstallAddon({document, id: ADDON_ID, name: ADDON_NAME});
 
   yield closeAboutDebugging(tab);
 }
--- a/devtools/client/aboutdebugging/test/browser_addons_install.js
+++ b/devtools/client/aboutdebugging/test/browser_addons_install.js
@@ -5,21 +5,24 @@
 const ADDON_ID = "test-devtools@mozilla.org";
 const ADDON_NAME = "test-devtools";
 
 add_task(function* () {
   let { tab, document } = yield openAboutDebugging("addons");
   yield waitForInitialAddonList(document);
 
   // Install this add-on, and verify that it appears in the about:debugging UI
-  yield installAddon(document, "addons/unpacked/install.rdf", ADDON_NAME,
-                     "test-devtools");
+  yield installAddon({
+    document,
+    path: "addons/unpacked/install.rdf",
+    name: ADDON_NAME,
+  });
 
   // Install the add-on, and verify that it disappears in the about:debugging UI
-  yield uninstallAddon(document, ADDON_ID, ADDON_NAME);
+  yield uninstallAddon({document, id: ADDON_ID, name: ADDON_NAME});
 
   yield closeAboutDebugging(tab);
 });
 
 add_task(function* () {
   let { tab, document } = yield openAboutDebugging("addons");
   yield waitForInitialAddonList(document);
 
--- a/devtools/client/aboutdebugging/test/browser_addons_reload.js
+++ b/devtools/client/aboutdebugging/test/browser_addons_reload.js
@@ -104,18 +104,21 @@ class TempWebExt {
   remove() {
     return this.tmpDir.remove(true);
   }
 }
 
 add_task(function* reloadButtonReloadsAddon() {
   const { tab, document } = yield openAboutDebugging("addons");
   yield waitForInitialAddonList(document);
-  yield installAddon(document, "addons/unpacked/install.rdf",
-                     ADDON_NAME, ADDON_NAME);
+  yield installAddon({
+    document,
+    path: "addons/unpacked/install.rdf",
+    name: ADDON_NAME,
+  });
 
   const reloadButton = getReloadButton(document, ADDON_NAME);
   is(reloadButton.disabled, false, "Reload button should not be disabled");
   is(reloadButton.title, "", "Reload button should not have a tooltip");
   const onInstalled = promiseAddonEvent("onInstalled");
 
   const onBootstrapInstallCalled = new Promise(done => {
     Services.obs.addObserver(function listener() {
--- a/devtools/client/aboutdebugging/test/browser_addons_toggle_debug.js
+++ b/devtools/client/aboutdebugging/test/browser_addons_toggle_debug.js
@@ -18,18 +18,21 @@ add_task(function* () {
     ]};
     SpecialPowers.pushPrefEnv(options, resolve);
   });
 
   let { tab, document } = yield openAboutDebugging("addons");
   yield waitForInitialAddonList(document);
 
   info("Install a test addon.");
-  yield installAddon(document, "addons/unpacked/install.rdf", ADDON_NAME,
-                     "test-devtools");
+  yield installAddon({
+    document,
+    path: "addons/unpacked/install.rdf",
+    name: ADDON_NAME,
+  });
 
   let addonDebugCheckbox = document.querySelector("#enable-addon-debugging");
   ok(!addonDebugCheckbox.checked, "Addons debugging should be disabled.");
 
   info("Check all debug buttons are disabled.");
   let debugButtons = [...document.querySelectorAll("#addons .debug-button")];
   ok(debugButtons.every(b => b.disabled), "Debug buttons should be disabled");
 
@@ -51,12 +54,12 @@ add_task(function* () {
   addonDebugCheckbox.click();
   yield onAddonsMutation;
 
   info("Check all debug buttons are disabled again.");
   debugButtons = [...document.querySelectorAll("#addons .debug-button")];
   ok(debugButtons.every(b => b.disabled), "Debug buttons should be disabled");
 
   info("Uninstall addon installed earlier.");
-  yield uninstallAddon(document, ADDON_ID, ADDON_NAME);
+  yield uninstallAddon({document, id: ADDON_ID, name: ADDON_NAME});
 
   yield closeAboutDebugging(tab);
 });
--- a/devtools/client/aboutdebugging/test/head.js
+++ b/devtools/client/aboutdebugging/test/head.js
@@ -1,27 +1,29 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 /* eslint-env browser */
 /* exported openAboutDebugging, changeAboutDebuggingHash, closeAboutDebugging,
    installAddon, uninstallAddon, waitForMutation, assertHasTarget,
    getServiceWorkerList, getTabList, openPanel, waitForInitialAddonList,
    waitForServiceWorkerRegistered, unregisterServiceWorker,
-   waitForDelayedStartupFinished */
+   waitForDelayedStartupFinished, setupTestAboutDebuggingWebExtension */
 /* import-globals-from ../../framework/test/shared-head.js */
 
 "use strict";
 
 // Load the shared-head file first.
 Services.scriptloader.loadSubScript(
   "chrome://mochitests/content/browser/devtools/client/framework/test/shared-head.js",
   this);
 
 const { AddonManager } = Cu.import("resource://gre/modules/AddonManager.jsm", {});
+const { Management } = Cu.import("resource://gre/modules/Extension.jsm", {});
+
 DevToolsUtils.testing = true;
 
 registerCleanupFunction(() => {
   DevToolsUtils.testing = false;
 });
 
 function* openAboutDebugging(page, win) {
   info("opening about:debugging");
@@ -143,55 +145,70 @@ function getServiceWorkerList(document) 
  * @param  {DOMDocument}  document   #tabs section container document
  * @return {DOMNode}                 target list or container element
  */
 function getTabList(document) {
   return document.querySelector("#tabs .target-list") ||
     document.querySelector("#tabs.targets");
 }
 
-function* installAddon(document, path, name, evt) {
+function* installAddon({document, path, name, isWebExtension}) {
   // Mock the file picker to select a test addon
   let MockFilePicker = SpecialPowers.MockFilePicker;
   MockFilePicker.init(null);
   let file = getSupportsFile(path);
   MockFilePicker.returnFiles = [file.file];
 
   let addonList = getAddonList(document);
   let addonListMutation = waitForMutation(addonList, { childList: true });
 
-  // Wait for a message sent by the addon's bootstrap.js file
-  let onAddonInstalled = new Promise(done => {
-    Services.obs.addObserver(function listener() {
-      Services.obs.removeObserver(listener, evt);
+  let onAddonInstalled;
+
+  if (isWebExtension) {
+    onAddonInstalled = new Promise(done => {
+      Management.on("startup", function listener(event, extension) {
+        if (extension.name != name) {
+          return;
+        }
 
-      done();
-    }, evt, false);
-  });
+        Management.off("startup", listener);
+        done();
+      });
+    });
+  } else {
+    // Wait for a "test-devtools" message sent by the addon's bootstrap.js file
+    onAddonInstalled = new Promise(done => {
+      Services.obs.addObserver(function listener() {
+        Services.obs.removeObserver(listener, "test-devtools");
+
+        done();
+      }, "test-devtools", false);
+    });
+  }
   // Trigger the file picker by clicking on the button
   document.getElementById("load-addon-from-file").click();
 
   yield onAddonInstalled;
   ok(true, "Addon installed and running its bootstrap.js file");
 
   // Check that the addon appears in the UI
   yield addonListMutation;
   let names = [...addonList.querySelectorAll(".target-name")];
   names = names.map(element => element.textContent);
   ok(names.includes(name),
     "The addon name appears in the list of addons: " + names);
 }
 
-function* uninstallAddon(document, addonId, addonName) {
+function* uninstallAddon({document, id, name}) {
   let addonList = getAddonList(document);
   let addonListMutation = waitForMutation(addonList, { childList: true });
 
   // Now uninstall this addon
   yield new Promise(done => {
-    AddonManager.getAddonByID(addonId, addon => {
+    AddonManager.getAddonByID(id, addon => {
       let listener = {
         onUninstalled: function (uninstalledAddon) {
           if (uninstalledAddon != addon) {
             return;
           }
           AddonManager.removeAddonListener(listener);
 
           done();
@@ -201,17 +218,17 @@ function* uninstallAddon(document, addon
       addon.uninstall();
     });
   });
 
   // Ensure that the UI removes the addon from the list
   yield addonListMutation;
   let names = [...addonList.querySelectorAll(".target-name")];
   names = names.map(element => element.textContent);
-  ok(!names.includes(addonName),
+  ok(!names.includes(name),
     "After uninstall, the addon name disappears from the list of addons: "
     + names);
 }
 
 /**
  * Returns a promise that will resolve when the add-on list has been updated.
  *
  * @param {Node} document
@@ -307,8 +324,46 @@ function waitForDelayedStartupFinished(w
     Services.obs.addObserver(function observer(subject, topic) {
       if (win == subject) {
         Services.obs.removeObserver(observer, topic);
         resolve();
       }
     }, "browser-delayed-startup-finished", false);
   });
 }
+
+/**
+ * open the about:debugging page and install an addon
+ */
+function* setupTestAboutDebuggingWebExtension(name, path) {
+  yield new Promise(resolve => {
+    let options = {"set": [
+      // Force enabling of addons debugging
+      ["devtools.chrome.enabled", true],
+      ["devtools.debugger.remote-enabled", true],
+      // Disable security prompt
+      ["devtools.debugger.prompt-connection", false],
+      // Enable Browser toolbox test script execution via env variable
+      ["devtools.browser-toolbox.allow-unsafe-script", true],
+    ]};
+    SpecialPowers.pushPrefEnv(options, resolve);
+  });
+
+  let { tab, document } = yield openAboutDebugging("addons");
+  yield waitForInitialAddonList(document);
+
+  yield installAddon({
+    document,
+    path,
+    name,
+    isWebExtension: true,
+  });
+
+  // Retrieve the DEBUG button for the addon
+  let names = [...document.querySelectorAll("#addons .target-name")];
+  let nameEl = names.filter(element => element.textContent === name)[0];
+  ok(name, "Found the addon in the list");
+  let targetElement = nameEl.parentNode.parentNode;
+  let debugBtn = targetElement.querySelector(".debug-button");
+  ok(debugBtn, "Found its debug button");
+
+  return { tab, document, debugBtn };
+}
--- a/devtools/client/debugger/test/mochitest/browser.ini
+++ b/devtools/client/debugger/test/mochitest/browser.ini
@@ -474,17 +474,17 @@ skip-if = e10s && debug
 skip-if = e10s && debug
 [browser_dbg_sources-large.js]
 [browser_dbg_sources-sorting.js]
 skip-if = e10s && debug
 [browser_dbg_sources-bookmarklet.js]
 skip-if = e10s && debug
 [browser_dbg_sources-webext-contentscript.js]
 [browser_dbg_split-console-paused-reload.js]
-skip-if = e10s && debug
+skip-if = true # Bug 1288348 - previously e10s && debug
 [browser_dbg_stack-01.js]
 skip-if = e10s && debug
 [browser_dbg_stack-02.js]
 skip-if = e10s && debug
 [browser_dbg_stack-03.js]
 skip-if = e10s # TODO
 [browser_dbg_stack-04.js]
 skip-if = e10s && debug
--- a/devtools/client/debugger/test/mochitest/browser_dbg_split-console-paused-reload.js
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_split-console-paused-reload.js
@@ -32,17 +32,36 @@ function* runTests() {
   yield ensureThreadClientState(panel, "paused");
   info("Breakpoint was hit.");
   EventUtils.sendMouseEvent({ type: "mousedown" },
     frames.selectedItem.target,
     dbgWin);
   info("The breadcrumb received focus.");
 
   // This is the meat of the test.
-  let result = toolbox.once("webconsole-ready", () => {
-    ok(toolbox.splitConsole, "Split console is shown.");
-    is(dbgWin.gThreadClient.state, "paused", "Execution is still paused.");
-    Services.prefs.clearUserPref("devtools.toolbox.splitconsoleEnabled");
-  });
-  EventUtils.synthesizeKey("VK_ESCAPE", {}, dbgWin);
-  yield result;
-  yield resumeDebuggerThenCloseAndFinish(panel);
+  let jsterm = yield getSplitConsole(toolbox);
+
+  is(dbgWin.gThreadClient.state, "paused", "Execution is still paused.");
+
+  let dbgFrameConsoleEvalResult = yield jsterm.execute("privateVar");
+
+  is(
+    dbgFrameConsoleEvalResult.querySelector(".console-string").textContent,
+    '"privateVarValue"',
+    "Got the expected split console result on paused debugger"
+  );
+
+  yield dbgWin.gThreadClient.resume();
+
+  is(dbgWin.gThreadClient.state, "attached", "Execution is resumed.");
+
+  // Get the last evaluation result adopted by the new debugger.
+  let mainTargetConsoleEvalResult = yield jsterm.execute("$_");
+
+  is(
+    mainTargetConsoleEvalResult.querySelector(".console-string").textContent,
+    '"privateVarValue"',
+    "Got the expected split console log on $_ executed on resumed debugger"
+  );
+
+  Services.prefs.clearUserPref("devtools.toolbox.splitconsoleEnabled");
+  yield closeDebuggerAndFinish(panel);
 }
--- a/devtools/client/debugger/test/mochitest/doc_split-console-paused-reload.html
+++ b/devtools/client/debugger/test/mochitest/doc_split-console-paused-reload.html
@@ -5,16 +5,18 @@
 <html>
   <head>
     <meta charset="utf-8"/>
     <title>Test page for opening a split-console when execution is paused</title>
   </head>
 
   <body>
     <script type="text/javascript">
-      function runDebuggerStatement() {
-        debugger;
+      function executeFunction() {
+        let privateVar = { propKey: "privateVarValue" };
+
+        window.foobar = "foobar";
       }
-      window.foobar = 1;
+      executeFunction();
     </script>
   </body>
 
 </html>
--- a/devtools/client/framework/attach-thread.js
+++ b/devtools/client/framework/attach-thread.js
@@ -84,26 +84,26 @@ function attachThread(toolbox) {
           box.PRIORITY_WARNING_HIGH
         );
       }
 
       deferred.resolve(threadClient);
     });
   };
 
-  if (target.isAddon) {
-    // Attaching an addon
+  if (target.isTabActor) {
+    // Attaching a tab, a browser process, or a WebExtensions add-on.
+    target.activeTab.attachThread(threadOptions, handleResponse);
+  } else if (target.isAddon) {
+    // Attaching a legacy addon.
     target.client.attachAddon(actor, res => {
       target.client.attachThread(res.threadActor, handleResponse);
     });
-  } else if (target.isTabActor) {
-    // Attaching a normal thread
-    target.activeTab.attachThread(threadOptions, handleResponse);
-  } else {
-    // Attaching the browser debugger
+  }  else {
+    // Attaching an old browser debugger or a content process.
     target.client.attachThread(chromeDebugger, handleResponse);
   }
 
   return deferred.promise;
 }
 
 function detachThread(threadClient) {
   threadClient.removeListener("paused");
--- a/devtools/client/framework/target.js
+++ b/devtools/client/framework/target.js
@@ -343,18 +343,25 @@ TabTarget.prototype = {
     return this._url;
   },
 
   get isRemote() {
     return !this.isLocalTab;
   },
 
   get isAddon() {
+    return !!(this._form && this._form.actor && (
+      this._form.actor.match(/conn\d+\.addon\d+/) ||
+      this._form.actor.match(/conn\d+\.webExtension\d+/)
+    ));
+  },
+
+  get isWebExtension() {
     return !!(this._form && this._form.actor &&
-              this._form.actor.match(/conn\d+\.addon\d+/));
+              this._form.actor.match(/conn\d+\.webExtension\d+/));
   },
 
   get isLocalTab() {
     return !!this._tab;
   },
 
   get isMultiProcess() {
     return !this.window;
--- a/devtools/client/framework/toolbox-process-window.js
+++ b/devtools/client/framework/toolbox-process-window.js
@@ -39,17 +39,21 @@ var connect = Task.async(function*() {
   });
   gClient = new DebuggerClient(transport);
   gClient.connect().then(() => {
     let addonID = getParameterByName("addonID");
 
     if (addonID) {
       gClient.listAddons(({addons}) => {
         let addonActor = addons.filter(addon => addon.id === addonID).pop();
-        openToolbox({ form: addonActor, chrome: true, isTabActor: false });
+        openToolbox({
+          form: addonActor,
+          chrome: true,
+          isTabActor: addonActor.isWebExtension ? true : false
+        });
       });
     } else {
       gClient.getProcess().then(aResponse => {
         openToolbox({ form: aResponse.form, chrome: true });
       });
     }
   });
 });
--- a/devtools/client/framework/toolbox.js
+++ b/devtools/client/framework/toolbox.js
@@ -923,17 +923,17 @@ Toolbox.prototype = {
       Services.focus.moveFocus(win, elm, type, 0);
     });
   },
 
   /**
    * Add buttons to the UI as specified in the devtools.toolbox.toolbarSpec pref
    */
   _buildButtons: function () {
-    if (!this.target.isAddon) {
+    if (!this.target.isAddon || this.target.isWebExtension) {
       this._buildPickerButton();
     }
 
     this.setToolboxButtonsVisibility();
 
     // Old servers don't have a GCLI Actor, so just return
     if (!this.target.hasActor("gcli")) {
       return promise.resolve();
--- a/devtools/client/shared/components/reps/grip.js
+++ b/devtools/client/shared/components/reps/grip.js
@@ -17,17 +17,17 @@ define(function (require, exports, modul
   // Shortcuts
   const { span } = React.DOM;
 
   /**
    * Renders generic grip. Grip is client representation
    * of remote JS object and is used as an input object
    * for this rep component.
    */
-  const Grip = React.createClass({
+  const GripRep = React.createClass({
     displayName: "Grip",
 
     propTypes: {
       object: React.PropTypes.object.isRequired,
       mode: React.PropTypes.string,
     },
 
     getTitle: function (object) {
@@ -114,16 +114,17 @@ define(function (require, exports, modul
         let value = ownProperties[name].value;
         props.push(PropRep(Object.assign({}, this.props, {
           key: name,
           mode: "tiny",
           name: name,
           object: value,
           equal: ": ",
           delim: ", ",
+          defaultRep: Grip
         })));
       });
 
       return props;
     },
 
     /**
      * Get the indexes of props in the object.
@@ -204,14 +205,16 @@ define(function (require, exports, modul
   // Registration
   function supportsObject(object, type) {
     if (!isGrip(object)) {
       return false;
     }
     return (object.preview && object.preview.ownProperties);
   }
 
-  // Exports from this module
-  exports.Grip = {
-    rep: Grip,
+  let Grip = {
+    rep: GripRep,
     supportsObject: supportsObject
   };
+
+  // Exports from this module
+  exports.Grip = Grip;
 });
--- a/devtools/client/shared/components/sidebar-toggle.css
+++ b/devtools/client/shared/components/sidebar-toggle.css
@@ -2,23 +2,31 @@
 /* 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/. */
 
 .sidebar-toggle {
   display: block;
 }
 
-.sidebar-toggle::before {
+.sidebar-toggle:-moz-locale-dir(ltr)::before,
+.sidebar-toggle.pane-collapsed:-moz-locale-dir(rtl)::before {
   background-image: var(--theme-pane-collapse-image);
 }
 
-.sidebar-toggle.pane-collapsed::before {
+.sidebar-toggle.pane-collapsed:-moz-locale-dir(ltr)::before,
+.sidebar-toggle:-moz-locale-dir(rtl)::before {
   background-image: var(--theme-pane-expand-image);
 }
 
 /* Rotate button icon 90deg if the toolbox container is
   in vertical mode (sidebar displayed under the main panel) */
 @media (max-width: 700px) {
-  .sidebar-toggle::before {
+  .sidebar-toggle:-moz-locale-dir(ltr)::before {
     transform: rotate(90deg);
   }
+
+  /* Since RTL swaps the used images, we need to flip them
+     the other way round */
+  .sidebar-toggle:-moz-locale-dir(rtl)::before {
+    transform: rotate(-90deg);
+  }
 }
--- a/devtools/client/shared/components/tabs/tabs.css
+++ b/devtools/client/shared/components/tabs/tabs.css
@@ -11,20 +11,16 @@
 
 .tabs .tabs-menu {
   display: table;
   list-style: none;
   padding: 0;
   margin: 0;
 }
 
-.tabs .tabs-menu-item {
-  float: inline-start;
-}
-
 .tabs .tabs-menu-item a {
   display: block;
   color: #A9A9A9;
   padding: 4px 8px;
   border: 1px solid transparent;
   text-decoration: none;
   white-space: nowrap;
 }
@@ -45,17 +41,17 @@
 .theme-dark .tabs,
 .theme-light .tabs {
   background: var(--theme-tab-toolbar-background);
 }
 
 .theme-dark .tabs .tabs-navigation,
 .theme-light .tabs .tabs-navigation {
   border-bottom: 1px solid var(--theme-splitter-color);
-  font-size: 12px;
+  font-size: 11px;
 }
 
 .theme-firebug .tabs .tabs-navigation {
   font-size: 11px;
 }
 
 .theme-dark .tabs .tabs-menu-item,
 .theme-light .tabs .tabs-menu-item {
--- a/devtools/client/shared/widgets/Tooltip.js
+++ b/devtools/client/shared/widgets/Tooltip.js
@@ -845,31 +845,36 @@ Heritage.extend(SwatchBasedEditorTooltip
   _openEyeDropper: function () {
     let {inspector, toolbox, telemetry} = this.inspector;
     telemetry.toolOpened("pickereyedropper");
     inspector.pickColorFromPage({copyOnSelect: false}).catch(e => console.error(e));
 
     inspector.once("color-picked", color => {
       toolbox.win.focus();
       this._selectColor(color);
+      this._onEyeDropperDone();
     });
 
     inspector.once("color-pick-canceled", () => {
-      this.eyedropperOpen = false;
-      this.activeSwatch = null;
+      this._onEyeDropperDone();
     });
 
     this.eyedropperOpen = true;
 
     // close the colorpicker tooltip so that only the eyedropper is open.
     this.hide();
 
     this.tooltip.emit("eyedropper-opened");
   },
 
+  _onEyeDropperDone: function () {
+    this.eyedropperOpen = false;
+    this.activeSwatch = null;
+  },
+
   _colorToRgba: function (color) {
     color = new colorUtils.CssColor(color);
     let rgba = color._getRGBATuple();
     return [rgba.r, rgba.g, rgba.b, rgba.a];
   },
 
   _toDefaultType: function (color) {
     let colorObj = new colorUtils.CssColor(color);
--- a/devtools/server/actors/addon.js
+++ b/devtools/server/actors/addon.js
@@ -129,16 +129,19 @@ BrowserAddonActor.prototype = {
 
   onUninstalled: function BAA_onUninstalled(aAddon) {
     if (aAddon != this._addon) {
       return;
     }
 
     if (this.attached) {
       this.onDetach();
+
+      // The BrowserAddonActor is not a TabActor and it has to send
+      // "tabDetached" directly to close the devtools toolbox window.
       this.conn.send({ from: this.actorID, type: "tabDetached" });
     }
 
     this.disconnect();
   },
 
   onAttach: function BAA_onAttach() {
     if (this.exited) {
--- a/devtools/server/actors/moz.build
+++ b/devtools/server/actors/moz.build
@@ -57,11 +57,12 @@ DevToolsModules(
     'styleeditor.js',
     'styles.js',
     'stylesheets.js',
     'timeline.js',
     'webapps.js',
     'webaudio.js',
     'webbrowser.js',
     'webconsole.js',
+    'webextension.js',
     'webgl.js',
     'worker.js',
 )
--- a/devtools/server/actors/webbrowser.js
+++ b/devtools/server/actors/webbrowser.js
@@ -1,16 +1,18 @@
 /* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
 /* vim: set ft=javascript ts=2 et sw=2 tw=80: */
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
+/* global XPCNativeWrapper */
+
 var { Ci, Cu } = require("chrome");
 var Services = require("Services");
 var { XPCOMUtils } = require("resource://gre/modules/XPCOMUtils.jsm");
 var promise = require("promise");
 var {
   ActorPool, createExtraActors, appendExtraActors, GeneratedLocation
 } = require("devtools/server/actors/common");
 var { DebuggerServer } = require("devtools/server/main");
@@ -18,16 +20,17 @@ var DevToolsUtils = require("devtools/sh
 var { assert } = DevToolsUtils;
 var { TabSources } = require("./utils/TabSources");
 var makeDebugger = require("./utils/make-debugger");
 
 loader.lazyRequireGetter(this, "RootActor", "devtools/server/actors/root", true);
 loader.lazyRequireGetter(this, "ThreadActor", "devtools/server/actors/script", true);
 loader.lazyRequireGetter(this, "unwrapDebuggerObjectGlobal", "devtools/server/actors/script", true);
 loader.lazyRequireGetter(this, "BrowserAddonActor", "devtools/server/actors/addon", true);
+loader.lazyRequireGetter(this, "WebExtensionActor", "devtools/server/actors/webextension", true);
 loader.lazyRequireGetter(this, "WorkerActorList", "devtools/server/actors/worker", true);
 loader.lazyRequireGetter(this, "ServiceWorkerRegistrationActorList", "devtools/server/actors/worker", true);
 loader.lazyRequireGetter(this, "ProcessActorList", "devtools/server/actors/process", true);
 loader.lazyImporter(this, "AddonManager", "resource://gre/modules/AddonManager.jsm");
 loader.lazyImporter(this, "ExtensionContent", "resource://gre/modules/ExtensionContent.jsm");
 
 // Assumptions on events module:
 // events needs to be dispatched synchronously,
@@ -553,44 +556,46 @@ BrowserTabList.prototype._listenForEvent
  * @param aShouldListen boolean
  *    True if we should add message listeners; false if we should remove them.
  * @param aGuard string
  *    The name of a guard property of 'this', indicating whether we're
  *    already listening for those messages.
  * @param aMessageNames array of strings
  *    An array of message names.
  */
-BrowserTabList.prototype._listenForMessagesIf = function (aShouldListen, aGuard, aMessageNames) {
-  if (!aShouldListen !== !this[aGuard]) {
-    let op = aShouldListen ? "addMessageListener" : "removeMessageListener";
-    for (let win of allAppShellDOMWindows(DebuggerServer.chromeWindowType)) {
-      for (let name of aMessageNames) {
-        win.messageManager[op](name, this);
+BrowserTabList.prototype._listenForMessagesIf =
+  function (shouldListen, guard, messageNames) {
+    if (!shouldListen !== !this[guard]) {
+      let op = shouldListen ? "addMessageListener" : "removeMessageListener";
+      for (let win of allAppShellDOMWindows(DebuggerServer.chromeWindowType)) {
+        for (let name of messageNames) {
+          win.messageManager[op](name, this);
+        }
       }
+      this[guard] = shouldListen;
     }
-    this[aGuard] = aShouldListen;
-  }
-};
+  };
 
 /**
  * Implement nsIMessageListener.
  */
-BrowserTabList.prototype.receiveMessage = DevToolsUtils.makeInfallible(function (message) {
-  let browser = message.target;
-  switch (message.name) {
-    case "DOMTitleChanged": {
-      let actor = this._actorByBrowser.get(browser);
-      if (actor) {
-        this._notifyListChanged();
-        this._checkListening();
+BrowserTabList.prototype.receiveMessage = DevToolsUtils.makeInfallible(
+  function (message) {
+    let browser = message.target;
+    switch (message.name) {
+      case "DOMTitleChanged": {
+        let actor = this._actorByBrowser.get(browser);
+        if (actor) {
+          this._notifyListChanged();
+          this._checkListening();
+        }
+        break;
       }
-      break;
     }
-  }
-});
+  });
 
 /**
  * Implement nsIDOMEventListener.
  */
 BrowserTabList.prototype.handleEvent =
 DevToolsUtils.makeInfallible(function (event) {
   let browser = event.target.linkedBrowser;
   switch (event.type) {
@@ -884,16 +889,26 @@ function TabActor(connection) {
 }
 
 // XXX (bug 710213): TabActor attach/detach/exit/disconnect is a
 // *complete* mess, needs to be rethought asap.
 
 TabActor.prototype = {
   traits: null,
 
+  // Optional console API listener options (e.g. used by the WebExtensionActor to
+  // filter console messages by addonID), set to an empty (no options) object by default.
+  consoleAPIListenerOptions: {},
+
+  // Optional TabSources filter function (e.g. used by the WebExtensionActor to filter
+  // sources by addonID), allow all sources by default.
+  _allowSource() {
+    return true;
+  },
+
   get exited() {
     return this._exited;
   },
 
   get attached() {
     return !!this._attached;
   },
 
@@ -1054,17 +1069,17 @@ TabActor.prototype = {
     }
     // Abrupt closing of the browser window may leave callbacks without a
     // currentURI.
     return null;
   },
 
   get sources() {
     if (!this._sources) {
-      this._sources = new TabSources(this.threadActor);
+      this._sources = new TabSources(this.threadActor, this._allowSource);
     }
     return this._sources;
   },
 
   /**
    * This is called by BrowserTabList.getList for existing tab actors prior to
    * calling |form| below.  It can be used to do any async work that may be
    * needed to assemble the form.
@@ -1359,27 +1374,38 @@ TabActor.prototype = {
       // Ignore the parent of the original document on non-e10s firefox,
       // as we get the xul window as parent and don't care about it.
       if (window.parent && window != this._originalWindow) {
         parentID = window.parent
                          .QueryInterface(Ci.nsIInterfaceRequestor)
                          .getInterface(Ci.nsIDOMWindowUtils)
                          .outerWindowID;
       }
+
+      // Collect the addonID from the document origin attributes.
+      let addonID = window.document.nodePrincipal.originAttributes.addonId;
+
       return {
-        id: id,
+        id,
+        parentID,
+        addonID,
         url: window.location.href,
         title: window.document.title,
-        parentID: parentID
       };
     });
   },
 
   _notifyDocShellsUpdate(docshells) {
     let windows = this._docShellsToWindows(docshells);
+
+    // Do not send the `frameUpdate` event if the windows array is empty.
+    if (windows.length == 0) {
+      return;
+    }
+
     this.conn.send({ from: this.actorID,
                      type: "frameUpdate",
                      frames: windows
                    });
   },
 
   _updateChildDocShells() {
     this._notifyDocShellsUpdate(this.docShells);
@@ -2022,17 +2048,17 @@ TabActor.prototype = {
    *         True if the window.console object is native, or false otherwise.
    */
   hasNativeConsoleAPI(window) {
     let isNative = false;
     try {
       // We are very explicitly examining the "console" property of
       // the non-Xrayed object here.
       let console = window.wrappedJSObject.console;
-      isNative = new XPCNativeWrapper(console).IS_NATIVE_CONSOLE
+      isNative = new XPCNativeWrapper(console).IS_NATIVE_CONSOLE;
     } catch (ex) {
       // ignore
     }
     return isNative;
   },
 
   /**
    * Create or return the StyleSheetActor for a style sheet. This method
@@ -2262,17 +2288,22 @@ function BrowserAddonList(connection) {
 }
 
 BrowserAddonList.prototype.getList = function () {
   let deferred = promise.defer();
   AddonManager.getAllAddons((addons) => {
     for (let addon of addons) {
       let actor = this._actorByAddonId.get(addon.id);
       if (!actor) {
-        actor = new BrowserAddonActor(this._connection, addon);
+        if (addon.isWebExtension) {
+          actor = new WebExtensionActor(this._connection, addon);
+        } else {
+          actor = new BrowserAddonActor(this._connection, addon);
+        }
+
         this._actorByAddonId.set(addon.id, actor);
       }
     }
     deferred.resolve([...this._actorByAddonId].map(([_, actor]) => actor));
   });
   return deferred.promise;
 };
 
@@ -2309,22 +2340,20 @@ BrowserAddonList.prototype._notifyListCh
   }
 };
 
 BrowserAddonList.prototype._adjustListener = function () {
   if (this._onListChanged) {
     // As long as the callback exists, we need to listen for changes
     // so we can notify about add-on changes.
     AddonManager.addAddonListener(this);
-  } else {
+  } else if (this._actorByAddonId.size === 0) {
     // When the callback does not exist, we only need to keep listening
     // if the actor cache will need adjusting when add-ons change.
-    if (this._actorByAddonId.size === 0) {
-      AddonManager.removeAddonListener(this);
-    }
+    AddonManager.removeAddonListener(this);
   }
 };
 
 exports.BrowserAddonList = BrowserAddonList;
 
 /**
  * The DebuggerProgressListener object is an nsIWebProgressListener which
  * handles onStateChange events for the inspected browser. If the user tries to
--- a/devtools/server/actors/webconsole.js
+++ b/devtools/server/actors/webconsole.js
@@ -589,18 +589,21 @@ WebConsoleActor.prototype =
             this.consoleServiceListener =
               new ConsoleServiceListener(window, this);
             this.consoleServiceListener.init();
           }
           startedListeners.push(listener);
           break;
         case "ConsoleAPI":
           if (!this.consoleAPIListener) {
+            // Create the consoleAPIListener (and apply the filtering options defined
+            // in the parent actor).
             this.consoleAPIListener =
-              new ConsoleAPIListener(window, this);
+              new ConsoleAPIListener(window, this,
+                                     this.parentActor.consoleAPIListenerOptions);
             this.consoleAPIListener.init();
           }
           startedListeners.push(listener);
           break;
         case "NetworkActivity":
           if (!this.networkMonitor) {
             // Create a StackTraceCollector that's going to be shared both by the
             // NetworkMonitorChild (getting messages about requests from parent) and
@@ -1291,17 +1294,29 @@ WebConsoleActor.prototype =
     // Ready to evaluate the string.
     helpers.evalInput = aString;
 
     let evalOptions;
     if (typeof aOptions.url == "string") {
       evalOptions = { url: aOptions.url };
     }
 
+    // If the debugger object is changed from the last evaluation,
+    // adopt this._lastConsoleInputEvaluation value in the new debugger,
+    // to prevents "Debugger.Object belongs to a different Debugger" exceptions
+    // related to the $_ bindings.
+    if (this._lastConsoleInputEvaluation &&
+        this._lastConsoleInputEvaluation.global !== dbgWindow) {
+      this._lastConsoleInputEvaluation = dbg.adoptDebuggeeValue(
+        this._lastConsoleInputEvaluation
+      );
+    }
+
     let result;
+
     if (frame) {
       result = frame.evalWithBindings(aString, bindings, evalOptions);
     }
     else {
       result = dbgWindow.executeInGlobalWithBindings(aString, bindings, evalOptions);
       // Attempt to initialize any declarations found in the evaluated string
       // since they may now be stuck in an "initializing" state due to the
       // error. Already-initialized bindings will be ignored.
copy from devtools/server/actors/chrome.js
copy to devtools/server/actors/webextension.js
--- a/devtools/server/actors/chrome.js
+++ b/devtools/server/actors/webextension.js
@@ -1,185 +1,333 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
-const { Ci } = require("chrome");
+const { Ci, Cu } = require("chrome");
 const Services = require("Services");
-const { DebuggerServer } = require("../main");
-const { getChildDocShells, TabActor } = require("./webbrowser");
+const { ChromeActor } = require("./chrome");
 const makeDebugger = require("./utils/make-debugger");
 
+var DevToolsUtils = require("devtools/shared/DevToolsUtils");
+var { assert } = DevToolsUtils;
+
+loader.lazyRequireGetter(this, "mapURIToAddonID", "devtools/server/actors/utils/map-uri-to-addon-id");
+loader.lazyRequireGetter(this, "unwrapDebuggerObjectGlobal", "devtools/server/actors/script", true);
+
+loader.lazyImporter(this, "AddonManager", "resource://gre/modules/AddonManager.jsm");
+loader.lazyImporter(this, "XPIProvider", "resource://gre/modules/addons/XPIProvider.jsm");
+
+const FALLBACK_DOC_MESSAGE = "Your addon does not have any document opened yet.";
+
 /**
- * Creates a TabActor for debugging all the chrome content in the
- * current process. Most of the implementation is inherited from TabActor.
- * ChromeActor is a child of RootActor, it can be instanciated via
- * RootActor.getProcess request.
- * ChromeActor exposes all tab actors via its form() request, like TabActor.
+ * Creates a TabActor for debugging all the contexts associated to a target WebExtensions
+ * add-on.
+ * Most of the implementation is inherited from ChromeActor (which inherits most of its
+ * implementation from TabActor).
+ * WebExtensionActor is a child of RootActor, it can be retrieved via
+ * RootActor.listAddons request.
+ * WebExtensionActor exposes all tab actors via its form() request, like TabActor.
  *
  * History lecture:
- * All tab actors used to also be registered as global actors,
- * so that the root actor was also exposing tab actors for the main process.
- * Tab actors ended up having RootActor as parent actor,
- * but more and more features of the tab actors were relying on TabActor.
- * So we are now exposing a process actor that offers the same API as TabActor
- * by inheriting its functionality.
- * Global actors are now only the actors that are meant to be global,
- * and are no longer related to any specific scope/document.
+ * The add-on actors used to not inherit TabActor because of the different way the
+ * add-on APIs where exposed to the add-on itself, and for this reason the Addon Debugger
+ * has only a sub-set of the feature available in the Tab or in the Browser Toolbox.
+ * In a WebExtensions add-on all the provided contexts (background and popup pages etc.),
+ * besides the Content Scripts which run in the content process, hooked to an existent
+ * tab, by creating a new WebExtensionActor which inherits from ChromeActor, we can
+ * provide a full features Addon Toolbox (which is basically like a BrowserToolbox which
+ * filters the visible sources and frames to the one that are related to the target
+ * add-on).
  *
- * @param aConnection DebuggerServerConnection
+ * @param conn DebuggerServerConnection
  *        The connection to the client.
+ * @param addon AddonWrapper
+ *        The target addon.
  */
-function ChromeActor(aConnection) {
-  TabActor.call(this, aConnection);
+function WebExtensionActor(conn, addon) {
+  ChromeActor.call(this, conn);
+
+  this.id = addon.id;
+  this.addon = addon;
+
+  // Bind the _allowSource helper to this, it is used in the
+  // TabActor to lazily create the TabSources instance.
+  this._allowSource = this._allowSource.bind(this);
 
-  // This creates a Debugger instance for chrome debugging all globals.
+  // Set the consoleAPIListener filtering options
+  // (retrieved and used in the related webconsole child actor).
+  this.consoleAPIListenerOptions = {
+    addonId: addon.id,
+  };
+
+  // This creates a Debugger instance for debugging all the add-on globals.
   this.makeDebugger = makeDebugger.bind(null, {
-    findDebuggees: dbg => dbg.findAllGlobals(),
-    shouldAddNewGlobalAsDebuggee: () => true
+    findDebuggees: dbg => {
+      return dbg.findAllGlobals().filter(this._shouldAddNewGlobalAsDebuggee);
+    },
+    shouldAddNewGlobalAsDebuggee: this._shouldAddNewGlobalAsDebuggee.bind(this),
   });
 
-  // Ensure catching the creation of any new content docshell
-  this.listenForNewDocShells = true;
+  // Discover the preferred debug global for the target addon
+  this.preferredTargetWindow = null;
+  this._findAddonPreferredTargetWindow();
+
+  AddonManager.addAddonListener(this);
+}
+exports.WebExtensionActor = WebExtensionActor;
+
+WebExtensionActor.prototype = Object.create(ChromeActor.prototype);
+
+WebExtensionActor.prototype.actorPrefix = "webExtension";
+WebExtensionActor.prototype.constructor = WebExtensionActor;
+
+// NOTE: This is needed to catch in the webextension webconsole all the
+// errors raised by the WebExtension internals that are not currently
+// associated with any window.
+WebExtensionActor.prototype.isRootActor = true;
+
+WebExtensionActor.prototype.form = function () {
+  assert(this.actorID, "addon should have an actorID.");
+
+  let baseForm = ChromeActor.prototype.form.call(this);
 
-  // Defines the default docshell selected for the tab actor
-  let window = Services.wm.getMostRecentWindow(DebuggerServer.chromeWindowType);
+  return Object.assign(baseForm, {
+    actor: this.actorID,
+    id: this.id,
+    name: this.addon.name,
+    url: this.addon.sourceURI ? this.addon.sourceURI.spec : undefined,
+    iconURL: this.addon.iconURL,
+    debuggable: this.addon.isDebuggable,
+    temporarilyInstalled: this.addon.temporarilyInstalled,
+    isWebExtension: this.addon.isWebExtension,
+  });
+};
 
-  // Default to any available top level window if there is no expected window
-  // (for example when we open firefox with -webide argument)
-  if (!window) {
-    window = Services.wm.getMostRecentWindow(null);
+WebExtensionActor.prototype._attach = function () {
+  // NOTE: we need to be sure that `this.window` can return a
+  // window before calling the ChromeActor.onAttach, or the TabActor
+  // will not be subscribed to the child doc shell updates.
+
+  // If a preferredTargetWindow exists, set it as the target for this actor
+  // when the client request to attach this actor.
+  if (this.preferredTargetWindow) {
+    this._setWindow(this.preferredTargetWindow);
+  } else {
+    this._createFallbackWindow();
   }
-  // On xpcshell, there is no window/docshell
-  let docShell = window ? window.QueryInterface(Ci.nsIInterfaceRequestor)
-                                .getInterface(Ci.nsIDocShell)
-                        : null;
-  Object.defineProperty(this, "docShell", {
-    value: docShell,
-    configurable: true
-  });
-}
-exports.ChromeActor = ChromeActor;
+
+  // Call ChromeActor's _attach to listen for any new/destroyed chrome docshell
+  ChromeActor.prototype._attach.apply(this);
+};
 
-ChromeActor.prototype = Object.create(TabActor.prototype);
+WebExtensionActor.prototype._detach = function () {
+  this._destroyFallbackWindow();
 
-ChromeActor.prototype.constructor = ChromeActor;
-
-ChromeActor.prototype.isRootActor = true;
+  // Call ChromeActor's _detach to unsubscribe new/destroyed chrome docshell listeners.
+  ChromeActor.prototype._detach.apply(this);
+};
 
 /**
- * Getter for the list of all docshells in this tabActor
- * @return {Array}
+ * Called when the actor is removed from the connection.
+ */
+WebExtensionActor.prototype.exit = function () {
+  AddonManager.removeAddonListener(this);
+
+  this.preferredTargetWindow = null;
+  this.addon = null;
+  this.id = null;
+
+  return ChromeActor.prototype.exit.apply(this);
+};
+
+// Addon Specific Remote Debugging requestTypes and methods.
+
+/**
+ * Reloads the addon.
  */
-Object.defineProperty(ChromeActor.prototype, "docShells", {
-  get: function () {
-    // Iterate over all top-level windows and all their docshells.
-    let docShells = [];
-    let e = Services.ww.getWindowEnumerator();
-    while (e.hasMoreElements()) {
-      let window = e.getNext();
-      let docShell = window.QueryInterface(Ci.nsIInterfaceRequestor)
-                           .getInterface(Ci.nsIWebNavigation)
-                           .QueryInterface(Ci.nsIDocShell);
-      docShells = docShells.concat(getChildDocShells(docShell));
-    }
+WebExtensionActor.prototype.onReload = function () {
+  return this.addon.reload()
+    .then(() => {
+      // send an empty response
+      return {};
+    });
+};
 
-    return docShells;
-  }
-});
-
-ChromeActor.prototype.observe = function (aSubject, aTopic, aData) {
-  TabActor.prototype.observe.call(this, aSubject, aTopic, aData);
-  if (!this.attached) {
-    return;
-  }
-  if (aTopic == "chrome-webnavigation-create") {
-    aSubject.QueryInterface(Ci.nsIDocShell);
-    this._onDocShellCreated(aSubject);
-  } else if (aTopic == "chrome-webnavigation-destroy") {
-    this._onDocShellDestroy(aSubject);
+/**
+ * Set the preferred global for the add-on (called from the AddonManager).
+ */
+WebExtensionActor.prototype.setOptions = function (addonOptions) {
+  if ("global" in addonOptions) {
+    // Set the proposed debug global as the preferred target window
+    // (the actor will eventually set it as the target once it is attached)
+    this.preferredTargetWindow = addonOptions.global;
   }
 };
 
-ChromeActor.prototype._attach = function () {
-  if (this.attached) {
-    return false;
+// AddonManagerListener callbacks.
+
+WebExtensionActor.prototype.onInstalled = function (addon) {
+  if (addon.id != this.id) {
+    return;
   }
 
-  TabActor.prototype._attach.call(this);
+  // Update the AddonManager's addon object on reload/update.
+  this.addon = addon;
+};
 
-  // Listen for any new/destroyed chrome docshell
-  Services.obs.addObserver(this, "chrome-webnavigation-create", false);
-  Services.obs.addObserver(this, "chrome-webnavigation-destroy", false);
+WebExtensionActor.prototype.onUninstalled = function (addon) {
+  if (addon != this.addon) {
+    return;
+  }
 
-  // Iterate over all top-level windows.
-  let docShells = [];
-  let e = Services.ww.getWindowEnumerator();
-  while (e.hasMoreElements()) {
-    let window = e.getNext();
-    let docShell = window.QueryInterface(Ci.nsIInterfaceRequestor)
-                         .getInterface(Ci.nsIWebNavigation)
-                         .QueryInterface(Ci.nsIDocShell);
-    if (docShell == this.docShell) {
-      continue;
-    }
-    this._progressListener.watch(docShell);
+  this.exit();
+};
+
+WebExtensionActor.prototype.onPropertyChanged = function (addon, changedPropNames) {
+  if (addon != this.addon) {
+    return;
+  }
+
+  // Refresh the preferred debug global on disabled/reloaded/upgraded addon.
+  if (changedPropNames.includes("debugGlobal")) {
+    this._findAddonPreferredTargetWindow();
   }
 };
 
-ChromeActor.prototype._detach = function () {
-  if (!this.attached) {
-    return false;
+// Private helpers
+
+WebExtensionActor.prototype._createFallbackWindow = function () {
+  if (this.fallbackWindow) {
+    // Skip if there is already an existent fallback window.
+    return;
   }
 
-  Services.obs.removeObserver(this, "chrome-webnavigation-create");
-  Services.obs.removeObserver(this, "chrome-webnavigation-destroy");
+  // Create an empty hidden window as a fallback (e.g. the background page could be
+  // not defined for the target add-on or not yet when the actor instance has been
+  // created).
+  this.fallbackWebNav = Services.appShell.createWindowlessBrowser(true);
+  this.fallbackWebNav.loadURI(
+    `data:text/html;charset=utf-8,${FALLBACK_DOC_MESSAGE}`,
+    0, null, null, null
+  );
 
-  // Iterate over all top-level windows.
-  let docShells = [];
-  let e = Services.ww.getWindowEnumerator();
-  while (e.hasMoreElements()) {
-    let window = e.getNext();
-    let docShell = window.QueryInterface(Ci.nsIInterfaceRequestor)
-                         .getInterface(Ci.nsIWebNavigation)
-                         .QueryInterface(Ci.nsIDocShell);
-    if (docShell == this.docShell) {
-      continue;
-    }
-    this._progressListener.unwatch(docShell);
-  }
+  this.fallbackDocShell = this.fallbackWebNav
+    .QueryInterface(Ci.nsIInterfaceRequestor)
+    .getInterface(Ci.nsIDocShell);
 
-  TabActor.prototype._detach.call(this);
+  Object.defineProperty(this, "docShell", {
+    value: this.fallbackDocShell,
+    configurable: true
+  });
+
+  // Save the reference to the fallback DOMWindow
+  this.fallbackWindow = this.fallbackDocShell.QueryInterface(Ci.nsIInterfaceRequestor)
+                                             .getInterface(Ci.nsIDOMWindow);
 };
 
-/* ThreadActor hooks. */
+WebExtensionActor.prototype._destroyFallbackWindow = function () {
+  if (this.fallbackWebNav) {
+    // Explicitly close the fallback windowless browser to prevent it to leak
+    // (and to prevent it to freeze devtools xpcshell tests).
+    this.fallbackWebNav.loadURI("about:blank", 0, null, null, null);
+    this.fallbackWebNav.close();
 
-/**
- * Prepare to enter a nested event loop by disabling debuggee events.
- */
-ChromeActor.prototype.preNest = function () {
-  // Disable events in all open windows.
-  let e = Services.wm.getEnumerator(null);
-  while (e.hasMoreElements()) {
-    let win = e.getNext();
-    let windowUtils = win.QueryInterface(Ci.nsIInterfaceRequestor)
-                         .getInterface(Ci.nsIDOMWindowUtils);
-    windowUtils.suppressEventHandling(true);
-    windowUtils.suspendTimeouts();
+    this.fallbackWebNav = null;
+    this.fallbackWindow = null;
   }
 };
 
 /**
- * Prepare to exit a nested event loop by enabling debuggee events.
+ * Discover the preferred debug global and switch to it if the addon has been attached.
  */
-ChromeActor.prototype.postNest = function (aNestData) {
-  // Enable events in all open windows.
-  let e = Services.wm.getEnumerator(null);
-  while (e.hasMoreElements()) {
-    let win = e.getNext();
-    let windowUtils = win.QueryInterface(Ci.nsIInterfaceRequestor)
-                         .getInterface(Ci.nsIDOMWindowUtils);
-    windowUtils.resumeTimeouts();
-    windowUtils.suppressEventHandling(false);
+WebExtensionActor.prototype._findAddonPreferredTargetWindow = function () {
+  return new Promise(resolve => {
+    let activeAddon = XPIProvider.activeAddons.get(this.id);
+
+    if (!activeAddon) {
+      // The addon is not active, the background page is going to be destroyed,
+      // navigate to the fallback window (if it already exists).
+      resolve(null);
+    } else {
+      AddonManager.getAddonByInstanceID(activeAddon.instanceID)
+        .then(privateWrapper => {
+          let targetWindow = privateWrapper.getDebugGlobal();
+
+          // Do not use the preferred global if it is not a DOMWindow as expected.
+          if (!(targetWindow instanceof Ci.nsIDOMWindow)) {
+            targetWindow = null;
+          }
+
+          resolve(targetWindow);
+        });
+    }
+  }).then(preferredTargetWindow => {
+    this.preferredTargetWindow = preferredTargetWindow;
+
+    if (!preferredTargetWindow) {
+      // Create a fallback window if no preferred target window has been found.
+      this._createFallbackWindow();
+    } else if (this.attached) {
+      // Change the top level document if the actor is already attached.
+      this._changeTopLevelDocument(preferredTargetWindow);
+    }
+  });
+};
+
+/**
+ * Return an array of the json details related to an array/iterator of docShells.
+ */
+WebExtensionActor.prototype._docShellsToWindows = function (docshells) {
+  return ChromeActor.prototype._docShellsToWindows.call(this, docshells)
+                    .filter(windowDetails => {
+                      // filter the docShells based on the addon id
+                      return windowDetails.addonID == this.id;
+                    });
+};
+
+/**
+ * Return true if the given source is associated with this addon and should be
+ * added to the visible sources (retrieved and used by the webbrowser actor module).
+ */
+WebExtensionActor.prototype._allowSource = function (source) {
+  try {
+    let uri = Services.io.newURI(source.url, null, null);
+    let addonID = mapURIToAddonID(uri);
+
+    return addonID == this.id;
+  } catch (e) {
+    return false;
   }
 };
+
+/**
+ * Return true if the given global is associated with this addon and should be
+ * added as a debuggee, false otherwise.
+ */
+WebExtensionActor.prototype._shouldAddNewGlobalAsDebuggee = function (newGlobal) {
+  const global = unwrapDebuggerObjectGlobal(newGlobal);
+
+  if (global instanceof Ci.nsIDOMWindow) {
+    return global.document.nodePrincipal.originAttributes.addonId == this.id;
+  }
+
+  try {
+    // This will fail for non-Sandbox objects, hence the try-catch block.
+    let metadata = Cu.getSandboxMetadata(global);
+    if (metadata) {
+      return metadata.addonID === this.id;
+    }
+  } catch (e) {
+    // Unable to retrieve the sandbox metadata.
+  }
+
+  return false;
+};
+
+/**
+ * Override WebExtensionActor requestTypes:
+ * - redefined `reload`, which should reload the target addon
+ *   (instead of the entire browser as the regular ChromeActor does).
+ */
+WebExtensionActor.prototype.requestTypes.reload = WebExtensionActor.prototype.onReload;
--- a/dom/base/nsDocument.cpp
+++ b/dom/base/nsDocument.cpp
@@ -2420,17 +2420,18 @@ nsDocument::FillStyleSet(StyleSetHandle 
 
     // Iterate backwards to maintain order
     for (StyleSheetHandle sheet : Reversed(mOnDemandBuiltInUASheets)) {
       if (sheet->IsApplicable()) {
         aStyleSet->PrependStyleSheet(SheetType::Agent, sheet);
       }
     }
   } else {
-    NS_ERROR("stylo: nsStyleSheetService doesn't handle ServoStyleSheets yet");
+    NS_WARNING("stylo: Not yet checking nsStyleSheetService for Servo-backed "
+               "documents. See bug 1290224");
   }
 
   AppendSheetsToStyleSet(aStyleSet, mAdditionalSheets[eAgentSheet],
                          SheetType::Agent);
   AppendSheetsToStyleSet(aStyleSet, mAdditionalSheets[eUserSheet],
                          SheetType::User);
   AppendSheetsToStyleSet(aStyleSet, mAdditionalSheets[eAuthorSheet],
                          SheetType::Doc);
@@ -13345,17 +13346,18 @@ nsIDocument::FlushUserFontSet()
       nsIPresShell* shell = GetShell();
       if (shell) {
         // XXXheycam ServoStyleSets don't support exposing @font-face rules yet.
         if (shell->StyleSet()->IsGecko()) {
           if (!shell->StyleSet()->AsGecko()->AppendFontFaceRules(rules)) {
             return;
           }
         } else {
-          NS_ERROR("stylo: ServoStyleSets cannot handle @font-face rules yet");
+          NS_WARNING("stylo: ServoStyleSets cannot handle @font-face rules yet. "
+                     "See bug 1290237.");
         }
       }
 
       bool changed = false;
 
       if (!mFontFaceSet && !rules.IsEmpty()) {
         nsCOMPtr<nsPIDOMWindowInner> window = do_QueryInterface(GetScopeObject());
         mFontFaceSet = new FontFaceSet(window, this);
@@ -13434,33 +13436,33 @@ nsIDocument::ReportHasScrollLinkedEffect
                                   "ScrollLinkedEffectFound2");
 }
 
 void
 nsIDocument::UpdateStyleBackendType()
 {
   MOZ_ASSERT(mStyleBackendType == StyleBackendType(0),
              "no need to call UpdateStyleBackendType now");
+
+  // Assume Gecko by default.
+  mStyleBackendType = StyleBackendType::Gecko;
+
 #ifdef MOZ_STYLO
   // XXX For now we use a Servo-backed style set only for (X)HTML documents
   // in content docshells.  This should let us avoid implementing XUL-specific
   // CSS features.  And apart from not supporting SVG properties in Servo
   // yet, the root SVG element likes to create a style sheet for an SVG
   // document before we have a pres shell (i.e. before we make the decision
   // here about whether to use a Gecko- or Servo-backed style system), so
   // we avoid Servo-backed style sets for SVG documents.
-  NS_ASSERTION(mDocumentContainer, "stylo: calling UpdateStyleBackendType "
-                                   "before we have a docshell");
-  mStyleBackendType =
-    nsLayoutUtils::SupportsServoStyleBackend(this) &&
-    mDocumentContainer ?
-      StyleBackendType::Servo :
-      StyleBackendType::Gecko;
-#else
-  mStyleBackendType = StyleBackendType::Gecko;
+  if (!mDocumentContainer) {
+    NS_WARNING("stylo: No docshell yet, assuming Gecko style system");
+  } else if (nsLayoutUtils::SupportsServoStyleBackend(this)) {
+    mStyleBackendType = StyleBackendType::Servo;
+  }
 #endif
 }
 
 nsString*
 nsDocument::CheckCustomElementName(const ElementCreationOptions& aOptions,
                                    const nsAString& aLocalName,
                                    uint32_t aNamespaceID,
                                    ErrorResult& rv)
--- a/dom/base/nsMimeTypeArray.cpp
+++ b/dom/base/nsMimeTypeArray.cpp
@@ -25,17 +25,18 @@ NS_IMPL_CYCLE_COLLECTING_ADDREF(nsMimeTy
 NS_IMPL_CYCLE_COLLECTING_RELEASE(nsMimeTypeArray)
 NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(nsMimeTypeArray)
   NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY
   NS_INTERFACE_MAP_ENTRY(nsISupports)
 NS_INTERFACE_MAP_END
 
 NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(nsMimeTypeArray,
                                       mWindow,
-                                      mMimeTypes)
+                                      mMimeTypes,
+                                      mCTPMimeTypes)
 
 nsMimeTypeArray::nsMimeTypeArray(nsPIDOMWindowInner* aWindow)
   : mWindow(aWindow)
 {
 }
 
 nsMimeTypeArray::~nsMimeTypeArray()
 {
@@ -52,16 +53,17 @@ nsMimeTypeArray::WrapObject(JSContext* a
 {
   return MimeTypeArrayBinding::Wrap(aCx, this, aGivenProto);
 }
 
 void
 nsMimeTypeArray::Refresh()
 {
   mMimeTypes.Clear();
+  mCTPMimeTypes.Clear();
 }
 
 nsPIDOMWindowInner*
 nsMimeTypeArray::GetParentObject() const
 {
   MOZ_ASSERT(mWindow);
   return mWindow;
 }
@@ -128,16 +130,20 @@ nsMimeTypeArray::NamedGetter(const nsASt
   nsString lowerName(aName);
   ToLowerCase(lowerName);
 
   nsMimeType* mimeType = FindMimeType(mMimeTypes, lowerName);
   if (mimeType) {
     aFound = true;
     return mimeType;
   }
+  nsMimeType* hiddenType = FindMimeType(mCTPMimeTypes, lowerName);
+  if (hiddenType) {
+    nsPluginArray::NotifyHiddenPluginTouched(hiddenType->GetEnabledPlugin());
+  }
 
   return nullptr;
 }
 
 uint32_t
 nsMimeTypeArray::Length()
 {
   if (ResistFingerprinting()) {
@@ -175,16 +181,17 @@ nsMimeTypeArray::EnsurePluginMimeTypes()
   ErrorResult rv;
   nsPluginArray *pluginArray =
     static_cast<Navigator*>(navigator.get())->GetPlugins(rv);
   if (!pluginArray) {
     return;
   }
 
   pluginArray->GetMimeTypes(mMimeTypes);
+  pluginArray->GetCTPMimeTypes(mCTPMimeTypes);
 }
 
 NS_IMPL_CYCLE_COLLECTION_ROOT_NATIVE(nsMimeType, AddRef)
 NS_IMPL_CYCLE_COLLECTION_UNROOT_NATIVE(nsMimeType, Release)
 
 NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(nsMimeType, mWindow, mPluginElement)
 
 nsMimeType::nsMimeType(nsPIDOMWindowInner* aWindow,
--- a/dom/base/nsMimeTypeArray.h
+++ b/dom/base/nsMimeTypeArray.h
@@ -43,16 +43,17 @@ protected:
   void EnsurePluginMimeTypes();
   void Clear();
 
   nsCOMPtr<nsPIDOMWindowInner> mWindow;
 
   // mMimeTypes contains MIME types handled by plugins or by an OS
   // PreferredApplicationHandler.
   nsTArray<RefPtr<nsMimeType> > mMimeTypes;
+  nsTArray<RefPtr<nsMimeType> > mCTPMimeTypes;
 };
 
 class nsMimeType final : public nsWrapperCache
 {
 public:
   NS_INLINE_DECL_CYCLE_COLLECTING_NATIVE_REFCOUNTING(nsMimeType)
   NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_NATIVE_CLASS(nsMimeType)
 
--- a/dom/base/nsPluginArray.cpp
+++ b/dom/base/nsPluginArray.cpp
@@ -109,16 +109,34 @@ nsPluginArray::GetMimeTypes(nsTArray<Ref
 
   GetPluginMimeTypes(mPlugins, aMimeTypes);
 
   // Alphabetize the enumeration order of non-hidden MIME types to reduce
   // fingerprintable entropy based on plugins' installation file times.
   aMimeTypes.Sort();
 }
 
+void
+nsPluginArray::GetCTPMimeTypes(nsTArray<RefPtr<nsMimeType>>& aMimeTypes)
+{
+  aMimeTypes.Clear();
+
+  if (!AllowPlugins()) {
+    return;
+  }
+
+  EnsurePlugins();
+
+  GetPluginMimeTypes(mCTPPlugins, aMimeTypes);
+
+  // Alphabetize the enumeration order of non-hidden MIME types to reduce
+  // fingerprintable entropy based on plugins' installation file times.
+  aMimeTypes.Sort();
+}
+
 nsPluginElement*
 nsPluginArray::Item(uint32_t aIndex)
 {
   bool unused;
   return IndexedGetter(aIndex, unused);
 }
 
 nsPluginElement*
@@ -231,31 +249,36 @@ nsPluginArray::NamedGetter(const nsAStri
 
   EnsurePlugins();
 
   nsPluginElement* plugin = FindPlugin(mPlugins, aName);
   aFound = (plugin != nullptr);
   if (!aFound) {
     nsPluginElement* hiddenPlugin = FindPlugin(mCTPPlugins, aName);
     if (hiddenPlugin) {
-      HiddenPluginEventInit init;
-      init.mTag = hiddenPlugin->PluginTag();
-      nsCOMPtr<nsIDocument> doc = hiddenPlugin->GetParentObject()->GetDoc();
-      RefPtr<HiddenPluginEvent> event =
-        HiddenPluginEvent::Constructor(doc, NS_LITERAL_STRING("HiddenPlugin"), init);
-      event->SetTarget(doc);
-      event->SetTrusted(true);
-      event->WidgetEventPtr()->mFlags.mOnlyChromeDispatch = true;
-      bool dummy;
-      doc->DispatchEvent(event, &dummy);
+      NotifyHiddenPluginTouched(hiddenPlugin);
     }
   }
   return plugin;
 }
 
+void nsPluginArray::NotifyHiddenPluginTouched(nsPluginElement* aHiddenElement)
+{
+  HiddenPluginEventInit init;
+  init.mTag = aHiddenElement->PluginTag();
+  nsCOMPtr<nsIDocument> doc = aHiddenElement->GetParentObject()->GetDoc();
+  RefPtr<HiddenPluginEvent> event =
+    HiddenPluginEvent::Constructor(doc, NS_LITERAL_STRING("HiddenPlugin"), init);
+  event->SetTarget(doc);
+  event->SetTrusted(true);
+  event->WidgetEventPtr()->mFlags.mOnlyChromeDispatch = true;
+  bool dummy;
+  doc->DispatchEvent(event, &dummy);
+}
+
 uint32_t
 nsPluginArray::Length()
 {
   if (!AllowPlugins() || ResistFingerprinting()) {
     return 0;
   }
 
   EnsurePlugins();
--- a/dom/base/nsPluginArray.h
+++ b/dom/base/nsPluginArray.h
@@ -36,16 +36,19 @@ public:
   // nsPluginArray registers itself as an observer with a weak reference.
   // This can't be done in the constructor, because at that point its
   // refcount is 0 (and it gets destroyed upon registration). So, Init()
   // must be called after construction.
   void Init();
   void Invalidate();
 
   void GetMimeTypes(nsTArray<RefPtr<nsMimeType>>& aMimeTypes);
+  void GetCTPMimeTypes(nsTArray<RefPtr<nsMimeType>>& aMimeTypes);
+
+  static void NotifyHiddenPluginTouched(nsPluginElement* aElement);
 
   // PluginArray WebIDL methods
 
   nsPluginElement* Item(uint32_t aIndex);
   nsPluginElement* NamedItem(const nsAString& aName);
   void Refresh(bool aReloadDocuments);
   nsPluginElement* IndexedGetter(uint32_t aIndex, bool &aFound);
   nsPluginElement* NamedGetter(const nsAString& aName, bool &aFound);
--- a/dom/canvas/WebGLContextVertices.cpp
+++ b/dom/canvas/WebGLContextVertices.cpp
@@ -341,26 +341,20 @@ WebGLContext::GetVertexAttrib(JSContext*
     switch (pname) {
     case LOCAL_GL_VERTEX_ATTRIB_ARRAY_BUFFER_BINDING:
         return WebGLObjectAsJSValue(cx, mBoundVertexArray->mAttribs[index].buf.get(), rv);
 
     case LOCAL_GL_VERTEX_ATTRIB_ARRAY_STRIDE:
         return JS::Int32Value(mBoundVertexArray->mAttribs[index].stride);
 
     case LOCAL_GL_VERTEX_ATTRIB_ARRAY_SIZE:
-        if (!mBoundVertexArray->mAttribs[index].enabled)
-            return JS::Int32Value(4);
-
         return JS::Int32Value(mBoundVertexArray->mAttribs[index].size);
 
     case LOCAL_GL_VERTEX_ATTRIB_ARRAY_TYPE:
-        if (!mBoundVertexArray->mAttribs[index].enabled)
-            return JS::NumberValue(uint32_t(LOCAL_GL_FLOAT));
-
-        return JS::NumberValue(uint32_t(mBoundVertexArray->mAttribs[index].type));
+        return JS::Int32Value(mBoundVertexArray->mAttribs[index].type);
 
     case LOCAL_GL_VERTEX_ATTRIB_ARRAY_INTEGER:
         if (IsWebGL2())
             return JS::BooleanValue(mBoundVertexArray->mAttribs[index].integer);
 
         break;
 
     case LOCAL_GL_VERTEX_ATTRIB_ARRAY_DIVISOR:
--- a/dom/canvas/WebGLTextureUpload.cpp
+++ b/dom/canvas/WebGLTextureUpload.cpp
@@ -257,16 +257,19 @@ WebGLTexture::TexOrSubImage(bool isSubIm
         }
 
         if (width && height && depth) {
             view.ComputeLengthAndData();
 
             bytes = view.DataAllowShared();
             byteCount = view.LengthAllowShared();
         }
+    } else if (isSubImage) {
+        mContext->ErrorInvalidValue("%s: `pixels` must not be null.", funcName);
+        return;
     }
 
     const bool isClientData = true;
     webgl::TexUnpackBytes blob(mContext, target, width, height, depth, isClientData,
                                bytes);
 
     if (bytes &&
         !ValidateUnpackBytes(mContext, funcName, width, height, depth, pi, byteCount,
new file mode 100644
--- /dev/null
+++ b/dom/canvas/crashtests/1284356-1.html
@@ -0,0 +1,9 @@
+<canvas id='i0'></canvas>
+<script>
+var c=document.getElementById('i0').getContext('2d');
+c.lineWidth=194.622602174;
+c.miterLimit=270.273509738;
+c.transform(0,0,0,0,0,0);
+c.globalCompositeOperation='soft-light';
+c.strokeText('a',200,273,722);
+</script>
\ No newline at end of file
new file mode 100644
--- /dev/null
+++ b/dom/canvas/crashtests/1284578-1.html
@@ -0,0 +1,8 @@
+<canvas id='i0'></canvas>
+<script>
+var c=document.getElementById('i0').getContext('2d');
+c.bezierCurveTo(157,351,351,44,946,701);
+c.quadraticCurveTo(260,-9007199254740991,945,145);
+c.translate(-9007199254740991,239);
+c.isPointInPath(988,439);
+</script>
\ No newline at end of file
new file mode 100644
--- /dev/null
+++ b/dom/canvas/crashtests/1287652-1.html
@@ -0,0 +1,8 @@
+<canvas id='i0'></canvas>
+<script>
+var c=document.getElementById('i0').getContext('2d');
+var g=c.createLinearGradient(59,9,38.89,-75.51);
+c.fillStyle=g;
+c.globalAlpha=0.62;
+c.fillText('a',0,24,30);
+</script>
--- a/dom/canvas/crashtests/crashtests.list
+++ b/dom/canvas/crashtests/crashtests.list
@@ -24,11 +24,14 @@ load 1161277-1.html
 load 1183363.html
 load 1190705.html
 load 1223740-1.html
 load 1225381-1.html
 skip-if(azureCairo) load 1229983-1.html
 load 1229932-1.html
 load 1244850-1.html
 load 1246775-1.html
+load 1284356-1.html
+load 1284578-1.html
 skip-if(d2d) load 1287515-1.html
+load 1287652-1.html
 load 1288872-1.html
 
--- a/dom/canvas/test/webgl-conf/generated-mochitest.ini
+++ b/dom/canvas/test/webgl-conf/generated-mochitest.ini
@@ -5833,17 +5833,16 @@ skip-if = (os == 'android' || os == 'lin
 [generated/test_2_conformance__textures__misc__tex-image-webgl.html]
 skip-if = (os == 'android' || os == 'linux' || (os == 'win' && os_version == '5.1') || (os == 'win' && os_version == '6.2'))
 [generated/test_2_conformance__textures__misc__tex-image-with-format-and-type.html]
 fail-if = (os == 'mac')
 skip-if = (os == 'android' || os == 'linux' || (os == 'win' && os_version == '5.1') || (os == 'win' && os_version == '6.2'))
 [generated/test_2_conformance__textures__misc__tex-image-with-invalid-data.html]
 skip-if = (os == 'android' || os == 'linux' || (os == 'win' && os_version == '5.1') || (os == 'win' && os_version == '6.2'))
 [generated/test_2_conformance__textures__misc__tex-sub-image-2d-bad-args.html]
-fail-if = (os == 'mac') || (os == 'win')
 skip-if = (os == 'android' || os == 'linux' || (os == 'win' && os_version == '5.1') || (os == 'win' && os_version == '6.2'))
 [generated/test_2_conformance__textures__misc__tex-sub-image-2d.html]
 skip-if = (os == 'android' || os == 'linux' || (os == 'win' && os_version == '5.1') || (os == 'win' && os_version == '6.2'))
 [generated/test_2_conformance__textures__misc__texparameter-test.html]
 skip-if = (os == 'android' || os == 'linux' || (os == 'win' && os_version == '5.1') || (os == 'win' && os_version == '6.2'))
 [generated/test_2_conformance__textures__misc__texture-active-bind-2.html]
 skip-if = (os == 'android' || os == 'linux' || (os == 'win' && os_version == '5.1') || (os == 'win' && os_version == '6.2'))
 [generated/test_2_conformance__textures__misc__texture-active-bind.html]
@@ -6717,17 +6716,16 @@ skip-if = (os == 'win' && debug)
 [generated/test_conformance__textures__misc__tex-image-canvas-corruption.html]
 [generated/test_conformance__textures__misc__tex-image-webgl.html]
 skip-if = (os == 'android')
 [generated/test_conformance__textures__misc__tex-image-with-format-and-type.html]
 [generated/test_conformance__textures__misc__tex-image-with-invalid-data.html]
 skip-if = (os == 'android')
 [generated/test_conformance__textures__misc__tex-input-validation.html]
 [generated/test_conformance__textures__misc__tex-sub-image-2d-bad-args.html]
-fail-if = (os == 'mac') || (os == 'win') || (os == 'android') || (os == 'linux')
 [generated/test_conformance__textures__misc__tex-sub-image-2d.html]
 [generated/test_conformance__textures__misc__texparameter-test.html]
 [generated/test_conformance__textures__misc__texture-active-bind-2.html]
 [generated/test_conformance__textures__misc__texture-active-bind.html]
 [generated/test_conformance__textures__misc__texture-attachment-formats.html]
 [generated/test_conformance__textures__misc__texture-clear.html]
 [generated/test_conformance__textures__misc__texture-complete.html]
 [generated/test_conformance__textures__misc__texture-copying-feedback-loops.html]
--- a/dom/canvas/test/webgl-conf/mochitest-errata.ini
+++ b/dom/canvas/test/webgl-conf/mochitest-errata.ini
@@ -143,30 +143,26 @@ fail-if = (os == 'mac') || (os == 'win')
 [generated/test_2_conformance2__rendering__draw-buffers.html]
 fail-if = (os == 'mac') || (os == 'win')
 [generated/test_2_conformance__textures__misc__tex-image-with-format-and-type.html]
 fail-if = (os == 'mac')
 [generated/test_2_conformance__attribs__gl-vertexattribpointer.html]
 fail-if = (os == 'mac') || (os == 'win')
 [generated/test_2_conformance2__glsl3__forbidden-operators.html]
 fail-if = (os == 'mac') || (os == 'win')
-[generated/test_conformance__textures__misc__tex-sub-image-2d-bad-args.html]
-fail-if = (os == 'mac') || (os == 'win') || (os == 'android') || (os == 'linux')
 [generated/test_2_conformance2__vertex_arrays__vertex-array-object.html]
 fail-if = (os == 'mac') || (os == 'win')
 [generated/test_2_conformance__rendering__negative-one-index.html]
 fail-if = (os == 'mac') || (os == 'win')
 [generated/test_conformance__extensions__oes-texture-half-float.html]
 fail-if = (os == 'mac') || (os == 'win') || (os == 'android') || (os == 'linux')
 [generated/test_2_conformance2__reading__read-pixels-pack-parameters.html]
 fail-if = (os == 'mac') || (os == 'win')
 [generated/test_conformance__attribs__gl-vertexattribpointer.html]
 fail-if = (os == 'mac') || (os == 'win') || (os == 'android') || (os == 'linux')
-[generated/test_2_conformance__textures__misc__tex-sub-image-2d-bad-args.html]
-fail-if = (os == 'mac') || (os == 'win')
 [generated/test_conformance__ogles__GL__biuDepthRange__biuDepthRange_001_to_002.html]
 fail-if = (os == 'android') || (os == 'linux')
 [generated/test_conformance__ogles__GL__gl_FragCoord__gl_FragCoord_001_to_003.html]
 fail-if = (os == 'android') || (os == 'linux')
 
 [generated/test_conformance__textures__misc__texture-size-limit.html]
 fail-if = (os == 'linux') || (os == 'android')
 skip-if = (os == 'linux' && asan)
--- a/dom/html/HTMLInputElement.cpp
+++ b/dom/html/HTMLInputElement.cpp
@@ -2360,32 +2360,16 @@ HTMLInputElement::GetValueIfStepped(int3
     } else if (aStep < 0) {
       value -= deltaFromStep;             // partial step
       value += step * Decimal(aStep + 1); // then remaining steps
     }
   } else {
     value += step * Decimal(aStep);
   }
 
-  // For date inputs, the value can hold a string that is not a day. We do not
-  // want to round it, as it might result in a step mismatch. Instead we want to
-  // clamp to the next valid value.
-  if (mType == NS_FORM_INPUT_DATE &&
-      NS_floorModulo(Decimal(value - GetStepBase()), GetStepScaleFactor()) != Decimal(0)) {
-    MOZ_ASSERT(GetStep() > Decimal(0));
-    Decimal validStep = EuclidLCM<Decimal>(GetStep().floor(),
-                                           GetStepScaleFactor().floor());
-    if (aStep > 0) {
-      value -= NS_floorModulo(value - GetStepBase(), validStep);
-      value += validStep;
-    } else if (aStep < 0) {
-      value -= NS_floorModulo(value - GetStepBase(), validStep);
-    }
-  }
-
   if (value < minimum) {
     value = minimum;
     deltaFromStep = NS_floorModulo(value - stepBase, step);
     if (deltaFromStep != Decimal(0)) {
       value += step - deltaFromStep;
     }
   }
   if (value > maximum) {
@@ -6898,16 +6882,21 @@ HTMLInputElement::GetStep() const
     return kStepAny;
   }
 
   Decimal step = StringToDecimal(stepStr);
   if (!step.isFinite() || step <= Decimal(0)) {
     step = GetDefaultStep();
   }
 
+  // For input type=date, we round the step value to have a rounded day.
+  if (mType == NS_FORM_INPUT_DATE) {
+    step = std::max(step.round(), Decimal(1));
+  }
+
   return step * GetStepScaleFactor();
 }
 
 // nsIConstraintValidation
 
 NS_IMETHODIMP
 HTMLInputElement::SetCustomValidity(const nsAString& aError)
 {
@@ -7501,26 +7490,16 @@ HTMLInputElement::GetValidationMessage(n
       nsXPIDLString message;
 
       Decimal value = GetValueAsDecimal();
       MOZ_ASSERT(!value.isNaN());
 
       Decimal step = GetStep();
       MOZ_ASSERT(step != kStepAny && step > Decimal(0));
 
-      // In case this is a date and the step is not an integer, we don't want to
-      // display the dates corresponding to the truncated timestamps of valueLow
-      // and valueHigh because they might suffer from a step mismatch as well.
-      // Instead we want the timestamps to correspond to a rounded day. That is,
-      // we want a multiple of the step scale factor (1 day) as well as of step.
-      if (mType == NS_FORM_INPUT_DATE) {
-        step = EuclidLCM<Decimal>(step.floor(),
-                                  GetStepScaleFactor().floor());
-      }
-
       Decimal stepBase = GetStepBase();
 
       Decimal valueLow = value - NS_floorModulo(value - stepBase, step);
       Decimal valueHigh = value + step - NS_floorModulo(value - stepBase, step);
 
       Decimal maximum = GetMaximum();
 
       if (maximum.isNaN() || valueHigh <= maximum) {
--- a/dom/html/HTMLTrackElement.cpp
+++ b/dom/html/HTMLTrackElement.cpp
@@ -71,16 +71,17 @@ static constexpr nsAttrValue::EnumTable 
 
 // Invalid values are treated as "metadata" in ParseAttribute, but if no value
 // at all is specified, it's treated as "subtitles" in GetKind
 static constexpr const nsAttrValue::EnumTable* kKindTableInvalidValueDefault = &kKindTable[4];
 
 /** HTMLTrackElement */
 HTMLTrackElement::HTMLTrackElement(already_AddRefed<mozilla::dom::NodeInfo>& aNodeInfo)
   : nsGenericHTMLElement(aNodeInfo)
+  , mLoadResourceDispatched(false)
 {
 }
 
 HTMLTrackElement::~HTMLTrackElement()
 {
 }
 
 NS_IMPL_ELEMENT_CLONE(HTMLTrackElement)
@@ -178,18 +179,55 @@ HTMLTrackElement::ParseAttribute(int32_t
   // Otherwise call the generic implementation.
   return nsGenericHTMLElement::ParseAttribute(aNamespaceID,
                                               aAttribute,
                                               aValue,
                                               aResult);
 }
 
 void
+HTMLTrackElement::SetSrc(const nsAString& aSrc, ErrorResult& aError)
+{
+  SetHTMLAttr(nsGkAtoms::src, aSrc, aError);
+  uint16_t oldReadyState = ReadyState();
+  SetReadyState(TextTrackReadyState::NotLoaded);
+  if (!mMediaParent) {
+    return;
+  }
+  if (mTrack && (oldReadyState != TextTrackReadyState::NotLoaded)) {
+    // Remove all the cues in MediaElement.
+    mMediaParent->RemoveTextTrack(mTrack);
+    // Recreate mTrack.
+    CreateTextTrack();
+  }
+  // Stop WebVTTListener.
+  mListener = nullptr;
+  if (mChannel) {
+    mChannel->Cancel(NS_BINDING_ABORTED);
+    mChannel = nullptr;
+  }
+
+  DispatchLoadResource();
+}
+
+void
+HTMLTrackElement::DispatchLoadResource()
+{
+  if (!mLoadResourceDispatched) {
+    RefPtr<Runnable> r = NewRunnableMethod(this, &HTMLTrackElement::LoadResource);
+    nsContentUtils::RunInStableState(r.forget());
+    mLoadResourceDispatched = true;
+  }
+}
+
+void
 HTMLTrackElement::LoadResource()
 {
+  mLoadResourceDispatched = false;
+
   // Find our 'src' url
   nsAutoString src;
   if (!GetAttr(kNameSpaceID_None, nsGkAtoms::src, src)) {
     return;
   }
 
   nsCOMPtr<nsIURI> uri;
   nsresult rv = NewURIFromString(src, getter_AddRefs(uri));
@@ -253,18 +291,17 @@ HTMLTrackElement::BindToTree(nsIDocument
     LOG(LogLevel::Debug, ("Track element sent notification to parent."));
 
     // We may already have a TextTrack at this point if GetTrack() has already
     // been called. This happens, for instance, if script tries to get the
     // TextTrack before its mTrackElement has been bound to the DOM tree.
     if (!mTrack) {
       CreateTextTrack();
     }
-    RefPtr<Runnable> r = NewRunnableMethod(this, &HTMLTrackElement::LoadResource);
-    nsContentUtils::RunInStableState(r.forget());
+    DispatchLoadResource();
   }
 
   return NS_OK;
 }
 
 void
 HTMLTrackElement::UnbindFromTree(bool aDeep, bool aNullParent)
 {
--- a/dom/html/HTMLTrackElement.h
+++ b/dom/html/HTMLTrackElement.h
@@ -41,20 +41,18 @@ public:
   {
     SetHTMLAttr(nsGkAtoms::kind, aKind, aError);
   }
 
   void GetSrc(DOMString& aSrc) const
   {
     GetHTMLURIAttr(nsGkAtoms::src, aSrc);
   }
-  void SetSrc(const nsAString& aSrc, ErrorResult& aError)
-  {
-    SetHTMLAttr(nsGkAtoms::src, aSrc, aError);
-  }
+
+  void SetSrc(const nsAString& aSrc, ErrorResult& aError);
 
   void GetSrclang(DOMString& aSrclang) const
   {
     GetHTMLAttr(nsGkAtoms::srclang, aSrclang);
   }
   void GetSrclang(nsAString& aSrclang) const
   {
     GetHTMLAttr(nsGkAtoms::srclang, aSrclang);
@@ -129,14 +127,18 @@ protected:
   friend class WebVTTListener;
 
   RefPtr<TextTrack> mTrack;
   nsCOMPtr<nsIChannel> mChannel;
   RefPtr<HTMLMediaElement> mMediaParent;
   RefPtr<WebVTTListener> mListener;
 
   void CreateTextTrack();
+
+private:
+  void DispatchLoadResource();
+  bool mLoadResourceDispatched;
 };
 
 } // namespace dom
 } // namespace mozilla
 
 #endif // mozilla_dom_HTMLTrackElement_h
--- a/dom/html/test/forms/test_step_attribute.html
+++ b/dom/html/test/forms/test_step_attribute.html
@@ -180,17 +180,17 @@ for (var test of data) {
 
       input.min = '2009-02-27';
       input.value = '2009-02-28';
       checkValidity(input, false, apply, { low: "2009-02-27", high: "2009-03-01" });
 
       input.min = '2009-02-01';
       input.step = '1.1';
       input.value = '2009-02-02';
-      checkValidity(input, false, apply, { low: "2009-02-01", high: "2009-02-12" });
+      checkValidity(input, true, apply);
 
       // Without any step attribute the date is valid
       input.removeAttribute('step');
       checkValidity(input, true, apply);
 
       input.min = '1950-01-01';
       input.step = '366';
       input.value = '1951-01-01';
@@ -198,31 +198,31 @@ for (var test of data) {
 
       input.min = '1951-01-01';
       input.step = '365';
       input.value = '1952-01-01';
       checkValidity(input, true, apply);
 
       input.step = '0.9';
       input.value = '1951-01-02';
-      checkValidity(input, false, apply, { low: "1951-01-01", high: "1951-01-10" });
-
-      input.value = '1951-01-10'
+      is(input.step, '0.9', "check that step value is unchanged");
       checkValidity(input, true, apply);
 
-      input.step = '0.5';
+      input.step = '0.4';
       input.value = '1951-01-02';
+      is(input.step, '0.4', "check that step value is unchanged");
       checkValidity(input, true, apply);
 
       input.step = '1.5';
-      input.value = '1951-01-03';
-      checkValidity(input, false, apply, { low: "1951-01-01", high: "1951-01-04" });
+      input.value = '1951-01-02';
+      is(input.step, '1.5', "check that step value is unchanged");
+      checkValidity(input, false, apply, { low: "1951-01-01", high: "1951-01-03" });
 
       input.value = '1951-01-08';
-      checkValidity(input, false, apply, { low: "1951-01-07", high: "1951-01-10" });
+      checkValidity(input, false, apply, { low: "1951-01-07", high: "1951-01-09" });
 
       input.step = '3000';
       input.min= '1968-01-01';
       input.value = '1968-05-12';
       checkValidity(input, false, apply, { low: "1968-01-01", high: "1976-03-19" });
 
       input.value = '1971-01-01';
       checkValidity(input, false, apply, { low: "1968-01-01", high: "1976-03-19" });
@@ -231,36 +231,36 @@ for (var test of data) {
       checkValidity(input, false, apply, { low: "1984-06-05", high: "1992-08-22" });
 
       input.value = '1984-06-05';
       checkValidity(input, true, apply);
 
       input.value = '1992-08-22';
       checkValidity(input, true, apply);
 
-      input.step = '1.1';
+      input.step = '2.1';
       input.min = '1991-01-01';
       input.value = '1991-01-01';
       checkValidity(input, true, apply);
 
       input.value = '1991-01-02';
-      checkValidity(input, false, apply, { low: "1991-01-01", high: "1991-01-12" });
+      checkValidity(input, false, apply, { low: "1991-01-01", high: "1991-01-03" });
 
-      input.value = '1991-01-12';
+      input.value = '1991-01-03';
       checkValidity(input, true, apply);
 
-      input.step = '1.1';
+      input.step = '2.1';
       input.min = '1969-12-20';
       input.value = '1969-12-20';
       checkValidity(input, true, apply);
 
       input.value = '1969-12-21';
-      checkValidity(input, false, apply, { low: "1969-12-20", high: "1969-12-31" });
+      checkValidity(input, false, apply, { low: "1969-12-20", high: "1969-12-22" });
 
-      input.value = '1969-12-31';
+      input.value = '1969-12-22';
       checkValidity(input, true, apply);
 
       break;
     case 'number':
       // When step=0, the allowed step is 1.
       input.step = '0';
       input.value = '1.2';
       checkValidity(input, false, apply, { low: 1, high: 2 });
--- a/dom/html/test/forms/test_stepup_stepdown.html
+++ b/dom/html/test/forms/test_stepup_stepdown.html
@@ -266,22 +266,22 @@ function checkStepDown()
     [ '2012-02-29',  null,  null,  null,  -1,   '2012-03-01',   false ],
     // Float values are rounded to integer (1.1 -> 1).
     [ '2012-01-02',  null,  null,  null,  1.1,  '2012-01-01',   false ],
     [ '2012-01-02',  null,  null,  null,  1.9,  '2012-01-01',   false ],
     // With step values.
     [ '2012-01-03',  '0.5', null,  null,  null, '2012-01-02',   false ],
     [ '2012-01-02',  '0.5', null,  null,  null, '2012-01-01',   false ],
     [ '2012-01-01',  '2',   null,  null,  null, '2011-12-30',   false ],
-    [ '2012-01-02',  '0.25',null,  null,  4,    '2012-01-01',   false ],
-    [ '2012-01-15',  '1.1',  '2012-01-01', null,  1,    '2012-01-12',   false ],
-    [ '2012-01-12',  '1.1',  '2012-01-01', null,  2,    '2012-01-01',   false ],
-    [ '2012-01-23',  '1.1',  '2012-01-01', null,  10,   '2012-01-12',   false ],
-    [ '2012-01-23',  '1.1',  '2012-01-01', null,  11,   '2012-01-01',   false ],
-    [ '1968-01-12',  '1.1',  '1968-01-01', null,  8,    '1968-01-01',   false ],
+    [ '2012-01-02',  '0.25',null,  null,  4,    '2011-12-29',   false ],
+    [ '2012-01-15',  '1.1',  '2012-01-01', null,  1,    '2012-01-14',   false ],
+    [ '2012-01-12',  '1.1',  '2012-01-01', null,  2,    '2012-01-10',   false ],
+    [ '2012-01-23',  '1.1',  '2012-01-01', null,  10,   '2012-01-13',   false ],
+    [ '2012-01-23',  '1.1',  '2012-01-01', null,  11,   '2012-01-12',   false ],
+    [ '1968-01-12',  '1.1',  '1968-01-01', null,  8,    '1968-01-04',   false ],
     // step = 0 isn't allowed (-> step = 1).
     [ '2012-01-02',  '0',   null,  null,  null, '2012-01-01',   false ],
     // step < 0 isn't allowed (-> step = 1).
     [ '2012-01-02',  '-1',  null,  null,  null, '2012-01-01',   false ],
     // step = NaN isn't allowed (-> step = 1).
     [ '2012-01-02',  'foo', null,  null,  null, '2012-01-01',   false ],
     // Min values testing.
     [ '2012-01-03',  '1',    'foo',        null,  2,     '2012-01-01',  false ],
@@ -583,24 +583,23 @@ function checkStepUp()
     [ '1968-12-29',  null,  null,  null,  4,    '1969-01-02',   false ],
     [ '1970-01-01',  null,  null,  null,  -365, '1969-01-01',   false ],
     [ '2012-03-01',  null,  null,  null,  -1,   '2012-02-29',   false ],
     // Float values are rounded to integer (1.1 -> 1).
     [ '2012-01-01',  null,  null,  null,  1.1,  '2012-01-02',   false ],
     [ '2012-01-01',  null,  null,  null,  1.9,  '2012-01-02',   false ],
     // With step values.
     [ '2012-01-01',  '0.5',  null,         null,  null, '2012-01-02',   false ],
-    [ '2012-01-01',  '0.5',  null,         null,  null, '2012-01-02',   false ],
     [ '2012-01-01',  '2',    null,         null,  null, '2012-01-03',   false ],
-    [ '2012-01-01',  '0.25', null,         null,  4,    '2012-01-02',   false ],
-    [ '2012-01-01',  '1.1',  '2012-01-01', null,  1,    '2012-01-12',   false ],
-    [ '2012-01-01',  '1.1',  '2012-01-01', null,  2,    '2012-01-12',   false ],
-    [ '2012-01-01',  '1.1',  '2012-01-01', null,  10,   '2012-01-12',   false ],
-    [ '2012-01-01',  '1.1',  '2012-01-01', null,  11,   '2012-01-23',   false ],
-    [ '1968-01-01',  '1.1',  '1968-01-01', null,  8,    '1968-01-12',   false ],
+    [ '2012-01-01',  '0.25', null,         null,  4,    '2012-01-05',   false ],
+    [ '2012-01-01',  '1.1',  '2012-01-01', null,  1,    '2012-01-02',   false ],
+    [ '2012-01-01',  '1.1',  '2012-01-01', null,  2,    '2012-01-03',   false ],
+    [ '2012-01-01',  '1.1',  '2012-01-01', null,  10,   '2012-01-11',   false ],
+    [ '2012-01-01',  '1.1',  '2012-01-01', null,  11,   '2012-01-12',   false ],
+    [ '1968-01-01',  '1.1',  '1968-01-01', null,  8,    '1968-01-09',   false ],
     // step = 0 isn't allowed (-> step = 1).
     [ '2012-01-01',  '0',   null,  null,  null, '2012-01-02',   false ],
     // step < 0 isn't allowed (-> step = 1).
     [ '2012-01-01',  '-1',  null,  null,  null, '2012-01-02',   false ],
     // step = NaN isn't allowed (-> step = 1).
     [ '2012-01-01',  'foo', null,  null,  null, '2012-01-02',   false ],
     // Min values testing.
     [ '2012-01-01',  '1',   'foo',         null,  null,  '2012-01-02',  false ],
--- a/dom/media/test/mochitest.ini
+++ b/dom/media/test/mochitest.ini
@@ -851,16 +851,18 @@ tags = webvtt
 tags = webvtt
 [test_texttrackregion.html]
 tags = webvtt
 [test_texttrack_moz.html]
 tags = webvtt
 [test_timeupdate_small_files.html]
 [test_trackelementevent.html]
 tags = webvtt
+[test_trackelementsrc.html]
+tags = webvtt
 [test_trackevent.html]
 tags = webvtt
 [test_unseekable.html]
 skip-if = toolkit == 'gonk' # bug 1128845 on gonk
 [test_video_to_canvas.html]
 [test_video_in_audio_element.html]
 [test_videoDocumentTitle.html]
 [test_VideoPlaybackQuality.html]
new file mode 100644
--- /dev/null
+++ b/dom/media/test/test_trackelementsrc.html
@@ -0,0 +1,55 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+  <meta charset="utf-8">
+  <title>Test for Bug 1281418 - Change the src attribue for TrackElement.</title>
+  <script type="text/javascript" src="/MochiKit/MochiKit.js"></script>
+  <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<p id="display"></p>
+<div id="content" style="display: none">
+</div>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+SimpleTest.waitForExplicitFinish();
+
+SpecialPowers.pushPrefEnv({"set": [["media.webvtt.enabled", true],
+  ["media.webvtt.regions.enabled", true]]}, function() {
+
+var video = document.createElement("video");
+video.src = "seek.webm";
+video.preload = "metadata";
+var trackElement = document.createElement("track");
+trackElement.src = "basic.vtt";
+trackElement.default = true;
+
+document.getElementById("content").appendChild(video);
+video.appendChild(trackElement);
+
+video.addEventListener("loadedmetadata", function metadata() {
+  if (trackElement.readyState <= 1) {
+    return setTimeout(metadata, 0);
+  }
+  is(video.textTracks.length, 1, "Length should be 1.");
+  is(video.textTracks[0].cues.length, 6, "Cue length should be 6.");
+
+  trackElement.src = "sequential.vtt";
+  trackElement.track.mode = "showing";
+  video.play();
+});
+
+video.addEventListener("ended", function end() {
+  is(trackElement.readyState, 2, "readyState should be 2.")
+  is(video.textTracks.length, 1, "Length should be 1.");
+  is(video.textTracks[0].cues.length, 3, "Cue length should be 3.");
+  SimpleTest.finish();
+});
+
+});
+</script>
+</pre>
+</body>
+</html>
+
--- a/dom/workers/test/serviceworkers/download/worker.js
+++ b/dom/workers/test/serviceworkers/download/worker.js
@@ -16,13 +16,15 @@ addEventListener('fetch', function(evt) 
   if (evt.request.url.indexOf('fake_download') === -1) {
     return;
   }
 
   // We should only get a single download fetch event. Automatically unregister.
   evt.respondWith(registration.unregister().then(function() {
     return new Response('service worker generated download', {
       headers: {
-        'Content-Disposition': 'attachment; filename="fake_download.bin"'
+        'Content-Disposition': 'attachment; filename="fake_download.bin"',
+        // fake encoding header that should have no effect
+        'Content-Encoding': 'gzip',
       }
     });
   }));
 });
--- a/image/decoders/nsPNGDecoder.cpp
+++ b/image/decoders/nsPNGDecoder.cpp
@@ -29,19 +29,22 @@ using namespace mozilla::gfx;
 using std::min;
 
 namespace mozilla {
 namespace image {
 
 static LazyLogModule sPNGLog("PNGDecoder");
 static LazyLogModule sPNGDecoderAccountingLog("PNGDecoderAccounting");
 
-// Limit image dimensions (bug #251381, #591822, and #967656)
-#ifndef MOZ_PNG_MAX_DIMENSION
-#  define MOZ_PNG_MAX_DIMENSION 32767
+// limit image dimensions (bug #251381, #591822, #967656, and #1283961)
+#ifndef MOZ_PNG_MAX_WIDTH
+#  define MOZ_PNG_MAX_WIDTH 0x7fffffff // Unlimited
+#endif
+#ifndef MOZ_PNG_MAX_HEIGHT
+#  define MOZ_PNG_MAX_HEIGHT 0x7fffffff // Unlimited
 #endif
 
 nsPNGDecoder::AnimFrameInfo::AnimFrameInfo()
  : mDispose(DisposalMethod::KEEP)
  , mBlend(BlendMethod::OVER)
  , mTimeout(0)
 { }
 
@@ -318,16 +321,17 @@ nsPNGDecoder::InitInternal()
     png_set_keep_unknown_chunks(mPNG, 1, color_chunks, 2);
   }
 
   png_set_keep_unknown_chunks(mPNG, 1, unused_chunks,
                               (int)sizeof(unused_chunks)/5);
 #endif
 
 #ifdef PNG_SET_USER_LIMITS_SUPPORTED
+  png_set_user_limits(mPNG, MOZ_PNG_MAX_WIDTH, MOZ_PNG_MAX_HEIGHT);
   if (mCMSMode != eCMSMode_Off) {
     png_set_chunk_malloc_max(mPNG, 4000000L);
   }
 #endif
 
 #ifdef PNG_READ_CHECK_FOR_INVALID_INDEX_SUPPORTED
   // Disallow palette-index checking, for speed; we would ignore the warning
   // anyhow.  This feature was added at libpng version 1.5.10 and is disabled
@@ -552,21 +556,16 @@ nsPNGDecoder::info_callback(png_structp 
 
   nsPNGDecoder* decoder =
                static_cast<nsPNGDecoder*>(png_get_progressive_ptr(png_ptr));
 
   // Always decode to 24-bit RGB or 32-bit RGBA
   png_get_IHDR(png_ptr, info_ptr, &width, &height, &bit_depth, &color_type,
                &interlace_type, &compression_type, &filter_type);
 
-  // Are we too big?
-  if (width > MOZ_PNG_MAX_DIMENSION || height > MOZ_PNG_MAX_DIMENSION) {
-    png_longjmp(decoder->mPNG, 1);
-  }
-
   const IntRect frameRect(0, 0, width, height);
 
   // Post our size to the superclass
   decoder->PostSize(frameRect.width, frameRect.height);
   if (decoder->HasError()) {
     // Setting the size led to an error.
     png_longjmp(decoder->mPNG, 1);
   }
--- a/js/src/builtin/Array.js
+++ b/js/src/builtin/Array.js
@@ -985,17 +985,17 @@ function ArrayConcat(arg1) {
         if (IsConcatSpreadable(E)) {
             // Step 5.c.ii.
             len = ToLength(E.length);
 
             // Step 5.c.iii.
             if (n + len > MAX_NUMERIC_INDEX)
                 ThrowTypeError(JSMSG_TOO_LONG_ARRAY);
 
-            if (IsPackedArray(E)) {
+            if (IsPackedArray(A) && IsPackedArray(E)) {
                 // Step 5.c.i, 5.c.iv, and 5.c.iv.5.
                 for (k = 0; k < len; k++) {
                     // Steps 5.c.iv.1-3.
                     // IsPackedArray(E) ensures that |k in E| is always true.
                     _DefineDataProperty(A, n, E[k]);
 
                     // Step 5.c.iv.4.
                     n++;
--- a/js/src/builtin/Intl.cpp
+++ b/js/src/builtin/Intl.cpp
@@ -1787,18 +1787,21 @@ InitDateTimeFormatClass(JSContext* cx, H
     {
         return nullptr;
     }
 
     // If the still-experimental DateTimeFormat.prototype.formatToParts method
     // is enabled, also add it.
     if (cx->compartment()->creationOptions().experimentalDateTimeFormatFormatToPartsEnabled()) {
         RootedValue ftp(cx);
-        if (!GlobalObject::getIntrinsicValue(cx, cx->global(),
-                                             cx->names().DateTimeFormatFormatToParts, &ftp))
+        HandlePropertyName name = cx->names().formatToParts;
+        if (!GlobalObject::getSelfHostedFunction(cx, cx->global(),
+                    cx->names().DateTimeFormatFormatToParts,
+                    name,
+                    0, &ftp))
         {
             return nullptr;
         }
 
         if (!DefineProperty(cx, proto, cx->names().formatToParts, ftp, nullptr, nullptr, 0))
             return nullptr;
     }
 
--- a/js/src/builtin/RegExp.cpp
+++ b/js/src/builtin/RegExp.cpp
@@ -1554,16 +1554,25 @@ js::RegExpPrototypeOptimizableRaw(JSCont
     if (!GetOwnNativeGetterPure(cx, proto, NameToId(cx->names().sticky), &stickyGetter))
         return false;
 
     if (stickyGetter != regexp_sticky) {
         *result = false;
         return true;
     }
 
+    JSNative unicodeGetter;
+    if (!GetOwnNativeGetterPure(cx, proto, NameToId(cx->names().unicode), &unicodeGetter))
+        return false;
+
+    if (unicodeGetter != regexp_unicode) {
+        *result = false;
+        return true;
+    }
+
     // Check if @@match, @@search, and exec are own data properties,
     // those values should be tested in selfhosted JS.
     bool has = false;
     if (!HasOwnDataPropertyPure(cx, proto, SYMBOL_TO_JSID(cx->wellKnownSymbols().match), &has))
         return false;
     if (!has) {
         *result = false;
         return true;
--- a/js/src/builtin/RegExp.js
+++ b/js/src/builtin/RegExp.js
@@ -574,16 +574,17 @@ function IsRegExpSplitOptimizable(rx, C)
     var RegExpCtor = GetBuiltinConstructor("RegExp");
     if (C !== RegExpCtor)
         return false;
 
     var RegExpProto = RegExpCtor.prototype;
     // If RegExpPrototypeOptimizable succeeds, `RegExpProto.exec` is guaranteed
     // to be a data property.
     return RegExpPrototypeOptimizable(RegExpProto) &&
+           RegExpInstanceOptimizable(rx, RegExpProto) &&
            RegExpProto.exec === RegExp_prototype_Exec;
 }
 
 // ES 2016 draft Mar 25, 2016 21.2.5.11.
 function RegExpSplit(string, limit) {
     // Step 1.
     var rx = this;
 
@@ -598,17 +599,18 @@ function RegExpSplit(string, limit) {
     var C = SpeciesConstructor(rx, GetBuiltinConstructor("RegExp"));
 
     // Step 5.
     var flags = ToString(rx.flags);
 
     // Steps 6-7.
     var unicodeMatching = callFunction(std_String_includes, flags, "u");
 
-    var optimizable = IsRegExpSplitOptimizable(rx, C);
+    var optimizable = IsRegExpSplitOptimizable(rx, C) &&
+                      (limit === undefined || typeof limit == "number");
     var splitter;
     if (optimizable) {
         // Steps 8-9 (skipped).
 
         // Step 10.
         // If split operation is optimizable, perform non-sticky match.
         splitter = regexp_construct_no_sticky(rx, flags);
     } else {
--- a/js/src/builtin/TestingFunctions.cpp
+++ b/js/src/builtin/TestingFunctions.cpp
@@ -859,33 +859,17 @@ GCState(JSContext* cx, unsigned argc, Va
     CallArgs args = CallArgsFromVp(argc, vp);
 
     if (args.length() != 0) {
         RootedObject callee(cx, &args.callee());
         ReportUsageError(cx, callee, "Too many arguments");
         return false;
     }
 
-    const char* state;
-    gc::State globalState = cx->runtime()->gc.state();
-    if (globalState == gc::NO_INCREMENTAL)
-        state = "none";
-    else if (globalState == gc::MARK)
-        state = "mark";
-    else if (globalState == gc::SWEEP)
-        state = "sweep";
-    else if (globalState == gc::FINALIZE)
-        state = "finalize";
-    else if (globalState == gc::COMPACT)
-        state = "compact";
-    else if (globalState == gc::DECOMMIT)
-        state = "decommit";
-    else
-        MOZ_CRASH("Unobserveable global GC state");
-
+    const char* state = StateName(cx->runtime()->gc.state());
     JSString* str = JS_NewStringCopyZ(cx, state);
     if (!str)
         return false;
     args.rval().setString(str);
     return true;
 }
 
 static bool
--- a/js/src/gc/GCRuntime.h
+++ b/js/src/gc/GCRuntime.h
@@ -684,18 +684,18 @@ class GCRuntime
         MOZ_ASSERT(nextCellUniqueId_ > 0);
         uint64_t uid = ++nextCellUniqueId_;
         return uid;
     }
 
   public:
     // Internal public interface
     State state() const { return incrementalState; }
-    bool isHeapCompacting() const { return state() == COMPACT; }
-    bool isForegroundSweeping() const { return state() == SWEEP; }
+    bool isHeapCompacting() const { return state() == State::Compact; }
+    bool isForegroundSweeping() const { return state() == State::Sweep; }
     bool isBackgroundSweeping() { return helperState.isBackgroundSweeping(); }
     void waitBackgroundSweepEnd() { helperState.waitBackgroundSweepEnd(); }
     void waitBackgroundSweepOrAllocEnd() {
         helperState.waitBackgroundSweepEnd();
         allocTask.cancel(GCParallelTask::CancelAndWait);
     }
 
     void requestMinorGC(JS::gcreason::Reason reason);
@@ -761,17 +761,17 @@ class GCRuntime
 #endif // DEBUG
 
     void setAlwaysPreserveCode() { alwaysPreserveCode = true; }
 
     bool isIncrementalGCAllowed() const { return incrementalAllowed; }
     void disallowIncrementalGC() { incrementalAllowed = false; }
 
     bool isIncrementalGCEnabled() const { return mode == JSGC_MODE_INCREMENTAL && incrementalAllowed; }
-    bool isIncrementalGCInProgress() const { return state() != NO_INCREMENTAL; }
+    bool isIncrementalGCInProgress() const { return state() != State::NotActive; }
 
     bool isGenerationalGCEnabled() const { return generationalDisabled == 0; }
     void disableGenerationalGC();
     void enableGenerationalGC();
 
     void disableCompactingGC();
     void enableCompactingGC();
     bool isCompactingGCEnabled() const;
--- a/js/src/gc/Marking.cpp
+++ b/js/src/gc/Marking.cpp
@@ -341,18 +341,18 @@ AssertZoneIsMarking(JS::Symbol* sym)
     MOZ_ASSERT(zone->isGCMarking() || zone->isAtomsZone());
 #endif
 }
 
 static void
 AssertRootMarkingPhase(JSTracer* trc)
 {
     MOZ_ASSERT_IF(trc->isMarkingTracer(),
-                  trc->runtime()->gc.state() == NO_INCREMENTAL ||
-                  trc->runtime()->gc.state() == MARK_ROOTS);
+                  trc->runtime()->gc.state() == State::NotActive ||
+                  trc->runtime()->gc.state() == State::MarkRoots);
 }
 
 
 /*** Tracing Interface ***************************************************************************/
 
 // The second parameter to BaseGCType is derived automatically based on T. The
 // relation here is that for any T, the TraceKind will automatically,
 // statically select the correct Cell layout for marking. Below, we instantiate
@@ -1940,17 +1940,17 @@ GCMarker::markDelayedChildren(Arena* are
      * allocatedDuringIncremental flag if we continue marking.
      */
 }
 
 bool
 GCMarker::markDelayedChildren(SliceBudget& budget)
 {
     GCRuntime& gc = runtime()->gc;
-    gcstats::AutoPhase ap(gc.stats, gc.state() == MARK, gcstats::PHASE_MARK_DELAYED);
+    gcstats::AutoPhase ap(gc.stats, gc.state() == State::Mark, gcstats::PHASE_MARK_DELAYED);
 
     MOZ_ASSERT(unmarkedArenaStackTop);
     do {
         /*
          * If marking gets delayed at the same arena again, we must repeat
          * marking of its things. For that we pop arena from the stack and
          * clear its hasDelayedMarking flag before we begin the marking.
          */
@@ -2434,17 +2434,17 @@ CheckIsMarkedThing(T* thingp)
 #undef IS_SAME_TYPE_OR
 
 #ifdef DEBUG
     MOZ_ASSERT(thingp);
     MOZ_ASSERT(*thingp);
     JSRuntime* rt = (*thingp)->runtimeFromAnyThread();
     MOZ_ASSERT_IF(!ThingIsPermanentAtomOrWellKnownSymbol(*thingp),
                   CurrentThreadCanAccessRuntime(rt) ||
-                  (rt->isHeapCollecting() && rt->gc.state() == SWEEP));
+                  (rt->isHeapCollecting() && rt->gc.state() == State::Sweep));
 #endif
 }
 
 template <typename T>
 static bool
 IsMarkedInternalCommon(T* thingp)
 {
     CheckIsMarkedThing(thingp);
--- a/js/src/gc/Statistics.h
+++ b/js/src/gc/Statistics.h
@@ -248,17 +248,17 @@ struct Statistics
 
     static const size_t MAX_NESTING = 20;
 
     struct SliceData {
         SliceData(SliceBudget budget, JS::gcreason::Reason reason, int64_t start,
                   double startTimestamp, size_t startFaults, gc::State initialState)
           : budget(budget), reason(reason),
             initialState(initialState),
-            finalState(gc::NO_INCREMENTAL),
+            finalState(gc::State::NotActive),
             resetReason(nullptr),
             start(start), startTimestamp(startTimestamp),
             startFaults(startFaults)
         {
             for (auto i : mozilla::MakeRange(NumTimingArrays))
                 mozilla::PodArrayZero(phaseTimes[i]);
         }
 
--- a/js/src/gc/Verifier.cpp
+++ b/js/src/gc/Verifier.cpp
@@ -198,17 +198,17 @@ gc::GCRuntime::startVerifyPreBarriers()
     trc->term = trc->edgeptr + size;
 
     if (!trc->nodemap.init())
         goto oom;
 
     /* Create the root node. */
     trc->curnode = MakeNode(trc, nullptr, JS::TraceKind(0));
 
-    incrementalState = MARK_ROOTS;
+    incrementalState = State::MarkRoots;
 
     /* Make all the roots be edges emanating from the root node. */
     markRuntime(trc, TraceRuntime, prep.session().lock);
 
     VerifyNode* node;
     node = trc->curnode;
     if (trc->edgeptr == trc->term)
         goto oom;
@@ -225,31 +225,31 @@ gc::GCRuntime::startVerifyPreBarriers()
             if (trc->edgeptr == trc->term)
                 goto oom;
         }
 
         node = NextNode(node);
     }
 
     verifyPreData = trc;
-    incrementalState = MARK;
+    incrementalState = State::Mark;
     marker.start();
 
     for (ZonesIter zone(rt, WithAtoms); !zone.done(); zone.next()) {
         PurgeJITCaches(zone);
         if (!zone->usedByExclusiveThread) {
             zone->setNeedsIncrementalBarrier(true, Zone::UpdateJit);
             zone->arenas.purge();
         }
     }
 
     return;
 
 oom:
-    incrementalState = NO_INCREMENTAL;
+    incrementalState = State::NotActive;
     js_delete(trc);
     verifyPreData = nullptr;
 }
 
 static bool
 IsMarkedOrAllocated(TenuredCell* cell)
 {
     return cell->isMarked() || cell->arena()->allocatedDuringIncremental;
@@ -337,17 +337,17 @@ gc::GCRuntime::endVerifyPreBarriers()
     /*
      * We need to bump gcNumber so that the methodjit knows that jitcode has
      * been discarded.
      */
     MOZ_ASSERT(trc->number == number);
     number++;
 
     verifyPreData = nullptr;
-    incrementalState = NO_INCREMENTAL;
+    incrementalState = State::NotActive;
 
     if (!compartmentCreated && IsIncrementalGCSafe(rt)) {
         CheckEdgeTracer cetrc(rt);
 
         /* Start after the roots. */
         VerifyNode* node = NextNode(trc->root);
         while ((char*)node < trc->edgeptr) {
             cetrc.node = node;
--- a/js/src/irregexp/RegExpEngine.cpp
+++ b/js/src/irregexp/RegExpEngine.cpp
@@ -281,23 +281,16 @@ GetCaseIndependentLetters(char16_t chara
                           const char16_t* choices,
                           size_t choices_length,
                           char16_t* letters)
 {
     size_t count = 0;
     for (size_t i = 0; i < choices_length; i++) {
         char16_t c = choices[i];
 
-        // The standard requires that non-ASCII characters cannot have ASCII
-        // character codes in their equivalence class, even though this
-        // situation occurs multiple times in the unicode tables.
-        static const unsigned kMaxAsciiCharCode = 127;
-        if (!unicode && character > kMaxAsciiCharCode && c <= kMaxAsciiCharCode)
-            continue;
-
         // Skip characters that can't appear in one byte strings.
         if (!unicode && ascii_subject && c > kMaxOneByteCharCode)
             continue;
 
         // Watch for duplicates.
         bool found = false;
         for (size_t j = 0; j < count; j++) {
             if (letters[j] == c) {
@@ -327,20 +320,50 @@ GetCaseIndependentLetters(char16_t chara
             unicode::ReverseFoldCase1(character),
             unicode::ReverseFoldCase2(character),
             unicode::ReverseFoldCase3(character),
         };
         return GetCaseIndependentLetters(character, ascii_subject, unicode,
                                          choices, ArrayLength(choices), letters);
     }
 
+    char16_t upper = unicode::ToUpperCase(character);
+    unicode::CodepointsWithSameUpperCase others(character);
+    char16_t other1 = others.other1();
+    char16_t other2 = others.other2();
+    char16_t other3 = others.other3();
+
+    // ES 2017 draft 996af87b7072b3c3dd2b1def856c66f456102215 21.2.4.2
+    // step 3.g.
+    // The standard requires that non-ASCII characters cannot have ASCII
+    // character codes in their equivalence class, even though this
+    // situation occurs multiple times in the unicode tables.
+    static const unsigned kMaxAsciiCharCode = 127;
+    if (upper <= kMaxAsciiCharCode) {
+        if (character > kMaxAsciiCharCode) {
+            // If Canonicalize(character) == character, all other characters
+            // should be ignored.
+            return GetCaseIndependentLetters(character, ascii_subject, unicode,
+                                             &character, 1, letters);
+        }
+
+        if (other1 > kMaxAsciiCharCode)
+            other1 = character;
+        if (other2 > kMaxAsciiCharCode)
+            other2 = character;
+        if (other3 > kMaxAsciiCharCode)
+            other3 = character;
+    }
+
     const char16_t choices[] = {
         character,
-        unicode::ToLowerCase(character),
-        unicode::ToUpperCase(character)
+        upper,
+        other1,
+        other2,
+        other3
     };
     return GetCaseIndependentLetters(character, ascii_subject, unicode,
                                      choices, ArrayLength(choices), letters);
 }
 
 static char16_t
 ConvertNonLatin1ToLatin1(char16_t c, bool unicode)
 {
--- a/js/src/jit-test/tests/gc/bug-1138390.js
+++ b/js/src/jit-test/tests/gc/bug-1138390.js
@@ -12,17 +12,17 @@ gczeal(0);
 gc();
 
 // Start an incremental GC that includes the atoms zone
 startgc(0);
 var g = newGlobal();
 
 // Start an off thread compilation that will not run until GC has finished
 if ("gcstate" in this)
-   assertEq(gcstate(), "mark");
+   assertEq(gcstate(), "Mark");
 g.offThreadCompileScript('23;', {});
 
 // Wait for the compilation to finish, which must finish the GC first
 assertEq(23, g.runOffThreadScript());
 if ("gcstate" in this)
-   assertEq(gcstate(), "none");
+   assertEq(gcstate(), "NotActive");
 
 print("done");
--- a/js/src/jit-test/tests/gc/incremental-abort.js
+++ b/js/src/jit-test/tests/gc/incremental-abort.js
@@ -22,31 +22,33 @@ function testAbort(zoneCount, objectCoun
                 "}",
                  { global: zone });
         zone.makeObjectGraph(objectCount);
         zones.push(zone);
     }
 
     var didAbort = false;
     startgc(sliceCount, "shrinking");
-    while (gcstate() !== "none") {
+    while (gcstate() !== "NotActive") {
         var state = gcstate();
         if (state == abortState) {
             abortgc();
             didAbort = true;
             break;
         }
 
         gcslice(sliceCount);
     }
 
-    assertEq(gcstate(), "none");
+    assertEq(gcstate(), "NotActive");
     if (abortState)
         assertEq(didAbort, true);
 
     return zones;
 }
 
 gczeal(0);
 testAbort(10, 10000, 10000);
-testAbort(10, 10000, 10000, "mark");
-testAbort(10, 10000, 10000, "sweep");
-testAbort(10, 10000, 10000, "compact");
+testAbort(10, 10000, 10000, "Mark");
+testAbort(10, 10000, 10000, "Sweep");
+testAbort(10, 10000, 10000, "Compact");
+// Note: we do not yield automatically before Finalize or Decommit, as they yield internally.
+// Thus, we may not witness an incremental state in this phase and cannot test it explicitly.
--- a/js/src/jit-test/tests/gc/incremental-compacting.js
+++ b/js/src/jit-test/tests/gc/incremental-compacting.js
@@ -22,21 +22,21 @@ function testCompacting(zoneCount, objec
                  "        objects.push({ serial: i }); " +
                 "}",
                  { global: zone });
         zone.makeObjectGraph(objectCount);
         zones.push(zone);
     }
 
     // Finish any alloc-triggered incremental GC
-    if (gcstate() !== "none")
+    if (gcstate() !== "NotActive")
         gc();
 
     startgc(sliceCount, "shrinking");
-    while (gcstate() !== "none") {
+    while (gcstate() !== "NotActive") {
         gcslice(sliceCount);
     }
 
     return zones;
 }
 
 testCompacting(1, 100000, 100000);
 testCompacting(2, 100000, 100000);
--- a/js/src/jit-test/tests/gc/incremental-state.js
+++ b/js/src/jit-test/tests/gc/incremental-state.js
@@ -1,63 +1,63 @@
 // Test expected state changes during collection.
 if (!("gcstate" in this))
     quit();
 
 gczeal(0);
 
 // Non-incremental GC.
 gc();
-assertEq(gcstate(), "none");
+assertEq(gcstate(), "NotActive");
 
 // Incremental GC in minimal slice. Note that finalization always uses zero-
 // sized slices while background finalization is on-going, so we need to loop.
 gcslice(1000000);
-while (gcstate() == "finalize") { gcslice(1); }
-while (gcstate() == "decommit") { gcslice(1); }
-assertEq(gcstate(), "none");
+while (gcstate() == "Finalize") { gcslice(1); }
+while (gcstate() == "Decommit") { gcslice(1); }
+assertEq(gcstate(), "NotActive");
 
 // Incremental GC in multiple slices: if marking takes more than one slice,
 // we yield before we start sweeping.
 gczeal(0);
 gcslice(1);
-assertEq(gcstate(), "mark");
+assertEq(gcstate(), "Mark");
 gcslice(1000000);
-assertEq(gcstate(), "mark");
+assertEq(gcstate(), "Mark");
 gcslice(1000000);
-while (gcstate() == "finalize") { gcslice(1); }
-while (gcstate() == "decommit") { gcslice(1); }
-assertEq(gcstate(), "none");
+while (gcstate() == "Finalize") { gcslice(1); }
+while (gcstate() == "Decommit") { gcslice(1); }
+assertEq(gcstate(), "NotActive");
 
 // Zeal mode 8: Incremental GC in two main slices:
 //   1) mark roots
 //   2) mark and sweep
 //   *) finalize.
 gczeal(8, 0);
 gcslice(1);
-assertEq(gcstate(), "mark");
+assertEq(gcstate(), "Mark");
 gcslice(1);
-while (gcstate() == "finalize") { gcslice(1); }
-while (gcstate() == "decommit") { gcslice(1); }
-assertEq(gcstate(), "none");
+while (gcstate() == "Finalize") { gcslice(1); }
+while (gcstate() == "Decommit") { gcslice(1); }
+assertEq(gcstate(), "NotActive");
 
 // Zeal mode 9: Incremental GC in two main slices:
 //   1) mark roots and marking
 //   2) new marking and sweeping
 //   *) finalize.
 gczeal(9, 0);
 gcslice(1);
-assertEq(gcstate(), "mark");
+assertEq(gcstate(), "Mark");
 gcslice(1);
-while (gcstate() == "finalize") { gcslice(1); }
-while (gcstate() == "decommit") { gcslice(1); }
-assertEq(gcstate(), "none");
+while (gcstate() == "Finalize") { gcslice(1); }
+while (gcstate() == "Decommit") { gcslice(1); }
+assertEq(gcstate(), "NotActive");
 
 // Zeal mode 10: Incremental GC in multiple slices (always yeilds before
 // sweeping). This test uses long slices to prove that this zeal mode yields
 // in sweeping, where normal IGC (above) does not.
 gczeal(10, 0);
 gcslice(1000000);
-assertEq(gcstate(), "sweep");
+assertEq(gcstate(), "Sweep");
 gcslice(1000000);
-while (gcstate() == "finalize") { gcslice(1); }
-while (gcstate() == "decommit") { gcslice(1); }
-assertEq(gcstate(), "none");
+while (gcstate() == "Finalize") { gcslice(1); }
+while (gcstate() == "Decommit") { gcslice(1); }
+assertEq(gcstate(), "NotActive");
--- a/js/src/jit/arm/Simulator-arm.cpp
+++ b/js/src/jit/arm/Simulator-arm.cpp
@@ -1497,21 +1497,19 @@ Simulator::exclusiveMonitorGetAndClear(b
 
 void
 Simulator::exclusiveMonitorClear()
 {
     exclusiveMonitorHeld_ = false;
 }
 
 int
-Simulator::readW(int32_t addr, SimInstruction* instr)
+Simulator::readW(int32_t addr, SimInstruction* instr, UnalignedPolicy f)
 {
-    // The regexp engine emits unaligned loads, so we don't check for them here
-    // like most of the other methods do.
-    if ((addr & 3) == 0 || !HasAlignmentFault()) {
+    if ((addr & 3) == 0 || (f == AllowUnaligned && !HasAlignmentFault())) {
         intptr_t* ptr = reinterpret_cast<intptr_t*>(addr);
         return *ptr;
     }
 
     // In WebAssembly, we want unaligned accesses to either raise a signal or
     // do the right thing. Making this simulator properly emulate the behavior
     // of raising a signal is complex, so as a special-case, when in wasm code,
     // we just do the right thing.
@@ -1522,19 +1520,19 @@ Simulator::readW(int32_t addr, SimInstru
         return value;
     }
 
     printf("Unaligned read at 0x%08x, pc=%p\n", addr, instr);
     MOZ_CRASH();
 }
 
 void
-Simulator::writeW(int32_t addr, int value, SimInstruction* instr)
+Simulator::writeW(int32_t addr, int value, SimInstruction* instr, UnalignedPolicy f)
 {
-    if ((addr & 3) == 0 || !HasAlignmentFault()) {
+    if ((addr & 3) == 0 || (f == AllowUnaligned && !HasAlignmentFault())) {
         intptr_t* ptr = reinterpret_cast<intptr_t*>(addr);
         *ptr = value;
         return;
     }
 
     // See the comments above in readW.
     if (wasm::IsPCInWasmCode(reinterpret_cast<void *>(get_pc()))) {
         char* ptr = reinterpret_cast<char*>(addr);
@@ -1587,70 +1585,104 @@ Simulator::writeExW(int32_t addr, int va
     if ((addr & 3) == 0) {
         SharedMem<int32_t*> ptr = SharedMem<int32_t*>::shared(reinterpret_cast<int32_t*>(addr));
         bool held;
         int32_t expected = int32_t(exclusiveMonitorGetAndClear(&held));
         if (!held)
             return 1;
         int32_t old = compareExchangeRelaxed(ptr, expected, int32_t(value));
         return old != expected;
-    } else {
-        printf("Unaligned write at 0x%08x, pc=%p\n", addr, instr);
-        MOZ_CRASH();
     }
+
+    printf("Unaligned write at 0x%08x, pc=%p\n", addr, instr);
+    MOZ_CRASH();
 }
 
 uint16_t
 Simulator::readHU(int32_t addr, SimInstruction* instr)
 {
     // The regexp engine emits unaligned loads, so we don't check for them here
     // like most of the other methods do.
     if ((addr & 1) == 0 || !HasAlignmentFault()) {
         uint16_t* ptr = reinterpret_cast<uint16_t*>(addr);
         return *ptr;
     }
+
+    // See comments in readW.
+    if (wasm::IsPCInWasmCode(reinterpret_cast<void *>(get_pc()))) {
+        char* ptr = reinterpret_cast<char*>(addr);
+        uint16_t value;
+        memcpy(&value, ptr, sizeof(value));
+        return value;
+    }
+
     printf("Unaligned unsigned halfword read at 0x%08x, pc=%p\n", addr, instr);
     MOZ_CRASH();
     return 0;
 }
 
 int16_t
 Simulator::readH(int32_t addr, SimInstruction* instr)
 {
     if ((addr & 1) == 0 || !HasAlignmentFault()) {
         int16_t* ptr = reinterpret_cast<int16_t*>(addr);
         return *ptr;
     }
+
+    // See comments in readW.
+    if (wasm::IsPCInWasmCode(reinterpret_cast<void *>(get_pc()))) {
+        char* ptr = reinterpret_cast<char*>(addr);
+        int16_t value;
+        memcpy(&value, ptr, sizeof(value));
+        return value;
+    }
+
     printf("Unaligned signed halfword read at 0x%08x\n", addr);
     MOZ_CRASH();
     return 0;
 }
 
 void
 Simulator::writeH(int32_t addr, uint16_t value, SimInstruction* instr)
 {
     if ((addr & 1) == 0 || !HasAlignmentFault()) {
         uint16_t* ptr = reinterpret_cast<uint16_t*>(addr);
         *ptr = value;
-    } else {
-        printf("Unaligned unsigned halfword write at 0x%08x, pc=%p\n", addr, instr);
-        MOZ_CRASH();
+        return;
     }
+
+    // See the comments above in readW.
+    if (wasm::IsPCInWasmCode(reinterpret_cast<void *>(get_pc()))) {
+        char* ptr = reinterpret_cast<char*>(addr);
+        memcpy(ptr, &value, sizeof(value));
+        return;
+    }
+
+    printf("Unaligned unsigned halfword write at 0x%08x, pc=%p\n", addr, instr);
+    MOZ_CRASH();
 }
 
 void
 Simulator::writeH(int32_t addr, int16_t value, SimInstruction* instr)
 {
     if ((addr & 1) == 0 || !HasAlignmentFault()) {
         int16_t* ptr = reinterpret_cast<int16_t*>(addr);
         *ptr = value;
-    } else {
-        printf("Unaligned halfword write at 0x%08x, pc=%p\n", addr, instr);
-        MOZ_CRASH();
+        return;
     }
+
+    // See the comments above in readW.
+    if (wasm::IsPCInWasmCode(reinterpret_cast<void *>(get_pc()))) {
+        char* ptr = reinterpret_cast<char*>(addr);
+        memcpy(ptr, &value, sizeof(value));
+        return;
+    }
+
+    printf("Unaligned halfword write at 0x%08x, pc=%p\n", addr, instr);
+    MOZ_CRASH();
 }
 
 uint16_t
 Simulator::readExHU(int32_t addr, SimInstruction* instr)
 {
     // The regexp engine emits unaligned loads, so we don't check for them here
     // like most of the other methods do.
     if ((addr & 1) == 0 || !HasAlignmentFault()) {
@@ -3189,19 +3221,19 @@ Simulator::decodeType2(SimInstruction* i
             uint8_t val = readBU(addr);
             set_register(rd, val);
         } else {
             uint8_t val = get_register(rd);
             writeB(addr, val);
         }
     } else {
         if (instr->hasL())
-            set_register(rd, readW(addr, instr));
+            set_register(rd, readW(addr, instr, AllowUnaligned));
         else
-            writeW(addr, get_register(rd), instr);
+            writeW(addr, get_register(rd), instr, AllowUnaligned);
     }
 }
 
 static uint32_t
 rotateBytes(uint32_t val, int32_t rotate)
 {
     switch (rotate) {
       default:
@@ -3456,19 +3488,19 @@ Simulator::decodeType3(SimInstruction* i
             uint8_t byte = readB(addr);
             set_register(rd, byte);
         } else {
             uint8_t byte = get_register(rd);
             writeB(addr, byte);
         }
     } else {
         if (instr->hasL())
-            set_register(rd, readW(addr, instr));
+            set_register(rd, readW(addr, instr, AllowUnaligned));
         else
-            writeW(addr, get_register(rd), instr);
+            writeW(addr, get_register(rd), instr, AllowUnaligned);
     }
 }
 
 void
 Simulator::decodeType4(SimInstruction* instr)
 {
     // Only allowed to be set in privileged mode.
     MOZ_ASSERT(instr->bit(22) == 0);
@@ -4283,18 +4315,20 @@ Simulator::decodeSpecialCondition(SimIns
               default:
                 MOZ_CRASH();
                 break;
             }
             int r = 0;
             while (r < regs) {
                 uint32_t data[2];
                 get_d_register(Vd + r, data);
-                writeW(address, data[0], instr);
-                writeW(address + 4, data[1], instr);
+                // TODO: We should AllowUnaligned here only if the alignment attribute of
+                // the instruction calls for default alignment.
+                writeW(address, data[0], instr, AllowUnaligned);
+                writeW(address + 4, data[1], instr, AllowUnaligned);
                 address += 8;
                 r++;
             }
             if (Rm != 15) {
                 if (Rm == 13)
                     set_register(Rn, address);
                 else
                     set_register(Rn, get_register(Rn) + get_register(Rm));
@@ -4322,18 +4356,20 @@ Simulator::decodeSpecialCondition(SimIns
                 break;
               default:
                 MOZ_CRASH();
                 break;
             }
             int r = 0;
             while (r < regs) {
                 uint32_t data[2];
-                data[0] = readW(address, instr);
-                data[1] = readW(address + 4, instr);
+                // TODO: We should AllowUnaligned here only if the alignment attribute of
+                // the instruction calls for default alignment.
+                data[0] = readW(address, instr, AllowUnaligned);
+                data[1] = readW(address + 4, instr, AllowUnaligned);
                 set_d_register(Vd + r, data);
                 address += 8;
                 r++;
             }
             if (Rm != 15) {
                 if (Rm == 13)
                     set_register(Rn, address);
                 else
--- a/js/src/jit/arm/Simulator-arm.h
+++ b/js/src/jit/arm/Simulator-arm.h
@@ -199,16 +199,30 @@ class Simulator
         bad_lr = -1,
         // A pc value used to signal the simulator to stop execution. Generally
         // the lr is set to this value on transition from native C code to
         // simulated execution, so that the simulator can "return" to the native
         // C code.
         end_sim_pc = -2
     };
 
+    // ForbidUnaligned means "always fault on unaligned access".
+    //
+    // AllowUnaligned means "allow the unaligned access if other conditions are
+    // met".  The "other conditions" vary with the instruction: For all
+    // instructions the base condition is !HasAlignmentFault(), ie, the chip is
+    // configured to allow unaligned accesses.  For instructions like VLD1
+    // there is an additional constraint that the alignment attribute in the
+    // instruction must be set to "default alignment".
+
+    enum UnalignedPolicy {
+        ForbidUnaligned,
+        AllowUnaligned
+    };
+
     bool init();
 
     // Checks if the current instruction should be executed based on its
     // condition bits.
     inline bool conditionallyExecute(SimInstruction* instr);
 
     // Helper functions to set the conditional flags in the architecture state.
     void setNZFlags(int32_t val);
@@ -256,18 +270,18 @@ class Simulator
     inline int16_t readH(int32_t addr, SimInstruction* instr);
     // Note: Overloaded on the sign of the value.
     inline void writeH(int32_t addr, uint16_t value, SimInstruction* instr);
     inline void writeH(int32_t addr, int16_t value, SimInstruction* instr);
 
     inline uint16_t readExHU(int32_t addr, SimInstruction* instr);
     inline int32_t writeExH(int32_t addr, uint16_t value, SimInstruction* instr);
 
-    inline int readW(int32_t addr, SimInstruction* instr);
-    inline void writeW(int32_t addr, int value, SimInstruction* instr);
+    inline int readW(int32_t addr, SimInstruction* instr, UnalignedPolicy f = ForbidUnaligned);
+    inline void writeW(int32_t addr, int value, SimInstruction* instr, UnalignedPolicy f = ForbidUnaligned);
 
     inline int readExW(int32_t addr, SimInstruction* instr);
     inline int writeExW(int32_t addr, int value, SimInstruction* instr);
 
     int32_t* readDW(int32_t addr);
     void writeDW(int32_t addr, int32_t value1, int32_t value2);
 
     int32_t readExDW(int32_t addr, int32_t* hibits);
--- a/js/src/jit/none/MacroAssembler-none.h
+++ b/js/src/jit/none/MacroAssembler-none.h
@@ -80,16 +80,17 @@ static constexpr Register64 ReturnReg64(
 
 static constexpr Register ABINonArgReg0 = { Registers::invalid_reg };
 static constexpr Register ABINonArgReg1 = { Registers::invalid_reg };
 static constexpr Register ABINonArgReturnReg0 = { Registers::invalid_reg };
 static constexpr Register ABINonArgReturnReg1 = { Registers::invalid_reg };
 
 static constexpr Register WasmTableCallPtrReg = { Registers::invalid_reg };
 static constexpr Register WasmTableCallSigReg = { Registers::invalid_reg };
+static constexpr Register WasmTlsReg = { Registers::invalid_reg };
 
 static constexpr uint32_t ABIStackAlignment = 4;
 static constexpr uint32_t CodeAlignment = 4;
 static constexpr uint32_t JitStackAlignment = 8;
 static constexpr uint32_t JitStackValueAlignment = JitStackAlignment / sizeof(Value);
 
 static const Scale ScalePointer = TimesOne;
 
--- a/js/src/jsapi-tests/moz.build
+++ b/js/src/jsapi-tests/moz.build
@@ -39,16 +39,17 @@ UNIFIED_SOURCES += [
     'testFreshGlobalEvalRedefinition.cpp',
     'testFunctionProperties.cpp',
     'testGCAllocator.cpp',
     'testGCCellPtr.cpp',
     'testGCChunkPool.cpp',
     'testGCExactRooting.cpp',
     'testGCFinalizeCallback.cpp',
     'testGCHeapPostBarriers.cpp',
+    'testGCHooks.cpp',
     'testGCMarking.cpp',
     'testGCOutOfMemory.cpp',
     'testGCStoreBufferRemoval.cpp',
     'testGCUniqueId.cpp',
     'testGCWeakCache.cpp',
     'testGCWeakRef.cpp',
     'testGetPropertyDescriptor.cpp',
     'testHashTable.cpp',
--- a/js/src/jsapi-tests/testGCFinalizeCallback.cpp
+++ b/js/src/jsapi-tests/testGCFinalizeCallback.cpp
@@ -98,17 +98,17 @@ BEGIN_TEST(testGCFinalizeCallback)
 
     /* Full GC with reset due to new compartment, becoming compartment GC. */
 
     FinalizeCalls = 0;
     JS_SetGCZeal(cx, 9, 1000000);
     JS::PrepareForFullGC(cx);
     js::SliceBudget budget(js::WorkBudget(1));
     cx->gc.startDebugGC(GC_NORMAL, budget);
-    CHECK(cx->gc.state() == js::gc::MARK);
+    CHECK(cx->gc.state() == js::gc::State::Mark);
     CHECK(cx->gc.isFullGc());
 
     JS::RootedObject global4(cx, createTestGlobal());
     budget = js::SliceBudget(js::WorkBudget(1));
     cx->gc.debugGCSlice(budget);
     while (cx->gc.isIncrementalGCInProgress())
         cx->gc.debugGCSlice(budget);
     CHECK(!cx->gc.isIncrementalGCInProgress());
new file mode 100644
--- /dev/null
+++ b/js/src/jsapi-tests/testGCHooks.cpp
@@ -0,0 +1,36 @@
+/* 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 "mozilla/UniquePtr.h"
+
+#include "js/GCAPI.h"
+
+#include "jsapi-tests/tests.h"
+
+static unsigned gSliceCallbackCount = 0;
+
+static void
+NonIncrementalGCSliceCallback(JSContext* cx, JS::GCProgress progress, const JS::GCDescription& desc)
+{
+    ++gSliceCallbackCount;
+    MOZ_RELEASE_ASSERT(progress == JS::GC_CYCLE_BEGIN || progress == JS::GC_CYCLE_END);
+    MOZ_RELEASE_ASSERT(desc.isCompartment_ == false);
+    MOZ_RELEASE_ASSERT(desc.invocationKind_ == GC_NORMAL);
+    MOZ_RELEASE_ASSERT(desc.reason_ == JS::gcreason::API);
+    if (progress == JS::GC_CYCLE_END) {
+        mozilla::UniquePtr<char16_t> summary(desc.formatSummaryMessage(cx));
+        mozilla::UniquePtr<char16_t> message(desc.formatSliceMessage(cx));
+        mozilla::UniquePtr<char16_t> json(desc.formatJSON(cx, 0));
+    }
+}
+
+BEGIN_TEST(testGCSliceCallback)
+{
+    JS::SetGCSliceCallback(cx, NonIncrementalGCSliceCallback);
+    JS_GC(cx);
+    JS::SetGCSliceCallback(cx, nullptr);
+    CHECK(gSliceCallbackCount == 2);
+    return true;
+}
+END_TEST(testGCSliceCallback)
--- a/js/src/jsgc.cpp
+++ b/js/src/jsgc.cpp
@@ -48,56 +48,59 @@
  * passed.
  *
  * Collector states
  * ----------------
  *
  * The collector proceeds through the following states, the current state being
  * held in JSRuntime::gcIncrementalState:
  *
- *  - MARK_ROOTS - marks the stack and other roots
- *  - MARK       - incrementally marks reachable things
- *  - SWEEP      - sweeps zones in groups and continues marking unswept zones
+ *  - MarkRoots  - marks the stack and other roots
+ *  - Mark       - incrementally marks reachable things
+ *  - Sweep      - sweeps zones in groups and continues marking unswept zones
+ *  - Finalize   - performs background finalization, concurrent with mutator
+ *  - Compact    - incrementally compacts by zone
+ *  - Decommit   - performs background decommit and chunk removal
  *
- * The MARK_ROOTS activity always takes place in the first slice. The next two
+ * The MarkRoots activity always takes place in the first slice. The next two
  * states can take place over one or more slices.
  *
  * In other words an incremental collection proceeds like this:
  *
- * Slice 1:   MARK_ROOTS: Roots pushed onto the mark stack.
- *            MARK:       The mark stack is processed by popping an element,
+ * Slice 1:   MarkRoots:  Roots pushed onto the mark stack.
+ *            Mark:       The mark stack is processed by popping an element,
  *                        marking it, and pushing its children.
  *
  *          ... JS code runs ...
  *
- * Slice 2:   MARK:       More mark stack processing.
+ * Slice 2:   Mark:       More mark stack processing.
  *
  *          ... JS code runs ...
  *
- * Slice n-1: MARK:       More mark stack processing.
+ * Slice n-1: Mark:       More mark stack processing.
  *
  *          ... JS code runs ...
  *
- * Slice n:   MARK:       Mark stack is completely drained.
- *            SWEEP:      Select first group of zones to sweep and sweep them.
+ * Slice n:   Mark:       Mark stack is completely drained.
+ *            Sweep:      Select first group of zones to sweep and sweep them.
  *
  *          ... JS code runs ...
  *
- * Slice n+1: SWEEP:      Mark objects in unswept zones that were newly
+ * Slice n+1: Sweep:      Mark objects in unswept zones that were newly
  *                        identified as alive (see below). Then sweep more zone
  *                        groups.
  *
  *          ... JS code runs ...
  *
- * Slice n+2: SWEEP:      Mark objects in unswept zones that were newly
+ * Slice n+2: Sweep:      Mark objects in unswept zones that were newly
  *                        identified as alive. Then sweep more zone groups.
  *
  *          ... JS code runs ...
  *
- * Slice m:   SWEEP:      Sweeping is finished, and background sweeping
+ * Slice m:   Sweep:      Sweeping is finished, and background sweeping
  *                        started on the helper thread.
  *
  *          ... JS code runs, remaining sweeping done on background thread ...
  *
  * When background sweeping finishes the GC is complete.
  *
  * Incremental marking
  * -------------------
@@ -136,17 +139,17 @@
  * about to be destroyed.
  *
  * Sweeping all finalizable objects in one go would introduce long pauses, so
  * instead sweeping broken up into groups of zones. Zones which are not yet
  * being swept are still marked, so the issue above does not apply.
  *
  * The order of sweeping is restricted by cross compartment pointers - for
  * example say that object |a| from zone A points to object |b| in zone B and
- * neither object was marked when we transitioned to the SWEEP phase. Imagine we
+ * neither object was marked when we transitioned to the Sweep phase. Imagine we
  * sweep B first and then return to the mutator. It's possible that the mutator
  * could cause |a| to become alive through a read barrier (perhaps it was a
  * shape that was accessed via a shape table). Then we would need to mark |b|,
  * which |a| points to, but |b| has already been swept.
  *
  * So if there is such a pointer then marking of zone B must not finish before
  * marking of zone A.  Pointers which form a cycle between zones therefore
  * restrict those zones to being swept at the same time, and these are found
@@ -817,17 +820,17 @@ GCRuntime::GCRuntime(JSRuntime* rt) :
     majorGCNumber(0),
     jitReleaseNumber(0),
     number(0),
     startNumber(0),
     isFull(false),
 #ifdef DEBUG
     disableStrictProxyCheckingCount(0),
 #endif
-    incrementalState(gc::NO_INCREMENTAL),
+    incrementalState(gc::State::NotActive),
     lastMarkSlice(false),
     sweepOnBackgroundThread(false),
     foundBlackGrayEdges(false),
     blocksToFreeAfterSweeping(JSRuntime::TEMP_LIFO_ALLOC_PRIMARY_CHUNK_SIZE),
     blocksToFreeAfterMinorGC(JSRuntime::TEMP_LIFO_ALLOC_PRIMARY_CHUNK_SIZE),
     zoneGroupIndex(0),
     zoneGroups(nullptr),
     currentZoneGroup(nullptr),
@@ -4171,17 +4174,17 @@ js::gc::MarkingValidator::nonIncremental
     /*
      * After this point, the function should run to completion, so we shouldn't
      * do anything fallible.
      */
     initialized = true;
 
     /* Re-do all the marking, but non-incrementally. */
     js::gc::State state = gc->incrementalState;
-    gc->incrementalState = MARK_ROOTS;
+    gc->incrementalState = State::MarkRoots;
 
     {
         gcstats::AutoPhase ap(gc->stats, gcstats::PHASE_MARK);
 
         {
             gcstats::AutoPhase ap(gc->stats, gcstats::PHASE_UNMARK);
 
             for (GCZonesIter zone(runtime); !zone.done(); zone.next())
@@ -4191,22 +4194,22 @@ js::gc::MarkingValidator::nonIncremental
             gcmarker->reset();
 
             for (auto chunk = gc->allNonEmptyChunks(); !chunk.done(); chunk.next())
                 chunk->bitmap.clear();
         }
 
         gc->markRuntime(gcmarker, GCRuntime::MarkRuntime, lock);
 
-        gc->incrementalState = MARK;
+        gc->incrementalState = State::Mark;
         auto unlimited = SliceBudget::unlimited();
         MOZ_RELEASE_ASSERT(gc->marker.drainMarkStack(unlimited));
     }
 
-    gc->incrementalState = SWEEP;
+    gc->incrementalState = State::Sweep;
     {
         gcstats::AutoPhase ap1(gc->stats, gcstats::PHASE_SWEEP);
         gcstats::AutoPhase ap2(gc->stats, gcstats::PHASE_SWEEP_MARK);
 
         gc->markAllWeakReferences(gcstats::PHASE_SWEEP_MARK_WEAK);
 
         /* Update zone state for gray marking. */
         for (GCZonesIter zone(runtime); !zone.done(); zone.next()) {
@@ -5616,44 +5619,48 @@ AutoTraceSession::~AutoTraceSession()
         runtime->heapState_ = prevState;
     }
 }
 
 void
 GCRuntime::resetIncrementalGC(const char* reason, AutoLockForExclusiveAccess& lock)
 {
     switch (incrementalState) {
-      case NO_INCREMENTAL:
+      case State::NotActive:
         return;
 
-      case MARK: {
+      case State::MarkRoots:
+        MOZ_CRASH("resetIncrementalGC did not expect MarkRoots state");
+        break;
+
+      case State::Mark: {
         /* Cancel any ongoing marking. */
         marker.reset();
         marker.stop();
         clearBufferedGrayRoots();
 
         for (GCCompartmentsIter c(rt); !c.done(); c.next())
             ResetGrayList(c);
 
         for (GCZonesIter zone(rt); !zone.done(); zone.next()) {
             MOZ_ASSERT(zone->isGCMarking());
             zone->setNeedsIncrementalBarrier(false, Zone::UpdateJit);
             zone->setGCState(Zone::NoGC);
         }
 
         blocksToFreeAfterSweeping.freeAll();
 
-        incrementalState = NO_INCREMENTAL;
+        incrementalState = State::NotActive;
 
         MOZ_ASSERT(!marker.shouldCheckCompartments());
 
         break;
       }
 
-      case SWEEP: {
+      case State::Sweep: {
         marker.reset();
 
         for (CompartmentsIter c(rt, SkipAtoms); !c.done(); c.next())
             c->scheduledForDestruction = false;
 
         /* Finish sweeping the current zone group, then abort. */
         abortSweepAfterCurrentGroup = true;
 
@@ -5668,68 +5675,65 @@ GCRuntime::resetIncrementalGC(const char
 
         {
             gcstats::AutoPhase ap(stats, gcstats::PHASE_WAIT_BACKGROUND_THREAD);
             rt->gc.waitBackgroundSweepOrAllocEnd();
         }
         break;
       }
 
-      case FINALIZE: {
+      case State::Finalize: {
         {
             gcstats::AutoPhase ap(stats, gcstats::PHASE_WAIT_BACKGROUND_THREAD);
             rt->gc.waitBackgroundSweepOrAllocEnd();
         }
 
         bool wasCompacting = isCompacting;
         isCompacting = false;
 
         auto unlimited = SliceBudget::unlimited();
         incrementalCollectSlice(unlimited, JS::gcreason::RESET, lock);
 
         isCompacting = wasCompacting;
 
         break;
       }
 
-      case COMPACT: {
+      case State::Compact: {
         bool wasCompacting = isCompacting;
 
         isCompacting = true;
         startedCompacting = true;
         zonesToMaybeCompact.clear();
 
         auto unlimited = SliceBudget::unlimited();
         incrementalCollectSlice(unlimited, JS::gcreason::RESET, lock);
 
         isCompacting = wasCompacting;
         break;
       }
 
-      case DECOMMIT: {
+      case State::Decommit: {
         auto unlimited = SliceBudget::unlimited();
         incrementalCollectSlice(unlimited, JS::gcreason::RESET, lock);
         break;
       }
-
-      default:
-        MOZ_CRASH("Invalid incremental GC state");
     }
 
     stats.reset(reason);
 
 #ifdef DEBUG
     assertBackgroundSweepingFinished();
     for (ZonesIter zone(rt, WithAtoms); !zone.done(); zone.next()) {
         MOZ_ASSERT(!zone->isCollecting());
         MOZ_ASSERT(!zone->needsIncrementalBarrier());
         MOZ_ASSERT(!zone->isOnList());
     }
     MOZ_ASSERT(zonesToMaybeCompact.isEmpty());
-    MOZ_ASSERT(incrementalState == NO_INCREMENTAL);
+    MOZ_ASSERT(incrementalState == State::NotActive);
 #endif
 }
 
 namespace {
 
 class AutoGCSlice {
   public:
     explicit AutoGCSlice(JSRuntime* rt);
@@ -5838,70 +5842,70 @@ GCRuntime::incrementalCollectSlice(Slice
         /*
          * Yields between slices occurs at predetermined points in these modes;
          * the budget is not used.
          */
         budget.makeUnlimited();
     }
 
     switch (incrementalState) {
-      case NO_INCREMENTAL:
+      case State::NotActive:
         initialReason = reason;
         cleanUpEverything = ShouldCleanUpEverything(reason, invocationKind);
         isCompacting = shouldCompact();
         lastMarkSlice = false;
 
-        incrementalState = MARK_ROOTS;
+        incrementalState = State::MarkRoots;
 
         MOZ_FALLTHROUGH;
 
-      case MARK_ROOTS:
+      case State::MarkRoots:
         if (!beginMarkPhase(reason, lock)) {
-            incrementalState = NO_INCREMENTAL;
+            incrementalState = State::NotActive;
             return;
         }
 
         if (!destroyingRuntime)
             pushZealSelectedObjects();
 
-        incrementalState = MARK;
+        incrementalState = State::Mark;
 
         if (isIncremental && useZeal && hasZealMode(ZealMode::IncrementalRootsThenFinish))
             break;
 
         MOZ_FALLTHROUGH;
 
-      case MARK:
+      case State::Mark:
         AutoGCRooter::traceAllWrappers(&marker);
 
         /* If we needed delayed marking for gray roots, then collect until done. */
         if (!hasBufferedGrayRoots()) {
             budget.makeUnlimited();
             isIncremental = false;
         }
 
         if (drainMarkStack(budget, gcstats::PHASE_MARK) == NotFinished)
             break;
 
         MOZ_ASSERT(marker.isDrained());
 
         if (!lastMarkSlice && isIncremental && useZeal &&
-            ((initialState == MARK && !hasZealMode(ZealMode::IncrementalRootsThenFinish)) ||
+            ((initialState == State::Mark && !hasZealMode(ZealMode::IncrementalRootsThenFinish)) ||
              hasZealMode(ZealMode::IncrementalMarkAllThenFinish)))
         {
             /*
              * Yield with the aim of starting the sweep in the next
              * slice.  We will need to mark anything new on the stack
-             * when we resume, so we stay in MARK state.
+             * when we resume, so we stay in Mark state.
              */
             lastMarkSlice = true;
             break;
         }
 
-        incrementalState = SWEEP;
+        incrementalState = State::Sweep;
 
         /*
          * This runs to completion, but we don't continue if the budget is
          * now exhasted.
          */
         beginSweepPhase(destroyingRuntime, lock);
         if (budget.isOverBudget())
             break;
@@ -5910,31 +5914,31 @@ GCRuntime::incrementalCollectSlice(Slice
          * Always yield here when running in incremental multi-slice zeal
          * mode, so RunDebugGC can reset the slice buget.
          */
         if (isIncremental && useZeal && hasZealMode(ZealMode::IncrementalMultipleSlices))
             break;
 
         MOZ_FALLTHROUGH;
 
-      case SWEEP:
+      case State::Sweep:
         if (sweepPhase(budget, lock) == NotFinished)
             break;
 
         endSweepPhase(destroyingRuntime, lock);
 
-        incrementalState = FINALIZE;
+        incrementalState = State::Finalize;
 
         /* Yield before compacting since it is not incremental. */
         if (isCompacting && isIncremental)
             break;
 
         MOZ_FALLTHROUGH;
 
-      case FINALIZE:
+      case State::Finalize:
         {
             gcstats::AutoPhase ap(stats, gcstats::PHASE_WAIT_BACKGROUND_THREAD);
 
             // Yield until background finalization is done.
             if (isIncremental) {
                 // Poll for end of background sweeping
                 AutoLockGC lock(rt);
                 if (isBackgroundSweeping())
@@ -5950,53 +5954,50 @@ GCRuntime::incrementalCollectSlice(Slice
             gcstats::AutoPhase ap1(stats, gcstats::PHASE_SWEEP);
             gcstats::AutoPhase ap2(stats, gcstats::PHASE_DESTROY);
             AutoSetThreadIsSweeping threadIsSweeping;
             FreeOp fop(rt);
             sweepZones(&fop, destroyingRuntime);
         }
 
         MOZ_ASSERT(!startedCompacting);
-        incrementalState = COMPACT;
+        incrementalState = State::Compact;
 
         MOZ_FALLTHROUGH;
 
-      case COMPACT:
+      case State::Compact:
         if (isCompacting) {
             if (!startedCompacting)
                 beginCompactPhase();
 
             if (compactPhase(reason, budget, lock) == NotFinished)
                 break;
 
             endCompactPhase(reason);
         }
 
         startDecommit();
-        incrementalState = DECOMMIT;
+        incrementalState = State::Decommit;
 
         MOZ_FALLTHROUGH;
 
-      case DECOMMIT:
+      case State::Decommit:
         {
             gcstats::AutoPhase ap(stats, gcstats::PHASE_WAIT_BACKGROUND_THREAD);
 
             // Yield until background decommit is done.
             if (isIncremental && decommitTask.isRunning())
                 break;
 
             decommitTask.join();
         }
 
         finishCollection(reason);
-        incrementalState = NO_INCREMENTAL;
+        incrementalState = State::NotActive;
         break;
-
-      default:
-        MOZ_CRASH("unexpected GC incrementalState");
     }
 }
 
 IncrementalSafety
 gc::IsIncrementalGCSafe(JSRuntime* rt)
 {
     MOZ_ASSERT(!rt->mainThread.suppressGC);
 
@@ -6149,17 +6150,17 @@ GCRuntime::gcCycle(bool nonincrementalBy
 
         stats.nonincremental("requested");
         budget.makeUnlimited();
     } else {
         budgetIncrementalGC(budget, session.lock);
     }
 
     /* The GC was reset, so we need a do-over. */
-    if (prevState != NO_INCREMENTAL && !isIncrementalGCInProgress())
+    if (prevState != State::NotActive && !isIncrementalGCInProgress())
         return true;
 
     TraceMajorGCStart();
 
     incrementalCollectSlice(budget, reason, session.lock);
 
 #ifndef JS_MORE_DETERMINISTIC
     nextFullGCTime = PRMJ_Now() + GC_IDLE_FULL_SPAN;
@@ -6356,17 +6357,17 @@ void
 GCRuntime::finishGC(JS::gcreason::Reason reason)
 {
     MOZ_ASSERT(isIncrementalGCInProgress());
 
     // If we're not collecting because we're out of memory then skip the
     // compacting phase if we need to finish an ongoing incremental GC
     // non-incrementally to avoid janking the browser.
     if (!IsOOMReason(initialReason)) {
-        if (incrementalState == COMPACT) {
+        if (incrementalState == State::Compact) {
             abortGC();
             return;
         }
 
         isCompacting = false;
     }
 
     collect(false, SliceBudget::unlimited(), reason);
@@ -6794,18 +6795,18 @@ GCRuntime::runDebugGC()
             invocationKind = GC_SHRINK;
         collect(false, budget, JS::gcreason::DEBUG_GC);
 
         /*
          * For multi-slice zeal, reset the slice size when we get to the sweep
          * or compact phases.
          */
         if (hasZealMode(ZealMode::IncrementalMultipleSlices)) {
-            if ((initialState == MARK && incrementalState == SWEEP) ||
-                (initialState == SWEEP && incrementalState == COMPACT))
+            if ((initialState == State::Mark && incrementalState == State::Sweep) ||
+                (initialState == State::Sweep && incrementalState == State::Compact))
             {
                 incrementalLimit = zealFrequency / 2;
             }
         }
     } else if (hasZealMode(ZealMode::Compact)) {
         gc(GC_SHRINK, JS::gcreason::DEBUG_GC);
     } else {
         gc(GC_NORMAL, JS::gcreason::DEBUG_GC);
@@ -7308,17 +7309,17 @@ JS_PUBLIC_API(bool)
 JS::IsIncrementalGCInProgress(JSContext* cx)
 {
     return cx->gc.isIncrementalGCInProgress() && !cx->gc.isVerifyPreBarriersEnabled();
 }
 
 JS_PUBLIC_API(bool)
 JS::IsIncrementalBarrierNeeded(JSContext* cx)
 {
-    return cx->gc.state() == gc::MARK && !cx->isHeapBusy();
+    return cx->gc.state() == gc::State::Mark && !cx->isHeapBusy();
 }
 
 struct IncrementalReferenceBarrierFunctor {
     template <typename T> void operator()(T* t) { T::writeBarrierPre(t); }
 };
 
 JS_PUBLIC_API(void)
 JS::IncrementalReferenceBarrier(GCCellPtr thing)
@@ -7591,28 +7592,22 @@ NewMemoryInfoObject(JSContext* cx)
     }
 
     return obj;
 }
 
 const char*
 StateName(State state)
 {
-    static const char* names[] = {
-        "None",
-        "MarkRoots",
-        "Mark",
-        "Sweep",
-        "Finalize",
-        "Compact",
-        "Decommit"
-    };
-    MOZ_ASSERT(ArrayLength(names) == NUM_STATES);
-    MOZ_ASSERT(state < NUM_STATES);
-    return names[state];
+    switch(state) {
+#define MAKE_CASE(name) case State::name: return #name;
+      GCSTATES(MAKE_CASE)
+#undef MAKE_CASE
+    }
+    MOZ_MAKE_COMPILER_ASSUME_IS_UNREACHABLE("invalide gc::State enum value");
 }
 
 void
 AutoAssertHeapBusy::checkCondition(JSRuntime *rt)
 {
     this->rt = rt;
     MOZ_ASSERT(rt->isHeapBusy());
 }
--- a/js/src/jsgc.h
+++ b/js/src/jsgc.h
@@ -38,26 +38,28 @@ struct Statistics;
 } // namespace gcstats
 
 class Nursery;
 
 namespace gc {
 
 struct FinalizePhase;
 
-enum State {
-    NO_INCREMENTAL,
-    MARK_ROOTS,
-    MARK,
-    SWEEP,
-    FINALIZE,
-    COMPACT,
-    DECOMMIT,
-
-    NUM_STATES
+#define GCSTATES(D) \
+    D(NotActive) \
+    D(MarkRoots) \
+    D(Mark) \
+    D(Sweep) \
+    D(Finalize) \
+    D(Compact) \
+    D(Decommit)
+enum class State {
+#define MAKE_STATE(name) name,
+    GCSTATES(MAKE_STATE)
+#undef MAKE_STATE
 };
 
 /*
  * Map from C++ type to alloc kind for non-object types. JSObject does not have
  * a 1:1 mapping, so must use Arena::thingSize.
  *
  * The AllocKind is available as MapTypeToFinalizeKind<SomeType>::kind.
  */
--- a/js/src/old-configure.in
+++ b/js/src/old-configure.in
@@ -523,16 +523,17 @@ if test "$ac_cv_static_assertion_macros_
 fi
 fi # COMPILE_ENVIRONMENT
 
 dnl ========================================================
 dnl Android libstdc++, placed here so it can use MOZ_ARCH
 dnl computed above.
 dnl ========================================================
 
+MOZ_ANDROID_CPU_ARCH
 MOZ_ANDROID_STLPORT
 
 dnl ========================================================
 dnl Suppress Clang Argument Warnings
 dnl ========================================================
 if test -n "${CLANG_CC}${CLANG_CL}"; then
     _WARNINGS_CFLAGS="-Qunused-arguments ${_WARNINGS_CFLAGS}"
     CPPFLAGS="-Qunused-arguments ${CPPFLAGS}"
new file mode 100644
--- /dev/null
+++ b/js/src/tests/ecma_6/Array/concat-proxy.js
@@ -0,0 +1,25 @@
+var BUGNUMBER = 1287520;
+var summary = 'Array.prototype.concat should check HasProperty everytime for non-dense array';
+
+print(BUGNUMBER + ": " + summary);
+
+var a = [1, 2, 3];
+a.constructor = {
+  [Symbol.species]: function(...args) {
+    var p = new Proxy(new Array(...args), {
+      defineProperty(target, propertyKey, receiver) {
+        if (propertyKey === "0") delete a[1];
+        return Reflect.defineProperty(target, propertyKey, receiver);
+      }
+    });
+    return p;
+  }
+};
+
+var p = a.concat();
+assertEq(0 in p, true);
+assertEq(1 in p, false);
+assertEq(2 in p, true);
+
+if (typeof reportCompare === "function")
+  reportCompare(true, true);
new file mode 100644
--- /dev/null
+++ b/js/src/tests/ecma_6/RegExp/ignoreCase-multiple.js
@@ -0,0 +1,71 @@
+var BUGNUMBER = 1280046;
+var summary = "ignoreCase match should perform Canonicalize both on input and pattern.";
+
+print(BUGNUMBER + ": " + summary);
+
+// Each element [code1, upper, code2] satisfies the following condition:
+//   ToUpperCase(code1) == upper
+//   ToUpperCase(code2) == upper
+var pairs =
+    [
+        // U+00B5: MICRO SIGN
+        // U+039C: GREEK CAPITAL LETTER MU
+        // U+03BC: GREEK SMALL LETTER MU
+        ["\u00B5", "\u039C", "\u03BC"],
+        // U+0345: COMBINING GREEK YPOGEGRAMMENI
+        // U+0399: GREEK CAPITAL LETTER IOTA
+        // U+03B9: GREEK SMALL LETTER IOTA
+        ["\u0345", "\u0399", "\u03B9"],
+        // U+03C2: GREEK SMALL LETTER FINAL SIGMA
+        // U+03A3: GREEK CAPITAL LETTER SIGMA
+        // U+03C3: GREEK SMALL LETTER SIGMA
+        ["\u03C2", "\u03A3", "\u03C3"],
+        // U+03D0: GREEK BETA SYMBOL
+        // U+0392: GREEK CAPITAL LETTER BETA
+        // U+03B2: GREEK SMALL LETTER BETA
+        ["\u03D0", "\u0392", "\u03B2"],
+        // U+03D1: GREEK THETA SYMBOL
+        // U+0398: GREEK CAPITAL LETTER THETA
+        // U+03B8: GREEK SMALL LETTER THETA
+        ["\u03D1", "\u0398", "\u03B8"],
+        // U+03D5: GREEK PHI SYMBOL
+        // U+03A6: GREEK CAPITAL LETTER PHI
+        // U+03C6: GREEK SMALL LETTER PHI
+        ["\u03D5", "\u03A6", "\u03C6"],
+        // U+03D6: GREEK PI SYMBOL
+        // U+03A0: GREEK CAPITAL LETTER PI
+        // U+03C0: GREEK SMALL LETTER PI
+        ["\u03D6", "\u03A0", "\u03C0"],
+        // U+03F0: GREEK KAPPA SYMBOL
+        // U+039A: GREEK CAPITAL LETTER KAPPA
+        // U+03BA: GREEK SMALL LETTER KAPPA
+        ["\u03F0", "\u039A", "\u03BA"],
+        // U+03F1: GREEK RHO SYMBOL
+        // U+03A1: GREEK CAPITAL LETTER RHO
+        // U+03C1: GREEK SMALL LETTER RHO
+        ["\u03F1", "\u03A1", "\u03C1"],
+        // U+03F5: GREEK LUNATE EPSILON SYMBOL
+        // U+0395: GREEK CAPITAL LETTER EPSILON
+        // U+03B5: GREEK SMALL LETTER EPSILON
+        ["\u03F5", "\u0395", "\u03B5"],
+        // U+1E9B: LATIN SMALL LETTER LONG S WITH DOT ABOVE
+        // U+1E60: LATIN CAPITAL LETTER S WITH DOT ABOVE
+        // U+1E61: LATIN SMALL LETTER S WITH DOT ABOVE
+        ["\u1E9B", "\u1E60", "\u1E61"],
+        // U+1FBE: GREEK PROSGEGRAMMENI
+        // U+0399: GREEK CAPITAL LETTER IOTA
+        // U+03B9: GREEK SMALL LETTER IOTA
+        ["\u1FBE", "\u0399", "\u03B9"],
+    ];
+
+for (var [code1, upper, code2] of pairs) {
+    assertEq(new RegExp(code1, "i").test(code2), true);
+    assertEq(new RegExp(code1, "i").test(upper), true);
+    assertEq(new RegExp(upper, "i").test(code1), true);
+    assertEq(new RegExp(upper, "i").test(code2), true);
+    assertEq(new RegExp(code2, "i").test(code1), true);
+    assertEq(new RegExp(code2, "i").test(upper), true);
+}
+
+if (typeof reportCompare === "function")
+    reportCompare(true, true);
new file mode 100644
--- /dev/null
+++ b/js/src/tests/ecma_6/RegExp/replace-global-unicode.js
@@ -0,0 +1,18 @@
+var BUGNUMBER = 1287524;
+var summary = 'RegExp.prototype[@@replace] should not use optimized path if RegExp.prototype.unicode is modified.';
+
+print(BUGNUMBER + ": " + summary);
+
+Object.defineProperty(RegExp.prototype, "unicode", {
+  get() {
+    RegExp.prototype.exec = () => null;
+  }
+});
+
+var rx = RegExp("a", "g");
+var s = "abba";
+var r = rx[Symbol.replace](s, "c");
+assertEq(r, "abba");
+
+if (typeof reportCompare === "function")
+  reportCompare(true, true);
new file mode 100644
--- /dev/null
+++ b/js/src/tests/ecma_6/RegExp/split-limit.js
@@ -0,0 +1,14 @@
+var BUGNUMBER = 1287525;
+var summary = "RegExp.prototype[@@split] shouldn't use optimized path if limit is not number.";
+
+print(BUGNUMBER + ": " + summary);
+
+var rx = /a/;
+var r = rx[Symbol.split]("abba", {valueOf() {
+  RegExp.prototype.exec = () => null;
+  return 100;
+}});
+assertEq(r.length, 1);
+
+if (typeof reportCompare === "function")
+    reportCompare(true, true);
new file mode 100644
--- /dev/null
+++ b/js/src/tests/ecma_6/RegExp/split-prop-access.js
@@ -0,0 +1,19 @@
+var BUGNUMBER = 1287525;
+var summary = 'String.prototype.split should call ToUint32(limit) before ToString(separator).';
+
+print(BUGNUMBER + ": " + summary);
+
+var accessed = false;
+
+var rx = /a/;
+Object.defineProperty(rx, Symbol.match, {
+  get() {
+    accessed = true;
+  }
+});
+rx[Symbol.split]("abba");
+
+assertEq(accessed, true);
+
+if (typeof reportCompare === "function")
+  reportCompare(true, true);
--- a/js/src/vm/Debugger.cpp
+++ b/js/src/vm/Debugger.cpp
@@ -2824,22 +2824,22 @@ Debugger::markCrossCompartmentEdges(JSTr
  * This method is also used during compacting GC to update cross compartment
  * pointers in zones that are not currently being compacted.
  */
 /* static */ void
 Debugger::markIncomingCrossCompartmentEdges(JSTracer* trc)
 {
     JSRuntime* rt = trc->runtime();
     gc::State state = rt->gc.state();
-    MOZ_ASSERT(state == gc::MARK_ROOTS || state == gc::COMPACT);
+    MOZ_ASSERT(state == gc::State::MarkRoots || state == gc::State::Compact);
 
     for (Debugger* dbg : rt->debuggerList) {
         Zone* zone = MaybeForwarded(dbg->object.get())->zone();
-        if ((state == gc::MARK_ROOTS && !zone->isCollecting()) ||
-            (state == gc::COMPACT && !zone->isGCCompacting()))
+        if ((state == gc::State::MarkRoots && !zone->isCollecting()) ||
+            (state == gc::State::Compact && !zone->isGCCompacting()))
         {
             dbg->markCrossCompartmentEdges(trc);
         }
     }
 }
 
 /*
  * This method has two tasks:
--- a/js/src/vm/RegExpObject.h
+++ b/js/src/vm/RegExpObject.h
@@ -321,16 +321,17 @@ class RegExpCompartment
      * the input/index properties faster.
      */
     ReadBarriered<ArrayObject*> matchResultTemplateObject_;
 
     /*
      * The shape of RegExp.prototype object that satisfies following:
      *   * RegExp.prototype.global getter is not modified
      *   * RegExp.prototype.sticky getter is not modified
+     *   * RegExp.prototype.unicode getter is not modified
      *   * RegExp.prototype.exec is an own data property
      *   * RegExp.prototype[@@match] is an own data property
      *   * RegExp.prototype[@@search] is an own data property
      */
     ReadBarriered<Shape*> optimizableRegExpPrototypeShape_;
 
     /*
      * The shape of RegExp instance that satisfies following:
--- a/js/src/vm/StructuredClone.cpp
+++ b/js/src/vm/StructuredClone.cpp
@@ -1375,18 +1375,25 @@ JSStructuredCloneWriter::transferOwnersh
         }
 
         LittleEndian::writeUint64(point++, PairToUInt64(tag, ownership));
         LittleEndian::writeUint64(point++, reinterpret_cast<uint64_t>(content));
         LittleEndian::writeUint64(point++, extraData);
     }
 
     MOZ_ASSERT(point <= out.rawBuffer() + out.count());
-    MOZ_ASSERT_IF(point < out.rawBuffer() + out.count(),
-                  uint32_t(LittleEndian::readUint64(point) >> 32) < SCTAG_TRANSFER_MAP_HEADER);
+#if DEBUG
+    // Make sure there aren't any more transfer map entries after the expected
+    // number we read out.
+    if (point < out.rawBuffer() + out.count()) {
+        uint32_t tag, data;
+        SCInput::getPair(point, &tag, &data);
+        MOZ_ASSERT(tag < SCTAG_TRANSFER_MAP_HEADER || tag >= SCTAG_TRANSFER_MAP_END_OF_BUILTIN_TYPES);
+    }
+#endif
 
     return true;
 }
 
 bool
 JSStructuredCloneWriter::write(HandleValue v)
 {
     if (!startWrite(v))
--- a/js/src/vm/Unicode.cpp
+++ b/js/src/vm/Unicode.cpp
@@ -767,16 +767,332 @@ const uint8_t unicode::index2[] = {
       0,   0,   0,   0,   0,   0,   5,   5,   5,   5,   5,   5,   5,   5,   5,   5,   5,   5,
       5,   5,   5,   5,   5,   5,   5,   5,   5,   5,   5,   5,   5,   5,   5,   5,   5,   5,
       5,   5,   5,   5,   5,   5,   5,   5,   5,   5,   5,   5,   5,   5,   5,   5,   5,   5,
       5,   5,   5,   5,   5,   5,   5,   5,   5,   0,   0,   0,   5,   5,   5,   5,   5,   5,
       0,   0,   5,   5,   5,   5,   5,   5,   0,   0,   5,   5,   5,   5,   5,   5,   0,   0,
       5,   5,   5,   0,   0,   0,
 };
 
+const CodepointsWithSameUpperCaseInfo unicode::js_codepoints_with_same_upper_info[] = {
+    {0, 0, 0},
+    {32, 0, 0},
+    {32, 232, 0},
+    {32, 300, 0},
+    {0, 200, 0},
+    {0, 268, 0},
+    {0, 775, 0},
+    {1, 0, 0},
+    {65336, 0, 0},
+    {65415, 0, 0},
+    {65268, 0, 0},
+    {210, 0, 0},
+    {206, 0, 0},
+    {205, 0, 0},
+    {79, 0, 0},
+    {202, 0, 0},
+    {203, 0, 0},
+    {207, 0, 0},
+    {211, 0, 0},
+    {209, 0, 0},
+    {213, 0, 0},
+    {214, 0, 0},
+    {218, 0, 0},
+    {217, 0, 0},
+    {219, 0, 0},
+    {1, 2, 0},
+    {0, 1, 0},
+    {65535, 0, 0},
+    {65439, 0, 0},
+    {65480, 0, 0},
+    {65406, 0, 0},
+    {10795, 0, 0},
+    {65373, 0, 0},
+    {10792, 0, 0},
+    {65341, 0, 0},
+    {69, 0, 0},
+    {71, 0, 0},
+    {0, 116, 7289},
+    {38, 0, 0},
+    {37, 0, 0},
+    {64, 0, 0},
+    {63, 0, 0},
+    {32, 62, 0},
+    {32, 96, 0},
+    {32, 57, 0},
+    {65452, 32, 7205},
+    {32, 86, 0},
+    {64793, 32, 0},
+    {32, 54, 0},
+    {32, 80, 0},
+    {31, 32, 0},
+    {32, 47, 0},
+    {0, 30, 0},
+    {0, 64, 0},
+    {0, 25, 0},
+    {65420, 0, 7173},
+    {0, 54, 0},
+    {64761, 0, 0},
+    {0, 22, 0},
+    {0, 48, 0},
+    {0, 15, 0},
+    {8, 0, 0},
+    {65506, 0, 0},
+    {65511, 0, 0},
+    {65521, 0, 0},
+    {65514, 0, 0},
+    {65482, 0, 0},
+    {65488, 0, 0},
+    {65472, 0, 0},
+    {65529, 0, 0},
+    {80, 0, 0},
+    {15, 0, 0},
+    {48, 0, 0},
+    {7264, 0, 0},
+    {1, 59, 0},
+    {0, 58, 0},
+    {65478, 0, 0},
+    {65528, 0, 0},
+    {65462, 0, 0},
+    {65527, 0, 0},
+    {58247, 58363, 0},
+    {65450, 0, 0},
+    {65436, 0, 0},
+    {65424, 0, 0},
+    {65408, 0, 0},
+    {65410, 0, 0},
+    {28, 0, 0},
+    {16, 0, 0},
+    {26, 0, 0},
+    {54793, 0, 0},
+    {61722, 0, 0},
+    {54809, 0, 0},
+    {54756, 0, 0},
+    {54787, 0, 0},
+    {54753, 0, 0},
+    {54754, 0, 0},
+    {54721, 0, 0},
+    {30204, 0, 0},
+    {23256, 0, 0},
+    {23228, 0, 0},
+};
+
+const uint8_t unicode::codepoints_with_same_upper_index1[] = {
+      0,   1,   2,   3,   4,   5,   6,   7,   8,   9,   0,   0,   0,  10,  11,  12,  13,  14,
+     15,  16,  17,  18,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,
+      0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,
+      0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,  19,  20,   0,   0,   0,   0,
+      0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,
+      0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,
+      0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,  21,  22,  23,  21,  24,  25,
+     26,  27,   0,   0,   0,   0,  28,  29,  30,   0,   0,   0,   0,   0,   0,   0,   0,   0,
+      0,   0,  31,  32,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,
+      0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,  33,  34,  21,  35,
+      0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,
+      0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,
+      0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,
+      0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,
+      0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,
+      0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,
+      0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,
+      0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,
+      0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,
+      0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,
+      0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,
+      0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,
+      0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,
+      0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,
+      0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,
+      0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,
+      0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,
+      0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,
+      0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,
+      0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,
+      0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,
+      0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,
+      0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,
+      0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,
+      0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,
+      0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,
+      0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,  36,
+     37,   0,  38,  39,  40,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,
+      0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,
+      0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,
+      0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,
+      0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,
+      0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,
+      0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,
+      0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,
+      0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,
+      0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,
+      0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,
+      0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,
+      0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,
+      0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,
+      0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,
+      0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,
+      0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,
+      0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,
+      0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,
+      0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,  41,   0,   0,   0,
+};
+
+const uint8_t unicode::codepoints_with_same_upper_index2[] = {
+      0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,
+      0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,
+      0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,
+      0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   1,   1,   1,   1,   1,   1,   1,
+      1,   2,   1,   1,   1,   1,   1,   1,   1,   1,   1,   3,   1,   1,   1,   1,   1,   1,
+      1,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   4,   0,   0,
+      0,   0,   0,   0,   0,   0,   0,   5,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,
+      0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,
+      0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,
+      0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,
+      0,   6,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   1,   1,   1,   1,   1,   1,
+      1,   1,   1,   1,   1,   1,   1,   1,   1,   1,   1,   1,   1,   1,   1,   1,   1,   0,
+      1,   1,   1,   1,   1,   1,   1,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,
+      0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,
+      0,   0,   0,   0,   7,   0,   7,   0,   7,   0,   7,   0,   7,   0,   7,   0,   7,   0,
+      7,   0,   7,   0,   7,   0,   7,   0,   7,   0,   7,   0,   7,   0,   7,   0,   7,   0,
+      7,   0,   7,   0,   7,   0,   7,   0,   7,   0,   7,   0,   7,   0,   7,   0,   0,   8,
+      7,   0,   7,   0,   7,   0,   0,   7,   0,   7,   0,   7,   0,   7,   0,   7,   0,   7,
+      0,   7,   0,   7,   0,   0,   7,   0,   7,   0,   7,   0,   7,   0,   7,   0,   7,   0,
+      7,   0,   7,   0,   7,   0,   7,   0,   7,   0,   7,   0,   7,   0,   7,   0,   7,   0,
+      7,   0,   7,   0,   7,   0,   7,   0,   7,   0,   7,   0,   7,   0,   7,   0,   9,   7,
+      0,   7,   0,   7,   0,  10,   0,  11,   7,   0,   7,   0,  12,   7,   0,  13,  13,   7,
+      0,   0,  14,  15,  16,   7,   0,  13,  17,   0,  18,  19,   7,   0,   0,   0,  18,  20,
+      0,  21,   7,   0,   7,   0,   7,   0,  22,   7,   0,  22,   0,   0,   7,   0,  22,   7,
+      0,  23,  23,   7,   0,   7,   0,  24,   7,   0,   0,   0,   7,   0,   0,   0,   0,   0,
+      0,   0,  25,  26,  27,  25,  26,  27,  25,  26,  27,   7,   0,   7,   0,   7,   0,   7,
+      0,   7,   0,   7,   0,   7,   0,   7,   0,   0,   7,   0,   7,   0,   7,   0,   7,   0,
+      7,   0,   7,   0,   7,   0,   7,   0,   7,   0,   0,  25,  26,  27,   7,   0,  28,  29,
+      7,   0,   7,   0,   7,   0,   7,   0,   7,   0,   7,   0,   7,   0,   7,   0,   7,   0,
+      7,   0,   7,   0,   7,   0,   7,   0,   7,   0,   7,   0,   7,   0,   7,   0,   7,   0,
+      7,   0,   7,   0,  30,   0,   7,   0,   7,   0,   7,   0,   7,   0,   7,   0,   7,   0,
+      7,   0,   7,   0,   7,   0,   0,   0,   0,   0,   0,   0,  31,   7,   0,  32,  33,   0,
+      0,   7,   0,  34,  35,  36,   7,   0,   7,   0,   7,   0,   7,   0,   7,   0,   0,   0,
+      0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,
+      0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,
+      0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,  37,   0,   0,
+      0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,
+      0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,
+      0,   0,   0,   0,   7,   0,   7,   0,   0,   0,   7,   0,   0,   0,   0,   0,   0,   0,
+      0,   0,   0,   0,   0,   0,   0,   0,  38,   0,  39,  39,  39,   0,  40,   0,  41,  41,
+      0,   1,  42,   1,   1,  43,   1,   1,  44,  45,  46,   1,  47,   1,   1,   1,  48,  49,
+      0,  50,   1,   1,  51,   1,   1,   1,   1,   1,   0,   0,   0,   0,   0,   0,  52,   0,
+      0,  53,   0,   0,  54,  55,  56,   0,  57,   0,   0,   0,  58,  59,  26,  27,   0,   0,
+     60,   0,   0,   0,   0,   0,   0,   0,   0,  61,  62,  63,   0,   0,   0,  64,  65,   0,
+      7,   0,   7,   0,   7,   0,   7,   0,   7,   0,   7,   0,   7,   0,   7,   0,   7,   0,
+      7,   0,   7,   0,   7,   0,  66,  67,   0,   0,   0,  68,   0,   7,   0,  69,   7,   0,
+      0,  30,  30,  30,  70,  70,  70,  70,  70,  70,  70,  70,  70,  70,  70,  70,  70,  70,
+     70,  70,   1,   1,   1,   1,   1,   1,   1,   1,   1,   1,   1,   1,   1,   1,   1,   1,
+      1,   1,   1,   1,   1,   1,   1,   1,   1,   1,   1,   1,   1,   1,   1,   1,   0,   0,
+      0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,
+      0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,
+      0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   7,   0,   7,   0,   7,   0,   7,   0,
+      7,   0,   7,   0,   7,   0,   7,   0,   7,   0,   7,   0,   7,   0,   7,   0,   7,   0,
+      7,   0,   7,   0,   7,   0,   7,   0,   0,   0,   0,   0,   0,   0,   0,   0,   7,   0,
+      7,   0,   7,   0,   7,   0,   7,   0,   7,   0,   7,   0,   7,   0,   7,   0,   7,   0,
+      7,   0,   7,   0,   7,   0,   7,   0,   7,   0,   7,   0,   7,   0,   7,   0,   7,   0,
+      7,   0,   7,   0,   7,   0,   7,   0,   7,   0,   7,   0,   7,   0,   7,   0,  71,   7,
+      0,   7,   0,   7,   0,   7,   0,   7,   0,   7,   0,   7,   0,   0,   7,   0,   7,   0,
+      7,   0,   7,   0,   7,   0,   7,   0,   7,   0,   7,   0,   7,   0,   7,   0,   7,   0,
+      7,   0,   7,   0,   7,   0,   7,   0,   7,   0,   7,   0,   7,   0,   7,   0,   7,   0,
+      7,   0,   7,   0,   7,   0,   7,   0,   7,   0,   7,   0,   7,   0,   7,   0,   7,   0,
+      7,   0,   7,   0,   7,   0,   7,   0,   7,   0,   7,   0,   7,   0,   7,   0,   7,   0,
+      7,   0,   7,   0,   7,   0,   7,   0,   7,   0,   7,   0,   0,   0,   0,   0,   0,   0,
+      0,   0,   0,  72,  72,  72,  72,  72,  72,  72,  72,  72,  72,  72,  72,  72,  72,  72,
+     72,  72,  72,  72,  72,  72,  72,  72,  72,  72,  72,  72,  72,  72,  72,  72,  72,  72,
+     72,  72,  72,  72,  72,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,
+      0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,
+      0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,
+      0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,
+      0,   0,   0,   0,   0,   0,  73,  73,  73,  73,  73,  73,  73,  73,  73,  73,  73,  73,
+     73,  73,  73,  73,  73,  73,  73,  73,  73,  73,  73,  73,  73,  73,  73,  73,  73,  73,
+     73,  73,  73,  73,  73,  73,  73,  73,   0,  73,   0,   0,   0,   0,   0,  73,   0,   0,
+      0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,
+      0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,
+      0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   7,   0,   7,   0,   7,   0,
+      7,   0,   7,   0,   7,   0,   7,   0,   7,   0,   7,   0,   7,   0,   7,   0,   7,   0,
+      7,   0,   7,   0,   7,   0,   7,   0,   7,   0,   7,   0,   7,   0,   7,   0,   7,   0,
+      7,   0,   7,   0,   7,   0,   7,   0,   7,   0,   7,   0,   7,   0,   7,   0,   7,   0,
+      7,   0,   7,   0,   7,   0,   7,   0,   7,   0,   7,   0,   7,   0,   7,   0,   7,   0,
+      7,   0,   7,   0,   7,   0,   7,   0,   7,   0,   7,   0,   7,   0,   7,   0,   7,   0,
+     74,  75,   7,   0,   7,   0,   7,   0,   7,   0,   7,   0,   7,   0,   7,   0,   7,   0,
+      7,   0,   7,   0,   7,   0,   7,   0,   7,   0,   7,   0,   7,   0,   7,   0,   7,   0,
+      7,   0,   7,   0,   7,   0,   7,   0,   7,   0,   7,   0,   7,   0,   7,   0,   7,   0,
+      0,   0,   0,   0,   0,  76,   0,   0,   0,   0,   7,   0,   7,   0,   7,   0,   7,   0,
+      7,   0,   7,   0,   7,   0,   7,   0,   7,   0,   7,   0,   7,   0,   7,   0,   7,   0,
+      7,   0,   7,   0,   7,   0,   0,   0,   0,   0,   0,   0,   0,   0,  77,  77,  77,  77,
+     77,  77,  77,  77,   0,   0,   0,   0,   0,   0,   0,   0,  77,  77,  77,  77,  77,  77,
+      0,   0,   0,   0,   0,   0,   0,   0,   0,   0,  77,  77,  77,  77,  77,  77,  77,  77,
+      0,   0,   0,   0,   0,   0,   0,   0,  77,  77,  77,  77,  77,  77,  77,  77,   0,   0,
+      0,   0,   0,   0,   0,   0,  77,  77,  77,  77,  77,  77,   0,   0,   0,   0,   0,   0,
+      0,   0,   0,   0,   0,  77,   0,  77,   0,  77,   0,  77,   0,   0,   0,   0,   0,   0,
+      0,   0,  77,  77,  77,  77,  77,  77,  77,  77,   0,   0,   0,   0,   0,   0,   0,   0,
+      0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,  77,  77,
+     77,  77,  77,  77,  77,  77,   0,   0,   0,   0,   0,   0,   0,   0,  77,  77,  77,  77,
+     77,  77,  77,  77,   0,   0,   0,   0,   0,   0,   0,   0,  77,  77,  77,  77,  77,  77,
+     77,  77,   0,   0,   0,   0,   0,   0,   0,   0,  77,  77,  78,  78,  79,   0,  80,   0,
+      0,   0,   0,   0,   0,   0,   0,   0,  81,  81,  81,  81,  79,   0,   0,   0,   0,   0,
+      0,   0,   0,   0,   0,   0,  77,  77,  82,  82,   0,   0,   0,   0,   0,   0,   0,   0,
+      0,   0,   0,   0,  77,  77,  83,  83,  69,   0,   0,   0,   0,   0,   0,   0,   0,   0,
+      0,   0,  84,  84,  85,  85,  79,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,
+      0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,
+      0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,
+      0,   0,   0,   0,   0,   0,  86,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,
+      0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,
+      0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,  87,  87,
+     87,  87,  87,  87,  87,  87,  87,  87,  87,  87,  87,  87,  87,  87,   0,   0,   0,   0,
+      0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   7,   0,   0,
+      0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,
+      0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,
+      0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,
+      0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,
+      0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,
+      0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,
+      0,   0,   0,   0,  88,  88,  88,  88,  88,  88,  88,  88,  88,  88,  88,  88,  88,  88,
+     88,  88,  88,  88,  88,  88,  88,  88,  88,  88,  88,  88,   0,   0,   0,   0,   0,   0,
+      0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,
+      0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,
+      0,   0,   0,   0,   0,   0,  72,  72,  72,  72,  72,  72,  72,  72,  72,  72,  72,  72,
+     72,  72,  72,  72,  72,  72,  72,  72,  72,  72,  72,  72,  72,  72,  72,  72,  72,  72,
+     72,  72,  72,  72,  72,  72,  72,  72,  72,  72,  72,  72,  72,  72,  72,  72,  72,   0,
+      0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,
+      0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,
+      0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   7,   0,  89,  90,  91,   0,
+      0,   7,   0,   7,   0,   7,   0,  92,  93,  94,  95,   0,   7,   0,   0,   7,   0,   0,
+      0,   0,   0,   0,   0,   0,  96,  96,   7,   0,   7,   0,   7,   0,   7,   0,   7,   0,
+      7,   0,   7,   0,   7,   0,   7,   0,   7,   0,   7,   0,   7,   0,   7,   0,   7,   0,
+      7,   0,   7,   0,   7,   0,   7,   0,   0,   0,   0,   0,   0,   0,   0,   7,   0,   7,
+      0,   0,   0,   0,   7,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,
+      7,   0,   7,   0,   7,   0,   7,   0,   7,   0,   7,   0,   7,   0,   7,   0,   7,   0,
+      7,   0,   7,   0,   7,   0,   7,   0,   7,   0,   7,   0,   7,   0,   7,   0,   7,   0,
+      7,   0,   7,   0,   7,   0,   7,   0,   7,   0,   0,   0,   0,   0,   0,   0,   0,   0,
+      0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   7,   0,   7,   0,   7,   0,   7,   0,
+      7,   0,   7,   0,   7,   0,   7,   0,   7,   0,   7,   0,   7,   0,   7,   0,   0,   0,
+      0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,
+      0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,
+      0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,
+      0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,
+      7,   0,   7,   0,   7,   0,   7,   0,   7,   0,   7,   0,   7,   0,   0,   0,   7,   0,
+      7,   0,   7,   0,   7,   0,   7,   0,   7,   0,   7,   0,   7,   0,   7,   0,   7,   0,
+      7,   0,   7,   0,   7,   0,   7,   0,   7,   0,   7,   0,   7,   0,   7,   0,   7,   0,
+      7,   0,   7,   0,   7,   0,   7,   0,   7,   0,   7,   0,   7,   0,   7,   0,   7,   0,
+      7,   0,   7,   0,   7,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   7,   0,   7,
+      0,  97,   7,   0,   7,   0,   7,   0,   7,   0,   7,   0,   0,   0,   0,   7,   0,  98,
+      0,   0,   7,   0,   7,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,
+      7,   0,   7,   0,   7,   0,   7,   0,   7,   0,  99,   0,   0,   0,   0,   0,   0,   0,
+      0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,
+      0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,
+      0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   1,   1,   1,   1,   1,   1,   1,
+      1,   1,   1,   1,   1,   1,   1,   1,   1,   1,   1,   1,   1,   1,   1,   1,   1,   1,
+      1,   0,   0,   0,   0,   0,
+};
+
 const FoldingInfo unicode::js_foldinfo[] = {
     {0, 0, 0, 0},
     {32, 0, 0, 0},
     {32, 8415, 0, 0},
     {32, 300, 0, 0},
     {0, 65504, 0, 0},
     {0, 65504, 8383, 0},
     {0, 65504, 268, 0},
--- a/js/src/vm/Unicode.h
+++ b/js/src/vm/Unicode.h
@@ -274,16 +274,86 @@ ToLowerCaseNonBMPTrail(char16_t lead, ch
     if (lead == LEAD && trail >= TRAIL_FROM && trail <= TRAIL_TO) \
         return trail + DIFF;
     FOR_EACH_NON_BMP_LOWERCASE(CALC_TRAIL)
 #undef CALL_TRAIL
 
     return trail;
 }
 
+/*
+ * For a codepoint C, CodepointsWithSameUpperCaseInfo stores three offsets
+ * from C to up to three codepoints with same uppercase (no codepoint in
+ * UnicodeData.txt has more than three such codepoints).
+ *
+ * To illustrate, consider the codepoint U+0399 GREEK CAPITAL LETTER IOTA, the
+ * uppercased form of these three codepoints:
+ *
+ *   U+03B9 GREEK SMALL LETTER IOTA
+ *   U+1FBE GREEK PROSGEGRAMMENI
+ *   U+0345 COMBINING GREEK YPOGEGRAMMENI
+ *
+ * For the CodepointsWithSameUpperCaseInfo corresponding to this codepoint,
+ * delta{1,2,3} are 16-bit modular deltas from 0x0399 to each respective
+ * codepoint:
+ *   uint16_t(0x03B9 - 0x0399),
+ *   uint16_t(0x1FBE - 0x0399),
+ *   uint16_t(0x0345 - 0x0399)
+ * in an unimportant order.
+ *
+ * If there are fewer than three other codepoints, some fields are zero.
+ * Consider the codepoint U+03B9 above, the other two codepoints U+1FBE and
+ * U+0345 have same uppercase (U+0399 is not).  For the
+ * CodepointsWithSameUpperCaseInfo corresponding to this codepoint,
+ * delta{1,2,3} are:
+ *   uint16_t(0x1FBE - 0x03B9),
+ *   uint16_t(0x0345 - 0x03B9),
+ *   uint16_t(0)
+ * in an unimportant order.
+ *
+ * Because multiple codepoints map to a single CodepointsWithSameUpperCaseInfo,
+ * a CodepointsWithSameUpperCaseInfo and its delta{1,2,3} have no meaning
+ * standing alone: they have meaning only with respect to a codepoint mapping
+ * to that CodepointsWithSameUpperCaseInfo.
+ */
+class CodepointsWithSameUpperCaseInfo
+{
+  public:
+    uint16_t delta1;
+    uint16_t delta2;
+    uint16_t delta3;
+};
+
+extern const uint8_t codepoints_with_same_upper_index1[];
+extern const uint8_t codepoints_with_same_upper_index2[];
+extern const CodepointsWithSameUpperCaseInfo js_codepoints_with_same_upper_info[];
+
+class CodepointsWithSameUpperCase
+{
+    const CodepointsWithSameUpperCaseInfo& info_;
+    const char16_t code_;
+
+    static const CodepointsWithSameUpperCaseInfo& computeInfo(char16_t code) {
+        const size_t shift = 6;
+        size_t index = codepoints_with_same_upper_index1[code >> shift];
+        index = codepoints_with_same_upper_index2[(index << shift) + (code & ((1 << shift) - 1))];
+        return js_codepoints_with_same_upper_info[index];
+    }
+
+  public:
+    explicit CodepointsWithSameUpperCase(char16_t code)
+      : info_(computeInfo(code)),
+        code_(code)
+    {}
+
+    char16_t other1() const { return uint16_t(code_) + info_.delta1; }
+    char16_t other2() const { return uint16_t(code_) + info_.delta2; }
+    char16_t other3() const { return uint16_t(code_) + info_.delta3; }
+};
+
 class FoldingInfo {
   public:
     uint16_t folding;
     uint16_t reverse1;
     uint16_t reverse2;
     uint16_t reverse3;
 };
 
--- a/js/src/vm/make_unicode.py
+++ b/js/src/vm/make_unicode.py
@@ -140,16 +140,21 @@ def make_non_bmp_convert_macro(out_file,
 def generate_unicode_stuff(unicode_data, case_folding,
                            data_file, non_bmp_file,
                            test_mapping, test_non_bmp_mapping,
                            test_space, test_icase):
     dummy = (0, 0, 0)
     table = [dummy]
     cache = {dummy: 0}
     index = [0] * (MAX + 1)
+    same_upper_map = {}
+    same_upper_dummy = (0, 0, 0)
+    same_upper_table = [same_upper_dummy]
+    same_upper_cache = {same_upper_dummy: 0}
+    same_upper_index = [0] * (MAX + 1)
     folding_map = {}
     rev_folding_map = {}
     folding_dummy = (0, 0, 0, 0)
     folding_table = [folding_dummy]
     folding_cache = {folding_dummy: 0}
     folding_index = [0] * (MAX + 1)
     test_table = {}
     test_space_table = []
@@ -167,16 +172,21 @@ def generate_unicode_stuff(unicode_data,
         category = row[2]
         alias = row[-5]
         uppercase = row[-3]
         lowercase = row[-2]
         flags = 0
 
         if uppercase:
             upper = int(uppercase, 16)
+
+            if upper not in same_upper_map:
+                same_upper_map[upper] = [code]
+            else:
+                same_upper_map[upper].append(code)
         else:
             upper = code
 
         if lowercase:
             lower = int(lowercase, 16)
         else:
             lower = code
 
@@ -211,16 +221,46 @@ def generate_unicode_stuff(unicode_data,
 
         i = cache.get(item)
         if i is None:
             assert item not in table
             cache[item] = i = len(table)
             table.append(item)
         index[code] = i
 
+    for code in range(0, MAX + 1):
+        entry = test_table.get(code)
+
+        if not entry:
+            continue
+
+        (upper, lower, name, alias) = entry
+
+        if upper not in same_upper_map:
+            continue
+
+        same_upper_ds = [v - code for v in same_upper_map[upper]]
+
+        assert len(same_upper_ds) <= 3
+        assert all([v > -65535 and v < 65535 for v in same_upper_ds])
+
+        same_upper = [v & 0xffff for v in same_upper_ds]
+        same_upper_0 = same_upper[0] if len(same_upper) >= 1 else 0
+        same_upper_1 = same_upper[1] if len(same_upper) >= 2 else 0
+        same_upper_2 = same_upper[2] if len(same_upper) >= 3 else 0
+
+        item = (same_upper_0, same_upper_1, same_upper_2)
+
+        i = same_upper_cache.get(item)
+        if i is None:
+            assert item not in same_upper_table
+            same_upper_cache[item] = i = len(same_upper_table)
+            same_upper_table.append(item)
+        same_upper_index[code] = i
+
     for row in read_case_folding(case_folding):
         code = row[0]
         mapping = row[2]
         folding_map[code] = mapping
 
         if code > MAX:
             non_bmp_folding_map[code] = mapping
             non_bmp_rev_folding_map[mapping] = code
@@ -305,17 +345,17 @@ def generate_unicode_stuff(unicode_data,
 
     test_mapping.write('/* Generated by make_unicode.py DO NOT MODIFY */\n')
     test_mapping.write(public_domain)
     test_mapping.write('var mapping = [\n')
     for code in range(0, MAX + 1):
         entry = test_table.get(code)
 
         if entry:
-            upper, lower, name, alias = entry
+            (upper, lower, name, alias) = entry
             test_mapping.write('  [' + hex(upper) + ', ' + hex(lower) + '], /* ' +
                        name + (' (' + alias + ')' if alias else '') + ' */\n')
         else:
             test_mapping.write('  [' + hex(code) + ', ' + hex(code) + '],\n')
     test_mapping.write('];')
     test_mapping.write("""
 assertEq(mapping.length, 0x10000);
 for (var i = 0; i <= 0xffff; i++) {
@@ -384,31 +424,45 @@ if (typeof reportCompare === "function")
     reportCompare(true, true);
 """)
 
     index1, index2, shift = splitbins(index)
 
     # Don't forget to update CharInfo in Unicode.cpp if you need to change this
     assert shift == 5
 
+    same_upper_index1, same_upper_index2, same_upper_shift = splitbins(same_upper_index)
+
+    # Don't forget to update CharInfo in Unicode.cpp if you need to change this
+    assert same_upper_shift == 6
+
     folding_index1, folding_index2, folding_shift = splitbins(folding_index)
 
     # Don't forget to update CharInfo in Unicode.cpp if you need to change this
     assert folding_shift == 6
 
     # verify correctness
     for char in index:
         test = table[index[char]]
 
         idx = index1[char >> shift]
         idx = index2[(idx << shift) + (char & ((1 << shift) - 1))]
 
         assert test == table[idx]
 
     # verify correctness
+    for char in same_upper_index:
+        test = same_upper_table[same_upper_index[char]]
+
+        idx = same_upper_index1[char >> same_upper_shift]
+        idx = same_upper_index2[(idx << same_upper_shift) + (char & ((1 << same_upper_shift) - 1))]
+
+        assert test == same_upper_table[idx]
+
+    # verify correctness
     for char in folding_index:
         test = folding_table[folding_index[char]]
 
         idx = folding_index1[char >> folding_shift]
         idx = folding_index2[(idx << folding_shift) + (char & ((1 << folding_shift) - 1))]
 
         assert test == folding_table[idx]
 
@@ -492,16 +546,29 @@ if (typeof reportCompare === "function")
         file.write('\n'.join(lines))
         file.write('\n};\n')
 
     dump(index1, 'index1', data_file)
     data_file.write('\n')
     dump(index2, 'index2', data_file)
     data_file.write('\n')
 
+    data_file.write('const CodepointsWithSameUpperCaseInfo unicode::js_codepoints_with_same_upper_info[] = {\n')
+    for d in same_upper_table:
+        data_file.write('    {')
+        data_file.write(', '.join((str(e) for e in d)))
+        data_file.write('},\n')
+    data_file.write('};\n')
+    data_file.write('\n')
+
+    dump(same_upper_index1, 'codepoints_with_same_upper_index1', data_file)
+    data_file.write('\n')
+    dump(same_upper_index2, 'codepoints_with_same_upper_index2', data_file)
+    data_file.write('\n')
+
     data_file.write('const FoldingInfo unicode::js_foldinfo[] = {\n')
     for d in folding_table:
         data_file.write('    {')
         data_file.write(', '.join((str(e) for e in d)))
         data_file.write('},\n')
     data_file.write('};\n')
     data_file.write('\n')
 
--- a/layout/base/ServoRestyleManager.cpp
+++ b/layout/base/ServoRestyleManager.cpp
@@ -45,24 +45,24 @@ ServoRestyleManager::PostRestyleEvent(El
   }
 
   presShell->GetDocument()->SetNeedStyleFlush();
 }
 
 void
 ServoRestyleManager::PostRestyleEventForLazyConstruction()
 {
-  NS_ERROR("stylo: ServoRestyleManager::PostRestyleEventForLazyConstruction not implemented");
+  NS_WARNING("stylo: ServoRestyleManager::PostRestyleEventForLazyConstruction not implemented");
 }
 
 void
 ServoRestyleManager::RebuildAllStyleData(nsChangeHint aExtraHint,
                                          nsRestyleHint aRestyleHint)
 {
-  NS_ERROR("stylo: ServoRestyleManager::RebuildAllStyleData not implemented");
+  NS_WARNING("stylo: ServoRestyleManager::RebuildAllStyleData not implemented");
 }
 
 void
 ServoRestyleManager::PostRebuildAllStyleDataEvent(nsChangeHint aExtraHint,
                                                   nsRestyleHint aRestyleHint)
 {
   MOZ_CRASH("stylo: ServoRestyleManager::PostRebuildAllStyleDataEvent not implemented");
 }
@@ -161,17 +161,17 @@ ServoRestyleManager::NoteRestyleHint(Ele
       if (cur->IsContent()) {
         cur->SetIsDirtyForServo();
       }
     }
   }
 
   // TODO: Handle all other nsRestyleHint values.
   if (aHint & ~(eRestyle_Self | eRestyle_Subtree | eRestyle_LaterSiblings)) {
-    NS_ERROR(nsPrintfCString("stylo: Unhandled restyle hint %s",
+    NS_WARNING(nsPrintfCString("stylo: Unhandled restyle hint %s",
                              RestyleManagerBase::RestyleHintToString(aHint).get()).get());
   }
 }
 
 void
 ServoRestyleManager::ProcessPendingRestyles()
 {
   if (!HasPendingRestyles()) {
@@ -221,32 +221,32 @@ ServoRestyleManager::ProcessPendingResty
 
   IncrementRestyleGeneration();
 }
 
 void
 ServoRestyleManager::RestyleForInsertOrChange(Element* aContainer,
                                               nsIContent* aChild)
 {
-  NS_ERROR("stylo: ServoRestyleManager::RestyleForInsertOrChange not implemented");
+  NS_WARNING("stylo: ServoRestyleManager::RestyleForInsertOrChange not implemented");
 }
 
 void
 ServoRestyleManager::RestyleForAppend(Element* aContainer,
                                       nsIContent* aFirstNewContent)
 {
-  NS_ERROR("stylo: ServoRestyleManager::RestyleForAppend not implemented");
+  NS_WARNING("stylo: ServoRestyleManager::RestyleForAppend not implemented");
 }
 
 void
 ServoRestyleManager::RestyleForRemove(Element* aContainer,
                                       nsIContent* aOldChild,
                                       nsIContent* aFollowingSibling)
 {
-  NS_ERROR("stylo: ServoRestyleManager::RestyleForRemove not implemented");
+  NS_WARNING("stylo: ServoRestyleManager::RestyleForRemove not implemented");
 }
 
 nsresult
 ServoRestyleManager::ContentStateChanged(nsIContent* aContent,
                                          EventStates aChangedBits)
 {
   if (!aContent->IsElement()) {
     return NS_OK;
@@ -291,28 +291,20 @@ ServoRestyleManager::AttributeWillChange
                                          int32_t aNameSpaceID,
                                          nsIAtom* aAttribute, int32_t aModType,
                                          const nsAttrValue* aNewValue)
 {
   ServoElementSnapshot* snapshot = SnapshotForElement(aElement);
   snapshot->AddAttrs(aElement);
 }
 
-void
-ServoRestyleManager::AttributeChanged(Element* aElement, int32_t aNameSpaceID,
-                                      nsIAtom* aAttribute, int32_t aModType,
-                                      const nsAttrValue* aOldValue)
-{
-  NS_ERROR("stylo: ServoRestyleManager::AttributeChanged not implemented");
-}
-
 nsresult
 ServoRestyleManager::ReparentStyleContext(nsIFrame* aFrame)
 {
-  NS_ERROR("stylo: ServoRestyleManager::ReparentStyleContext not implemented");
+  NS_WARNING("stylo: ServoRestyleManager::ReparentStyleContext not implemented");
   return NS_OK;
 }
 
 ServoElementSnapshot*
 ServoRestyleManager::SnapshotForElement(Element* aElement)
 {
   ServoElementSnapshot* snapshot = mModifiedElements.LookupOrAdd(aElement);
   if (!snapshot->HasAny(
--- a/layout/base/ServoRestyleManager.h
+++ b/layout/base/ServoRestyleManager.h
@@ -57,19 +57,22 @@ public:
                         nsIContent* aFollowingSibling);
   nsresult ContentStateChanged(nsIContent* aContent,
                                EventStates aStateMask);
   void AttributeWillChange(dom::Element* aElement,
                            int32_t aNameSpaceID,
                            nsIAtom* aAttribute,
                            int32_t aModType,
                            const nsAttrValue* aNewValue);
+
+  // XXXbholley: We should assert that the element is already snapshotted.
   void AttributeChanged(dom::Element* aElement, int32_t aNameSpaceID,
                         nsIAtom* aAttribute, int32_t aModType,
-                        const nsAttrValue* aOldValue);
+                        const nsAttrValue* aOldValue) {}
+
   nsresult ReparentStyleContext(nsIFrame* aFrame);
 
   bool HasPendingRestyles() { return !mModifiedElements.IsEmpty(); }
 
 protected:
   ~ServoRestyleManager() {}
 
 private:
--- a/layout/base/nsCSSFrameConstructor.cpp
+++ b/layout/base/nsCSSFrameConstructor.cpp
@@ -2686,17 +2686,17 @@ nsCSSFrameConstructor::ConstructRootFram
 
   // Set up our style rule observer.
   // XXXbz wouldn't this make more sense as part of presshell init?
   if (styleSet->IsGecko()) {
     // XXXheycam We don't support XBL bindings providing style to
     // ServoStyleSets yet.
     styleSet->AsGecko()->SetBindingManager(mDocument->BindingManager());
   } else {
-    NS_ERROR("stylo: cannot get ServoStyleSheets from XBL bindings yet");
+    NS_WARNING("stylo: cannot get ServoStyleSheets from XBL bindings yet. See bug 1290276.");
   }
 
   // --------- BUILD VIEWPORT -----------
   RefPtr<nsStyleContext> viewportPseudoStyle =
     styleSet->ResolveAnonymousBoxStyle(nsCSSAnonBoxes::viewport, nullptr);
   ViewportFrame* viewportFrame =
     NS_NewViewportFrame(mPresShell, viewportPseudoStyle);
 
--- a/layout/base/nsDocumentViewer.cpp
+++ b/layout/base/nsDocumentViewer.cpp
@@ -2342,17 +2342,18 @@ nsDocumentViewer::CreateStyleSet(nsIDocu
       for (StyleSheetHandle sheet : *sheetService->AgentStyleSheets()) {
         styleSet->AppendStyleSheet(SheetType::Agent, sheet);
       }
       for (StyleSheetHandle sheet : Reversed(*sheetService->UserStyleSheets())) {
         styleSet->PrependStyleSheet(SheetType::User, sheet);
       }
     }
   } else {
-    NS_ERROR("stylo: nsStyleSheetService doesn't handle ServoStyleSheets yet");
+    NS_WARNING("stylo: Not yet checking nsStyleSheetService for Servo-backed "
+               "documents. See bug 1290224");
   }
 
   // Caller will handle calling EndUpdate, per contract.
   return styleSet;
 }
 
 NS_IMETHODIMP
 nsDocumentViewer::ClearHistoryEntry()
--- a/layout/base/nsPresContext.cpp
+++ b/layout/base/nsPresContext.cpp
@@ -1862,18 +1862,18 @@ nsPresContext::MediaFeatureValuesChanged
   if (mShell) {
     // XXXheycam ServoStyleSets don't support responding to medium
     // changes yet.
     if (mShell->StyleSet()->IsGecko()) {
       if (mShell->StyleSet()->AsGecko()->MediumFeaturesChanged()) {
         aRestyleHint |= eRestyle_Subtree;
       }
     } else {
-      NS_ERROR("stylo: ServoStyleSets don't support responding to medium "
-               "changes yet");
+      NS_WARNING("stylo: ServoStyleSets don't support responding to medium "
+                 "changes yet. See bug 1290228.");
     }
   }
 
   if (mUsesViewportUnits && mPendingViewportChange) {
     // Rebuild all style data without rerunning selector matching.
     aRestyleHint |= eRestyle_ForceDescendants;
   }
 
--- a/layout/base/nsPresShell.cpp
+++ b/layout/base/nsPresShell.cpp
@@ -4227,18 +4227,18 @@ PresShell::DocumentStatesChanged(nsIDocu
   NS_PRECONDITION(!mIsDocumentGone, "Unexpected DocumentStatesChanged");
   NS_PRECONDITION(aDocument == mDocument, "Unexpected aDocument");
 
   nsStyleSet* styleSet = mStyleSet->GetAsGecko();
   if (!styleSet) {
     // XXXheycam ServoStyleSets don't support document state selectors,
     // but these are only used in chrome documents, which we are not
     // aiming to support yet.
-    NS_ERROR("stylo: ServoStyleSets cannot respond to document state "
-             "changes yet");
+    NS_WARNING("stylo: ServoStyleSets cannot respond to document state "
+               "changes yet (only matters for chrome documents). See bug 1290285.");
     return;
   }
 
   if (mDidInitialize &&
       styleSet->HasDocumentStateDependentStyle(mDocument->GetRootElement(),
                                                aStateMask)) {
     mPresContext->RestyleManager()->PostRestyleEvent(mDocument->GetRootElement(),
                                                      eRestyle_Subtree,
--- a/layout/style/Loader.cpp
+++ b/layout/style/Loader.cpp
@@ -1289,17 +1289,17 @@ Loader::PrepareSheet(StyleSheetHandle aS
                      nsMediaList* aMediaList,
                      Element* aScopeElement,
                      bool isAlternate)
 {
   NS_PRECONDITION(aSheet, "Must have a sheet!");
 
   // XXXheycam Need to set media, title, etc. on ServoStyleSheets.
   if (aSheet->IsServo()) {
-    NS_ERROR("stylo: should set metadata on ServoStyleSheets");
+    NS_WARNING("stylo: should set metadata on ServoStyleSheets. See bug 1290209.");
     return;
   }
 
   CSSStyleSheet* sheet = aSheet->AsGecko();
 
   RefPtr<nsMediaList> mediaList(aMediaList);
 
   if (!aMediaString.IsEmpty()) {
@@ -1971,17 +1971,17 @@ Loader::DoSheetComplete(SheetLoadData* a
                                            aLoadData->mSheet->GetReferrerPolicy());
         NS_ASSERTION(sheet->IsComplete(),
                      "Should only be caching complete sheets");
         mSheets->mCompleteSheets.Put(&key, sheet);
 #ifdef MOZ_XUL
       }
 #endif
     } else {
-      NS_ERROR("stylo: not caching ServoStyleSheet");
+      NS_WARNING("stylo: Stylesheet caching not yet supported - see bug 1290218.");
     }
   }
 
   NS_RELEASE(aLoadData);  // this will release parents and siblings and all that
 }
 
 nsresult
 Loader::LoadInlineStyle(nsIContent* aElement,
--- a/layout/style/ServoStyleSet.cpp
+++ b/layout/style/ServoStyleSet.cpp
@@ -373,27 +373,27 @@ ServoStyleSet::ProbePseudoElementStyle(E
   }
   return ProbePseudoElementStyle(aParentElement, aType, aParentContext);
 }
 
 nsRestyleHint
 ServoStyleSet::HasStateDependentStyle(dom::Element* aElement,
                                       EventStates aStateMask)
 {
-  NS_ERROR("stylo: HasStateDependentStyle not implemented");
+  NS_WARNING("stylo: HasStateDependentStyle always returns zero!");
   return nsRestyleHint(0);
 }
 
 nsRestyleHint
 ServoStyleSet::HasStateDependentStyle(dom::Element* aElement,
                                       CSSPseudoElementType aPseudoType,
                                      dom::Element* aPseudoElement,
                                      EventStates aStateMask)
 {
-  NS_ERROR("stylo: HasStateDependentStyle not implemented");
+  NS_WARNING("stylo: HasStateDependentStyle always returns zero!");
   return nsRestyleHint(0);
 }
 
 nsRestyleHint
 ServoStyleSet::ComputeRestyleHint(dom::Element* aElement,
                                   ServoElementSnapshot* aSnapshot)
 {
   return Servo_ComputeRestyleHint(aElement, aSnapshot, mRawSet.get());
--- a/layout/xul/test/test_windowminmaxsize.xul
+++ b/layout/xul/test/test_windowminmaxsize.xul
@@ -194,17 +194,22 @@ function nextPopupTest(panel)
     setattr("maxwidth");
     setattr("maxheight");
 
     // Remove the flexibility as it causes the resizer to not shrink down
     // when resizing.
     if ("last" in popupTests[gTestId])
       document.getElementById("popupresizer").removeAttribute("flex");
 
-    panel.openPopup();
+    // Prevent event loop starvation as a result of popup events being
+    // synchronous. See bug 1131576.
+    SimpleTest.executeSoon(() => {
+      // Non-chrome shells require focus to open a popup.
+      SimpleTest.waitForFocus(() => { panel.openPopup() });
+    });
   }
 }
 
 function titledPanelWindowOpened(panelwindow)
 {
   var panel = panelwindow.document.documentElement.firstChild;
   panel.openPopup();
   panel.addEventListener("popupshown", () => doTitledPanelTest(panel), false);
--- a/media/libpng/pnglibconf.h
+++ b/media/libpng/pnglibconf.h
@@ -1,35 +1,38 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 #ifndef PNGLCONF_H
 #define PNGLCONF_H
 
-/* limit image dimensions (bug #251381, #591822, and #967656) */
-#ifndef MOZ_PNG_MAX_DIMENSION
-# define MOZ_PNG_MAX_DIMENSION 32767
+/* Limit image dimensions (bug #251381, #591822, #967656, and #1283961) */
+#ifndef MOZ_PNG_MAX_WIDTH
+#  define MOZ_PNG_MAX_WIDTH 0x7fffffffL /* Unlimited */
+#endif
+#ifndef MOZ_PNG_MAX_HEIGHT
+#  define MOZ_PNG_MAX_HEIGHT 0x7fffffffL /* Unlimited */
 #endif
 
 #define PNG_API_RULE 0
 #define PNG_COST_SHIFT 3
 #define PNG_GAMMA_THRESHOLD_FIXED 5000
 #define PNG_IDAT_READ_SIZE PNG_ZBUF_SIZE
 #define PNG_INFLATE_BUF_SIZE 1024
 #define PNG_LINKAGE_API extern
 #define PNG_LINKAGE_CALLBACK extern
 #define PNG_LINKAGE_DATA extern
 #define PNG_LINKAGE_FUNCTION extern
 #define PNG_MAX_GAMMA_8 11
 #define PNG_sRGB_PROFILE_CHECKS -1
 #define PNG_USER_CHUNK_CACHE_MAX 128
 #define PNG_USER_CHUNK_MALLOC_MAX 4000000L
-#define PNG_USER_HEIGHT_MAX MOZ_PNG_MAX_DIMENSION
-#define PNG_USER_WIDTH_MAX MOZ_PNG_MAX_DIMENSION
+#define PNG_USER_HEIGHT_MAX MOZ_PNG_MAX_WIDTH
+#define PNG_USER_WIDTH_MAX MOZ_PNG_MAX_HEIGHT
 #define PNG_WEIGHT_SHIFT 8
 #define PNG_ZBUF_SIZE 8192
 #define PNG_Z_DEFAULT_COMPRESSION (-1)
 #define PNG_Z_DEFAULT_NOFILTER_STRATEGY 0
 #define PNG_Z_DEFAULT_STRATEGY 1
 
 #ifdef _MSC_VER
 /* The PNG_PEDANTIC_WARNINGS (attributes) fail to build with some MSC
--- a/modules/libpref/init/all.js
+++ b/modules/libpref/init/all.js
@@ -4564,17 +4564,16 @@ pref("gfx.content.use-native-pushlayer",
 #ifdef ANDROID
 pref("gfx.apitrace.enabled",false);
 #endif
 
 #ifdef MOZ_X11
 pref("gfx.content.use-native-pushlayer", true);
 #ifdef MOZ_WIDGET_GTK
 pref("gfx.xrender.enabled",false);
-pref("widget.allow-gtk-dark-theme", false);
 #endif
 #endif
 
 #ifdef XP_WIN
 pref("gfx.content.use-native-pushlayer", true);
 
 // Whether to disable the automatic detection and use of direct2d.
 pref("gfx.direct2d.disabled", false);
--- a/netwerk/protocol/http/nsHttpChannelAuthProvider.cpp
+++ b/netwerk/protocol/http/nsHttpChannelAuthProvider.cpp
@@ -1285,16 +1285,23 @@ NS_IMETHODIMP nsHttpChannelAuthProvider:
     if (!mAuthChannel)
         return NS_OK;
 
     if (userCancel) {
         if (!mRemainingChallenges.IsEmpty()) {
             // there are still some challenges to process, do so
             nsresult rv;
 
+            // Get rid of current continuationState to avoid reusing it in
+            // next challenges since it is no longer relevant.
+            if (mProxyAuth) {
+                NS_IF_RELEASE(mProxyAuthContinuationState);
+            } else {
+                NS_IF_RELEASE(mAuthContinuationState);
+            }
             nsAutoCString creds;
             rv = GetCredentials(mRemainingChallenges.get(), mProxyAuth, creds);
             if (NS_SUCCEEDED(rv)) {
                 // GetCredentials loaded the credentials from the cache or
                 // some other way in a synchronous manner, process those
                 // credentials now
                 mRemainingChallenges.Truncate();
                 return ContinueOnAuthAvailable(creds);
--- a/python/mozbuild/mozbuild/compilation/codecomplete.py
+++ b/python/mozbuild/mozbuild/compilation/codecomplete.py
@@ -42,17 +42,22 @@ class Introspection(MachCommandBase):
             path_arg.relpath())
 
         if make_dir is None and make_target is None:
             return 1
 
         build_vars = util.get_build_vars(make_dir, self)
 
         if what.endswith('.c'):
+            cc = 'CC'
             name = 'COMPILE_CFLAGS'
         else:
+            cc = 'CXX'
             name = 'COMPILE_CXXFLAGS'
 
         if name not in build_vars:
             return
 
+        # Drop the first flag since that is the pathname of the compiler.
+        flags = (shell_split(build_vars[cc]) + shell_split(build_vars[name]))[1:]
+
         print(' '.join(shell_quote(arg)
-                       for arg in util.sanitize_cflags(shell_split(build_vars[name]))))
+                       for arg in util.sanitize_cflags(flags)))
--- a/security/sandbox/linux/SandboxFilter.cpp
+++ b/security/sandbox/linux/SandboxFilter.cpp
@@ -669,16 +669,21 @@ public:
     case __NR_clone:
       return ClonePolicy(Error(EPERM));
 
 #ifdef __NR_fadvise64
     case __NR_fadvise64:
       return Allow();
 #endif
 
+#ifdef __NR_fadvise64_64
+    case __NR_fadvise64_64:
+      return Allow();
+#endif
+
     case __NR_fallocate:
       return Allow();
 
     case __NR_get_mempolicy:
       return Allow();
 
 #endif // DESKTOP
 
--- a/security/sandbox/mac/Sandbox.mm
+++ b/security/sandbox/mac/Sandbox.mm
@@ -453,20 +453,16 @@ static const char contentSandboxRules[] 
   "      (iokit-user-client-class \"AppleGraphicsControlClient\")\n"
   "      (iokit-user-client-class \"AppleGraphicsPolicyClient\"))\n"
   "\n"
   "; bug 1153809\n"
   "  (allow iokit-open\n"
   "      (iokit-user-client-class \"NVDVDContextTesla\")\n"
   "      (iokit-user-client-class \"Gen6DVDContext\"))\n"
   "\n"
-  "; bug 1190032\n"
-  "  (allow file*\n"
-  "      (home-regex \"/Library/Caches/TemporaryItems/plugtmp.*\"))\n"
-  "\n"
   "; bug 1201935\n"
   "  (allow file-read*\n"
   "      (home-subpath \"/Library/Caches/TemporaryItems\"))\n"
   "\n"
   "; bug 1237847\n"
   "  (allow file-read*\n"
   "      (subpath appTempDir))\n"
   "  (allow file-write*\n"
--- a/services/sync/locales/en-US/sync.properties
+++ b/services/sync/locales/en-US/sync.properties
@@ -1,14 +1,14 @@
 # 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/.
 
 # %1: the user name (Ed), %2: the app name (Firefox), %3: the operating system (Android)
-client.name2 = %1$S's %2$S on %3$S
+client.name2 = %1$S’s %2$S on %3$S
 
 # %S is the date and time at which the last sync successfully completed
 lastSync2.label = Last sync: %S
 
 # signInToSync.description is the tooltip for the Sync buttons when Sync is
 # not configured.
 signInToSync.description = Sign In To Sync
 
--- a/services/sync/modules/addonsreconciler.js
+++ b/services/sync/modules/addonsreconciler.js
@@ -429,17 +429,18 @@ AddonsReconciler.prototype = {
       let record = {
         id: id,
         guid: guid,
         enabled: enabled,
         installed: true,
         modified: now,
         type: addon.type,
         scope: addon.scope,
-        foreignInstall: addon.foreignInstall
+        foreignInstall: addon.foreignInstall,
+        isSyncable: addon.isSyncable,
       };
       this._addons[id] = record;
       this._log.debug("Adding change because add-on not present locally: " +
                       id);
       this._addChange(now, CHANGE_INSTALLED, record);
       return;
     }
 
--- a/services/sync/modules/addonutils.js
+++ b/services/sync/modules/addonutils.js
@@ -369,16 +369,19 @@ AddonUtilsInternal.prototype = {
     if (!addon.sourceURI) {
       this._log.info("Skipping install of add-on because missing " +
                      "sourceURI: " + addon.id);
       return false;
     }
     // Verify that the source URI uses TLS. We don't allow installs from
     // insecure sources for security reasons. The Addon Manager ensures
     // that cert validation etc is performed.
+    // (We should also consider just dropping this entirely and calling
+    // XPIProvider.isInstallAllowed, but that has additional semantics we might
+    // need to think through...)
     let requireSecureURI = true;
     if (options && options.requireSecureURI !== undefined) {
       requireSecureURI = options.requireSecureURI;
     }
 
     if (requireSecureURI) {
       let scheme = addon.sourceURI.scheme;
       if (scheme != "https") {
--- a/services/sync/modules/engines/addons.js
+++ b/services/sync/modules/engines/addons.js
@@ -20,20 +20,23 @@
  * We currently synchronize:
  *
  *  - Installations
  *  - Uninstallations
  *  - User enabling and disabling
  *
  * Synchronization is influenced by the following preferences:
  *
- *  - services.sync.addons.ignoreRepositoryChecking
  *  - services.sync.addons.ignoreUserEnabledChanges
  *  - services.sync.addons.trustedSourceHostnames
  *
+ *  and also influenced by whether addons have repository caching enabled and
+ *  whether they allow installation of addons from insecure options (both of
+ *  which are themselves influenced by the "extensions." pref branch)
+ *
  * See the documentation in services-sync.js for the behavior of these prefs.
  */
 "use strict";
 
 var {classes: Cc, interfaces: Ci, utils: Cu} = Components;
 
 Cu.import("resource://services-sync/addonutils.js");
 Cu.import("resource://services-sync/addonsreconciler.js");
@@ -273,30 +276,38 @@ AddonsStore.prototype = {
       // is our current policy.
       if (record.source != "amo") {
         this._log.info("Ignoring unknown add-on source (" + record.source + ")" +
                        " for " + record.id);
         return;
       }
     }
 
+    // Ignore incoming records for which an existing non-syncable addon
+    // exists.
+    let existingMeta = this.reconciler.addons[record.addonID];
+    if (existingMeta && !this.isAddonSyncable(existingMeta)) {
+      this._log.info("Ignoring incoming record for an existing but non-syncable addon", record.addonID);
+      return;
+    }
+
     Store.prototype.applyIncoming.call(this, record);
   },
 
 
   /**
    * Provides core Store API to create/install an add-on from a record.
    */
   create: function create(record) {
     let cb = Async.makeSpinningCallback();
     AddonUtils.installAddons([{
       id:               record.addonID,
       syncGUID:         record.id,
       enabled:          record.enabled,
-      requireSecureURI: !Svc.Prefs.get("addons.ignoreRepositoryChecking", false),
+      requireSecureURI: this._extensionsPrefs.get("install.requireSecureOrigin", true),
     }], cb);
 
     // This will throw if there was an error. This will get caught by the sync
     // engine and the record will try to be applied later.
     let results = cb.wait();
 
     if (results.skipped.includes(record.addonID)) {
       this._log.info("Add-on skipped: " + record.addonID);
@@ -526,17 +537,20 @@ AddonsStore.prototype = {
    */
   isAddonSyncable: function isAddonSyncable(addon) {
     // Currently, we limit syncable add-ons to those that are:
     //   1) In a well-defined set of types
     //   2) Installed in the current profile
     //   3) Not installed by a foreign entity (i.e. installed by the app)
     //      since they act like global extensions.
     //   4) Is not a hotfix.
-    //   5) Are installed from AMO
+    //   5) The addons XPIProvider doesn't veto it (i.e not being installed in
+    //      the profile directory, or any other reasons it says the addon can't
+    //      be synced)
+    //   6) Are installed from AMO
 
     // We could represent the test as a complex boolean expression. We go the
     // verbose route so the failure reason is logged.
     if (!addon) {
       this._log.debug("Null object passed to isAddonSyncable.");
       return false;
     }
 
@@ -546,35 +560,45 @@ AddonsStore.prototype = {
       return false;
     }
 
     if (!(addon.scope & AddonManager.SCOPE_PROFILE)) {
       this._log.debug(addon.id + " not syncable: not installed in profile.");
       return false;
     }
 
+    // If the addon manager says it's not syncable, we skip it.
+    if (!addon.isSyncable) {
+      this._log.debug(addon.id + " not syncable: vetoed by the addon manager.");
+      return false;
+    }
+
     // This may be too aggressive. If an add-on is downloaded from AMO and
     // manually placed in the profile directory, foreignInstall will be set.
     // Arguably, that add-on should be syncable.
     // TODO Address the edge case and come up with more robust heuristics.
     if (addon.foreignInstall) {
       this._log.debug(addon.id + " not syncable: is foreign install.");
       return false;
     }
 
     // Ignore hotfix extensions (bug 741670). The pref may not be defined.
+    // XXX - note that addon.isSyncable will be false for hotfix addons, so
+    // this check isn't strictly necessary - except for Sync tests which aren't
+    // setup to create a "real" hotfix addon. This can be removed once those
+    // tests are fixed (but keeping it doesn't hurt either)
     if (this._extensionsPrefs.get("hotfix.id", null) == addon.id) {
       this._log.debug(addon.id + " not syncable: is a hotfix.");
       return false;
     }
 
-    // We provide a back door to skip the repository checking of an add-on.
-    // This is utilized by the tests to make testing easier. Users could enable
-    // this, but it would sacrifice security.
-    if (Svc.Prefs.get("addons.ignoreRepositoryChecking", false)) {
+    // If the AddonRepository's cache isn't enabled (which it typically isn't
+    // in tests), getCachedAddonByID always returns null - so skip the check
+    // in that case.
+    if (!AddonRepository.cacheEnabled) {
       return true;
     }
 
     let cb = Async.makeSyncCallback();
     AddonRepository.getCachedAddonByID(addon.id, cb);
     let result = Async.waitForSyncCallback(cb);
 
     if (!result) {
--- a/services/sync/modules/policies.js
+++ b/services/sync/modules/policies.js
@@ -16,16 +16,23 @@ Cu.import("resource://services-sync/util
 Cu.import("resource://services-common/logmanager.js");
 Cu.import("resource://services-common/async.js");
 
 XPCOMUtils.defineLazyModuleGetter(this, "Status",
                                   "resource://services-sync/status.js");
 XPCOMUtils.defineLazyModuleGetter(this, "AddonManager",
                                   "resource://gre/modules/AddonManager.jsm");
 
+// Get the value for an interval that's stored in preferences. To save users
+// from themselves (and us from them!) the minimum time they can specify
+// is 60s.
+function getThrottledIntervalPreference(prefName) {
+  return Math.max(Svc.Prefs.get(prefName), 60) * 1000;
+}
+
 this.SyncScheduler = function SyncScheduler(service) {
   this.service = service;
   this.init();
 }
 SyncScheduler.prototype = {
   _log: Log.repository.getLogger("Sync.SyncScheduler"),
 
   _fatalLoginStatus: [LOGIN_FAILED_NO_USERNAME,
@@ -43,22 +50,22 @@ SyncScheduler.prototype = {
     this._log.trace("Setting SyncScheduler policy values to defaults.");
 
     let service = Cc["@mozilla.org/weave/service;1"]
                     .getService(Ci.nsISupports)
                     .wrappedJSObject;
 
     let part = service.fxAccountsEnabled ? "fxa" : "sync11";
     let prefSDInterval = "scheduler." + part + ".singleDeviceInterval";
-    this.singleDeviceInterval = Svc.Prefs.get(prefSDInterval) * 1000;
+    this.singleDeviceInterval = getThrottledIntervalPreference(prefSDInterval);
 
-    this.idleInterval         = Svc.Prefs.get("scheduler.idleInterval")         * 1000;
-    this.activeInterval       = Svc.Prefs.get("scheduler.activeInterval")       * 1000;
-    this.immediateInterval    = Svc.Prefs.get("scheduler.immediateInterval")    * 1000;
-    this.eolInterval          = Svc.Prefs.get("scheduler.eolInterval")          * 1000;
+    this.idleInterval         = getThrottledIntervalPreference("scheduler.idleInterval");
+    this.activeInterval       = getThrottledIntervalPreference("scheduler.activeInterval");
+    this.immediateInterval    = getThrottledIntervalPreference("scheduler.immediateInterval");
+    this.eolInterval          = getThrottledIntervalPreference("scheduler.eolInterval");
 
     // A user is non-idle on startup by default.
     this.idle = false;
 
     this.hasIncomingItems = false;
 
     this.clearSyncTriggers();
   },
--- a/services/sync/services-sync.js
+++ b/services/sync/services-sync.js
@@ -33,19 +33,16 @@ pref("services.sync.engine.tabs", true);
 pref("services.sync.engine.tabs.filteredUrls", "^(about:.*|chrome://weave/.*|wyciwyg:.*|file:.*|blob:.*)$");
 
 pref("services.sync.jpake.serverURL", "https://setup.services.mozilla.com/");
 pref("services.sync.jpake.pollInterval", 1000);
 pref("services.sync.jpake.firstMsgMaxTries", 300); // 5 minutes
 pref("services.sync.jpake.lastMsgMaxTries", 300);  // 5 minutes
 pref("services.sync.jpake.maxTries", 10);
 
-// Allow add-ons to be synced from non-trusted sources.
-pref("services.sync.addons.ignoreRepositoryChecking", false);
-
 // If true, add-on sync ignores changes to the user-enabled flag. This
 // allows people to have the same set of add-ons installed across all
 // profiles while maintaining different enabled states.
 pref("services.sync.addons.ignoreUserEnabledChanges", false);
 
 // Comma-delimited list of hostnames to trust for add-on install.
 pref("services.sync.addons.trustedSourceHostnames", "addons.mozilla.org");
 
--- a/services/sync/tests/unit/head_helpers.js
+++ b/services/sync/tests/unit/head_helpers.js
@@ -1,14 +1,15 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 Cu.import("resource://services-common/async.js");
 Cu.import("resource://testing-common/services/common/utils.js");
 Cu.import("resource://testing-common/PlacesTestUtils.jsm");
+Cu.import("resource://services-sync/util.js");
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 
 XPCOMUtils.defineLazyGetter(this, 'SyncPingSchema', function() {
   let ns = {};
   Cu.import("resource://gre/modules/FileUtils.jsm", ns);
   let stream = Cc["@mozilla.org/network/file-input-stream;1"]
                .createInstance(Ci.nsIFileInputStream);
   let jsonReader = Cc["@mozilla.org/dom/json;1"]
@@ -385,8 +386,17 @@ function sync_engine_and_validate_telem(
     }
     if (caughtError) {
       Svc.Obs.notify("weave:service:sync:error", caughtError);
     } else {
       Svc.Obs.notify("weave:service:sync:finish");
     }
   });
 }
+
+// Avoid an issue where `client.name2` containing unicode characters causes
+// a number of tests to fail, due to them assuming that we do not need to utf-8
+// encode or decode data sent through the mocked server (see bug 1268912).
+Utils.getDefaultDeviceName = function() {
+  return "Test device name";
+};
+
+
new file mode 100644
--- /dev/null
+++ b/services/sync/tests/unit/systemaddon-search.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<searchresults total_results="1">
+  <addon id="5618">
+  <name>System Add-on Test</name>
+  <type id="1">Extension</type>
+  <guid>system1@tests.mozilla.org</guid>
+  <slug>addon11</slug>
+  <version>1.0</version>
+
+  <compatible_applications><application>
+      <name>Firefox</name>
+      <application_id>1</application_id>
+      <min_version>3.6</min_version>
+      <max_version>*</max_version>
+      <appID>xpcshell@tests.mozilla.org</appID>
+    </application></compatible_applications>
+  <all_compatible_os><os>ALL</os></all_compatible_os>
+
+  <install os="ALL" size="999">http://127.0.0.1:8888/system.xpi</install>
+    <created epoch="1252903662">
+      2009-09-14T04:47:42Z
+    </created>
+    <last_updated epoch="1315255329">
+      2011-09-05T20:42:09Z
+    </last_updated>
+    </addon>
+</searchresults>
--- a/services/sync/tests/unit/test_addon_utils.js
+++ b/services/sync/tests/unit/test_addon_utils.js
@@ -98,18 +98,16 @@ add_test(function test_ignore_untrusted_
 });
 
 add_test(function test_source_uri_rewrite() {
   _("Ensure that a 'src=api' query string is rewritten to 'src=sync'");
 
   // This tests for conformance with bug 708134 so server-side metrics aren't
   // skewed.
 
-  Svc.Prefs.set("addons.ignoreRepositoryChecking", true);
-
   // We resort to monkeypatching because of the API design.
   let oldFunction = AddonUtils.__proto__.installAddonFromSearchResult;
 
   let installCalled = false;
   AddonUtils.__proto__.installAddonFromSearchResult =
     function testInstallAddon(addon, metadata, cb) {
 
     do_check_eq(SERVER_ADDRESS + "/require.xpi?src=sync",
@@ -134,11 +132,10 @@ add_test(function test_source_uri_rewrit
     requireSecureURI: false,
   }
   AddonUtils.installAddons([installOptions], installCallback);
 
   installCallback.wait();
   do_check_true(installCalled);
   AddonUtils.__proto__.installAddonFromSearchResult = oldFunction;
 
-  Svc.Prefs.reset("addons.ignoreRepositoryChecking");
   server.stop(run_next_test);
 });
--- a/services/sync/tests/unit/test_addons_engine.js
+++ b/services/sync/tests/unit/test_addons_engine.js
@@ -11,16 +11,17 @@ Cu.import("resource://services-sync/addo
 Cu.import("resource://services-sync/engines/addons.js");
 Cu.import("resource://services-sync/service.js");
 Cu.import("resource://services-sync/util.js");
 Cu.import("resource://testing-common/services/sync/utils.js");
 
 var prefs = new Preferences();
 prefs.set("extensions.getAddons.get.url",
           "http://localhost:8888/search/guid:%IDS%");
+prefs.set("extensions.install.requireSecureOrigin", false);
 
 loadAddonTestFunctions();
 startupManager();
 
 var engineManager = Service.engineManager;
 
 engineManager.register(AddonsEngine);
 var engine = engineManager.get("addons");
@@ -30,18 +31,16 @@ var tracker = engine._tracker;
 function advance_test() {
   reconciler._addons = {};
   reconciler._changes = [];
 
   let cb = Async.makeSpinningCallback();
   reconciler.saveState(null, cb);
   cb.wait();
 
-  Svc.Prefs.reset("addons.ignoreRepositoryChecking");
-
   run_next_test();
 }
 
 // This is a basic sanity test for the unit test itself. If this breaks, the
 // add-ons API likely changed upstream.
 add_test(function test_addon_install() {
   _("Ensure basic add-on APIs work as expected.");
 
@@ -99,17 +98,16 @@ add_test(function test_get_changed_ids()
   do_check_eq("object", typeof(changes));
   do_check_eq(1, Object.keys(changes).length);
   do_check_true(guid1 in changes);
   do_check_eq(changeTime, changes[guid1]);
 
   tracker.clearChangedIDs();
 
   _("Ensure reconciler changes are populated.");
-  Svc.Prefs.set("addons.ignoreRepositoryChecking", true);
   let addon = installAddon("test_bootstrap1_1");
   tracker.clearChangedIDs(); // Just in case.
   changes = engine.getChangedIDs();
   do_check_eq("object", typeof(changes));
   do_check_eq(1, Object.keys(changes).length);
   do_check_true(addon.syncGUID in changes);
   _("Change time: " + changeTime + ", addon change: " + changes[addon.syncGUID]);
   do_check_true(changes[addon.syncGUID] >= changeTime);
@@ -146,19 +144,16 @@ add_test(function test_get_changed_ids()
 });
 
 add_test(function test_disabled_install_semantics() {
   _("Ensure that syncing a disabled add-on preserves proper state.");
 
   // This is essentially a test for bug 712542, which snuck into the original
   // add-on sync drop. It ensures that when an add-on is installed that the
   // disabled state and incoming syncGUID is preserved, even on the next sync.
-
-  Svc.Prefs.set("addons.ignoreRepositoryChecking", true);
-
   const USER       = "foo";
   const PASSWORD   = "password";
   const PASSPHRASE = "abcdeabcdeabcdeabcdeabcdea";
   const ADDON_ID   = "addon1@tests.mozilla.org";
 
   let server = new SyncServer();
   server.start();
   new SyncTestingInfrastructure(server.server, USER, PASSWORD, PASSPHRASE);
--- a/services/sync/tests/unit/test_addons_store.js
+++ b/services/sync/tests/unit/test_addons_store.js
@@ -5,23 +5,43 @@
 
 Cu.import("resource://gre/modules/Log.jsm");
 Cu.import("resource://gre/modules/Preferences.jsm");
 Cu.import("resource://services-sync/addonutils.js");
 Cu.import("resource://services-sync/engines/addons.js");
 Cu.import("resource://services-sync/service.js");
 Cu.import("resource://services-sync/util.js");
 Cu.import("resource://testing-common/services/sync/utils.js");
+Cu.import("resource://gre/modules/FileUtils.jsm");
 
 const HTTP_PORT = 8888;
 
 var prefs = new Preferences();
 
 prefs.set("extensions.getAddons.get.url", "http://localhost:8888/search/guid:%IDS%");
+prefs.set("extensions.install.requireSecureOrigin", false);
+
+const SYSTEM_ADDON_ID = "system1@tests.mozilla.org";
+let systemAddonFile;
+
+// The system add-on must be installed before AddonManager is started.
+function loadSystemAddon() {
+  let addonFilename = SYSTEM_ADDON_ID + ".xpi";
+  const distroDir = FileUtils.getDir("ProfD", ["sysfeatures", "app0"], true);
+  do_get_file(ExtensionsTestPath("/data/system_addons/system1_1.xpi")).copyTo(distroDir, addonFilename);
+  systemAddonFile = FileUtils.File(distroDir.path);
+  systemAddonFile.append(addonFilename);
+  systemAddonFile.lastModifiedTime = Date.now();
+  // As we're not running in application, we need to setup the features directory
+  // used by system add-ons.
+  registerDirectory("XREAppFeat", distroDir);
+}
+
 loadAddonTestFunctions();
+loadSystemAddon();
 startupManager();
 
 Service.engineManager.register(AddonsEngine);
 var engine     = Service.engineManager.get("addons");
 var tracker    = engine._tracker;
 var store      = engine._store;
 var reconciler = engine._reconciler;
 
@@ -52,29 +72,34 @@ function createAndStartHTTPServer(port) 
 
     server.registerFile("/search/guid:bootstrap1%40tests.mozilla.org",
                         do_get_file("bootstrap1-search.xml"));
     server.registerFile("/bootstrap1.xpi", do_get_file(bootstrap1XPI));
 
     server.registerFile("/search/guid:missing-xpi%40tests.mozilla.org",
                         do_get_file("missing-xpi-search.xml"));
 
+    server.registerFile("/search/guid:system1%40tests.mozilla.org",
+                        do_get_file("systemaddon-search.xml"));
+    server.registerFile("/system.xpi", systemAddonFile);
+
     server.start(port);
 
     return server;
   } catch (ex) {
     _("Got exception starting HTTP server on port " + port);
     _("Error: " + Log.exceptionStr(ex));
     do_throw(ex);
   }
 }
 
 function run_test() {
   initTestLogging("Trace");
   Log.repository.getLogger("Sync.Engine.Addons").level = Log.Level.Trace;
+  Log.repository.getLogger("Sync.Tracker.Addons").level = Log.Level.Trace;
   Log.repository.getLogger("Sync.AddonsRepository").level =
     Log.Level.Trace;
 
   reconciler.startListening();
 
   // Don't flush to disk in the middle of an event listener!
   // This causes test hangs on WinXP.
   reconciler._shouldPersist = false;
@@ -189,41 +214,44 @@ add_test(function test_apply_uninstall()
   do_check_eq(null, addon);
 
   run_next_test();
 });
 
 add_test(function test_addon_syncability() {
   _("Ensure isAddonSyncable functions properly.");
 
-  Svc.Prefs.set("addons.ignoreRepositoryChecking", true);
   Svc.Prefs.set("addons.trustedSourceHostnames",
                 "addons.mozilla.org,other.example.com");
 
   do_check_false(store.isAddonSyncable(null));
 
   let addon = installAddon("test_bootstrap1_1");
   do_check_true(store.isAddonSyncable(addon));
 
   let dummy = {};
-  const KEYS = ["id", "syncGUID", "type", "scope", "foreignInstall"];
+  const KEYS = ["id", "syncGUID", "type", "scope", "foreignInstall", "isSyncable"];
   for (let k of KEYS) {
     dummy[k] = addon[k];
   }
 
   do_check_true(store.isAddonSyncable(dummy));
 
   dummy.type = "UNSUPPORTED";
   do_check_false(store.isAddonSyncable(dummy));
   dummy.type = addon.type;
 
   dummy.scope = 0;
   do_check_false(store.isAddonSyncable(dummy));
   dummy.scope = addon.scope;
 
+  dummy.isSyncable = false;
+  do_check_false(store.isAddonSyncable(dummy));
+  dummy.isSyncable = addon.isSyncable;
+
   dummy.foreignInstall = true;
   do_check_false(store.isAddonSyncable(dummy));
   dummy.foreignInstall = false;
 
   uninstallAddon(addon);
 
   do_check_false(store.isSourceURITrusted(null));
 
@@ -263,27 +291,25 @@ add_test(function test_addon_syncability
   Svc.Prefs.reset("addons.trustedSourceHostnames");
 
   run_next_test();
 });
 
 add_test(function test_ignore_hotfixes() {
   _("Ensure that hotfix extensions are ignored.");
 
-  Svc.Prefs.set("addons.ignoreRepositoryChecking", true);
-
   // A hotfix extension is one that has the id the same as the
   // extensions.hotfix.id pref.
   let prefs = new Preferences("extensions.");
 
   let addon = installAddon("test_bootstrap1_1");
   do_check_true(store.isAddonSyncable(addon));
 
   let dummy = {};
-  const KEYS = ["id", "syncGUID", "type", "scope", "foreignInstall"];
+  const KEYS = ["id", "syncGUID", "type", "scope", "foreignInstall", "isSyncable"];
   for (let k of KEYS) {
     dummy[k] = addon[k];
   }
 
   // Basic sanity check.
   do_check_true(store.isAddonSyncable(dummy));
 
   prefs.set("hotfix.id", dummy.id);
@@ -296,28 +322,25 @@ add_test(function test_ignore_hotfixes()
   // Need to delete pref before changing type.
   prefSvc.deleteBranch("hotfix.id");
   prefSvc.setIntPref("hotfix.id", 0xdeadbeef);
 
   do_check_true(store.isAddonSyncable(dummy));
 
   uninstallAddon(addon);
 
-  Svc.Prefs.reset("addons.ignoreRepositoryChecking");
   prefs.reset("hotfix.id");
 
   run_next_test();
 });
 
 
 add_test(function test_get_all_ids() {
   _("Ensures that getAllIDs() returns an appropriate set.");
 
-  Svc.Prefs.set("addons.ignoreRepositoryChecking", true);
-
   _("Installing two addons.");
   let addon1 = installAddon("test_install1");
   let addon2 = installAddon("test_bootstrap1_1");
 
   _("Ensure they're syncable.");
   do_check_true(store.isAddonSyncable(addon1));
   do_check_true(store.isAddonSyncable(addon2));
 
@@ -326,17 +349,16 @@ add_test(function test_get_all_ids() {
   do_check_eq("object", typeof(ids));
   do_check_eq(2, Object.keys(ids).length);
   do_check_true(addon1.syncGUID in ids);
   do_check_true(addon2.syncGUID in ids);
 
   addon1.install.cancel();
   uninstallAddon(addon2);
 
-  Svc.Prefs.reset("addons.ignoreRepositoryChecking");
   run_next_test();
 });
 
 add_test(function test_change_item_id() {
   _("Ensures that changeItemID() works properly.");
 
   let addon = installAddon("test_bootstrap1_1");
 
@@ -352,19 +374,16 @@ add_test(function test_change_item_id() 
   uninstallAddon(newAddon);
 
   run_next_test();
 });
 
 add_test(function test_create() {
   _("Ensure creating/installing an add-on from a record works.");
 
-  // Set this so that getInstallFromSearchResult doesn't end up
-  // failing the install due to an insecure source URI scheme.
-  Svc.Prefs.set("addons.ignoreRepositoryChecking", true);
   let server = createAndStartHTTPServer(HTTP_PORT);
 
   let addon = installAddon("test_bootstrap1_1");
   let id = addon.id;
   uninstallAddon(addon);
 
   let guid = Utils.makeGUID();
   let record = createRecordForThisApp(guid, id, true, false);
@@ -374,17 +393,16 @@ add_test(function test_create() {
 
   let newAddon = getAddonFromAddonManagerByID(id);
   do_check_neq(null, newAddon);
   do_check_eq(guid, newAddon.syncGUID);
   do_check_false(newAddon.userDisabled);
 
   uninstallAddon(newAddon);
 
-  Svc.Prefs.reset("addons.ignoreRepositoryChecking");
   server.stop(run_next_test);
 });
 
 add_test(function test_create_missing_search() {
   _("Ensures that failed add-on searches are handled gracefully.");
 
   let server = createAndStartHTTPServer(HTTP_PORT);
 
@@ -411,66 +429,110 @@ add_test(function test_create_bad_instal
   // The handler returns a search result but the XPI will 404.
   const id = "missing-xpi@tests.mozilla.org";
   let guid = Utils.makeGUID();
   let record = createRecordForThisApp(guid, id, true, false);
 
   let failed = store.applyIncomingBatch([record]);
   // This addon had no source URI so was skipped - but it's not treated as
   // failure.
-  do_check_eq(0, failed.length);
+  // XXX - this test isn't testing what we thought it was. Previously the addon
+  // was not being installed due to requireSecureURL checking *before* we'd
+  // attempted to get the XPI.
+  // With requireSecureURL disabled we do see a download failure, but the addon
+  // *does* get added to |failed|.
+  // FTR: onDownloadFailed() is called with ERROR_NETWORK_FAILURE, so it's going
+  // to be tricky to distinguish a 404 from other transient network errors
+  // where we do want the addon to end up in |failed|.
+  // This is being tracked in bug 1284778.
+  //do_check_eq(0, failed.length);
 
   let addon = getAddonFromAddonManagerByID(id);
   do_check_eq(null, addon);
 
   server.stop(run_next_test);
 });
 
+add_test(function test_ignore_system() {
+  _("Ensure we ignore system addons");
+  // Our system addon should not appear in getAllIDs
+  engine._refreshReconcilerState();
+  let num = 0;
+  for (let guid in store.getAllIDs()) {
+    num += 1;
+    let addon = reconciler.getAddonStateFromSyncGUID(guid);
+    do_check_neq(addon.id, SYSTEM_ADDON_ID);
+  }
+  do_check_true(num > 1, "should have seen at least one.")
+  run_next_test();
+});
+
+add_test(function test_incoming_system() {
+  _("Ensure we handle incoming records that refer to a system addon");
+  // eg, loop initially had a normal addon but it was then "promoted" to be a
+  // system addon but wanted to keep the same ID. The server record exists due
+  // to this.
+
+  // before we start, ensure the system addon isn't disabled.
+  do_check_false(getAddonFromAddonManagerByID(SYSTEM_ADDON_ID).userDisabled);
+
+  // Now simulate an incoming record with the same ID as the system addon,
+  // but flagged as disabled - it should not be applied.
+  let server = createAndStartHTTPServer(HTTP_PORT);
+  // We make the incoming record flag the system addon as disabled - it should
+  // be ignored.
+  let guid = Utils.makeGUID();
+  let record = createRecordForThisApp(guid, SYSTEM_ADDON_ID, false, false);
+
+  let failed = store.applyIncomingBatch([record]);
+  do_check_eq(0, failed.length);
+
+  // The system addon should still not be userDisabled.
+  do_check_false(getAddonFromAddonManagerByID(SYSTEM_ADDON_ID).userDisabled);
+
+  server.stop(run_next_test);
+});
+
 add_test(function test_wipe() {
   _("Ensures that wiping causes add-ons to be uninstalled.");
 
   let addon1 = installAddon("test_bootstrap1_1");
 
-  Svc.Prefs.set("addons.ignoreRepositoryChecking", true);
   store.wipe();
 
   let addon = getAddonFromAddonManagerByID(addon1.id);
   do_check_eq(null, addon);
 
-  Svc.Prefs.reset("addons.ignoreRepositoryChecking");
-
   run_next_test();
 });
 
 add_test(function test_wipe_and_install() {
   _("Ensure wipe followed by install works.");
 
   // This tests the reset sync flow where remote data is replaced by local. The
   // receiving client will see a wipe followed by a record which should undo
   // the wipe.
   let installed = installAddon("test_bootstrap1_1");
 
   let record = createRecordForThisApp(installed.syncGUID, installed.id, true,
                                       false);
 
-  Svc.Prefs.set("addons.ignoreRepositoryChecking", true);
   store.wipe();
 
   let deleted = getAddonFromAddonManagerByID(installed.id);
   do_check_null(deleted);
 
   // Re-applying the record can require re-fetching the XPI.
   let server = createAndStartHTTPServer(HTTP_PORT);
 
   store.applyIncoming(record);
 
   let fetched = getAddonFromAddonManagerByID(record.addonID);
   do_check_true(!!fetched);
 
-  Svc.Prefs.reset("addons.ignoreRepositoryChecking");
   server.stop(run_next_test);
 });
 
 add_test(function cleanup() {
   // There's an xpcom-shutdown hook for this, but let's give this a shot.
   reconciler.stopListening();
   run_next_test();
 });
--- a/services/sync/tests/unit/test_addons_tracker.js
+++ b/services/sync/tests/unit/test_addons_tracker.js
@@ -6,17 +6,16 @@
 Cu.import("resource://gre/modules/AddonManager.jsm");
 Cu.import("resource://services-sync/engines/addons.js");
 Cu.import("resource://services-sync/constants.js");
 Cu.import("resource://services-sync/service.js");
 Cu.import("resource://services-sync/util.js");
 
 loadAddonTestFunctions();
 startupManager();
-Svc.Prefs.set("addons.ignoreRepositoryChecking", true);
 Svc.Prefs.set("engine.addons", true);
 
 Service.engineManager.register(AddonsEngine);
 var engine     = Service.engineManager.get("addons");
 var reconciler = engine._reconciler;
 var store      = engine._store;
 var tracker    = engine._tracker;
 
--- a/services/sync/tests/unit/test_syncscheduler.js
+++ b/services/sync/tests/unit/test_syncscheduler.js
@@ -143,26 +143,37 @@ add_test(function test_prefAttributes() 
   do_check_eq(scheduler.idleInterval,
               Svc.Prefs.get("scheduler.idleInterval") * 1000);
   do_check_eq(scheduler.activeInterval,
               Svc.Prefs.get("scheduler.activeInterval") * 1000);
   do_check_eq(scheduler.immediateInterval,
               Svc.Prefs.get("scheduler.immediateInterval") * 1000);
 
   _("Custom values for prefs will take effect after a restart.");
-  Svc.Prefs.set("scheduler.sync11.singleDeviceInterval", 42);
-  Svc.Prefs.set("scheduler.idleInterval", 23);
-  Svc.Prefs.set("scheduler.activeInterval", 18);
+  Svc.Prefs.set("scheduler.sync11.singleDeviceInterval", 420);
+  Svc.Prefs.set("scheduler.idleInterval", 230);
+  Svc.Prefs.set("scheduler.activeInterval", 180);
   Svc.Prefs.set("scheduler.immediateInterval", 31415);
   scheduler.setDefaults();
-  do_check_eq(scheduler.idleInterval, 23000);
-  do_check_eq(scheduler.singleDeviceInterval, 42000);
-  do_check_eq(scheduler.activeInterval, 18000);
+  do_check_eq(scheduler.idleInterval, 230000);
+  do_check_eq(scheduler.singleDeviceInterval, 420000);
+  do_check_eq(scheduler.activeInterval, 180000);
   do_check_eq(scheduler.immediateInterval, 31415000);
 
+  _("Custom values for interval prefs can't be less than 60 seconds.");
+  Svc.Prefs.set("scheduler.sync11.singleDeviceInterval", 42);
+  Svc.Prefs.set("scheduler.idleInterval", 50);
+  Svc.Prefs.set("scheduler.activeInterval", 50);
+  Svc.Prefs.set("scheduler.immediateInterval", 10);
+  scheduler.setDefaults();
+  do_check_eq(scheduler.idleInterval, 60000);
+  do_check_eq(scheduler.singleDeviceInterval, 60000);
+  do_check_eq(scheduler.activeInterval, 60000);
+  do_check_eq(scheduler.immediateInterval, 60000);
+
   Svc.Prefs.resetBranch("");
   scheduler.setDefaults();
   run_next_test();
 });
 
 add_identity_test(this, function* test_updateClientMode() {
   _("Test updateClientMode adjusts scheduling attributes based on # of clients appropriately");
   do_check_eq(scheduler.syncThreshold, SINGLE_USER_THRESHOLD);
--- a/services/sync/tests/unit/xpcshell.ini
+++ b/services/sync/tests/unit/xpcshell.ini
@@ -7,16 +7,17 @@ support-files =
   addon1-search.xml
   bootstrap1-search.xml
   fake_login_manager.js
   missing-sourceuri.xml
   missing-xpi-search.xml
   places_v10_from_v11.sqlite
   rewrite-search.xml
   sync_ping_schema.json
+  systemaddon-search.xml
   !/services/common/tests/unit/head_helpers.js
   !/toolkit/mozapps/extensions/test/xpcshell/head_addons.js
 
 # The manifest is roughly ordered from low-level to high-level. When making
 # systemic sweeping changes, this makes it easier to identify errors closer to
 # the source.
 
 # Ensure we can import everything.
--- a/taskcluster/ci/legacy/tasks/builds/sm_base.yml
+++ b/taskcluster/ci/legacy/tasks/builds/sm_base.yml
@@ -1,12 +1,12 @@
 $inherits:
   from: 'tasks/builds/firefox_docker_base.yml'
 task:
-  workerType: spidermonkey
+  workerType: dbg-linux64
 
   routes:
     - 'index.buildbot.branches.{{project}}.sm-plain'
     - 'index.buildbot.revisions.{{head_rev}}.{{project}}.sm-plain'
 
   scopes:
     - 'docker-worker:cache:tooltool-cache'
     - 'docker-worker:cache:level-{{level}}-{{project}}-build-spidermonkey-workspace'
--- a/taskcluster/ci/legacy/tasks/builds/sm_msan.yml
+++ b/taskcluster/ci/legacy/tasks/builds/sm_msan.yml
@@ -9,8 +9,9 @@ task:
       SPIDERMONKEY_VARIANT: 'msan'
       TOOLTOOL_MANIFEST: 'browser/config/tooltool-manifests/linux64/msan.manifest'
   metadata:
     name: '[TC] Spidermonkey Memory Sanitizer'
     description: 'Spidermonkey Memory Sanitizer'
   extra:
     treeherder:
       symbol: msan
+      tier: 3
--- a/taskcluster/ci/legacy/tasks/builds/sm_tsan.yml
+++ b/taskcluster/ci/legacy/tasks/builds/sm_tsan.yml
@@ -9,8 +9,9 @@ task:
       SPIDERMONKEY_VARIANT: 'tsan'
       TOOLTOOL_MANIFEST: 'browser/config/tooltool-manifests/linux64/tsan.manifest'
   metadata:
     name: '[TC] Spidermonkey Thread Sanitizer'
     description: 'Spidermonkey Thread Sanitizer'
   extra:
     treeherder:
       symbol: tsan
+      tier: 3
--- a/taskcluster/ci/legacy/tasks/tests/eslint-gecko.yml
+++ b/taskcluster/ci/legacy/tasks/tests/eslint-gecko.yml
@@ -24,16 +24,17 @@ task:
       - /home/worker/workspace/gecko
       - bash
       - -cx
       - >
           cd /home/worker/workspace/gecko/tools/lint/eslint &&
           /build/tooltool.py fetch -m manifest.tt &&
           tar xvfz eslint.tar.gz &&
           rm eslint.tar.gz &&
+          ln -s ../eslint-plugin-mozilla node_modules &&
           cd ../../.. &&
           tools/lint/eslint/node_modules/.bin/eslint --quiet --plugin html --ext [.js,.jsm,.jsx,.xml,.html] -f tools/lint/eslint-formatter .
 
   extra:
     locations:
         build: null
         tests: null
     treeherder:
--- a/testing/mochitest/runrobocop.py
+++ b/testing/mochitest/runrobocop.py
@@ -222,16 +222,19 @@ class RobocopTestRunner(MochitestDesktop
         self.options.extraPrefs.append('browser.search.suggest.enabled=true')
         self.options.extraPrefs.append('browser.search.suggest.prompted=true')
         self.options.extraPrefs.append('layout.css.devPixelsPerPx=1.0')
         self.options.extraPrefs.append('browser.chrome.dynamictoolbar=false')
         self.options.extraPrefs.append('browser.snippets.enabled=false')
         self.options.extraPrefs.append('browser.casting.enabled=true')
         self.options.extraPrefs.append('extensions.autoupdate.enabled=false')
 
+        # Override the telemetry init delay for integration testing.
+        self.options.extraPrefs.append('toolkit.telemetry.initDelay=1')
+
         self.options.extensionsToExclude.extend([
             'mochikit@mozilla.org',
             'worker-test@mozilla.org.xpi',
             'workerbootstrap-test@mozilla.org.xpi',
             'indexedDB-test@mozilla.org.xpi',
         ])
 
         manifest = MochitestDesktop.buildProfile(self, self.options)
--- a/testing/tps/tps/testrunner.py
+++ b/testing/tps/tps/testrunner.py
@@ -60,20 +60,22 @@ class TPSTestRunner(object):
         'browser.dom.window.dump.enabled': True,
         'browser.sessionstore.resume_from_crash': False,
         'browser.shell.checkDefaultBrowser': False,
         'browser.tabs.warnOnClose': False,
         'browser.warnOnQuit': False,
         # Allow installing extensions dropped into the profile folder
         'extensions.autoDisableScopes': 10,
         'extensions.getAddons.get.url': 'http://127.0.0.1:4567/addons/api/%IDS%.xml',
+        # Our pretend addons server doesn't support metadata...
+        'extensions.getAddons.cache.enabled': False,
+        'extensions.install.requireSecureOrigin': False,
         'extensions.update.enabled': False,
         # Don't open a dialog to show available add-on updates
         'extensions.update.notifyUser': False,
-        'services.sync.addons.ignoreRepositoryChecking': True,
         'services.sync.firstSync': 'notReady',
         'services.sync.lastversion': '1.0',
         'toolkit.startup.max_resumed_crashes': -1,
         # hrm - not sure what the release/beta channels will do?
         'xpinstall.signatures.required': False,
     }
 
     debug_preferences = {
--- a/testing/web-platform/meta/html/semantics/forms/constraints/form-validation-reportValidity.html.ini
+++ b/testing/web-platform/meta/html/semantics/forms/constraints/form-validation-reportValidity.html.ini
@@ -1,13 +1,10 @@
 [form-validation-reportValidity.html]
   type: testharness
-  expected: TIMEOUT
-  disabled:
-    if debug and (os == "mac"): https://bugzilla.mozilla.org/show_bug.cgi?id=1273105
   [[INPUT in TEXT status\] suffering from being too long]
     expected: FAIL
 
   [[INPUT in TEXT status\] suffering from being too long (in a form)]
     expected: FAIL
 
   [[INPUT in SEARCH status\] suffering from being too long]
     expected: FAIL
--- a/toolkit/components/extensions/Extension.jsm
+++ b/toolkit/components/extensions/Extension.jsm
@@ -51,16 +51,17 @@ XPCOMUtils.defineLazyModuleGetter(this, 
                                   "resource://gre/modules/Task.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "AppConstants",
                                   "resource://gre/modules/AppConstants.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "MessageChannel",
                                   "resource://gre/modules/MessageChannel.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "AddonManager",
                                   "resource://gre/modules/AddonManager.jsm");
 
+Cu.import("resource://gre/modules/ExtensionContent.jsm");
 Cu.import("resource://gre/modules/ExtensionManagement.jsm");
 
 const BASE_SCHEMA = "chrome://extensions/content/schemas/manifest.json";
 const CATEGORY_EXTENSION_SCHEMAS = "webextension-schemas";
 const CATEGORY_EXTENSION_SCRIPTS = "webextension-scripts";
 
 Cu.import("resource://gre/modules/ExtensionUtils.jsm");
 var {
@@ -321,17 +322,17 @@ class ProxyContext extends ExtensionCont
 
   get externallyVisible() {
     return false;
   }
 }
 
 function findPathInObject(obj, path) {
   for (let elt of path) {
-    obj = obj[elt];
+    obj = obj[elt] || undefined;
   }
   return obj;
 }
 
 let ParentAPIManager = {
   proxyContexts: new Map(),
 
   init() {
--- a/toolkit/components/extensions/ExtensionContent.jsm
+++ b/toolkit/components/extensions/ExtensionContent.jsm
@@ -626,17 +626,24 @@ DocumentManager = {
       }
       return null;
     };
 
     let promises = Array.from(this.enumerateWindows(global.docShell), executeInWin)
                         .filter(promise => promise);
 
     if (!promises.length) {
-      return Promise.reject({message: `No matching window`});
+      let details = {};
+      for (let key of ["all_frames", "frame_id", "matches_about_blank", "matchesHost"]) {
+        if (key in options) {
+          details[key] = options[key];
+        }
+      }
+
+      return Promise.reject({message: `No window matching ${JSON.stringify(details)}`});
     }
     if (options.all_frames) {
       return Promise.all(promises);
     }
     if (promises.length > 1) {
       return Promise.reject({message: `Internal error: Script matched multiple windows`});
     }
     return promises[0];
new file mode 100644
--- /dev/null
+++ b/toolkit/components/extensions/ExtensionXPCShellUtils.jsm
@@ -0,0 +1,285 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+