Bug 1368775 - Add search suggestions, pref configuration and new telemetry to Activity Stream draft bug1368775
authork88hudson <khudson@mozilla.com>
Tue, 30 May 2017 14:06:48 -0400
changeset 586570 91ebf00cfbfa01f8fca0305de3f35c78017e60dd
parent 586536 39d5cc0fda5e16c49a59d29d4ca186a5534cc88b
child 631046 0feeb126e6d43e1af60315579a85e80e4d4d8f7f
push id61468
push userkhudson@mozilla.com
push dateTue, 30 May 2017 19:47:32 +0000
bugs1368775
milestone55.0a1
Bug 1368775 - Add search suggestions, pref configuration and new telemetry to Activity Stream MozReview-Commit-ID: 6ZZxDp04oPX
browser/extensions/activity-stream/README.md
browser/extensions/activity-stream/bootstrap.js
browser/extensions/activity-stream/common/Actions.jsm
browser/extensions/activity-stream/common/Reducers.jsm
browser/extensions/activity-stream/data/content/activity-stream.bundle.js
browser/extensions/activity-stream/data/content/activity-stream.css
browser/extensions/activity-stream/data/content/activity-stream.html
browser/extensions/activity-stream/data/content/assets/glyph-search-history.svg
browser/extensions/activity-stream/data/locales.json
browser/extensions/activity-stream/lib/ActivityStream.jsm
browser/extensions/activity-stream/lib/ActivityStreamMessageChannel.jsm
browser/extensions/activity-stream/lib/ActivityStreamPrefs.jsm
browser/extensions/activity-stream/lib/LocalizationFeed.jsm
browser/extensions/activity-stream/lib/PlacesFeed.jsm
browser/extensions/activity-stream/lib/SearchFeed.jsm
browser/extensions/activity-stream/lib/Store.jsm
browser/extensions/activity-stream/lib/TelemetryFeed.jsm
browser/extensions/activity-stream/lib/TelemetrySender.jsm
browser/extensions/activity-stream/lib/TopSitesFeed.jsm
browser/extensions/activity-stream/test/.eslintrc.js
browser/extensions/activity-stream/test/functional/mochitest/.eslintrc.js
browser/extensions/activity-stream/test/schemas/pings.js
browser/extensions/activity-stream/test/unit/common/Reducers.test.js
browser/extensions/activity-stream/test/unit/lib/ActivityStream.test.js
browser/extensions/activity-stream/test/unit/lib/ActivityStreamMessageChannel.test.js
browser/extensions/activity-stream/test/unit/lib/ActivityStreamPrefs.test.js
browser/extensions/activity-stream/test/unit/lib/LocalizationFeed.test.js
browser/extensions/activity-stream/test/unit/lib/PlacesFeed.test.js
browser/extensions/activity-stream/test/unit/lib/SearchFeed.test.js
browser/extensions/activity-stream/test/unit/lib/Store.test.js
browser/extensions/activity-stream/test/unit/lib/TelemetryFeed.test.js
browser/extensions/activity-stream/test/unit/lib/TelemetrySender.test.js
browser/extensions/activity-stream/test/unit/lib/TopSitesFeed.test.js
browser/extensions/activity-stream/test/unit/lib/init-store.test.js
browser/extensions/activity-stream/test/unit/unit-entry.js
browser/extensions/activity-stream/test/unit/utils.js
--- a/browser/extensions/activity-stream/README.md
+++ b/browser/extensions/activity-stream/README.md
@@ -1,8 +1,10 @@
 # activity-stream
 
 This system add-on replaces the new tab page in Firefox with a new design and
 functionality as part of the Activity Stream project. It can be enabled (or disabled)
 via the browser.newtabpage.activity-stream.enabled pref.
 
 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) for more detail.
--- a/browser/extensions/activity-stream/bootstrap.js
+++ b/browser/extensions/activity-stream/bootstrap.js
@@ -1,32 +1,57 @@
 /* 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/. */
-/* globals Components, XPCOMUtils, Preferences, Services, ActivityStream */
 "use strict";
 
-const {utils: Cu} = Components;
+const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
+Cu.importGlobalProperties(["fetch"]);
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
-Cu.import("resource://gre/modules/Preferences.jsm");
-Cu.import("resource://gre/modules/Services.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "Preferences",
+  "resource://gre/modules/Preferences.jsm");
 
-XPCOMUtils.defineLazyModuleGetter(this, "ActivityStream",
-  "resource://activity-stream/lib/ActivityStream.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Services",
+  "resource://gre/modules/Services.jsm");
 
+const ACTIVITY_STREAM_ENABLED_PREF = "browser.newtabpage.activity-stream.enabled";
 const BROWSER_READY_NOTIFICATION = "browser-ui-startup-complete";
-const ACTIVITY_STREAM_ENABLED_PREF = "browser.newtabpage.activity-stream.enabled";
+const REASON_SHUTDOWN_ON_PREF_CHANGE = "PREF_OFF";
 const REASON_STARTUP_ON_PREF_CHANGE = "PREF_ON";
-const REASON_SHUTDOWN_ON_PREF_CHANGE = "PREF_OFF";
+const RESOURCE_BASE = "resource://activity-stream";
 
 const ACTIVITY_STREAM_OPTIONS = {newTabURL: "about:newtab"};
 
 let activityStream;
+let modulesToUnload = new Set();
 let startupData;
 let startupReason;
+let waitingForBrowserReady = true;
+
+// Lazily load ActivityStream then find related modules to unload
+XPCOMUtils.defineLazyModuleGetter(this, "ActivityStream",
+  "resource://activity-stream/lib/ActivityStream.jsm", null, null, () => {
+    // Helper to fetch a resource directory listing and call back with each item
+    const processListing = async(uri, cb) => (await (await fetch(uri)).text())
+      .split("\n").slice(2).forEach(line => cb(line.split(" ").slice(1)));
+
+    // Look for modules one level deeper than the top resource URI
+    processListing(RESOURCE_BASE, ([directory, , , type]) => {
+      if (type === "DIRECTORY") {
+        // Look into this directory for .jsm files
+        const subDir = `${RESOURCE_BASE}/${directory}`;
+        processListing(subDir, ([name]) => {
+          if (name && name.search(/\.jsm$/) !== -1) {
+            modulesToUnload.add(`${subDir}/${name}`);
+          }
+        });
+      }
+    });
+  });
 
 /**
  * init - Initializes an instance of ActivityStream. This could be called by
  *        the startup() function exposed by bootstrap.js, or it could be called
  *        when ACTIVITY_STREAM_ENABLED_PREF is changed from false to true.
  *
  * @param  {string} reason - Reason for initialization. Could be install, upgrade, or PREF_ON
  */
@@ -45,64 +70,94 @@ function init(reason) {
  *          called by the shutdown() function exposed by bootstrap.js, or it could
  *          be called when ACTIVITY_STREAM_ENABLED_PREF is changed from true to false.
  *
  * @param  {type} reason Reason for uninitialization. Could be uninstall, upgrade, or PREF_OFF
  */
 function uninit(reason) {
   if (activityStream) {
     activityStream.uninit(reason);
-    activityStream = null;
   }
 }
 
 /**
  * onPrefChanged - handler for changes to ACTIVITY_STREAM_ENABLED_PREF
  *
  * @param  {bool} isEnabled Determines whether Activity Stream is enabled
  */
 function onPrefChanged(isEnabled) {
   if (isEnabled) {
     init(REASON_STARTUP_ON_PREF_CHANGE);
   } else {
     uninit(REASON_SHUTDOWN_ON_PREF_CHANGE);
   }
 }
 
+/**
+ * onBrowserReady - Continues startup of the add-on after browser is ready.
+ */
+function onBrowserReady() {
+  waitingForBrowserReady = false;
+
+  // Listen for changes to the pref that enables Activity Stream
+  Preferences.observe(ACTIVITY_STREAM_ENABLED_PREF, onPrefChanged);
+
+  // Only initialize if the pref is true
+  if (Preferences.get(ACTIVITY_STREAM_ENABLED_PREF)) {
+    init(startupReason);
+  }
+}
+
+/**
+ * observe - nsIObserver callback to handle various browser notifications.
+ */
 function observe(subject, topic, data) {
   switch (topic) {
     case BROWSER_READY_NOTIFICATION:
-      // Listen for changes to the pref that enables Activity Stream
-      Preferences.observe(ACTIVITY_STREAM_ENABLED_PREF, onPrefChanged);
-      // Only initialize if the pref is true
-      if (Preferences.get(ACTIVITY_STREAM_ENABLED_PREF)) {
-        init(startupReason);
-        Services.obs.removeObserver(this, BROWSER_READY_NOTIFICATION);
-      }
+      Services.obs.removeObserver(observe, BROWSER_READY_NOTIFICATION);
+      onBrowserReady();
       break;
   }
 }
 
 // The functions below are required by bootstrap.js
 
 this.install = function install(data, reason) {};
 
 this.startup = function startup(data, reason) {
-  // Only start Activity Stream up when the browser UI is ready
-  Services.obs.addObserver(observe, BROWSER_READY_NOTIFICATION);
-
   // Cache startup data which contains stuff like the version number, etc.
   // so we can use it when we init
   startupData = data;
   startupReason = reason;
+
+  // Only start Activity Stream up when the browser UI is ready
+  if (Cc["@mozilla.org/toolkit/app-startup;1"].getService(Ci.nsIAppStartup).startingUp) {
+    Services.obs.addObserver(observe, BROWSER_READY_NOTIFICATION);
+  } else {
+    // Handle manual install or automatic install after manual uninstall
+    onBrowserReady();
+  }
 };
 
 this.shutdown = function shutdown(data, reason) {
   // Uninitialize Activity Stream
   startupData = null;
   startupReason = null;
   uninit(reason);
 
-  // Stop listening to the pref that enables Activity Stream
-  Preferences.ignore(ACTIVITY_STREAM_ENABLED_PREF, onPrefChanged);
+  // Stop waiting for browser to be ready
+  if (waitingForBrowserReady) {
+    Services.obs.removeObserver(observe, BROWSER_READY_NOTIFICATION);
+  } else {
+    // Stop listening to the pref that enables Activity Stream
+    Preferences.ignore(ACTIVITY_STREAM_ENABLED_PREF, onPrefChanged);
+  }
+
+  // Unload any add-on modules that might might have been imported
+  modulesToUnload.forEach(Cu.unload);
 };
 
-this.uninstall = function uninstall(data, reason) {};
+this.uninstall = function uninstall(data, reason) {
+  if (activityStream) {
+    activityStream.uninstall(reason);
+    activityStream = null;
+  }
+};
--- a/browser/extensions/activity-stream/common/Actions.jsm
+++ b/browser/extensions/activity-stream/common/Actions.jsm
@@ -25,25 +25,23 @@ const actionTypes = [
   "INIT",
   "LOCALE_UPDATED",
   "NEW_TAB_INITIAL_STATE",
   "NEW_TAB_LOAD",
   "NEW_TAB_UNLOAD",
   "NEW_TAB_VISIBLE",
   "OPEN_NEW_WINDOW",
   "OPEN_PRIVATE_WINDOW",
-  "PERFORM_SEARCH",
   "PLACES_BOOKMARK_ADDED",
   "PLACES_BOOKMARK_CHANGED",
   "PLACES_BOOKMARK_REMOVED",
   "PLACES_HISTORY_CLEARED",
   "PLACES_LINK_BLOCKED",
   "PLACES_LINK_DELETED",
   "SCREENSHOT_UPDATED",
-  "SEARCH_STATE_UPDATED",
   "TELEMETRY_PERFORMANCE_EVENT",
   "TELEMETRY_UNDESIRED_EVENT",
   "TELEMETRY_USER_EVENT",
   "TOP_SITES_UPDATED",
   "UNINIT"
 // The line below creates an object like this:
 // {
 //   INIT: "INIT",
--- a/browser/extensions/activity-stream/common/Reducers.jsm
+++ b/browser/extensions/activity-stream/common/Reducers.jsm
@@ -16,25 +16,16 @@ const INITIAL_STATE = {
     // The version of the system-addon
     version: null
   },
   TopSites: {
     // Have we received real data from history yet?
     initialized: false,
     // The history (and possibly default) links
     rows: []
-  },
-  Search: {
-    // The search engine currently set by the browser
-    currentEngine: {
-      name: "",
-      icon: ""
-    },
-    // All possible search engines
-    engines: []
   }
 };
 
 function App(prevState = INITIAL_STATE.App, action) {
   switch (action.type) {
     case at.INIT:
       return Object.assign({}, action.data || {}, {initialized: true});
     case at.LOCALE_UPDATED: {
@@ -95,28 +86,12 @@ function TopSites(prevState = INITIAL_ST
     case at.PLACES_LINK_BLOCKED:
       newRows = prevState.rows.filter(val => val.url !== action.data.url);
       return Object.assign({}, prevState, {rows: newRows});
     default:
       return prevState;
   }
 }
 
-function Search(prevState = INITIAL_STATE.Search, action) {
-  switch (action.type) {
-    case at.SEARCH_STATE_UPDATED: {
-      if (!action.data) {
-        return prevState;
-      }
-      let {currentEngine, engines} = action.data;
-      return Object.assign({}, prevState, {
-        currentEngine,
-        engines
-      });
-    }
-    default:
-      return prevState;
-  }
-}
 this.INITIAL_STATE = INITIAL_STATE;
-this.reducers = {TopSites, App, Search};
+this.reducers = {TopSites, App};
 
 this.EXPORTED_SYMBOLS = ["reducers", "INITIAL_STATE"];
--- a/browser/extensions/activity-stream/data/content/activity-stream.bundle.js
+++ b/browser/extensions/activity-stream/data/content/activity-stream.bundle.js
@@ -92,17 +92,17 @@ var BACKGROUND_PROCESS = 2;
  *                       Use this in action creators if you need different logic
  *                       for ui/background processes.
  */
 
 const globalImportContext = typeof Window === "undefined" ? BACKGROUND_PROCESS : UI_CODE;
 // Export for tests
 
 
-const actionTypes = ["BLOCK_URL", "BOOKMARK_URL", "DELETE_BOOKMARK_BY_ID", "DELETE_HISTORY_URL", "INIT", "LOCALE_UPDATED", "NEW_TAB_INITIAL_STATE", "NEW_TAB_LOAD", "NEW_TAB_UNLOAD", "NEW_TAB_VISIBLE", "OPEN_NEW_WINDOW", "OPEN_PRIVATE_WINDOW", "PERFORM_SEARCH", "PLACES_BOOKMARK_ADDED", "PLACES_BOOKMARK_CHANGED", "PLACES_BOOKMARK_REMOVED", "PLACES_HISTORY_CLEARED", "PLACES_LINK_BLOCKED", "PLACES_LINK_DELETED", "SCREENSHOT_UPDATED", "SEARCH_STATE_UPDATED", "TELEMETRY_PERFORMANCE_EVENT", "TELEMETRY_UNDESIRED_EVENT", "TELEMETRY_USER_EVENT", "TOP_SITES_UPDATED", "UNINIT"
+const actionTypes = ["BLOCK_URL", "BOOKMARK_URL", "DELETE_BOOKMARK_BY_ID", "DELETE_HISTORY_URL", "INIT", "LOCALE_UPDATED", "NEW_TAB_INITIAL_STATE", "NEW_TAB_LOAD", "NEW_TAB_UNLOAD", "NEW_TAB_VISIBLE", "OPEN_NEW_WINDOW", "OPEN_PRIVATE_WINDOW", "PLACES_BOOKMARK_ADDED", "PLACES_BOOKMARK_CHANGED", "PLACES_BOOKMARK_REMOVED", "PLACES_HISTORY_CLEARED", "PLACES_LINK_BLOCKED", "PLACES_LINK_DELETED", "SCREENSHOT_UPDATED", "TELEMETRY_PERFORMANCE_EVENT", "TELEMETRY_UNDESIRED_EVENT", "TELEMETRY_USER_EVENT", "TOP_SITES_UPDATED", "UNINIT"
 // The line below creates an object like this:
 // {
 //   INIT: "INIT",
 //   UNINIT: "UNINIT"
 // }
 // It prevents accidentally adding a different key/value name.
 ].reduce((obj, type) => {
   obj[type] = type;return obj;
@@ -435,17 +435,17 @@ module.exports = class DetectUserSession
 
 /***/ }),
 /* 6 */
 /***/ (function(module, exports, __webpack_require__) {
 
 "use strict";
 
 
-/* globals sendAsyncMessage, addMessageListener */
+/* eslint-env mozilla/frame-script */
 
 var _require = __webpack_require__(14);
 
 const createStore = _require.createStore,
       combineReducers = _require.combineReducers,
       applyMiddleware = _require.applyMiddleware;
 
 var _require2 = __webpack_require__(1);
@@ -539,25 +539,16 @@ const INITIAL_STATE = {
     // The version of the system-addon
     version: null
   },
   TopSites: {
     // Have we received real data from history yet?
     initialized: false,
     // The history (and possibly default) links
     rows: []
-  },
-  Search: {
-    // The search engine currently set by the browser
-    currentEngine: {
-      name: "",
-      icon: ""
-    },
-    // All possible search engines
-    engines: []
   }
 };
 
 function App() {
   let prevState = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : INITIAL_STATE.App;
   let action = arguments[1];
 
   switch (action.type) {
@@ -632,40 +623,17 @@ function TopSites() {
     case at.PLACES_LINK_BLOCKED:
       newRows = prevState.rows.filter(val => val.url !== action.data.url);
       return Object.assign({}, prevState, { rows: newRows });
     default:
       return prevState;
   }
 }
 
-function Search() {
-  let prevState = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : INITIAL_STATE.Search;
-  let action = arguments[1];
-
-  switch (action.type) {
-    case at.SEARCH_STATE_UPDATED:
-      {
-        if (!action.data) {
-          return prevState;
-        }
-        var _action$data3 = action.data;
-        let currentEngine = _action$data3.currentEngine,
-            engines = _action$data3.engines;
-
-        return Object.assign({}, prevState, {
-          currentEngine,
-          engines
-        });
-      }
-    default:
-      return prevState;
-  }
-}
-var reducers = { TopSites, App, Search };
+var reducers = { TopSites, App };
 module.exports = {
   reducers,
   INITIAL_STATE
 };
 
 /***/ }),
 /* 8 */
 /***/ (function(module, exports) {
@@ -768,79 +736,95 @@ var _require = __webpack_require__(3);
 
 const injectIntl = _require.injectIntl;
 
 const ContextMenu = __webpack_require__(9);
 
 var _require2 = __webpack_require__(1);
 
 const actionTypes = _require2.actionTypes,
-      actionCreators = _require2.actionCreators;
+      ac = _require2.actionCreators;
 
 
 class LinkMenu extends React.Component {
   getBookmarkStatus(site) {
     return site.bookmarkGuid ? {
       id: "menu_action_remove_bookmark",
       icon: "bookmark-remove",
       action: "DELETE_BOOKMARK_BY_ID",
-      data: site.bookmarkGuid
+      data: site.bookmarkGuid,
+      userEvent: "BOOKMARK_DELETE"
     } : {
       id: "menu_action_bookmark",
       icon: "bookmark",
       action: "BOOKMARK_URL",
-      data: site.url
+      data: site.url,
+      userEvent: "BOOKMARK_ADD"
     };
   }
   getDefaultContextMenu(site) {
     return [{
       id: "menu_action_open_new_window",
       icon: "new-window",
       action: "OPEN_NEW_WINDOW",
-      data: { url: site.url }
+      data: { url: site.url },
+      userEvent: "OPEN_NEW_WINDOW"
     }, {
       id: "menu_action_open_private_window",
       icon: "new-window-private",
       action: "OPEN_PRIVATE_WINDOW",
-      data: { url: site.url }
+      data: { url: site.url },
+      userEvent: "OPEN_PRIVATE_WINDOW"
     }];
   }
   getOptions() {
     var _props = this.props;
     const dispatch = _props.dispatch,
-          site = _props.site;
+          site = _props.site,
+          index = _props.index,
+          source = _props.source;
 
     // default top sites have a limited set of context menu options
 
     let options = this.getDefaultContextMenu(site);
 
     // all other top sites have all the following context menu options
     if (!site.isDefault) {
       options = [this.getBookmarkStatus(site), { type: "separator" }, ...options, { type: "separator" }, {
         id: "menu_action_dismiss",
         icon: "dismiss",
         action: "BLOCK_URL",
-        data: site.url
+        data: site.url,
+        userEvent: "BLOCK"
       }, {
         id: "menu_action_delete",
         icon: "delete",
         action: "DELETE_HISTORY_URL",
-        data: site.url
+        data: site.url,
+        userEvent: "DELETE"
       }];
     }
     options.forEach(option => {
-      let action = option.action,
-          data = option.data,
-          id = option.id,
-          type = option.type;
+      const action = option.action,
+            data = option.data,
+            id = option.id,
+            type = option.type,
+            userEvent = option.userEvent;
       // Convert message ids to localized labels and add onClick function
 
       if (!type && id) {
         option.label = this.props.intl.formatMessage(option);
-        option.onClick = () => dispatch(actionCreators.SendToMain({ type: actionTypes[action], data }));
+        option.onClick = () => {
+          dispatch(ac.SendToMain({ type: actionTypes[action], data }));
+          dispatch(ac.UserEvent({
+            event: userEvent,
+            source,
+            action_position: index
+          }));
+        };
       }
     });
 
     // this is for a11y - we want to know which item is the first and which item
     // is the last, so we can close the context menu accordingly
     options[0].first = true;
     options[options.length - 1].last = true;
     return options;
@@ -856,104 +840,98 @@ class LinkMenu extends React.Component {
 module.exports = injectIntl(LinkMenu);
 module.exports._unconnected = LinkMenu;
 
 /***/ }),
 /* 11 */
 /***/ (function(module, exports, __webpack_require__) {
 
 "use strict";
+/* globals ContentSearchUIController */
 
 
 const React = __webpack_require__(0);
 
 var _require = __webpack_require__(2);
 
 const connect = _require.connect;
 
 var _require2 = __webpack_require__(3);
 
 const FormattedMessage = _require2.FormattedMessage,
       injectIntl = _require2.injectIntl;
 
 var _require3 = __webpack_require__(1);
 
-const actionTypes = _require3.actionTypes,
-      actionCreators = _require3.actionCreators;
+const ac = _require3.actionCreators;
 
 
 class Search extends React.Component {
   constructor(props) {
     super(props);
-    this.state = { searchString: "" };
     this.onClick = this.onClick.bind(this);
-    this.onChange = this.onChange.bind(this);
-  }
-
-  componentWillMount() {
-    // Trigger initialization of ContentSearch in case it hasn't happened yet
-    dispatchEvent(new CustomEvent("ContentSearchClient", { detail: {} }));
+    this.onInputMount = this.onInputMount.bind(this);
   }
 
-  performSearch(options) {
-    let searchData = {
-      engineName: options.engineName,
-      searchString: options.searchString,
-      searchPurpose: "newtab",
-      healthReportKey: "newtab"
-    };
-    this.props.dispatch(actionCreators.SendToMain({ type: actionTypes.PERFORM_SEARCH, data: searchData }));
+  handleEvent(event) {
+    // Also track search events with our own telemetry
+    if (event.detail.type === "Search") {
+      this.props.dispatch(ac.UserEvent({ event: "SEARCH" }));
+    }
   }
   onClick(event) {
-    const currentEngine = this.props.Search.currentEngine;
+    this.controller.search(event);
+  }
+  onInputMount(input) {
+    if (input) {
+      this.controller = new ContentSearchUIController(input, input.parentNode, "newtab", "activity");
+      addEventListener("ContentSearchClient", this);
+    } else {
+      this.controller = null;
+      removeEventListener("ContentSearchClient", this);
+    }
+  }
 
-    event.preventDefault();
-    this.performSearch({ engineName: currentEngine.name, searchString: this.state.searchString });
-  }
-  onChange(event) {
-    this.setState({ searchString: event.target.value });
-  }
   render() {
     return React.createElement(
       "form",
       { className: "search-wrapper" },
       React.createElement(
         "label",
         { htmlFor: "search-input", className: "search-label" },
         React.createElement(
           "span",
           { className: "sr-only" },
           React.createElement(FormattedMessage, { id: "search_web_placeholder" })
         )
       ),
       React.createElement("input", {
         id: "search-input",
         maxLength: "256",
-        onChange: this.onChange,
         placeholder: this.props.intl.formatMessage({ id: "search_web_placeholder" }),
+        ref: this.onInputMount,
         title: this.props.intl.formatMessage({ id: "search_web_placeholder" }),
-        type: "search",
-        value: this.state.searchString }),
+        type: "search" }),
       React.createElement(
         "button",
         {
           className: "search-button",
           onClick: this.onClick,
           title: this.props.intl.formatMessage({ id: "search_button" }) },
         React.createElement(
           "span",
           { className: "sr-only" },
           React.createElement(FormattedMessage, { id: "search_button" })
         )
       )
     );
   }
 }
 
-module.exports = connect(state => ({ Search: state.Search }))(injectIntl(Search));
+module.exports = connect()(injectIntl(Search));
 module.exports._unconnected = Search;
 
 /***/ }),
 /* 12 */
 /***/ (function(module, exports, __webpack_require__) {
 
 "use strict";
 
@@ -966,41 +944,54 @@ const connect = _require.connect;
 
 var _require2 = __webpack_require__(3);
 
 const FormattedMessage = _require2.FormattedMessage;
 
 const shortURL = __webpack_require__(13);
 const LinkMenu = __webpack_require__(10);
 
+var _require3 = __webpack_require__(1);
+
+const ac = _require3.actionCreators;
+
+const TOP_SITES_SOURCE = "TOP_SITES";
+
 class TopSite extends React.Component {
   constructor(props) {
     super(props);
     this.state = { showContextMenu: false, activeTile: null };
   }
   toggleContextMenu(event, index) {
     this.setState({ showContextMenu: true, activeTile: index });
   }
+  trackClick() {
+    this.props.dispatch(ac.UserEvent({
+      event: "CLICK",
+      source: TOP_SITES_SOURCE,
+      action_position: this.props.index
+    }));
+  }
   render() {
     var _props = this.props;
     const link = _props.link,
           index = _props.index,
           dispatch = _props.dispatch;
 
     const isContextMenuOpen = this.state.showContextMenu && this.state.activeTile === index;
     const title = shortURL(link);
     const screenshotClassName = `screenshot${link.screenshot ? " active" : ""}`;
     const topSiteOuterClassName = `top-site-outer${isContextMenuOpen ? " active" : ""}`;
     const style = { backgroundImage: link.screenshot ? `url(${link.screenshot})` : "none" };
     return React.createElement(
       "li",
       { className: topSiteOuterClassName, key: link.url },
       React.createElement(
         "a",
-        { href: link.url },
+        { onClick: () => this.trackClick(), href: link.url },
         React.createElement(
           "div",
           { className: "tile", "aria-hidden": true },
           React.createElement(
             "span",
             { className: "letter-fallback" },
             title[0]
           ),
@@ -1025,33 +1016,38 @@ class TopSite extends React.Component {
           `Open context menu for ${title}`
         )
       ),
       React.createElement(LinkMenu, {
         dispatch: dispatch,
         visible: isContextMenuOpen,
         onUpdate: val => this.setState({ showContextMenu: val }),
         site: link,
-        index: index })
+        index: index,
+        source: TOP_SITES_SOURCE })
     );
   }
 }
 
 const TopSites = props => React.createElement(
   "section",
   null,
   React.createElement(
     "h3",
     { className: "section-title" },
     React.createElement(FormattedMessage, { id: "header_top_sites" })
   ),
   React.createElement(
     "ul",
     { className: "top-sites-list" },
-    props.TopSites.rows.map((link, index) => React.createElement(TopSite, { dispatch: props.dispatch, key: link.url, link: link, index: index }))
+    props.TopSites.rows.map((link, index) => React.createElement(TopSite, {
+      key: link.url,
+      dispatch: props.dispatch,
+      link: link,
+      index: index }))
   )
 );
 
 module.exports = connect(state => ({ TopSites: state.TopSites }))(TopSites);
 module.exports._unconnected = TopSites;
 module.exports.TopSite = TopSite;
 
 /***/ }),
--- a/browser/extensions/activity-stream/data/content/activity-stream.css
+++ b/browser/extensions/activity-stream/data/content/activity-stream.css
@@ -238,159 +238,75 @@ main {
 
 .search-wrapper {
   cursor: default;
   display: flex;
   position: relative;
   margin: 0 0 48px;
   width: 100%;
   height: 36px; }
-  .search-wrapper .search-container {
-    z-index: 1001;
-    background: #FFF;
-    position: absolute;
-    left: 0;
-    right: 0;
-    top: 100%;
-    margin-top: -2px;
-    border: 1px solid #BFBFBF;
-    font-size: 12px;
-    box-shadow: 0 5px 10px rgba(0, 0, 0, 0.1);
-    overflow: hidden; }
-    .search-wrapper .search-container .search-title {
-      color: #666;
-      padding: 5px 10px;
-      background-color: #F7F7F7;
-      display: flex;
-      align-items: center;
-      word-break: break-all; }
-      .search-wrapper .search-container .search-title p {
-        margin: 0; }
-      .search-wrapper .search-container .search-title #current-engine-icon {
-        margin-inline-end: 8px; }
-    .search-wrapper .search-container section {
-      border-bottom: 1px solid #EAEAEA;
-      margin-bottom: 0; }
-    .search-wrapper .search-container .search-suggestions ul {
-      padding: 0;
-      margin: 0;
-      list-style: none; }
-      .search-wrapper .search-container .search-suggestions ul li a {
-        cursor: default;
-        color: #000;
-        display: block;
-        padding: 4px 36px; }
-        .search-wrapper .search-container .search-suggestions ul li a:hover, .search-wrapper .search-container .search-suggestions ul li a.active {
-          background: #0996F8;
-          color: #FFF; }
-    .search-wrapper .search-container .history-search-suggestions {
-      border-bottom: 0; }
-      .search-wrapper .search-container .history-search-suggestions ul {
-        padding: 0;
-        margin: 0;
-        list-style: none; }
-        .search-wrapper .search-container .history-search-suggestions ul li a {
-          cursor: default;
-          color: #000;
-          display: block;
-          padding: 4px 10px; }
-          .search-wrapper .search-container .history-search-suggestions ul li a:hover, .search-wrapper .search-container .history-search-suggestions ul li a.active {
-            background: #0996F8;
-            color: #FFF; }
-          .search-wrapper .search-container .history-search-suggestions ul li a:hover > #historyIcon,
-          .search-wrapper .search-container .history-search-suggestions ul li a.active > #historyIcon {
-            background-image: url("assets/glyph-search-history.svg#search-history-active"); }
-    .search-wrapper .search-container .history-search-suggestions #historyIcon {
-      width: 16px;
-      height: 16px;
-      display: inline-block;
-      margin-inline-end: 10px;
-      margin-bottom: -3px;
-      background-image: url("assets/glyph-search-history.svg#search-history"); }
-    .search-wrapper .search-container .search-partners ul {
-      padding: 0;
-      margin: 0;
-      list-style: none; }
-      .search-wrapper .search-container .search-partners ul li {
-        display: inline-block;
-        padding: 5px 0; }
-        .search-wrapper .search-container .search-partners ul li a {
-          display: block;
-          padding: 3px 16px;
-          border-right: 1px solid #BFBFBF; }
-        .search-wrapper .search-container .search-partners ul li:hover, .search-wrapper .search-container .search-partners ul li.active {
-          background: #0996F8;
-          color: #FFF; }
-          .search-wrapper .search-container .search-partners ul li:hover a, .search-wrapper .search-container .search-partners ul li.active a {
-            border-color: transparent; }
-    .search-wrapper .search-container .search-settings button {
-      color: #666;
-      margin: 0;
-      padding: 0;
-      height: 32px;
-      text-align: center;
-      width: 100%;
-      border-style: solid none none;
-      border-radius: 0;
-      background: #F7F7F7;
-      border-top: 0; }
-      .search-wrapper .search-container .search-settings button:hover, .search-wrapper .search-container .search-settings button.active {
-        background: #EBEBEB;
-        box-shadow: none; }
   .search-wrapper input {
     border: 0;
     box-shadow: 0 1px 0 0 rgba(0, 0, 0, 0.1);
     flex-grow: 1;
     margin: 0;
     outline: none;
     padding: 0 12px 0 35px;
     height: 100%;
-    border-top-left-radius: 4px;
-    border-bottom-left-radius: 4px;
+    border-radius: 4px 0 0 4px;
     padding-inline-start: 35px; }
     .search-wrapper input:focus {
       border-color: #0996F8;
       box-shadow: 0 0 0 2px #0996F8;
       z-index: 1; }
-    .search-wrapper input:focus + button {
+    .search-wrapper input:focus + .search-button {
       z-index: 1;
       box-shadow: 0 0 0 2px #0996F8;
       background-color: #0996F8;
       background-image: url("assets/glyph-forward-16-white.svg");
       color: #FFF; }
-  .search-wrapper input:dir(rtl) {
-    border-radius: 0 4px 4px 0; }
+    .search-wrapper input[aria-expanded="true"] {
+      border-radius: 4px 0 0 0; }
+    .search-wrapper input:dir(rtl) {
+      border-radius: 0 4px 4px 0; }
+      .search-wrapper input:dir(rtl)[aria-expanded="true"] {
+        border-radius: 0 4px 0 0; }
   .search-wrapper .search-label {
     background: url("assets/glyph-search-16.svg") no-repeat center center/20px;
     position: absolute;
     top: 0;
     offset-inline-start: 0;
     height: 100%;
     width: 35px;
     display: flex;
     align-items: center;
     justify-content: center;
     z-index: 2; }
-  .search-wrapper button {
+  .search-wrapper .search-button {
     border-radius: 0 3px 3px 0;
     margin-inline-start: -1px;
     border: 0;
     width: 36px;
     padding: 0;
     box-shadow: 0 1px 0 0 rgba(0, 0, 0, 0.1);
     background: #FFF url("assets/glyph-forward-16.svg") no-repeat center center;
     background-size: 16px 16px; }
-    .search-wrapper button:hover {
+    .search-wrapper .search-button:hover {
       z-index: 1;
       box-shadow: 0 1px 0 0 rgba(0, 0, 1, 0.5);
       background-color: #0996F8;
       background-image: url("assets/glyph-forward-16-white.svg");
-      color: #FFF; }
-  .search-wrapper button:dir(rtl) {
-    transform: scaleX(-1); }
+      color: #FFF;
+      cursor: pointer; }
+    .search-wrapper .search-button:dir(rtl) {
+      transform: scaleX(-1); }
+  .search-wrapper .contentSearchSuggestionTable {
+    transform: translate(-2px, 2px); }
+    .search-wrapper .contentSearchSuggestionTable:dir(rtl) {
+      transform: translate(2px, 2px); }
 
 .context-menu {
   display: block;
   position: absolute;
   font-size: 14px;
   box-shadow: 0 5px 10px rgba(0, 0, 0, 0.3), 0 0 0 1px rgba(0, 0, 0, 0.2);
   top: 6.75px;
   offset-inline-start: 100%;
--- a/browser/extensions/activity-stream/data/content/activity-stream.html
+++ b/browser/extensions/activity-stream/data/content/activity-stream.html
@@ -1,17 +1,19 @@
 <!doctype html>
 <html lang="en-us" dir="ltr">
   <head>
     <meta charset="utf-8">
     <title></title>
+    <link rel="stylesheet" href="chrome://browser/content/contentSearchUI.css" />
     <link rel="stylesheet" href="resource://activity-stream/data/content/activity-stream.css" />
   </head>
   <body class="activity-stream">
     <div id="root"></div>
+    <script src="chrome://browser/content/contentSearchUI.js"></script>
     <script src="resource://activity-stream/vendor/react.js"></script>
     <script src="resource://activity-stream/vendor/react-dom.js"></script>
     <script src="resource://activity-stream/vendor/react-intl.js"></script>
     <script src="resource://activity-stream/vendor/redux.js"></script>
     <script src="resource://activity-stream/vendor/react-redux.js"></script>
     <script src="resource://activity-stream/data/content/activity-stream.bundle.js"></script>
   </body>
 </html>
deleted file mode 100644
--- a/browser/extensions/activity-stream/data/content/assets/glyph-search-history.svg
+++ /dev/null
@@ -1,19 +0,0 @@
-<?xml version="1.0"?>
-<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="16" height="16" viewBox="0 0 16 16">
-  <style>
-    use:not(:target) {
-      display: none;
-    }
-    use {
-      fill: graytext;
-    }
-    use[id$="-active"] {
-      fill: HighlightText;
-    }
-  </style>
-  <defs>
-    <path id="search-history-glyph" d="M8,1C4.1,1,1,4.1,1,8c0,3.9,3.1,7,7,7c3.9,0,7-3.1,7-7 C15,4.1,11.9,1,8,1z M8,13.3c-2.9,0-5.3-2.4-5.3-5.3S5.1,2.7,8,2.7c2.9,0,5.3,2.4,5.3,5.3S10.9,13.3,8,13.3z M10.5,7H9V5 c0-0.6-0.4-1-1-1S7,4.4,7,5v3c0,0.6,0.4,1,1,1h2.5c0.6,0,1-0.4,1-1C11.5,7.4,11.1,7,10.5,7z"/>
-  </defs>
-  <use id="search-history" xlink:href="#search-history-glyph"/>
-  <use id="search-history-active" xlink:href="#search-history-glyph"/>
-</svg>
--- a/browser/extensions/activity-stream/data/locales.json
+++ b/browser/extensions/activity-stream/data/locales.json
@@ -125,26 +125,46 @@
     "menu_action_remove_bookmark": "Əlfəcini sil",
     "menu_action_copy_address": "Ünvanı köçür",
     "menu_action_email_link": "Keçidi e-poçt ilə göndər…",
     "menu_action_open_new_window": "Yeni Pəncərədə Aç",
     "menu_action_open_private_window": "Yeni Məxfi Pəncərədə Aç",
     "menu_action_dismiss": "Rədd et",
     "menu_action_delete": "Tarixçədən Sil",
     "search_for_something_with": "{search_term} üçün bununla axtar:",
+    "search_button": "Axtar",
     "search_header": "{search_engine_name} Axtarış",
     "search_web_placeholder": "İnternetdə Axtar",
     "search_settings": "Axtarış Tənzimləmələrini Dəyiş",
     "welcome_title": "Yeni vərəqə xoş gəldiniz",
     "welcome_body": "Firefox bu səhifədə ən uyğun əlfəcin, məqalə, video və son ziyarət etdiyiniz səhifələri göstərərək onları rahat tapmağınıza kömək edəcək.",
     "welcome_label": "Seçilmişləriniz təyin edilir",
     "time_label_less_than_minute": "<1d",
     "time_label_minute": "{number}d",
     "time_label_hour": "{number}s",
-    "time_label_day": "{number}g"
+    "time_label_day": "{number}g",
+    "settings_pane_button_label": "Yeni Vərəq səhifənizi özəlləşdirin",
+    "settings_pane_header": "Yeni Vərəq Nizamlamaları",
+    "settings_pane_body": "Yeni vərəq açdığınızda nə görəcəyinizi seçin.",
+    "settings_pane_search_header": "Axtar",
+    "settings_pane_search_body": "Yeni vərəqinizdən Web-də axtarış edin.",
+    "settings_pane_topsites_header": "Qabaqcıl Saytlar",
+    "settings_pane_topsites_body": "Ən çox ziyarət etdiyiniz saytları görün.",
+    "settings_pane_topsites_options_showmore": "İki sətir göstər",
+    "settings_pane_highlights_header": "Seçilmişlər",
+    "settings_pane_highlights_body": "Son gəzmə tarixçəniz və yeni yaradılar əlfəcinlərinizə göz gəzdirin.",
+    "settings_pane_done_button": "Oldu",
+    "edit_topsites_button_text": "Redaktə et",
+    "edit_topsites_button_label": "Qabaqcıl Saytlar bölümünüzü fərdiləşdirin",
+    "edit_topsites_showmore_button": "Daha çox göstər",
+    "edit_topsites_showless_button": "Daha az göstər",
+    "edit_topsites_done_button": "Oldu",
+    "edit_topsites_pin_button": "Bu saytı sabitlə",
+    "edit_topsites_edit_button": "Bu saytı düzəlt",
+    "edit_topsites_dismiss_button": "Bu saytı çıxart"
   },
   "be": {
     "newtab_page_title": "Новая картка",
     "default_label_loading": "Загрузка…",
     "header_top_sites": "Папулярныя сайты",
     "header_highlights": "Выбранае",
     "type_label_visited": "Наведанае",
     "type_label_bookmarked": "У закладках",
@@ -370,17 +390,66 @@
     "edit_topsites_button_label": "Upravit oddíl Top stránek",
     "edit_topsites_showmore_button": "Zobrazit více",
     "edit_topsites_showless_button": "Zobrazit méně",
     "edit_topsites_done_button": "Hotovo",
     "edit_topsites_pin_button": "Připnout tuto stránku",
     "edit_topsites_edit_button": "Upravit tuto stránku",
     "edit_topsites_dismiss_button": "Skrýt tuto stránku"
   },
-  "cy": {},
+  "cy": {
+    "newtab_page_title": "Tab Newydd",
+    "default_label_loading": "Llwytho…",
+    "header_top_sites": "Hoff Wefannau",
+    "header_highlights": "Goreuon",
+    "type_label_visited": "Ymwelwyd",
+    "type_label_bookmarked": "Nod Tudalen",
+    "type_label_synced": "Cydweddwyd o ddyfais arall",
+    "type_label_open": "Ar Agor",
+    "type_label_topic": "Pwnc",
+    "menu_action_bookmark": "Nod Tudalen",
+    "menu_action_remove_bookmark": "Tynnu Nod Tudalen",
+    "menu_action_copy_address": "Copïo'r Cyfeiriad",
+    "menu_action_email_link": "Dolen E-bost…",
+    "menu_action_open_new_window": "Agor Ffenestr Newydd",
+    "menu_action_open_private_window": "Agor mewn Ffenestr Preifat Newydd",
+    "menu_action_dismiss": "Cau",
+    "menu_action_delete": "Dileu o'r Hanes",
+    "search_for_something_with": "Chwilio am {search_term} gyda:",
+    "search_button": "Chwilio",
+    "search_header": "{search_engine_name} Chwilio",
+    "search_web_placeholder": "Chwilio'r We",
+    "search_settings": "Newid y Gosodiadau Chwilio",
+    "welcome_title": "Croeso i dab newydd",
+    "welcome_body": "Bydd Firefox yn defnyddio'r gofod hwn i ddangos y nodau tudalen, erthyglau, fideos a thudalennau mwyaf perthnasol i chi, a thudalennau fuoch yn ymweld â nhw'n ddiweddar, fel bod modd i chi ddychwelydd atyn nhw'n hawdd.",
+    "welcome_label": "Adnabod eich Goreuon",
+    "time_label_less_than_minute": "<1m",
+    "time_label_minute": "{number}m",
+    "time_label_hour": "{number}a",
+    "time_label_day": "{number}d",
+    "settings_pane_button_label": "Cyfaddasu eich tudalen Tab Newydd",
+    "settings_pane_header": "Dewisiadau Tab Newydd",
+    "settings_pane_body": "Dewis beth rydych yn ei weld pan fyddwch yn agor tab newydd.",
+    "settings_pane_search_header": "Chwilio",
+    "settings_pane_search_body": "Chwilio'r We o'ch tab newydd.",
+    "settings_pane_topsites_header": "Hoff Wefannau",
+    "settings_pane_topsites_body": "Cael mynediad at y gwefannau rydych yn ymweld â nhw amlaf.",
+    "settings_pane_topsites_options_showmore": "Dangos dwy res",
+    "settings_pane_highlights_header": "Goreuon",
+    "settings_pane_highlights_body": "Edrych nôl ar eich hanes pori a nodau tudalen diweddar.",
+    "settings_pane_done_button": "Gorffen",
+    "edit_topsites_button_text": "Golygu",
+    "edit_topsites_button_label": "Cyfaddasu eich adran Hoff Wefannau",
+    "edit_topsites_showmore_button": "Dangos rhagor",
+    "edit_topsites_showless_button": "Dangos llai",
+    "edit_topsites_done_button": "Gorffen",
+    "edit_topsites_pin_button": "Pinio'r wefan",
+    "edit_topsites_edit_button": "Golygu'r wefan",
+    "edit_topsites_dismiss_button": "Dileu'r wefan"
+  },
   "da": {
     "newtab_page_title": "Nyt faneblad",
     "default_label_loading": "Indlæser…",
     "header_top_sites": "Mest besøgte websider",
     "header_highlights": "Højdepunkter",
     "type_label_visited": "Besøgt",
     "type_label_bookmarked": "Bogmærket",
     "type_label_synced": "Synkroniseret fra en anden enhed",
@@ -390,26 +459,46 @@
     "menu_action_remove_bookmark": "Fjern bogmærke",
     "menu_action_copy_address": "Kopier adresse",
     "menu_action_email_link": "Send link…",
     "menu_action_open_new_window": "Åbn i et nyt vindue",
     "menu_action_open_private_window": "Åbn i et nyt privat vindue",
     "menu_action_dismiss": "Afvis",
     "menu_action_delete": "Slet fra historik",
     "search_for_something_with": "Søg efter {search_term} med:",
+    "search_button": "Søg",
     "search_header": "{search_engine_name}-søgning",
     "search_web_placeholder": "Søg på internettet",
     "search_settings": "Skift søgeindstillinger",
     "welcome_title": "Velkommen til nyt faneblad",
     "welcome_body": "Firefox vil bruge denne plads til at vise dine mest relevante bogmærker, artikler, videoer og sider, du har besøgt for nylig - så kan du nemmere finde dem.",
     "welcome_label": "Finder dine højdepunkter",
     "time_label_less_than_minute": "<1 m.",
     "time_label_minute": "{number} m.",
     "time_label_hour": "{number} t.",
-    "time_label_day": "{number} d."
+    "time_label_day": "{number} d.",
+    "settings_pane_button_label": "Tilpas siden Nyt faneblad",
+    "settings_pane_header": "Indstillinger for Nyt faneblad",
+    "settings_pane_body": "Vælg, hvad der vises, når du åbner et nyt faneblad.",
+    "settings_pane_search_header": "Søgning",
+    "settings_pane_search_body": "Søg på nettet fra Nyt faneblad.",
+    "settings_pane_topsites_header": "Mest besøgte websider",
+    "settings_pane_topsites_body": "Adgang til de websider, du besøger oftest.",
+    "settings_pane_topsites_options_showmore": "Vis to rækker",
+    "settings_pane_highlights_header": "Højdepunkter",
+    "settings_pane_highlights_body": "Se tilbage på din seneste browserhistorik og nyligt oprettede bogmærker.",
+    "settings_pane_done_button": "Færdig",
+    "edit_topsites_button_text": "Rediger",
+    "edit_topsites_button_label": "Tilpas afsnittet Mest besøgte websider",
+    "edit_topsites_showmore_button": "Vis flere",
+    "edit_topsites_showless_button": "Vis færre",
+    "edit_topsites_done_button": "Færdig",
+    "edit_topsites_pin_button": "Fastgør denne webside",
+    "edit_topsites_edit_button": "Rediger denne webside",
+    "edit_topsites_dismiss_button": "Afvis denne webside"
   },
   "de": {
     "newtab_page_title": "Neuer Tab",
     "default_label_loading": "Wird geladen…",
     "header_top_sites": "Meistbesuchte Seiten",
     "header_highlights": "Wichtige Seiten",
     "type_label_visited": "Besucht",
     "type_label_bookmarked": "Lesezeichen",
@@ -657,16 +746,25 @@
     "edit_topsites_button_label": "Customize your Top Sites section",
     "edit_topsites_showmore_button": "Show more",
     "edit_topsites_showless_button": "Show less",
     "edit_topsites_done_button": "Done",
     "edit_topsites_pin_button": "Pin this site",
     "edit_topsites_unpin_button": "Unpin this site",
     "edit_topsites_edit_button": "Edit this site",
     "edit_topsites_dismiss_button": "Dismiss this site",
+    "edit_topsites_add_button": "Add",
+    "topsites_form_add_header": "New Top Site",
+    "topsites_form_edit_header": "Edit Top Site",
+    "topsites_form_title_placeholder": "Enter a title",
+    "topsites_form_url_placeholder": "Type or paste a URL",
+    "topsites_form_add_button": "Add",
+    "topsites_form_save_button": "Save",
+    "topsites_form_cancel_button": "Cancel",
+    "topsites_form_url_validation": "Valid URL required",
     "pocket_read_more": "Popular Topics:",
     "pocket_read_even_more": "View More Stories",
     "pocket_feedback_header": "The best of the web, curated by over 25 million people.",
     "pocket_feedback_body": "Pocket, a part of the Mozilla family, will help connect you to high-quality content that you may not have found otherwise.",
     "pocket_send_feedback": "Send Feedback"
   },
   "en-ZA": {},
   "eo": {
@@ -683,26 +781,28 @@
     "menu_action_remove_bookmark": "Forigi legosignon",
     "menu_action_copy_address": "Kopii adreson",
     "menu_action_email_link": "Sendi ligilon retpoŝte…",
     "menu_action_open_new_window": "Malfermi en nova fenestro",
     "menu_action_open_private_window": "Malfermi en nova privata fenestro",
     "menu_action_dismiss": "Ignori",
     "menu_action_delete": "Forigi el historio",
     "search_for_something_with": "Serĉi {search_term} per:",
+    "search_button": "Serĉi",
     "search_header": "Serĉo de {search_engine_name}",
     "search_web_placeholder": "Serĉi la Teksaĵon",
     "search_settings": "Modifi serĉajn agordojn",
     "welcome_title": "Bonvenon al nova langeto",
     "welcome_body": "Firefox uzos tiun ĉi spacon por montri al vi viaj plej gravajn legosignojn, artikolojn, filmetojn kaj paĝojn, kiujn vi vizitis antaŭ nelonge, tiel ke vi povos reiri al ili facile.",
     "welcome_label": "Elstaraĵoj identigataj",
     "time_label_less_than_minute": "<1m",
     "time_label_minute": "{number}m",
     "time_label_hour": "{number}h",
-    "time_label_day": "{number}t"
+    "time_label_day": "{number}t",
+    "settings_pane_button_label": "Personecigi la paĝon por novaj langetoj"
   },
   "es-AR": {
     "newtab_page_title": "Nueva pestaña",
     "default_label_loading": "Cargando…",
     "header_top_sites": "Más visitados",
     "header_highlights": "Destacados",
     "type_label_visited": "Visitados",
     "type_label_bookmarked": "Marcados",
@@ -913,26 +1013,46 @@
     "menu_action_remove_bookmark": "Eemalda järjehoidja",
     "menu_action_copy_address": "Kopeeri aadress",
     "menu_action_email_link": "Saada link e-postiga…",
     "menu_action_open_new_window": "Ava uues aknas",
     "menu_action_open_private_window": "Ava uues privaatses aknas",
     "menu_action_dismiss": "Peida",
     "menu_action_delete": "Kustuta ajaloost",
     "search_for_something_with": "Otsi fraasi {search_term}, kasutades otsingumootorit:",
+    "search_button": "Otsi",
     "search_header": "{search_engine_name}",
     "search_web_placeholder": "Otsi veebist",
     "search_settings": "Muuda otsingu sätteid",
     "welcome_title": "Tere tulemast uuele kaardile",
     "welcome_body": "Firefox kasutab seda lehte, et kuvada sulle kõige olulisemaid järjehoidjaid, artikleid, videoid ja lehti, mida oled hiljuti külastanud, nii et pääseksid kergelt nende juurde tagasi.",
     "welcome_label": "Esiletõstetava sisu tuvastamine",
     "time_label_less_than_minute": "<1m",
     "time_label_minute": "{number}m",
     "time_label_hour": "{number}t",
-    "time_label_day": "{number}p"
+    "time_label_day": "{number}p",
+    "settings_pane_button_label": "Kohanda uue kaardi lehte",
+    "settings_pane_header": "Uue kaardi sätted",
+    "settings_pane_body": "Vali asjad, mida soovid uue kaardi avamisel näha.",
+    "settings_pane_search_header": "Otsi",
+    "settings_pane_search_body": "Veebis otsimine uuel kaardil.",
+    "settings_pane_topsites_header": "Top saidid",
+    "settings_pane_topsites_body": "Ligipääs enim külastatud veebilehtedele.",
+    "settings_pane_topsites_options_showmore": "Kuvatakse kahel real",
+    "settings_pane_highlights_header": "Esiletõstetud",
+    "settings_pane_highlights_body": "Tagasivaade hiljutisele lehitsemisajaloole ning lisatud järjehoidjatele.",
+    "settings_pane_done_button": "Valmis",
+    "edit_topsites_button_text": "Muuda",
+    "edit_topsites_button_label": "Kohanda top saitide osa",
+    "edit_topsites_showmore_button": "Kuva rohkem",
+    "edit_topsites_showless_button": "Näita vähem",
+    "edit_topsites_done_button": "Valmis",
+    "edit_topsites_pin_button": "Kinnita see sait",
+    "edit_topsites_edit_button": "Muuda seda saiti",
+    "edit_topsites_dismiss_button": "Peida see sait"
   },
   "eu": {},
   "fa": {
     "newtab_page_title": "زبانه جدید",
     "default_label_loading": "در حال بارگیری…",
     "header_top_sites": "سایت‌های برتر",
     "header_highlights": "برجسته‌ها",
     "type_label_visited": "مشاهده شده",
@@ -1217,27 +1337,97 @@
     "menu_action_remove_bookmark": "הסרת סימניה",
     "menu_action_copy_address": "העתקת כתובת",
     "menu_action_email_link": "שליחת קישור בדוא״ל…",
     "menu_action_open_new_window": "פתיחה בחלון חדש",
     "menu_action_open_private_window": "פתיחה בלשונית פרטית חדשה",
     "menu_action_dismiss": "ביטול",
     "menu_action_delete": "מחיקה מההיסטוריה",
     "search_for_something_with": "חיפוש אחר {search_term} עם:",
+    "search_button": "חיפוש",
     "search_header": "חיפוש ב־{search_engine_name}",
     "search_web_placeholder": "חיפוש ברשת",
     "search_settings": "שינוי הגדרות חיפוש",
     "welcome_title": "ברוכים הבאים לדף הלשונית החדשה",
     "welcome_body": "Firefox ישתמש באזור זה כדי להציג את הסימניות הרלוונטיות ביותר, מאמרים, סרטוני וידאו ודפים שביקרת בהם לאחרונה, כך שניתן יהיה לגשת אליהם שוב בקלות.",
+    "welcome_label": "תחומי העניין שלך מזוהים",
     "time_label_less_than_minute": "פחות מדקה",
     "time_label_minute": "{number} דקות",
     "time_label_hour": "{number} שעות",
-    "time_label_day": "{number} ימים"
+    "time_label_day": "{number} ימים",
+    "settings_pane_button_label": "התאמה אישית של דף הלשונית החדשה שלך",
+    "settings_pane_header": "העדפות לשונית חדשה",
+    "settings_pane_body": "ניתן לבחור מה יופיע בפניך בעת פתיחת לשונית חדשה.",
+    "settings_pane_search_header": "חיפוש",
+    "settings_pane_search_body": "חיפוש באינטרנט ישירות מהלשונית החדשה שלך.",
+    "settings_pane_topsites_header": "אתרים מובילים",
+    "settings_pane_topsites_body": "גישה לאתרים בהם ביקרת הכי הרבה.",
+    "settings_pane_topsites_options_showmore": "הצגת שתי שורות",
+    "settings_pane_highlights_header": "המלצות",
+    "settings_pane_highlights_body": "ניתן להסתכל על היסטוריית הגלישה העדכנית שלך ועל הסימניות האחרונות שנוצרו.",
+    "settings_pane_done_button": "סיום",
+    "edit_topsites_button_text": "עריכה",
+    "edit_topsites_button_label": "התאמת אגף האתרים המובילים שלך",
+    "edit_topsites_showmore_button": "להציג יותר",
+    "edit_topsites_showless_button": "להציג פחות",
+    "edit_topsites_done_button": "בוצע",
+    "edit_topsites_pin_button": "נעיצת אתר זה",
+    "edit_topsites_edit_button": "עריכת אתר זה",
+    "edit_topsites_dismiss_button": "התעלמות מאתר זה"
   },
-  "hi-IN": {},
+  "hi-IN": {
+    "newtab_page_title": "नया टैब",
+    "default_label_loading": "लोड हो रहा है…",
+    "header_top_sites": "सर्वोच्च साइटें",
+    "header_highlights": "प्रमुखताएँ",
+    "type_label_visited": "देखी गई",
+    "type_label_bookmarked": "पुस्तचिह्न लगाया हुआ",
+    "type_label_synced": "किसी अन्य उपकरण से समकालीन किया गया",
+    "type_label_open": "खोलें",
+    "type_label_topic": "विषय",
+    "menu_action_bookmark": "पुस्तचिह्न",
+    "menu_action_remove_bookmark": "पुस्तचिह्न हटाएँ",
+    "menu_action_copy_address": "पता कॉपी करें",
+    "menu_action_email_link": "ईमेल लिंक…",
+    "menu_action_open_new_window": "एक नई विंडो में खोलें",
+    "menu_action_open_private_window": "एक नई निजी विंडो में खोलें",
+    "menu_action_dismiss": "निरस्त करें",
+    "menu_action_delete": "इतिहास से मिटाएँ",
+    "search_for_something_with": "इस के साथ {search_term} के लिए खोजें:",
+    "search_button": "खोज",
+    "search_header": "{search_engine_name} खोज",
+    "search_web_placeholder": "वेब पर खोजें",
+    "search_settings": "खोज सेटिंग बदलें",
+    "welcome_title": "नए टैब में आपका स्वागत है",
+    "welcome_body": "Firefox यह जगह आपके सर्वाधिक प्रासंगिक पुस्तचिन्ह, लेख, वीडियो और पृष्ठों जिनका आपने हाल ही में दौरा किया है उनको दर्शाने के लिए करेगा, ताकि आप बाद में उन तक आसानी से वापस जा सकें.",
+    "welcome_label": "आपके प्रमुखताओं की पहचान की जा रही है",
+    "time_label_less_than_minute": "<1मि0",
+    "time_label_minute": "{number}मि0",
+    "time_label_hour": "{number}मि0",
+    "time_label_day": "{number}दिन",
+    "settings_pane_button_label": "अपने नए टैब पृष्ठ को अनुकूलित करें",
+    "settings_pane_header": "नयी टैब वरीयताएँ",
+    "settings_pane_body": "चयन करें कि नया टैब खोलने पर आप क्या देखें.",
+    "settings_pane_search_header": "खोज",
+    "settings_pane_search_body": "अपने नए टैब से वेब पर खोजें.",
+    "settings_pane_topsites_header": "सर्वोच्च साइटें",
+    "settings_pane_topsites_body": "आपके द्वारा सबसे ज्यादा खोजी जाने वाली वेबसाइट्स देखें.",
+    "settings_pane_topsites_options_showmore": "दो पंक्तियाँ दिखाएँ",
+    "settings_pane_highlights_header": "प्रमुखताएँ",
+    "settings_pane_highlights_body": "अपने हाल के ब्राउज़िंग इतिहास और नए बनाए गए पुस्तचिन्हों को वापस देखें.",
+    "settings_pane_done_button": "संपन्न",
+    "edit_topsites_button_text": "संपादित करें",
+    "edit_topsites_button_label": "अपने शीर्ष साइट्स अनुभाग को अनुकूलित करें",
+    "edit_topsites_showmore_button": "अधिक दिखाएँ",
+    "edit_topsites_showless_button": "कम दिखाएँ",
+    "edit_topsites_done_button": "पूर्ण",
+    "edit_topsites_pin_button": "इस साइट को पिन करें",
+    "edit_topsites_edit_button": "इस साइट को संपादित करें",
+    "edit_topsites_dismiss_button": "इस साइट को ख़ारिज करें"
+  },
   "hr": {
     "newtab_page_title": "Nova kartica",
     "default_label_loading": "Učitavanje…",
     "header_top_sites": "Najbolje stranice",
     "header_highlights": "Istaknuto",
     "type_label_visited": "Posjećeno",
     "type_label_bookmarked": "Zabilježeno",
     "type_label_synced": "Sinkronizirano s drugog uređaja",
@@ -1247,26 +1437,46 @@
     "menu_action_remove_bookmark": "Ukloni zabilješku",
     "menu_action_copy_address": "Kopiraj adresu",
     "menu_action_email_link": "Pošalji poveznicu e-poštom…",
     "menu_action_open_new_window": "Otvori u novom prozoru",
     "menu_action_open_private_window": "Otvori u novom privatnom prozoru",
     "menu_action_dismiss": "Odbaci",
     "menu_action_delete": "Obriši iz povijesti",
     "search_for_something_with": "Traži {search_term} s:",
+    "search_button": "Traži",
     "search_header": "{search_engine_name} pretraživanje",
     "search_web_placeholder": "Pretraži web",
     "search_settings": "Promijeni postavke pretraživanja",
     "welcome_title": "Dobro došli u novu karticu",
     "welcome_body": "Firefox će koristiti ovaj prostor kako bi vam pokazao najbitnije zabilješke, članke, video uratke i stranice koje ste nedavno posjetili, tako da se možete lako vratiti na njih.",
     "welcome_label": "Identificiranje istaknutog",
     "time_label_less_than_minute": "<1m",
     "time_label_minute": "{number}m",
     "time_label_hour": "{number}h",
-    "time_label_day": "{number}d"
+    "time_label_day": "{number}d",
+    "settings_pane_button_label": "Prilagodite svoju početnu stranicu nove kartice",
+    "settings_pane_header": "Postavke nove kartice",
+    "settings_pane_body": "Odaberite što ćete vidjeti kada otvorite novu karticu.",
+    "settings_pane_search_header": "Traži",
+    "settings_pane_search_body": "Pretražite Web iz nove kartice.",
+    "settings_pane_topsites_header": "Najbolje stranice",
+    "settings_pane_topsites_body": "Pristupite stranicama koje najčešće posjećujete.",
+    "settings_pane_topsites_options_showmore": "Prikaži dva reda",
+    "settings_pane_highlights_header": "Istaknuto",
+    "settings_pane_highlights_body": "Osvrnite se na nedavno posjećene stranice i nove zabilješke.",
+    "settings_pane_done_button": "Gotovo",
+    "edit_topsites_button_text": "Uredi",
+    "edit_topsites_button_label": "Prilagodite odjel s najboljim stranicama",
+    "edit_topsites_showmore_button": "Prikaži više",
+    "edit_topsites_showless_button": "Prikaži manje",
+    "edit_topsites_done_button": "Gotovo",
+    "edit_topsites_pin_button": "Zakači stranicu",
+    "edit_topsites_edit_button": "Uredi ovu stranicu",
+    "edit_topsites_dismiss_button": "Odbaci stranicu"
   },
   "hsb": {
     "newtab_page_title": "Nowy rajtark",
     "default_label_loading": "Začituje so…",
     "header_top_sites": "Najhusćišo wopytane sydła",
     "header_highlights": "Wjerški",
     "type_label_visited": "Wopytany",
     "type_label_bookmarked": "Jako zapołožka składowany",
@@ -1690,26 +1900,46 @@
     "menu_action_remove_bookmark": "즐겨찾기 삭제",
     "menu_action_copy_address": "주소 복사",
     "menu_action_email_link": "메일로 링크 보내기…",
     "menu_action_open_new_window": "새 창에서 열기",
     "menu_action_open_private_window": "새 사생활 보호 창에서 열기",
     "menu_action_dismiss": "닫기",
     "menu_action_delete": "방문 기록에서 삭제",
     "search_for_something_with": "다음에서 {search_term} 검색:",
+    "search_button": "검색",
     "search_header": "{search_engine_name} 검색",
     "search_web_placeholder": "웹 검색",
     "search_settings": "검색 설정 바꾸기",
     "welcome_title": "새 탭을 소개합니다",
     "welcome_body": "최근에 방문한 관련있는 즐겨찾기나 글, 동영상, 페이지를 Firefox가 여기에 표시해서 쉽게 다시 찾아볼 수 있게 할 것입니다.",
     "welcome_label": "하이라이트 확인",
     "time_label_less_than_minute": "<1분",
     "time_label_minute": "{number}분",
     "time_label_hour": "{number}시",
-    "time_label_day": "{number}일"
+    "time_label_day": "{number}일",
+    "settings_pane_button_label": "새 탭 페이지 꾸미기",
+    "settings_pane_header": "새 탭 설정",
+    "settings_pane_body": "새 탭을 열 때 어떤 화면을 볼지 선택하세요.",
+    "settings_pane_search_header": "검색",
+    "settings_pane_search_body": "새 탭에서 웹을 검색하세요.",
+    "settings_pane_topsites_header": "상위 사이트",
+    "settings_pane_topsites_body": "가장 많이 방문한 웹 사이트에 접근하세요.",
+    "settings_pane_topsites_options_showmore": "두 줄로 보기",
+    "settings_pane_highlights_header": "하이라이트",
+    "settings_pane_highlights_body": "최근 방문 기록과 북마크를 살펴보세요.",
+    "settings_pane_done_button": "완료",
+    "edit_topsites_button_text": "수정",
+    "edit_topsites_button_label": "상위 사이트 영역 꾸미기",
+    "edit_topsites_showmore_button": "더보기",
+    "edit_topsites_showless_button": "줄이기",
+    "edit_topsites_done_button": "완료",
+    "edit_topsites_pin_button": "이 사이트 고정",
+    "edit_topsites_edit_button": "이 사이트 수정",
+    "edit_topsites_dismiss_button": "이 사이트 제거"
   },
   "ku": {},
   "lij": {
     "newtab_page_title": "Neuvo Feuggio",
     "default_label_loading": "Carego…",
     "header_top_sites": "I megio sciti",
     "header_highlights": "In evidensa",
     "type_label_visited": "Vixitou",
@@ -1771,25 +2001,46 @@
     "menu_action_remove_bookmark": "ລຶບບຸກມາກອອກ",
     "menu_action_copy_address": "ສຳເນົາທີ່ຢູ່",
     "menu_action_email_link": "ລີ້ງອີເມວ…",
     "menu_action_open_new_window": "ເປີດລີ້ງໃນຫນ້າຕ່າງໃຫມ່",
     "menu_action_open_private_window": "ເປີດໃນຫນ້າຕ່າງສ່ວນຕົວໃຫມ່",
     "menu_action_dismiss": "ຍົກເລີກ",
     "menu_action_delete": "ລຶບອອກຈາກປະຫວັດການນຳໃຊ້",
     "search_for_something_with": "ຄົ້ນຫາສໍາລັບ {search_term} ດ້ວຍ:",
+    "search_button": "ຊອກ​ຫາ",
     "search_header": "ຄົ້ນຫາ {search_engine_name}",
     "search_web_placeholder": "ຄົ້ນຫາເວັບ",
     "search_settings": "ປ່ຽນການຕັ້ງຄ່າການຄົ້ນຫາ",
     "welcome_title": "ຍິນດີຕອນຮັບເຂົ້າສູ່ແຖບໃຫມ່",
+    "welcome_body": "Firefox ຈະໃຊ້ພື້ນທີ່ນີ້ເພື່ອສະແດງໃຫ້ເຫັນບຸກມາກທີ່ກ່ຽວຂ້ອງທີ່ສຸດຂອງທ່ານ, ບົດຄວາມ, ວິດີໂອ, ແລະ ຫນ້າທີ່ທ່ານຫາກາໄດ້ເຂົ້າໄປເບິງ, ສະນັ້ນທ່ານຈຶ່ງສາມາດກັບໄປເບິງຄືນອີກໄດ້ຢ່າງງ່າຍດາຍ.",
     "welcome_label": "ກໍາລັງລະບຸລາຍການເດັ່ນຂອງທ່ານ",
     "time_label_less_than_minute": "<1 ນາທີ",
     "time_label_minute": "{number} ນາທີ",
     "time_label_hour": "{number} ຊົ່ວໂມງ",
-    "time_label_day": "{number} ມື້"
+    "time_label_day": "{number} ມື້",
+    "settings_pane_button_label": "ປັບແຕ່ງຫນ້າແທັບໃຫມ່ຂອງທ່ານ",
+    "settings_pane_header": "ການຕັ້ງຄ່າແທັບໃຫມ່",
+    "settings_pane_body": "ເລືອກສິ່ງທີ່ທ່ານເຫັນເມື່ອທ່ານເປີດແທັບໃຫມ່.",
+    "settings_pane_search_header": "ຊອກຫາ",
+    "settings_pane_search_body": "ຊອກຫາເວັບຈາກແທັບໃຫມ່ຂອງທ່ານ.",
+    "settings_pane_topsites_header": "ເວັບໄຊທ໌ຍອດນິຍົມ",
+    "settings_pane_topsites_body": "ເຂົ້າເວັບໄຊທ໌ທີ່ທ່ານໄດ້ເຂົ້າໄປຫລາຍທີ່ສຸດ.",
+    "settings_pane_topsites_options_showmore": "ສະແດງເປັນສອງແຖວ",
+    "settings_pane_highlights_header": "ຈຸດເດັ່ນ",
+    "settings_pane_highlights_body": "ຍ້ອນຄືນກັບໄປເບິງປະຫວັດການທ່ອງເວັບທີ່ຫາກາເຂົ້າໄປ ແລະ ບຸກມາກທີ່ໄດ້ຮັບການສ້າງຂື້ນມາໃຫມ່ຂອງທ່ານ.",
+    "settings_pane_done_button": "ສຳເລັດ",
+    "edit_topsites_button_text": "ແກ້ໄຂ",
+    "edit_topsites_button_label": "ປັບແຕ່ງພາກສ່ວນເວັບໄຊທ໌ຍອດນິຍົມຂອງທ່ານ",
+    "edit_topsites_showmore_button": "ສະແດງເພີ່ມເຕີມ",
+    "edit_topsites_showless_button": "ສະແດງນ້ອຍລົງ",
+    "edit_topsites_done_button": "ສຳເລັດ",
+    "edit_topsites_pin_button": "Pin ເວັບໄຊທ໌ນີ້",
+    "edit_topsites_edit_button": "ແກ້ໄຂເວັບໄຊທ໌ນີ້",
+    "edit_topsites_dismiss_button": "ຍົກເລີກເວັບໄຊທ໌ນີ້"
   },
   "lt": {
     "newtab_page_title": "Nauja kortelė",
     "default_label_loading": "Įkeliama…",
     "header_top_sites": "Lankomiausios svetainės",
     "header_highlights": "Akcentai",
     "type_label_visited": "Aplankyti",
     "type_label_bookmarked": "Adresyne",
@@ -1832,17 +2083,19 @@
     "edit_topsites_showmore_button": "Rodyti daugiau",
     "edit_topsites_showless_button": "Rodyti mažiau",
     "edit_topsites_done_button": "Atlikta",
     "edit_topsites_pin_button": "Įsegti šią svetainę",
     "edit_topsites_edit_button": "Redaguoti šią svetainę",
     "edit_topsites_dismiss_button": "Paslėpti šią svetainę"
   },
   "ltg": {},
-  "lv": {},
+  "lv": {
+    "newtab_page_title": "Jauna cilne"
+  },
   "mai": {},
   "mk": {},
   "ml": {},
   "mn": {},
   "mr": {},
   "ms": {
     "newtab_page_title": "Tab Baru",
     "default_label_loading": "Memuatkan…",
@@ -1888,17 +2141,66 @@
     "edit_topsites_button_label": "Sesuaikan bahagian Laman Teratas anda",
     "edit_topsites_showmore_button": "Papar selanjutnya",
     "edit_topsites_showless_button": "Papar minima",
     "edit_topsites_done_button": "Siap",
     "edit_topsites_pin_button": "Pin laman ini",
     "edit_topsites_edit_button": "Edit laman ini",
     "edit_topsites_dismiss_button": "Buang laman ini"
   },
-  "my": {},
+  "my": {
+    "newtab_page_title": "တပ်ဗ်အသစ်ဖွင့်",
+    "default_label_loading": "ရယူနေသှ်…",
+    "header_top_sites": "အများဆုံးသုံးဆိုက်များ",
+    "header_highlights": "အသားပေးဖော်ပြချက်များ",
+    "type_label_visited": "သွားလည်ခဲ့သော",
+    "type_label_bookmarked": "စာအမှတ်မှတ်ထားသော",
+    "type_label_synced": "အခြားပစ္စည်းတစ်ခုမှရယူထားသှ်",
+    "type_label_open": "ဖွင့်ပါ",
+    "type_label_topic": "အကြောင်းအရာ",
+    "menu_action_bookmark": "စာအမှတ်",
+    "menu_action_remove_bookmark": "စာအမှတ်အားဖယ်ပါ",
+    "menu_action_copy_address": "လိပ်စာအားကူးယူပါ",
+    "menu_action_email_link": "လင်ခ့်အားအီးမေလ်းဖြင့်ပို့ပါ…",
+    "menu_action_open_new_window": "အခြားဝင်းဒိုးတစ်ခုမှဖွင့်ပါ",
+    "menu_action_open_private_window": "အခြားတစ်ကိုယ်ရေသုံးဝင်းဒိုးတစ်ခုဖွင့်ပါ",
+    "menu_action_dismiss": "ပိတ်လိုက်ပါ",
+    "menu_action_delete": "မှတ်တမ်းမှ ဖျက်ပါ",
+    "search_for_something_with": "{search_term} အားရှာပါ -",
+    "search_button": "ရှာ",
+    "search_header": "{search_engine_name} ရှာဖွေမှု",
+    "search_web_placeholder": "ဝတ်ဘ်ပေါ်တွင် ရှာဖွေခြင်း",
+    "search_settings": "ရှာဖွေမှုအပြင်အဆင်အားပြောင်းလဲပါ",
+    "welcome_title": "တပ်ဗ်အသစ်တစ်ခုမှကြိုဆိုပါတယ်",
+    "welcome_body": "ယခုနေရာအား Firefox မှ အသင့်လျော်ဆုံး သင်သွားလည်ခဲ့ဖူးသော စာအမှတ်များ၊ ဆောင်းပါးများ၊ ရုပ်ရှင်များ နှင့် စာမျက်နှာများအား ပြသဖို့အသုံးပြုမည်ဖြစ်ပါတယ်။",
+    "welcome_label": "သင့် အသားပေးဖော်ပြချက်များကိုသတိထားမည်",
+    "time_label_less_than_minute": "<1မီတာ",
+    "time_label_minute": "{number}မီတာ",
+    "time_label_hour": "{number}အမြင့်",
+    "time_label_day": "{number}နေ့",
+    "settings_pane_button_label": "သင့်တပ်ဗ်အသစ်စာမျက်နှာအား ပြင်ဆင်မည်",
+    "settings_pane_header": "စာတပ်ဗ်အသစ်အပြင်အဆင်များ",
+    "settings_pane_body": "သင် တပ်ဗ်အသစ်ဖွင့်လိုက်ပါကမြင်ရမည့်အရာများကိုရွေးချယ်ပါ",
+    "settings_pane_search_header": "ရှာဖွေပါ",
+    "settings_pane_search_body": "ဝက်ဘ်ပေါ်တွင် သင့်တပ်ဗ်အသစ်မှရှာဖွေပါ",
+    "settings_pane_topsites_header": "ထိပ်တန်းဝတ်ဘ်ဆိုက်များ",
+    "settings_pane_topsites_body": "သင်အများဆုံးသွားလည်သော ဝတ်ဘ်ဆိုက်များကို ရယူပါ",
+    "settings_pane_topsites_options_showmore": "အတန်းနှစ်တန်းနှင့်ပြပါ",
+    "settings_pane_highlights_header": "အသားပေးဖော်ပြချက်များ",
+    "settings_pane_highlights_body": "သင်လတ်တလောသွားလည်ထားသော မှတ်တမ်းနှင့် အသစ်ဖန်တီးထားသော စာအမှတ်များအား ပြန်ကြည့်ပါ",
+    "settings_pane_done_button": "ပြီးပြီ",
+    "edit_topsites_button_text": "ပြင်ဆင်မည်",
+    "edit_topsites_button_label": "သင့်ထိပ်တန်းဆိုက် အမြင်အားပြင်ဆင်ပါ",
+    "edit_topsites_showmore_button": "ထပ်ပြပါ",
+    "edit_topsites_showless_button": "ချုံ့ပြရန်",
+    "edit_topsites_done_button": "ပြီးပြီ",
+    "edit_topsites_pin_button": "ဝတ်ဆိုဒ်အားpinလုပ်ထားမည်",
+    "edit_topsites_edit_button": "ဆိုက်အားပြင်မည်",
+    "edit_topsites_dismiss_button": "ဆိုက်အားဖျက်လိုက်မည်"
+  },
   "nb-NO": {
     "newtab_page_title": "Ny fane",
     "default_label_loading": "Laster …",
     "header_top_sites": "Mest besøkte nettsider",
     "header_highlights": "Høydepunkter",
     "type_label_visited": "Besøkt",
     "type_label_bookmarked": "Bokmerket",
     "type_label_synced": "Synkronisert fra annen enhet",
@@ -1908,26 +2210,46 @@
     "menu_action_remove_bookmark": "Fjern bokmerke",
     "menu_action_copy_address": "Kopier adresse",
     "menu_action_email_link": "Send lenke på e-post …",
     "menu_action_open_new_window": "Åpne i nytt vindu",
     "menu_action_open_private_window": "Åpne i nytt privat vindu",
     "menu_action_dismiss": "Avslå",
     "menu_action_delete": "Slett fra historikk",
     "search_for_something_with": "Søk etter {search_term} med:",
+    "search_button": "Søk",
     "search_header": "{search_engine_name}-søk",
     "search_web_placeholder": "Søk på nettet",
     "search_settings": "Endre søkeinnstillinger",
     "welcome_title": "Velkommen til ny fane",
     "welcome_body": "Firefox vil bruke denne plassen til å vise deg de mest relevante bokmerkene, artiklene, videoene og sidene du nettopp har besøkt, slik at du enkelt kan finne tilbake til de.",
     "welcome_label": "Identifiserer dine høydepunkter",
     "time_label_less_than_minute": "<1 m",
     "time_label_minute": "{number} m",
     "time_label_hour": "{number} t",
-    "time_label_day": "{number} d"
+    "time_label_day": "{number} d",
+    "settings_pane_button_label": "Tilpass siden for Ny fane",
+    "settings_pane_header": "Innstillinger for Ny fane",
+    "settings_pane_body": "Velg hva som vises når du åpner en ny fane.",
+    "settings_pane_search_header": "Søk",
+    "settings_pane_search_body": "Søk på nettet fra din nye fane.",
+    "settings_pane_topsites_header": "Mest besøkte",
+    "settings_pane_topsites_body": "Tilgang til nettsidene du besøker mest.",
+    "settings_pane_topsites_options_showmore": "Vis to rader",
+    "settings_pane_highlights_header": "Høydepunkter",
+    "settings_pane_highlights_body": "Se tilbake på din siste nettleserhistorikk og nyopprettede bokmerker.",
+    "settings_pane_done_button": "Ferdig",
+    "edit_topsites_button_text": "Rediger",
+    "edit_topsites_button_label": "Tilpass seksjonen Mest besøkte",
+    "edit_topsites_showmore_button": "Vis mer",
+    "edit_topsites_showless_button": "Vis mindre",
+    "edit_topsites_done_button": "Ferdig",
+    "edit_topsites_pin_button": "Fest nettsiden",
+    "edit_topsites_edit_button": "Rediger denne nettsiden",
+    "edit_topsites_dismiss_button": "Avvis denne nettsiden"
   },
   "ne-NP": {
     "newtab_page_title": "नयाँ ट्याब",
     "default_label_loading": "लोड हुदैँछ...",
     "header_top_sites": "शीर्ष साइटहरु",
     "header_highlights": "विशेषताहरू",
     "type_label_visited": "भ्रमण गरिएको",
     "type_label_bookmarked": "पुस्तकचिनो लागाइएको",
@@ -2155,21 +2477,21 @@
     "edit_topsites_dismiss_button": "Odrzuć tę stronę"
   },
   "pt-BR": {
     "newtab_page_title": "Nova aba",
     "default_label_loading": "Carregando…",
     "header_top_sites": "Sites preferidos",
     "header_highlights": "Destaques",
     "type_label_visited": "Visitado",
-    "type_label_bookmarked": "Favorito",
+    "type_label_bookmarked": "Adicionado aos favoritos",
     "type_label_synced": "Sincronizado a partir de outro dispositivo",
     "type_label_open": "Abrir",
     "type_label_topic": "Tópico",
-    "menu_action_bookmark": "Favoritos",
+    "menu_action_bookmark": "Adicionar aos favoritos",
     "menu_action_remove_bookmark": "Remover favorito",
     "menu_action_copy_address": "Copiar endereço",
     "menu_action_email_link": "Enviar link por e-mail…",
     "menu_action_open_new_window": "Abrir em uma nova janela",
     "menu_action_open_private_window": "Abrir em uma nova janela privativa",
     "menu_action_dismiss": "Dispensar",
     "menu_action_delete": "Excluir do histórico",
     "search_for_something_with": "Pesquisar por {search_term} com:",
@@ -2500,26 +2822,46 @@
     "menu_action_remove_bookmark": "Hiqe Faqerojtësin",
     "menu_action_copy_address": "Kopjoje Adresën",
     "menu_action_email_link": "Dërgoni Lidhje me Email…",
     "menu_action_open_new_window": "Hape në Dritare të Re",
     "menu_action_open_private_window": "Hape në Dritare të Re Private",
     "menu_action_dismiss": "Hidhe tej",
     "menu_action_delete": "Fshije prej Historiku",
     "search_for_something_with": "Kërko për {search_term} me:",
+    "search_button": "Kërko",
     "search_header": "Kërkim me {search_engine_name}",
     "search_web_placeholder": "Kërkoni në Web",
     "search_settings": "Ndryshoji Rregullimet e Kërkimit",
     "welcome_title": "Mirë se vini te skedë e re",
     "welcome_body": "Firefox-i do ta përdorë këtë hapësirë për t’ju shfaqur faqerojtësit, artikujt, videot dhe faqet më me peshë që keni vizituar së fundi, që kështu të mund të ktheheni lehtë në to.",
     "welcome_label": "Po identifikohen Highlights tuaj",
     "time_label_less_than_minute": "<1m",
     "time_label_minute": "{number}m",
     "time_label_hour": "{number}h",
-    "time_label_day": "{number}d"
+    "time_label_day": "{number}d",
+    "settings_pane_button_label": "Personalizoni faqen tuaj Skedë e Re",
+    "settings_pane_header": "Parapëlqime për Skedë të Re",
+    "settings_pane_body": "Zgjidhni ç’doni të shihni kur hapni një skedë të re.",
+    "settings_pane_search_header": "Kërko",
+    "settings_pane_search_body": "Kërkoni në Web prej skedës tuaj të re.",
+    "settings_pane_topsites_header": "Sajte Kryesues",
+    "settings_pane_topsites_body": "Hyni te sajtet që vizitoni më shpesh.",
+    "settings_pane_topsites_options_showmore": "Shfaq dy rreshta",
+    "settings_pane_highlights_header": "Në Pah",
+    "settings_pane_highlights_body": "Rikthejuni historikut të shfletimeve të fundit dhe faqerojtësve të krijuar rishtas.",
+    "settings_pane_done_button": "U bë",
+    "edit_topsites_button_text": "Përpunoni",
+    "edit_topsites_button_label": "Personalizoni ndarjen tuaj Sajte Kryesues",
+    "edit_topsites_showmore_button": "Shfaq më tepër",
+    "edit_topsites_showless_button": "Shfaq më pak",
+    "edit_topsites_done_button": "U bë",
+    "edit_topsites_pin_button": "Fiksoje këtë sajt",
+    "edit_topsites_edit_button": "Përpunoni këtë sajt",
+    "edit_topsites_dismiss_button": "Hidhe tej këtë sajt"
   },
   "sr": {
     "newtab_page_title": "Нови језичак",
     "default_label_loading": "Учитавање…",
     "header_top_sites": "Популарни сајтови",
     "header_highlights": "Истакнути",
     "type_label_visited": "Посећене",
     "type_label_bookmarked": "Забележено",
@@ -2632,38 +2974,58 @@
     "menu_action_remove_bookmark": "ఇష్టాంశాన్ని తొలగించు",
     "menu_action_copy_address": "చిరునామా కాపీ చెయ్యండి",
     "menu_action_email_link": "ఈమెయిలు లింకు…",
     "menu_action_open_new_window": "కొత్త విండోలో తెరువు",
     "menu_action_open_private_window": "కొత్త వ్యక్తిగత విండోలో తెరువు",
     "menu_action_dismiss": "విస్మరించు",
     "menu_action_delete": "చరిత్ర నుంచి తీసివేయి",
     "search_for_something_with": "{search_term} కోసం దీని సాయంతో వెతుకు:",
+    "search_button": "వెతకండి",
     "search_header": "{search_engine_name} శోధన",
-    "search_web_placeholder": "వెబ్ లో వెతకండి",
+    "search_web_placeholder": "జాలంలో వెతకండి",
     "search_settings": "శోధన అమరికలు మార్చు",
     "welcome_title": "కొత్త ట్యాబుకు స్వాగతం",
     "welcome_body": "సముచితమైన మీ ఇష్టాంశాలను, వ్యాసాలను, వీడియోలను, ఇంకా మీరు ఇటీవలే చూసిన పేజీలను మీకు తేలిగ్గా అందుబాటులో ఉంచేందుకు Firefox ఈ జాగాని వాడుకుంటుంది.",
     "welcome_label": "మీ ముఖ్యాంశాలను గుర్తిస్తున్నది",
     "time_label_less_than_minute": "<1ని",
     "time_label_minute": "{number}ని",
     "time_label_hour": "{number}గం",
-    "time_label_day": "{number}రో"
+    "time_label_day": "{number}రో",
+    "settings_pane_button_label": "మీ కొత్త ట్యాబు పేజీని మలచుకోండి",
+    "settings_pane_header": "కొత్త ట్యాబు అభిరుచులు",
+    "settings_pane_body": "మీరు కొత్త ట్యాబు తెరిచినప్పుడు ఏం చూడాలో ఎంచుకోండి.",
+    "settings_pane_search_header": "వెతకడం",
+    "settings_pane_search_body": "కొత్త ట్యాబు నుండే జాలంలో వెతకండి.",
+    "settings_pane_topsites_header": "మేటి సైట్లు",
+    "settings_pane_topsites_body": "మీరు ఎక్కువగా చూసే వెబ్‌సైట్లను చూడండి.",
+    "settings_pane_topsites_options_showmore": "రెండు వరుసలు చూపించు",
+    "settings_pane_highlights_header": "విశేషాలు",
+    "settings_pane_highlights_body": "మీ ఇటీవలి విహరణ చరిత్రనూ కొత్త ఇష్టాంశాలను చూడండి.",
+    "settings_pane_done_button": "పూర్తయింది",
+    "edit_topsites_button_text": "మార్చు",
+    "edit_topsites_button_label": "మీ మేటి సైట్ల విభాగాన్ని మలచుకోండి",
+    "edit_topsites_showmore_button": "ఇంకా చూపించు",
+    "edit_topsites_showless_button": "కొన్నే చూపించు",
+    "edit_topsites_done_button": "పూర్తయింది",
+    "edit_topsites_pin_button": "ఈ సైటును ఇక్కడ గుచ్చు",
+    "edit_topsites_edit_button": "ఈ సైటును మార్చు",
+    "edit_topsites_dismiss_button": "ఈ సైటుని తీసివేయి"
   },
   "th": {
     "newtab_page_title": "แท็บใหม่",
     "default_label_loading": "กำลังโหลด…",
     "header_top_sites": "ไซต์เด่น",
     "header_highlights": "รายการเด่น",
     "type_label_visited": "เยี่ยมชมแล้ว",
-    "type_label_bookmarked": "คั่นหน้าแล้ว",
+    "type_label_bookmarked": "มีที่คั่นหน้าแล้ว",
     "type_label_synced": "ซิงค์จากอุปกรณ์อื่น",
     "type_label_open": "เปิด",
     "type_label_topic": "หัวข้อ",
-    "menu_action_bookmark": "ที่คั่นหน้า",
+    "menu_action_bookmark": "เพิ่มที่คั่นหน้า",
     "menu_action_remove_bookmark": "เอาที่คั่นหน้าออก",
     "menu_action_copy_address": "คัดลอกที่อยู่",
     "menu_action_email_link": "ส่งอีเมลลิงก์…",
     "menu_action_open_new_window": "เปิดในหน้าต่างใหม่",
     "menu_action_open_private_window": "เปิดในหน้าต่างส่วนตัวใหม่",
     "menu_action_dismiss": "ยกเลิก",
     "menu_action_delete": "ลบออกจากประวัติ",
     "search_for_something_with": "ค้นหาสำหรับ {search_term} ด้วย:",
@@ -2682,16 +3044,17 @@
     "settings_pane_header": "ตั้งค่าแท็บใหม่",
     "settings_pane_body": "เลือกสิ่งที่คุณเห็นเมื่อคุณเปิดแท็บใหม่",
     "settings_pane_search_header": "ค้นหา",
     "settings_pane_search_body": "ค้นหาเว็บจากแท็บใหม่ของคุณ",
     "settings_pane_topsites_header": "ไซต์เด่น",
     "settings_pane_topsites_body": "เข้าถึงเว็บไซต์ที่คุณเยี่ยมชมมากที่สุด",
     "settings_pane_topsites_options_showmore": "แสดงสองแถว",
     "settings_pane_highlights_header": "รายการเด่น",
+    "settings_pane_highlights_body": "มองย้อนกลับมาดูประวัติการท่องเว็บเมื่อเร็ว ๆ นี้และที่คั่นหน้าที่สร้างใหม่ของคุณ",
     "settings_pane_done_button": "เสร็จสิ้น",
     "edit_topsites_button_text": "แก้ไข",
     "edit_topsites_button_label": "ปรับแต่งส่วนไซต์เด่นของคุณ",
     "edit_topsites_showmore_button": "แสดงเพิ่มเติม",
     "edit_topsites_showless_button": "แสดงน้อยลง",
     "edit_topsites_done_button": "เสร็จสิ้น",
     "edit_topsites_pin_button": "ปักหมุดไซต์นี้",
     "edit_topsites_edit_button": "แก้ไขไซต์นี้",
@@ -2873,16 +3236,17 @@
     "welcome_title": "نئے ٹیب میں خوش آمدید",
     "welcome_body": "اس جگہ کا استعمال کرنے ہوئے Firefox آپکی متعلقہ نشانیاں، عبارات، وڈیوز اور صفحات جن کا حال ہی میں ص آُپ نے دورہ کیا ہے دکھائے گا۔ تاکہ آپ ان تک واپس آسانی سے پہنچ سکیں۔",
     "welcome_label": "آپکی جھلکیوں کی نشاندہی کر رہا ہے",
     "time_label_less_than_minute": "<1m",
     "time_label_minute": "{number}m",
     "time_label_hour": "{number}h",
     "time_label_day": "{number}d",
     "settings_pane_button_label": "اپنے نئے ٹیب کہ صفحہ کی تخصیص کریں",
+    "settings_pane_header": "نئے َٹیب کی ترجیحات",
     "settings_pane_search_header": "تلاش",
     "settings_pane_search_body": "اپنے نئے ٹیب سے وہب پر تلاش کریں۔",
     "settings_pane_topsites_header": "بہترین سائٹیں",
     "settings_pane_topsites_options_showmore": "دو قطاریں دکھائیں",
     "settings_pane_highlights_header": "شہ سرخياں",
     "settings_pane_done_button": "ہوگیا",
     "edit_topsites_button_text": "تدوین",
     "edit_topsites_done_button": "ہوگیا",
--- a/browser/extensions/activity-stream/lib/ActivityStream.jsm
+++ b/browser/extensions/activity-stream/lib/ActivityStream.jsm
@@ -1,78 +1,136 @@
 /* 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/. */
-/* globals LocalizationFeed, NewTabInit, SearchFeed, TelemetryFeed, TopSitesFeed, XPCOMUtils */
 "use strict";
 
 const {utils: Cu} = Components;
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 const {Store} = Cu.import("resource://activity-stream/lib/Store.jsm", {});
 const {actionTypes: at} = Cu.import("resource://activity-stream/common/Actions.jsm", {});
 
+const REASON_ADDON_UNINSTALL = 6;
+
+XPCOMUtils.defineLazyModuleGetter(this, "DefaultPrefs",
+  "resource://activity-stream/lib/ActivityStreamPrefs.jsm");
+
 // Feeds
 XPCOMUtils.defineLazyModuleGetter(this, "LocalizationFeed",
   "resource://activity-stream/lib/LocalizationFeed.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "NewTabInit",
   "resource://activity-stream/lib/NewTabInit.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "PlacesFeed",
   "resource://activity-stream/lib/PlacesFeed.jsm");
 
-XPCOMUtils.defineLazyModuleGetter(this, "SearchFeed",
-  "resource://activity-stream/lib/SearchFeed.jsm");
-
 XPCOMUtils.defineLazyModuleGetter(this, "TelemetryFeed",
   "resource://activity-stream/lib/TelemetryFeed.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "TopSitesFeed",
   "resource://activity-stream/lib/TopSitesFeed.jsm");
 
-const feeds = {
-  // When you add a feed here:
-  // 1. The key in this object should directly refer to a pref, not including the
-  //    prefix (so "feeds.newtabinit" refers to the
-  //    "browser.newtabpage.activity-stream.feeds.newtabinit" pref)
-  // 2. The value should be a function that returns a feed.
+const PREFS_CONFIG = [
+  // When you add a feed pref here:
+  // 1. The pref should be prefixed with "feeds."
+  // 2. The init property should be a function that instantiates your Feed
   // 3. You should use XPCOMUtils.defineLazyModuleGetter to import the Feed,
   //    so it isn't loaded until the feed is enabled.
-  "feeds.localization": () => new LocalizationFeed(),
-  "feeds.newtabinit": () => new NewTabInit(),
-  "feeds.places": () => new PlacesFeed(),
-  "feeds.search": () => new SearchFeed(),
-  "feeds.telemetry": () => new TelemetryFeed(),
-  "feeds.topsites": () => new TopSitesFeed()
-};
+  {
+    name: "feeds.localization",
+    title: "Initialize strings and detect locale for Activity Stream",
+    value: true,
+    init: () => new LocalizationFeed()
+  },
+  {
+    name: "feeds.newtabinit",
+    title: "Sends a copy of the state to each new tab that is opened",
+    value: true,
+    init: () => new NewTabInit()
+  },
+  {
+    name: "feeds.places",
+    title: "Listens for and relays various Places-related events",
+    value: true,
+    init: () => new PlacesFeed()
+  },
+  {
+    name: "feeds.telemetry",
+    title: "Relays telemetry-related actions to TelemetrySender",
+    value: true,
+    init: () => new TelemetryFeed()
+  },
+  {
+    name: "feeds.topsites",
+    title: "Queries places and gets metadata for Top Sites section",
+    value: true,
+    init: () => new TopSitesFeed()
+  },
+  // End feeds
+
+  {
+    name: "telemetry",
+    title: "Enable system error and usage data collection",
+    value: false
+  },
+  {
+    name: "telemetry.log",
+    title: "Log telemetry events in the console",
+    value: false
+  },
+  {
+    name: "telemetry.ping.endpoint",
+    title: "Telemetry server endpoint",
+    value: "https://tiles.services.mozilla.com/v3/links/activity-stream"
+  }
+];
+
+const feeds = {};
+for (const pref of PREFS_CONFIG) {
+  if (pref.name.match(/^feeds\./)) {
+    feeds[pref.name] = pref.init;
+  }
+}
 
 this.ActivityStream = class ActivityStream {
 
   /**
    * constructor - Initializes an instance of ActivityStream
    *
    * @param  {object} options Options for the ActivityStream instance
    * @param  {string} options.id Add-on ID. e.g. "activity-stream@mozilla.org".
    * @param  {string} options.version Version of the add-on. e.g. "0.1.0"
    * @param  {string} options.newTabURL URL of New Tab page on which A.S. is displayed. e.g. "about:newtab"
    */
   constructor(options = {}) {
     this.initialized = false;
     this.options = options;
     this.store = new Store();
     this.feeds = feeds;
+    this._defaultPrefs = new DefaultPrefs(PREFS_CONFIG);
   }
   init() {
     this.initialized = true;
+    this._defaultPrefs.init();
     this.store.init(this.feeds);
     this.store.dispatch({
       type: at.INIT,
       data: {version: this.options.version}
     });
   }
   uninit() {
     this.store.dispatch({type: at.UNINIT});
     this.store.uninit();
+
     this.initialized = false;
   }
+  uninstall(reason) {
+    if (reason === REASON_ADDON_UNINSTALL) {
+      // This resets all prefs in the config to their default values,
+      // so we DON'T want to do this on an upgrade/downgrade, only on a
+      // real uninstall
+      this._defaultPrefs.reset();
+    }
+  }
 };
 
 this.EXPORTED_SYMBOLS = ["ActivityStream"];
--- a/browser/extensions/activity-stream/lib/ActivityStreamMessageChannel.jsm
+++ b/browser/extensions/activity-stream/lib/ActivityStreamMessageChannel.jsm
@@ -1,12 +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/. */
-/* globals AboutNewTab, RemotePages, XPCOMUtils */
 
 "use strict";
 
 const {utils: Cu} = Components;
 
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 
 const {
new file mode 100644
--- /dev/null
+++ b/browser/extensions/activity-stream/lib/ActivityStreamPrefs.jsm
@@ -0,0 +1,82 @@
+/* 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 {utils: Cu} = Components;
+Cu.import("resource://gre/modules/Preferences.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+
+const ACTIVITY_STREAM_PREF_BRANCH = "browser.newtabpage.activity-stream.";
+
+this.Prefs = class Prefs extends Preferences {
+
+  /**
+   * Prefs - A wrapper around Preferences that always sets the branch to
+   *         ACTIVITY_STREAM_PREF_BRANCH
+   */
+  constructor(branch = ACTIVITY_STREAM_PREF_BRANCH) {
+    super({branch});
+    this._branchName = branch;
+  }
+  get branchName() {
+    return this._branchName;
+  }
+};
+
+this.DefaultPrefs = class DefaultPrefs {
+
+  /**
+   * DefaultPrefs - A helper for setting and resetting default prefs for the add-on
+   *
+   * @param  {Array} config An array of configuration objects with the following properties:
+   *         {string} .name The name of the pref
+   *         {string} .title (optional) A description of the pref
+   *         {bool|string|number} .value The default value for the pref
+   * @param  {string} branch (optional) The pref branch (defaults to ACTIVITY_STREAM_PREF_BRANCH)
+   */
+  constructor(config, branch = ACTIVITY_STREAM_PREF_BRANCH) {
+    this._config = config;
+    this.branch = Services.prefs.getDefaultBranch(branch);
+  }
+
+  /**
+   * _setDefaultPref - Sets the default value (not user-defined) for a given pref
+   *
+   * @param  {string} key The name of the pref
+   * @param  {type} val The default value of the pref
+   */
+  _setDefaultPref(key, val) {
+    switch (typeof val) {
+      case "boolean":
+        this.branch.setBoolPref(key, val);
+        break;
+      case "number":
+        this.branch.setIntPref(key, val);
+        break;
+      case "string":
+        this.branch.setStringPref(key, val);
+        break;
+    }
+  }
+
+  /**
+   * init - Set default prefs for all prefs in the config
+   */
+  init() {
+    for (const pref of this._config) {
+      this._setDefaultPref(pref.name, pref.value);
+    }
+  }
+
+  /**
+   * reset - Resets all user-defined prefs for prefs in ._config to their defaults
+   */
+  reset() {
+    for (const pref of this._config) {
+      this.branch.clearUserPref(pref.name);
+    }
+  }
+};
+
+this.EXPORTED_SYMBOLS = ["DefaultPrefs", "Prefs"];
--- a/browser/extensions/activity-stream/lib/LocalizationFeed.jsm
+++ b/browser/extensions/activity-stream/lib/LocalizationFeed.jsm
@@ -1,12 +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/. */
- /* globals Services, XPCOMUtils */
 "use strict";
 
 const {utils: Cu} = Components;
 const {actionTypes: at, actionCreators: ac} = Cu.import("resource://activity-stream/common/Actions.jsm", {});
 
 Cu.importGlobalProperties(["fetch"]);
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 
--- a/browser/extensions/activity-stream/lib/PlacesFeed.jsm
+++ b/browser/extensions/activity-stream/lib/PlacesFeed.jsm
@@ -1,12 +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/. */
- /* globals ContentSearch, XPCOMUtils, PlacesUtils, NewTabUtils, Services */
 "use strict";
 
 const {utils: Cu, interfaces: Ci} = Components;
 const {actionTypes: at, actionCreators: ac} = Cu.import("resource://activity-stream/common/Actions.jsm", {});
 
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "NewTabUtils",
deleted file mode 100644
--- a/browser/extensions/activity-stream/lib/SearchFeed.jsm
+++ /dev/null
@@ -1,77 +0,0 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
- /* globals ContentSearch, XPCOMUtils, Services */
-"use strict";
-
-const {utils: Cu} = Components;
-const {actionTypes: at, actionCreators: ac} = Cu.import("resource://activity-stream/common/Actions.jsm", {});
-const SEARCH_ENGINE_TOPIC = "browser-search-engine-modified";
-
-Cu.import("resource://gre/modules/XPCOMUtils.jsm");
-
-XPCOMUtils.defineLazyModuleGetter(this, "ContentSearch",
-  "resource:///modules/ContentSearch.jsm");
-XPCOMUtils.defineLazyModuleGetter(this, "Services",
-  "resource://gre/modules/Services.jsm");
-
-this.SearchFeed = class SearchFeed {
-  addObservers() {
-    Services.obs.addObserver(this, SEARCH_ENGINE_TOPIC);
-
-    // Notice when ContentSearch.init would be lazily loaded from nsBrowserGlue
-    this.contentSearch = new Promise(resolve => Services.mm.addMessageListener(
-      "ContentSearch", (this._onMessage = () => {
-        Services.mm.removeMessageListener("ContentSearch", this._onMessage);
-        resolve(ContentSearch);
-      })));
-  }
-  removeObservers() {
-    Services.obs.removeObserver(this, SEARCH_ENGINE_TOPIC);
-    Services.mm.removeMessageListener("ContentSearch", this._onMessage);
-  }
-
-  observe(subject, topic, data) {
-    switch (topic) {
-      case SEARCH_ENGINE_TOPIC:
-        if (data !== "engine-default") {
-          this.getState();
-        }
-        break;
-    }
-  }
-
-  async getState() {
-    // Wait for ContentSearch to be lazily loaded before getting state
-    const state = await (await this.contentSearch).currentStateObj(true);
-    const engines = state.engines.map(engine => ({
-      name: engine.name,
-      icon: engine.iconBuffer
-    }));
-    const currentEngine = {
-      name: state.currentEngine.name,
-      icon: state.currentEngine.iconBuffer
-    };
-    const action = {type: at.SEARCH_STATE_UPDATED, data: {engines, currentEngine}};
-    this.store.dispatch(ac.BroadcastToContent(action));
-  }
-  performSearch(browser, data) {
-    ContentSearch.performSearch({target: browser}, data);
-  }
-
-  async onAction(action) {
-    switch (action.type) {
-      case at.INIT:
-        this.addObservers();
-        await this.getState();
-        break;
-      case at.PERFORM_SEARCH:
-        this.performSearch(action._target.browser, action.data);
-        break;
-      case at.UNINIT:
-        this.removeObservers();
-        break;
-    }
-  }
-};
-this.EXPORTED_SYMBOLS = ["SearchFeed"];
--- a/browser/extensions/activity-stream/lib/Store.jsm
+++ b/browser/extensions/activity-stream/lib/Store.jsm
@@ -1,22 +1,22 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-/* global Preferences */
 "use strict";
 
 const {utils: Cu} = Components;
 
 const {redux} = Cu.import("resource://activity-stream/vendor/Redux.jsm", {});
 const {reducers} = Cu.import("resource://activity-stream/common/Reducers.jsm", {});
 const {ActivityStreamMessageChannel} = Cu.import("resource://activity-stream/lib/ActivityStreamMessageChannel.jsm", {});
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 
-const PREF_PREFIX = "browser.newtabpage.activity-stream.";
-Cu.import("resource://gre/modules/Preferences.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Prefs",
+  "resource://activity-stream/lib/ActivityStreamPrefs.jsm");
 
 /**
  * Store - This has a similar structure to a redux store, but includes some extra
  *         functionality to allow for routing of actions between the Main processes
  *         and child processes via a ActivityStreamMessageChannel.
  *         It also accepts an array of "Feeds" on inititalization, which
  *         can listen for any action that is dispatched through the store.
  */
@@ -32,16 +32,17 @@ this.Store = class Store {
     // store.dispatch() will call store._store.dispatch();
     ["dispatch", "getState", "subscribe"].forEach(method => {
       this[method] = function(...args) {
         return this._store[method](...args);
       }.bind(this);
     });
     this.feeds = new Map();
     this._feedFactories = null;
+    this._prefs = new Prefs();
     this._prefHandlers = new Map();
     this._messageChannel = new ActivityStreamMessageChannel({dispatch: this.dispatch});
     this._store = redux.createStore(
       redux.combineReducers(reducers),
       redux.applyMiddleware(this._middleware, this._messageChannel.middleware)
     );
   }
 
@@ -89,62 +90,56 @@ this.Store = class Store {
   /**
    * maybeStartFeedAndListenForPrefChanges - Listen for pref changes that turn a
    *     feed off/on, and as long as that pref was not explicitly set to
    *     false, initialize the feed immediately.
    *
    * @param  {string} name The name of a feed, as defined in the object passed
    *                       to Store.init
    */
-  maybeStartFeedAndListenForPrefChanges(name) {
-    const prefName = PREF_PREFIX + name;
-
-    // If the pref was never set, set it to true by default.
-    if (!Preferences.has(prefName)) {
-      Preferences.set(prefName, true);
-    }
-
+  maybeStartFeedAndListenForPrefChanges(prefName) {
     // Create a listener that turns the feed off/on based on changes
     // to the pref, and cache it so we can unlisten on shut-down.
-    const onPrefChanged = isEnabled => (isEnabled ? this.initFeed(name) : this.uninitFeed(name));
+    const onPrefChanged = isEnabled => (isEnabled ? this.initFeed(prefName) : this.uninitFeed(prefName));
     this._prefHandlers.set(prefName, onPrefChanged);
-    Preferences.observe(prefName, onPrefChanged);
+    this._prefs.observe(prefName, onPrefChanged);
 
     // TODO: This should propbably be done in a generic pref manager for Activity Stream.
     // If the pref is true, start the feed immediately.
-    if (Preferences.get(prefName)) {
-      this.initFeed(name);
+    if (this._prefs.get(prefName)) {
+      this.initFeed(prefName);
     }
   }
 
   /**
    * init - Initializes the ActivityStreamMessageChannel channel, and adds feeds.
    *
-   * @param  {array} feeds An array of objects with an optional .onAction method
+   * @param  {array} feedConstructors An array of configuration objects for feeds
+   *                 each with .name (the name of the pref for the feed) and .init,
+   *                 a function that returns an instance of the feed
    */
   init(feedConstructors) {
     if (feedConstructors) {
       this._feedFactories = feedConstructors;
-      for (const name of Object.keys(feedConstructors)) {
-        this.maybeStartFeedAndListenForPrefChanges(name);
+      for (const pref of Object.keys(feedConstructors)) {
+        this.maybeStartFeedAndListenForPrefChanges(pref);
       }
     }
     this._messageChannel.createChannel();
   }
 
   /**
    * uninit -  Uninitalizes each feed, clears them, and destroys the message
    *           manager channel.
    *
    * @return {type}  description
    */
   uninit() {
     this.feeds.forEach(feed => this.uninitFeed(feed));
-    this._prefHandlers.forEach((handler, pref) => Preferences.ignore(pref, handler));
+    this._prefHandlers.forEach((handler, pref) => this._prefs.ignore(pref, handler));
     this._prefHandlers.clear();
     this._feedFactories = null;
     this.feeds.clear();
     this._messageChannel.destroyChannel();
   }
 };
 
-this.PREF_PREFIX = PREF_PREFIX;
-this.EXPORTED_SYMBOLS = ["Store", "PREF_PREFIX"];
+this.EXPORTED_SYMBOLS = ["Store"];
--- a/browser/extensions/activity-stream/lib/TelemetryFeed.jsm
+++ b/browser/extensions/activity-stream/lib/TelemetryFeed.jsm
@@ -1,12 +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/. */
-/* globals XPCOMUtils, gUUIDGenerator, ClientID */
 
 "use strict";
 
 const {utils: Cu} = Components;
 const {actionTypes: at, actionUtils: au} = Cu.import("resource://activity-stream/common/Actions.jsm", {});
 
 Cu.import("resource://gre/modules/ClientID.jsm");
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
--- a/browser/extensions/activity-stream/lib/TelemetrySender.jsm
+++ b/browser/extensions/activity-stream/lib/TelemetrySender.jsm
@@ -1,12 +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/. */
-/* globals Preferences, Services, XPCOMUtils */
 
 const {interfaces: Ci, utils: Cu} = Components;
 
 Cu.import("resource://gre/modules/Preferences.jsm");
 Cu.importGlobalProperties(["fetch"]);
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/Console.jsm"); // eslint-disable-line no-console
--- a/browser/extensions/activity-stream/lib/TopSitesFeed.jsm
+++ b/browser/extensions/activity-stream/lib/TopSitesFeed.jsm
@@ -1,12 +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/. */
- /* globals NewTabUtils, PreviewProvider */
 "use strict";
 
 const {utils: Cu} = Components;
 const {actionTypes: at, actionCreators: ac} = Cu.import("resource://activity-stream/common/Actions.jsm", {});
 
 Cu.import("resource://gre/modules/NewTabUtils.jsm");
 Cu.import("resource:///modules/PreviewProvider.jsm");
 
--- a/browser/extensions/activity-stream/test/.eslintrc.js
+++ b/browser/extensions/activity-stream/test/.eslintrc.js
@@ -1,11 +1,12 @@
 module.exports = {
   "env": {
     "node": true,
     "es6": true,
     "mocha": true
   },
   "globals": {
     "assert": true,
-    "sinon": true
+    "sinon": true,
+    "chai": true
   }
 };
--- a/browser/extensions/activity-stream/test/functional/mochitest/.eslintrc.js
+++ b/browser/extensions/activity-stream/test/functional/mochitest/.eslintrc.js
@@ -26,16 +26,17 @@ module.exports = {
     "OpenBrowserWindow": false,
     "Preferences": false,
     "registerCleanupFunction": false,
     "requestLongerTimeout": false,
     "Services": false,
     "SimpleTest": false,
     "SpecialPowers": false,
     "TestUtils": false,
+    "thisTestLeaksUncaughtRejectionsAndShouldBeFixed": false,
     "todo": false,
     "todo_is": false,
     "todo_isnot": false,
     "waitForClipboard": false,
     "waitForExplicitFinish": false,
     "waitForFocus": false
   }
 };
--- a/browser/extensions/activity-stream/test/schemas/pings.js
+++ b/browser/extensions/activity-stream/test/schemas/pings.js
@@ -1,9 +1,10 @@
 const Joi = require("joi-browser");
+const {MAIN_MESSAGE_TYPE, CONTENT_MESSAGE_TYPE} = require("common/Actions.jsm");
 
 const baseKeys = {
   client_id: Joi.string().required(),
   addon_version: Joi.string().required(),
   locale: Joi.string().required(),
   session_id: Joi.string(),
   page: Joi.valid(["about:home", "about:newtab"])
 };
@@ -16,16 +17,39 @@ const UserEventPing = Joi.object().keys(
   source: Joi.string().required(),
   event: Joi.string().required(),
   action: Joi.valid("activity_stream_user_event").required(),
   metadata_source: Joi.string(),
   highlight_type: Joi.valid(["bookmarks", "recommendation", "history"]),
   recommender_type: Joi.string()
 }));
 
+// Use this to validate actions generated from Redux
+const UserEventAction = Joi.object().keys({
+  type: Joi.string().required(),
+  data: Joi.object().keys({
+    event: Joi.valid([
+      "CLICK",
+      "SEARCH",
+      "BLOCK",
+      "DELETE",
+      "OPEN_NEW_WINDOW",
+      "OPEN_PRIVATE_WINDOW",
+      "BOOKMARK_DELETE",
+      "BOOKMARK_ADD"
+    ]).required(),
+    source: Joi.valid(["TOP_SITES"]),
+    action_position: Joi.number().integer()
+  }).required(),
+  meta: Joi.object().keys({
+    to: Joi.valid(MAIN_MESSAGE_TYPE).required(),
+    from: Joi.valid(CONTENT_MESSAGE_TYPE).required()
+  }).required()
+});
+
 const UndesiredPing = Joi.object().keys(Object.assign({}, baseKeys, {
   source: Joi.string().required(),
   event: Joi.string().required(),
   action: Joi.valid("activity_stream_undesired_event").required(),
   value: Joi.number().required()
 }));
 
 const PerfPing = Joi.object().keys(Object.assign({}, baseKeys, {
@@ -37,21 +61,53 @@ const PerfPing = Joi.object().keys(Objec
 
 const SessionPing = Joi.object().keys(Object.assign({}, baseKeys, {
   session_id: baseKeys.session_id.required(),
   page: baseKeys.page.required(),
   session_duration: Joi.number().integer().required(),
   action: Joi.valid("activity_stream_session").required()
 }));
 
-function assertMatchesSchema(ping, schema) {
-  assert.isNull(Joi.validate(ping, schema).error);
+function chaiAssertions(_chai, utils) {
+  const {Assertion} = _chai;
+
+  Assertion.addMethod("validate", function(schema, schemaName) {
+    const {error} = Joi.validate(this._obj, schema);
+    this.assert(
+      !error,
+      `Expected to be ${schemaName ? `a valid ${schemaName}` : "valid"} but there were errors: ${error}`
+    );
+  });
+
+  const assertions = {
+    /**
+     * assert.validate - Validates an item given a Joi schema
+     *
+     * @param  {any} actual The item to validate
+     * @param  {obj} schema A Joi schema
+     */
+    validate(actual, schema, schemaName) {
+      new Assertion(actual).validate(schema, schemaName);
+    },
+
+    /**
+     * isUserEventAction - Passes if the item is a valid UserEvent action
+     *
+     * @param  {any} actual The item to validate
+     */
+    isUserEventAction(actual) {
+      new Assertion(actual).validate(UserEventAction, "UserEventAction");
+    }
+  };
+
+  Object.assign(_chai.assert, assertions);
 }
 
 module.exports = {
   baseKeys,
   BasePing,
   UndesiredPing,
   UserEventPing,
+  UserEventAction,
   PerfPing,
   SessionPing,
-  assertMatchesSchema
+  chaiAssertions
 };
--- a/browser/extensions/activity-stream/test/unit/common/Reducers.test.js
+++ b/browser/extensions/activity-stream/test/unit/common/Reducers.test.js
@@ -1,10 +1,10 @@
 const {reducers, INITIAL_STATE} = require("common/Reducers.jsm");
-const {TopSites, Search, App} = reducers;
+const {TopSites, App} = reducers;
 const {actionTypes: at} = require("common/Actions.jsm");
 
 describe("Reducers", () => {
   describe("App", () => {
     it("should return the initial state", () => {
       const nextState = App(undefined, {type: "FOO"});
       assert.equal(nextState, INITIAL_STATE.App);
     });
@@ -103,27 +103,9 @@ describe("Reducers", () => {
       events.forEach(event => {
         const oldState = {rows: [{url: "foo.com"}, {url: "bar.com"}]};
         const action = {type: event, data: {url: "bar.com"}};
         const nextState = TopSites(oldState, action);
         assert.deepEqual(nextState.rows, [{url: "foo.com"}]);
       });
     });
   });
-  describe("Search", () => {
-    it("should return the initial state", () => {
-      const nextState = Search(undefined, {type: "FOO"});
-      assert.equal(nextState, INITIAL_STATE.Search);
-    });
-    it("should not update state for empty action.data on Search", () => {
-      const nextState = Search(undefined, {type: at.SEARCH_STATE_UPDATED});
-      assert.equal(nextState, INITIAL_STATE.Search);
-    });
-    it("should update the current engine and the engines on SEARCH_STATE_UPDATED", () => {
-      const newEngine = {name: "Google", iconBuffer: "icon.ico"};
-      const nextState = Search(undefined, {type: at.SEARCH_STATE_UPDATED, data: {currentEngine: newEngine, engines: [newEngine]}});
-      assert.equal(nextState.currentEngine.name, newEngine.name);
-      assert.equal(nextState.currentEngine.icon, newEngine.icon);
-      assert.equal(nextState.engines[0].name, newEngine.name);
-      assert.equal(nextState.engines[0].icon, newEngine.icon);
-    });
-  });
 });
--- a/browser/extensions/activity-stream/test/unit/lib/ActivityStream.test.js
+++ b/browser/extensions/activity-stream/test/unit/lib/ActivityStream.test.js
@@ -1,55 +1,60 @@
 const injector = require("inject!lib/ActivityStream.jsm");
 
+const REASON_ADDON_UNINSTALL = 6;
+
 describe("ActivityStream", () => {
   let sandbox;
   let as;
   let ActivityStream;
   function Fake() {}
-  before(() => {
+
+  beforeEach(() => {
     sandbox = sinon.sandbox.create();
     ({ActivityStream} = injector({
       "lib/LocalizationFeed.jsm": {LocalizationFeed: Fake},
       "lib/NewTabInit.jsm": {NewTabInit: Fake},
       "lib/PlacesFeed.jsm": {PlacesFeed: Fake},
-      "lib/SearchFeed.jsm": {SearchFeed: Fake},
       "lib/TelemetryFeed.jsm": {TelemetryFeed: Fake},
       "lib/TopSitesFeed.jsm": {TopSitesFeed: Fake}
     }));
+    as = new ActivityStream();
+    sandbox.stub(as.store, "init");
+    sandbox.stub(as.store, "uninit");
+    sandbox.stub(as._defaultPrefs, "init");
+    sandbox.stub(as._defaultPrefs, "reset");
   });
 
   afterEach(() => sandbox.restore());
 
-  beforeEach(() => {
-    as = new ActivityStream();
-    sandbox.stub(as.store, "init");
-    sandbox.stub(as.store, "uninit");
-  });
-
   it("should exist", () => {
     assert.ok(ActivityStream);
   });
   it("should initialize with .initialized=false", () => {
     assert.isFalse(as.initialized, ".initialized");
   });
   describe("#init", () => {
     beforeEach(() => {
       as.init();
     });
+    it("should initialize default prefs", () => {
+      assert.calledOnce(as._defaultPrefs.init);
+    });
     it("should set .initialized to true", () => {
       assert.isTrue(as.initialized, ".initialized");
     });
     it("should call .store.init", () => {
       assert.calledOnce(as.store.init);
     });
     it("should emit an INIT event with the right version", () => {
       as = new ActivityStream({version: "1.2.3"});
       sandbox.stub(as.store, "init");
       sandbox.stub(as.store, "dispatch");
+      sandbox.stub(as._defaultPrefs, "init");
 
       as.init();
 
       assert.calledOnce(as.store.dispatch);
       const action = as.store.dispatch.firstCall.args[0];
       assert.propertyVal(action.data, "version", "1.2.3");
     });
   });
@@ -60,16 +65,26 @@ describe("ActivityStream", () => {
     });
     it("should set .initialized to false", () => {
       assert.isFalse(as.initialized, ".initialized");
     });
     it("should call .store.uninit", () => {
       assert.calledOnce(as.store.uninit);
     });
   });
+  describe("#uninstall", () => {
+    it("should reset default prefs if the reason is REASON_ADDON_UNINSTALL", () => {
+      as.uninstall(REASON_ADDON_UNINSTALL);
+      assert.calledOnce(as._defaultPrefs.reset);
+    });
+    it("should not reset default prefs if the reason is something else", () => {
+      as.uninstall("foo");
+      assert.notCalled(as._defaultPrefs.reset);
+    });
+  });
   describe("feeds", () => {
     it("should create a Localization feed", () => {
       const feed = as.feeds["feeds.localization"]();
       assert.instanceOf(feed, Fake);
     });
     it("should create a NewTabInit feed", () => {
       const feed = as.feeds["feeds.newtabinit"]();
       assert.instanceOf(feed, Fake);
@@ -81,14 +96,10 @@ describe("ActivityStream", () => {
     it("should create a TopSites feed", () => {
       const feed = as.feeds["feeds.topsites"]();
       assert.instanceOf(feed, Fake);
     });
     it("should create a Telemetry feed", () => {
       const feed = as.feeds["feeds.telemetry"]();
       assert.instanceOf(feed, Fake);
     });
-    it("should create a Search feed", () => {
-      const feed = as.feeds["feeds.search"]();
-      assert.instanceOf(feed, Fake);
-    });
   });
 });
--- a/browser/extensions/activity-stream/test/unit/lib/ActivityStreamMessageChannel.test.js
+++ b/browser/extensions/activity-stream/test/unit/lib/ActivityStreamMessageChannel.test.js
@@ -4,38 +4,35 @@ const {createStore, applyMiddleware} = r
 const {actionTypes: at, actionCreators: ac} = require("common/Actions.jsm");
 
 const OPTIONS = ["pageURL, outgoingMessageName", "incomingMessageName", "dispatch"];
 
 describe("ActivityStreamMessageChannel", () => {
   let globals;
   let dispatch;
   let mm;
-  before(() => {
+  beforeEach(() => {
     function RP(url) {
       this.url = url;
       this.messagePorts = [];
       this.addMessageListener = globals.sandbox.spy();
       this.sendAsyncMessage = globals.sandbox.spy();
       this.destroy = globals.sandbox.spy();
     }
     globals = new GlobalOverrider();
     globals.set("AboutNewTab", {
       override: globals.sandbox.spy(),
       reset: globals.sandbox.spy()
     });
     globals.set("RemotePages", RP);
     dispatch = globals.sandbox.spy();
-  });
-  beforeEach(() => {
     mm = new ActivityStreamMessageChannel({dispatch});
   });
 
-  afterEach(() => globals.reset());
-  after(() => globals.restore());
+  afterEach(() => globals.restore());
 
   it("should exist", () => {
     assert.ok(ActivityStreamMessageChannel);
   });
   it("should apply default options", () => {
     mm = new ActivityStreamMessageChannel();
     OPTIONS.forEach(o => assert.equal(mm[o], DEFAULT_OPTIONS[o], o));
   });
@@ -126,16 +123,22 @@ describe("ActivityStreamMessageChannel",
       it("should dispatch a NEW_TAB_UNLOAD action", () => {
         const t = {portID: "foo"};
         sinon.stub(mm, "onActionFromContent");
         mm.onNewTabUnload({target: t});
         assert.calledWith(mm.onActionFromContent, {type: at.NEW_TAB_UNLOAD}, "foo");
       });
     });
     describe("#onMessage", () => {
+      let sandbox;
+      beforeEach(() => {
+        sandbox = sinon.sandbox.create();
+        sandbox.spy(global.Components.utils, "reportError");
+      });
+      afterEach(() => sandbox.restore());
       it("should report an error if the msg.data is missing", () => {
         mm.onMessage({target: {portID: "foo"}});
         assert.calledOnce(global.Components.utils.reportError);
       });
       it("should report an error if the msg.data.type is missing", () => {
         mm.onMessage({target: {portID: "foo"}, data: "foo"});
         assert.calledOnce(global.Components.utils.reportError);
       });
new file mode 100644
--- /dev/null
+++ b/browser/extensions/activity-stream/test/unit/lib/ActivityStreamPrefs.test.js
@@ -0,0 +1,63 @@
+const ACTIVITY_STREAM_PREF_BRANCH = "browser.newtabpage.activity-stream.";
+const {Prefs, DefaultPrefs} = require("lib/ActivityStreamPrefs.jsm");
+
+const TEST_PREF_CONFIG = [
+  {name: "foo", value: true},
+  {name: "bar", value: "BAR"},
+  {name: "baz", value: 1}
+];
+
+describe("ActivityStreamPrefs", () => {
+  describe("Prefs", () => {
+    it("should have get, set, and observe methods", () => {
+      const p = new Prefs();
+      assert.property(p, "get");
+      assert.property(p, "set");
+      assert.property(p, "observe");
+    });
+    describe(".branchName", () => {
+      it("should return the activity stream branch by default", () => {
+        const p = new Prefs();
+        assert.equal(p.branchName, ACTIVITY_STREAM_PREF_BRANCH);
+      });
+      it("should return the custom branch name if it was passed to the constructor", () => {
+        const p = new Prefs("foo");
+        assert.equal(p.branchName, "foo");
+      });
+    });
+  });
+
+  describe("DefaultPrefs", () => {
+    describe("#init", () => {
+      let defaultPrefs;
+      beforeEach(() => {
+        defaultPrefs = new DefaultPrefs(TEST_PREF_CONFIG);
+        sinon.spy(defaultPrefs.branch, "setBoolPref");
+        sinon.spy(defaultPrefs.branch, "setStringPref");
+        sinon.spy(defaultPrefs.branch, "setIntPref");
+      });
+      it("should initialize a boolean pref", () => {
+        defaultPrefs.init();
+        assert.calledWith(defaultPrefs.branch.setBoolPref, "foo", true);
+      });
+      it("should initialize a string pref", () => {
+        defaultPrefs.init();
+        assert.calledWith(defaultPrefs.branch.setStringPref, "bar", "BAR");
+      });
+      it("should initialize a integer pref", () => {
+        defaultPrefs.init();
+        assert.calledWith(defaultPrefs.branch.setIntPref, "baz", 1);
+      });
+    });
+    describe("#reset", () => {
+      it("should clear user preferences for each pref in the config", () => {
+        const defaultPrefs = new DefaultPrefs(TEST_PREF_CONFIG);
+        sinon.spy(defaultPrefs.branch, "clearUserPref");
+        defaultPrefs.reset();
+        for (const pref of TEST_PREF_CONFIG) {
+          assert.calledWith(defaultPrefs.branch.clearUserPref, pref.name);
+        }
+      });
+    });
+  });
+});
--- a/browser/extensions/activity-stream/test/unit/lib/LocalizationFeed.test.js
+++ b/browser/extensions/activity-stream/test/unit/lib/LocalizationFeed.test.js
@@ -14,29 +14,32 @@ const TEST_STRINGS = {
     too: "Boo"
   },
   "ru": {foo: "Baz"}
 };
 
 describe("Localization Feed", () => {
   let feed;
   let globals;
-  before(() => {
+  let sandbox;
+  beforeEach(() => {
     globals = new GlobalOverrider();
-  });
-  beforeEach(() => {
+    sandbox = globals.sandbox;
     feed = new LocalizationFeed();
     feed.store = {dispatch: sinon.spy()};
+
+    sandbox.stub(global.Services.locale, "getRequestedLocale");
   });
   afterEach(() => {
     globals.restore();
   });
 
   it("should fetch strings on init", async () => {
-    sinon.stub(feed, "updateLocale");
+    sandbox.stub(feed, "updateLocale");
+    sandbox.stub(global, "fetch");
     fetch.returns(Promise.resolve({json() { return Promise.resolve(TEST_STRINGS); }}));
 
     await feed.init();
 
     assert.deepEqual(feed.allStrings, TEST_STRINGS);
     assert.calledOnce(feed.updateLocale);
   });
 
@@ -110,19 +113,23 @@ describe("Localization Feed", () => {
       feed.observe(null, "some-other-notification");
 
       assert.notCalled(feed.updateLocale);
     });
   });
 
   describe("#onAction", () => {
     it("should addObserver on INIT", () => {
+      const stub = sandbox.stub(global.Services.obs, "addObserver");
+
       feed.onAction({type: at.INIT});
 
-      assert.calledOnce(global.Services.obs.addObserver);
+      assert.calledOnce(stub);
     });
     it("should removeObserver on UNINIT", () => {
+      const stub = sandbox.stub(global.Services.obs, "removeObserver");
+
       feed.onAction({type: at.UNINIT});
 
-      assert.calledOnce(global.Services.obs.removeObserver);
+      assert.calledOnce(stub);
     });
   });
 });
--- a/browser/extensions/activity-stream/test/unit/lib/PlacesFeed.test.js
+++ b/browser/extensions/activity-stream/test/unit/lib/PlacesFeed.test.js
@@ -23,16 +23,19 @@ describe("PlacesFeed", () => {
         deleteHistoryEntry: sandbox.spy(),
         blockURL: sandbox.spy()
       }
     });
     globals.set("PlacesUtils", {
       history: {addObserver: sandbox.spy(), removeObserver: sandbox.spy()},
       bookmarks: {TYPE_BOOKMARK, addObserver: sandbox.spy(), removeObserver: sandbox.spy()}
     });
+    sandbox.spy(global.Services.obs, "addObserver");
+    sandbox.spy(global.Services.obs, "removeObserver");
+    sandbox.spy(global.Components.utils, "reportError");
 
     feed = new PlacesFeed();
     feed.store = {dispatch: sinon.spy()};
   });
   afterEach(() => globals.restore());
 
   it("should have a HistoryObserver that dispatches to the store", () => {
     assert.instanceOf(feed.historyObserver, HistoryObserver);
deleted file mode 100644
--- a/browser/extensions/activity-stream/test/unit/lib/SearchFeed.test.js
+++ /dev/null
@@ -1,84 +0,0 @@
-"use strict";
-const {SearchFeed} = require("lib/SearchFeed.jsm");
-const {GlobalOverrider} = require("test/unit/utils");
-const {actionTypes: at} = require("common/Actions.jsm");
-const fakeEngines = [{name: "Google", iconBuffer: "icon.ico"}];
-describe("Search Feed", () => {
-  let feed;
-  let globals;
-  before(() => {
-    globals = new GlobalOverrider();
-    globals.set("ContentSearch", {
-      currentStateObj: globals.sandbox.spy(() => Promise.resolve({engines: fakeEngines, currentEngine: {}})),
-      performSearch: globals.sandbox.spy((browser, searchData) => Promise.resolve({browser, searchData}))
-    });
-  });
-  beforeEach(() => {
-    feed = new SearchFeed();
-    feed.store = {dispatch: sinon.spy()};
-  });
-  afterEach(() => globals.reset());
-  after(() => globals.restore());
-
-  it("should call get state (with true) from the content search provider on INIT", async() => {
-    await feed.onAction({type: at.INIT});
-
-    // calling currentStateObj with 'true' allows us to return a data uri for the
-    // icon, instead of an array buffer
-    assert.calledWith(global.ContentSearch.currentStateObj, true);
-  });
-  it("should get the the state on INIT", () => {
-    sinon.stub(feed, "getState");
-    feed.onAction({type: at.INIT});
-    assert.calledOnce(feed.getState);
-  });
-  it("should add observers on INIT", () => {
-    sinon.stub(feed, "addObservers");
-    feed.onAction({type: at.INIT});
-    assert.calledOnce(feed.addObservers);
-  });
-  it("should remove observers on UNINIT", () => {
-    sinon.stub(feed, "removeObservers");
-    feed.onAction({type: at.UNINIT});
-    assert.calledOnce(feed.removeObservers);
-  });
-  it("should add event handlers on INIT", () => {
-    feed.onAction({type: at.INIT});
-
-    assert.calledOnce(global.Services.obs.addObserver);
-    assert.calledOnce(global.Services.mm.addMessageListener);
-  });
-  it("should remove event handlers on UNINIT", () => {
-    feed.onAction({type: at.UNINIT});
-
-    assert.calledOnce(global.Services.obs.removeObserver);
-    assert.calledOnce(global.Services.mm.removeMessageListener);
-  });
-  it("should dispatch one event with the state", async() => {
-    feed.contentSearch = Promise.resolve(global.ContentSearch);
-
-    await feed.getState();
-
-    assert.calledOnce(feed.store.dispatch);
-  });
-  it("should perform a search on PERFORM_SEARCH", () => {
-    sinon.stub(feed, "performSearch");
-    feed.onAction({_target: {browser: {}}, type: at.PERFORM_SEARCH});
-    assert.calledOnce(feed.performSearch);
-  });
-  it("should call performSearch with an action", () => {
-    const action = {_target: {browser: "browser"}, data: {searchString: "hello"}};
-    feed.performSearch(action._target.browser, action.data);
-    assert.calledWith(global.ContentSearch.performSearch, {target: action._target.browser}, action.data);
-  });
-  it("should get the state if we change the search engines", () => {
-    sinon.stub(feed, "getState");
-    feed.observe(null, "browser-search-engine-modified", "engine-current");
-    assert.calledOnce(feed.getState);
-  });
-  it("shouldn't get the state if it's not the right notification", () => {
-    sinon.stub(feed, "getState");
-    feed.observe(null, "some-other-notification", "engine-current");
-    assert.notCalled(feed.getState);
-  });
-});
--- a/browser/extensions/activity-stream/test/unit/lib/Store.test.js
+++ b/browser/extensions/activity-stream/test/unit/lib/Store.test.js
@@ -1,40 +1,34 @@
 const injector = require("inject!lib/Store.jsm");
 const {createStore} = require("redux");
 const {addNumberReducer} = require("test/unit/utils");
-const {GlobalOverrider} = require("test/unit/utils");
+const {FakePrefs} = require("test/unit/utils");
 describe("Store", () => {
   let Store;
-  let Preferences;
   let sandbox;
   let store;
-  let globals;
-  let PREF_PREFIX;
   beforeEach(() => {
-    globals = new GlobalOverrider();
-    sandbox = globals.sandbox;
-    Preferences = new Map();
-    Preferences.observe = sandbox.spy();
-    Preferences.ignore = sandbox.spy();
-    globals.set("Preferences", Preferences);
+    sandbox = sinon.sandbox.create();
     function ActivityStreamMessageChannel(options) {
       this.dispatch = options.dispatch;
       this.createChannel = sandbox.spy();
       this.destroyChannel = sandbox.spy();
       this.middleware = sandbox.spy(s => next => action => next(action));
     }
-    ({Store, PREF_PREFIX} = injector({"lib/ActivityStreamMessageChannel.jsm": {ActivityStreamMessageChannel}}));
+    ({Store} = injector({
+      "lib/ActivityStreamMessageChannel.jsm": {ActivityStreamMessageChannel},
+      "lib/ActivityStreamPrefs.jsm": {Prefs: FakePrefs}
+    }));
     store = new Store();
   });
   afterEach(() => {
-    Preferences.clear();
-    globals.restore();
+    sandbox.restore();
   });
-  it("should have an .feeds property that is a Map", () => {
+  it("should have a .feeds property that is a Map", () => {
     assert.instanceOf(store.feeds, Map);
     assert.equal(store.feeds.size, 0, ".feeds.size");
   });
   it("should have a redux store at ._store", () => {
     assert.ok(store._store);
     assert.property(store, "dispatch");
     assert.property(store, "getState");
   });
@@ -44,17 +38,17 @@ describe("Store", () => {
   });
   it("should connect the ActivityStreamMessageChannel's middleware", () => {
     store.dispatch({type: "FOO"});
     assert.calledOnce(store._messageChannel.middleware);
   });
   describe("#initFeed", () => {
     it("should add an instance of the feed to .feeds", () => {
       class Foo {}
-      Preferences.set(`${PREF_PREFIX}foo`, false);
+      store._prefs.set("foo", false);
       store.init({foo: () => new Foo()});
       store.initFeed("foo");
 
       assert.isTrue(store.feeds.has("foo"), "foo is set");
       assert.instanceOf(store.feeds.get("foo"), Foo);
     });
     it("should add a .store property to the feed", () => {
       class Foo {}
@@ -93,44 +87,36 @@ describe("Store", () => {
       assert.isFalse(store.feeds.has("foo"), "foo is not in .feeds");
     });
   });
   describe("maybeStartFeedAndListenForPrefChanges", () => {
     beforeEach(() => {
       sinon.stub(store, "initFeed");
       sinon.stub(store, "uninitFeed");
     });
-    it("should set the new pref in Preferences to true, if it was never defined", () => {
-      store.maybeStartFeedAndListenForPrefChanges("foo");
-      assert.isTrue(Preferences.get(`${PREF_PREFIX}foo`));
-    });
-    it("should not override the pref if it was already set", () => {
-      Preferences.set(`${PREF_PREFIX}foo`, false);
-      store.maybeStartFeedAndListenForPrefChanges("foo");
-      assert.isFalse(Preferences.get(`${PREF_PREFIX}foo`));
-    });
     it("should initialize the feed if the Pref is set to true", () => {
-      Preferences.set(`${PREF_PREFIX}foo`, true);
+      store._prefs.set("foo", true);
       store.maybeStartFeedAndListenForPrefChanges("foo");
       assert.calledWith(store.initFeed, "foo");
     });
     it("should not initialize the feed if the Pref is set to false", () => {
-      Preferences.set(`${PREF_PREFIX}foo`, false);
+      store._prefs.set("foo", false);
       store.maybeStartFeedAndListenForPrefChanges("foo");
       assert.notCalled(store.initFeed);
     });
     it("should observe the pref", () => {
+      sinon.stub(store._prefs, "observe");
       store.maybeStartFeedAndListenForPrefChanges("foo");
-      assert.calledWith(Preferences.observe, `${PREF_PREFIX}foo`, store._prefHandlers.get(`${PREF_PREFIX}foo`));
+      assert.calledWith(store._prefs.observe, "foo", store._prefHandlers.get("foo"));
     });
     describe("handler", () => {
       let handler;
       beforeEach(() => {
         store.maybeStartFeedAndListenForPrefChanges("foo");
-        handler = store._prefHandlers.get(`${PREF_PREFIX}foo`);
+        handler = store._prefHandlers.get("foo");
       });
       it("should initialize the feed if called with true", () => {
         handler(true);
         assert.calledWith(store.initFeed, "foo");
       });
       it("should uninitialize the feed if called with false", () => {
         handler(false);
         assert.calledWith(store.uninitFeed, "foo");
@@ -146,16 +132,19 @@ describe("Store", () => {
     });
     it("should initialize the ActivityStreamMessageChannel channel", () => {
       store.init();
       assert.calledOnce(store._messageChannel.createChannel);
     });
   });
   describe("#uninit", () => {
     it("should clear .feeds, ._prefHandlers, and ._feedFactories", () => {
+      store._prefs.set("a", true);
+      store._prefs.set("b", true);
+      store._prefs.set("c", true);
       store.init({
         a: () => ({}),
         b: () => ({}),
         c: () => ({})
       });
 
       store.uninit();
 
@@ -176,16 +165,17 @@ describe("Store", () => {
     });
   });
   describe("#dispatch", () => {
     it("should call .onAction of each feed", () => {
       const {dispatch} = store;
       const sub = {onAction: sinon.spy()};
       const action = {type: "FOO"};
 
+      store._prefs.set("sub", true);
       store.init({sub: () => sub});
 
       dispatch(action);
 
       assert.calledWith(sub.onAction, action);
     });
     it("should call the reducers", () => {
       const {dispatch} = store;
--- a/browser/extensions/activity-stream/test/unit/lib/TelemetryFeed.test.js
+++ b/browser/extensions/activity-stream/test/unit/lib/TelemetryFeed.test.js
@@ -1,18 +1,17 @@
 const injector = require("inject!lib/TelemetryFeed.jsm");
 const {GlobalOverrider} = require("test/unit/utils");
 const {actionCreators: ac, actionTypes: at} = require("common/Actions.jsm");
 const {
   BasePing,
   UndesiredPing,
   UserEventPing,
   PerfPing,
-  SessionPing,
-  assertMatchesSchema
+  SessionPing
 } = require("test/schemas/pings");
 
 const FAKE_TELEMETRY_ID = "foo123";
 const FAKE_UUID = "{foo-123-foo}";
 
 describe("TelemetryFeed", () => {
   let globals;
   let sandbox;
@@ -98,107 +97,107 @@ describe("TelemetryFeed", () => {
       assert.calledWith(instance.sendEvent, instance.createSessionEndEvent.firstCall.returnValue);
     });
   });
   describe("ping creators", () => {
     beforeEach(async () => await instance.init());
     describe("#createPing", () => {
       it("should create a valid base ping without a session if no portID is supplied", () => {
         const ping = instance.createPing();
-        assertMatchesSchema(ping, BasePing);
+        assert.validate(ping, BasePing);
         assert.notProperty(ping, "session_id");
       });
       it("should create a valid base ping with session info if a portID is supplied", () => {
         // Add a session
         const portID = "foo";
         instance.addSession(portID);
         const sessionID = instance.sessions.get(portID).session_id;
 
         // Create a ping referencing the session
         const ping = instance.createPing(portID);
-        assertMatchesSchema(ping, BasePing);
+        assert.validate(ping, BasePing);
 
         // Make sure we added the right session-related stuff to the ping
         assert.propertyVal(ping, "session_id", sessionID);
         assert.propertyVal(ping, "page", "about:newtab");
       });
     });
     describe("#createUserEvent", () => {
       it("should create a valid event", () => {
         const portID = "foo";
         const data = {source: "TOP_SITES", event: "CLICK"};
         const action = ac.SendToMain(ac.UserEvent(data), portID);
         const session = addSession(portID);
         const ping = instance.createUserEvent(action);
 
         // Is it valid?
-        assertMatchesSchema(ping, UserEventPing);
+        assert.validate(ping, UserEventPing);
         // Does it have the right session_id?
         assert.propertyVal(ping, "session_id", session.session_id);
       });
     });
     describe("#createUndesiredEvent", () => {
       it("should create a valid event without a session", () => {
         const action = ac.UndesiredEvent({source: "TOP_SITES", event: "MISSING_IMAGE", value: 10});
         const ping = instance.createUndesiredEvent(action);
 
         // Is it valid?
-        assertMatchesSchema(ping, UndesiredPing);
+        assert.validate(ping, UndesiredPing);
         // Does it have the right value?
         assert.propertyVal(ping, "value", 10);
       });
       it("should create a valid event with a session", () => {
         const portID = "foo";
         const data = {source: "TOP_SITES", event: "MISSING_IMAGE", value: 10};
         const action = ac.SendToMain(ac.UndesiredEvent(data), portID);
         const session = addSession(portID);
         const ping = instance.createUndesiredEvent(action);
 
         // Is it valid?
-        assertMatchesSchema(ping, UndesiredPing);
+        assert.validate(ping, UndesiredPing);
         // Does it have the right session_id?
         assert.propertyVal(ping, "session_id", session.session_id);
         // Does it have the right value?
         assert.propertyVal(ping, "value", 10);
       });
     });
     describe("#createPerformanceEvent", () => {
       it("should create a valid event without a session", () => {
         const action = ac.PerfEvent({event: "SCREENSHOT_FINISHED", value: 100});
         const ping = instance.createPerformanceEvent(action);
 
         // Is it valid?
-        assertMatchesSchema(ping, PerfPing);
+        assert.validate(ping, PerfPing);
         // Does it have the right value?
         assert.propertyVal(ping, "value", 100);
       });
       it("should create a valid event with a session", () => {
         const portID = "foo";
         const data = {event: "PAGE_LOADED", value: 100};
         const action = ac.SendToMain(ac.PerfEvent(data), portID);
         const session = addSession(portID);
         const ping = instance.createPerformanceEvent(action);
 
         // Is it valid?
-        assertMatchesSchema(ping, PerfPing);
+        assert.validate(ping, PerfPing);
         // Does it have the right session_id?
         assert.propertyVal(ping, "session_id", session.session_id);
         // Does it have the right value?
         assert.propertyVal(ping, "value", 100);
       });
     });
     describe("#createSessionEndEvent", () => {
       it("should create a valid event", () => {
         const ping = instance.createSessionEndEvent({
           session_id: FAKE_UUID,
           page: "about:newtab",
           session_duration: 12345
         });
         // Is it valid?
-        assertMatchesSchema(ping, SessionPing);
+        assert.validate(ping, SessionPing);
         assert.propertyVal(ping, "session_id", FAKE_UUID);
         assert.propertyVal(ping, "page", "about:newtab");
         assert.propertyVal(ping, "session_duration", 12345);
       });
     });
   });
   describe("#sendEvent", () => {
     it("should call telemetrySender", async () => {
--- a/browser/extensions/activity-stream/test/unit/lib/TelemetrySender.test.js
+++ b/browser/extensions/activity-stream/test/unit/lib/TelemetrySender.test.js
@@ -13,41 +13,38 @@ let fakePrefs;
 const prefInitHook = function() {
   fakePrefs = this; // eslint-disable-line consistent-this
 };
 const tsArgs = {prefInitHook};
 
 describe("TelemetrySender", () => {
   let globals;
   let tSender;
+  let sandbox;
   let fetchStub;
   const fakeEndpointUrl = "http://127.0.0.1/stuff";
   const fakePingJSON = JSON.stringify({action: "fake_action", monkey: 1});
   const fakeFetchHttpErrorResponse = {ok: false, status: 400};
   const fakeFetchSuccessResponse = {ok: true, status: 200};
 
-  before(() => {
+  beforeEach(() => {
     globals = new GlobalOverrider();
-
-    fetchStub = globals.sandbox.stub();
+    sandbox = globals.sandbox;
+    fetchStub = sandbox.stub();
 
     globals.set("Preferences", FakePrefs);
     globals.set("fetch", fetchStub);
-  });
-
-  beforeEach(() => {
+    sandbox.spy(global.Components.utils, "reportError");
   });
 
   afterEach(() => {
-    globals.reset();
+    globals.restore();
     FakePrefs.prototype.prefs = {};
   });
 
-  after(() => globals.restore());
-
   it("should construct the Prefs object", () => {
     globals.sandbox.spy(global, "Preferences");
 
     tSender = new TelemetrySender(tsArgs);
 
     assert.calledOnce(global.Preferences);
   });
 
--- a/browser/extensions/activity-stream/test/unit/lib/TopSitesFeed.test.js
+++ b/browser/extensions/activity-stream/test/unit/lib/TopSitesFeed.test.js
@@ -7,21 +7,20 @@ const FAKE_LINKS = new Array(TOP_SITES_S
 const FAKE_SCREENSHOT = "data123";
 
 describe("Top Sites Feed", () => {
   let feed;
   let globals;
   let sandbox;
   let links;
   let clock;
-  before(() => {
+
+  beforeEach(() => {
     globals = new GlobalOverrider();
     sandbox = globals.sandbox;
-  });
-  beforeEach(() => {
     globals.set("NewTabUtils", {activityStreamLinks: {getTopSites: sandbox.spy(() => Promise.resolve(links))}});
     globals.set("PreviewProvider", {getThumbnail: sandbox.spy(() => Promise.resolve(FAKE_SCREENSHOT))});
     feed = new TopSitesFeed();
     feed.store = {dispatch: sinon.spy(), getState() { return {TopSites: {rows: Array(12).fill("site")}}; }};
     links = FAKE_LINKS;
     clock = sinon.useFakeTimers();
   });
   afterEach(() => {
--- a/browser/extensions/activity-stream/test/unit/lib/init-store.test.js
+++ b/browser/extensions/activity-stream/test/unit/lib/init-store.test.js
@@ -1,25 +1,22 @@
 const initStore = require("content-src/lib/init-store");
 const {GlobalOverrider, addNumberReducer} = require("test/unit/utils");
 const {actionCreators: ac} = require("common/Actions.jsm");
 
 describe("initStore", () => {
   let globals;
   let store;
-  before(() => {
+  beforeEach(() => {
     globals = new GlobalOverrider();
     globals.set("sendAsyncMessage", globals.sandbox.spy());
     globals.set("addMessageListener", globals.sandbox.spy());
-  });
-  beforeEach(() => {
     store = initStore({number: addNumberReducer});
   });
-  afterEach(() => globals.reset());
-  after(() => globals.restore());
+  afterEach(() => globals.restore());
   it("should create a store with the provided reducers", () => {
     assert.ok(store);
     assert.property(store.getState(), "number");
   });
   it("should add a listener for incoming actions", () => {
     assert.calledWith(global.addMessageListener, initStore.INCOMING_MESSAGE_NAME);
     const callback = global.addMessageListener.firstCall.args[1];
     globals.sandbox.spy(store, "dispatch");
--- a/browser/extensions/activity-stream/test/unit/unit-entry.js
+++ b/browser/extensions/activity-stream/test/unit/unit-entry.js
@@ -1,44 +1,59 @@
-const {GlobalOverrider} = require("test/unit/utils");
+const {GlobalOverrider, FakePrefs} = require("test/unit/utils");
+const {chaiAssertions} = require("test/schemas/pings");
 
-const req = require.context(".", true, /\.test\.js$/);
+const req = require.context(".", true, /\.test\.jsx?$/);
 const files = req.keys();
 
 // This exposes sinon assertions to chai.assert
 sinon.assert.expose(assert, {prefix: ""});
 
+chai.use(chaiAssertions);
+
 let overrider = new GlobalOverrider();
 overrider.set({
   Components: {
     interfaces: {},
     utils: {
-      import: overrider.sandbox.spy(),
-      importGlobalProperties: overrider.sandbox.spy(),
-      reportError: overrider.sandbox.spy(),
+      import() {},
+      importGlobalProperties() {},
+      reportError() {},
       now: () => window.performance.now()
     }
   },
+  // eslint-disable-next-line object-shorthand
+  ContentSearchUIController: function() {}, // NB: This is a function/constructor
+  dump() {},
+  fetch() {},
+  Preferences: FakePrefs,
+  Services: {
+    locale: {getRequestedLocale() {}},
+    mm: {
+      addMessageListener: (msg, cb) => cb(),
+      removeMessageListener() {}
+    },
+    obs: {
+      addObserver() {},
+      removeObserver() {}
+    },
+    prefs: {
+      getDefaultBranch() {
+        return {
+          setBoolPref() {},
+          setIntPref() {},
+          setStringPref() {},
+          clearUserPref() {}
+        };
+      }
+    }
+  },
   XPCOMUtils: {
-    defineLazyModuleGetter: overrider.sandbox.spy(),
-    defineLazyServiceGetter: overrider.sandbox.spy(),
-    generateQI: overrider.sandbox.stub().returns(() => {})
-  },
-  dump: overrider.sandbox.spy(),
-  fetch: overrider.sandbox.stub(),
-  Services: {
-    locale: {getRequestedLocale: overrider.sandbox.stub()},
-    mm: {
-      addMessageListener: overrider.sandbox.spy((msg, cb) => cb()),
-      removeMessageListener: overrider.sandbox.spy()
-    },
-    obs: {
-      addObserver: overrider.sandbox.spy(),
-      removeObserver: overrider.sandbox.spy()
-    }
+    defineLazyModuleGetter() {},
+    defineLazyServiceGetter() {},
+    generateQI() { return {}; }
   }
 });
 
 describe("activity-stream", () => {
-  afterEach(() => overrider.reset());
   after(() => overrider.restore());
   files.forEach(file => req(file));
 });
--- a/browser/extensions/activity-stream/test/unit/utils.js
+++ b/browser/extensions/activity-stream/test/unit/utils.js
@@ -49,16 +49,17 @@ class GlobalOverrider {
    */
   set(key, value) {
     if (!value && typeof key === "object") {
       const overrides = key;
       Object.keys(overrides).forEach(k => this._override(k, overrides[k]));
     } else {
       this._override(key, value);
     }
+    return value;
   }
 
   /**
    * reset - Reset the global sandbox, so all state on spies, stubs etc. is cleared.
    *         You probably want to call this after each test.
    */
   reset() {
     this.sandbox.reset();