Merge m-c to inbound, a=merge
authorWes Kocher <wkocher@mozilla.com>
Thu, 21 Apr 2016 15:02:19 -0700
changeset 332261 3d46eafd05b975b83d95a9c28f77c2e153422f2f
parent 332260 987e2c82c04fa03260845b0c65e700455d3b6221 (current diff)
parent 332177 0891f0fa044cba28024849803e170ed7700e01e0 (diff)
child 332262 8b29568cb7e23d313b054d5cfcb02a62d24b504e
push id6048
push userkmoir@mozilla.com
push dateMon, 06 Jun 2016 19:02:08 +0000
treeherdermozilla-beta@46d72a56c57d [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmerge
milestone48.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Merge m-c to inbound, a=merge MozReview-Commit-ID: 5AQXGbI0ke2
devtools/client/sourceeditor/codemirror/addon/comment/comment.js
devtools/client/sourceeditor/codemirror/addon/comment/continuecomment.js
devtools/client/sourceeditor/codemirror/addon/dialog/dialog.css
devtools/client/sourceeditor/codemirror/addon/dialog/dialog.js
devtools/client/sourceeditor/codemirror/addon/edit/closebrackets.js
devtools/client/sourceeditor/codemirror/addon/edit/closetag.js
devtools/client/sourceeditor/codemirror/addon/edit/continuelist.js
devtools/client/sourceeditor/codemirror/addon/edit/matchbrackets.js
devtools/client/sourceeditor/codemirror/addon/edit/matchtags.js
devtools/client/sourceeditor/codemirror/addon/edit/trailingspace.js
devtools/client/sourceeditor/codemirror/addon/fold/brace-fold.js
devtools/client/sourceeditor/codemirror/addon/fold/comment-fold.js
devtools/client/sourceeditor/codemirror/addon/fold/foldcode.js
devtools/client/sourceeditor/codemirror/addon/fold/foldgutter.css
devtools/client/sourceeditor/codemirror/addon/fold/foldgutter.js
devtools/client/sourceeditor/codemirror/addon/fold/indent-fold.js
devtools/client/sourceeditor/codemirror/addon/fold/markdown-fold.js
devtools/client/sourceeditor/codemirror/addon/fold/xml-fold.js
devtools/client/sourceeditor/codemirror/addon/hint/show-hint.js
devtools/client/sourceeditor/codemirror/addon/search/match-highlighter.js
devtools/client/sourceeditor/codemirror/addon/search/searchcursor.js
devtools/client/sourceeditor/codemirror/addon/selection/active-line.js
devtools/client/sourceeditor/codemirror/addon/selection/mark-selection.js
devtools/client/sourceeditor/codemirror/addon/tern/tern.css
devtools/client/sourceeditor/codemirror/addon/tern/tern.js
devtools/client/sourceeditor/codemirror/keymap/emacs.js
devtools/client/sourceeditor/codemirror/keymap/sublime.js
devtools/client/sourceeditor/codemirror/keymap/vim.js
devtools/client/sourceeditor/codemirror/lib/codemirror.css
devtools/client/sourceeditor/codemirror/lib/codemirror.js
devtools/client/sourceeditor/codemirror/mode/css.js
devtools/client/sourceeditor/codemirror/mode/htmlmixed.js
devtools/client/sourceeditor/codemirror/mode/javascript.js
devtools/client/sourceeditor/codemirror/mode/xml.js
devtools/client/sourceeditor/test/codemirror/comment_test.js
devtools/client/sourceeditor/test/codemirror/doc_test.js
devtools/client/sourceeditor/test/codemirror/driver.js
devtools/client/sourceeditor/test/codemirror/emacs_test.js
devtools/client/sourceeditor/test/codemirror/mode/javascript/test.js
devtools/client/sourceeditor/test/codemirror/mode_test.css
devtools/client/sourceeditor/test/codemirror/mode_test.js
devtools/client/sourceeditor/test/codemirror/multi_test.js
devtools/client/sourceeditor/test/codemirror/search_test.js
devtools/client/sourceeditor/test/codemirror/sublime_test.js
devtools/client/sourceeditor/test/codemirror/test.js
mobile/android/app/mobile.js
services/common/KintoCertificateBlocklist.js
--- a/browser/app/profile/firefox.js
+++ b/browser/app/profile/firefox.js
@@ -41,17 +41,17 @@ pref("xpinstall.customConfirmationUI", t
 
 // Preferences for AMO integration
 pref("extensions.getAddons.cache.enabled", true);
 pref("extensions.getAddons.maxResults", 15);
 pref("extensions.getAddons.get.url", "https://services.addons.mozilla.org/%LOCALE%/firefox/api/%API_VERSION%/search/guid:%IDS%?src=firefox&appOS=%OS%&appVersion=%VERSION%");
 pref("extensions.getAddons.getWithPerformance.url", "https://services.addons.mozilla.org/%LOCALE%/firefox/api/%API_VERSION%/search/guid:%IDS%?src=firefox&appOS=%OS%&appVersion=%VERSION%&tMain=%TIME_MAIN%&tFirstPaint=%TIME_FIRST_PAINT%&tSessionRestored=%TIME_SESSION_RESTORED%");
 pref("extensions.getAddons.search.browseURL", "https://addons.mozilla.org/%LOCALE%/firefox/search?q=%TERMS%&platform=%OS%&appver=%VERSION%");
 pref("extensions.getAddons.search.url", "https://services.addons.mozilla.org/%LOCALE%/firefox/api/%API_VERSION%/search/%TERMS%/all/%MAX_RESULTS%/%OS%/%VERSION%/%COMPATIBILITY_MODE%?src=firefox");
-pref("extensions.webservice.discoverURL", "https://services.addons.mozilla.org/%LOCALE%/firefox/discovery/pane/%VERSION%/%OS%/%COMPATIBILITY_MODE%");
+pref("extensions.webservice.discoverURL", "https://discovery.addons.mozilla.org/%LOCALE%/firefox/discovery/pane/%VERSION%/%OS%/%COMPATIBILITY_MODE%");
 pref("extensions.getAddons.recommended.url", "https://services.addons.mozilla.org/%LOCALE%/%APP%/api/%API_VERSION%/list/recommended/all/%MAX_RESULTS%/%OS%/%VERSION%?src=firefox");
 pref("extensions.getAddons.link.url", "https://addons.mozilla.org/%LOCALE%/firefox/");
 
 // Blocklist preferences
 pref("extensions.blocklist.enabled", true);
 // OneCRL freshness checking depends on this value, so if you change it,
 // please also update security.onecrl.maximum_staleness_in_seconds.
 pref("extensions.blocklist.interval", 86400);
@@ -63,16 +63,22 @@ pref("extensions.blocklist.detailsURL", 
 pref("extensions.blocklist.itemURL", "https://blocklist.addons.mozilla.org/%LOCALE%/%APP%/blocked/%blockID%");
 
 // Kinto blocklist preferences
 pref("services.kinto.base", "https://firefox.settings.services.mozilla.com/v1");
 pref("services.kinto.changes.path", "/buckets/monitor/collections/changes/records");
 pref("services.kinto.bucket", "blocklists");
 pref("services.kinto.onecrl.collection", "certificates");
 pref("services.kinto.onecrl.checked", 0);
+pref("services.kinto.addons.collection", "addons");
+pref("services.kinto.addons.checked", 0);
+pref("services.kinto.plugins.collection", "plugins");
+pref("services.kinto.plugins.checked", 0);
+pref("services.kinto.gfx.collection", "gfx");
+pref("services.kinto.gfx.checked", 0);
 
 // for now, let's keep kinto update out of the release channel
 #ifdef RELEASE_BUILD
 pref("services.kinto.update_enabled", false);
 #else
 pref("services.kinto.update_enabled", true);
 #endif
 
--- a/browser/base/content/browser.css
+++ b/browser/base/content/browser.css
@@ -2,16 +2,17 @@
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 @namespace url("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul");
 @namespace html url("http://www.w3.org/1999/xhtml");
 @namespace svg url("http://www.w3.org/2000/svg");
 
 :root {
+  --identity-popup-expander-width: 38px;
   --panelui-subview-transition-duration: 150ms;
 }
 
 #main-window:not([chromehidden~="toolbar"]) {
 %ifdef XP_MACOSX
   min-width: 335px;
 %else
   min-width: 300px;
--- a/browser/base/content/browser.js
+++ b/browser/base/content/browser.js
@@ -6482,20 +6482,23 @@ var gIdentityHandler = {
   get _identityPopup () {
     delete this._identityPopup;
     return this._identityPopup = document.getElementById("identity-popup");
   },
   get _identityBox () {
     delete this._identityBox;
     return this._identityBox = document.getElementById("identity-box");
   },
-  get _identityPopupContentHost () {
-    delete this._identityPopupContentHost;
-    return this._identityPopupContentHost =
-      document.getElementById("identity-popup-content-host");
+  get _identityPopupContentHosts () {
+    delete this._identityPopupContentHosts;
+    return this._identityPopupContentHosts = [...document.querySelectorAll(".identity-popup-headline.host")];
+  },
+  get _identityPopupContentHostless () {
+    delete this._identityPopupContentHostless;
+    return this._identityPopupContentHostless = [...document.querySelectorAll(".identity-popup-headline.hostless")];
   },
   get _identityPopupContentOwner () {
     delete this._identityPopupContentOwner;
     return this._identityPopupContentOwner =
       document.getElementById("identity-popup-content-owner");
   },
   get _identityPopupContentSupp () {
     delete this._identityPopupContentSupp;
@@ -6928,62 +6931,64 @@ var gIdentityHandler = {
       updateAttribute(element, "isbroken", this._isBroken);
     }
 
     // Initialize the optional strings to empty values
     let supplemental = "";
     let verifier = "";
     let host = "";
     let owner = "";
-    let crop = "start";
+    let hostless = false;
 
     try {
       host = this.getEffectiveHost();
     } catch (e) {
       // Some URIs might have no hosts.
     }
 
     // Fallback for special protocols.
     if (!host) {
       host = this._uri.specIgnoringRef;
       // Special URIs without a host (eg, about:) should crop the end so
       // the protocol can be seen.
-      crop = "end";
+      hostless = true;
     }
 
     // Fill in the CA name if we have a valid TLS certificate.
     if (this._isSecure) {
       verifier = this._identityBox.tooltipText;
     }
 
     // Fill in organization information if we have a valid EV certificate.
     if (this._isEV) {
-      crop = "end";
-
       let iData = this.getIdentityData();
       host = owner = iData.subjectOrg;
       verifier = this._identityBox.tooltipText;
 
       // Build an appropriate supplemental block out of whatever location data we have
       if (iData.city)
         supplemental += iData.city + "\n";
       if (iData.state && iData.country)
         supplemental += gNavigatorBundle.getFormattedString("identity.identified.state_and_country",
                                                             [iData.state, iData.country]);
       else if (iData.state) // State only
         supplemental += iData.state;
       else if (iData.country) // Country only
         supplemental += iData.country;
     }
 
-    // Push the appropriate strings out to the UI. Need to use |value| for the
-    // host as it's a <label> that will be cropped if too long. Using
-    // |textContent| would simply wrap the value.
-    this._identityPopupContentHost.setAttribute("crop", crop);
-    this._identityPopupContentHost.setAttribute("value", host);
+    // Push the appropriate strings out to the UI.
+    this._identityPopupContentHosts.forEach((el) => {
+      el.textContent = host;
+      el.hidden = hostless;
+    });
+    this._identityPopupContentHostless.forEach((el) => {
+      el.setAttribute("value", host);
+      el.hidden = !hostless;
+    });
     this._identityPopupContentOwner.textContent = owner;
     this._identityPopupContentSupp.textContent = supplemental;
     this._identityPopupContentVerif.textContent = verifier;
 
     // Update per-site permissions section.
     this.updateSitePermissions();
   },
 
--- a/browser/base/content/browser.xul
+++ b/browser/base/content/browser.xul
@@ -849,17 +849,17 @@
                       class="menuitem-iconic subviewbutton"
                       label="&showAllBookmarks2.label;"
                       command="Browser:ShowAllBookmarks"
                       key="manBookmarkKb"/>
             <menuseparator/>
             <menuitem label="&recentBookmarks.label;"
                       id="BMB_recentBookmarks"
                       disabled="true"
-                      class="subviewbutton"/>
+                      class="menuitem-iconic subviewbutton"/>
             <menuseparator/>
             <menu id="BMB_bookmarksToolbar"
                   class="menu-iconic bookmark-item subviewbutton"
                   label="&personalbarCmd.label;"
                   container="true">
               <menupopup id="BMB_bookmarksToolbarPopup"
                          placespopup="true"
                          context="placesContext"
--- a/browser/base/content/report-phishing-overlay.xul
+++ b/browser/base/content/report-phishing-overlay.xul
@@ -21,15 +21,15 @@
               label="&reportDeceptiveSiteMenu.title;"
               accesskey="&reportDeceptiveSiteMenu.accesskey;"
               insertbefore="aboutSeparator"
               observes="reportPhishingBroadcaster"
               oncommand="openUILink(gSafeBrowsing.getReportURL('Phish'), event);"
               onclick="checkForMiddleClick(this, event);"/>
     <menuitem id="menu_HelpPopup_reportPhishingErrortoolmenu"
               label="&safeb.palm.notdeceptive.label;"
-              accesskey="&reportDeceptiveSiteMenu.accesskey;"
+              accesskey="&safeb.palm.notdeceptive.accesskey;"
               insertbefore="aboutSeparator"
               observes="reportPhishingErrorBroadcaster"
               oncommand="openUILinkIn(gSafeBrowsing.getReportURL('PhishMistake'), 'tab');"
               onclick="checkForMiddleClick(this, event);"/>
   </menupopup>
 </overlay>
--- a/browser/base/content/test/general/browser.ini
+++ b/browser/base/content/test/general/browser.ini
@@ -16,16 +16,17 @@ support-files =
   browser_fxa_oauth.html
   browser_fxa_oauth_with_keys.html
   browser_fxa_web_channel.html
   browser_registerProtocolHandler_notification.html
   browser_star_hsts.sjs
   browser_tab_dragdrop2_frame1.xul
   browser_web_channel.html
   browser_web_channel_iframe.html
+  bug1262648_string_with_newlines.dtd
   bug592338.html
   bug792517-2.html
   bug792517.html
   bug792517.sjs
   bug839103.css
   contextmenu_common.js
   ctxmenu-image.png
   discovery.html
--- a/browser/base/content/test/general/browser_misused_characters_in_strings.js
+++ b/browser/base/content/test/general/browser_misused_characters_in_strings.js
@@ -122,26 +122,39 @@ add_task(function* checkAllTheProperties
     let bundle = new StringBundle(uri.spec);
     let entities = bundle.getAll();
     for (let entity of entities) {
       testForErrors(uri.spec, entity.key, entity.value);
     }
   }
 });
 
+var checkDTD = Task.async(function* (aURISpec) {
+  let rawContents = yield fetchFile(aURISpec);
+  // The regular expression below is adapted from:
+  // https://hg.mozilla.org/mozilla-central/file/68c0b7d6f16ce5bb023e08050102b5f2fe4aacd8/python/compare-locales/compare_locales/parser.py#l233
+  let entities = rawContents.match(/<!ENTITY\s+([\w\.]*)\s+("[^"]*"|'[^']*')\s*>/g);
+  for (let entity of entities) {
+    let [, key, str] = entity.match(/<!ENTITY\s+([\w\.]*)\s+("[^"]*"|'[^']*')\s*>/);
+    // The matched string includes the enclosing quotation marks,
+    // we need to slice them off.
+    str = str.slice(1, -1);
+    testForErrors(aURISpec, key, str);
+  }
+});
+
 add_task(function* checkAllTheDTDs() {
   let appDir = Services.dirsvc.get("XCurProcD", Ci.nsIFile);
   let uris = yield generateURIsFromDirTree(appDir, [".dtd"]);
   ok(uris.length, `Found ${uris.length} .dtd files to scan for misused characters`);
+  for (let uri of uris) {
+    yield checkDTD(uri.spec);
+  }
 
-  for (let uri of uris) {
-    let rawContents = yield fetchFile(uri.spec);
-    let entities = rawContents.match(/ENTITY\s+([\w\.]*)\s+["'](.*)["']/g);
-    for (let entity of entities) {
-      let [, key, str] = entity.match(/ENTITY\s+([\w\.]*)\s+["'](.*)["']/);
-      testForErrors(uri.spec, key, str);
-    }
-  }
+  // This support DTD file supplies a string with a newline to make sure
+  // the regex in checkDTD works correctly for that case.
+  let dtdLocation = gTestPath.replace(/\/[^\/]*$/i, "/bug1262648_string_with_newlines.dtd");
+  yield checkDTD(dtdLocation);
 });
 
 add_task(function* ensureWhiteListIsEmpty() {
   is(gWhitelist.length, 0, "No remaining whitelist entries exist");
 });
--- a/browser/base/content/test/general/browser_save_private_link_perwindowpb.js
+++ b/browser/base/content/test/general/browser_save_private_link_perwindowpb.js
@@ -1,140 +1,116 @@
 /* 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/. */
 
-var {LoadContextInfo} = Cu.import("resource://gre/modules/LoadContextInfo.jsm", null);
+function createTemporarySaveDirectory() {
+  var saveDir = Cc["@mozilla.org/file/directory_service;1"]
+                  .getService(Ci.nsIProperties)
+                  .get("TmpD", Ci.nsIFile);
+  saveDir.append("testsavedir");
+  if (!saveDir.exists())
+    saveDir.create(Ci.nsIFile.DIRECTORY_TYPE, 0o755);
+  return saveDir;
+}
 
-function test() {
-  // initialization
-  waitForExplicitFinish();
-  let windowsToClose = [];
-  let testURI = "http://mochi.test:8888/browser/browser/base/content/test/general/bug792517.html";
-  let fileName;
-  let MockFilePicker = SpecialPowers.MockFilePicker;
-  let cache = Cc["@mozilla.org/netwerk/cache-storage-service;1"]
-              .getService(Ci.nsICacheStorageService);
-
-  function checkDiskCacheFor(filename, goon) {
+function promiseNoCacheEntry(filename) {
+  return new Promise((resolve, reject) => {
     Visitor.prototype = {
       onCacheStorageInfo: function(num, consumption)
       {
         info("disk storage contains " + num + " entries");
       },
       onCacheEntryInfo: function(uri)
       {
-        var urispec = uri.asciiSpec;
+        let urispec = uri.asciiSpec;
         info(urispec);
         is(urispec.includes(filename), false, "web content present in disk cache");
       },
       onCacheEntryVisitCompleted: function()
       {
-        goon();
+        resolve();
       }
     };
     function Visitor() {}
 
-    var storage = cache.diskCacheStorage(LoadContextInfo.default, false);
+    let cache = Cc["@mozilla.org/netwerk/cache-storage-service;1"]
+                .getService(Ci.nsICacheStorageService);
+    let {LoadContextInfo} = Cu.import("resource://gre/modules/LoadContextInfo.jsm", null);
+    let storage = cache.diskCacheStorage(LoadContextInfo.default, false);
     storage.asyncVisitStorage(new Visitor(), true /* Do walk entries */);
-  }
-
-  function onTransferComplete(downloadSuccess) {
-    ok(downloadSuccess, "Image file should have been downloaded successfully");
-
-    // Give the request a chance to finish and create a cache entry
-    executeSoon(function() {
-      checkDiskCacheFor(fileName, finish);
-      mockTransferCallback = null;
-    });
-  }
-
-  function createTemporarySaveDirectory() {
-    var saveDir = Cc["@mozilla.org/file/directory_service;1"]
-                    .getService(Ci.nsIProperties)
-                    .get("TmpD", Ci.nsIFile);
-    saveDir.append("testsavedir");
-    if (!saveDir.exists())
-      saveDir.create(Ci.nsIFile.DIRECTORY_TYPE, 0o755);
-    return saveDir;
-  }
-
-  function doTest(aIsPrivateMode, aWindow, aCallback) {
-    function contextMenuOpened(event) {
-      cache.clear();
+  });
+}
 
-      aWindow.document.removeEventListener("popupshown", contextMenuOpened);
-
-      // Create the folder the image will be saved into.
-      var destDir = createTemporarySaveDirectory();
-      var destFile = destDir.clone();
-
-      MockFilePicker.displayDirectory = destDir;
-      MockFilePicker.showCallback = function(fp) {
-        fileName = fp.defaultString;
-        destFile.append (fileName);
-        MockFilePicker.returnFiles = [destFile];
-        MockFilePicker.filterIndex = 1; // kSaveAsType_URL
-      };
+function promiseImageDownloaded() {
+  return new Promise((resolve, reject) => {
+    let fileName;
+    let MockFilePicker = SpecialPowers.MockFilePicker;
+    MockFilePicker.init(window);
 
-      mockTransferCallback = onTransferComplete;
-      mockTransferRegisterer.register();
+    function onTransferComplete(downloadSuccess) {
+      ok(downloadSuccess, "Image file should have been downloaded successfully " + fileName);
 
-      registerCleanupFunction(function () {
-        mockTransferRegisterer.unregister();
-        MockFilePicker.cleanup();
-        destDir.remove(true);
-      });
-
-      // Select "Save Image As" option from context menu
-      var saveVideoCommand = aWindow.document.getElementById("context-saveimage");
-      saveVideoCommand.doCommand();
-
-      event.target.hidePopup();
+      // Give the request a chance to finish and create a cache entry
+      resolve(fileName);
     }
 
-    aWindow.gBrowser.addEventListener("pageshow", function pageShown(event) {
-      // If data: --url PAC file isn't loaded soon enough, we may get about:privatebrowsing loaded
-      if (event.target.location == "about:blank" ||
-          event.target.location == "about:privatebrowsing") {
-        aWindow.gBrowser.selectedBrowser.loadURI(testURI);
-        return;
-      }
-      aWindow.gBrowser.removeEventListener("pageshow", pageShown);
-
-      waitForFocus(function () {
-        aWindow.document.addEventListener("popupshown", contextMenuOpened, false);
-        var img = aWindow.gBrowser.selectedBrowser.contentDocument.getElementById("img");
-        EventUtils.synthesizeMouseAtCenter(img,
-                                           { type: "contextmenu", button: 2 },
-                                           aWindow.gBrowser.contentWindow);
-      }, aWindow.gBrowser.selectedBrowser);
-    });
-  }
+    // Create the folder the image will be saved into.
+    var destDir = createTemporarySaveDirectory();
+    var destFile = destDir.clone();
 
-  function testOnWindow(aOptions, aCallback) {
-    whenNewWindowLoaded(aOptions, function(aWin) {
-      windowsToClose.push(aWin);
-      // execute should only be called when need, like when you are opening
-      // web pages on the test. If calling executeSoon() is not necesary, then
-      // call whenNewWindowLoaded() instead of testOnWindow() on your test.
-      executeSoon(() => aCallback(aWin));
-    });
-  };
+    MockFilePicker.displayDirectory = destDir;
+    MockFilePicker.showCallback = function(fp) {
+      fileName = fp.defaultString;
+      destFile.append (fileName);
+      MockFilePicker.returnFiles = [destFile];
+      MockFilePicker.filterIndex = 1; // kSaveAsType_URL
+    };
 
-   // this function is called after calling finish() on the test.
-  registerCleanupFunction(function() {
-    windowsToClose.forEach(function(aWin) {
-      aWin.close();
+    mockTransferCallback = onTransferComplete;
+    mockTransferRegisterer.register();
+
+    registerCleanupFunction(function () {
+      mockTransferCallback = null;
+      mockTransferRegisterer.unregister();
+      MockFilePicker.cleanup();
+      destDir.remove(true);
     });
-  });
 
-  MockFilePicker.init(window);
-  // then test when on private mode
-  testOnWindow({private: true}, function(aWin) {
-    doTest(true, aWin, finish);
   });
 }
 
+add_task(function* () {
+  let testURI = "http://mochi.test:8888/browser/browser/base/content/test/general/bug792517.html";
+  let privateWindow = yield BrowserTestUtils.openNewBrowserWindow({private: true});
+  let tab = yield BrowserTestUtils.openNewForegroundTab(privateWindow.gBrowser, testURI);
+
+  let contextMenu = privateWindow.document.getElementById("contentAreaContextMenu");
+  let popupShown = BrowserTestUtils.waitForEvent(contextMenu, "popupshown");
+  let popupHidden = BrowserTestUtils.waitForEvent(contextMenu, "popuphidden");
+  yield BrowserTestUtils.synthesizeMouseAtCenter("#img", {
+    type: "contextmenu",
+    button: 2
+  }, tab.linkedBrowser);
+  yield popupShown;
+
+  let cache = Cc["@mozilla.org/netwerk/cache-storage-service;1"]
+              .getService(Ci.nsICacheStorageService);
+  cache.clear();
+
+  let imageDownloaded = promiseImageDownloaded();
+  // Select "Save Image As" option from context menu
+  privateWindow.document.getElementById("context-saveimage").doCommand();
+
+  contextMenu.hidePopup();
+  yield popupHidden;
+
+  // wait for image download
+  let fileName = yield imageDownloaded;
+  yield promiseNoCacheEntry(fileName);
+
+  yield BrowserTestUtils.closeWindow(privateWindow);
+});
+
 Cc["@mozilla.org/moz/jssubscript-loader;1"]
   .getService(Ci.mozIJSSubScriptLoader)
   .loadSubScript("chrome://mochitests/content/browser/toolkit/content/tests/browser/common/mockTransfer.js",
                  this);
new file mode 100644
--- /dev/null
+++ b/browser/base/content/test/general/bug1262648_string_with_newlines.dtd
@@ -0,0 +1,3 @@
+<!ENTITY foo.bar    "This string
+contains
+newlines!">
\ No newline at end of file
--- a/browser/base/content/test/general/head.js
+++ b/browser/base/content/test/general/head.js
@@ -606,19 +606,29 @@ var FullZoomHelper = {
  * @resolves to the received event
  * @rejects if a valid load event is not received within a meaningful interval
  */
 function promiseTabLoadEvent(tab, url)
 {
   let deferred = Promise.defer();
   info("Wait tab event: load");
 
+  function handle(loadedUrl) {
+    if (loadedUrl === "about:blank" || (url && loadedUrl !== url)) {
+      info(`Skipping spurious load event for ${loadedUrl}`);
+      return false;
+    }
+
+    info("Tab event received: load");
+    return true;
+  }
+
   // Create two promises: one resolved from the content process when the page
   // loads and one that is rejected if we take too long to load the url.
-  let loaded = BrowserTestUtils.browserLoaded(tab.linkedBrowser, false, url);
+  let loaded = BrowserTestUtils.browserLoaded(tab.linkedBrowser, false, handle);
 
   let timeout = setTimeout(() => {
     deferred.reject(new Error("Timed out while waiting for a 'load' event"));
   }, 30000);
 
   loaded.then(() => {
     clearTimeout(timeout);
     deferred.resolve()
--- a/browser/components/controlcenter/content/panel.inc.xul
+++ b/browser/components/controlcenter/content/panel.inc.xul
@@ -5,29 +5,31 @@
 <panel id="identity-popup"
        type="arrow"
        hidden="true"
        onpopupshown="gIdentityHandler.onPopupShown(event);"
        onpopuphidden="gIdentityHandler.onPopupHidden(event);"
        orient="vertical">
 
   <broadcasterset>
-    <broadcaster id="identity-popup-content-host" class="identity-popup-headline" crop="start"/>
     <broadcaster id="identity-popup-mcb-learn-more" class="text-link plain" value="&identity.learnMore;"/>
     <broadcaster id="identity-popup-insecure-login-forms-learn-more" class="text-link plain" value="&identity.learnMore;"/>
   </broadcasterset>
 
   <panelmultiview id="identity-popup-multiView"
                   mainViewId="identity-popup-mainView">
     <panelview id="identity-popup-mainView" flex="1">
 
       <!-- Security Section -->
       <hbox id="identity-popup-security" class="identity-popup-section">
         <vbox id="identity-popup-security-content" flex="1">
-          <label observes="identity-popup-content-host"/>
+          <label class="plain">
+            <label class="identity-popup-headline host"></label>
+            <label class="identity-popup-headline hostless" crop="end"/>
+          </label>
           <description class="identity-popup-connection-not-secure"
                        value="&identity.connectionNotSecure;"
                        when-connection="not-secure secure-cert-user-overridden"/>
           <description class="identity-popup-connection-secure"
                        value="&identity.connectionSecure;"
                        when-connection="secure secure-ev"/>
           <description value="&identity.connectionInternal;"
                        when-connection="chrome"/>
@@ -91,17 +93,20 @@
           <description>&identity.permissionsEmpty;</description>
         </vbox>
       </hbox>
     </panelview>
 
     <!-- Security SubView -->
     <panelview id="identity-popup-securityView" flex="1">
       <vbox id="identity-popup-securityView-header">
-        <label observes="identity-popup-content-host"/>
+        <label class="plain">
+          <label class="identity-popup-headline host"></label>
+          <label class="identity-popup-headline hostless" crop="end"/>
+        </label>
         <description class="identity-popup-connection-not-secure"
                      value="&identity.connectionNotSecure;"
                      when-connection="not-secure secure-cert-user-overridden"/>
         <description class="identity-popup-connection-secure"
                      value="&identity.connectionSecure;"
                      when-connection="secure secure-ev"/>
       </vbox>
 
--- a/browser/components/downloads/DownloadsCommon.jsm
+++ b/browser/components/downloads/DownloadsCommon.jsm
@@ -457,25 +457,25 @@ this.DownloadsCommon = {
     } else {
       promiseShouldLaunch = Promise.resolve(true);
     }
 
     promiseShouldLaunch.then(shouldLaunch => {
       if (!shouldLaunch) {
         return;
       }
-  
+
       // Actually open the file.
       try {
         if (aMimeInfo && aMimeInfo.preferredAction == aMimeInfo.useHelperApp) {
           aMimeInfo.launchWithFile(aFile);
           return;
         }
       } catch (ex) { }
-  
+
       // If either we don't have the mime info, or the preferred action failed,
       // attempt to launch the file directly.
       try {
         aFile.launch();
       } catch (ex) {
         // If launch fails, try sending it through the system's external "file:"
         // URL handler.
         Cc["@mozilla.org/uriloader/external-protocol-service;1"]
@@ -516,59 +516,93 @@ this.DownloadsCommon = {
       }
     }
   },
 
   /**
    * Displays an alert message box which asks the user if they want to
    * unblock the downloaded file or not.
    *
-   * @param aVerdict
-   *        The detailed reason why the download was blocked, according to the
-   *        "Downloads.Error.BLOCK_VERDICT_" constants. If an unknown reason is
-   *        specified, "Downloads.Error.BLOCK_VERDICT_MALWARE" is assumed.
-   * @param aOwnerWindow
-   *        The window with which this action is associated.
+   * @param options
+   *        An object with the following properties:
+   *        {
+   *          verdict:
+   *            The detailed reason why the download was blocked, according to
+   *            the "Downloads.Error.BLOCK_VERDICT_" constants. If an unknown
+   *            reason is specified, "Downloads.Error.BLOCK_VERDICT_MALWARE" is
+   *            assumed.
+   *          window:
+   *            The window with which this action is associated.
+   *          dialogType:
+   *            String that determines which actions are available:
+   *             - "unblock" to offer just "unblock".
+   *             - "chooseUnblock" to offer "unblock" and "confirmBlock".
+   *             - "chooseOpen" to offer "open" and "confirmBlock".
+   *        }
    *
    * @return {Promise}
    * @resolves String representing the action that should be executed:
+   *            - "open" to allow the download and open the file.
    *            - "unblock" to allow the download without opening the file.
    *            - "confirmBlock" to delete the blocked data permanently.
    *            - "cancel" to do nothing and cancel the operation.
    */
-  confirmUnblockDownload: Task.async(function* (aVerdict, aOwnerWindow) {
+  confirmUnblockDownload: Task.async(function* ({ verdict, window,
+                                                  dialogType }) {
     let s = DownloadsCommon.strings;
-    let title = s.unblockHeader;
-    let buttonFlags = (Ci.nsIPrompt.BUTTON_TITLE_IS_STRING * Ci.nsIPrompt.BUTTON_POS_0) +
-                      (Ci.nsIPrompt.BUTTON_TITLE_CANCEL * Ci.nsIPrompt.BUTTON_POS_1);
-    let type = "";
-    let message = s.unblockTip;
-    let unblockButton = s.unblockButtonContinue;
-    let confirmBlockButton = s.unblockButtonCancel;
+
+    // All the dialogs have an action button and a cancel button, while only
+    // some of them have an additonal button to remove the file. The cancel
+    // button must always be the one at BUTTON_POS_1 because this is the value
+    // returned by confirmEx when using ESC or closing the dialog (bug 345067).
+    let title = s.unblockHeaderUnblock;
+    let firstButtonText = s.unblockButtonUnblock;
+    let firstButtonAction = "unblock";
+    let buttonFlags =
+        (Ci.nsIPrompt.BUTTON_TITLE_IS_STRING * Ci.nsIPrompt.BUTTON_POS_0) +
+        (Ci.nsIPrompt.BUTTON_TITLE_CANCEL * Ci.nsIPrompt.BUTTON_POS_1);
 
-    switch (aVerdict) {
+    switch (dialogType) {
+      case "unblock":
+        // Use only the unblock action. The default is to cancel.
+        buttonFlags += Ci.nsIPrompt.BUTTON_POS_1_DEFAULT;
+        break;
+      case "chooseUnblock":
+        // Use the unblock and remove file actions. The default is remove file.
+        buttonFlags +=
+          (Ci.nsIPrompt.BUTTON_TITLE_IS_STRING * Ci.nsIPrompt.BUTTON_POS_2) +
+          Ci.nsIPrompt.BUTTON_POS_2_DEFAULT;
+        break;
+      case "chooseOpen":
+        // Use the unblock and open file actions. The default is open file.
+        title = s.unblockHeaderOpen;
+        firstButtonText = s.unblockButtonOpen;
+        firstButtonAction = "open";
+        buttonFlags +=
+          (Ci.nsIPrompt.BUTTON_TITLE_IS_STRING * Ci.nsIPrompt.BUTTON_POS_2) +
+          Ci.nsIPrompt.BUTTON_POS_0_DEFAULT;
+        break;
+      default:
+        Cu.reportError("Unexpected dialog type: " + dialogType);
+        return "cancel";
+    }
+
+    let message;
+    switch (verdict) {
       case Downloads.Error.BLOCK_VERDICT_UNCOMMON:
-        type = s.unblockTypeUncommon;
-        buttonFlags += (Ci.nsIPrompt.BUTTON_TITLE_IS_STRING * Ci.nsIPrompt.BUTTON_POS_2) +
-                       Ci.nsIPrompt.BUTTON_POS_0_DEFAULT;
+        message = s.unblockTypeUncommon;
         break;
       case Downloads.Error.BLOCK_VERDICT_POTENTIALLY_UNWANTED:
-        type = s.unblockTypePotentiallyUnwanted;
-        buttonFlags += (Ci.nsIPrompt.BUTTON_TITLE_IS_STRING * Ci.nsIPrompt.BUTTON_POS_2) +
-                       Ci.nsIPrompt.BUTTON_POS_2_DEFAULT;
+        message = s.unblockTypePotentiallyUnwanted;
         break;
       default: // Assume Downloads.Error.BLOCK_VERDICT_MALWARE
-        type = s.unblockTypeMalware;
-        buttonFlags += Ci.nsIPrompt.BUTTON_POS_1_DEFAULT;
+        message = s.unblockTypeMalware;
         break;
     }
-
-    if (type) {
-      message = type + "\n\n" + message;
-    }
+    message += "\n\n" + s.unblockTip;
 
     Services.ww.registerNotification(function onOpen(subj, topic) {
       if (topic == "domwindowopened" && subj instanceof Ci.nsIDOMWindow) {
         // Make sure to listen for "DOMContentLoaded" because it is fired
         // before the "load" event.
         subj.addEventListener("DOMContentLoaded", function onLoad() {
           subj.removeEventListener("DOMContentLoaded", onLoad);
           if (subj.document.documentURI ==
@@ -579,22 +613,20 @@ this.DownloadsCommon = {
               // Change the dialog to use a warning icon.
               dialog.classList.add("alert-dialog");
             }
           }
         });
       }
     });
 
-    // The ordering of the ok/cancel buttons is used this way to allow "cancel"
-    // to have the same result as hitting the ESC or Close button (see bug 345067).
-    let rv = Services.prompt.confirmEx(aOwnerWindow, title, message, buttonFlags,
-                                       unblockButton, null, confirmBlockButton,
-                                       null, {});
-    return ["unblock", "cancel", "confirmBlock"][rv];
+    let rv = Services.prompt.confirmEx(window, title, message, buttonFlags,
+                                       firstButtonText, null,
+                                       s.unblockButtonConfirmBlock, null, {});
+    return [firstButtonAction, "cancel", "confirmBlock"][rv];
   }),
 };
 
 XPCOMUtils.defineLazyGetter(this.DownloadsCommon, "log", () => {
   return DownloadsLogger.log.bind(DownloadsLogger);
 });
 XPCOMUtils.defineLazyGetter(this.DownloadsCommon, "error", () => {
   return DownloadsLogger.error.bind(DownloadsLogger);
--- a/browser/components/downloads/DownloadsViewUI.jsm
+++ b/browser/components/downloads/DownloadsViewUI.jsm
@@ -232,17 +232,17 @@ this.DownloadsViewUI.DownloadElementShel
         }
       } else if (this.download.canceled) {
         stateLabel = s.stateCanceled;
       } else if (this.download.error.becauseBlockedByParentalControls) {
         stateLabel = s.stateBlockedParentalControls;
       } else if (this.download.error.becauseBlockedByReputationCheck) {
         switch (this.download.error.reputationCheckVerdict) {
           case Downloads.Error.BLOCK_VERDICT_UNCOMMON:
-            stateLabel = s.blockedUncommon;
+            stateLabel = s.blockedUncommon2;
             break;
           case Downloads.Error.BLOCK_VERDICT_POTENTIALLY_UNWANTED:
             stateLabel = s.blockedPotentiallyUnwanted;
             break;
           default: // Assume Downloads.Error.BLOCK_VERDICT_MALWARE
             stateLabel = s.blockedMalware;
             break;
         }
@@ -266,21 +266,28 @@ this.DownloadsViewUI.DownloadElementShel
 
   /**
    * Shows the appropriate unblock dialog based on the verdict, and executes the
    * action selected by the user in the dialog, which may involve unblocking,
    * opening or removing the file.
    *
    * @param window
    *        The window to which the dialog should be anchored.
+   * @param dialogType
+   *        Can be "unblock", "chooseUnblock", or "chooseOpen".
    */
-  confirmUnblock(window) {
-    let verdict = this.download.error.reputationCheckVerdict;
-    DownloadsCommon.confirmUnblockDownload(verdict, window).then(action => {
-      if (action == "unblock") {
+  confirmUnblock(window, dialogType) {
+    DownloadsCommon.confirmUnblockDownload({
+      verdict: this.download.error.reputationCheckVerdict,
+      window,
+      dialogType,
+    }).then(action => {
+      if (action == "open") {
+        return this.download.unblock().then(() => this.downloadsCmd_open());
+      } else if (action == "unblock") {
         return this.download.unblock();
       } else if (action == "confirmBlock") {
         return this.download.confirmBlock();
       }
     }).catch(Cu.reportError);
   },
 
   /**
@@ -318,16 +325,18 @@ this.DownloadsViewUI.DownloadElementShel
     switch (aCommand) {
       case "downloadsCmd_retry":
         return this.download.canceled || this.download.error;
       case "downloadsCmd_pauseResume":
         return this.download.hasPartialData && !this.download.error;
       case "downloadsCmd_openReferrer":
         return !!this.download.source.referrer;
       case "downloadsCmd_confirmBlock":
+      case "downloadsCmd_chooseUnblock":
+      case "downloadsCmd_chooseOpen":
       case "downloadsCmd_unblock":
         return this.download.hasBlockedData;
     }
     return false;
   },
 
   downloadsCmd_cancel() {
     // This is the correct way to avoid race conditions when cancelling.
--- a/browser/components/downloads/content/allDownloadsViewOverlay.js
+++ b/browser/components/downloads/content/allDownloadsViewOverlay.js
@@ -372,17 +372,25 @@ HistoryDownloadElementShell.prototype = 
     }
     if (this._historyDownload) {
       let uri = NetUtil.newURI(this.download.source.url);
       PlacesUtils.bhistory.removePage(uri);
     }
   },
 
   downloadsCmd_unblock() {
-    this.confirmUnblock(window);
+    this.confirmUnblock(window, "unblock");
+  },
+
+  downloadsCmd_chooseUnblock() {
+    this.confirmUnblock(window, "chooseUnblock");
+  },
+
+  downloadsCmd_chooseOpen() {
+    this.confirmUnblock(window, "chooseOpen");
   },
 
   // Returns whether or not the download handled by this shell should
   // show up in the search results for the given term.  Both the display
   // name for the download and the url are searched.
   matchesSearchTerm(aTerm) {
     if (!aTerm) {
       return true;
--- a/browser/components/downloads/content/allDownloadsViewOverlay.xul
+++ b/browser/components/downloads/content/allDownloadsViewOverlay.xul
@@ -58,16 +58,20 @@
               events="focus,select,contextmenu"
               oncommandupdate="goUpdateDownloadCommands();">
     <command id="downloadsCmd_pauseResume"
              oncommand="goDoCommand('downloadsCmd_pauseResume')"/>
     <command id="downloadsCmd_cancel"
              oncommand="goDoCommand('downloadsCmd_cancel')"/>
     <command id="downloadsCmd_unblock"
              oncommand="goDoCommand('downloadsCmd_unblock')"/>
+    <command id="downloadsCmd_chooseUnblock"
+             oncommand="goDoCommand('downloadsCmd_chooseUnblock')"/>
+    <command id="downloadsCmd_chooseOpen"
+             oncommand="goDoCommand('downloadsCmd_chooseOpen')"/>
     <command id="downloadsCmd_confirmBlock"
              oncommand="goDoCommand('downloadsCmd_confirmBlock')"/>
     <command id="downloadsCmd_open"
              oncommand="goDoCommand('downloadsCmd_open')"/>
     <command id="downloadsCmd_show"
              oncommand="goDoCommand('downloadsCmd_show')"/>
     <command id="downloadsCmd_retry"
              oncommand="goDoCommand('downloadsCmd_retry')"/>
@@ -87,18 +91,18 @@
               label="&cmd.resume.label;"
               accesskey="&cmd.resume.accesskey;"/>
     <menuitem command="downloadsCmd_cancel"
               class="downloadCancelMenuItem"
               label="&cmd.cancel.label;"
               accesskey="&cmd.cancel.accesskey;"/>
     <menuitem command="downloadsCmd_unblock"
               class="downloadUnblockMenuItem"
-              label="&cmd.unblock.label;"
-              accesskey="&cmd.unblock.accesskey;"/>
+              label="&cmd.unblock2.label;"
+              accesskey="&cmd.unblock2.accesskey;"/>
     <menuitem command="cmd_delete"
               class="downloadRemoveFromHistoryMenuItem"
               label="&cmd.removeFromHistory.label;"
               accesskey="&cmd.removeFromHistory.accesskey;"/>
     <menuitem command="downloadsCmd_show"
               class="downloadShowMenuItem"
 #ifdef XP_MACOSX
               label="&cmd.showMac.label;"
--- a/browser/components/downloads/content/download.xml
+++ b/browser/components/downloads/content/download.xml
@@ -58,19 +58,22 @@
                     tooltiptext="&cmd.showMac.label;"
 #else
                     tooltiptext="&cmd.show.label;"
 #endif
                     oncommand="DownloadsView.onDownloadCommand(event, 'downloadsCmd_show');"/>
         <xul:button class="downloadButton downloadConfirmBlock downloadIconCancel"
                     tooltiptext="&cmd.removeFile.label;"
                     oncommand="DownloadsView.onDownloadCommand(event, 'downloadsCmd_confirmBlock');"/>
-        <xul:button class="downloadButton downloadUnblock downloadIconShow"
-                    tooltiptext="&cmd.unblock.label;"
-                    oncommand="DownloadsView.onDownloadCommand(event, 'downloadsCmd_unblock');"/>
+        <xul:button class="downloadButton downloadChooseUnblock downloadIconShow"
+                    tooltiptext="&cmd.chooseUnblock.label;"
+                    oncommand="DownloadsView.onDownloadCommand(event, 'downloadsCmd_chooseUnblock');"/>
+        <xul:button class="downloadButton downloadChooseOpen downloadIconShow"
+                    tooltiptext="&cmd.chooseOpen.label;"
+                    oncommand="DownloadsView.onDownloadCommand(event, 'downloadsCmd_chooseOpen');"/>
       </xul:stack>
     </content>
   </binding>
 
   <binding id="download-toolbarbutton"
            extends="chrome://global/content/bindings/toolbarbutton.xml#toolbarbutton">
     <content>
       <children />
--- a/browser/components/downloads/content/downloads.css
+++ b/browser/components/downloads/content/downloads.css
@@ -122,23 +122,32 @@ richlistitem.download button {
 .download-state:not(          [state="8"]  /* Blocked (dirty)    */)
                                            .downloadConfirmBlock,
 .download-state[state="8"]:not(.temporary-block)
                                            .downloadConfirmBlock,
 .download-state[state="8"].temporary-block:not([verdict="Malware"])
                                            .downloadConfirmBlock,
 
 /* Blocked (dirty) downloads that have not been confirmed and
-   have temporary data, for cases other than Malware. */
+   have temporary data, for the Potentially Unwanted case. */
 .download-state:not(          [state="8"]  /* Blocked (dirty)    */)
-                                           .downloadUnblock,
+                                           .downloadChooseUnblock,
 .download-state[state="8"]:not(.temporary-block)
-                                           .downloadUnblock,
-.download-state[state="8"].temporary-block[verdict="Malware"]
-                                           .downloadUnblock,
+                                           .downloadChooseUnblock,
+.download-state[state="8"].temporary-block:not([verdict="PotentiallyUnwanted"])
+                                           .downloadChooseUnblock,
+
+/* Blocked (dirty) downloads that have not been confirmed and
+   have temporary data, for the Uncommon case. */
+.download-state:not(          [state="8"]  /* Blocked (dirty)    */)
+                                           .downloadChooseOpen,
+.download-state[state="8"]:not(.temporary-block)
+                                           .downloadChooseOpen,
+.download-state[state="8"].temporary-block:not([verdict="Uncommon"])
+                                           .downloadChooseOpen,
 
 .download-state:not(:-moz-any([state="2"], /* Failed             */
                               [state="3"]) /* Canceled           */)
                                            .downloadRetry,
 
 .download-state:not(          [state="1"]  /* Finished           */)
                                            .downloadShow
 
--- a/browser/components/downloads/content/downloads.js
+++ b/browser/components/downloads/content/downloads.js
@@ -1087,17 +1087,27 @@ DownloadsViewItem.prototype = {
   cmd_delete() {
     DownloadsCommon.removeAndFinalizeDownload(this.download);
     PlacesUtils.bhistory.removePage(
                            NetUtil.newURI(this.download.source.url));
   },
 
   downloadsCmd_unblock() {
     DownloadsPanel.hidePanel();
-    this.confirmUnblock(window);
+    this.confirmUnblock(window, "unblock");
+  },
+
+  downloadsCmd_chooseUnblock() {
+    DownloadsPanel.hidePanel();
+    this.confirmUnblock(window, "chooseUnblock");
+  },
+
+  downloadsCmd_chooseOpen() {
+    DownloadsPanel.hidePanel();
+    this.confirmUnblock(window, "chooseOpen");
   },
 
   downloadsCmd_open() {
     this.download.launch().catch(Cu.reportError);
 
     // We explicitly close the panel here to give the user the feedback that
     // their click has been received, and we're handling the action.
     // Otherwise, we'd have to wait for the file-type handler to execute
--- a/browser/components/downloads/content/downloadsOverlay.xul
+++ b/browser/components/downloads/content/downloadsOverlay.xul
@@ -18,16 +18,20 @@
     <command id="downloadsCmd_doDefault"
              oncommand="goDoCommand('downloadsCmd_doDefault')"/>
     <command id="downloadsCmd_pauseResume"
              oncommand="goDoCommand('downloadsCmd_pauseResume')"/>
     <command id="downloadsCmd_cancel"
              oncommand="goDoCommand('downloadsCmd_cancel')"/>
     <command id="downloadsCmd_unblock"
              oncommand="goDoCommand('downloadsCmd_unblock')"/>
+    <command id="downloadsCmd_chooseUnblock"
+             oncommand="goDoCommand('downloadsCmd_chooseUnblock')"/>
+    <command id="downloadsCmd_chooseOpen"
+             oncommand="goDoCommand('downloadsCmd_chooseOpen')"/>
     <command id="downloadsCmd_confirmBlock"
              oncommand="goDoCommand('downloadsCmd_confirmBlock')"/>
     <command id="downloadsCmd_open"
              oncommand="goDoCommand('downloadsCmd_open')"/>
     <command id="downloadsCmd_show"
              oncommand="goDoCommand('downloadsCmd_show')"/>
     <command id="downloadsCmd_retry"
              oncommand="goDoCommand('downloadsCmd_retry')"/>
@@ -66,18 +70,18 @@
                   label="&cmd.resume.label;"
                   accesskey="&cmd.resume.accesskey;"/>
         <menuitem command="downloadsCmd_cancel"
                   class="downloadCancelMenuItem"
                   label="&cmd.cancel.label;"
                   accesskey="&cmd.cancel.accesskey;"/>
         <menuitem command="downloadsCmd_unblock"
                   class="downloadUnblockMenuItem"
-                  label="&cmd.unblock.label;"
-                  accesskey="&cmd.unblock.accesskey;"/>
+                  label="&cmd.unblock2.label;"
+                  accesskey="&cmd.unblock2.accesskey;"/>
         <menuitem command="cmd_delete"
                   class="downloadRemoveFromHistoryMenuItem"
                   label="&cmd.removeFromHistory.label;"
                   accesskey="&cmd.removeFromHistory.accesskey;"/>
         <menuitem command="downloadsCmd_show"
                   class="downloadShowMenuItem"
 #ifdef XP_MACOSX
                   label="&cmd.showMac.label;"
--- a/browser/components/downloads/test/browser/browser_confirm_unblock_download.js
+++ b/browser/components/downloads/test/browser/browser_confirm_unblock_download.js
@@ -26,21 +26,91 @@ function addDialogOpenObserver(buttonAct
           let doc = subj.document.documentElement;
           doc.getButton(buttonAction).click();
         }
       });
     }
   });
 }
 
-add_task(function* test_confirm_unblock_dialog_unblock() {
-  addDialogOpenObserver("accept");
-  let result = yield DownloadsCommon.confirmUnblockDownload(Downloads.Error.BLOCK_VERDICT_MALWARE,
-                                                            window);
-  is(result, "unblock");
+function* assertDialogResult({ args, buttonToClick, expectedResult }) {
+  addDialogOpenObserver(buttonToClick);
+  is(yield DownloadsCommon.confirmUnblockDownload(args), expectedResult);
+}
+
+/**
+ * Tests the "unblock" dialog, for each of the possible verdicts.
+ */
+add_task(function* test_unblock_dialog_unblock() {
+  for (let verdict of [Downloads.Error.BLOCK_VERDICT_MALWARE,
+                       Downloads.Error.BLOCK_VERDICT_POTENTIALLY_UNWANTED,
+                       Downloads.Error.BLOCK_VERDICT_UNCOMMON]) {
+    let args = { verdict, window, dialogType: "unblock" };
+
+    // Test both buttons.
+    yield assertDialogResult({
+      args,
+      buttonToClick: "accept",
+      expectedResult: "unblock",
+    });
+    yield assertDialogResult({
+      args,
+      buttonToClick: "cancel",
+      expectedResult: "cancel",
+    });
+  }
 });
 
-add_task(function* test_confirm_unblock_dialog_keep_safe() {
-  addDialogOpenObserver("cancel");
-  let result = yield DownloadsCommon.confirmUnblockDownload(Downloads.Error.BLOCK_VERDICT_MALWARE,
-                                                            window);
-  is(result, "cancel");
+/**
+ * Tests the "chooseUnblock" dialog for potentially unwanted downloads.
+ */
+add_task(function* test_chooseUnblock_dialog() {
+  let args = {
+    verdict: Downloads.Error.BLOCK_VERDICT_POTENTIALLY_UNWANTED,
+    window,
+    dialogType: "chooseUnblock",
+  };
+
+  // Test each of the three buttons.
+  yield assertDialogResult({
+    args,
+    buttonToClick: "accept",
+    expectedResult: "unblock",
+  });
+  yield assertDialogResult({
+    args,
+    buttonToClick: "cancel",
+    expectedResult: "cancel",
+  });
+  yield assertDialogResult({
+    args,
+    buttonToClick: "extra1",
+    expectedResult: "confirmBlock",
+  });
 });
+
+/**
+ * Tests the "chooseOpen" dialog for uncommon downloads.
+ */
+add_task(function* test_chooseOpen_dialog() {
+  let args = {
+    verdict: Downloads.Error.BLOCK_VERDICT_UNCOMMON,
+    window,
+    dialogType: "chooseOpen",
+  };
+
+  // Test each of the three buttons.
+  yield assertDialogResult({
+    args,
+    buttonToClick: "accept",
+    expectedResult: "open",
+  });
+  yield assertDialogResult({
+    args,
+    buttonToClick: "cancel",
+    expectedResult: "cancel",
+  });
+  yield assertDialogResult({
+    args,
+    buttonToClick: "extra1",
+    expectedResult: "confirmBlock",
+  });
+});
--- a/browser/locales/en-US/chrome/browser/downloads/downloads.dtd
+++ b/browser/locales/en-US/chrome/browser/downloads/downloads.dtd
@@ -61,28 +61,37 @@
 <!ENTITY cmd.copyDownloadLink.label       "Copy Download Link">
 <!ENTITY cmd.copyDownloadLink.accesskey   "L">
 <!ENTITY cmd.removeFromHistory.label      "Remove From History">
 <!ENTITY cmd.removeFromHistory.accesskey  "e">
 <!ENTITY cmd.clearList.label              "Clear List">
 <!ENTITY cmd.clearList.accesskey          "a">
 <!ENTITY cmd.clearDownloads.label         "Clear Downloads">
 <!ENTITY cmd.clearDownloads.accesskey     "D">
-<!-- LOCALIZATION NOTE (cmd.unblock.label):
-     This command may be shown in the context menu, as a menu button item, or as
-     a text link when malware or potentially unwanted downloads are blocked.
+<!-- LOCALIZATION NOTE (cmd.unblock2.label):
+     This command is shown in the context menu when downloads are blocked.
      -->
-<!ENTITY cmd.unblock.label                "Unblock">
-<!ENTITY cmd.unblock.accesskey            "U">
+<!ENTITY cmd.unblock2.label               "Allow Download">
+<!ENTITY cmd.unblock2.accesskey           "o">
 <!-- LOCALIZATION NOTE (cmd.removeFile.label):
-     This command may be shown in the context menu or as a menu button label
-     when malware or potentially unwanted downloads are blocked.
+     This is the tooltip of the action button shown when malware is blocked.
      -->
 <!ENTITY cmd.removeFile.label             "Remove File">
-<!ENTITY cmd.removeFile.accesskey         "m">
+<!-- LOCALIZATION NOTE (cmd.chooseUnblock.tooltip):
+     This is the tooltip of the action button shown when potentially unwanted
+     downloads are blocked. This opens a dialog where the user can choose
+     whether to unblock or remove the download. Removing is the default option.
+     -->
+<!ENTITY cmd.chooseUnblock.label          "Remove File or Allow Download">
+<!-- LOCALIZATION NOTE (cmd.chooseOpen.tooltip):
+     This is the tooltip of the action button shown when uncommon downloads are
+     blocked.This opens a dialog where the user can choose whether to open the
+     file or remove the download. Opening is the default option.
+     -->
+<!ENTITY cmd.chooseOpen.label             "Open or Remove File">
 
 <!-- LOCALIZATION NOTE (blocked.label):
      Shown as a tag before the file name for some types of blocked downloads.
      Note: This string doesn't exist in the UI yet.  See bug 1053890.
      -->
 <!ENTITY blocked.label                    "BLOCKED">
 
 <!-- LOCALIZATION NOTE (learnMore.label):
--- a/browser/locales/en-US/chrome/browser/downloads/downloads.properties
+++ b/browser/locales/en-US/chrome/browser/downloads/downloads.properties
@@ -34,41 +34,43 @@ stateBlockedParentalControls=Blocked by 
 # languages:
 # http://support.microsoft.com/kb/174360
 stateBlockedPolicy=Blocked by your security zone policy
 # LOCALIZATION NOTE (stateDirty):
 # Indicates that the download was blocked after scanning.
 stateDirty=Blocked: May contain a virus or spyware
 
 # LOCALIZATION NOTE (blockedMalware, blockedPotentiallyUnwanted,
-#                    blockedUncommon):
+#                    blockedUncommon2):
 # These strings are shown in the panel for some types of blocked downloads, and
 # are immediately followed by the "Learn More" link, thus they must end with a
 # period.  You may need to adjust "downloadDetails.width" in "downloads.dtd" if
 # this turns out to be longer than the other existing status strings.
 # Note: These strings don't exist in the UI yet.  See bug 1053890.
 blockedMalware=This file contains a virus or malware.
 blockedPotentiallyUnwanted=This file may harm your computer.
-blockedUncommon=This file may not be safe to open.
+blockedUncommon2=This file is not commonly downloaded.
 
-# LOCALIZATION NOTE (unblockHeader, unblockTypeMalware,
-#                    unblockTypePotentiallyUnwanted, unblockTypeUncommon,
-#                    unblockTip, unblockButtonContinue, unblockButtonCancel):
+# LOCALIZATION NOTE (unblockHeaderUnblock, unblockHeaderOpen,
+#                    unblockTypeMalware, unblockTypePotentiallyUnwanted,
+#                    unblockTypeUncommon, unblockTip, unblockButtonOpen,
+#                    unblockButtonUnblock, unblockButtonConfirmBlock):
 # These strings are displayed in the dialog shown when the user asks a blocked
 # download to be unblocked.  The severity of the threat is expressed in
 # descending order by the unblockType strings, it is higher for files detected
 # as malware and lower for uncommon downloads.
-# Note: These strings don't exist in the UI yet.  See bug 1053890.
-unblockHeader=Are you sure you want to unblock this file?
+unblockHeaderUnblock=Are you sure you want to allow this download?
+unblockHeaderOpen=Are you sure you want to open this file?
 unblockTypeMalware=This file contains a virus or other malware that will harm your computer.
 unblockTypePotentiallyUnwanted=This file, disguised as a helpful download, will make unexpected changes to your programs and settings.
 unblockTypeUncommon=This file has been downloaded from an unfamiliar and potentially dangerous website and may not be safe to open.
 unblockTip=You can search for an alternate download source or try to download the file again later.
-unblockButtonContinue=Unblock anyway
-unblockButtonCancel=Keep me safe
+unblockButtonOpen=Open
+unblockButtonUnblock=Allow download
+unblockButtonConfirmBlock=Remove file
 
 # LOCALIZATION NOTE (sizeWithUnits):
 # %1$S is replaced with the size number, and %2$S with the measurement unit.
 sizeWithUnits=%1$S %2$S
 sizeUnknown=Unknown size
 
 # LOCALIZATION NOTE (shortTimeLeftSeconds, shortTimeLeftMinutes,
 #                    shortTimeLeftHours, shortTimeLeftDays):
--- a/browser/locales/en-US/chrome/browser/safebrowsing/phishing-afterload-warning-message.dtd
+++ b/browser/locales/en-US/chrome/browser/safebrowsing/phishing-afterload-warning-message.dtd
@@ -1,16 +1,23 @@
 <!-- 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 safeb.palm.accept.label "Get me out of here!">
 <!ENTITY safeb.palm.decline.label "Ignore this warning">
-<!-- Localization note (safeb.palm.notdeceptive.label) - Label of the Help menu item. -->
+<!-- Localization note (safeb.palm.notdeceptive.label) - Label of the Help menu
+  item. Either this or reportDeceptiveSiteMenu.label from report-phishing.dtd is
+  shown. -->
 <!ENTITY safeb.palm.notdeceptive.label "This isn’t a deceptive site…">
+<!-- Localization note (safeb.palm.notdeceptive.accesskey) - Because
+  safeb.palm.notdeceptive.label and reportDeceptiveSiteMenu.title from
+  report-phishing.dtd are never shown at the same time, the same accesskey can
+  be used for them. -->
+<!ENTITY safeb.palm.notdeceptive.accesskey "d">
 <!ENTITY safeb.palm.reportPage.label "Why was this page blocked?">
 <!ENTITY safeb.palm.whyForbidden.label "Why was this page blocked?">
 
 <!ENTITY safeb.blocked.malwarePage.title "Reported Attack Page!">
 <!-- Localization note (safeb.blocked.malwarePage.shortDesc) - Please don't translate the contents of the <span id="malware_sitename"/> tag.  It will be replaced at runtime with a domain name (e.g. www.badsite.com) -->
 <!ENTITY safeb.blocked.malwarePage.shortDesc "This web page at <span id='malware_sitename'/> has been reported as an attack page and has been blocked based on your security preferences.">
 <!ENTITY safeb.blocked.malwarePage.longDesc "<p>Attack pages try to install programs that steal private information, use your computer to attack others, or damage your system.</p><p>Some attack pages intentionally distribute harmful software, but many are compromised without the knowledge or permission of their owners.</p>">
 
--- a/browser/locales/en-US/chrome/browser/safebrowsing/report-phishing.dtd
+++ b/browser/locales/en-US/chrome/browser/safebrowsing/report-phishing.dtd
@@ -1,6 +1,13 @@
 <!-- This Source Code Form is subject to the terms of the Mozilla Public
    - License, v. 2.0. If a copy of the MPL was not distributed with this
    - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
 
+<!-- Localization note (reportDeceptiveSiteMenu.title) - Label of the Help menu
+  item. Either this or safeb.palm.notdeceptive.label from
+  phishing-afterload-warning-message.dtd is shown. -->
 <!ENTITY reportDeceptiveSiteMenu.title      "Report deceptive site…">
+<!-- Localization note (reportDeceptiveSiteMenu.accesskey) - Because
+  safeb.palm.notdeceptive.label from phishing-afterload-warning-message.dtd and
+  reportDeceptiveSiteMenu.title are never shown at the same time, the same
+  accesskey can be used for them. -->
 <!ENTITY reportDeceptiveSiteMenu.accesskey  "D">
--- a/browser/themes/shared/controlcenter/panel.inc.css
+++ b/browser/themes/shared/controlcenter/panel.inc.css
@@ -98,16 +98,17 @@
   background-position: 1em 1em;
   background-size: 24px auto;
 }
 
 #identity-popup-security-content,
 #identity-popup-permissions-content,
 #tracking-protection-content {
   padding: 0.5em 0 1em;
+  /* .identity-popup-headline.host depends on this width */
   -moz-padding-start: calc(2em + 24px);
   -moz-padding-end: 1em;
 }
 
 #identity-popup-securityView:-moz-locale-dir(rtl),
 #identity-popup-security-content:-moz-locale-dir(rtl),
 #identity-popup-permissions-content:-moz-locale-dir(rtl),
 #tracking-protection-content:-moz-locale-dir(rtl) {
@@ -115,17 +116,17 @@
 }
 
 /* EXPAND BUTTON */
 
 .identity-popup-expander {
   margin: 0;
   padding: 4px 0;
   min-width: auto;
-  width: 38px;
+  width: var(--identity-popup-expander-width);
   border: 0 none;
   -moz-appearance: none;
   background-image: url("chrome://browser/skin/controlcenter/arrow-subview.svg"),
                     linear-gradient(rgba(255,255,255,0.3), transparent);
   background-size: 16px, auto;
   background-position: center;
   background-repeat: no-repeat;
   background-color: transparent;
@@ -176,16 +177,23 @@
   margin: 0;
 }
 
 .identity-popup-headline {
   margin: 3px 0 4px;
   font-size: 150%;
 }
 
+.identity-popup-headline.host {
+  word-wrap: break-word;
+  /* 1em + 2em + 24px is #identity-popup-security-content padding
+   * 30em is .panel-mainview:not([panelid="PanelUI-popup"]) width */
+  max-width: calc(30rem - 3rem - 24px - var(--identity-popup-expander-width))
+}
+
 .identity-popup-warning-gray {
   -moz-padding-start: 24px;
   background: url(chrome://browser/skin/controlcenter/warning-gray.svg) no-repeat 0 50%;
 }
 
 .identity-popup-warning-yellow {
   -moz-padding-start: 24px;
   background: url(chrome://browser/skin/controlcenter/warning-yellow.svg) no-repeat 0 50%;
--- a/devtools/client/commandline/test/browser.ini
+++ b/devtools/client/commandline/test/browser.ini
@@ -29,16 +29,18 @@ support-files =
   browser_cmd_appcache_valid_page1.html
   browser_cmd_appcache_valid_page2.html
   browser_cmd_appcache_valid_page3.html
 [browser_cmd_commands.js]
 [browser_cmd_cookie.js]
 support-files =
  browser_cmd_cookie.html
 [browser_cmd_cookie_host.js]
+support-files =
+ browser_cmd_cookie.html
 [browser_cmd_csscoverage_oneshot.js]
 support-files =
  browser_cmd_csscoverage_page1.html
  browser_cmd_csscoverage_page2.html
  browser_cmd_csscoverage_page3.html
  browser_cmd_csscoverage_sheetA.css
  browser_cmd_csscoverage_sheetB.css
  browser_cmd_csscoverage_sheetC.css
--- a/devtools/client/commandline/test/browser_cmd_cookie.html
+++ b/devtools/client/commandline/test/browser_cmd_cookie.html
@@ -6,13 +6,14 @@
 </head>
 <body>
 
   <p>Cookie test</p>
   <p id=result></p>
   <script type="text/javascript">
     document.cookie = "zap=zep";
     document.cookie = "zip=zop";
+    document.cookie = "zig=zag; domain=.mochi.test";
     document.getElementById("result").innerHTML = document.cookie;
   </script>
 
 </body>
 </html>
--- a/devtools/client/commandline/test/browser_cmd_cookie_host.js
+++ b/devtools/client/commandline/test/browser_cmd_cookie_host.js
@@ -1,23 +1,25 @@
 /* Any copyright is dedicated to the Public Domain.
  * http://creativecommons.org/publicdomain/zero/1.0/ */
 
+"use strict";
+
 // Tests that the cookie command works for host with a port specified
 
-const TEST_URI = "http://mochi.test:8888/browser/devtools/client/commandline/"+
+const TEST_URI = "http://mochi.test:8888/browser/devtools/client/commandline/" +
                  "test/browser_cmd_cookie.html";
 
 function test() {
   helpers.addTabWithToolbar(TEST_URI, function(options) {
     return helpers.audit(options, [
         {
           setup: 'cookie list',
           exec: {
-            output: [ /zap=zep/, /zip=zop/ ],
+            output: [ /zap=zep/, /zip=zop/, /zig=zag/ ],
           }
         },
         {
           setup: "cookie set zup banana",
           check: {
             args: {
               name: { value: 'zup' },
               value: { value: 'banana' },
@@ -25,15 +27,15 @@ function test() {
           },
           exec: {
             output: ""
           }
         },
         {
           setup: "cookie list",
           exec: {
-            output: [ /zap=zep/, /zip=zop/, /zup=banana/, /Edit/ ]
+            output: [ /zap=zep/, /zip=zop/, /zig=zag/, /zup=banana/, /Edit/ ]
           }
         }
     ]);
   }).then(finish, helpers.handleError);
 }
 
new file mode 100644
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/addon-source/browser_dbg_addon_webext_contentscript/manifest.json
@@ -0,0 +1,18 @@
+{
+  "manifest_version": 2,
+  "name": "test content script sources",
+  "description": "test content script sources",
+  "version": "0.1.0",
+  "applications": {
+    "gecko": {
+      "id": "test-contentscript-sources@mozilla.com"
+    }
+  },
+  "content_scripts": [
+    {
+      "matches": ["<all_urls>"],
+      "js": ["webext-content-script.js"],
+      "run_at": "document_start"
+    }
+  ]
+}
new file mode 100644
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/addon-source/browser_dbg_addon_webext_contentscript/webext-content-script.js
@@ -0,0 +1,1 @@
+console.log("CONTENT SCRIPT LOADED");
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..484fdd73d8477155422d9d5ccf6b7c2bdefd8c6e
GIT binary patch
literal 661
zc$^FHW@Zs#U|`^2@Z@RtSh=&(tPsen0b&UT8HV!Iq|}NM-Q@hdlGMBs-Qwh;%z_fV
ztm4oRP6p<mb6CScxU_<sfsy3}GXn#d@IH55``mfYtEW%;Uk@ny_MN-%D|aAA8Q&)!
zj=&Ee%gf74121iP_Q>m;#;NnZSGBdyYM*4e^s{o`%A-d+U#_g`)YP<_c~o^}<w{p6
zshvk(&f?lu#&tD@i|gbGw%@-sf9zsox8Ai|>!n6RNM!NVtRhB+0Js+-YI{7EegC!p
z2$0vr$iTo0^g?c8US?WqG04^Vd0=;k|K`Ew?$*AOd`$)dZSQSdFB=Pd$>{lL+oPB)
zx%F1-7T4aE^74$xzPMXw_Mb7YJtlEI;fZMNiuf0MwzR#!VJ6SZES347(MN3!X9m;f
zLno)%-L`C;cRZol)A#BBpQ1-r7X^oTwr&dzI<VvD<g1h2Uhy8itf$PmX{O}SBOV<|
zPBL5ndM#Zb681Sl>V>RQlGc@#hCzOY8+EP=9Fbjo@cV+0yOvM+|I9hx=>F+m#liDM
z4impkO<Cu8eWui*itJ?9<;rFa+^>C~=bWeu@MdI^W5yMq5)2>!3>StijUX28*ky&p
nE?Nj7n~oXO$fh3#l4t?X3JG`&BU#x%+L(ZFBal7^($4?@DqHZo
--- a/devtools/client/debugger/test/mochitest/browser.ini
+++ b/devtools/client/debugger/test/mochitest/browser.ini
@@ -3,16 +3,17 @@ tags = devtools
 subsuite = devtools
 skip-if = (os == 'linux' && debug && bits == 32)
 support-files =
   addon1.xpi
   addon2.xpi
   addon3.xpi
   addon4.xpi
   addon5.xpi
+  addon-webext-contentscript.xpi
   code_binary_search.coffee
   code_binary_search.js
   code_binary_search.map
   code_blackboxing_blackboxme.js
   code_blackboxing_one.js
   code_blackboxing_three.js
   code_blackboxing_two.js
   code_blackboxing_unblackbox.min.js
@@ -104,16 +105,17 @@ support-files =
   doc_scope-variable.html
   doc_scope-variable-2.html
   doc_scope-variable-3.html
   doc_scope-variable-4.html
   doc_script-eval.html
   doc_script-bookmarklet.html
   doc_script-switching-01.html
   doc_script-switching-02.html
+  doc_script_webext_contentscript.html
   doc_split-console-paused-reload.html
   doc_step-many-statements.html
   doc_step-out.html
   doc_terminate-on-tab-close.html
   doc_watch-expressions.html
   doc_watch-expression-button.html
   doc_with-frame.html
   doc_worker-source-map.html
@@ -461,16 +463,17 @@ skip-if = true # non-named eval sources 
 skip-if = e10s && debug
 [browser_dbg_sources-labels.js]
 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
 [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
new file mode 100644
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_sources-webext-contentscript.js
@@ -0,0 +1,61 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Make sure eval scripts appear in the source list
+ */
+
+const TAB_URL = EXAMPLE_URL + "doc_script_webext_contentscript.html";
+
+function test() {
+  let gPanel, gDebugger;
+  let gSources, gAddon;
+
+  let cleanup = function* (e) {
+    if (gAddon) {
+      // Remove the addon, if any.
+      yield removeAddon(gAddon);
+    }
+    if (gPanel) {
+      // Close the debugger panel, if any.
+      yield closeDebuggerAndFinish(gPanel);
+    } else {
+      // If no debugger panel was opened, call finish directly.
+      finish();
+    }
+  };
+
+  return Task.spawn(function* () {
+    gAddon = yield addAddon(EXAMPLE_URL + "/addon-webext-contentscript.xpi");
+
+    [,, gPanel] = yield initDebugger(TAB_URL);
+    gDebugger = gPanel.panelWin;
+    gSources = gDebugger.DebuggerView.Sources;
+
+    // Wait for a SOURCE_SHOWN event for at most 4 seconds.
+    yield Promise.race([
+      waitForDebuggerEvents(gPanel, gDebugger.EVENTS.SOURCE_SHOWN),
+      waitForTime(4000),
+    ]);
+
+    is(gSources.values.length, 1, "Should have 1 source");
+
+    let item = gSources.getItemForAttachment(attachment => {
+      return attachment.source.url.includes("moz-extension");
+    });
+
+    ok(item, "Got the expected WebExtensions ContentScript source");
+    ok(item && item.attachment.source.url.includes(item.attachment.group),
+       "The source is in the expected source group");
+    is(item && item.attachment.label, "webext-content-script.js",
+       "Got the expected filename in the label");
+
+    yield cleanup();
+  }).catch((e) => {
+    ok(false, `Got an unexpected exception: ${e}`);
+    // Cleanup in case of failures in the above task.
+    return Task.spawn(cleanup);
+  });
+}
new file mode 100644
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/doc_script_webext_contentscript.html
@@ -0,0 +1,13 @@
+<!-- Any copyright is dedicated to the Public Domain.
+     http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!doctype html>
+
+<html>
+  <head>
+    <meta charset="utf-8"/>
+    <title>Debugger test page</title>
+  </head>
+
+  <body>
+  </body>
+</html>
--- a/devtools/client/framework/test/shared-head.js
+++ b/devtools/client/framework/test/shared-head.js
@@ -160,48 +160,70 @@ function synthesizeKeyFromKeyTag(key) {
     accelKey: !!modifiersAttr.match("accel")
   };
 
   info("Synthesizing key " + name + " " + JSON.stringify(modifiers));
   EventUtils.synthesizeKey(name, modifiers);
 }
 
 /**
- * Wait for eventName on target.
- * @param {Object} target An observable object that either supports on/off or
- * addEventListener/removeEventListener
+ * Wait for eventName on target to be delivered a number of times.
+ *
+ * @param {Object} target
+ *        An observable object that either supports on/off or
+ *        addEventListener/removeEventListener
  * @param {String} eventName
- * @param {Boolean} useCapture Optional, for
- *        addEventListener/removeEventListener
+ * @param {Number} numTimes
+ *        Number of deliveries to wait for.
+ * @param {Boolean} useCapture
+ *        Optional, for addEventListener/removeEventListener
  * @return A promise that resolves when the event has been handled
  */
-function once(target, eventName, useCapture = false) {
+function waitForNEvents(target, eventName, numTimes, useCapture = false) {
   info("Waiting for event: '" + eventName + "' on " + target + ".");
 
   let deferred = promise.defer();
+  let count = 0;
 
   for (let [add, remove] of [
     ["addEventListener", "removeEventListener"],
     ["addListener", "removeListener"],
     ["on", "off"]
   ]) {
     if ((add in target) && (remove in target)) {
       target[add](eventName, function onEvent(...aArgs) {
         info("Got event: '" + eventName + "' on " + target + ".");
-        target[remove](eventName, onEvent, useCapture);
-        deferred.resolve.apply(deferred, aArgs);
+        if (++count == numTimes) {
+          target[remove](eventName, onEvent, useCapture);
+          deferred.resolve.apply(deferred, aArgs);
+        }
       }, useCapture);
       break;
     }
   }
 
   return deferred.promise;
 }
 
 /**
+ * Wait for eventName on target.
+ *
+ * @param {Object} target
+ *        An observable object that either supports on/off or
+ *        addEventListener/removeEventListener
+ * @param {String} eventName
+ * @param {Boolean} useCapture
+ *        Optional, for addEventListener/removeEventListener
+ * @return A promise that resolves when the event has been handled
+ */
+function once(target, eventName, useCapture = false) {
+  return waitForNEvents(target, eventName, 1, useCapture);
+}
+
+/**
  * Some tests may need to import one or more of the test helper scripts.
  * A test helper script is simply a js file that contains common test code that
  * is either not common-enough to be in head.js, or that is located in a
  * separate directory.
  * The script will be loaded synchronously and in the test's scope.
  * @param {String} filePath The file path, relative to the current directory.
  *                 Examples:
  *                 - "helper_attributes_test_runner.js"
@@ -218,16 +240,30 @@ function loadHelperScript(filePath) {
  */
 function waitForTick() {
   let deferred = promise.defer();
   executeSoon(deferred.resolve);
   return deferred.promise;
 }
 
 /**
+ * This shouldn't be used in the tests, but is useful when writing new tests or
+ * debugging existing tests in order to introduce delays in the test steps
+ *
+ * @param {Number} ms
+ *        The time to wait
+ * @return A promise that resolves when the time is passed
+ */
+function wait(ms) {
+  let def = promise.defer();
+  content.setTimeout(def.resolve, ms);
+  return def.promise;
+}
+
+/**
  * Open the toolbox in a given tab.
  * @param {XULNode} tab The tab the toolbox should be opened in.
  * @param {String} toolId Optional. The ID of the tool to be selected.
  * @param {String} hostType Optional. The type of toolbox host to be used.
  * @return {Promise} Resolves with the toolbox, when it has been opened.
  */
 var openToolboxForTab = Task.async(function* (tab, toolId, hostType) {
   info("Opening the toolbox");
--- a/devtools/client/framework/toolbox-process-window.js
+++ b/devtools/client/framework/toolbox-process-window.js
@@ -57,16 +57,18 @@ var connect = Task.async(function*() {
 // Certain options should be toggled since we can assume chrome debugging here
 function setPrefDefaults() {
   Services.prefs.setBoolPref("devtools.inspector.showUserAgentStyles", true);
   Services.prefs.setBoolPref("devtools.performance.ui.show-platform-data", true);
   Services.prefs.setBoolPref("devtools.inspector.showAllAnonymousContent", true);
   Services.prefs.setBoolPref("browser.dom.window.dump.enabled", true);
   Services.prefs.setBoolPref("devtools.command-button-noautohide.enabled", true);
   Services.prefs.setBoolPref("devtools.scratchpad.enabled", true);
+  // Bug 1225160 - Using source maps with browser debugging can lead to a crash
+  Services.prefs.setBoolPref("devtools.debugger.source-maps-enabled", false);
 }
 
 window.addEventListener("load", function() {
   let cmdClose = document.getElementById("toolbox-cmd-close");
   cmdClose.addEventListener("command", onCloseCommand);
   setPrefDefaults();
   connect().catch(e => {
     let errorMessageContainer = document.getElementById("error-message-container");
--- a/devtools/client/inspector/computed/test/head.js
+++ b/devtools/client/inspector/computed/test/head.js
@@ -8,83 +8,25 @@ Services.scriptloader.loadSubScript(
   "chrome://mochitests/content/browser/devtools/client/inspector/test/head.js",
   this);
 
 registerCleanupFunction(() => {
   Services.prefs.clearUserPref("devtools.defaultColorUnit");
 });
 
 /**
- * Open the toolbox, with the inspector tool visible, and the computed-view
- * sidebar tab selected.
- * @return a promise that resolves when the inspector is ready and the computed
- * view is visible and ready
- */
-function openComputedView() {
-  return openInspectorSidebarTab("computedview").then(({toolbox, inspector}) => {
-    return {
-      toolbox,
-      inspector,
-      view: inspector.computedview.view
-    };
-  });
-}
-
-/**
- * Get the NodeFront for a given css selector, via the protocol
- *
- * @param {String} selector
- * @param {InspectorPanel} inspector
- *        The instance of InspectorPanel currently loaded in the toolbox
- * @return {Promise} Resolves to the NodeFront instance
- */
-function getNodeFront(selector, {walker}) {
-  return walker.querySelector(walker.rootNode, selector);
-}
-
-/**
- * Listen for a new tab to open and return a promise that resolves when one
- * does and completes the load event.
- *
- * @return a promise that resolves to the tab object
- */
-var waitForTab = Task.async(function*() {
-  info("Waiting for a tab to open");
-  yield once(gBrowser.tabContainer, "TabOpen");
-  let tab = gBrowser.selectedTab;
-  let browser = tab.linkedBrowser;
-  yield once(browser, "load", true);
-  info("The tab load completed");
-  return tab;
-});
-
-/**
  * Dispatch the copy event on the given element
  */
 function fireCopyEvent(element) {
   let evt = element.ownerDocument.createEvent("Event");
   evt.initEvent("copy", true, true);
   element.dispatchEvent(evt);
 }
 
 /**
- * Simulate the key input for the given input in the window.
- *
- * @param {String} input
- *        The string value to input
- * @param {Window} win
- *        The window containing the panel
- */
-function synthesizeKeys(input, win) {
-  for (let key of input.split("")) {
-    EventUtils.synthesizeKey(key, {}, win);
-  }
-}
-
-/**
  * Get references to the name and value span nodes corresponding to a given
  * property name in the computed-view
  *
  * @param {CssComputedView} view
  *        The instance of the computed view panel
  * @param {String} name
  *        The name of the property to retrieve
  * @return an object {nameSpan, valueSpan}
--- a/devtools/client/inspector/rules/test/head.js
+++ b/devtools/client/inspector/rules/test/head.js
@@ -37,86 +37,16 @@ addTab = function(url) {
     info("Loading the helper frame script " + FRAME_SCRIPT_URL);
     let browser = tab.linkedBrowser;
     browser.messageManager.loadFrameScript(FRAME_SCRIPT_URL, false);
     return tab;
   });
 };
 
 /**
- * Open the toolbox, with the inspector tool visible, and the rule-view
- * sidebar tab selected.
- *
- * @return a promise that resolves when the inspector is ready and the rule
- * view is visible and ready
- */
-function openRuleView() {
-  return openInspectorSidebarTab("ruleview").then(data => {
-    return {
-      toolbox: data.toolbox,
-      inspector: data.inspector,
-      testActor: data.testActor,
-      view: data.inspector.ruleview.view
-    };
-  });
-}
-
-/**
- * Set the inspector's current selection to null so that no node is selected
- *
- * @param {InspectorPanel} inspector
- *        The instance of InspectorPanel currently loaded in the toolbox
- * @return a promise that resolves when the inspector is updated
- */
-function clearCurrentNodeSelection(inspector) {
-  info("Clearing the current selection");
-  let updated = inspector.once("inspector-updated");
-  inspector.selection.setNodeFront(null);
-  return updated;
-}
-
-/**
- * Wait for eventName on target to be delivered a number of times.
- *
- * @param {Object} target
- *        An observable object that either supports on/off or
- *        addEventListener/removeEventListener
- * @param {String} eventName
- * @param {Number} numTimes
- *        Number of deliveries to wait for.
- * @param {Boolean} useCapture
- *        Optional, for addEventListener/removeEventListener
- * @return A promise that resolves when the event has been handled
- */
-function waitForNEvents(target, eventName, numTimes, useCapture = false) {
-  info("Waiting for event: '" + eventName + "' on " + target + ".");
-
-  let deferred = promise.defer();
-  let count = 0;
-
-  for (let [add, remove] of [
-    ["addEventListener", "removeEventListener"],
-    ["addListener", "removeListener"],
-    ["on", "off"]
-  ]) {
-    if ((add in target) && (remove in target)) {
-      target[add](eventName, function onEvent(...aArgs) {
-        if (++count == numTimes) {
-          target[remove](eventName, onEvent, useCapture);
-          deferred.resolve.apply(deferred, aArgs);
-        }
-      }, useCapture);
-      break;
-    }
-  }
-
-  return deferred.promise;
-}
-
-/**
  * Wait for a content -> chrome message on the message manager (the window
  * messagemanager is used).
  *
  * @param {String} name
  *        The message name
  * @return {Promise} A promise that resolves to the response data when the
  * message has been received
  */
@@ -280,32 +210,16 @@ function assertHoverTooltipOn(tooltip, e
  */
 function* hideTooltipAndWaitForRuleViewChanged(tooltip, view) {
   let onModified = view.once("ruleview-changed");
   tooltip.hide();
   yield onModified;
 }
 
 /**
- * Listen for a new tab to open and return a promise that resolves when one
- * does and completes the load event.
- *
- * @return a promise that resolves to the tab object
- */
-var waitForTab = Task.async(function* () {
-  info("Waiting for a tab to open");
-  yield once(gBrowser.tabContainer, "TabOpen");
-  let tab = gBrowser.selectedTab;
-  let browser = tab.linkedBrowser;
-  yield once(browser, "load", true);
-  info("The tab load completed");
-  return tab;
-});
-
-/**
  * Polls a given generator function waiting for it to return true.
  *
  * @param {Function} validatorFn
  *        A validator generator function that returns a boolean.
  *        This is called every few milliseconds to check if the result is true.
  *        When it is true, the promise resolves.
  * @param {String} name
  *        Optional name of the test. This is used to generate
@@ -344,30 +258,16 @@ var getFontFamilyDataURL = Task.async(fu
       "black" : "white";
 
   let {data} = yield nodeFront.getFontFamilyDataURL(font, fillStyle);
   let dataURL = yield data.string();
   return dataURL;
 });
 
 /**
- * Simulate the key input for the given input in the window.
- *
- * @param {String} input
- *        The string value to input
- * @param {Window} win
- *        The window containing the panel
- */
-function synthesizeKeys(input, win) {
-  for (let key of input.split("")) {
-    EventUtils.synthesizeKey(key, {}, win);
-  }
-}
-
-/**
  * Get the DOMNode for a css rule in the rule-view that corresponds to the given
  * selector
  *
  * @param {CssRuleView} view
  *        The instance of the rule-view panel
  * @param {String} selectorText
  *        The selector in the rule-view for which the rule
  *        object is wanted
--- a/devtools/client/inspector/shared/test/browser.ini
+++ b/devtools/client/inspector/shared/test/browser.ini
@@ -7,21 +7,27 @@ support-files =
   doc_content_stylesheet.xul
   doc_content_stylesheet_imported.css
   doc_content_stylesheet_imported2.css
   doc_content_stylesheet_linked.css
   doc_content_stylesheet_script.css
   doc_content_stylesheet_xul.css
   doc_frame_script.js
   head.js
+  !/devtools/client/commandline/test/helpers.js
+  !/devtools/client/inspector/test/head.js
+  !/devtools/client/framework/test/shared-head.js
+  !/devtools/client/shared/test/test-actor.js
+  !/devtools/client/shared/test/test-actor-registry.js
 
 [browser_styleinspector_context-menu-copy-color_01.js]
 [browser_styleinspector_context-menu-copy-color_02.js]
 [browser_styleinspector_context-menu-copy-urls.js]
 [browser_styleinspector_csslogic-content-stylesheets.js]
+skip-if = e10s && debug # Bug 1250058 (docshell leak when opening 2 toolboxes)
 [browser_styleinspector_output-parser.js]
 [browser_styleinspector_refresh_when_active.js]
 [browser_styleinspector_tooltip-background-image.js]
 [browser_styleinspector_tooltip-closes-on-new-selection.js]
 skip-if = e10s # Bug 1111546 (e10s)
 [browser_styleinspector_tooltip-longhand-fontfamily.js]
 [browser_styleinspector_tooltip-multiple-background-images.js]
 [browser_styleinspector_tooltip-shorthand-fontfamily.js]
--- a/devtools/client/inspector/shared/test/browser_styleinspector_context-menu-copy-color_01.js
+++ b/devtools/client/inspector/shared/test/browser_styleinspector_context-menu-copy-color_01.js
@@ -6,41 +6,30 @@
 // Test "Copy color" item of the context menu #1: Test _isColorPopup.
 
 const TEST_URI = `
   <div style="color: #123ABC; margin: 0px; background: span[data-color];">
     Test "Copy color" context menu option
   </div>
 `;
 
-const TEST_CASES = [
-  {
-    viewName: "RuleView",
-    initializer: openRuleView
-  },
-  {
-    viewName: "ComputedView",
-    initializer: openComputedView
-  }
-];
-
 add_task(function* () {
   // Test is slow on Linux EC2 instances - Bug 1137765
   requestLongerTimeout(2);
   yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
-
-  for (let test of TEST_CASES) {
-    yield testView(test);
-  }
+  let {inspector} = yield openInspector();
+  yield testView("ruleview", inspector);
+  yield testView("computedview", inspector);
 });
 
-function* testView({viewName, initializer}) {
-  info("Testing " + viewName);
+function* testView(viewId, inspector) {
+  info("Testing " + viewId);
 
-  let {inspector, view} = yield initializer();
+  yield inspector.sidebar.select(viewId);
+  let view = inspector[viewId].view;
   yield selectNode("div", inspector);
 
   testIsColorValueNode(view);
   testIsColorPopupOnAllNodes(view);
   yield clearCurrentNodeSelection(inspector);
 }
 
 /**
--- a/devtools/client/inspector/shared/test/browser_styleinspector_context-menu-copy-color_02.js
+++ b/devtools/client/inspector/shared/test/browser_styleinspector_context-menu-copy-color_02.js
@@ -14,16 +14,17 @@ const TEST_URI = `
   </style>
   <div>Testing the color picker tooltip!</div>
 `;
 
 add_task(function* () {
   yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
 
   let {inspector, view} = yield openRuleView();
+
   yield testCopyToClipboard(inspector, view);
   yield testManualEdit(inspector, view);
   yield testColorPickerEdit(inspector, view);
 });
 
 function* testCopyToClipboard(inspector, view) {
   info("Testing that color is copied to clipboard");
 
--- a/devtools/client/inspector/shared/test/browser_styleinspector_context-menu-copy-urls.js
+++ b/devtools/client/inspector/shared/test/browser_styleinspector_context-menu-copy-urls.js
@@ -34,34 +34,34 @@ add_task(function*() {
 
   yield addTab("data:text/html;charset=utf8," + encodeURIComponent(PAGE_CONTENT));
 
   yield startTest();
 });
 
 function* startTest() {
   info("Opening rule view");
-  let ruleViewData = yield openRuleView();
+  let {inspector, view} = yield openRuleView();
 
   info("Test valid background image URL in rule view");
-  yield testCopyUrlToClipboard(ruleViewData, "data-uri", ".valid-background", TEST_DATA_URI);
-  yield testCopyUrlToClipboard(ruleViewData, "url", ".valid-background", TEST_DATA_URI);
+  yield testCopyUrlToClipboard({view, inspector}, "data-uri", ".valid-background", TEST_DATA_URI);
+  yield testCopyUrlToClipboard({view, inspector}, "url", ".valid-background", TEST_DATA_URI);
   info("Test invalid background image URL in rue view");
-  yield testCopyUrlToClipboard(ruleViewData, "data-uri", ".invalid-background", ERROR_MESSAGE);
-  yield testCopyUrlToClipboard(ruleViewData, "url", ".invalid-background", INVALID_IMAGE_URI);
+  yield testCopyUrlToClipboard({view, inspector}, "data-uri", ".invalid-background", ERROR_MESSAGE);
+  yield testCopyUrlToClipboard({view, inspector}, "url", ".invalid-background", INVALID_IMAGE_URI);
 
   info("Opening computed view");
-  let computedViewData = yield openComputedView();
+  view = selectComputedView(inspector);
 
   info("Test valid background image URL in computed view");
-  yield testCopyUrlToClipboard(computedViewData, "data-uri", ".valid-background", TEST_DATA_URI);
-  yield testCopyUrlToClipboard(computedViewData, "url", ".valid-background", TEST_DATA_URI);
+  yield testCopyUrlToClipboard({view, inspector}, "data-uri", ".valid-background", TEST_DATA_URI);
+  yield testCopyUrlToClipboard({view, inspector}, "url", ".valid-background", TEST_DATA_URI);
   info("Test invalid background image URL in computed view");
-  yield testCopyUrlToClipboard(computedViewData, "data-uri", ".invalid-background", ERROR_MESSAGE);
-  yield testCopyUrlToClipboard(computedViewData, "url", ".invalid-background", INVALID_IMAGE_URI);
+  yield testCopyUrlToClipboard({view, inspector}, "data-uri", ".invalid-background", ERROR_MESSAGE);
+  yield testCopyUrlToClipboard({view, inspector}, "url", ".invalid-background", INVALID_IMAGE_URI);
 }
 
 function* testCopyUrlToClipboard({view, inspector}, type, selector, expected) {
   info("Select node in inspector panel");
   yield selectNode(selector, inspector);
 
   info("Retrieve background-image link for selected node in current styleinspector view");
   let property = getBackgroundImageProperty(view, selector);
--- a/devtools/client/inspector/shared/test/browser_styleinspector_csslogic-content-stylesheets.js
+++ b/devtools/client/inspector/shared/test/browser_styleinspector_csslogic-content-stylesheets.js
@@ -22,36 +22,36 @@ const XUL_PRINCIPAL = ssm.createCodebase
 
 add_task(function*() {
   requestLongerTimeout(2);
 
   info("Checking stylesheets on HTML document");
   yield addTab(TEST_URI_HTML);
   let target = getNode("#target");
 
-  let {inspector} = yield openRuleView();
+  let {inspector} = yield openInspector();
   yield selectNode("#target", inspector);
 
   info("Checking stylesheets");
   yield checkSheets(target);
 
   info("Checking authored stylesheets");
   yield addTab(TEST_URI_AUTHOR);
 
-  ({inspector} = yield openRuleView());
+  ({inspector} = yield openInspector());
   target = getNode("#target");
   yield selectNode("#target", inspector);
   yield checkSheets(target);
 
   info("Checking stylesheets on XUL document");
   info("Allowing XUL content");
   allowXUL();
   yield addTab(TEST_URI_XUL);
 
-  ({inspector} = yield openRuleView());
+  ({inspector} = yield openInspector());
   target = getNode("#target");
   yield selectNode("#target", inspector);
 
   yield checkSheets(target);
   info("Disallowing XUL content");
   disallowXUL();
 });
 
--- a/devtools/client/inspector/shared/test/browser_styleinspector_refresh_when_active.js
+++ b/devtools/client/inspector/shared/test/browser_styleinspector_refresh_when_active.js
@@ -9,28 +9,29 @@
 const TEST_URI = `
   <div id="one" style="color:red;">one</div>
   <div id="two" style="color:blue;">two</div>
 `;
 
 add_task(function*() {
   yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
   let {inspector, view} = yield openRuleView();
+
   yield selectNode("#one", inspector);
 
   is(getRuleViewPropertyValue(view, "element", "color"), "red",
     "The rule-view shows the properties for test node one");
 
   let cView = inspector.computedview.view;
   let prop = getComputedViewProperty(cView, "color");
   ok(!prop, "The computed-view doesn't show the properties for test node one");
 
   info("Switching to the computed-view");
   let onComputedViewReady = inspector.once("computed-view-refreshed");
-  yield openComputedView();
+  selectComputedView(inspector);
   yield onComputedViewReady;
 
   ok(getComputedViewPropertyValue(cView, "color"), "#F00",
     "The computed-view shows the properties for test node one");
 
   info("Selecting test node two");
   yield selectNode("#two", inspector);
 
--- a/devtools/client/inspector/shared/test/browser_styleinspector_tooltip-background-image.js
+++ b/devtools/client/inspector/shared/test/browser_styleinspector_tooltip-background-image.js
@@ -38,17 +38,17 @@ add_task(function*() {
   yield testDivRuleView(view);
 
   info("Testing that image preview tooltips show even when there are " +
     "fields being edited");
   yield testTooltipAppearsEvenInEditMode(view);
 
   info("Switching over to the computed-view");
   let onComputedViewReady = inspector.once("computed-view-refreshed");
-  ({view} = yield openComputedView());
+  view = selectComputedView(inspector);
   yield onComputedViewReady;
 
   info("Testing that the background-image computed style has a tooltip too");
   yield testComputedView(view);
 });
 
 function* testBodyRuleView(view) {
   info("Testing tooltips in the rule view");
--- a/devtools/client/inspector/shared/test/browser_styleinspector_tooltip-closes-on-new-selection.js
+++ b/devtools/client/inspector/shared/test/browser_styleinspector_tooltip-closes-on-new-selection.js
@@ -12,17 +12,17 @@ add_task(function*() {
   yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
   let {inspector, view} = yield openRuleView();
   yield selectNode(".one", inspector);
 
   info("Testing rule view tooltip closes on new selection");
   yield testRuleView(view, inspector);
 
   info("Testing computed view tooltip closes on new selection");
-  ({view} = yield openComputedView());
+  view = selectComputedView(inspector);
   yield testComputedView(view, inspector);
 });
 
 function* testRuleView(ruleView, inspector) {
   info("Showing the tooltip");
 
   let tooltip = ruleView.tooltips.previewTooltip;
   tooltip.setTextContent({messages: ["rule-view tooltip"]});
--- a/devtools/client/inspector/shared/test/browser_styleinspector_tooltip-longhand-fontfamily.js
+++ b/devtools/client/inspector/shared/test/browser_styleinspector_tooltip-longhand-fontfamily.js
@@ -20,17 +20,17 @@ const TEST_URI = `
 add_task(function*() {
   yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
   let {inspector, view} = yield openRuleView();
   yield selectNode("#testElement", inspector);
   yield testRuleView(view, inspector.selection.nodeFront);
 
   info("Opening the computed view");
   let onComputedViewReady = inspector.once("computed-view-refreshed");
-  ({inspector, view} = yield openComputedView());
+  view = selectComputedView(inspector);
   yield onComputedViewReady;
 
   yield testComputedView(view, inspector.selection.nodeFront);
 
   yield testExpandedComputedViewProperty(view, inspector.selection.nodeFront);
 });
 
 function* testRuleView(ruleView, nodeFront) {
--- a/devtools/client/inspector/shared/test/browser_styleinspector_tooltip-multiple-background-images.js
+++ b/devtools/client/inspector/shared/test/browser_styleinspector_tooltip-multiple-background-images.js
@@ -20,35 +20,37 @@ add_task(function* () {
   let TEST_STYLE = "h1 {background: url(" + YELLOW_DOT + "), url(" + BLUE_DOT + ");}";
 
   let PAGE_CONTENT = "<style>" + TEST_STYLE + "</style>" +
     "<h1>browser_styleinspector_tooltip-multiple-background-images.js</h1>";
 
   yield addTab("data:text/html;charset=utf-8,background image tooltip test");
   content.document.body.innerHTML = PAGE_CONTENT;
 
-  yield testRuleViewUrls();
-  yield testComputedViewUrls();
+  let {inspector} = yield openInspector();
+  yield testRuleViewUrls(inspector);
+  yield testComputedViewUrls(inspector);
 });
 
-function* testRuleViewUrls() {
+function* testRuleViewUrls(inspector) {
   info("Testing tooltips in the rule view");
-
-  let {view, inspector} = yield openRuleView();
+  let view = selectRuleView(inspector);
   yield selectNode("h1", inspector);
 
   let {valueSpan} = getRuleViewProperty(view, "h1", "background");
   yield performChecks(view, valueSpan);
 }
 
-function* testComputedViewUrls() {
+function* testComputedViewUrls(inspector) {
   info("Testing tooltips in the computed view");
 
-  let {view, inspector} = yield openComputedView();
-  yield inspector.once("computed-view-refreshed");
+  let onComputedViewReady = inspector.once("computed-view-refreshed");
+  let view = selectComputedView(inspector);
+  yield onComputedViewReady;
+
   let {valueSpan} = getComputedViewProperty(view, "background-image");
 
   yield performChecks(view, valueSpan);
 }
 
 /**
  * A helper that checks url() tooltips contain correct images
  */
--- a/devtools/client/inspector/shared/test/browser_styleinspector_tooltip-shorthand-fontfamily.js
+++ b/devtools/client/inspector/shared/test/browser_styleinspector_tooltip-shorthand-fontfamily.js
@@ -13,16 +13,17 @@ const TEST_URI = `
     }
   </style>
   <div id="testElement">test element</div>
 `;
 
 add_task(function*() {
   yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
   let {inspector, view} = yield openRuleView();
+
   yield selectNode("#testElement", inspector);
   yield testRuleView(view, inspector.selection.nodeFront);
 });
 
 function* testRuleView(ruleView, nodeFront) {
   info("Testing font-family tooltips in the rule view");
 
   let tooltip = ruleView.tooltips.previewTooltip;
--- a/devtools/client/inspector/shared/test/browser_styleinspector_transform-highlighter-01.js
+++ b/devtools/client/inspector/shared/test/browser_styleinspector_transform-highlighter-01.js
@@ -15,29 +15,30 @@ const TEST_URI = `
   Test the css transform highlighter
 `;
 
 const TYPE = "CssTransformHighlighter";
 
 add_task(function*() {
   yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
   let {inspector, view} = yield openRuleView();
+
   let overlay = view.highlighters;
 
   ok(!overlay.highlighters[TYPE], "No highlighter exists in the rule-view");
   let h = yield overlay._getHighlighter(TYPE);
   ok(overlay.highlighters[TYPE],
     "The highlighter has been created in the rule-view");
   is(h, overlay.highlighters[TYPE], "The right highlighter has been created");
   let h2 = yield overlay._getHighlighter(TYPE);
   is(h, h2,
     "The same instance of highlighter is returned everytime in the rule-view");
 
   let onComputedViewReady = inspector.once("computed-view-refreshed");
-  let {view: cView} = yield openComputedView();
+  let cView = selectComputedView(inspector);
   yield onComputedViewReady;
   overlay = cView.highlighters;
 
   ok(!overlay.highlighters[TYPE], "No highlighter exists in the computed-view");
   h = yield overlay._getHighlighter(TYPE);
   ok(overlay.highlighters[TYPE],
     "The highlighter has been created in the computed-view");
   is(h, overlay.highlighters[TYPE], "The right highlighter has been created");
--- a/devtools/client/inspector/shared/test/browser_styleinspector_transform-highlighter-02.js
+++ b/devtools/client/inspector/shared/test/browser_styleinspector_transform-highlighter-02.js
@@ -33,17 +33,17 @@ add_task(function*() {
 
   info("Faking a mousemove on a transform property");
   ({valueSpan} = getRuleViewProperty(view, "body", "transform"));
   let onHighlighterShown = hs.once("highlighter-shown");
   hs._onMouseMove({target: valueSpan});
   yield onHighlighterShown;
 
   let onComputedViewReady = inspector.once("computed-view-refreshed");
-  let {view: cView} = yield openComputedView();
+  let cView = selectComputedView(inspector);
   yield onComputedViewReady;
   hs = cView.highlighters;
 
   ok(!hs.highlighters[TYPE], "No highlighter exists in the computed-view (1)");
 
   info("Faking a mousemove on a non-transform property");
   ({valueSpan} = getComputedViewProperty(cView, "color"));
   hs._onMouseMove({target: valueSpan});
--- a/devtools/client/inspector/shared/test/head.js
+++ b/devtools/client/inspector/shared/test/head.js
@@ -1,74 +1,52 @@
 /* vim: set ft=javascript ts=2 et sw=2 tw=80: */
 /* Any copyright is dedicated to the Public Domain.
  http://creativecommons.org/publicdomain/zero/1.0/ */
+/* import-globals-from ../../test/head.js */
 
 "use strict";
 
-var Cu = Components.utils;
-var {require} = Cu.import("resource://devtools/shared/Loader.jsm", {});
-var {gDevTools} = require("devtools/client/framework/devtools");
-var {TargetFactory} = require("devtools/client/framework/target");
-var {CssRuleView, _ElementStyle} = require("devtools/client/inspector/rules/rules");
+// Import the inspector's head.js first (which itself imports shared-head.js).
+Services.scriptloader.loadSubScript(
+  "chrome://mochitests/content/browser/devtools/client/inspector/test/head.js",
+  this);
+
+var {CssRuleView} = require("devtools/client/inspector/rules/rules");
 var {CssLogic, CssSelector} = require("devtools/shared/inspector/css-logic");
-var DevToolsUtils = require("devtools/shared/DevToolsUtils");
-var promise = require("promise");
-var {editableField, getInplaceEditorForSpan: inplaceEditor} =
+var {getInplaceEditorForSpan: inplaceEditor} =
   require("devtools/client/shared/inplace-editor");
-var {console} = Cu.import("resource://gre/modules/Console.jsm", {});
-
-// All tests are asynchronous
-waitForExplicitFinish();
 
 const TEST_URL_ROOT =
   "http://example.com/browser/devtools/client/inspector/shared/test/";
 const TEST_URL_ROOT_SSL =
   "https://example.com/browser/devtools/client/inspector/shared/test/";
 const ROOT_TEST_DIR = getRootDirectory(gTestPath);
 const FRAME_SCRIPT_URL = ROOT_TEST_DIR + "doc_frame_script.js";
 
-// Auto clean-up when a test ends
-registerCleanupFunction(function*() {
-  let target = TargetFactory.forTab(gBrowser.selectedTab);
-  yield gDevTools.closeToolbox(target);
-
-  while (gBrowser.tabs.length > 1) {
-    gBrowser.removeCurrentTab();
-  }
-});
-
-// Uncomment this pref to dump all devtools emitted events to the console.
-// Services.prefs.setBoolPref("devtools.dump.emit", true);
-
-// Set the testing flag on gDevTools and reset it when the test ends
-DevToolsUtils.testing = true;
-registerCleanupFunction(() => DevToolsUtils.testing = false);
-
 // Clean-up all prefs that might have been changed during a test run
 // (safer here because if the test fails, then the pref is never reverted)
 registerCleanupFunction(() => {
-  Services.prefs.clearUserPref("devtools.inspector.activeSidebar");
-  Services.prefs.clearUserPref("devtools.dump.emit");
   Services.prefs.clearUserPref("devtools.defaultColorUnit");
 });
 
 /**
  * The functions found below are here to ease test development and maintenance.
  * Most of these functions are stateless and will require some form of context
  * (the instance of the current toolbox, or inspector panel for instance).
  *
  * Most of these functions are async too and return promises.
  *
  * All tests should follow the following pattern:
  *
  * add_task(function*() {
  *   yield addTab(TEST_URI);
- *   let {toolbox, inspector, view} = yield openComputedView();
- *
+ *   let {toolbox, inspector} = yield openInspector();
+ *   inspector.sidebar.select(viewId);
+ *   let view = inspector[viewId].view;
  *   yield selectNode("#test", inspector);
  *   yield someAsyncTestFunction(view);
  * });
  *
  * add_task is the way to define the testcase in the test file. It accepts
  * a single generator-function argument.
  * The generator function should yield any async call.
  *
@@ -87,273 +65,31 @@ registerCleanupFunction(() => {
  * UTILS
  * *********************************************
  * General test utilities.
  * Add new tabs, open the toolbox and switch to the various panels, select
  * nodes, get node references, ...
  */
 
 /**
- * Add a new test tab in the browser and load the given url.
- *
- * @param {String} url
- *        The url to be loaded in the new tab
- * @return a promise that resolves to the tab object when the url is loaded
- */
-function addTab(url) {
-  info("Adding a new tab with URL: '" + url + "'");
-  let def = promise.defer();
-
-  window.focus();
-
-  let tab = window.gBrowser.selectedTab = window.gBrowser.addTab(url);
-  let browser = tab.linkedBrowser;
-
-  info("Loading the helper frame script " + FRAME_SCRIPT_URL);
-  browser.messageManager.loadFrameScript(FRAME_SCRIPT_URL, false);
-
-  browser.addEventListener("load", function onload() {
-    browser.removeEventListener("load", onload, true);
-    info("URL '" + url + "' loading complete");
-
-    def.resolve(tab);
-  }, true);
-
-  return def.promise;
-}
-
-/**
- * Simple DOM node accesor function that takes either a node or a string css
- * selector as argument and returns the corresponding node
- *
- * @param {String|DOMNode} nodeOrSelector
- * @return {DOMNode|CPOW} Note that in e10s mode a CPOW object is returned which
- * doesn't implement *all* of the DOMNode's properties
- */
-function getNode(nodeOrSelector) {
-  info("Getting the node for '" + nodeOrSelector + "'");
-  return typeof nodeOrSelector === "string" ?
-    content.document.querySelector(nodeOrSelector) :
-    nodeOrSelector;
-}
-
-/**
- * Get the NodeFront for a given css selector, via the protocol
- *
- * @param {String} selector
- * @param {InspectorPanel} inspector
- *        The instance of InspectorPanel currently loaded in the toolbox
- * @return {Promise} Resolves to the NodeFront instance
- */
-function getNodeFront(selector, {walker}) {
-  return walker.querySelector(walker.rootNode, selector);
-}
-
-/*
- * Set the inspector's current selection to a node or to the first match of the
- * given css selector.
- *
- * @param {String|NodeFront} data
- *        The node to select
- * @param {InspectorPanel} inspector
- *        The instance of InspectorPanel currently loaded in the toolbox
- * @param {String} reason
- *        Defaults to "test" which instructs the inspector not
- *        to highlight the node upon selection
- * @return {Promise} Resolves when the inspector is updated with the new node
- */
-var selectNode = Task.async(function*(data, inspector, reason="test") {
-  info("Selecting the node for '" + data + "'");
-  let nodeFront = data;
-  if (!data._form) {
-    nodeFront = yield getNodeFront(data, inspector);
-  }
-  let updated = inspector.once("inspector-updated");
-  inspector.selection.setNodeFront(nodeFront, reason);
-  yield updated;
-});
-
-/**
- * Set the inspector's current selection to null so that no node is selected
- *
- * @param {InspectorPanel} inspector
- *        The instance of InspectorPanel currently loaded in the toolbox
- * @return a promise that resolves when the inspector is updated
- */
-function clearCurrentNodeSelection(inspector) {
-  info("Clearing the current selection");
-  let updated = inspector.once("inspector-updated");
-  inspector.selection.setNodeFront(null);
-  return updated;
-}
-
-/**
- * Open the toolbox, with the inspector tool visible.
- *
- * @return a promise that resolves when the inspector is ready
+ * The rule-view tests rely on a frame-script to be injected in the content test
+ * page. So override the shared-head's addTab to load the frame script after the
+ * tab was added.
+ * FIXME: Refactor the rule-view tests to use the testActor instead of a frame
+ * script, so they can run on remote targets too.
  */
-var openInspector = Task.async(function*() {
-  info("Opening the inspector");
-  let target = TargetFactory.forTab(gBrowser.selectedTab);
-
-  let inspector, toolbox;
-
-  // Checking if the toolbox and the inspector are already loaded
-  // The inspector-updated event should only be waited for if the inspector
-  // isn't loaded yet
-  toolbox = gDevTools.getToolbox(target);
-  if (toolbox) {
-    inspector = toolbox.getPanel("inspector");
-    if (inspector) {
-      info("Toolbox and inspector already open");
-      return {
-        toolbox: toolbox,
-        inspector: inspector
-      };
-    }
-  }
-
-  info("Opening the toolbox");
-  toolbox = yield gDevTools.showToolbox(target, "inspector");
-  yield waitForToolboxFrameFocus(toolbox);
-  inspector = toolbox.getPanel("inspector");
-
-  info("Waiting for the inspector to update");
-  if (inspector._updateProgress) {
-    yield inspector.once("inspector-updated");
-  }
-
-  return {
-    toolbox: toolbox,
-    inspector: inspector
-  };
-});
-
-/**
- * Wait for the toolbox frame to receive focus after it loads
- *
- * @param {Toolbox} toolbox
- * @return a promise that resolves when focus has been received
- */
-function waitForToolboxFrameFocus(toolbox) {
-  info("Making sure that the toolbox's frame is focused");
-  let def = promise.defer();
-  let win = toolbox.frame.contentWindow;
-  waitForFocus(def.resolve, win);
-  return def.promise;
-}
-
-/**
- * Open the toolbox, with the inspector tool visible, and the sidebar that
- * corresponds to the given id selected
- *
- * @return a promise that resolves when the inspector is ready and the sidebar
- * view is visible and ready
- */
-var openInspectorSideBar = Task.async(function*(id) {
-  let {toolbox, inspector} = yield openInspector();
-
-  info("Selecting the " + id + " sidebar");
-  inspector.sidebar.select(id);
-
-  return {
-    toolbox: toolbox,
-    inspector: inspector,
-    view: inspector[id].view
-  };
-});
-
-/**
- * Open the toolbox, with the inspector tool visible, and the computed-view
- * sidebar tab selected.
- *
- * @return a promise that resolves when the inspector is ready and the computed
- * view is visible and ready
- */
-function openComputedView() {
-  return openInspectorSideBar("computedview");
-}
-
-/**
- * Open the toolbox, with the inspector tool visible, and the rule-view
- * sidebar tab selected.
- *
- * @return a promise that resolves when the inspector is ready and the rule
- * view is visible and ready
- */
-function openRuleView() {
-  return openInspectorSideBar("ruleview");
-}
-
-/**
- * Wait for eventName on target to be delivered a number of times.
- *
- * @param {Object} target
- *        An observable object that either supports on/off or
- *        addEventListener/removeEventListener
- * @param {String} eventName
- * @param {Number} numTimes
- *        Number of deliveries to wait for.
- * @param {Boolean} useCapture
- *        Optional, for addEventListener/removeEventListener
- * @return A promise that resolves when the event has been handled
- */
-function waitForNEvents(target, eventName, numTimes, useCapture = false) {
-  info("Waiting for event: '" + eventName + "' on " + target + ".");
-
-  let deferred = promise.defer();
-  let count = 0;
-
-  for (let [add, remove] of [
-    ["addEventListener", "removeEventListener"],
-    ["addListener", "removeListener"],
-    ["on", "off"]
-  ]) {
-    if ((add in target) && (remove in target)) {
-      target[add](eventName, function onEvent(...aArgs) {
-        if (++count == numTimes) {
-          target[remove](eventName, onEvent, useCapture);
-          deferred.resolve.apply(deferred, aArgs);
-        }
-      }, useCapture);
-      break;
-    }
-  }
-
-  return deferred.promise;
-}
-
-/**
- * Wait for eventName on target.
- *
- * @param {Object} target
- *        An observable object that either supports on/off or
- *        addEventListener/removeEventListener
- * @param {String} eventName
- * @param {Boolean} useCapture
- *        Optional, for addEventListener/removeEventListener
- * @return A promise that resolves when the event has been handled
- */
-function once(target, eventName, useCapture=false) {
-  return waitForNEvents(target, eventName, 1, useCapture);
-}
-
-/**
- * This shouldn't be used in the tests, but is useful when writing new tests or
- * debugging existing tests in order to introduce delays in the test steps
- *
- * @param {Number} ms
- *        The time to wait
- * @return A promise that resolves when the time is passed
- */
-function wait(ms) {
-  let def = promise.defer();
-  content.setTimeout(def.resolve, ms);
-  return def.promise;
-}
+var _addTab = addTab;
+addTab = function(url) {
+  return _addTab(url).then(tab => {
+    info("Loading the helper frame script " + FRAME_SCRIPT_URL);
+    let browser = tab.linkedBrowser;
+    browser.messageManager.loadFrameScript(FRAME_SCRIPT_URL, false);
+    return tab;
+  });
+};
 
 /**
  * Wait for a content -> chrome message on the message manager (the window
  * messagemanager is used).
  *
  * @param {String} name
  *        The message name
  * @return {Promise} A promise that resolves to the response data when the
@@ -488,49 +224,16 @@ function assertHoverTooltipOn(tooltip, e
   return isHoverTooltipTarget(tooltip, element).then(() => {
     ok(true, "A tooltip is defined on hover of the given element");
   }, () => {
     ok(false, "No tooltip is defined on hover of the given element");
   });
 }
 
 /**
- * Listen for a new tab to open and return a promise that resolves when one
- * does and completes the load event.
- *
- * @return a promise that resolves to the tab object
- */
-var waitForTab = Task.async(function*() {
-  info("Waiting for a tab to open");
-  yield once(gBrowser.tabContainer, "TabOpen");
-  let tab = gBrowser.selectedTab;
-  let browser = tab.linkedBrowser;
-  yield once(browser, "load", true);
-  info("The tab load completed");
-  return tab;
-});
-
-/**
- * @see SimpleTest.waitForClipboard
- *
- * @param {Function} setup
- *        Function to execute before checking for the
- *        clipboard content
- * @param {String|Boolean} expected
- *        An expected string or validator function
- * @return a promise that resolves when the expected string has been found or
- * the validator function has returned true, rejects otherwise.
- */
-function waitForClipboard(setup, expected) {
-  let def = promise.defer();
-  SimpleTest.waitForClipboard(expected, setup, def.resolve, def.reject);
-  return def.promise;
-}
-
-/**
  * Polls a given function waiting for it to return true.
  *
  * @param {Function} validatorFn
  *        A validator function that returns a boolean.
  *        This is called every few milliseconds to check if the result is true.
  *        When it is true, the promise resolves.
  * @param {String} name
  *        Optional name of the test. This is used to generate
@@ -550,46 +253,16 @@ function waitForSuccess(validatorFn, nam
     }
   }
   wait(validatorFn);
 
   return def.promise;
 }
 
 /**
- * Create a new style tag containing the given style text and append it to the
- * document's head node
- *
- * @param {Document} doc
- * @param {String} style
- * @return {DOMNode} The newly created style node
- */
-function addStyle(doc, style) {
-  info("Adding a new style tag to the document with style content: " +
-    style.substring(0, 50));
-  let node = doc.createElement("style");
-  node.setAttribute("type", "text/css");
-  node.textContent = style;
-  doc.getElementsByTagName("head")[0].appendChild(node);
-  return node;
-}
-
-/**
- * Checks whether the inspector's sidebar corresponding to the given id already
- * exists
- *
- * @param {InspectorPanel}
- * @param {String}
- * @return {Boolean}
- */
-function hasSideBarTab(inspector, id) {
-  return !!inspector.sidebar.getWindowForTab(id);
-}
-
-/**
  * Get the dataURL for the font family tooltip.
  *
  * @param {String} font
  *        The font family value.
  * @param {object} nodeFront
  *        The NodeActor that will used to retrieve the dataURL for the
  *        font family tooltip contents.
  */
@@ -597,30 +270,16 @@ var getFontFamilyDataURL = Task.async(fu
   let fillStyle = (Services.prefs.getCharPref("devtools.theme") === "light") ?
       "black" : "white";
 
   let {data} = yield nodeFront.getFontFamilyDataURL(font, fillStyle);
   let dataURL = yield data.string();
   return dataURL;
 });
 
-/**
- * Simulate the key input for the given input in the window.
- *
- * @param {String} input
- *        The string value to input
- * @param {Window} win
- *        The window containing the panel
- */
-function synthesizeKeys(input, win) {
-  for (let key of input.split("")) {
-    EventUtils.synthesizeKey(key, {}, win);
-  }
-}
-
 /* *********************************************
  * RULE-VIEW
  * *********************************************
  * Rule-view related test utility functions
  * This object contains functions to get rules, get properties, ...
  */
 
 /**
@@ -916,82 +575,8 @@ function getComputedViewProperty(view, n
  * @param {String} name
  *        The name of the property to retrieve
  * @return {String} The property value
  */
 function getComputedViewPropertyValue(view, name, propertyName) {
   return getComputedViewProperty(view, name, propertyName)
     .valueSpan.textContent;
 }
-
-/* *********************************************
- * STYLE-EDITOR
- * *********************************************
- * Style-editor related utility functions.
- */
-
-/**
- * Wait for the toolbox to emit the styleeditor-selected event and when done
- * wait for the stylesheet identified by href to be loaded in the stylesheet
- * editor
- *
- * @param {Toolbox} toolbox
- * @param {String} href
- *        Optional, if not provided, wait for the first editor to be ready
- * @return a promise that resolves to the editor when the stylesheet editor is
- * ready
- */
-function waitForStyleEditor(toolbox, href) {
-  let def = promise.defer();
-
-  info("Waiting for the toolbox to switch to the styleeditor");
-  toolbox.once("styleeditor-selected").then(() => {
-    let panel = toolbox.getCurrentPanel();
-    ok(panel && panel.UI, "Styleeditor panel switched to front");
-
-    // A helper that resolves the promise once it receives an editor that
-    // matches the expected href. Returns false if the editor was not correct.
-    let gotEditor = (event, editor) => {
-      let currentHref = editor.styleSheet.href;
-      if (!href || (href && currentHref.endsWith(href))) {
-        info("Stylesheet editor selected");
-        panel.UI.off("editor-selected", gotEditor);
-
-        editor.getSourceEditor().then(sourceEditor => {
-          info("Stylesheet editor fully loaded");
-          def.resolve(sourceEditor);
-        });
-
-        return true;
-      }
-
-      info("The editor was incorrect. Waiting for editor-selected event.");
-      return false;
-    };
-
-    // The expected editor may already be selected. Check the if the currently
-    // selected editor is the expected one and if not wait for an
-    // editor-selected event.
-    if (!gotEditor("styleeditor-selected", panel.UI.selectedEditor)) {
-      // The expected editor is not selected (yet). Wait for it.
-      panel.UI.on("editor-selected", gotEditor);
-    }
-  });
-
-  return def.promise;
-}
-
-/**
- * Reload the current page and wait for the inspector to be initialized after
- * the navigation
- *
- * @param {InspectorPanel} inspector
- *        The instance of InspectorPanel currently loaded in the toolbox
- * @return a promise that resolves after page reload and inspector
- * initialization
- */
-function reloadPage(inspector) {
-  let onNewRoot = inspector.once("new-root");
-  content.location.reload();
-  return onNewRoot.then(() => {
-    inspector.markup._waitForChildren();
-  });
-}
--- a/devtools/client/inspector/test/head.js
+++ b/devtools/client/inspector/test/head.js
@@ -133,16 +133,30 @@ var selectNode = Task.async(function*(se
   info("Selecting the node for '" + selector + "'");
   let nodeFront = yield getNodeFront(selector, inspector);
   let updated = inspector.once("inspector-updated");
   inspector.selection.setNodeFront(nodeFront, reason);
   yield updated;
 });
 
 /**
+ * Set the inspector's current selection to null so that no node is selected
+ *
+ * @param {InspectorPanel} inspector
+ *        The instance of InspectorPanel currently loaded in the toolbox
+ * @return a promise that resolves when the inspector is updated
+ */
+function clearCurrentNodeSelection(inspector) {
+  info("Clearing the current selection");
+  let updated = inspector.once("inspector-updated");
+  inspector.selection.setNodeFront(null);
+  return updated;
+}
+
+/**
  * Open the inspector in a tab with given URL.
  * @param {string} url  The URL to open.
  * @param {String} hostType Optional hostType, as defined in Toolbox.HostType
  * @return A promise that is resolved once the tab and inspector have loaded
  *         with an object: { tab, toolbox, inspector }.
  */
 var openInspectorForURL = Task.async(function*(url, hostType) {
   let tab = yield addTab(url);
@@ -205,35 +219,96 @@ var clickOnInspectMenuItem = Task.async(
   yield contextClosed;
 
   return getActiveInspector();
 });
 
 /**
  * Open the toolbox, with the inspector tool visible, and the one of the sidebar
  * tabs selected.
- * @param {String} id The ID of the sidebar tab to be opened
- * @param {String} hostType Optional hostType, as defined in Toolbox.HostType
+ *
+ * @param {String} id
+ *        The ID of the sidebar tab to be opened
  * @return a promise that resolves when the inspector is ready and the tab is
  * visible and ready
  */
-var openInspectorSidebarTab = Task.async(function*(id, hostType) {
+var openInspectorSidebarTab = Task.async(function* (id) {
   let {toolbox, inspector, testActor} = yield openInspector();
 
   info("Selecting the " + id + " sidebar");
   inspector.sidebar.select(id);
 
   return {
     toolbox,
     inspector,
     testActor
   };
 });
 
 /**
+ * Open the toolbox, with the inspector tool visible, and the rule-view
+ * sidebar tab selected.
+ *
+ * @return a promise that resolves when the inspector is ready and the rule view
+ * is visible and ready
+ */
+function openRuleView() {
+  return openInspectorSidebarTab("ruleview").then(data => {
+    return {
+      toolbox: data.toolbox,
+      inspector: data.inspector,
+      testActor: data.testActor,
+      view: data.inspector.ruleview.view
+    };
+  });
+}
+
+/**
+ * Open the toolbox, with the inspector tool visible, and the computed-view
+ * sidebar tab selected.
+ *
+ * @return a promise that resolves when the inspector is ready and the computed
+ * view is visible and ready
+ */
+function openComputedView() {
+  return openInspectorSidebarTab("computedview").then(data => {
+    return {
+      toolbox: data.toolbox,
+      inspector: data.inspector,
+      testActor: data.testActor,
+      view: data.inspector.computedview.view
+    };
+  });
+}
+
+/**
+ * Select the rule view sidebar tab on an already opened inspector panel.
+ *
+ * @param {InspectorPanel} inspector
+ *        The opened inspector panel
+ * @return {CssRuleView} the rule view
+ */
+function selectRuleView(inspector) {
+  inspector.sidebar.select("ruleview");
+  return inspector.ruleview.view;
+}
+
+/**
+ * Select the computed view sidebar tab on an already opened inspector panel.
+ *
+ * @param {InspectorPanel} inspector
+ *        The opened inspector panel
+ * @return {CssComputedView} the computed view
+ */
+function selectComputedView(inspector) {
+  inspector.sidebar.select("computedview");
+  return inspector.computedview.view;
+}
+
+/**
  * Get the NodeFront for a node that matches a given css selector, via the
  * protocol.
  * @param {String|NodeFront} selector
  * @param {InspectorPanel} inspector The instance of InspectorPanel currently
  * loaded in the toolbox
  * @return {Promise} Resolves to the NodeFront instance
  */
 function getNodeFront(selector, {walker}) {
@@ -667,8 +742,38 @@ function containsFocus(doc, container) {
   while (elm) {
     if (elm === container) {
       return true;
     }
     elm = elm.parentNode;
   }
   return false;
 }
+
+/**
+ * Listen for a new tab to open and return a promise that resolves when one
+ * does and completes the load event.
+ *
+ * @return a promise that resolves to the tab object
+ */
+var waitForTab = Task.async(function*() {
+  info("Waiting for a tab to open");
+  yield once(gBrowser.tabContainer, "TabOpen");
+  let tab = gBrowser.selectedTab;
+  let browser = tab.linkedBrowser;
+  yield once(browser, "load", true);
+  info("The tab load completed");
+  return tab;
+});
+
+/**
+ * Simulate the key input for the given input in the window.
+ *
+ * @param {String} input
+ *        The string value to input
+ * @param {Window} win
+ *        The window containing the panel
+ */
+function synthesizeKeys(input, win) {
+  for (let key of input.split("")) {
+    EventUtils.synthesizeKey(key, {}, win);
+  }
+}
--- a/devtools/client/jsonview/css/tabs.css
+++ b/devtools/client/jsonview/css/tabs.css
@@ -137,17 +137,17 @@
 .theme-light .tabs .tabs-menu-item a {
   border: none !important;
   background-color: transparent !important;
   padding: 5px 15px;
 }
 
 .theme-dark .tabs .tabs-menu-item:hover,
 .theme-light .tabs .tabs-menu-item:hover {
-  background-color: rgba(170,170,170,.2);
+  background-color: var(--toolbar-tab-hover);
 }
 
 .theme-dark .tabs .tabs-menu-item.is-active,
 .theme-dark .tabs .tabs-menu-item.is-active:hover,
 .theme-light .tabs .tabs-menu-item.is-active,
 .theme-light .tabs .tabs-menu-item.is-active:hover {
   background-color: var(--theme-selection-background);
 }
@@ -156,17 +156,17 @@
 .theme-dark .tabs .tabs-menu-item.is-active:hover a,
 .theme-light .tabs .tabs-menu-item.is-active a,
 .theme-light .tabs .tabs-menu-item.is-active:hover a {
   color: var(--theme-selection-color);
 }
 
 .theme-dark .tabs .tabs-menu-item:active:hover,
 .theme-light .tabs .tabs-menu-item:active:hover {
-  background-color: rgba(170,170,170,.4);
+  background-color: var(--toolbar-tab-hover-active);
 }
 
 .theme-dark .tabs .tabs-menu-item.is-active,
 .theme-light .tabs .tabs-menu-item.is-active {
   box-shadow: 0 2px 0 #d7f1ff inset,
               0 8px 3px -5px #2b82bf inset,
               0 -2px 0 rgba(0,0,0,.06) inset;
 }
@@ -185,17 +185,17 @@
 .theme-dark .tabs .tabs-menu-item a:hover,
 .theme-dark .tabs .tabs-menu-item a {
   border: none !important;
   background-color: transparent !important;
   padding: 5px 15px;
 }
 
 .theme-dark .tabs .tabs-menu-item:active:hover {
-  background-color: hsla(206,37%,4%,.4);
+  background-color: hsla(206, 37%, 4%, .4); /* --toolbar-tab-hover-active */
 }
 
 .theme-dark .tabs .tabs-menu-item.is-active {
   box-shadow: 0px 2px 0px #D7F1FF inset,
    0px 8px 3px -5px #2B82BF inset,
    0px -2px 0px rgba(0, 0, 0, 0.2) inset;
 }
 
--- a/devtools/client/jsonview/css/toolbar.css
+++ b/devtools/client/jsonview/css/toolbar.css
@@ -74,21 +74,21 @@
 .theme-light .toolbar .btn {
   min-width: 78px;
   min-height: 18px;
   color: var(--theme-content-color1);
   text-shadow: none;
   margin: 1px 2px 1px 2px;
   border: none;
   border-radius: 0;
-  background-color: rgba(170, 170, 170, .2); /* Splitter */
+  background-color: rgba(170, 170, 170, .2); /* --toolbar-tab-hover */
   transition: background 0.05s ease-in-out;
 }
 
 .theme-dark .toolbar .btn:hover,
 .theme-light .toolbar .btn:hover {
   background: rgba(170, 170, 170, .3); /* Splitters */
 }
 
 .theme-dark .toolbar .btn:not([disabled]):hover:active,
 .theme-light .toolbar .btn:not([disabled]):hover:active {
-  background: rgba(170, 170, 170, .4); /* Splitters */
+  background: rgba(170, 170, 170, .4); /* --toolbar-tab-hover-active */
 }
--- a/devtools/client/locales/en-US/responsive.properties
+++ b/devtools/client/locales/en-US/responsive.properties
@@ -6,27 +6,34 @@
 # available from the Web Developer sub-menu -> 'Responsive Design Mode'.
 #
 # The correct localization of this file might be to keep it in
 # English, or another language commonly spoken among web developers.
 # You want to make that choice consistent across the developer tools.
 # A good criteria is the language in which you'd find the best
 # documentation on web development on the web.
 
-# LOCALIZATION NOTE  (responsive.title): the title displayed in the global
-# toolbar
-responsive.title=Responsive Design Mode
+# LOCALIZATION NOTE (responsive.editDeviceList): option displayed in the device
+# selector
+responsive.editDeviceList=Edit list…
 
 # LOCALIZATION NOTE (responsive.exit): tooltip text of the exit button.
 responsive.exit=Close Responsive Design Mode
 
+# LOCALIZATION NOTE (responsive.done): button text in the device list modal
+responsive.done=Done
+
 # LOCALIZATION NOTE (responsive.noDeviceSelected): placeholder text for the
 # device selector
 responsive.noDeviceSelected=no device selected
 
+# LOCALIZATION NOTE  (responsive.title): the title displayed in the global
+# toolbar
+responsive.title=Responsive Design Mode
+
 # LOCALIZATION NOTE  (responsive.screenshot): tooltip of the screenshot button.
 responsive.screenshot=Take a screenshot of the viewport
 
 # LOCALIZATION NOTE (responsive.screenshotGeneratedFilename): The auto generated
 # filename.
 # The first argument (%1$S) is the date string in yyyy-mm-dd format and the
 # second argument (%2$S) is the time string in HH.MM.SS format.
 responsive.screenshotGeneratedFilename=Screen Shot %1$S at %2$S
--- a/devtools/client/responsive.html/actions/devices.js
+++ b/devtools/client/responsive.html/actions/devices.js
@@ -2,16 +2,18 @@
  * 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 {
   ADD_DEVICE,
   ADD_DEVICE_TYPE,
+  UPDATE_DEVICE_DISPLAYED,
+  UPDATE_DEVICE_MODAL_OPEN,
 } = require("./index");
 
 module.exports = {
 
   addDevice(device, deviceType) {
     return {
       type: ADD_DEVICE,
       device,
@@ -21,9 +23,25 @@ module.exports = {
 
   addDeviceType(deviceType) {
     return {
       type: ADD_DEVICE_TYPE,
       deviceType,
     };
   },
 
+  updateDeviceDisplayed(device, deviceType, displayed) {
+    return {
+      type: UPDATE_DEVICE_DISPLAYED,
+      device,
+      deviceType,
+      displayed,
+    };
+  },
+
+  updateDeviceModalOpen(isOpen) {
+    return {
+      type: UPDATE_DEVICE_MODAL_OPEN,
+      isOpen,
+    };
+  },
+
 };
--- a/devtools/client/responsive.html/actions/index.js
+++ b/devtools/client/responsive.html/actions/index.js
@@ -33,16 +33,22 @@ createEnum([
   "ROTATE_VIEWPORT",
 
   // Take a screenshot of the viewport.
   "TAKE_SCREENSHOT_START",
 
   // Indicates when the screenshot action ends.
   "TAKE_SCREENSHOT_END",
 
+  // Update the device display state in the device selector.
+  "UPDATE_DEVICE_DISPLAYED",
+
+  // Update the device modal open state.
+  "UPDATE_DEVICE_MODAL_OPEN",
+
 ], module.exports);
 
 /**
  * Create a simple enum-like object with keys mirrored to values from an array.
  * This makes comparison to a specfic value simpler without having to repeat and
  * mis-type the value.
  */
 function createEnum(array, target) {
--- a/devtools/client/responsive.html/actions/screenshot.js
+++ b/devtools/client/responsive.html/actions/screenshot.js
@@ -1,54 +1,48 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 /* eslint-env browser */
 
 "use strict";
 
-const HTML_NS = "http://www.w3.org/1999/xhtml";
-
 const {
   TAKE_SCREENSHOT_START,
   TAKE_SCREENSHOT_END,
 } = require("./index");
 
-const { getRect } = require("devtools/shared/layout/utils");
 const { getFormatStr } = require("../utils/l10n");
 const { getToplevelWindow } = require("sdk/window/utils");
-const { Task: { spawn, async } } = require("resource://gre/modules/Task.jsm");
+const { Task: { spawn } } = require("resource://gre/modules/Task.jsm");
+const e10s = require("../utils/e10s");
 
 const BASE_URL = "resource://devtools/client/responsive.html";
 const audioCamera = new window.Audio(`${BASE_URL}/audio/camera-click.mp3`);
 
+const animationFrame = () => new Promise(resolve => {
+  window.requestAnimationFrame(resolve);
+});
+
 function getFileName() {
   let date = new Date();
   let month = ("0" + (date.getMonth() + 1)).substr(-2);
   let day = ("0" + date.getDate()).substr(-2);
   let dateString = [date.getFullYear(), month, day].join("-");
   let timeString = date.toTimeString().replace(/:/g, ".").split(" ")[0];
 
   return getFormatStr("responsive.screenshotGeneratedFilename", dateString,
                       timeString);
 }
 
 function createScreenshotFor(node) {
-  let { top, left, width, height } = getRect(window, node, window);
+  let mm = node.frameLoader.messageManager;
 
-  const canvas = document.createElementNS(HTML_NS, "canvas");
-  const ctx = canvas.getContext("2d");
-  const ratio = window.devicePixelRatio;
-  canvas.width = width * ratio;
-  canvas.height = height * ratio;
-  ctx.scale(ratio, ratio);
-  ctx.drawWindow(window, left, top, width, height, "#fff");
-
-  return canvas.toDataURL("image/png", "");
+  return e10s.request(mm, "RequestScreenshot");
 }
 
 function saveToFile(data, filename) {
   return spawn(function* () {
     const chromeWindow = getToplevelWindow(window);
     const chromeDocument = chromeWindow.document;
 
     // append .png extension to filename if it doesn't exist
@@ -68,22 +62,21 @@ function simulateCameraEffects(node) {
 module.exports = {
 
   takeScreenshot() {
     return function* (dispatch, getState) {
       yield dispatch({ type: TAKE_SCREENSHOT_START });
 
       // Waiting the next repaint, to ensure the react components
       // can be properly render after the action dispatched above
-      window.requestAnimationFrame(async(function* () {
-        let iframe = document.querySelector("iframe");
-        let data = createScreenshotFor(iframe);
+      yield animationFrame();
+
+      let iframe = document.querySelector("iframe");
+      let data = yield createScreenshotFor(iframe);
 
-        simulateCameraEffects(iframe);
+      simulateCameraEffects(iframe);
 
-        yield saveToFile(data, getFileName());
+      yield saveToFile(data, getFileName());
 
-        dispatch({ type: TAKE_SCREENSHOT_END });
-      }));
+      dispatch({ type: TAKE_SCREENSHOT_END });
     };
   }
-
 };
--- a/devtools/client/responsive.html/app.js
+++ b/devtools/client/responsive.html/app.js
@@ -6,24 +6,30 @@
 
 "use strict";
 
 const { createClass, createFactory, PropTypes, DOM: dom } =
   require("devtools/client/shared/vendor/react");
 const { connect } = require("devtools/client/shared/vendor/react-redux");
 
 const {
+  updateDeviceDisplayed,
+  updateDeviceModalOpen,
+} = require("./actions/devices");
+const {
   changeDevice,
   resizeViewport,
   rotateViewport
 } = require("./actions/viewports");
 const { takeScreenshot } = require("./actions/screenshot");
-const Types = require("./types");
+const DeviceModal = createFactory(require("./components/device-modal"));
+const GlobalToolbar = createFactory(require("./components/global-toolbar"));
 const Viewports = createFactory(require("./components/viewports"));
-const GlobalToolbar = createFactory(require("./components/global-toolbar"));
+const { updateDeviceList } = require("./devices");
+const Types = require("./types");
 
 let App = createClass({
   propTypes: {
     devices: PropTypes.shape(Types.devices).isRequired,
     location: Types.location.isRequired,
     viewports: PropTypes.arrayOf(PropTypes.shape(Types.viewport)).isRequired,
     screenshot: PropTypes.shape(Types.screenshot).isRequired,
   },
@@ -41,48 +47,63 @@ let App = createClass({
   onContentResize({ width, height }) {
     window.postMessage({
       type: "content-resize",
       width,
       height,
     }, "*");
   },
 
+  onDeviceListUpdate(devices) {
+    updateDeviceList(devices);
+  },
+
   onExit() {
     window.postMessage({ type: "exit" }, "*");
   },
 
   onResizeViewport(id, width, height) {
     this.props.dispatch(resizeViewport(id, width, height));
   },
 
   onRotateViewport(id) {
     this.props.dispatch(rotateViewport(id));
   },
 
   onScreenshot() {
     this.props.dispatch(takeScreenshot());
   },
 
+  onUpdateDeviceDisplayed(device, deviceType, displayed) {
+    this.props.dispatch(updateDeviceDisplayed(device, deviceType, displayed));
+  },
+
+  onUpdateDeviceModalOpen(isOpen) {
+    this.props.dispatch(updateDeviceModalOpen(isOpen));
+  },
+
   render() {
     let {
       devices,
       location,
       screenshot,
       viewports,
     } = this.props;
 
     let {
       onBrowserMounted,
       onChangeViewportDevice,
       onContentResize,
+      onDeviceListUpdate,
       onExit,
       onResizeViewport,
       onRotateViewport,
       onScreenshot,
+      onUpdateDeviceDisplayed,
+      onUpdateDeviceModalOpen,
     } = this;
 
     return dom.div(
       {
         id: "app",
       },
       GlobalToolbar({
         screenshot,
@@ -94,15 +115,22 @@ let App = createClass({
         location,
         screenshot,
         viewports,
         onBrowserMounted,
         onChangeViewportDevice,
         onContentResize,
         onRotateViewport,
         onResizeViewport,
+        onUpdateDeviceModalOpen,
+      }),
+      DeviceModal({
+        devices,
+        onDeviceListUpdate,
+        onUpdateDeviceDisplayed,
+        onUpdateDeviceModalOpen,
       })
     );
   },
 
 });
 
 module.exports = connect(state => state)(App);
--- a/devtools/client/responsive.html/components/browser.js
+++ b/devtools/client/responsive.html/components/browser.js
@@ -8,17 +8,17 @@
 
 const { Task } = require("resource://gre/modules/Task.jsm");
 const DevToolsUtils = require("devtools/shared/DevToolsUtils");
 const { getToplevelWindow } = require("sdk/window/utils");
 const { DOM: dom, createClass, addons, PropTypes } =
   require("devtools/client/shared/vendor/react");
 
 const Types = require("../types");
-const { waitForMessage } = require("../utils/e10s");
+const e10s = require("../utils/e10s");
 
 module.exports = createClass({
   /**
    * This component is not allowed to depend directly on frequently changing
    * data (width, height) due to the use of `dangerouslySetInnerHTML` below.
    * Any changes in props will cause the <iframe> to be removed and added again,
    * throwing away the current state of the page.
    */
@@ -40,44 +40,43 @@ module.exports = createClass({
     let { onContentResize } = this;
     let browser = this.refs.browserContainer.querySelector("iframe.browser");
     let mm = browser.frameLoader.messageManager;
 
     // Notify tests when the content has received a resize event.  This is not
     // quite the same timing as when we _set_ a new size around the browser,
     // since it still needs to do async work before the content is actually
     // resized to match.
-    mm.addMessageListener("ResponsiveMode:OnContentResize", onContentResize);
+    e10s.on(mm, "OnContentResize", onContentResize);
 
-    let ready = waitForMessage(mm, "ResponsiveMode:ChildScriptReady");
+    let ready = e10s.once(mm, "ChildScriptReady");
     mm.loadFrameScript("resource://devtools/client/responsivedesign/" +
                        "responsivedesign-child.js", true);
     yield ready;
 
     let browserWindow = getToplevelWindow(window);
     let requiresFloatingScrollbars =
       !browserWindow.matchMedia("(-moz-overlay-scrollbars)").matches;
-    let started = waitForMessage(mm, "ResponsiveMode:Start:Done");
-    mm.sendAsyncMessage("ResponsiveMode:Start", {
+
+    yield e10s.request(mm, "Start", {
       requiresFloatingScrollbars,
       // Tests expect events on resize to yield on various size changes
       notifyOnResize: DevToolsUtils.testing,
     });
-    yield started;
 
     // manager.js waits for this signal before allowing browser tests to start
     this.props.onBrowserMounted();
   }),
 
   componentWillUnmount() {
     let { onContentResize } = this;
     let browser = this.refs.browserContainer.querySelector("iframe.browser");
     let mm = browser.frameLoader.messageManager;
-    mm.removeMessageListener("ResponsiveMode:OnContentResize", onContentResize);
-    mm.sendAsyncMessage("ResponsiveMode:Stop");
+    e10s.off(mm, "OnContentResize", onContentResize);
+    e10s.emit(mm, "Stop");
   },
 
   onContentResize(msg) {
     let { onContentResize } = this.props;
     let { width, height } = msg.data;
     onContentResize({
       width,
       height,
new file mode 100644
--- /dev/null
+++ b/devtools/client/responsive.html/components/device-modal.js
@@ -0,0 +1,139 @@
+/* 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 { DOM: dom, createClass, PropTypes, addons } =
+  require("devtools/client/shared/vendor/react");
+const { getStr } = require("../utils/l10n");
+const Types = require("../types");
+
+module.exports = createClass({
+  propTypes: {
+    devices: PropTypes.shape(Types.devices).isRequired,
+    onDeviceListUpdate: PropTypes.func.isRequired,
+    onUpdateDeviceDisplayed: PropTypes.func.isRequired,
+    onUpdateDeviceModalOpen: PropTypes.func.isRequired,
+  },
+
+  displayName: "DeviceModal",
+
+  mixins: [ addons.PureRenderMixin ],
+
+  getInitialState() {
+    return {};
+  },
+
+  componentWillReceiveProps(nextProps) {
+    let {
+      devices,
+    } = nextProps;
+
+    for (let type of devices.types) {
+      for (let device of devices[type]) {
+        this.setState({
+          [device.name]: device.displayed,
+        });
+      }
+    }
+  },
+
+  onDeviceCheckboxClick({ target }) {
+    this.setState({
+      [target.value]: !this.state[target.value]
+    });
+  },
+
+  onDeviceModalSubmit() {
+    let {
+      devices,
+      onDeviceListUpdate,
+      onUpdateDeviceDisplayed,
+      onUpdateDeviceModalOpen,
+    } = this.props;
+
+    let displayedDeviceList = [];
+
+    for (let type of devices.types) {
+      for (let device of devices[type]) {
+        if (this.state[device.name] != device.displayed) {
+          onUpdateDeviceDisplayed(device, type, this.state[device.name]);
+        }
+
+        if (this.state[device.name]) {
+          displayedDeviceList.push(device.name);
+        }
+      }
+    }
+
+    onDeviceListUpdate(displayedDeviceList);
+    onUpdateDeviceModalOpen(false);
+  },
+
+  render() {
+    let {
+      devices,
+      onUpdateDeviceModalOpen,
+    } = this.props;
+
+    let modalClass = "device-modal container";
+
+    if (!devices.isModalOpen) {
+      modalClass += " hidden";
+    }
+
+    return dom.div(
+      {
+        className: modalClass,
+      },
+      dom.button({
+        id: "device-close-button",
+        className: "toolbar-button devtools-button",
+        onClick: () => onUpdateDeviceModalOpen(false),
+      }),
+      dom.div(
+        {
+          className: "device-modal-content",
+        },
+        devices.types.map(type => {
+          return dom.div(
+            {
+              className: "device-type",
+              key: type,
+            },
+            dom.header(
+              {
+                className: "device-header",
+              },
+              type
+            ),
+            devices[type].map(device => {
+              return dom.label(
+                {
+                  className: "device-label",
+                  key: device.name,
+                },
+                dom.input({
+                  className: "device-input-checkbox",
+                  type: "checkbox",
+                  value: device.name,
+                  checked: this.state[device.name],
+                  onChange: this.onDeviceCheckboxClick,
+                }),
+                device.name
+              );
+            })
+          );
+        })
+      ),
+      dom.button(
+        {
+          id: "device-submit-button",
+          onClick: this.onDeviceModalSubmit,
+        },
+        getStr("responsive.done")
+      )
+    );
+  },
+});
--- a/devtools/client/responsive.html/components/device-selector.js
+++ b/devtools/client/responsive.html/components/device-selector.js
@@ -4,36 +4,44 @@
 
 "use strict";
 
 const { getStr } = require("../utils/l10n");
 const { DOM: dom, createClass, PropTypes, addons } =
   require("devtools/client/shared/vendor/react");
 
 const Types = require("../types");
+const OPEN_DEVICE_MODAL_VALUE = "OPEN_DEVICE_MODAL";
 
 module.exports = createClass({
   propTypes: {
     devices: PropTypes.shape(Types.devices).isRequired,
     selectedDevice: PropTypes.string.isRequired,
     onChangeViewportDevice: PropTypes.func.isRequired,
     onResizeViewport: PropTypes.func.isRequired,
+    onUpdateDeviceModalOpen: PropTypes.func.isRequired,
   },
 
   displayName: "DeviceSelector",
 
   mixins: [ addons.PureRenderMixin ],
 
   onSelectChange({ target }) {
     let {
       devices,
       onChangeViewportDevice,
       onResizeViewport,
+      onUpdateDeviceModalOpen,
     } = this.props;
 
+    if (target.value === OPEN_DEVICE_MODAL_VALUE) {
+      onUpdateDeviceModalOpen(true);
+      return;
+    }
+
     for (let type of devices.types) {
       for (let device of devices[type]) {
         if (device.name === target.value) {
           onResizeViewport(device.width, device.height);
           break;
         }
       }
     }
@@ -45,17 +53,19 @@ module.exports = createClass({
     let {
       devices,
       selectedDevice,
     } = this.props;
 
     let options = [];
     for (let type of devices.types) {
       for (let device of devices[type]) {
-        options.push(device);
+        if (device.displayed) {
+          options.push(device);
+        }
       }
     }
 
     let selectClass = "viewport-device-selector";
     if (selectedDevice) {
       selectClass += " selected";
     }
 
@@ -70,13 +80,16 @@ module.exports = createClass({
         disabled: true,
         hidden: true,
       }, getStr("responsive.noDeviceSelected")),
       options.map(device => {
         return dom.option({
           key: device.name,
           value: device.name,
         }, device.name);
-      })
+      }),
+      dom.option({
+        value: OPEN_DEVICE_MODAL_VALUE,
+      }, getStr("responsive.editDeviceList"))
     );
   },
 
 });
--- a/devtools/client/responsive.html/components/global-toolbar.js
+++ b/devtools/client/responsive.html/components/global-toolbar.js
@@ -25,17 +25,17 @@ module.exports = createClass({
       onExit,
       onScreenshot,
       screenshot,
     } = this.props;
 
     return dom.header(
       {
         id: "global-toolbar",
-        className: "toolbar",
+        className: "container",
       },
       dom.span(
         {
           className: "title",
         },
         getStr("responsive.title")),
       dom.button({
         id: "global-screenshot-button",
--- a/devtools/client/responsive.html/components/moz.build
+++ b/devtools/client/responsive.html/components/moz.build
@@ -1,16 +1,17 @@
 # -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*-
 # vim: set filetype=python:
 # 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/.
 
 DevToolsModules(
     'browser.js',
+    'device-modal.js',
     'device-selector.js',
     'global-toolbar.js',
     'resizable-viewport.js',
     'viewport-dimension.js',
     'viewport-toolbar.js',
     'viewport.js',
     'viewports.js',
 )
--- a/devtools/client/responsive.html/components/resizable-viewport.js
+++ b/devtools/client/responsive.html/components/resizable-viewport.js
@@ -23,16 +23,17 @@ module.exports = createClass({
     location: Types.location.isRequired,
     screenshot: PropTypes.shape(Types.screenshot).isRequired,
     viewport: PropTypes.shape(Types.viewport).isRequired,
     onBrowserMounted: PropTypes.func.isRequired,
     onChangeViewportDevice: PropTypes.func.isRequired,
     onContentResize: PropTypes.func.isRequired,
     onResizeViewport: PropTypes.func.isRequired,
     onRotateViewport: PropTypes.func.isRequired,
+    onUpdateDeviceModalOpen: PropTypes.func.isRequired,
   },
 
   displayName: "ResizableViewport",
 
   getInitialState() {
     return {
       isResizing: false,
       lastClientX: 0,
@@ -118,16 +119,17 @@ module.exports = createClass({
       location,
       screenshot,
       viewport,
       onBrowserMounted,
       onChangeViewportDevice,
       onContentResize,
       onResizeViewport,
       onRotateViewport,
+      onUpdateDeviceModalOpen,
     } = this.props;
 
     let resizeHandleClass = "viewport-resize-handle";
     if (screenshot.isCapturing) {
       resizeHandleClass += " hidden";
     }
 
     let contentClass = "viewport-content";
@@ -140,16 +142,17 @@ module.exports = createClass({
         className: "resizable-viewport",
       },
       ViewportToolbar({
         devices,
         selectedDevice: viewport.device,
         onChangeViewportDevice,
         onResizeViewport,
         onRotateViewport,
+        onUpdateDeviceModalOpen,
       }),
       dom.div(
         {
           className: contentClass,
           style: {
             width: viewport.width + "px",
             height: viewport.height + "px",
           },
--- a/devtools/client/responsive.html/components/viewport-toolbar.js
+++ b/devtools/client/responsive.html/components/viewport-toolbar.js
@@ -12,40 +12,43 @@ const DeviceSelector = createFactory(req
 
 module.exports = createClass({
   propTypes: {
     devices: PropTypes.shape(Types.devices).isRequired,
     selectedDevice: PropTypes.string.isRequired,
     onChangeViewportDevice: PropTypes.func.isRequired,
     onResizeViewport: PropTypes.func.isRequired,
     onRotateViewport: PropTypes.func.isRequired,
+    onUpdateDeviceModalOpen: PropTypes.func.isRequired,
   },
 
   displayName: "ViewportToolbar",
 
   mixins: [ addons.PureRenderMixin ],
 
   render() {
     let {
       devices,
       selectedDevice,
       onChangeViewportDevice,
       onResizeViewport,
       onRotateViewport,
+      onUpdateDeviceModalOpen,
     } = this.props;
 
     return dom.div(
       {
-        className: "toolbar viewport-toolbar",
+        className: "viewport-toolbar container",
       },
       DeviceSelector({
         devices,
         selectedDevice,
         onChangeViewportDevice,
         onResizeViewport,
+        onUpdateDeviceModalOpen,
       }),
       dom.button({
         className: "viewport-rotate-button toolbar-button devtools-button",
         onClick: onRotateViewport,
       })
     );
   },
 
--- a/devtools/client/responsive.html/components/viewport.js
+++ b/devtools/client/responsive.html/components/viewport.js
@@ -17,16 +17,17 @@ module.exports = createClass({
     location: Types.location.isRequired,
     screenshot: PropTypes.shape(Types.screenshot).isRequired,
     viewport: PropTypes.shape(Types.viewport).isRequired,
     onBrowserMounted: PropTypes.func.isRequired,
     onChangeViewportDevice: PropTypes.func.isRequired,
     onContentResize: PropTypes.func.isRequired,
     onResizeViewport: PropTypes.func.isRequired,
     onRotateViewport: PropTypes.func.isRequired,
+    onUpdateDeviceModalOpen: PropTypes.func.isRequired,
   },
 
   displayName: "Viewport",
 
   onChangeViewportDevice(device) {
     let {
       viewport,
       onChangeViewportDevice,
@@ -54,18 +55,19 @@ module.exports = createClass({
   },
 
   render() {
     let {
       devices,
       location,
       screenshot,
       viewport,
+      onBrowserMounted,
       onContentResize,
-      onBrowserMounted,
+      onUpdateDeviceModalOpen,
     } = this.props;
 
     let {
       onChangeViewportDevice,
       onRotateViewport,
       onResizeViewport,
     } = this;
 
@@ -78,16 +80,17 @@ module.exports = createClass({
         location,
         screenshot,
         viewport,
         onBrowserMounted,
         onChangeViewportDevice,
         onContentResize,
         onResizeViewport,
         onRotateViewport,
+        onUpdateDeviceModalOpen,
       }),
       ViewportDimension({
         viewport,
         onChangeViewportDevice,
         onResizeViewport,
       })
     );
   },
--- a/devtools/client/responsive.html/components/viewports.js
+++ b/devtools/client/responsive.html/components/viewports.js
@@ -16,31 +16,33 @@ module.exports = createClass({
     location: Types.location.isRequired,
     screenshot: PropTypes.shape(Types.screenshot).isRequired,
     viewports: PropTypes.arrayOf(PropTypes.shape(Types.viewport)).isRequired,
     onBrowserMounted: PropTypes.func.isRequired,
     onChangeViewportDevice: PropTypes.func.isRequired,
     onContentResize: PropTypes.func.isRequired,
     onResizeViewport: PropTypes.func.isRequired,
     onRotateViewport: PropTypes.func.isRequired,
+    onUpdateDeviceModalOpen: PropTypes.func.isRequired,
   },
 
   displayName: "Viewports",
 
   render() {
     let {
       devices,
       location,
       screenshot,
       viewports,
       onBrowserMounted,
       onChangeViewportDevice,
       onContentResize,
       onResizeViewport,
       onRotateViewport,
+      onUpdateDeviceModalOpen,
     } = this.props;
 
     return dom.div(
       {
         id: "viewports",
       },
       viewports.map(viewport => {
         return Viewport({
@@ -49,14 +51,15 @@ module.exports = createClass({
           location,
           screenshot,
           viewport,
           onBrowserMounted,
           onChangeViewportDevice,
           onContentResize,
           onResizeViewport,
           onRotateViewport,
+          onUpdateDeviceModalOpen,
         });
       })
     );
   },
 
 });
new file mode 100644
--- /dev/null
+++ b/devtools/client/responsive.html/devices.js
@@ -0,0 +1,81 @@
+/* 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 Services = require("Services");
+const { Task } = require("resource://gre/modules/Task.jsm");
+const { GetDevices } = require("devtools/client/shared/devices");
+const { addDevice, addDeviceType } = require("./actions/devices");
+
+const DISPLAYED_DEVICES_PREF = "devtools.responsive.html.displayedDeviceList";
+
+/**
+ * Get the device catalog and load the devices onto the store.
+ *
+ * @param  {Function} dispatch
+ *         Action dispatch function
+ */
+let initDevices = Task.async(function* (dispatch) {
+  let deviceList = loadDeviceList();
+  let devices = yield GetDevices();
+
+  for (let type of devices.TYPES) {
+    dispatch(addDeviceType(type));
+    for (let device of devices[type]) {
+      if (device.os == "fxos") {
+        continue;
+      }
+
+      let newDevice = Object.assign({}, device, {
+        displayed: deviceList.includes(device.name) ?
+                   true :
+                   !!device.featured,
+      });
+
+      if (newDevice.displayed) {
+        deviceList.push(newDevice.name);
+      }
+
+      dispatch(addDevice(newDevice, type));
+    }
+  }
+
+  updateDeviceList(deviceList);
+});
+
+/**
+ * Returns an array containing the user preference of displayed devices.
+ *
+ * @return {Array} containing the device names that are to be displayed in the
+ *         device catalog.
+ */
+function loadDeviceList() {
+  let deviceList = [];
+
+  if (Services.prefs.prefHasUserValue(DISPLAYED_DEVICES_PREF)) {
+    try {
+      deviceList = JSON.parse(Services.prefs.getCharPref(
+        DISPLAYED_DEVICES_PREF));
+    } catch (e) {
+      console.error(e);
+    }
+  }
+
+  return deviceList;
+}
+
+/**
+ * Update the displayed device list preference with the given device list.
+ *
+ * @param  {Array} devices
+ *         Array of device names that are displayed in the device catalog.
+ */
+function updateDeviceList(devices) {
+  Services.prefs.setCharPref(DISPLAYED_DEVICES_PREF, JSON.stringify(devices));
+}
+
+exports.initDevices = initDevices;
+exports.loadDeviceList = loadDeviceList;
+exports.updateDeviceList = updateDeviceList;
--- a/devtools/client/responsive.html/index.css
+++ b/devtools/client/responsive.html/index.css
@@ -1,25 +1,29 @@
 /* TODO: May break up into component local CSS.  Pending future discussions by
  * React component group on how to best handle CSS. */
 
 /**
  * CSS Variables specific to the responsive design mode
  */
 
 .theme-light {
-  --box-shadow: 0 4px 4px 0 rgba(155, 155, 155, 0.26);
+  --rdm-box-shadow: 0 4px 4px 0 rgba(155, 155, 155, 0.26);
+  --submit-button-active-background-color: rgba(0,0,0,0.12);
+  --submit-button-active-color: var(--theme-body-color);
   --viewport-active-color: var(--theme-body-color);
   --viewport-selection-arrow: url("./images/select-arrow.svg#light");
   --viewport-selection-arrow-selected:
     url("./images/select-arrow.svg#light-selected");
 }
 
 .theme-dark {
-  --box-shadow: 0 4px 4px 0 rgba(105, 105, 105, 0.26);
+  --rdm-box-shadow: 0 4px 4px 0 rgba(105, 105, 105, 0.26);
+  --submit-button-active-background-color: var(--toolbar-tab-hover-active);
+  --submit-button-active-color: var(--theme-selection-color);
   --viewport-active-color: var(--theme-selection-color);
   --viewport-selection-arrow: url("./images/select-arrow.svg#dark");
   --viewport-selection-arrow-selected:
     url("./images/select-arrow.svg#dark-selected");
 }
 
 * {
   box-sizing: border-box;
@@ -46,20 +50,20 @@ html, body {
 
   /* Snap to the top of the app when there isn't enough vertical space anymore
      to center the viewports (so we don't lose the global toolbar) */
   position: sticky;
   top: 0;
 }
 
 /**
- * Common style for toolbars and toolbar buttons
+ * Common style for containers and toolbar buttons
  */
 
-.toolbar {
+.container {
   background-color: var(--theme-toolbar-background);
   border: 1px solid var(--theme-splitter-color);
 }
 
 .toolbar-button {
   margin: 1px 3px;
   width: 16px;
   height: 16px;
@@ -75,17 +79,17 @@ html, body {
 
 /**
  * Global Toolbar
  */
 
 #global-toolbar {
   color: var(--theme-body-color-alt);
   border-radius: 2px;
-  box-shadow: var(--box-shadow);
+  box-shadow: var(--rdm-box-shadow);
   margin: 30px 0;
   padding: 4px 5px;
   display: inline-flex;
   -moz-user-select: none;
 }
 
 #global-toolbar > .title {
   border-right: 1px solid var(--theme-splitter-color);
@@ -137,17 +141,17 @@ html, body {
 .viewport {
   display: inline-block;
   /* Align all viewports to the top */
   vertical-align: top;
 }
 
 .resizable-viewport {
   border: 1px solid var(--theme-splitter-color);
-  box-shadow: var(--box-shadow);
+  box-shadow: var(--rdm-box-shadow);
   position: relative;
 }
 
 /**
  * Viewport Toolbar
  */
 
 .viewport-toolbar {
@@ -303,8 +307,93 @@ html, body {
   background: transparent;
   border: none;
   text-align: center;
 }
 
 .viewport-dimension-separator {
   -moz-user-select: none;
 }
+
+/**
+ * Device Modal
+ */
+
+.device-modal {
+  border-radius: 2px;
+  box-shadow: var(--rdm-box-shadow);
+  position: absolute;
+  margin: auto;
+  top: 0;
+  bottom: 0;
+  left: 0;
+  right: 0;
+  width: 642px;
+  height: 612px;
+}
+
+.device-modal.hidden {
+  display: none;
+}
+
+.device-modal-content {
+  display: flex;
+  flex-direction: column;
+  flex-wrap: wrap;
+  overflow: auto;
+  height: 550px;
+  width: 600px;
+  margin: 20px;
+}
+
+#device-close-button,
+#device-close-button::before {
+  position: absolute;
+  top: 5px;
+  right: 2px;
+  width: 12px;
+  height: 12px;
+}
+
+#device-close-button::before {
+  background-image: url("./images/close.svg");
+  margin: -6px 0 0 -6px;
+}
+
+.device-type {
+  display: flex;
+  flex-direction: column;
+  padding: 10px;
+}
+
+.device-header {
+  font-weight: bold;
+  text-transform: capitalize;
+  padding: 0 0 3px 23px;
+}
+
+.device-label {
+  padding-bottom: 3px;
+}
+
+.device-input-checkbox {
+  margin-right: 5px;
+}
+
+#device-submit-button {
+  background-color: var(--theme-tab-toolbar-background);
+  border-width: 1px 0 0 0;
+  border-top-width: 1px;
+  border-top-style: solid;
+  border-top-color: var(--theme-splitter-color);
+  color: var(--theme-body-color);
+  width: 100%;
+  height: 20px;
+}
+
+#device-submit-button:hover {
+  background-color: var(--toolbar-tab-hover);
+}
+
+#device-submit-button:hover:active {
+  background-color: var(--submit-button-active-background-color);
+  color: var(--submit-button-active-color);
+}
--- a/devtools/client/responsive.html/index.js
+++ b/devtools/client/responsive.html/index.js
@@ -8,50 +8,50 @@
 
 const { utils: Cu } = Components;
 const { BrowserLoader } =
   Cu.import("resource://devtools/client/shared/browser-loader.js", {});
 const { require } = BrowserLoader({
   baseURI: "resource://devtools/client/responsive.html/",
   window: this
 });
-const { GetDevices } = require("devtools/client/shared/devices");
+const { Task } = require("resource://gre/modules/Task.jsm");
 const Telemetry = require("devtools/client/shared/telemetry");
 const { loadSheet } = require("sdk/stylesheet/utils");
 
 const { createFactory, createElement } =
   require("devtools/client/shared/vendor/react");
 const ReactDOM = require("devtools/client/shared/vendor/react-dom");
 const { Provider } = require("devtools/client/shared/vendor/react-redux");
 
+const { initDevices } = require("./devices");
 const App = createFactory(require("./app"));
 const Store = require("./store");
-const { addDevice, addDeviceType } = require("./actions/devices");
 const { changeLocation } = require("./actions/location");
 const { addViewport, resizeViewport } = require("./actions/viewports");
 
 let bootstrap = {
 
   telemetry: new Telemetry(),
 
   store: null,
 
-  init() {
+  init: Task.async(function* () {
     // Load a special UA stylesheet to reset certain styles such as dropdown
     // lists.
     loadSheet(window,
               "resource://devtools/client/responsive.html/responsive-ua.css",
               "agent");
     this.telemetry.toolOpened("responsive");
     let store = this.store = Store();
+    yield initDevices(this.dispatch.bind(this));
     let provider = createElement(Provider, { store }, App());
     ReactDOM.render(provider, document.querySelector("#root"));
-    this.initDevices();
     window.postMessage({ type: "init" }, "*");
-  },
+  }),
 
   destroy() {
     this.store = null;
     this.telemetry.toolClosed("responsive");
     this.telemetry = null;
   },
 
   /**
@@ -64,29 +64,16 @@ let bootstrap = {
       // If actions are dispatched after store is destroyed, ignore them.  This
       // can happen in tests that close the tool quickly while async tasks like
       // initDevices() below are still pending.
       return;
     }
     this.store.dispatch(action);
   },
 
-  initDevices() {
-    GetDevices().then(devices => {
-      for (let type of devices.TYPES) {
-        this.dispatch(addDeviceType(type));
-        for (let device of devices[type]) {
-          if (device.os != "fxos") {
-            this.dispatch(addDevice(device, type));
-          }
-        }
-      }
-    });
-  },
-
 };
 
 window.addEventListener("load", function onLoad() {
   window.removeEventListener("load", onLoad);
   bootstrap.init();
 });
 
 window.addEventListener("unload", function onUnload() {
--- a/devtools/client/responsive.html/moz.build
+++ b/devtools/client/responsive.html/moz.build
@@ -11,16 +11,17 @@ DIRS += [
     'images',
     'reducers',
     'utils',
 ]
 
 DevToolsModules(
     'app.js',
     'constants.js',
+    'devices.js',
     'index.css',
     'manager.js',
     'reducers.js',
     'responsive-ua.css',
     'store.js',
     'types.js',
 )
 
--- a/devtools/client/responsive.html/reducers/devices.js
+++ b/devtools/client/responsive.html/reducers/devices.js
@@ -2,20 +2,23 @@
  * 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 {
   ADD_DEVICE,
   ADD_DEVICE_TYPE,
+  UPDATE_DEVICE_DISPLAYED,
+  UPDATE_DEVICE_MODAL_OPEN,
 } = require("../actions/index");
 
 const INITIAL_DEVICES = {
   types: [],
+  isModalOpen: false,
 };
 
 let reducers = {
 
   [ADD_DEVICE](devices, { device, deviceType }) {
     return Object.assign({}, devices, {
       [deviceType]: [...devices[deviceType], device],
     });
@@ -23,16 +26,36 @@ let reducers = {
 
   [ADD_DEVICE_TYPE](devices, { deviceType }) {
     return Object.assign({}, devices, {
       types: [...devices.types, deviceType],
       [deviceType]: [],
     });
   },
 
+  [UPDATE_DEVICE_DISPLAYED](devices, { device, deviceType, displayed }) {
+    let newDevices = devices[deviceType].map(d => {
+      if (d == device) {
+        d.displayed = displayed;
+      }
+
+      return d;
+    });
+
+    return Object.assign({}, devices, {
+      [deviceType]: newDevices,
+    });
+  },
+
+  [UPDATE_DEVICE_MODAL_OPEN](devices, { isOpen }) {
+    return Object.assign({}, devices, {
+      isModalOpen: isOpen,
+    });
+  },
+
 };
 
 module.exports = function(devices = INITIAL_DEVICES, action) {
   let reducer = reducers[action.type];
   if (!reducer) {
     return devices;
   }
   return reducer(devices, action);
--- a/devtools/client/responsive.html/test/browser/browser.ini
+++ b/devtools/client/responsive.html/test/browser/browser.ini
@@ -4,14 +4,16 @@ subsuite = devtools
 skip-if = (!e10s && debug) # Bug 1262416 - Intermittent crash at MessageLoop::DeletePendingTasks
 support-files =
   devices.json
   head.js
   !/devtools/client/commandline/test/helpers.js
   !/devtools/client/framework/test/shared-head.js
   !/devtools/client/framework/test/shared-redux-head.js
 
+[browser_device_modal_exit.js]
+[browser_device_modal_submit.js]
 [browser_device_width.js]
 [browser_exit_button.js]
 [browser_mouse_resize.js]
 [browser_resize_cmd.js]
 [browser_screenshot_button.js]
 [browser_viewport_basics.js]
new file mode 100644
--- /dev/null
+++ b/devtools/client/responsive.html/test/browser/browser_device_modal_exit.js
@@ -0,0 +1,38 @@
+/* Any copyright is dedicated to the Public Domain.
+http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test submitting display device changes on the device modal
+
+const TEST_URL = "data:text/html;charset=utf-8,";
+
+addRDMTask(TEST_URL, function* ({ ui }) {
+  let { store, document } = ui.toolWindow;
+  let modal = document.querySelector(".device-modal");
+  let closeButton = document.querySelector("#device-close-button");
+
+  // Wait until the viewport has been added
+  yield waitUntilState(store, state => state.viewports.length == 1);
+
+  openDeviceModal(ui);
+
+  let deviceListBefore = loadDeviceList();
+
+  info("Check the first unchecked device and exit the modal.");
+  let uncheckedCb = [...document.querySelectorAll(".device-input-checkbox")]
+    .filter(cb => !cb.checked)[0];
+  let value = uncheckedCb.value;
+  uncheckedCb.click();
+  closeButton.click();
+
+  ok(modal.classList.contains("hidden"),
+    "The device modal is hidden on exit.");
+
+  info("Check that the device list remains unchanged after exitting.");
+  let deviceListAfter = loadDeviceList();
+  is(deviceListBefore.length, deviceListAfter.length,
+    "Got expected number of displayed devices.");
+  ok(!deviceListAfter.includes(value),
+    value + " was not added to displayed device list.");
+});
new file mode 100644
--- /dev/null
+++ b/devtools/client/responsive.html/test/browser/browser_device_modal_submit.js
@@ -0,0 +1,59 @@
+/* Any copyright is dedicated to the Public Domain.
+http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test submitting display device changes on the device modal
+
+const TEST_URL = "data:text/html;charset=utf-8,";
+
+addRDMTask(TEST_URL, function* ({ ui }) {
+  let { store, document } = ui.toolWindow;
+  let modal = document.querySelector(".device-modal");
+  let select = document.querySelector(".viewport-device-selector");
+  let submitButton = document.querySelector("#device-submit-button");
+
+  // Wait until the viewport has been added
+  yield waitUntilState(store, state => state.viewports.length == 1);
+
+  openDeviceModal(ui);
+
+  info("Checking displayed device checkboxes are checked in the device modal.");
+  let checkedCbs = [...document.querySelectorAll(".device-input-checkbox")]
+    .filter(cb => cb.checked);
+  let deviceList = loadDeviceList();
+
+  is(deviceList.length, checkedCbs.length,
+    "Got expected number of displayed devices.");
+
+  for (let cb of checkedCbs) {
+    ok(deviceList.includes(cb.value), cb.value + " is correctly checked.");
+  }
+
+  info("Check the first unchecked device and submit new device list.");
+  let uncheckedCb = [...document.querySelectorAll(".device-input-checkbox")]
+    .filter(cb => !cb.checked)[0];
+  let value = uncheckedCb.value;
+  uncheckedCb.click();
+  submitButton.click();
+
+  ok(modal.classList.contains("hidden"),
+    "The device modal is hidden on submit.");
+
+  info("Checking new device is added to the displayed device list.");
+  deviceList = loadDeviceList();
+  ok(deviceList.includes(value), value + " added to displayed device list.");
+
+  info("Checking new device is added to the device selector.");
+  let options = [...select.options];
+  is(options.length - 2, deviceList.length,
+    "Got expected number of devices in device selector.");
+  ok(options.filter(o => o.value === value)[0],
+    value + " added to the device selector.");
+
+  info("Reopen device modal and check new device is correctly checked");
+  openDeviceModal(ui);
+  ok([...document.querySelectorAll(".device-input-checkbox")]
+    .filter(cb => cb.checked && cb.value === value)[0],
+    value + " is checked in the device modal.");
+});
--- a/devtools/client/responsive.html/test/browser/devices.json
+++ b/devtools/client/responsive.html/test/browser/devices.json
@@ -1,10 +1,10 @@
 {
-  "TYPES": [ "phones" ],
+  "TYPES": [ "phones", "tablets", "laptops", "televisions", "consoles", "watches" ],
   "phones": [
     {
       "name": "Firefox OS Flame",
       "width": 320,
       "height": 570,
       "pixelRatio": 1.5,
       "userAgent": "Mozilla/5.0 (Mobile; rv:39.0) Gecko/39.0 Firefox/39.0",
       "touch": true,
@@ -15,11 +15,637 @@
       "name": "Alcatel One Touch Fire",
       "width": 320,
       "height": 480,
       "pixelRatio": 1,
       "userAgent": "Mozilla/5.0 (Mobile; ALCATELOneTouch4012X; rv:28.0) Gecko/28.0 Firefox/28.0",
       "touch": true,
       "firefoxOS": true,
       "os": "fxos"
+    },
+    {
+      "name": "Alcatel One Touch Fire C",
+      "width": 320,
+      "height": 480,
+      "pixelRatio": 1,
+      "userAgent": "Mozilla/5.0 (Mobile; ALCATELOneTouch4019X; rv:28.0) Gecko/28.0 Firefox/28.0",
+      "touch": true,
+      "firefoxOS": true,
+      "os": "fxos"
+    },
+    {
+      "name": "Alcatel One Touch Fire E",
+      "width": 320,
+      "height": 480,
+      "pixelRatio": 2,
+      "userAgent": "Mozilla/5.0 (Mobile; ALCATELOneTouch6015X; rv:32.0) Gecko/32.0 Firefox/32.0",
+      "touch": true,
+      "firefoxOS": true,
+      "os": "fxos"
+    },
+    {
+      "name": "Apple iPhone 4",
+      "width": 320,
+      "height": 480,
+      "pixelRatio": 2,
+      "userAgent": "Mozilla/5.0 (iPhone; U; CPU iPhone OS 4_2_1 like Mac OS X; en-us) AppleWebKit/533.17.9 (KHTML, like Gecko) Version/5.0.2 Mobile/8C148 Safari/6533.18.5",
+      "touch": true,
+      "firefoxOS": false,
+      "os": "ios"
+    },
+    {
+      "name": "Apple iPhone 5",
+      "width": 320,
+      "height": 568,
+      "pixelRatio": 2,
+      "userAgent": "Mozilla/5.0 (iPhone; CPU iPhone OS 7_0 like Mac OS X; en-us) AppleWebKit/537.51.1 (KHTML, like Gecko) Version/7.0 Mobile/11A465 Safari/9537.53",
+      "touch": true,
+      "firefoxOS": false,
+      "os": "ios"
+    },
+    {
+      "name": "Apple iPhone 5s",
+      "width": 320,
+      "height": 568,
+      "pixelRatio": 2,
+      "userAgent": "Mozilla/5.0 (iPhone; CPU iPhone OS 9_2_1 like Mac OS X) AppleWebKit/601.1.46 (KHTML, like Gecko) Version/9.0 Mobile/13D15 Safari/601.1",
+      "touch": true,
+      "firefoxOS": false,
+      "os": "ios",
+      "featured": true
+    },
+    {
+      "name": "Apple iPhone 6",
+      "width": 375,
+      "height": 667,
+      "pixelRatio": 2,
+      "userAgent": "Mozilla/5.0 (iPhone; CPU iPhone OS 8_0 like Mac OS X) AppleWebKit/600.1.3 (KHTML, like Gecko) Version/8.0 Mobile/12A4345d Safari/600.1.4",
+      "touch": true,
+      "firefoxOS": false,
+      "os": "ios"
+    },
+    {
+      "name": "Apple iPhone 6 Plus",
+      "width": 414,
+      "height": 736,
+      "pixelRatio": 3,
+      "userAgent": "Mozilla/5.0 (iPhone; CPU iPhone OS 8_0 like Mac OS X) AppleWebKit/600.1.3 (KHTML, like Gecko) Version/8.0 Mobile/12A4345d Safari/600.1.4",
+      "touch": true,
+      "firefoxOS": false,
+      "os": "ios",
+      "featured": true
+    },
+    {
+      "name": "Apple iPhone 6s",
+      "width": 375,
+      "height": 667,
+      "pixelRatio": 2,
+      "userAgent": "Mozilla/5.0 (iPhone; CPU iPhone OS 8_0 like Mac OS X) AppleWebKit/600.1.3 (KHTML, like Gecko) Version/8.0 Mobile/12A4345d Safari/600.1.4",
+      "touch": true,
+      "firefoxOS": false,
+      "os": "ios",
+      "featured": true
+    },
+    {
+      "name": "Apple iPhone 6s Plus",
+      "width": 414,
+      "height": 736,
+      "pixelRatio": 3,
+      "userAgent": "Mozilla/5.0 (iPhone; CPU iPhone OS 8_0 like Mac OS X) AppleWebKit/600.1.3 (KHTML, like Gecko) Version/8.0 Mobile/12A4345d Safari/600.1.4",
+      "touch": true,
+      "firefoxOS": false,
+      "os": "ios"
+    },
+    {
+      "name": "BlackBerry Z30",
+      "width": 360,
+      "height": 640,
+      "pixelRatio": 2,
+      "userAgent": "Mozilla/5.0 (BB10; Touch) AppleWebKit/537.10+ (KHTML, like Gecko) Version/10.0.9.2372 Mobile Safari/537.10+",
+      "touch": true,
+      "firefoxOS": false,
+      "os": "blackberryos"
+    },
+    {
+      "name": "Geeksphone Keon",
+      "width": 320,
+      "height": 480,
+      "pixelRatio": 1,
+      "userAgent": "Mozilla/5.0 (Mobile; rv:39.0) Gecko/39.0 Firefox/39.0",
+      "touch": true,
+      "firefoxOS": true,
+      "os": "android"
+    },
+    {
+      "name": "Geeksphone Peak, Revolution",
+      "width": 360,
+      "height": 640,
+      "pixelRatio": 1.5,
+      "userAgent": "Mozilla/5.0 (Mobile; rv:39.0) Gecko/39.0 Firefox/39.0",
+      "touch": true,
+      "firefoxOS": true,
+      "os": "android"
+    },
+    {
+      "name": "Google Nexus S",
+      "width": 320,
+      "height": 533,
+      "pixelRatio": 1.5,
+      "userAgent": "Mozilla/5.0 (Linux; U; Android 2.3.4; en-us; Nexus S Build/GRJ22) AppleWebKit/533.1 (KHTML, like Gecko) Version/4.0 Mobile Safari/533.1",
+      "touch": true,
+      "firefoxOS": true,
+      "os": "android"
+    },
+    {
+      "name": "Google Nexus 4",
+      "width": 384,
+      "height": 640,
+      "pixelRatio": 2,
+      "userAgent": "Mozilla/5.0 (Linux; Android 4.4.4; en-us; Nexus 4 Build/JOP40D) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/42.0.2307.2 Mobile Safari/537.36",
+      "touch": true,
+      "firefoxOS": true,
+      "os": "android",
+      "featured": true
+    },
+    {
+      "name": "Google Nexus 5",
+      "width": 360,
+      "height": 640,
+      "pixelRatio": 3,
+      "userAgent": "Mozilla/5.0 (Linux; Android 4.4.4; en-us; Nexus 5 Build/JOP40D) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/42.0.2307.2 Mobile Safari/537.36",
+      "touch": true,
+      "firefoxOS": true,
+      "os": "android",
+      "featured": true
+    },
+    {
+      "name": "Google Nexus 6",
+      "width": 412,
+      "height": 732,
+      "pixelRatio": 3.5,
+      "userAgent": "Mozilla/5.0 (Linux; Android 5.1.1; Nexus 6 Build/LYZ28E) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/48.0.2564.23 Mobile Safari/537.36",
+      "touch": true,
+      "firefoxOS": true,
+      "os": "android",
+      "featured": true
+    },
+    {
+      "name": "Intex Cloud Fx",
+      "width": 320,
+      "height": 480,
+      "pixelRatio": 1,
+      "userAgent": "Mozilla/5.0 (Mobile; rv:32.0) Gecko/32.0 Firefox/32.0",
+      "touch": true,
+      "firefoxOS": true,
+      "os": "fxos"
+    },
+    {
+      "name": "KDDI Fx0",
+      "width": 360,
+      "height": 640,
+      "pixelRatio": 2,
+      "userAgent": "Mozilla/5.0 (Mobile; LGL25; rv:32.0) Gecko/32.0 Firefox/32.0",
+      "touch": true,
+      "firefoxOS": true,
+      "os": "fxos"
+    },
+    {
+      "name": "LG Fireweb",
+      "width": 320,
+      "height": 480,
+      "pixelRatio": 1,
+      "userAgent": "Mozilla/5.0 (Mobile; LG-D300; rv:18.1) Gecko/18.1 Firefox/18.1",
+      "touch": true,
+      "firefoxOS": true,
+      "os": "fxos"
+    },
+    {
+      "name": "LG Optimus L70",
+      "width": 384,
+      "height": 640,
+      "pixelRatio": 1.25,
+      "userAgent": "Mozilla/5.0 (Linux; U; Android 4.4.2; en-us; LGMS323 Build/KOT49I.MS32310c) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/30.0.1599.103 Mobile Safari/537.36",
+      "touch": true,
+      "firefoxOS": false,
+      "os": "android"
+    },
+    {
+      "name": "Nokia Lumia 520",
+      "width": 320,
+      "height": 533,
+      "pixelRatio": 1.4,
+      "userAgent": "Mozilla/5.0 (compatible; MSIE 10.0; Windows Phone 8.0; Trident/6.0; IEMobile/10.0; ARM; Touch; NOKIA; Lumia 520)",
+      "touch": true,
+      "firefoxOS": false,
+      "os": "android",
+      "featured": true
+    },
+    {
+      "name": "Nokia N9",
+      "width": 360,
+      "height": 640,
+      "pixelRatio": 1,
+      "userAgent": "Mozilla/5.0 (MeeGo; NokiaN9) AppleWebKit/534.13 (KHTML, like Gecko) NokiaBrowser/8.5.0 Mobile Safari/534.13",
+      "touch": true,
+      "firefoxOS": false,
+      "os": "android"
+    },
+    {
+      "name": "OnePlus One",
+      "width": 360,
+      "height": 640,
+      "pixelRatio": 3,
+      "userAgent": "Mozilla/5.0 (Android 5.1.1; Mobile; rv:43.0) Gecko/43.0 Firefox/43.0",
+      "touch": true,
+      "firefoxOS": false,
+      "os": "android"
+    },
+    {
+      "name": "Samsung Galaxy S3",
+      "width": 360,
+      "height": 640,
+      "pixelRatio": 2,
+      "userAgent": "Mozilla/5.0 (Linux; U; Android 4.0; en-us; GT-I9300 Build/IMM76D) AppleWebKit/534.30 (KHTML, like Gecko) Version/4.0 Mobile Safari/534.30",
+      "touch": true,
+      "firefoxOS": false,
+      "os": "android"
+    },
+    {
+      "name": "Samsung Galaxy S4",
+      "width": 360,
+      "height": 640,
+      "pixelRatio": 3,
+      "userAgent": "Mozilla/5.0 (Linux; Android 4.4.2; GT-I9505 Build/JDQ39) AppleWebKit/537.36 (KHTML, like Gecko) Version/1.5 Chrome/28.0.1500.94 Mobile Safari/537.36",
+      "touch": true,
+      "firefoxOS": false,
+      "os": "android"
+    },
+    {
+      "name": "Samsung Galaxy S5",
+      "width": 360,
+      "height": 640,
+      "pixelRatio": 3,
+      "userAgent": "Mozilla/5.0 (Linux; Android 4.4.2; GT-I9505 Build/JDQ39) AppleWebKit/537.36 (KHTML, like Gecko) Version/1.5 Chrome/28.0.1500.94 Mobile Safari/537.36",
+      "touch": true,
+      "firefoxOS": false,
+      "os": "android",
+      "featured": true
+    },
+    {
+      "name": "Samsung Galaxy S6",
+      "width": 360,
+      "height": 640,
+      "pixelRatio": 4,
+      "userAgent": "Mozilla/5.0 (Linux; Android 4.4.2; GT-I9505 Build/JDQ39) AppleWebKit/537.36 (KHTML, like Gecko) Version/1.5 Chrome/28.0.1500.94 Mobile Safari/537.36",
+      "touch": true,
+      "firefoxOS": false,
+      "os": "android",
+      "featured": true
+    },
+    {
+      "name": "Sony Xperia Z3",
+      "width": 360,
+      "height": 640,
+      "pixelRatio": 3,
+      "userAgent": "Mozilla/5.0 (Mobile; rv:39.0) Gecko/39.0 Firefox/39.0",
+      "touch": true,
+      "firefoxOS": true,
+      "os": "android"
+    },
+    {
+      "name": "Spice Fire One Mi-FX1",
+      "width": 320,
+      "height": 480,
+      "pixelRatio": 1,
+      "userAgent": "Mozilla/5.0 (Mobile; rv:28.0) Gecko/28.0 Firefox/28.0",
+      "touch": true,
+      "firefoxOS": true,
+      "os": "fxos"
+    },
+    {
+      "name": "Symphony GoFox F15",
+      "width": 320,
+      "height": 480,
+      "pixelRatio": 1,
+      "userAgent": "Mozilla/5.0 (Mobile; rv:30.0) Gecko/30.0 Firefox/30.0",
+      "touch": true,
+      "firefoxOS": true,
+      "os": "fxos"
+    },
+    {
+      "name": "ZTE Open",
+      "width": 320,
+      "height": 480,
+      "pixelRatio": 1,
+      "userAgent": "Mozilla/5.0 (Mobile; ZTEOPEN; rv:18.1) Gecko/18.0 Firefox/18.1",
+      "touch": true,
+      "firefoxOS": true,
+      "os": "fxos"
+    },
+    {
+      "name": "ZTE Open II",
+      "width": 320,
+      "height": 480,
+      "pixelRatio": 1,
+      "userAgent": "Mozilla/5.0 (Mobile; OPEN2; rv:28.0) Gecko/28.0 Firefox/28.0",
+      "touch": true,
+      "firefoxOS": true,
+      "os": "fxos"
+    },
+    {
+      "name": "ZTE Open C",
+      "width": 320,
+      "height": 450,
+      "pixelRatio": 1.5,
+      "userAgent": "Mozilla/5.0 (Mobile; OPENC; rv:32.0) Gecko/32.0 Firefox/32.0",
+      "touch": true,
+      "firefoxOS": true,
+      "os": "fxos"
+    },
+    {
+      "name": "Zen Fire 105",
+      "width": 320,
+      "height": 480,
+      "pixelRatio": 1,
+      "userAgent": "Mozilla/5.0 (Mobile; rv:28.0) Gecko/28.0 Firefox/28.0",
+      "touch": true,
+      "firefoxOS": true,
+      "os": "fxos"
+    }
+  ],
+  "tablets": [
+    {
+      "name": "Amazon Kindle Fire HDX 8.9",
+      "width": 1280,
+      "height": 800,
+      "pixelRatio": 2,
+      "userAgent": "Mozilla/5.0 (Linux; U; en-us; KFAPWI Build/JDQ39) AppleWebKit/535.19 (KHTML, like Gecko) Silk/3.13 Safari/535.19 Silk-Accelerated=true",
+      "touch": true,
+      "firefoxOS": false,
+      "os": "fireos",
+      "featured": true
+    },
+    {
+      "name": "Apple iPad",
+      "width": 1024,
+      "height": 768,
+      "pixelRatio": 2,
+      "userAgent": "Mozilla/5.0 (iPad; CPU OS 7_0 like Mac OS X) AppleWebKit/537.51.1 (KHTML, like Gecko) Version/7.0 Mobile/11A465 Safari/9537.53",
+      "touch": true,
+      "firefoxOS": false,
+      "os": "ios"
+    },
+    {
+      "name": "Apple iPad Air 2",
+      "width": 1024,
+      "height": 768,
+      "pixelRatio": 2,
+      "userAgent": "Mozilla/5.0 (iPad; CPU OS 4_3_5 like Mac OS X; en-us) AppleWebKit/533.17.9 (KHTML, like Gecko) Version/5.0.2 Mobile/8L1 Safari/6533.18.5",
+      "touch": true,
+      "firefoxOS": false,
+      "os": "ios",
+      "featured": true
+    },
+    {
+      "name": "Apple iPad Mini",
+      "width": 1024,
+      "height": 768,
+      "pixelRatio": 1,
+      "userAgent": "Mozilla/5.0 (iPad; CPU OS 4_3_5 like Mac OS X; en-us) AppleWebKit/533.17.9 (KHTML, like Gecko) Version/5.0.2 Mobile/8L1 Safari/6533.18.5",
+      "touch": true,
+      "firefoxOS": false,
+      "os": "ios"
+    },
+    {
+      "name": "Apple iPad Mini 2",
+      "width": 1024,
+      "height": 768,
+      "pixelRatio": 2,
+      "userAgent": "Mozilla/5.0 (iPad; CPU OS 4_3_5 like Mac OS X; en-us) AppleWebKit/533.17.9 (KHTML, like Gecko) Version/5.0.2 Mobile/8L1 Safari/6533.18.5",
+      "touch": true,
+      "firefoxOS": false,
+      "os": "ios",
+      "featured": true
+    },
+    {
+      "name": "BlackBerry PlayBook",
+      "width": 1024,
+      "height": 600,
+      "pixelRatio": 1,
+      "userAgent": "Mozilla/5.0 (PlayBook; U; RIM Tablet OS 2.1.0; en-US) AppleWebKit/536.2+ (KHTML like Gecko) Version/7.2.1.0 Safari/536.2+",
+      "touch": true,
+      "firefoxOS": false,
+      "os": "blackberryos"
+    },
+    {
+      "name": "Foxconn InFocus",
+      "width": 1280,
+      "height": 800,
+      "pixelRatio": 1,
+      "userAgent": "Mozilla/5.0 (Tablet; rv:32.0) Gecko/32.0 Firefox/32.0",
+      "touch": true,
+      "firefoxOS": true,
+      "os": "android"
+    },
+    {
+      "name": "Google Nexus 7",
+      "width": 960,
+      "height": 600,
+      "pixelRatio": 2,
+      "userAgent": "Mozilla/5.0 (Linux; Android 4.3; Nexus 7 Build/JSS15Q) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/42.0.2307.2 Mobile Safari/537.36",
+      "touch": true,
+      "firefoxOS": false,
+      "os": "android",
+      "featured": true
+    },
+    {
+      "name": "Google Nexus 10",
+      "width": 1280,
+      "height": 800,
+      "pixelRatio": 2,
+      "userAgent": "Mozilla/5.0 (Linux; Android 4.3; Nexus 10 Build/JSS15Q) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/42.0.2307.2 Mobile Safari/537.36",
+      "touch": true,
+      "firefoxOS": false,
+      "os": "android"
+    },
+    {
+      "name": "Samsung Galaxy Note 2",
+      "width": 360,
+      "height": 640,
+      "pixelRatio": 2,
+      "userAgent": "Mozilla/5.0 (Linux; U; Android 4.1; en-us; GT-N7100 Build/JRO03C) AppleWebKit/534.30 (KHTML, like Gecko) Version/4.0 Mobile Safari/534.30",
+      "touch": true,
+      "firefoxOS": false,
+      "os": "android"
+    },
+    {
+      "name": "Samsung Galaxy Note 3",
+      "width": 360,
+      "height": 640,
+      "pixelRatio": 3,
+      "userAgent": "Mozilla/5.0 (Linux; U; Android 4.3; en-us; SM-N900T Build/JSS15J) AppleWebKit/534.30 (KHTML, like Gecko) Version/4.0 Mobile Safari/534.30",
+      "touch": true,
+      "firefoxOS": false,
+      "os": "android",
+      "featured": true
+    },
+    {
+      "name": "Tesla Model S",
+      "width": 1200,
+      "height": 1920,
+      "pixelRatio": 1,
+      "userAgent": "Mozilla/5.0 (X11; Linux) AppleWebKit/534.34 (KHTML, like Gecko) QtCarBrowser Safari/534.34",
+      "touch": true,
+      "firefoxOS": false,
+      "os": "linux"
+    },
+    {
+      "name": "VIA Vixen",
+      "width": 1024,
+      "height": 600,
+      "pixelRatio": 1,
+      "userAgent": "Mozilla/5.0 (Tablet; rv:32.0) Gecko/32.0 Firefox/32.0",
+      "touch": true,
+      "firefoxOS": true,
+      "os": "fxos"
+    }
+  ],
+  "laptops": [
+    {
+      "name": "Laptop (1366 x 768)",
+      "width": 1366,
+      "height": 768,
+      "pixelRatio": 1,
+      "userAgent": "",
+      "touch": false,
+      "firefoxOS": false,
+      "os": "windows",
+      "featured": true
+    },
+    {
+      "name": "Laptop (1920 x 1080)",
+      "width": 1280,
+      "height": 720,
+      "pixelRatio": 1.5,
+      "userAgent": "",
+      "touch": false,
+      "firefoxOS": false,
+      "os": "windows",
+      "featured": true
+    },
+    {
+      "name": "Laptop (1920 x 1080) with touch",
+      "width": 1280,
+      "height": 720,
+      "pixelRatio": 1.5,
+      "userAgent": "",
+      "touch": true,
+      "firefoxOS": false,
+      "os": "windows"
+    }
+  ],
+  "televisions": [
+    {
+      "name": "720p HD Television",
+      "width": 1280,
+      "height": 720,
+      "pixelRatio": 1,
+      "userAgent": "",
+      "touch": false,
+      "firefoxOS": true,
+      "os": "custom"
+    },
+    {
+      "name": "1080p Full HD Television",
+      "width": 1920,
+      "height": 1080,
+      "pixelRatio": 1,
+      "userAgent": "",
+      "touch": false,
+      "firefoxOS": true,
+      "os": "custom"
+    },
+    {
+      "name": "4K Ultra HD Television",
+      "width": 3840,
+      "height": 2160,
+      "pixelRatio": 1,
+      "userAgent": "",
+      "touch": false,
+      "firefoxOS": true,
+      "os": "custom"
+    }
+  ],
+  "consoles": [
+    {
+      "name": "Nintendo 3DS",
+      "width": 320,
+      "height": 240,
+      "pixelRatio": 1,
+      "userAgent": "Mozilla/5.0 (Nintendo 3DS; U; ; en) Version/1.7585.EU",
+      "touch": true,
+      "firefoxOS": false,
+      "os": "nintendo"
+    },
+    {
+      "name": "Nintendo Wii U Gamepad",
+      "width": 854,
+      "height": 480,
+      "pixelRatio": 0.87,
+      "userAgent": "Mozilla/5.0 (Nintendo WiiU) AppleWebKit/536.28 (KHTML, like Gecko) NX/3.0.3.12.15 NintendoBrowser/4.1.1.9601.EU",
+      "touch": true,
+      "firefoxOS": false,
+      "os": "nintendo"
+    },
+    {
+      "name": "Sony PlayStation Vita",
+      "width": 960,
+      "height": 544,
+      "pixelRatio": 1,
+      "userAgent": "Mozilla/5.0 (Playstation Vita 1.61) AppleWebKit/531.22.8 (KHTML, like Gecko) Silk/3.2",
+      "touch": true,
+      "firefoxOS": false,
+      "os": "playstation"
+    }
+  ],
+  "watches": [
+    {
+      "name": "LG G Watch",
+      "width": 280,
+      "height": 280,
+      "pixelRatio": 1,
+      "userAgent": "",
+      "touch": true,
+      "firefoxOS": true,
+      "os": "android"
+    },
+    {
+      "name": "LG G Watch R",
+      "width": 320,
+      "height": 320,
+      "pixelRatio": 1,
+      "userAgent": "",
+      "touch": true,
+      "firefoxOS": true,
+      "os": "android"
+    },
+    {
+      "name": "Motorola Moto 360",
+      "width": 320,
+      "height": 290,
+      "pixelRatio": 1,
+      "userAgent": "Mozilla/5.0 (Linux; Android 5.0.1; Moto 360 Build/LWX48T) AppleWebkit/537.36 (KHTML, like Gecko) Chrome/19.77.34.5 Mobile Safari/537.36",
+      "touch": true,
+      "firefoxOS": true,
+      "os": "android"
+    },
+    {
+      "name": "Samsung Gear Live",
+      "width": 320,
+      "height": 320,
+      "pixelRatio": 1,
+      "userAgent": "",
+      "touch": true,
+      "firefoxOS": true,
+      "os": "android"
     }
   ]
 }
--- a/devtools/client/responsive.html/test/browser/head.js
+++ b/devtools/client/responsive.html/test/browser/head.js
@@ -21,26 +21,31 @@ Services.scriptloader.loadSubScript(
   this);
 
 const TEST_URI_ROOT = "http://example.com/browser/devtools/client/responsive.html/test/browser/";
 
 SimpleTest.requestCompleteLog();
 SimpleTest.waitForExplicitFinish();
 
 DevToolsUtils.testing = true;
+Services.prefs.clearUserPref("devtools.responsive.html.displayedDeviceList");
 Services.prefs.setCharPref("devtools.devices.url",
   TEST_URI_ROOT + "devices.json");
 Services.prefs.setBoolPref("devtools.responsive.html.enabled", true);
 
 registerCleanupFunction(() => {
   DevToolsUtils.testing = false;
   Services.prefs.clearUserPref("devtools.devices.url");
   Services.prefs.clearUserPref("devtools.responsive.html.enabled");
+  Services.prefs.clearUserPref("devtools.responsive.html.displayedDeviceList");
 });
 const { ResponsiveUIManager } = Cu.import("resource://devtools/client/responsivedesign/responsivedesign.jsm", {});
+const { loadDeviceList } = require("devtools/client/responsive.html/devices");
+
+const OPEN_DEVICE_MODAL_VALUE = "OPEN_DEVICE_MODAL";
 
 /**
  * Open responsive design mode for the given tab.
  */
 var openRDM = Task.async(function* (tab) {
   info("Opening responsive design mode");
   let manager = ResponsiveUIManager;
   let ui = yield manager.openIfNeeded(window, tab);
@@ -120,8 +125,30 @@ var setViewportSize = Task.async(functio
   info(`Current size: ${size.width} x ${size.height}, ` +
        `set to: ${width} x ${height}`);
   if (size.width != width || size.height != height) {
     let resized = waitForViewportResizeTo(ui, width, height);
     ui.setViewportSize(width, height);
     yield resized;
   }
 });
+
+function openDeviceModal(ui) {
+  let { document } = ui.toolWindow;
+  let select = document.querySelector(".viewport-device-selector");
+  let modal = document.querySelector(".device-modal");
+  let editDeviceOption = [...select.options].filter(o => {
+    return o.value === OPEN_DEVICE_MODAL_VALUE;
+  })[0];
+
+  info("Checking initial device modal state");
+  ok(modal.classList.contains("hidden"),
+    "The device modal is hidden by default.");
+
+  info("Opening device modal through device selector.");
+  EventUtils.synthesizeMouseAtCenter(select, {type: "mousedown"},
+    ui.toolWindow);
+  EventUtils.synthesizeMouseAtCenter(editDeviceOption, {type: "mouseup"},
+    ui.toolWindow);
+
+  ok(!modal.classList.contains("hidden"),
+    "The device modal is displayed.");
+}
new file mode 100644
--- /dev/null
+++ b/devtools/client/responsive.html/test/unit/test_update_device_displayed.js
@@ -0,0 +1,37 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test updating the device `displayed` property
+
+const {
+  addDevice,
+  addDeviceType,
+  updateDeviceDisplayed,
+} = require("devtools/client/responsive.html/actions/devices");
+
+add_task(function* () {
+  let store = Store();
+  const { getState, dispatch } = store;
+
+  let device = {
+    "name": "Firefox OS Flame",
+    "width": 320,
+    "height": 570,
+    "pixelRatio": 1.5,
+    "userAgent": "Mozilla/5.0 (Mobile; rv:39.0) Gecko/39.0 Firefox/39.0",
+    "touch": true,
+    "firefoxOS": true,
+    "os": "fxos"
+  };
+
+  dispatch(addDeviceType("phones"));
+  dispatch(addDevice(device, "phones"));
+  dispatch(updateDeviceDisplayed(device, "phones", true));
+
+  equal(getState().devices.phones.length, 1,
+    "Correct number of phones");
+  ok(getState().devices.phones[0].displayed,
+    "Device phone list contains enabled Firefox OS Flame");
+});
--- a/devtools/client/responsive.html/test/unit/xpcshell.ini
+++ b/devtools/client/responsive.html/test/unit/xpcshell.ini
@@ -6,8 +6,9 @@ firefox-appdir = browser
 
 [test_add_device.js]
 [test_add_device_type.js]
 [test_add_viewport.js]
 [test_change_location.js]
 [test_change_viewport_device.js]
 [test_resize_viewport.js]
 [test_rotate_viewport.js]
+[test_update_device_displayed.js]
--- a/devtools/client/responsive.html/types.js
+++ b/devtools/client/responsive.html/types.js
@@ -27,19 +27,22 @@ const device = {
   pixelRatio: PropTypes.number,
 
   // The user agent string of the device
   userAgent: PropTypes.string,
 
   // Whether or not it is a touch device
   touch: PropTypes.bool,
 
-  //  The operating system of the device
+  // The operating system of the device
   os: PropTypes.String,
 
+  // Whether or not the device is displayed in the device selector
+  displayed: PropTypes.bool,
+
 };
 
 /**
  * A list of devices and their types that can be displayed in the viewport.
  */
 exports.devices = {
 
   // An array of device types
@@ -58,16 +61,19 @@ exports.devices = {
   televisions: PropTypes.arrayOf(PropTypes.shape(device)),
 
   // An array of console devices
   consoles: PropTypes.arrayOf(PropTypes.shape(device)),
 
   // An array of watch devices
   watches: PropTypes.arrayOf(PropTypes.shape(device)),
 
+  // Whether or not the device modal is open
+  isModalOpen: PropTypes.bool,
+
 };
 
 /**
  * The location of the document displayed in the viewport(s).
  */
 exports.location = PropTypes.string;
 
 /**
--- a/devtools/client/responsive.html/utils/e10s.js
+++ b/devtools/client/responsive.html/utils/e10s.js
@@ -1,23 +1,103 @@
 /* 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 promise = require("promise");
+const { defer } = require("promise");
+
+// The prefix used for RDM messages in content.
+// see: devtools/client/responsivedesign/responsivedesign-child.js
+const MESSAGE_PREFIX = "ResponsiveMode:";
+const REQUEST_DONE_SUFFIX = ":Done";
 
-module.exports = {
+/**
+ * Registers a message `listener` that is called every time messages of
+ * specified `message` is emitted on the given message manager.
+ * @param {nsIMessageListenerManager} mm
+ *    The Message Manager
+ * @param {String} message
+ *    The message. It will be prefixed with the constant `MESSAGE_PREFIX`
+ * @param {Function} listener
+ *    The listener function that processes the message.
+ */
+function on(mm, message, listener) {
+  mm.addMessageListener(MESSAGE_PREFIX + message, listener);
+}
+exports.on = on;
 
-  waitForMessage(mm, message) {
-    let deferred = promise.defer();
+/**
+ * Removes a message `listener` for the specified `message` on the given
+ * message manager.
+ * @param {nsIMessageListenerManager} mm
+ *    The Message Manager
+ * @param {String} message
+ *    The message. It will be prefixed with the constant `MESSAGE_PREFIX`
+ * @param {Function} listener
+ *    The listener function that processes the message.
+ */
+function off(mm, message, listener) {
+  mm.removeMessageListener(MESSAGE_PREFIX + message, listener);
+}
+exports.off = off;
 
-    let onMessage = event => {
-      mm.removeMessageListener(message, onMessage);
-      deferred.resolve();
-    };
-    mm.addMessageListener(message, onMessage);
+/**
+ * Resolves a promise the next time the specified `message` is sent over the
+ * given message manager.
+ * @param {nsIMessageListenerManager} mm
+ *    The Message Manager
+ * @param {String} message
+ *    The message. It will be prefixed with the constant `MESSAGE_PREFIX`
+ * @returns {Promise}
+ *    A promise that is resolved when the given message is emitted.
+ */
+function once(mm, message) {
+  let { resolve, promise } = defer();
+
+  on(mm, message, function onMessage({data}) {
+    off(mm, message, onMessage);
+    resolve(data);
+  });
+
+  return promise;
+}
+exports.once = once;
 
-    return deferred.promise;
-  },
+/**
+ * Asynchronously emit a `message` to the listeners of the given message
+ * manager.
+ *
+ * @param {nsIMessageListenerManager} mm
+ *    The Message Manager
+ * @param {String} message
+ *    The message. It will be prefixed with the constant `MESSAGE_PREFIX`.
+ * @param {Object} data
+ *    A JSON object containing data to be delivered to the listeners.
+ */
+function emit(mm, message, data) {
+  mm.sendAsyncMessage(MESSAGE_PREFIX + message, data);
+}
+exports.emit = emit;
 
-};
+/**
+ * Asynchronously send a "request" over the given message manager, and returns
+ * a promise that is resolved when the request is complete.
+ *
+ * @param {nsIMessageListenerManager} mm
+ *    The Message Manager
+ * @param {String} message
+ *    The message. It will be prefixed with the constant `MESSAGE_PREFIX`, and
+ *    also suffixed with `REQUEST_DONE_SUFFIX` for the reply.
+ * @param {Object} data
+ *    A JSON object containing data to be delivered to the listeners.
+ * @returns {Promise}
+ *    A promise that is resolved when the request is done.
+ */
+function request(mm, message, data) {
+  let done = once(mm, message + REQUEST_DONE_SUFFIX);
+
+  emit(mm, message, data);
+
+  return done;
+}
+exports.request = request;
--- a/devtools/client/responsivedesign/responsivedesign-child.js
+++ b/devtools/client/responsivedesign/responsivedesign-child.js
@@ -145,22 +145,24 @@ var global = this;
     docShell.contentViewer.sticky = false;
     docShell.contentViewer.hide();
     docShell.contentViewer.show();
     docShell.contentViewer.sticky = isSticky;
   }
 
   function screenshot() {
     let canvas = content.document.createElementNS("http://www.w3.org/1999/xhtml", "canvas");
-    let width = content.innerWidth;
-    let height = content.innerHeight;
+    let ratio = content.devicePixelRatio;
+    let width = content.innerWidth * ratio;
+    let height = content.innerHeight * ratio;
     canvas.mozOpaque = true;
     canvas.width = width;
     canvas.height = height;
     let ctx = canvas.getContext("2d");
+    ctx.scale(ratio, ratio);
     ctx.drawWindow(content, content.scrollX, content.scrollY, width, height, "#fff");
     sendAsyncMessage("ResponsiveMode:RequestScreenshot:Done", canvas.toDataURL());
   }
 
   var WebProgressListener = {
     onLocationChange(webProgress, request, URI, flags) {
       if (flags & Ci.nsIWebProgressListener.LOCATION_CHANGE_SAME_DOCUMENT) {
         return;
--- a/devtools/client/shared/components/reps/reps.css
+++ b/devtools/client/shared/components/reps/reps.css
@@ -1,112 +1,125 @@
 /* vim:set ts=2 sw=2 sts=2 et: */
 /* 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/. */
 
+.theme-dark,
+.theme-light {
+  --number-color: var(--theme-highlight-green);
+  --string-color: var(--theme-highlight-orange);
+  --null-color: var(--theme-comment);
+  --object-color: var(--theme-body-color);
+  --caption-color: var(--theme-highlight-blue);
+  --location-color: var(--theme-content-color1);
+  --source-link-color: var(--theme-highlight-blue);
+  --node-color: var(--theme-highlight-bluegrey);
+  --reference-color: var(--theme-highlight-purple);
+}
+
+.theme-firebug {
+  --number-color: #000088;
+  --string-color: #FF0000;
+  --null-color: #787878;
+  --object-color: DarkGreen;
+  --caption-color: #444444;
+  --location-color: #555555;
+  --source-link-color: blue;
+  --node-color: rgb(0, 0, 136);
+  --reference-color: rgb(102, 102, 255);
+}
+
+/******************************************************************************/
+
 .objectLink:hover {
   cursor: pointer;
   text-decoration: underline;
 }
 
-/******************************************************************************/
-
 .inline {
   display: inline;
   white-space: normal;
 }
 
 .objectBox-object {
   font-weight: bold;
-  color: DarkGreen;
+  color: var(--object-color);
   white-space: pre-wrap;
 }
 
 .objectBox-string,
 .objectBox-text,
 .objectLink-textNode,
 .objectBox-table {
   white-space: pre-wrap;
 }
 
 .objectBox-number,
 .objectLink-styleRule,
 .objectLink-element,
 .objectLink-textNode,
 .objectBox-array > .length {
-  color: #000088;
+  color: var(--number-color);
 }
 
 .objectBox-string {
-  color: #FF0000;
+  color: var(--string-color);
 }
 
 .objectLink-function,
 .objectBox-stackTrace,
 .objectLink-profile {
-  color: DarkGreen;
+  color: var(--object-color);
 }
 
 .objectLink-Location {
   font-style: italic;
-  color: #555555;
+  color: var(--location-color);
 }
 
 .objectBox-null,
 .objectBox-undefined,
 .objectBox-hint,
 .logRowHint {
   font-style: italic;
-  color: #787878;
-}
-
-.objectBox-scope {
-  color: #707070;
-}
-.objectBox-optimizedAway {
-  color: #909090;
+  color: var(--null-color);
 }
 
 .objectLink-sourceLink {
   position: absolute;
   right: 4px;
   top: 2px;
   padding-left: 8px;
   font-weight: bold;
-  color: #0000FF;
-}
-
-.objectLink-sourceLink > .systemLink {
-  float: right;
-  color: #FF0000;
+  color: var(--source-link-color);
 }
 
 /******************************************************************************/
 
 .objectLink-event,
 .objectLink-eventLog,
 .objectLink-regexp,
 .objectLink-object,
 .objectLink-Date {
   font-weight: bold;
-  color: DarkGreen;
+  color: var(--object-color);
   white-space: pre-wrap;
 }
 
 /******************************************************************************/
 
 .objectLink-object .nodeName,
 .objectLink-NamedNodeMap .nodeName,
 .objectLink-NamedNodeMap .objectEqual,
 .objectLink-NamedNodeMap .arrayLeftBracket,
 .objectLink-NamedNodeMap .arrayRightBracket,
 .objectLink-Attr .attrEqual,
 .objectLink-Attr .attrTitle {
-  color: rgb(0, 0, 136)
+  color: var(--node-color);
 }
 
 .objectLink-object .nodeName {
   font-weight: normal;
 }
 
 /******************************************************************************/
 
@@ -128,68 +141,41 @@
   margin-left: 4px;
 }
 
 /******************************************************************************/
 /* Cycle reference*/
 
 .objectLink-Reference {
   font-weight: bold;
-  color: rgb(102, 102, 255);
+  color: var(--reference-color);
 }
 
 .objectBox-array > .objectTitle {
   font-weight: bold;
-  color: DarkGreen;
+  color: var(--object-color);
 }
 
-/******************************************************************************/
-
 .caption {
   font-weight: bold;
-  color:  #444444;
+  color:  var(--caption-color);
 }
 
 /******************************************************************************/
-/* Light Theme & Dark Theme */
-
-.theme-dark .domLabel,
-.theme-light .domLabel {
-  color: var(--theme-highlight-blue);
-}
-
-.theme-dark .objectBox-array .length,
-.theme-light .objectBox-array .length,
-.theme-dark .objectBox-number,
-.theme-light .objectBox-number {
-  color: var(--theme-highlight-green);
-}
-
-.theme-dark .objectBox-string,
-.theme-light .objectBox-string {
-  color: var(--theme-highlight-orange);
-}
+/* Themes */
 
 .theme-dark .objectBox-null,
 .theme-dark .objectBox-undefined,
 .theme-light .objectBox-null,
 .theme-light .objectBox-undefined {
   font-style: normal;
-  color: var(--theme-comment);
-}
-
-.theme-dark .objectBox-array,
-.theme-light .objectBox-array {
-  color: var(--theme-body-color);
 }
 
 .theme-dark .objectBox-object,
 .theme-light .objectBox-object {
   font-weight: normal;
-  color: var(--theme-highlight-blue);
   white-space: pre-wrap;
 }
 
 .theme-dark .caption,
 .theme-light .caption {
   font-weight: normal;
-  color: var(--theme-highlight-blue);
 }
--- a/devtools/client/shared/components/tree/tree-view.css
+++ b/devtools/client/shared/components/tree/tree-view.css
@@ -41,20 +41,16 @@
 }
 
 .treeTable .treeRow.hasChildren > .treeLabelCell > .treeLabel:hover {
   cursor: pointer;
   color: var(--tree-link-color);
   text-decoration: underline;
 }
 
-.treeTable .treeRow:hover {
-  background-color: var(--theme-body-background);
-}
-
 /* Filtering */
 .treeTable .treeRow.hidden {
   display: none;
 }
 
 /******************************************************************************/
 /* Toggle Icon */
 
@@ -77,27 +73,16 @@
 /* Spinner (used for async fetch). Needs to have higher priority than
    theme toggle icons */
 .treeTable .treeRow.hasChildren.loading > .treeLabelCell > .treeIcon {
   background-image: url(chrome://devtools/skin/images/firebug/spinner.png) !important;
   background-position: 2px 1px !important;
   background-size: 9px 9px !important;
 }
 
-/* Default toggle icon. The immediate children operator must be
- used here since there might be nested tree components inside
- a tree and we don't want to alter those. */
-.treeTable .treeRow.hasChildren > .treeLabelCell > .treeIcon {
-  background-image: url(chrome://devtools/skin/images/firebug/twisty-closed-firebug.svg);
-}
-
-.treeTable .treeRow.hasChildren.opened > .treeLabelCell > .treeIcon {
-  background-image: url(chrome://devtools/skin/images/firebug/twisty-open-firebug.svg);
-}
-
 /******************************************************************************/
 /* Header */
 
 .treeTable .treeHeaderRow {
   height: 18px;
 }
 
 .treeTable .treeHeaderCell {
@@ -144,58 +129,61 @@
           transparent 80%);
 }
 
 /******************************************************************************/
 /* Themes */
 
 /* Light Theme: toggle icon */
 .theme-light .treeTable .treeRow.hasChildren > .treeLabelCell > .treeIcon {
-  background-image: url("chrome://devtools/skin/images/controls.png");
+  background-image: url(chrome://devtools/skin/images/controls.png);
   background-size: 56px 28px;
   background-position: 0 -14px;
 }
 
 .theme-light .treeTable .treeRow.hasChildren.opened > .treeLabelCell > .treeIcon {
-  background-image: url("chrome://devtools/skin/images/controls.png");
+  background-image: url(chrome://devtools/skin/images/controls.png);
   background-size: 56px 28px;
   background-position: -14px -14px;
 }
 
 /* Dark Theme: toggle icon */
 .theme-dark .treeTable .treeRow.hasChildren > .treeLabelCell > .treeIcon {
-  background-image: url("chrome://devtools/skin/images/controls.png");
+  background-image: url(chrome://devtools/skin/images/controls.png);
   background-size: 56px 28px;
   background-position: -28px -14px;
 }
 
 .theme-dark .treeTable .treeRow.hasChildren.opened > .treeLabelCell > .treeIcon {
-  background-image: url("chrome://devtools/skin/images/controls.png");
+  background-image: url(chrome://devtools/skin/images/controls.png);
   background-size: 56px 28px;
   background-position: -42px -14px;
 }
 
-.theme-dark .treeTable .treeRow:hover {
-  background-color: var(--theme-selection-background-semitransparent);
+/* Dark and Light Themes: Support for retina displays */
+@media (min-resolution: 1.1dppx) {
+  .theme-dark .treeTable .treeRow.hasChildren > .treeLabelCell > .treeIcon,
+  .theme-light .treeTable .treeRow.hasChildren > .treeLabelCell > .treeIcon {
+    background-image: url("chrome://devtools/skin/images/controls@2x.png");
+  }
 }
 
-/* Dark and Light Themes: colors */
 .theme-light .treeTable .treeRow:hover,
 .theme-dark .treeTable .treeRow:hover {
   background-color: var(--theme-selection-background) !important;
 }
 
+.theme-firebug .treeTable .treeRow:hover {
+  background-color: var(--theme-body-background);
+}
+
 .theme-light .treeTable .treeLabel,
 .theme-dark .treeTable .treeLabel {
   color: var(--theme-highlight-pink);
 }
 
 .theme-light .treeTable .treeRow.hasChildren > .treeLabelCell > .treeLabel:hover {
   color: var(--theme-highlight-pink);
 }
 
-/* Dark and Light Themes: Support for retina displays */
-@media (min-resolution: 1.1dppx) {
-.theme-dark .treeTable .treeRow.hasChildren > .treeLabelCell > .treeIcon,
-.theme-light .treeTable .treeRow.hasChildren > .treeLabelCell > .treeIcon {
-  background-image: url("chrome://devtools/skin/images/controls@2x.png");
+.theme-firebug .treeTable .treeLabel {
+  color: var(--theme-body-color);
 }
-}
--- a/devtools/client/sourceeditor/codemirror/README
+++ b/devtools/client/sourceeditor/codemirror/README
@@ -1,16 +1,16 @@
 This is the CodeMirror editor packaged for the Mozilla Project. CodeMirror
 is a JavaScript component that provides a code editor in the browser. When
 a mode is available for the language you are coding in, it will color your
 code, and optionally help with indentation.
 
 # Upgrade
 
-Currently used version is 5.13.0. To upgrade, download a new version of
+Currently used version is 5.13.2. To upgrade, download a new version of
 CodeMirror from the project's page [1] and replace all JavaScript and
 CSS files inside the codemirror directory [2].
 
 To confirm the functionality run mochitests for the following components:
 
  * sourceeditor
  * scratchpad
  * debugger
old mode 100755
new mode 100644
old mode 100755
new mode 100644
old mode 100755
new mode 100644
old mode 100755
new mode 100644
old mode 100755
new mode 100644
old mode 100755
new mode 100644
old mode 100755
new mode 100644
old mode 100755
new mode 100644
old mode 100755
new mode 100644
old mode 100755
new mode 100644
old mode 100755
new mode 100644
old mode 100755
new mode 100644
old mode 100755
new mode 100644
old mode 100755
new mode 100644
old mode 100755
new mode 100644
old mode 100755
new mode 100644
old mode 100755
new mode 100644
old mode 100755
new mode 100644
old mode 100755
new mode 100644
old mode 100755
new mode 100644
old mode 100755
new mode 100644
old mode 100755
new mode 100644
old mode 100755
new mode 100644
old mode 100755
new mode 100644
old mode 100755
new mode 100644
old mode 100755
new mode 100644
old mode 100755
new mode 100644
old mode 100755
new mode 100644
old mode 100755
new mode 100644
--- a/devtools/client/sourceeditor/codemirror/lib/codemirror.css
+++ b/devtools/client/sourceeditor/codemirror/lib/codemirror.css
@@ -186,16 +186,17 @@ div.CodeMirror span.CodeMirror-nonmatchi
   right: 0; bottom: 0;
 }
 .CodeMirror-gutter-filler {
   left: 0; bottom: 0;
 }
 
 .CodeMirror-gutters {
   position: absolute; left: 0; top: 0;
+  min-height: 100%;
   z-index: 3;
 }
 .CodeMirror-gutter {
   white-space: normal;
   height: 100%;
   display: inline-block;
   vertical-align: top;
   margin-bottom: -30px;
@@ -240,16 +241,18 @@ div.CodeMirror span.CodeMirror-nonmatchi
   white-space: pre;
   word-wrap: normal;
   line-height: inherit;
   color: inherit;
   z-index: 2;
   position: relative;
   overflow: visible;
   -webkit-tap-highlight-color: transparent;
+  -webkit-font-variant-ligatures: none;
+  font-variant-ligatures: none;
 }
 .CodeMirror-wrap pre {
   word-wrap: break-word;
   white-space: pre-wrap;
   word-break: normal;
 }
 
 .CodeMirror-linebackground {
old mode 100755
new mode 100644
--- a/devtools/client/sourceeditor/codemirror/lib/codemirror.js
+++ b/devtools/client/sourceeditor/codemirror/lib/codemirror.js
@@ -758,48 +758,44 @@
         update.visible = visibleLines(cm.display, cm.doc, viewport);
         if (update.visible.from >= cm.display.viewFrom && update.visible.to <= cm.display.viewTo)
           break;
       }
       if (!updateDisplayIfNeeded(cm, update)) break;
       updateHeightsInViewport(cm);
       var barMeasure = measureForScrollbars(cm);
       updateSelection(cm);
+      updateScrollbars(cm, barMeasure);
       setDocumentHeight(cm, barMeasure);
-      updateScrollbars(cm, barMeasure);
-    }
-
-    if (parseInt(cm.display.gutters.style.height) > cm.display.scroller.clientHeight)
-      cm.display.gutters.style.height = cm.display.scroller.clientHeight + "px"
+    }
 
     update.signal(cm, "update", cm);
     if (cm.display.viewFrom != cm.display.reportedViewFrom || cm.display.viewTo != cm.display.reportedViewTo) {
       update.signal(cm, "viewportChange", cm, cm.display.viewFrom, cm.display.viewTo);
       cm.display.reportedViewFrom = cm.display.viewFrom; cm.display.reportedViewTo = cm.display.viewTo;
     }
   }
 
   function updateDisplaySimple(cm, viewport) {
     var update = new DisplayUpdate(cm, viewport);
     if (updateDisplayIfNeeded(cm, update)) {
       updateHeightsInViewport(cm);
       postUpdateDisplay(cm, update);
       var barMeasure = measureForScrollbars(cm);
       updateSelection(cm);
+      updateScrollbars(cm, barMeasure);
       setDocumentHeight(cm, barMeasure);
-      updateScrollbars(cm, barMeasure);
       update.finish();
     }
   }
 
   function setDocumentHeight(cm, measure) {
     cm.display.sizer.style.minHeight = measure.docHeight + "px";
     cm.display.heightForcer.style.top = measure.docHeight + "px";
-    cm.display.gutters.style.height = Math.max(measure.docHeight + cm.display.barHeight + scrollGap(cm),
-                                               measure.clientHeight) + "px";
+    cm.display.gutters.style.height = (measure.docHeight + cm.display.barHeight + scrollGap(cm)) + "px";
   }
 
   // Read the actual heights of the rendered lines, and update their
   // stored heights to match.
   function updateHeightsInViewport(cm) {
     var display = cm.display;
     var prevBottom = display.lineDiv.offsetTop;
     for (var i = 0; i < display.view.length; i++) {
@@ -3114,20 +3110,20 @@
       cm.display.sizer.style.minWidth = op.adjustWidthTo + "px";
       if (op.maxScrollLeft < cm.doc.scrollLeft)
         setScrollLeft(cm, Math.min(cm.display.scroller.scrollLeft, op.maxScrollLeft), true);
       cm.display.maxLineChanged = false;
     }
 
     if (op.preparedSelection)
       cm.display.input.showSelection(op.preparedSelection);
+    if (op.updatedDisplay || op.startHeight != cm.doc.height)
+      updateScrollbars(cm, op.barMeasure);
     if (op.updatedDisplay)
       setDocumentHeight(cm, op.barMeasure);
-    if (op.updatedDisplay || op.startHeight != cm.doc.height)
-      updateScrollbars(cm, op.barMeasure);
 
     if (op.selectionChanged) restartBlink(cm);
 
     if (cm.state.focused && op.updateInput)
       cm.display.input.reset(op.typing);
     if (op.focus && op.focus == activeElt() && (!document.hasFocus || document.hasFocus()))
       ensureFocus(op.cm);
   }
@@ -8888,12 +8884,12 @@
         order.push(new BidiSpan(order[0].level, len, len));
 
       return order;
     };
   })();
 
   // THE END
 
-  CodeMirror.version = "5.13.0";
+  CodeMirror.version = "5.13.2";
 
   return CodeMirror;
 });
old mode 100755
new mode 100644
old mode 100755
new mode 100644
old mode 100755
new mode 100644
old mode 100755
new mode 100644
old mode 100755
new mode 100644
old mode 100755
new mode 100644
old mode 100755
new mode 100644
old mode 100755
new mode 100644
old mode 100755
new mode 100644
old mode 100755
new mode 100644
old mode 100755
new mode 100644
old mode 100755
new mode 100644
old mode 100755
new mode 100644
old mode 100755
new mode 100644
old mode 100755
new mode 100644
--- a/devtools/client/sourceeditor/test/codemirror/test.js
+++ b/devtools/client/sourceeditor/test/codemirror/test.js
@@ -1481,34 +1481,34 @@ testCM("lineWidgetChanged", function(cm)
     // If the widget is measured at a width much narrower than it is displayed at, the underHalf children will span two lines and break the test.
     // If the widget is measured at a width much wider than it is displayed at, the overHalf children will combine and break the test.
     // Note that this test only checks widgets where coverGutter is true, because these require extra styling to get the width right.
     // It may also be worthwhile to check this for non-coverGutter widgets.
     // Visually:
     // Good:
     // | ------------- display width ------------- |
     // | ------- widget-width when measured ------ |
-    // | | -- under-half -- | | -- under-half -- | |
+    // | | -- under-half -- | | -- under-half -- | | 
     // | | --- over-half --- |                     |
     // | | --- over-half --- |                     |
     // Height: measured as 3 lines, same as it will be when actually displayed
 
     // Bad (too narrow):
     // | ------------- display width ------------- |
     // | ------ widget-width when measured ----- |  < -- uh oh
     // | | -- under-half -- |                    |
     // | | -- under-half -- |                    |  < -- when measured, shoved to next line
     // | | --- over-half --- |                   |
     // | | --- over-half --- |                   |
     // Height: measured as 4 lines, more than expected . Will be displayed as 3 lines!
 
     // Bad (too wide):
     // | ------------- display width ------------- |
     // | -------- widget-width when measured ------- | < -- uh oh
-    // | | -- under-half -- | | -- under-half -- |   |
+    // | | -- under-half -- | | -- under-half -- |   | 
     // | | --- over-half --- | | --- over-half --- | | < -- when measured, combined on one line
     // Height: measured as 2 lines, less than expected. Will be displayed as 3 lines!
 
     var barelyUnderHalfWidthHtml = '<div style="display: inline-block; height: 1px; width: '+(285 - halfScrollbarWidth)+'px;"></div>';
     var barelyOverHalfWidthHtml = '<div style="display: inline-block; height: 1px; width: '+(305 - halfScrollbarWidth)+'px;"></div>';
     node.innerHTML = new Array(3).join(barelyUnderHalfWidthHtml) + new Array(3).join(barelyOverHalfWidthHtml);
     node.style.cssText = "background: yellow;font-size:0;line-height: " + (expectedWidgetHeight/expectedLinesInWidget) + "px;";
     return node;
--- a/devtools/client/storage/test/browser.ini
+++ b/devtools/client/storage/test/browser.ini
@@ -15,16 +15,17 @@ support-files =
   storage-unsecured-iframe.html
   storage-updates.html
   head.js
   !/devtools/client/framework/test/shared-head.js
 
 [browser_storage_basic.js]
 [browser_storage_cache_error.js]
 [browser_storage_cookies_delete_all.js]
+[browser_storage_cookies_domain.js]
 [browser_storage_cookies_edit.js]
 [browser_storage_cookies_edit_keyboard.js]
 [browser_storage_cookies_tab_navigation.js]
 [browser_storage_delete.js]
 [browser_storage_delete_all.js]
 [browser_storage_delete_tree.js]
 [browser_storage_dynamic_updates.js]
 [browser_storage_empty_objectstores.js]
new file mode 100644
--- /dev/null
+++ b/devtools/client/storage/test/browser_storage_cookies_domain.js
@@ -0,0 +1,21 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/* import-globals-from ../../framework/test/shared-head.js */
+
+"use strict";
+
+// Test that cookies with domain equal to full host name are listed.
+// E.g., ".example.org" vs. example.org). Bug 1149497.
+
+add_task(function* () {
+  yield openTabAndSetupStorage(MAIN_DOMAIN + "storage-cookies.html");
+
+  yield checkState([
+    [["cookies", "test1.example.org"],
+      ["test1", "test2", "test3", "test4", "test5"]],
+  ]);
+
+  yield finishTests();
+});
--- a/devtools/client/storage/test/storage-cookies.html
+++ b/devtools/client/storage/test/storage-cookies.html
@@ -5,21 +5,20 @@
   -->
   <head>
     <meta charset="utf-8">
     <title>Storage inspector cookie test</title>
   </head>
   <body>
     <script type="application/javascript;version=1.7">
     "use strict";
-    let partialHostname = location.hostname.match(/^[^.]+(\..*)$/)[1];
     let expiresIn24Hours = new Date(Date.now() + 60 * 60 * 24 * 1000).toUTCString();
     for (let i = 1; i <= 5; i++) {
-    let cookieString = "test" + i + "=value" + i +
-    ";expires=" + expiresIn24Hours + ";path=/browser";
-    if (i % 2) {
-    cookieString += ";domain=.example.org";
-    }
-    document.cookie = cookieString;
+      let cookieString = "test" + i + "=value" + i +
+        ";expires=" + expiresIn24Hours + ";path=/browser";
+      if (i % 2) {
+        cookieString += ";domain=test1.example.org";
+      }
+      document.cookie = cookieString;
     }
     </script>
   </body>
 </html>
--- a/devtools/client/styleeditor/test/browser_styleeditor_filesave.js
+++ b/devtools/client/styleeditor/test/browser_styleeditor_filesave.js
@@ -3,19 +3,16 @@
    http://creativecommons.org/publicdomain/zero/1.0/ */
 "use strict";
 
 // Test that 'Save' function works.
 
 const TESTCASE_URI_HTML = TEST_BASE_HTTP + "simple.html";
 const TESTCASE_URI_CSS = TEST_BASE_HTTP + "simple.css";
 
-var Cc = Components.classes;
-var Ci = Components.interfaces;
-
 var tempScope = {};
 Components.utils.import("resource://gre/modules/FileUtils.jsm", tempScope);
 Components.utils.import("resource://gre/modules/NetUtil.jsm", tempScope);
 var FileUtils = tempScope.FileUtils;
 var NetUtil = tempScope.NetUtil;
 
 add_task(function* () {
   let htmlFile = yield copy(TESTCASE_URI_HTML, "simple.html");
--- a/devtools/client/styleeditor/test/browser_styleeditor_media_sidebar_links.js
+++ b/devtools/client/styleeditor/test/browser_styleeditor_media_sidebar_links.js
@@ -41,17 +41,17 @@ function testNumberOfLinks(editor) {
   is(conditions[3].querySelectorAll(responsiveModeToggleClass).length, 2,
        "There should be 2 responsive mode links in the media rule");
 }
 
 function* testMediaLink(editor, tab, ui, itemIndex, type, value) {
   let sidebar = editor.details.querySelector(".stylesheet-sidebar");
   let conditions = sidebar.querySelectorAll(".media-rule-condition");
 
-  let onMediaChange = once("media-list-changed", ui);
+  let onMediaChange = once(ui, "media-list-changed");
   let onContentResize = waitForResizeTo(ResponsiveUIManager, type, value);
 
   info("Launching responsive mode");
   conditions[itemIndex].querySelector(responsiveModeToggleClass).click();
 
   ResponsiveUIManager.getResponsiveUIForTab(tab).transitionsEnabled = false;
 
   info("Waiting for the @media list to update");
@@ -66,18 +66,18 @@ function* testMediaLink(editor, tab, ui,
 
   let dimension = (yield getSizing())[type];
   is(dimension, value, `${type} should be properly set.`);
 }
 
 function* closeRDM(tab, ui) {
   info("Closing responsive mode");
   ResponsiveUIManager.toggle(window, tab);
-  let onMediaChange = once("media-list-changed", ui);
-  yield once("off", ResponsiveUIManager);
+  let onMediaChange = once(ui, "media-list-changed");
+  yield once(ResponsiveUIManager, "off");
   yield onMediaChange;
   ok(!ResponsiveUIManager.isActiveForTab(tab),
      "Responsive mode should no longer be active.");
 }
 
 function doFinalChecks(editor) {
   let sidebar = editor.details.querySelector(".stylesheet-sidebar");
   let conditions = sidebar.querySelectorAll(".media-rule-condition");
@@ -110,24 +110,16 @@ function* getSizing() {
     return {
       width: content.innerWidth,
       height: content.innerHeight
     };
   });
   return sizing;
 }
 
-function once(event, target) {
-  let deferred = promise.defer();
-  target.once(event, () => {
-    deferred.resolve();
-  });
-  return deferred.promise;
-}
-
 function openEditor(editor) {
   getLinkFor(editor).click();
 
   return editor.getSourceEditor();
 }
 
 function getLinkFor(editor) {
   return editor.summary.querySelector(".stylesheet-name");
--- a/devtools/client/styleeditor/test/browser_styleeditor_sourcemap_watching.js
+++ b/devtools/client/styleeditor/test/browser_styleeditor_sourcemap_watching.js
@@ -10,19 +10,16 @@ const TESTCASE_URI_REG_CSS = TEST_BASE_H
 const TESTCASE_URI_SCSS = TEST_BASE_HTTP + "sourcemap-sass/sourcemaps.scss";
 const TESTCASE_URI_MAP = TEST_BASE_HTTP + "sourcemap-css/sourcemaps.css.map";
 const TESTCASE_SCSS_NAME = "sourcemaps.scss";
 
 const TRANSITIONS_PREF = "devtools.styleeditor.transitions";
 
 const CSS_TEXT = "* { color: blue }";
 
-const Cc = Components.classes;
-const Ci = Components.interfaces;
-
 const {FileUtils} = Components.utils.import("resource://gre/modules/FileUtils.jsm", {});
 const {NetUtil} = Components.utils.import("resource://gre/modules/NetUtil.jsm", {});
 
 add_task(function* () {
   yield new Promise(resolve => {
     SpecialPowers.pushPrefEnv({"set": [
       [TRANSITIONS_PREF, false]
     ]}, resolve);
--- a/devtools/client/styleeditor/test/browser_styleeditor_sync.js
+++ b/devtools/client/styleeditor/test/browser_styleeditor_sync.js
@@ -1,19 +1,16 @@
 /* vim: set ts=2 et sw=2 tw=80: */
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 "use strict";
 
 // Test that changes in the style inspector are synchronized into the
 // style editor.
 
-/* import-globals-from ../../inspector/shared/test/head.js */
-Services.scriptloader.loadSubScript("chrome://mochitests/content/browser/devtools/client/inspector/shared/test/head.js", this);
-
 const TESTCASE_URI = TEST_BASE_HTTP + "sync.html";
 
 const expectedText = `
   body {
     border-width: 15px;
     /*! color: red; */
   }
 
--- a/devtools/client/styleeditor/test/browser_styleeditor_syncAddRule.js
+++ b/devtools/client/styleeditor/test/browser_styleeditor_syncAddRule.js
@@ -1,18 +1,15 @@
 /* vim: set ts=2 et sw=2 tw=80: */
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 "use strict";
 
 // Test that adding a new rule is synced to the style editor.
 
-/* import-globals-from ../../inspector/shared/test/head.js */
-Services.scriptloader.loadSubScript("chrome://mochitests/content/browser/devtools/client/inspector/shared/test/head.js", this);
-
 const TESTCASE_URI = TEST_BASE_HTTP + "sync.html";
 
 const expectedText = `
 #testid {
 }`;
 
 add_task(function* () {
   yield addTab(TESTCASE_URI);
--- a/devtools/client/styleeditor/test/browser_styleeditor_syncAlreadyOpen.js
+++ b/devtools/client/styleeditor/test/browser_styleeditor_syncAlreadyOpen.js
@@ -1,46 +1,43 @@
 /* vim: set ts=2 et sw=2 tw=80: */
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 "use strict";
 
 // Test that changes in the style inspector are synchronized into the
 // style editor.
 
-/* import-globals-from ../../inspector/shared/test/head.js */
-Services.scriptloader.loadSubScript("chrome://mochitests/content/browser/devtools/client/inspector/shared/test/head.js", this);
-
 const TESTCASE_URI = TEST_BASE_HTTP + "sync.html";
 
 const expectedText = `
   body {
     border-width: 15px;
     color: red;
   }
 
   #testid {
     /*! font-size: 4em; */
   }
   `;
 
 add_task(function* () {
   yield addTab(TESTCASE_URI);
 
-  let { inspector, view } = yield openRuleView();
+  let { inspector, view, toolbox } = yield openRuleView();
 
   // In this test, make sure the style editor is open before making
   // changes in the inspector.
   let { ui } = yield openStyleEditor();
   let editor = yield ui.editors[0].getSourceEditor();
 
   let onEditorChange = promise.defer();
   editor.sourceEditor.on("change", onEditorChange.resolve);
 
-  yield openRuleView();
+  yield toolbox.getPanel("inspector");
   yield selectNode("#testid", inspector);
   let ruleEditor = getRuleViewRuleEditor(view, 1);
 
   // Disable the "font-size" property.
   let propEditor = ruleEditor.rule.textProps[0].editor;
   let onModification = view.once("ruleview-changed");
   propEditor.enable.click();
   yield onModification;
--- a/devtools/client/styleeditor/test/browser_styleeditor_syncEditSelector.js
+++ b/devtools/client/styleeditor/test/browser_styleeditor_syncEditSelector.js
@@ -1,19 +1,16 @@
 /* vim: set ts=2 et sw=2 tw=80: */
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 "use strict";
 
 // Test that changes in the style inspector are synchronized into the
 // style editor.
 
-/* import-globals-from ../../inspector/shared/test/head.js */
-Services.scriptloader.loadSubScript("chrome://mochitests/content/browser/devtools/client/inspector/shared/test/head.js", this);
-
 const TESTCASE_URI = TEST_BASE_HTTP + "sync.html";
 
 const expectedText = `
   body {
     border-width: 15px;
     color: red;
   }
 
--- a/devtools/client/styleeditor/test/browser_styleeditor_syncIntoRuleView.js
+++ b/devtools/client/styleeditor/test/browser_styleeditor_syncIntoRuleView.js
@@ -1,19 +1,16 @@
 /* vim: set ts=2 et sw=2 tw=80: */
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 "use strict";
 
 // Test that changes in the style editor are synchronized into the
 // style inspector.
 
-/* import-globals-from ../../inspector/shared/test/head.js */
-Services.scriptloader.loadSubScript("chrome://mochitests/content/browser/devtools/client/inspector/shared/test/head.js", this);
-
 const TEST_URI = `
   <style type='text/css'>
     div { background-color: seagreen; }
   </style>
   <div id='testid' class='testclass'>Styled Node</div>
 `;
 
 const TESTCASE_CSS_SOURCE = "#testid { color: chartreuse; }";
--- a/devtools/client/styleeditor/test/head.js
+++ b/devtools/client/styleeditor/test/head.js
@@ -1,53 +1,47 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 /* All top-level definitions here are exports.  */
 /* eslint no-unused-vars: [2, {"vars": "local"}] */
 
 "use strict";
 
+/* import-globals-from ../../inspector/shared/test/head.js */
+Services.scriptloader.loadSubScript(
+  "chrome://mochitests/content/browser/devtools/client/inspector/shared/test/head.js", this);
+
 const TEST_BASE = "chrome://mochitests/content/browser/devtools/client/styleeditor/test/";
 const TEST_BASE_HTTP = "http://example.com/browser/devtools/client/styleeditor/test/";
 const TEST_BASE_HTTPS = "https://example.com/browser/devtools/client/styleeditor/test/";
 const TEST_HOST = "mochi.test:8888";
 
-var {require} = Cu.import("resource://devtools/shared/Loader.jsm", {});
-var {TargetFactory} = require("devtools/client/framework/target");
-var promise = require("promise");
-var DevToolsUtils = require("devtools/shared/DevToolsUtils");
-
-DevToolsUtils.testing = true;
-SimpleTest.registerCleanupFunction(() => {
-  DevToolsUtils.testing = false;
-});
-
 /**
  * Add a new test tab in the browser and load the given url.
  * @param {String} url The url to be loaded in the new tab
  * @param {Window} win The window to add the tab to (default: current window).
  * @return a promise that resolves to the tab object when the url is loaded
  */
-function addTab(url, win) {
+var addTab = function(url, win) {
   info("Adding a new tab with URL: '" + url + "'");
   let def = promise.defer();
 
   let targetWindow = win || window;
   let targetBrowser = targetWindow.gBrowser;
 
   let tab = targetBrowser.selectedTab = targetBrowser.addTab(url);
   targetBrowser.selectedBrowser.addEventListener("load", function onload() {
     targetBrowser.selectedBrowser.removeEventListener("load", onload, true);
     info("URL '" + url + "' loading complete");
     def.resolve(tab);
   }, true);
 
   return def.promise;
-}
+};
 
 /**
  * Navigate the currently selected tab to a new URL and wait for it to load.
  * @param {String} url The url to be loaded in the current tab.
  * @return a promise that resolves when the page has fully loaded.
  */
 var navigateTo = Task.async(function* (url) {
   info(`Navigating to ${url}`);
@@ -74,25 +68,16 @@ var reloadPageAndWaitForStyleSheets = Ta
   info("Reloading the page.");
 
   let onReset = ui.once("stylesheets-reset");
   let browser = gBrowser.selectedBrowser;
   yield ContentTask.spawn(browser, null, "() => content.location.reload()");
   yield onReset;
 });
 
-registerCleanupFunction(function* () {
-  while (gBrowser.tabs.length > 1) {
-    let target = TargetFactory.forTab(gBrowser.selectedTab);
-    yield gDevTools.closeToolbox(target);
-
-    gBrowser.removeCurrentTab();
-  }
-});
-
 /**
  * Open the style editor for the current tab.
  */
 var openStyleEditor = Task.async(function* (tab) {
   if (!tab) {
     tab = gBrowser.selectedTab;
   }
   let target = TargetFactory.forTab(tab);
@@ -120,17 +105,17 @@ var openStyleEditorForURL = Task.async(f
  *
  * @param {String} selector
  *        The selector used to obtain the element.
  * @param {String} pseudo
  *        pseudo id to query, or null.
  * @param {String} name
  *        name of the property.
  */
-function* getComputedStyleProperty(args) {
+var getComputedStyleProperty = function* (args) {
   return yield ContentTask.spawn(gBrowser.selectedBrowser, args,
     function({selector, pseudo, name}) {
       let element = content.document.querySelector(selector);
       let style = content.getComputedStyle(element, pseudo);
       return style.getPropertyValue(name);
     }
   );
-}
+};
--- a/devtools/client/themes/floating-scrollbars-dark-theme.css
+++ b/devtools/client/themes/floating-scrollbars-dark-theme.css
@@ -23,17 +23,17 @@ xul|scrollbar[orient="vertical"] {
 
 xul|scrollbar[orient="horizontal"] {
   margin-top: -10px;
   min-height: 10px;
   max-height: 10px;
 }
 
 xul|scrollbar xul|thumb {
-  background-color: rgba(170,170,170,0.2) !important;
+  background-color: rgba(170, 170, 170, .2) !important; /* --toolbar-tab-hover */
   -moz-appearance: none !important;
   border-width: 0px !important;
   border-radius: 3px !important;
 }
 
 :root[platform="mac"] xul|slider {
   -moz-appearance: none !important;
 }
--- a/devtools/client/themes/toolbars.css
+++ b/devtools/client/themes/toolbars.css
@@ -1,15 +1,17 @@
 /* vim:set ts=2 sw=2 sts=2 et: */
 /* 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/. */
 
 /* CSS Variables specific to the devtools toolbar that aren't defined by the themes */
 .theme-light {
+  --toolbar-tab-hover: rgba(170, 170, 170, .2);
+  --toolbar-tab-hover-active: rgba(170, 170, 170, .4);
   --searchbox-background-color: #ffee99;
   --searchbox-border-color: #ffbf00;
   --searcbox-no-match-background-color: #ffe5e5;
   --searcbox-no-match-border-color: #e52e2e;
   --magnifying-glass-image: url(images/magnifying-glass-light.png);
   --magnifying-glass-image-2x: url(images/magnifying-glass-light@2x.png);
   --command-pick-image: url(images/command-pick.svg);
   --tool-options-image: url(images/tool-options.svg);
@@ -17,16 +19,18 @@
   --icon-filter: invert(1);
   --dock-bottom-image: url(chrome://devtools/skin/images/dock-bottom.svg);
   --dock-side-image: url(chrome://devtools/skin/images/dock-side.svg);
   --dock-undock-image: url(chrome://devtools/skin/images/dock-undock.svg);
   --toolbar-button-border-color: rgba(170, 170, 170, .5);
 }
 
 .theme-dark {
+  --toolbar-tab-hover: hsla(206, 37%, 4%, .2);
+  --toolbar-tab-hover-active: hsla(206, 37%, 4%, .4);
   --searchbox-background-color: #4d4222;
   --searchbox-border-color: #d99f2b;
   --searcbox-no-match-background-color: #402325;
   --searcbox-no-match-border-color: #cc3d3d;
   --magnifying-glass-image: url(images/magnifying-glass.png);
   --magnifying-glass-image-2x: url(images/magnifying-glass@2x.png);
   --command-pick-image: url(images/command-pick.svg);
   --tool-options-image: url(images/tool-options.svg);
@@ -266,17 +270,17 @@
   margin-inline-start: .5em !important;
   font-weight: 600;
 }
 
 /* Text-only buttons */
 .theme-light .devtools-toolbarbutton[label]:not([text-as-image]):not([type=menu-button]),
 .theme-light .devtools-toolbarbutton[data-text-only],
 .theme-light #toolbox-buttons .devtools-toolbarbutton[text-as-image] {
-  background-color: rgba(170, 170, 170, .2); /* Splitter */
+  background-color: var(--toolbar-tab-hover);
 }
 .theme-dark .devtools-toolbarbutton[label]:not([text-as-image]):not([type=menu-button]),
 .theme-dark .devtools-toolbarbutton[data-text-only],
 .theme-dark #toolbox-buttons .devtools-toolbarbutton[text-as-image] {
   background-color: rgba(0, 0, 0, .2); /* Splitter */
 }
 
 /* Text-only button states */
@@ -294,17 +298,17 @@
 .theme-dark .devtools-button:not(:empty):not([disabled]):hover:active,
 .theme-dark #toolbox-buttons .devtools-toolbarbutton:not([disabled])[text-as-image]:hover:active,
 .theme-dark .devtools-toolbarbutton:not(:-moz-any([checked=true],[disabled],[text-as-image]))[label]:hover:active {
   background: rgba(0, 0, 0, .4); /* Splitters */
 }
 .theme-light .devtools-button:not(:empty):not([disabled]):hover:active,
 .theme-light #toolbox-buttons .devtools-toolbarbutton:not([disabled])[text-as-image]:hover:active,
 .theme-light .devtools-toolbarbutton:not(:-moz-any([checked=true],[disabled],[text-as-image]))[label]:hover:active {
-  background: rgba(170, 170, 170, .4); /* Splitters */
+  background: var(--toolbar-tab-hover-active);
 }
 
 .theme-dark .devtools-toolbarbutton:not([disabled])[label][checked=true],
 .theme-dark .devtools-toolbarbutton:not([disabled])[label][open],
 .theme-dark .devtools-button:not(:empty)[checked=true],
 .theme-dark #toolbox-buttons .devtools-toolbarbutton[text-as-image][checked=true] {
   background: rgba(29, 79, 115, .7); /* Select highlight blue */
   color: var(--theme-selection-color);
@@ -724,20 +728,20 @@
   padding: 0 8px;
   margin: 0;
   width: 32px;
   position: relative;
   -moz-user-focus: normal;
 }
 
 .command-button:hover {
-  background-color: hsla(206,37%,4%,.2);
+  background-color: var(--toolbar-tab-hover);
 }
 .command-button:hover:active, .command-button[checked=true]:not(:hover) {
-  background-color: hsla(206,37%,4%,.4);
+  background-color: var(--toolbar-tab-hover-active)
 }
 
 .command-button > image {
   -moz-appearance: none;
   width: 16px;
   height: 16px;
   background-size: cover;
   background-position: 0 center;
@@ -862,31 +866,29 @@
 }
 
 .theme-light .devtools-tab {
   color: var(--theme-body-color);
   border-color: var(--theme-splitter-color);
 }
 
 .theme-dark .devtools-tab:hover {
-  background-color: hsla(206,37%,4%,.2);
   color: #ced3d9;
 }
 
-.theme-light .devtools-tab:hover {
-  background-color: rgba(170,170,170,.2);
+.devtools-tab:hover {
+  background-color: var(--toolbar-tab-hover);
 }
 
 .theme-dark .devtools-tab:hover:active {
-  background-color: hsla(206,37%,4%,.4);
   color: var(--theme-selection-color);
 }
 
-.theme-light .devtools-tab:hover:active {
-  background-color: rgba(170,170,170,.4);
+.devtools-tab:hover:active {
+  background-color: var(--toolbar-tab-hover-active);
 }
 
 .devtools-tab:not([selected])[highlighted] {
   box-shadow: 0 2px 0 var(--theme-highlight-green) inset;
 }
 
 .theme-dark .devtools-tab:not([selected])[highlighted] {
   background-color: hsla(99,100%,14%,.2);
--- a/devtools/server/actors/storage.js
+++ b/devtools/server/actors/storage.js
@@ -526,17 +526,17 @@ StorageActors.createActor({
    * Given a cookie object and a host, figure out if the cookie is valid for
    * that host.
    */
   isCookieAtHost: function(cookie, host) {
     if (cookie.host == null) {
       return host == null;
     }
     if (cookie.host.startsWith(".")) {
-      return host.endsWith(cookie.host);
+      return ("." + host).endsWith(cookie.host);
     }
     if (cookie.host === "") {
       return host.startsWith("file://" + cookie.path);
     }
     return cookie.host == host;
   },
 
   toStoreObject: function(cookie) {
@@ -897,17 +897,17 @@ var cookieHelpers = {
   },
 
   _removeCookies: function(host, opts = {}) {
     function hostMatches(cookieHost, matchHost) {
       if (cookieHost == null) {
         return matchHost == null;
       }
       if (cookieHost.startsWith(".")) {
-        return matchHost.endsWith(cookieHost);
+        return ("." + matchHost).endsWith(cookieHost);
       }
       return cookieHost == host;
     }
 
     let enumerator = Services.cookies.getCookiesFromHost(host);
     while (enumerator.hasMoreElements()) {
       let cookie = enumerator.getNext().QueryInterface(Ci.nsICookie2);
       if (hostMatches(cookie.host, host) &&
--- a/devtools/server/actors/webbrowser.js
+++ b/devtools/server/actors/webbrowser.js
@@ -23,16 +23,17 @@ Cu.import("resource://gre/modules/XPCOMU
 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, "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,
 // by calling the listeners in the order or registration.
 loader.lazyRequireGetter(this, "events", "sdk/event/core");
 
 loader.lazyRequireGetter(this, "StyleSheetActor", "devtools/server/actors/stylesheets", true);
 
@@ -713,17 +714,19 @@ function TabActor(aConnection)
   this._sources = null;
 
   // Map of DOM stylesheets to StyleSheetActors
   this._styleSheetActors = new Map();
 
   this._shouldAddNewGlobalAsDebuggee = this._shouldAddNewGlobalAsDebuggee.bind(this);
 
   this.makeDebugger = makeDebugger.bind(null, {
-    findDebuggees: () => this.windows,
+    findDebuggees: () => {
+      return this.windows.concat(this.webextensionsContentScriptGlobals);
+    },
     shouldAddNewGlobalAsDebuggee: this._shouldAddNewGlobalAsDebuggee
   });
 
   // Flag eventually overloaded by sub classes in order to watch new docshells
   // Used on b2g to catch activity frames and in chrome to list all frames
   this.listenForNewDocShells = Services.appinfo.processType == Services.appinfo.PROCESS_TYPE_CONTENT;
 
   this.traits = {
@@ -801,16 +804,29 @@ TabActor.prototype = {
       return this.docShell
         .QueryInterface(Ci.nsIInterfaceRequestor)
         .getInterface(Ci.nsIDOMWindow);
     }
     return null;
   },
 
   /**
+   * Getter for the WebExtensions ContentScript globals related to the
+   * current tab content's DOM window.
+   */
+  get webextensionsContentScriptGlobals() {
+    // Ignore xpcshell runtime which spawn TabActors without a window.
+    if (this.window) {
+      return ExtensionContent.getContentScriptGlobalsForWindow(this.window);
+    }
+
+    return [];
+  },
+
+  /**
    * Getter for the list of all content DOM windows in this tabActor
    * @return {Array}
    */
   get windows() {
     return this.docShells.map(docShell => {
       return docShell.QueryInterface(Ci.nsIInterfaceRequestor)
                      .getInterface(Ci.nsIDOMWindow);
     });
--- a/devtools/shared/gcli/commands/cookie.js
+++ b/devtools/shared/gcli/commands/cookie.js
@@ -54,17 +54,20 @@ function translateExpires(expires) {
 /**
  * Check if a given cookie matches a given host
  */
 function isCookieAtHost(cookie, host) {
   if (cookie.host == null) {
     return host == null;
   }
   if (cookie.host.startsWith(".")) {
-    return host.endsWith(cookie.host);
+    return ("." + host).endsWith(cookie.host);
+  }
+  if (cookie.host === "") {
+    return host.startsWith("file://" + cookie.path);
   }
   return cookie.host == host;
 }
 
 exports.items = [
   {
     name: "cookie",
     description: l10n.lookup("cookieDesc"),
--- a/dom/webidl/AddonManager.webidl
+++ b/dom/webidl/AddonManager.webidl
@@ -1,15 +1,16 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this file,
  * You can obtain one at http://mozilla.org/MPL/2.0/.
  */
 
-/* We need a JSImplementation but cannot get one without a contract ID. Since
-   This object is only ever created from JS we don't need a real contract ID. */
+/* We need a JSImplementation but cannot get one without a contract ID.
+   Since Addon and AddonInstall are only ever created from JS they don't need
+   real contract IDs. */
 [ChromeOnly, JSImplementation="dummy"]
 interface Addon {
   // The add-on's ID.
   readonly attribute DOMString id;
   // The add-on's version.
   readonly attribute DOMString version;
   // The add-on's type (extension, theme, etc.).
   readonly attribute DOMString type;
@@ -18,24 +19,54 @@ interface Addon {
   // The add-on's description in the current locale.
   readonly attribute DOMString description;
   // If the user has enabled this add-on, note that it still may not be running
   // depending on whether enabling requires a restart or if the add-on is
   // incompatible in some way.
   readonly attribute boolean isEnabled;
   // If the add-on is currently active in the browser.
   readonly attribute boolean isActive;
+
+  Promise<boolean> uninstall();
+};
+
+[ChromeOnly, JSImplementation="dummy"]
+interface AddonInstall : EventTarget {
+  // One of the STATE_* symbols from AddonManager.jsm
+  readonly attribute DOMString state;
+  // One of the ERROR_* symbols from AddonManager.jsm, or null
+  readonly attribute DOMString? error;
+  // How many bytes have been downloaded
+  readonly attribute long long progress;
+  // How many total bytes will need to be downloaded or -1 if unknown
+  readonly attribute long long maxProgress;
+
+  Promise<void> install();
+  Promise<void> cancel();
+};
+
+dictionary addonInstallOptions {
+  required DOMString url;
 };
 
 [HeaderFile="mozilla/AddonManagerWebAPI.h",
  Func="mozilla::AddonManagerWebAPI::IsAPIEnabled",
  NavigatorProperty="mozAddonManager",
  JSImplementation="@mozilla.org/addon-web-api/manager;1"]
 interface AddonManager {
   /**
    * Gets information about an add-on
    *
    * @param  id
    *         The ID of the add-on to test for.
    * @return A promise. It will resolve to an Addon if the add-on is installed.
    */
   Promise<Addon> getAddonByID(DOMString id);
+
+  /**
+   * Creates an AddonInstall object for a given URL.
+   *
+   * @param options
+   *        Only one supported option: 'url', the URL of the addon to install.
+   * @return A promise that resolves to an instance of AddonInstall.
+   */
+  Promise<AddonInstall> createInstall(optional addonInstallOptions options);
 };
--- a/layout/base/AccessibleCaretManager.cpp
+++ b/layout/base/AccessibleCaretManager.cpp
@@ -853,27 +853,34 @@ AccessibleCaretManager::SetSelectionDrag
 void
 AccessibleCaretManager::SelectMoreIfPhoneNumber() const
 {
   SetSelectionDirection(eDirNext);
   ExtendPhoneNumberSelection(NS_LITERAL_STRING("forward"));
 
   SetSelectionDirection(eDirPrevious);
   ExtendPhoneNumberSelection(NS_LITERAL_STRING("backward"));
+
+  SetSelectionDirection(eDirNext);
 }
 
 void
 AccessibleCaretManager::ExtendPhoneNumberSelection(const nsAString& aDirection) const
 {
   nsIDocument* doc = mPresShell->GetDocument();
 
   // Extend the phone number selection until we find a boundary.
   Selection* selection = GetSelection();
 
   while (selection) {
+    // Backup the anchor focus range since both anchor node and focus node might
+    // be changed after calling Selection::Modify().
+    RefPtr<nsRange> oldAnchorFocusRange =
+      selection->GetAnchorFocusRange()->CloneRange();
+
     // Save current Focus position, and extend the selection one char.
     nsINode* focusNode = selection->GetFocusNode();
     uint32_t focusOffset = selection->FocusOffset();
     selection->Modify(NS_LITERAL_STRING("extend"),
                       aDirection,
                       NS_LITERAL_STRING("character"));
 
     // If the selection didn't change, (can't extend further), we're done.
@@ -883,20 +890,19 @@ AccessibleCaretManager::ExtendPhoneNumbe
     }
 
     // If the changed selection isn't a valid phone number, we're done.
     nsAutoString selectedText;
     selection->Stringify(selectedText);
     nsAutoString phoneRegex(NS_LITERAL_STRING("(^\\+)?[0-9\\s,\\-.()*#pw]{1,30}$"));
 
     if (!nsContentUtils::IsPatternMatching(selectedText, phoneRegex, doc)) {
-      // Backout the undesired selection extend, (collapse to original
-      // Anchor, extend to original Focus), before exit.
-      selection->Collapse(selection->GetAnchorNode(), selection->AnchorOffset());
-      selection->Extend(focusNode, focusOffset);
+      // Backout the undesired selection extend, restore the old anchor focus
+      // range before exit.
+      selection->SetAnchorFocusToRange(oldAnchorFocusRange);
       return;
     }
   }
 }
 
 void
 AccessibleCaretManager::SetSelectionDirection(nsDirection aDir) const
 {
--- a/mobile/android/app/mobile.js
+++ b/mobile/android/app/mobile.js
@@ -240,16 +240,22 @@ pref("extensions.blocklist.url", "https:
 pref("extensions.blocklist.detailsURL", "https://www.mozilla.com/%LOCALE%/blocklist/");
 
 // Kinto blocklist preferences
 pref("services.kinto.base", "https://firefox.settings.services.mozilla.com/v1");
 pref("services.kinto.changes.path", "/buckets/monitor/collections/changes/records");
 pref("services.kinto.bucket", "blocklists");
 pref("services.kinto.onecrl.collection", "certificates");
 pref("services.kinto.onecrl.checked", 0);
+pref("services.kinto.addons.collection", "addons");
+pref("services.kinto.addons.checked", 0);
+pref("services.kinto.plugins.collection", "plugins");
+pref("services.kinto.plugins.checked", 0);
+pref("services.kinto.gfx.collection", "gfx");
+pref("services.kinto.gfx.checked", 0);
 
 // for now, let's keep kinto update out of the release channel (pending
 // collection signatures)
 #ifdef RELEASE_BUILD
 pref("services.kinto.update_enabled", false);
 #else
 pref("services.kinto.update_enabled", true);
 #endif
--- a/mobile/android/base/java/org/mozilla/gecko/ActionBarTextSelection.java
+++ b/mobile/android/base/java/org/mozilla/gecko/ActionBarTextSelection.java
@@ -5,16 +5,17 @@
 package org.mozilla.gecko;
 
 import org.mozilla.gecko.gfx.BitmapUtils;
 import org.mozilla.gecko.gfx.BitmapUtils.BitmapLoader;
 import org.mozilla.gecko.gfx.ImmutableViewportMetrics;
 import org.mozilla.gecko.gfx.Layer;
 import org.mozilla.gecko.gfx.LayerView;
 import org.mozilla.gecko.gfx.LayerView.DrawListener;
+import org.mozilla.gecko.menu.GeckoMenu;
 import org.mozilla.gecko.menu.GeckoMenuItem;
 import org.mozilla.gecko.text.TextSelection;
 import org.mozilla.gecko.util.FloatUtils;
 import org.mozilla.gecko.util.GeckoEventListener;
 import org.mozilla.gecko.util.ThreadUtils;
 import org.mozilla.gecko.ActionModeCompat.Callback;
 import org.mozilla.gecko.AppConstants.Versions;
 
@@ -244,16 +245,17 @@ class ActionBarTextSelection extends Lay
             return;
         }
 
         final Context context = anchorHandle.getContext();
         if (context instanceof ActionModeCompat.Presenter) {
             final ActionModeCompat.Presenter presenter = (ActionModeCompat.Presenter) context;
             mCallback = new TextSelectionActionModeCallback(items);
             presenter.startActionModeCompat(mCallback);
+            mCallback.animateIn();
         }
     }
 
     private void endActionMode() {
         Context context = anchorHandle.getContext();
         if (context instanceof ActionModeCompat.Presenter) {
             final ActionModeCompat.Presenter presenter = (ActionModeCompat.Presenter) context;
             presenter.endActionModeCompat();
@@ -316,18 +318,24 @@ class ActionBarTextSelection extends Lay
 
         public void updateItems(JSONArray items) {
             mItems = items;
             if (mActionMode != null) {
                 mActionMode.invalidate();
             }
         }
 
+        public void animateIn() {
+            if (mActionMode != null) {
+                mActionMode.animateIn();
+            }
+        }
+
         @Override
-        public boolean onPrepareActionMode(final ActionModeCompat mode, final Menu menu) {
+        public boolean onPrepareActionMode(final ActionModeCompat mode, final GeckoMenu menu) {
             // Android would normally expect us to only update the state of menu items here
             // To make the js-java interaction a bit simpler, we just wipe out the menu here and recreate all
             // the javascript menu items in onPrepare instead. This will be called any time invalidate() is called on the
             // action mode.
             menu.clear();
 
             int length = mItems.length();
             for (int i = 0; i < length; i++) {
@@ -349,17 +357,17 @@ class ActionBarTextSelection extends Lay
                 } catch (Exception ex) {
                     Log.i(LOGTAG, "Exception building menu", ex);
                 }
             }
             return true;
         }
 
         @Override
-        public boolean onCreateActionMode(ActionModeCompat mode, Menu menu) {
+        public boolean onCreateActionMode(ActionModeCompat mode, GeckoMenu unused) {
             mActionMode = mode;
             return true;
         }
 
         @Override
         public boolean onActionItemClicked(ActionModeCompat mode, MenuItem item) {
             try {
                 final JSONObject obj = mItems.getJSONObject(item.getItemId());
--- a/mobile/android/base/java/org/mozilla/gecko/ActionModeCompat.java
+++ b/mobile/android/base/java/org/mozilla/gecko/ActionModeCompat.java
@@ -1,16 +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/. */
 
 package org.mozilla.gecko;
 
+import org.mozilla.gecko.menu.GeckoMenu;
+import org.mozilla.gecko.menu.GeckoMenuItem;
 import org.mozilla.gecko.widget.GeckoPopupMenu;
-import org.mozilla.gecko.menu.GeckoMenuItem;
 
 import android.view.Gravity;
 import android.view.Menu;
 import android.view.MenuItem;
 import android.view.View;
 import android.widget.Toast;
 
 class ActionModeCompat implements GeckoPopupMenu.OnMenuItemClickListener,
@@ -21,21 +22,21 @@ class ActionModeCompat implements GeckoP
     private final Callback mCallback;
     private final ActionModeCompatView mView;
     private final Presenter mPresenter;
 
     /* A set of callbacks to be called during this ActionMode's lifecycle. These will control the
      * creation, interaction with, and destruction of menuitems for the view */
     public static interface Callback {
         /* Called when action mode is first created. Implementors should use this to inflate menu resources. */
-        public boolean onCreateActionMode(ActionModeCompat mode, Menu menu);
+        public boolean onCreateActionMode(ActionModeCompat mode, GeckoMenu menu);
 
         /* Called to refresh an action mode's action menu. Called whenever the mode is invalidated. Implementors
          * should use this to enable/disable/show/hide menu items. */
-        public boolean onPrepareActionMode(ActionModeCompat mode, Menu menu);
+        public boolean onPrepareActionMode(ActionModeCompat mode, GeckoMenu menu);
 
         /* Called to report a user click on an action button. */
         public boolean onActionItemClicked(ActionModeCompat mode, MenuItem item);
 
         /* Called when an action mode is about to be exited and destroyed. */
         public void onDestroyActionMode(ActionModeCompat mode);
     }
 
@@ -54,17 +55,20 @@ class ActionModeCompat implements GeckoP
         mCallback = callback;
 
         mView = view;
         mView.initForMode(this);
     }
 
     public void finish() {
         // Clearing the menu will also clear the ActionItemBar
-        mView.getMenu().clear();
+        final GeckoMenu menu = mView.getMenu();
+        menu.clear();
+        menu.close();
+
         if (mCallback != null) {
             mCallback.onDestroyActionMode(this);
         }
     }
 
     public CharSequence getTitle() {
         return mView.getTitle();
     }
@@ -72,27 +76,31 @@ class ActionModeCompat implements GeckoP
     public void setTitle(CharSequence title) {
         mView.setTitle(title);
     }
 
     public void setTitle(int resId) {
         mView.setTitle(resId);
     }
 
-    public Menu getMenu() {
+    public GeckoMenu getMenu() {
         return mView.getMenu();
     }
 
     public void invalidate() {
         if (mCallback != null) {
             mCallback.onPrepareActionMode(this, mView.getMenu());
         }
         mView.invalidate();
     }
 
+    public void animateIn() {
+        mView.animateIn();
+    }
+
     /* GeckoPopupMenu.OnMenuItemClickListener */
     @Override
     public boolean onMenuItemClick(MenuItem item) {
         if (mCallback != null) {
             return mCallback.onActionItemClicked(this, item);
         }
         return false;
     }
--- a/mobile/android/base/java/org/mozilla/gecko/ActionModeCompatView.java
+++ b/mobile/android/base/java/org/mozilla/gecko/ActionModeCompatView.java
@@ -54,17 +54,17 @@ class ActionModeCompatView extends Linea
     public void init(Context context) {
         LayoutInflater.from(context).inflate(R.layout.actionbar, this);
 
         mTitleView = (Button) findViewById(R.id.actionmode_title);
         mMenuButton = (ImageButton) findViewById(R.id.actionbar_menu);
         mActionButtonBar = (ViewGroup) findViewById(R.id.actionbar_buttons);
 
         mPopupMenu = new GeckoPopupMenu(getContext(), mMenuButton);
-        ((GeckoMenu) mPopupMenu.getMenu()).setActionItemBarPresenter(this);
+        mPopupMenu.getMenu().setActionItemBarPresenter(this);
 
         mMenuButton.setOnClickListener(new View.OnClickListener() {
             @Override
             public void onClick(View v) {
                 openMenu();
             }
         });
     }
@@ -82,17 +82,17 @@ class ActionModeCompatView extends Linea
     public void setTitle(CharSequence title) {
         mTitleView.setText(title);
     }
 
     public void setTitle(int resId) {
         mTitleView.setText(resId);
     }
 
-    public Menu getMenu() {
+    public GeckoMenu getMenu() {
         return mPopupMenu.getMenu();
     }
 
     @Override
     public void invalidate() {
         // onFinishInflate may not have been called yet on some versions of Android
         if (mPopupMenu != null && mMenuButton != null) {
             mMenuButton.setVisibility(mPopupMenu.getMenu().hasVisibleItems() ? View.VISIBLE : View.GONE);
@@ -167,11 +167,15 @@ class ActionModeCompatView extends Linea
         t.setDuration(duration);
 
         ScaleAnimation s = new ScaleAnimation(1f, 1f, 0f, 1f,
                                               Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f);
         s.setDuration((long) (duration * 1.5f));
 
         mTitleView.startAnimation(t);
         mActionButtonBar.startAnimation(s);
-        mMenuButton.startAnimation(s);
+
+        if ((mMenuButton.getVisibility() == View.VISIBLE) &&
+            (mPopupMenu.getMenu().size() > 0)) {
+            mMenuButton.startAnimation(s);
+        }
     }
 }
--- a/mobile/android/base/java/org/mozilla/gecko/BrowserApp.java
+++ b/mobile/android/base/java/org/mozilla/gecko/BrowserApp.java
@@ -645,16 +645,22 @@ public class BrowserApp extends GeckoApp
         mBrowserToolbar.setTabHistoryController(tabHistoryController);
 
         final String action = intent.getAction();
         if (Intent.ACTION_VIEW.equals(action)) {
             // Show the target URL immediately in the toolbar.
             mBrowserToolbar.setTitle(intent.getDataString());
 
             showTabQueuePromptIfApplicable(intent);
+        } else if (ACTION_VIEW_MULTIPLE.equals(action) && savedInstanceState == null) {
+            // We only want to handle this intent if savedInstanceState is null. In the case where
+            // savedInstanceState is not null this activity is being re-created and we already
+            // opened tabs for the URLs the last time. Our session store will take care of restoring
+            // them.
+            openMultipleTabsFromIntent(intent);
         } else if (GuestSession.NOTIFICATION_INTENT.equals(action)) {
             GuestSession.handleIntent(this, intent);
         } else if (TabQueueHelper.LOAD_URLS_ACTION.equals(action)) {
             Telemetry.sendUIEvent(TelemetryContract.Event.ACTION, TelemetryContract.Method.NOTIFICATION, "tabqueue");
         }
 
         if (HardwareUtils.isTablet()) {
             mTabStrip = (TabStripInterface) (((ViewStub) findViewById(R.id.tablet_tab_strip)).inflate());
@@ -1026,16 +1032,33 @@ public class BrowserApp extends GeckoApp
                 @Override
                 public void run() {
                     showNormalTabs();
                 }
             });
         }
     }
 
+    private void openMultipleTabsFromIntent(final Intent intent) {
+        final List<String> urls = intent.getStringArrayListExtra("urls");
+        if (urls != null) {
+            openUrls(urls);
+        }
+
+        // Launched from a "content notification"
+        if (intent.hasExtra(CheckForUpdatesAction.EXTRA_CONTENT_NOTIFICATION)) {
+            Telemetry.startUISession(TelemetryContract.Session.EXPERIMENT, FeedService.getEnabledExperiment(this));
+
+            Telemetry.sendUIEvent(TelemetryContract.Event.ACTION, TelemetryContract.Method.NOTIFICATION, "content_update");
+            Telemetry.sendUIEvent(TelemetryContract.Event.LOAD_URL, TelemetryContract.Method.INTENT, "content_update");
+
+            Telemetry.stopUISession(TelemetryContract.Session.EXPERIMENT, FeedService.getEnabledExperiment(this));
+        }
+    }
+
     @Override
     public void onResume() {
         super.onResume();
 
         // Needed for Adjust to get accurate session measurements
         AdjustConstants.getAdjustHelper().onResume();
 
         final String args = ContextUtils.getStringExtra(getIntent(), "args");
@@ -3770,30 +3793,17 @@ public class BrowserApp extends GeckoApp
                 public void run() {
                     openQueuedTabs();
                 }
             });
         }
 
         // Custom intent action for opening multiple URLs at once
         if (isViewMultipleAction) {
-            List<String> urls = intent.getStringArrayListExtra("urls");
-            if (urls != null) {
-                openUrls(urls);
-            }
-
-            // Launched from a "content notification"
-            if (intent.hasExtra(CheckForUpdatesAction.EXTRA_CONTENT_NOTIFICATION)) {
-                Telemetry.startUISession(TelemetryContract.Session.EXPERIMENT, FeedService.getEnabledExperiment(this));
-
-                Telemetry.sendUIEvent(TelemetryContract.Event.ACTION, TelemetryContract.Method.NOTIFICATION, "content_update");
-                Telemetry.sendUIEvent(TelemetryContract.Event.LOAD_URL, TelemetryContract.Method.INTENT, "content_update");
-
-                Telemetry.stopUISession(TelemetryContract.Session.EXPERIMENT, FeedService.getEnabledExperiment(this));
-            }
+            openMultipleTabsFromIntent(intent);
         }
 
         if (!mInitialized || !Intent.ACTION_MAIN.equals(action)) {
             return;
         }
 
         // Check to see how many times the app has been launched.
         final String keyName = getPackageName() + ".feedback_launch_count";
@@ -3987,22 +3997,19 @@ public class BrowserApp extends GeckoApp
         // If actionMode is null, we're not currently showing one. Flip to the action mode view
         if (mActionMode == null) {
             mActionBarFlipper.showNext();
             DynamicToolbarAnimator toolbar = mLayerView.getDynamicToolbarAnimator();
 
             // If the toolbar is dynamic and not currently showing, just slide it in
             if (mDynamicToolbar.isEnabled() && toolbar.getToolbarTranslation() != 0) {
                 mDynamicToolbar.setTemporarilyVisible(true, VisibilityTransition.ANIMATE);
-            } else {
-                // Otherwise, we animate the actionbar itself
-                mActionBar.animateIn();
             }
-
             mDynamicToolbar.setPinned(true, PinReason.ACTION_MODE);
+
         } else {
             // Otherwise, we're already showing an action mode. Just finish it and show the new one
             mActionMode.finish();
         }
 
         mActionMode = new ActionModeCompat(BrowserApp.this, callback, mActionBar);
         if (callback.onCreateActionMode(mActionMode, mActionMode.getMenu())) {
             mActionMode.invalidate();
--- a/mobile/android/base/java/org/mozilla/gecko/db/BrowserDatabaseHelper.java
+++ b/mobile/android/base/java/org/mozilla/gecko/db/BrowserDatabaseHelper.java
@@ -617,18 +617,18 @@ public final class BrowserDatabaseHelper
                     cursor.getColumnIndexOrThrow(History.VISITS));
         } finally {
             cursor.close();
         }
 
         final int visitsToSynthesize = knownVisits - baseNumberOfVisits;
 
         if (visitsToSynthesize < 0) {
-            throw new IllegalStateException(
-                    "History visits count (for guid=" + guid + ") was less than base number of visit: " + baseNumberOfVisits);
+            Log.w(LOGTAG, guid + " # of visits(" + knownVisits + ") less than # of hist.ext.db visits(" + baseNumberOfVisits + ")");
+            return 0;
         }
 
         return visitsToSynthesize;
     }
 
     private ContentValues[] generateSynthesizedVisits(int numberOfVisits, @NonNull String guid, @NonNull Long baseDate) {
         final ContentValues[] fakeVisits = new ContentValues[numberOfVisits];
 
--- a/mobile/android/base/java/org/mozilla/gecko/db/BrowserProvider.java
+++ b/mobile/android/base/java/org/mozilla/gecko/db/BrowserProvider.java
@@ -1042,17 +1042,17 @@ public class BrowserProvider extends Sha
                 } else {
                     debug("Using sort order " + sortOrder + ".");
                 }
 
                 qb.setProjectionMap(BOOKMARKS_PROJECTION_MAP);
 
                 if (hasFaviconsInProjection(projection)) {
                     qb.setTables(VIEW_BOOKMARKS_WITH_FAVICONS);
-                } else if (selection.contains(Bookmarks.ANNOTATION_KEY)) {
+                } else if (selection != null && selection.contains(Bookmarks.ANNOTATION_KEY)) {
                     qb.setTables(VIEW_BOOKMARKS_WITH_ANNOTATIONS);
 
                     groupBy = uri.getQueryParameter(BrowserContract.PARAM_GROUP_BY);
                 } else {
                     qb.setTables(TABLE_BOOKMARKS);
                 }
 
                 break;
--- a/mobile/android/base/java/org/mozilla/gecko/home/RecentTabsPanel.java
+++ b/mobile/android/base/java/org/mozilla/gecko/home/RecentTabsPanel.java
@@ -17,16 +17,17 @@ import org.mozilla.gecko.GeckoAppShell;
 import org.mozilla.gecko.GeckoEvent;
 import org.mozilla.gecko.GeckoProfile;
 import org.mozilla.gecko.R;
 import org.mozilla.gecko.SessionParser;
 import org.mozilla.gecko.Telemetry;
 import org.mozilla.gecko.TelemetryContract;
 import org.mozilla.gecko.db.BrowserContract.CommonColumns;
 import org.mozilla.gecko.db.BrowserContract.URLColumns;
+import org.mozilla.gecko.reader.SavedReaderViewHelper;
 import org.mozilla.gecko.util.EventCallback;
 import org.mozilla.gecko.util.NativeEventListener;
 import org.mozilla.gecko.util.NativeJSObject;
 import org.mozilla.gecko.util.ThreadUtils;
 
 import android.content.Context;
 import android.database.Cursor;
 import android.database.MatrixCursor;
@@ -281,16 +282,25 @@ public class RecentTabsPanel extends Hom
             row.add(url);
             row.add(title);
             row.add(type);
             row.add(data);
         }
 
         @Override
         public Cursor loadCursor() {
+            // TwoLinePageRow requires the SavedReaderViewHelper to be initialised. Usually this is
+            // done as part of BrowserDatabaseHelper.onOpen(), however we don't actually access
+            // the DB when showing the Recent Tabs panel, hence it's possible that the SavedReaderViewHelper
+            // isn't loaded. Therefore we need to explicitly force loading here.
+            // Note: loadCursor is run on a background thread, hence it's safe to do this here.
+            // (loading time is a few ms, and hence shouldn't impact overall loading time for this
+            // panel in any significant way).
+            SavedReaderViewHelper.getSavedReaderViewHelper(getContext()).loadItems();
+
             final Context context = getContext();
 
             final MatrixCursor c = new MatrixCursor(new String[] { RecentTabs._ID,
                                                                    RecentTabs.URL,
                                                                    RecentTabs.TITLE,
                                                                    RecentTabs.TYPE,
                                                                    RecentTabs.DATA});
 
--- a/mobile/android/base/java/org/mozilla/gecko/widget/GeckoPopupMenu.java
+++ b/mobile/android/base/java/org/mozilla/gecko/widget/GeckoPopupMenu.java
@@ -77,17 +77,17 @@ public class GeckoPopupMenu implements G
         setAnchor(anchor);
     }
 
     /**
      * Returns the menu that is current being shown.
      *
      * @return The menu being shown.
      */
-    public Menu getMenu() {
+    public GeckoMenu getMenu() {
         return mMenu;
     }
 
     /**
      * Returns the menu inflater that was used to create the menu.
      *
      * @return The menu inflater used.
      */
--- a/mobile/android/tests/browser/robocop/testAccessibleCarets.html
+++ b/mobile/android/tests/browser/robocop/testAccessibleCarets.html
@@ -33,11 +33,13 @@
       rows="3" cols="8">הספר הוא טוב</textarea>
 
     <br>
     <input id="LTRphone" style="direction: ltr;" size="40"
       value="09876543210 .-.)(wp#*1034103410341034X">
     <br>
     <input id="RTLphone" style="direction: rtl;" size="40"
       value="התקשר +972 3 7347514 במשך זמן טוב">
+    <br>
+    <div><input value="DDs12">3 45<em id="bug1265750"> 678</em> 90</div>
 
   </body>
 </html>
--- a/mobile/android/tests/browser/robocop/testAccessibleCarets.js
+++ b/mobile/android/tests/browser/robocop/testAccessibleCarets.js
@@ -159,30 +159,32 @@ add_task(function* testAccessibleCarets(
 
   let ce_RTL_elem = doc.getElementById("RTLcontenteditable");
   let tc_RTL_elem = doc.getElementById("RTLtextContent");
   let i_RTL_elem = doc.getElementById("RTLinput");
   let ta_RTL_elem = doc.getElementById("RTLtextarea");
 
   let ip_LTR_elem = doc.getElementById("LTRphone");
   let ip_RTL_elem = doc.getElementById("RTLphone");
+  let bug1265750_elem = doc.getElementById("bug1265750");
 
   // Locate longpress midpoints for test elements, ensure expactations.
   let ce_LTR_midPoint = getCharPressPoint(doc, ce_LTR_elem, 0, "F");
   let tc_LTR_midPoint = getCharPressPoint(doc, tc_LTR_elem, 0, "O");
   let i_LTR_midPoint = getCharPressPoint(doc, i_LTR_elem, 0, "T");
   let ta_LTR_midPoint = getCharPressPoint(doc, ta_LTR_elem, 0, "W");
 
   let ce_RTL_midPoint = getCharPressPoint(doc, ce_RTL_elem, 0, "א");
   let tc_RTL_midPoint = getCharPressPoint(doc, tc_RTL_elem, 0, "ת");
   let i_RTL_midPoint = getCharPressPoint(doc, i_RTL_elem, 0, "ל");
   let ta_RTL_midPoint = getCharPressPoint(doc, ta_RTL_elem, 0, "ה");
 
   let ip_LTR_midPoint = getCharPressPoint(doc, ip_LTR_elem, 8, "2");
   let ip_RTL_midPoint = getCharPressPoint(doc, ip_RTL_elem, 9, "2");
+  let bug1265750_midPoint = getCharPressPoint(doc, bug1265750_elem, 2, "7");
 
   // Longpress various LTR content elements. Test focused element against
   // expected, and selected text against expected.
   let result = getLongPressResult(browser, ce_LTR_midPoint);
   is(result.focusedElement, ce_LTR_elem, "Focused element should match expected.");
   is(result.text, "Find", "Selected text should match expected text.");
 
   result = getLongPressResult(browser, tc_LTR_midPoint);
@@ -199,16 +201,21 @@ add_task(function* testAccessibleCarets(
 
   result = getLongPressResult(browser, ip_LTR_midPoint);
   is(result.focusedElement, ip_LTR_elem, "Focused element should match expected.");
   is(result.text, "09876543210 .-.)(wp#*103410341",
     "Selected phone number should match expected text.");
   is(result.text.length, 30,
     "Selected phone number length should match expected maximum.");
 
+  result = getLongPressResult(browser, bug1265750_midPoint);
+  is(result.focusedElement, null, "Focused element should match expected.");
+  is(result.text, "3 45 678 90",
+    "Selected phone number should match expected text.");
+
   // Longpress various RTL content elements. Test focused element against
   // expected, and selected text against expected.
   result = getLongPressResult(browser, ce_RTL_midPoint);
   is(result.focusedElement, ce_RTL_elem, "Focused element should match expected.");
   is(result.text, "איפה", "Selected text should match expected text.");
 
   result = getLongPressResult(browser, tc_RTL_midPoint);
   is(result.focusedElement, null, "No focused element is expected.");
rename from services/common/KintoCertificateBlocklist.js
rename to services/common/KintoBlocklist.js
--- a/services/common/KintoCertificateBlocklist.js
+++ b/services/common/KintoBlocklist.js
@@ -1,115 +1,185 @@
 /* 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 = ["OneCRLClient"];
+this.EXPORTED_SYMBOLS = ["AddonBlocklistClient",
+                         "GfxBlocklistClient",
+                         "OneCRLBlocklistClient",
+                         "PluginBlocklistClient",
+                         "FILENAME_ADDONS_JSON",
+                         "FILENAME_GFX_JSON",
+                         "FILENAME_PLUGINS_JSON"];
 
 const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
 
-Cu.import("resource://services-common/kinto-offline-client.js");
+Cu.import("resource://gre/modules/Services.jsm");
+const { Task } = Cu.import("resource://gre/modules/Task.jsm");
+const { OS } = Cu.import("resource://gre/modules/osfile.jsm");
 
-Cu.import("resource://gre/modules/Services.jsm");
-Cu.import("resource://gre/modules/Task.jsm");
-Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+const { loadKinto } = Cu.import("resource://services-common/kinto-offline-client.js");
 
-XPCOMUtils.defineLazyServiceGetter(this, "uuidgen",
-                                   "@mozilla.org/uuid-generator;1",
-                                   "nsIUUIDGenerator");
+const PREF_KINTO_BASE                    = "services.kinto.base";
+const PREF_KINTO_BUCKET                  = "services.kinto.bucket";
+const PREF_KINTO_ONECRL_COLLECTION       = "services.kinto.onecrl.collection";
+const PREF_KINTO_ONECRL_CHECKED_SECONDS  = "services.kinto.onecrl.checked";
+const PREF_KINTO_ADDONS_COLLECTION       = "services.kinto.addons.collection";
+const PREF_KINTO_ADDONS_CHECKED_SECONDS  = "services.kinto.addons.checked";
+const PREF_KINTO_PLUGINS_COLLECTION      = "services.kinto.plugins.collection";
+const PREF_KINTO_PLUGINS_CHECKED_SECONDS = "services.kinto.plugins.checked";
+const PREF_KINTO_GFX_COLLECTION          = "services.kinto.gfx.collection";
+const PREF_KINTO_GFX_CHECKED_SECONDS     = "services.kinto.gfx.checked";
 
-const PREF_KINTO_BASE = "services.kinto.base";
-const PREF_KINTO_BUCKET = "services.kinto.bucket";
-const PREF_KINTO_ONECRL_COLLECTION = "services.kinto.onecrl.collection";
-const PREF_KINTO_ONECRL_CHECKED_SECONDS = "services.kinto.onecrl.checked";
+this.FILENAME_ADDONS_JSON  = "blocklist-addons.json";
+this.FILENAME_GFX_JSON     = "blocklist-gfx.json";
+this.FILENAME_PLUGINS_JSON = "blocklist-plugins.json";
 
-const RE_UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
 
-// Kinto.js assumes version 4 UUIDs but allows you to specify custom
-// validators and generators. The tooling that generates records in the
-// certificates collection currently uses a version 1 UUID so we must
-// specify a validator that's less strict. We must also supply a generator
-// since Kinto.js does not allow one without the other.
-function makeIDSchema() {
-  return {
-    validate: RE_UUID.test.bind(RE_UUID),
-    generate: function() {
-      return uuidgen.generateUUID().toString();
-    }
-  };
-}
+/**
+ * Helper to instantiate a Kinto client based on preferences for remote server
+ * URL and bucket name. It uses the `FirefoxAdapter` which relies on SQLite to
+ * persist the local DB.
+ */
+function kintoClient() {
+  let base = Services.prefs.getCharPref(PREF_KINTO_BASE);
+  let bucket = Services.prefs.getCharPref(PREF_KINTO_BUCKET);
+
+  let Kinto = loadKinto();
 
-// A Kinto based client to keep the OneCRL certificate blocklist up to date.
-function CertBlocklistClient() {
-  // maybe sync the collection of certificates with remote data.
-  // lastModified - the lastModified date (on the server, milliseconds since
-  // epoch) of data in the remote collection
-  // serverTime - the time on the server (milliseconds since epoch)
-  // returns a promise which rejects on sync failure
-  this.maybeSync = function(lastModified, serverTime) {
-    let base = Services.prefs.getCharPref(PREF_KINTO_BASE);
-    let bucket = Services.prefs.getCharPref(PREF_KINTO_BUCKET);
+  let FirefoxAdapter = Kinto.adapters.FirefoxAdapter;
 
-    let Kinto = loadKinto();
+  let config = {
+    remote: base,
+    bucket: bucket,
+    adapter: FirefoxAdapter,
+  };
 
-    let FirefoxAdapter = Kinto.adapters.FirefoxAdapter;
+  return new Kinto(config);
+}
 
 
-    let certList = Cc["@mozilla.org/security/certblocklist;1"]
-                     .getService(Ci.nsICertBlocklist);
+class BlocklistClient {
 
-    // Future blocklist clients can extract the sync-if-stale logic. For
-    // now, since this is currently the only client, we'll do this here.
-    let config = {
-      remote: base,
-      bucket: bucket,
-      adapter: FirefoxAdapter,
-    };
+  constructor(collectionName, lastCheckTimePref, processCallback) {
+    this.collectionName = collectionName;
+    this.lastCheckTimePref = lastCheckTimePref;
+    this.processCallback = processCallback;
+  }
 
-    let db = new Kinto(config);
-    let collectionName = Services.prefs.getCharPref(PREF_KINTO_ONECRL_COLLECTION,
-                                                    "certificates");
-    let blocklist = db.collection(collectionName,
-                                  { idSchema: makeIDSchema() });
+  /**
+   * Synchronize from Kinto server, if necessary.
+   *
+   * @param {int}  lastModified the lastModified date (on the server) for
+                                the remote collection.
+   * @param {Date} serverTime   the current date return by the server.
+   * @return {Promise}          which rejects on sync or process failure.
+   */
+  maybeSync(lastModified, serverTime) {
+    let db = kintoClient();
+    let collection = db.collection(this.collectionName);
 
-    let updateLastCheck = function() {
-      let checkedServerTimeInSeconds = Math.round(serverTime / 1000);
-      Services.prefs.setIntPref(PREF_KINTO_ONECRL_CHECKED_SECONDS,
-                                checkedServerTimeInSeconds);
-    }
+    return Task.spawn((function* syncCollection() {
+      try {
+        yield collection.db.open();
 
-    return Task.spawn(function* () {
-      try {
-        yield blocklist.db.open();
-        let collectionLastModified = yield blocklist.db.getLastModified();
-        // if the data is up to date, there's no need to sync. We still need
+        let collectionLastModified = yield collection.db.getLastModified();
+        // If the data is up to date, there's no need to sync. We still need
         // to record the fact that a check happened.
         if (lastModified <= collectionLastModified) {
-          updateLastCheck();
+          this.updateLastCheck(serverTime);
           return;
         }
-        yield blocklist.sync();
-        let list = yield blocklist.list();
-        for (let item of list.data) {
-          if (item.issuerName && item.serialNumber) {
-            certList.revokeCertByIssuerAndSerial(item.issuerName,
-                                                 item.serialNumber);
-          } else if (item.subject && item.pubKeyHash) {
-            certList.revokeCertBySubjectAndPubKey(item.subject,
-                                                  item.pubKeyHash);
-          } else {
-            throw new Error("Cert blocklist record has incomplete data");
-          }
-        }
-        // We explicitly do not want to save entries or update the
-        // last-checked time if sync fails
-        certList.saveEntries();
-        updateLastCheck();
+        // Fetch changes from server.
+        yield collection.sync();
+        // Read local collection of records.
+        let list = yield collection.list();
+
+        yield this.processCallback(list.data);
+
+        // Track last update.
+        this.updateLastCheck(serverTime);
       } finally {
-        blocklist.db.close()
+        collection.db.close();
       }
-    });
+    }).bind(this));
+  }
+
+  /**
+   * Save last time server was checked in users prefs.
+   *
+   * @param {Date} serverTime   the current date return by server.
+   */
+  updateLastCheck(serverTime) {
+    let checkedServerTimeInSeconds = Math.round(serverTime / 1000);
+    Services.prefs.setIntPref(this.lastCheckTimePref, checkedServerTimeInSeconds);
   }
 }
 
-this.OneCRLClient = new CertBlocklistClient();
+/**
+ * Revoke the appropriate certificates based on the records from the blocklist.
+ *
+ * @param {Object} records   current records in the local db.
+ */
+function* updateCertBlocklist(records) {
+  let certList = Cc["@mozilla.org/security/certblocklist;1"]
+                   .getService(Ci.nsICertBlocklist);
+  for (let item of records) {
+    if (item.issuerName && item.serialNumber) {
+      certList.revokeCertByIssuerAndSerial(item.issuerName,
+                                           item.serialNumber);
+    } else if (item.subject && item.pubKeyHash) {
+      certList.revokeCertBySubjectAndPubKey(item.subject,
+                                            item.pubKeyHash);
+    } else {
+      throw new Error("Cert blocklist record has incomplete data");
+    }
+  }
+  certList.saveEntries();
+}
+
+/**
+ * Write list of records into JSON file, and notify nsBlocklistService.
+ *
+ * @param {String} filename  path relative to profile dir.
+ * @param {Object} records   current records in the local db.
+ */
+function* updateJSONBlocklist(filename, records) {
+  // Write JSON dump for synchronous load at startup.
+  const path = OS.Path.join(OS.Constants.Path.profileDir, filename);
+  const serialized = JSON.stringify({data: records}, null, 2);
+  try {
+    yield OS.File.writeAtomic(path, serialized, {tmpPath: path + ".tmp"});
+
+    // Notify change to `nsBlocklistService`
+    const eventData = {filename: filename};
+    Services.cpmm.sendAsyncMessage("Blocklist:reload-from-disk", eventData);
+  } catch(e) {
+    Cu.reportError(e);
+  }
+}
+
+
+this.OneCRLBlocklistClient = new BlocklistClient(
+  Services.prefs.getCharPref(PREF_KINTO_ONECRL_COLLECTION),
+  PREF_KINTO_ONECRL_CHECKED_SECONDS,
+  updateCertBlocklist
+);
+
+this.AddonBlocklistClient = new BlocklistClient(
+  Services.prefs.getCharPref(PREF_KINTO_ADDONS_COLLECTION),
+  PREF_KINTO_ADDONS_CHECKED_SECONDS,
+  updateJSONBlocklist.bind(undefined, FILENAME_ADDONS_JSON)
+);
+
+this.GfxBlocklistClient = new BlocklistClient(
+  Services.prefs.getCharPref(PREF_KINTO_GFX_COLLECTION),
+  PREF_KINTO_GFX_CHECKED_SECONDS,
+  updateJSONBlocklist.bind(undefined, FILENAME_GFX_JSON)
+);
+
+this.PluginBlocklistClient = new BlocklistClient(
+  Services.prefs.getCharPref(PREF_KINTO_PLUGINS_COLLECTION),
+  PREF_KINTO_PLUGINS_CHECKED_SECONDS,
+  updateJSONBlocklist.bind(undefined, FILENAME_PLUGINS_JSON)
+);
--- a/services/common/kinto-updater.js
+++ b/services/common/kinto-updater.js
@@ -4,33 +4,41 @@
 
 this.EXPORTED_SYMBOLS = ["checkVersions", "addTestKintoClient"];
 
 const { classes: Cc, Constructor: CC, interfaces: Ci, utils: Cu } = Components;
 
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/Task.jsm");
 Cu.importGlobalProperties(['fetch']);
+const BlocklistClients = Cu.import("resource://services-common/KintoBlocklist.js", {});
 
-const PREF_KINTO_CHANGES_PATH = "services.kinto.changes.path";
-const PREF_KINTO_BASE = "services.kinto.base";
-const PREF_KINTO_BUCKET = "services.kinto.bucket";
-const PREF_KINTO_LAST_UPDATE = "services.kinto.last_update_seconds";
-const PREF_KINTO_LAST_ETAG = "services.kinto.last_etag";
+const PREF_KINTO_CHANGES_PATH       = "services.kinto.changes.path";
+const PREF_KINTO_BASE               = "services.kinto.base";
+const PREF_KINTO_BUCKET             = "services.kinto.bucket";
+const PREF_KINTO_LAST_UPDATE        = "services.kinto.last_update_seconds";
+const PREF_KINTO_LAST_ETAG          = "services.kinto.last_etag";
 const PREF_KINTO_CLOCK_SKEW_SECONDS = "services.kinto.clock_skew_seconds";
-const PREF_KINTO_ONECRL_COLLECTION = "services.kinto.onecrl.collection";
+const PREF_KINTO_ONECRL_COLLECTION  = "services.kinto.onecrl.collection";
+
 
-const kintoClients = {
+const gBlocklistClients = {
+  [BlocklistClients.OneCRLBlocklistClient.collectionName]: BlocklistClients.OneCRLBlocklistClient,
+  [BlocklistClients.AddonBlocklistClient.collectionName]: BlocklistClients.AddonBlocklistClient,
+  [BlocklistClients.GfxBlocklistClient.collectionName]: BlocklistClients.GfxBlocklistClient,
+  [BlocklistClients.PluginBlocklistClient.collectionName]: BlocklistClients.PluginBlocklistClient
 };
 
+// Add a blocklist client for testing purposes. Do not use for any other purpose
+this.addTestKintoClient = (name, client) => { gBlocklistClients[name] = client; }
+
 // This is called by the ping mechanism.
 // returns a promise that rejects if something goes wrong
 this.checkVersions = function() {
-
-  return Task.spawn(function *() {
+  return Task.spawn(function* syncClients() {
     // Fetch a versionInfo object that looks like:
     // {"data":[
     //   {
     //     "host":"kinto-ota.dev.mozaws.net",
     //     "last_modified":1450717104423,
     //     "bucket":"blocklists",
     //     "collection":"certificates"
     //    }]}
@@ -73,24 +81,24 @@ this.checkVersions = function() {
     let firstError;
     for (let collectionInfo of versionInfo.data) {
       // Skip changes that don't concern configured blocklist bucket.
       if (collectionInfo.bucket != blocklistsBucket) {
         continue;
       }
 
       let collection = collectionInfo.collection;
-      let kintoClient = kintoClients[collection];
-      if (kintoClient && kintoClient.maybeSync) {
+      let client = gBlocklistClients[collection];
+      if (client && client.maybeSync) {
         let lastModified = 0;
         if (collectionInfo.last_modified) {
           lastModified = collectionInfo.last_modified;
         }
         try {
-          yield kintoClient.maybeSync(lastModified, serverTimeMillis);
+          yield client.maybeSync(lastModified, serverTimeMillis);
         } catch (e) {
           if (!firstError) {
             firstError = e;
           }
         }
       }
     }
     if (firstError) {
@@ -100,17 +108,8 @@ this.checkVersions = function() {
 
     // Save current Etag for next poll.
     if (response.headers.has("ETag")) {
       const currentEtag = response.headers.get("ETag");
       Services.prefs.setCharPref(PREF_KINTO_LAST_ETAG, currentEtag);
     }
   });
 };
-
-// Add a kintoClient for testing purposes. Do not use for any other purpose
-this.addTestKintoClient = function(name, kintoClient) {
-  kintoClients[name] = kintoClient;
-};
-
-// Add the various things that we know want updates
-const KintoBlocklist = Cu.import("resource://services-common/KintoCertificateBlocklist.js", {});
-kintoClients[Services.prefs.getCharPref(PREF_KINTO_ONECRL_COLLECTION)]  = KintoBlocklist.OneCRLClient;
--- a/services/common/moz.build
+++ b/services/common/moz.build
@@ -13,17 +13,17 @@ EXTRA_COMPONENTS += [
     'servicesComponents.manifest',
 ]
 
 EXTRA_JS_MODULES['services-common'] += [
     'async.js',
     'kinto-http-client.js',
     'kinto-offline-client.js',
     'kinto-updater.js',
-    'KintoCertificateBlocklist.js',
+    'KintoBlocklist.js',
     'logmanager.js',
     'observers.js',
     'rest.js',
     'stringbundle.js',
     'utils.js',
 ]
 
 if CONFIG['MOZ_WIDGET_TOOLKIT'] != 'android':
new file mode 100644
--- /dev/null
+++ b/services/common/tests/unit/test_kintoAddonPluginBlocklist.js
@@ -0,0 +1,412 @@
+const { Constructor: CC } = Components;
+
+const KEY_PROFILEDIR = "ProfD";
+
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://testing-common/httpd.js");
+Cu.import("resource://gre/modules/Timer.jsm");
+const { FileUtils } = Cu.import("resource://gre/modules/FileUtils.jsm");
+const { OS } = Cu.import("resource://gre/modules/osfile.jsm");
+
+const { loadKinto } = Cu.import("resource://services-common/kinto-offline-client.js");
+const KintoBlocklist = Cu.import("resource://services-common/KintoBlocklist.js");
+
+const BinaryInputStream = CC("@mozilla.org/binaryinputstream;1",
+  "nsIBinaryInputStream", "setInputStream");
+
+const gBlocklistClients = [
+  {client: KintoBlocklist.AddonBlocklistClient, filename: KintoBlocklist.FILENAME_ADDONS_JSON, testData: ["i808","i720", "i539"]},
+  {client: KintoBlocklist.PluginBlocklistClient, filename: KintoBlocklist.FILENAME_PLUGINS_JSON, testData: ["p1044","p32","p28"]},
+  {client: KintoBlocklist.GfxBlocklistClient, filename: KintoBlocklist.FILENAME_GFX_JSON, testData: ["g204","g200","g36"]},
+];
+
+
+let server;
+let kintoClient;
+
+function kintoCollection(collectionName) {
+  if (!kintoClient) {
+    const Kinto = loadKinto();
+    const FirefoxAdapter = Kinto.adapters.FirefoxAdapter;
+    const config = {
+      // Set the remote to be some server that will cause test failure when
+      // hit since we should never hit the server directly, only via maybeSync()
+      remote: "https://firefox.settings.services.mozilla.com/v1/",
+      adapter: FirefoxAdapter,
+      bucket: "blocklists"
+    };
+    kintoClient = new Kinto(config);
+  }
+  return kintoClient.collection(collectionName);
+}
+
+function* readJSON(filepath) {
+  const binaryData = yield OS.File.read(filepath);
+  const textData = (new TextDecoder()).decode(binaryData);
+  return Promise.resolve(JSON.parse(textData));
+}
+
+function* clear_state() {
+  for (let {client} of gBlocklistClients) {
+    // Remove last server times.
+    Services.prefs.clearUserPref(client.lastCheckTimePref);
+
+    // Clear local DB.
+    const collection = kintoCollection(client.collectionName);
+    try {
+      yield collection.db.open();
+      yield collection.clear();
+    } finally {
+      yield collection.db.close();
+    }
+  }
+
+  // Remove profile data.
+  for (let {filename} of gBlocklistClients) {
+    const blocklist = FileUtils.getFile(KEY_PROFILEDIR, [filename]);
+    if (blocklist.exists()) {
+      blocklist.remove(true);
+    }
+  }
+}
+
+
+function run_test() {
+  // Set up an HTTP Server
+  server = new HttpServer();
+  server.start(-1);
+
+  // Point the blocklist clients to use this local HTTP server.
+  Services.prefs.setCharPref("services.kinto.base",
+                             `http://localhost:${server.identity.primaryPort}/v1`);
+
+  // Setup server fake responses.
+  function handleResponse(request, response) {
+    try {
+      const sample = getSampleResponse(request, server.identity.primaryPort);
+      if (!sample) {
+        do_throw(`unexpected ${request.method} request for ${request.path}?${request.queryString}`);
+      }
+
+      response.setStatusLine(null, sample.status.status,
+                             sample.status.statusText);
+      // send the headers
+      for (let headerLine of sample.sampleHeaders) {
+        let headerElements = headerLine.split(':');
+        response.setHeader(headerElements[0], headerElements[1].trimLeft());
+      }
+      response.setHeader("Date", (new Date()).toUTCString());
+
+      response.write(sample.responseBody);
+      response.finish();
+    } catch (e) {
+      do_print(e);
+    }
+  }
+  const configPath = "/v1/";
+  const addonsRecordsPath  = "/v1/buckets/blocklists/collections/addons/records";
+  const gfxRecordsPath     = "/v1/buckets/blocklists/collections/gfx/records";
+  const pluginsRecordsPath = "/v1/buckets/blocklists/collections/plugins/records";
+  server.registerPathHandler(configPath, handleResponse);
+  server.registerPathHandler(addonsRecordsPath, handleResponse);
+  server.registerPathHandler(gfxRecordsPath, handleResponse);
+  server.registerPathHandler(pluginsRecordsPath, handleResponse);
+
+
+  run_next_test();
+
+  do_register_cleanup(function() {
+    server.stop(() => { });
+  });
+}
+
+add_task(function* test_records_obtained_from_server_are_stored_in_db(){
+  for (let {client} of gBlocklistClients) {
+    // Test an empty db populates
+    let result = yield client.maybeSync(2000, Date.now());
+
+    // Open the collection, verify it's been populated:
+    // Our test data has a single record; it should be in the local collection
+    let collection = kintoCollection(client.collectionName);
+    yield collection.db.open();
+    let list = yield collection.list();
+    equal(list.data.length, 1);
+    yield collection.db.close();
+  }
+});
+add_task(clear_state);
+
+add_task(function* test_list_is_written_to_file_in_profile(){
+  for (let {client, filename, testData} of gBlocklistClients) {
+    const profFile = FileUtils.getFile(KEY_PROFILEDIR, [filename]);
+    strictEqual(profFile.exists(), false);
+
+    let result = yield client.maybeSync(2000, Date.now());
+
+    strictEqual(profFile.exists(), true);
+    const content = yield readJSON(profFile.path);
+    equal(content.data[0].blockID, testData[testData.length - 1]);
+  }
+});
+add_task(clear_state);
+
+add_task(function* test_current_server_time_is_saved_in_pref(){
+  for (let {client} of gBlocklistClients) {
+    const before = Services.prefs.getIntPref(client.lastCheckTimePref);
+    const serverTime = Date.now();
+    yield client.maybeSync(2000, serverTime);
+    const after = Services.prefs.getIntPref(client.lastCheckTimePref);
+    equal(after, Math.round(serverTime / 1000));
+  }
+});
+add_task(clear_state);
+
+add_task(function* test_update_json_file_when_addons_has_changes(){
+  for (let {client, filename, testData} of gBlocklistClients) {
+    yield client.maybeSync(2000, Date.now() - 1000);
+    const before = Services.prefs.getIntPref(client.lastCheckTimePref);
+    const profFile = FileUtils.getFile(KEY_PROFILEDIR, [filename]);
+    const fileLastModified = profFile.lastModifiedTime = profFile.lastModifiedTime - 1000;
+    const serverTime = Date.now();
+
+    yield client.maybeSync(3001, serverTime);
+
+    // File was updated.
+    notEqual(fileLastModified, profFile.lastModifiedTime);
+    const content = yield readJSON(profFile.path);
+    deepEqual(content.data.map((r) => r.blockID), testData);
+    // Server time was updated.
+    const after = Services.prefs.getIntPref(client.lastCheckTimePref);
+    equal(after, Math.round(serverTime / 1000));
+  }
+});
+add_task(clear_state);
+
+add_task(function* test_sends_reload_message_when_blocklist_has_changes(){
+  for (let {client, filename} of gBlocklistClients) {
+    let received = yield new Promise((resolve, reject) => {
+      Services.ppmm.addMessageListener("Blocklist:reload-from-disk", {
+        receiveMessage(aMsg) { resolve(aMsg) }
+      });
+
+      client.maybeSync(2000, Date.now() - 1000);
+    });
+
+    equal(received.data.filename, filename);
+  }
+});
+add_task(clear_state);
+
+add_task(function* test_do_nothing_when_blocklist_is_up_to_date(){
+  for (let {client, filename} of gBlocklistClients) {
+    yield client.maybeSync(2000, Date.now() - 1000);
+    const before = Services.prefs.getIntPref(client.lastCheckTimePref);
+    const profFile = FileUtils.getFile(KEY_PROFILEDIR, [filename]);
+    const fileLastModified = profFile.lastModifiedTime = profFile.lastModifiedTime - 1000;
+    const serverTime = Date.now();
+
+    yield client.maybeSync(3000, serverTime);
+
+    // File was not updated.
+    equal(fileLastModified, profFile.lastModifiedTime);
+    // Server time was updated.
+    const after = Services.prefs.getIntPref(client.lastCheckTimePref);
+    equal(after, Math.round(serverTime / 1000));
+  }
+});
+add_task(clear_state);
+
+
+
+// get a response for a given request from sample data
+function getSampleResponse(req, port) {
+  const responses = {
+    "OPTIONS": {
+      "sampleHeaders": [
+        "Access-Control-Allow-Headers: Content-Length,Expires,Backoff,Retry-After,Last-Modified,Total-Records,ETag,Pragma,Cache-Control,authorization,content-type,if-none-match,Alert,Next-Page",
+        "Access-Control-Allow-Methods: GET,HEAD,OPTIONS,POST,DELETE,OPTIONS",
+        "Access-Control-Allow-Origin: *",
+        "Content-Type: application/json; charset=UTF-8",
+        "Server: waitress"
+      ],
+      "status": {status: 200, statusText: "OK"},
+      "responseBody": "null"
+    },
+    "GET:/v1/?": {
+      "sampleHeaders": [
+        "Access-Control-Allow-Origin: *",
+        "Access-Control-Expose-Headers: Retry-After, Content-Length, Alert, Backoff",
+        "Content-Type: application/json; charset=UTF-8",
+        "Server: waitress"
+      ],
+      "status": {status: 200, statusText: "OK"},
+      "responseBody": JSON.stringify({"settings":{"cliquet.batch_max_requests":25}, "url":`http://localhost:${port}/v1/`, "documentation":"https://kinto.readthedocs.org/", "version":"1.5.1", "commit":"cbc6f58", "hello":"kinto"})
+    },
+    "GET:/v1/buckets/blocklists/collections/addons/records?_sort=-last_modified": {
+      "sampleHeaders": [
+        "Access-Control-Allow-Origin: *",
+        "Access-Control-Expose-Headers: Retry-After, Content-Length, Alert, Backoff",
+        "Content-Type: application/json; charset=UTF-8",
+        "Server: waitress",
+        "Etag: \"3000\""
+      ],
+      "status": {status: 200, statusText: "OK"},
+      "responseBody": JSON.stringify({"data":[{
+        "prefs": [],
+        "blockID": "i539",
+        "last_modified": 3000,
+        "versionRange": [{
+          "targetApplication": [],
+          "maxVersion": "*",
+          "minVersion": "0",
+          "severity": "1"
+        }],
+        "guid": "ScorpionSaver@jetpack",
+        "id": "9d500963-d80e-3a91-6e74-66f3811b99cc"
+      }]})
+    },
+    "GET:/v1/buckets/blocklists/collections/plugins/records?_sort=-last_modified": {
+      "sampleHeaders": [
+        "Access-Control-Allow-Origin: *",
+        "Access-Control-Expose-Headers: Retry-After, Content-Length, Alert, Backoff",
+        "Content-Type: application/json; charset=UTF-8",
+        "Server: waitress",
+        "Etag: \"3000\""
+      ],
+      "status": {status: 200, statusText: "OK"},
+      "responseBody": JSON.stringify({"data":[{
+        "matchFilename": "NPFFAddOn.dll",
+        "blockID": "p28",
+        "id": "7b1e0b3c-e390-a817-11b6-a6887f65f56e",
+        "last_modified": 3000,
+        "versionRange": []
+      }]})
+    },
+    "GET:/v1/buckets/blocklists/collections/gfx/records?_sort=-last_modified": {
+      "sampleHeaders": [
+        "Access-Control-Allow-Origin: *",
+        "Access-Control-Expose-Headers: Retry-After, Content-Length, Alert, Backoff",
+        "Content-Type: application/json; charset=UTF-8",
+        "Server: waitress",
+        "Etag: \"3000\""
+      ],
+      "status": {status: 200, statusText: "OK"},
+      "responseBody": JSON.stringify({"data":[{
+        "driverVersionComparator": "LESS_THAN_OR_EQUAL",
+        "driverVersion": "8.17.12.5896",
+        "vendor": "0x10de",
+        "blockID": "g36",
+        "feature": "DIRECT3D_9_LAYERS",
+        "devices": ["0x0a6c"],
+        "featureStatus": "BLOCKED_DRIVER_VERSION",
+        "last_modified": 3000,
+        "os": "WINNT 6.1",
+        "id": "3f947f16-37c2-4e96-d356-78b26363729b"
+      }]})
+    },
+    "GET:/v1/buckets/blocklists/collections/addons/records?_sort=-last_modified&_since=3000": {
+      "sampleHeaders": [
+        "Access-Control-Allow-Origin: *",
+        "Access-Control-Expose-Headers: Retry-After, Content-Length, Alert, Backoff",
+        "Content-Type: application/json; charset=UTF-8",
+        "Server: waitress",
+        "Etag: \"4000\""
+      ],
+      "status": {status: 200, statusText: "OK"},
+      "responseBody": JSON.stringify({"data":[{
+        "prefs": [],
+        "blockID": "i808",
+        "last_modified": 4000,
+        "versionRange": [{
+          "targetApplication": [],
+          "maxVersion": "*",
+          "minVersion": "0",
+          "severity": "3"
+        }],
+        "guid": "{c96d1ae6-c4cf-4984-b110-f5f561b33b5a}",
+        "id": "9ccfac91-e463-c30c-f0bd-14143794a8dd"
+      }, {
+        "prefs": ["browser.startup.homepage"],
+        "blockID": "i720",
+        "last_modified": 3500,
+        "versionRange": [{
+          "targetApplication": [],
+          "maxVersion": "*",
+          "minVersion": "0",
+          "severity": "1"
+        }],
+        "guid": "FXqG@xeeR.net",
+        "id": "cf9b3129-a97e-dbd7-9525-a8575ac03c25"
+      }]})
+    },
+    "GET:/v1/buckets/blocklists/collections/plugins/records?_sort=-last_modified&_since=3000": {
+      "sampleHeaders": [
+        "Access-Control-Allow-Origin: *",
+        "Access-Control-Expose-Headers: Retry-After, Content-Length, Alert, Backoff",
+        "Content-Type: application/json; charset=UTF-8",
+        "Server: waitress",
+        "Etag: \"4000\""
+      ],
+      "status": {status: 200, statusText: "OK"},
+      "responseBody": JSON.stringify({"data":[{
+        "infoURL": "https://get.adobe.com/flashplayer/",
+        "blockID": "p1044",
+        "matchFilename": "libflashplayer\\.so",
+        "last_modified": 4000,
+        "versionRange": [{
+          "targetApplication": [],
+          "minVersion": "11.2.202.509",
+          "maxVersion": "11.2.202.539",
+          "severity": "0",
+          "vulnerabilityStatus": "1"
+        }],
+        "os": "Linux",
+        "id": "aabad965-e556-ffe7-4191-074f5dee3df3"
+      }, {
+        "matchFilename": "npViewpoint.dll",
+        "blockID": "p32",
+        "id": "1f48af42-c508-b8ef-b8d5-609d48e4f6c9",
+        "last_modified": 3500,
+        "versionRange": [{
+          "targetApplication": [{
+            "minVersion": "3.0",
+            "guid": "{ec8030f7-c20a-464f-9b0e-13a3a9e97384}",
+            "maxVersion": "*"
+          }]
+        }]
+      }]})
+    },
+    "GET:/v1/buckets/blocklists/collections/gfx/records?_sort=-last_modified&_since=3000": {
+      "sampleHeaders": [
+        "Access-Control-Allow-Origin: *",
+        "Access-Control-Expose-Headers: Retry-After, Content-Length, Alert, Backoff",
+        "Content-Type: application/json; charset=UTF-8",
+        "Server: waitress",
+        "Etag: \"4000\""
+      ],
+      "status": {status: 200, statusText: "OK"},
+      "responseBody": JSON.stringify({"data":[{
+        "vendor": "0x8086",
+        "blockID": "g204",
+        "feature": "WEBGL_MSAA",
+        "devices": [],
+        "id": "c96bca82-e6bd-044d-14c4-9c1d67e9283a",
+        "last_modified": 4000,
+        "os": "Darwin 10",
+        "featureStatus": "BLOCKED_DEVICE"
+      }, {
+        "vendor": "0x10de",
+        "blockID": "g200",
+        "feature": "WEBGL_MSAA",
+        "devices": [],
+        "id": "c3a15ba9-e0e2-421f-e399-c995e5b8d14e",
+        "last_modified": 3500,
+        "os": "Darwin 11",
+        "featureStatus": "BLOCKED_DEVICE"
+      }]})
+    }
+  };
+  return responses[`${req.method}:${req.path}?${req.queryString}`] ||
+         responses[req.method];
+
+}
--- a/services/common/tests/unit/test_kintoCertBlocklist.js
+++ b/services/common/tests/unit/test_kintoCertBlocklist.js
@@ -1,21 +1,19 @@
-/* Any copyright is dedicated to the Public Domain.
-   http://creativecommons.org/publicdomain/zero/1.0/ */
-
 const { Constructor: CC } = Components;
 
-Cu.import("resource://services-common/KintoCertificateBlocklist.js");
-Cu.import("resource://services-common/kinto-offline-client.js");
 Cu.import("resource://testing-common/httpd.js");
 
+const { OneCRLBlocklistClient } = Cu.import("resource://services-common/KintoBlocklist.js");
+const { loadKinto } = Cu.import("resource://services-common/kinto-offline-client.js");
+
 const BinaryInputStream = CC("@mozilla.org/binaryinputstream;1",
   "nsIBinaryInputStream", "setInputStream");
 
-var server;
+let server;
 
 // set up what we need to make storage adapters
 const Kinto = loadKinto();
 const FirefoxAdapter = Kinto.adapters.FirefoxAdapter;
 const kintoFilename = "kinto.sqlite";
 
 let kintoClient;
 
@@ -43,86 +41,86 @@ add_task(function* test_something(){
   const recordsPath = "/v1/buckets/blocklists/collections/certificates/records";
 
   Services.prefs.setCharPref("services.kinto.base",
                              `http://localhost:${server.identity.primaryPort}/v1`);
 
   // register a handler
   function handleResponse (request, response) {
     try {
-      const sampled = getSampleResponse(request, server.identity.primaryPort);
-      if (!sampled) {
+      const sample = getSampleResponse(request, server.identity.primaryPort);
+      if (!sample) {
         do_throw(`unexpected ${request.method} request for ${request.path}?${request.queryString}`);
       }
 
-      response.setStatusLine(null, sampled.status.status,
-                             sampled.status.statusText);
+      response.setStatusLine(null, sample.status.status,
+                             sample.status.statusText);
       // send the headers
-      for (let headerLine of sampled.sampleHeaders) {
+      for (let headerLine of sample.sampleHeaders) {
         let headerElements = headerLine.split(':');
         response.setHeader(headerElements[0], headerElements[1].trimLeft());
       }
       response.setHeader("Date", (new Date()).toUTCString());
 
-      response.write(sampled.responseBody);
+      response.write(sample.responseBody);
     } catch (e) {
-      dump(`${e}\n`);
+      do_print(e);
     }
   }
   server.registerPathHandler(configPath, handleResponse);
   server.registerPathHandler(recordsPath, handleResponse);
 
   // Test an empty db populates
-  let result = yield OneCRLClient.maybeSync(2000, Date.now());
+  let result = yield OneCRLBlocklistClient.maybeSync(2000, Date.now());
 
   // Open the collection, verify it's been populated:
   // Our test data has a single record; it should be in the local collection
   let collection = do_get_kinto_collection("certificates");
   yield collection.db.open();
   let list = yield collection.list();
   do_check_eq(list.data.length, 1);
   yield collection.db.close();
 
   // Test the db is updated when we call again with a later lastModified value
-  result = yield OneCRLClient.maybeSync(4000, Date.now());
+  result = yield OneCRLBlocklistClient.maybeSync(4000, Date.now());
 
   // Open the collection, verify it's been updated:
   // Our test data now has two records; both should be in the local collection
   collection = do_get_kinto_collection("certificates");
   yield collection.db.open();
   list = yield collection.list();
   do_check_eq(list.data.length, 3);
   yield collection.db.close();
 
   // Try to maybeSync with the current lastModified value - no connection
   // should be attempted.
   // Clear the kinto base pref so any connections will cause a test failure
   Services.prefs.clearUserPref("services.kinto.base");
-  yield OneCRLClient.maybeSync(4000, Date.now());
+  yield OneCRLBlocklistClient.maybeSync(4000, Date.now());
 
   // Try again with a lastModified value at some point in the past
-  yield OneCRLClient.maybeSync(3000, Date.now());
+  yield OneCRLBlocklistClient.maybeSync(3000, Date.now());
 
   // Check the OneCRL check time pref is modified, even if the collection
   // hasn't changed
   Services.prefs.setIntPref("services.kinto.onecrl.checked", 0);
-  yield OneCRLClient.maybeSync(3000, Date.now());
+  yield OneCRLBlocklistClient.maybeSync(3000, Date.now());
   let newValue = Services.prefs.getIntPref("services.kinto.onecrl.checked");
   do_check_neq(newValue, 0);
 });
 
 function run_test() {
   // Set up an HTTP Server
   server = new HttpServer();
   server.start(-1);
 
   run_next_test();
 
   do_register_cleanup(function() {
-    server.stop(function() { });
+    server.stop(() => { });
   });
 }
 
 // get a response for a given request from sample data
 function getSampleResponse(req, port) {
   const responses = {
     "OPTIONS": {
       "sampleHeaders": [
--- a/services/common/tests/unit/test_kinto_updater.js
+++ b/services/common/tests/unit/test_kinto_updater.js
@@ -1,11 +1,8 @@
-/* Any copyright is dedicated to the Public Domain.
-   http://creativecommons.org/publicdomain/zero/1.0/ */
-
 Cu.import("resource://services-common/kinto-updater.js")
 Cu.import("resource://testing-common/httpd.js");
 
 var server;
 
 const PREF_KINTO_BASE = "services.kinto.base";
 const PREF_LAST_UPDATE = "services.kinto.last_update_seconds";
 const PREF_LAST_ETAG = "services.kinto.last_etag";
--- a/services/common/tests/unit/xpcshell.ini
+++ b/services/common/tests/unit/xpcshell.ini
@@ -6,16 +6,17 @@ skip-if = toolkit == 'gonk'
 support-files =
   test_storage_adapter/**
 
 # Test load modules first so syntax failures are caught early.
 [test_load_modules.js]
 
 [test_kinto.js]
 [test_kinto_updater.js]
+[test_kintoAddonPluginBlocklist.js]
 [test_kintoCertBlocklist.js]
 [test_storage_adapter.js]
 
 [test_utils_atob.js]
 [test_utils_convert_string.js]
 [test_utils_dateprefs.js]
 [test_utils_deepCopy.js]
 [test_utils_encodeBase32.js]
--- a/testing/mochitest/tests/Harness_sanity/mochitest.ini
+++ b/testing/mochitest/tests/Harness_sanity/mochitest.ini
@@ -27,16 +27,17 @@ support-files =
     file_app.sjs
     file_app.template.webapp
     app.html
 [test_SpecialPowersPushPrefEnv.html]
 [test_SimpletestGetTestFileURL.html]
 [test_SpecialPowersLoadChromeScript.html]
 support-files = SpecialPowersLoadChromeScript.js
 [test_SpecialPowersLoadChromeScript_function.html]
+[test_SpecialPowersLoadPrivilegedScript.html]
 [test_bug649012.html]
 [test_bug816847.html]
 skip-if = toolkit == 'android' || e10s #No test app installed
 [test_sanity_cleanup.html]
 [test_sanity_cleanup2.html]
 [test_sanityEventUtils.html]
 skip-if = buildapp == 'mulet' || toolkit == 'android' #bug 688052
 [test_sanitySimpletest.html]
new file mode 100644
--- /dev/null
+++ b/testing/mochitest/tests/Harness_sanity/test_SpecialPowersLoadPrivilegedScript.html
@@ -0,0 +1,37 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+  <title>Test for SpecialPowers.loadChromeScript</title>
+  <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+SimpleTest.waitForExplicitFinish();
+
+function loadPrivilegedScriptTest() {
+  var Cc = Components.classes;
+  var Ci = Components.interfaces;
+  function isMainProcess() {
+    return Cc["@mozilla.org/xre/app-info;1"].
+             getService(Ci.nsIXULRuntime).
+             processType == Ci.nsIXULRuntime.PROCESS_TYPE_DEFAULT;
+  }
+  port.postMessage({'isMainProcess': isMainProcess()});
+}
+
+var contentProcessType = SpecialPowers.isMainProcess();
+var port;
+try {
+  port = SpecialPowers.loadPrivilegedScript(loadPrivilegedScriptTest.toSource());
+} catch (e) {
+  ok(false, "loadPrivilegedScript shoulde not throw");
+}
+port.onmessage = (e) => {
+  is(contentProcessType, e.data['isMainProcess'], "content and the script should be in the same process");
+  SimpleTest.finish();
+};
+</script>
--- a/testing/specialpowers/content/specialpowersAPI.js
+++ b/testing/specialpowers/content/specialpowersAPI.js
@@ -451,16 +451,36 @@ SpecialPowersAPI.prototype = {
   get MockColorPicker() {
     return MockColorPicker;
   },
 
   get MockPermissionPrompt() {
     return MockPermissionPrompt;
   },
 
+  /*
+   * Load a privileged script that runs same-process. This is different from
+   * |loadChromeScript|, which will run in the parent process in e10s mode.
+   */
+  loadPrivilegedScript: function (aFunction) {
+    var str = "(" + aFunction.toString() + ")();";
+    var systemPrincipal = Services.scriptSecurityManager.getSystemPrincipal();
+    var sb = Cu.Sandbox(systemPrincipal);
+    var window = this.window.get();
+    var mc = new window.MessageChannel();
+    sb.port = mc.port1;
+    try {
+      sb.eval(str);
+    } catch (e) {
+      throw wrapIfUnwrapped(e);
+    }
+
+    return mc.port2;
+  },
+
   loadChromeScript: function (urlOrFunction) {
     // Create a unique id for this chrome script
     let uuidGenerator = Cc["@mozilla.org/uuid-generator;1"]
                           .getService(Ci.nsIUUIDGenerator);
     let id = uuidGenerator.generateUUID().toString();
 
     // Tells chrome code to evaluate this chrome script
     let scriptArgs = { id };
--- a/testing/taskcluster/scripts/phone-builder/post-build.sh
+++ b/testing/taskcluster/scripts/phone-builder/post-build.sh
@@ -8,17 +8,17 @@ if [ -f balrog_credentials ]; then
 fi
 
 mkdir -p $HOME/artifacts
 mkdir -p $HOME/artifacts-public
 
 DEVICE=${TARGET%%-*}
 
 mv $WORKSPACE/B2G/upload/sources.xml $HOME/artifacts/sources.xml
-mv $WORKSPACE/B2G/upload-public/b2g-*.android*-arm.tar.gz $HOME/artifacts/b2g-android*-arm.tar.gz
+mv $WORKSPACE/B2G/upload/b2g-*.linux-androideabi-arm.tar.gz $HOME/artifacts/b2g-android-arm.tar.gz
 mv $WORKSPACE/B2G/upload/${TARGET}.zip $HOME/artifacts/${TARGET}.zip
 mv $WORKSPACE/B2G/upload/gaia.zip $HOME/artifacts/gaia.zip
 
 # Upload public images as public artifacts on Nexus 4 KK and Nexus 5 L
 if [ "${TARGET}" = "nexus-4-kk" -o "${TARGET}" = "nexus-5-l" ]; then
   mv $HOME/artifacts/${TARGET}.zip $HOME/artifacts-public/
 fi
 
--- a/toolkit/components/extensions/ExtensionContent.jsm
+++ b/toolkit/components/extensions/ExtensionContent.jsm
@@ -54,16 +54,22 @@ var {
 
 function isWhenBeforeOrSame(when1, when2) {
   let table = {"document_start": 0,
                "document_end": 1,
                "document_idle": 2};
   return table[when1] <= table[when2];
 }
 
+function getInnerWindowID(window) {
+  return window.QueryInterface(Ci.nsIInterfaceRequestor)
+    .getInterface(Ci.nsIDOMWindowUtils)
+    .currentInnerWindowID;
+}
+
 // This is the fairly simple API that we inject into content
 // scripts.
 var api = context => {
   return {
     runtime: {
       connect: function(extensionId, connectInfo) {
         if (!connectInfo) {
           connectInfo = extensionId;
@@ -321,17 +327,25 @@ class ExtensionContext extends BaseConte
       // because it enables us to create the APIs object in this sandbox object and then copying it
       // into the iframe's window, see Bug 1214658 for rationale)
       this.sandbox = Cu.Sandbox(contentWindow, {
         sandboxPrototype: contentWindow,
         wantXrays: false,
         isWebExtensionContentScript: true,
       });
     } else {
+      // sandbox metadata is needed to be recognized and supported in
+      // the Developer Tools of the tab where the content script is running.
+      let metadata = {
+        "inner-window-id": getInnerWindowID(contentWindow),
+        addonId: attrs.addonId,
+      };
+
       this.sandbox = Cu.Sandbox(prin, {
+        metadata,
         sandboxPrototype: contentWindow,
         wantXrays: true,
         isWebExtensionContentScript: true,
         wantGlobalProperties: ["XMLHttpRequest"],
       });
     }
 
     let delegate = {
@@ -402,22 +416,16 @@ class ExtensionContext extends BaseConte
       Cu.createObjectIn(this.contentWindow, {defineAs: "browser"});
       Cu.createObjectIn(this.contentWindow, {defineAs: "chrome"});
     }
     Cu.nukeSandbox(this.sandbox);
     this.sandbox = null;
   }
 }
 
-function windowId(window) {
-  return window.QueryInterface(Ci.nsIInterfaceRequestor)
-               .getInterface(Ci.nsIDOMWindowUtils)
-               .currentInnerWindowID;
-}
-
 // Responsible for creating ExtensionContexts and injecting content
 // scripts into them when new documents are created.
 DocumentManager = {
   extensionCount: 0,
 
   // Map[windowId -> Map[extensionId -> ExtensionContext]]
   contentScriptWindows: new Map(),
 
@@ -552,33 +560,44 @@ DocumentManager = {
     yield window;
 
     for (let i = 0; i < docShell.childCount; i++) {
       let child = docShell.getChildAt(i).QueryInterface(Ci.nsIDocShell);
       yield* this.enumerateWindows(child);
     }
   },
 
+  getContentScriptGlobalsForWindow(window) {
+    let winId = getInnerWindowID(window);
+    let extensions = this.contentScriptWindows.get(winId);
+
+    if (extensions) {
+      return Array.from(extensions.values(), ctx => ctx.sandbox);
+    }
+
+    return [];
+  },
+
   getContentScriptContext(extensionId, window) {
-    let winId = windowId(window);
+    let winId = getInnerWindowID(window);
     if (!this.contentScriptWindows.has(winId)) {
       this.contentScriptWindows.set(winId, new Map());
     }
 
     let extensions = this.contentScriptWindows.get(winId);
     if (!extensions.has(extensionId)) {
       let context = new ExtensionContext(extensionId, window);
       extensions.set(extensionId, context);
     }
 
     return extensions.get(extensionId);
   },
 
   getExtensionPageContext(extensionId, window) {
-    let winId = windowId(window);
+    let winId = getInnerWindowID(window);
 
     let context = this.extensionPageWindows.get(winId);
     if (!context) {
       let context = new ExtensionContext(extensionId, window, {isExtensionPage: true});
       this.extensionPageWindows.set(winId, context);
     }
 
     return context;
@@ -640,17 +659,17 @@ DocumentManager = {
         for (let script of extension.scripts) {
           if (script.matches(window)) {
             let context = this.getContentScriptContext(extensionId, window);
             context.addScript(script);
           }
         }
       }
     } else {
-      let contexts = this.contentScriptWindows.get(windowId(window)) || new Map();
+      let contexts = this.contentScriptWindows.get(getInnerWindowID(window)) || new Map();
       for (let context of contexts.values()) {
         context.triggerScripts(state);
       }
     }
   },
 };
 
 // Represents a browser extension in the content process.
@@ -763,17 +782,17 @@ class ExtensionGlobal {
   }
 
   uninit() {
     this.global.sendAsyncMessage("Extension:RemoveTopWindowID", {windowId: this.windowId});
   }
 
   get messageFilterStrict() {
     return {
-      innerWindowID: windowId(this.global.content),
+      innerWindowID: getInnerWindowID(this.global.content),
     };
   }
 
   receiveMessage({target, messageName, recipient, data}) {
     switch (messageName) {
       case "Extension:Capture":
         return this.handleExtensionCapture(data.width, data.height, data.options);
       case "Extension:DetectLanguage":
@@ -866,11 +885,19 @@ this.ExtensionContent = {
   init(global) {
     this.globals.set(global, new ExtensionGlobal(global));
   },
 
   uninit(global) {
     this.globals.get(global).uninit();
     this.globals.delete(global);
   },
+
+  // This helper is exported to be integrated in the devtools RDP actors,
+  // that can use it to retrieve the existent WebExtensions ContentScripts
+  // of a target window and be able to show the ContentScripts source in the
+  // DevTools Debugger panel.
+  getContentScriptGlobalsForWindow(window) {
+    return DocumentManager.getContentScriptGlobalsForWindow(window);
+  },
 };
 
 ExtensionManager.init();
--- a/toolkit/components/extensions/ext-downloads.js
+++ b/toolkit/components/extensions/ext-downloads.js
@@ -588,16 +588,77 @@ extensions.registerSchemaAPI("downloads"
           return download.download.showContainingDirectory();
         }).then(() => {
           return true;
         }).catch(error => {
           return Promise.reject({message: error.message});
         });
       },
 
+      getFileIcon(downloadId, options) {
+        return DownloadMap.lazyInit().then(() => {
+          let size = options && options.size ? options.size : 32;
+          let download = DownloadMap.fromId(downloadId).download;
+          let pathPrefix = "";
+          let path;
+
+          if (download.succeeded) {
+            let file = FileUtils.File(download.target.path);
+            path = Services.io.newFileURI(file).spec;
+          } else {
+            path = OS.Path.basename(download.target.path);
+            pathPrefix = "//";
+          }
+
+          return new Promise((resolve, reject) => {
+            let chromeWebNav = Services.appShell.createWindowlessBrowser(true);
+            chromeWebNav
+              .QueryInterface(Ci.nsIInterfaceRequestor)
+              .getInterface(Ci.nsIDocShell)
+              .createAboutBlankContentViewer(Services.scriptSecurityManager.getSystemPrincipal());
+
+            let img = chromeWebNav.document.createElement("img");
+            img.width = size;
+            img.height = size;
+
+            let handleLoad;
+            let handleError;
+            const cleanup = () => {
+              img.removeEventListener("load", handleLoad);
+              img.removeEventListener("error", handleError);
+              chromeWebNav.close();
+              chromeWebNav = null;
+            };
+
+            handleLoad = () => {
+              let canvas = chromeWebNav.document.createElement("canvas");
+              canvas.width = size;
+              canvas.height = size;
+              let context = canvas.getContext("2d");
+              context.drawImage(img, 0, 0, size, size);
+              let dataURL = canvas.toDataURL("image/png");
+              cleanup();
+              resolve(dataURL);
+            };
+
+            handleError = (error) => {
+              Cu.reportError(error);
+              cleanup();
+              reject(new Error("An unexpected error occurred"));
+            };
+
+            img.addEventListener("load", handleLoad);
+            img.addEventListener("error", handleError);
+            img.src = `moz-icon:${pathPrefix}${path}?size=${size}`;
+          });
+        }).catch((error) => {
+          return Promise.reject({message: error.message});
+        });
+      },
+
       // When we do setShelfEnabled(), check for additional "downloads.shelf" permission.
       // i.e.:
       // setShelfEnabled(enabled) {
       //   if (!extension.hasPermission("downloads.shelf")) {
       //     throw new context.cloneScope.Error("Permission denied because 'downloads.shelf' permission is missing.");
       //   }
       //   ...
       // }
--- a/toolkit/components/extensions/schemas/downloads.json
+++ b/toolkit/components/extensions/schemas/downloads.json
@@ -515,31 +515,33 @@
             "parameters": [],
             "type": "function"
           }
         ]
       },
       {
         "name": "getFileIcon",
         "type": "function",
-        "unsupported": true,
+        "async": "callback",
         "description": "Retrieve an icon for the specified download. For new downloads, file icons are available after the <a href='#event-onCreated'>onCreated</a> event has been received. The image returned by this function while a download is in progress may be different from the image returned after the download is complete. Icon retrieval is done by querying the underlying operating system or toolkit depending on the platform. The icon that is returned will therefore depend on a number of factors including state of the download, platform, registered file types and visual theme. If a file icon cannot be determined, <a href='extension.html#property-lastError'>chrome.extension.lastError</a> will contain an error message.",
         "parameters": [
           {
             "description": "The identifier for the download.",
             "name": "downloadId",
             "type": "integer"
           },
           {
             "name": "options",
             "optional": true,
             "properties": {
               "size": {
                 "description": "The size of the icon.  The returned icon will be square with dimensions size * size pixels.  The default size for the icon is 32x32 pixels.",
                 "optional": true,
+                "minimum": 1,
+                "maximum": 127,
                 "type": "integer"
               }
             },
             "type": "object"
           },
           {
             "name": "callback",
             "parameters": [
new file mode 100644
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/file_csp.html
@@ -0,0 +1,14 @@
+<!DOCTYPE HTML>
+
+<html>
+<head>
+<meta charset="utf-8">
+</head>
+<body>
+
+<div id="test">Sample text</div>
+<img id="bad-image" src="http://example.org/tests/toolkit/components/extensions/test/mochitest/file_image_bad.png" />
+<script id="bad-script" type="text/javascript" src="http://example.org/tests/toolkit/components/extensions/test/mochitest/file_script_bad.js"></script>
+
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/file_csp.html^headers^
@@ -0,0 +1,1 @@
+Content-Security-Policy: default-src 'self'
new file mode 100644
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/file_mixed.html
@@ -0,0 +1,13 @@
+<!DOCTYPE HTML>
+
+<html>
+<head>
+<meta charset="utf-8">
+</head>
+<body>
+
+<div id="test">Sample text</div>
+<img id="bad-image" src="http://example.com/tests/toolkit/components/extensions/test/mochitest/file_image_bad.png" />
+
+</body>
+</html>
--- a/toolkit/components/extensions/test/mochitest/mochitest.ini
+++ b/toolkit/components/extensions/test/mochitest/mochitest.ini
@@ -1,12 +1,15 @@
 [DEFAULT]
 skip-if = buildapp == 'mulet' || asan
 support-files =
   head.js
+  file_mixed.html
+  file_csp.html
+  file_csp.html^headers^
   file_WebRequest_page1.html
   file_WebRequest_page2.html
   file_WebRequest_page3.html
   file_webNavigation_clientRedirect.html
   file_webNavigation_clientRedirect_httpHeaders.html
   file_webNavigation_clientRedirect_httpHeaders.html^headers^
   file_webNavigation_frameClientRedirect.html
   file_webNavigation_frameRedirect.html
@@ -32,18 +35,19 @@ support-files =
 
 [test_ext_extension.html]
 [test_ext_simple.html]
 [test_ext_schema.html]
 skip-if = e10s # Uses a console montitor. Actual code does not depend on e10s.
 [test_ext_geturl.html]
 [test_ext_contentscript.html]
 skip-if = buildapp == 'b2g' # runat != document_idle is not supported.
+[test_ext_contentscript_api_injection.html]
 [test_ext_contentscript_create_iframe.html]
-[test_ext_contentscript_api_injection.html]
+[test_ext_contentscript_devtools_metadata.html]
 [test_ext_downloads.html]
 [test_ext_exclude_include_globs.html]
 [test_ext_i18n_css.html]
 [test_ext_generate.html]
 [test_ext_idle.html]
 [test_ext_localStorage.html]
 [test_ext_onmessage_removelistener.html]
 [test_ext_notifications.html]
--- a/toolkit/components/extensions/test/mochitest/test_chrome_ext_downloads_misc.html
+++ b/toolkit/components/extensions/test/mochitest/test_chrome_ext_downloads_misc.html
@@ -737,16 +737,80 @@ add_task(function* test_erase() {
   ], {inorder: false});
   is(msg.status, "success", "received 2 onErased events");
 
   msg = yield runInExtension("search", {});
   is(msg.status, "success", "search succeded");
   is(msg.result.length, 0, "search found 0 downloads");
 });
 
+function loadImage(img, data) {
+  return new Promise((resolve) => {
+    let handle = () => {
+      img.removeEventListener("load", handle);
+      resolve();
+    };
+    img.addEventListener("load", handle);
+    img.src = data;
+  });
+}
+
+add_task(function* test_getFileIcon() {
+  let img = document.createElement("img");
+  let msg = yield runInExtension("download", {url: TXT_URL});
+  is(msg.status, "success", "download() succeeded");
+  const id = msg.result;
+
+  msg = yield runInExtension("getFileIcon", id);
+  is(msg.status, "success", "getFileIcon() succeeded");
+  yield loadImage(img, msg.result);
+  is(img.height, 32, "returns an icon with the right height");
+  is(img.width, 32, "returns an icon with the right width");
+
+  msg = yield runInExtension("waitForEvents", [
+    {type: "onCreated", data: {id, url: TXT_URL}},
+    {type: "onChanged"},
+  ]);
+  is(msg.status, "success", "got events");
+
+  msg = yield runInExtension("getFileIcon", id);
+  is(msg.status, "success", "getFileIcon() succeeded");
+  yield loadImage(img, msg.result);
+  is(img.height, 32, "returns an icon with the right height after download");
+  is(img.width, 32, "returns an icon with the right width after download");
+
+  msg = yield runInExtension("getFileIcon", id + 100);
+  is(msg.status, "error", "getFileIcon() failed");
+  ok(msg.errmsg.includes("Invalid download id"), "download id is invalid");
+
+  msg = yield runInExtension("getFileIcon", id, {size: 127});
+  is(msg.status, "success", "getFileIcon() succeeded");
+  yield loadImage(img, msg.result);
+  is(img.height, 127, "returns an icon with the right custom height");
+  is(img.width, 127, "returns an icon with the right custom width");
+
+  msg = yield runInExtension("getFileIcon", id, {size: 1});
+  is(msg.status, "success", "getFileIcon() succeeded");
+  yield loadImage(img, msg.result);
+  is(img.height, 1, "returns an icon with the right custom height");
+  is(img.width, 1, "returns an icon with the right custom width");
+
+  msg = yield runInExtension("getFileIcon", id, {size: "foo"});
+  is(msg.status, "error", "getFileIcon() fails");
+  ok(msg.errmsg.includes("Error processing size"), "size is not a number");
+
+  msg = yield runInExtension("getFileIcon", id, {size: 0});
+  is(msg.status, "error", "getFileIcon() fails");
+  ok(msg.errmsg.includes("Error processing size"), "size is too small");
+
+  msg = yield runInExtension("getFileIcon", id, {size: 128});
+  is(msg.status, "error", "getFileIcon() fails");
+  ok(msg.errmsg.includes("Error processing size"), "size is too big");
+});
+
 add_task(function* cleanup() {
   yield extension.unload();
 });
 
 </script>
 
 </body>
 </html>
new file mode 100644
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/test_ext_contentscript_devtools_metadata.html
@@ -0,0 +1,81 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+  <title>Test for Sandbox metadata on WebExtensions ContentScripts</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">
+"use strict";
+
+add_task(function* test_contentscript_devtools_sandbox_metadata() {
+  function contentScript() {
+    browser.runtime.sendMessage("contentScript.executed");
+  }
+
+  function backgroundScript() {
+    browser.runtime.onMessage.addListener((msg) => {
+      if (msg == "contentScript.executed") {
+        browser.test.notifyPass("contentScript.executed");
+      }
+    });
+  }
+
+  let extensionData = {
+    manifest: {
+      content_scripts: [
+        {
+          "matches": ["http://mochi.test/*/file_sample.html"],
+          "js": ["content_script.js"],
+          "run_at": "document_idle",
+        },
+      ],
+    },
+
+    background: "new " + backgroundScript,
+    files: {
+      "content_script.js": "new " + contentScript,
+    },
+  };
+
+  let extension = ExtensionTestUtils.loadExtension(extensionData);
+
+  yield extension.startup();
+
+  let win = window.open("file_sample.html");
+
+  let innerWindowID = SpecialPowers.wrap(win)
+                                   .QueryInterface(SpecialPowers.Ci.nsIInterfaceRequestor)
+                                   .getInterface(SpecialPowers.Ci.nsIDOMWindowUtils)
+                                   .currentInnerWindowID;
+
+  yield extension.awaitFinish("contentScript.executed");
+
+  const {ExtensionContent} = SpecialPowers.Cu.import(
+    "resource://gre/modules/ExtensionContent.jsm", {}
+  );
+
+  let res = ExtensionContent.getContentScriptGlobalsForWindow(win);
+  is(res.length, 1, "Got the expected array of globals");
+  let metadata = SpecialPowers.Cu.getSandboxMetadata(res[0]) || {};
+
+  is(metadata.addonId, extension.id, "Got the expected addonId");
+  is(metadata["inner-window-id"], innerWindowID, "Got the expected inner-window-id");
+
+  yield extension.unload();
+  info("extension unloaded");
+
+  res = ExtensionContent.getContentScriptGlobalsForWindow(win);
+  is(res.length, 0, "No content scripts globals found once the extension is unloaded");
+
+  win.close();
+});
+</script>
+
+</body>
+</html>
--- a/toolkit/components/extensions/test/mochitest/test_ext_notifications.html
+++ b/toolkit/components/extensions/test/mochitest/test_ext_notifications.html
@@ -7,16 +7,22 @@
   <script type="text/javascript" src="/tests/SimpleTest/ExtensionTestUtils.js"></script>
   <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
 </head>
 <body>
 
 <script type="text/javascript">
 "use strict";
 
+// A 1x1 PNG image.
+// Source: https://commons.wikimedia.org/wiki/File:1x1.png (Public Domain)
+let image = atob("iVBORw0KGgoAAAANSUhEUgAAAAEAAAABAQMAAAAl21bKAAAAA1BMVEUAA" +
+                 "ACnej3aAAAAAXRSTlMAQObYZgAAAApJREFUCNdjYAAAAAIAAeIhvDMAAAAASUVORK5CYII=");
+const IMAGE_ARRAYBUFFER = Uint8Array.from(image, byte => byte.charCodeAt(0)).buffer;
+
 add_task(function* test_notification() {
   function backgroundScript() {
     let opts = {
       type: "basic",
       title: "Testing Notification",
       message: "Carry on",
     };
 
@@ -136,16 +142,17 @@ add_task(function* test_notifications_em
   yield extension.awaitFinish("getAll empty");
   yield extension.unload();
 });
 
 add_task(function* test_notifications_populated_getAll() {
   function backgroundScript() {
     let opts = {
       type: "basic",
+      iconUrl: "a.png",
       title: "Testing Notification",
       message: "Carry on",
     };
 
     browser.notifications.create("p1", opts).then(() => {
       return browser.notifications.create("p2", opts);
     }).then(() => {
       return browser.notifications.getAll();
@@ -165,16 +172,19 @@ add_task(function* test_notifications_po
     });
   }
 
   let extension = ExtensionTestUtils.loadExtension({
     manifest: {
       permissions: ["notifications"],
     },
     background: `(${backgroundScript})()`,
+    files: {
+      "a.png": IMAGE_ARRAYBUFFER,
+    },
   });
   yield extension.startup();
   yield extension.awaitFinish("getAll populated");
   yield extension.unload();
 });
 
 </script>
 
--- a/toolkit/components/extensions/test/mochitest/test_ext_web_accessible_resources.html
+++ b/toolkit/components/extensions/test/mochitest/test_ext_web_accessible_resources.html
@@ -10,16 +10,56 @@
 </head>
 <body>
 
 <script type="text/javascript">
 "use strict";
 
 /* eslint-disable mozilla/balanced-listeners */
 
+SimpleTest.registerCleanupFunction(() => {
+  SpecialPowers.clearUserPref("security.mixed_content.block_display_content");
+});
+
+let image = atob("iVBORw0KGgoAAAANSUhEUgAAAAEAAAABAQMAAAAl21bKAAAAA1BMVEUAA" +
+                 "ACnej3aAAAAAXRSTlMAQObYZgAAAApJREFUCNdjYAAAAAIAAeIhvDMAAAAASUVORK5CYII=");
+const IMAGE_ARRAYBUFFER = Uint8Array.from(image, byte => byte.charCodeAt(0)).buffer;
+
+function testImageLoading(src, expectedAction) {
+  let imageLoadingPromise = new Promise((resolve, reject) => {
+    let cleanupListeners;
+    let testImage = document.createElement("img");
+    testImage.setAttribute("src", src);
+
+    let loadListener = () => {
+      cleanupListeners();
+      resolve(expectedAction === "loaded");
+    };
+
+    let errorListener = () => {
+      cleanupListeners();
+      resolve(expectedAction === "blocked");
+    };
+
+    cleanupListeners = () => {
+      testImage.removeEventListener("load", loadListener);
+      testImage.removeEventListener("error", errorListener);
+    };
+
+    testImage.addEventListener("load", loadListener);
+    testImage.addEventListener("error", errorListener);
+
+    document.body.appendChild(testImage);
+  });
+
+  imageLoadingPromise.then(success => {
+    browser.runtime.sendMessage({name: "image-loading", expectedAction, success});
+  });
+}
+
 add_task(function* test_web_accessible_resources() {
   function background() {
     let gotURL;
     let tabId;
 
     function loadFrame(url) {
       return new Promise(resolve => {
         browser.tabs.sendMessage(tabId, ["load-iframe", url], reply => {
@@ -135,12 +175,178 @@ add_task(function* test_web_accessible_r
   let win = window.open("http://example.com/");
 
   yield extension.awaitFinish("web-accessible-resources");
 
   win.close();
 
   yield extension.unload();
 });
+
+add_task(function* test_web_accessible_resources_csp() {
+  function background() {
+    browser.runtime.onMessage.addListener((msg, sender) => {
+      if (msg.name === "image-loading") {
+        browser.test.assertTrue(msg.success, `Image was ${msg.expectedAction}`);
+        browser.test.sendMessage(`image-${msg.expectedAction}`);
+      } else {
+        browser.test.sendMessage(msg);
+      }
+    });
+
+    browser.test.sendMessage("background-ready");
+  }
+
+  function content() {
+    window.addEventListener("message", function rcv(event) {
+      browser.runtime.sendMessage("script-ran");
+      window.removeEventListener("message", rcv, false);
+    }, false);
+
+    testImageLoading(browser.extension.getURL("image.png"), "loaded");
+
+    let testScript = document.createElement("script");
+    testScript.setAttribute("src", browser.extension.getURL("test_script.js"));
+    document.head.appendChild(testScript);
+    browser.runtime.sendMessage("script-loaded");
+  }
+
+  function testScript() {
+    window.postMessage("test-script-loaded", "*");
+  }
+
+  let extension = ExtensionTestUtils.loadExtension({
+    manifest: {
+      "content_scripts": [{
+        "matches": ["http://example.com/*/file_csp.html"],
+        "run_at": "document_start",
+        "js": ["content_script_helper.js", "content_script.js"],
+      }],
+      "web_accessible_resources": [
+        "image.png",
+        "test_script.js",
+      ],
+    },
+    background: `(${background})()`,
+    files: {
+      "content_script_helper.js": `${testImageLoading}`,
+      "content_script.js": `(${content})()`,
+      "test_script.js": `(${testScript})()`,
+      "image.png": IMAGE_ARRAYBUFFER,
+    },
+  });
+
+  // This is used to watch the blocked data bounce off CSP.
+  function examiner() {
+    SpecialPowers.addObserver(this, "csp-on-violate-policy", false);
+  }
+
+  let cspEventCount = 0;
+
+  examiner.prototype = {
+    observe: function(subject, topic, data) {
+      cspEventCount++;
+      let spec = SpecialPowers.wrap(subject).QueryInterface(SpecialPowers.Ci.nsIURI).spec;
+      ok(spec.includes("file_image_bad.png") || spec.includes("file_script_bad.js"),
+         `Expected file: ${spec} rejected by CSP`);
+    },
+
+    // We must eventually call this to remove the listener,
+    // or mochitests might get borked.
+    remove: function() {
+      SpecialPowers.removeObserver(this, "csp-on-violate-policy");
+    },
+  };
+
+  let observer = new examiner();
+
+  yield Promise.all([extension.startup(), extension.awaitMessage("background-ready")]);
+
+  let win = window.open("http://example.com/tests/toolkit/components/extensions/test/mochitest/file_csp.html");
+
+  yield Promise.all([
+    extension.awaitMessage("image-loaded"),
+    extension.awaitMessage("script-loaded"),
+    extension.awaitMessage("script-ran"),
+  ]);
+  is(cspEventCount, 2, "Two items were rejected by CSP");
+  win.close();
+
+  observer.remove();
+  yield extension.unload();
+});
+
+add_task(function* test_web_accessible_resources_mixed_content() {
+  function background() {
+    browser.runtime.onMessage.addListener(msg => {
+      if (msg.name === "image-loading") {
+        browser.test.assertTrue(msg.success, `Image was ${msg.expectedAction}`);
+        browser.test.sendMessage(`image-${msg.expectedAction}`);
+      } else {
+        browser.test.sendMessage(msg);
+        if (msg === "accessible-script-loaded") {
+          browser.test.notifyPass("mixed-test");
+        }
+      }
+    });
+
+    browser.test.sendMessage("background-ready");
+  }
+
+  function content() {
+    testImageLoading("http://example.com/tests/toolkit/components/extensions/test/mochitest/file_image_bad.png", "blocked");
+    testImageLoading(browser.extension.getURL("image.png"), "loaded");
+
+    let testScript = document.createElement("script");
+    testScript.setAttribute("src", browser.extension.getURL("test_script.js"));
+    document.head.appendChild(testScript);
+
+    window.addEventListener("message", event => {
+      browser.runtime.sendMessage(event.data);
+    });
+  }
+
+  function testScript() {
+    window.postMessage("accessible-script-loaded", "*");
+  }
+
+  let extension = ExtensionTestUtils.loadExtension({
+    manifest: {
+      "content_scripts": [{
+        "matches": ["https://example.com/*/file_mixed.html"],
+        "run_at": "document_start",
+        "js": ["content_script_helper.js", "content_script.js"],
+      }],
+      "web_accessible_resources": [
+        "image.png",
+        "test_script.js",
+      ],
+    },
+    background: `(${background})()`,
+    files: {
+      "content_script_helper.js": `${testImageLoading}`,
+      "content_script.js": `(${content})()`,
+      "test_script.js": `(${testScript})()`,
+      "image.png": IMAGE_ARRAYBUFFER,
+    },
+  });
+
+  SpecialPowers.setBoolPref("security.mixed_content.block_display_content", true);
+
+  yield Promise.all([extension.startup(), extension.awaitMessage("background-ready")]);
+
+  let win = window.open("https://example.com/tests/toolkit/components/extensions/test/mochitest/file_mixed.html");
+
+  yield Promise.all([
+    extension.awaitMessage("image-blocked"),
+    extension.awaitMessage("image-loaded"),
+    extension.awaitMessage("accessible-script-loaded"),
+  ]);
+  yield extension.awaitFinish("mixed-test");
+  win.close();
+
+  yield extension.unload();
+});
+
 </script>
 
 </body>
 </html>
--- a/toolkit/components/reader/JSDOMParser.js
+++ b/toolkit/components/reader/JSDOMParser.js
@@ -1,8 +1,9 @@
+/*eslint-env es6:false*/
 /*
  * DO NOT MODIFY THIS FILE DIRECTLY!
  *
  * This is a shared library that is maintained in an external repo:
  * https://github.com/mozilla/readability
  */
 
 /* This Source Code Form is subject to the terms of the Mozilla Public
@@ -693,22 +694,23 @@
       var arr = [];
       getHTML(this);
       return arr.join("");
     },
 
     set innerHTML(html) {
       var parser = new JSDOMParser();
       var node = parser.parse(html);
-      for (let i = this.childNodes.length; --i >= 0;) {
+      var i;
+      for (i = this.childNodes.length; --i >= 0;) {
         this.childNodes[i].parentNode = null;
       }
       this.childNodes = node.childNodes;
       this.children = node.children;
-      for (let i = this.childNodes.length; --i >= 0;) {
+      for (i = this.childNodes.length; --i >= 0;) {
         this.childNodes[i].parentNode = this;
       }
     },
 
     set textContent(text) {
       // clear parentNodes for existing children
       for (var i = this.childNodes.length; --i >= 0;) {
         this.childNodes[i].parentNode = null;
@@ -1083,26 +1085,26 @@
       var c = this.nextChar();
 
       if (c === undefined)
         return null;
 
       // Read any text as Text node
       if (c !== "<") {
         --this.currentChar;
-        let node = new Text();
+        var textNode = new Text();
         var n = this.html.indexOf("<", this.currentChar);
         if (n === -1) {
-          node.innerHTML = this.html.substring(this.currentChar, this.html.length);
+          textNode.innerHTML = this.html.substring(this.currentChar, this.html.length);
           this.currentChar = this.html.length;
         } else {
-          node.innerHTML = this.html.substring(this.currentChar, n);
+          textNode.innerHTML = this.html.substring(this.currentChar, n);
           this.currentChar = n;
         }
-        return node;
+        return textNode;
       }
 
       c = this.peekNext();
 
       // Read Comment node. Normally, Comment nodes know their inner
       // textContent, but we don't really care about Comment nodes (we throw
       // them away in readChildren()). So just returning an empty Comment node
       // here is sufficient.
--- a/toolkit/components/reader/Readability.js
+++ b/toolkit/components/reader/Readability.js
@@ -1,8 +1,9 @@
+/*eslint-env es6:false*/
 /*
  * DO NOT MODIFY THIS FILE DIRECTLY!
  *
  * This is a shared library that is maintained in an external repo:
  * https://github.com/mozilla/readability
  */
 
 /*
@@ -59,32 +60,33 @@ var Readability = function(uri, doc, opt
 
   // A list of the ETag headers of pages we've parsed, in case they happen to match,
   // we'll know it's a duplicate.
   this._pageETags = {};
 
   // Make an AJAX request for each page and append it to the document.
   this._curPageNum = 1;
 
+  var logEl;
+
   // Control whether log messages are sent to the console
   if (this._debug) {
-    function logEl(e) {
+    logEl = function(e) {
       var rv = e.nodeName + " ";
       if (e.nodeType == e.TEXT_NODE) {
         return rv + '("' + e.textContent + '")';
       }
       var classDesc = e.className && ("." + e.className.replace(/ /g, "."));
       var elDesc = "";
-      if (e.id) {
+      if (e.id)
         elDesc = "(#" + e.id + classDesc + ")";
-      } else if (classDesc) {
+      else if (classDesc)
         elDesc = "(" + classDesc + ")";
-      }
       return rv + elDesc;
-    }
+    };
     this.log = function () {
       if ("dump" in root) {
         var msg = Array.prototype.map.call(arguments, function(x) {
           return (x && x.nodeName) ? logEl(x) : x;
         }).join(" ");
         dump("Reader: (Readability) " + msg + "\n");
       } else if ("console" in root) {
         var args = ["Reader: (Readability) "].concat(arguments);
@@ -113,20 +115,20 @@ Readability.prototype = {
   DEFAULT_MAX_PAGES: 5,
 
   // Element tags to score by default.
   DEFAULT_TAGS_TO_SCORE: "section,h2,h3,h4,h5,h6,p,td,pre".toUpperCase().split(","),
 
   // All of the regular expressions in use within readability.
   // Defined up here so we don't instantiate them repeatedly in loops.
   REGEXPS: {
-    unlikelyCandidates: /banner|combx|comment|community|disqus|extra|foot|header|menu|related|remark|rss|share|shoutbox|sidebar|skyscraper|sponsor|ad-break|agegate|pagination|pager|popup/i,
+    unlikelyCandidates: /banner|combx|comment|community|disqus|extra|foot|header|menu|modal|related|remark|rss|share|shoutbox|sidebar|skyscraper|sponsor|ad-break|agegate|pagination|pager|popup/i,
     okMaybeItsACandidate: /and|article|body|column|main|shadow/i,
     positive: /article|body|content|entry|hentry|main|page|pagination|post|text|blog|story/i,
-    negative: /hidden|^hid$| hid$| hid |^hid |banner|combx|comment|com-|contact|foot|footer|footnote|masthead|media|meta|outbrain|promo|related|scroll|share|shoutbox|sidebar|skyscraper|sponsor|shopping|tags|tool|widget/i,
+    negative: /hidden|^hid$| hid$| hid |^hid |banner|combx|comment|com-|contact|foot|footer|footnote|masthead|media|meta|modal|outbrain|promo|related|scroll|share|shoutbox|sidebar|skyscraper|sponsor|shopping|tags|tool|widget/i,
     extraneous: /print|archive|comment|discuss|e[\-]?mail|share|reply|all|login|sign|single|utility/i,
     byline: /byline|author|dateline|writtenby/i,
     replaceFonts: /<(\/?)font[^>]*>/gi,
     normalize: /\s{2,}/g,
     videos: /\/\/(www\.)?(dailymotion|youtube|youtube-nocookie|player\.vimeo)\.com/i,
     nextLink: /(next|weiter|continue|>([^\|]|$)|»([^\|]|$))/i,
     prevLink: /(prev|earl|old|new|<|«)/i,
     whitespace: /^\s*$/,
@@ -193,22 +195,24 @@ Readability.prototype = {
     return Array.prototype.concat.apply([], nodeLists);
   },
 
   _getAllNodesWithTag: function(node, tagNames) {
     if (node.querySelectorAll) {
       return node.querySelectorAll(tagNames.join(','));
     }
     return [].concat.apply([], tagNames.map(function(tag) {
-      return node.getElementsByTagName(tag);
+      var collection = node.getElementsByTagName(tag);
+      return Array.isArray(collection) ? collection : Array.from(collection);
     }));
   },
 
   /**
-   * Converts each <a> and <img> uri in the given element to an absolute URI.
+   * Converts each <a> and <img> uri in the given element to an absolute URI,
+   * ignoring #ref URIs.
    *
    * @param Element
    * @return void
    */
   _fixRelativeUris: function(articleContent) {
     var scheme = this._uri.scheme;
     var prePath = this._uri.prePath;
     var pathBase = this._uri.pathBase;
@@ -225,16 +229,20 @@ Readability.prototype = {
       // Prepath-rooted relative URI.
       if (uri[0] == "/")
         return prePath + uri;
 
       // Dotslash relative URI.
       if (uri.indexOf("./") === 0)
         return pathBase + uri.slice(2);
 
+      // Ignore hash URIs:
+      if (uri[0] == "#")
+        return uri;
+
       // Standard relative URI; add entire path. pathBase already includes a
       // trailing "/".
       return pathBase + uri;
     }
 
     var links = articleContent.getElementsByTagName("a");
     this._forEachNode(links, function(link) {
       var href = link.getAttribute("href");
@@ -369,19 +377,19 @@ Readability.prototype = {
       // <p> block.
       var replaced = false;
 
       // If we find a <br> chain, remove the <br>s until we hit another element
       // or non-whitespace. This leaves behind the first <br> in the chain
       // (which will be replaced with a <p> later).
       while ((next = this._nextElement(next)) && (next.tagName == "BR")) {
         replaced = true;
-        let sibling = next.nextSibling;
+        var brSibling = next.nextSibling;
         next.parentNode.removeChild(next);
-        next = sibling;
+        next = brSibling;
       }
 
       // If we removed a <br> chain, replace the remaining <br> with a <p>. Add
       // all sibling nodes as children of the <p> until we hit another <br>
       // chain.
       if (replaced) {
         var p = this._doc.createElement("p");
         br.parentNode.replaceChild(p, br);
@@ -391,17 +399,17 @@ Readability.prototype = {
           // If we've hit another <br><br>, we're done adding children to this <p>.
           if (next.tagName == "BR") {
             var nextElem = this._nextElement(next);
             if (nextElem && nextElem.tagName == "BR")
               break;
           }
 
           // Otherwise, make this node a child of the new <p>.
-          let sibling = next.nextSibling;
+          var sibling = next.nextSibling;
           p.appendChild(next);
           next = sibling;
         }
       }
     });
   },
 
   _setNodeTag: function (node, tag) {
@@ -742,17 +750,22 @@ Readability.prototype = {
             this._initializeNode(ancestor);
             candidates.push(ancestor);
           }
 
           // Node score divider:
           // - parent:             1 (no division)
           // - grandparent:        2
           // - great grandparent+: ancestor level * 3
-          var scoreDivider = level < 2 ? level + 1 : level * 3;
+          if (level === 0)
+            var scoreDivider = 1;
+          else if (level === 1)
+            scoreDivider = 2;
+          else
+            scoreDivider = level * 3;
           ancestor.readability.contentScore += contentScore / scoreDivider;
         });
       });
 
       // After we've calculated scores, loop through all of the possible
       // candidate nodes we found and find the one with the highest score.
       var topCandidates = [];
       for (var c = 0, cl = candidates.length; c < cl; c += 1) {
@@ -855,17 +868,18 @@ Readability.prototype = {
             append = true;
           } else if (sibling.nodeName === "P") {
             var linkDensity = this._getLinkDensity(sibling);
             var nodeContent = this._getInnerText(sibling);
             var nodeLength = nodeContent.length;
 
             if (nodeLength > 80 && linkDensity < 0.25) {
               append = true;
-            } else if (nodeLength < 80 && linkDensity === 0 && nodeContent.search(/\.( |$)/) !== -1) {
+            } else if (nodeLength < 80 && nodeLength > 0 && linkDensity === 0 &&
+                       nodeContent.search(/\.( |$)/) !== -1) {
               append = true;
             }
           }
         }
 
         if (append) {
           this.log("Appending node:", sibling);
 
@@ -1140,17 +1154,17 @@ Readability.prototype = {
    * This is the amount of text that is inside a link divided by the total text in the node.
    *
    * @param Element
    * @return number (float)
   **/
   _getLinkDensity: function(element) {
     var textLength = this._getInnerText(element).length;
     if (textLength === 0)
-      return undefined;
+      return 0;
 
     var linkLength = 0;
 
     // XXX implement _reduceNodeList?
     this._forEachNode(element.getElementsByTagName("a"), function(linkNode) {
       linkLength += this._getInnerText(linkNode).length;
     });
 
--- a/toolkit/mozapps/extensions/AddonManager.jsm
+++ b/toolkit/mozapps/extensions/AddonManager.jsm
@@ -2779,23 +2779,120 @@ var AddonManagerInternal = {
     return aValue;
   },
 
   get hotfixID() {
     return gHotfixID;
   },
 
   webAPI: {
-    getAddonByID(id) {
+    // installs maps integer ids to AddonInstall instances.
+    installs: new Map(),
+    nextInstall: 0,
+
+    sendEvent: null,
+    setEventHandler(fn) {
+      this.sendEvent = fn;
+    },
+
+    getAddonByID(target, id) {
       return new Promise(resolve => {
         AddonManager.getAddonByID(id, (addon) => {
           resolve(webAPIForAddon(addon));
         });
       });
-    }
+    },
+
+    // helper to copy (and convert) the properties we care about
+    copyProps(install, obj) {
+      obj.state = AddonManager.stateToString(install.state);
+      obj.error = AddonManager.errorToString(install.error);
+      obj.progress = install.progress;
+      obj.maxProgress = install.maxProgress;
+    },