Merge mozilla-central to inbound. a=merge CLOSED TREE
authorGurzau Raul <rgurzau@mozilla.com>
Sat, 18 May 2019 00:49:22 +0300
changeset 474365 6e4c58629a7cf3448d2d82a8287aef23ade8f6b5
parent 474338 865768838154812b6afd52a0ced86fe582953d6f (current diff)
parent 474364 1ae707852b608ea77dc82c892f25e169cbc316b5 (diff)
child 474366 dad8b92996d58543ed200d08c8f733ff05bdaefc
push id113149
push userrgurzau@mozilla.com
push dateFri, 17 May 2019 21:50:06 +0000
treeherdermozilla-inbound@6e4c58629a7c [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmerge
milestone68.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Merge mozilla-central to inbound. a=merge CLOSED TREE
browser/components/newtab/README.md
toolkit/components/places/tests/sync/test_bookmark_validation.js
--- a/.eslintignore
+++ b/.eslintignore
@@ -96,22 +96,51 @@ devtools/server/tests/browser/storage-*.
 !devtools/server/tests/browser/storage-unsecured-iframe.html
 devtools/server/tests/browser/stylesheets-nested-iframes.html
 devtools/client/shared/webpack/shims/test/test_clipboard.html
 devtools/shared/qrcode/tests/mochitest/test_decode.html
 devtools/shared/tests/mochitest/*.html
 devtools/shared/webconsole/test/test_*.html
 devtools/client/webreplay/mochitest/examples/*.html
 
-# Soon to be removed, the new/ directory is explicitly excluded below due to
-# also being an imported repository.
-devtools/client/debugger/**
+# Ignore devtools debugger files
+# Keep in sync with devtools/client/debugger/.eslintignore
+devtools/client/debugger/assets/*
+devtools/client/debugger/src/test/examples/**
+devtools/client/debugger/src/test/integration/**
+devtools/client/debugger/src/test/unit-sources/**
+devtools/client/debugger/src/**/fixtures/**
+devtools/client/debugger/src/test/mochitest/**
+devtools/client/debugger/bin/
+devtools/client/debugger/packages/**/fixtures/**
+devtools/client/debugger/node_modules
+devtools/client/debugger/out
+
+# Ignore devtools debugger files
+# Keep in sync with devtools/client/debugger/.prettierignore
+devtools/client/debugger/src/workers/parser/tests/fixtures/functionNames.js
+devtools/client/debugger/src/workers/parser/tests/fixtures/scopes/*.js
+devtools/client/debugger/src/workers/parser/tests/fixtures/pause/*.js
+devtools/client/debugger/src/test/mochitest/examples/babel/polyfill-bundle.js
+devtools/client/debugger/src/test/mochitest/examples/babel/fixtures/*/input.js
+devtools/client/debugger/src/test/mochitest/examples/babel/fixtures/*/output.js
+devtools/client/debugger/src/test/mochitest/examples/babel/fixtures/*/output.js.map
+devtools/client/debugger/src/test/mochitest/examples/ember/quickstart
+
+# Ignore devtools debugger files which aren't intended for linting, and also
+# aren't included in any .eslintignore or .prettierignore file.
+# See https://github.com/firefox-devtools/debugger/blob/master/package.json#L24
+devtools/client/debugger/configs/**
+devtools/client/debugger/dist/**
+devtools/client/debugger/flow-typed/**
+devtools/client/debugger/images/**
+devtools/client/debugger/test/**
+devtools/client/debugger/index.html
 
 # Ignore devtools imported repositories
-devtools/client/debugger/**
 devtools/client/shared/components/reps/**
 
 # Ignore devtools preferences files
 devtools/client/preferences/**
 devtools/client/webide/preferences/**
 devtools/shared/preferences/**
 devtools/startup/preferences/devtools-startup.js
 
new file mode 100644
--- /dev/null
+++ b/.prettierrc
@@ -0,0 +1,5 @@
+{
+  "printWidth": 80,
+  "tabWidth": 2,
+  "trailingComma": "es5"
+}
--- a/browser/app/profile/firefox.js
+++ b/browser/app/profile/firefox.js
@@ -1183,17 +1183,16 @@ pref("services.sync.prefs.sync.accessibi
 pref("services.sync.prefs.sync.accessibility.typeaheadfind.linksonly", true);
 pref("services.sync.prefs.sync.addons.ignoreUserEnabledChanges", true);
 // The addons prefs related to repository verification are intentionally
 // not synced for security reasons. If a system is compromised, a user
 // could weaken the pref locally, install an add-on from an untrusted
 // source, and this would propagate automatically to other,
 // uncompromised Sync-connected devices.
 pref("services.sync.prefs.sync.browser.contentblocking.category", true);
-pref("services.sync.prefs.sync.browser.contentblocking.features.standard", true);
 pref("services.sync.prefs.sync.browser.contentblocking.features.strict", true);
 pref("services.sync.prefs.sync.browser.contentblocking.introCount", true);
 pref("services.sync.prefs.sync.browser.crashReports.unsubmittedCheck.autoSubmit2", true);
 pref("services.sync.prefs.sync.browser.ctrlTab.recentlyUsedOrder", true);
 pref("services.sync.prefs.sync.browser.download.useDownloadDir", true);
 pref("services.sync.prefs.sync.browser.formfill.enable", true);
 pref("services.sync.prefs.sync.browser.link.open_newwindow", true);
 pref("services.sync.prefs.sync.browser.newtabpage.activity-stream.showSearch", true);
@@ -1586,17 +1585,17 @@ pref("browser.contentblocking.trackingpr
 pref("browser.contentblocking.rejecttrackers.control-center.ui.enabled", true);
 
 pref("browser.contentblocking.control-center.ui.showBlockedLabels", true);
 pref("browser.contentblocking.control-center.ui.showAllowedLabels", false);
 
 pref("browser.contentblocking.cryptomining.preferences.ui.enabled", true);
 pref("browser.contentblocking.fingerprinting.preferences.ui.enabled", true);
 
-// Possible values for browser.contentblocking.features.* prefs:
+// Possible values for browser.contentblocking.features.strict pref:
 //   Tracking Protection:
 //     "tp": tracking protection enabled
 //     "-tp": tracking protection disabled
 //   Tracking Protection in private windows:
 //     "tpPrivate": tracking protection in private windows enabled
 //     "-tpPrivate": tracking protection in private windows disabled
 //   Fingerprinting:
 //     "fp": fingerprinting blocking enabled
@@ -1605,27 +1604,18 @@ pref("browser.contentblocking.fingerprin
 //     "cm": cryptomining blocking enabled
 //     "-cm": cryptomining blocking disabled
 //   Cookie behavior:
 //     "cookieBehavior0": cookie behaviour BEHAVIOR_ACCEPT
 //     "cookieBehavior1": cookie behaviour BEHAVIOR_REJECT_FOREIGN
 //     "cookieBehavior2": cookie behaviour BEHAVIOR_REJECT
 //     "cookieBehavior3": cookie behaviour BEHAVIOR_LIMIT_FOREIGN
 //     "cookieBehavior4": cookie behaviour BEHAVIOR_REJECT_TRACKER
-// One value from each section must be included in each browser.contentblocking.features.* pref.
+// One value from each section must be included in the browser.contentblocking.features.strict pref.
 pref("browser.contentblocking.features.strict", "tp,tpPrivate,cookieBehavior4,cm,fp");
-// Enable blocking access to storage from tracking resources only in nightly
-// and early beta. By default the value is "cookieBehavior0": BEHAVIOR_ACCEPT
-// Enable cryptomining blocking in standard in nightly and early beta.
-// Enable fingerprinting blocking in standard in nightly and early beta.
-#ifdef EARLY_BETA_OR_EARLIER
-pref("browser.contentblocking.features.standard", "-tp,tpPrivate,cookieBehavior4,cm,fp");
-#else
-pref("browser.contentblocking.features.standard", "-tp,tpPrivate,cookieBehavior0,-cm,-fp");
-#endif
 
 // Enable the Report Breakage UI on Nightly and Beta but not on Release yet.
 #ifdef EARLY_BETA_OR_EARLIER
 pref("browser.contentblocking.reportBreakage.enabled", true);
 #else
 pref("browser.contentblocking.reportBreakage.enabled", false);
 #endif
 // Show report breakage for tracking cookies in all channels.
--- a/browser/components/BrowserGlue.jsm
+++ b/browser/components/BrowserGlue.jsm
@@ -1034,18 +1034,18 @@ BrowserGlue.prototype = {
     os.removeObserver(this, "xpi-signature-changed");
     os.removeObserver(this, "sync-ui-state:update");
     os.removeObserver(this, "shield-init-complete");
 
     Services.prefs.removeObserver("permissions.eventTelemetry.enabled", this._togglePermissionPromptTelemetry);
     Services.prefs.removeObserver("privacy.trackingprotection", this._matchCBCategory);
     Services.prefs.removeObserver("network.cookie.cookieBehavior", this._matchCBCategory);
     Services.prefs.removeObserver(ContentBlockingCategoriesPrefs.PREF_CB_CATEGORY, this._updateCBCategory);
-    Services.prefs.removeObserver("browser.contentblocking.features.standard", this._setPrefExpectations);
-    Services.prefs.removeObserver("browser.contentblocking.features.strict", this._setPrefExpectations);
+    Services.prefs.removeObserver("privacy.trackingprotection", this._setPrefExpectations);
+    Services.prefs.removeObserver("browser.contentblocking.features.strict", this._setPrefExpectationsAndUpdate);
   },
 
   // runs on startup, before the first command line handler is invoked
   // (i.e. before the first window is opened)
   _beforeUIStartup: function BG__beforeUIStartup() {
     SessionStartup.init();
 
     if (Services.prefs.prefHasUserValue(PREF_PDFJS_ENABLED_CACHE_STATE)) {
@@ -1386,35 +1386,39 @@ BrowserGlue.prototype = {
 
     this._firstWindowTelemetry(aWindow);
     this._firstWindowLoaded();
 
     this._collectStartupConditionsTelemetry();
 
     // Set the default favicon size for UI views that use the page-icon protocol.
     PlacesUtils.favicons.setDefaultIconURIPreferredSize(16 * aWindow.devicePixelRatio);
-    this._setPrefExpectations();
+    this._setPrefExpectationsAndUpdate();
     this._matchCBCategory();
 
     // This observes the entire privacy.trackingprotection.* pref tree.
     Services.prefs.addObserver("privacy.trackingprotection", this._matchCBCategory);
     Services.prefs.addObserver("network.cookie.cookieBehavior", this._matchCBCategory);
     Services.prefs.addObserver(ContentBlockingCategoriesPrefs.PREF_CB_CATEGORY, this._updateCBCategory);
     Services.prefs.addObserver("media.autoplay.default", this._updateAutoplayPref);
-    Services.prefs.addObserver("browser.contentblocking.features.standard", this._setPrefExpectations);
-    Services.prefs.addObserver("browser.contentblocking.features.strict", this._setPrefExpectations);
+    Services.prefs.addObserver("privacy.trackingprotection", this._setPrefExpectations);
+    Services.prefs.addObserver("browser.contentblocking.features.strict", this._setPrefExpectationsAndUpdate);
   },
 
   _updateAutoplayPref() {
     let blocked = Services.prefs.getIntPref("media.autoplay.default", 1);
     Services.telemetry.scalarSet("media.autoplay_default_blocked", blocked);
   },
 
   _setPrefExpectations() {
     ContentBlockingCategoriesPrefs.setPrefExpectations();
+  },
+
+  _setPrefExpectationsAndUpdate() {
+    ContentBlockingCategoriesPrefs.setPrefExpectations();
     ContentBlockingCategoriesPrefs.updateCBCategory();
   },
 
   _matchCBCategory() {
     ContentBlockingCategoriesPrefs.matchCBCategory();
   },
 
   _updateCBCategory() {
@@ -3013,108 +3017,99 @@ BrowserGlue.prototype = {
 
   QueryInterface: ChromeUtils.generateQI([Ci.nsIObserver,
                                           Ci.nsISupportsWeakReference]),
 };
 
 var ContentBlockingCategoriesPrefs = {
   PREF_CB_CATEGORY: "browser.contentblocking.category",
   PREF_STRICT_DEF: "browser.contentblocking.features.strict",
-  PREF_STANDARD_DEF: "browser.contentblocking.features.standard",
   switchingCategory: false,
 
   setPrefExpectations() {
-    // The prefs inside CATEGORY_PREFS are initial values, these values then get set.
-    // If the pref remains null, then it will expect the default value,
-    // but the UI will not respond correctly.
+    // The prefs inside CATEGORY_PREFS are initial values.
+    // If the pref remains null, then it will expect the default value.
+    // The "standard" category is defined as expecting all 5 default values.
     this.CATEGORY_PREFS = {
       strict: {
         "network.cookie.cookieBehavior": null,
         "privacy.trackingprotection.pbmode.enabled": null,
         "privacy.trackingprotection.enabled": null,
         "privacy.trackingprotection.fingerprinting.enabled": null,
         "privacy.trackingprotection.cryptomining.enabled": null,
       },
       standard: {
         "network.cookie.cookieBehavior": null,
         "privacy.trackingprotection.pbmode.enabled": null,
         "privacy.trackingprotection.enabled": null,
         "privacy.trackingprotection.fingerprinting.enabled": null,
         "privacy.trackingprotection.cryptomining.enabled": null,
       },
     };
-    let types = ["strict", "standard"];
-    for (let type of types) {
-      let rulesArray;
-      if (type == "strict") {
-        rulesArray = Services.prefs.getStringPref(this.PREF_STRICT_DEF).split(",");
-      } else {
-        rulesArray = Services.prefs.getStringPref(this.PREF_STANDARD_DEF).split(",");
-      }
-      for (let item of rulesArray) {
-        switch (item) {
-        case "tp":
-          this.CATEGORY_PREFS[type]["privacy.trackingprotection.enabled"] = true;
-          break;
-        case "-tp":
-          this.CATEGORY_PREFS[type]["privacy.trackingprotection.enabled"] = false;
-          break;
-        case "tpPrivate":
-          this.CATEGORY_PREFS[type]["privacy.trackingprotection.pbmode.enabled"] = true;
-          break;
-        case "-tpPrivate":
-          this.CATEGORY_PREFS[type]["privacy.trackingprotection.pbmode.enabled"] = false;
-          break;
-        case "fp":
-          this.CATEGORY_PREFS[type]["privacy.trackingprotection.fingerprinting.enabled"] = true;
-          break;
-        case "-fp":
-          this.CATEGORY_PREFS[type]["privacy.trackingprotection.fingerprinting.enabled"] = false;
-          break;
-        case "cm":
-          this.CATEGORY_PREFS[type]["privacy.trackingprotection.cryptomining.enabled"] = true;
-          break;
-        case "-cm":
-          this.CATEGORY_PREFS[type]["privacy.trackingprotection.cryptomining.enabled"] = false;
-          break;
-        case "cookieBehavior0":
-          this.CATEGORY_PREFS[type]["network.cookie.cookieBehavior"] = Ci.nsICookieService.BEHAVIOR_ACCEPT;
-          break;
-        case "cookieBehavior1":
-          this.CATEGORY_PREFS[type]["network.cookie.cookieBehavior"] = Ci.nsICookieService.BEHAVIOR_REJECT_FOREIGN;
-          break;
-        case "cookieBehavior2":
-          this.CATEGORY_PREFS[type]["network.cookie.cookieBehavior"] = Ci.nsICookieService.BEHAVIOR_REJECT;
-          break;
-        case "cookieBehavior3":
-          this.CATEGORY_PREFS[type]["network.cookie.cookieBehavior"] = Ci.nsICookieService.BEHAVIOR_LIMIT_FOREIGN;
-          break;
-        case "cookieBehavior4":
-          this.CATEGORY_PREFS[type]["network.cookie.cookieBehavior"] = Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER;
-          break;
-        default:
-          Cu.reportError(`Error: Unknown rule observed ${item}`);
-        }
+    let type = "strict";
+    let rulesArray = Services.prefs.getStringPref(this.PREF_STRICT_DEF).split(",");
+    for (let item of rulesArray) {
+      switch (item) {
+      case "tp":
+        this.CATEGORY_PREFS[type]["privacy.trackingprotection.enabled"] = true;
+        break;
+      case "-tp":
+        this.CATEGORY_PREFS[type]["privacy.trackingprotection.enabled"] = false;
+        break;
+      case "tpPrivate":
+        this.CATEGORY_PREFS[type]["privacy.trackingprotection.pbmode.enabled"] = true;
+        break;
+      case "-tpPrivate":
+        this.CATEGORY_PREFS[type]["privacy.trackingprotection.pbmode.enabled"] = false;
+        break;
+      case "fp":
+        this.CATEGORY_PREFS[type]["privacy.trackingprotection.fingerprinting.enabled"] = true;
+        break;
+      case "-fp":
+        this.CATEGORY_PREFS[type]["privacy.trackingprotection.fingerprinting.enabled"] = false;
+        break;
+      case "cm":
+        this.CATEGORY_PREFS[type]["privacy.trackingprotection.cryptomining.enabled"] = true;
+        break;
+      case "-cm":
+        this.CATEGORY_PREFS[type]["privacy.trackingprotection.cryptomining.enabled"] = false;
+        break;
+      case "cookieBehavior0":
+        this.CATEGORY_PREFS[type]["network.cookie.cookieBehavior"] = Ci.nsICookieService.BEHAVIOR_ACCEPT;
+        break;
+      case "cookieBehavior1":
+        this.CATEGORY_PREFS[type]["network.cookie.cookieBehavior"] = Ci.nsICookieService.BEHAVIOR_REJECT_FOREIGN;
+        break;
+      case "cookieBehavior2":
+        this.CATEGORY_PREFS[type]["network.cookie.cookieBehavior"] = Ci.nsICookieService.BEHAVIOR_REJECT;
+        break;
+      case "cookieBehavior3":
+        this.CATEGORY_PREFS[type]["network.cookie.cookieBehavior"] = Ci.nsICookieService.BEHAVIOR_LIMIT_FOREIGN;
+        break;
+      case "cookieBehavior4":
+        this.CATEGORY_PREFS[type]["network.cookie.cookieBehavior"] = Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER;
+        break;
+      default:
+        Cu.reportError(`Error: Unknown rule observed ${item}`);
       }
     }
   },
 
   /**
    * Checks if CB prefs match perfectly with one of our pre-defined categories.
    */
   prefsMatch(category) {
     // The category pref must be either unset, or match.
     if (Services.prefs.prefHasUserValue(this.PREF_CB_CATEGORY) &&
         Services.prefs.getStringPref(this.PREF_CB_CATEGORY) != category) {
       return false;
     }
     for (let pref in this.CATEGORY_PREFS[category]) {
       let value = this.CATEGORY_PREFS[category][pref];
       if (value == null) {
-        Cu.reportError(`Error: ${pref} has not been defined in ${category}`);
         if (Services.prefs.prefHasUserValue(pref)) {
           return false;
         }
       } else {
         let prefType = Services.prefs.getPrefType(pref);
         if ((prefType == Services.prefs.PREF_BOOL && Services.prefs.getBoolPref(pref) != value) ||
             (prefType == Services.prefs.PREF_INT && Services.prefs.getIntPref(pref) != value) ||
             (prefType == Services.prefs.PREF_STRING && Services.prefs.getStringPref(pref) != value)) {
@@ -3170,17 +3165,16 @@ var ContentBlockingCategoriesPrefs = {
     if (category == "custom") {
       return;
     }
 
     for (let pref in this.CATEGORY_PREFS[category]) {
       let value = this.CATEGORY_PREFS[category][pref];
       if (!Services.prefs.prefIsLocked(pref)) {
         if (value == null) {
-          Cu.reportError(`Error: ${pref} has not been defined in ${category}`);
           Services.prefs.clearUserPref(pref);
         } else {
           switch (Services.prefs.getPrefType(pref)) {
           case Services.prefs.PREF_BOOL:
             Services.prefs.setBoolPref(pref, value);
             break;
           case Services.prefs.PREF_INT:
             Services.prefs.setIntPref(pref, value);
--- a/browser/components/newtab/.mcignore
+++ b/browser/components/newtab/.mcignore
@@ -9,14 +9,17 @@ npm-debug.log
 /.git/
 /bin/prerender.js
 /bin/prerender.js.map
 /data/locales.json
 /dist/
 /logs/
 /node_modules/
 
+# ignore README since it's GitHub specific
+/README.md
+
 # also ignores ping centre tests
 ping-centre/
 
 # ignore things from about:library for now
 aboutlibrary/
 content-src/aboutlibrary/
deleted file mode 100644
--- a/browser/components/newtab/README.md
+++ /dev/null
@@ -1,24 +0,0 @@
-# activity-stream
-
-[![Task Status](https://github.taskcluster.net/v1/repository/mozilla/activity-stream/master/badge.svg)](https://github.taskcluster.net/v1/repository/mozilla/activity-stream/master/latest)
-
-This system add-on replaces the new tab page in Firefox with a new design and
-functionality as part of the Activity Stream project.
-
-The files in this directory, including vendor dependencies, are imported from the
-system-addon directory in https://github.com/mozilla/activity-stream.
-
-Read [docs/v2-system-addon](https://github.com/mozilla/activity-stream/tree/master/docs/v2-system-addon/1.GETTING_STARTED.md) for more detail.
-
-## Where should I file bugs?
-
-We regularly check the ActivityStream:NewTab component on Bugzilla.
-
-## For Developers
-
-If you are interested in contributing, take a look at [this guide](contributing.md) on where to find us and how to contribute,
-and [this guide](docs/v2-system-addon/1.GETTING_STARTED.md) for getting your development environment set up.
-
-## For Localizers
-
-Activity Stream localization is managed via [Pontoon](https://pontoon.mozilla.org/projects/activity-stream-new-tab/), not direct pull requests to the repository. If you want to fix a typo, add a new language, or simply know more about localization, please get in touch with the [existing localization team](https://pontoon.mozilla.org/teams/) for your language, or Mozilla’s [l10n-drivers](https://wiki.mozilla.org/L10n:Mozilla_Team#Mozilla_Corporation) for guidance.
--- a/browser/components/newtab/content-src/asrouter/components/RichText/RichText.jsx
+++ b/browser/components/newtab/content-src/asrouter/components/RichText/RichText.jsx
@@ -19,17 +19,18 @@ const ALLOWED_TAGS = {
  */
 export function convertLinks(links, sendClick, doNotAutoBlock, openNewWindow = false) {
   if (links) {
     return Object.keys(links).reduce((acc, linkTag) => {
       const {action} = links[linkTag];
       // Setting the value to false will not include the attribute in the anchor
       const url = action ? false : safeURI(links[linkTag].url);
 
-      acc[linkTag] = (<a href={url}
+      acc[linkTag] = (<a href={url} // eslint-disable-line jsx-a11y/anchor-has-content
+        // eslint was getting a false positive caused by the dynamic injection of content.
         target={openNewWindow ? "_blank" : ""}
         data-metric={links[linkTag].metric}
         data-action={action}
         data-args={links[linkTag].args}
         data-do_not_autoblock={doNotAutoBlock}
         onClick={sendClick} />);
       return acc;
     }, {});
--- a/browser/components/newtab/content-src/asrouter/components/SnippetBase/_SnippetBase.scss
+++ b/browser/components/newtab/content-src/asrouter/components/SnippetBase/_SnippetBase.scss
@@ -93,8 +93,28 @@
   position: absolute;
   top: 0;
   width: 100%;
 
   span {
     vertical-align: middle;
   }
 }
+
+// We show snippet icons for both themes and conditionally hide
+// based on which theme is currently active
+body {
+  &:not([lwt-newtab-brighttext]) {
+    .icon-dark-theme,
+    .icon.icon-dark-theme,
+    .scene2Icon .icon-dark-theme {
+      display: none;
+    }
+  }
+
+  &[lwt-newtab-brighttext] {
+    .icon-light-theme,
+    .icon.icon-light-theme,
+    .scene2Icon .icon-light-theme {
+      display: none;
+    }
+  }
+}
--- a/browser/components/newtab/content-src/asrouter/templates/EOYSnippet/EOYSnippet.schema.json
+++ b/browser/components/newtab/content-src/asrouter/templates/EOYSnippet/EOYSnippet.schema.json
@@ -1,12 +1,12 @@
 {
   "title": "EOYSnippet",
   "description": "Fundraising Snippet",
-  "version": "1.0.0",
+  "version": "1.1.0",
   "type": "object",
   "definitions": {
     "plainText": {
       "description": "Plain text (no HTML allowed)",
       "type": "string"
     },
     "richText": {
       "description": "Text with HTML subset allowed: i, b, u, strong, em, br",
@@ -71,26 +71,34 @@
       "type": "string",
       "description": "Default donation_amount_second. Donation amount button that's selected by default.",
       "default": "donation_amount_second"
     },
     "icon": {
       "type": "string",
       "description": "Snippet icon. 64x64px. SVG or PNG preferred."
     },
+    "icon_dark_theme": {
+      "type": "string",
+      "description": "Snippet icon. Dark theme variant. 64x64px. SVG or PNG preferred."
+    },
     "title": {
       "allOf": [
         {"$ref": "#/definitions/plainText"},
         {"description": "Snippet title displayed before snippet text"}
       ]
     },
     "title_icon": {
       "type": "string",
       "description": "Small icon that shows up before the title / text. 16x16px. SVG or PNG preferred. Grayscale."
     },
+    "title_icon_dark_theme": {
+      "type": "string",
+      "description": "Small icon that shows up before the title / text. Dark theme variant. 16x16px. SVG or PNG preferred. Grayscale."
+    },
     "button_label": {
       "allOf": [
         {"$ref": "#/definitions/plainText"},
         {"description": "Text for a button next to main snippet text that links to button_url. Requires button_url."}
       ]
     },
     "button_color": {
       "type": "string",
--- a/browser/components/newtab/content-src/asrouter/templates/FXASignupSnippet/FXASignupSnippet.schema.json
+++ b/browser/components/newtab/content-src/asrouter/templates/FXASignupSnippet/FXASignupSnippet.schema.json
@@ -1,12 +1,12 @@
 {
   "title": "FXASignupSnippet",
   "description": "A snippet template for FxA sign up/sign in",
-  "version": "1.0.0",
+  "version": "1.1.0",
   "type": "object",
   "definitions": {
     "plainText": {
       "description": "Plain text (no HTML allowed)",
       "type": "string"
     },
     "richText": {
       "description": "Text with HTML subset allowed: i, b, u, strong, em, br",
@@ -42,20 +42,28 @@
         {"$ref": "#/definitions/richText"},
         {"description": "Main body text of snippet. HTML subset allowed: i, b, u, strong, em, br"}
       ]
     },
     "scene1_icon": {
       "type": "string",
       "description": "Snippet icon. 64x64px. SVG or PNG preferred."
     },
+    "scene1_icon_dark_theme": {
+      "type": "string",
+      "description": "Snippet icon. Dark theme variant. 64x64px. SVG or PNG preferred."
+    },
     "scene1_title_icon": {
       "type": "string",
       "description": "Small icon that shows up before the title / text. 16x16px. SVG or PNG preferred. Grayscale."
     },
+    "scene1_title_icon_dark_theme": {
+      "type": "string",
+      "description": "Small icon that shows up before the title / text. Dark theme variant. 16x16px. SVG or PNG preferred. Grayscale."
+    },
     "scene2_email_placeholder_text": {
       "type": "string",
       "description": "Value to show while input is empty.",
       "default": "Your email here"
     },
     "scene2_button_label": {
       "type": "string",
       "description": "Label for form submit button",
--- a/browser/components/newtab/content-src/asrouter/templates/NewsletterSnippet/NewsletterSnippet.schema.json
+++ b/browser/components/newtab/content-src/asrouter/templates/NewsletterSnippet/NewsletterSnippet.schema.json
@@ -1,12 +1,12 @@
 {
   "title": "NewsletterSnippet",
   "description": "A snippet template for send to device mobile download",
-  "version": "1.0.0",
+  "version": "1.1.0",
   "type": "object",
   "definitions": {
     "plainText": {
       "description": "Plain text (no HTML allowed)",
       "type": "string"
     },
     "richText": {
       "description": "Text with HTML subset allowed: i, b, u, strong, em, br",
@@ -47,20 +47,28 @@
         {"$ref": "#/definitions/richText"},
         {"description": "Main body text of snippet. HTML subset allowed: i, b, u, strong, em, br"}
       ]
     },
     "scene1_icon": {
       "type": "string",
       "description": "Snippet icon. 64x64px. SVG or PNG preferred."
     },
+    "scene1_icon_dark_theme": {
+      "type": "string",
+      "description": "Snippet icon. Dark theme variant. 64x64px. SVG or PNG preferred."
+    },
     "scene1_title_icon": {
       "type": "string",
       "description": "Small icon that shows up before the title / text. 16x16px. SVG or PNG preferred. Grayscale."
     },
+    "scene1_title_icon_dark_theme": {
+      "type": "string",
+      "description": "Small icon that shows up before the title / text. Dark theme variant. 16x16px. SVG or PNG preferred. Grayscale."
+    },
     "scene2_email_placeholder_text": {
       "type": "string",
       "description": "Value to show while input is empty.",
       "default": "Your email here"
     },
     "scene2_button_label": {
       "type": "string",
       "description": "Label for form submit button",
--- a/browser/components/newtab/content-src/asrouter/templates/ReturnToAMO/ReturnToAMO.jsx
+++ b/browser/components/newtab/content-src/asrouter/templates/ReturnToAMO/ReturnToAMO.jsx
@@ -1,11 +1,14 @@
 import React from "react";
 import {RichText} from "../../components/RichText/RichText";
 
+// Alt text if available; in the future this should come from the server. See bug 1551711
+const ICON_ALT_TEXT = "";
+
 export class ReturnToAMO extends React.PureComponent {
   constructor(props) {
     super(props);
     this.onClickAddExtension = this.onClickAddExtension.bind(this);
     this.onBlockButton = this.onBlockButton.bind(this);
   }
 
   componentDidMount() {
@@ -29,17 +32,17 @@ export class ReturnToAMO extends React.P
     document.body.classList.remove("welcome", "hide-main", "amo");
     this.props.sendUserActionTelemetry({
       event: "BLOCK",
       id: this.props.UISurface,
     });
   }
 
   renderText() {
-    const customElement = <img src={this.props.content.addon_icon} width="20px" height="20px" />;
+    const customElement = <img src={this.props.content.addon_icon} width="20px" height="20px" alt={ICON_ALT_TEXT} />;
     return (<RichText
       customElements={{icon: customElement}}
       amo_html={this.props.content.text}
       localization_id="amo_html" />);
   }
 
   render() {
     const {content} = this.props;
--- a/browser/components/newtab/content-src/asrouter/templates/SendToDeviceSnippet/SendToDeviceSnippet.schema.json
+++ b/browser/components/newtab/content-src/asrouter/templates/SendToDeviceSnippet/SendToDeviceSnippet.schema.json
@@ -1,12 +1,12 @@
 {
   "title": "SendToDeviceSnippet",
   "description": "A snippet template for send to device mobile download",
-  "version": "1.0.0",
+  "version": "1.1.0",
   "type": "object",
   "definitions": {
     "plainText": {
       "description": "Plain text (no HTML allowed)",
       "type": "string"
     },
     "richText": {
       "description": "Text with HTML subset allowed: i, b, u, strong, em, br",
@@ -52,24 +52,36 @@
         {"$ref": "#/definitions/richText"},
         {"description": "Main body text of snippet. HTML subset allowed: i, b, u, strong, em, br"}
       ]
     },
     "scene1_icon": {
       "type": "string",
       "description": "Snippet icon. 64x64px. SVG or PNG preferred."
     },
+    "scene1_icon_dark_theme": {
+      "type": "string",
+      "description": "Snippet icon. Dark theme variant. 64x64px. SVG or PNG preferred."
+    },
     "scene2_icon": {
       "type": "string",
+      "description": "(send to device) Image to display above the form. Dark theme variant. 98x98px. SVG or PNG preferred."
+    },
+    "scene2_icon_dark_theme": {
+      "type": "string",
       "description": "(send to device) Image to display above the form. 98x98px. SVG or PNG preferred."
     },
     "scene1_title_icon": {
       "type": "string",
       "description": "Small icon that shows up before the title / text. 16x16px. SVG or PNG preferred. Grayscale."
     },
+    "scene1_title_icon_dark_theme": {
+      "type": "string",
+      "description": "Small icon that shows up before the title / text. Dark theme variant. 16x16px. SVG or PNG preferred. Grayscale."
+    },
     "scene2_button_label": {
       "type": "string",
       "description": "Label for form submit button",
       "default": "Send"
     },
     "scene2_input_placeholder": {
       "type": "string",
       "description": "(send to device) Value to show while input is empty.",
--- a/browser/components/newtab/content-src/asrouter/templates/SimpleBelowSearchSnippet/SimpleBelowSearchSnippet.jsx
+++ b/browser/components/newtab/content-src/asrouter/templates/SimpleBelowSearchSnippet/SimpleBelowSearchSnippet.jsx
@@ -1,14 +1,16 @@
 import React from "react";
 import {RichText} from "../../components/RichText/RichText";
 import {safeURI} from "../../template-utils";
 import {SnippetBase} from "../../components/SnippetBase/SnippetBase";
 
 const DEFAULT_ICON_PATH = "chrome://branding/content/icon64.png";
+// Alt text if available; in the future this should come from the server. See bug 1551711
+const ICON_ALT_TEXT = "";
 
 export class SimpleBelowSearchSnippet extends React.PureComponent {
   renderText() {
     const {props} = this;
     return (<RichText text={props.content.text}
       customElements={this.props.customElements}
       localization_id="text"
       links={props.content.links}
@@ -19,16 +21,17 @@ export class SimpleBelowSearchSnippet ex
     const {props} = this;
     let className = "SimpleBelowSearchSnippet";
 
     if (props.className) {
       className += ` ${props.className}`;
     }
 
     return (<SnippetBase {...props} className={className} textStyle={this.props.textStyle}>
-      <img src={safeURI(props.content.icon) || DEFAULT_ICON_PATH} className="icon" />
+      <img src={safeURI(props.content.icon) || DEFAULT_ICON_PATH} className="icon icon-light-theme" alt={ICON_ALT_TEXT} />
+      <img src={safeURI(props.content.icon_dark_theme || props.content.icon) || DEFAULT_ICON_PATH} className="icon icon-dark-theme" alt={ICON_ALT_TEXT} />
       <div>
         <p className="body">{this.renderText()}</p>
         {this.props.extraContent}
       </div>
     </SnippetBase>);
   }
 }
--- a/browser/components/newtab/content-src/asrouter/templates/SimpleBelowSearchSnippet/SimpleBelowSearchSnippet.schema.json
+++ b/browser/components/newtab/content-src/asrouter/templates/SimpleBelowSearchSnippet/SimpleBelowSearchSnippet.schema.json
@@ -1,12 +1,12 @@
 {
   "title": "SimpleBelowSearchSnippet",
   "description": "A simple template with just an icon and rich text. It gets inserted below the Activity Stream search box.",
-  "version": "1.1.0",
+  "version": "1.2.0",
   "type": "object",
   "definitions": {
     "richText": {
       "description": "Text with HTML subset allowed: i, b, u, strong, em, br",
       "type": "string"
     },
     "link_url": {
       "description": "Target for links or buttons",
@@ -20,16 +20,20 @@
         {"$ref": "#/definitions/richText"},
         {"description": "Main body text of snippet. HTML subset allowed: i, b, u, strong, em, br"}
       ]
     },
     "icon": {
       "type": "string",
       "description": "Snippet icon. 64x64px. SVG or PNG preferred."
     },
+    "icon_dark_theme": {
+      "type": "string",
+      "description": "Snippet icon. Dark theme variant. 64x64px. SVG or PNG preferred."
+    },
     "block_button_text": {
       "type": "string",
       "description": "Tooltip text used for dismiss button.",
       "default": "Remove this"
     },
     "do_not_autoblock": {
       "type": "boolean",
       "description": "Used to prevent blocking the snippet after the CTA link has been clicked"
--- a/browser/components/newtab/content-src/asrouter/templates/SimpleSnippet/SimpleSnippet.jsx
+++ b/browser/components/newtab/content-src/asrouter/templates/SimpleSnippet/SimpleSnippet.jsx
@@ -1,16 +1,18 @@
 import {Button} from "../../components/Button/Button";
 import {ConditionalWrapper} from "../../components/ConditionalWrapper/ConditionalWrapper";
 import React from "react";
 import {RichText} from "../../components/RichText/RichText";
 import {safeURI} from "../../template-utils";
 import {SnippetBase} from "../../components/SnippetBase/SnippetBase";
 
 const DEFAULT_ICON_PATH = "chrome://branding/content/icon64.png";
+// Alt text if available; in the future this should come from the server. See bug 1551711
+const ICON_ALT_TEXT = "";
 
 export class SimpleSnippet extends React.PureComponent {
   constructor(props) {
     super(props);
     this.onButtonClick = this.onButtonClick.bind(this);
   }
 
   onButtonClick() {
@@ -36,18 +38,26 @@ export class SimpleSnippet extends React
   renderTitle() {
     const {title} = this.props.content;
     return title ?
       <h3 className={`title ${this._shouldRenderButton() ? "title-inline" : ""}`}>{this.renderTitleIcon()} {title}</h3> :
       null;
   }
 
   renderTitleIcon() {
-    const titleIcon = safeURI(this.props.content.title_icon);
-    return titleIcon ? <span className="titleIcon" style={{backgroundImage: `url("${titleIcon}")`}} /> : null;
+    const titleIconLight = safeURI(this.props.content.title_icon);
+    const titleIconDark = safeURI(this.props.content.title_icon_dark_theme || this.props.content.title_icon);
+    if (!titleIconLight) {
+      return null;
+    }
+
+    return (<React.Fragment>
+        <span className="titleIcon icon-light-theme" style={{backgroundImage: `url("${titleIconLight}")`}} />
+        <span className="titleIcon icon-dark-theme" style={{backgroundImage: `url("${titleIconDark}")`}} />
+      </React.Fragment>);
   }
 
   renderButton() {
     const {props} = this;
     if (!this._shouldRenderButton()) {
       return null;
     }
 
@@ -78,24 +88,26 @@ export class SimpleSnippet extends React
     return <div className="innerContentWrapper">{children}</div>;
   }
 
   renderSectionHeader() {
     const {props} = this;
 
     // an icon and text must be specified to render the section header
     if (props.content.section_title_icon && props.content.section_title_text) {
-      const sectionTitleIcon = safeURI(props.content.section_title_icon);
+      const sectionTitleIconLight = safeURI(props.content.section_title_icon);
+      const sectionTitleIconDark = safeURI(props.content.section_title_icon_dark_theme || props.content.section_title_icon);
       const sectionTitleURL = props.content.section_title_url;
 
       return (
         <div className="section-header">
           <h3 className="section-title">
             <ConditionalWrapper condition={sectionTitleURL} wrap={this.wrapSectionHeader(sectionTitleURL)}>
-              <span className="icon icon-small-spacer" style={{backgroundImage: `url("${sectionTitleIcon}")`}} />
+              <span className="icon icon-small-spacer icon-light-theme" style={{backgroundImage: `url("${sectionTitleIconLight}")`}} />
+              <span className="icon icon-small-spacer icon-dark-theme" style={{backgroundImage: `url("${sectionTitleIconDark}")`}} />
               <span className="section-title-text">{props.content.section_title_text}</span>
             </ConditionalWrapper>
           </h3>
         </div>
       );
     }
 
     return null;
@@ -114,17 +126,18 @@ export class SimpleSnippet extends React
     }
     if (sectionHeader) {
       className += " has-section-header";
     }
 
     return (<SnippetBase {...props} className={className} textStyle={this.props.textStyle}>
       {sectionHeader}
       <ConditionalWrapper condition={sectionHeader} wrap={this.wrapSnippetContent}>
-        <img src={safeURI(props.content.icon) || DEFAULT_ICON_PATH} className="icon" />
+        <img src={safeURI(props.content.icon) || DEFAULT_ICON_PATH} className="icon icon-light-theme" alt={ICON_ALT_TEXT} />
+        <img src={safeURI(props.content.icon_dark_theme || props.content.icon) || DEFAULT_ICON_PATH} className="icon icon-dark-theme" alt={ICON_ALT_TEXT} />
         <div>
           {this.renderTitle()} <p className="body">{this.renderText()}</p>
           {this.props.extraContent}
         </div>
         {<div>{this.renderButton()}</div>}
       </ConditionalWrapper>
     </SnippetBase>);
   }
--- a/browser/components/newtab/content-src/asrouter/templates/SimpleSnippet/SimpleSnippet.schema.json
+++ b/browser/components/newtab/content-src/asrouter/templates/SimpleSnippet/SimpleSnippet.schema.json
@@ -30,20 +30,28 @@
         {"$ref": "#/definitions/richText"},
         {"description": "Main body text of snippet. HTML subset allowed: i, b, u, strong, em, br"}
       ]
     },
     "icon": {
       "type": "string",
       "description": "Snippet icon. 64x64px. SVG or PNG preferred."
     },
+    "icon_dark_theme": {
+      "type": "string",
+      "description": "Snippet icon, dark theme variant. 64x64px. SVG or PNG preferred."
+    },
     "title_icon": {
       "type": "string",
       "description": "Small icon that shows up before the title / text. 16x16px. SVG or PNG preferred. Grayscale."
     },
+    "title_icon_dark_theme": {
+      "type": "string",
+      "description": "Small icon that shows up before the title / text. Dark theme variant. 16x16px. SVG or PNG preferred. Grayscale."
+    },
     "button_action": {
       "type": "string",
       "description": "The type of action the button should trigger."
     },
     "button_url": {
       "allOf": [
         {"$ref": "#/definitions/link_url"},
         {"description": "A url, button_label links to this"}
@@ -97,16 +105,20 @@
           "description": "Additional parameters for link action, example which specific menu the button should open"
         }
       }
     },
     "section_title_icon": {
       "type": "string",
       "description": "Section title icon. 16x16px. SVG or PNG preferred. section_title_text must also be specified to display."
     },
+    "section_title_icon_dark_theme": {
+      "type": "string",
+      "description": "Section title icon, dark theme variant. 16x16px. SVG or PNG preferred. section_title_text must also be specified to display."
+    },
     "section_title_text": {
       "type": "string",
       "description": "Section title text. section_title_icon must also be specified to display."
     },
     "section_title_url": {
       "allOf": [
         {"$ref": "#/definitions/link_url"},
         {"description": "A url, section_title_text links to this"}
--- a/browser/components/newtab/content-src/asrouter/templates/SubmitFormSnippet/SubmitFormSnippet.jsx
+++ b/browser/components/newtab/content-src/asrouter/templates/SubmitFormSnippet/SubmitFormSnippet.jsx
@@ -1,14 +1,18 @@
 import {Button} from "../../components/Button/Button";
 import React from "react";
 import {RichText} from "../../components/RichText/RichText";
+import {safeURI} from "../../template-utils";
 import {SimpleSnippet} from "../SimpleSnippet/SimpleSnippet";
 import {SnippetBase} from "../../components/SnippetBase/SnippetBase";
 
+// Alt text if available; in the future this should come from the server. See bug 1551711
+const ICON_ALT_TEXT = "";
+
 export class SubmitFormSnippet extends React.PureComponent {
   constructor(props) {
     super(props);
     this.expandSnippet = this.expandSnippet.bind(this);
     this.handleSubmit = this.handleSubmit.bind(this);
     this.handleSubmitAttempt = this.handleSubmitAttempt.bind(this);
     this.onInputChange = this.onInputChange.bind(this);
     this.state = {
@@ -150,25 +154,28 @@ export class SubmitFormSnippet extends R
     const placholder = this.props.content.scene2_email_placeholder_text || this.props.content.scene2_input_placeholder;
     return (<input
       ref="mainInput"
       type={this.props.inputType || "email"}
       className={`mainInput${(this.state.submitAttempted ? "" : " clean")}`}
       name="email"
       required={true}
       placeholder={placholder}
-      onChange={this.props.validateInput ? this.onInputChange : null}
-      autoFocus={true} />);
+      onChange={this.props.validateInput ? this.onInputChange : null} />);
   }
 
   renderSignupView() {
     const {content} = this.props;
     const containerClass = `SubmitFormSnippet ${this.props.className}`;
     return (<SnippetBase {...this.props} className={containerClass} footerDismiss={true}>
-        {content.scene2_icon ? <div className="scene2Icon"><img src={content.scene2_icon} /></div> : null}
+        {content.scene2_icon ?
+          <div className="scene2Icon">
+            <img src={safeURI(content.scene2_icon)} className="icon-light-theme" alt={ICON_ALT_TEXT} />
+            <img src={safeURI(content.scene2_icon_dark_theme || content.scene2_icon)} className="icon-dark-theme" alt={ICON_ALT_TEXT} />
+          </div> : null}
         <div className="message">
           <p>
             {content.scene2_title && <h3 className="scene2Title">{content.scene2_title}</h3>}
             {" "}
             {content.scene2_text && <RichText scene2_text={content.scene2_text} localization_id="scene2_text" />}
           </p>
         </div>
         <form action={this.props.form_action} method={this.props.form_method} onSubmit={this.handleSubmit} ref="form">
--- a/browser/components/newtab/content-src/asrouter/templates/SubmitFormSnippet/SubmitFormSnippet.schema.json
+++ b/browser/components/newtab/content-src/asrouter/templates/SubmitFormSnippet/SubmitFormSnippet.schema.json
@@ -1,12 +1,12 @@
 {
   "title": "SubmitFormSnippet",
   "description": "A template with two states: a SimpleSnippet and another that contains a form",
-  "version": "1.0.0",
+  "version": "1.1.0",
   "type": "object",
   "definitions": {
     "plainText": {
       "description": "Plain text (no HTML allowed)",
       "type": "string"
     },
     "richText": {
       "description": "Text with HTML subset allowed: i, b, u, strong, em, br",
@@ -50,20 +50,28 @@
         {"$ref": "#/definitions/richText"},
         {"description": "Main body text of snippet. HTML subset allowed: i, b, u, strong, em, br"}
       ]
     },
     "scene1_icon": {
       "type": "string",
       "description": "Snippet icon. 64x64px. SVG or PNG preferred."
     },
+    "scene1_icon_dark_theme": {
+      "type": "string",
+      "description": "Snippet icon. Dark theme variant. 64x64px. SVG or PNG preferred."
+    },
     "scene1_title_icon": {
       "type": "string",
       "description": "Small icon that shows up before the title / text. 16x16px. SVG or PNG preferred. Grayscale."
     },
+    "scene1_title_icon_dark_theme": {
+      "type": "string",
+      "description": "Small icon that shows up before the title / text. Dark theme variant. 16x16px. SVG or PNG preferred. Grayscale."
+    },
     "form_action": {
       "type": "string",
       "description": "Endpoint to submit form data."
     },
     "success_title": {
       "type": "string",
       "description": "(send to device) Title shown before text on successful registration."
     },
@@ -98,16 +106,20 @@
     "scene2_dismiss_button_text": {
       "type": "string",
       "description": "Label for the dismiss button when the sign-up form is expanded."
     },
     "scene2_icon": {
       "type": "string",
       "description": "(send to device) Image to display above the form. 98x98px. SVG or PNG preferred."
     },
+    "scene2_icon_dark_theme": {
+      "type": "string",
+      "description": "(send to device) Image to display above the form. Dark theme variant. 98x98px. SVG or PNG preferred."
+    },
     "scene2_newsletter": {
       "type": "string",
       "description": "Newsletter/basket id user is subscribing to. Must be a value from the 'Slug' column here: https://basket.mozilla.org/news/. Default 'mozilla-foundation'."
     },
     "hidden_inputs": {
       "type": "object",
       "description": "Each entry represents a hidden input, key is used as value for the name property."
     },
--- a/browser/components/newtab/content-src/asrouter/templates/Trailhead/_Trailhead.scss
+++ b/browser/components/newtab/content-src/asrouter/templates/Trailhead/_Trailhead.scss
@@ -366,19 +366,20 @@
     line-height: 1.5;
     font-weight: 200;
   }
 
   .onboardingButton {
     color: var(--newtab-text-conditional-color);
     background: var(--trailhead-card-button-background-color);
     border: 0;
-    height: 30px;
+    margin: 14px;
     min-width: 70%;
-    padding: 0 14px;
+    padding: 6px 14px;
+    white-space: pre-wrap;
 
     &:focus,
     &:hover {
       box-shadow: none;
       background: var(--trailhead-card-button-background-hover-color);
     }
 
     &:focus {
@@ -386,19 +387,18 @@
     }
 
     &:active {
       background: var(--trailhead-card-button-background-active-color);
     }
   }
 
   .onboardingButtonContainer {
-    height: 60px;
     position: absolute;
-    bottom: 0;
+    bottom: 16px;
     left: 0;
     width: 100%;
     text-align: center;
   }
 }
 
 .inline-onboarding {
   &.activity-stream.welcome {
--- a/browser/components/newtab/content-src/components/ASRouterAdmin/ASRouterAdmin.jsx
+++ b/browser/components/newtab/content-src/components/ASRouterAdmin/ASRouterAdmin.jsx
@@ -467,17 +467,17 @@ export class ASRouterAdminInner extends 
       {messagesToShow.map(msg => this.renderMessageItem(msg))}
     </tbody></table>);
   }
 
   renderMessageFilter() {
     if (!this.state.providers) {
       return null;
     }
-    return (<p>Show messages from <select value={this.state.messageFilter} onChange={this.onChangeMessageFilter}>
+    return (<p>Show messages from <select value={this.state.messageFilter} onBlur={this.onChangeMessageFilter}>
       <option value="all">all providers</option>
       {this.state.providers.map(provider => (<option key={provider.id} value={provider.id}>{provider.id}</option>))}
     </select></p>);
   }
 
   renderTableHead() {
     return (<thead>
       <tr className="message-item">
@@ -536,17 +536,17 @@ export class ASRouterAdminInner extends 
     if (!this.state.pasteFromClipboard) {
       return null;
     }
     const errors = this.refs.targetingParamsEval && this.refs.targetingParamsEval.innerText.length;
     return (
       <ModalOverlay title="New targeting parameters" button_label={errors ? "Cancel" : "Done"} onDoneButton={this.onPasteTargetingParams}>
         <div className="onboardingMessage">
           <p>
-            <textarea onChange={this.onNewTargetingParams} value={this.state.newStringTargetingParameters} autoFocus={true} rows="20" cols="60" />
+            <textarea onChange={this.onNewTargetingParams} value={this.state.newStringTargetingParameters} rows="20" cols="60" />
           </p>
           <p ref="targetingParamsEval" />
         </div>
       </ModalOverlay>
     );
   }
 
   renderTargetingParameters() {
--- a/browser/components/newtab/content-src/components/DiscoveryStreamComponents/CardGrid/CardGrid.jsx
+++ b/browser/components/newtab/content-src/components/DiscoveryStreamComponents/CardGrid/CardGrid.jsx
@@ -4,35 +4,35 @@ import React from "react";
 
 export class CardGrid extends React.PureComponent {
   renderCards() {
     const recs = this.props.data.recommendations.slice(0, this.props.items);
     const cards = [];
 
     for (let index = 0; index < this.props.items; index++) {
       const rec = recs[index];
-      cards.push(rec ? (
+      cards.push(!rec || rec.placeholder ? (
+        <PlaceholderDSCard key={`dscard-${index}`} />
+      ) : (
         <DSCard
           key={`dscard-${index}`}
           pos={rec.pos}
           campaignId={rec.campaign_id}
           image_src={rec.image_src}
           raw_image_src={rec.raw_image_src}
           title={rec.title}
           excerpt={rec.excerpt}
           url={rec.url}
           id={rec.id}
           type={this.props.type}
           context={rec.context}
           dispatch={this.props.dispatch}
           source={rec.domain}
           pocket_id={rec.pocket_id}
           bookmarkGuid={rec.bookmarkGuid} />
-      ) : (
-        <PlaceholderDSCard key={`dscard-${index}`} />
       ));
     }
 
     let divisibility = ``;
 
     if (this.props.items % 4 === 0) {
       divisibility = `divisible-by-4`;
     } else if (this.props.items % 3 === 0) {
--- a/browser/components/newtab/content-src/components/DiscoveryStreamComponents/Hero/Hero.jsx
+++ b/browser/components/newtab/content-src/components/DiscoveryStreamComponents/Hero/Hero.jsx
@@ -32,49 +32,45 @@ export class Hero extends React.PureComp
 
   renderHero() {
     let [heroRec, ...otherRecs] = this.props.data.recommendations.slice(0, this.props.items);
     this.heroRec = heroRec;
 
     const cards = [];
     for (let index = 0; index < this.props.items - 1; index++) {
       const rec = otherRecs[index];
-      cards.push(rec ? (
+      cards.push(!rec || rec.placeholder ? (
+        <PlaceholderDSCard key={`dscard-${index}`} />
+      ) : (
         <DSCard
         campaignId={rec.campaign_id}
         key={`dscard-${index}`}
         image_src={rec.image_src}
         raw_image_src={rec.raw_image_src}
         title={rec.title}
         url={rec.url}
         id={rec.id}
         pos={rec.pos}
         type={this.props.type}
         dispatch={this.props.dispatch}
         context={rec.context}
         source={rec.domain}
         pocket_id={rec.pocket_id}
         bookmarkGuid={rec.bookmarkGuid} />
-      ) : (
-        <PlaceholderDSCard key={`dscard-${index}`} />
       ));
     }
 
-    let list = (
-      <List
-        recStartingPoint={1}
-        data={this.props.data}
-        hasImages={true}
-        hasBorders={this.props.border === `border`}
-        items={this.props.items - 1}
-        type={`Hero`} />
-    );
+    let heroCard = null;
 
-    return (
-      <div className={`ds-hero ds-hero-${this.props.border}`}>
+    if (!heroRec || heroRec.placeholder) {
+      heroCard = (
+        <PlaceholderDSCard />
+      );
+    } else {
+      heroCard = (
         <div className="ds-hero-item">
           <SafeAnchor
             className="wrapper"
             dispatch={this.props.dispatch}
             onLinkClick={this.onLinkClick}
             url={heroRec.url}>
             <div className="img-wrapper">
               <DSImage extraClassNames="img" source={heroRec.image_src} rawSource={heroRec.raw_image_src} />
@@ -103,16 +99,32 @@ export class Hero extends React.PureComp
             intl={this.props.intl}
             url={heroRec.url}
             title={heroRec.title}
             source={heroRec.domain}
             type={this.props.type}
             pocket_id={heroRec.pocket_id}
             bookmarkGuid={heroRec.bookmarkGuid} />
         </div>
+      );
+    }
+
+    let list = (
+      <List
+        recStartingPoint={1}
+        data={this.props.data}
+        hasImages={true}
+        hasBorders={this.props.border === `border`}
+        items={this.props.items - 1}
+        type={`Hero`} />
+    );
+
+    return (
+      <div className={`ds-hero ds-hero-${this.props.border}`}>
+        {heroCard}
         <div className={`${this.props.subComponentType}`}>
           { this.props.subComponentType === `cards` ? cards : list }
         </div>
       </div>
     );
   }
 
   render() {
--- a/browser/components/newtab/content-src/components/DiscoveryStreamComponents/List/List.jsx
+++ b/browser/components/newtab/content-src/components/DiscoveryStreamComponents/List/List.jsx
@@ -87,34 +87,34 @@ export const PlaceholderListItem = props
  */
 export function _List(props) {
   const renderList = () => {
     const recs = props.data.recommendations.slice(props.recStartingPoint, props.recStartingPoint + props.items);
     const recMarkup = [];
 
     for (let index = 0; index < props.items; index++) {
       const rec = recs[index];
-      recMarkup.push(rec ? (
+      recMarkup.push(!rec || rec.placeholder ? (
+        <PlaceholderListItem key={`ds-list-item-${index}`} />
+      ) : (
         <ListItem key={`ds-list-item-${index}`}
         dispatch={props.dispatch}
         campaignId={rec.campaign_id}
         domain={rec.domain}
         excerpt={rec.excerpt}
         id={rec.id}
         image_src={rec.image_src}
         raw_image_src={rec.raw_image_src}
         pos={rec.pos}
         title={rec.title}
         context={rec.context}
         type={props.type}
         url={rec.url}
         pocket_id={rec.pocket_id}
         bookmarkGuid={rec.bookmarkGuid} />
-      ) : (
-        <PlaceholderListItem key={`ds-list-item-${index}`} />
       ));
     }
 
     const listStyles = [
       "ds-list",
       props.fullWidth ? "ds-list-full-width" : "",
       props.hasBorders ? "ds-list-borders" : "",
       props.hasImages ? "ds-list-images" : "",
--- a/browser/components/newtab/content-src/components/DiscoveryStreamComponents/List/_List.scss
+++ b/browser/components/newtab/content-src/components/DiscoveryStreamComponents/List/_List.scss
@@ -201,17 +201,20 @@
 
     display: flex;
     justify-content: space-between;
     height: 100%;
   }
 
   .ds-list-item-excerpt {
     @include limit-visibile-lines(2, $item-line-height, $item-font-size);
-    color: $grey-10-80;
+    @include dark-theme-only {
+      color: $grey-10-80;
+    }
+    color: $grey-50;
     margin: 4px 0 8px;
   }
 
   p {
     font-size: $item-font-size * 1px;
     line-height: $item-line-height * 1px;
     margin: 0;
   }
--- a/browser/components/newtab/content-src/lib/selectLayoutRender.js
+++ b/browser/components/newtab/content-src/lib/selectLayoutRender.js
@@ -34,88 +34,109 @@ export const selectLayoutRender = (state
     }
 
     return {
       ...data,
       recommendations,
     };
   }
 
-  function maybeInjectSpocs(data, spocsConfig) {
-    // Do we ever expect to possibly have a spoc.
-    if (data && spocsConfig && spocsConfig.positions && spocsConfig.positions.length) {
-      // We expect a spoc, spocs are loaded, but the server returned no spocs.
-      if (!spocs.data.spocs || !spocs.data.spocs.length) {
-        return data;
-      }
-
-      // We expect a spoc, spocs are loaded, and we have spocs available.
-      return rollForSpocs(data, spocsConfig);
-    }
-
-    return data;
-  }
-
   const positions = {};
   const DS_COMPONENTS = ["Message", "SectionTitle", "Navigation",
     "CardGrid", "Hero", "HorizontalRule", "List"];
 
   const filterArray = [];
 
   if (!prefs["feeds.topsites"]) {
     filterArray.push("TopSites");
   }
 
   if (!prefs["feeds.section.topstories"]) {
     filterArray.push(...DS_COMPONENTS);
   }
 
+  const placeholderComponent = component => {
+    const data = {
+      recommendations: [],
+    };
+
+    let items = 0;
+    if (component.properties && component.properties.items) {
+      items = component.properties.items;
+    }
+    for (let i = 0; i < items; i++) {
+      data.recommendations.push({"placeholder": true});
+    }
+
+    return {...component, data};
+  };
+
   const handleComponent = component => {
     positions[component.type] = positions[component.type] || 0;
 
-    let {data} = feeds.data[component.feed.url];
+    const feed = feeds.data[component.feed.url];
+    let data = {
+      recommendations: [],
+    };
+    if (feed && feed.data) {
+      data = {
+        ...feed.data,
+        recommendations: [...feed.data.recommendations],
+      };
+    }
 
     if (component && component.properties && component.properties.offset) {
       data = {
         ...data,
         recommendations: data.recommendations.slice(component.properties.offset),
       };
     }
 
-    data = maybeInjectSpocs(data, component.spocs);
+    // Do we ever expect to possibly have a spoc.
+    if (data && component.spocs && component.spocs.positions && component.spocs.positions.length) {
+      // We expect a spoc, spocs are loaded, and the server returned spocs.
+      if (spocs.loaded && spocs.data.spocs && spocs.data.spocs.length) {
+        data = rollForSpocs(data, component.spocs);
+      }
+    }
 
     let items = 0;
     if (component.properties && component.properties.items) {
       items = Math.min(component.properties.items, data.recommendations.length);
     }
 
     // loop through a component items
     // Store the items position sequentially for multiple components of the same type.
     // Example: A second card grid starts pos offset from the last card grid.
     for (let i = 0; i < items; i++) {
-      data.recommendations[i].pos = positions[component.type]++;
+      data.recommendations[i] = {
+        ...data.recommendations[i],
+        pos: positions[component.type]++,
+      };
     }
 
     return {...component, data};
   };
 
   const renderLayout = () => {
     const renderedLayoutArray = [];
     for (const row of layout.filter(r => r.components.filter(c => !filterArray.includes(c.type)).length)) {
       let components = [];
       renderedLayoutArray.push({
         ...row,
         components,
       });
-      for (const component of row.components) {
+      for (const component of row.components.filter(c => !filterArray.includes(c.type))) {
         if (component.feed) {
           const spocsConfig = component.spocs;
-          // Are we still waiting on a feed/spocs, render what we have, and bail out early.
+          // Are we still waiting on a feed/spocs, render what we have,
+          // add a placeholder for this component, and bail out early.
           if (!feeds.data[component.feed.url] ||
             (spocsConfig && spocsConfig.positions && spocsConfig.positions.length && !spocs.loaded)) {
+            components.push(placeholderComponent(component));
             return renderedLayoutArray;
           }
           components.push(handleComponent(component));
         } else {
           components.push(component);
         }
       }
     }
--- a/browser/components/newtab/contributing.md
+++ b/browser/components/newtab/contributing.md
@@ -54,16 +54,32 @@ You have identified the bug, written cod
 All code is added using a pull request against the `master` branch of our repo.  Before submitting a PR, please go through this checklist:
 - all [unit tests](#unit-tests) must pass
 - if you haven't written unit tests for your patch, eyebrows will be curmudgeonly furrowed (write unit tests!)
 - if your pull request fixes a particular ticket (it does, right?), please use the `fixes #nnn` github annotation to indicate this
 - please add a `PR / Needs review` tag to your PR (if you have permission).  This starts the code review process.  If you cannot add a tag, don't worry, we will add it during triage.
 - if you can pick a module owner to be your reviewer by including `r? @username` in the comment (if not, don't worry, we will assign a reviewer)
 - make sure your PR will merge gracefully with `master` at the time you create the PR, and that your commit history is 'clean'
 
+### Setting up pre-push hooks
+
+If you contribute often and would like to set up a pre-push hook to always run `npm lint` before you push to Github,
+you can run the following from the root of the activity-stream directory:
+
+```
+cp hooks/pre-push .git/hooks/pre-push && chmod +x .git/hooks/pre-push
+```
+
+Your hook should now run whenever you run `git push`. To skip it, use the `--no-verify` option:
+
+```
+git push --no-verify
+```
+
+
 ## Code Reviews ##
 
 You have created a PR and submitted it to the repo, and now are waiting patiently for you code review feedback.  One of the projects
 module owners will be along and will either:
 - make suggestions for some improvements
 - give you an `R+` in the comments section, indicating the review is done and the code can be merged
 
 Typically, you will iterate on the PR, making changes and pushing your changes to new commits on the PR.  When the reviewer is
--- a/browser/components/newtab/css/activity-stream-linux.css
+++ b/browser/components/newtab/css/activity-stream-linux.css
@@ -2376,18 +2376,20 @@ main {
   .ds-list-item .ds-list-item-excerpt {
     -webkit-box-orient: vertical;
     display: -webkit-box;
     font-size: 14px;
     -webkit-line-clamp: 2;
     line-height: 20px;
     max-height: 2.85714em;
     overflow: hidden;
-    color: rgba(249, 249, 250, 0.8);
+    color: #737373;
     margin: 4px 0 8px; }
+    [lwt-newtab-brighttext] .ds-list-item .ds-list-item-excerpt {
+      color: rgba(249, 249, 250, 0.8); }
   .ds-list-item p {
     font-size: 14px;
     line-height: 20px;
     margin: 0; }
   .ds-list-item .ds-list-item-info,
   .ds-list-item .ds-list-item-context {
     -webkit-box-orient: vertical;
     display: -webkit-box;
@@ -2879,16 +2881,26 @@ main {
   background: rgba(215, 215, 219, 0.6);
   text-align: center;
   position: absolute;
   top: 0;
   width: 100%; }
   .snippets-preview-banner span {
     vertical-align: middle; }
 
+body:not([lwt-newtab-brighttext]) .icon-dark-theme,
+body:not([lwt-newtab-brighttext]) .icon.icon-dark-theme,
+body:not([lwt-newtab-brighttext]) .scene2Icon .icon-dark-theme {
+  display: none; }
+
+body[lwt-newtab-brighttext] .icon-light-theme,
+body[lwt-newtab-brighttext] .icon.icon-light-theme,
+body[lwt-newtab-brighttext] .scene2Icon .icon-light-theme {
+  display: none; }
+
 .activity-stream.modal-open {
   overflow: hidden; }
 
 .modalOverlayOuter {
   background: var(--newtab-overlay-color);
   height: 100%;
   position: fixed;
   top: 0;
@@ -3982,30 +3994,30 @@ a.firstrun-link {
     margin: 0 0 60px;
     color: var(--newtab-text-conditional-color);
     line-height: 1.5;
     font-weight: 200; }
   .trailheadCard .onboardingButton {
     color: var(--newtab-text-conditional-color);
     background: var(--trailhead-card-button-background-color);
     border: 0;
-    height: 30px;
+    margin: 14px;
     min-width: 70%;
-    padding: 0 14px; }
+    padding: 6px 14px;
+    white-space: pre-wrap; }
     .trailheadCard .onboardingButton:focus, .trailheadCard .onboardingButton:hover {
       box-shadow: none;
       background: var(--trailhead-card-button-background-hover-color); }
     .trailheadCard .onboardingButton:focus {
       outline: dotted 1px; }
     .trailheadCard .onboardingButton:active {
       background: var(--trailhead-card-button-background-active-color); }
   .trailheadCard .onboardingButtonContainer {
-    height: 60px;
     position: absolute;
-    bottom: 0;
+    bottom: 16px;
     left: 0;
     width: 100%;
     text-align: center; }
 
 .inline-onboarding.activity-stream.welcome {
   overflow-y: scroll; }
 
 .inline-onboarding .modalOverlayInner {
--- a/browser/components/newtab/css/activity-stream-mac.css
+++ b/browser/components/newtab/css/activity-stream-mac.css
@@ -2379,18 +2379,20 @@ main {
   .ds-list-item .ds-list-item-excerpt {
     -webkit-box-orient: vertical;
     display: -webkit-box;
     font-size: 14px;
     -webkit-line-clamp: 2;
     line-height: 20px;
     max-height: 2.85714em;
     overflow: hidden;
-    color: rgba(249, 249, 250, 0.8);
+    color: #737373;
     margin: 4px 0 8px; }
+    [lwt-newtab-brighttext] .ds-list-item .ds-list-item-excerpt {
+      color: rgba(249, 249, 250, 0.8); }
   .ds-list-item p {
     font-size: 14px;
     line-height: 20px;
     margin: 0; }
   .ds-list-item .ds-list-item-info,
   .ds-list-item .ds-list-item-context {
     -webkit-box-orient: vertical;
     display: -webkit-box;
@@ -2882,16 +2884,26 @@ main {
   background: rgba(215, 215, 219, 0.6);
   text-align: center;
   position: absolute;
   top: 0;
   width: 100%; }
   .snippets-preview-banner span {
     vertical-align: middle; }
 
+body:not([lwt-newtab-brighttext]) .icon-dark-theme,
+body:not([lwt-newtab-brighttext]) .icon.icon-dark-theme,
+body:not([lwt-newtab-brighttext]) .scene2Icon .icon-dark-theme {
+  display: none; }
+
+body[lwt-newtab-brighttext] .icon-light-theme,
+body[lwt-newtab-brighttext] .icon.icon-light-theme,
+body[lwt-newtab-brighttext] .scene2Icon .icon-light-theme {
+  display: none; }
+
 .activity-stream.modal-open {
   overflow: hidden; }
 
 .modalOverlayOuter {
   background: var(--newtab-overlay-color);
   height: 100%;
   position: fixed;
   top: 0;
@@ -3985,30 +3997,30 @@ a.firstrun-link {
     margin: 0 0 60px;
     color: var(--newtab-text-conditional-color);
     line-height: 1.5;
     font-weight: 200; }
   .trailheadCard .onboardingButton {
     color: var(--newtab-text-conditional-color);
     background: var(--trailhead-card-button-background-color);
     border: 0;
-    height: 30px;
+    margin: 14px;
     min-width: 70%;
-    padding: 0 14px; }
+    padding: 6px 14px;
+    white-space: pre-wrap; }
     .trailheadCard .onboardingButton:focus, .trailheadCard .onboardingButton:hover {
       box-shadow: none;
       background: var(--trailhead-card-button-background-hover-color); }
     .trailheadCard .onboardingButton:focus {
       outline: dotted 1px; }
     .trailheadCard .onboardingButton:active {
       background: var(--trailhead-card-button-background-active-color); }
   .trailheadCard .onboardingButtonContainer {
-    height: 60px;
     position: absolute;
-    bottom: 0;
+    bottom: 16px;
     left: 0;
     width: 100%;
     text-align: center; }
 
 .inline-onboarding.activity-stream.welcome {
   overflow-y: scroll; }
 
 .inline-onboarding .modalOverlayInner {
--- a/browser/components/newtab/css/activity-stream-windows.css
+++ b/browser/components/newtab/css/activity-stream-windows.css
@@ -2376,18 +2376,20 @@ main {
   .ds-list-item .ds-list-item-excerpt {
     -webkit-box-orient: vertical;
     display: -webkit-box;
     font-size: 14px;
     -webkit-line-clamp: 2;
     line-height: 20px;
     max-height: 2.85714em;
     overflow: hidden;
-    color: rgba(249, 249, 250, 0.8);
+    color: #737373;
     margin: 4px 0 8px; }
+    [lwt-newtab-brighttext] .ds-list-item .ds-list-item-excerpt {
+      color: rgba(249, 249, 250, 0.8); }
   .ds-list-item p {
     font-size: 14px;
     line-height: 20px;
     margin: 0; }
   .ds-list-item .ds-list-item-info,
   .ds-list-item .ds-list-item-context {
     -webkit-box-orient: vertical;
     display: -webkit-box;
@@ -2879,16 +2881,26 @@ main {
   background: rgba(215, 215, 219, 0.6);
   text-align: center;
   position: absolute;
   top: 0;
   width: 100%; }
   .snippets-preview-banner span {
     vertical-align: middle; }
 
+body:not([lwt-newtab-brighttext]) .icon-dark-theme,
+body:not([lwt-newtab-brighttext]) .icon.icon-dark-theme,
+body:not([lwt-newtab-brighttext]) .scene2Icon .icon-dark-theme {
+  display: none; }
+
+body[lwt-newtab-brighttext] .icon-light-theme,
+body[lwt-newtab-brighttext] .icon.icon-light-theme,
+body[lwt-newtab-brighttext] .scene2Icon .icon-light-theme {
+  display: none; }
+
 .activity-stream.modal-open {
   overflow: hidden; }
 
 .modalOverlayOuter {
   background: var(--newtab-overlay-color);
   height: 100%;
   position: fixed;
   top: 0;
@@ -3982,30 +3994,30 @@ a.firstrun-link {
     margin: 0 0 60px;
     color: var(--newtab-text-conditional-color);
     line-height: 1.5;
     font-weight: 200; }
   .trailheadCard .onboardingButton {
     color: var(--newtab-text-conditional-color);
     background: var(--trailhead-card-button-background-color);
     border: 0;
-    height: 30px;
+    margin: 14px;
     min-width: 70%;
-    padding: 0 14px; }
+    padding: 6px 14px;
+    white-space: pre-wrap; }
     .trailheadCard .onboardingButton:focus, .trailheadCard .onboardingButton:hover {
       box-shadow: none;
       background: var(--trailhead-card-button-background-hover-color); }
     .trailheadCard .onboardingButton:focus {
       outline: dotted 1px; }
     .trailheadCard .onboardingButton:active {
       background: var(--trailhead-card-button-background-active-color); }
   .trailheadCard .onboardingButtonContainer {
-    height: 60px;
     position: absolute;
-    bottom: 0;
+    bottom: 16px;
     left: 0;
     width: 100%;
     text-align: center; }
 
 .inline-onboarding.activity-stream.welcome {
   overflow-y: scroll; }
 
 .inline-onboarding .modalOverlayInner {
--- a/browser/components/newtab/data/content/activity-stream.bundle.js
+++ b/browser/components/newtab/data/content/activity-stream.bundle.js
@@ -1337,17 +1337,17 @@ class ASRouterAdminInner extends react__
 
   renderMessageFilter() {
     if (!this.state.providers) {
       return null;
     }
 
     return react__WEBPACK_IMPORTED_MODULE_4___default.a.createElement("p", null, "Show messages from ", react__WEBPACK_IMPORTED_MODULE_4___default.a.createElement("select", {
       value: this.state.messageFilter,
-      onChange: this.onChangeMessageFilter
+      onBlur: this.onChangeMessageFilter
     }, react__WEBPACK_IMPORTED_MODULE_4___default.a.createElement("option", {
       value: "all"
     }, "all providers"), this.state.providers.map(provider => react__WEBPACK_IMPORTED_MODULE_4___default.a.createElement("option", {
       key: provider.id,
       value: provider.id
     }, provider.id))));
   }
 
@@ -1434,17 +1434,16 @@ class ASRouterAdminInner extends react__
       title: "New targeting parameters",
       button_label: errors ? "Cancel" : "Done",
       onDoneButton: this.onPasteTargetingParams
     }, react__WEBPACK_IMPORTED_MODULE_4___default.a.createElement("div", {
       className: "onboardingMessage"
     }, react__WEBPACK_IMPORTED_MODULE_4___default.a.createElement("p", null, react__WEBPACK_IMPORTED_MODULE_4___default.a.createElement("textarea", {
       onChange: this.onNewTargetingParams,
       value: this.state.newStringTargetingParameters,
-      autoFocus: true,
       rows: "20",
       cols: "60"
     })), react__WEBPACK_IMPORTED_MODULE_4___default.a.createElement("p", {
       ref: "targetingParamsEval"
     })));
   }
 
   renderTargetingParameters() {
@@ -2782,17 +2781,19 @@ module.exports = ReactDOM;
 
 "use strict";
 __webpack_require__.r(__webpack_exports__);
 /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "ReturnToAMO", function() { return ReturnToAMO; });
 /* harmony import */ var react__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(11);
 /* harmony import */ var react__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(react__WEBPACK_IMPORTED_MODULE_0__);
 /* harmony import */ var _components_RichText_RichText__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(18);
 
-
+ // Alt text if available; in the future this should come from the server. See bug 1551711
+
+const ICON_ALT_TEXT = "";
 class ReturnToAMO extends react__WEBPACK_IMPORTED_MODULE_0___default.a.PureComponent {
   constructor(props) {
     super(props);
     this.onClickAddExtension = this.onClickAddExtension.bind(this);
     this.onBlockButton = this.onBlockButton.bind(this);
   }
 
   componentDidMount() {
@@ -2819,17 +2820,18 @@ class ReturnToAMO extends react__WEBPACK
       id: this.props.UISurface
     });
   }
 
   renderText() {
     const customElement = react__WEBPACK_IMPORTED_MODULE_0___default.a.createElement("img", {
       src: this.props.content.addon_icon,
       width: "20px",
-      height: "20px"
+      height: "20px",
+      alt: ICON_ALT_TEXT
     });
     return react__WEBPACK_IMPORTED_MODULE_0___default.a.createElement(_components_RichText_RichText__WEBPACK_IMPORTED_MODULE_1__["RichText"], {
       customElements: {
         icon: customElement
       },
       amo_html: this.props.content.text,
       localization_id: "amo_html"
     });
@@ -2899,17 +2901,19 @@ function convertLinks(links, sendClick, 
   if (links) {
     return Object.keys(links).reduce((acc, linkTag) => {
       const {
         action
       } = links[linkTag]; // Setting the value to false will not include the attribute in the anchor
 
       const url = action ? false : Object(_template_utils__WEBPACK_IMPORTED_MODULE_3__["safeURI"])(links[linkTag].url);
       acc[linkTag] = react__WEBPACK_IMPORTED_MODULE_1___default.a.createElement("a", {
-        href: url,
+        href: url // eslint-disable-line jsx-a11y/anchor-has-content
+        // eslint was getting a false positive caused by the dynamic injection of content.
+        ,
         target: openNewWindow ? "_blank" : "",
         "data-metric": links[linkTag].metric,
         "data-action": action,
         "data-args": links[linkTag].args,
         "data-do_not_autoblock": doNotAutoBlock,
         onClick: sendClick
       });
       return acc;
@@ -2955,41 +2959,41 @@ function safeURI(url) {
 
   return isAllowed ? url : "";
 }
 
 /***/ }),
 /* 20 */
 /***/ (function(module) {
 
-module.exports = {"title":"EOYSnippet","description":"Fundraising Snippet","version":"1.0.0","type":"object","definitions":{"plainText":{"description":"Plain text (no HTML allowed)","type":"string"},"richText":{"description":"Text with HTML subset allowed: i, b, u, strong, em, br","type":"string"},"link_url":{"description":"Target for links or buttons","type":"string","format":"uri"}},"properties":{"donation_form_url":{"type":"string","description":"Url to the donation form."},"currency_code":{"type":"string","description":"The code for the currency. Examle gbp, cad, usd.","default":"usd"},"locale":{"type":"string","description":"String for the locale code.","default":"en-US"},"text":{"allOf":[{"$ref":"#/definitions/richText"},{"description":"Main body text of snippet. HTML subset allowed: i, b, u, strong, em, br"}]},"text_color":{"type":"string","description":"Modify the text message color"},"background_color":{"type":"string","description":"Snippet background color."},"highlight_color":{"type":"string","description":"Paragraph em highlight color."},"donation_amount_first":{"type":"number","description":"First button amount."},"donation_amount_second":{"type":"number","description":"Second button amount."},"donation_amount_third":{"type":"number","description":"Third button amount."},"donation_amount_fourth":{"type":"number","description":"Fourth button amount."},"selected_button":{"type":"string","description":"Default donation_amount_second. Donation amount button that's selected by default.","default":"donation_amount_second"},"icon":{"type":"string","description":"Snippet icon. 64x64px. SVG or PNG preferred."},"title":{"allOf":[{"$ref":"#/definitions/plainText"},{"description":"Snippet title displayed before snippet text"}]},"title_icon":{"type":"string","description":"Small icon that shows up before the title / text. 16x16px. SVG or PNG preferred. Grayscale."},"button_label":{"allOf":[{"$ref":"#/definitions/plainText"},{"description":"Text for a button next to main snippet text that links to button_url. Requires button_url."}]},"button_color":{"type":"string","description":"The text color of the button. Valid CSS color."},"button_background_color":{"type":"string","description":"The background color of the button. Valid CSS color."},"block_button_text":{"type":"string","description":"Tooltip text used for dismiss button."},"monthly_checkbox_label_text":{"type":"string","description":"Label text for monthly checkbox.","default":"Make my donation monthly"},"test":{"type":"string","description":"Different styles for the snippet. Options are bold and takeover."},"do_not_autoblock":{"type":"boolean","description":"Used to prevent blocking the snippet after the CTA (link or button) has been clicked"},"links":{"additionalProperties":{"url":{"allOf":[{"$ref":"#/definitions/link_url"},{"description":"The url where the link points to."}]},"metric":{"type":"string","description":"Custom event name sent with telemetry event."},"args":{"type":"string","description":"Additional parameters for link action, example which specific menu the button should open"}}}},"additionalProperties":false,"required":["text","donation_form_url","donation_amount_first","donation_amount_second","donation_amount_third","donation_amount_fourth","button_label","currency_code"],"dependencies":{"button_color":["button_label"],"button_background_color":["button_label"]}};
+module.exports = {"title":"EOYSnippet","description":"Fundraising Snippet","version":"1.1.0","type":"object","definitions":{"plainText":{"description":"Plain text (no HTML allowed)","type":"string"},"richText":{"description":"Text with HTML subset allowed: i, b, u, strong, em, br","type":"string"},"link_url":{"description":"Target for links or buttons","type":"string","format":"uri"}},"properties":{"donation_form_url":{"type":"string","description":"Url to the donation form."},"currency_code":{"type":"string","description":"The code for the currency. Examle gbp, cad, usd.","default":"usd"},"locale":{"type":"string","description":"String for the locale code.","default":"en-US"},"text":{"allOf":[{"$ref":"#/definitions/richText"},{"description":"Main body text of snippet. HTML subset allowed: i, b, u, strong, em, br"}]},"text_color":{"type":"string","description":"Modify the text message color"},"background_color":{"type":"string","description":"Snippet background color."},"highlight_color":{"type":"string","description":"Paragraph em highlight color."},"donation_amount_first":{"type":"number","description":"First button amount."},"donation_amount_second":{"type":"number","description":"Second button amount."},"donation_amount_third":{"type":"number","description":"Third button amount."},"donation_amount_fourth":{"type":"number","description":"Fourth button amount."},"selected_button":{"type":"string","description":"Default donation_amount_second. Donation amount button that's selected by default.","default":"donation_amount_second"},"icon":{"type":"string","description":"Snippet icon. 64x64px. SVG or PNG preferred."},"icon_dark_theme":{"type":"string","description":"Snippet icon. Dark theme variant. 64x64px. SVG or PNG preferred."},"title":{"allOf":[{"$ref":"#/definitions/plainText"},{"description":"Snippet title displayed before snippet text"}]},"title_icon":{"type":"string","description":"Small icon that shows up before the title / text. 16x16px. SVG or PNG preferred. Grayscale."},"title_icon_dark_theme":{"type":"string","description":"Small icon that shows up before the title / text. Dark theme variant. 16x16px. SVG or PNG preferred. Grayscale."},"button_label":{"allOf":[{"$ref":"#/definitions/plainText"},{"description":"Text for a button next to main snippet text that links to button_url. Requires button_url."}]},"button_color":{"type":"string","description":"The text color of the button. Valid CSS color."},"button_background_color":{"type":"string","description":"The background color of the button. Valid CSS color."},"block_button_text":{"type":"string","description":"Tooltip text used for dismiss button."},"monthly_checkbox_label_text":{"type":"string","description":"Label text for monthly checkbox.","default":"Make my donation monthly"},"test":{"type":"string","description":"Different styles for the snippet. Options are bold and takeover."},"do_not_autoblock":{"type":"boolean","description":"Used to prevent blocking the snippet after the CTA (link or button) has been clicked"},"links":{"additionalProperties":{"url":{"allOf":[{"$ref":"#/definitions/link_url"},{"description":"The url where the link points to."}]},"metric":{"type":"string","description":"Custom event name sent with telemetry event."},"args":{"type":"string","description":"Additional parameters for link action, example which specific menu the button should open"}}}},"additionalProperties":false,"required":["text","donation_form_url","donation_amount_first","donation_amount_second","donation_amount_third","donation_amount_fourth","button_label","currency_code"],"dependencies":{"button_color":["button_label"],"button_background_color":["button_label"]}};
 
 /***/ }),
 /* 21 */
 /***/ (function(module) {
 
-module.exports = {"title":"SimpleSnippet","description":"A simple template with an icon, text, and optional button.","version":"1.1.1","type":"object","definitions":{"plainText":{"description":"Plain text (no HTML allowed)","type":"string"},"richText":{"description":"Text with HTML subset allowed: i, b, u, strong, em, br","type":"string"},"link_url":{"description":"Target for links or buttons","type":"string","format":"uri"}},"properties":{"title":{"allOf":[{"$ref":"#/definitions/plainText"},{"description":"Snippet title displayed before snippet text"}]},"text":{"allOf":[{"$ref":"#/definitions/richText"},{"description":"Main body text of snippet. HTML subset allowed: i, b, u, strong, em, br"}]},"icon":{"type":"string","description":"Snippet icon. 64x64px. SVG or PNG preferred."},"title_icon":{"type":"string","description":"Small icon that shows up before the title / text. 16x16px. SVG or PNG preferred. Grayscale."},"button_action":{"type":"string","description":"The type of action the button should trigger."},"button_url":{"allOf":[{"$ref":"#/definitions/link_url"},{"description":"A url, button_label links to this"}]},"button_action_args":{"type":"string","description":"Additional parameters for button action, example which specific menu the button should open"},"button_label":{"allOf":[{"$ref":"#/definitions/plainText"},{"description":"Text for a button next to main snippet text that links to button_url. Requires button_url."}]},"button_color":{"type":"string","description":"The text color of the button. Valid CSS color."},"button_background_color":{"type":"string","description":"The background color of the button. Valid CSS color."},"block_button_text":{"type":"string","description":"Tooltip text used for dismiss button.","default":"Remove this"},"tall":{"type":"boolean","description":"To be used by fundraising only, increases height to roughly 120px. Defaults to false."},"do_not_autoblock":{"type":"boolean","description":"Used to prevent blocking the snippet after the CTA (link or button) has been clicked"},"links":{"additionalProperties":{"url":{"allOf":[{"$ref":"#/definitions/link_url"},{"description":"The url where the link points to."}]},"metric":{"type":"string","description":"Custom event name sent with telemetry event."},"args":{"type":"string","description":"Additional parameters for link action, example which specific menu the button should open"}}},"section_title_icon":{"type":"string","description":"Section title icon. 16x16px. SVG or PNG preferred. section_title_text must also be specified to display."},"section_title_text":{"type":"string","description":"Section title text. section_title_icon must also be specified to display."},"section_title_url":{"allOf":[{"$ref":"#/definitions/link_url"},{"description":"A url, section_title_text links to this"}]}},"additionalProperties":false,"required":["text"],"dependencies":{"button_action":["button_label"],"button_url":["button_label"],"button_color":["button_label"],"button_background_color":["button_label"],"section_title_url":["section_title_text"]}};
+module.exports = {"title":"SimpleSnippet","description":"A simple template with an icon, text, and optional button.","version":"1.1.1","type":"object","definitions":{"plainText":{"description":"Plain text (no HTML allowed)","type":"string"},"richText":{"description":"Text with HTML subset allowed: i, b, u, strong, em, br","type":"string"},"link_url":{"description":"Target for links or buttons","type":"string","format":"uri"}},"properties":{"title":{"allOf":[{"$ref":"#/definitions/plainText"},{"description":"Snippet title displayed before snippet text"}]},"text":{"allOf":[{"$ref":"#/definitions/richText"},{"description":"Main body text of snippet. HTML subset allowed: i, b, u, strong, em, br"}]},"icon":{"type":"string","description":"Snippet icon. 64x64px. SVG or PNG preferred."},"icon_dark_theme":{"type":"string","description":"Snippet icon, dark theme variant. 64x64px. SVG or PNG preferred."},"title_icon":{"type":"string","description":"Small icon that shows up before the title / text. 16x16px. SVG or PNG preferred. Grayscale."},"title_icon_dark_theme":{"type":"string","description":"Small icon that shows up before the title / text. Dark theme variant. 16x16px. SVG or PNG preferred. Grayscale."},"button_action":{"type":"string","description":"The type of action the button should trigger."},"button_url":{"allOf":[{"$ref":"#/definitions/link_url"},{"description":"A url, button_label links to this"}]},"button_action_args":{"type":"string","description":"Additional parameters for button action, example which specific menu the button should open"},"button_label":{"allOf":[{"$ref":"#/definitions/plainText"},{"description":"Text for a button next to main snippet text that links to button_url. Requires button_url."}]},"button_color":{"type":"string","description":"The text color of the button. Valid CSS color."},"button_background_color":{"type":"string","description":"The background color of the button. Valid CSS color."},"block_button_text":{"type":"string","description":"Tooltip text used for dismiss button.","default":"Remove this"},"tall":{"type":"boolean","description":"To be used by fundraising only, increases height to roughly 120px. Defaults to false."},"do_not_autoblock":{"type":"boolean","description":"Used to prevent blocking the snippet after the CTA (link or button) has been clicked"},"links":{"additionalProperties":{"url":{"allOf":[{"$ref":"#/definitions/link_url"},{"description":"The url where the link points to."}]},"metric":{"type":"string","description":"Custom event name sent with telemetry event."},"args":{"type":"string","description":"Additional parameters for link action, example which specific menu the button should open"}}},"section_title_icon":{"type":"string","description":"Section title icon. 16x16px. SVG or PNG preferred. section_title_text must also be specified to display."},"section_title_icon_dark_theme":{"type":"string","description":"Section title icon, dark theme variant. 16x16px. SVG or PNG preferred. section_title_text must also be specified to display."},"section_title_text":{"type":"string","description":"Section title text. section_title_icon must also be specified to display."},"section_title_url":{"allOf":[{"$ref":"#/definitions/link_url"},{"description":"A url, section_title_text links to this"}]}},"additionalProperties":false,"required":["text"],"dependencies":{"button_action":["button_label"],"button_url":["button_label"],"button_color":["button_label"],"button_background_color":["button_label"],"section_title_url":["section_title_text"]}};
 
 /***/ }),
 /* 22 */
 /***/ (function(module) {
 
-module.exports = {"title":"FXASignupSnippet","description":"A snippet template for FxA sign up/sign in","version":"1.0.0","type":"object","definitions":{"plainText":{"description":"Plain text (no HTML allowed)","type":"string"},"richText":{"description":"Text with HTML subset allowed: i, b, u, strong, em, br","type":"string"},"link_url":{"description":"Target for links or buttons","type":"string","format":"uri"}},"properties":{"scene1_title":{"allof":[{"$ref":"#/definitions/plainText"},{"description":"snippet title displayed before snippet text"}]},"scene1_text":{"allOf":[{"$ref":"#/definitions/richText"},{"description":"Main body text of snippet. HTML subset allowed: i, b, u, strong, em, br"}]},"scene2_title":{"allOf":[{"$ref":"#/definitions/plainText"},{"description":"Title displayed before text in scene 2. Should be plain text."}]},"scene2_text":{"allOf":[{"$ref":"#/definitions/richText"},{"description":"Main body text of snippet. HTML subset allowed: i, b, u, strong, em, br"}]},"scene1_icon":{"type":"string","description":"Snippet icon. 64x64px. SVG or PNG preferred."},"scene1_title_icon":{"type":"string","description":"Small icon that shows up before the title / text. 16x16px. SVG or PNG preferred. Grayscale."},"scene2_email_placeholder_text":{"type":"string","description":"Value to show while input is empty.","default":"Your email here"},"scene2_button_label":{"type":"string","description":"Label for form submit button","default":"Sign me up"},"scene2_dismiss_button_text":{"type":"string","description":"Label for the dismiss button when the sign-up form is expanded.","default":"Dismiss"},"hidden_inputs":{"type":"object","description":"Each entry represents a hidden input, key is used as value for the name property.","properties":{"action":{"type":"string","enum":["email"]},"context":{"type":"string","enum":["fx_desktop_v3"]},"entrypoint":{"type":"string","enum":["snippets"]},"service":{"type":"string","enum":["sync"]},"utm_content":{"type":"number","description":"Firefox version number"},"utm_source":{"type":"string","enum":["snippet"]},"utm_campaign":{"type":"string","description":"(fxa) Value to pass through to GA as utm_campaign."},"utm_term":{"type":"string","description":"(fxa) Value to pass through to GA as utm_term."},"additionalProperties":false}},"scene1_button_label":{"allOf":[{"$ref":"#/definitions/plainText"},{"description":"Text for a button next to main snippet text that links to button_url. Requires button_url."}],"default":"Learn more"},"scene1_button_color":{"type":"string","description":"The text color of the button. Valid CSS color."},"scene1_button_background_color":{"type":"string","description":"The background color of the button. Valid CSS color."},"do_not_autoblock":{"type":"boolean","description":"Used to prevent blocking the snippet after the CTA (link or button) has been clicked","default":false},"utm_campaign":{"type":"string","description":"(fxa) Value to pass through to GA as utm_campaign."},"utm_term":{"type":"string","description":"(fxa) Value to pass through to GA as utm_term."},"links":{"additionalProperties":{"url":{"allOf":[{"$ref":"#/definitions/link_url"},{"description":"The url where the link points to."}]},"metric":{"type":"string","description":"Custom event name sent with telemetry event."}}}},"additionalProperties":false,"required":["scene1_text","scene2_text","scene1_button_label"],"dependencies":{"scene1_button_color":["scene1_button_label"],"scene1_button_background_color":["scene1_button_label"]}};
+module.exports = {"title":"FXASignupSnippet","description":"A snippet template for FxA sign up/sign in","version":"1.1.0","type":"object","definitions":{"plainText":{"description":"Plain text (no HTML allowed)","type":"string"},"richText":{"description":"Text with HTML subset allowed: i, b, u, strong, em, br","type":"string"},"link_url":{"description":"Target for links or buttons","type":"string","format":"uri"}},"properties":{"scene1_title":{"allof":[{"$ref":"#/definitions/plainText"},{"description":"snippet title displayed before snippet text"}]},"scene1_text":{"allOf":[{"$ref":"#/definitions/richText"},{"description":"Main body text of snippet. HTML subset allowed: i, b, u, strong, em, br"}]},"scene2_title":{"allOf":[{"$ref":"#/definitions/plainText"},{"description":"Title displayed before text in scene 2. Should be plain text."}]},"scene2_text":{"allOf":[{"$ref":"#/definitions/richText"},{"description":"Main body text of snippet. HTML subset allowed: i, b, u, strong, em, br"}]},"scene1_icon":{"type":"string","description":"Snippet icon. 64x64px. SVG or PNG preferred."},"scene1_icon_dark_theme":{"type":"string","description":"Snippet icon. Dark theme variant. 64x64px. SVG or PNG preferred."},"scene1_title_icon":{"type":"string","description":"Small icon that shows up before the title / text. 16x16px. SVG or PNG preferred. Grayscale."},"scene1_title_icon_dark_theme":{"type":"string","description":"Small icon that shows up before the title / text. Dark theme variant. 16x16px. SVG or PNG preferred. Grayscale."},"scene2_email_placeholder_text":{"type":"string","description":"Value to show while input is empty.","default":"Your email here"},"scene2_button_label":{"type":"string","description":"Label for form submit button","default":"Sign me up"},"scene2_dismiss_button_text":{"type":"string","description":"Label for the dismiss button when the sign-up form is expanded.","default":"Dismiss"},"hidden_inputs":{"type":"object","description":"Each entry represents a hidden input, key is used as value for the name property.","properties":{"action":{"type":"string","enum":["email"]},"context":{"type":"string","enum":["fx_desktop_v3"]},"entrypoint":{"type":"string","enum":["snippets"]},"service":{"type":"string","enum":["sync"]},"utm_content":{"type":"number","description":"Firefox version number"},"utm_source":{"type":"string","enum":["snippet"]},"utm_campaign":{"type":"string","description":"(fxa) Value to pass through to GA as utm_campaign."},"utm_term":{"type":"string","description":"(fxa) Value to pass through to GA as utm_term."},"additionalProperties":false}},"scene1_button_label":{"allOf":[{"$ref":"#/definitions/plainText"},{"description":"Text for a button next to main snippet text that links to button_url. Requires button_url."}],"default":"Learn more"},"scene1_button_color":{"type":"string","description":"The text color of the button. Valid CSS color."},"scene1_button_background_color":{"type":"string","description":"The background color of the button. Valid CSS color."},"do_not_autoblock":{"type":"boolean","description":"Used to prevent blocking the snippet after the CTA (link or button) has been clicked","default":false},"utm_campaign":{"type":"string","description":"(fxa) Value to pass through to GA as utm_campaign."},"utm_term":{"type":"string","description":"(fxa) Value to pass through to GA as utm_term."},"links":{"additionalProperties":{"url":{"allOf":[{"$ref":"#/definitions/link_url"},{"description":"The url where the link points to."}]},"metric":{"type":"string","description":"Custom event name sent with telemetry event."}}}},"additionalProperties":false,"required":["scene1_text","scene2_text","scene1_button_label"],"dependencies":{"scene1_button_color":["scene1_button_label"],"scene1_button_background_color":["scene1_button_label"]}};
 
 /***/ }),
 /* 23 */
 /***/ (function(module) {
 
-module.exports = {"title":"NewsletterSnippet","description":"A snippet template for send to device mobile download","version":"1.0.0","type":"object","definitions":{"plainText":{"description":"Plain text (no HTML allowed)","type":"string"},"richText":{"description":"Text with HTML subset allowed: i, b, u, strong, em, br","type":"string"},"link_url":{"description":"Target for links or buttons","type":"string","format":"uri"}},"properties":{"locale":{"type":"string","description":"Two to five character string for the locale code","default":"en-US"},"scene1_title":{"allof":[{"$ref":"#/definitions/plainText"},{"description":"snippet title displayed before snippet text"}]},"scene1_text":{"allOf":[{"$ref":"#/definitions/richText"},{"description":"Main body text of snippet. HTML subset allowed: i, b, u, strong, em, br"}]},"scene2_title":{"allOf":[{"$ref":"#/definitions/plainText"},{"description":"Title displayed before text in scene 2. Should be plain text."}]},"scene2_text":{"allOf":[{"$ref":"#/definitions/richText"},{"description":"Main body text of snippet. HTML subset allowed: i, b, u, strong, em, br"}]},"scene1_icon":{"type":"string","description":"Snippet icon. 64x64px. SVG or PNG preferred."},"scene1_title_icon":{"type":"string","description":"Small icon that shows up before the title / text. 16x16px. SVG or PNG preferred. Grayscale."},"scene2_email_placeholder_text":{"type":"string","description":"Value to show while input is empty.","default":"Your email here"},"scene2_button_label":{"type":"string","description":"Label for form submit button","default":"Sign me up"},"scene2_privacy_html":{"type":"string","description":"(send to device) Html for disclaimer and link underneath input box."},"scene2_dismiss_button_text":{"type":"string","description":"Label for the dismiss button when the sign-up form is expanded.","default":"Dismiss"},"hidden_inputs":{"type":"object","description":"Each entry represents a hidden input, key is used as value for the name property.","properties":{"fmt":{"type":"string","description":"","default":"H"}}},"scene1_button_label":{"allOf":[{"$ref":"#/definitions/plainText"},{"description":"Text for a button next to main snippet text that links to button_url. Requires button_url."}],"default":"Learn more"},"scene1_button_color":{"type":"string","description":"The text color of the button. Valid CSS color."},"scene1_button_background_color":{"type":"string","description":"The background color of the button. Valid CSS color."},"do_not_autoblock":{"type":"boolean","description":"Used to prevent blocking the snippet after the CTA (link or button) has been clicked","default":false},"success_text":{"type":"string","description":"Message shown on successful registration."},"error_text":{"type":"string","description":"Message shown if registration failed."},"scene2_newsletter":{"type":"string","description":"Newsletter/basket id user is subscribing to.","default":"mozilla-foundation"},"links":{"additionalProperties":{"url":{"allOf":[{"$ref":"#/definitions/link_url"},{"description":"The url where the link points to."}]},"metric":{"type":"string","description":"Custom event name sent with telemetry event."}}}},"additionalProperties":false,"required":["scene1_text","scene2_text","scene1_button_label"],"dependencies":{"scene1_button_color":["scene1_button_label"],"scene1_button_background_color":["scene1_button_label"]}};
+module.exports = {"title":"NewsletterSnippet","description":"A snippet template for send to device mobile download","version":"1.1.0","type":"object","definitions":{"plainText":{"description":"Plain text (no HTML allowed)","type":"string"},"richText":{"description":"Text with HTML subset allowed: i, b, u, strong, em, br","type":"string"},"link_url":{"description":"Target for links or buttons","type":"string","format":"uri"}},"properties":{"locale":{"type":"string","description":"Two to five character string for the locale code","default":"en-US"},"scene1_title":{"allof":[{"$ref":"#/definitions/plainText"},{"description":"snippet title displayed before snippet text"}]},"scene1_text":{"allOf":[{"$ref":"#/definitions/richText"},{"description":"Main body text of snippet. HTML subset allowed: i, b, u, strong, em, br"}]},"scene2_title":{"allOf":[{"$ref":"#/definitions/plainText"},{"description":"Title displayed before text in scene 2. Should be plain text."}]},"scene2_text":{"allOf":[{"$ref":"#/definitions/richText"},{"description":"Main body text of snippet. HTML subset allowed: i, b, u, strong, em, br"}]},"scene1_icon":{"type":"string","description":"Snippet icon. 64x64px. SVG or PNG preferred."},"scene1_icon_dark_theme":{"type":"string","description":"Snippet icon. Dark theme variant. 64x64px. SVG or PNG preferred."},"scene1_title_icon":{"type":"string","description":"Small icon that shows up before the title / text. 16x16px. SVG or PNG preferred. Grayscale."},"scene1_title_icon_dark_theme":{"type":"string","description":"Small icon that shows up before the title / text. Dark theme variant. 16x16px. SVG or PNG preferred. Grayscale."},"scene2_email_placeholder_text":{"type":"string","description":"Value to show while input is empty.","default":"Your email here"},"scene2_button_label":{"type":"string","description":"Label for form submit button","default":"Sign me up"},"scene2_privacy_html":{"type":"string","description":"(send to device) Html for disclaimer and link underneath input box."},"scene2_dismiss_button_text":{"type":"string","description":"Label for the dismiss button when the sign-up form is expanded.","default":"Dismiss"},"hidden_inputs":{"type":"object","description":"Each entry represents a hidden input, key is used as value for the name property.","properties":{"fmt":{"type":"string","description":"","default":"H"}}},"scene1_button_label":{"allOf":[{"$ref":"#/definitions/plainText"},{"description":"Text for a button next to main snippet text that links to button_url. Requires button_url."}],"default":"Learn more"},"scene1_button_color":{"type":"string","description":"The text color of the button. Valid CSS color."},"scene1_button_background_color":{"type":"string","description":"The background color of the button. Valid CSS color."},"do_not_autoblock":{"type":"boolean","description":"Used to prevent blocking the snippet after the CTA (link or button) has been clicked","default":false},"success_text":{"type":"string","description":"Message shown on successful registration."},"error_text":{"type":"string","description":"Message shown if registration failed."},"scene2_newsletter":{"type":"string","description":"Newsletter/basket id user is subscribing to.","default":"mozilla-foundation"},"links":{"additionalProperties":{"url":{"allOf":[{"$ref":"#/definitions/link_url"},{"description":"The url where the link points to."}]},"metric":{"type":"string","description":"Custom event name sent with telemetry event."}}}},"additionalProperties":false,"required":["scene1_text","scene2_text","scene1_button_label"],"dependencies":{"scene1_button_color":["scene1_button_label"],"scene1_button_background_color":["scene1_button_label"]}};
 
 /***/ }),
 /* 24 */
 /***/ (function(module) {
 
-module.exports = {"title":"SendToDeviceSnippet","description":"A snippet template for send to device mobile download","version":"1.0.0","type":"object","definitions":{"plainText":{"description":"Plain text (no HTML allowed)","type":"string"},"richText":{"description":"Text with HTML subset allowed: i, b, u, strong, em, br","type":"string"},"link_url":{"description":"Target for links or buttons","type":"string","format":"uri"}},"properties":{"locale":{"type":"string","description":"Two to five character string for the locale code","default":"en-US"},"country":{"type":"string","description":"Two character string for the country code (used for SMS)","default":"us"},"scene1_title":{"allof":[{"$ref":"#/definitions/plainText"},{"description":"snippet title displayed before snippet text"}]},"scene1_text":{"allOf":[{"$ref":"#/definitions/richText"},{"description":"Main body text of snippet. HTML subset allowed: i, b, u, strong, em, br"}]},"scene2_title":{"allOf":[{"$ref":"#/definitions/plainText"},{"description":"Title displayed before text in scene 2. Should be plain text."}]},"scene2_text":{"allOf":[{"$ref":"#/definitions/richText"},{"description":"Main body text of snippet. HTML subset allowed: i, b, u, strong, em, br"}]},"scene1_icon":{"type":"string","description":"Snippet icon. 64x64px. SVG or PNG preferred."},"scene2_icon":{"type":"string","description":"(send to device) Image to display above the form. 98x98px. SVG or PNG preferred."},"scene1_title_icon":{"type":"string","description":"Small icon that shows up before the title / text. 16x16px. SVG or PNG preferred. Grayscale."},"scene2_button_label":{"type":"string","description":"Label for form submit button","default":"Send"},"scene2_input_placeholder":{"type":"string","description":"(send to device) Value to show while input is empty.","default":"Your email here"},"scene2_disclaimer_html":{"type":"string","description":"(send to device) Html for disclaimer and link underneath input box."},"scene2_dismiss_button_text":{"type":"string","description":"Label for the dismiss button when the sign-up form is expanded.","default":"Dismiss"},"hidden_inputs":{"type":"object","description":"Each entry represents a hidden input, key is used as value for the name property.","properties":{"action":{"type":"string","enum":["email"]},"context":{"type":"string","enum":["fx_desktop_v3"]},"entrypoint":{"type":"string","enum":["snippets"]},"service":{"type":"string","enum":["sync"]},"utm_content":{"type":"string","description":"Firefox version number"},"utm_source":{"type":"string","enum":["snippet"]},"utm_campaign":{"type":"string","description":"(fxa) Value to pass through to GA as utm_campaign."},"utm_term":{"type":"string","description":"(fxa) Value to pass through to GA as utm_term."},"additionalProperties":false}},"scene1_button_label":{"allOf":[{"$ref":"#/definitions/plainText"},{"description":"Text for a button next to main snippet text that links to button_url. Requires button_url."}],"default":"Learn more"},"scene1_button_color":{"type":"string","description":"The text color of the button. Valid CSS color."},"scene1_button_background_color":{"type":"string","description":"The background color of the button. Valid CSS color."},"do_not_autoblock":{"type":"boolean","description":"Used to prevent blocking the snippet after the CTA (link or button) has been clicked","default":false},"success_title":{"type":"string","description":"(send to device) Title shown before text on successful registration."},"success_text":{"type":"string","description":"Message shown on successful registration."},"error_text":{"type":"string","description":"Message shown if registration failed."},"include_sms":{"type":"boolean","description":"(send to device) Allow users to send an SMS message with the form?","default":false},"message_id_sms":{"type":"string","description":"(send to device) Newsletter/basket id representing the SMS message to be sent."},"message_id_email":{"type":"string","description":"(send to device) Newsletter/basket id representing the email message to be sent. Must be a value from the 'Slug' column here: https://basket.mozilla.org/news/."},"utm_campaign":{"type":"string","description":"(fxa) Value to pass through to GA as utm_campaign."},"utm_term":{"type":"string","description":"(fxa) Value to pass through to GA as utm_term."},"links":{"additionalProperties":{"url":{"allOf":[{"$ref":"#/definitions/link_url"},{"description":"The url where the link points to."}]},"metric":{"type":"string","description":"Custom event name sent with telemetry event."}}}},"additionalProperties":false,"required":["scene1_text","scene2_text","scene1_button_label"],"dependencies":{"scene1_button_color":["scene1_button_label"],"scene1_button_background_color":["scene1_button_label"]}};
+module.exports = {"title":"SendToDeviceSnippet","description":"A snippet template for send to device mobile download","version":"1.1.0","type":"object","definitions":{"plainText":{"description":"Plain text (no HTML allowed)","type":"string"},"richText":{"description":"Text with HTML subset allowed: i, b, u, strong, em, br","type":"string"},"link_url":{"description":"Target for links or buttons","type":"string","format":"uri"}},"properties":{"locale":{"type":"string","description":"Two to five character string for the locale code","default":"en-US"},"country":{"type":"string","description":"Two character string for the country code (used for SMS)","default":"us"},"scene1_title":{"allof":[{"$ref":"#/definitions/plainText"},{"description":"snippet title displayed before snippet text"}]},"scene1_text":{"allOf":[{"$ref":"#/definitions/richText"},{"description":"Main body text of snippet. HTML subset allowed: i, b, u, strong, em, br"}]},"scene2_title":{"allOf":[{"$ref":"#/definitions/plainText"},{"description":"Title displayed before text in scene 2. Should be plain text."}]},"scene2_text":{"allOf":[{"$ref":"#/definitions/richText"},{"description":"Main body text of snippet. HTML subset allowed: i, b, u, strong, em, br"}]},"scene1_icon":{"type":"string","description":"Snippet icon. 64x64px. SVG or PNG preferred."},"scene1_icon_dark_theme":{"type":"string","description":"Snippet icon. Dark theme variant. 64x64px. SVG or PNG preferred."},"scene2_icon":{"type":"string","description":"(send to device) Image to display above the form. Dark theme variant. 98x98px. SVG or PNG preferred."},"scene2_icon_dark_theme":{"type":"string","description":"(send to device) Image to display above the form. 98x98px. SVG or PNG preferred."},"scene1_title_icon":{"type":"string","description":"Small icon that shows up before the title / text. 16x16px. SVG or PNG preferred. Grayscale."},"scene1_title_icon_dark_theme":{"type":"string","description":"Small icon that shows up before the title / text. Dark theme variant. 16x16px. SVG or PNG preferred. Grayscale."},"scene2_button_label":{"type":"string","description":"Label for form submit button","default":"Send"},"scene2_input_placeholder":{"type":"string","description":"(send to device) Value to show while input is empty.","default":"Your email here"},"scene2_disclaimer_html":{"type":"string","description":"(send to device) Html for disclaimer and link underneath input box."},"scene2_dismiss_button_text":{"type":"string","description":"Label for the dismiss button when the sign-up form is expanded.","default":"Dismiss"},"hidden_inputs":{"type":"object","description":"Each entry represents a hidden input, key is used as value for the name property.","properties":{"action":{"type":"string","enum":["email"]},"context":{"type":"string","enum":["fx_desktop_v3"]},"entrypoint":{"type":"string","enum":["snippets"]},"service":{"type":"string","enum":["sync"]},"utm_content":{"type":"string","description":"Firefox version number"},"utm_source":{"type":"string","enum":["snippet"]},"utm_campaign":{"type":"string","description":"(fxa) Value to pass through to GA as utm_campaign."},"utm_term":{"type":"string","description":"(fxa) Value to pass through to GA as utm_term."},"additionalProperties":false}},"scene1_button_label":{"allOf":[{"$ref":"#/definitions/plainText"},{"description":"Text for a button next to main snippet text that links to button_url. Requires button_url."}],"default":"Learn more"},"scene1_button_color":{"type":"string","description":"The text color of the button. Valid CSS color."},"scene1_button_background_color":{"type":"string","description":"The background color of the button. Valid CSS color."},"do_not_autoblock":{"type":"boolean","description":"Used to prevent blocking the snippet after the CTA (link or button) has been clicked","default":false},"success_title":{"type":"string","description":"(send to device) Title shown before text on successful registration."},"success_text":{"type":"string","description":"Message shown on successful registration."},"error_text":{"type":"string","description":"Message shown if registration failed."},"include_sms":{"type":"boolean","description":"(send to device) Allow users to send an SMS message with the form?","default":false},"message_id_sms":{"type":"string","description":"(send to device) Newsletter/basket id representing the SMS message to be sent."},"message_id_email":{"type":"string","description":"(send to device) Newsletter/basket id representing the email message to be sent. Must be a value from the 'Slug' column here: https://basket.mozilla.org/news/."},"utm_campaign":{"type":"string","description":"(fxa) Value to pass through to GA as utm_campaign."},"utm_term":{"type":"string","description":"(fxa) Value to pass through to GA as utm_term."},"links":{"additionalProperties":{"url":{"allOf":[{"$ref":"#/definitions/link_url"},{"description":"The url where the link points to."}]},"metric":{"type":"string","description":"Custom event name sent with telemetry event."}}}},"additionalProperties":false,"required":["scene1_text","scene2_text","scene1_button_label"],"dependencies":{"scene1_button_color":["scene1_button_label"],"scene1_button_background_color":["scene1_button_label"]}};
 
 /***/ }),
 /* 25 */
 /***/ (function(module, __webpack_exports__, __webpack_require__) {
 
 "use strict";
 __webpack_require__.r(__webpack_exports__);
 /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "_StartupOverlay", function() { return _StartupOverlay; });
@@ -8030,34 +8034,34 @@ class DSEmptyState_DSEmptyState extends 
 
 class CardGrid_CardGrid extends external_React_default.a.PureComponent {
   renderCards() {
     const recs = this.props.data.recommendations.slice(0, this.props.items);
     const cards = [];
 
     for (let index = 0; index < this.props.items; index++) {
       const rec = recs[index];
-      cards.push(rec ? external_React_default.a.createElement(DSCard_DSCard, {
+      cards.push(!rec || rec.placeholder ? external_React_default.a.createElement(PlaceholderDSCard, {
+        key: `dscard-${index}`
+      }) : external_React_default.a.createElement(DSCard_DSCard, {
         key: `dscard-${index}`,
         pos: rec.pos,
         campaignId: rec.campaign_id,
         image_src: rec.image_src,
         raw_image_src: rec.raw_image_src,
         title: rec.title,
         excerpt: rec.excerpt,
         url: rec.url,
         id: rec.id,
         type: this.props.type,
         context: rec.context,
         dispatch: this.props.dispatch,
         source: rec.domain,
         pocket_id: rec.pocket_id,
         bookmarkGuid: rec.bookmarkGuid
-      }) : external_React_default.a.createElement(PlaceholderDSCard, {
-        key: `dscard-${index}`
       }));
     }
 
     let divisibility = ``;
 
     if (this.props.items % 4 === 0) {
       divisibility = `divisible-by-4`;
     } else if (this.props.items % 3 === 0) {
@@ -8215,34 +8219,34 @@ const PlaceholderListItem = props => ext
 
 function _List(props) {
   const renderList = () => {
     const recs = props.data.recommendations.slice(props.recStartingPoint, props.recStartingPoint + props.items);
     const recMarkup = [];
 
     for (let index = 0; index < props.items; index++) {
       const rec = recs[index];
-      recMarkup.push(rec ? external_React_default.a.createElement(List_ListItem, {
+      recMarkup.push(!rec || rec.placeholder ? external_React_default.a.createElement(PlaceholderListItem, {
+        key: `ds-list-item-${index}`
+      }) : external_React_default.a.createElement(List_ListItem, {
         key: `ds-list-item-${index}`,
         dispatch: props.dispatch,
         campaignId: rec.campaign_id,
         domain: rec.domain,
         excerpt: rec.excerpt,
         id: rec.id,
         image_src: rec.image_src,
         raw_image_src: rec.raw_image_src,
         pos: rec.pos,
         title: rec.title,
         context: rec.context,
         type: props.type,
         url: rec.url,
         pocket_id: rec.pocket_id,
         bookmarkGuid: rec.bookmarkGuid
-      }) : external_React_default.a.createElement(PlaceholderListItem, {
-        key: `ds-list-item-${index}`
       }));
     }
 
     const listStyles = ["ds-list", props.fullWidth ? "ds-list-full-width" : "", props.hasBorders ? "ds-list-borders" : "", props.hasImages ? "ds-list-images" : "", props.hasNumbers ? "ds-list-numbers" : ""];
     return external_React_default.a.createElement("ul", {
       className: listStyles.join(" ")
     }, recMarkup);
   };
@@ -8314,89 +8318,97 @@ class Hero_Hero extends external_React_d
 
   renderHero() {
     let [heroRec, ...otherRecs] = this.props.data.recommendations.slice(0, this.props.items);
     this.heroRec = heroRec;
     const cards = [];
 
     for (let index = 0; index < this.props.items - 1; index++) {
       const rec = otherRecs[index];
-      cards.push(rec ? external_React_default.a.createElement(DSCard_DSCard, {
+      cards.push(!rec || rec.placeholder ? external_React_default.a.createElement(PlaceholderDSCard, {
+        key: `dscard-${index}`
+      }) : external_React_default.a.createElement(DSCard_DSCard, {
         campaignId: rec.campaign_id,
         key: `dscard-${index}`,
         image_src: rec.image_src,
         raw_image_src: rec.raw_image_src,
         title: rec.title,
         url: rec.url,
         id: rec.id,
         pos: rec.pos,
         type: this.props.type,
         dispatch: this.props.dispatch,
         context: rec.context,
         source: rec.domain,
         pocket_id: rec.pocket_id,
         bookmarkGuid: rec.bookmarkGuid
-      }) : external_React_default.a.createElement(PlaceholderDSCard, {
-        key: `dscard-${index}`
+      }));
+    }
+
+    let heroCard = null;
+
+    if (!heroRec || heroRec.placeholder) {
+      heroCard = external_React_default.a.createElement(PlaceholderDSCard, null);
+    } else {
+      heroCard = external_React_default.a.createElement("div", {
+        className: "ds-hero-item"
+      }, external_React_default.a.createElement(SafeAnchor_SafeAnchor, {
+        className: "wrapper",
+        dispatch: this.props.dispatch,
+        onLinkClick: this.onLinkClick,
+        url: heroRec.url
+      }, external_React_default.a.createElement("div", {
+        className: "img-wrapper"
+      }, external_React_default.a.createElement(DSImage_DSImage, {
+        extraClassNames: "img",
+        source: heroRec.image_src,
+        rawSource: heroRec.raw_image_src
+      })), external_React_default.a.createElement("div", {
+        className: "meta"
+      }, external_React_default.a.createElement("div", {
+        className: "header-and-excerpt"
+      }, heroRec.context ? external_React_default.a.createElement("p", {
+        className: "context"
+      }, heroRec.context) : external_React_default.a.createElement("p", {
+        className: "source"
+      }, heroRec.domain), external_React_default.a.createElement("header", null, heroRec.title), external_React_default.a.createElement("p", {
+        className: "excerpt"
+      }, heroRec.excerpt))), external_React_default.a.createElement(ImpressionStats["ImpressionStats"], {
+        campaignId: heroRec.campaignId,
+        rows: [{
+          id: heroRec.id,
+          pos: heroRec.pos
+        }],
+        dispatch: this.props.dispatch,
+        source: this.props.type
+      })), external_React_default.a.createElement(DSLinkMenu, {
+        id: heroRec.id,
+        index: heroRec.pos,
+        dispatch: this.props.dispatch,
+        intl: this.props.intl,
+        url: heroRec.url,
+        title: heroRec.title,
+        source: heroRec.domain,
+        type: this.props.type,
+        pocket_id: heroRec.pocket_id,
+        bookmarkGuid: heroRec.bookmarkGuid
       }));
     }
 
     let list = external_React_default.a.createElement(List, {
       recStartingPoint: 1,
       data: this.props.data,
       hasImages: true,
       hasBorders: this.props.border === `border`,
       items: this.props.items - 1,
       type: `Hero`
     });
     return external_React_default.a.createElement("div", {
       className: `ds-hero ds-hero-${this.props.border}`
-    }, external_React_default.a.createElement("div", {
-      className: "ds-hero-item"
-    }, external_React_default.a.createElement(SafeAnchor_SafeAnchor, {
-      className: "wrapper",
-      dispatch: this.props.dispatch,
-      onLinkClick: this.onLinkClick,
-      url: heroRec.url
-    }, external_React_default.a.createElement("div", {
-      className: "img-wrapper"
-    }, external_React_default.a.createElement(DSImage_DSImage, {
-      extraClassNames: "img",
-      source: heroRec.image_src,
-      rawSource: heroRec.raw_image_src
-    })), external_React_default.a.createElement("div", {
-      className: "meta"
-    }, external_React_default.a.createElement("div", {
-      className: "header-and-excerpt"
-    }, heroRec.context ? external_React_default.a.createElement("p", {
-      className: "context"
-    }, heroRec.context) : external_React_default.a.createElement("p", {
-      className: "source"
-    }, heroRec.domain), external_React_default.a.createElement("header", null, heroRec.title), external_React_default.a.createElement("p", {
-      className: "excerpt"
-    }, heroRec.excerpt))), external_React_default.a.createElement(ImpressionStats["ImpressionStats"], {
-      campaignId: heroRec.campaignId,
-      rows: [{
-        id: heroRec.id,
-        pos: heroRec.pos
-      }],
-      dispatch: this.props.dispatch,
-      source: this.props.type
-    })), external_React_default.a.createElement(DSLinkMenu, {
-      id: heroRec.id,
-      index: heroRec.pos,
-      dispatch: this.props.dispatch,
-      intl: this.props.intl,
-      url: heroRec.url,
-      title: heroRec.title,
-      source: heroRec.domain,
-      type: this.props.type,
-      pocket_id: heroRec.pocket_id,
-      bookmarkGuid: heroRec.bookmarkGuid
-    })), external_React_default.a.createElement("div", {
+    }, heroCard, external_React_default.a.createElement("div", {
       className: `${this.props.subComponentType}`
     }, this.props.subComponentType === `cards` ? cards : list));
   }
 
   render() {
     const {
       data
     } = this.props; // Handle a render before feed has been fetched by displaying nothing
@@ -8531,88 +8543,112 @@ const selectLayoutRender = (state, prefs
       }
     }
 
     return { ...data,
       recommendations
     };
   }
 
-  function maybeInjectSpocs(data, spocsConfig) {
-    // Do we ever expect to possibly have a spoc.
-    if (data && spocsConfig && spocsConfig.positions && spocsConfig.positions.length) {
-      // We expect a spoc, spocs are loaded, but the server returned no spocs.
-      if (!spocs.data.spocs || !spocs.data.spocs.length) {
-        return data;
-      } // We expect a spoc, spocs are loaded, and we have spocs available.
-
-
-      return rollForSpocs(data, spocsConfig);
-    }
-
-    return data;
-  }
-
   const positions = {};
   const DS_COMPONENTS = ["Message", "SectionTitle", "Navigation", "CardGrid", "Hero", "HorizontalRule", "List"];
   const filterArray = [];
 
   if (!prefs["feeds.topsites"]) {
     filterArray.push("TopSites");
   }
 
   if (!prefs["feeds.section.topstories"]) {
     filterArray.push(...DS_COMPONENTS);
   }
 
+  const placeholderComponent = component => {
+    const data = {
+      recommendations: []
+    };
+    let items = 0;
+
+    if (component.properties && component.properties.items) {
+      items = component.properties.items;
+    }
+
+    for (let i = 0; i < items; i++) {
+      data.recommendations.push({
+        "placeholder": true
+      });
+    }
+
+    return { ...component,
+      data
+    };
+  };
+
   const handleComponent = component => {
     positions[component.type] = positions[component.type] || 0;
-    let {
-      data
-    } = feeds.data[component.feed.url];
+    const feed = feeds.data[component.feed.url];
+    let data = {
+      recommendations: []
+    };
+
+    if (feed && feed.data) {
+      data = { ...feed.data,
+        recommendations: [...feed.data.recommendations]
+      };
+    }
 
     if (component && component.properties && component.properties.offset) {
       data = { ...data,
         recommendations: data.recommendations.slice(component.properties.offset)
       };
-    }
-
-    data = maybeInjectSpocs(data, component.spocs);
+    } // Do we ever expect to possibly have a spoc.
+
+
+    if (data && component.spocs && component.spocs.positions && component.spocs.positions.length) {
+      // We expect a spoc, spocs are loaded, and the server returned spocs.
+      if (spocs.loaded && spocs.data.spocs && spocs.data.spocs.length) {
+        data = rollForSpocs(data, component.spocs);
+      }
+    }
+
     let items = 0;
 
     if (component.properties && component.properties.items) {
       items = Math.min(component.properties.items, data.recommendations.length);
     } // loop through a component items
     // Store the items position sequentially for multiple components of the same type.
     // Example: A second card grid starts pos offset from the last card grid.
 
 
     for (let i = 0; i < items; i++) {
-      data.recommendations[i].pos = positions[component.type]++;
+      data.recommendations[i] = { ...data.recommendations[i],
+        pos: positions[component.type]++
+      };
     }
 
     return { ...component,
       data
     };
   };
 
   const renderLayout = () => {
     const renderedLayoutArray = [];
 
     for (const row of layout.filter(r => r.components.filter(c => !filterArray.includes(c.type)).length)) {
       let components = [];
       renderedLayoutArray.push({ ...row,
         components
       });
 
-      for (const component of row.components) {
+      for (const component of row.components.filter(c => !filterArray.includes(c.type))) {
         if (component.feed) {
-          const spocsConfig = component.spocs; // Are we still waiting on a feed/spocs, render what we have, and bail out early.
+          const spocsConfig = component.spocs; // Are we still waiting on a feed/spocs, render what we have,
+          // add a placeholder for this component, and bail out early.
 
           if (!feeds.data[component.feed.url] || spocsConfig && spocsConfig.positions && spocsConfig.positions.length && !spocs.loaded) {
+            components.push(placeholderComponent(component));
             return renderedLayoutArray;
           }
 
           components.push(handleComponent(component));
         } else {
           components.push(component);
         }
       }
@@ -9137,17 +9173,19 @@ class SnippetBase_SnippetBase extends ex
 function _extends() { _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; return _extends.apply(this, arguments); }
 
 
 
 
 
 
 
-const DEFAULT_ICON_PATH = "chrome://branding/content/icon64.png";
+const DEFAULT_ICON_PATH = "chrome://branding/content/icon64.png"; // Alt text if available; in the future this should come from the server. See bug 1551711
+
+const ICON_ALT_TEXT = "";
 class SimpleSnippet_SimpleSnippet extends external_React_default.a.PureComponent {
   constructor(props) {
     super(props);
     this.onButtonClick = this.onButtonClick.bind(this);
   }
 
   onButtonClick() {
     if (this.props.provider !== "preview") {
@@ -9183,23 +9221,34 @@ class SimpleSnippet_SimpleSnippet extend
       title
     } = this.props.content;
     return title ? external_React_default.a.createElement("h3", {
       className: `title ${this._shouldRenderButton() ? "title-inline" : ""}`
     }, this.renderTitleIcon(), " ", title) : null;
   }
 
   renderTitleIcon() {
-    const titleIcon = Object(template_utils["safeURI"])(this.props.content.title_icon);
-    return titleIcon ? external_React_default.a.createElement("span", {
-      className: "titleIcon",
+    const titleIconLight = Object(template_utils["safeURI"])(this.props.content.title_icon);
+    const titleIconDark = Object(template_utils["safeURI"])(this.props.content.title_icon_dark_theme || this.props.content.title_icon);
+
+    if (!titleIconLight) {
+      return null;
+    }
+
+    return external_React_default.a.createElement(external_React_default.a.Fragment, null, external_React_default.a.createElement("span", {
+      className: "titleIcon icon-light-theme",
       style: {
-        backgroundImage: `url("${titleIcon}")`
-      }
-    }) : null;
+        backgroundImage: `url("${titleIconLight}")`
+      }
+    }), external_React_default.a.createElement("span", {
+      className: "titleIcon icon-dark-theme",
+      style: {
+        backgroundImage: `url("${titleIconDark}")`
+      }
+    }));
   }
 
   renderButton() {
     const {
       props
     } = this;
 
     if (!this._shouldRenderButton()) {
@@ -9241,29 +9290,35 @@ class SimpleSnippet_SimpleSnippet extend
   }
 
   renderSectionHeader() {
     const {
       props
     } = this; // an icon and text must be specified to render the section header
 
     if (props.content.section_title_icon && props.content.section_title_text) {
-      const sectionTitleIcon = Object(template_utils["safeURI"])(props.content.section_title_icon);
+      const sectionTitleIconLight = Object(template_utils["safeURI"])(props.content.section_title_icon);
+      const sectionTitleIconDark = Object(template_utils["safeURI"])(props.content.section_title_icon_dark_theme || props.content.section_title_icon);
       const sectionTitleURL = props.content.section_title_url;
       return external_React_default.a.createElement("div", {
         className: "section-header"
       }, external_React_default.a.createElement("h3", {
         className: "section-title"
       }, external_React_default.a.createElement(ConditionalWrapper, {
         condition: sectionTitleURL,
         wrap: this.wrapSectionHeader(sectionTitleURL)
       }, external_React_default.a.createElement("span", {
-        className: "icon icon-small-spacer",
+        className: "icon icon-small-spacer icon-light-theme",
         style: {
-          backgroundImage: `url("${sectionTitleIcon}")`
+          backgroundImage: `url("${sectionTitleIconLight}")`
+        }
+      }), external_React_default.a.createElement("span", {
+        className: "icon icon-small-spacer icon-dark-theme",
+        style: {
+          backgroundImage: `url("${sectionTitleIconDark}")`
         }
       }), external_React_default.a.createElement("span", {
         className: "section-title-text"
       }, props.content.section_title_text))));
     }
 
     return null;
   }
@@ -9290,17 +9345,22 @@ class SimpleSnippet_SimpleSnippet extend
     return external_React_default.a.createElement(SnippetBase_SnippetBase, _extends({}, props, {
       className: className,
       textStyle: this.props.textStyle
     }), sectionHeader, external_React_default.a.createElement(ConditionalWrapper, {
       condition: sectionHeader,
       wrap: this.wrapSnippetContent
     }, external_React_default.a.createElement("img", {
       src: Object(template_utils["safeURI"])(props.content.icon) || DEFAULT_ICON_PATH,
-      className: "icon"
+      className: "icon icon-light-theme",
+      alt: ICON_ALT_TEXT
+    }), external_React_default.a.createElement("img", {
+      src: Object(template_utils["safeURI"])(props.content.icon_dark_theme || props.content.icon) || DEFAULT_ICON_PATH,
+      className: "icon icon-dark-theme",
+      alt: ICON_ALT_TEXT
     }), external_React_default.a.createElement("div", null, this.renderTitle(), " ", external_React_default.a.createElement("p", {
       className: "body"
     }, this.renderText()), this.props.extraContent), external_React_default.a.createElement("div", null, this.renderButton())));
   }
 
 }
 // CONCATENATED MODULE: ./content-src/asrouter/templates/EOYSnippet/EOYSnippet.jsx
 function EOYSnippet_extends() { EOYSnippet_extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; return EOYSnippet_extends.apply(this, arguments); }
@@ -9453,16 +9513,19 @@ var FXASignupSnippet_schema = __webpack_
 // CONCATENATED MODULE: ./content-src/asrouter/templates/SubmitFormSnippet/SubmitFormSnippet.jsx
 function SubmitFormSnippet_extends() { SubmitFormSnippet_extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; return SubmitFormSnippet_extends.apply(this, arguments); }
 
 
 
 
 
 
+ // Alt text if available; in the future this should come from the server. See bug 1551711
+
+const SubmitFormSnippet_ICON_ALT_TEXT = "";
 class SubmitFormSnippet_SubmitFormSnippet extends external_React_default.a.PureComponent {
   constructor(props) {
     super(props);
     this.expandSnippet = this.expandSnippet.bind(this);
     this.handleSubmit = this.handleSubmit.bind(this);
     this.handleSubmitAttempt = this.handleSubmitAttempt.bind(this);
     this.onInputChange = this.onInputChange.bind(this);
     this.state = {
@@ -9679,33 +9742,38 @@ class SubmitFormSnippet_SubmitFormSnippe
     const placholder = this.props.content.scene2_email_placeholder_text || this.props.content.scene2_input_placeholder;
     return external_React_default.a.createElement("input", {
       ref: "mainInput",
       type: this.props.inputType || "email",
       className: `mainInput${this.state.submitAttempted ? "" : " clean"}`,
       name: "email",
       required: true,
       placeholder: placholder,
-      onChange: this.props.validateInput ? this.onInputChange : null,
-      autoFocus: true
+      onChange: this.props.validateInput ? this.onInputChange : null
     });
   }
 
   renderSignupView() {
     const {
       content
     } = this.props;
     const containerClass = `SubmitFormSnippet ${this.props.className}`;
     return external_React_default.a.createElement(SnippetBase_SnippetBase, SubmitFormSnippet_extends({}, this.props, {
       className: containerClass,
       footerDismiss: true
     }), content.scene2_icon ? external_React_default.a.createElement("div", {
       className: "scene2Icon"
     }, external_React_default.a.createElement("img", {
-      src: content.scene2_icon
+      src: Object(template_utils["safeURI"])(content.scene2_icon),
+      className: "icon-light-theme",
+      alt: SubmitFormSnippet_ICON_ALT_TEXT
+    }), external_React_default.a.createElement("img", {
+      src: Object(template_utils["safeURI"])(content.scene2_icon_dark_theme || content.scene2_icon),
+      className: "icon-dark-theme",
+      alt: SubmitFormSnippet_ICON_ALT_TEXT
     })) : null, external_React_default.a.createElement("div", {
       className: "message"
     }, external_React_default.a.createElement("p", null, content.scene2_title && external_React_default.a.createElement("h3", {
       className: "scene2Title"
     }, content.scene2_title), " ", content.scene2_text && external_React_default.a.createElement(RichText["RichText"], {
       scene2_text: content.scene2_text,
       localization_id: "scene2_text"
     }))), external_React_default.a.createElement("form", {
@@ -9925,17 +9993,19 @@ const SendToDeviceSnippet = props => {
 };
 // CONCATENATED MODULE: ./content-src/asrouter/templates/SimpleBelowSearchSnippet/SimpleBelowSearchSnippet.jsx
 function SimpleBelowSearchSnippet_extends() { SimpleBelowSearchSnippet_extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; return SimpleBelowSearchSnippet_extends.apply(this, arguments); }
 
 
 
 
 
-const SimpleBelowSearchSnippet_DEFAULT_ICON_PATH = "chrome://branding/content/icon64.png";
+const SimpleBelowSearchSnippet_DEFAULT_ICON_PATH = "chrome://branding/content/icon64.png"; // Alt text if available; in the future this should come from the server. See bug 1551711
+
+const SimpleBelowSearchSnippet_ICON_ALT_TEXT = "";
 class SimpleBelowSearchSnippet_SimpleBelowSearchSnippet extends external_React_default.a.PureComponent {
   renderText() {
     const {
       props
     } = this;
     return external_React_default.a.createElement(RichText["RichText"], {
       text: props.content.text,
       customElements: this.props.customElements,
@@ -9955,17 +10025,22 @@ class SimpleBelowSearchSnippet_SimpleBel
       className += ` ${props.className}`;
     }
 
     return external_React_default.a.createElement(SnippetBase_SnippetBase, SimpleBelowSearchSnippet_extends({}, props, {
       className: className,
       textStyle: this.props.textStyle
     }), external_React_default.a.createElement("img", {
       src: Object(template_utils["safeURI"])(props.content.icon) || SimpleBelowSearchSnippet_DEFAULT_ICON_PATH,
-      className: "icon"
+      className: "icon icon-light-theme",
+      alt: SimpleBelowSearchSnippet_ICON_ALT_TEXT
+    }), external_React_default.a.createElement("img", {
+      src: Object(template_utils["safeURI"])(props.content.icon_dark_theme || props.content.icon) || SimpleBelowSearchSnippet_DEFAULT_ICON_PATH,
+      className: "icon icon-dark-theme",
+      alt: SimpleBelowSearchSnippet_ICON_ALT_TEXT
     }), external_React_default.a.createElement("div", null, external_React_default.a.createElement("p", {
       className: "body"
     }, this.renderText()), this.props.extraContent));
   }
 
 }
 // CONCATENATED MODULE: ./content-src/asrouter/templates/template-manifest.jsx
 /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "SnippetsTemplates", function() { return SnippetsTemplates; });
new file mode 100644
--- /dev/null
+++ b/browser/components/newtab/docs/index.rst
@@ -0,0 +1,65 @@
+======================
+Firefox Home (New Tab)
+======================
+
+All files related to Firefox Home, which includes content that appears on `about:home`,
+`about:newtab`, and `about:welcome`, can we found in the `browser/components/newtab` directory.
+Some of these source files (such as `.js`, `.jsx`, and `.sass`) require an additional build step.
+We are working on migrating this to work with `mach`, but in the meantime, please
+follow the following steps if you need to make changes in this directory:
+
+For .jsm files
+---------------
+
+No build step is necessary. Use `mach` and run mochi tests according to your regular Firefox workflow.
+
+For .js, .jsx, .sass, or .css files
+-----------------------------------
+
+Prerequisites
+`````````````
+
+You will need the following:
+
+- Node.js 8+ (On Mac, the best way to install Node.js is to use the [install link on the Node.js homepage](https://nodejs.org/en/))
+- npm (packaged with Node.js)
+
+To install dependencies, run the following from the root of the mozilla-central repository
+(or cd into browser/components/newtab to omit the `--prefix` in any of these commands):
+
+.. code-block:: shell
+
+  npm install --prefix browser/components/newtab
+
+
+Which files should you edit?
+````````````````````````````
+
+You should not make changes to `.js` or `.css` files in `browser/components/newtab/css` or
+`browser/components/newtab/data` directory. Instead, you should edit the `.jsx`, `.js`, and `.sass` files
+in `browser/components/newtab/content-src` directory.
+
+Building assets and running Firefox
+```````````````````````````````````
+
+To build assets and run Firefox, run the following from the root of the mozilla-central repository:
+
+.. code-block:: shell
+
+  npm run bundle --prefix browser/components/newtab && ./mach run
+
+Running tests
+`````````````
+
+Mochi tests and xpcshell tests can be run normally. To run our additional unit tests, you can run the following:
+
+.. code-block:: shell
+
+  npm test --prefix browser/components/newtab
+
+The Newtab team is currently responsible for fixing any test failures that result from changes
+until these tests are running in Try, so this is currently an optional step.
+
+GitHub workflow
+---------------
+The files in this directory, including vendor dependencies, are synchronized with the https://github.com/mozilla/activity-stream repository. If you prefer a GitHub-based workflow, you can look at the documentation there to learn more.
--- a/browser/components/newtab/docs/v2-system-addon/data_dictionary.md
+++ b/browser/components/newtab/docs/v2-system-addon/data_dictionary.md
@@ -206,17 +206,17 @@ Schema definitions/validations that can 
 | `action_position` | [Optional] The index of the element in the `source` that was clicked. | :one:
 | `action` | [Required] Either `activity_stream_event`, `activity_stream_session`, or `activity_stream_performance`. | :one:
 | `addon_version` | [Required] Firefox build ID, i.e. `Services.appinfo.appBuildID`. | :one:
 | `client_id` | [Required] An identifier for this client. | :one:
 | `card_type` | [Optional] ("bookmark", "pocket", "trending", "pinned", "search") | :one:
 | `search_vendor` | [Optional] the vendor of the search shortcut, one of ("google", "amazon", "wikipedia", "duckduckgo", "bing", etc.). This field only exists when `card_type = "search"` | :one:
 | `date` | [Auto populated by Onyx] The date in YYYY-MM-DD format. | :three:
 | `experiment_id` | [Optional] The unique identifier for a specific experiment. | :one:
-| `event_id` | [Required] An identifier shared by multiple performance pings that describe ane entire request flow. | :one:
+| `event_id` | [Required] An identifier shared by multiple performance pings that describe an entire request flow. | :one:
 | `event` | [Required] The type of event. Any user defined string ("click", "share", "delete", "more_items") | :one:
 | `highlight_type` | [Optional] Either ["bookmarks", "recommendation", "history"]. | :one:
 | `impression_id` | [Optional] The unique impression identifier for a specific client. | :one:
 | `ip` | [Auto populated by Onyx] The IP address of the client. | :two:
 | `locale` | [Auto populated by Onyx] The browser chrome's language (eg. en-US). | :two:
 | `load_trigger_ts` | [Optional][Server Counter][Server Alert for too many omissions]  DOMHighResTimeStamp of the action perceived by the user to trigger the load of this page. | :one:
 | `load_trigger_type` | [Server Counter][Server Alert for too many omissions] Either ["first_window_opened", "menu_plus_or_keyboard", "unexpected"]. | :one:
 | `metadata_source` | [Optional] The source of which we computed metadata. Either (`MetadataService` or `Local` or `TippyTopProvider`). | :one:
new file mode 100644
--- /dev/null
+++ b/browser/components/newtab/hooks/pre-push
@@ -0,0 +1,11 @@
+#!/bin/sh
+# Recommended pre-push hook for activity-stream
+
+hookName=`basename "$0"`
+
+if ! command -v node >/dev/null 2>&1; then
+  echo "Can't find node in PATH, trying to find a node binary on your system"
+fi
+
+echo "Running $hookName hook..."
+npm run lint
--- a/browser/components/newtab/lib/ASRouter.jsm
+++ b/browser/components/newtab/lib/ASRouter.jsm
@@ -700,16 +700,25 @@ class _ASRouter {
         message_id: message.id,
         action: "asrouter_undesired_event",
         event: "TARGETING_EXPRESSION_ERROR",
         value: type,
       }));
     }
   }
 
+  async _hasAddonAttributionData() {
+    try {
+      const data = await AttributionCode.getAttrDataAsync() || {};
+      return data.source === "addons.mozilla.org";
+    } catch (e) {
+      return false;
+    }
+  }
+
   /**
    * _generateTrailheadBranches - Generates and returns Trailhead configuration and chooses an experiment
    *                             based on clientID and locale.
    * @returns {{experiment: string, interrupt: string, triplet: string}}
    */
   async _generateTrailheadBranches() {
     let experiment = "";
     let interrupt;
@@ -719,17 +728,17 @@ class _ASRouter {
     const overrideValue = Services.prefs.getStringPref(TRAILHEAD_CONFIG.OVERRIDE_PREF, "");
     if (overrideValue) {
       [interrupt, triplet] = overrideValue.split("-");
       return {experiment, interrupt, triplet: triplet || ""};
     }
 
     const locale = Services.locale.appLocaleAsLangTag;
 
-    if (TRAILHEAD_CONFIG.LOCALES.includes(locale)) {
+    if (TRAILHEAD_CONFIG.LOCALES.includes(locale) && !(await this._hasAddonAttributionData())) {
       const {userId} = ClientEnvironment;
       experiment = await chooseBranch(`${userId}-trailhead-experiments`, TRAILHEAD_CONFIG.EXPERIMENT_RATIOS);
 
       // For the interrupts experiment,
       // we randomly assign an interrupt and always use the "supercharge" triplet.
       if (experiment === "interrupts") {
         interrupt =  await chooseBranch(`${userId}-interrupts-branch`, TRAILHEAD_CONFIG.BRANCHES.interrupts);
         if (["join", "sync", "cards"].includes(interrupt)) {
--- a/browser/components/newtab/lib/SnippetsTestMessageProvider.jsm
+++ b/browser/components/newtab/lib/SnippetsTestMessageProvider.jsm
@@ -1,83 +1,107 @@
 /* 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 TEST_ICON = "chrome://branding/content/icon64.png";
 const TEST_ICON_16 = "chrome://branding/content/icon16.png";
+const TEST_ICON_BW = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAQAAAC1+jfqAAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAAAmJLR0QA/4ePzL8AAAAHdElNRQfjBQ8QDifrKGc/AAABf0lEQVQoz4WRO08UUQCFvztzd1AgG9jRgGwkhEoMIYGSygYt+A00tpZGY0jYxAJKEwkNjX9AK2xACx4dhFiQQCiMMRr2kYXdnQcz7L0z91qAMVac6hTfSU7OgVsk/prtyfSNfRb7ge2cd7dmVucP/wM2lwqVqoyICahRx9Nz71+8AnAAvlTct+dSYDBYcgJ+Fj68XFu/AfamnIoWFoHFYrAUuYMSn55/fAIOxIs1t4MhQpNxRYsUD0ld7r8DCfZph4QecrqkhCREgMLSeISQkAy0UBgE0CYgIkeRA9HdsCQhpEGCxichpItHigEcPH4XJLRbTf8STY0iiiuu60Ifxexx04F0N+aCgJCAhPQmD/cp/RC5A79WvUyhUHSIidAIoESv9VfAhW9n8+XqTCoyMsz1cviMMrGz9BrjAuboYHZajyXCInEocI8yvccbC+0muABanR4/tONjQz3DzgNKtj9sfv66XD9B/3tT9g/akb7h0bJwzxqqmlRHLr4rLPwBlYWoYj77l2AAAAAldEVYdGRhdGU6Y3JlYXRlADIwMTktMDUtMTVUMTY6MTQ6MzkrMDA6MDD5/4XBAAAAJXRFWHRkYXRlOm1vZGlmeQAyMDE5LTA1LTE1VDE2OjE0OjM5KzAwOjAwiKI9fQAAAABJRU5ErkJggg==";
 
 const MESSAGES = () => ([
   {
     "id": "SIMPLE_TEST_1",
     "template": "simple_snippet",
     "campaign": "test_campaign_blocking",
     "content": {
       "icon": TEST_ICON,
+      "icon_dark_theme": TEST_ICON_BW,
       "title": "Firefox Account!",
       "title_icon": TEST_ICON_16,
+      "title_icon_dark_theme": TEST_ICON_BW,
+      "text": "<syncLink>Sync it, link it, take it with you</syncLink>. All this and more with a Firefox Account.",
+      "links": {"syncLink": {"url": "https://www.mozilla.org/en-US/firefox/accounts"}},
+      "block_button_text": "Block",
+    },
+  },
+  {
+    "id": "SIMPLE_TEST_1_NO_DARK_THEME",
+    "template": "simple_snippet",
+    "campaign": "test_campaign_blocking",
+    "content": {
+      "icon": TEST_ICON,
+      "icon_dark_theme": "",
+      "title": "Firefox Account!",
+      "title_icon": TEST_ICON_16,
+      "title_icon_dark_theme": "",
       "text": "<syncLink>Sync it, link it, take it with you</syncLink>. All this and more with a Firefox Account.",
       "links": {"syncLink": {"url": "https://www.mozilla.org/en-US/firefox/accounts"}},
       "block_button_text": "Block",
     },
   },
   {
     "id": "SIMPLE_TEST_1_SAME_CAMPAIGN",
     "template": "simple_snippet",
     "campaign": "test_campaign_blocking",
     "content": {
       "icon": TEST_ICON,
+      "icon_dark_theme": TEST_ICON_BW,
       "text": "<syncLink>Sync it, link it, take it with you</syncLink>. All this and more with a Firefox Account.",
       "links": {"syncLink": {"url": "https://www.mozilla.org/en-US/firefox/accounts"}},
       "block_button_text": "Block",
     },
   },
   {
     "id": "SIMPLE_TEST_TALL",
     "template": "simple_snippet",
     "content": {
       "icon": TEST_ICON,
+      "icon_dark_theme": TEST_ICON_BW,
       "text": "<syncLink>Sync it, link it, take it with you</syncLink>. All this and more with a Firefox Account.",
       "links": {"syncLink": {"url": "https://www.mozilla.org/en-US/firefox/accounts"}},
       "button_label": "Get one now!",
       "button_url": "https://www.mozilla.org/en-US/firefox/accounts",
       "block_button_text": "Block",
       "tall": true,
     },
   },
   {
     "id": "SIMPLE_TEST_BUTTON_URL_1",
     "template": "simple_snippet",
     "content": {
       "icon": TEST_ICON,
+      "icon_dark_theme": TEST_ICON_BW,
       "button_label": "Get one now!",
       "button_url": "https://www.mozilla.org/en-US/firefox/accounts",
       "text": "Sync it, link it, take it with you. All this and more with a Firefox Account.",
       "block_button_text": "Block",
     },
   },
   {
     "id": "SIMPLE_WITH_TITLE_TEST_1",
     "template": "simple_snippet",
     "content": {
       "icon": TEST_ICON,
+      "icon_dark_theme": TEST_ICON_BW,
       "title": "Ready to sync?",
       "text": "Get connected with a <syncLink>Firefox account</syncLink>.",
       "links": {"syncLink": {"url": "https://www.mozilla.org/en-US/firefox/accounts"}},
       "block_button_text": "Block",
     },
   },
   {
     "id": "NEWSLETTER_TEST_DEFAULTS",
     "template": "newsletter_snippet",
     "content": {
       "scene1_icon": TEST_ICON,
+      "scene1_icon_dark_theme": TEST_ICON_BW,
       "scene1_title": "Be a part of a movement.",
       "scene1_title_icon": TEST_ICON_16,
+      "scene1_title_icon_dark_theme": TEST_ICON_BW,
       "scene1_text": "Internet shutdowns, hackers, harassment &ndash; the health of the internet is on the line. Sign up and Mozilla will keep you updated on how you can help.",
       "scene1_button_label": "Continue",
       "scene1_button_color": "#712b00",
       "scene1_button_background_color": "#ff9400",
       "scene2_title": "Let's do this!",
       "locale": "en-CA",
       "scene2_dismiss_button_text": "Dismiss",
       "scene2_text": "Sign up for the Mozilla newsletter and we will keep you updated on how you can help.",
@@ -88,16 +112,17 @@ const MESSAGES = () => ([
       "links": {"privacyLink": {"url": "https://www.mozilla.org/privacy/websites/?sample_rate=0.001&snippet_name=7894"}},
     },
   },
   {
     "id": "NEWSLETTER_TEST_1",
     "template": "newsletter_snippet",
     "content": {
       "scene1_icon": TEST_ICON,
+      "scene1_icon_dark_theme": TEST_ICON_BW,
       "scene1_title": "Be a part of a movement.",
       "scene1_title_icon": "",
       "scene1_text": "Internet shutdowns, hackers, harassment &ndash; the health of the internet is on the line. Sign up and Mozilla will keep you updated on how you can help.",
       "scene1_button_label": "Continue",
       "scene1_button_color": "#712b00",
       "scene1_button_background_color": "#ff9400",
       "scene2_title": "Let's do this!",
       "locale": "en-CA",
@@ -112,23 +137,25 @@ const MESSAGES = () => ([
       "links": {"privacyLink": {"url": "https://www.mozilla.org/privacy/websites/?sample_rate=0.001&snippet_name=7894"}},
     },
   },
   {
     "id": "FXA_SNIPPET_TEST_1",
     "template": "fxa_signup_snippet",
     "content": {
       "scene1_icon": TEST_ICON,
+      "scene1_icon_dark_theme": TEST_ICON_BW,
       "scene1_button_label": "Get connected with sync!",
       "scene1_button_color": "#712b00",
       "scene1_button_background_color": "#ff9400",
 
       "scene1_text": "Connect to Firefox by securely syncing passwords, bookmarks, and open tabs.",
       "scene1_title": "Browser better.",
       "scene1_title_icon": TEST_ICON_16,
+      "scene1_title_icon_dark_theme": TEST_ICON_BW,
 
       "scene2_text": "Connect to your Firefox account to securely sync passwords, bookmarks, and open tabs.",
       "scene2_title": "Title 123",
       "scene2_email_placeholder_text": "Your email",
       "scene2_button_label": "Continue",
       "scene2_dismiss_button_text": "Dismiss",
     },
   },
@@ -141,24 +168,62 @@ const MESSAGES = () => ([
       country: "us",
       message_id_sms: "ff-mobilesn-download",
       message_id_email: "download-firefox-mobile",
 
       scene1_button_background_color: "#6200a4",
       scene1_button_color: "#FFFFFF",
       scene1_button_label: "Install now",
       scene1_icon: TEST_ICON,
+      scene1_icon_dark_theme: TEST_ICON_BW,
       scene1_text: "Browse without compromise with Firefox Mobile.",
       scene1_title: "Full-featured. Customizable. Lightning fast",
       scene1_title_icon: TEST_ICON_16,
+      scene1_title_icon_dark_theme: TEST_ICON_BW,
 
       scene2_button_label: "Send",
       scene2_disclaimer_html: "The intended recipient of the email must have consented. <privacyLink>Learn more</privacyLink>.",
       scene2_dismiss_button_text: "Dismiss",
       scene2_icon: TEST_ICON,
+      scene2_icon_dark_theme: TEST_ICON_BW,
+      scene2_input_placeholder: "Your email address or phone number",
+      scene2_text: "Send Firefox to your phone and take a powerful independent browser with you.",
+      scene2_title: "Let's do this!",
+
+      error_text: "Oops, there was a problem.",
+      success_title: "Your download link was sent.",
+      success_text: "Check your device for the email message!",
+      links: {"privacyLink": {"url": "https://www.mozilla.org/privacy/websites/?sample_rate=0.001&snippet_name=7894"}},
+    },
+  },
+  {
+    id: "SNIPPETS_SEND_TO_DEVICE_TEST_NO_DARK_THEME",
+    template: "send_to_device_snippet",
+    content: {
+      include_sms: true,
+      locale: "en-CA",
+      country: "us",
+      message_id_sms: "ff-mobilesn-download",
+      message_id_email: "download-firefox-mobile",
+
+      scene1_button_background_color: "#6200a4",
+      scene1_button_color: "#FFFFFF",
+      scene1_button_label: "Install now",
+      scene1_icon: TEST_ICON,
+      scene1_icon_dark_theme: "",
+      scene1_text: "Browse without compromise with Firefox Mobile.",
+      scene1_title: "Full-featured. Customizable. Lightning fast",
+      scene1_title_icon: TEST_ICON_16,
+      scene1_title_icon_dark_theme: "",
+
+      scene2_button_label: "Send",
+      scene2_disclaimer_html: "The intended recipient of the email must have consented. <privacyLink>Learn more</privacyLink>.",
+      scene2_dismiss_button_text: "Dismiss",
+      scene2_icon: TEST_ICON,
+      scene2_icon_dark_theme: "",
       scene2_input_placeholder: "Your email address or phone number",
       scene2_text: "Send Firefox to your phone and take a powerful independent browser with you.",
       scene2_title: "Let's do this!",
 
       error_text: "Oops, there was a problem.",
       success_title: "Your download link was sent.",
       success_text: "Check your device for the email message!",
       links: {"privacyLink": {"url": "https://www.mozilla.org/privacy/websites/?sample_rate=0.001&snippet_name=7894"}},
@@ -168,32 +233,34 @@ const MESSAGES = () => ([
     "id": "EOY_TEST_1",
     "template": "eoy_snippet",
     "content": {
       "highlight_color": "#f05",
       "background_color": "#ddd",
       "text_color": "yellow",
       "selected_button": "donation_amount_first",
       "icon": TEST_ICON,
+      "icon_dark_theme": TEST_ICON_BW,
       "button_label": "Donate",
       "monthly_checkbox_label_text": "Make my donation monthly",
       "currency_code": "usd",
       "donation_amount_first": 50,
       "donation_amount_second": 25,
       "donation_amount_third": 10,
       "donation_amount_fourth": 5,
       "donation_form_url": "https://donate.mozilla.org/pl/?utm_source=desktop-snippet&amp;utm_medium=snippet&amp;utm_campaign=donate&amp;utm_term=7556",
       "text": "Big corporations want to restrict how we access the web. Fake news is making it harder for us to find the truth. Online bullies are silencing inspired voices. The <em>not-for-profit Mozilla Foundation</em> fights for a healthy internet with programs like our Tech Policy Fellowships and Internet Health Report; <b>will you donate today</b>?",
     },
   },
   {
     "id": "EOY_BOLD_TEST_1",
     "template": "eoy_snippet",
     "content": {
       "icon": TEST_ICON,
+      "icon_dark_theme": TEST_ICON_BW,
       "selected_button": "donation_amount_second",
       "button_label": "Donate",
       "monthly_checkbox_label_text": "Make my donation monthly",
       "currency_code": "usd",
       "donation_amount_first": 50,
       "donation_amount_second": 25,
       "donation_amount_third": 10,
       "donation_amount_fourth": 5,
@@ -202,16 +269,17 @@ const MESSAGES = () => ([
       "test": "bold",
     },
   },
   {
     "id": "EOY_TAKEOVER_TEST_1",
     "template": "eoy_snippet",
     "content": {
       "icon": TEST_ICON,
+      "icon_dark_theme": TEST_ICON_BW,
       "button_label": "Donate",
       "monthly_checkbox_label_text": "Make my donation monthly",
       "currency_code": "usd",
       "donation_amount_first": 50,
       "donation_amount_second": 25,
       "donation_amount_third": 10,
       "donation_amount_fourth": 5,
       "donation_form_url": "https://donate.mozilla.org",
@@ -221,42 +289,45 @@ const MESSAGES = () => ([
   },
   {
     "id": "SIMPLE_TEST_WITH_SECTION_HEADING",
     "template": "simple_snippet",
     "content": {
       "button_label": "Get one now!",
       "button_url": "https://www.mozilla.org/en-US/firefox/accounts",
       "icon": TEST_ICON,
+      "icon_dark_theme": TEST_ICON_BW,
       "title": "Firefox Account!",
       "text": "<syncLink>Sync it, link it, take it with you</syncLink>. All this and more with a Firefox Account.",
       "links": {"syncLink": {"url": "https://www.mozilla.org/en-US/firefox/accounts"}},
       "block_button_text": "Block",
       "section_title_icon": "resource://activity-stream/data/content/assets/glyph-pocket-16.svg",
       "section_title_text": "Messages from Mozilla",
     },
   },
   {
     "id": "SIMPLE_TEST_WITH_SECTION_HEADING_AND_LINK",
     "template": "simple_snippet",
     "content": {
       "icon": TEST_ICON,
+      "icon_dark_theme": TEST_ICON_BW,
       "title": "Firefox Account!",
       "text": "Sync it, link it, take it with you. All this and more with a Firefox Account.",
       "block_button_text": "Block",
       "section_title_icon": "resource://activity-stream/data/content/assets/glyph-pocket-16.svg",
       "section_title_text": "Messages from Mozilla (click for info)",
       "section_title_url": "https://www.mozilla.org/about",
     },
   },
   {
     "id": "SIMPLE_BELOW_SEARCH_TEST_1",
     "template": "simple_below_search_snippet",
     "content": {
       "icon": TEST_ICON,
+      "icon_dark_theme": TEST_ICON_BW,
       "text": "Securely store passwords, bookmarks, and more with a Firefox Account. <syncLink>Sign up</syncLink>",
       "links": {"syncLink": {"url": "https://www.mozilla.org/en-US/firefox/accounts"}},
       "block_button_text": "Block",
     },
   },
 ]);
 
 const SnippetsTestMessageProvider = {
--- a/browser/components/newtab/moz.build
+++ b/browser/components/newtab/moz.build
@@ -4,16 +4,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/.
 
 with Files("**"):
     BUG_COMPONENT = ("Firefox", "Activity Streams: Newtab")
 
 BROWSER_CHROME_MANIFESTS += ['test/browser/browser.ini']
 
+SPHINX_TREES['docs'] = 'docs'
+
 XPCSHELL_TESTS_MANIFESTS += [
     'test/xpcshell/xpcshell.ini',
 ]
 
 XPIDL_SOURCES += [
     'nsIAboutNewTabService.idl',
 ]
 
--- a/browser/components/newtab/package-lock.json
+++ b/browser/components/newtab/package-lock.json
@@ -1750,42 +1750,16 @@
         "has-value": "^1.0.0",
         "isobject": "^3.0.1",
         "set-value": "^2.0.0",
         "to-object-path": "^0.3.0",
         "union-value": "^1.0.0",
         "unset-value": "^1.0.0"
       }
     },
-    "caller-callsite": {
-      "version": "2.0.0",
-      "resolved": "https://registry.npmjs.org/caller-callsite/-/caller-callsite-2.0.0.tgz",
-      "integrity": "sha1-hH4PzgoiN1CpoCfFSzNzGtMVQTQ=",
-      "dev": true,
-      "requires": {
-        "callsites": "^2.0.0"
-      },
-      "dependencies": {
-        "callsites": {
-          "version": "2.0.0",
-          "resolved": "https://registry.npmjs.org/callsites/-/callsites-2.0.0.tgz",
-          "integrity": "sha1-BuuE8A7qQT2oav/vrL/7Ngk7PFA=",
-          "dev": true
-        }
-      }
-    },
-    "caller-path": {
-      "version": "2.0.0",
-      "resolved": "https://registry.npmjs.org/caller-path/-/caller-path-2.0.0.tgz",
-      "integrity": "sha1-Ro+DBE42mrIBD6xfBs7uFbsssfQ=",
-      "dev": true,
-      "requires": {
-        "caller-callsite": "^2.0.0"
-      }
-    },
     "callsite": {
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/callsite/-/callsite-1.0.0.tgz",
       "integrity": "sha1-KAOY5dZkvXQDi28JBRU+borxvCA=",
       "dev": true
     },
     "callsites": {
       "version": "3.1.0",
@@ -1911,22 +1885,16 @@
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.0.tgz",
       "integrity": "sha512-xDbVgyfDTT2piup/h8dK/y4QZfJRSa73bw1WZ8b4XM1o7fsFubUVGYcE+1ANtOzJJELGpYoG2961z0Z6OAld9A==",
       "dev": true,
       "requires": {
         "tslib": "^1.9.0"
       }
     },
-    "ci-info": {
-      "version": "2.0.0",
-      "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-2.0.0.tgz",
-      "integrity": "sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==",
-      "dev": true
-    },
     "cipher-base": {
       "version": "1.0.4",
       "resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.4.tgz",
       "integrity": "sha512-Kkht5ye6ZGmwv40uUDZztayT2ThLQGfnj/T71N/XzeZeo3nf8foyW7zGTsPYkEya3m5f3cAypH+qe7YOrM1U2Q==",
       "dev": true,
       "requires": {
         "inherits": "^2.0.1",
         "safe-buffer": "^5.0.1"
@@ -2191,57 +2159,16 @@
       "dev": true
     },
     "core-util-is": {
       "version": "1.0.2",
       "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz",
       "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=",
       "dev": true
     },
-    "cosmiconfig": {
-      "version": "5.1.0",
-      "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-5.1.0.tgz",
-      "integrity": "sha512-kCNPvthka8gvLtzAxQXvWo4FxqRB+ftRZyPZNuab5ngvM9Y7yw7hbEysglptLgpkGX9nAOKTBVkHUAe8xtYR6Q==",
-      "dev": true,
-      "requires": {
-        "import-fresh": "^2.0.0",
-        "is-directory": "^0.3.1",
-        "js-yaml": "^3.9.0",
-        "lodash.get": "^4.4.2",
-        "parse-json": "^4.0.0"
-      },
-      "dependencies": {
-        "import-fresh": {
-          "version": "2.0.0",
-          "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-2.0.0.tgz",
-          "integrity": "sha1-2BNVwVYS04bGH53dOSLUMEgipUY=",
-          "dev": true,
-          "requires": {
-            "caller-path": "^2.0.0",
-            "resolve-from": "^3.0.0"
-          }
-        },
-        "parse-json": {
-          "version": "4.0.0",
-          "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz",
-          "integrity": "sha1-vjX1Qlvh9/bHRxhPmKeIy5lHfuA=",
-          "dev": true,
-          "requires": {
-            "error-ex": "^1.3.1",
-            "json-parse-better-errors": "^1.0.1"
-          }
-        },
-        "resolve-from": {
-          "version": "3.0.0",
-          "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-3.0.0.tgz",
-          "integrity": "sha1-six699nWiBvItuZTM17rywoYh0g=",
-          "dev": true
-        }
-      }
-    },
     "cpx": {
       "version": "1.5.0",
       "resolved": "https://registry.npmjs.org/cpx/-/cpx-1.5.0.tgz",
       "integrity": "sha1-GFvgGFEdhycN7czCkxceN2VauI8=",
       "dev": true,
       "requires": {
         "babel-runtime": "^6.9.2",
         "chokidar": "^1.6.0",
@@ -5189,22 +5116,16 @@
       "dev": true
     },
     "get-func-name": {
       "version": "2.0.0",
       "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.0.tgz",
       "integrity": "sha1-6td0q+5y4gQJQzoGY2YCPdaIekE=",
       "dev": true
     },
-    "get-stdin": {
-      "version": "6.0.0",
-      "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-6.0.0.tgz",
-      "integrity": "sha512-jp4tHawyV7+fkkSKyvjuLZswblUtz+SQKzSWnBbii16BuZksJlU1wuBYXY75r+duh/llF1ur6oNwi+2ZzjKZ7g==",
-      "dev": true
-    },
     "get-stream": {
       "version": "4.1.0",
       "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz",
       "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==",
       "dev": true,
       "requires": {
         "pump": "^3.0.0"
       }
@@ -5640,87 +5561,16 @@
       }
     },
     "https-browserify": {
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/https-browserify/-/https-browserify-1.0.0.tgz",
       "integrity": "sha1-7AbBDgo0wPL68Zn3/X/Hj//QPHM=",
       "dev": true
     },
-    "husky": {
-      "version": "1.3.1",
-      "resolved": "https://registry.npmjs.org/husky/-/husky-1.3.1.tgz",
-      "integrity": "sha512-86U6sVVVf4b5NYSZ0yvv88dRgBSSXXmHaiq5pP4KDj5JVzdwKgBjEtUPOm8hcoytezFwbU+7gotXNhpHdystlg==",
-      "dev": true,
-      "requires": {
-        "cosmiconfig": "^5.0.7",
-        "execa": "^1.0.0",
-        "find-up": "^3.0.0",
-        "get-stdin": "^6.0.0",
-        "is-ci": "^2.0.0",
-        "pkg-dir": "^3.0.0",
-        "please-upgrade-node": "^3.1.1",
-        "read-pkg": "^4.0.1",
-        "run-node": "^1.0.0",
-        "slash": "^2.0.0"
-      },
-      "dependencies": {
-        "execa": {
-          "version": "1.0.0",
-          "resolved": "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz",
-          "integrity": "sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==",
-          "dev": true,
-          "requires": {
-            "cross-spawn": "^6.0.0",
-            "get-stream": "^4.0.0",
-            "is-stream": "^1.1.0",
-            "npm-run-path": "^2.0.0",
-            "p-finally": "^1.0.0",
-            "signal-exit": "^3.0.0",
-            "strip-eof": "^1.0.0"
-          }
-        },
-        "get-stream": {
-          "version": "4.1.0",
-          "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz",
-          "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==",
-          "dev": true,
-          "requires": {
-            "pump": "^3.0.0"
-          }
-        },
-        "parse-json": {
-          "version": "4.0.0",
-          "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz",
-          "integrity": "sha1-vjX1Qlvh9/bHRxhPmKeIy5lHfuA=",
-          "dev": true,
-          "requires": {
-            "error-ex": "^1.3.1",
-            "json-parse-better-errors": "^1.0.1"
-          }
-        },
-        "pify": {
-          "version": "3.0.0",
-          "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz",
-          "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=",
-          "dev": true
-        },
-        "read-pkg": {
-          "version": "4.0.1",
-          "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-4.0.1.tgz",
-          "integrity": "sha1-ljYlN48+HE1IyFhytabsfV0JMjc=",
-          "dev": true,
-          "requires": {
-            "normalize-package-data": "^2.3.2",
-            "parse-json": "^4.0.0",
-            "pify": "^3.0.0"
-          }
-        }
-      }
-    },
     "iconv-lite": {
       "version": "0.4.24",
       "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
       "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
       "dev": true,
       "requires": {
         "safer-buffer": ">= 2.1.2 < 3"
       }
@@ -5939,25 +5789,16 @@
       "dev": true
     },
     "is-callable": {
       "version": "1.1.4",
       "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.1.4.tgz",
       "integrity": "sha512-r5p9sxJjYnArLjObpjA4xu5EKI3CuKHkJXMhT7kwbpUyIFD1n5PMAsoPvWnvtZiNz7LjkYDRZhd7FlI0eMijEA==",
       "dev": true
     },
-    "is-ci": {
-      "version": "2.0.0",
-      "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-2.0.0.tgz",
-      "integrity": "sha512-YfJT7rkpQB0updsdHLGWrvhBJfcfzNNawYDNIyQXJz0IViGf75O8EBPKSdvw2rF+LGCsX4FZ8tcr3b19LcZq4w==",
-      "dev": true,
-      "requires": {
-        "ci-info": "^2.0.0"
-      }
-    },
     "is-data-descriptor": {
       "version": "0.1.4",
       "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz",
       "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=",
       "dev": true,
       "requires": {
         "kind-of": "^3.0.2"
       }
@@ -5982,22 +5823,16 @@
         "kind-of": {
           "version": "5.1.0",
           "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz",
           "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==",
           "dev": true
         }
       }
     },
-    "is-directory": {
-      "version": "0.3.1",
-      "resolved": "https://registry.npmjs.org/is-directory/-/is-directory-0.3.1.tgz",
-      "integrity": "sha1-YTObbyR1/Hcv2cnYP1yFddwVSuE=",
-      "dev": true
-    },
     "is-dotfile": {
       "version": "1.0.3",
       "resolved": "https://registry.npmjs.org/is-dotfile/-/is-dotfile-1.0.3.tgz",
       "integrity": "sha1-pqLzL/0t+wT1yiXs0Pa4PPeYoeE=",
       "dev": true
     },
     "is-equal-shallow": {
       "version": "0.1.3",
@@ -7236,22 +7071,16 @@
       "dev": true
     },
     "lodash.flattendeep": {
       "version": "4.4.0",
       "resolved": "https://registry.npmjs.org/lodash.flattendeep/-/lodash.flattendeep-4.4.0.tgz",
       "integrity": "sha1-+wMJF/hqMTTlvJvsDWngAT3f7bI=",
       "dev": true
     },
-    "lodash.get": {
-      "version": "4.4.2",
-      "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz",
-      "integrity": "sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk=",
-      "dev": true
-    },
     "lodash.isempty": {
       "version": "4.4.0",
       "resolved": "https://registry.npmjs.org/lodash.isempty/-/lodash.isempty-4.4.0.tgz",
       "integrity": "sha1-b4bL7di+TsmHvpqvM8loTbGzHn4=",
       "dev": true
     },
     "lodash.isequal": {
       "version": "4.5.0",
@@ -8855,25 +8684,16 @@
       "version": "3.0.0",
       "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-3.0.0.tgz",
       "integrity": "sha512-/E57AYkoeQ25qkxMj5PBOVgF8Kiu/h7cYS30Z5+R7WaiCCBfLq58ZI/dSeaEKb9WVJV5n/03QwrN3IeWIFllvw==",
       "dev": true,
       "requires": {
         "find-up": "^3.0.0"
       }
     },
-    "please-upgrade-node": {
-      "version": "3.1.1",
-      "resolved": "https://registry.npmjs.org/please-upgrade-node/-/please-upgrade-node-3.1.1.tgz",
-      "integrity": "sha512-KY1uHnQ2NlQHqIJQpnh/i54rKkuxCEBx+voJIS/Mvb+L2iYd2NMotwduhKTMjfC1uKoX3VXOxLjIYG66dfJTVQ==",
-      "dev": true,
-      "requires": {
-        "semver-compare": "^1.0.0"
-      }
-    },
     "pluralize": {
       "version": "1.2.1",
       "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-1.2.1.tgz",
       "integrity": "sha1-0aIUg/0iu0HlihL6NCGCMUCJfEU=",
       "dev": true
     },
     "pontoon-to-json": {
       "version": "2.0.0",
@@ -10009,22 +9829,16 @@
       "version": "2.3.0",
       "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.3.0.tgz",
       "integrity": "sha1-A3GrSuC91yDUFm19/aZP96RFpsA=",
       "dev": true,
       "requires": {
         "is-promise": "^2.1.0"
       }
     },
-    "run-node": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/run-node/-/run-node-1.0.0.tgz",
-      "integrity": "sha512-kc120TBlQ3mih1LSzdAJXo4xn/GWS2ec0l3S+syHDXP9uRr0JAT8Qd3mdMuyjqCzeZktgP3try92cEgf9Nks8A==",
-      "dev": true
-    },
     "run-queue": {
       "version": "1.0.3",
       "resolved": "https://registry.npmjs.org/run-queue/-/run-queue-1.0.3.tgz",
       "integrity": "sha1-6Eg5bwV9Ij8kOGkkYY4laUFh7Ec=",
       "dev": true,
       "requires": {
         "aproba": "^1.1.1"
       }
@@ -11142,22 +10956,16 @@
       }
     },
     "semver": {
       "version": "5.6.0",
       "resolved": "https://registry.npmjs.org/semver/-/semver-5.6.0.tgz",
       "integrity": "sha512-RS9R6R35NYgQn++fkDWaOmqGoj4Ek9gGs+DPxNUZKuwE183xjJroKvyo1IzVFeXvUrvmALy6FWD5xrdJT25gMg==",
       "dev": true
     },
-    "semver-compare": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/semver-compare/-/semver-compare-1.0.0.tgz",
-      "integrity": "sha1-De4hahyUGrN+nvsXiPavxf9VN/w=",
-      "dev": true
-    },
     "serialize-javascript": {
       "version": "1.7.0",
       "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-1.7.0.tgz",
       "integrity": "sha512-ke8UG8ulpFOxO8f8gRYabHQe/ZntKlcig2Mp+8+URDP1D8vJZ0KUt7LYo07q25Z/+JVSgpr/cui9PIp5H6/+nA==",
       "dev": true
     },
     "set-blocking": {
       "version": "2.0.0",
@@ -11264,22 +11072,16 @@
         "@sinonjs/formatio": "^3.2.1",
         "@sinonjs/samsam": "^3.3.1",
         "diff": "^3.5.0",
         "lolex": "^4.0.1",
         "nise": "^1.4.10",
         "supports-color": "^5.5.0"
       }
     },
-    "slash": {
-      "version": "2.0.0",
-      "resolved": "https://registry.npmjs.org/slash/-/slash-2.0.0.tgz",
-      "integrity": "sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A==",
-      "dev": true
-    },
     "slice-ansi": {
       "version": "2.1.0",
       "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-2.1.0.tgz",
       "integrity": "sha512-Qu+VC3EwYLldKa1fCxuuvULvSJOKEgk9pi8dZeCVK7TqBfUNTH4sFkk4joj8afVSfAYgJoSOetjx9QWOJ5mYoQ==",
       "dev": true,
       "requires": {
         "ansi-styles": "^3.2.0",
         "astral-regex": "^1.0.0",
--- a/browser/components/newtab/package.json
+++ b/browser/components/newtab/package.json
@@ -36,17 +36,16 @@
     "eslint-plugin-json": "1.4.0",
     "eslint-plugin-jsx-a11y": "6.2.1",
     "eslint-plugin-mozilla": "1.2.1",
     "eslint-plugin-no-unsanitized": "3.0.2",
     "eslint-plugin-promise": "4.1.1",
     "eslint-plugin-react": "7.12.4",
     "eslint-plugin-react-hooks": "1.6.0",
     "eslint-watch": "5.1.2",
-    "husky": "1.3.1",
     "istanbul-instrumenter-loader": "3.0.1",
     "joi-browser": "13.4.0",
     "karma": "4.1.0",
     "karma-chai": "0.1.0",
     "karma-coverage-istanbul-reporter": "2.0.5",
     "karma-firefox-launcher": "1.1.0",
     "karma-mocha": "1.3.0",
     "karma-mocha-reporter": "2.2.5",
@@ -128,22 +127,21 @@
     "testmc": "npm-run-all testmc:*",
     "testmc:lint": "npm run lint",
     "testmc:build": "npm run bundle:webpack && npm run bundle:locales",
     "testmc:unit": "karma start karma.mc.config.js",
     "tddmc": "karma start karma.mc.config.js --tdd",
     "debugcoverage": "open logs/coverage/index.html",
     "lint": "npm-run-all lint:*",
     "lint:eslint": "esw --ext=.js,.jsm,.json,.jsx .",
-    "lint:jsx-a11y": "esw --config=.eslintrc.jsx-a11y.js --ext=.jsx content-src/asrouter/components/ModalOverlay content-src/asrouter/templates/OnboardingMessage content-src/asrouter/templates/Trailhead",
+    "lint:jsx-a11y": "esw --config=.eslintrc.jsx-a11y.js --ext=.jsx content-src/asrouter content-src/components/ASRouterAdmin",
     "lint:sasslint": "sass-lint -v -q",
     "strings-import": "node ./bin/strings-import.js",
     "test": "npm run testmc",
     "tdd": "npm run tddmc",
-    "prepush": "npm run lint && npm run yamscripts",
     "vendor": "npm-run-all vendor:*",
     "vendor:react": "node ./bin/vendor-react.js",
     "help": "yamscripts help",
     "yamscripts": "yamscripts compile",
     "__": "# NOTE: THESE SCRIPTS ARE COMPILED!!! EDIT yamscripts.yml instead!!!"
   },
   "title": "Activity Stream",
   "permissions": {
--- a/browser/components/newtab/test/browser/browser.ini
+++ b/browser/components/newtab/test/browser/browser.ini
@@ -3,20 +3,22 @@ support-files =
   blue_page.html
   red_page.html
   head.js
 prefs =
   browser.newtabpage.activity-stream.debug=false
   browser.newtabpage.activity-stream.discoverystream.endpoints=data:
   browser.newtabpage.activity-stream.feeds.section.topstories=true
   browser.newtabpage.activity-stream.feeds.section.topstories.options={}
+  browser.newtabpage.activity-stream.asrouter.devtoolsEnabled=true
 
 [browser_activity_stream_strings.js]
 [browser_as_load_location.js]
 [browser_as_render.js]
+[browser_asrouter_snippets.js]
 [browser_asrouter_targeting.js]
 [browser_asrouter_trigger_listeners.js]
 [browser_discovery_styles.js]
 [browser_enabled_newtabpage.js]
 [browser_highlights_section.js]
 [browser_getScreenshots.js]
 [browser_newtab_overrides.js]
 [browser_packaged_as_locales.js]
new file mode 100644
--- /dev/null
+++ b/browser/components/newtab/test/browser/browser_asrouter_snippets.js
@@ -0,0 +1,37 @@
+"use strict";
+
+const {ASRouter} = ChromeUtils.import("resource://activity-stream/lib/ASRouter.jsm");
+
+test_newtab({
+  async before() {
+    let data = ASRouter.state.messages.find(m => m.id === "SIMPLE_BELOW_SEARCH_TEST_1");
+    ASRouter.messageChannel.sendAsyncMessage("ASRouter:parent-to-child", {type: "SET_MESSAGE", data});
+  },
+  test: async function test_simple_below_search_snippet() {
+    // Verify the simple_below_search_snippet renders in container below searchbox
+    // and nothing is rendered in the footer.
+    await ContentTaskUtils.waitForCondition(
+      () => content.document.querySelector(".below-search-snippet .SimpleBelowSearchSnippet"),
+      "Should find the snippet inside the below search container");
+
+    is(0, content.document.querySelector("#footer-asrouter-container").childNodes.length,
+      "Should not find any snippets in the footer container");
+  },
+});
+
+test_newtab({
+  async before() {
+    let data = ASRouter.state.messages.find(m => m.id === "SIMPLE_TEST_1");
+    ASRouter.messageChannel.sendAsyncMessage("ASRouter:parent-to-child", {type: "SET_MESSAGE", data});
+  },
+  test: async function test_simple_snippet() {
+    // Verify the simple_snippet renders in the footer and the container below
+    // searchbox is not rendered.
+    await ContentTaskUtils.waitForCondition(
+      () => content.document.querySelector("#footer-asrouter-container .SimpleSnippet"),
+      "Should find the snippet inside the footer container");
+
+    ok(!content.document.querySelector(".below-search-snippet"),
+      "Should not find any snippets below search");
+  },
+});
--- a/browser/components/newtab/test/unit/asrouter/ASRouter.test.js
+++ b/browser/components/newtab/test/unit/asrouter/ASRouter.test.js
@@ -1703,16 +1703,20 @@ describe("ASRouter", () => {
         assert.propertyVal(result, "experiment", expected.experiment);
         assert.propertyVal(result, "interrupt", expected.interrupt);
         assert.propertyVal(result, "triplet", expected.triplet);
       }
       it("should return control experience with no experiment if locale is NOT in TRAILHEAD_LOCALES", async () => {
         sandbox.stub(global.Services.locale, "appLocaleAsLangTag").get(() => "zh-CN");
         checkReturnValue({experiment: "", interrupt: "control", triplet: ""});
       });
+      it("should return control experience with no experiment if attribution data contains an addon source", async () => {
+        sandbox.stub(fakeAttributionCode, "getAttrDataAsync").resolves({source: "addons.mozilla.org"});
+        checkReturnValue({experiment: "", interrupt: "control", triplet: ""});
+      });
       it("should use values in override pref if it is set with no experiment", async () => {
         getStringPrefStub.withArgs(TRAILHEAD_CONFIG.OVERRIDE_PREF).returns("join-privacy");
         checkReturnValue({experiment: "", interrupt: "join", triplet: "privacy"});
 
         getStringPrefStub.withArgs(TRAILHEAD_CONFIG.OVERRIDE_PREF).returns("nofirstrun");
         checkReturnValue({experiment: "", interrupt: "nofirstrun", triplet: ""});
       });
       it("should return control experience with no experiment if locale is NOT in TRAILHEAD_LOCALES", async () => {
@@ -1723,16 +1727,29 @@ describe("ASRouter", () => {
         sandbox.stub(global.Services.locale, "appLocaleAsLangTag").get(() => "zh-CN");
         checkReturnValue({experiment: "", interrupt: "control", triplet: ""});
       });
       it("should roll for experiment if locale is in TRAILHEAD_LOCALES", async () => {
         sandbox.stub(global.Sampling, "ratioSample").resolves(1); // 1 = interrupts experiment
         sandbox.stub(global.Services.locale, "appLocaleAsLangTag").get(() => "en-US");
         checkReturnValue({experiment: "interrupts", interrupt: "join", triplet: "supercharge"});
       });
+      it("should roll for experiment if attribution data is empty", async () => {
+        sandbox.stub(global.Sampling, "ratioSample").resolves(1); // 1 = interrupts experiment
+        sandbox.stub(global.Services.locale, "appLocaleAsLangTag").get(() => "en-US");
+        sandbox.stub(fakeAttributionCode, "getAttrDataAsync").resolves(null);
+
+        checkReturnValue({experiment: "interrupts", interrupt: "join", triplet: "supercharge"});
+      });
+      it("should roll for experiment if attribution data rejects with an error", async () => {
+        sandbox.stub(global.Sampling, "ratioSample").resolves(1); // 1 = interrupts experiment
+        sandbox.stub(global.Services.locale, "appLocaleAsLangTag").get(() => "en-US");
+        sandbox.stub(fakeAttributionCode, "getAttrDataAsync").rejects(new Error("whoops"));
+        checkReturnValue({experiment: "interrupts", interrupt: "join", triplet: "supercharge"});
+      });
       it("should roll a triplet experiment", async () => {
         sandbox.stub(global.Sampling, "ratioSample").resolves(2); // 2 = triplets experiment
         sandbox.stub(global.Services.locale, "appLocaleAsLangTag").get(() => "en-US");
         checkReturnValue({experiment: "triplets", interrupt: "join", triplet: "multidevice"});
       });
       it("should roll no experiment", async () => {
         sandbox.stub(global.Sampling, "ratioSample").resolves(0); // 0 = no experiment
         sandbox.stub(global.Services.locale, "appLocaleAsLangTag").get(() => "en-US");
--- a/browser/components/newtab/test/unit/asrouter/templates/SimpleBelowSearchSnippet.test.jsx
+++ b/browser/components/newtab/test/unit/asrouter/templates/SimpleBelowSearchSnippet.test.jsx
@@ -35,13 +35,18 @@ describe("SimpleBelowSearchSnippet", () 
     sandbox.restore();
   });
 
   it("should render .text", () => {
     const wrapper = mountAndCheckProps({text: "bar"});
     assert.equal(wrapper.find(".body").text(), "bar");
   });
 
-  it("should render .icon", () => {
+  it("should render .icon (light theme)", () => {
     const wrapper = mountAndCheckProps({icon: "data:image/gif;base64,R0lGODl"});
-    assert.equal(wrapper.find(".icon").prop("src"), "data:image/gif;base64,R0lGODl");
+    assert.equal(wrapper.find(".icon-light-theme").prop("src"), "data:image/gif;base64,R0lGODl");
+  });
+
+  it("should render .icon (dark theme)", () => {
+    const wrapper = mountAndCheckProps({icon_dark_theme: "data:image/gif;base64,R0lGODl"});
+    assert.equal(wrapper.find(".icon-dark-theme").prop("src"), "data:image/gif;base64,R0lGODl");
   });
 });
--- a/browser/components/newtab/test/unit/asrouter/templates/SimpleSnippet.test.jsx
+++ b/browser/components/newtab/test/unit/asrouter/templates/SimpleSnippet.test.jsx
@@ -45,19 +45,27 @@ describe("SimpleSnippet", () => {
   it("should not render title element if no .title prop is supplied", () => {
     const wrapper = mountAndCheckProps();
     assert.lengthOf(wrapper.find(".title"), 0);
   });
   it("should render .title", () => {
     const wrapper = mountAndCheckProps({title: "Foo"});
     assert.equal(wrapper.find(".title").text().trim(), "Foo");
   });
-  it("should render .icon", () => {
+  it("should render a light theme variant .icon", () => {
     const wrapper = mountAndCheckProps({icon: "data:image/gif;base64,R0lGODl"});
-    assert.equal(wrapper.find(".icon").prop("src"), "data:image/gif;base64,R0lGODl");
+    assert.equal(wrapper.find(".icon-light-theme").prop("src"), "data:image/gif;base64,R0lGODl");
+  });
+  it("should render a dark theme variant .icon", () => {
+    const wrapper = mountAndCheckProps({icon_dark_theme: "data:image/gif;base64,R0lGODl"});
+    assert.equal(wrapper.find(".icon-dark-theme").prop("src"), "data:image/gif;base64,R0lGODl");
+  });
+  it("should render a light theme variant .icon as fallback", () => {
+    const wrapper = mountAndCheckProps({icon_dark_theme: "", icon: "data:image/gif;base64,R0lGODp"});
+    assert.equal(wrapper.find(".icon-dark-theme").prop("src"), "data:image/gif;base64,R0lGODp");
   });
   it("should render .button_label and default className", () => {
     const wrapper = mountAndCheckProps({
       button_label: "Click here",
       button_action: "OPEN_APPLICATIONS_MENU",
       button_action_args: "appMenu",
     });
 
@@ -76,23 +84,35 @@ describe("SimpleSnippet", () => {
   it("should wrap the main content if a section header is present", () => {
     const wrapper = mountAndCheckProps({
       section_title_icon: "data:image/gif;base64,R0lGODl",
       section_title_text: "Messages from Mozilla",
     });
 
     assert.lengthOf(wrapper.find(".innerContentWrapper"), 1);
   });
-  it("should render a section header if text and icon are specified", () => {
+  it("should render a section header if text and icon (light-theme) are specified", () => {
     const wrapper = mountAndCheckProps({
       section_title_icon: "data:image/gif;base64,R0lGODl",
       section_title_text: "Messages from Mozilla",
     });
 
-    assert.equal(wrapper.find(".section-title .icon").prop("style").backgroundImage, 'url("data:image/gif;base64,R0lGODl")');
+    assert.equal(wrapper.find(".section-title .icon-light-theme").prop("style").backgroundImage, 'url("data:image/gif;base64,R0lGODl")');
+    assert.equal(wrapper.find(".section-title-text").text().trim(), "Messages from Mozilla");
+    // ensure there is no <a> when a section_title_url is not specified
+    assert.lengthOf(wrapper.find(".section-title a"), 0);
+  });
+  it("should render a section header if text and icon (light-theme) are specified", () => {
+    const wrapper = mountAndCheckProps({
+      section_title_icon: "data:image/gif;base64,R0lGODl",
+      section_title_icon_dark_theme: "data:image/gif;base64,R0lGODl",
+      section_title_text: "Messages from Mozilla",
+    });
+
+    assert.equal(wrapper.find(".section-title .icon-dark-theme").prop("style").backgroundImage, 'url("data:image/gif;base64,R0lGODl")');
     assert.equal(wrapper.find(".section-title-text").text().trim(), "Messages from Mozilla");
     // ensure there is no <a> when a section_title_url is not specified
     assert.lengthOf(wrapper.find(".section-title a"), 0);
   });
   it("should render a section header wrapped in an <a> tag if a url is provided", () => {
     const wrapper = mountAndCheckProps({
       section_title_icon: "data:image/gif;base64,R0lGODl",
       section_title_text: "Messages from Mozilla",
--- a/browser/components/newtab/test/unit/asrouter/templates/SubmitFormSnippet.test.jsx
+++ b/browser/components/newtab/test/unit/asrouter/templates/SubmitFormSnippet.test.jsx
@@ -53,19 +53,23 @@ describe("SubmitFormSnippet", () => {
   it("should not render title element if no .title prop is supplied", () => {
     const wrapper = mountAndCheckProps();
     assert.lengthOf(wrapper.find(".title"), 0);
   });
   it("should render .title", () => {
     const wrapper = mountAndCheckProps({scene1_title: "Foo"});
     assert.equal(wrapper.find(".title").text().trim(), "Foo");
   });
-  it("should render .icon", () => {
+  it("should render light-theme .icon", () => {
     const wrapper = mountAndCheckProps({scene1_icon: "data:image/gif;base64,R0lGODl"});
-    assert.equal(wrapper.find(".icon").prop("src"), "data:image/gif;base64,R0lGODl");
+    assert.equal(wrapper.find(".icon-light-theme").prop("src"), "data:image/gif;base64,R0lGODl");
+  });
+  it("should render dark-theme .icon", () => {
+    const wrapper = mountAndCheckProps({scene1_icon_dark_theme: "data:image/gif;base64,R0lGODl"});
+    assert.equal(wrapper.find(".icon-dark-theme").prop("src"), "data:image/gif;base64,R0lGODl");
   });
   it("should render .button_label and default className", () => {
     const wrapper = mountAndCheckProps({scene1_button_label: "Click here"});
 
     const button = wrapper.find("button.ASRouterButton");
     assert.equal(button.text(), "Click here");
     assert.equal(button.prop("className"), "ASRouterButton secondary");
   });
--- a/browser/components/newtab/test/unit/content-src/components/ASRouterAdmin.test.jsx
+++ b/browser/components/newtab/test/unit/content-src/components/ASRouterAdmin.test.jsx
@@ -167,17 +167,17 @@ describe("ASRouterAdmin", () => {
       it("should hide message if provider filter changes", () => {
         wrapper.setState({
           messageFilter: "messageProvider",
           messages: [{id: "foo", provider: "messageProvider"}],
         });
 
         assert.lengthOf(wrapper.find(".message-id"), 1);
 
-        wrapper.find("select").simulate("change", {target: {value: "bar"}});
+        wrapper.find("select").simulate("blur", {target: {value: "bar"}});
 
         assert.lengthOf(wrapper.find(".message-id"), 0);
       });
     });
   });
   describe("#DiscoveryStream", () => {
     it("should render a DiscoveryStreamAdmin component", () => {
       wrapper = shallow(<DiscoveryStreamAdmin otherPrefs={{}} state={{
--- a/browser/components/newtab/test/unit/content-src/lib/selectLayoutRender.test.js
+++ b/browser/components/newtab/test/unit/content-src/lib/selectLayoutRender.test.js
@@ -1,16 +1,15 @@
 import {combineReducers, createStore} from "redux";
 import {actionTypes as at} from "common/Actions.jsm";
 import {GlobalOverrider} from "test/unit/utils";
 import {reducers} from "common/Reducers.jsm";
 import {selectLayoutRender} from "content-src/lib/selectLayoutRender";
-
-const FAKE_LAYOUT = [{width: 3, components: [{type: "foo", feed: {url: "foo.com"}}]}];
-const FAKE_FEEDS = {"foo.com": {data: {recommendations: ["foo", "bar"]}}};
+const FAKE_LAYOUT = [{width: 3, components: [{type: "foo", feed: {url: "foo.com"}, properties: {items: 2}}]}];
+const FAKE_FEEDS = {"foo.com": {data: {recommendations: [{id: "foo"}, {id: "bar"}]}}};
 
 describe("selectLayoutRender", () => {
   let store;
   let globals;
 
   beforeEach(() => {
     globals = new GlobalOverrider();
     store = createStore(combineReducers(reducers));
@@ -34,39 +33,39 @@ describe("selectLayoutRender", () => {
     store.dispatch({type: at.DISCOVERY_STREAM_LAYOUT_UPDATE, data: {layout: FAKE_LAYOUT}});
     store.dispatch({type: at.DISCOVERY_STREAM_FEED_UPDATE, data: {feed: FAKE_FEEDS["foo.com"], url: "foo.com"}});
     store.dispatch({type: at.DISCOVERY_STREAM_FEEDS_UPDATE});
 
     const {layoutRender} = selectLayoutRender(store.getState().DiscoveryStream, {}, []);
 
     assert.lengthOf(layoutRender, 1);
     assert.propertyVal(layoutRender[0], "width", 3);
-    assert.deepEqual(layoutRender[0].components[0], {type: "foo", feed: {url: "foo.com"}, data: {recommendations: ["foo", "bar"]}});
+    assert.deepEqual(layoutRender[0].components[0], {type: "foo", feed: {url: "foo.com"}, properties: {items: 2}, data: {recommendations: [{id: "foo", pos: 0}, {id: "bar", pos: 1}]}});
   });
 
-  it("should return layout property without data if feed isn't available", () => {
+  it("should return layout with placeholder data if feed isn't available", () => {
     store.dispatch({type: at.DISCOVERY_STREAM_LAYOUT_UPDATE, data: {layout: FAKE_LAYOUT}});
     store.dispatch({type: at.DISCOVERY_STREAM_FEEDS_UPDATE});
 
     const {layoutRender} = selectLayoutRender(store.getState().DiscoveryStream, {}, []);
 
     assert.lengthOf(layoutRender, 1);
     assert.propertyVal(layoutRender[0], "width", 3);
-    assert.deepEqual(layoutRender[0].components.length, 0);
+    assert.deepEqual(layoutRender[0].components[0].data.recommendations, [{placeholder: true}, {placeholder: true}]);
   });
 
   it("should return feed data offset by layout set prop", () => {
     const fakeLayout = [{width: 3, components: [{type: "foo", properties: {offset: 1}, feed: {url: "foo.com"}}]}];
     store.dispatch({type: at.DISCOVERY_STREAM_LAYOUT_UPDATE, data: {layout: fakeLayout}});
     store.dispatch({type: at.DISCOVERY_STREAM_FEED_UPDATE, data: {feed: FAKE_FEEDS["foo.com"], url: "foo.com"}});
     store.dispatch({type: at.DISCOVERY_STREAM_FEEDS_UPDATE});
 
     const {layoutRender} = selectLayoutRender(store.getState().DiscoveryStream, {}, []);
 
-    assert.deepEqual(layoutRender[0].components[0].data, {recommendations: ["bar"]});
+    assert.deepEqual(layoutRender[0].components[0].data, {recommendations: [{id: "bar"}]});
   });
 
   it("should return spoc result and spocs fill for rolls below the probability", () => {
     const fakeSpocConfig = {positions: [{index: 0}, {index: 1}], probability: 0.5};
     const fakeLayout = [{width: 3, components: [{type: "foo", feed: {url: "foo.com"}, spocs: fakeSpocConfig}]}];
     const fakeSpocsData = {lastUpdated: 0, spocs: {spocs: ["fooSpoc", "barSpoc"]}};
 
     store.dispatch({type: at.DISCOVERY_STREAM_LAYOUT_UPDATE, data: {layout: fakeLayout}});
@@ -76,18 +75,18 @@ describe("selectLayoutRender", () => {
     const randomStub = globals.sandbox.stub(global.Math, "random").returns(0.1);
 
     const {spocsFill, layoutRender} = selectLayoutRender(store.getState().DiscoveryStream, {}, []);
 
     assert.calledTwice(randomStub);
     assert.lengthOf(layoutRender, 1);
     assert.deepEqual(layoutRender[0].components[0].data.recommendations[0], "fooSpoc");
     assert.deepEqual(layoutRender[0].components[0].data.recommendations[1], "barSpoc");
-    assert.deepEqual(layoutRender[0].components[0].data.recommendations[2], "foo");
-    assert.deepEqual(layoutRender[0].components[0].data.recommendations[3], "bar");
+    assert.deepEqual(layoutRender[0].components[0].data.recommendations[2], {id: "foo"});
+    assert.deepEqual(layoutRender[0].components[0].data.recommendations[3], {id: "bar"});
 
     assert.deepEqual(spocsFill, [
       {id: undefined, reason: "n/a", displayed: 1, full_recalc: 0},
       {id: undefined, reason: "n/a", displayed: 1, full_recalc: 0},
     ]);
   });
 
   it("should return spoc result and spocs fill when there are more positions than spocs", () => {
@@ -102,18 +101,18 @@ describe("selectLayoutRender", () => {
     const randomStub = globals.sandbox.stub(global.Math, "random").returns(0.1);
 
     const {spocsFill, layoutRender} = selectLayoutRender(store.getState().DiscoveryStream, {}, []);
 
     assert.calledTwice(randomStub);
     assert.lengthOf(layoutRender, 1);
     assert.deepEqual(layoutRender[0].components[0].data.recommendations[0], "fooSpoc");
     assert.deepEqual(layoutRender[0].components[0].data.recommendations[1], "barSpoc");
-    assert.deepEqual(layoutRender[0].components[0].data.recommendations[2], "foo");
-    assert.deepEqual(layoutRender[0].components[0].data.recommendations[3], "bar");
+    assert.deepEqual(layoutRender[0].components[0].data.recommendations[2], {id: "foo"});
+    assert.deepEqual(layoutRender[0].components[0].data.recommendations[3], {id: "bar"});
 
     assert.deepEqual(spocsFill, [
       {id: undefined, reason: "n/a", displayed: 1, full_recalc: 0},
       {id: undefined, reason: "n/a", displayed: 1, full_recalc: 0},
     ]);
   });
 
   it("should report non-displayed spocs with reason as probability_selection and out_of_position", () => {
@@ -126,19 +125,19 @@ describe("selectLayoutRender", () => {
     store.dispatch({type: at.DISCOVERY_STREAM_FEEDS_UPDATE});
     store.dispatch({type: at.DISCOVERY_STREAM_SPOCS_UPDATE, data: fakeSpocsData});
     const randomStub = globals.sandbox.stub(global.Math, "random");
 
     const {spocsFill, layoutRender} = selectLayoutRender(store.getState().DiscoveryStream, {}, [0.7, 0.3, 0.8]);
 
     assert.notCalled(randomStub);
     assert.lengthOf(layoutRender, 1);
-    assert.deepEqual(layoutRender[0].components[0].data.recommendations[0], "foo");
+    assert.deepEqual(layoutRender[0].components[0].data.recommendations[0], {id: "foo"});
     assert.deepEqual(layoutRender[0].components[0].data.recommendations[1], "fooSpoc");
-    assert.deepEqual(layoutRender[0].components[0].data.recommendations[2], "bar");
+    assert.deepEqual(layoutRender[0].components[0].data.recommendations[2], {id: "bar"});
 
     assert.deepEqual(spocsFill, [
       {id: undefined, reason: "n/a", displayed: 1, full_recalc: 0},
       {id: undefined, reason: "probability_selection", displayed: 0, full_recalc: 0},
       {id: undefined, reason: "out_of_position", displayed: 0, full_recalc: 0},
     ]);
   });
 
@@ -152,18 +151,18 @@ describe("selectLayoutRender", () => {
     store.dispatch({type: at.DISCOVERY_STREAM_FEEDS_UPDATE});
     store.dispatch({type: at.DISCOVERY_STREAM_SPOCS_UPDATE, data: fakeSpocsData});
     const randomStub = globals.sandbox.stub(global.Math, "random").returns(0.6);
 
     const {spocsFill, layoutRender} = selectLayoutRender(store.getState().DiscoveryStream, {}, []);
 
     assert.calledTwice(randomStub);
     assert.lengthOf(layoutRender, 1);
-    assert.deepEqual(layoutRender[0].components[0].data.recommendations[0], "foo");
-    assert.deepEqual(layoutRender[0].components[0].data.recommendations[1], "bar");
+    assert.deepEqual(layoutRender[0].components[0].data.recommendations[0], {id: "foo"});
+    assert.deepEqual(layoutRender[0].components[0].data.recommendations[1], {id: "bar"});
 
     assert.deepEqual(spocsFill, [
       {id: undefined, reason: "probability_selection", displayed: 0, full_recalc: 0},
       {id: undefined, reason: "out_of_position", displayed: 0, full_recalc: 0},
     ]);
   });
 
   it("Subsequent render should return spoc result for cached rolls below the probability", () => {
@@ -178,18 +177,18 @@ describe("selectLayoutRender", () => {
     const randomStub = globals.sandbox.stub(global.Math, "random");
 
     const {spocsFill, layoutRender} = selectLayoutRender(store.getState().DiscoveryStream, {}, [0.4, 0.3]);
 
     assert.notCalled(randomStub);
     assert.lengthOf(layoutRender, 1);
     assert.deepEqual(layoutRender[0].components[0].data.recommendations[0], "fooSpoc");
     assert.deepEqual(layoutRender[0].components[0].data.recommendations[1], "barSpoc");
-    assert.deepEqual(layoutRender[0].components[0].data.recommendations[2], "foo");
-    assert.deepEqual(layoutRender[0].components[0].data.recommendations[3], "bar");
+    assert.deepEqual(layoutRender[0].components[0].data.recommendations[2], {id: "foo"});
+    assert.deepEqual(layoutRender[0].components[0].data.recommendations[3], {id: "bar"});
 
     assert.deepEqual(spocsFill, [
       {id: undefined, reason: "n/a", displayed: 1, full_recalc: 0},
       {id: undefined, reason: "n/a", displayed: 1, full_recalc: 0},
     ]);
   });
 
   it("Subsequent render should not return spoc result for cached rolls above the probability", () => {
@@ -202,18 +201,18 @@ describe("selectLayoutRender", () => {
     store.dispatch({type: at.DISCOVERY_STREAM_FEEDS_UPDATE});
     store.dispatch({type: at.DISCOVERY_STREAM_SPOCS_UPDATE, data: fakeSpocsData});
     const randomStub = globals.sandbox.stub(global.Math, "random");
 
     const {spocsFill, layoutRender} = selectLayoutRender(store.getState().DiscoveryStream, {}, [0.6, 0.7]);
 
     assert.notCalled(randomStub);
     assert.lengthOf(layoutRender, 1);
-    assert.deepEqual(layoutRender[0].components[0].data.recommendations[0], "foo");
-    assert.deepEqual(layoutRender[0].components[0].data.recommendations[1], "bar");
+    assert.deepEqual(layoutRender[0].components[0].data.recommendations[0], {id: "foo"});
+    assert.deepEqual(layoutRender[0].components[0].data.recommendations[1], {id: "bar"});
 
     assert.deepEqual(spocsFill, [
       {id: undefined, reason: "probability_selection", displayed: 0, full_recalc: 0},
       {id: undefined, reason: "out_of_position", displayed: 0, full_recalc: 0},
     ]);
   });
 
   it("Subsequent render should return spoc result by cached rolls probability", () => {
@@ -226,19 +225,19 @@ describe("selectLayoutRender", () => {
     store.dispatch({type: at.DISCOVERY_STREAM_FEEDS_UPDATE});
     store.dispatch({type: at.DISCOVERY_STREAM_SPOCS_UPDATE, data: fakeSpocsData});
     const randomStub = globals.sandbox.stub(global.Math, "random");
 
     const {spocsFill, layoutRender} = selectLayoutRender(store.getState().DiscoveryStream, {}, [0.7, 0.2]);
 
     assert.notCalled(randomStub);
     assert.lengthOf(layoutRender, 1);
-    assert.deepEqual(layoutRender[0].components[0].data.recommendations[0], "foo");
+    assert.deepEqual(layoutRender[0].components[0].data.recommendations[0], {id: "foo"});
     assert.deepEqual(layoutRender[0].components[0].data.recommendations[1], "fooSpoc");
-    assert.deepEqual(layoutRender[0].components[0].data.recommendations[2], "bar");
+    assert.deepEqual(layoutRender[0].components[0].data.recommendations[2], {id: "bar"});
 
     assert.deepEqual(spocsFill, [
       {id: undefined, reason: "n/a", displayed: 1, full_recalc: 0},
       {id: undefined, reason: "out_of_position", displayed: 0, full_recalc: 0},
     ]);
   });
 
   it("should return a layout with feeds of items length with positions", () => {
@@ -278,17 +277,19 @@ describe("selectLayoutRender", () => {
     }];
     store.dispatch({type: at.DISCOVERY_STREAM_LAYOUT_UPDATE, data: {layout: fakeLayout}});
     store.dispatch({type: at.DISCOVERY_STREAM_FEED_UPDATE, data: {feed: {data: {recommendations: []}}, url: "foo2.com"}});
 
     const {layoutRender} = selectLayoutRender(store.getState().DiscoveryStream, {}, []);
 
     assert.equal(layoutRender[0].components[0].type, "foo1");
     assert.equal(layoutRender[0].components[1].type, "foo2");
-    assert.equal(layoutRender[0].components[2], undefined);
+    assert.isTrue(layoutRender[0].components[2].data.recommendations[0].placeholder);
+    assert.lengthOf(layoutRender[0].components, 3);
+    assert.isUndefined(layoutRender[0].components[3]);
   });
   it("should render everything if everything is ready", () => {
     const fakeLayout = [{
       width: 3,
       components: [
         {type: "foo1"},
         {type: "foo2", properties: {items: 3}, feed: {url: "foo2.com"}},
         {type: "foo3", properties: {items: 3}, feed: {url: "foo3.com"}},
@@ -324,17 +325,17 @@ describe("selectLayoutRender", () => {
     store.dispatch({type: at.DISCOVERY_STREAM_FEED_UPDATE, data: {feed: {data: {recommendations: []}}, url: "foo2.com"}});
     store.dispatch({type: at.DISCOVERY_STREAM_FEED_UPDATE, data: {feed: {data: {recommendations: []}}, url: "foo3.com"}});
     store.dispatch({type: at.DISCOVERY_STREAM_FEED_UPDATE, data: {feed: {data: {recommendations: []}}, url: "foo4.com"}});
 
     const {layoutRender} = selectLayoutRender(store.getState().DiscoveryStream, {}, []);
 
     assert.equal(layoutRender[0].components[0].type, "foo1");
     assert.equal(layoutRender[0].components[1].type, "foo2");
-    assert.equal(layoutRender[0].components[2], undefined);
+    assert.deepEqual(layoutRender[0].components[2].data.recommendations, [{placeholder: true}, {placeholder: true}, {placeholder: true}]);
   });
   it("should not render a spoc if there are no available spocs", () => {
     const fakeLayout = [{
       width: 3,
       components: [
         {type: "foo1"},
         {type: "foo2", properties: {items: 3}, feed: {url: "foo2.com"}},
         {type: "foo3", properties: {items: 3}, feed: {url: "foo3.com"}, spocs: {positions: [{index: 0, probability: 1}]}},
@@ -367,9 +368,24 @@ describe("selectLayoutRender", () => {
     }];
     store.dispatch({type: at.DISCOVERY_STREAM_LAYOUT_UPDATE, data: {layout: fakeLayout}});
 
     const {layoutRender} = selectLayoutRender(store.getState().DiscoveryStream, {"feeds.topsites": true}, []);
 
     assert.equal(layoutRender[0].components[0].type, "TopSites");
     assert.equal(layoutRender[1], undefined);
   });
+  it("should not render a component if filtered", () => {
+    const fakeLayout = [{
+      width: 3,
+      components: [
+        {type: "Message"},
+        {type: "TopSites"},
+      ],
+    }];
+    store.dispatch({type: at.DISCOVERY_STREAM_LAYOUT_UPDATE, data: {layout: fakeLayout}});
+
+    const {layoutRender} = selectLayoutRender(store.getState().DiscoveryStream, {"feeds.topsites": true}, []);
+
+    assert.equal(layoutRender[0].components[0].type, "TopSites");
+    assert.equal(layoutRender[0].components[1], undefined);
+  });
 });
--- a/browser/components/newtab/yamscripts.yml
+++ b/browser/components/newtab/yamscripts.yml
@@ -59,27 +59,23 @@ scripts:
 
   tddmc: karma start karma.mc.config.js --tdd
 
   debugcoverage: open logs/coverage/index.html
 
 # lint: Run eslint and sass-lint
   lint:
     eslint: esw --ext=.js,.jsm,.json,.jsx .
-    jsx-a11y: esw --config=.eslintrc.jsx-a11y.js --ext=.jsx content-src/asrouter/components/ModalOverlay content-src/asrouter/templates/OnboardingMessage content-src/asrouter/templates/Trailhead
+    jsx-a11y: esw --config=.eslintrc.jsx-a11y.js --ext=.jsx content-src/asrouter content-src/components/ASRouterAdmin 
     sasslint: sass-lint -v -q
 
 # strings-import: Replace local strings with those from l10n-central
   strings-import: node ./bin/strings-import.js
 
 # test: Run all tests once
   test: =>testmc
 
 # tdd: Run content tests continuously
   tdd: =>tddmc
 
-  # This is just to make sure we don't make commits with failing tests
-  # or uncompiled yamscripts.yml. Run automatically with husky.
-  prepush: =>lint && =>yamscripts
-
   # Utility scripts for use when vendoring in Node packages
   vendor:
     react: node ./bin/vendor-react.js
--- a/browser/components/preferences/in-content/preferences.xul
+++ b/browser/components/preferences/in-content/preferences.xul
@@ -164,17 +164,17 @@
     <vbox class="main-content" flex="1" align="start">
       <vbox class="pane-container">
         <hbox class="sticky-container" pack="end" align="top">
           <hbox id="policies-container" align="stretch" flex="1" hidden="true">
             <hbox align="top">
               <image class="info-icon"></image>
             </hbox>
             <hbox align="center" flex="1">
-              <label class="policies-label" flex="1" data-l10n-id="policies-notice"></label>
+              <label class="policies-label" flex="1" data-l10n-id="managed-notice"></label>
             </hbox>
           </hbox>
           <textbox
             is="search-textbox" id="searchInput"
             data-l10n-id="search-input-box"
             data-l10n-attrs="style"
             hidden="true" clickSelectsAll="true"/>
         </hbox>
--- a/browser/components/preferences/in-content/privacy.js
+++ b/browser/components/preferences/in-content/privacy.js
@@ -75,17 +75,16 @@ Preferences.addAll([
   { id: "browser.formfill.enable", type: "bool" },
   { id: "privacy.history.custom", type: "bool" },
   // Cookies
   { id: "network.cookie.cookieBehavior", type: "int" },
   { id: "network.cookie.lifetimePolicy", type: "int" },
   { id: "network.cookie.blockFutureCookies", type: "bool" },
   // Content blocking category
   { id: "browser.contentblocking.category", type: "string"},
-  { id: "browser.contentblocking.features.standard", type: "string"},
   { id: "browser.contentblocking.features.strict", type: "string"},
 
   // Clear Private Data
   { id: "privacy.sanitize.sanitizeOnShutdown", type: "bool" },
   { id: "privacy.sanitize.timeSpan", type: "int" },
   // Do not track
   { id: "privacy.donottrackheader.enabled", type: "bool" },
 
@@ -472,32 +471,33 @@ var gPrivacyPane = {
       gPrivacyPane.readBlockCookies.bind(gPrivacyPane));
     Preferences.get("browser.contentblocking.category").on("change",
       gPrivacyPane.highlightCBCategory);
 
     // If any relevant content blocking pref changes, show a warning that the changes will
     // not be implemented until they refresh their tabs.
     for (let pref of CONTENT_BLOCKING_PREFS) {
       Preferences.get(pref).on("change", gPrivacyPane.maybeNotifyUserToReload);
+      // If the value changes, run populateCategoryContents, since that change might have been
+      // triggered by a default value changing in the standard category.
+      Preferences.get(pref).on("change", gPrivacyPane.populateCategoryContents);
     }
     Preferences.get("urlclassifier.trackingTable").on("change", gPrivacyPane.maybeNotifyUserToReload);
     for (let button of document.querySelectorAll(".reload-tabs-button")) {
       button.addEventListener("command", gPrivacyPane.reloadAllOtherTabs);
     }
 
     let cryptoMinersOption = document.getElementById("contentBlockingCryptominersOption");
     let fingerprintersOption = document.getElementById("contentBlockingFingerprintersOption");
 
     cryptoMinersOption.hidden =
       !Services.prefs.getBoolPref("browser.contentblocking.cryptomining.preferences.ui.enabled");
     fingerprintersOption.hidden =
       !Services.prefs.getBoolPref("browser.contentblocking.fingerprinting.preferences.ui.enabled");
 
-    Preferences.get("browser.contentblocking.features.standard").on("change",
-      this.populateCategoryContents);
     Preferences.get("browser.contentblocking.features.strict").on("change",
       this.populateCategoryContents);
     this.populateCategoryContents();
     this.highlightCBCategory();
     this.readBlockCookies();
 
     let link = document.getElementById("contentBlockingLearnMore");
     let contentBlockingUrl = Services.urlFormatter.formatURLPref("app.support.baseURL") + "content-blocking";
@@ -508,24 +508,48 @@ var gPrivacyPane = {
     let warningLinks = document.getElementsByClassName("content-blocking-warning-learn-how");
     for (let warningLink of warningLinks) {
       warningLink.setAttribute("href", contentBlockingTour);
     }
   },
 
   populateCategoryContents() {
     for (let type of ["strict", "standard"]) {
-      let rulesArray, selector;
+      let rulesArray = [];
+      let selector;
       if (type == "strict") {
         selector = "#contentBlockingOptionStrict";
         rulesArray = Services.prefs.getStringPref("browser.contentblocking.features.strict").split(",");
       } else {
         selector = "#contentBlockingOptionStandard";
-        let rulesString = Services.prefs.getStringPref("browser.contentblocking.features.standard");
-        rulesArray = rulesString.split(",");
+        // In standard show/hide UI items based on the default values of the relevant prefs.
+        let defaults = Services.prefs.getDefaultBranch("");
+
+        let cookieBehavior = defaults.getIntPref("network.cookie.cookieBehavior");
+        switch (cookieBehavior) {
+        case Ci.nsICookieService.BEHAVIOR_ACCEPT:
+          rulesArray.push("cookieBehavior0");
+          break;
+        case Ci.nsICookieService.BEHAVIOR_REJECT_FOREIGN:
+          rulesArray.push("cookieBehavior1");
+          break;
+        case Ci.nsICookieService.BEHAVIOR_REJECT:
+          rulesArray.push("cookieBehavior2");
+          break;
+        case Ci.nsICookieService.BEHAVIOR_LIMIT_FOREIGN:
+          rulesArray.push("cookieBehavior3");
+          break;
+        case Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER:
+          rulesArray.push("cookieBehavior4");
+          break;
+        }
+        rulesArray.push(defaults.getBoolPref("privacy.trackingprotection.cryptomining.enabled") ? "cm" : "-cm");
+        rulesArray.push(defaults.getBoolPref("privacy.trackingprotection.fingerprinting.enabled") ? "fp" : "-fp");
+        rulesArray.push(defaults.getBoolPref("privacy.trackingprotection.enabled") ? "tp" : "-tp");
+        rulesArray.push(defaults.getBoolPref("privacy.trackingprotection.pbmode.enabled") ? "tpPrivate" : "-tpPrivate");
       }
       // Hide all cookie options first, until we learn which one should be showing.
       document.querySelector(selector + " .all-cookies-option").hidden = true;
       document.querySelector(selector + " .unvisited-cookies-option").hidden = true;
       document.querySelector(selector + " .third-party-tracking-cookies-option").hidden = true;
       document.querySelector(selector + " .all-third-party-cookies-option").hidden = true;
 
       for (let item of rulesArray) {
--- a/browser/components/preferences/in-content/tests/browser_contentblocking_categories.js
+++ b/browser/components/preferences/in-content/tests/browser_contentblocking_categories.js
@@ -8,54 +8,67 @@ ChromeUtils.defineModuleGetter(this, "Pr
 
 const TP_PREF = "privacy.trackingprotection.enabled";
 const TP_PBM_PREF = "privacy.trackingprotection.pbmode.enabled";
 const NCB_PREF = "network.cookie.cookieBehavior";
 const CAT_PREF = "browser.contentblocking.category";
 const FP_PREF = "privacy.trackingprotection.fingerprinting.enabled";
 const CM_PREF = "privacy.trackingprotection.cryptomining.enabled";
 const STRICT_DEF_PREF = "browser.contentblocking.features.strict";
-const STANDARD_DEF_PREF = "browser.contentblocking.features.standard";
 
-// Tests that the content blocking standard category definition changes the behavior
-// of the standard category pref and all prefs it controls.
+// Tests that the content blocking standard category definition is based on the default settings of
+// the content blocking prefs.
 // Changing the definition does not remove the user from the category.
 add_task(async function testContentBlockingStandardDefinition() {
-  let defaults = Services.prefs.getDefaultBranch("");
-  let originalStandardPref = defaults.getStringPref(STANDARD_DEF_PREF);
-  defaults.setStringPref(STANDARD_DEF_PREF, "tp,tpPrivate,fp,cm,cookieBehavior4");
+  Services.prefs.setStringPref(CAT_PREF, "strict");
+  Services.prefs.setStringPref(CAT_PREF, "standard");
   is(Services.prefs.getStringPref(CAT_PREF), "standard", `${CAT_PREF} starts on standard`);
 
-  ok(!Services.prefs.prefHasUserValue(STANDARD_DEF_PREF), `We changed the default value of ${STANDARD_DEF_PREF}`);
-  is(Services.prefs.getStringPref(STANDARD_DEF_PREF), "tp,tpPrivate,fp,cm,cookieBehavior4", "The pref changed to what we set.");
-
-  is(Services.prefs.getBoolPref(TP_PREF), true, `${TP_PREF} pref has been set to true`);
-  is(Services.prefs.getBoolPref(TP_PBM_PREF), true, `${TP_PBM_PREF} pref has been set to true`);
-  is(Services.prefs.getBoolPref(FP_PREF), true, `${CM_PREF} pref has been set to true`);
-  is(Services.prefs.getBoolPref(CM_PREF), true, `${CM_PREF} pref has been set to true`);
-  is(Services.prefs.getIntPref(NCB_PREF), Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER, `${NCB_PREF} has been set to BEHAVIOR_REJECT_TRACKER`);
-
-  // Note, if a pref is not listed it will use the default value, however this is only meant as a
-  // backup if a mistake is made. The UI will not respond correctly.
-  defaults.setStringPref(STANDARD_DEF_PREF, "");
   ok(!Services.prefs.prefHasUserValue(TP_PREF), `${TP_PREF} pref has the default value`);
   ok(!Services.prefs.prefHasUserValue(TP_PBM_PREF), `${TP_PBM_PREF} pref has the default value`);
   ok(!Services.prefs.prefHasUserValue(FP_PREF), `${FP_PREF} pref has the default value`);
   ok(!Services.prefs.prefHasUserValue(CM_PREF), `${CM_PREF} pref has the default value`);
   ok(!Services.prefs.prefHasUserValue(NCB_PREF), `${NCB_PREF} pref has the default value`);
 
-  defaults.setStringPref(STANDARD_DEF_PREF, "-tpPrivate,-fp,-cm,-tp,cookieBehavior2");
-  is(Services.prefs.getBoolPref(TP_PREF), false, `${TP_PREF} pref has been set to false`);
-  is(Services.prefs.getBoolPref(TP_PBM_PREF), false, `${TP_PBM_PREF} pref has been set to false`);
-  is(Services.prefs.getBoolPref(FP_PREF), false, `${FP_PREF} pref has been set to false`);
-  is(Services.prefs.getBoolPref(CM_PREF), false, `${CM_PREF} pref has been set to false`);
-  is(Services.prefs.getIntPref(NCB_PREF), Ci.nsICookieService.BEHAVIOR_REJECT, `${NCB_PREF} has been set to BEHAVIOR_REJECT_TRACKER`);
+  let defaults = Services.prefs.getDefaultBranch("");
+  let originalTP = defaults.getBoolPref(TP_PREF);
+  let originalTPPBM = defaults.getBoolPref(TP_PBM_PREF);
+  let originalFP = defaults.getBoolPref(FP_PREF);
+  let originalCM = defaults.getBoolPref(CM_PREF);
+  let originalNCB = defaults.getIntPref(NCB_PREF);
+
+  let nonDefaultNCB;
+  switch (originalNCB) {
+  case Ci.nsICookieService.BEHAVIOR_ACCEPT:
+    nonDefaultNCB = Ci.nsICookieService.BEHAVIOR_REJECT;
+    break;
+  default:
+    nonDefaultNCB = Ci.nsICookieService.BEHAVIOR_ACCEPT;
+    break;
+  }
+  defaults.setIntPref(NCB_PREF, nonDefaultNCB);
+  defaults.setBoolPref(TP_PREF, !originalTP);
+  defaults.setBoolPref(TP_PBM_PREF, !originalTPPBM);
+  defaults.setBoolPref(FP_PREF, !originalFP);
+  defaults.setBoolPref(CM_PREF, !originalCM);
+  defaults.setIntPref(NCB_PREF, !originalNCB);
+
+  ok(!Services.prefs.prefHasUserValue(TP_PREF), `${TP_PREF} pref has the default value`);
+  ok(!Services.prefs.prefHasUserValue(TP_PBM_PREF), `${TP_PBM_PREF} pref has the default value`);
+  ok(!Services.prefs.prefHasUserValue(FP_PREF), `${FP_PREF} pref has the default value`);
+  ok(!Services.prefs.prefHasUserValue(CM_PREF), `${CM_PREF} pref has the default value`);
+  ok(!Services.prefs.prefHasUserValue(NCB_PREF), `${NCB_PREF} pref has the default value`);
 
   // cleanup
-  defaults.setStringPref(STANDARD_DEF_PREF, originalStandardPref);
+  defaults.setIntPref(NCB_PREF, originalNCB);
+  defaults.setBoolPref(TP_PREF, originalTP);
+  defaults.setBoolPref(TP_PBM_PREF, originalTPPBM);
+  defaults.setBoolPref(FP_PREF, originalFP);
+  defaults.setBoolPref(CM_PREF, originalCM);
+  defaults.setIntPref(NCB_PREF, originalNCB);
 });
 
 // Tests that the content blocking strict category definition changes the behavior
 // of the strict category pref and all prefs it controls.
 // Changing the definition does not remove the user from the category.
 add_task(async function testContentBlockingStrictDefinition() {
   let defaults = Services.prefs.getDefaultBranch("");
   let originalStrictPref = defaults.getStringPref(STRICT_DEF_PREF);
--- a/browser/components/urlbar/UrlbarInput.jsm
+++ b/browser/components/urlbar/UrlbarInput.jsm
@@ -638,17 +638,16 @@ class UrlbarInput {
     // TODO (Bug 1522902): This promise is necessary for tests, because some
     // tests are not listening for completion when starting a query through
     // other methods than startQuery (input events for example).
     this.lastQueryContextPromise = this.controller.startQuery(new UrlbarQueryContext({
       allowAutofill,
       isPrivate: this.isPrivate,
       maxResults: UrlbarPrefs.get("maxRichResults"),
       muxer: "UnifiedComplete",
-      providers: ["UnifiedComplete"],
       searchString,
       userContextId: this.window.gBrowser.selectedBrowser.getAttribute("usercontextid"),
     }));
   }
 
   /**
    * Sets the input's value, starts a search, and opens the popup.
    *
--- a/browser/components/urlbar/UrlbarProviderOpenTabs.jsm
+++ b/browser/components/urlbar/UrlbarProviderOpenTabs.jsm
@@ -90,23 +90,37 @@ class ProviderOpenTabs extends UrlbarPro
    * Returns the type of this provider.
    * @returns {integer} one of the types from UrlbarUtils.PROVIDER_TYPE.*
    */
   get type() {
     return UrlbarUtils.PROVIDER_TYPE.PROFILE;
   }
 
   /**
-   * Returns the sources returned by this provider.
-   * @returns {array} one or multiple types from UrlbarUtils.RESULT_SOURCE.*
+   * Whether this provider should be invoked for the given context.
+   * If this method returns false, the providers manager won't start a query
+   * with this provider, to save on resources.
+   * @param {UrlbarQueryContext} queryContext The query context object
+   * @returns {boolean} Whether this provider should be invoked for the search.
    */
-  get sources() {
-    return [
-      UrlbarUtils.RESULT_SOURCE.TABS,
-    ];
+  isActive(queryContext) {
+    // For now we don't actually use this provider to query open tabs, instead
+    // we join the temp table in UnifiedComplete.
+    return false;
+  }
+
+  /**
+   * Whether this provider wants to restrict results to just itself.
+   * Other providers won't be invoked, unless this provider doesn't
+   * support the current query.
+   * @param {UrlbarQueryContext} queryContext The query context object
+   * @returns {boolean} Whether this provider wants to restrict results.
+   */
+  isRestricting(queryContext) {
+    return false;
   }
 
   /**
    * Registers a tab as open.
    * @param {string} url Address of the tab
    * @param {integer} userContextId Containers user context id
    */
   registerOpenTab(url, userContextId = 0) {
--- a/browser/components/urlbar/UrlbarProviderUnifiedComplete.jsm
+++ b/browser/components/urlbar/UrlbarProviderUnifiedComplete.jsm
@@ -58,28 +58,35 @@ class ProviderUnifiedComplete extends Ur
    * Returns the type of this provider.
    * @returns {integer} one of the types from UrlbarUtils.PROVIDER_TYPE.*
    */
   get type() {
     return UrlbarUtils.PROVIDER_TYPE.IMMEDIATE;
   }
 
   /**
-   * Returns the sources returned by this provider.
-   * @returns {array} one or multiple types from UrlbarUtils.RESULT_SOURCE.*
+   * Whether this provider should be invoked for the given context.
+   * If this method returns false, the providers manager won't start a query
+   * with this provider, to save on resources.
+   * @param {UrlbarQueryContext} queryContext The query context object
+   * @returns {boolean} Whether this provider should be invoked for the search.
    */
-  get sources() {
-    return [
-      UrlbarUtils.RESULT_SOURCE.BOOKMARKS,
-      UrlbarUtils.RESULT_SOURCE.HISTORY,
-      UrlbarUtils.RESULT_SOURCE.SEARCH,
-      UrlbarUtils.RESULT_SOURCE.TABS,
-      UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
-      UrlbarUtils.RESULT_SOURCE.OTHER_NETWORK,
-    ];
+  isActive(queryContext) {
+    return true;
+  }
+
+  /**
+   * Whether this provider wants to restrict results to just itself.
+   * Other providers won't be invoked, unless this provider doesn't
+   * support the current query.
+   * @param {UrlbarQueryContext} queryContext The query context object
+   * @returns {boolean} Whether this provider wants to restrict results.
+   */
+  isRestricting(queryContext) {
+    return false;
   }
 
   /**
    * Starts querying.
    * @param {object} queryContext The query context object
    * @param {function} addCallback Callback invoked by the provider to add a new
    *        match.
    * @returns {Promise} resolved when the query stops.
--- a/browser/components/urlbar/UrlbarProvidersManager.jsm
+++ b/browser/components/urlbar/UrlbarProvidersManager.jsm
@@ -36,33 +36,28 @@ var localMuxerModules = {
   UrlbarMuxerUnifiedComplete: "resource:///modules/UrlbarMuxerUnifiedComplete.jsm",
 };
 
 // To improve dataflow and reduce UI work, when a match is added by a
 // non-immediate provider, we notify it to the controller after a delay, so
 // that we can chunk matches coming in that timeframe into a single call.
 const CHUNK_MATCHES_DELAY_MS = 16;
 
-const DEFAULT_PROVIDERS = ["UnifiedComplete"];
 const DEFAULT_MUXER = "UnifiedComplete";
 
 /**
  * Class used to create a manager.
  * The manager is responsible to keep a list of providers, instantiate query
  * objects and pass those to the providers.
  */
 class ProvidersManager {
   constructor() {
     // Tracks the available providers.
-    // This is a double map, first it maps by PROVIDER_TYPE, then
-    // registerProvider maps by provider.name: { type: { name: provider }}
-    this.providers = new Map();
-    for (let type of Object.values(UrlbarUtils.PROVIDER_TYPE)) {
-      this.providers.set(type, new Map());
-    }
+    // This is a sorted array, with IMMEDIATE providers at the top.
+    this.providers = [];
     for (let [symbol, module] of Object.entries(localProviderModules)) {
       let {[symbol]: provider} = ChromeUtils.import(module, {});
       this.registerProvider(provider);
     }
     // Tracks ongoing Query instances by queryContext.
     this.queries = new Map();
 
     // Interrupt() allows to stop any running SQL query, some provider may be
@@ -85,26 +80,33 @@ class ProvidersManager {
   registerProvider(provider) {
     if (!provider || !(provider instanceof UrlbarProvider)) {
       throw new Error(`Trying to register an invalid provider`);
     }
     if (!Object.values(UrlbarUtils.PROVIDER_TYPE).includes(provider.type)) {
       throw new Error(`Unknown provider type ${provider.type}`);
     }
     logger.info(`Registering provider ${provider.name}`);
-    this.providers.get(provider.type).set(provider.name, provider);
+    if (provider.type == UrlbarUtils.PROVIDER_TYPE.IMMEDIATE) {
+      this.providers.unshift(provider);
+    } else {
+      this.providers.push(provider);
+    }
   }
 
   /**
    * Unregisters a previously registered provider object.
    * @param {object} provider
    */
   unregisterProvider(provider) {
     logger.info(`Unregistering provider ${provider.name}`);
-    this.providers.get(provider.type).delete(provider.name);
+    let index = this.providers.indexOf(provider);
+    if (index != -1) {
+      this.providers.splice(index, 1);
+    }
   }
 
   /**
    * Registers a muxer object with the manager.
    * @param {object} muxer a UrlbarMuxer object
    */
   registerMuxer(muxer) {
     if (!muxer || !(muxer instanceof UrlbarMuxer)) {
@@ -134,19 +136,22 @@ class ProvidersManager {
 
     // Define the muxer to use.
     let muxerName = queryContext.muxer || DEFAULT_MUXER;
     logger.info(`Using muxer ${muxerName}`);
     let muxer = this.muxers.get(muxerName);
     if (!muxer) {
       throw new Error(`Muxer with name ${muxerName} not found`);
     }
-    // Define the list of providers to use.
-    let providers = queryContext.providers || DEFAULT_PROVIDERS;
-    providers = filterProviders(this.providers, providers);
+
+    // If the queryContext specifies a list of providers to use, filter on it,
+    // otherwise just pass the full list of providers.
+    let providers = queryContext.providers ?
+                      this.providers.filter(p => queryContext.providers.includes(p.name)) :
+                      this.providers;
 
     let query = new Query(queryContext, controller, muxer, providers);
     this.queries.set(queryContext, query);
     await query.start();
   }
 
   /**
    * Cancels a running query.
@@ -208,65 +213,65 @@ class Query {
     this.context = queryContext;
     this.context.results = [];
     this.muxer = muxer;
     this.controller = controller;
     this.providers = providers;
     this.started = false;
     this.canceled = false;
     this.complete = false;
-    // Array of acceptable RESULT_SOURCE values for this query. Providers not
-    // returning any of these will be skipped, as well as results not part of
-    // this subset (Note we still expect the provider to do its own internal
-    // filtering, our additional filtering will be for sanity).
+
+    // Array of acceptable RESULT_SOURCE values for this query. Providers can
+    // use queryContext.acceptableSources to decide whether they want to be
+    // invoked or not.
+    // This is also used to filter results in add().
     this.acceptableSources = [];
   }
 
   /**
    * Starts querying.
    */
   async start() {
     if (this.started) {
       throw new Error("This Query has been started already");
     }
     this.started = true;
     UrlbarTokenizer.tokenize(this.context);
+
     this.acceptableSources = getAcceptableMatchSources(this.context);
     logger.debug(`Acceptable sources ${this.acceptableSources}`);
+    // Pass a copy so the provider can't modify our local version.
+    this.context.acceptableSources = this.acceptableSources.slice();
 
+    // Check which providers should be queried.
+    let providers = this.providers.filter(p => p.isActive(this.context));
+    // Check if any of the remaining providers wants to restrict the search.
+    let restrictProviders = providers.filter(p => p.isRestricting(this.context));
+    if (restrictProviders.length) {
+      providers = restrictProviders;
+    }
+
+    // Start querying providers.
     let promises = [];
-    for (let provider of this.providers.get(UrlbarUtils.PROVIDER_TYPE.IMMEDIATE).values()) {
+    let delayStarted = false;
+    for (let provider of providers) {
       if (this.canceled) {
         break;
       }
-      // Immediate type providers may return heuristic results, that usually can
-      // bypass suggest.* preferences, so we always execute them, regardless of
-      // this.acceptableSources, and filter results in add().
+      if (provider.type != UrlbarUtils.PROVIDER_TYPE.IMMEDIATE && !delayStarted) {
+        delayStarted = true;
+        // Tracks the delay timer. We will fire (in this specific case, cancel
+        // would do the same, since the callback is empty) the timer when the
+        // search is canceled, unblocking start().
+        this._sleepTimer = new SkippableTimer(() => {}, UrlbarPrefs.get("delay"));
+        await this._sleepTimer.promise;
+      }
       promises.push(provider.startQuery(this.context, this.add.bind(this)));
     }
 
-    // Tracks the delay timer. We will fire (in this specific case, cancel would
-    // do the same, since the callback is empty) the timer when the search is
-    // canceled, unblocking start().
-    this._sleepTimer = new SkippableTimer(() => {}, UrlbarPrefs.get("delay"));
-    await this._sleepTimer.promise;
-
-    for (let providerType of [UrlbarUtils.PROVIDER_TYPE.NETWORK,
-                              UrlbarUtils.PROVIDER_TYPE.PROFILE,
-                              UrlbarUtils.PROVIDER_TYPE.EXTENSION]) {
-      for (let provider of this.providers.get(providerType).values()) {
-        if (this.canceled) {
-          break;
-        }
-        if (this._providerHasAcceptableSources(provider)) {
-          promises.push(provider.startQuery(this.context, this.add.bind(this)));
-        }
-      }
-    }
-
     logger.info(`Queried ${promises.length} providers`);
     if (promises.length) {
       await Promise.all(promises.map(p => p.catch(Cu.reportError)));
 
       if (this._chunkTimer) {
         // All the providers are done returning results, so we can stop chunking.
         await this._chunkTimer.fire();
       }
@@ -281,20 +286,18 @@ class Query {
    * Cancels this query.
    * @note Invoking cancel multiple times is a no-op.
    */
   cancel() {
     if (this.canceled) {
       return;
     }
     this.canceled = true;
-    for (let providers of this.providers.values()) {
-      for (let provider of providers.values()) {
-        provider.cancelQuery(this.context);
-      }
+    for (let provider of this.providers) {
+      provider.cancelQuery(this.context);
     }
     if (this._chunkTimer) {
       this._chunkTimer.cancel().catch(Cu.reportError);
     }
     if (this._sleepTimer) {
       this._sleepTimer.fire().catch(Cu.reportError);
     }
   }
@@ -344,25 +347,16 @@ class Query {
     // If the provider is not of immediate type, chunk results, to improve the
     // dataflow and reduce UI flicker.
     if (provider.type == UrlbarUtils.PROVIDER_TYPE.IMMEDIATE) {
       notifyResults();
     } else if (!this._chunkTimer) {
       this._chunkTimer = new SkippableTimer(notifyResults, CHUNK_MATCHES_DELAY_MS);
     }
   }
-
-  /**
-   * Returns whether a provider's sources are acceptable for this query.
-   * @param {object} provider A provider object.
-   * @returns {boolean}whether the provider sources are acceptable.
-   */
-  _providerHasAcceptableSources(provider) {
-    return provider.sources.some(s => this.acceptableSources.includes(s));
-  }
 }
 
 /**
  * Class used to create a timer that can be manually fired, to immediately
  * invoke the callback, or canceled, as necessary.
  * Examples:
  *   let timer = new SkippableTimer();
  *   // Invokes the callback immediately without waiting for the delay.
@@ -475,25 +469,8 @@ function getAcceptableMatchSources(conte
         if (!restrictTokenType) {
           acceptedSources.push(source);
         }
         break;
     }
   }
   return acceptedSources;
 }
-
-/* Given a providers Map and a list of provider names, produces a filtered
- * Map containing only the provided names.
- * @param providersMap {Map} providers mapped by type and name
- * @param names {array} list of provider names to retain
- * @returns {Map} a new filtered providers Map
- */
-function filterProviders(providersMap, names) {
-  let providers = new Map();
-  for (let [type, providersByName] of providersMap) {
-    providers.set(type, new Map());
-    for (let name of Array.from(providersByName.keys()).filter(n => names.includes(n))) {
-      providers.get(type).set(name, providersByName.get(name));
-    }
-  }
-  return providers;
-}
--- a/browser/components/urlbar/UrlbarUtils.jsm
+++ b/browser/components/urlbar/UrlbarUtils.jsm
@@ -481,21 +481,35 @@ class UrlbarProvider {
   /**
    * The type of the provider, must be one of UrlbarUtils.PROVIDER_TYPE.
    * @abstract
    */
   get type() {
     throw new Error("Trying to access the base class, must be overridden");
   }
   /**
-   * List of UrlbarUtils.RESULT_SOURCE, representing the data sources used by
-   * the provider.
+   * Whether this provider should be invoked for the given context.
+   * If this method returns false, the providers manager won't start a query
+   * with this provider, to save on resources.
+   * @param {UrlbarQueryContext} queryContext The query context object
+   * @returns {boolean} Whether this provider should be invoked for the search.
    * @abstract
    */
-  get sources() {
+  isActive(queryContext) {
+    throw new Error("Trying to access the base class, must be overridden");
+  }
+  /**
+   * Whether this provider wants to restrict results to just itself.
+   * Other providers won't be invoked, unless this provider doesn't
+   * support the current query.
+   * @param {UrlbarQueryContext} queryContext The query context object
+   * @returns {boolean} Whether this provider wants to restrict results.
+   * @abstract
+   */
+  isRestricting(queryContext) {
     throw new Error("Trying to access the base class, must be overridden");
   }
   /**
    * Starts querying.
    * @param {UrlbarQueryContext} queryContext The query context object
    * @param {function} addCallback Callback invoked by the provider to add a new
    *        result. A UrlbarResult should be passed to it.
    * @note Extended classes should return a Promise resolved when the provider
--- a/browser/components/urlbar/tests/unit/head.js
+++ b/browser/components/urlbar/tests/unit/head.js
@@ -72,61 +72,68 @@ function promiseControllerNotification(c
     controller.addQueryListener(proxifiedObserver);
   });
 }
 
 /**
  * A basic test provider, returning all the provided matches.
  */
 class TestProvider extends UrlbarProvider {
-  constructor(matches, cancelCallback) {
+  constructor(matches, cancelCallback, type = UrlbarUtils.PROVIDER_TYPE.PROFILE) {
     super();
     this._name = "TestProvider" + Math.floor(Math.random() * 100000);
     this._cancelCallback = cancelCallback;
     this._matches = matches;
+    this._type = type;
   }
   get name() {
     return this._name;
   }
   get type() {
-    return UrlbarUtils.PROVIDER_TYPE.PROFILE;
+    return this._type;
   }
-  get sources() {
-    return this._matches.map(r => r.source);
+  isActive(context) {
+    Assert.ok(context, "context is passed-in");
+    return true;
+  }
+  isRestricting(context) {
+    Assert.ok(context, "context is passed-in");
+    return false;
   }
   async startQuery(context, add) {
     Assert.ok(context, "context is passed-in");
     Assert.equal(typeof add, "function", "add is a callback");
     this._context = context;
     for (const match of this._matches) {
       add(this, match);
     }
   }
   cancelQuery(context) {
     // If the query was created but didn't run, this_context will be undefined.
     if (this._context) {
-      Assert.equal(this._context, context, "context is the same");
+      Assert.equal(this._context, context, "cancelQuery: context is the same");
     }
     if (this._cancelCallback) {
       this._cancelCallback();
     }
   }
 }
 
 /**
  * Helper function to clear the existing providers and register a basic provider
  * that returns only the results given.
  *
  * @param {array} matches The matches for the provider to return.
  * @param {function} [cancelCallback] Optional, called when the query provider
  *                                    receives a cancel instruction.
+ * @param {UrlbarUtils.PROVIDER_TYPE} type The provider type.
  * @returns {string} name of the registered provider
  */
-function registerBasicTestProvider(matches, cancelCallback) {
-  let provider = new TestProvider(matches, cancelCallback);
+function registerBasicTestProvider(matches = [], cancelCallback, type) {
+  let provider = new TestProvider(matches, cancelCallback, type);
   UrlbarProvidersManager.registerProvider(provider);
   return provider.name;
 }
 
 // Creates an HTTP server for the test.
 function makeTestServer(port = -1) {
   let httpServer = new HttpServer();
   httpServer.start(port);
--- a/browser/components/urlbar/tests/unit/test_UrlbarController_telemetry.js
+++ b/browser/components/urlbar/tests/unit/test_UrlbarController_telemetry.js
@@ -28,18 +28,21 @@ class DelayedProvider extends UrlbarProv
     this._name = "TestProvider" + Math.floor(Math.random() * 100000);
   }
   get name() {
     return this._name;
   }
   get type() {
     return UrlbarUtils.PROVIDER_TYPE.PROFILE;
   }
-  get sources() {
-    return [UrlbarUtils.RESULT_SOURCE.TABS];
+  isActive(context) {
+    return true;
+  }
+  isRestricting(context) {
+    return false;
   }
   async startQuery(context, add) {
     Assert.ok(context, "context is passed-in");
     Assert.equal(typeof add, "function", "add is a callback");
     this._add = add;
     await new Promise(resolve => {
       this._resultsAdded = resolve;
     });
--- a/browser/components/urlbar/tests/unit/test_providersManager_filtering.js
+++ b/browser/components/urlbar/tests/unit/test_providersManager_filtering.js
@@ -1,85 +1,109 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 "use strict";
 
-add_task(async function test_filtering() {
+/**
+ * A test controller.
+ */
+class TestUrlbarController extends UrlbarController {
+  constructor() {
+    super({
+      browserWindow: {
+        location: {
+          href: AppConstants.BROWSER_CHROME_URL,
+        },
+      },
+    });
+  }
+}
+
+add_task(async function test_filtering_disable_only_source() {
   let match = new UrlbarResult(UrlbarUtils.RESULT_TYPE.TAB_SWITCH,
                                UrlbarUtils.RESULT_SOURCE.TABS,
                                { url: "http://mozilla.org/foo/" });
   let providerName = registerBasicTestProvider([match]);
   let context = createContext(undefined, {providers: [providerName]});
-  let controller = new UrlbarController({
-    browserWindow: {
-      location: {
-        href: AppConstants.BROWSER_CHROME_URL,
-      },
-    },
-  });
+  let controller = new TestUrlbarController();
 
   info("Disable the only available source, should get no matches");
   Services.prefs.setBoolPref("browser.urlbar.suggest.openpage", false);
   let promise = Promise.race([
     promiseControllerNotification(controller, "onQueryResults", false),
     promiseControllerNotification(controller, "onQueryFinished"),
   ]);
   await controller.startQuery(context);
   await promise;
   Services.prefs.clearUserPref("browser.urlbar.suggest.openpage");
+  UrlbarProvidersManager.unregisterProvider({name: providerName});
+});
 
+add_task(async function test_filtering_disable_one_source() {
   let matches = [
-    match,
+    new UrlbarResult(UrlbarUtils.RESULT_TYPE.TAB_SWITCH,
+                     UrlbarUtils.RESULT_SOURCE.TABS,
+                     { url: "http://mozilla.org/foo/" }),
     new UrlbarResult(UrlbarUtils.RESULT_TYPE.TAB_SWITCH,
                      UrlbarUtils.RESULT_SOURCE.HISTORY,
                      { url: "http://mozilla.org/foo/" }),
   ];
-  providerName = registerBasicTestProvider(matches);
-  context = createContext(undefined, {providers: [providerName]});
+  let providerName = registerBasicTestProvider(matches);
+  let context = createContext(undefined, {providers: [providerName]});
+  let controller = new TestUrlbarController();
 
   info("Disable one of the sources, should get a single match");
   Services.prefs.setBoolPref("browser.urlbar.suggest.history", false);
-  promise = Promise.all([
+  let promise = Promise.all([
     promiseControllerNotification(controller, "onQueryResults"),
     promiseControllerNotification(controller, "onQueryFinished"),
   ]);
   await controller.startQuery(context, controller);
   await promise;
-  Assert.deepEqual(context.results, [match]);
+  Assert.deepEqual(context.results, matches.slice(0, 1));
   Services.prefs.clearUserPref("browser.urlbar.suggest.history");
+  UrlbarProvidersManager.unregisterProvider({name: providerName});
+});
+
+add_task(async function test_filtering_restriction_token() {
+  let matches = [
+    new UrlbarResult(UrlbarUtils.RESULT_TYPE.TAB_SWITCH,
+                     UrlbarUtils.RESULT_SOURCE.TABS,
+                     { url: "http://mozilla.org/foo/" }),
+    new UrlbarResult(UrlbarUtils.RESULT_TYPE.TAB_SWITCH,
+                     UrlbarUtils.RESULT_SOURCE.HISTORY,
+                     { url: "http://mozilla.org/foo/" }),
+  ];
+  let providerName = registerBasicTestProvider(matches);
+  let context = createContext(`foo ${UrlbarTokenizer.RESTRICT.OPENPAGE}`,
+                              {providers: [providerName]});
+  let controller = new TestUrlbarController();
 
   info("Use a restriction character, should get a single match");
-  context = createContext(`foo ${UrlbarTokenizer.RESTRICT.OPENPAGE}`,
-                          {providers: [providerName]});
-  promise = Promise.all([
+  let promise = Promise.all([
     promiseControllerNotification(controller, "onQueryResults"),
     promiseControllerNotification(controller, "onQueryFinished"),
   ]);
   await controller.startQuery(context, controller);
   await promise;
-  Assert.deepEqual(context.results, [match]);
+  Assert.deepEqual(context.results, matches.slice(0, 1));
+  UrlbarProvidersManager.unregisterProvider({name: providerName});
 });
 
 add_task(async function test_filter_javascript() {
-  let controller = new UrlbarController({
-    browserWindow: {
-      location: {
-        href: AppConstants.BROWSER_CHROME_URL,
-      },
-    },
-  });
   let match = new UrlbarResult(UrlbarUtils.RESULT_TYPE.TAB_SWITCH,
                                UrlbarUtils.RESULT_SOURCE.TABS,
                                { url: "http://mozilla.org/foo/" });
   let jsMatch = new UrlbarResult(UrlbarUtils.RESULT_TYPE.TAB_SWITCH,
                                  UrlbarUtils.RESULT_SOURCE.HISTORY,
                                  { url: "javascript:foo" });
   let providerName = registerBasicTestProvider([match, jsMatch]);
   let context = createContext(undefined, {providers: [providerName]});
+  let controller = new TestUrlbarController();
 
   info("By default javascript should be filtered out");
   let promise = promiseControllerNotification(controller, "onQueryResults");
   await controller.startQuery(context, controller);
   await promise;
   Assert.deepEqual(context.results, [match]);
 
   info("Except when the user explicitly starts the search with javascript:");
@@ -93,242 +117,275 @@ add_task(async function test_filter_java
   info("Disable javascript filtering");
   Services.prefs.setBoolPref("browser.urlbar.filter.javascript", false);
   context = createContext(undefined, {providers: [providerName]});
   promise = promiseControllerNotification(controller, "onQueryResults");
   await controller.startQuery(context, controller);
   await promise;
   Assert.deepEqual(context.results, [match, jsMatch]);
   Services.prefs.clearUserPref("browser.urlbar.filter.javascript");
+  UrlbarProvidersManager.unregisterProvider({name: providerName});
 });
 
-add_task(async function test_filter_sources() {
-  let controller = new UrlbarController({
-    browserWindow: {
-      location: {
-        href: AppConstants.BROWSER_CHROME_URL,
-      },
-    },
-  });
-
+add_task(async function test_filter_isActive() {
   let goodMatches = [
     new UrlbarResult(UrlbarUtils.RESULT_TYPE.TAB_SWITCH,
                      UrlbarUtils.RESULT_SOURCE.TABS,
                      { url: "http://mozilla.org/foo/" }),
     new UrlbarResult(UrlbarUtils.RESULT_TYPE.URL,
                      UrlbarUtils.RESULT_SOURCE.HISTORY,
                      { url: "http://mozilla.org/foo/" }),
   ];
-  /**
-   * A test provider that should be invoked.
-   */
-  class TestProvider extends UrlbarProvider {
-    get name() {
-      return "GoodProvider";
-    }
-    get type() {
-      return UrlbarUtils.PROVIDER_TYPE.PROFILE;
-    }
-    get sources() {
-      return [
-        UrlbarUtils.RESULT_SOURCE.TABS,
-        UrlbarUtils.RESULT_SOURCE.HISTORY,
-      ];
-    }
-    async startQuery(context, add) {
-      Assert.ok(true, "expected provider was invoked");
-      for (const match of goodMatches) {
-        add(this, match);
-      }
-    }
-    cancelQuery(context) {}
-  }
-  UrlbarProvidersManager.registerProvider(new TestProvider());
+  let providerName = registerBasicTestProvider(goodMatches);
 
   let badMatches = [
     new UrlbarResult(UrlbarUtils.RESULT_TYPE.URL,
                      UrlbarUtils.RESULT_SOURCE.BOOKMARKS,
                      { url: "http://mozilla.org/foo/" }),
   ];
-
   /**
    * A test provider that should not be invoked.
    */
   class NoInvokeProvider extends UrlbarProvider {
     get name() {
       return "BadProvider";
     }
     get type() {
       return UrlbarUtils.PROVIDER_TYPE.PROFILE;
     }
-    get sources() {
-      return [UrlbarUtils.RESULT_SOURCE.BOOKMARKS];
+    isActive(context) {
+      info("Acceptable sources: " + context.acceptableSources);
+      return context.acceptableSources.includes(UrlbarUtils.RESULT_SOURCE.BOOKMARKS);
+    }
+    isRestricting(context) {
+      return false;
     }
     async startQuery(context, add) {
       Assert.ok(false, "Provider should no be invoked");
       for (const match of badMatches) {
         add(this, match);
       }
     }
     cancelQuery(context) {}
   }
-
   UrlbarProvidersManager.registerProvider(new NoInvokeProvider());
 
   let context = createContext(undefined, {
     sources: [UrlbarUtils.RESULT_SOURCE.TABS],
-    providers: ["GoodProvider", "BadProvider"],
+    providers: [providerName, "BadProvider"],
   });
+  let controller = new TestUrlbarController();
 
   info("Only tabs should be returned");
   let promise = promiseControllerNotification(controller, "onQueryResults");
   await controller.startQuery(context, controller);
   await promise;
   Assert.deepEqual(context.results.length, 1, "Should find only one match");
   Assert.deepEqual(context.results[0].source, UrlbarUtils.RESULT_SOURCE.TABS,
                    "Should find only a tab match");
+  UrlbarProvidersManager.unregisterProvider({name: providerName});
+  UrlbarProvidersManager.unregisterProvider({name: "BadProvider"});
+});
+
+add_task(async function test_filter_queryContext() {
+  let providerName = registerBasicTestProvider();
+
+  /**
+   * A test provider that should not be invoked because of queryContext.providers.
+   */
+  class NoInvokeProvider extends UrlbarProvider {
+    get name() {
+      return "BadProvider";
+    }
+    get type() {
+      return UrlbarUtils.PROVIDER_TYPE.PROFILE;
+    }
+    isActive(context) {
+      return true;
+    }
+    isRestricting(context) {
+      return false;
+    }
+    async startQuery(context, add) {
+      Assert.ok(false, "Provider should no be invoked");
+    }
+    cancelQuery(context) {}
+  }
+  UrlbarProvidersManager.registerProvider(new NoInvokeProvider());
+
+  let context = createContext(undefined, {
+    providers: [providerName],
+  });
+  let controller = new TestUrlbarController();
+
+  await controller.startQuery(context, controller);
+  UrlbarProvidersManager.unregisterProvider({name: providerName});
+  UrlbarProvidersManager.unregisterProvider({name: "BadProvider"});
 });
 
 add_task(async function test_nofilter_immediate() {
   // Checks that even if a provider returns a result that should be filtered out
   // it will still be invoked if it's of type immediate, and only the heuristic
   // result is returned.
-  let controller = new UrlbarController({
-    browserWindow: {
-      location: {
-        href: AppConstants.BROWSER_CHROME_URL,
-      },
-    },
-  });
-
   let matches = [
     new UrlbarResult(UrlbarUtils.RESULT_TYPE.TAB_SWITCH,
                      UrlbarUtils.RESULT_SOURCE.TABS,
                      { url: "http://mozilla.org/foo/" }),
     new UrlbarResult(UrlbarUtils.RESULT_TYPE.TAB_SWITCH,
                      UrlbarUtils.RESULT_SOURCE.TABS,
                      { url: "http://mozilla.org/foo2/" }),
   ];
   matches[0].heuristic = true;
-
-  /**
-   * A test provider that should be invoked.
-   */
-  class TestProvider extends UrlbarProvider {
-    get name() {
-      return "GoodProvider";
-    }
-    get type() {
-      return UrlbarUtils.PROVIDER_TYPE.IMMEDIATE;
-    }
-    get sources() {
-      return [
-        UrlbarUtils.RESULT_SOURCE.TABS,
-      ];
-    }
-    async startQuery(context, add) {
-      Assert.ok(true, "expected provider was invoked");
-      for (let match of matches) {
-        add(this, match);
-      }
-    }
-    cancelQuery(context) {}
-  }
-  UrlbarProvidersManager.registerProvider(new TestProvider());
+  let providerName = registerBasicTestProvider(matches, undefined,
+    UrlbarUtils.PROVIDER_TYPE.IMMEDIATE);
 
   let context = createContext(undefined, {
     sources: [UrlbarUtils.RESULT_SOURCE.SEARCH],
-    providers: ["GoodProvider"],
+    providers: [providerName],
   });
+  let controller = new TestUrlbarController();
+
   // Disable search matches through prefs.
   Services.prefs.setBoolPref("browser.urlbar.suggest.openpage", false);
-
   info("Only 1 heuristic tab result should be returned");
   let promise = promiseControllerNotification(controller, "onQueryResults");
   await controller.startQuery(context, controller);
   await promise;
   Services.prefs.clearUserPref("browser.urlbar.suggest.openpage");
   Assert.deepEqual(context.results.length, 1, "Should find only one match");
   Assert.deepEqual(context.results[0].source, UrlbarUtils.RESULT_SOURCE.TABS,
                    "Should find only a tab match");
+  UrlbarProvidersManager.unregisterProvider({name: providerName});
 });
 
 add_task(async function test_nofilter_restrict() {
   // Checks that even if a pref is disabled, we still return results on a
   // restriction token.
-  let controller = new UrlbarController({
-    browserWindow: {
-      location: {
-        href: AppConstants.BROWSER_CHROME_URL,
-      },
-    },
-  });
-
   let matches = [
     new UrlbarResult(UrlbarUtils.RESULT_TYPE.TAB_SWITCH,
                      UrlbarUtils.RESULT_SOURCE.TABS,
                      { url: "http://mozilla.org/foo_tab/" }),
     new UrlbarResult(UrlbarUtils.RESULT_TYPE.URL,
                      UrlbarUtils.RESULT_SOURCE.BOOKMARKS,
                      { url: "http://mozilla.org/foo_bookmark/" }),
     new UrlbarResult(UrlbarUtils.RESULT_TYPE.URL,
                      UrlbarUtils.RESULT_SOURCE.HISTORY,
                      { url: "http://mozilla.org/foo_history/" }),
     new UrlbarResult(UrlbarUtils.RESULT_TYPE.SEARCH,
                      UrlbarUtils.RESULT_SOURCE.SEARCH,
                      { engine: "noengine" }),
   ];
-
   /**
    * A test provider.
    */
   class TestProvider extends UrlbarProvider {
     get name() {
       return "MyProvider";
     }
     get type() {
       return UrlbarUtils.PROVIDER_TYPE.IMMEDIATE;
     }
-    get sources() {
-      return [
-        UrlbarUtils.RESULT_SOURCE.TABS,
-        UrlbarUtils.RESULT_SOURCE.BOOKMARKS,
-        UrlbarUtils.RESULT_SOURCE.HISTORY,
-        UrlbarUtils.RESULT_SOURCE.SEARCH,
-      ];
+    isActive(context) {
+      Assert.equal(context.acceptableSources.length, 1,
+                   "Check acceptableSources");
+      return true;
+    }
+    isRestricting(context) {
+      return false;
     }
     async startQuery(context, add) {
       Assert.ok(true, "expected provider was invoked");
       for (let match of matches) {
         add(this, match);
       }
     }
-    cancelQuery(context) {}
+    cancelQuery(context) {
+    }
   }
-  UrlbarProvidersManager.registerProvider(new TestProvider());
+  let provider = new TestProvider();
+  UrlbarProvidersManager.registerProvider(provider);
 
   let typeToPropertiesMap = new Map([
     ["HISTORY", {source: "HISTORY", pref: "history"}],
     ["BOOKMARK", {source: "BOOKMARKS", pref: "bookmark"}],
     ["OPENPAGE", {source: "TABS", pref: "openpage"}],
     ["SEARCH", {source: "SEARCH", pref: "searches"}],
   ]);
   for (let [type, token] of Object.entries(UrlbarTokenizer.RESTRICT)) {
     let properties = typeToPropertiesMap.get(type);
     if (!properties) {
       continue;
     }
     info("Restricting on " + type);
     let context = createContext(token + " foo", {
       providers: ["MyProvider"],
     });
+    let controller = new TestUrlbarController();
     // Disable the corresponding pref.
     const pref = "browser.urlbar.suggest." + properties.pref;
     info("Disabling " + pref);
     Services.prefs.setBoolPref(pref, false);
     await controller.startQuery(context, controller);
     Assert.equal(context.results.length, 1, "Should find one result");
     Assert.equal(context.results[0].source,
                  UrlbarUtils.RESULT_SOURCE[properties.source],
                  "Check result source");
     Services.prefs.clearUserPref(pref);
   }
+  UrlbarProvidersManager.unregisterProvider(provider);
 });
+
+add_task(async function test_filter_isRestricting() {
+  /**
+   * A test provider that should be invoked and is restricting.
+   */
+  class TestProvider extends UrlbarProvider {
+    get name() {
+      return "GoodProvider";
+    }
+    get type() {
+      return UrlbarUtils.PROVIDER_TYPE.PROFILE;
+    }
+    isActive(context) {
+      return true;
+    }
+    isRestricting(context) {
+      return true;
+    }
+    async startQuery(context, add) {
+      Assert.ok(true, "expected provider was invoked");
+    }
+    cancelQuery(context) {}
+  }
+  UrlbarProvidersManager.registerProvider(new TestProvider());
+
+  /**
+   * A test provider that should not be invoked because the other one is restricting.
+   */
+  class NoInvokeProvider extends UrlbarProvider {
+    get name() {
+      return "BadProvider";
+    }
+    get type() {
+      return UrlbarUtils.PROVIDER_TYPE.PROFILE;
+    }
+    isActive(context) {
+      return true;
+    }
+    isRestricting(context) {
+      return false;
+    }
+    async startQuery(context, add) {
+      Assert.ok(false, "Provider should no be invoked");
+    }
+    cancelQuery(context) {}
+  }
+  UrlbarProvidersManager.registerProvider(new NoInvokeProvider());
+
+  let context = createContext(undefined, {
+    providers: ["GoodProvider", "BadProvider"],
+  });
+  let controller = new TestUrlbarController();
+
+  await controller.startQuery(context, controller);
+  UrlbarProvidersManager.unregisterProvider({name: "GoodProvider"});
+  UrlbarProvidersManager.unregisterProvider({name: "BadProvider"});
+});
--- a/browser/docs/AddressBar.rst
+++ b/browser/docs/AddressBar.rst
@@ -74,16 +74,18 @@ It is augmented as it progresses through
     sources; // {array} If provided is the list of sources, as defined by
              // RESULT_SOURCE.*, that can be returned by the model.
 
     // Properties added by the Model.
     preselected; // {boolean} whether the first result should be preselected.
     results; // {array} list of UrlbarResult objects.
     tokens; // {array} tokens extracted from the searchString, each token is an
             // object in the form {type, value, lowerCaseValue}.
+    acceptableSources; // {array} list of UrlbarUtils.RESULT_SOURCE that the
+                       // model will accept for this context.
   }
 
 
 The Model
 =========
 
 The *Model* is the component responsible for retrieving search results based on
 the user's input, and sorting them accordingly to their importance.
@@ -151,56 +153,70 @@ implementation details may vary deeply a
   Internal providers can access the Places database through the
   *PlacesUtils.promiseLargeCacheDBConnection* utility.
 
 .. highlight:: JavaScript
 .. code::
 
   class UrlbarProvider {
     /**
-    * Unique name for the provider, used by the context to filter on providers.
-    * Not using a unique name will cause the newest registration to win.
-    * @abstract
-    */
+     * Unique name for the provider, used by the context to filter on providers.
+     * Not using a unique name will cause the newest registration to win.
+     * @abstract
+     */
     get name() {
       return "UrlbarProviderBase";
     }
     /**
-    * The type of the provider, must be one of UrlbarUtils.PROVIDER_TYPE.
-    * @abstract
-    */
+     * The type of the provider, must be one of UrlbarUtils.PROVIDER_TYPE.
+     * @abstract
+     */
     get type() {
       throw new Error("Trying to access the base class, must be overridden");
     }
     /**
-    * List of UrlbarUtils.RESULT_SOURCE, representing the data sources used by
-    * the provider.
-    * @abstract
-    */
-    get sources() {
+     * Whether this provider should be invoked for the given context.
+     * If this method returns false, the providers manager won't start a query
+     * with this provider, to save on resources.
+     * @param {UrlbarQueryContext} queryContext The query context object
+     * @returns {boolean} Whether this provider should be invoked for the search.
+     * @abstract
+     */
+    isActive(queryContext) {
       throw new Error("Trying to access the base class, must be overridden");
     }
     /**
-    * Starts querying.
-    * @param {UrlbarQueryContext} queryContext The query context object
-    * @param {function} addCallback Callback invoked by the provider to add a new
-    *        result. A UrlbarResult should be passed to it.
-    * @note Extended classes should return a Promise resolved when the provider
-    *       is done searching AND returning results.
-    * @abstract
-    */
+     * Whether this provider wants to restrict results to just itself.
+     * Other providers won't be invoked, unless this provider doesn't
+     * support the current query.
+     * @param {UrlbarQueryContext} queryContext The query context object
+     * @returns {boolean} Whether this provider wants to restrict results.
+     * @abstract
+     */
+    isRestricting(queryContext) {
+      throw new Error("Trying to access the base class, must be overridden");
+    }
+    /**
+     * Starts querying.
+     * @param {UrlbarQueryContext} queryContext The query context object
+     * @param {function} addCallback Callback invoked by the provider to add a new
+     *        result. A UrlbarResult should be passed to it.
+     * @note Extended classes should return a Promise resolved when the provider
+     *       is done searching AND returning results.
+     * @abstract
+     */
     startQuery(queryContext, addCallback) {
       throw new Error("Trying to access the base class, must be overridden");
     }
     /**
-    * Cancels a running query,
-    * @param {UrlbarQueryContext} queryContext The query context object to cancel
-    *        query for.
-    * @abstract
-    */
+     * Cancels a running query,
+     * @param {UrlbarQueryContext} queryContext The query context object to cancel
+     *        query for.
+     * @abstract
+     */
     cancelQuery(queryContext) {
       throw new Error("Trying to access the base class, must be overridden");
     }
   }
 
 UrlbarMuxer
 -----------
 
@@ -213,28 +229,28 @@ indicated by the UrlbarQueryContext.muxe
   The Muxer is a replaceable component, as such what is described here is a
   reference for the default View, but may not be valid for other implementations.
 
 .. highlight:: JavaScript
 .. code::
 
   class UrlbarMuxer {
     /**
-    * Unique name for the muxer, used by the context to sort results.
-    * Not using a unique name will cause the newest registration to win.
-    * @abstract
-    */
+     * Unique name for the muxer, used by the context to sort results.
+     * Not using a unique name will cause the newest registration to win.
+     * @abstract
+     */
     get name() {
       return "UrlbarMuxerBase";
     }
     /**
-    * Sorts UrlbarQueryContext results in-place.
-    * @param {UrlbarQueryContext} queryContext the context to sort results for.
-    * @abstract
-    */
+     * Sorts UrlbarQueryContext results in-place.
+     * @param {UrlbarQueryContext} queryContext the context to sort results for.
+     * @abstract
+     */
     sort(queryContext) {
       throw new Error("Trying to access the base class, must be overridden");
     }
   }
 
 
 The Controller
 ==============
--- a/browser/locales/en-US/browser/preferences/preferences.ftl
+++ b/browser/locales/en-US/browser/preferences/preferences.ftl
@@ -27,21 +27,17 @@ pref-page =
 search-input-box =
     .style = width: 15.4em
     .placeholder =
         { PLATFORM() ->
             [windows] Find in Options
            *[other] Find in Preferences
         }
 
-policies-notice =
-    { PLATFORM() ->
-        [windows] Your organization has disabled the ability to change some options.
-       *[other] Your organization has disabled the ability to change some preferences.
-    }
+managed-notice = Your browser is being managed by your organization.
 
 pane-general-title = General
 category-general =
     .tooltiptext = { pane-general-title }
 
 pane-home-title = Home
 category-home =
     .tooltiptext = { pane-home-title }
--- a/browser/themes/shared/incontentprefs/preferences.inc.css
+++ b/browser/themes/shared/incontentprefs/preferences.inc.css
@@ -743,16 +743,20 @@ image.update-throbber {
 
 #policies-container {
   background-color: var(--in-content-warning-container);
   padding: 0px 8px;
   margin-inline-end: 16px;
   border-radius: 2px;
 }
 
+.policies-label {
+  margin-left: 2px;
+}
+
 .info-icon {
   list-style-image: url("chrome://browser/skin/identity-icon.svg");
   width: 16px;
   height: 16px;
   margin-top: calc((32px - 16px) / 2);
 }
 
 .sticky-container {
--- a/devtools/client/debugger/.eslintignore
+++ b/devtools/client/debugger/.eslintignore
@@ -1,8 +1,10 @@
+# Keep in sync with top-level .eslintignore
+
 assets/*
 src/test/examples/**
 src/test/integration/**
 src/test/unit-sources/**
 src/**/fixtures/**
 src/test/mochitest/**
 bin/
 packages/**/fixtures/**
--- a/devtools/client/debugger/.eslintrc
+++ b/devtools/client/debugger/.eslintrc
@@ -78,33 +78,27 @@
       }
     ],
 
     // Check for import errors.
     "import/no-duplicates": "error",
     "import/named": "error",
     "import/export": "error",
 
-    // Enforce the spacing around the * in generator functions.
-    "generator-star-spacing": [2, "after"],
-
     "flowtype/define-flow-type": 1,
     "flowtype/use-flow-type": 1,
 
     // Disallow flow control that escapes from "finally".
     "no-unsafe-finally": "error",
 
     // Disallow using variables outside the blocks they are defined (especially
     // since only let and const are used, see "no-var").
     "block-scoped-var": 2,
     // Require camel case names
     "camelcase": 2,
-    // Allow trailing commas for easy list extension.  Having them does not
-    // impair readability, but also not required either.
-    "comma-dangle": 0,
     // Warn about cyclomatic complexity in functions.
     "complexity": ["error", { "max": 22 }],
     // Don't warn for inconsistent naming when capturing this (not so important
     // with auto-binding fat arrow functions).
     "consistent-this": 0,
     // Enforce curly brace conventions for all control statements.
     "curly": 2,
     // Don't require a default case in switch statements. Avoid being forced to
@@ -119,36 +113,22 @@
     // This makes the code more verbose and hard to read. Our engine already
     // does a fantastic job assigning a name to the function, which includes
     // the enclosing function name, and worst case you have a line number that
     // you can just look up.
     "func-names": 0,
     // Allow use of function declarations and expressions.
     "func-style": 0,
     // Deprecated, will be removed in 1.0.
-    "generator-star": 0,
-    // Deprecated, will be removed in 1.0.
     "global-strict": 0,
     // Only useful in a node environment.
     "handle-callback-err": 0,
-    // Allow mixed 'LF' and 'CRLF' as linebreaks.
-    "linebreak-style": 0,
     // Don't enforce the maximum depth that blocks can be nested. The complexity
     // rule is a better rule to check this.
     "max-depth": 0,
-    // Maximum length of a line.
-    "max-len": [
-      2,
-      80,
-      2,
-      {
-        "ignoreUrls": true,
-        "ignorePattern": "\\s*require\\s*\\(|^\\s*loader\\.lazy|-\\*-"
-      }
-    ],
     // Maximum depth callbacks can be nested.
     "max-nested-callbacks": [2, 4],
     // Don't limit the number of parameters that can be used in a function.
     "max-params": 0,
     // Don't limit the maximum number of statement allowed in a function. We
     // already have the complexity rule that's a better measurement.
     "max-statements": 0,
     // Require a capital letter for constructors, only check if all new
@@ -159,18 +139,16 @@
     "no-array-constructor": 2,
     // Allow use of bitwise operators.
     "no-bitwise": 0,
     // Disallow use of arguments.caller or arguments.callee.
     "no-caller": 2,
     // Disallow the catch clause parameter name being the same as a variable in
     // the outer scope, to avoid confusion.
     "no-catch-shadow": 2,
-    // Deprecated, will be removed in 1.0.
-    "no-comma-dangle": 0,
     // Disallow assignment in conditional expressions.
     "no-cond-assign": 2,
     // Allow using the console API.
     "no-console": 0,
     // Allow using constant expressions in conditions like while (true)
     "no-constant-condition": 0,
     // Allow use of the continue statement.
     "no-continue": 0,
@@ -207,24 +185,20 @@
     // Disallow assigning to the exception in a catch block.
     "no-ex-assign": 2,
     // Disallow adding to native types
     "no-extend-native": 2,
     // Disallow unnecessary function binding.
     "no-extra-bind": 2,
     // Disallow double-negation boolean casts in a boolean context.
     "no-extra-boolean-cast": 2,
-    // Allow unnecessary parentheses, as they may make the code more readable.
-    "no-extra-parens": 0,
     // Deprecated, will be removed in 1.0.
     "no-extra-strict": 0,
     // Disallow fallthrough of case statements, except if there is a comment.
     "no-fallthrough": 2,
-    // Allow the use of leading or trailing decimal points in numeric literals.
-    "no-floating-decimal": 0,
     // Disallow comments inline after code.
     "no-inline-comments": 2,
     // Disallow if as the only statement in an else block.
     "no-lonely-if": 2,
     // Allow mixing regular variable and require declarations (not a node env).
     "no-mixed-requires": 0,
     // Disallow use of multiline strings (use template strings instead).
     "no-multi-str": 2,
@@ -255,18 +229,16 @@
     // Allow using process.exit (not a node environment).
     "no-process-exit": 0,
     // Disallow usage of __proto__ property.
     "no-proto": 2,
     // Disallow declaring the same variable more than once (we use let anyway).
     "no-redeclare": 2,
     // Disallow multiple spaces in a regular expression literal.
     "no-regex-spaces": 2,
-    // Allow reserved words being used as object literal keys.
-    "no-reserved-keys": 0,
     // Don't restrict usage of specified node modules (not a node environment).
     "no-restricted-modules": 0,
     // Disallow use of assignment in return statement. It is preferable for a
     // single line of code to have only one easily predictable effect.
     "no-return-assign": 2,
     // Allow use of javascript: urls.
     "no-script-url": 0,
     // Disallow comparisons where both sides are exactly the same.
@@ -276,18 +248,16 @@
     // Warn about declaration of variables already declared in the outer scope.
     // This isn't an error because it sometimes is useful to use the same name
     // in a small helper function rather than having to come up with another
     // random name.
     // Still, making this a warning can help people avoid being confused.
     "no-shadow": 2,
     // Disallow shadowing of names such as arguments.
     "no-shadow-restricted-names": 2,
-    // Deprecated, will be removed in 1.0.
-    "no-space-before-semi": 0,
     // Disallow sparse arrays, eg. let arr = [,,2].
     // Array destructuring is fine though:
     // for (let [, breakpointPromise] of aPromises)
     "no-sparse-arrays": 2,
     // Allow use of synchronous methods (not a node environment).
     "no-sync": 0,
     // Allow the use of ternary operators.
     "no-ternary": 0,
@@ -319,59 +289,36 @@
     // Disallow use of the with statement.
     "no-with": 2,
     // Dont require method and property shorthand syntax for object literals.
     // We use this in the code a lot, but not consistently, and this seems more
     // like something to check at code review time.
     "object-shorthand": 0,
     // Allow more than one variable declaration per function.
     "one-var": 0,
-    // Disallow padding within blocks.
-    //"padded-blocks": [2, "never"],
-    // Dont require quotes around object literal property names.
-    "quote-props": 0,
-    // Double quotes should be used.
-    "quotes": [2, "double", "avoid-escape"],
     // Require use of the second argument for parseInt().
     "radix": 2,
     // Dont require to sort variables within the same declaration block.
     // Anyway, one-var is disabled.
     "sort-vars": 0,
-    // Deprecated, will be removed in 1.0.
-    "space-after-function-name": 0,
-    // Deprecated, will be removed in 1.0.
-    "space-before-function-parentheses": 0,
-    // Disallow space before function opening parenthesis.
-    //"space-before-function-paren": [2, "never"],
-    // Disable the rule that checks if spaces inside {} and [] are there or not.
-    // Our code is split on conventions, and itd be nice to have 2 rules
-    // instead, one for [] and one for {}. So, disabling until we write them.
-    "space-in-brackets": 0,
-    // Deprecated, will be removed in 1.0.
-    "space-unary-word-ops": 0,
     // Require a space immediately following the // in a line comment.
     "spaced-comment": [2, "always"],
     // Require "use strict" to be defined globally in the script.
     "strict": [2, "global"],
     // Disallow comparisons with the value NaN.
     "use-isnan": 2,
     // Warn about invalid JSDoc comments.
     // Disabled for now because of https://github.com/eslint/eslint/issues/2270
     // The rule fails on some jsdoc comments like in:
     // devtools/client/webconsole/console-output.js
     "valid-jsdoc": 0,
     // Ensure that the results of typeof are compared against a valid string.
     "valid-typeof": 2,
     // Allow vars to be declared anywhere in the scope.
     "vars-on-top": 0,
-    // Dont require immediate function invocation to be wrapped in parentheses.
-    "wrap-iife": 0,
-    // Don't require regex literals to be wrapped in parentheses (which
-    // supposedly prevent them from being mistaken for division operators).
-    "wrap-regex": 0,
     // Disallow Yoda conditions (where literal value comes first).
     "yoda": 2,
 
     // And these are the rules that haven't been discussed so far, and that are
     // disabled for now until we introduce them, one at a time.
 
     // Require for-in loops to have an if statement.
     "guard-for-in": 0,
@@ -417,23 +364,19 @@
     // var foo = "Copyright \251";
     "no-octal-escape": 0,
     // disallow use of undefined when initializing variables
     "no-undef-init": 0,
     // disallow usage of expressions in statement position
     "no-unused-expressions": 0,
     // disallow use of void operator
     "no-void": 0,
-    // disallow wrapping of non-IIFE statements in parens
-    "no-wrap-func": 0,
     // require assignment operator shorthand where possible or prohibit it
     // entirely
     "operator-assignment": 0,
-    // enforce operators to be placed before or after line breaks
-    "operator-linebreak": 0,
 
     // Rules from the prettier plugin
     "prettier/prettier": "error",
 
     "file-header/file-header": [
       "error",
       [
         "This Source Code Form is subject to the terms of the Mozilla Public",
--- a/devtools/client/debugger/.prettierignore
+++ b/devtools/client/debugger/.prettierignore
@@ -1,8 +1,10 @@
+# Keep in sync with top-level .eslintignore
+
 src/workers/parser/tests/fixtures/functionNames.js
 src/workers/parser/tests/fixtures/scopes/*.js
 src/workers/parser/tests/fixtures/pause/*.js
 src/test/mochitest/examples/babel/polyfill-bundle.js
 src/test/mochitest/examples/babel/fixtures/*/input.js
 src/test/mochitest/examples/babel/fixtures/*/output.js
 src/test/mochitest/examples/babel/fixtures/*/output.js.map
 src/test/mochitest/examples/ember/quickstart
--- a/devtools/client/debugger/bin/module-manifest.json
+++ b/devtools/client/debugger/bin/module-manifest.json
@@ -10,17 +10,17 @@
           "0": 0,
           "1": 1
         }
       },
       "chunks": {
         "byName": {},
         "byBlocks": {},
         "usedIds": {
-          "1": 1
+          "0": 0
         }
       }
     }
   ],
   "extract-text-webpack-plugin ../../extract-text-webpack-plugin/dist ../../css-loader/index.js??ref--3-1!../../postcss-loader/lib/index.js!../../react-aria-components/src/tabs/tab.css": [
     {
       "modules": {
         "byIdentifier": {
@@ -31,17 +31,17 @@
           "0": 0,
           "1": 1
         }
       },
       "chunks": {
         "byName": {},
         "byBlocks": {},
         "usedIds": {
-          "1": 1
+          "0": 0
         }
       }
     }
   ],
   "extract-text-webpack-plugin ../../extract-text-webpack-plugin/dist ../../css-loader/index.js??ref--3-1!../../postcss-loader/lib/index.js!../../react-aria-components/src/tabs/tab-list.css": [
     {
       "modules": {
         "byIdentifier": {
@@ -52,17 +52,17 @@
           "0": 0,
           "1": 1
         }
       },
       "chunks": {
         "byName": {},
         "byBlocks": {},
         "usedIds": {
-          "1": 1
+          "0": 0
         }
       }
     }
   ],
   "extract-text-webpack-plugin ../../extract-text-webpack-plugin/dist ../../css-loader/index.js??ref--3-1!../../postcss-loader/lib/index.js!../../devtools-contextmenu/menu.css": [
     {
       "modules": {
         "byIdentifier": {
@@ -73,17 +73,17 @@
           "0": 0,
           "1": 1
         }
       },
       "chunks": {
         "byName": {},
         "byBlocks": {},
         "usedIds": {
-          "1": 1
+          "0": 0
         }
       }
     }
   ],
   "extract-text-webpack-plugin ../../extract-text-webpack-plugin/dist ../../css-loader/index.js??ref--3-1!../../postcss-loader/lib/index.js!../../../packages/devtools-components/src/tree.css": [
     {
       "modules": {
         "byIdentifier": {
@@ -94,17 +94,17 @@
           "0": 0,
           "1": 1
         }
       },
       "chunks": {
         "byName": {},
         "byBlocks": {},
         "usedIds": {
-          "1": 1
+          "0": 0
         }
       }
     }
   ],
   "extract-text-webpack-plugin ../../extract-text-webpack-plugin/dist ../../css-loader/index.js??ref--3-1!../../postcss-loader/lib/index.js!../../../packages/devtools-reps/src/object-inspector/components/ObjectInspector.css": [
     {
       "modules": {
         "byIdentifier": {
@@ -115,17 +115,17 @@
           "0": 0,
           "1": 1
         }
       },
       "chunks": {
         "byName": {},
         "byBlocks": {},
         "usedIds": {
-          "1": 1
+          "0": 0
         }
       }
     }
   ],
   "extract-text-webpack-plugin ../../extract-text-webpack-plugin/dist ../../css-loader/index.js??ref--3-1!../../postcss-loader/lib/index.js!../../../packages/devtools-reps/src/reps/reps.css": [
     {
       "modules": {
         "byIdentifier": {
@@ -136,17 +136,17 @@
           "0": 0,
           "1": 1
         }
       },
       "chunks": {
         "byName": {},
         "byBlocks": {},
         "usedIds": {
-          "1": 1
+          "0": 0
         }
       }
     }
   ],
   "modules": {
     "byIdentifier": {
       "external \"devtools/client/shared/vendor/react-prop-types\"": 0,
       "external \"devtools/client/shared/vendor/react-dom-factories\"": 1,
@@ -632,17 +632,19 @@
       "../../babel-loader/lib/index.js?ignore=src/lib!../../../packages/devtools-reps/src/reps/object-with-url.js": 481,
       "../../babel-loader/lib/index.js?ignore=src/lib!../../../packages/devtools-reps/src/object-inspector/index.js": 482,
       "../../babel-loader/lib/index.js?ignore=src/lib!../../../packages/devtools-reps/src/object-inspector/components/ObjectInspector.js": 483,
       "external \"devtools/client/shared/vendor/react-redux\"": 484,
       "../../babel-loader/lib/index.js?ignore=src/lib!../../../packages/devtools-reps/src/object-inspector/actions.js": 485,
       "../../extract-text-webpack-plugin/dist/loader.js??ref--3-0!../../css-loader/index.js??ref--3-1!../../postcss-loader/lib/index.js!../../../packages/devtools-reps/src/object-inspector/components/ObjectInspector.css": 486,
       "../../babel-loader/lib/index.js?ignore=src/lib!../../../packages/devtools-reps/src/object-inspector/components/ObjectInspectorItem.js": 487,
       "../../babel-loader/lib/index.js?ignore=src/lib!../../../packages/devtools-reps/src/object-inspector/utils/selection.js": 488,
-      "../../css-loader/lib/css-base.js": 489
+      "../../css-loader/lib/css-base.js": 489,
+      "external \"devtools/client/framework/menu\"": 490,
+      "external \"devtools/client/framework/menu-item\"": 491
     },
     "usedIds": {
       "0": 0,
       "1": 1,
       "2": 2,
       "3": 3,
       "4": 4,
       "5": 5,
@@ -1124,17 +1126,19 @@
       "481": 481,
       "482": 482,
       "483": 483,
       "484": 484,
       "485": 485,
       "486": 486,
       "487": 487,
       "488": 488,
-      "489": 489
+      "489": 489,
+      "490": 490,
+      "491": 491
     }
   },
   "chunks": {
     "byName": {
       "parser-worker": 0,
       "vendors": 1,
       "reps": 2,
       "source-map-worker": 3,
--- a/devtools/client/debugger/configs/mozilla-central-mappings.js
+++ b/devtools/client/debugger/configs/mozilla-central-mappings.js
@@ -20,14 +20,16 @@ module.exports = Object.assign(
     immutable: "devtools/client/shared/vendor/immutable",
     lodash: "devtools/client/shared/vendor/lodash",
     react: "devtools/client/shared/vendor/react",
     "react-dom": "devtools/client/shared/vendor/react-dom",
     "react-dom-factories": "devtools/client/shared/vendor/react-dom-factories",
     "react-redux": "devtools/client/shared/vendor/react-redux",
     redux: "devtools/client/shared/vendor/redux",
     "prop-types": "devtools/client/shared/vendor/react-prop-types",
+    "devtools-modules/src/menu": "devtools/client/framework/menu",
+    "devtools-modules/src/menu-item": "devtools/client/framework/menu-item",
     "devtools-services": "Services",
     "wasmparser/dist/WasmParser": "devtools/client/shared/vendor/WasmParser",
     "wasmparser/dist/WasmDis": "devtools/client/shared/vendor/WasmDis"
   },
   EXCLUDED_FILES
 );
--- a/devtools/client/debugger/dist/parser-worker.js
+++ b/devtools/client/debugger/dist/parser-worker.js
@@ -4818,17 +4818,17 @@ function isSpreadProperty(node, opts) {
 
 /***/ }),
 /* 6 */,
 /* 7 */
 /***/ (function(module, exports, __webpack_require__) {
 
 /* 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/. */
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
 
 const networkRequest = __webpack_require__(13);
 const workerUtils = __webpack_require__(14);
 
 module.exports = {
   networkRequest,
   workerUtils
 };
--- a/devtools/client/debugger/dist/pretty-print-worker.js
+++ b/devtools/client/debugger/dist/pretty-print-worker.js
@@ -6078,17 +6078,17 @@ Object.defineProperty(exports, '__esModu
 
 /***/ }),
 
 /***/ 7:
 /***/ (function(module, exports, __webpack_require__) {
 
 /* 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/. */
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
 
 const networkRequest = __webpack_require__(13);
 const workerUtils = __webpack_require__(14);
 
 module.exports = {
   networkRequest,
   workerUtils
 };
--- a/devtools/client/debugger/dist/search-worker.js
+++ b/devtools/client/debugger/dist/search-worker.js
@@ -994,17 +994,17 @@ module.exports = toString;
 
 /***/ }),
 
 /***/ 7:
 /***/ (function(module, exports, __webpack_require__) {
 
 /* 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/. */
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
 
 const networkRequest = __webpack_require__(13);
 const workerUtils = __webpack_require__(14);
 
 module.exports = {
   networkRequest,
   workerUtils
 };
--- a/devtools/client/debugger/dist/vendors.js
+++ b/devtools/client/debugger/dist/vendors.js
@@ -1,18 +1,18 @@
 (function webpackUniversalModuleDefinition(root, factory) {
 	if(typeof exports === 'object' && typeof module === 'object')
-		module.exports = factory(require("devtools/client/shared/vendor/react-prop-types"), require("devtools/client/shared/vendor/react-dom-factories"), require("devtools/client/shared/vendor/react"), require("Services"), require("devtools/shared/flags"), require("devtools/client/shared/vendor/react-dom"), require("devtools/client/shared/vendor/lodash"));
+		module.exports = factory(require("devtools/client/shared/vendor/react-prop-types"), require("devtools/client/shared/vendor/react-dom-factories"), require("devtools/client/shared/vendor/react"), require("Services"), require("devtools/shared/flags"), require("devtools/client/shared/vendor/react-dom"), require("devtools/client/shared/vendor/lodash"), require("devtools/client/framework/menu"), require("devtools/client/framework/menu-item"));
 	else if(typeof define === 'function' && define.amd)
-		define(["devtools/client/shared/vendor/react-prop-types", "devtools/client/shared/vendor/react-dom-factories", "devtools/client/shared/vendor/react", "Services", "devtools/shared/flags", "devtools/client/shared/vendor/react-dom", "devtools/client/shared/vendor/lodash"], factory);
+		define(["devtools/client/shared/vendor/react-prop-types", "devtools/client/shared/vendor/react-dom-factories", "devtools/client/shared/vendor/react", "Services", "devtools/shared/flags", "devtools/client/shared/vendor/react-dom", "devtools/client/shared/vendor/lodash", "devtools/client/framework/menu", "devtools/client/framework/menu-item"], factory);
 	else {
-		var a = typeof exports === 'object' ? factory(require("devtools/client/shared/vendor/react-prop-types"), require("devtools/client/shared/vendor/react-dom-factories"), require("devtools/client/shared/vendor/react"), require("Services"), require("devtools/shared/flags"), require("devtools/client/shared/vendor/react-dom"), require("devtools/client/shared/vendor/lodash")) : factory(root["devtools/client/shared/vendor/react-prop-types"], root["devtools/client/shared/vendor/react-dom-factories"], root["devtools/client/shared/vendor/react"], root["Services"], root["devtools/shared/flags"], root["devtools/client/shared/vendor/react-dom"], root["devtools/client/shared/vendor/lodash"]);
+		var a = typeof exports === 'object' ? factory(require("devtools/client/shared/vendor/react-prop-types"), require("devtools/client/shared/vendor/react-dom-factories"), require("devtools/client/shared/vendor/react"), require("Services"), require("devtools/shared/flags"), require("devtools/client/shared/vendor/react-dom"), require("devtools/client/shared/vendor/lodash"), require("devtools/client/framework/menu"), require("devtools/client/framework/menu-item")) : factory(root["devtools/client/shared/vendor/react-prop-types"], root["devtools/client/shared/vendor/react-dom-factories"], root["devtools/client/shared/vendor/react"], root["Services"], root["devtools/shared/flags"], root["devtools/client/shared/vendor/react-dom"], root["devtools/client/shared/vendor/lodash"], root["devtools/client/framework/menu"], root["devtools/client/framework/menu-item"]);
 		for(var i in a) (typeof exports === 'object' ? exports : root)[i] = a[i];
 	}
-})(typeof self !== 'undefined' ? self : this, function(__WEBPACK_EXTERNAL_MODULE_0__, __WEBPACK_EXTERNAL_MODULE_1__, __WEBPACK_EXTERNAL_MODULE_6__, __WEBPACK_EXTERNAL_MODULE_37__, __WEBPACK_EXTERNAL_MODULE_103__, __WEBPACK_EXTERNAL_MODULE_112__, __WEBPACK_EXTERNAL_MODULE_417__) {
+})(typeof self !== 'undefined' ? self : this, function(__WEBPACK_EXTERNAL_MODULE_0__, __WEBPACK_EXTERNAL_MODULE_1__, __WEBPACK_EXTERNAL_MODULE_6__, __WEBPACK_EXTERNAL_MODULE_37__, __WEBPACK_EXTERNAL_MODULE_103__, __WEBPACK_EXTERNAL_MODULE_112__, __WEBPACK_EXTERNAL_MODULE_417__, __WEBPACK_EXTERNAL_MODULE_490__, __WEBPACK_EXTERNAL_MODULE_491__) {
 return /******/ (function(modules) { // webpackBootstrap
 /******/ 	// The module cache
 /******/ 	var installedModules = {};
 /******/
 /******/ 	// The require function
 /******/ 	function __webpack_require__(moduleId) {
 /******/
 /******/ 		// Check if module is in cache
@@ -159,17 +159,17 @@ var _tree = __webpack_require__(109);
 var _tree2 = _interopRequireDefault(_tree);
 
 function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
 
 module.exports = {
   Tree: _tree2.default
 }; /* 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/. */
+    * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
 
 /***/ }),
 
 /***/ 109:
 /***/ (function(module, exports, __webpack_require__) {
 
 "use strict";
 
@@ -1471,31 +1471,27 @@ module.exports = g;
 
 "use strict";
 
 
 /* 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/. */
 
-const Menu = __webpack_require__(421);
-const MenuItem = __webpack_require__(423);
 const { PrefsHelper } = __webpack_require__(424);
 const KeyShortcuts = __webpack_require__(425);
 const { ZoomKeys } = __webpack_require__(426);
 const EventEmitter = __webpack_require__(65);
 const asyncStorage = __webpack_require__(427);
 const SourceUtils = __webpack_require__(428);
 const Telemetry = __webpack_require__(429);
 const { getUnicodeHostname, getUnicodeUrlPath, getUnicodeUrl } = __webpack_require__(430);
 
 module.exports = {
   KeyShortcuts,
-  Menu,
-  MenuItem,
   PrefsHelper,
   ZoomKeys,
   asyncStorage,
   EventEmitter,
   SourceUtils,
   Telemetry,
   getUnicodeHostname,
   getUnicodeUrlPath,
@@ -2524,17 +2520,18 @@ var substr = 'ab'.substr(-1) === 'b'
 
 "use strict";
 
 
 /* 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/. */
 
-const { Menu, MenuItem } = __webpack_require__(183);
+const Menu = __webpack_require__(490);
+const MenuItem = __webpack_require__(491);
 
 function inToolbox() {
   try {
     return window.parent.document.documentURI.startsWith("about:devtools-toolbox");
   } catch (e) {
     // If `window` is not available, it's very likely that we are in the toolbox.
     return true;
   }
@@ -2666,267 +2663,16 @@ function buildMenu(items) {
 
 module.exports = {
   showMenu,
   buildMenu
 };
 
 /***/ }),
 
-/***/ 421:
-/***/ (function(module, exports, __webpack_require__) {
-
-"use strict";
-
-
-var _devtoolsServices = __webpack_require__(37);
-
-var _devtoolsServices2 = _interopRequireDefault(_devtoolsServices);
-
-function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
-
-const { appinfo } = _devtoolsServices2.default; /* 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/. */
-
-const isMacOS = appinfo.OS === "Darwin";
-
-const EventEmitter = __webpack_require__(65);
-
-/**
- * Formats key for use in tooltips
- * For macOS we use the following unicode
- *
- * cmd ⌘ = \u2318
- * shift ⇧ – \u21E7
- * option (alt) ⌥ \u2325
- *
- * For Win/Lin this replaces CommandOrControl or CmdOrCtrl with Ctrl
- *
- * @static
- */
-function formatKeyShortcut(shortcut) {
-  if (isMacOS) {
-    return shortcut.replace(/Shift\+/g, "\u21E7").replace(/Command\+|Cmd\+/g, "\u2318").replace(/CommandOrControl\+|CmdOrCtrl\+/g, "\u2318").replace(/Alt\+/g, "\u2325");
-  }
-  return shortcut.replace(/CommandOrControl\+|CmdOrCtrl\+/g, `${L10N.getStr("ctrl")}+`).replace(/Shift\+/g, "Shift+");
-}
-
-function inToolbox() {
-  try {
-    return window.parent.document.documentURI.startsWith("about:devtools-toolbox");
-  } catch (e) {
-    // If `window` is not available, it's very likely that we are in the toolbox.
-    return true;
-  }
-}
-
-/**
- * A partial implementation of the Menu API provided by electron:
- * https://github.com/electron/electron/blob/master/docs/api/menu.md.
- *
- * Extra features:
- *  - Emits an 'open' and 'close' event when the menu is opened/closed
-
- * @param String id (non standard)
- *        Needed so tests can confirm the XUL implementation is working
- */
-function Menu({ id = null } = {}) {
-  this.menuitems = [];
-  this.id = id;
-
-  Object.defineProperty(this, "items", {
-    get() {
-      return this.menuitems;
-    }
-  });
-
-  EventEmitter.decorate(this);
-}
-
-/**
- * Add an item to the end of the Menu
- *
- * @param {MenuItem} menuItem
- */
-Menu.prototype.append = function (menuItem) {
-  this.menuitems.push(menuItem);
-};
-
-/**
- * Add an item to a specified position in the menu
- *
- * @param {int} pos
- * @param {MenuItem} menuItem
- */
-Menu.prototype.insert = function (pos, menuItem) {
-  throw Error("Not implemented");
-};
-
-// Copied from m-c DevToolsUtils.
-function getTopWindow(win) {
-  return win.windowRoot ? win.windowRoot.ownerGlobal : win.top;
-}
-
-/**
- * Show the Menu at a specified location on the screen
- *
- * Missing features:
- *   - browserWindow - BrowserWindow (optional) - Default is null.
- *   - positioningItem Number - (optional) OS X
- *
- * @param {int} screenX
- * @param {int} screenY
- * @param Toolbox toolbox (non standard)
- *        Needed so we in which window to inject XUL
- */
-Menu.prototype.popup = function (screenX, screenY, doc) {
-  const win = doc.defaultView;
-  doc = getTopWindow(doc.defaultView).document;
-  let popupset = doc.querySelector("popupset");
-  if (!popupset) {
-    popupset = doc.createXULElement("popupset");
-    doc.documentElement.appendChild(popupset);
-  }
-  // See bug 1285229, on Windows, opening the same popup multiple times in a
-  // row ends up duplicating the popup. The newly inserted popup doesn't
-  // dismiss the old one. So remove any previously displayed popup before
-  // opening a new one.
-  let popup = popupset.querySelector("menupopup[menu-api=\"true\"]");
-  if (popup) {
-    popup.hidePopup();
-  }
-
-  popup = this.createPopup(doc);
-  popup.setAttribute("menu-api", "true");
-
-  if (this.id) {
-    popup.id = this.id;
-  }
-  this._createMenuItems(popup);
-  // The context menu will be created in the topmost chrome window. Hide it manually when
-  // the owner document is unloaded.
-  const onWindowUnload = () => popup.hidePopup();
-  win.addEventListener("unload", onWindowUnload);
-
-  // Remove the menu from the DOM once it's hidden.
-  popup.addEventListener("popuphidden", e => {
-    if (e.target === popup) {
-      win.removeEventListener("unload", onWindowUnload);
-      popup.remove();
-      this.emit("close", popup);
-    }
-  });
-
-  popup.addEventListener("popupshown", e => {
-    if (e.target === popup) {
-      this.emit("open", popup);
-    }
-  });
-
-  popupset.appendChild(popup);
-  popup.openPopupAtScreen(screenX, screenY, true);
-};
-
-Menu.prototype.createPopup = function (doc) {
-  const popup = doc.createXULElement("menupopup");
-  popup.setAttribute("menu-api", "true");
-  popup.setAttribute("consumeoutsideclicks", "false");
-  popup.setAttribute("incontentshell", "false");
-  return popup;
-};
-
-Menu.prototype._createMenuItems = function (parent) {
-  let doc = parent.ownerDocument;
-  this.menuitems.forEach(item => {
-    if (!item.visible) {
-      return;
-    }
-
-    if (item.submenu) {
-      let menupopup = doc.createXULElement("menupopup");
-      item.submenu._createMenuItems(menupopup);
-
-      let menuitem = doc.createXULElement("menuitem");
-      menuitem.setAttribute("label", item.label);
-      if (!inToolbox()) {
-        menuitem.textContent = item.label;
-      }
-
-      let menu = doc.createXULElement("menu");
-      menu.appendChild(menuitem);
-      menu.appendChild(menupopup);
-      if (item.disabled) {
-        menu.setAttribute("disabled", "true");
-      }
-      if (item.accesskey) {
-        menu.setAttribute("accesskey", item.accesskey);
-      }
-      if (item.id) {
-        menu.id = item.id;
-      }
-      if (item.accelerator) {
-        menuitem.setAttribute("acceltext", formatKeyShortcut(item.accelerator));
-      }
-      parent.appendChild(menu);
-    } else if (item.type === "separator") {
-      let menusep = doc.createXULElement("menuseparator");
-      parent.appendChild(menusep);
-    } else {
-      let menuitem = doc.createXULElement("menuitem");
-      menuitem.setAttribute("label", item.label);
-
-      if (!inToolbox()) {
-        menuitem.textContent = item.label;
-      }
-
-      menuitem.addEventListener("command", () => item.click());
-
-      if (item.type === "checkbox") {
-        menuitem.setAttribute("type", "checkbox");
-      }
-      if (item.type === "radio") {
-        menuitem.setAttribute("type", "radio");
-      }
-      if (item.disabled) {
-        menuitem.setAttribute("disabled", "true");
-      }
-      if (item.checked) {
-        menuitem.setAttribute("checked", "true");
-      }
-      if (item.accesskey) {
-        menuitem.setAttribute("accesskey", item.accesskey);
-      }
-      if (item.id) {
-        menuitem.id = item.id;
-      }
-      if (item.accelerator) {
-        menuitem.setAttribute("acceltext", formatKeyShortcut(item.accelerator));
-      }
-      parent.appendChild(menuitem);
-    }
-  });
-};
-
-Menu.setApplicationMenu = () => {
-  throw Error("Not implemented");
-};
-
-Menu.sendActionToFirstResponder = () => {
-  throw Error("Not implemented");
-};
-
-Menu.buildFromTemplate = () => {
-  throw Error("Not implemented");
-};
-
-module.exports = Menu;
-
-/***/ }),
-
 /***/ 422:
 /***/ (function(module, exports, __webpack_require__) {
 
 "use strict";
 
 
 /* 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
@@ -2955,89 +2701,16 @@ p.defer = function defer() {
     promise: promise
   };
 };
 
 module.exports = p;
 
 /***/ }),
 
-/***/ 423:
-/***/ (function(module, exports, __webpack_require__) {
-
-"use strict";
-
-
-/* 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/. */
-
-/**
- * A partial implementation of the MenuItem API provided by electron:
- * https://github.com/electron/electron/blob/master/docs/api/menu-item.md.
- *
- * Missing features:
- *   - id String - Unique within a single menu. If defined then it can be used
- *                 as a reference to this item by the position attribute.
- *   - role String - Define the action of the menu item; when specified the
- *                   click property will be ignored
- *   - sublabel String
- *   - icon NativeImage
- *   - position String - This field allows fine-grained definition of the
- *                       specific location within a given menu.
- *
- * Implemented features:
- *  @param Object options
- *    Function click
- *      Will be called with click(menuItem, browserWindow) when the menu item
- *       is clicked
- *    String type
- *      Can be normal, separator, submenu, checkbox or radio
- *    String label
- *    Boolean enabled
- *      If false, the menu item will be greyed out and unclickable.
- *    Boolean checked
- *      Should only be specified for checkbox or radio type menu items.
- *    Menu submenu
- *      Should be specified for submenu type menu items. If submenu is specified,
- *      the type: 'submenu' can be omitted. If the value is not a Menu then it
- *      will be automatically converted to one using Menu.buildFromTemplate.
- *    Boolean visible
- *      If false, the menu item will be entirely hidden.
- *    String accelerator
- *      If specified, will be used as accelerator text for MenuItem
- */
-function MenuItem({
-  accesskey = null,
-  checked = false,
-  click = () => {},
-  disabled = false,
-  label = "",
-  id = null,
-  submenu = null,
-  type = "normal",
-  visible = true,
-  accelerator = ""
-} = {}) {
-  this.accesskey = accesskey;
-  this.checked = checked;
-  this.click = click;
-  this.disabled = disabled;
-  this.id = id;
-  this.label = label;
-  this.submenu = submenu;
-  this.type = type;
-  this.visible = visible;
-  this.accelerator = accelerator;
-}
-
-module.exports = MenuItem;
-
-/***/ }),
-
 /***/ 424:
 /***/ (function(module, exports, __webpack_require__) {
 
 "use strict";
 
 
 /* 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
@@ -6032,16 +5705,20 @@ function createStructuredSelector(select
   });
 }
 
 /***/ }),
 
 /***/ 445:
 /***/ (function(module, exports, __webpack_require__) {
 
+/* 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/>. */
+
 const SplitBox = __webpack_require__(446);
 
 module.exports = SplitBox;
 
 /***/ }),
 
 /***/ 446:
 /***/ (function(module, exports, __webpack_require__) {
@@ -6414,16 +6091,30 @@ function move(array, moveIndex, toIndex)
     // move right
     return [].concat(_toConsumableArray(array.slice(0, moveIndex)), _toConsumableArray(array.slice(moveIndex + 1, toIndex + 1)), [item], _toConsumableArray(array.slice(toIndex + 1, length)));
   }
   return array;
 }
 
 /***/ }),
 
+/***/ 490:
+/***/ (function(module, exports) {
+
+module.exports = __WEBPACK_EXTERNAL_MODULE_490__;
+
+/***/ }),
+
+/***/ 491:
+/***/ (function(module, exports) {
+
+module.exports = __WEBPACK_EXTERNAL_MODULE_491__;
+
+/***/ }),
+
 /***/ 6:
 /***/ (function(module, exports) {
 
 module.exports = __WEBPACK_EXTERNAL_MODULE_6__;
 
 /***/ }),
 
 /***/ 62:
@@ -7492,17 +7183,17 @@ var __WEBPACK_AMD_DEFINE_ARRAY__, __WEBP
 
 /***/ }),
 
 /***/ 7:
 /***/ (function(module, exports, __webpack_require__) {
 
 /* 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/. */
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
 
 const networkRequest = __webpack_require__(13);
 const workerUtils = __webpack_require__(14);
 
 module.exports = {
   networkRequest,
   workerUtils
 };
--- a/devtools/client/debugger/jest-test.config.js
+++ b/devtools/client/debugger/jest-test.config.js
@@ -10,31 +10,31 @@ module.exports = {
   testURL: "http://localhost/",
   testPathIgnorePatterns: [
     "/node_modules/",
     "/helpers/",
     "/fixtures/",
     "src/test/mochitest/examples/",
     "<rootDir>/firefox",
     "package.json",
-    "<rootDir>/packages"
+    "<rootDir>/packages",
   ],
   modulePathIgnorePatterns: ["test/mochitest", "firefox"],
   collectCoverageFrom: [
     "src/**/*.js",
     "!src/**/fixtures/*.js",
     "!src/test/**/*.js",
     "!src/components/stories/**/*.js",
     "!**/*.mock.js",
-    "!**/*.spec.js"
+    "!**/*.spec.js",
   ],
   transformIgnorePatterns: ["node_modules/(?!(devtools-|react-aria-))"],
   setupTestFrameworkScriptFile: "<rootDir>/src/test/tests-setup.js",
   setupFiles: ["<rootDir>/src/test/shim.js", "jest-localstorage-mock"],
   snapshotSerializers: [
     "jest-serializer-babel-ast",
-    "enzyme-to-json/serializer"
+    "enzyme-to-json/serializer",
   ],
   moduleNameMapper: {
     "\\.css$": "<rootDir>/src/test/__mocks__/styleMock.js",
-    "\\.svg$": "<rootDir>/src/test/__mocks__/svgMock.js"
-  }
+    "\\.svg$": "<rootDir>/src/test/__mocks__/svgMock.js",
+  },
 };
--- a/devtools/client/debugger/jest.config.js
+++ b/devtools/client/debugger/jest.config.js
@@ -4,11 +4,11 @@
 
 const { resolve } = require("path");
 const rootDir = resolve(__dirname);
 module.exports = {
   rootDir,
   reporters: ["default"],
   projects: [
     "<rootDir>/jest-test.config.js",
-    "<rootDir>/packages/*/jest.config.js"
-  ]
+    "<rootDir>/packages/*/jest.config.js",
+  ],
 };
--- a/devtools/client/debugger/package.json
+++ b/devtools/client/debugger/package.json
@@ -48,17 +48,17 @@
   "dependencies": {
     "@babel/core": "^7.0.0-beta.55",
     "@babel/parser": "^7.0.0-beta.55",
     "@babel/template": "^7.0.0-beta.55",
     "@babel/types": "^7.0.0-beta.55",
     "babel-plugin-transform-imports": "^1.5.0",
     "codemirror": "^5.28.0",
     "devtools-environment": "^0.0.6",
-    "devtools-launchpad": "^0.0.152",
+    "devtools-launchpad": "^0.0.153",
     "devtools-linters": "^0.0.4",
     "devtools-reps": "0.23.0",
     "devtools-source-map": "0.16.0",
     "devtools-splitter": "^0.0.8",
     "devtools-utils": "0.0.14",
     "fuzzaldrin-plus": "^0.6.0",
     "immutable": "^3.8.2",
     "lodash": "^4.17.4",
--- a/devtools/client/debugger/packages/devtools-components/index.js
+++ b/devtools/client/debugger/packages/devtools-components/index.js
@@ -1,9 +1,9 @@
 /* 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/. */
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
 
 import Tree from "./src/tree";
 
 module.exports = {
-  Tree
+  Tree,
 };
--- a/devtools/client/debugger/packages/devtools-components/jest.config.js
+++ b/devtools/client/debugger/packages/devtools-components/jest.config.js
@@ -1,20 +1,24 @@
+/* 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/>. */
+
 const { resolve } = require("path");
 const rootDir = resolve(__dirname, "src");
 module.exports = {
   rootDir,
   displayName: "devtools-components test",
   setupFiles: [
     "<rootDir>/../../../src/test/__mocks__/request-animation-frame.js",
-    "<rootDir>/tests/setup.js"
+    "<rootDir>/tests/setup.js",
   ],
   testMatch: ["**/tests/**/*.js"],
   testPathIgnorePatterns: [
     "/node_modules/",
     "<rootDir>/tests/__mocks__/",
-    "<rootDir>/tests/setup.js"
+    "<rootDir>/tests/setup.js",
   ],
   testURL: "http://localhost/",
   moduleNameMapper: {
-    "\\.css$": "<rootDir>/../../../src/test/__mocks__/styleMock.js"
-  }
+    "\\.css$": "<rootDir>/../../../src/test/__mocks__/styleMock.js",
+  },
 };
--- a/devtools/client/debugger/packages/devtools-components/postcss.config.js
+++ b/devtools/client/debugger/packages/devtools-components/postcss.config.js
@@ -1,37 +1,37 @@
 /* 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/. */
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
 
 const mapUrl = require("postcss-url-mapper");
 const MC_PATH = "resource://devtools/client/debugger/images/";
 const EXPRESS_PATH = "/devtools-components/images/";
 
 function mapUrlProduction(url, type) {
-  return url.replace("/images/arrow.svg", MC_PATH + "arrow.svg");
+  return url.replace("/images/arrow.svg", `${MC_PATH}arrow.svg`);
 }
 
 function mapUrlDevelopment(url) {
-  return url.replace("/images/arrow.svg", EXPRESS_PATH + "arrow.svg");
+  return url.replace("/images/arrow.svg", `${EXPRESS_PATH}arrow.svg`);
 }
 
 module.exports = ({ file, options, env }) => {
   // Here we don't want to do anything for storybook since we serve the images thanks
   // to the `-s ./src` option in the `storybook` command (see package.json).
   if (env === "storybook") {
     return {};
   }
 
   // This will be used when creating a bundle for mozilla-central (from devtools-reps
   // or debugger.html).
   if (env === "production") {
     return {
-      plugins: [mapUrl(mapUrlProduction)]
+      plugins: [mapUrl(mapUrlProduction)],
     };
   }
 
   // This will be used when using this module in launchpad mode. We set a unique path so
   // we can serve images from express.
   return {
-    plugins: [mapUrl(mapUrlDevelopment)]
+    plugins: [mapUrl(mapUrlDevelopment)],
   };
 };
--- a/devtools/client/debugger/packages/devtools-components/src/tests/tree.js
+++ b/devtools/client/debugger/packages/devtools-components/src/tests/tree.js
@@ -16,17 +16,17 @@ function mountTree(overrides = {}) {
   return mount(
     createFactory(
       class container extends Component {
         constructor(props) {
           super(props);
           const state = {
             expanded: overrides.expanded || new Set(),
             focused: overrides.focused,
-            active: overrides.active
+            active: overrides.active,
           };
           delete overrides.focused;
           delete overrides.active;
           this.state = state;
         }
 
         render() {
           return Tree(
@@ -67,17 +67,17 @@ function mountTree(overrides = {}) {
                   this.setState(previousState => {
                     const expanded = new Set(previousState.expanded);
                     expanded.delete(x);
                     return { expanded };
                   });
                 },
                 isExpanded: x => this.state && this.state.expanded.has(x),
                 focused: this.state.focused,
-                active: this.state.active
+                active: this.state.active,
               },
               overrides
             )
           );
         }
       }
     )()
   );
@@ -98,25 +98,25 @@ describe("Tree", () => {
       autoExpandDepth: 2,
       autoExpandNodeChildrenLimit: 50,
       getChildren: item => {
         if (item === "N") {
           return children;
         }
 
         return TEST_TREE.children[item] || [];
-      }
+      },
     });
     const ids = getTreeNodes(wrapper).map(node => node.prop("id"));
     expect(ids).toMatchSnapshot();
   });
 
   it("is accessible", () => {
     const wrapper = mountTree({
-      expanded: new Set("ABCDEFGHIJMN".split(""))
+      expanded: new Set("ABCDEFGHIJMN".split("")),
     });
     expect(wrapper.getDOMNode().getAttribute("role")).toBe("tree");
     expect(wrapper.getDOMNode().getAttribute("tabIndex")).toBe("0");
 
     const expected = {
       A: { id: "key-A", level: 1, expanded: true },
       B: { id: "key-B", level: 2, expanded: true },
       C: { id: "key-C", level: 2, expanded: true },
@@ -126,92 +126,92 @@ describe("Tree", () => {
       G: { id: "key-G", level: 3, expanded: true },
       H: { id: "key-H", level: 3, expanded: true },
       I: { id: "key-I", level: 3, expanded: true },
       J: { id: "key-J", level: 3, expanded: true },
       K: { id: "key-K", level: 4, expanded: undefined },
       L: { id: "key-L", level: 4, expanded: undefined },
       M: { id: "key-M", level: 1, expanded: true },
       N: { id: "key-N", level: 2, expanded: true },
-      O: { id: "key-O", level: 3, expanded: undefined }
+      O: { id: "key-O", level: 3, expanded: undefined },
     };
 
     getTreeNodes(wrapper).forEach(node => {
       const key = node.prop("id").replace("key-", "");
       const item = expected[key];
 
       expect(node.prop("id")).toBe(item.id);
       expect(node.prop("role")).toBe("treeitem");
       expect(node.prop("aria-level")).toBe(item.level);
       expect(node.prop("aria-expanded")).toBe(item.expanded);
     });
   });
 
   it("renders as expected", () => {
     const wrapper = mountTree({
-      expanded: new Set("ABCDEFGHIJKLMNO".split(""))
+      expanded: new Set("ABCDEFGHIJKLMNO".split("")),
     });
 
     expect(formatTree(wrapper)).toMatchSnapshot();
   });
 
   it("renders as expected when passed a className", () => {
     const wrapper = mountTree({
-      className: "testClassName"
+      className: "testClassName",
     });
 
     expect(wrapper.find(".tree").hasClass("testClassName")).toBe(true);
   });
 
   it("renders as expected when passed a style", () => {
     const wrapper = mountTree({
       style: {
-        color: "red"
-      }
+        color: "red",
+      },
     });
 
     expect(wrapper.getDOMNode().style.color).toBe("red");
   });
 
   it("renders as expected when passed a label", () => {
     const wrapper = mountTree({
-      label: "testAriaLabel"
+      label: "testAriaLabel",
     });
     expect(wrapper.getDOMNode().getAttribute("aria-label")).toBe(
       "testAriaLabel"
     );
   });
 
   it("renders as expected when passed an aria-labelledby", () => {
     const wrapper = mountTree({
-      labelledby: "testAriaLabelBy"
+      labelledby: "testAriaLabelBy",
     });
     expect(wrapper.getDOMNode().getAttribute("aria-labelledby")).toBe(
       "testAriaLabelBy"
     );
   });
 
   it("renders as expected with collapsed nodes", () => {
     const wrapper = mountTree({
-      expanded: new Set("MNO".split(""))
+      expanded: new Set("MNO".split("")),
     });
     expect(formatTree(wrapper)).toMatchSnapshot();
   });
 
   it("renders as expected when passed autoDepth:1", () => {
     const wrapper = mountTree({
-      autoExpandDepth: 1
+      autoExpandDepth: 1,
     });
     expect(formatTree(wrapper)).toMatchSnapshot();
   });
 
   it("calls shouldItemUpdate when provided", () => {
     const shouldItemUpdate = jest.fn((prev, next) => true);
     const wrapper = mountTree({
-      shouldItemUpdate
+      shouldItemUpdate,
     });
     expect(formatTree(wrapper)).toMatchSnapshot();
     expect(shouldItemUpdate.mock.calls).toHaveLength(0);
 
     wrapper
       .find("Tree")
       .first()
       .instance()
@@ -226,60 +226,60 @@ describe("Tree", () => {
     expect(shouldItemUpdate.mock.calls[1][1]).toBe("M");
     expect(shouldItemUpdate.mock.results[1].value).toBe(true);
   });
 
   it("active item - renders as expected when clicking away", () => {
     const wrapper = mountTree({
       expanded: new Set("ABCDEFGHIJKLMNO".split("")),
       focused: "G",
-      active: "G"
+      active: "G",
     });
     expect(formatTree(wrapper)).toMatchSnapshot();
     expect(wrapper.find(".active").prop("id")).toBe("key-G");
 
     getTreeNodes(wrapper)
       .first()
       .simulate("click");
     expect(formatTree(wrapper)).toMatchSnapshot();
     expect(wrapper.find(".focused").prop("id")).toBe("key-A");
     expect(wrapper.find(".active").exists()).toBe(false);
   });
 
   it("active item - renders as expected when tree blurs", () => {
     const wrapper = mountTree({
       expanded: new Set("ABCDEFGHIJKLMNO".split("")),
       focused: "G",
-      active: "G"
+      active: "G",
     });
     expect(formatTree(wrapper)).toMatchSnapshot();
     expect(wrapper.find(".active").prop("id")).toBe("key-G");
 
     wrapper.simulate("blur");
     expect(formatTree(wrapper)).toMatchSnapshot();
     expect(wrapper.find(".active").exists()).toBe(false);
   });
 
   it("active item - renders as expected when moving away with keyboard", () => {
     const wrapper = mountTree({
       expanded: new Set("ABCDEFGHIJKLMNO".split("")),
       focused: "L",
-      active: "L"
+      active: "L",
     });
     expect(formatTree(wrapper)).toMatchSnapshot();
     expect(wrapper.find(".active").prop("id")).toBe("key-L");
 
     simulateKeyDown(wrapper, "ArrowUp");
     expect(wrapper.find(".active").exists()).toBe(false);
   });
 
   it("active item - renders as expected when using keyboard and Enter", () => {
     const wrapper = mountTree({
       expanded: new Set("ABCDEFGHIJKLMNO".split("")),
-      focused: "L"
+      focused: "L",
     });
     wrapper.getDOMNode().focus();
     expect(formatTree(wrapper)).toMatchSnapshot();
     expect(wrapper.find(".active").exists()).toBe(false);
 
     simulateKeyDown(wrapper, "Enter");
     expect(wrapper.find(".active").prop("id")).toBe("key-L");
 
@@ -292,34 +292,34 @@ describe("Tree", () => {
     expect(wrapper.getDOMNode().ownerDocument.activeElement).toBe(
       wrapper.getDOMNode()
     );
   });
 
   it("active item - renders as expected when using keyboard and Space", () => {
     const wrapper = mountTree({
       expanded: new Set("ABCDEFGHIJKLMNO".split("")),
-      focused: "L"
+      focused: "L",
     });
     wrapper.getDOMNode().focus();
     expect(formatTree(wrapper)).toMatchSnapshot();
     expect(wrapper.find(".active").exists()).toBe(false);
 
     simulateKeyDown(wrapper, " ");
     expect(wrapper.find(".active").prop("id")).toBe("key-L");
 
     simulateKeyDown(wrapper, "Escape");
     expect(wrapper.find(".active").exists()).toBe(false);
   });
 
   it("active item - focus is inside the tree node when possible", () => {
     const wrapper = mountTree({
       expanded: new Set("ABCDEFGHIJKLMNO".split("")),
       focused: "L",
-      renderItem: renderItemWithFocusableContent
+      renderItem: renderItemWithFocusableContent,
     });
     wrapper.getDOMNode().focus();
     expect(formatTree(wrapper)).toMatchSnapshot();
     expect(wrapper.find(".active").exists()).toBe(false);
     expect(wrapper.getDOMNode().ownerDocument.activeElement).toBe(
       wrapper.getDOMNode()
     );
 
@@ -330,17 +330,17 @@ describe("Tree", () => {
       wrapper.find("#active-anchor").getDOMNode()
     );
   });
 
   it("active item - navigate inside the tree node", () => {
     const wrapper = mountTree({
       expanded: new Set("ABCDEFGHIJKLMNO".split("")),
       focused: "L",
-      renderItem: renderItemWithFocusableContent
+      renderItem: renderItemWithFocusableContent,
     });
     wrapper.getDOMNode().focus();
     simulateKeyDown(wrapper, "Enter");
     expect(formatTree(wrapper)).toMatchSnapshot();
     expect(wrapper.find(".active").prop("id")).toBe("key-L");
     expect(wrapper.getDOMNode().ownerDocument.activeElement).toBe(
       wrapper.find("#active-anchor").getDOMNode()
     );
@@ -359,17 +359,17 @@ describe("Tree", () => {
       wrapper.find("#active-anchor").getDOMNode()
     );
   });
 
   it("active item - focus is inside the tree node and then blur", () => {
     const wrapper = mountTree({
       expanded: new Set("ABCDEFGHIJKLMNO".split("")),
       focused: "L",
-      renderItem: renderItemWithFocusableContent
+      renderItem: renderItemWithFocusableContent,
     });
     wrapper.getDOMNode().focus();
     simulateKeyDown(wrapper, "Enter");
     expect(formatTree(wrapper)).toMatchSnapshot();
     expect(wrapper.find(".active").prop("id")).toBe("key-L");
     expect(wrapper.getDOMNode().ownerDocument.activeElement).toBe(
       wrapper.find("#active-anchor").getDOMNode()
     );
@@ -379,17 +379,17 @@ describe("Tree", () => {
     expect(wrapper.getDOMNode().ownerDocument.activeElement).toBe(
       wrapper.getDOMNode().ownerDocument.body
     );
   });
 
   it("renders as expected when given a focused item", () => {
     const wrapper = mountTree({
       expanded: new Set("ABCDEFGHIJKLMNO".split("")),
-      focused: "G"
+      focused: "G",
     });
     expect(formatTree(wrapper)).toMatchSnapshot();
     expect(wrapper.getDOMNode().getAttribute("aria-activedescendant")).toBe(
       "key-G"
     );
     expect(wrapper.find(".focused").prop("id")).toBe("key-G");
 
     getTreeNodes(wrapper)
@@ -416,17 +416,17 @@ describe("Tree", () => {
       false
     );
     expect(wrapper.find(".focused").exists()).toBe(false);
   });
 
   it("renders as expected when navigating up with the keyboard", () => {
     const wrapper = mountTree({
       expanded: new Set("ABCDEFGHIJKLMNO".split("")),
-      focused: "L"
+      focused: "L",
     });
     expect(formatTree(wrapper)).toMatchSnapshot();
     expect(wrapper.getDOMNode().getAttribute("aria-activedescendant")).toBe(
       "key-L"
     );
     expect(wrapper.find(".focused").prop("id")).toBe("key-L");
 
     simulateKeyDown(wrapper, "ArrowUp");
@@ -442,17 +442,17 @@ describe("Tree", () => {
       "key-E"
     );
     expect(wrapper.find(".focused").prop("id")).toBe("key-E");
   });
 
   it("renders as expected navigating up with the keyboard on a root", () => {
     const wrapper = mountTree({
       expanded: new Set("ABCDEFGHIJKLMNO".split("")),
-      focused: "A"
+      focused: "A",
     });
     expect(formatTree(wrapper)).toMatchSnapshot();
     expect(wrapper.getDOMNode().getAttribute("aria-activedescendant")).toBe(
       "key-A"
     );
     expect(wrapper.find(".focused").prop("id")).toBe("key-A");
 
     simulateKeyDown(wrapper, "ArrowUp");
@@ -461,17 +461,17 @@ describe("Tree", () => {
       "key-A"
     );
     expect(wrapper.find(".focused").prop("id")).toBe("key-A");
   });
 
   it("renders as expected when navigating down with the keyboard", () => {
     const wrapper = mountTree({
       expanded: new Set("ABCDEFGHIJKLMNO".split("")),
-      focused: "K"
+      focused: "K",
     });
     expect(formatTree(wrapper)).toMatchSnapshot();
     expect(wrapper.getDOMNode().getAttribute("aria-activedescendant")).toBe(
       "key-K"
     );
     expect(wrapper.find(".focused").prop("id")).toBe("key-K");
 
     simulateKeyDown(wrapper, "ArrowDown");
@@ -487,17 +487,17 @@ describe("Tree", () => {
       "key-F"
     );
     expect(wrapper.find(".focused").prop("id")).toBe("key-F");
   });
 
   it("renders as expected navigating down with keyboard on last node", () => {
     const wrapper = mountTree({
       expanded: new Set("ABCDEFGHIJKLMNO".split("")),
-      focused: "O"
+      focused: "O",
     });
     expect(formatTree(wrapper)).toMatchSnapshot();
     expect(wrapper.getDOMNode().getAttribute("aria-activedescendant")).toBe(
       "key-O"
     );
     expect(wrapper.find(".focused").prop("id")).toBe("key-O");
 
     simulateKeyDown(wrapper, "ArrowDown");
@@ -506,17 +506,17 @@ describe("Tree", () => {
       "key-O"
     );
     expect(wrapper.find(".focused").prop("id")).toBe("key-O");
   });
 
   it("renders as expected when navigating with right/left arrows", () => {
     const wrapper = mountTree({
       expanded: new Set("ABCDEFGHIJKLMNO".split("")),
-      focused: "L"
+      focused: "L",
     });
     expect(formatTree(wrapper)).toMatchSnapshot();
     expect(wrapper.getDOMNode().getAttribute("aria-activedescendant")).toBe(
       "key-L"
     );
     expect(wrapper.find(".focused").prop("id")).toBe("key-L");
 
     simulateKeyDown(wrapper, "ArrowLeft");
@@ -545,17 +545,17 @@ describe("Tree", () => {
     expect(wrapper.getDOMNode().getAttribute("aria-activedescendant")).toBe(
       "key-K"
     );
     expect(wrapper.find(".focused").prop("id")).toBe("key-K");
   });
 
   it("renders as expected when navigating with left arrows on roots", () => {
     const wrapper = mountTree({
-      focused: "M"
+      focused: "M",
     });
     expect(formatTree(wrapper)).toMatchSnapshot();
     expect(wrapper.getDOMNode().getAttribute("aria-activedescendant")).toBe(
       "key-M"
     );
     expect(wrapper.find(".focused").prop("id")).toBe("key-M");
 
     simulateKeyDown(wrapper, "ArrowLeft");
@@ -570,17 +570,17 @@ describe("Tree", () => {
     expect(wrapper.getDOMNode().getAttribute("aria-activedescendant")).toBe(
       "key-A"
     );
     expect(wrapper.find(".focused").prop("id")).toBe("key-A");
   });
 
   it("renders as expected when navigating with home/end", () => {
     const wrapper = mountTree({
-      focused: "M"
+      focused: "M",
     });
     expect(formatTree(wrapper)).toMatchSnapshot();
     expect(wrapper.getDOMNode().getAttribute("aria-activedescendant")).toBe(
       "key-M"
     );
     expect(wrapper.find(".focused").prop("id")).toBe("key-M");
 
     simulateKeyDown(wrapper, "Home");
@@ -638,17 +638,17 @@ describe("Tree", () => {
       "key-A"
     );
     expect(wrapper.find(".focused").prop("id")).toBe("key-A");
   });
 
   it("renders as expected navigating with arrows on unexpandable roots", () => {
     const wrapper = mountTree({
       focused: "A",
-      isExpandable: item => false
+      isExpandable: item => false,
     });
     expect(formatTree(wrapper)).toMatchSnapshot();
 
     simulateKeyDown(wrapper, "ArrowRight");
     expect(formatTree(wrapper)).toMatchSnapshot();
     expect(wrapper.getDOMNode().getAttribute("aria-activedescendant")).toBe(
       "key-M"
     );
@@ -666,17 +666,17 @@ describe("Tree", () => {
         wrapper.setState(() => {
           return { focused: x };
         });
     });
 
     const wrapper = mountTree({
       expanded: new Set("ABCDEFGHIJKLMNO".split("")),
       focused: "I",
-      onFocus
+      onFocus,
     });
 
     simulateKeyDown(wrapper, "ArrowUp");
     expect(onFocus.mock.calls[0][0]).toBe("H");
 
     simulateKeyDown(wrapper, "ArrowUp");
     expect(onFocus.mock.calls[1][0]).toBe("C");
 
@@ -688,82 +688,82 @@ describe("Tree", () => {
     expect(onFocus.mock.calls[3][0]).toBe("B");
 
     simulateKeyDown(wrapper, "ArrowDown");
     expect(onFocus.mock.calls[4][0]).toBe("E");
   });
 
   it("focus treeRef when a node is clicked", () => {
     const wrapper = mountTree({
-      expanded: new Set("ABCDEFGHIJKLMNO".split(""))
+      expanded: new Set("ABCDEFGHIJKLMNO".split("")),
     });
     const treeRef = wrapper
       .find("Tree")
       .first()
       .instance().treeRef.current;
     treeRef.focus = jest.fn();
 
     getTreeNodes(wrapper)
       .first()
       .simulate("click");
     expect(treeRef.focus.mock.calls).toHaveLength(1);
   });
 
   it("doesn't focus treeRef when focused is null", () => {
     const wrapper = mountTree({
       expanded: new Set("ABCDEFGHIJKLMNO".split("")),
-      focused: "A"
+      focused: "A",
     });
     const treeRef = wrapper
       .find("Tree")
       .first()
       .instance().treeRef.current;
     treeRef.focus = jest.fn();
     wrapper.simulate("blur");
     expect(treeRef.focus.mock.calls).toHaveLength(0);
   });
 
   it("ignores key strokes when pressing modifiers", () => {
     const wrapper = mountTree({
       expanded: new Set("ABCDEFGHIJKLMNO".split("")),
-      focused: "L"
+      focused: "L",
     });
     expect(formatTree(wrapper)).toMatchSnapshot();
     expect(wrapper.getDOMNode().getAttribute("aria-activedescendant")).toBe(
       "key-L"
     );
     expect(wrapper.find(".focused").prop("id")).toBe("key-L");
 
     const testKeys = [
       { key: "ArrowDown" },
       { key: "ArrowUp" },
       { key: "ArrowLeft" },
-      { key: "ArrowRight" }
+      { key: "ArrowRight" },
     ];
     const modifiers = [
       { altKey: true },
       { ctrlKey: true },
       { metaKey: true },
-      { shiftKey: true }
+      { shiftKey: true },
     ];
 
     for (const key of testKeys) {
       for (const modifier of modifiers) {
         wrapper.simulate("keydown", Object.assign({}, key, modifier));
         expect(formatTree(wrapper)).toMatchSnapshot();
         expect(wrapper.getDOMNode().getAttribute("aria-activedescendant")).toBe(
           "key-L"
         );
       }
     }
   });
 
   it("renders arrows as expected when nodes are expanded", () => {
     const wrapper = mountTree({
-      expanded: new Set("ABCDEFGHIJKLMNO".split(""))
+      expanded: new Set("ABCDEFGHIJKLMNO".split("")),
     });
     expect(formatTree(wrapper)).toMatchSnapshot();
 
     getTreeNodes(wrapper).forEach(n => {
       if ("ABECDMN".split("").includes(getSanitizedNodeText(n))) {
         expect(n.find(".arrow.expanded").exists()).toBe(true);
       } else {
         expect(n.find(".arrow").exists()).toBe(false);
@@ -779,41 +779,41 @@ describe("Tree", () => {
       const arrow = n.find(".arrow");
       expect(arrow.exists()).toBe(true);
       expect(arrow.hasClass("expanded")).toBe(false);
     });
   });
 
   it("uses isExpandable prop if it exists to render tree nodes", () => {
     const wrapper = mountTree({
-      isExpandable: item => item === "A"
+      isExpandable: item => item === "A",
     });
     expect(formatTree(wrapper)).toMatchSnapshot();
   });
 
   it("adds the expected data-expandable attribute", () => {
     const wrapper = mountTree({
-      isExpandable: item => item === "A"
+      isExpandable: item => item === "A",
     });
     const nodes = getTreeNodes(wrapper);
     expect(nodes.at(0).prop("data-expandable")).toBe(true);
     expect(nodes.at(1).prop("data-expandable")).toBe(false);
   });
 });
 
 function getTreeNodes(wrapper) {
   return wrapper.find(".tree-node");
 }
 
 function simulateKeyDown(wrapper, key, options) {
   wrapper.simulate("keydown", {
     key,
     preventDefault: () => {},
     stopPropagation: () => {},
-    ...options
+    ...options,
   });
 }
 
 function renderItemWithFocusableContent(x, depth, focused, arrow) {
   const children = [arrow, focused ? "[" : null, x];
   if (x === "L") {
     children.push(dom.a({ id: "active-anchor", href: "#" }, " anchor"));
   }
@@ -899,28 +899,28 @@ var TEST_TREE = {
     G: [],
     H: [],
     I: [],
     J: [],
     K: [],
     L: [],
     M: ["N"],
     N: ["O"],
-    O: []
+    O: [],
   },
   parent: {
     A: null,
     B: "A",
     C: "A",
     D: "A",
     E: "B",
     F: "B",
     G: "B",
     H: "C",
     I: "C",
     J: "D",
     K: "E",
     L: "E",
     M: null,
     N: "M",
-    O: "N"
-  }
+    O: "N",
+  },
 };
--- a/devtools/client/debugger/packages/devtools-components/src/tree.js
+++ b/devtools/client/debugger/packages/devtools-components/src/tree.js
@@ -16,43 +16,43 @@ const AUTO_EXPAND_DEPTH = 0;
 // full version at https://stackoverflow.com/questions/1599660.
 const FOCUSABLE_SELECTOR = [
   "a[href]:not([tabindex='-1'])",
   "button:not([disabled]):not([tabindex='-1'])",
   "iframe:not([tabindex='-1'])",
   "input:not([disabled]):not([tabindex='-1'])",
   "select:not([disabled]):not([tabindex='-1'])",
   "textarea:not([disabled]):not([tabindex='-1'])",
-  "[tabindex]:not([tabindex='-1'])"
+  "[tabindex]:not([tabindex='-1'])",
 ].join(", ");
 
 /**
  * An arrow that displays whether its node is expanded (▼) or collapsed
  * (▶). When its node has no children, it is hidden.
  */
 class ArrowExpander extends Component {
   static get propTypes() {
     return {
-      expanded: PropTypes.bool
+      expanded: PropTypes.bool,
     };
   }
 
   shouldComponentUpdate(nextProps, nextState) {
     return this.props.expanded !== nextProps.expanded;
   }
 
   render() {
     const { expanded } = this.props;
 
     const classNames = ["arrow"];
     if (expanded) {
       classNames.push("expanded");
     }
     return dom.button({
-      className: classNames.join(" ")
+      className: classNames.join(" "),
     });
   }
 }
 
 const treeIndent = dom.span({ className: "tree-indent" }, "\u200B");
 const treeLastIndent = dom.span(
   { className: "tree-indent tree-last-indent" },
   "\u200B"
@@ -66,17 +66,17 @@ class TreeNode extends Component {
       depth: PropTypes.number.isRequired,
       focused: PropTypes.bool.isRequired,
       active: PropTypes.bool.isRequired,
       expanded: PropTypes.bool.isRequired,
       item: PropTypes.any.isRequired,
       isExpandable: PropTypes.bool.isRequired,
       onClick: PropTypes.func,
       shouldItemUpdate: PropTypes.func,
-      renderItem: PropTypes.func.isRequired
+      renderItem: PropTypes.func.isRequired,
     };
   }
 
   constructor(props) {
     super(props);
 
     this.treeNodeRef = React.createRef();
 
@@ -171,40 +171,40 @@ class TreeNode extends Component {
     const {
       depth,
       id,
       item,
       focused,
       active,
       expanded,
       renderItem,
-      isExpandable
+      isExpandable,
     } = this.props;
 
     const arrow = isExpandable
       ? ArrowExpanderFactory({
           item,
-          expanded
+          expanded,
         })
       : null;
 
     let ariaExpanded;
     if (this.props.isExpandable) {
       ariaExpanded = false;
     }
     if (this.props.expanded) {
       ariaExpanded = true;
     }
 
-    const indents = Array.from({length: depth}, (_, i) => {
+    const indents = Array.from({ length: depth }, (_, i) => {
       if (i == depth - 1) {
         return treeLastIndent;
       }
       return treeIndent;
-    })
+    });
 
     const items = indents.concat(
       renderItem(item, depth, focused, arrow, expanded)
     );
 
     return dom.div(
       {
         id,
@@ -212,17 +212,17 @@ class TreeNode extends Component {
           active ? " active" : ""
         }`,
         onClick: this.props.onClick,
         onKeyDownCapture: active ? this._onKeyDown : null,
         role: "treeitem",
         ref: this.treeNodeRef,
         "aria-level": depth + 1,
         "aria-expanded": ariaExpanded,
-        "data-expandable": this.props.isExpandable
+        "data-expandable": this.props.isExpandable,
       },
       ...items
     );
   }
 }
 
 const ArrowExpanderFactory = createFactory(ArrowExpander);
 const TreeNodeFactory = createFactory(TreeNode);
@@ -468,32 +468,32 @@ class Tree extends Component {
       // within the tree node.
       onActivate: PropTypes.func,
       isExpandable: PropTypes.func,
       // Additional classes to add to the root element.
       className: PropTypes.string,
       // style object to be applied to the root element.
       style: PropTypes.object,
       // Prevents blur when Tree loses focus
-      preventBlur: PropTypes.bool
+      preventBlur: PropTypes.bool,
     };
   }
 
   static get defaultProps() {
     return {
       autoExpandDepth: AUTO_EXPAND_DEPTH,
-      autoExpandAll: true
+      autoExpandAll: true,
     };
   }
 
   constructor(props) {
     super(props);
 
     this.state = {
-      seen: new Set()
+      seen: new Set(),
     };
 
     this.treeRef = React.createRef();
 
     this._onExpand = oncePerAnimationFrame(this._onExpand).bind(this);
     this._onCollapse = oncePerAnimationFrame(this._onCollapse).bind(this);
     this._focusPrevNode = oncePerAnimationFrame(this._focusPrevNode).bind(this);
     this._focusNextNode = oncePerAnimationFrame(this._focusNextNode).bind(this);
@@ -972,17 +972,17 @@ class Tree extends Component {
           if (this.props.isExpanded(item)) {
             this.props.onCollapse(item, e.altKey);
           } else {
             this.props.onExpand(item, e.altKey);
           }
 
           // Focus should always remain on the tree container itself.
           this.treeRef.current.focus();
-        }
+        },
       });
     });
 
     const style = Object.assign({}, this.props.style || {});
 
     return dom.div(
       {
         className: `tree ${this.props.className ? this.props.className : ""}`,
@@ -1007,16 +1007,16 @@ class Tree extends Component {
           ) {
             this._focus(traversal[0].item);
           }
         },
         onBlur: this._onBlur,
         "aria-label": this.props.label,
         "aria-labelledby": this.props.labelledby,
         "aria-activedescendant": focused && this.props.getKey(focused),
-        style
+        style,
       },
       nodes
     );
   }
 }
 
 export default Tree;
--- a/devtools/client/debugger/packages/devtools-components/stories/index.js
+++ b/devtools/client/debugger/packages/devtools-components/stories/index.js
@@ -1,5 +1,5 @@
 /* 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/. */
+ * 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/>. */
 
 require("./tree");
--- a/devtools/client/debugger/packages/devtools-components/stories/tree.js
+++ b/devtools/client/debugger/packages/devtools-components/stories/tree.js
@@ -1,11 +1,11 @@
 /* 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/. */
+ * 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 React from "react";
 const { Component, createFactory, createElement } = React;
 
 import Components from "../index";
 const Tree = createFactory(Components.Tree);
 import { storiesOf } from "@storybook/react";
 
@@ -30,95 +30,114 @@ storiesOf("Tree", module)
       autoExpandDepth: Infinity,
       getRoots: () => ["A", "P", "M", "Q", "W", "R"],
     });
   })
   .add("focused node", () => {
     return renderTree({
       focused: "W",
       getRoots: () => ["A", "W"],
-      expanded: new Set(["A"])
+      expanded: new Set(["A"]),
     });
   })
   .add("variable height nodes", () => {
-    const nodes = Array.from({length: 10})
-      .map((_, i) => `item ${i + 1} - `.repeat(10 + Math.random() * 50));
-    return renderTree({
-      getRoots: () => ["ROOT"],
-      expanded: new Set(["ROOT"])
-    }, {
-      children: {"ROOT": nodes}
-    });
+    const nodes = Array.from({ length: 10 }).map((_, i) =>
+      `item ${i + 1} - `.repeat(10 + Math.random() * 50)
+    );
+    return renderTree(
+      {
+        getRoots: () => ["ROOT"],
+        expanded: new Set(["ROOT"]),
+      },
+      {
+        children: { ROOT: nodes },
+      }
+    );
   })
   .add("scrollable tree", () => {
-    const nodes = Array.from({length: 500}).map((_, i) => (i + 1).toString());
+    const nodes = Array.from({ length: 500 }).map((_, i) => (i + 1).toString());
 
     class container extends Component {
       constructor(props) {
         super(props);
         const state = {
           expanded: new Set(),
-          focused: null
+          focused: null,
         };
         this.state = state;
       }
 
       render() {
-        return createElement("div", {},
-          createElement("label", {
-            style: {position: "fixed", right: 0},
-          },
+        return createElement(
+          "div",
+          {},
+          createElement(
+            "label",
+            {
+              style: { position: "fixed", right: 0 },
+            },
             "Enter node number to set focus on: ",
             createElement("input", {
               type: "number",
               min: 1,
               max: 500,
               onInput: e => {
                 // Storing balue since `e` can be reused by the time the setState
                 // callback is called.
                 const value = e.target.value.toString();
                 this.setState(previousState => {
-                  return {focused: value};
+                  return { focused: value };
                 });
-              }
-            }),
+              },
+            })
           ),
-          createTreeElement({getRoots: () => nodes}, this, {})
+          createTreeElement({ getRoots: () => nodes }, this, {})
         );
       }
     }
     return createFactory(container)();
   })
   .add("scrollable tree with focused node", () => {
-    const nodes = Array.from({length: 500}).map((_, i) => `item ${i + 1}`);
-    return renderTree({
-      focused: "item 250",
-      getRoots: () => nodes,
-    }, {});
+    const nodes = Array.from({ length: 500 }).map((_, i) => `item ${i + 1}`);
+    return renderTree(
+      {
+        focused: "item 250",
+        getRoots: () => nodes,
+      },
+      {}
+    );
   })
   .add("1000 items tree", () => {
-    const nodes = Array.from({length: 1000}).map((_, i) => `item-${i + 1}`);
-    return renderTree({
-      getRoots: () => ["ROOT"],
-      expanded: new Set()
-    }, {
-      children: {"ROOT": nodes}
-    });
+    const nodes = Array.from({ length: 1000 }).map((_, i) => `item-${i + 1}`);
+    return renderTree(
+      {
+        getRoots: () => ["ROOT"],
+        expanded: new Set(),
+      },
+      {
+        children: { ROOT: nodes },
+      }
+    );
   })
   .add("30,000 items tree", () => {
-    const nodes = Array.from({length: 1000}).map((_, i) => `item-${i + 1}`);
-    return renderTree({
-      getRoots: () => nodes,
-      expanded: new Set(Array.from({length: 2000}).map((_, i) => `item-${i + 1}`))
-    }, {
-      children: Array.from({length: 2000}).reduce((res, _, i) => {
-        res[`item-${i + 1}`] = [`item-${i + 1001}`];
-        return res;
-      }, {})
-    });
+    const nodes = Array.from({ length: 1000 }).map((_, i) => `item-${i + 1}`);
+    return renderTree(
+      {
+        getRoots: () => nodes,
+        expanded: new Set(
+          Array.from({ length: 2000 }).map((_, i) => `item-${i + 1}`)
+        ),
+      },
+      {
+        children: Array.from({ length: 2000 }).reduce((res, _, i) => {
+          res[`item-${i + 1}`] = [`item-${i + 1001}`];
+          return res;
+        }, {}),
+      }
+    );
   });
 
 // Encoding of the following tree/forest:
 //
 // A
 // |-- B
 // |   |-- E
 // |   |   |-- K
@@ -192,53 +211,57 @@ const TEST_TREE = {
 };
 
 function renderTree(props, tree = TEST_TREE) {
   class container extends Component {
     constructor(props2) {
       super(props2);
       const state = {
         expanded: props2.expanded || new Set(),
-        focused: props2.focused
+        focused: props2.focused,
       };
       delete props2.focused;
       this.state = state;
     }
 
     render() {
       return createTreeElement(props, this, tree);
     }
   }
   return createFactory(container)();
 }
 
 function createTreeElement(props, context, tree) {
-  return Tree(Object.assign({
-    getParent: x => tree.parent && tree.parent[x],
-    getChildren: x => tree.children && tree.children[x]
-      ? tree.children[x]
-      : [],
-    renderItem: (x, depth, focused, arrow, expanded) => [arrow, x],
-    getRoots: () => ["A"],
-    getKey: x => "key-" + x,
-    onFocus: x => {
-      context.setState(previousState => {
-        return {focused: x};
-      });
-    },
-    onExpand: x => {
-      context.setState(previousState => {
-        const expanded = new Set(previousState.expanded);
-        expanded.add(x);
-        return {expanded};
-      });
-    },
-    onCollapse: x => {
-      context.setState(previousState => {
-        const expanded = new Set(previousState.expanded);
-        expanded.delete(x);
-        return {expanded};
-      });
-    },
-    isExpanded: x => context.state && context.state.expanded.has(x),
-    focused: context.state.focused,
-  }, props));
+  return Tree(
+    Object.assign(
+      {
+        getParent: x => tree.parent && tree.parent[x],
+        getChildren: x =>
+          tree.children && tree.children[x] ? tree.children[x] : [],
+        renderItem: (x, depth, focused, arrow, expanded) => [arrow, x],
+        getRoots: () => ["A"],
+        getKey: x => `key-${x}`,
+        onFocus: x => {
+          context.setState(previousState => {
+            return { focused: x };
+          });
+        },
+        onExpand: x => {
+          context.setState(previousState => {
+            const expanded = new Set(previousState.expanded);
+            expanded.add(x);
+            return { expanded };
+          });
+        },
+        onCollapse: x => {
+          context.setState(previousState => {
+            const expanded = new Set(previousState.expanded);
+            expanded.delete(x);
+            return { expanded };
+          });
+        },
+        isExpanded: x => context.state && context.state.expanded.has(x),
+        focused: context.state.focused,
+      },
+      props
+    )
+  );
 }
--- a/devtools/client/debugger/packages/devtools-components/webpack.config.js
+++ b/devtools/client/debugger/packages/devtools-components/webpack.config.js
@@ -1,14 +1,14 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
 
 const { toolboxConfig } = require("devtools-launchpad/index");
-const {isDevelopment} = require("devtools-config");
+const { isDevelopment } = require("devtools-config");
 
 const path = require("path");
 const projectPath = path.join(__dirname);
 
 const webpackConfig = {
   entry: {
     "devtools-components": path.join(projectPath, "index.js"),
   },
@@ -18,22 +18,25 @@ const webpackConfig = {
     filename: "[name].js",
     publicPath: "/assets/build",
     libraryTarget: "umd",
   },
   resolve: {
     alias: {
       "devtools/client/shared/vendor/react": "react",
       "devtools/client/shared/vendor/react-dom": "react-dom",
-      "devtools/client/shared/vendor/react-dom-factories": "react-dom-factories",
+      "devtools/client/shared/vendor/react-dom-factories":
+        "react-dom-factories",
       "devtools/client/shared/vendor/react-prop-types": "prop-types",
-      "Services": path.join(__dirname,
-        "node_modules/devtools-modules/client/shared/shim/Services"),
-    }
-  }
+      Services: path.join(
+        __dirname,
+        "node_modules/devtools-modules/client/shared/shim/Services"
+      ),
+    },
+  },
 };
 
 const extra = {
   disablePostCSS: true,
 };
 webpackConfig.plugins = [];
 if (!isDevelopment()) {
   extra.excludeMap = {
--- a/devtools/client/debugger/packages/devtools-reps/bin/dev-server.js
+++ b/devtools/client/debugger/packages/devtools-reps/bin/dev-server.js
@@ -1,25 +1,27 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
 
 const path = require("path");
 const toolbox = require("devtools-launchpad/index");
 const serve = require("express-static");
 const config = require("../config");
 
-let webpackConfig = require("../webpack.config");
+const webpackConfig = require("../webpack.config");
 
-let { app } = toolbox.startDevServer(config, webpackConfig, __dirname);
+const { app } = toolbox.startDevServer(config, webpackConfig, __dirname);
 
 // Serve devtools-reps images
 app.use(
   "/devtools-reps/images/",
   serve(path.join(__dirname, "../src/shared/images"))
 );
 
 // As well as devtools-components ones, with a different path, which we are going to
 // write in the postCSS config in development mode.
 app.use(
   "/devtools-components/images/",
-  serve(path.join(__dirname, "../../../node_modules/devtools-components/src/images"))
+  serve(
+    path.join(__dirname, "../../../node_modules/devtools-components/src/images")
+  )
 );
--- a/devtools/client/debugger/packages/devtools-reps/config.js
+++ b/devtools/client/debugger/packages/devtools-reps/config.js
@@ -1,16 +1,20 @@
+/* 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/>. */
+
 module.exports = {
-  "title": "Reps",
-  "hotReloading": true,
-  "defaultURL": "https://nchevobbe.github.io/demo/console-test-app.html",
-  "environment": "development",
-  "theme": "light",
-  "firefox": {
-    "webSocketConnection": false,
-    "host": "localhost",
-    "webSocketPort": 9000,
-    "tcpPort": 6080,
+  title: "Reps",
+  hotReloading: true,
+  defaultURL: "https://nchevobbe.github.io/demo/console-test-app.html",
+  environment: "development",
+  theme: "light",
+  firefox: {
+    webSocketConnection: false,
+    host: "localhost",
+    webSocketPort: 9000,
+    tcpPort: 6080,
   },
-  "development": {
-    "serverPort": 8000
-  }
+  development: {
+    serverPort: 8000,
+  },
 };
--- a/devtools/client/debugger/packages/devtools-reps/jest.config.js
+++ b/devtools/client/debugger/packages/devtools-reps/jest.config.js
@@ -1,26 +1,30 @@
+/* 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/>. */
+
 const { resolve } = require("path");
 const rootDir = resolve(__dirname, "src");
 module.exports = {
   rootDir,
   displayName: "devtools-reps test",
   setupFiles: [
     "<rootDir>/../../../src/test/__mocks__/request-animation-frame.js",
     "<rootDir>/test/__mocks__/selection.js",
-    "<rootDir>/test/setup.js"
+    "<rootDir>/test/setup.js",
   ],
   setupTestFrameworkScriptFile: "<rootDir>/test/setup-file.js",
   testMatch: ["**/tests/**/*.js"],
   testPathIgnorePatterns: [
     "/node_modules/",
     "<rootDir>/test/",
     "<rootDir>/reps/tests/test-helpers",
     "<rootDir>/utils/tests/fixtures/",
     "<rootDir>/object-inspector/tests/__mocks__/",
-    "<rootDir>/object-inspector/tests/test-utils"
+    "<rootDir>/object-inspector/tests/test-utils",
   ],
   testURL: "http://localhost/",
   transformIgnorePatterns: ["node_modules/(?!devtools-)"],
   moduleNameMapper: {
-    "\\.css$": "<rootDir>/../../../src/test/__mocks__/styleMock.js"
-  }
+    "\\.css$": "<rootDir>/../../../src/test/__mocks__/styleMock.js",
+  },
 };
--- a/devtools/client/debugger/packages/devtools-reps/package.json
+++ b/devtools/client/debugger/packages/devtools-reps/package.json
@@ -33,17 +33,17 @@
     "redux": "^3.7.2"
   },
   "devDependencies": {
     "@sucrase/webpack-object-rest-spread-plugin": "^1.0.0",
     "babel-plugin-syntax-object-rest-spread": "^6.13.0",
     "babel-plugin-transform-es2015-modules-commonjs": "^6.26.0",
     "babel-preset-react": "^6.24.1",
     "devtools-config": "^0.0.16",
-    "devtools-launchpad": "^0.0.152",
+    "devtools-launchpad": "^0.0.153",
     "devtools-license-check": "^0.7.0",
     "devtools-modules": "~1.1.0",
     "devtools-services": "^0.0.1",
     "enzyme": "^3.3.0",
     "enzyme-adapter-react-16": "^1.1.1",
     "enzyme-to-json": "^3.3.1",
     "eslint": "^5.0.0",
     "eslint-plugin-mozilla": "1.1.3",
--- a/devtools/client/debugger/packages/devtools-reps/postcss.config.js
+++ b/devtools/client/debugger/packages/devtools-reps/postcss.config.js
@@ -19,16 +19,16 @@ function mapUrlDevelopment(url) {
     url = url.replace(`^/images/${img}`, `${EXPRESS_PATH}${img}`);
   }
   return url;
 }
 
 module.exports = ({ file, options, env }) => {
   if (env === "production") {
     return {
-      plugins: [mapUrl(mapUrlProduction)]
+      plugins: [mapUrl(mapUrlProduction)],
     };
   }
 
   return {
-    plugins: [mapUrl(mapUrlDevelopment)]
+    plugins: [mapUrl(mapUrlDevelopment)],
   };
 };
--- a/devtools/client/debugger/packages/devtools-reps/src/index.js
+++ b/devtools/client/debugger/packages/devtools-reps/src/index.js
@@ -7,28 +7,28 @@
 const { MODE } = require("./reps/constants");
 const { REPS, getRep } = require("./reps/rep");
 const objectInspector = require("./object-inspector");
 
 const {
   parseURLEncodedText,
   parseURLParams,
   maybeEscapePropertyName,
-  getGripPreviewItems
+  getGripPreviewItems,
 } = require("./reps/rep-utils");
 
 module.exports = {
   REPS,
   getRep,
   MODE,
   maybeEscapePropertyName,
   parseURLEncodedText,
   parseURLParams,
   getGripPreviewItems,
-  objectInspector
+  objectInspector,
 };
 
 export type {
   RdpGrip,
   GripProperties,
   Node,
-  Grip
+  Grip,
 } from "./object-inspector/types";
--- a/devtools/client/debugger/packages/devtools-reps/src/launchpad/actions/expressions.js
+++ b/devtools/client/debugger/packages/devtools-reps/src/launchpad/actions/expressions.js
@@ -17,38 +17,38 @@ function evaluateInput(input) {
 }
 
 function addExpression(input, packet) {
   return {
     key: generateKey(),
     type: constants.ADD_EXPRESSION,
     value: {
       input,
-      packet
-    }
+      packet,
+    },
   };
 }
 
 function clearExpressions() {
   return {
-    type: constants.CLEAR_EXPRESSIONS
+    type: constants.CLEAR_EXPRESSIONS,
   };
 }
 
 function showResultPacket(key) {
   return {
     key,
-    type: constants.SHOW_RESULT_PACKET
+    type: constants.SHOW_RESULT_PACKET,
   };
 }
 
 function hideResultPacket(key) {
   return {
     key,
-    type: constants.HIDE_RESULT_PACKET
+    type: constants.HIDE_RESULT_PACKET,
   };
 }
 
 function createObjectClient(grip) {
   return function({ dispatch, client }) {
     return client.getObjectClient(grip);
   };
 }
@@ -67,10 +67,10 @@ function releaseActor(actor) {
 module.exports = {
   addExpression,
   clearExpressions,
   evaluateInput,
   showResultPacket,
   hideResultPacket,
   createObjectClient,
   createLongStringClient,
-  releaseActor
+  releaseActor,
 };
--- a/devtools/client/debugger/packages/devtools-reps/src/launchpad/actions/index.js
+++ b/devtools/client/debugger/packages/devtools-reps/src/launchpad/actions/index.js
@@ -2,10 +2,10 @@
  * 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/>. */
 
 const expressions = require("./expressions");
 const input = require("./input");
 
 module.exports = {
   ...expressions,
-  ...input
+  ...input,
 };
--- a/devtools/client/debugger/packages/devtools-reps/src/launchpad/actions/input.js
+++ b/devtools/client/debugger/packages/devtools-reps/src/launchpad/actions/input.js
@@ -8,32 +8,32 @@ const { generateKey } = require("../util
 
 function addInput(input) {
   return ({ dispatch }) => {
     dispatch(expressionsActions.evaluateInput(input));
 
     dispatch({
       key: generateKey(),
       type: constants.ADD_INPUT,
-      value: input
+      value: input,
     });
   };
 }
 
 function changeCurrentInput(input) {
   return {
     type: constants.CHANGE_CURRENT_INPUT,
-    value: input
+    value: input,
   };
 }
 
 function navigateInputHistory(dir) {
   return {
     type: constants.NAVIGATE_INPUT_HISTORY,
-    value: dir
+    value: dir,
   };
 }
 
 module.exports = {
   addInput,
   changeCurrentInput,
-  navigateInputHistory
+  navigateInputHistory,
 };
--- a/devtools/client/debugger/packages/devtools-reps/src/launchpad/components/Console.js
+++ b/devtools/client/debugger/packages/devtools-reps/src/launchpad/components/Console.js
@@ -24,17 +24,17 @@ class Console extends Component {
       addInput: PropTypes.func.isRequired,
       changeCurrentInput: PropTypes.func.isRequired,
       clearExpressions: PropTypes.func.isRequired,
       currentInputValue: PropTypes.string.isRequired,
       evaluateInput: PropTypes.func.isRequired,
       expressions: PropTypes.object.isRequired,
       hideResultPacket: PropTypes.func.isRequired,
       navigateInputHistory: PropTypes.func.isRequired,
-      showResultPacket: PropTypes.func.isRequired
+      showResultPacket: PropTypes.func.isRequired,
     };
   }
 
   componentDidMount() {
     shortcuts.on("CmdOrCtrl+Shift+L", this.props.clearExpressions);
   }
 
   componentWillUnmount() {
@@ -46,42 +46,42 @@ class Console extends Component {
       addInput,
       changeCurrentInput,
       clearExpressions,
       currentInputValue,
       evaluateInput,
       expressions,
       hideResultPacket,
       navigateInputHistory,
-      showResultPacket
+      showResultPacket,
     } = this.props;
 
     return dom.main(
       {},
       Header({
         addInput,
         changeCurrentInput,
         clearResultsList: clearExpressions,
         currentInputValue,
         evaluate: evaluateInput,
-        navigateInputHistory
+        navigateInputHistory,
       }),
       ResultsList({
         expressions: expressions.reverse(),
         hideResultPacket,
-        showResultPacket
+        showResultPacket,
       })
     );
   }
 }
 
 function mapStateToProps(state) {
   return {
     expressions: selectors.getExpressions(state),
-    currentInputValue: selectors.getCurrentInputValue(state)
+    currentInputValue: selectors.getCurrentInputValue(state),
   };
 }
 
 function mapDispatchToProps(dispatch) {
   return bindActionCreators(require("../actions"), dispatch);
 }
 
 module.exports = connect(
--- a/devtools/client/debugger/packages/devtools-reps/src/launchpad/components/Header.js
+++ b/devtools/client/debugger/packages/devtools-reps/src/launchpad/components/Header.js
@@ -13,17 +13,17 @@ require("./Header.css");
 class Header extends Component {
   static get propTypes() {
     return {
       addInput: PropTypes.func.isRequired,
       changeCurrentInput: PropTypes.func.isRequired,
       clearResultsList: PropTypes.func.isRequired,
       currentInputValue: PropTypes.string,
       evaluate: PropTypes.func.isRequired,
-      navigateInputHistory: PropTypes.func.isRequired
+      navigateInputHistory: PropTypes.func.isRequired,
     };
   }
 
   constructor(props) {
     super(props);
     this.onSubmitForm = this.onSubmitForm.bind(this);
     this.onInputChange = this.onInputChange.bind(this);
     this.onInputKeyDown = this.onInputKeyDown.bind(this);
@@ -63,23 +63,23 @@ class Header extends Component {
         dom.h1({}, "Reps"),
         dom.input({
           type: "text",
           placeholder: "Enter an expression",
           name: "expression",
           value: currentInputValue || "",
           autoFocus: true,
           onChange: this.onInputChange,
-          onKeyDown: this.onInputKeyDown
+          onKeyDown: this.onInputKeyDown,
         }),
         dom.button(
           {
             className: "clear-button",
             type: "button",
-            onClick: this.onClearButtonClick
+            onClick: this.onClearButtonClick,
           },
           "Clear"
         )
       ),
       QuickLinks({ evaluate })
     );
   }
 }
--- a/devtools/client/debugger/packages/devtools-reps/src/launchpad/components/QuickLinks.js
+++ b/devtools/client/debugger/packages/devtools-reps/src/launchpad/components/QuickLinks.js
@@ -7,17 +7,17 @@ const { Component } = React;
 const PropTypes = require("prop-types");
 const dom = require("react-dom-factories");
 require("./QuickLinks.css");
 const samples = require("../samples.js");
 
 class QuickLinks extends Component {
   static get propTypes() {
     return {
-      evaluate: PropTypes.func.isRequired
+      evaluate: PropTypes.func.isRequired,
     };
   }
 
   constructor(props) {
     super(props);
     this.evaluateExpressions = this.evaluateExpressions.bind(this);
     this.renderLinks = this.renderLinks.bind(this);
   }
@@ -34,17 +34,17 @@ class QuickLinks extends Component {
         {
           key: label,
           title:
             label === "yolo"
               ? "Add all sample expressions"
               : `Add ${length} ${label} sample expression${
                   length > 1 ? "s" : ""
                 }`,
-          onClick: () => this.evaluateExpressions(expressions)
+          onClick: () => this.evaluateExpressions(expressions),
         },
         label
       );
     });
   }
 
   render() {
     return dom.div({ className: "quick-links" }, this.renderLinks());
--- a/devtools/client/debugger/packages/devtools-reps/src/launchpad/components/Result.js
+++ b/devtools/client/debugger/packages/devtools-reps/src/launchpad/components/Result.js
@@ -10,17 +10,17 @@ const { MODE } = require("../../reps/con
 const { ObjectInspector } = require("../../index").objectInspector;
 const { Rep } = require("../../reps/rep");
 
 class Result extends Component {
   static get propTypes() {
     return {
       expression: PropTypes.object.isRequired,
       showResultPacket: PropTypes.func.isRequired,
-      hideResultPacket: PropTypes.func.isRequired
+      hideResultPacket: PropTypes.func.isRequired,
     };
   }
 
   constructor(props) {
     super(props);
     this.copyPacketToClipboard = this.copyPacketToClipboard.bind(this);
     this.onHeaderClick = this.onHeaderClick.bind(this);
     this.renderRepInAllModes = this.renderRepInAllModes.bind(this);
@@ -57,78 +57,78 @@ class Result extends Component {
 
   renderRep({ object, modeKey }) {
     const path = Symbol(modeKey + object.actor);
 
     return dom.div(
       {
         className: "rep-element",
         key: path.toString(),
-        "data-mode": modeKey
+        "data-mode": modeKey,
       },
       ObjectInspector({
         roots: [
           {
             path,
             contents: {
-              value: object
-            }
-          }
+              value: object,
+            },
+          },
         ],
         autoExpandDepth: 0,
         mode: MODE[modeKey],
         // The following properties are optional function props called by the
         // objectInspector on some occasions. Here we pass dull functions that
         // only logs the parameters with which the callback was called.
         onCmdCtrlClick: (node, { depth, event, focused, expanded }) =>
           console.log("CmdCtrlClick", {
             node,
             depth,
             event,
             focused,
-            expanded
+            expanded,
           }),
         onInspectIconClick: nodeFront =>
           console.log("inspectIcon click", { nodeFront }),
         onViewSourceInDebugger: location =>
           console.log("onViewSourceInDebugger", { location }),
         recordTelemetryEvent: (eventName, extra = {}) => {
           console.log("📊", eventName, "📊", extra);
-        }
+        },
       })
     );
   }
 
   renderPacket(expression) {
     const { packet, showPacket } = expression;
     const headerClassName = showPacket ? "packet-expanded" : "packet-collapsed";
     const headerLabel = showPacket
       ? "Hide expression packet"
       : "Show expression packet";
 
     return dom.div(
       { className: "packet" },
       dom.header(
         {
           className: headerClassName,
-          onClick: this.onHeaderClick
+          onClick: this.onHeaderClick,
         },
         headerLabel,
         dom.span({ className: "copy-label" }, "Copy"),
         dom.button(
           {
             className: "copy-packet-button",
-            onClick: e => this.copyPacketToClipboard(e, packet.result)
+            onClick: e => this.copyPacketToClipboard(e, packet.result),
           },
           "grip"
         ),
         dom.button(
           {
             className: "copy-packet-button",
-            onClick: e => this.copyPacketToClipboard(e, packet)
+            onClick: e => this.copyPacketToClipboard(e, packet),
           },
           "packet"
         )
       ),
       ...(showPacket
         ? Object.keys(packet).map(k =>
             dom.div(
               { className: "packet-rep" },
@@ -144,17 +144,17 @@ class Result extends Component {
     const { expression } = this.props;
     const { input, packet } = expression;
     return dom.div(
       { className: "rep-row" },
       dom.div({ className: "rep-input" }, input),
       dom.div(
         { className: "reps" },
         this.renderRepInAllModes({
-          object: packet.exception || packet.result
+          object: packet.exception || packet.result,
         })
       ),
       this.renderPacket(expression)
     );
   }
 }
 
 module.exports = Result;
--- a/devtools/client/debugger/packages/devtools-reps/src/launchpad/components/ResultsList.js
+++ b/devtools/client/debugger/packages/devtools-reps/src/launchpad/components/ResultsList.js
@@ -10,33 +10,33 @@ const ImPropTypes = require("react-immut
 
 const Result = createFactory(require("./Result"));
 
 class ResultsList extends Component {
   static get propTypes() {
     return {
       expressions: ImPropTypes.map.isRequired,
       showResultPacket: PropTypes.func.isRequired,
-      hideResultPacket: PropTypes.func.isRequired
+      hideResultPacket: PropTypes.func.isRequired,
     };
   }
 
   render() {
     const { expressions, showResultPacket, hideResultPacket } = this.props;
 
     return dom.div(
       { className: "expressions" },
       expressions
         .entrySeq()
         .toJS()
         .map(([key, expression]) =>
           Result({
             key,
             expression: expression.toJS(),
             showResultPacket: () => showResultPacket(key),
-            hideResultPacket: () => hideResultPacket(key)
+            hideResultPacket: () => hideResultPacket(key),
           })
         )
     );
   }
 }
 
 module.exports = ResultsList;
--- a/devtools/client/debugger/packages/devtools-reps/src/launchpad/constants.js
+++ b/devtools/client/debugger/packages/devtools-reps/src/launchpad/constants.js
@@ -10,10 +10,10 @@ module.exports = {
   CLEAR_EXPRESSIONS: Symbol("CLEAR_EXPRESSIONS"),
   SHOW_RESULT_PACKET: Symbol("SHOW_RESULT_PACKET"),
   HIDE_RESULT_PACKET: Symbol("HIDE_RESULT_PACKET"),
   ADD_INPUT: Symbol("ADD_INPUT"),
   CHANGE_CURRENT_INPUT: Symbol("CHANGE_CURRENT_INPUT"),
   NAVIGATE_INPUT_HISTORY: Symbol("NAVIGATE_INPUT_HISTORY"),
   DIR_FORWARD: Symbol("DIR_FORWARD "),
   DIR_BACKWARD: Symbol("DIR_BACKWARD"),
-  LS_EXPRESSIONS_KEY: "LS_EXPRESSIONS_KEY"
+  LS_EXPRESSIONS_KEY: "LS_EXPRESSIONS_KEY",
 };
--- a/devtools/client/debugger/packages/devtools-reps/src/launchpad/index.js
+++ b/devtools/client/debugger/packages/devtools-reps/src/launchpad/index.js
@@ -19,33 +19,33 @@ require("./launchpad.css");
 function onConnect(connection) {
   if (!connection) {
     return;
   }
 
   const client = {
     clientCommands: {
       evaluate: input =>
-        connection.tabConnection.tabTarget.activeConsole.evaluateJSAsync(input)
+        connection.tabConnection.tabTarget.activeConsole.evaluateJSAsync(input),
     },
 
     createObjectClient: function(grip) {
       return connection.tabConnection.threadClient.pauseGrip(grip);
     },
     createLongStringClient: function(grip) {
       return connection.tabConnection.tabTarget.activeConsole.longString(grip);
     },
     releaseActor: function(actor) {
       return connection.tabConnection.debuggerClient.release(actor);
-    }
+    },
   };
 
   const store = configureStore({
     makeThunkArgs: (args, state) => ({ ...args, client }),
-    client
+    client,
   });
   renderRoot(React, ReactDOM, RepsConsole, store);
 }
 
 function onConnectionError(e) {
   const h1 = document.createElement("h1");
   h1.innerText = `An error occured during the connection: «${e.message}»`;
   console.warn("An error occured during the connection", e);
--- a/devtools/client/debugger/packages/devtools-reps/src/launchpad/reducers/index.js
+++ b/devtools/client/debugger/packages/devtools-reps/src/launchpad/reducers/index.js
@@ -4,10 +4,10 @@
 
 const expressions = require("./expressions");
 const input = require("./input");
 const { objectInspector } = require("../../index");
 
 module.exports = {
   expressions,
   input,
-  objectInspector: objectInspector.reducer.default
+  objectInspector: objectInspector.reducer.default,
 };
--- a/devtools/client/debugger/packages/devtools-reps/src/launchpad/reducers/input.js
+++ b/devtools/client/debugger/packages/devtools-reps/src/launchpad/reducers/input.js
@@ -3,17 +3,17 @@
  * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
 
 const constants = require("../constants");
 const Immutable = require("immutable");
 
 const initialState = Immutable.Map({
   currentValue: "",
   currentNavigationKey: null,
-  history: Immutable.OrderedMap()
+  history: Immutable.OrderedMap(),
 });
 
 function update(state = initialState, action) {
   const { type, value, key } = action;
 
   const currentValue = state.get("currentValue");
   const currentNavigationKey = state.get("currentNavigationKey");
   const history = state.get("history");
--- a/devtools/client/debugger/packages/devtools-reps/src/launchpad/samples.js
+++ b/devtools/client/debugger/packages/devtools-reps/src/launchpad/samples.js
@@ -20,17 +20,17 @@ const samples = {
     `x = document.createElementNS("http://www.w3.org/2000/svg", "clipPath");
      x.setAttribute("id", "myNodeId");
      x.setAttribute("class", "my-class and another");
      x;`,
     "document.createComment('my comment node')",
     "document.createTextNode('foo')",
     `x = document.createAttribute('foo');
      x.value = "bar";
-     x;`
+     x;`,
   ],
 
   "map & sets": [
     `
       ({
         "small set": new Set([1,2,3,4]),
         "small map": new Map([
           ["a", {suba: 1}],
@@ -72,17 +72,17 @@ const samples = {
             {
               [String.fromCharCode(65 + i)]: i + 1,
               document
             },
             Symbol(i + 1)
           ])
         ),
       })
-    `
+    `,
   ],
 
   number: ["1", "-1", "-3.14", "0", "-0", "Infinity", "-Infinity", "NaN"],
 
   object: [
     "x = {a: 2}",
     `
 Object.create(null, Object.getOwnPropertyDescriptors({
@@ -119,46 +119,46 @@ Object.create(null, Object.getOwnPropert
   get myThrowingGetter() {
     return a.b.c.d.e.f;
   },
   get myLongStringGetter() {
     return "ab ".repeat(1e5)
   },
   set mySetterOnly(x) {}
 }))
-`
+`,
   ],
 
   promise: [
     "Promise.resolve([1, 2, 3])",
     "Promise.reject(new Error('This is wrong'))",
-    "new Promise(() => {})"
+    "new Promise(() => {})",
   ],
 
   proxy: [
     `
     var handler = {
         get: function(target, name) {
             return name in target ?
                 target[name] :
                 37;
         }
     };
     new Proxy({a: 1}, handler);
-  `
+  `,
   ],
 
   regexp: ["new RegExp('^[-]?[0-9]+[.]?[0-9]+$')"],
 
   string: [
     "'foo'",
     "'bar\nbaz\nyup'",
     "'http://example.com'",
     "'blah'.repeat(10000)",
-    "'http://example.com '.repeat(1000)"
+    "'http://example.com '.repeat(1000)",
   ],
 
   symbol: ["Symbol('foo')", "Symbol()"],
 
   errors: [
     "throw new Error('This is a simple error message.');",
     `
       var error = new Error('Complicated error message');
@@ -180,17 +180,17 @@ Object.create(null, Object.getOwnPropert
     var error = new Error('Complicated error message');
     error.stack =
       "onPacket@resource://devtools/shared/base-loader.js -> resource://devtools/shared/client/debugger-client.js:856:9\\n" +
       "send/<@resource://devtools/shared/base-loader.js -> resource://devtools/shared/transport/transport.js:569:13\\n" +
       "exports.makeInfallible/<@resource://devtools/shared/base-loader.js -> resource://devtools/shared/ThreadSafeDevToolsUtils.js:109:14\\n" +
       "exports.makeInfallible/<@resource://devtools/shared/base-loader.js -> resource://devtools/shared/ThreadSafeDevToolsUtils.js:109:14\\n";
 
     error;
-    `
-  ]
+    `,
+  ],
 };
 
 samples.yolo = Object.keys(samples).reduce((res, key) => {
   return [...res, ...samples[key]];
 }, []);
 
 module.exports = samples;
--- a/devtools/client/debugger/packages/devtools-reps/src/launchpad/selectors/index.js
+++ b/devtools/client/debugger/packages/devtools-reps/src/launchpad/selectors/index.js
@@ -11,10 +11,10 @@ function getInputState(state) {
 }
 
 function getCurrentInputValue(state) {
   return getInputState(state).get("currentValue");
 }
 
 module.exports = {
   getCurrentInputValue,
-  getExpressions
+  getExpressions,
 };
--- a/devtools/client/debugger/packages/devtools-reps/src/launchpad/store.js
+++ b/devtools/client/debugger/packages/devtools-reps/src/launchpad/store.js
@@ -12,10 +12,10 @@ const reducers = require("./reducers");
 function configureStore(options, client) {
   return createStore(
     combineReducers(reducers),
     applyMiddleware(thunk(options.makeThunkArgs), promise, logger)
   );
 }
 
 module.exports = {
-  configureStore
+  configureStore,
 };
--- a/devtools/client/debugger/packages/devtools-reps/src/launchpad/utils/redux/middleware/promise.js
+++ b/devtools/client/debugger/packages/devtools-reps/src/launchpad/utils/redux/middleware/promise.js
@@ -18,39 +18,39 @@ function promiseMiddleware({ dispatch, g
     }
 
     const promiseInst = action[PROMISE];
     const seqId = seqIdGen().toString();
 
     // Create a new action that doesn't have the promise field and has
     // the `seqId` field that represents the sequence id
     action = Object.assign(filterByKey(action, key => key !== PROMISE), {
-      seqId
+      seqId,
     });
 
     dispatch(Object.assign({}, action, { status: "start" }));
 
     // Return the promise so action creators can still compose if they want to.
     return Promise.resolve(promiseInst)
       .finally(() => new Promise(resolve => executeSoon(resolve)))
       .then(
         value => {
           dispatch(
             Object.assign({}, action, {
               status: "done",
-              value: value
+              value: value,
             })
           );
           return value;
         },
         error => {
           dispatch(
             Object.assign({}, action, {
               status: "error",
-              error: error.message || error
+              error: error.message || error,
             })
           );
           return Promise.reject(error);
         }
       );
   };
 }
 
--- a/devtools/client/debugger/packages/devtools-reps/src/launchpad/utils/utils.js
+++ b/devtools/client/debugger/packages/devtools-reps/src/launchpad/utils/utils.js
@@ -13,17 +13,17 @@ function defer() {
   const promise = new Promise((res, rej) => {
     resolve = res;
     reject = rej;
   });
 
   return {
     resolve,
     reject,
-    promise
+    promise,
   };
 }
 
 /**
  * Takes a function and executes it on the next tick.
  *
  * @param function fn
  */
@@ -51,10 +51,10 @@ function filterByKey(obj, predicate) {
 function generateKey() {
   return `${performance.now()}`;
 }
 
 module.exports = {
   defer,
   executeSoon,
   filterByKey,
-  generateKey
+  generateKey,
 };
--- a/devtools/client/debugger/packages/devtools-reps/src/object-inspector/actions.js
+++ b/devtools/client/debugger/packages/devtools-reps/src/object-inspector/actions.js
@@ -8,34 +8,34 @@ import type { GripProperties, Node, Prop
 
 const { loadItemProperties } = require("./utils/load-properties");
 const { getLoadedProperties, getActors } = require("./reducer");
 
 type Dispatch = ReduxAction => void;
 
 type ThunkArg = {
   getState: () => {},
-  dispatch: Dispatch
+  dispatch: Dispatch,
 };
 
 /**
  * This action is responsible for expanding a given node, which also means that
  * it will call the action responsible to fetch properties.
  */
 function nodeExpand(node: Node, actor) {
   return async ({ dispatch, getState }: ThunkArg) => {
     dispatch({ type: "NODE_EXPAND", data: { node } });
     dispatch(nodeLoadProperties(node, actor));
   };
 }
 
 function nodeCollapse(node: Node) {
   return {
     type: "NODE_COLLAPSE",
-    data: { node }
+    data: { node },
   };
 }
 
 /*
  * This action checks if we need to fetch properties, entries, prototype and
  * symbols for a given node. If we do, it will call the appropriate ObjectClient
  * functions.
  */
@@ -64,17 +64,17 @@ function nodeLoadProperties(node: Node, 
 
 function nodePropertiesLoaded(
   node: Node,
   actor?: string,
   properties: GripProperties
 ) {
   return {
     type: "NODE_PROPERTIES_LOADED",
-    data: { node, actor, properties }
+    data: { node, actor, properties },
   };
 }
 
 function closeObjectInspector() {
   return async ({ getState, client }: ThunkArg) => {
     releaseActors(getState(), client);
   };
 }
@@ -87,17 +87,17 @@ function closeObjectInspector() {
  * It takes a props argument which reflects what is passed by the upper-level
  * consumer.
  */
 function rootsChanged(props: Props) {
   return async ({ dispatch, client, getState }: ThunkArg) => {
     releaseActors(getState(), client);
     dispatch({
       type: "ROOTS_CHANGED",
-      data: props
+      data: props,
     });
   };
 }
 
 function releaseActors(state, client) {
   const actors = getActors(state);
   for (const actor of actors) {
     client.releaseActor(actor);
@@ -116,26 +116,26 @@ function invokeGetter(
       const result = await objectClient.getPropertyValue(
         getterName,
         receiverId
       );
       dispatch({
         type: "GETTER_INVOKED",
         data: {
           node,
-          result
-        }
+          result,
+        },
       });
     } catch (e) {
       console.error(e);
     }
   };
 }
 
 module.exports = {
   closeObjectInspector,
   invokeGetter,
   nodeExpand,
   nodeCollapse,
   nodeLoadProperties,
   nodePropertiesLoaded,
-  rootsChanged
+  rootsChanged,
 };
--- a/devtools/client/debugger/packages/devtools-reps/src/object-inspector/components/ObjectInspector.js
+++ b/devtools/client/debugger/packages/devtools-reps/src/object-inspector/components/ObjectInspector.js
@@ -22,24 +22,24 @@ const Utils = require("../utils");
 const { renderRep, shouldRenderRootsInReps } = Utils;
 const {
   getChildrenWithEvaluations,
   getActor,
   getParent,
   getValue,
   nodeIsPrimitive,
   nodeHasGetter,
-  nodeHasSetter
+  nodeHasSetter,
 } = Utils.node;
 
 import type { CachedNodes, Props } from "../types";
 
 type DefaultProps = {
   autoExpandAll: boolean,
-  autoExpandDepth: number
+  autoExpandDepth: number,
 };
 
 // This implements a component that renders an interactive inspector
 // for looking at JavaScript objects. It expects descriptions of
 // objects from the protocol, and will dynamically fetch children
 // properties as objects are expanded.
 //
 // If you want to inspect a single object, pass the name and the
@@ -158,17 +158,17 @@ class ObjectInspector extends Component<
   getItemChildren(item: Node): Array<Node> | NodeContents | null {
     const { loadedProperties, evaluations } = this.props;
     const { cachedNodes } = this;
 
     return getChildrenWithEvaluations({
       evaluations,
       loadedProperties,
       cachedNodes,
-      item
+      item,
     });
   }
 
   getRoots(): Array<Node> {
     return this.props.roots;
   }
 
   getNodeKey(item: Node): string {
@@ -193,17 +193,17 @@ class ObjectInspector extends Component<
     if (!this.isNodeExpandable(item)) {
       return;
     }
 
     const {
       nodeExpand,
       nodeCollapse,
       recordTelemetryEvent,
-      roots
+      roots,
     } = this.props;
 
     if (expand === true) {
       const actor = getActor(item, roots);
       nodeExpand(item, actor);
       if (recordTelemetryEvent) {
         recordTelemetryEvent("object_expanded");
       }
@@ -247,24 +247,24 @@ class ObjectInspector extends Component<
 
   render() {
     const {
       autoExpandAll = true,
       autoExpandDepth = 1,
       focusable = true,
       disableWrap = false,
       expandedPaths,
-      inline
+      inline,
     } = this.props;
 
     return Tree({
       className: classnames({
         inline,
         nowrap: disableWrap,
-        "object-inspector": true
+        "object-inspector": true,
       }),
 
       autoExpandAll,
       autoExpandDepth,
 
       isExpanded: item => expandedPaths && expandedPaths.has(item.path),
       isExpandable: this.isNodeExpandable,
       focused: this.focusedItem,
@@ -284,27 +284,27 @@ class ObjectInspector extends Component<
       renderItem: (item, depth, focused, arrow, expanded) =>
         ObjectInspectorItem({
           ...this.props,
           item,
           depth,
           focused,
           arrow,
           expanded,
-          setExpanded: this.setExpanded
-        })
+          setExpanded: this.setExpanded,
+        }),
     });
   }
 }
 
 function mapStateToProps(state, props) {
   return {
     expandedPaths: selectors.getExpandedPaths(state),
     loadedProperties: selectors.getLoadedProperties(state),
-    evaluations: selectors.getEvaluations(state)
+    evaluations: selectors.getEvaluations(state),
   };
 }
 
 const OI = connect(
   mapStateToProps,
   actions
 )(ObjectInspector);
 
--- a/devtools/client/debugger/packages/devtools-reps/src/object-inspector/components/ObjectInspectorItem.js
+++ b/devtools/client/debugger/packages/devtools-reps/src/object-inspector/components/ObjectInspectorItem.js
@@ -33,17 +33,17 @@ const {
   nodeIsUninitializedBinding,
   nodeIsUnmappedBinding,
   nodeIsUnscopedBinding,
   nodeIsWindow,
   nodeIsLongString,
   nodeHasFullText,
   nodeHasGetter,
   getNonPrototypeParentGripValue,
-  getParentGripValue
+  getParentGripValue,
 } = Utils.node;
 
 type Props = {
   item: Node,
   depth: number,
   expanded: boolean,
   focused: boolean,
   arrow: ReactElement,
@@ -51,103 +51,103 @@ type Props = {
   mode: Mode,
   dimTopLevelWindow: boolean,
   invokeGetter: () => void,
   onDoubleClick: ?(
     item: Node,
     options: {
       depth: number,
       focused: boolean,
-      expanded: boolean
+      expanded: boolean,
     }
   ) => any,
   onCmdCtrlClick: ?(
     item: Node,
     options: {
       depth: number,
       event: SyntheticEvent,
       focused: boolean,
-      expanded: boolean
+      expanded: boolean,
     }
   ) => any,
   onLabelClick: ?(
     item: Node,
     options: {
       depth: number,
       focused: boolean,
       expanded: boolean,
-      setExpanded: (Node, boolean) => any
+      setExpanded: (Node, boolean) => any,
     }
-  ) => any
+  ) => any,
 };
 
 class ObjectInspectorItem extends Component<Props> {
   // eslint-disable-next-line complexity
   getLabelAndValue(): {
     value?: string | Element,
-    label?: string
+    label?: string,
   } {
     const { item, depth, expanded, mode } = this.props;
 
     const label = item.name;
     const isPrimitive = nodeIsPrimitive(item);
 
     if (nodeIsOptimizedOut(item)) {
       return {
         label,
-        value: dom.span({ className: "unavailable" }, "(optimized away)")
+        value: dom.span({ className: "unavailable" }, "(optimized away)"),
       };
     }
 
     if (nodeIsUninitializedBinding(item)) {
       return {
         label,
-        value: dom.span({ className: "unavailable" }, "(uninitialized)")
+        value: dom.span({ className: "unavailable" }, "(uninitialized)"),
       };
     }
 
     if (nodeIsUnmappedBinding(item)) {
       return {
         label,
-        value: dom.span({ className: "unavailable" }, "(unmapped)")
+        value: dom.span({ className: "unavailable" }, "(unmapped)"),
       };
     }
 
     if (nodeIsUnscopedBinding(item)) {
       return {
         label,
-        value: dom.span({ className: "unavailable" }, "(unscoped)")
+        value: dom.span({ className: "unavailable" }, "(unscoped)"),
       };
     }
 
     const itemValue = getValue(item);
     const unavailable =
       isPrimitive &&
       itemValue &&
       itemValue.hasOwnProperty &&
       itemValue.hasOwnProperty("unavailable");
 
     if (nodeIsMissingArguments(item) || unavailable) {
       return {
         label,
-        value: dom.span({ className: "unavailable" }, "(unavailable)")
+        value: dom.span({ className: "unavailable" }, "(unavailable)"),
       };
     }
 
     if (
       nodeIsFunction(item) &&
       !nodeIsGetter(item) &&
       !nodeIsSetter(item) &&
       (mode === MODE.TINY || !mode)
     ) {
       return {
         label: Utils.renderRep(item, {
           ...this.props,
-          functionName: label
-        })
+          functionName: label,
+        }),
       };
     }
 
     if (
       nodeHasProperties(item) ||
       nodeHasAccessors(item) ||
       nodeIsMapEntry(item) ||
       nodeIsLongString(item) ||
@@ -158,80 +158,80 @@ class ObjectInspectorItem extends Compon
         repProps.mode = mode === MODE.LONG ? MODE.SHORT : MODE.TINY;
       }
       if (expanded) {
         repProps.mode = MODE.TINY;
       }
 
       if (nodeIsLongString(item)) {
         repProps.member = {
-          open: nodeHasFullText(item) && expanded
+          open: nodeHasFullText(item) && expanded,
         };
       }
 
       if (nodeHasGetter(item)) {
         const targetGrip = getParentGripValue(item);
         const receiverGrip = getNonPrototypeParentGripValue(item);
         if (targetGrip && receiverGrip) {
           Object.assign(repProps, {
             onInvokeGetterButtonClick: () =>
               this.props.invokeGetter(
                 item,
                 targetGrip,
                 receiverGrip.actor,
                 item.name
-              )
+              ),
           });
         }
       }
 
       return {
         label,
-        value: Utils.renderRep(item, repProps)
+        value: Utils.renderRep(item, repProps),
       };
     }
 
     return {
-      label
+      label,
     };
   }
 
   getTreeItemProps(): Object {
     const {
       item,
       depth,
       focused,
       expanded,
       onCmdCtrlClick,
       onDoubleClick,
-      dimTopLevelWindow
+      dimTopLevelWindow,
     } = this.props;
 
     const parentElementProps: Object = {
       className: classnames("node object-node", {
         focused,
         lessen:
           !expanded &&
           (nodeIsDefaultProperties(item) ||
             nodeIsPrototype(item) ||
             nodeIsGetter(item) ||
             nodeIsSetter(item) ||
             (dimTopLevelWindow === true && nodeIsWindow(item) && depth === 0)),
-        block: nodeIsBlock(item)
+        block: nodeIsBlock(item),
       }),
       onClick: e => {
         if (
           onCmdCtrlClick &&
           ((isMacOS && e.metaKey) || (!isMacOS && e.ctrlKey))
         ) {
           onCmdCtrlClick(item, {
             depth,
             event: e,
             focused,
-            expanded
+            expanded,
           });
           e.stopPropagation();
           return;
         }
 
         // If this click happened because the user selected some text, bail out.
         // Note that if the user selected some text before and then clicks here,
         // the previously selected text will be first unselected, unless the
@@ -239,26 +239,26 @@ class ObjectInspectorItem extends Compon
         // image, clicking on it does not remove any existing text selection.
         // So we need to also check if the arrow was clicked.
         if (
           Utils.selection.documentHasSelection() &&
           !(e.target && e.target.matches && e.target.matches(".arrow"))
         ) {
           e.stopPropagation();
         }
-      }
+      },
     };
 
     if (onDoubleClick) {
       parentElementProps.onDoubleClick = e => {
         e.stopPropagation();
         onDoubleClick(item, {
           depth,
           focused,
-          expanded
+          expanded,
         });
       };
     }
 
     return parentElementProps;
   }
 
   renderLabel(label: string) {
@@ -278,20 +278,20 @@ class ObjectInspectorItem extends Compon
               if (Utils.selection.documentHasSelection()) {
                 return;
               }
 
               onLabelClick(item, {
                 depth,
                 focused,
                 expanded,
-                setExpanded: this.props.setExpanded
+                setExpanded: this.props.setExpanded,
               });
             }
-          : undefined
+          : undefined,
       },
       label
     );
   }
 
   render() {
     const { arrow } = this.props;
 
--- a/devtools/client/debugger/packages/devtools-reps/src/object-inspector/reducer.js
+++ b/devtools/client/debugger/packages/devtools-reps/src/object-inspector/reducer.js
@@ -4,31 +4,31 @@
 
 import type { ReduxAction, State } from "./types";
 
 function initialState() {
   return {
     expandedPaths: new Set(),
     loadedProperties: new Map(),
     evaluations: new Map(),
-    actors: new Set()
+    actors: new Set(),
   };
 }
 
 function reducer(
   state: State = initialState(),
   action: ReduxAction = {}
 ): State {
   const { type, data } = action;
 
   const cloneState = overrides => ({ ...state, ...overrides });
 
   if (type === "NODE_EXPAND") {
     return cloneState({
-      expandedPaths: new Set(state.expandedPaths).add(data.node.path)
+      expandedPaths: new Set(state.expandedPaths).add(data.node.path),
     });
   }
 
   if (type === "NODE_COLLAPSE") {
     const expandedPaths = new Set(state.expandedPaths);
     expandedPaths.delete(data.node.path);
     return cloneState({ expandedPaths });
   }
@@ -36,35 +36,35 @@ function reducer(
   if (type === "NODE_PROPERTIES_LOADED") {
     return cloneState({
       actors: data.actor
         ? new Set(state.actors || []).add(data.actor)
         : state.actors,
       loadedProperties: new Map(state.loadedProperties).set(
         data.node.path,
         action.data.properties
-      )
+      ),
     });
   }
 
   if (type === "ROOTS_CHANGED") {
     return cloneState();
   }
 
   if (type === "GETTER_INVOKED") {
     return cloneState({
       actors: data.actor
         ? new Set(state.actors || []).add(data.result.from)
         : state.actors,
       evaluations: new Map(state.evaluations).set(data.node.path, {
         getterValue:
           data.result &&
           data.result.value &&
-          (data.result.value.return || data.result.value.throw)
-      })
+          (data.result.value.return || data.result.value.throw),
+      }),
     });
   }
 
   // NOTE: we clear the state on resume because otherwise the scopes pane
   // would be out of date. Bug 1514760
   if (type === "RESUME" || type == "NAVIGATE") {
     return initialState();
   }
@@ -101,16 +101,16 @@ function getEvaluations(state) {
 }
 
 const selectors = {
   getActors,
   getEvaluations,
   getExpandedPathKeys,
   getExpandedPaths,
   getLoadedProperties,
-  getLoadedPropertyKeys
+  getLoadedPropertyKeys,
 };
 
 Object.defineProperty(module.exports, "__esModule", {
-  value: true
+  value: true,
 });
 module.exports = selectors;
 module.exports.default = reducer;
--- a/devtools/client/debugger/packages/devtools-reps/src/object-inspector/stubs/grip.js
+++ b/devtools/client/debugger/packages/devtools-reps/src/object-inspector/stubs/grip.js
@@ -5,60 +5,60 @@
 const stubs = new Map();
 
 stubs.set("proto-properties-symbols", {
   ownProperties: {
     a: {
       configurable: true,
       enumerable: true,
       writable: true,
-      value: 1
-    }
+      value: 1,
+    },
   },
   from: "server2.conn13.child19/propertyIterator160",
   prototype: {
     type: "object",
     actor: "server2.conn13.child19/obj162",
     class: "Object",
     extensible: true,
     frozen: false,
     sealed: false,
     ownPropertyLength: 15,
     preview: {
       kind: "Object",
       ownProperties: {},
       ownSymbols: [],
       ownPropertiesLength: 15,
       ownSymbolsLength: 0,
-      safeGetterValues: {}
-    }
+      safeGetterValues: {},
+    },
   },
   ownSymbols: [
     {
       name: "Symbol()",
       descriptor: {
         configurable: true,
         enumerable: true,
         writable: true,
-        value: "hello"
-      }
-    }
-  ]
+        value: "hello",
+      },
+    },
+  ],
 });
 
 stubs.set("longs-string-safe-getter", {
   ownProperties: {
     baseVal: {
       getterValue: {
         type: "longString",
         initial: "data:image/png;base64,initial",
         length: 95080,
-        actor: "server1.conn1.child1/longString28"
+        actor: "server1.conn1.child1/longString28",
       },
       getterPrototypeLevel: 1,
       enumerable: true,
-      writable: true
-    }
+      writable: true,
+    },
   },
-  from: "server1.conn1.child1/propertyIterator30"
+  from: "server1.conn1.child1/propertyIterator30",
 });
 
 module.exports = stubs;
--- a/devtools/client/debugger/packages/devtools-reps/src/object-inspector/stubs/map.js
+++ b/devtools/client/debugger/packages/devtools-reps/src/object-inspector/stubs/map.js
@@ -15,140 +15,140 @@ stubs.set("properties", {
     sealed: false,
     ownPropertyLength: 11,
     preview: {
       kind: "Object",
       ownProperties: {},
       ownSymbols: [],
       ownPropertiesLength: 11,
       ownSymbolsLength: 2,
-      safeGetterValues: {}
-    }
+      safeGetterValues: {},
+    },
   },
   ownProperties: {},
   ownSymbols: [],
   safeGetterValues: {
     size: {
       getterValue: 2,
       getterPrototypeLevel: 2,
       enumerable: false,
-      writable: true
-    }
-  }
+      writable: true,
+    },
+  },
 });
 
 stubs.set("11-entries", {
   ownProperties: {
     "0": {
       enumerable: true,
       value: {
         type: "mapEntry",
         preview: {
           key: "key-0",
-          value: "value-0"
-        }
-      }
+          value: "value-0",
+        },
+      },
     },
     "1": {
       enumerable: true,
       value: {
         type: "mapEntry",
         preview: {
           key: "key-1",
-          value: "value-1"
-        }
-      }
+          value: "value-1",
+        },
+      },
     },
     "2": {
       enumerable: true,
       value: {
         type: "mapEntry",
         preview: {
           key: "key-2",
-          value: "value-2"
-        }
-      }
+          value: "value-2",
+        },
+      },
     },
     "3": {
       enumerable: true,
       value: {
         type: "mapEntry",
         preview: {
           key: "key-3",
-          value: "value-3"
-        }
-      }
+          value: "value-3",
+        },
+      },
     },
     "4": {
       enumerable: true,
       value: {
         type: "mapEntry",
         preview: {
           key: "key-4",
-          value: "value-4"
-        }
-      }
+          value: "value-4",
+        },
+      },
     },
     "5": {
       enumerable: true,
       value: {
         type: "mapEntry",
         preview: {
           key: "key-5",
-          value: "value-5"
-        }
-      }
+          value: "value-5",
+        },
+      },
     },
     "6": {
       enumerable: true,
       value: {
         type: "mapEntry",
         preview: {
           key: "key-6",
-          value: "value-6"
-        }
-      }
+          value: "value-6",
+        },
+      },
     },
     "7": {
       enumerable: true,
       value: {
         type: "mapEntry",
         preview: {
           key: "key-7",
-          value: "value-7"
-        }
-      }
+          value: "value-7",
+        },
+      },
     },
     "8": {
       enumerable: true,
       value: {
         type: "mapEntry",
         preview: {
           key: "key-8",
-          value: "value-8"
-        }
-      }
+          value: "value-8",
+        },
+      },
     },
     "9": {
       enumerable: true,
       value: {
         type: "mapEntry",
         preview: {
           key: "key-9",
-          value: "value-9"
-        }
-      }
+          value: "value-9",
+        },
+      },
     },
     "10": {
       enumerable: true,
       value: {
         type: "mapEntry",
         preview: {
           key: "key-10",
-          value: "value-10"
-        }
-      }
-    }
+          value: "value-10",
+        },
+      },
+    },
   },
-  from: "server4.conn4.child19/propertyIterator54"
+  from: "server4.conn4.child19/propertyIterator54",
 });
 
 module.exports = stubs;
--- a/devtools/client/debugger/packages/devtools-reps/src/object-inspector/stubs/performance.js
+++ b/devtools/client/debugger/packages/devtools-reps/src/object-inspector/stubs/performance.js
@@ -24,199 +24,199 @@ stubs.set("performance", {
           value: {
             type: "object",
             actor: "server2.conn4.child1/obj34",
             class: "Function",
             extensible: true,
             frozen: false,
             sealed: false,
             name: "now",
-            displayName: "now"
-          }
+            displayName: "now",
+          },
         },
         getEntries: {
           configurable: true,
           enumerable: true,
           writable: true,
           value: {
             type: "object",
             actor: "server2.conn4.child1/obj35",
             class: "Function",
             extensible: true,
             frozen: false,
             sealed: false,
             name: "getEntries",
-            displayName: "getEntries"
-          }
+            displayName: "getEntries",
+          },
         },
         getEntriesByType: {
           configurable: true,
           enumerable: true,
           writable: true,
           value: {
             type: "object",
             actor: "server2.conn4.child1/obj36",
             class: "Function",
             extensible: true,
             frozen: false,
             sealed: false,
             name: "getEntriesByType",
-            displayName: "getEntriesByType"
-          }
+            displayName: "getEntriesByType",
+          },
         },
         getEntriesByName: {
           configurable: true,
           enumerable: true,
           writable: true,
           value: {
             type: "object",
             actor: "server2.conn4.child1/obj37",
             class: "Function",
             extensible: true,
             frozen: false,
             sealed: false,
             name: "getEntriesByName",
-            displayName: "getEntriesByName"
-          }
+            displayName: "getEntriesByName",
+          },
         },
         clearResourceTimings: {
           configurable: true,
           enumerable: true,
           writable: true,
           value: {
             type: "object",
             actor: "server2.conn4.child1/obj38",
             class: "Function",
             extensible: true,
             frozen: false,
             sealed: false,
             name: "clearResourceTimings",
-            displayName: "clearResourceTimings"
-          }
+            displayName: "clearResourceTimings",
+          },
         },
         setResourceTimingBufferSize: {
           configurable: true,
           enumerable: true,
           writable: true,
           value: {
             type: "object",
             actor: "server2.conn4.child1/obj39",
             class: "Function",
             extensible: true,
             frozen: false,
             sealed: false,
             name: "setResourceTimingBufferSize",
-            displayName: "setResourceTimingBufferSize"
-          }
+            displayName: "setResourceTimingBufferSize",
+          },
         },
         mark: {
           configurable: true,
           enumerable: true,
           writable: true,
           value: {
             type: "object",
             actor: "server2.conn4.child1/obj40",
             class: "Function",
             extensible: true,
             frozen: false,
             sealed: false,
             name: "mark",
-            displayName: "mark"
-          }
+            displayName: "mark",
+          },
         },
         clearMarks: {
           configurable: true,
           enumerable: true,
           writable: true,
           value: {
             type: "object",
             actor: "server2.conn4.child1/obj41",
             class: "Function",
             extensible: true,
             frozen: false,
             sealed: false,
             name: "clearMarks",
-            displayName: "clearMarks"
-          }
+            displayName: "clearMarks",
+          },
         },
         measure: {
           configurable: true,
           enumerable: true,
           writable: true,
           value: {
             type: "object",
             actor: "server2.conn4.child1/obj42",
             class: "Function",
             extensible: true,
             frozen: false,
             sealed: false,
             name: "measure",
-            displayName: "measure"
-          }
+            displayName: "measure",
+          },
         },
         clearMeasures: {
           configurable: true,
           enumerable: true,
           writable: true,
           value: {
             type: "object",
             actor: "server2.conn4.child1/obj43",
             class: "Function",
             extensible: true,
             frozen: false,
             sealed: false,
             name: "clearMeasures",
-            displayName: "clearMeasures"
-          }
-        }
+            displayName: "clearMeasures",
+          },
+        },
       },
-      ownPropertiesLength: 16
-    }
+      ownPropertiesLength: 16,
+    },
   },
   ownProperties: {
     userTimingJsNow: {
       configurable: true,
       enumerable: true,
       writable: true,
-      value: false
+      value: false,
     },
     userTimingJsNowPrefixed: {
       configurable: true,
       enumerable: true,
       writable: true,
-      value: false
+      value: false,
     },
     userTimingJsUserTiming: {
       configurable: true,
       enumerable: true,
       writable: true,
-      value: false
+      value: false,
     },
     userTimingJsUserTimingPrefixed: {
       configurable: true,
       enumerable: true,
       writable: true,
-      value: false
+      value: false,
     },
     userTimingJsPerformanceTimeline: {
       configurable: true,
       enumerable: true,
       writable: true,
-      value: false
+      value: false,
     },
     userTimingJsPerformanceTimelinePrefixed: {
       configurable: true,
       enumerable: true,
       writable: true,
-      value: false
+      value: false,
     },
     timeOrigin: {
       enumerable: true,
       writable: true,
-      value: 1500971976372.9033
+      value: 1500971976372.9033,
     },
     timing: {
       enumerable: true,
       writable: true,
       value: {
         type: "object",
         actor: "server2.conn4.child1/obj44",
         class: "PerformanceTiming",
@@ -228,75 +228,75 @@ stubs.set("performance", {
           kind: "Object",
           ownProperties: {},
           ownPropertiesLength: 0,
           safeGetterValues: {
             navigationStart: {
               getterValue: 1500971976373,
               getterPrototypeLevel: 1,
               enumerable: true,
-              writable: true
+              writable: true,
             },
             unloadEventStart: {
               getterValue: 0,
               getterPrototypeLevel: 1,
               enumerable: true,
-              writable: true
+              writable: true,
             },
             unloadEventEnd: {
               getterValue: 0,
               getterPrototypeLevel: 1,
               enumerable: true,
-              writable: true
+              writable: true,
             },
             redirectStart: {
               getterValue: 0,
               getterPrototypeLevel: 1,
               enumerable: true,
-              writable: true
+              writable: true,
             },
             redirectEnd: {
               getterValue: 0,
               getterPrototypeLevel: 1,
               enumerable: true,
-              writable: true
+              writable: true,
             },
             fetchStart: {
               getterValue: 1500971982226,
               getterPrototypeLevel: 1,
               enumerable: true,
-              writable: true
+              writable: true,
             },
             domainLookupStart: {
               getterValue: 1500971982251,
               getterPrototypeLevel: 1,
               enumerable: true,
-              writable: true
+              writable: true,
             },
             domainLookupEnd: {
               getterValue: 1500971982255,
               getterPrototypeLevel: 1,
               enumerable: true,
-              writable: true
+              writable: true,
             },
             connectStart: {
               getterValue: 1500971982255,
               getterPrototypeLevel: 1,
               enumerable: true,
-              writable: true
+              writable: true,
             },
             connectEnd: {
               getterValue: 1500971982638,
               getterPrototypeLevel: 1,
               enumerable: true,
-              writable: true
-            }
-          }
-        }
-      }
+              writable: true,
+            },
+          },
+        },
+      },
     },
     navigation: {
       enumerable: true,
       writable: true,
       value: {
         type: "object",
         actor: "server2.conn4.child1/obj45",
         class: "PerformanceNavigation",
@@ -308,42 +308,42 @@ stubs.set("performance", {
           kind: "Object",
           ownProperties: {},
           ownPropertiesLength: 0,
           safeGetterValues: {
             type: {
               getterValue: 0,
               getterPrototypeLevel: 1,
               enumerable: true,
-              writable: true
+              writable: true,
             },
             redirectCount: {
               getterValue: 0,
               getterPrototypeLevel: 1,
               enumerable: true,
-              writable: true
-            }
-          }
-        }
-      }
+              writable: true,
+            },
+          },
+        },
+      },
     },
     onresourcetimingbufferfull: {
       enumerable: true,
       writable: true,
       value: {
-        type: "null"
-      }
-    }
+        type: "null",
+      },
+    },
   },
   safeGetterValues: {
     timeOrigin: {
       getterValue: 1500971976372.9033,
       getterPrototypeLevel: 1,
       enumerable: true,
-      writable: true
+      writable: true,
     },
     timing: {
       getterValue: {
         type: "object",
         actor: "server2.conn4.child1/obj44",
         class: "PerformanceTiming",
         extensible: true,
         frozen: false,
@@ -353,78 +353,78 @@ stubs.set("performance", {
           kind: "Object",
           ownProperties: {},
           ownPropertiesLength: 0,
           safeGetterValues: {
             navigationStart: {
               getterValue: 1500971976373,
               getterPrototypeLevel: 1,
               enumerable: true,
-              writable: true
+              writable: true,
             },
             unloadEventStart: {
               getterValue: 0,
               getterPrototypeLevel: 1,
               enumerable: true,
-              writable: true
+              writable: true,
             },
             unloadEventEnd: {
               getterValue: 0,
               getterPrototypeLevel: 1,
               enumerable: true,
-              writable: true
+              writable: true,
             },
             redirectStart: {
               getterValue: 0,
               getterPrototypeLevel: 1,
               enumerable: true,
-              writable: true
+              writable: true,
             },
             redirectEnd: {
               getterValue: 0,
               getterPrototypeLevel: 1,
               enumerable: true,
-              writable: true
+              writable: true,
             },
             fetchStart: {
               getterValue: 1500971982226,
               getterPrototypeLevel: 1,
               enumerable: true,
-              writable: true
+              writable: true,