Bug 1544126 - Add FxA snippet, pinned targeting and bug fixes to Activity Stream r=k88hudson
authorEd Lee <edilee@mozilla.com>
Mon, 15 Apr 2019 14:12:34 +0000
changeset 469498 4b8c7a55d97a
parent 469497 9573e2163ca4
child 469499 eb773d96778e
push id35873
push userccoroiu@mozilla.com
push dateMon, 15 Apr 2019 21:36:26 +0000
treeherdermozilla-central@b8f49a14c458 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersk88hudson
bugs1544126
milestone68.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1544126 - Add FxA snippet, pinned targeting and bug fixes to Activity Stream r=k88hudson Differential Revision: https://phabricator.services.mozilla.com/D27387
browser/components/newtab/content-src/asrouter/docs/experiment-guide.md
browser/components/newtab/content-src/asrouter/schemas/panel/cfr-fxa-bookmark.schema.json
browser/components/newtab/content-src/asrouter/templates/SimpleBelowSearchSnippet/SimpleBelowSearchSnippet.jsx
browser/components/newtab/content-src/asrouter/templates/SimpleBelowSearchSnippet/SimpleBelowSearchSnippet.schema.json
browser/components/newtab/content-src/asrouter/templates/template-manifest.jsx
browser/components/newtab/data/content/activity-stream.bundle.js
browser/components/newtab/lib/CFRMessageProvider.jsm
browser/components/newtab/lib/SnippetsTestMessageProvider.jsm
browser/components/newtab/test/unit/asrouter/CFRMessageProvider.test.js
browser/components/newtab/test/unit/asrouter/SnippetsTestMessageProvider.test.js
browser/components/newtab/test/unit/asrouter/schemas/panel/cfr-fxa-bookmark.schema.test.js
browser/components/newtab/test/unit/asrouter/templates/SimpleBelowSearchSnippet.test.jsx
new file mode 100644
--- /dev/null
+++ b/browser/components/newtab/content-src/asrouter/docs/experiment-guide.md
@@ -0,0 +1,52 @@
+# How to run experiments with ASRouter
+
+This guide will tell you how to run an experiment with ASRouter messages.
+Note that the actual experiment proccess and infrastructure is handled by
+the experiments team (#ask-experimenter).
+
+## Why run an experiment
+
+* To measure the effect of a message on a Firefox metric (e.g. retention)
+* To test a potentially risky message on a smaller group of users
+* To compare the performance of multiple variants of messages in a controlled way
+
+## Choose cohort IDs and request an experiment
+
+First you should decide on a cohort ID (this can be any arbitrary unique string) for each
+individual group you need to segment for your experiment.
+
+For example, if I want to test two variants of an FXA Snippet, I might have two cohort IDs,
+`FXA_SNIPPET_V1` and `FXA_SNIPPET_V2`.
+
+You will then [request](https://experimenter.services.mozilla.com/) a new "pref-flip" study with the Firefox Experiments team.
+The preferences you will submit will be based on the cohort IDs you chose.
+
+For the FXA Snippet example, your preference name would be `browser.newtabpage.activity-stream.asrouter.providers.snippets` and values would be:
+
+Control (default value)
+```json
+{"id":"snippets","enabled":true,"type":"remote","url":"https://snippets.cdn.mozilla.net/%STARTPAGE_VERSION%/%NAME%/%VERSION%/%APPBUILDID%/%BUILD_TARGET%/%LOCALE%/release/%OS_VERSION%/%DISTRIBUTION%/%DISTRIBUTION_VERSION%/","updateCycleInMs":14400000}
+```
+
+Variant 1:
+```json
+{"id":"snippets", "cohort": "FXA_SNIPPET_V1", "enabled":true,"type":"remote","url":"https://snippets.cdn.mozilla.net/%STARTPAGE_VERSION%/%NAME%/%VERSION%/%APPBUILDID%/%BUILD_TARGET%/%LOCALE%/release/%OS_VERSION%/%DISTRIBUTION%/%DISTRIBUTION_VERSION%/","updateCycleInMs":14400000}
+```
+
+Variant 2:
+```json
+{"id":"snippets", "cohort": "FXA_SNIPPET_V1", "enabled":true,"type":"remote","url":"https://snippets.cdn.mozilla.net/%STARTPAGE_VERSION%/%NAME%/%VERSION%/%APPBUILDID%/%BUILD_TARGET%/%LOCALE%/release/%OS_VERSION%/%DISTRIBUTION%/%DISTRIBUTION_VERSION%/","updateCycleInMs":14400000}
+```
+
+## Add targeting to your messages
+
+You must now check for the cohort ID in the `targeting` expression of the messages you want to include in your experiments.
+
+For the previous example, you wold include the following to target the first cohort:
+
+```json
+{
+  "targeting": "providerCohorts.snippets == \"FXA_SNIPPET_V1\""
+}
+
+```
new file mode 100644
--- /dev/null
+++ b/browser/components/newtab/content-src/asrouter/schemas/panel/cfr-fxa-bookmark.schema.json
@@ -0,0 +1,128 @@
+{
+  "title": "CFRFxABookmark",
+  "description": "A message shown in the bookmark panel when user adds or edits a bookmark",
+  "version": "1.0.0",
+  "type": "object",
+  "definitions": {
+    "plainText": {
+      "description": "Plain text (no HTML allowed)",
+      "type": "string"
+    },
+    "richText": {
+      "description": "Text with HTML subset allowed: i, b, u, strong, em, br",
+      "type": "string"
+    },
+    "link_url": {
+      "description": "Target for links or buttons",
+      "type": "string",
+      "format": "uri"
+    }
+  },
+  "properties": {
+    "title": {
+      "description": "Shown at the top of the message in the largest font size.",
+      "oneOf": [
+        {
+          "allOf": [
+            {"$ref": "#/definitions/richText"},
+            {"description": "Message to be shown"}
+          ]
+        },
+        {
+          "type": "object",
+          "properties": {
+            "string_id": {
+              "type": "string",
+              "description": "Fluent id of localized string"
+            }
+          },
+          "required": ["string_id"]
+        }
+      ]
+    },
+    "text": {
+      "description": "Longest part of the message, below the title, provides explanation.",
+      "oneOf": [
+        {
+          "allOf": [
+            {"$ref": "#/definitions/richText"},
+            {"description": "Message to be shown"}
+          ]
+        },
+        {
+          "type": "object",
+          "properties": {
+            "string_id": {
+              "type": "string",
+              "description": "Fluent id of localized string"
+            }
+          },
+          "required": ["string_id"]
+        }
+      ]
+    },
+    "link": {
+      "description": "Link shown at the bottom of the message, call to action",
+      "properties": {
+        "text": {
+          "description": "Message shown as part of anchor tag",
+          "oneOf": [
+            {
+              "allOf": [
+                {"$ref": "#/definitions/richText"},
+                {"description": "Message to be shown"}
+              ]
+            },
+            {
+              "type": "object",
+              "properties": {
+                "string_id": {
+                  "type": "string",
+                  "description": "Fluent id of localized string"
+                }
+              },
+              "required": ["string_id"]
+            }
+          ] 
+        },
+        "url": {
+          "description": "Value for href attribute of the anchor",
+          "allOf": [
+            {"$ref": "#/definitions/link_url"},
+            {"description": "Link that opens in a new tab"}
+          ]
+        }
+      },
+      "required": ["text", "url"]
+    },
+    "info_icon": {
+      "type": "object",
+      "description": "The small icon displayed in the top right corner of the panel. Not configurable, only the tooltip text." ,
+      "properties": {
+        "tooltiptext": {
+          "oneOf": [
+            {
+              "allOf": [
+                {"$ref": "#/definitions/plainText"},
+                {"description": "Message to be shown"}
+              ]
+            },
+            {
+              "type": "object",
+              "properties": {
+                "string_id": {
+                  "type": "string",
+                  "description": "Fluent id of localized string"
+                }
+              },
+              "required": ["string_id"]
+            }
+          ]
+        }
+      },
+      "required": ["tooltiptext"]
+    }
+  },
+  "additionalProperties": false,
+  "required": ["title", "text", "link", "info_icon"]
+}
new file mode 100644
--- /dev/null
+++ b/browser/components/newtab/content-src/asrouter/templates/SimpleBelowSearchSnippet/SimpleBelowSearchSnippet.jsx
@@ -0,0 +1,34 @@
+import React from "react";
+import {RichText} from "../../components/RichText/RichText";
+import {safeURI} from "../../template-utils";
+import {SnippetBase} from "../../components/SnippetBase/SnippetBase";
+
+const DEFAULT_ICON_PATH = "chrome://branding/content/icon64.png";
+
+export class SimpleBelowSearchSnippet extends React.PureComponent {
+  renderText() {
+    const {props} = this;
+    return (<RichText text={props.content.text}
+      customElements={this.props.customElements}
+      localization_id="text"
+      links={props.content.links}
+      sendClick={props.sendClick} />);
+  }
+
+  render() {
+    const {props} = this;
+    let className = "SimpleBelowSearchSnippet";
+
+    if (props.className) {
+      className += ` ${props.className}`;
+    }
+
+    return (<SnippetBase {...props} className={className} textStyle={this.props.textStyle}>
+      <img src={safeURI(props.content.icon) || DEFAULT_ICON_PATH} className="icon" />
+      <div>
+        <p className="body">{this.renderText()}</p>
+        {this.props.extraContent}
+      </div>
+    </SnippetBase>);
+  }
+}
new file mode 100644
--- /dev/null
+++ b/browser/components/newtab/content-src/asrouter/templates/SimpleBelowSearchSnippet/SimpleBelowSearchSnippet.schema.json
@@ -0,0 +1,59 @@
+{
+  "title": "SimpleBelowSearchSnippet",
+  "description": "A simple template with just an icon and rich text. It gets inserted below the Activity Stream search box.",
+  "version": "1.1.0",
+  "type": "object",
+  "definitions": {
+    "richText": {
+      "description": "Text with HTML subset allowed: i, b, u, strong, em, br",
+      "type": "string"
+    },
+    "link_url": {
+      "description": "Target for links or buttons",
+      "type": "string",
+      "format": "uri"
+    }
+  },
+  "properties": {
+    "text": {
+      "allOf": [
+        {"$ref": "#/definitions/richText"},
+        {"description": "Main body text of snippet. HTML subset allowed: i, b, u, strong, em, br"}
+      ]
+    },
+    "icon": {
+      "type": "string",
+      "description": "Snippet icon. 64x64px. SVG or PNG preferred."
+    },
+    "block_button_text": {
+      "type": "string",
+      "description": "Tooltip text used for dismiss button.",
+      "default": "Remove this"
+    },
+    "do_not_autoblock": {
+      "type": "boolean",
+      "description": "Used to prevent blocking the snippet after the CTA link has been clicked"
+    },
+    "links": {
+      "additionalProperties": {
+        "url": {
+          "allOf": [
+            {"$ref": "#/definitions/link_url"},
+            {"description": "The url where the link points to."}
+          ]
+        },
+        "metric": {
+          "type": "string",
+          "description": "Custom event name sent with telemetry event."
+        },
+        "args": {
+          "type": "string",
+          "description": "Additional parameters for link action, example which specific menu the button should open"
+        }
+      }
+    }
+  },
+  "additionalProperties": false,
+  "required": ["text"],
+  "dependencies": {}
+}
--- a/browser/components/newtab/content-src/asrouter/templates/template-manifest.jsx
+++ b/browser/components/newtab/content-src/asrouter/templates/template-manifest.jsx
@@ -1,14 +1,16 @@
 import {EOYSnippet} from "./EOYSnippet/EOYSnippet";
 import {FXASignupSnippet} from "./FXASignupSnippet/FXASignupSnippet";
 import {NewsletterSnippet} from "./NewsletterSnippet/NewsletterSnippet";
 import {SendToDeviceSnippet} from "./SendToDeviceSnippet/SendToDeviceSnippet";
+import {SimpleBelowSearchSnippet} from "./SimpleBelowSearchSnippet/SimpleBelowSearchSnippet";
 import {SimpleSnippet} from "./SimpleSnippet/SimpleSnippet";
 
 // Key names matching schema name of templates
 export const SnippetsTemplates = {
   simple_snippet: SimpleSnippet,
   newsletter_snippet: NewsletterSnippet,
   fxa_signup_snippet: FXASignupSnippet,
   send_to_device_snippet: SendToDeviceSnippet,
   eoy_snippet: EOYSnippet,
+  simple_below_search_snippet: SimpleBelowSearchSnippet,
 };
--- a/browser/components/newtab/data/content/activity-stream.bundle.js
+++ b/browser/components/newtab/data/content/activity-stream.bundle.js
@@ -9755,30 +9755,76 @@ const SendToDeviceSnippet = props => {
   return external_React_default.a.createElement(SubmitFormSnippet_SubmitFormSnippet, SendToDeviceSnippet_extends({}, propsWithDefaults, {
     form_method: "POST",
     className: "send_to_device_snippet",
     inputType: propsWithDefaults.content.include_sms ? "text" : "email",
     validateInput: propsWithDefaults.content.include_sms ? validateInput : null,
     processFormData: processFormData
   }));
 };
+// CONCATENATED MODULE: ./content-src/asrouter/templates/SimpleBelowSearchSnippet/SimpleBelowSearchSnippet.jsx
+function SimpleBelowSearchSnippet_extends() { SimpleBelowSearchSnippet_extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; return SimpleBelowSearchSnippet_extends.apply(this, arguments); }
+
+
+
+
+
+const SimpleBelowSearchSnippet_DEFAULT_ICON_PATH = "chrome://branding/content/icon64.png";
+class SimpleBelowSearchSnippet_SimpleBelowSearchSnippet extends external_React_default.a.PureComponent {
+  renderText() {
+    const {
+      props
+    } = this;
+    return external_React_default.a.createElement(RichText["RichText"], {
+      text: props.content.text,
+      customElements: this.props.customElements,
+      localization_id: "text",
+      links: props.content.links,
+      sendClick: props.sendClick
+    });
+  }
+
+  render() {
+    const {
+      props
+    } = this;
+    let className = "SimpleBelowSearchSnippet";
+
+    if (props.className) {
+      className += ` ${props.className}`;
+    }
+
+    return external_React_default.a.createElement(SnippetBase_SnippetBase, SimpleBelowSearchSnippet_extends({}, props, {
+      className: className,
+      textStyle: this.props.textStyle
+    }), external_React_default.a.createElement("img", {
+      src: Object(template_utils["safeURI"])(props.content.icon) || SimpleBelowSearchSnippet_DEFAULT_ICON_PATH,
+      className: "icon"
+    }), external_React_default.a.createElement("div", null, external_React_default.a.createElement("p", {
+      className: "body"
+    }, this.renderText()), this.props.extraContent));
+  }
+
+}
 // CONCATENATED MODULE: ./content-src/asrouter/templates/template-manifest.jsx
 /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "SnippetsTemplates", function() { return SnippetsTemplates; });
 
 
 
 
+
  // Key names matching schema name of templates
 
 const SnippetsTemplates = {
   simple_snippet: SimpleSnippet_SimpleSnippet,
   newsletter_snippet: NewsletterSnippet,
   fxa_signup_snippet: FXASignupSnippet,
   send_to_device_snippet: SendToDeviceSnippet,
-  eoy_snippet: EOYSnippet
+  eoy_snippet: EOYSnippet,
+  simple_below_search_snippet: SimpleBelowSearchSnippet_SimpleBelowSearchSnippet
 };
 
 /***/ }),
 /* 56 */
 /***/ (function(module, __webpack_exports__, __webpack_require__) {
 
 "use strict";
 __webpack_require__.r(__webpack_exports__);
--- a/browser/components/newtab/lib/CFRMessageProvider.jsm
+++ b/browser/components/newtab/lib/CFRMessageProvider.jsm
@@ -38,18 +38,20 @@ const REDDIT_ENHANCEMENT_PARAMS = {
   min_frecency: 10000,
 };
 const PINNED_TABS_TARGET_SITES = [
   "docs.google.com", "www.docs.google.com", "calendar.google.com",
   "messenger.com", "www.messenger.com", "web.whatsapp.com", "mail.google.com",
   "outlook.live.com", "facebook.com", "www.facebook.com", "twitter.com", "www.twitter.com",
   "reddit.com", "www.reddit.com", "github.com", "www.github.com", "youtube.com", "www.youtube.com",
   "feedly.com", "www.feedly.com", "drive.google.com", "amazon.com", "www.amazon.com",
-  "messages.android.com",
+  "messages.android.com", "amazon.ca", "www.amazon.ca", "amazon.com.au", "www.amazon.com.au",
+  "amazon.co.uk", "www.amazon.co.uk", "amazon.fr", "www.amazon.fr", "amazon.de", "www.amazon.de",
 ];
+const PINNED_TABS_TARGET_LOCALES = ["en-US", "en-CA", "en-AU", "en-GB", "en-ZA", "en-NZ", "fr", "de"];
 
 const CFR_MESSAGES = [
   {
     id: "FACEBOOK_CONTAINER_3",
     template: "cfr_doorhanger",
     content: {
       category: "cfrAddons",
       bucket_id: "CFR_M1",
@@ -345,17 +347,17 @@ const CFR_MESSAGES = [
           label: {string_id: "cfr-doorhanger-extension-manage-settings-button"},
           action: {
             type: "OPEN_PREFERENCES_PAGE",
             data: {category: "general-cfrfeatures"},
           },
         }],
       },
     },
-    targeting: `locale == "en-US" && !hasPinnedTabs && recentVisits[.timestamp > (currentDate|date - 3600 * 1000 * 1)]|length >= 3`,
+    targeting: `locale in ${JSON.stringify(PINNED_TABS_TARGET_LOCALES)} && !hasPinnedTabs && recentVisits[.timestamp > (currentDate|date - 3600 * 1000 * 1)]|length >= 3`,
     frequency: {lifetime: 3},
     trigger: {id: "frequentVisits", params: PINNED_TABS_TARGET_SITES},
   },
 ];
 
 const CFRMessageProvider = {
   getMessages() {
     return CFR_MESSAGES.filter(msg => !msg.exclude);
--- a/browser/components/newtab/lib/SnippetsTestMessageProvider.jsm
+++ b/browser/components/newtab/lib/SnippetsTestMessageProvider.jsm
@@ -242,16 +242,26 @@ const MESSAGES = () => ([
       "title": "Firefox Account!",
       "text": "Sync it, link it, take it with you. All this and more with a Firefox Account.",
       "block_button_text": "Block",
       "section_title_icon": "resource://activity-stream/data/content/assets/glyph-pocket-16.svg",
       "section_title_text": "Messages from Mozilla (click for info)",
       "section_title_url": "https://www.mozilla.org/about",
     },
   },
+  {
+    "id": "SIMPLE_BELOW_SEARCH_TEST_1",
+    "template": "simple_below_search_snippet",
+    "content": {
+      "icon": TEST_ICON,
+      "text": "Securely store passwords, bookmarks, and more with a Firefox Account. <syncLink>Sign up</syncLink>",
+      "links": {"syncLink": {"url": "https://www.mozilla.org/en-US/firefox/accounts"}},
+      "block_button_text": "Block",
+    },
+  },
 ]);
 
 const SnippetsTestMessageProvider = {
   getMessages() {
     return MESSAGES()
       // Ensures we never actually show test except when triggered by debug tools
       .map(message => ({...message, targeting: `providerCohorts.snippets_local_testing == "SHOW_TEST"`}));
   },
--- a/browser/components/newtab/test/unit/asrouter/CFRMessageProvider.test.js
+++ b/browser/components/newtab/test/unit/asrouter/CFRMessageProvider.test.js
@@ -27,23 +27,25 @@ describe("CFRMessageProvider", () => {
       // Ensure that the CFR messages that are recommending an addon have this targeting.
       // In the future when we can do targeting based on category, this test will change.
       // See bug 1494778 and 1497653
       if (message.id !== "PIN_TAB") {
         assert.include(message.targeting, `(xpinstallEnabled == true)`);
       }
     }
   });
-  it("should restrict all messages to `en` locale for now", () => {
-    for (const message of messages) {
-      if (message.id !== "PIN_TAB") {
-        assert.include(message.targeting, `localeLanguageCode == "en"`);
-      } else {
-        assert.include(message.targeting, `locale == "en-US"`);
-      }
+  it("should restrict all messages to `en` locale for now (PIN TAB is handled separately)", () => {
+    for (const message of messages.filter(m => m.id !== "PIN_TAB")) {
+      assert.include(message.targeting, `localeLanguageCode == "en"`);
     }
   });
+  it("should restrict locale for PIN_TAB message", () => {
+    const pinTabMessage = messages.find(m => m.id === "PIN_TAB");
+
+    // 6 en-* locales, fr and de
+    assert.lengthOf(pinTabMessage.targeting.match(/en-|fr|de/g), 8);
+  });
   it("should contain `www.` version of the hosts", () => {
     const pinTabMessage = messages.find(m => m.id === "PIN_TAB");
 
     assert.isTrue(pinTabMessage.trigger.params.filter(host => host.startsWith("www.")).length > 0);
   });
 });
--- a/browser/components/newtab/test/unit/asrouter/SnippetsTestMessageProvider.test.js
+++ b/browser/components/newtab/test/unit/asrouter/SnippetsTestMessageProvider.test.js
@@ -1,19 +1,21 @@
 import EOYSnippetSchema from "../../../content-src/asrouter/templates/EOYSnippet/EOYSnippet.schema.json";
+import SimpleBelowSearchSnippetSchema from "../../../content-src/asrouter/templates/SimpleBelowSearchSnippet/SimpleBelowSearchSnippet.schema.json";
 import SimpleSnippetSchema from "../../../content-src/asrouter/templates/SimpleSnippet/SimpleSnippet.schema.json";
 import {SnippetsTestMessageProvider} from "../../../lib/SnippetsTestMessageProvider.jsm";
 import SubmitFormSnippetSchema from "../../../content-src/asrouter/templates/SubmitFormSnippet/SubmitFormSnippet.schema.json";
 
 const schemas = {
   "simple_snippet": SimpleSnippetSchema,
   "newsletter_snippet": SubmitFormSnippetSchema,
   "fxa_signup_snippet": SubmitFormSnippetSchema,
   "send_to_device_snippet": SubmitFormSnippetSchema,
   "eoy_snippet": EOYSnippetSchema,
+  "simple_below_search_snippet": SimpleBelowSearchSnippetSchema,
 };
 
 describe("SnippetsTestMessageProvider", () => {
   let messages = SnippetsTestMessageProvider.getMessages();
 
   it("should return an array of messages", () => {
     assert.isArray(messages);
   });
new file mode 100644
--- /dev/null
+++ b/browser/components/newtab/test/unit/asrouter/schemas/panel/cfr-fxa-bookmark.schema.test.js
@@ -0,0 +1,34 @@
+import schema from "content-src/asrouter/schemas/panel/cfr-fxa-bookmark.schema.json";
+
+const DEFAULT_CONTENT = {
+  "title": "Sync your bookmarks everywhere",
+  "text": "Great find! Now don't be left without this bookmark.",
+  "link": {
+    "text": "Sync bookmarks now",
+    "url": "https://mozilla.com",
+  },
+  "info_icon": {
+    "tooltiptext": "Learn more",
+  },
+};
+
+const L10N_CONTENT = {
+  "title": {string_id: "cfr-bookmark-title"},
+  "text": {string_id: "cfr-bookmark-body"},
+  "link": {
+    "text": {string_id: "cfr-bookmark-link-text"},
+    "url": "https://mozilla.com",
+  },
+  "info_icon": {
+    "tooltiptext": {string_id: "cfr-bookmark-tooltip-text"},
+  },
+};
+
+describe("CFR FxA Message Schema", () => {
+  it("should validate DEFAULT_CONTENT", () => {
+    assert.jsonSchema(DEFAULT_CONTENT, schema);
+  });
+  it("should validate L10N_CONTENT", () => {
+    assert.jsonSchema(L10N_CONTENT, schema);
+  });
+});
new file mode 100644
--- /dev/null
+++ b/browser/components/newtab/test/unit/asrouter/templates/SimpleBelowSearchSnippet.test.jsx
@@ -0,0 +1,47 @@
+import {mount} from "enzyme";
+import React from "react";
+import schema from "content-src/asrouter/templates/SimpleBelowSearchSnippet/SimpleBelowSearchSnippet.schema.json";
+import {SimpleBelowSearchSnippet} from "content-src/asrouter/templates/SimpleBelowSearchSnippet/SimpleBelowSearchSnippet.jsx";
+
+const DEFAULT_CONTENT = {text: "foo"};
+
+describe("SimpleBelowSearchSnippet", () => {
+  let sandbox;
+  let sendUserActionTelemetryStub;
+
+  /**
+   * mountAndCheckProps - Mounts a SimpleBelowSearchSnippet with DEFAULT_CONTENT extended with any props
+   *                      passed in the content param and validates props against the schema.
+   * @param {obj} content Object containing custom message content (e.g. {text, icon})
+   * @returns enzyme wrapper for SimpleSnippet
+   */
+  function mountAndCheckProps(content = {}, provider = "test-provider") {
+    const props = {
+      content: {...DEFAULT_CONTENT, ...content},
+      provider,
+      sendUserActionTelemetry: sendUserActionTelemetryStub,
+      onAction: sandbox.stub(),
+    };
+    assert.jsonSchema(props.content, schema);
+    return mount(<SimpleBelowSearchSnippet {...props} />);
+  }
+
+  beforeEach(() => {
+    sandbox = sinon.createSandbox();
+    sendUserActionTelemetryStub = sandbox.stub();
+  });
+
+  afterEach(() => {
+    sandbox.restore();
+  });
+
+  it("should render .text", () => {
+    const wrapper = mountAndCheckProps({text: "bar"});
+    assert.equal(wrapper.find(".body").text(), "bar");
+  });
+
+  it("should render .icon", () => {
+    const wrapper = mountAndCheckProps({icon: "data:image/gif;base64,R0lGODl"});
+    assert.equal(wrapper.find(".icon").prop("src"), "data:image/gif;base64,R0lGODl");
+  });
+});