Bug 1478870 - Add component-ified tooling, ordered onboarding and bug fixes to Activity Stream. r?ursula draft
authorEd Lee <edilee@mozilla.com>
Fri, 27 Jul 2018 13:01:36 -0700
changeset 823659 8c3844b0c616e62008346e6228da2f34e991c22c
parent 823638 35a17ebc4ee64460cdac22d3fb2a57e1215e9b0f
push id117755
push userbmo:edilee@mozilla.com
push dateFri, 27 Jul 2018 20:03:09 +0000
reviewersursula
bugs1478870
milestone63.0a1
Bug 1478870 - Add component-ified tooling, ordered onboarding and bug fixes to Activity Stream. r?ursula MozReview-Commit-ID: K14RSdAbVH7
browser/components/newtab/aboutNewTabService.js
browser/components/newtab/bin/prepare-mochitests-dev
browser/components/newtab/bin/process-system-addon-for-package.js
browser/components/newtab/bin/render-activity-stream-html.js
browser/components/newtab/bin/update-version.js
browser/components/newtab/content-src/asrouter/schemas/message-format.md
browser/components/newtab/content-src/asrouter/schemas/provider-response.schema.json
browser/components/newtab/docs/v2-system-addon/1.GETTING_STARTED.md
browser/components/newtab/lib/ASRouter.jsm
browser/components/newtab/lib/ASRouterTargeting.jsm
browser/components/newtab/lib/ActivityStreamMessageChannel.jsm
browser/components/newtab/lib/OnboardingMessageProvider.jsm
browser/components/newtab/locales/az/strings.properties
browser/components/newtab/locales/da/strings.properties
browser/components/newtab/locales/es-ES/strings.properties
browser/components/newtab/locales/gn/strings.properties
browser/components/newtab/locales/ja-JP-mac/strings.properties
browser/components/newtab/locales/ja/strings.properties
browser/components/newtab/locales/ta/strings.properties
browser/components/newtab/locales/zh-TW/strings.properties
browser/components/newtab/mochitest.sh
browser/components/newtab/package.json
browser/components/newtab/prerendered/locales/az/activity-stream-prerendered-noscripts.html
browser/components/newtab/prerendered/locales/az/activity-stream-prerendered.html
browser/components/newtab/prerendered/locales/az/activity-stream-strings.js
browser/components/newtab/prerendered/locales/da/activity-stream-strings.js
browser/components/newtab/prerendered/locales/gn/activity-stream-strings.js
browser/components/newtab/prerendered/locales/ja-JP-mac/activity-stream-strings.js
browser/components/newtab/prerendered/locales/ja/activity-stream-strings.js
browser/components/newtab/prerendered/locales/ta/activity-stream-strings.js
browser/components/newtab/prerendered/locales/zh-TW/activity-stream-strings.js
browser/components/newtab/test/browser/browser_asrouter_targeting.js
browser/components/newtab/test/browser/browser_newtab_overrides.js
browser/components/newtab/test/browser/browser_packaged_as_locales.js
browser/components/newtab/test/unit/asrouter/ASRouter.test.js
browser/components/newtab/test/unit/asrouter/constants.js
browser/components/newtab/test/xpcshell/test_AboutNewTabService.js
browser/components/newtab/yamscripts.yml
--- a/browser/components/newtab/aboutNewTabService.js
+++ b/browser/components/newtab/aboutNewTabService.js
@@ -15,32 +15,31 @@ ChromeUtils.defineModuleGetter(this, "Ab
                                "resource:///modules/AboutNewTab.jsm");
 
 const TOPIC_APP_QUIT = "quit-application-granted";
 const TOPIC_LOCALES_CHANGE = "intl:app-locales-changed";
 const TOPIC_CONTENT_DOCUMENT_INTERACTIVE = "content-document-interactive";
 
 // Automated tests ensure packaged locales are in this list. Copied output of:
 // https://github.com/mozilla/activity-stream/blob/master/bin/render-activity-stream-html.js
-const ACTIVITY_STREAM_LOCALES = "en-US ach an ar ast az be bg bn-BD bn-IN br bs ca cak crh cs cy da de dsb el en-CA en-GB eo es-AR es-CL es-ES es-MX et eu fa ff fi fr fy-NL ga-IE gd gl gn gu-IN he hi-IN hr hsb hu hy-AM ia id it ja ja-JP-mac ka kab kk km kn ko lij lo lt ltg lv mai mk ml mr ms my nb-NO ne-NP nl nn-NO oc pa-IN pl pt-BR pt-PT rm ro ru si sk sl sq sr sv-SE ta te th tl tr uk ur uz vi zh-CN zh-TW".split(" ");
+const ACTIVITY_STREAM_BCP47 = "en-US ach an ar ast az be bg bn-BD bn-IN br bs ca cak crh cs cy da de dsb el en-CA en-GB eo es-AR es-CL es-ES es-MX et eu fa ff fi fr fy-NL ga-IE gd gl gn gu-IN he hi-IN hr hsb hu hy-AM ia id it ja ja-JP-macos ka kab kk km kn ko lij lo lt ltg lv mai mk ml mr ms my nb-NO ne-NP nl nn-NO oc pa-IN pl pt-BR pt-PT rm ro ru si sk sl sq sr sv-SE ta te th tl tr uk ur uz vi zh-CN zh-TW".split(" ");
 
 const ABOUT_URL = "about:newtab";
 const BASE_URL = "resource://activity-stream/";
 const ACTIVITY_STREAM_PAGES = new Set(["home", "newtab", "welcome"]);
 
 const IS_MAIN_PROCESS = Services.appinfo.processType === Services.appinfo.PROCESS_TYPE_DEFAULT;
 const IS_PRIVILEGED_PROCESS = Services.appinfo.remoteType === E10SUtils.PRIVILEGED_REMOTE_TYPE;
 
 const IS_RELEASE_OR_BETA = AppConstants.RELEASE_OR_BETA;
 
 const PREF_SEPARATE_PRIVILEGED_CONTENT_PROCESS = "browser.tabs.remote.separatePrivilegedContentProcess";
 const PREF_ACTIVITY_STREAM_PRERENDER_ENABLED = "browser.newtabpage.activity-stream.prerender";
 const PREF_ACTIVITY_STREAM_DEBUG = "browser.newtabpage.activity-stream.debug";
 
-
 function AboutNewTabService() {
   Services.obs.addObserver(this, TOPIC_APP_QUIT);
   Services.obs.addObserver(this, TOPIC_LOCALES_CHANGE);
   Services.prefs.addObserver(PREF_SEPARATE_PRIVILEGED_CONTENT_PROCESS, this);
   Services.prefs.addObserver(PREF_ACTIVITY_STREAM_PRERENDER_ENABLED, this);
   if (!IS_RELEASE_OR_BETA) {
     Services.prefs.addObserver(PREF_ACTIVITY_STREAM_DEBUG, this);
   }
@@ -98,35 +97,33 @@ AboutNewTabService.prototype = {
   _overridden: false,
   willNotifyUser: false,
 
   classID: Components.ID("{dfcd2adc-7867-4d3a-ba70-17501f208142}"),
   QueryInterface: ChromeUtils.generateQI([
     Ci.nsIAboutNewTabService,
     Ci.nsIObserver
   ]),
-  _xpcom_categories: [{
-    service: true
-  }],
+  _xpcom_categories: [{service: true}],
 
   observe(subject, topic, data) {
     switch (topic) {
       case "nsPref:changed":
         if (data === PREF_SEPARATE_PRIVILEGED_CONTENT_PROCESS) {
           this._privilegedContentProcess = Services.prefs.getBoolPref(PREF_SEPARATE_PRIVILEGED_CONTENT_PROCESS);
         } else if (data === PREF_ACTIVITY_STREAM_PRERENDER_ENABLED) {
           this._activityStreamPrerender = Services.prefs.getBoolPref(PREF_ACTIVITY_STREAM_PRERENDER_ENABLED);
           this.notifyChange();
         } else if (!IS_RELEASE_OR_BETA && data === PREF_ACTIVITY_STREAM_DEBUG) {
           this._activityStreamDebug = Services.prefs.getBoolPref(PREF_ACTIVITY_STREAM_DEBUG, false);
           this.updatePrerenderedPath();
           this.notifyChange();
         }
         break;
-      case TOPIC_CONTENT_DOCUMENT_INTERACTIVE:
+      case TOPIC_CONTENT_DOCUMENT_INTERACTIVE: {
         const win = subject.defaultView;
 
         // It seems like "content-document-interactive" is triggered multiple
         // times for a single window. The first event always seems to be an
         // HTMLDocument object that contains a non-null window reference
         // whereas the remaining ones seem to be proxied objects.
         // https://searchfox.org/mozilla-central/rev/d2966246905102b36ef5221b0e3cbccf7ea15a86/devtools/server/actors/object.js#100-102
         if (win === null) {
@@ -173,16 +170,17 @@ AboutNewTabService.prototype = {
         // There is a possibility that DOMContentLoaded won't be fired. This
         // unload event (which cannot be cancelled) will attempt to remove
         // the listener for the DOMContentLoaded event.
         const onUnloaded = () => {
           subject.removeEventListener("DOMContentLoaded", onLoaded);
         };
         subject.addEventListener("unload", onUnloaded, {once: true});
         break;
+      }
       case TOPIC_APP_QUIT:
         this.uninit();
         if (IS_MAIN_PROCESS) {
           AboutNewTab.uninit();
         } else if (IS_PRIVILEGED_PROCESS) {
           Services.obs.removeObserver(this, TOPIC_CONTENT_DOCUMENT_INTERACTIVE);
         }
         break;
@@ -272,27 +270,27 @@ AboutNewTabService.prototype = {
     return url;
   },
 
   get newTabURL() {
     return this._newTabURL;
   },
 
   set newTabURL(aNewTabURL) {
-    aNewTabURL = aNewTabURL.trim();
-    if (aNewTabURL === ABOUT_URL) {
+    let newTabURL = aNewTabURL.trim();
+    if (newTabURL === ABOUT_URL) {
       // avoid infinite redirects in case one sets the URL to about:newtab
       this.resetNewTabURL();
       return;
-    } else if (aNewTabURL === "") {
-      aNewTabURL = "about:blank";
+    } else if (newTabURL === "") {
+      newTabURL = "about:blank";
     }
 
     this.toggleActivityStream(false);
-    this._newTabURL = aNewTabURL;
+    this._newTabURL = newTabURL;
     this._overridden = true;
     this.notifyChange();
   },
 
   get overridden() {
     return this._overridden;
   },
 
@@ -306,21 +304,24 @@ AboutNewTabService.prototype = {
 
   get activityStreamDebug() {
     return this._activityStreamDebug;
   },
 
   get activityStreamLocale() {
     // Pick the best available locale to match the app locales
     return Services.locale.negotiateLanguages(
-      Services.locale.getAppLocalesAsLangTags(),
-      ACTIVITY_STREAM_LOCALES,
+      Services.locale.getAppLocalesAsBCP47(),
+      ACTIVITY_STREAM_BCP47,
       // defaultLocale's strings aren't necessarily packaged, but en-US' are
-      "en-US"
-    )[0];
+      "en-US",
+      Services.locale.langNegStrategyLookup
+    // Convert the BCP47 to lang tag, which is what is used in our paths, as a
+    // workaround for bug 1478930 negotiating incorrectly with lang tags
+    )[0].replace(/^(ja-JP-mac)os$/, "$1");
   },
 
   resetNewTabURL() {
     this._overridden = false;
     this._newTabURL = ABOUT_URL;
     this.toggleActivityStream(true, true);
     this.notifyChange();
   },
--- a/browser/components/newtab/bin/prepare-mochitests-dev
+++ b/browser/components/newtab/bin/prepare-mochitests-dev
@@ -21,17 +21,17 @@ fi
 # Compute the mozilla-central path based on whether AS_PINE_TEST_DIR is set
 # (i.e. whether this script has been called from test-merges.js)
 if [ -z ${AS_PINE_TEST_DIR+x} ]; then
   FIREFOX_PATH="$ROOT/../../mozilla-central"
 else
   FIREFOX_PATH=${AS_PINE_TEST_DIR}/mozilla-central
 fi
 
-MC_MODULE_PATH="$FIREFOX_PATH/browser/extensions/activity-stream"
+MC_MODULE_PATH="$FIREFOX_PATH/browser/components/newtab"
 
 # By default, just use mozilla-central + the export.  If ENABLE_MC_AS is set to
 # 1, patch on top of mozilla-central + the export to turn on the AS pref and
 # turn on the tests.  Once AS is on by default in mozilla-central, stuff
 # related to ENABLE_MC_AS can go away entirely.
 ENABLE_MC_AS=${ENABLE_MC_AS-0}
 
 # This will either download or update the local Firefox repo
deleted file mode 100755
--- a/browser/components/newtab/bin/process-system-addon-for-package.js
+++ /dev/null
@@ -1,21 +0,0 @@
-#! /usr/bin/env node
-"use strict";
-
-const MIN_FIREFOX_VERSION = "55.0a1";
-
-/* globals cd, mv, sed */
-require("shelljs/global");
-
-cd(process.argv[2]);
-
-// Convert install.rdf.in to install.rdf without substitutions
-mv("install.rdf.in", "install.rdf");
-sed("-i", /^#filter substitution/, "", "install.rdf");
-sed("-i", /(<em:minVersion>).+(<\/em:minVersion>)/, `$1${MIN_FIREFOX_VERSION}$2`, "install.rdf");
-sed("-i", /(<em:maxVersion>).+(<\/em:maxVersion>)/, "$1*$2", "install.rdf");
-
-// Convert jar.mn to chrome.manifest with just manifest
-mv("jar.mn", "chrome.manifest");
-sed("-i", /^[^%].*$/, "", "chrome.manifest");
-sed("-i", /^% (content.*) %(.*)$/, "$1 $2", "chrome.manifest");
-sed("-i", /^% (resource.*) %.*$/, "$1 .", "chrome.manifest");
--- a/browser/components/newtab/bin/render-activity-stream-html.js
+++ b/browser/components/newtab/bin/render-activity-stream-html.js
@@ -240,13 +240,16 @@ function main() { // eslint-disable-line
 
   if (skippedLocales.length) {
     console.log("\x1b[33m", `Skipped the following locales because they use the same strings as ${DEFAULT_LOCALE} or its language locale: ${skippedLocales.join(", ")}`, "\x1b[0m");
   }
   if (extraLocales.length) {
     console.log("\x1b[33m", `Skipped the following locales because they are not in CENTRAL_LOCALES: ${extraLocales.join(", ")}`, "\x1b[0m");
   }
 
+  // Convert ja-JP-mac lang tag to ja-JP-macos bcp47 to work around bug 1478930
+  const bcp47String = localizedLocales.join(" ").replace(/(ja-JP-mac)/, "$1os");
+
   // Provide some help to copy/paste locales if tests are failing
-  console.log(`\nIf aboutNewTabService tests are failing for unexpected locales, make sure its list is updated:\nconst ACTIVITY_STREAM_LOCALES = "${localizedLocales.join(" ")}".split(" ");`);
+  console.log(`\nIf aboutNewTabService tests are failing for unexpected locales, make sure its list is updated:\nconst ACTIVITY_STREAM_BCP47 = "${bcp47String}".split(" ");`);
 }
 
 main();
deleted file mode 100644
--- a/browser/components/newtab/bin/update-version.js
+++ /dev/null
@@ -1,62 +0,0 @@
-#! /usr/bin/env node
-/* globals cd, sed */
-"use strict";
-
-/**
- * Generate update install.rdf.in in the given directory with a version string
- * composed of YYYY.MM.DD.${minuteOfDay}-${github_commit_hash}.
- *
- * @note The github hash is taken from the github repo in the current directory
- * the script is run in.
- *
- * @note The minute of the day was chosen so that the version number is
- * (more-or-less) consistently increasing (modulo clock-skew and builds that
- * happen within a minute of each other), and although it's UTC, it won't likely
- * be confused with something in a readers own time zone.
- *
- * @example generated version string: 2017.08.28.1217-ebda466c
- */
-const process = require("process");
-require("shelljs/global");
-const simpleGit = require("simple-git")(process.cwd());
-
-const time = new Date();
-const minuteOfDay = time.getUTCHours() * 60 + time.getUTCMinutes();
-
-/**
- * Return the given string padded with 0s out to the given width.
- *
- * XXX we should ditch this function in favor of using padStart once
- * we start requiring Node 8.
- *
- * @param {any} s - the string to pad, will be coerced to String first
- * @param {Number} width - what's the desired width?
- */
-function zeroPadStart(s, width) {
-  let padded = String(s);
-  while (padded.length < width) {
-    padded = `0${padded}`;
-  }
-
-  return padded;
-}
-
-// git rev-parse --short HEAD
-simpleGit.revparse(["--short", "HEAD"], (err, gitHash) => {
-  if (err) {
-    // eslint-disable-next-line no-console
-    console.error(`SimpleGit.revparse failed: ${err}`);
-    throw new Error(`SimpleGit.revparse failed: ${err}`);
-  }
-
-  // eslint-disable-next-line prefer-template
-  let versionString = String(time.getUTCFullYear()) +
-    "." + zeroPadStart(time.getUTCMonth() + 1, 2) +
-    "." + zeroPadStart(time.getUTCDate(), 2) +
-    "." + zeroPadStart(minuteOfDay, 4) +
-    "-" + gitHash.trim();
-
-  cd(process.argv[2]);
-  sed("-i", /(<em:version>).+(<\/em:version>)$/, `$1${versionString}$2`,
-      "install.rdf.in");
-});
--- a/browser/components/newtab/content-src/asrouter/schemas/message-format.md
+++ b/browser/components/newtab/content-src/asrouter/schemas/message-format.md
@@ -2,16 +2,18 @@
 
 Field name | Type     | Required | Description | Example / Note
 ---        | ---      | ---      | ---         | ---
 `id`       | `string` | Yes | A unique identifier for the message that should not conflict with any other previous message | `ONBOARDING_1`
 `template` | `string` | Yes | An id matching an existing Activity Stream Router template | [See example](https://github.com/mozilla/activity-stream/blob/33669c67c2269078a6d3d6d324fb48175d98f634/system-addon/content-src/message-center/templates/SimpleSnippet.jsx)
 `publish_start` | `date` | No | When to start showing the message | `1524474850876`
 `publish_end` | `date` | No | When to stop showing the message | `1524474850876`
 `content` | `object` | Yes | An object containing all variables/props to be rendered in the template. Subset of allowed tags detailed below. | [See example below](#html-subset)
+`bundled` | `integer` | No | The number of messages of the same template this one should be shown with | [See example below](#a-bundled-message-example)
+`order` | `integer` | No | If bundled with other messages of the same template, which order should this one be placed in? Defaults to 0 if no order is desired | [See example below](#a-bundled-message-example)
 `campaign` | `string` | No | Campaign id that the message belongs to | `RustWebAssembly`
 `targeting` | `string` `JEXL` | No | A [JEXL expression](http://normandy.readthedocs.io/en/latest/user/filter_expressions.html#jexl-basics) with all targeting information needed in order to decide if the message is shown | Not yet implemented, [Examples](#targeting-attributes)
 `trigger` | `string` | No | An event or condition upon which the message will be immediately shown. This can be combined with `targeting`. Messages that define a trigger will not be shown during non-trigger-based passive message rotation.
 `frequency` | `object` | No | A definition for frequency cap information for the message
 `frequency.lifetime` | `integer` | No | The maximum number of lifetime impressions for the message.
 `frequency.custom` | `array` | No | An array of frequency cap definition objects including `period`, a time period in milliseconds, and `cap`, a max number of impressions for that period.
 
 ### Message example
@@ -26,16 +28,45 @@ Field name | Type     | Required | Descr
   targeting: "hasFxAccount && !addonsInfo.addons['activity-stream@mozilla.org']",
   frequency: {
     lifetime: 20,
     custom: [{period: "daily", cap: 5}, {period: 3600000, cap: 1}]
   }
 }
 ```
 
+### A Bundled Message example
+The following 2 messages have a `bundled` property, indicating that they should be shown together, since they have the same template. The number `2` indicates that this message should be shown in a bundle of 2 messages of the same template. The order property defines that ONBOARDING_2 should be shown after ONBOARDING_3 in the bundle.
+```javascript
+{
+  id: "ONBOARDING_2",
+  template: "onboarding",
+  bundled: 2,
+  order: 2,
+  content: {
+    title: "Private Browsing",
+    body: "Browse by yourself. Private Browsing with Tracking Protection blocks online trackers that follow you around the web."
+  },
+  targeting: "",
+  trigger: "firstRun"
+}
+{
+  id: "ONBOARDING_3",
+  template: "onboarding",
+  bundled: 2,
+  order: 1,
+  content: {
+    title: "Find it faster",
+    body: "Access all of your favorite search engines with a click. Search the whole Web or just one website from the search box."
+  },
+  targeting: "",
+  trigger: "firstRun"
+}
+```
+
 ### HTML subset
 The following tags are allowed in the content of the snippet: `i, b, u, strong, em, br`.
 
 Links cannot be rendered using regular anchor tags because [Fluent does not allow for href attributes](https://github.com/projectfluent/fluent.js/blob/a03d3aa833660f8c620738b26c80e46b1a4edb05/fluent-dom/src/overlay.js#L13). They will be wrapped in custom tags, for example `<cta>link</cta>` and the url will be provided as part of the payload:
 ```
 {
   "id": "7899",
   "content": {
--- a/browser/components/newtab/content-src/asrouter/schemas/provider-response.schema.json
+++ b/browser/components/newtab/content-src/asrouter/schemas/provider-response.schema.json
@@ -16,16 +16,25 @@
             "type": "string",
             "description": "A unique identifier for the message that should not conflict with any other previous message"
           },
           "template": {
             "type": "string",
             "description": "An id matching an existing Activity Stream Router template",
             "enum": ["simple_snippet"]
           },
+          "bundled": {
+            "type": "integer",
+            "description": "The number of messages of the same template this one should be shown with (optional)"
+          },
+          "order": {
+            "type": "integer",
+            "minimum": 0,
+            "description": "If bundled with other messages of the same template, which order should this one be placed in? (optional - defaults to 0)"
+          },
           "content": {
             "type": "object",
             "description": "An object containing all variables/props to be rendered in the template. See individual template schemas for details."
           },
           "targeting": {
             "type": "string",
             "description": "a JEXL expression representing targeting information"
           },
--- a/browser/components/newtab/docs/v2-system-addon/1.GETTING_STARTED.md
+++ b/browser/components/newtab/docs/v2-system-addon/1.GETTING_STARTED.md
@@ -13,17 +13,17 @@
 If you just want to try out the current version of Activity Stream in Firefox, you can
 install [Firefox Nightly](https://www.mozilla.org/en-US/firefox/channel/desktop/#nightly)
 or any version of Firefox >= 57.0. If you still don't see activity stream, go to `about:config`,
 and make sure the `browser.newtabpage.activity-stream.enabled` pref is set to `true`.
 
 ## Source code and submitting pull requests
 
 A copy of the code in the [system-addon/](../../system-addon/) subdirectory of this repository
-is exported to Mozilla central on a regular basis, which can be found at [browser/extensions/activity-stream](https://searchfox.org/mozilla-central/source/browser/extensions/activity-stream).
+is exported to Mozilla central on a regular basis, which can be found at [browser/components/newtab](https://searchfox.org/mozilla-central/source/browser/components/newtab).
 Keep in mind that some of these files are generated, so if you intend on editing any files, you should
 do so in the Github version.
 
 Pull requests should be sent against the master branch of https://github.com/mozilla/activity-stream,
 NOT against Mozilla central.
 
 ## Prerequisites for development
 
--- a/browser/components/newtab/lib/ASRouter.jsm
+++ b/browser/components/newtab/lib/ASRouter.jsm
@@ -13,20 +13,21 @@ ChromeUtils.defineModuleGetter(this, "AS
   "resource://activity-stream/lib/ASRouterTargeting.jsm");
 
 const INCOMING_MESSAGE_NAME = "ASRouter:child-to-parent";
 const OUTGOING_MESSAGE_NAME = "ASRouter:parent-to-child";
 const ONE_HOUR_IN_MS = 60 * 60 * 1000;
 const SNIPPETS_ENDPOINT_PREF = "browser.newtabpage.activity-stream.asrouter.snippetsUrl";
 // List of hosts for endpoints that serve router messages.
 // Key is allowed host, value is a name for the endpoint host.
-const WHITELIST_HOSTS = {
+const DEFAULT_WHITELIST_HOSTS = {
   "activity-stream-icons.services.mozilla.com": "production",
   "snippets-admin.mozilla.org": "preview"
 };
+const SNIPPETS_ENDPOINT_WHITELIST = "browser.newtab.activity-stream.asrouter.whitelistHosts";
 
 const MessageLoaderUtils = {
   /**
    * _localLoader - Loads messages for a local provider (i.e. one that lives in mozilla central)
    *
    * @param {obj} provider An AS router provider
    * @param {Array} provider.messages An array of messages
    * @returns {Array} the array of messages
@@ -224,16 +225,17 @@ class _ASRouter {
    * @param {obj} storage an AS storage instance
    * @memberof _ASRouter
    */
   async init(channel, storage) {
     this.messageChannel = channel;
     this.messageChannel.addMessageListener(INCOMING_MESSAGE_NAME, this.onMessage);
     this._addASRouterPrefListener();
     this._storage = storage;
+    this.WHITELIST_HOSTS = this._loadSnippetsWhitelistHosts();
 
     const blockList = await this._storage.get("blockList") || [];
     const impressions = await this._storage.get("impressions") || {};
     await this.setState({blockList, impressions});
     await this.loadMessagesFromAllProviders();
 
     // sets .initialized to true and resolves .waitForInitialized promise
     this._finishInitializing();
@@ -278,18 +280,22 @@ class _ASRouter {
     }
     if (!message) {
       // If there was no messages with this trigger, try finding a regular targeted message
       message = await ASRouterTargeting.findMatchingMessage({messages, impressions, target});
     }
     return message;
   }
 
+  _orderBundle(bundle) {
+    return bundle.sort((a, b) => a.order - b.order);
+  }
+
   async _getBundledMessages(originalMessage, target, data, force = false) {
-    let result = [{content: originalMessage.content, id: originalMessage.id}];
+    let result = [{content: originalMessage.content, id: originalMessage.id, order: originalMessage.order || 0}];
 
     // First, find all messages of same template. These are potential matching targeting candidates
     let bundledMessagesOfSameTemplate = this._getUnblockedMessages()
                                           .filter(msg => msg.bundled && msg.template === originalMessage.template && msg.id !== originalMessage.id);
 
     if (force) {
       // Forcefully show the messages without targeting matching - this is for about:newtab#asrouter to show the messages
       for (const message of bundledMessagesOfSameTemplate) {
@@ -304,30 +310,31 @@ class _ASRouter {
         // Find a message that matches the targeting context - or break if there are no matching messages
         const message = await this._findMessage(bundledMessagesOfSameTemplate, target, data);
         if (!message) {
           /* istanbul ignore next */ // Code coverage in mochitests
           break;
         }
         // Only copy the content of the message (that's what the UI cares about)
         // Also delete the message we picked so we don't pick it again
-        result.push({content: message.content, id: message.id});
+        result.push({content: message.content, id: message.id, order: message.order || 0});
         bundledMessagesOfSameTemplate.splice(bundledMessagesOfSameTemplate.findIndex(msg => msg.id === message.id), 1);
         // Stop once we have enough messages to fill a bundle
         if (result.length === originalMessage.bundled) {
           break;
         }
       }
     }
 
     // If we did not find enough messages to fill the bundle, do not send the bundle down
     if (result.length < originalMessage.bundled) {
       return null;
     }
-    return {bundle: result, provider: originalMessage.provider, template: originalMessage.template};
+
+    return {bundle: this._orderBundle(result), provider: originalMessage.provider, template: originalMessage.template};
   }
 
   _getUnblockedMessages() {
     let {state} = this;
     return state.messages.filter(item => !state.blockList.includes(item.id));
   }
 
   async _sendMessageToTarget(message, target, data, force = false) {
@@ -463,28 +470,52 @@ class _ASRouter {
     } else {
       win.openLinkIn(url, where, params);
     }
   }
 
   _validPreviewEndpoint(url) {
     try {
       const endpoint = new URL(url);
-      if (!WHITELIST_HOSTS[endpoint.host]) {
+      if (!this.WHITELIST_HOSTS[endpoint.host]) {
         Cu.reportError(`The preview URL host ${endpoint.host} is not in the whitelist.`);
       }
       if (endpoint.protocol !== "https:") {
         Cu.reportError("The URL protocol is not https.");
       }
-      return (endpoint.protocol === "https:" && WHITELIST_HOSTS[endpoint.host]);
+      return (endpoint.protocol === "https:" && this.WHITELIST_HOSTS[endpoint.host]);
     } catch (e) {
       return false;
     }
   }
 
+  _loadSnippetsWhitelistHosts() {
+    let additionalHosts = [];
+    const whitelistPrefValue = Services.prefs.getStringPref(SNIPPETS_ENDPOINT_WHITELIST, "");
+    try {
+      additionalHosts = JSON.parse(whitelistPrefValue);
+    } catch (e) {
+      if (whitelistPrefValue) {
+        Cu.reportError(`Pref ${SNIPPETS_ENDPOINT_WHITELIST} value is not valid JSON`);
+      }
+    }
+
+    if (!additionalHosts.length) {
+      return DEFAULT_WHITELIST_HOSTS;
+    }
+
+    // If there are additional hosts we want to whitelist, add them as
+    // `preview` so that the updateCycle is 0
+    return additionalHosts.reduce((whitelist_hosts, host) => {
+      whitelist_hosts[host] = "preview";
+      Services.console.logStringMessage(`Adding ${host} to whitelist hosts.`);
+      return whitelist_hosts;
+    }, {...DEFAULT_WHITELIST_HOSTS});
+  }
+
   async _addPreviewEndpoint(url) {
     const providers = [...this.state.providers];
     if (this._validPreviewEndpoint(url) && !providers.find(p => p.url === url)) {
       // Set update cycle to 0 to fetch new content on every page refresh
       providers.push({id: "preview", type: "remote", url, updateCycleInMs: 0});
       await this.setState({providers});
     }
   }
--- a/browser/components/newtab/lib/ASRouterTargeting.jsm
+++ b/browser/components/newtab/lib/ASRouterTargeting.jsm
@@ -35,16 +35,21 @@ const TopFrecentSitesCache = {
           topsiteFrecency: FRECENT_SITES_MIN_FRECENCY,
           onePerDomain: true,
           includeFavicon: false
         });
         this._lastUpdated = now;
       }
       resolve(this._topFrecentSites);
     });
+  },
+  // For testing
+  expire() {
+    this._lastUpdated = 0;
+    this._topFrecentSites = null;
   }
 };
 
 /**
  * removeRandomItemFromArray - Removes a random item from the array and returns it.
  *
  * @param {Array} arr An array of items
  * @returns one of the items in the array
@@ -210,9 +215,11 @@ this.ASRouterTargeting = {
       ) {
         match = candidate;
       }
     }
     return match;
   }
 };
 
-this.EXPORTED_SYMBOLS = ["ASRouterTargeting", "removeRandomItemFromArray"];
+// Export for testing
+this.TopFrecentSitesCache = TopFrecentSitesCache;
+this.EXPORTED_SYMBOLS = ["ASRouterTargeting", "TopFrecentSitesCache"];
--- a/browser/components/newtab/lib/ActivityStreamMessageChannel.jsm
+++ b/browser/components/newtab/lib/ActivityStreamMessageChannel.jsm
@@ -1,15 +1,16 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
 ChromeUtils.import("resource:///modules/AboutNewTab.jsm");
+/* globals RemotePages */ // Remove when updating eslint-plugin-mozilla 0.14.0+
 ChromeUtils.import("resource://gre/modules/remotepagemanager/RemotePageManagerParent.jsm");
 
 const {actionCreators: ac, actionTypes: at, actionUtils: au} = ChromeUtils.import("resource://activity-stream/common/Actions.jsm", {});
 
 const ABOUT_NEW_TAB_URL = "about:newtab";
 const ABOUT_HOME_URL = "about:home";
 
 const DEFAULT_OPTIONS = {
--- a/browser/components/newtab/lib/OnboardingMessageProvider.jsm
+++ b/browser/components/newtab/lib/OnboardingMessageProvider.jsm
@@ -3,63 +3,67 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 "use strict";
 
 const ONBOARDING_MESSAGES = [
   {
     id: "ONBOARDING_1",
     template: "onboarding",
     bundled: 3,
+    order: 2,
     content: {
       title: "Private Browsing",
       text: "Browse by yourself. Private Browsing with Tracking Protection blocks online trackers that follow you around the web.",
       icon: "privatebrowsing",
       button_label: "Try It Now",
       button_action: "OPEN_PRIVATE_BROWSER_WINDOW"
     },
     trigger: "firstRun"
   },
   {
     id: "ONBOARDING_2",
     template: "onboarding",
     bundled: 3,
+    order: 3,
     content: {
       title: "Screenshots",
       text: "Take, save and share screenshots - without leaving Firefox. Capture a region or an entire page as you browse. Then save to the web for easy access and sharing.",
       icon: "screenshots",
       button_label: "Try It Now",
       button_action: "OPEN_URL",
       button_action_params: "https://screenshots.firefox.com/#tour"
     },
     trigger: "firstRun"
   },
   {
     id: "ONBOARDING_3",
     template: "onboarding",
     bundled: 3,
+    order: 1,
     content: {
       title: "Add-ons",
       text: "Add even more features that make Firefox work harder for you. Compare prices, check the weather or express your personality with a custom theme.",
       icon: "addons",
       button_label: "Try It Now",
       button_action: "OPEN_ABOUT_PAGE",
       button_action_params: "addons"
     },
     targeting: "isInExperimentCohort == 1",
     trigger: "firstRun"
   },
   {
     id: "ONBOARDING_4",
     template: "onboarding",
     bundled: 3,
+    order: 1,
     content: {
-      title: "Extensions",
-      text: "Make browsing faster, smarter, or safer with browser apps. Protect passwords, find deals, download videos, and much more. You can even block annoying ads with extensions like Ghostery.",
+      title: "Block Ads with Ghostery",
+      text: "Browse faster, smarter, or safer with extensions like Ghostery, which lets you block annoying ads.",
       icon: "gift",
-      button_label: "Get Ghostery",
+      button_label: "Try It Now",
       button_action: "OPEN_URL",
       button_action_params: "https://addons.mozilla.org/en-US/firefox/addon/ghostery/"
     },
     targeting: "isInExperimentCohort == 2",
     trigger: "firstRun"
   }
 ];
 
--- a/browser/components/newtab/locales/az/strings.properties
+++ b/browser/components/newtab/locales/az/strings.properties
@@ -100,17 +100,17 @@ prefs_topsites_description=Ən çox ziyarət etdiyiniz saytlar
 prefs_topstories_description2=İnternetin ən yaxşı məzmunları, sizə görə fərdiləşdirilmiş
 prefs_topstories_options_sponsored_label=Sponsorlaşdırılmış Hekayələr
 prefs_topstories_sponsored_learn_more=Ətraflı öyrən
 prefs_highlights_description=Saxladığınız və ya ziyarət etdiyiniz saytlardan seçmələr
 prefs_highlights_options_visited_label=Baxılmış Səhifələr
 prefs_highlights_options_download_label=Son Endirmələr
 prefs_highlights_options_pocket_label=Pocket-ə Saxlanılan Səhifələr
 prefs_snippets_description=Mozilla və Firefoxdan yeniliklər
-settings_pane_button_label=Yeni Vərəq səhifənizi özəlləşdirin
+settings_pane_button_label=Yeni Vərəq səhifənizi fərdiləşdirin
 settings_pane_topsites_header=Qabaqcıl Saytlar
 settings_pane_highlights_header=Seçilmişlər
 settings_pane_highlights_options_bookmarks=Əlfəcinlər
 # LOCALIZATION NOTE(settings_pane_snippets_header): For the "Snippets" feature
 # traditionally on about:home. Alternative translation options: "Small Note" or
 # something that expresses the idea of "a small message, shortened from
 # something else, and non-essential but also not entirely trivial and useless."
 settings_pane_snippets_header=Hissələr
--- a/browser/components/newtab/locales/da/strings.properties
+++ b/browser/components/newtab/locales/da/strings.properties
@@ -201,16 +201,18 @@ firstrun_learn_more_link=Læs mere om Firefox-konti
 # LOCALIZATION NOTE (firstrun_form_header and firstrun_form_sub_header):
 # firstrun_form_sub_header is a continuation of firstrun_form_header, they are one sentence.
 # firstrun_form_header is displayed more boldly as the call to action.
 firstrun_form_header=Indtast din mailadresse
 firstrun_form_sub_header=for at fortsætte til Firefox Sync.
 
 firstrun_email_input_placeholder=Mailadresse
 
+firstrun_invalid_input=En gyldig mailadresse er påkrævet
+
 # LOCALIZATION NOTE (firstrun_extra_legal_links): {terms} is equal to firstrun_terms_of_service, and
 # {privacy} is equal to firstrun_privacy_notice. {terms} and {privacy} are clickable links.
 firstrun_extra_legal_links=Ved at fortsætte godkender du vores {terms} og {privacy}.
 firstrun_terms_of_service=tjenestevilkår
 firstrun_privacy_notice=privatlivspolitik
 
 firstrun_continue_to_login=Fortsæt
 firstrun_skip_login=Spring dette trin over
--- a/browser/components/newtab/locales/es-ES/strings.properties
+++ b/browser/components/newtab/locales/es-ES/strings.properties
@@ -186,16 +186,17 @@ firstrun_learn_more_link=Descubra más sobre las Cuentas de Firefox
 # LOCALIZATION NOTE (firstrun_form_header and firstrun_form_sub_header):
 # firstrun_form_sub_header is a continuation of firstrun_form_header, they are one sentence.
 # firstrun_form_header is displayed more boldly as the call to action.
 firstrun_form_header=Introduzca su correo electrónico
 firstrun_form_sub_header=para acceder a Firefox Sync.
 
 firstrun_email_input_placeholder=Correo electrónico
 
+firstrun_invalid_input=Se requiere un correo válido
 
 # LOCALIZATION NOTE (firstrun_extra_legal_links): {terms} is equal to firstrun_terms_of_service, and
 # {privacy} is equal to firstrun_privacy_notice. {terms} and {privacy} are clickable links.
 firstrun_extra_legal_links=Al continuar aceptas los {terms} y el {privacy}.
 firstrun_terms_of_service=Términos del servicio
 firstrun_privacy_notice=Aviso de privacidad
 
 firstrun_continue_to_login=Continuar
--- a/browser/components/newtab/locales/gn/strings.properties
+++ b/browser/components/newtab/locales/gn/strings.properties
@@ -186,16 +186,18 @@ firstrun_learn_more_link=Eikuaave Firefo
 # LOCALIZATION NOTE (firstrun_form_header and firstrun_form_sub_header):
 # firstrun_form_sub_header is a continuation of firstrun_form_header, they are one sentence.
 # firstrun_form_header is displayed more boldly as the call to action.
 firstrun_form_header=Emoinge ne ñandutiveve
 firstrun_form_sub_header=eike hag̃ua Firefox Sync-pe.
 
 firstrun_email_input_placeholder=Ñandutiveve
 
+firstrun_invalid_input=Eikotevẽ peteĩ ñanduti veve oikóva
+
 # LOCALIZATION NOTE (firstrun_extra_legal_links): {terms} is equal to firstrun_terms_of_service, and
 # {privacy} is equal to firstrun_privacy_notice. {terms} and {privacy} are clickable links.
 firstrun_extra_legal_links=Ejapóva, emoneĩ ko'ã {terms} ha {privacy}.
 firstrun_terms_of_service=Mba'epytyvõrã ñemboguata
 firstrun_privacy_notice=Ñemigua purureko
 
 firstrun_continue_to_login=Eku'ejey
 firstrun_skip_login=Ehejánte kóva
--- a/browser/components/newtab/locales/ja-JP-mac/strings.properties
+++ b/browser/components/newtab/locales/ja-JP-mac/strings.properties
@@ -186,16 +186,18 @@ firstrun_learn_more_link=Firefox Accounts に関する詳細情報
 # LOCALIZATION NOTE (firstrun_form_header and firstrun_form_sub_header):
 # firstrun_form_sub_header is a continuation of firstrun_form_header, they are one sentence.
 # firstrun_form_header is displayed more boldly as the call to action.
 firstrun_form_header=メールアドレスを入力してください
 firstrun_form_sub_header=Firefox Sync の利用を続けるために必要です
 
 firstrun_email_input_placeholder=メールアドレス
 
+firstrun_invalid_input=メールアドレスを正しく入力してください
+
 # LOCALIZATION NOTE (firstrun_extra_legal_links): {terms} is equal to firstrun_terms_of_service, and
 # {privacy} is equal to firstrun_privacy_notice. {terms} and {privacy} are clickable links.
 firstrun_extra_legal_links=続行すると、{terms} と {privacy} に同意したものとみなします。
 firstrun_terms_of_service=サービス利用規約
 firstrun_privacy_notice=プライバシーに関する通知
 
 firstrun_continue_to_login=続ける
 firstrun_skip_login=この手順をスキップ
--- a/browser/components/newtab/locales/ja/strings.properties
+++ b/browser/components/newtab/locales/ja/strings.properties
@@ -186,16 +186,18 @@ firstrun_learn_more_link=Firefox Accounts に関する詳細情報
 # LOCALIZATION NOTE (firstrun_form_header and firstrun_form_sub_header):
 # firstrun_form_sub_header is a continuation of firstrun_form_header, they are one sentence.
 # firstrun_form_header is displayed more boldly as the call to action.
 firstrun_form_header=メールアドレスを入力してください
 firstrun_form_sub_header=Firefox Sync の利用を続けるために必要です
 
 firstrun_email_input_placeholder=メールアドレス
 
+firstrun_invalid_input=メールアドレスを正しく入力してください
+
 # LOCALIZATION NOTE (firstrun_extra_legal_links): {terms} is equal to firstrun_terms_of_service, and
 # {privacy} is equal to firstrun_privacy_notice. {terms} and {privacy} are clickable links.
 firstrun_extra_legal_links=続行すると、{terms} と {privacy} に同意したものとみなします。
 firstrun_terms_of_service=サービス利用規約
 firstrun_privacy_notice=プライバシーに関する通知
 
 firstrun_continue_to_login=続ける
 firstrun_skip_login=この手順をスキップ
--- a/browser/components/newtab/locales/ta/strings.properties
+++ b/browser/components/newtab/locales/ta/strings.properties
@@ -186,16 +186,18 @@ firstrun_learn_more_link=பயர்பாக்சு கணக்கைப் பற்றி மேலும் தெரிந்து கொள்ளவும்
 # LOCALIZATION NOTE (firstrun_form_header and firstrun_form_sub_header):
 # firstrun_form_sub_header is a continuation of firstrun_form_header, they are one sentence.
 # firstrun_form_header is displayed more boldly as the call to action.
 firstrun_form_header=உங்களின் மின்னஞ்சலை உள்ளிடுக
 firstrun_form_sub_header=பயர்பாக்சு ஒத்திசையைத் தொடர.
 
 firstrun_email_input_placeholder=மின்னஞ்சல்
 
+firstrun_invalid_input=நம்பகரமான மின்னஞ்சல் தேவை
+
 # LOCALIZATION NOTE (firstrun_extra_legal_links): {terms} is equal to firstrun_terms_of_service, and
 # {privacy} is equal to firstrun_privacy_notice. {terms} and {privacy} are clickable links.
 firstrun_extra_legal_links=தொடர்வதன் மூலம், தாங்கள் {terms} மற்றும் {privacy} ஒப்புக்கொள்கின்றீர்கள்.
 firstrun_terms_of_service=சேவையின் விதிமுறைகள்
 firstrun_privacy_notice=தனியுரிமை அறிவிப்பு
 
 firstrun_continue_to_login=தொடர்க
 firstrun_skip_login=இந்த படிநிலையைத் தவிர்
--- a/browser/components/newtab/locales/zh-TW/strings.properties
+++ b/browser/components/newtab/locales/zh-TW/strings.properties
@@ -35,17 +35,17 @@ menu_action_open_private_window=用新隱私視窗開啟
 menu_action_dismiss=隱藏
 menu_action_delete=從瀏覽紀錄刪除
 menu_action_pin=釘選
 menu_action_unpin=取消釘選
 confirm_history_delete_p1=您確定要刪除此頁面的所有瀏覽紀錄?
 # LOCALIZATION NOTE (confirm_history_delete_notice_p2): this string is displayed in
 # the same dialog as confirm_history_delete_p1. "This action" refers to deleting a
 # page from history.
-confirm_history_delete_notice_p2=無法還原此操作。
+confirm_history_delete_notice_p2=此動作無法復原。
 menu_action_save_to_pocket=儲存至 Pocket
 menu_action_delete_pocket=從 Pocket 刪除
 menu_action_archive_pocket=在 Pocket 裡封存
 
 # LOCALIZATION NOTE (menu_action_show_file_*): These are platform specific strings
 # found in the context menu of an item that has been downloaded. The intention behind
 # "this action" is that it will show where the downloaded file exists on the file system
 # for each operating system.
@@ -150,17 +150,17 @@ highlights_empty_state=開始上網,我們就會把您在網路上發現的好文章、影片、剛加入書籤的頁面顯示於此。
 # {provider} is replaced by the name of the content provider for this section.
 topstories_empty_state=所有文章都讀完啦!晚點再來,{provider} 將提供更多推薦故事。等不及了?選擇熱門主題,看看 Web 上各式精采資訊。
 
 # LOCALIZATION NOTE (manual_migration_explanation2): This message is shown to encourage users to
 # import their browser profile from another browser they might be using.
 manual_migration_explanation2=試試將其他瀏覽器的書籤、瀏覽記錄與密碼匯入 Firefox。
 # LOCALIZATION NOTE (manual_migration_cancel_button): This message is shown on a button that cancels the
 # process of importing another browser’s profile into Firefox.
-manual_migration_cancel_button=不必了
+manual_migration_cancel_button=不要,謝謝
 # LOCALIZATION NOTE (manual_migration_import_button): This message is shown on a button that starts the process
 # of importing another browser’s profile profile into Firefox.
 manual_migration_import_button=立即匯入
 
 # LOCALIZATION NOTE (error_fallback_default_*): This message and suggested
 # action link are shown in each section of UI that fails to render
 error_fallback_default_info=唉唷,載入內容時發生錯誤。
 error_fallback_default_refresh_suggestion=請重新整理頁面再試一次。
@@ -182,17 +182,17 @@ section_menu_action_privacy_notice=隱私權公告
 firstrun_title=Firefox 隨身帶著走
 firstrun_content=在您的任何裝置上取得書籤、瀏覽紀錄、密碼及其他設定。
 firstrun_learn_more_link=了解 Firefox Accounts 的更多資訊
 
 # LOCALIZATION NOTE (firstrun_form_header and firstrun_form_sub_header):
 # firstrun_form_sub_header is a continuation of firstrun_form_header, they are one sentence.
 # firstrun_form_header is displayed more boldly as the call to action.
 firstrun_form_header=輸入您的電子郵件地址
-firstrun_form_sub_header=繼續前往 Firefox Sync。
+firstrun_form_sub_header=繼續前往 Firefox Sync
 
 firstrun_email_input_placeholder=電子郵件
 
 firstrun_invalid_input=必須輸入有效的電子郵件地址
 
 # LOCALIZATION NOTE (firstrun_extra_legal_links): {terms} is equal to firstrun_terms_of_service, and
 # {privacy} is equal to firstrun_privacy_notice. {terms} and {privacy} are clickable links.
 firstrun_extra_legal_links=若繼續,代表您同意{terms}及{privacy}。
--- a/browser/components/newtab/mochitest.sh
+++ b/browser/components/newtab/mochitest.sh
@@ -10,15 +10,14 @@ cd /mozilla-central && hg pull && hg upd
 
 # Build Activity Stream and copy the output to m-c
 cd /activity-stream && npm install . && npm run buildmc
 
 # Build latest m-c with Activity Stream changes
 cd /mozilla-central && ./mach build \
   && ./mach test browser_parsable_css \
   && ./mach lint -l eslint -l codespell browser/components/newtab \
-  && ./mach test browser/components/newtab --headless \
   && ./mach test browser/components/newtab/test/browser --headless \
   && ./mach test browser/components/newtab/test/xpcshell \
   && ./mach test browser/components/preferences/in-content/tests/browser_hometab_restore_defaults.js --headless \
   && ./mach test browser/components/preferences/in-content/tests/browser_newtab_menu.js --headless \
   && ./mach test browser/components/enterprisepolicies/tests/browser/browser_policy_set_homepage.js --headless \
   && ./mach test browser/components/preferences/in-content/tests/browser_search_subdialogs_within_preferences_1.js --headless
--- a/browser/components/newtab/package.json
+++ b/browser/components/newtab/package.json
@@ -85,37 +85,36 @@
   ],
   "license": "MPL-2.0",
   "main": "bootstrap.js",
   "repository": "mozilla/activity-stream",
   "config": {
     "mc_dir": "../mozilla-central"
   },
   "scripts": {
-    "mochitest": "(cd $npm_package_config_mc_dir && ./mach mochitest browser/extensions/activity-stream/test/functional/mochitest --headless)",
-    "mochitest-debug": "(cd $npm_package_config_mc_dir && ./mach mochitest --jsdebugger browser/extensions/activity-stream/test/functional/mochitest)",
+    "mochitest": "(cd $npm_package_config_mc_dir && ./mach mochitest browser/components/newtab/test/browser --headless)",
+    "mochitest-debug": "(cd $npm_package_config_mc_dir && ./mach mochitest --jsdebugger browser/components/newtab/test/browser)",
     "bundle": "npm-run-all bundle:*",
     "bundle:locales": "pontoon-to-json --src locales --dest data",
     "bundle:webpack": "webpack --config webpack.system-addon.config.js",
     "bundle:css": "node-sass --source-map true --source-map-contents content-src/styles -o css",
     "bundle:html": "rimraf prerendered && webpack --config webpack.prerender.config.js && node ./bin/render-activity-stream-html.js",
     "buildmc": "npm-run-all buildmc:*",
-    "prebuildmc": "rimraf $npm_package_config_mc_dir/browser/extensions/activity-stream/",
+    "prebuildmc": "rimraf $npm_package_config_mc_dir/browser/components/newtab/",
     "buildmc:bundle": "npm run  bundle",
-    "buildmc:copy": "rsync --exclude-from .mcignore -a . $npm_package_config_mc_dir/browser/extensions/activity-stream/",
-    "buildmc:version": "node ./bin/update-version.js $npm_package_config_mc_dir/browser/extensions/activity-stream",
+    "buildmc:copy": "rsync --exclude-from .mcignore -a . $npm_package_config_mc_dir/browser/components/newtab/",
     "buildmc:stringsExport": "cp locales/en-US/strings.properties $npm_package_config_mc_dir/browser/locales/en-US/chrome/browser/activity-stream/newtab.properties",
     "buildmc:copyPingCentre": "cpx \"ping-centre/PingCentre.jsm\" $npm_package_config_mc_dir/browser/modules",
     "startmc": "npm-run-all --parallel startmc:*",
     "prestartmc": "npm run buildmc",
-    "startmc:copy": "cpx \"{{,.}*,!(node_modules)/**/{,.}*}\" $npm_package_config_mc_dir/browser/extensions/activity-stream/ -w",
+    "startmc:copy": "cpx \"{{,.}*,!(node_modules)/**/{,.}*}\" $npm_package_config_mc_dir/browser/components/newtab/ -w",
     "startmc:copyPingCentre": "npm run buildmc:copyPingCentre -- -w",
     "startmc:webpack": "npm run bundle:webpack -- -w",
     "startmc:css": "npm run bundle:css && npm run bundle:css -- -w",
-    "importmc": "rsync --exclude-from .mcignore -a $npm_package_config_mc_dir/browser/extensions/activity-stream/ .",
+    "importmc": "rsync --exclude-from .mcignore -a $npm_package_config_mc_dir/browser/components/newtab/ .",
     "testmc": "npm-run-all testmc:*",
     "testmc:lint": "npm run lint",
     "testmc:build": "npm run bundle:webpack && npm run bundle:locales",
     "testmc:unit": "karma start karma.mc.config.js || (cat logs/coverage/text.txt && exit 2)",
     "posttestmc": "cat logs/coverage/text-summary.txt",
     "tddmc": "karma start karma.mc.config.js --tdd",
     "debugcoverage": "open logs/coverage/report-html/index.html",
     "lint": "npm-run-all lint:*",
--- a/browser/components/newtab/prerendered/locales/az/activity-stream-prerendered-noscripts.html
+++ b/browser/components/newtab/prerendered/locales/az/activity-stream-prerendered-noscripts.html
@@ -4,14 +4,14 @@
     <meta charset="utf-8">
     <meta http-equiv="Content-Security-Policy" content="default-src 'none'; script-src 'unsafe-inline' resource: chrome:; connect-src https:; img-src https: data: blob:; style-src 'unsafe-inline';">
     <title>Yeni Vərəq</title>
     <link rel="icon" type="image/png" href="chrome://branding/content/icon32.png"/>
     <link rel="stylesheet" href="chrome://browser/content/contentSearchUI.css" />
     <link rel="stylesheet" href="resource://activity-stream/css/activity-stream.css" />
   </head>
   <body class="activity-stream">
-    <div id="root"><div data-reactroot=""><div class="outer-wrapper fixed-to-top"><main><div class="non-collapsible-section"><div class="search-wrapper"><label for="newtab-search-text" class="search-label"><span class="sr-only"><span>İnternetdə Axtar</span></span></label><input type="search" id="newtab-search-text" maxLength="256" placeholder="İnternetdə Axtar" title="İnternetdə Axtar"/><button id="searchSubmit" class="search-button" title="Axtar"><span class="sr-only"><span>Axtar</span></span></button></div></div><div class="body-wrapper"><div class="sections-list"><section class="collapsible-section top-sites animation-enabled" data-section-id="topsites"><div class="section-top-bar"><h3 class="section-title"><span class="click-target"><span class="icon icon-small-spacer icon-topsites"></span><span>Qabaqcıl Saytlar</span></span></h3><div><button class="context-menu-button icon"><span class="sr-only"><span>Kontekst menyusu bölməsini aç</span></span></button></div></div><div class="section-body"><ul class="top-sites-list"><li class="top-site-outer placeholder "><div class="top-site-inner"><a><div class="tile" aria-hidden="true"><div class="screenshot" style="background-image:none"></div></div><div class="title "><span dir="auto"></span></div></a><button class="context-menu-button edit-button icon" title="Bu saytı düzəlt"></button></div></li><li class="top-site-outer placeholder "><div class="top-site-inner"><a><div class="tile" aria-hidden="true"><div class="screenshot" style="background-image:none"></div></div><div class="title "><span dir="auto"></span></div></a><button class="context-menu-button edit-button icon" title="Bu saytı düzəlt"></button></div></li><li class="top-site-outer placeholder "><div class="top-site-inner"><a><div class="tile" aria-hidden="true"><div class="screenshot" style="background-image:none"></div></div><div class="title "><span dir="auto"></span></div></a><button class="context-menu-button edit-button icon" title="Bu saytı düzəlt"></button></div></li><li class="top-site-outer placeholder "><div class="top-site-inner"><a><div class="tile" aria-hidden="true"><div class="screenshot" style="background-image:none"></div></div><div class="title "><span dir="auto"></span></div></a><button class="context-menu-button edit-button icon" title="Bu saytı düzəlt"></button></div></li><li class="top-site-outer placeholder "><div class="top-site-inner"><a><div class="tile" aria-hidden="true"><div class="screenshot" style="background-image:none"></div></div><div class="title "><span dir="auto"></span></div></a><button class="context-menu-button edit-button icon" title="Bu saytı düzəlt"></button></div></li><li class="top-site-outer placeholder "><div class="top-site-inner"><a><div class="tile" aria-hidden="true"><div class="screenshot" style="background-image:none"></div></div><div class="title "><span dir="auto"></span></div></a><button class="context-menu-button edit-button icon" title="Bu saytı düzəlt"></button></div></li><li class="top-site-outer placeholder hide-for-narrow"><div class="top-site-inner"><a><div class="tile" aria-hidden="true"><div class="screenshot" style="background-image:none"></div></div><div class="title "><span dir="auto"></span></div></a><button class="context-menu-button edit-button icon" title="Bu saytı düzəlt"></button></div></li><li class="top-site-outer placeholder hide-for-narrow"><div class="top-site-inner"><a><div class="tile" aria-hidden="true"><div class="screenshot" style="background-image:none"></div></div><div class="title "><span dir="auto"></span></div></a><button class="context-menu-button edit-button icon" title="Bu saytı düzəlt"></button></div></li></ul><div class="edit-topsites-wrapper"></div></div></section><section class="collapsible-section section normal-cards animation-enabled" data-section-id="topstories"><div class="section-top-bar"><h3 class="section-title"><span class="click-target"><span class="icon icon-small-spacer icon-pocket"></span><span>Pocket məsləhət görür</span></span></h3><div><button class="context-menu-button icon"><span class="sr-only"><span>Kontekst menyusu bölməsini aç</span></span></button></div></div><div class="section-body"><ul class="section-list" style="padding:0"></ul><div class="topic"><span><span>Məşhur Mövzular:</span></span><ul></ul></div></div></section><section class="collapsible-section section normal-cards animation-enabled" data-section-id="highlights"><div class="section-top-bar"><h3 class="section-title"><span class="click-target"><span class="icon icon-small-spacer icon-highlights"></span><span>Seçilmişlər</span></span></h3><div><button class="context-menu-button icon"><span class="sr-only"><span>Kontekst menyusu bölməsini aç</span></span></button></div></div><div class="section-body"><ul class="section-list" style="padding:0"></ul></div></section></div><div class="prefs-button"><button class="icon icon-settings" title="Yeni Vərəq səhifənizi özəlləşdirin"></button></div></div></main></div></div></div>
+    <div id="root"><div data-reactroot=""><div class="outer-wrapper fixed-to-top"><main><div class="non-collapsible-section"><div class="search-wrapper"><label for="newtab-search-text" class="search-label"><span class="sr-only"><span>İnternetdə Axtar</span></span></label><input type="search" id="newtab-search-text" maxLength="256" placeholder="İnternetdə Axtar" title="İnternetdə Axtar"/><button id="searchSubmit" class="search-button" title="Axtar"><span class="sr-only"><span>Axtar</span></span></button></div></div><div class="body-wrapper"><div class="sections-list"><section class="collapsible-section top-sites animation-enabled" data-section-id="topsites"><div class="section-top-bar"><h3 class="section-title"><span class="click-target"><span class="icon icon-small-spacer icon-topsites"></span><span>Qabaqcıl Saytlar</span></span></h3><div><button class="context-menu-button icon"><span class="sr-only"><span>Kontekst menyusu bölməsini aç</span></span></button></div></div><div class="section-body"><ul class="top-sites-list"><li class="top-site-outer placeholder "><div class="top-site-inner"><a><div class="tile" aria-hidden="true"><div class="screenshot" style="background-image:none"></div></div><div class="title "><span dir="auto"></span></div></a><button class="context-menu-button edit-button icon" title="Bu saytı düzəlt"></button></div></li><li class="top-site-outer placeholder "><div class="top-site-inner"><a><div class="tile" aria-hidden="true"><div class="screenshot" style="background-image:none"></div></div><div class="title "><span dir="auto"></span></div></a><button class="context-menu-button edit-button icon" title="Bu saytı düzəlt"></button></div></li><li class="top-site-outer placeholder "><div class="top-site-inner"><a><div class="tile" aria-hidden="true"><div class="screenshot" style="background-image:none"></div></div><div class="title "><span dir="auto"></span></div></a><button class="context-menu-button edit-button icon" title="Bu saytı düzəlt"></button></div></li><li class="top-site-outer placeholder "><div class="top-site-inner"><a><div class="tile" aria-hidden="true"><div class="screenshot" style="background-image:none"></div></div><div class="title "><span dir="auto"></span></div></a><button class="context-menu-button edit-button icon" title="Bu saytı düzəlt"></button></div></li><li class="top-site-outer placeholder "><div class="top-site-inner"><a><div class="tile" aria-hidden="true"><div class="screenshot" style="background-image:none"></div></div><div class="title "><span dir="auto"></span></div></a><button class="context-menu-button edit-button icon" title="Bu saytı düzəlt"></button></div></li><li class="top-site-outer placeholder "><div class="top-site-inner"><a><div class="tile" aria-hidden="true"><div class="screenshot" style="background-image:none"></div></div><div class="title "><span dir="auto"></span></div></a><button class="context-menu-button edit-button icon" title="Bu saytı düzəlt"></button></div></li><li class="top-site-outer placeholder hide-for-narrow"><div class="top-site-inner"><a><div class="tile" aria-hidden="true"><div class="screenshot" style="background-image:none"></div></div><div class="title "><span dir="auto"></span></div></a><button class="context-menu-button edit-button icon" title="Bu saytı düzəlt"></button></div></li><li class="top-site-outer placeholder hide-for-narrow"><div class="top-site-inner"><a><div class="tile" aria-hidden="true"><div class="screenshot" style="background-image:none"></div></div><div class="title "><span dir="auto"></span></div></a><button class="context-menu-button edit-button icon" title="Bu saytı düzəlt"></button></div></li></ul><div class="edit-topsites-wrapper"></div></div></section><section class="collapsible-section section normal-cards animation-enabled" data-section-id="topstories"><div class="section-top-bar"><h3 class="section-title"><span class="click-target"><span class="icon icon-small-spacer icon-pocket"></span><span>Pocket məsləhət görür</span></span></h3><div><button class="context-menu-button icon"><span class="sr-only"><span>Kontekst menyusu bölməsini aç</span></span></button></div></div><div class="section-body"><ul class="section-list" style="padding:0"></ul><div class="topic"><span><span>Məşhur Mövzular:</span></span><ul></ul></div></div></section><section class="collapsible-section section normal-cards animation-enabled" data-section-id="highlights"><div class="section-top-bar"><h3 class="section-title"><span class="click-target"><span class="icon icon-small-spacer icon-highlights"></span><span>Seçilmişlər</span></span></h3><div><button class="context-menu-button icon"><span class="sr-only"><span>Kontekst menyusu bölməsini aç</span></span></button></div></div><div class="section-body"><ul class="section-list" style="padding:0"></ul></div></section></div><div class="prefs-button"><button class="icon icon-settings" title="Yeni Vərəq səhifənizi fərdiləşdirin"></button></div></div></main></div></div></div>
     <div id="snippets-container">
       <div id="snippets"></div>
     </div>
   </body>
 </html>
--- a/browser/components/newtab/prerendered/locales/az/activity-stream-prerendered.html
+++ b/browser/components/newtab/prerendered/locales/az/activity-stream-prerendered.html
@@ -4,17 +4,17 @@
     <meta charset="utf-8">
     <meta http-equiv="Content-Security-Policy" content="default-src 'none'; script-src 'unsafe-inline' resource: chrome:; connect-src https:; img-src https: data: blob:; style-src 'unsafe-inline';">
     <title>Yeni Vərəq</title>
     <link rel="icon" type="image/png" href="chrome://branding/content/icon32.png"/>
     <link rel="stylesheet" href="chrome://browser/content/contentSearchUI.css" />
     <link rel="stylesheet" href="resource://activity-stream/css/activity-stream.css" />
   </head>
   <body class="activity-stream">
-    <div id="root"><div data-reactroot=""><div class="outer-wrapper fixed-to-top"><main><div class="non-collapsible-section"><div class="search-wrapper"><label for="newtab-search-text" class="search-label"><span class="sr-only"><span>İnternetdə Axtar</span></span></label><input type="search" id="newtab-search-text" maxLength="256" placeholder="İnternetdə Axtar" title="İnternetdə Axtar"/><button id="searchSubmit" class="search-button" title="Axtar"><span class="sr-only"><span>Axtar</span></span></button></div></div><div class="body-wrapper"><div class="sections-list"><section class="collapsible-section top-sites animation-enabled" data-section-id="topsites"><div class="section-top-bar"><h3 class="section-title"><span class="click-target"><span class="icon icon-small-spacer icon-topsites"></span><span>Qabaqcıl Saytlar</span></span></h3><div><button class="context-menu-button icon"><span class="sr-only"><span>Kontekst menyusu bölməsini aç</span></span></button></div></div><div class="section-body"><ul class="top-sites-list"><li class="top-site-outer placeholder "><div class="top-site-inner"><a><div class="tile" aria-hidden="true"><div class="screenshot" style="background-image:none"></div></div><div class="title "><span dir="auto"></span></div></a><button class="context-menu-button edit-button icon" title="Bu saytı düzəlt"></button></div></li><li class="top-site-outer placeholder "><div class="top-site-inner"><a><div class="tile" aria-hidden="true"><div class="screenshot" style="background-image:none"></div></div><div class="title "><span dir="auto"></span></div></a><button class="context-menu-button edit-button icon" title="Bu saytı düzəlt"></button></div></li><li class="top-site-outer placeholder "><div class="top-site-inner"><a><div class="tile" aria-hidden="true"><div class="screenshot" style="background-image:none"></div></div><div class="title "><span dir="auto"></span></div></a><button class="context-menu-button edit-button icon" title="Bu saytı düzəlt"></button></div></li><li class="top-site-outer placeholder "><div class="top-site-inner"><a><div class="tile" aria-hidden="true"><div class="screenshot" style="background-image:none"></div></div><div class="title "><span dir="auto"></span></div></a><button class="context-menu-button edit-button icon" title="Bu saytı düzəlt"></button></div></li><li class="top-site-outer placeholder "><div class="top-site-inner"><a><div class="tile" aria-hidden="true"><div class="screenshot" style="background-image:none"></div></div><div class="title "><span dir="auto"></span></div></a><button class="context-menu-button edit-button icon" title="Bu saytı düzəlt"></button></div></li><li class="top-site-outer placeholder "><div class="top-site-inner"><a><div class="tile" aria-hidden="true"><div class="screenshot" style="background-image:none"></div></div><div class="title "><span dir="auto"></span></div></a><button class="context-menu-button edit-button icon" title="Bu saytı düzəlt"></button></div></li><li class="top-site-outer placeholder hide-for-narrow"><div class="top-site-inner"><a><div class="tile" aria-hidden="true"><div class="screenshot" style="background-image:none"></div></div><div class="title "><span dir="auto"></span></div></a><button class="context-menu-button edit-button icon" title="Bu saytı düzəlt"></button></div></li><li class="top-site-outer placeholder hide-for-narrow"><div class="top-site-inner"><a><div class="tile" aria-hidden="true"><div class="screenshot" style="background-image:none"></div></div><div class="title "><span dir="auto"></span></div></a><button class="context-menu-button edit-button icon" title="Bu saytı düzəlt"></button></div></li></ul><div class="edit-topsites-wrapper"></div></div></section><section class="collapsible-section section normal-cards animation-enabled" data-section-id="topstories"><div class="section-top-bar"><h3 class="section-title"><span class="click-target"><span class="icon icon-small-spacer icon-pocket"></span><span>Pocket məsləhət görür</span></span></h3><div><button class="context-menu-button icon"><span class="sr-only"><span>Kontekst menyusu bölməsini aç</span></span></button></div></div><div class="section-body"><ul class="section-list" style="padding:0"></ul><div class="topic"><span><span>Məşhur Mövzular:</span></span><ul></ul></div></div></section><section class="collapsible-section section normal-cards animation-enabled" data-section-id="highlights"><div class="section-top-bar"><h3 class="section-title"><span class="click-target"><span class="icon icon-small-spacer icon-highlights"></span><span>Seçilmişlər</span></span></h3><div><button class="context-menu-button icon"><span class="sr-only"><span>Kontekst menyusu bölməsini aç</span></span></button></div></div><div class="section-body"><ul class="section-list" style="padding:0"></ul></div></section></div><div class="prefs-button"><button class="icon icon-settings" title="Yeni Vərəq səhifənizi özəlləşdirin"></button></div></div></main></div></div></div>
+    <div id="root"><div data-reactroot=""><div class="outer-wrapper fixed-to-top"><main><div class="non-collapsible-section"><div class="search-wrapper"><label for="newtab-search-text" class="search-label"><span class="sr-only"><span>İnternetdə Axtar</span></span></label><input type="search" id="newtab-search-text" maxLength="256" placeholder="İnternetdə Axtar" title="İnternetdə Axtar"/><button id="searchSubmit" class="search-button" title="Axtar"><span class="sr-only"><span>Axtar</span></span></button></div></div><div class="body-wrapper"><div class="sections-list"><section class="collapsible-section top-sites animation-enabled" data-section-id="topsites"><div class="section-top-bar"><h3 class="section-title"><span class="click-target"><span class="icon icon-small-spacer icon-topsites"></span><span>Qabaqcıl Saytlar</span></span></h3><div><button class="context-menu-button icon"><span class="sr-only"><span>Kontekst menyusu bölməsini aç</span></span></button></div></div><div class="section-body"><ul class="top-sites-list"><li class="top-site-outer placeholder "><div class="top-site-inner"><a><div class="tile" aria-hidden="true"><div class="screenshot" style="background-image:none"></div></div><div class="title "><span dir="auto"></span></div></a><button class="context-menu-button edit-button icon" title="Bu saytı düzəlt"></button></div></li><li class="top-site-outer placeholder "><div class="top-site-inner"><a><div class="tile" aria-hidden="true"><div class="screenshot" style="background-image:none"></div></div><div class="title "><span dir="auto"></span></div></a><button class="context-menu-button edit-button icon" title="Bu saytı düzəlt"></button></div></li><li class="top-site-outer placeholder "><div class="top-site-inner"><a><div class="tile" aria-hidden="true"><div class="screenshot" style="background-image:none"></div></div><div class="title "><span dir="auto"></span></div></a><button class="context-menu-button edit-button icon" title="Bu saytı düzəlt"></button></div></li><li class="top-site-outer placeholder "><div class="top-site-inner"><a><div class="tile" aria-hidden="true"><div class="screenshot" style="background-image:none"></div></div><div class="title "><span dir="auto"></span></div></a><button class="context-menu-button edit-button icon" title="Bu saytı düzəlt"></button></div></li><li class="top-site-outer placeholder "><div class="top-site-inner"><a><div class="tile" aria-hidden="true"><div class="screenshot" style="background-image:none"></div></div><div class="title "><span dir="auto"></span></div></a><button class="context-menu-button edit-button icon" title="Bu saytı düzəlt"></button></div></li><li class="top-site-outer placeholder "><div class="top-site-inner"><a><div class="tile" aria-hidden="true"><div class="screenshot" style="background-image:none"></div></div><div class="title "><span dir="auto"></span></div></a><button class="context-menu-button edit-button icon" title="Bu saytı düzəlt"></button></div></li><li class="top-site-outer placeholder hide-for-narrow"><div class="top-site-inner"><a><div class="tile" aria-hidden="true"><div class="screenshot" style="background-image:none"></div></div><div class="title "><span dir="auto"></span></div></a><button class="context-menu-button edit-button icon" title="Bu saytı düzəlt"></button></div></li><li class="top-site-outer placeholder hide-for-narrow"><div class="top-site-inner"><a><div class="tile" aria-hidden="true"><div class="screenshot" style="background-image:none"></div></div><div class="title "><span dir="auto"></span></div></a><button class="context-menu-button edit-button icon" title="Bu saytı düzəlt"></button></div></li></ul><div class="edit-topsites-wrapper"></div></div></section><section class="collapsible-section section normal-cards animation-enabled" data-section-id="topstories"><div class="section-top-bar"><h3 class="section-title"><span class="click-target"><span class="icon icon-small-spacer icon-pocket"></span><span>Pocket məsləhət görür</span></span></h3><div><button class="context-menu-button icon"><span class="sr-only"><span>Kontekst menyusu bölməsini aç</span></span></button></div></div><div class="section-body"><ul class="section-list" style="padding:0"></ul><div class="topic"><span><span>Məşhur Mövzular:</span></span><ul></ul></div></div></section><section class="collapsible-section section normal-cards animation-enabled" data-section-id="highlights"><div class="section-top-bar"><h3 class="section-title"><span class="click-target"><span class="icon icon-small-spacer icon-highlights"></span><span>Seçilmişlər</span></span></h3><div><button class="context-menu-button icon"><span class="sr-only"><span>Kontekst menyusu bölməsini aç</span></span></button></div></div><div class="section-body"><ul class="section-list" style="padding:0"></ul></div></section></div><div class="prefs-button"><button class="icon icon-settings" title="Yeni Vərəq səhifənizi fərdiləşdirin"></button></div></div></main></div></div></div>
     <div id="snippets-container">
       <div id="snippets"></div>
     </div>
     <script>
 // Don't directly load the following scripts as part of html to let the page
 // finish loading to render the content sooner.
 for (const src of [
   "resource://activity-stream/prerendered/static/activity-stream-initial-state.js",
--- a/browser/components/newtab/prerendered/locales/az/activity-stream-strings.js
+++ b/browser/components/newtab/prerendered/locales/az/activity-stream-strings.js
@@ -46,17 +46,17 @@ window.gActivityStreamStrings = {
   "prefs_topstories_description2": "İnternetin ən yaxşı məzmunları, sizə görə fərdiləşdirilmiş",
   "prefs_topstories_options_sponsored_label": "Sponsorlaşdırılmış Hekayələr",
   "prefs_topstories_sponsored_learn_more": "Ətraflı öyrən",
   "prefs_highlights_description": "Saxladığınız və ya ziyarət etdiyiniz saytlardan seçmələr",
   "prefs_highlights_options_visited_label": "Baxılmış Səhifələr",
   "prefs_highlights_options_download_label": "Son Endirmələr",
   "prefs_highlights_options_pocket_label": "Pocket-ə Saxlanılan Səhifələr",
   "prefs_snippets_description": "Mozilla və Firefoxdan yeniliklər",
-  "settings_pane_button_label": "Yeni Vərəq səhifənizi özəlləşdirin",
+  "settings_pane_button_label": "Yeni Vərəq səhifənizi fərdiləşdirin",
   "settings_pane_topsites_header": "Qabaqcıl Saytlar",
   "settings_pane_highlights_header": "Seçilmişlər",
   "settings_pane_highlights_options_bookmarks": "Əlfəcinlər",
   "settings_pane_snippets_header": "Hissələr",
   "edit_topsites_button_text": "Redaktə et",
   "edit_topsites_edit_button": "Bu saytı düzəlt",
   "topsites_form_add_header": "Yeni Qabaqcıl Saytlar",
   "topsites_form_edit_header": "Qabaqcıl Saytları Dəyişdir",
--- a/browser/components/newtab/prerendered/locales/da/activity-stream-strings.js
+++ b/browser/components/newtab/prerendered/locales/da/activity-stream-strings.js
@@ -91,17 +91,17 @@ window.gActivityStreamStrings = {
   "section_menu_action_move_down": "Flyt ned",
   "section_menu_action_privacy_notice": "Privatlivspolitik",
   "firstrun_title": "Tag Firefox med dig",
   "firstrun_content": "Få adgang til din historik, dine bogmærker, adgangskoder og andre indstillinger på alle dine enheder.",
   "firstrun_learn_more_link": "Læs mere om Firefox-konti",
   "firstrun_form_header": "Indtast din mailadresse",
   "firstrun_form_sub_header": "for at fortsætte til Firefox Sync.",
   "firstrun_email_input_placeholder": "Mailadresse",
-  "firstrun_invalid_input": "Valid email required",
+  "firstrun_invalid_input": "En gyldig mailadresse er påkrævet",
   "firstrun_extra_legal_links": "Ved at fortsætte godkender du vores {terms} og {privacy}.",
   "firstrun_terms_of_service": "tjenestevilkår",
   "firstrun_privacy_notice": "privatlivspolitik",
   "firstrun_continue_to_login": "Fortsæt",
   "firstrun_skip_login": "Spring dette trin over",
   "prefs_restore_defaults_button": "Gendan standarder",
   "settings_pane_header": "Indstillinger for Nyt faneblad",
   "settings_pane_body2": "Vælg, hvad du vil se på denne side.",
--- a/browser/components/newtab/prerendered/locales/gn/activity-stream-strings.js
+++ b/browser/components/newtab/prerendered/locales/gn/activity-stream-strings.js
@@ -91,15 +91,15 @@ window.gActivityStreamStrings = {
   "section_menu_action_move_down": "Guejy",
   "section_menu_action_privacy_notice": "Marandu’i ñemiguáva",
   "firstrun_title": "Egueraha Firefox nendive",
   "firstrun_content": "Eike nde techaukaha, tembiasakue, ñe’ẽñemi ha ambueve ñemoĩporã opaite nde mba’e’okápe.",
   "firstrun_learn_more_link": "Eikuaave Firefox Accounts rehegua",
   "firstrun_form_header": "Emoinge ne ñandutiveve",
   "firstrun_form_sub_header": "eike hag̃ua Firefox Sync-pe.",
   "firstrun_email_input_placeholder": "Ñandutiveve",
-  "firstrun_invalid_input": "Valid email required",
+  "firstrun_invalid_input": "Eikotevẽ peteĩ ñanduti veve oikóva",
   "firstrun_extra_legal_links": "Ejapóva, emoneĩ ko'ã {terms} ha {privacy}.",
   "firstrun_terms_of_service": "Mba'epytyvõrã ñemboguata",
   "firstrun_privacy_notice": "Ñemigua purureko",
   "firstrun_continue_to_login": "Eku'ejey",
   "firstrun_skip_login": "Ehejánte kóva"
 };
--- a/browser/components/newtab/prerendered/locales/ja-JP-mac/activity-stream-strings.js
+++ b/browser/components/newtab/prerendered/locales/ja-JP-mac/activity-stream-strings.js
@@ -91,15 +91,15 @@ window.gActivityStreamStrings = {
   "section_menu_action_move_down": "下へ移動",
   "section_menu_action_privacy_notice": "プライバシー通知",
   "firstrun_title": "Firefox をあなたとともに",
   "firstrun_content": "すべての端末で、ブックマーク、履歴、パスワード、その他の設定を取得できます。",
   "firstrun_learn_more_link": "Firefox Accounts に関する詳細情報",
   "firstrun_form_header": "メールアドレスを入力してください",
   "firstrun_form_sub_header": "Firefox Sync の利用を続けるために必要です",
   "firstrun_email_input_placeholder": "メールアドレス",
-  "firstrun_invalid_input": "Valid email required",
+  "firstrun_invalid_input": "メールアドレスを正しく入力してください",
   "firstrun_extra_legal_links": "続行すると、{terms} と {privacy} に同意したものとみなします。",
   "firstrun_terms_of_service": "サービス利用規約",
   "firstrun_privacy_notice": "プライバシーに関する通知",
   "firstrun_continue_to_login": "続ける",
   "firstrun_skip_login": "この手順をスキップ"
 };
--- a/browser/components/newtab/prerendered/locales/ja/activity-stream-strings.js
+++ b/browser/components/newtab/prerendered/locales/ja/activity-stream-strings.js
@@ -91,15 +91,15 @@ window.gActivityStreamStrings = {
   "section_menu_action_move_down": "下へ移動",
   "section_menu_action_privacy_notice": "プライバシー通知",
   "firstrun_title": "Firefox をあなたとともに",
   "firstrun_content": "すべての端末で、ブックマーク、履歴、パスワード、その他の設定を取得できます。",
   "firstrun_learn_more_link": "Firefox Accounts に関する詳細情報",
   "firstrun_form_header": "メールアドレスを入力してください",
   "firstrun_form_sub_header": "Firefox Sync の利用を続けるために必要です",
   "firstrun_email_input_placeholder": "メールアドレス",
-  "firstrun_invalid_input": "Valid email required",
+  "firstrun_invalid_input": "メールアドレスを正しく入力してください",
   "firstrun_extra_legal_links": "続行すると、{terms} と {privacy} に同意したものとみなします。",
   "firstrun_terms_of_service": "サービス利用規約",
   "firstrun_privacy_notice": "プライバシーに関する通知",
   "firstrun_continue_to_login": "続ける",
   "firstrun_skip_login": "この手順をスキップ"
 };
--- a/browser/components/newtab/prerendered/locales/ta/activity-stream-strings.js
+++ b/browser/components/newtab/prerendered/locales/ta/activity-stream-strings.js
@@ -91,15 +91,15 @@ window.gActivityStreamStrings = {
   "section_menu_action_move_down": "கீழே நகர்த்து",
   "section_menu_action_privacy_notice": "தனியுரிமை அறிவிப்பு",
   "firstrun_title": "பயர்பாக்சை உடன் எடுத்துச் செல்லுங்கள்",
   "firstrun_content": "உங்கள் அனைத்துச் சாதனங்களிலும் உள்ள உங்களின் புத்தகக்குறிகள், வரலாறு, கடவுச்சொற்கள் மற்றும் பிற அமைப்புகளைப் பெறுங்கள்.",
   "firstrun_learn_more_link": "பயர்பாக்சு கணக்கைப் பற்றி மேலும் தெரிந்து கொள்ளவும்",
   "firstrun_form_header": "உங்களின் மின்னஞ்சலை உள்ளிடுக",
   "firstrun_form_sub_header": "பயர்பாக்சு ஒத்திசையைத் தொடர.",
   "firstrun_email_input_placeholder": "மின்னஞ்சல்",
-  "firstrun_invalid_input": "Valid email required",
+  "firstrun_invalid_input": "நம்பகரமான மின்னஞ்சல் தேவை",
   "firstrun_extra_legal_links": "தொடர்வதன் மூலம், தாங்கள் {terms} மற்றும் {privacy} ஒப்புக்கொள்கின்றீர்கள்.",
   "firstrun_terms_of_service": "சேவையின் விதிமுறைகள்",
   "firstrun_privacy_notice": "தனியுரிமை அறிவிப்பு",
   "firstrun_continue_to_login": "தொடர்க",
   "firstrun_skip_login": "இந்த படிநிலையைத் தவிர்"
 };
--- a/browser/components/newtab/prerendered/locales/zh-TW/activity-stream-strings.js
+++ b/browser/components/newtab/prerendered/locales/zh-TW/activity-stream-strings.js
@@ -15,17 +15,17 @@ window.gActivityStreamStrings = {
   "menu_action_remove_bookmark": "移除書籤",
   "menu_action_open_new_window": "用新視窗開啟",
   "menu_action_open_private_window": "用新隱私視窗開啟",
   "menu_action_dismiss": "隱藏",
   "menu_action_delete": "從瀏覽紀錄刪除",
   "menu_action_pin": "釘選",
   "menu_action_unpin": "取消釘選",
   "confirm_history_delete_p1": "您確定要刪除此頁面的所有瀏覽紀錄?",
-  "confirm_history_delete_notice_p2": "無法還原此操作。",
+  "confirm_history_delete_notice_p2": "此動作無法復原。",
   "menu_action_save_to_pocket": "儲存至 Pocket",
   "menu_action_delete_pocket": "從 Pocket 刪除",
   "menu_action_archive_pocket": "在 Pocket 裡封存",
   "menu_action_show_file_mac_os": "顯示於 Finder",
   "menu_action_show_file_windows": "開啟所在資料夾",
   "menu_action_show_file_linux": "開啟所在資料夾",
   "menu_action_show_file_default": "顯示檔案",
   "menu_action_open_file": "開啟檔案",
@@ -72,34 +72,34 @@ window.gActivityStreamStrings = {
   "topsites_form_cancel_button": "取消",
   "topsites_form_url_validation": "請輸入有效的網址",
   "topsites_form_image_validation": "圖片載入失敗,請改用不同網址。",
   "pocket_read_more": "熱門主題:",
   "pocket_read_even_more": "檢視更多文章",
   "highlights_empty_state": "開始上網,我們就會把您在網路上發現的好文章、影片、剛加入書籤的頁面顯示於此。",
   "topstories_empty_state": "所有文章都讀完啦!晚點再來,{provider} 將提供更多推薦故事。等不及了?選擇熱門主題,看看 Web 上各式精采資訊。",
   "manual_migration_explanation2": "試試將其他瀏覽器的書籤、瀏覽記錄與密碼匯入 Firefox。",
-  "manual_migration_cancel_button": "不必了",
+  "manual_migration_cancel_button": "不要,謝謝",
   "manual_migration_import_button": "立即匯入",
   "error_fallback_default_info": "唉唷,載入內容時發生錯誤。",
   "error_fallback_default_refresh_suggestion": "請重新整理頁面再試一次。",
   "section_menu_action_remove_section": "移除段落",
   "section_menu_action_collapse_section": "摺疊段落",
   "section_menu_action_expand_section": "展開段落",
   "section_menu_action_manage_section": "管理段落",
   "section_menu_action_manage_webext": "管理擴充套件",
   "section_menu_action_add_topsite": "新增熱門網站",
   "section_menu_action_move_up": "上移",
   "section_menu_action_move_down": "下移",
   "section_menu_action_privacy_notice": "隱私權公告",
   "firstrun_title": "Firefox 隨身帶著走",
   "firstrun_content": "在您的任何裝置上取得書籤、瀏覽紀錄、密碼及其他設定。",
   "firstrun_learn_more_link": "了解 Firefox Accounts 的更多資訊",
   "firstrun_form_header": "輸入您的電子郵件地址",
-  "firstrun_form_sub_header": "繼續前往 Firefox Sync。",
+  "firstrun_form_sub_header": "繼續前往 Firefox Sync",
   "firstrun_email_input_placeholder": "電子郵件",
   "firstrun_invalid_input": "必須輸入有效的電子郵件地址",
   "firstrun_extra_legal_links": "若繼續,代表您同意{terms}及{privacy}。",
   "firstrun_terms_of_service": "服務條款",
   "firstrun_privacy_notice": "隱私權公告",
   "firstrun_continue_to_login": "繼續",
   "firstrun_skip_login": "跳過這步"
 };
--- a/browser/components/newtab/test/browser/browser_asrouter_targeting.js
+++ b/browser/components/newtab/test/browser/browser_asrouter_targeting.js
@@ -1,23 +1,23 @@
-ChromeUtils.defineModuleGetter(this, "ASRouterTargeting",
-"resource://activity-stream/lib/ASRouterTargeting.jsm");
+const {ASRouterTargeting, TopFrecentSitesCache} =
+  ChromeUtils.import("resource://activity-stream/lib/ASRouterTargeting.jsm", {});
+const {AddonTestUtils} =
+  ChromeUtils.import("resource://testing-common/AddonTestUtils.jsm", {});
 ChromeUtils.defineModuleGetter(this, "ProfileAge",
   "resource://gre/modules/ProfileAge.jsm");
 ChromeUtils.defineModuleGetter(this, "AddonManager",
   "resource://gre/modules/AddonManager.jsm");
 ChromeUtils.defineModuleGetter(this, "ShellService",
   "resource:///modules/ShellService.jsm");
 ChromeUtils.defineModuleGetter(this, "NewTabUtils",
   "resource://gre/modules/NewTabUtils.jsm");
 ChromeUtils.defineModuleGetter(this, "PlacesTestUtils",
   "resource://testing-common/PlacesTestUtils.jsm");
 
-const {AddonTestUtils} = ChromeUtils.import("resource://testing-common/AddonTestUtils.jsm", {});
-
 // ASRouterTargeting.isMatch
 add_task(async function should_do_correct_targeting() {
   is(await ASRouterTargeting.isMatch("FOO", {}, {FOO: true}), true, "should return true for a matching value");
   is(await ASRouterTargeting.isMatch("!FOO", {}, {FOO: true}), false, "should return false for a non-matching value");
 });
 
 add_task(async function should_handle_async_getters() {
   const context = {get FOO() { return Promise.resolve(true); }};
@@ -221,9 +221,13 @@ add_task(async function checkFrecentSite
 
   message = {id: "foo", targeting: `'mozilla2.com' in topFrecentSites[.lastVisitDate >= ${timeDaysAgo(0) - 1}]|mapToProperty('host')`};
   is(await ASRouterTargeting.findMatchingMessage({messages: [message], target: {}}), undefined,
     "should not select incorrect item when filtering by lastVisitDate");
 
   message = {id: "foo", targeting: `(topFrecentSites[.frecency >= 900 && .lastVisitDate >= ${timeDaysAgo(1) - 1}]|mapToProperty('host') intersect ['mozilla3.com', 'mozilla2.com', 'mozilla1.com'])|length > 0`};
   is(await ASRouterTargeting.findMatchingMessage({messages: [message], target: {}}), message,
     "should select correct item when filtering by frecency and lastVisitDate with multiple candidate domains");
+
+  // Cleanup
+  await clearHistoryAndBookmarks();
+  TopFrecentSitesCache.expire();
 });
--- a/browser/components/newtab/test/browser/browser_newtab_overrides.js
+++ b/browser/components/newtab/test/browser/browser_newtab_overrides.js
@@ -2,51 +2,58 @@
 
 ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
 ChromeUtils.import("resource://gre/modules/Services.jsm");
 
 XPCOMUtils.defineLazyServiceGetter(this, "aboutNewTabService",
                                    "@mozilla.org/browser/aboutnewtab-service;1",
                                    "nsIAboutNewTabService");
 
-registerCleanupFunction(function() {
+registerCleanupFunction(() => {
   aboutNewTabService.resetNewTabURL();
 });
 
+function nextChangeNotificationPromise(aNewURL, testMessage) {
+  return TestUtils.topicObserved("newtab-url-changed", function observer(aSubject, aData) { // jshint unused:false
+    Assert.equal(aData, aNewURL, testMessage);
+    return true;
+  });
+}
+
 /*
  * Tests that the default newtab page is always returned when one types "about:newtab" in the URL bar,
  * even when overridden.
  */
 add_task(async function redirector_ignores_override() {
   let overrides = [
     "chrome://browser/content/aboutRobots.xhtml",
-    "about:home",
+    "about:home"
   ];
 
   for (let overrideURL of overrides) {
     let notificationPromise = nextChangeNotificationPromise(overrideURL, `newtab page now points to ${overrideURL}`);
     aboutNewTabService.newTabURL = overrideURL;
 
     await notificationPromise;
     Assert.ok(aboutNewTabService.overridden, "url has been overridden");
 
     let tabOptions = {
       gBrowser,
-      url: "about:newtab",
+      url: "about:newtab"
     };
 
     /*
      * Simulate typing "about:newtab" in the url bar.
      *
      * Bug 1240169 - We expect the redirector to lead the user to "about:newtab", the default URL,
      * due to invoking AboutRedirector. A user interacting with the chrome otherwise would lead
      * to the overriding URLs.
      */
-    await BrowserTestUtils.withNewTab(tabOptions, async function(browser) {
-      await ContentTask.spawn(browser, {}, async function() {
+    await BrowserTestUtils.withNewTab(tabOptions, async browser => {
+      await ContentTask.spawn(browser, {}, async () => {
         Assert.equal(content.location.href, "about:newtab", "Got right URL");
         Assert.equal(content.document.location.href, "about:newtab", "Got right URL");
         Assert.notEqual(content.document.nodePrincipal,
           Services.scriptSecurityManager.getSystemPrincipal(),
           "activity stream principal should not match systemPrincipal");
       });
     }); // jshint ignore:line
   }
@@ -54,70 +61,63 @@ add_task(async function redirector_ignor
 
 /*
  * Tests loading an overridden newtab page by simulating opening a newtab page from chrome
  */
 add_task(async function override_loads_in_browser() {
   let overrides = [
     "chrome://browser/content/aboutRobots.xhtml",
     "about:home",
-    " about:home",
+    " about:home"
   ];
 
   for (let overrideURL of overrides) {
     let notificationPromise = nextChangeNotificationPromise(overrideURL.trim(), `newtab page now points to ${overrideURL}`);
     aboutNewTabService.newTabURL = overrideURL;
 
     await notificationPromise;
     Assert.ok(aboutNewTabService.overridden, "url has been overridden");
 
     // simulate a newtab open as a user would
     BrowserOpenTab(); // jshint ignore:line
 
     let browser = gBrowser.selectedBrowser;
     await BrowserTestUtils.browserLoaded(browser);
 
-    await ContentTask.spawn(browser, {url: overrideURL}, async function(args) {
+    await ContentTask.spawn(browser, {url: overrideURL}, async args => {
       Assert.equal(content.location.href, args.url.trim(), "Got right URL");
       Assert.equal(content.document.location.href, args.url.trim(), "Got right URL");
     }); // jshint ignore:line
     BrowserTestUtils.removeTab(gBrowser.selectedTab);
   }
 });
 
 /*
  * Tests edge cases when someone overrides the newtabpage with whitespace
  */
 add_task(async function override_blank_loads_in_browser() {
   let overrides = [
     "",
     " ",
     "\n\t",
-    " about:blank",
+    " about:blank"
   ];
 
   for (let overrideURL of overrides) {
     let notificationPromise = nextChangeNotificationPromise("about:blank", "newtab page now points to about:blank");
     aboutNewTabService.newTabURL = overrideURL;
 
     await notificationPromise;
     Assert.ok(aboutNewTabService.overridden, "url has been overridden");
 
     // simulate a newtab open as a user would
     BrowserOpenTab(); // jshint ignore:line
 
     let browser = gBrowser.selectedBrowser;
     await BrowserTestUtils.browserLoaded(browser);
 
-    await ContentTask.spawn(browser, {}, async function() {
+    await ContentTask.spawn(browser, {}, async () => {
       Assert.equal(content.location.href, "about:blank", "Got right URL");
       Assert.equal(content.document.location.href, "about:blank", "Got right URL");
     }); // jshint ignore:line
     BrowserTestUtils.removeTab(gBrowser.selectedTab);
   }
 });
-
-function nextChangeNotificationPromise(aNewURL, testMessage) {
-  return TestUtils.topicObserved("newtab-url-changed", function observer(aSubject, aData) { // jshint unused:false
-      Assert.equal(aData, aNewURL, testMessage);
-      return true;
-  });
-}
--- a/browser/components/newtab/test/browser/browser_packaged_as_locales.js
+++ b/browser/components/newtab/test/browser/browser_packaged_as_locales.js
@@ -54,17 +54,17 @@ add_task(async function test_all_package
   let gotID = false;
   const listing = await (await fetch("resource://activity-stream/prerendered/")).text();
   for (const line of listing.split("\n").slice(2)) {
     const [file, , , type] = line.split(" ").slice(1);
     if (type === "DIRECTORY") {
       const locale = file.replace("/", "");
       if (locale !== "static") {
         const url = await getUrlForLocale(locale);
-        Assert[locale === "en-US" ? "equal" : "notEqual"](url, DEFAULT_URL, `can reference "${locale}" files`);
+        Assert.equal(url, DEFAULT_URL.replace("en-US", locale), `can reference "${locale}" files`);
 
         // Specially remember if we saw an ID locale packaged as it can be
         // easily ignored by source control, e.g., .gitignore
         gotID |= locale === "id";
       }
     }
   }
 
--- a/browser/components/newtab/test/unit/asrouter/ASRouter.test.js
+++ b/browser/components/newtab/test/unit/asrouter/ASRouter.test.js
@@ -123,33 +123,51 @@ describe("ASRouter", () => {
       const {length} = Router.state.providers;
       await Router.observe("", "", "remotePref");
 
       const provider = Router.state.providers.find(p => p.url === "baz.com");
 
       assert.lengthOf(Router.state.providers, length);
       assert.isDefined(provider);
     });
+    it("should load additional whitelisted hosts", async () => {
+      getStringPrefStub.returns("[\"whitelist.com\"]");
+      await createRouterAndInit();
+
+      assert.propertyVal(Router.WHITELIST_HOSTS, "whitelist.com", "preview");
+      // Should still include the defaults
+      assert.lengthOf(Object.keys(Router.WHITELIST_HOSTS), 3);
+    });
+    it("should fallback to defaults if pref parsing fails", async () => {
+      getStringPrefStub.returns("err");
+      await createRouterAndInit();
+
+      assert.lengthOf(Object.keys(Router.WHITELIST_HOSTS), 2);
+      assert.propertyVal(Router.WHITELIST_HOSTS, "snippets-admin.mozilla.org", "preview");
+      assert.propertyVal(Router.WHITELIST_HOSTS, "activity-stream-icons.services.mozilla.com", "production");
+    });
   });
 
   describe("#loadMessagesFromAllProviders", () => {
     function assertRouterContainsMessages(messages) {
       const messageIdsInRouter = Router.state.messages.map(m => m.id);
       for (const message of messages) {
         assert.include(messageIdsInRouter, message.id);
       }
     }
 
     it("should load provider endpoint based on pref", async () => {
       getStringPrefStub.reset();
       getStringPrefStub.returns("example.com");
       await createRouterAndInit();
 
-      assert.calledOnce(getStringPrefStub);
+      // Get snippets endpoint url, get the whitelisted hosts for endpoints
+      assert.calledTwice(getStringPrefStub);
       assert.calledWithExactly(getStringPrefStub, "remotePref", "");
+      assert.calledWithExactly(getStringPrefStub, "browser.newtab.activity-stream.asrouter.whitelistHosts", "");
       assert.isDefined(Router.state.providers.find(p => p.url === "example.com"));
     });
     it("should not trigger an update if not enough time has passed for a provider", async () => {
       await createRouterAndInit([
         {id: "remotey", type: "remote", url: "http://fake.com/endpoint", updateCycleInMs: 300}
       ]);
 
       const previousState = Router.state;
@@ -243,16 +261,28 @@ describe("ASRouter", () => {
       await Router.setState({messages: [{id: "foo1", template: "simple_template", bundled: 1, content: {title: "Foo1", body: "Foo123-1"}}]});
       const msg = fakeAsyncMessage({type: "CONNECT_UI_REQUEST"});
       await Router.onMessage(msg);
       const [currentMessage] = Router.state.messages.filter(message => message.id === Router.state.lastMessageId);
       assert.calledWith(msg.target.sendAsyncMessage, PARENT_TO_CHILD_MESSAGE_NAME);
       assert.equal(msg.target.sendAsyncMessage.firstCall.args[1].type, "SET_BUNDLED_MESSAGES");
       assert.equal(msg.target.sendAsyncMessage.firstCall.args[1].data.bundle[0].content, currentMessage.content);
     });
+    it("should properly order the message's bundle if specified", async () => {
+      // force the only messages to be a bundled messages so getRandomItemFromArray picks one of them
+      const firstMessage = {id: "foo2", template: "simple_template", bundled: 2, order: 1, content: {title: "Foo2", body: "Foo123-2"}};
+      const secondMessage = {id: "foo1", template: "simple_template", bundled: 2, order: 2, content: {title: "Foo1", body: "Foo123-1"}};
+      await Router.setState({messages: [secondMessage, firstMessage]});
+      const msg = fakeAsyncMessage({type: "CONNECT_UI_REQUEST"});
+      await Router.onMessage(msg);
+      assert.calledWith(msg.target.sendAsyncMessage, PARENT_TO_CHILD_MESSAGE_NAME);
+      assert.equal(msg.target.sendAsyncMessage.firstCall.args[1].type, "SET_BUNDLED_MESSAGES");
+      assert.equal(msg.target.sendAsyncMessage.firstCall.args[1].data.bundle[0].content, firstMessage.content);
+      assert.equal(msg.target.sendAsyncMessage.firstCall.args[1].data.bundle[1].content, secondMessage.content);
+    });
     it("should return a null bundle if we do not have enough messages to fill the bundle", async () => {
       // force the only message to be a bundled message that needs 2 messages in the bundle
       await Router.setState({messages: [{id: "foo1", template: "simple_template", bundled: 2, content: {title: "Foo1", body: "Foo123-1"}}]});
       const bundle = await Router._getBundledMessages(Router.state.messages[0]);
       assert.equal(bundle, null);
     });
     it("should send a CLEAR_ALL message if no bundle available", async () => {
       // force the only message to be a bundled message that needs 2 messages in the bundle
@@ -414,32 +444,32 @@ describe("ASRouter", () => {
       const msg = fakeAsyncMessage({type: "OVERRIDE_MESSAGE", data: {id: testMessage1.id}});
       await Router.onMessage(msg);
 
       // Expected object should have some properties of the original message it picked (testMessage1)
       // plus the bundled content of the others that it picked of the same template (testMessage2)
       const expectedObj = {
         template: testMessage1.template,
         provider: testMessage1.provider,
-        bundle: [{content: testMessage1.content, id: testMessage1.id}, {content: testMessage2.content, id: testMessage2.id}]
+        bundle: [{content: testMessage1.content, id: testMessage1.id, order: 1}, {content: testMessage2.content, id: testMessage2.id}]
       };
       assert.calledWith(msg.target.sendAsyncMessage, PARENT_TO_CHILD_MESSAGE_NAME, {type: "SET_BUNDLED_MESSAGES", data: expectedObj});
     });
     it("should properly pick another message of the same template if it is bundled; force = false", async () => {
       // forcefully pick a message which needs to be bundled (the second message in FAKE_LOCAL_MESSAGES)
       const [, testMessage1, testMessage2] = Router.state.messages;
       const msg = fakeAsyncMessage({type: "OVERRIDE_MESSAGE", data: {id: testMessage1.id}});
       await Router.setMessageById(testMessage1.id, msg.target, false);
 
       // Expected object should have some properties of the original message it picked (testMessage1)
       // plus the bundled content of the others that it picked of the same template (testMessage2)
       const expectedObj = {
         template: testMessage1.template,
         provider: testMessage1.provider,
-        bundle: [{content: testMessage1.content, id: testMessage1.id}, {content: testMessage2.content, id: testMessage2.id}]
+        bundle: [{content: testMessage1.content, id: testMessage1.id, order: 1}, {content: testMessage2.content, id: testMessage2.id, order: 2}]
       };
       assert.calledWith(msg.target.sendAsyncMessage, PARENT_TO_CHILD_MESSAGE_NAME, {type: "SET_BUNDLED_MESSAGES", data: expectedObj});
     });
     it("should get the bundle and send the message if the message has a bundle", async () => {
       sandbox.stub(Router, "sendNextMessage").resolves();
       const msg = fakeAsyncMessage({type: "GET_NEXT_MESSAGE"});
       msg.bundled = 2; // force this message to want to be bundled
       await Router.onMessage(msg);
--- a/browser/components/newtab/test/unit/asrouter/constants.js
+++ b/browser/components/newtab/test/unit/asrouter/constants.js
@@ -1,16 +1,16 @@
 export const CHILD_TO_PARENT_MESSAGE_NAME = "ASRouter:child-to-parent";
 export const PARENT_TO_CHILD_MESSAGE_NAME = "ASRouter:parent-to-child";
 export const EXPERIMENT_PREF = "asrouterExperimentEnabled";
 
 export const FAKE_LOCAL_MESSAGES = [
   {id: "foo", template: "simple_template", content: {title: "Foo", body: "Foo123"}},
-  {id: "foo1", template: "simple_template", bundled: 2, content: {title: "Foo1", body: "Foo123-1"}},
-  {id: "foo2", template: "simple_template", bundled: 2, content: {title: "Foo2", body: "Foo123-2"}},
+  {id: "foo1", template: "simple_template", bundled: 2, order: 1, content: {title: "Foo1", body: "Foo123-1"}},
+  {id: "foo2", template: "simple_template", bundled: 2, order: 2, content: {title: "Foo2", body: "Foo123-2"}},
   {id: "bar", template: "fancy_template", content: {title: "Foo", body: "Foo123"}},
   {id: "baz", content: {title: "Foo", body: "Foo123"}}
 ];
 export const FAKE_LOCAL_PROVIDER = {id: "onboarding", type: "local", messages: FAKE_LOCAL_MESSAGES};
 
 export const FAKE_REMOTE_MESSAGES = [
   {id: "qux", template: "simple_template", content: {title: "Qux", body: "hello world"}}
 ];
--- a/browser/components/newtab/test/xpcshell/test_AboutNewTabService.js
+++ b/browser/components/newtab/test/xpcshell/test_AboutNewTabService.js
@@ -26,16 +26,48 @@ const ACTIVITY_STREAM_DEBUG_PREF = "brow
 function cleanup() {
   Services.prefs.clearUserPref(ACTIVITY_STREAM_PRERENDER_PREF);
   Services.prefs.clearUserPref(ACTIVITY_STREAM_DEBUG_PREF);
   aboutNewTabService.resetNewTabURL();
 }
 
 registerCleanupFunction(cleanup);
 
+function nextChangeNotificationPromise(aNewURL, testMessage) {
+  return new Promise(resolve => {
+    Services.obs.addObserver(function observer(aSubject, aTopic, aData) { // jshint unused:false
+      Services.obs.removeObserver(observer, aTopic);
+      Assert.equal(aData, aNewURL, testMessage);
+      resolve();
+    }, "newtab-url-changed");
+  });
+}
+
+function setBoolPrefAndWaitForChange(pref, value, testMessage) {
+  return new Promise(resolve => {
+    Services.obs.addObserver(function observer(aSubject, aTopic, aData) { // jshint unused:false
+      Services.obs.removeObserver(observer, aTopic);
+      Assert.equal(aData, aboutNewTabService.newTabURL, testMessage);
+      resolve();
+    }, "newtab-url-changed");
+
+    Services.prefs.setBoolPref(pref, value);
+  });
+}
+
+function setupASPrerendered() {
+  if (Services.prefs.getBoolPref(ACTIVITY_STREAM_PRERENDER_PREF)) {
+    return Promise.resolve();
+  }
+
+  let notificationPromise = nextChangeNotificationPromise("about:newtab");
+  Services.prefs.setBoolPref(ACTIVITY_STREAM_PRERENDER_PREF, true);
+  return notificationPromise;
+}
+
 add_task(async function test_as_and_prerender_initialized() {
   Assert.ok(aboutNewTabService.activityStreamEnabled,
     ".activityStreamEnabled should be set to the correct initial value");
   Assert.equal(aboutNewTabService.activityStreamPrerender, Services.prefs.getBoolPref(ACTIVITY_STREAM_PRERENDER_PREF),
     ".activityStreamPrerender should be set to the correct initial value");
   // This pref isn't defined on release or beta, so we fall back to false
   Assert.equal(aboutNewTabService.activityStreamDebug, Services.prefs.getBoolPref(ACTIVITY_STREAM_DEBUG_PREF, false),
     ".activityStreamDebug should be set to the correct initial value");
@@ -160,20 +192,18 @@ add_task(function test_locale() {
   Assert.equal(aboutNewTabService.activityStreamLocale, "en-US",
     "The locale for testing should be en-US");
 });
 
 /**
  * Tests reponse to updates to prefs
  */
 add_task(async function test_updates() {
-  /*
-   * Simulates a "cold-boot" situation, with some pref already set before testing a series
-   * of changes.
-   */
+   // Simulates a "cold-boot" situation, with some pref already set before testing a series
+   // of changes.
   await setupASPrerendered();
 
   aboutNewTabService.resetNewTabURL(); // need to set manually because pref notifs are off
   let notificationPromise;
 
   // test update fires on override and reset
   let testURL = "https://example.com/";
   notificationPromise = nextChangeNotificationPromise(
@@ -192,41 +222,8 @@ add_task(async function test_updates() {
   // reset twice, only one notification for default URL
   notificationPromise = nextChangeNotificationPromise(
     "about:newtab", "reset occurs");
   aboutNewTabService.resetNewTabURL();
   await notificationPromise;
 
   cleanup();
 });
-
-function nextChangeNotificationPromise(aNewURL, testMessage) {
-  return new Promise(resolve => {
-    Services.obs.addObserver(function observer(aSubject, aTopic, aData) { // jshint unused:false
-      Services.obs.removeObserver(observer, aTopic);
-      Assert.equal(aData, aNewURL, testMessage);
-      resolve();
-    }, "newtab-url-changed");
-  });
-}
-
-function setBoolPrefAndWaitForChange(pref, value, testMessage) {
-  return new Promise(resolve => {
-    Services.obs.addObserver(function observer(aSubject, aTopic, aData) { // jshint unused:false
-      Services.obs.removeObserver(observer, aTopic);
-      Assert.equal(aData, aboutNewTabService.newTabURL, testMessage);
-      resolve();
-    }, "newtab-url-changed");
-
-    Services.prefs.setBoolPref(pref, value);
-  });
-}
-
-
-function setupASPrerendered() {
-  if (Services.prefs.getBoolPref(ACTIVITY_STREAM_PRERENDER_PREF)) {
-    return Promise.resolve();
-  }
-
-  let notificationPromise = nextChangeNotificationPromise("about:newtab");
-  Services.prefs.setBoolPref(ACTIVITY_STREAM_PRERENDER_PREF, true);
-  return notificationPromise;
-}
--- a/browser/components/newtab/yamscripts.yml
+++ b/browser/components/newtab/yamscripts.yml
@@ -1,50 +1,49 @@
 # This file compiles to package.json scripts.
 # When you add or modify anything, you *MUST* run:
 #      npm run yamscripts
 # to compile your changes.
 
 scripts:
   # Run the activity-stream mochitests
-  mochitest: (cd $npm_package_config_mc_dir && ./mach mochitest browser/extensions/activity-stream/test/functional/mochitest --headless)
+  mochitest: (cd $npm_package_config_mc_dir && ./mach mochitest browser/components/newtab/test/browser --headless)
 
   # Run the activity-stream mochitests with the browser toolbox debugger.
   # Often handy in combination with adding a "debugger" statement in your
   # mochitest somewhere.
-  mochitest-debug: (cd $npm_package_config_mc_dir && ./mach mochitest --jsdebugger browser/extensions/activity-stream/test/functional/mochitest)
+  mochitest-debug: (cd $npm_package_config_mc_dir && ./mach mochitest --jsdebugger browser/components/newtab/test/browser)
 
 # bundle: Build all assets for activity stream
   bundle:
     locales: pontoon-to-json --src locales --dest data
     webpack: webpack --config webpack.system-addon.config.js
     css: node-sass --source-map true --source-map-contents content-src/styles -o css
     html: rimraf prerendered && webpack --config webpack.prerender.config.js && node ./bin/render-activity-stream-html.js
 
 # buildmc: Export the bootstraped add-on to mozilla central
   buildmc:
-    pre: rimraf $npm_package_config_mc_dir/browser/extensions/activity-stream/
+    pre: rimraf $npm_package_config_mc_dir/browser/components/newtab/
     bundle: => bundle
-    copy: rsync --exclude-from .mcignore -a . $npm_package_config_mc_dir/browser/extensions/activity-stream/
-    version: node ./bin/update-version.js $npm_package_config_mc_dir/browser/extensions/activity-stream
+    copy: rsync --exclude-from .mcignore -a . $npm_package_config_mc_dir/browser/components/newtab/
     stringsExport: cp locales/en-US/strings.properties $npm_package_config_mc_dir/browser/locales/en-US/chrome/browser/activity-stream/newtab.properties
     copyPingCentre: cpx "ping-centre/PingCentre.jsm" $npm_package_config_mc_dir/browser/modules
 
 # startmc: Start developing the bootstrapped add-on
   startmc:
     _parallel: true
     pre: =>buildmc
     # This copies only the system addon sub-folder; changing anything outside of it will need a full rebuild.
-    copy: cpx "{{,.}*,!(node_modules)/**/{,.}*}" $npm_package_config_mc_dir/browser/extensions/activity-stream/ -w
+    copy: cpx "{{,.}*,!(node_modules)/**/{,.}*}" $npm_package_config_mc_dir/browser/components/newtab/ -w
     copyPingCentre: =>buildmc:copyPingCentre -- -w
     webpack: =>bundle:webpack -- -w
     css: =>bundle:css && =>bundle:css -- -w
 
   # importmc: Import changes from mc to github repo
-  importmc: rsync --exclude-from .mcignore -a $npm_package_config_mc_dir/browser/extensions/activity-stream/ .
+  importmc: rsync --exclude-from .mcignore -a $npm_package_config_mc_dir/browser/components/newtab/ .
 
   testmc:
     lint: =>lint
     build: =>bundle:webpack && =>bundle:locales
     unit: karma start karma.mc.config.js || (cat logs/coverage/text.txt && exit 2)
     post: cat logs/coverage/text-summary.txt
 
   tddmc: karma start karma.mc.config.js --tdd