Merge mozilla-central to autoland
authorCarsten "Tomcat" Book <cbook@mozilla.com>
Fri, 29 Jul 2016 12:36:04 +0200
changeset 332456 4907812dbb41d1a4f88a5770c4410dce5f4a37cd
parent 332455 b2c393a4ffdb333fee14ca64978d4307537b6436 (current diff)
parent 332279 2ea3d51ba1bb9f5c3b6921c43ea63f70b4fdf5d2 (diff)
child 332457 0f6396641036e51528865fd2a216482270c1ff35
push id9858
push userjlund@mozilla.com
push dateMon, 01 Aug 2016 14:37:10 +0000
treeherdermozilla-aurora@203106ef6cb6 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
milestone50.0a1
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";
+
+this.EXPORTED_SYMBOLS = ["ExtensionTestUtils"];
+
+const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
+
+Components.utils.import("resource://gre/modules/Task.jsm");
+Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "Extension",
+                                  "resource://gre/modules/Extension.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "FileUtils",
+                                  "resource://gre/modules/FileUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Schemas",
+                                  "resource://gre/modules/Schemas.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Services",
+                                  "resource://gre/modules/Services.jsm");
+
+XPCOMUtils.defineLazyServiceGetter(this, "uuidGenerator",
+                                   "@mozilla.org/uuid-generator;1", "nsIUUIDGenerator");
+
+/* exported ExtensionTestUtils */
+
+let BASE_MANIFEST = Object.freeze({
+  "applications": Object.freeze({
+    "gecko": Object.freeze({
+      "id": "test@web.ext",
+    }),
+  }),
+
+  "manifest_version": 2,
+
+  "name": "name",
+  "version": "0",
+});
+
+class ExtensionWrapper {
+  constructor(extension, testScope) {
+    this.extension = extension;
+    this.testScope = testScope;
+
+    this.state = "uninitialized";
+
+    this.testResolve = null;
+    this.testDone = new Promise(resolve => { this.testResolve = resolve; });
+
+    this.messageHandler = new Map();
+    this.messageAwaiter = new Map();