summaryrefslogtreecommitdiffstats
path: root/browser/components/newtab/test/unit/asrouter/templates
diff options
context:
space:
mode:
Diffstat (limited to 'browser/components/newtab/test/unit/asrouter/templates')
-rw-r--r--browser/components/newtab/test/unit/asrouter/templates/EOYSnippet.test.jsx213
-rw-r--r--browser/components/newtab/test/unit/asrouter/templates/ExtensionDoorhanger.test.jsx112
-rw-r--r--browser/components/newtab/test/unit/asrouter/templates/FXASignupSnippet.test.jsx106
-rw-r--r--browser/components/newtab/test/unit/asrouter/templates/NewsletterSnippet.test.jsx108
-rw-r--r--browser/components/newtab/test/unit/asrouter/templates/SendToDeviceSnippet.test.jsx277
-rw-r--r--browser/components/newtab/test/unit/asrouter/templates/SimpleBelowSearchSnippet.test.jsx81
-rw-r--r--browser/components/newtab/test/unit/asrouter/templates/SimpleSnippet.test.jsx259
-rw-r--r--browser/components/newtab/test/unit/asrouter/templates/SubmitFormSnippet.test.jsx354
-rw-r--r--browser/components/newtab/test/unit/asrouter/templates/isEmailOrPhoneNumber.test.js56
9 files changed, 1566 insertions, 0 deletions
diff --git a/browser/components/newtab/test/unit/asrouter/templates/EOYSnippet.test.jsx b/browser/components/newtab/test/unit/asrouter/templates/EOYSnippet.test.jsx
new file mode 100644
index 0000000000..bd4ab00468
--- /dev/null
+++ b/browser/components/newtab/test/unit/asrouter/templates/EOYSnippet.test.jsx
@@ -0,0 +1,213 @@
+import { EOYSnippet } from "content-src/asrouter/templates/EOYSnippet/EOYSnippet";
+import { GlobalOverrider } from "test/unit/utils";
+import { mount } from "enzyme";
+import React from "react";
+import { FluentBundle, FluentResource } from "@fluent/bundle";
+import { LocalizationProvider, ReactLocalization } from "@fluent/react";
+import schema from "content-src/asrouter/templates/EOYSnippet/EOYSnippet.schema.json";
+
+const DEFAULT_CONTENT = {
+ text: "foo",
+ donation_amount_first: 50,
+ donation_amount_second: 25,
+ donation_amount_third: 10,
+ donation_amount_fourth: 5,
+ donation_form_url: "https://submit.form",
+ button_label: "Donate",
+};
+
+describe("EOYSnippet", () => {
+ let sandbox;
+ let wrapper;
+
+ function mockL10nWrapper(content) {
+ const bundle = new FluentBundle("en-US");
+ for (const [id, value] of Object.entries(content)) {
+ if (typeof value === "string") {
+ bundle.addResource(new FluentResource(`${id} = ${value}`));
+ }
+ }
+ const l10n = new ReactLocalization([bundle]);
+ return {
+ wrappingComponent: LocalizationProvider,
+ wrappingComponentProps: { l10n },
+ };
+ }
+
+ /**
+ * mountAndCheckProps - Mounts a EOYSnippet 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, title})
+ * @returns enzyme wrapper for EOYSnippet
+ */
+ function mountAndCheckProps(content = {}, provider = "test-provider") {
+ const props = {
+ content: Object.assign({}, DEFAULT_CONTENT, content),
+ provider,
+ onAction: sandbox.stub(),
+ onBlock: sandbox.stub(),
+ sendClick: sandbox.stub(),
+ };
+ const comp = mount(
+ <EOYSnippet {...props} />,
+ mockL10nWrapper(props.content)
+ );
+ // Check schema with the final props the component receives (including defaults)
+ assert.jsonSchema(comp.children().get(0).props.content, schema);
+ return comp;
+ }
+
+ beforeEach(() => {
+ sandbox = sinon.createSandbox();
+ wrapper = mountAndCheckProps();
+ });
+
+ afterEach(() => {
+ sandbox.restore();
+ });
+
+ it("should have the correct defaults", () => {
+ wrapper = mountAndCheckProps();
+ // SendToDeviceSnippet is a wrapper around SubmitFormSnippet
+ const { props } = wrapper.children().get(0);
+
+ const defaultProperties = Object.keys(schema.properties).filter(
+ prop => schema.properties[prop].default
+ );
+ assert.lengthOf(defaultProperties, 4);
+ defaultProperties.forEach(prop =>
+ assert.propertyVal(props.content, prop, schema.properties[prop].default)
+ );
+ });
+
+ it("should render 4 donation options", () => {
+ assert.lengthOf(wrapper.find("input[type='radio']"), 4);
+ });
+
+ it("should have a data-metric field", () => {
+ assert.ok(wrapper.find("form[data-metric='EOYSnippetForm']").exists());
+ });
+
+ it("should select the second donation option", () => {
+ wrapper = mountAndCheckProps({ selected_button: "donation_amount_second" });
+
+ assert.propertyVal(
+ wrapper.find("input[type='radio']").get(1).props,
+ "defaultChecked",
+ true
+ );
+ });
+
+ it("should set frequency value to monthly", () => {
+ const form = wrapper.find("form").instance();
+ assert.equal(form.querySelector("[name='frequency']").value, "single");
+
+ form.querySelector("#monthly-checkbox").checked = true;
+ wrapper.find("form").simulate("submit");
+
+ assert.equal(form.querySelector("[name='frequency']").value, "monthly");
+ });
+
+ it("should block after submitting the form", () => {
+ const onBlockStub = sandbox.stub();
+ wrapper.setProps({ onBlock: onBlockStub });
+
+ wrapper.find("form").simulate("submit");
+
+ assert.calledOnce(onBlockStub);
+ });
+
+ it("should not block if do_not_autoblock is true", () => {
+ const onBlockStub = sandbox.stub();
+ wrapper = mountAndCheckProps({ do_not_autoblock: true });
+ wrapper.setProps({ onBlock: onBlockStub });
+
+ wrapper.find("form").simulate("submit");
+
+ assert.notCalled(onBlockStub);
+ });
+
+ it("should report form submissions", () => {
+ wrapper = mountAndCheckProps();
+ const { sendClick } = wrapper.props();
+
+ wrapper.find("form").simulate("submit");
+
+ assert.calledOnce(sendClick);
+ assert.equal(
+ sendClick.firstCall.args[0].target.dataset.metric,
+ "EOYSnippetForm"
+ );
+ });
+
+ it("it should preserve URL GET params as hidden inputs", () => {
+ wrapper = mountAndCheckProps({
+ donation_form_url:
+ "https://donate.mozilla.org/pl/?utm_source=desktop-snippet&amp;utm_medium=snippet&amp;utm_campaign=donate&amp;utm_term=7556",
+ });
+
+ const hiddenInputs = wrapper.find("input[type='hidden']");
+
+ assert.propertyVal(
+ hiddenInputs.find("[name='utm_source']").props(),
+ "value",
+ "desktop-snippet"
+ );
+ assert.propertyVal(
+ hiddenInputs.find("[name='amp;utm_medium']").props(),
+ "value",
+ "snippet"
+ );
+ assert.propertyVal(
+ hiddenInputs.find("[name='amp;utm_campaign']").props(),
+ "value",
+ "donate"
+ );
+ assert.propertyVal(
+ hiddenInputs.find("[name='amp;utm_term']").props(),
+ "value",
+ "7556"
+ );
+ });
+
+ describe("locale", () => {
+ let stub;
+ let globals;
+ beforeEach(() => {
+ globals = new GlobalOverrider();
+ stub = sandbox.stub().returns({ format: () => {} });
+
+ globals = new GlobalOverrider();
+ globals.set({ Intl: { NumberFormat: stub } });
+ });
+ afterEach(() => {
+ globals.restore();
+ });
+
+ it("should use content.locale for Intl", () => {
+ // triggers component rendering and calls the function we're testing
+ wrapper.setProps({
+ content: {
+ locale: "locale-foo",
+ donation_form_url: DEFAULT_CONTENT.donation_form_url,
+ },
+ });
+
+ assert.calledOnce(stub);
+ assert.calledWithExactly(stub, "locale-foo", sinon.match.object);
+ });
+
+ it("should use navigator.language as locale fallback", () => {
+ // triggers component rendering and calls the function we're testing
+ wrapper.setProps({
+ content: {
+ locale: null,
+ donation_form_url: DEFAULT_CONTENT.donation_form_url,
+ },
+ });
+
+ assert.calledOnce(stub);
+ assert.calledWithExactly(stub, navigator.language, sinon.match.object);
+ });
+ });
+});
diff --git a/browser/components/newtab/test/unit/asrouter/templates/ExtensionDoorhanger.test.jsx b/browser/components/newtab/test/unit/asrouter/templates/ExtensionDoorhanger.test.jsx
new file mode 100644
index 0000000000..bef14c6982
--- /dev/null
+++ b/browser/components/newtab/test/unit/asrouter/templates/ExtensionDoorhanger.test.jsx
@@ -0,0 +1,112 @@
+import { CFRMessageProvider } from "lib/CFRMessageProvider.sys.mjs";
+import CFRDoorhangerSchema from "content-src/asrouter/templates/CFR/templates/ExtensionDoorhanger.schema.json";
+import CFRChicletSchema from "content-src/asrouter/templates/CFR/templates/CFRUrlbarChiclet.schema.json";
+import InfoBarSchema from "content-src/asrouter/templates/CFR/templates/InfoBar.schema.json";
+
+const SCHEMAS = {
+ cfr_urlbar_chiclet: CFRChicletSchema,
+ cfr_doorhanger: CFRDoorhangerSchema,
+ milestone_message: CFRDoorhangerSchema,
+ infobar: InfoBarSchema,
+};
+
+const DEFAULT_CONTENT = {
+ layout: "addon_recommendation",
+ category: "dummyCategory",
+ bucket_id: "some_bucket_id",
+ notification_text: "Recommendation",
+ heading_text: "Recommended Extension",
+ info_icon: {
+ label: { attributes: { tooltiptext: "Why am I seeing this" } },
+ sumo_path: "extensionrecommendations",
+ },
+ addon: {
+ id: "1234",
+ title: "Addon name",
+ icon: "https://mozilla.org/icon",
+ author: "Author name",
+ amo_url: "https://example.com",
+ },
+ text: "Description of addon",
+ buttons: {
+ primary: {
+ label: {
+ value: "Add Now",
+ attributes: { accesskey: "A" },
+ },
+ action: {
+ type: "INSTALL_ADDON_FROM_URL",
+ data: { url: "https://example.com" },
+ },
+ },
+ secondary: [
+ {
+ label: {
+ value: "Not Now",
+ attributes: { accesskey: "N" },
+ },
+ action: { type: "CANCEL" },
+ },
+ ],
+ },
+};
+
+const L10N_CONTENT = {
+ layout: "addon_recommendation",
+ category: "dummyL10NCategory",
+ bucket_id: "some_bucket_id",
+ notification_text: { string_id: "notification_text_id" },
+ heading_text: { string_id: "heading_text_id" },
+ info_icon: {
+ label: { string_id: "why_seeing_this" },
+ sumo_path: "extensionrecommendations",
+ },
+ addon: {
+ id: "1234",
+ title: "Addon name",
+ icon: "https://mozilla.org/icon",
+ author: "Author name",
+ amo_url: "https://example.com",
+ },
+ text: { string_id: "text_id" },
+ buttons: {
+ primary: {
+ label: { string_id: "btn_ok_id" },
+ action: {
+ type: "INSTALL_ADDON_FROM_URL",
+ data: { url: "https://example.com" },
+ },
+ },
+ secondary: [
+ {
+ label: { string_id: "btn_cancel_id" },
+ action: { type: "CANCEL" },
+ },
+ ],
+ },
+};
+
+describe("ExtensionDoorhanger", () => {
+ it("should validate DEFAULT_CONTENT", async () => {
+ const messages = await CFRMessageProvider.getMessages();
+ let doorhangerMessage = messages.find(m => m.id === "FACEBOOK_CONTAINER_3");
+ assert.ok(doorhangerMessage, "Message found");
+ assert.jsonSchema(
+ { ...doorhangerMessage, content: DEFAULT_CONTENT },
+ CFRDoorhangerSchema
+ );
+ });
+ it("should validate L10N_CONTENT", async () => {
+ const messages = await CFRMessageProvider.getMessages();
+ let doorhangerMessage = messages.find(m => m.id === "FACEBOOK_CONTAINER_3");
+ assert.ok(doorhangerMessage, "Message found");
+ assert.jsonSchema(
+ { ...doorhangerMessage, content: L10N_CONTENT },
+ CFRDoorhangerSchema
+ );
+ });
+ it("should validate all messages from CFRMessageProvider", async () => {
+ const messages = await CFRMessageProvider.getMessages();
+ messages.forEach(msg => assert.jsonSchema(msg, SCHEMAS[msg.template]));
+ });
+});
diff --git a/browser/components/newtab/test/unit/asrouter/templates/FXASignupSnippet.test.jsx b/browser/components/newtab/test/unit/asrouter/templates/FXASignupSnippet.test.jsx
new file mode 100644
index 0000000000..56828d266b
--- /dev/null
+++ b/browser/components/newtab/test/unit/asrouter/templates/FXASignupSnippet.test.jsx
@@ -0,0 +1,106 @@
+import { FXASignupSnippet } from "content-src/asrouter/templates/FXASignupSnippet/FXASignupSnippet";
+import { mount } from "enzyme";
+import React from "react";
+import { FluentBundle, FluentResource } from "@fluent/bundle";
+import { LocalizationProvider, ReactLocalization } from "@fluent/react";
+import schema from "content-src/asrouter/templates/FXASignupSnippet/FXASignupSnippet.schema.json";
+import { SnippetsTestMessageProvider } from "lib/SnippetsTestMessageProvider.sys.mjs";
+
+describe("FXASignupSnippet", () => {
+ let DEFAULT_CONTENT;
+ let sandbox;
+
+ function mockL10nWrapper(content) {
+ const bundle = new FluentBundle("en-US");
+ for (const [id, value] of Object.entries(content)) {
+ if (typeof value === "string") {
+ bundle.addResource(new FluentResource(`${id} = ${value}`));
+ }
+ }
+ const l10n = new ReactLocalization([bundle]);
+ return {
+ wrappingComponent: LocalizationProvider,
+ wrappingComponentProps: { l10n },
+ };
+ }
+
+ function mountAndCheckProps(content = {}) {
+ const props = {
+ id: "foo123",
+ content: Object.assign(
+ { utm_campaign: "foo", utm_term: "bar" },
+ DEFAULT_CONTENT,
+ content
+ ),
+ onBlock() {},
+ onDismiss: sandbox.stub(),
+ sendUserActionTelemetry: sandbox.stub(),
+ onAction: sandbox.stub(),
+ };
+ const comp = mount(
+ <FXASignupSnippet {...props} />,
+ mockL10nWrapper(props.content)
+ );
+ // Check schema with the final props the component receives (including defaults)
+ assert.jsonSchema(comp.children().get(0).props.content, schema);
+ return comp;
+ }
+
+ beforeEach(async () => {
+ sandbox = sinon.createSandbox();
+ DEFAULT_CONTENT = (await SnippetsTestMessageProvider.getMessages()).find(
+ msg => msg.template === "fxa_signup_snippet"
+ ).content;
+ });
+ afterEach(() => {
+ sandbox.restore();
+ });
+
+ it("should have the correct defaults", () => {
+ const defaults = {
+ id: "foo123",
+ onBlock() {},
+ content: {},
+ onDismiss: sandbox.stub(),
+ sendUserActionTelemetry: sandbox.stub(),
+ onAction: sandbox.stub(),
+ };
+ const wrapper = mount(
+ <FXASignupSnippet {...defaults} />,
+ mockL10nWrapper(DEFAULT_CONTENT)
+ );
+ // FXASignupSnippet is a wrapper around SubmitFormSnippet
+ const { props } = wrapper.children().get(0);
+
+ const defaultProperties = Object.keys(schema.properties).filter(
+ prop => schema.properties[prop].default
+ );
+ assert.lengthOf(defaultProperties, 5);
+ defaultProperties.forEach(prop =>
+ assert.propertyVal(props.content, prop, schema.properties[prop].default)
+ );
+
+ const defaultHiddenProperties = Object.keys(
+ schema.properties.hidden_inputs.properties
+ ).filter(prop => schema.properties.hidden_inputs.properties[prop].default);
+ assert.lengthOf(defaultHiddenProperties, 0);
+ });
+
+ it("should have a form_action", () => {
+ const wrapper = mountAndCheckProps();
+
+ assert.propertyVal(
+ wrapper.children().get(0).props,
+ "form_action",
+ "https://accounts.firefox.com/"
+ );
+ });
+
+ it("should navigate to scene2", () => {
+ const wrapper = mountAndCheckProps({});
+
+ wrapper.find(".ASRouterButton").simulate("click");
+
+ assert.lengthOf(wrapper.find(".mainInput"), 1);
+ });
+});
diff --git a/browser/components/newtab/test/unit/asrouter/templates/NewsletterSnippet.test.jsx b/browser/components/newtab/test/unit/asrouter/templates/NewsletterSnippet.test.jsx
new file mode 100644
index 0000000000..cb80abdae0
--- /dev/null
+++ b/browser/components/newtab/test/unit/asrouter/templates/NewsletterSnippet.test.jsx
@@ -0,0 +1,108 @@
+import { mount } from "enzyme";
+import { NewsletterSnippet } from "content-src/asrouter/templates/NewsletterSnippet/NewsletterSnippet";
+import React from "react";
+import { FluentBundle, FluentResource } from "@fluent/bundle";
+import { LocalizationProvider, ReactLocalization } from "@fluent/react";
+import schema from "content-src/asrouter/templates/NewsletterSnippet/NewsletterSnippet.schema.json";
+import { SnippetsTestMessageProvider } from "lib/SnippetsTestMessageProvider.sys.mjs";
+
+describe("NewsletterSnippet", () => {
+ let sandbox;
+ let DEFAULT_CONTENT;
+
+ function mockL10nWrapper(content) {
+ const bundle = new FluentBundle("en-US");
+ for (const [id, value] of Object.entries(content)) {
+ if (typeof value === "string") {
+ bundle.addResource(new FluentResource(`${id} = ${value}`));
+ }
+ }
+ const l10n = new ReactLocalization([bundle]);
+ return {
+ wrappingComponent: LocalizationProvider,
+ wrappingComponentProps: { l10n },
+ };
+ }
+
+ function mountAndCheckProps(content = {}) {
+ const props = {
+ id: "foo123",
+ content: Object.assign({}, DEFAULT_CONTENT, content),
+ onBlock() {},
+ onDismiss: sandbox.stub(),
+ sendUserActionTelemetry: sandbox.stub(),
+ onAction: sandbox.stub(),
+ };
+ const comp = mount(
+ <NewsletterSnippet {...props} />,
+ mockL10nWrapper(props.content)
+ );
+ // Check schema with the final props the component receives (including defaults)
+ assert.jsonSchema(comp.children().get(0).props.content, schema);
+ return comp;
+ }
+
+ beforeEach(async () => {
+ sandbox = sinon.createSandbox();
+ DEFAULT_CONTENT = (await SnippetsTestMessageProvider.getMessages()).find(
+ msg => msg.template === "newsletter_snippet"
+ ).content;
+ });
+ afterEach(() => {
+ sandbox.restore();
+ });
+
+ describe("schema test", () => {
+ it("should validate the schema and defaults", () => {
+ const wrapper = mountAndCheckProps();
+ wrapper.find(".ASRouterButton").simulate("click");
+ assert.equal(wrapper.find(".mainInput").instance().type, "email");
+ });
+
+ it("should have all of the default fields", () => {
+ const defaults = {
+ id: "foo123",
+ content: {},
+ onBlock() {},
+ onDismiss: sandbox.stub(),
+ sendUserActionTelemetry: sandbox.stub(),
+ onAction: sandbox.stub(),
+ };
+ const wrapper = mount(
+ <NewsletterSnippet {...defaults} />,
+ mockL10nWrapper(DEFAULT_CONTENT)
+ );
+ // NewsletterSnippet is a wrapper around SubmitFormSnippet
+ const { props } = wrapper.children().get(0);
+
+ // the `locale` properties gets used as part of hidden_fields so we
+ // check for it separately
+ const properties = { ...schema.properties };
+ const { locale } = properties;
+ delete properties.locale;
+
+ const defaultProperties = Object.keys(properties).filter(
+ prop => properties[prop].default
+ );
+ assert.lengthOf(defaultProperties, 6);
+ defaultProperties.forEach(prop =>
+ assert.propertyVal(props.content, prop, properties[prop].default)
+ );
+
+ const defaultHiddenProperties = Object.keys(
+ schema.properties.hidden_inputs.properties
+ ).filter(
+ prop => schema.properties.hidden_inputs.properties[prop].default
+ );
+ assert.lengthOf(defaultHiddenProperties, 1);
+ defaultHiddenProperties.forEach(prop =>
+ assert.propertyVal(
+ props.content.hidden_inputs,
+ prop,
+ schema.properties.hidden_inputs.properties[prop].default
+ )
+ );
+ assert.propertyVal(props.content.hidden_inputs, "lang", locale.default);
+ });
+ });
+});
diff --git a/browser/components/newtab/test/unit/asrouter/templates/SendToDeviceSnippet.test.jsx b/browser/components/newtab/test/unit/asrouter/templates/SendToDeviceSnippet.test.jsx
new file mode 100644
index 0000000000..3c60967643
--- /dev/null
+++ b/browser/components/newtab/test/unit/asrouter/templates/SendToDeviceSnippet.test.jsx
@@ -0,0 +1,277 @@
+import { mount } from "enzyme";
+import React from "react";
+import { FluentBundle, FluentResource } from "@fluent/bundle";
+import { LocalizationProvider, ReactLocalization } from "@fluent/react";
+import schema from "content-src/asrouter/templates/SendToDeviceSnippet/SendToDeviceSnippet.schema.json";
+import {
+ SendToDeviceSnippet,
+ SendToDeviceScene2Snippet,
+} from "content-src/asrouter/templates/SendToDeviceSnippet/SendToDeviceSnippet";
+import { SnippetsTestMessageProvider } from "lib/SnippetsTestMessageProvider.sys.mjs";
+
+async function testBodyContains(body, key, value) {
+ const regex = new RegExp(
+ `Content-Disposition: form-data; name="${key}"${value}`
+ );
+ const match = regex.exec(body);
+ return match;
+}
+
+/**
+ * Simulates opening the second panel (form view), filling in the input, and submitting
+ * @param {EnzymeWrapper} wrapper A SendToDevice wrapper
+ * @param {string} value Email or phone number
+ * @param {function?} setCustomValidity setCustomValidity stub
+ */
+function openFormAndSetValue(wrapper, value, setCustomValidity = () => {}) {
+ // expand
+ wrapper.find(".ASRouterButton").simulate("click");
+ // Fill in email
+ const input = wrapper.find(".mainInput");
+ input.instance().value = value;
+ input.simulate("change", { target: { value, setCustomValidity } });
+ wrapper.find("form").simulate("submit");
+}
+
+describe("SendToDeviceSnippet", () => {
+ let sandbox;
+ let fetchStub;
+ let jsonResponse;
+ let DEFAULT_CONTENT;
+ let DEFAULT_SCENE2_CONTENT;
+
+ function mockL10nWrapper(content) {
+ const bundle = new FluentBundle("en-US");
+ for (const [id, value] of Object.entries(content)) {
+ if (typeof value === "string") {
+ bundle.addResource(new FluentResource(`${id} = ${value}`));
+ }
+ }
+ const l10n = new ReactLocalization([bundle]);
+ return {
+ wrappingComponent: LocalizationProvider,
+ wrappingComponentProps: { l10n },
+ };
+ }
+
+ function mountAndCheckProps(content = {}) {
+ const props = {
+ id: "foo123",
+ content: Object.assign({}, DEFAULT_CONTENT, content),
+ onBlock() {},
+ onDismiss: sandbox.stub(),
+ sendUserActionTelemetry: sandbox.stub(),
+ onAction: sandbox.stub(),
+ };
+ const comp = mount(
+ <SendToDeviceSnippet {...props} />,
+ mockL10nWrapper(props.content)
+ );
+ // Check schema with the final props the component receives (including defaults)
+ assert.jsonSchema(comp.children().get(0).props.content, schema);
+ return comp;
+ }
+
+ beforeEach(async () => {
+ DEFAULT_CONTENT = (await SnippetsTestMessageProvider.getMessages()).find(
+ msg => msg.template === "send_to_device_snippet"
+ ).content;
+ DEFAULT_SCENE2_CONTENT = (
+ await SnippetsTestMessageProvider.getMessages()
+ ).find(msg => msg.template === "send_to_device_scene2_snippet").content;
+ sandbox = sinon.createSandbox();
+ jsonResponse = { status: "ok" };
+ fetchStub = sandbox
+ .stub(global, "fetch")
+ .returns(Promise.resolve({ json: () => Promise.resolve(jsonResponse) }));
+ });
+ afterEach(() => {
+ sandbox.restore();
+ });
+
+ it("should have the correct defaults", () => {
+ const defaults = {
+ id: "foo123",
+ onBlock() {},
+ content: {},
+ onDismiss: sandbox.stub(),
+ sendUserActionTelemetry: sandbox.stub(),
+ onAction: sandbox.stub(),
+ form_method: "POST",
+ };
+ const wrapper = mount(
+ <SendToDeviceSnippet {...defaults} />,
+ mockL10nWrapper(DEFAULT_CONTENT)
+ );
+ // SendToDeviceSnippet is a wrapper around SubmitFormSnippet
+ const { props } = wrapper.children().get(0);
+
+ const defaultProperties = Object.keys(schema.properties).filter(
+ prop => schema.properties[prop].default
+ );
+ assert.lengthOf(defaultProperties, 7);
+ defaultProperties.forEach(prop =>
+ assert.propertyVal(props.content, prop, schema.properties[prop].default)
+ );
+
+ const defaultHiddenProperties = Object.keys(
+ schema.properties.hidden_inputs.properties
+ ).filter(prop => schema.properties.hidden_inputs.properties[prop].default);
+ assert.lengthOf(defaultHiddenProperties, 0);
+ });
+
+ describe("form input", () => {
+ it("should set the input type to text if content.include_sms is true", () => {
+ const wrapper = mountAndCheckProps({ include_sms: true });
+ wrapper.find(".ASRouterButton").simulate("click");
+ assert.equal(wrapper.find(".mainInput").instance().type, "text");
+ });
+ it("should set the input type to email if content.include_sms is false", () => {
+ const wrapper = mountAndCheckProps({ include_sms: false });
+ wrapper.find(".ASRouterButton").simulate("click");
+ assert.equal(wrapper.find(".mainInput").instance().type, "email");
+ });
+ it("should validate the input with isEmailOrPhoneNumber if include_sms is true", () => {
+ const wrapper = mountAndCheckProps({ include_sms: true });
+ const setCustomValidity = sandbox.stub();
+ openFormAndSetValue(wrapper, "foo", setCustomValidity);
+ assert.calledWith(
+ setCustomValidity,
+ "Must be an email or a phone number."
+ );
+ });
+ it("should not custom validate the input if include_sms is false", () => {
+ const wrapper = mountAndCheckProps({ include_sms: false });
+ const setCustomValidity = sandbox.stub();
+ openFormAndSetValue(wrapper, "foo", setCustomValidity);
+ assert.notCalled(setCustomValidity);
+ });
+ });
+
+ describe("submitting", () => {
+ it("should send the right information to basket.mozilla.org/news/subscribe for an email", async () => {
+ const wrapper = mountAndCheckProps({
+ locale: "fr-CA",
+ include_sms: true,
+ message_id_email: "foo",
+ });
+
+ openFormAndSetValue(wrapper, "foo@bar.com");
+ wrapper.find("form").simulate("submit");
+
+ assert.calledOnce(fetchStub);
+ const [request] = fetchStub.firstCall.args;
+
+ assert.equal(request.url, "https://basket.mozilla.org/news/subscribe/");
+ const body = await request.text();
+ assert.ok(testBodyContains(body, "email", "foo@bar.com"), "has email");
+ assert.ok(testBodyContains(body, "lang", "fr-CA"), "has lang");
+ assert.ok(
+ testBodyContains(body, "newsletters", "foo"),
+ "has newsletters"
+ );
+ assert.ok(
+ testBodyContains(body, "source_url", "foo"),
+ "https%3A%2F%2Fsnippets.mozilla.com%2Fshow%2Ffoo123"
+ );
+ });
+ it("should send the right information for an sms", async () => {
+ const wrapper = mountAndCheckProps({
+ locale: "fr-CA",
+ include_sms: true,
+ message_id_sms: "foo",
+ country: "CA",
+ });
+
+ openFormAndSetValue(wrapper, "5371283767");
+ wrapper.find("form").simulate("submit");
+
+ assert.calledOnce(fetchStub);
+ const [request] = fetchStub.firstCall.args;
+
+ assert.equal(
+ request.url,
+ "https://basket.mozilla.org/news/subscribe_sms/"
+ );
+ const body = await request.text();
+ assert.ok(
+ testBodyContains(body, "mobile_number", "5371283767"),
+ "has number"
+ );
+ assert.ok(testBodyContains(body, "lang", "fr-CA"), "has lang");
+ assert.ok(testBodyContains(body, "country", "CA"), "CA");
+ assert.ok(testBodyContains(body, "msg_name", "foo"), "has msg_name");
+ });
+ });
+
+ describe("SendToDeviceScene2Snippet", () => {
+ function mountWithProps(content = {}) {
+ const props = {
+ id: "foo123",
+ content: Object.assign({}, DEFAULT_SCENE2_CONTENT, content),
+ onBlock() {},
+ onDismiss: sandbox.stub(),
+ sendUserActionTelemetry: sandbox.stub(),
+ onAction: sandbox.stub(),
+ };
+ return mount(
+ <SendToDeviceScene2Snippet {...props} />,
+ mockL10nWrapper(props.content)
+ );
+ }
+
+ it("should render scene 2", () => {
+ const wrapper = mountWithProps();
+
+ assert.lengthOf(wrapper.find(".scene2Icon"), 1, "Found scene 2 icon");
+ assert.lengthOf(
+ wrapper.find(".scene2Title"),
+ 0,
+ "Should not have a large header"
+ );
+ });
+ it("should have block button", () => {
+ const wrapper = mountWithProps();
+
+ assert.lengthOf(
+ wrapper.find(".blockButton"),
+ 1,
+ "Found the block button"
+ );
+ });
+ it("should render title text", () => {
+ const wrapper = mountWithProps();
+
+ assert.lengthOf(
+ wrapper.find(".section-title-text"),
+ 1,
+ "Found the section title"
+ );
+ assert.lengthOf(
+ wrapper.find(".section-title .icon"),
+ 2, // light and dark theme
+ "Found scene 2 title"
+ );
+ });
+ it("should wrap the header in an anchor tag if condition is defined", () => {
+ const sectionTitleProp = {
+ section_title_url: "https://support.mozilla.org",
+ };
+ let wrapper = mountWithProps(sectionTitleProp);
+
+ const element = wrapper.find(".section-title a");
+ assert.lengthOf(element, 1);
+ });
+ it("should render a header without an anchor", () => {
+ const sectionTitleProp = {
+ section_title_url: undefined,
+ };
+ let wrapper = mountWithProps(sectionTitleProp);
+ assert.lengthOf(wrapper.find(".section-title a"), 0);
+ assert.equal(
+ wrapper.find(".section-title").instance().innerText,
+ DEFAULT_SCENE2_CONTENT.section_title_text
+ );
+ });
+ });
+});
diff --git a/browser/components/newtab/test/unit/asrouter/templates/SimpleBelowSearchSnippet.test.jsx b/browser/components/newtab/test/unit/asrouter/templates/SimpleBelowSearchSnippet.test.jsx
new file mode 100644
index 0000000000..df9e544a54
--- /dev/null
+++ b/browser/components/newtab/test/unit/asrouter/templates/SimpleBelowSearchSnippet.test.jsx
@@ -0,0 +1,81 @@
+import { mount } from "enzyme";
+import React from "react";
+import { FluentBundle, FluentResource } from "@fluent/bundle";
+import { LocalizationProvider, ReactLocalization } from "@fluent/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;
+
+ function mockL10nWrapper(content) {
+ const bundle = new FluentBundle("en-US");
+ for (const [id, value] of Object.entries(content)) {
+ if (typeof value === "string") {
+ bundle.addResource(new FluentResource(`${id} = ${value}`));
+ }
+ }
+ const l10n = new ReactLocalization([bundle]);
+ return {
+ wrappingComponent: LocalizationProvider,
+ wrappingComponentProps: { l10n },
+ };
+ }
+
+ /**
+ * 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} />,
+ mockL10nWrapper(props.content)
+ );
+ }
+
+ 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 (light theme)", () => {
+ const wrapper = mountAndCheckProps({
+ icon: "data:image/gif;base64,R0lGODl",
+ });
+ assert.equal(
+ wrapper.find(".icon-light-theme").prop("src"),
+ "data:image/gif;base64,R0lGODl"
+ );
+ });
+
+ it("should render .icon (dark theme)", () => {
+ const wrapper = mountAndCheckProps({
+ icon_dark_theme: "data:image/gif;base64,R0lGODl",
+ });
+ assert.equal(
+ wrapper.find(".icon-dark-theme").prop("src"),
+ "data:image/gif;base64,R0lGODl"
+ );
+ });
+});
diff --git a/browser/components/newtab/test/unit/asrouter/templates/SimpleSnippet.test.jsx b/browser/components/newtab/test/unit/asrouter/templates/SimpleSnippet.test.jsx
new file mode 100644
index 0000000000..7c169525e4
--- /dev/null
+++ b/browser/components/newtab/test/unit/asrouter/templates/SimpleSnippet.test.jsx
@@ -0,0 +1,259 @@
+import { mount } from "enzyme";
+import React from "react";
+import { FluentBundle, FluentResource } from "@fluent/bundle";
+import { LocalizationProvider, ReactLocalization } from "@fluent/react";
+import schema from "content-src/asrouter/templates/SimpleSnippet/SimpleSnippet.schema.json";
+import { SimpleSnippet } from "content-src/asrouter/templates/SimpleSnippet/SimpleSnippet.jsx";
+
+const DEFAULT_CONTENT = { text: "foo" };
+
+describe("SimpleSnippet", () => {
+ let sandbox;
+ let onBlockStub;
+ let sendUserActionTelemetryStub;
+
+ function mockL10nWrapper(content) {
+ const bundle = new FluentBundle("en-US");
+ for (const [id, value] of Object.entries(content)) {
+ if (typeof value === "string") {
+ bundle.addResource(new FluentResource(`${id} = ${value}`));
+ }
+ }
+ const l10n = new ReactLocalization([bundle]);
+ return {
+ wrappingComponent: LocalizationProvider,
+ wrappingComponentProps: { l10n },
+ };
+ }
+
+ /**
+ * mountAndCheckProps - Mounts a SimpleSnippet 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, title})
+ * @returns enzyme wrapper for SimpleSnippet
+ */
+ function mountAndCheckProps(content = {}, provider = "test-provider") {
+ const props = {
+ content: Object.assign({}, DEFAULT_CONTENT, content),
+ provider,
+ onBlock: onBlockStub,
+ sendUserActionTelemetry: sendUserActionTelemetryStub,
+ onAction: sandbox.stub(),
+ };
+ assert.jsonSchema(props.content, schema);
+ return mount(<SimpleSnippet {...props} />, mockL10nWrapper(props.content));
+ }
+
+ beforeEach(() => {
+ sandbox = sinon.createSandbox();
+ onBlockStub = sandbox.stub();
+ sendUserActionTelemetryStub = sandbox.stub();
+ });
+
+ afterEach(() => {
+ sandbox.restore();
+ });
+
+ it("should have the correct defaults", () => {
+ const wrapper = mountAndCheckProps();
+ [["button", "title", "block_button_text"]].forEach(prop => {
+ const props = wrapper.find(prop[0]).props();
+ assert.propertyVal(props, prop[1], schema.properties[prop[2]].default);
+ });
+ });
+
+ it("should render .text", () => {
+ const wrapper = mountAndCheckProps({ text: "bar" });
+ assert.equal(wrapper.find(".body").text(), "bar");
+ });
+ it("should not render title element if no .title prop is supplied", () => {
+ const wrapper = mountAndCheckProps();
+ assert.lengthOf(wrapper.find(".title"), 0);
+ });
+ it("should render .title", () => {
+ const wrapper = mountAndCheckProps({ title: "Foo" });
+ assert.equal(wrapper.find(".title").text().trim(), "Foo");
+ });
+ it("should render a light theme variant .icon", () => {
+ const wrapper = mountAndCheckProps({
+ icon: "data:image/gif;base64,R0lGODl",
+ });
+ assert.equal(
+ wrapper.find(".icon-light-theme").prop("src"),
+ "data:image/gif;base64,R0lGODl"
+ );
+ });
+ it("should render a dark theme variant .icon", () => {
+ const wrapper = mountAndCheckProps({
+ icon_dark_theme: "data:image/gif;base64,R0lGODl",
+ });
+ assert.equal(
+ wrapper.find(".icon-dark-theme").prop("src"),
+ "data:image/gif;base64,R0lGODl"
+ );
+ });
+ it("should render a light theme variant .icon as fallback", () => {
+ const wrapper = mountAndCheckProps({
+ icon_dark_theme: "",
+ icon: "data:image/gif;base64,R0lGODp",
+ });
+ assert.equal(
+ wrapper.find(".icon-dark-theme").prop("src"),
+ "data:image/gif;base64,R0lGODp"
+ );
+ });
+ it("should render .button_label and default className", () => {
+ const wrapper = mountAndCheckProps({
+ button_label: "Click here",
+ button_action: "OPEN_APPLICATIONS_MENU",
+ button_action_args: "appMenu",
+ });
+
+ const button = wrapper.find("button.ASRouterButton");
+ button.simulate("click");
+
+ assert.equal(button.text(), "Click here");
+ assert.equal(button.prop("className"), "ASRouterButton secondary");
+ assert.calledOnce(wrapper.props().onAction);
+ assert.calledWithExactly(wrapper.props().onAction, {
+ type: "OPEN_APPLICATIONS_MENU",
+ data: { args: "appMenu" },
+ });
+ });
+ it("should not wrap the main content if a section header is not present", () => {
+ const wrapper = mountAndCheckProps({ text: "bar" });
+ assert.lengthOf(wrapper.find(".innerContentWrapper"), 0);
+ });
+ it("should wrap the main content if a section header is present", () => {
+ const wrapper = mountAndCheckProps({
+ section_title_icon: "data:image/gif;base64,R0lGODl",
+ section_title_text: "Messages from Mozilla",
+ });
+
+ assert.lengthOf(wrapper.find(".innerContentWrapper"), 1);
+ });
+ it("should render a section header if text and icon (light-theme) are specified", () => {
+ const wrapper = mountAndCheckProps({
+ section_title_icon: "data:image/gif;base64,R0lGODl",
+ section_title_text: "Messages from Mozilla",
+ });
+
+ assert.equal(
+ wrapper.find(".section-title .icon-light-theme").prop("style")
+ .backgroundImage,
+ 'url("data:image/gif;base64,R0lGODl")'
+ );
+ assert.equal(
+ wrapper.find(".section-title-text").text().trim(),
+ "Messages from Mozilla"
+ );
+ // ensure there is no <a> when a section_title_url is not specified
+ assert.lengthOf(wrapper.find(".section-title a"), 0);
+ });
+ it("should render a section header if text and icon (light-theme) are specified", () => {
+ const wrapper = mountAndCheckProps({
+ section_title_icon: "data:image/gif;base64,R0lGODl",
+ section_title_icon_dark_theme: "data:image/gif;base64,R0lGODl",
+ section_title_text: "Messages from Mozilla",
+ });
+
+ assert.equal(
+ wrapper.find(".section-title .icon-dark-theme").prop("style")
+ .backgroundImage,
+ 'url("data:image/gif;base64,R0lGODl")'
+ );
+ assert.equal(
+ wrapper.find(".section-title-text").text().trim(),
+ "Messages from Mozilla"
+ );
+ // ensure there is no <a> when a section_title_url is not specified
+ assert.lengthOf(wrapper.find(".section-title a"), 0);
+ });
+ it("should render a section header wrapped in an <a> tag if a url is provided", () => {
+ const wrapper = mountAndCheckProps({
+ section_title_icon: "data:image/gif;base64,R0lGODl",
+ section_title_text: "Messages from Mozilla",
+ section_title_url: "https://www.mozilla.org",
+ });
+
+ assert.equal(
+ wrapper.find(".section-title a").prop("href"),
+ "https://www.mozilla.org"
+ );
+ });
+ it("should send an OPEN_URL action when button_url is defined and button is clicked", () => {
+ const wrapper = mountAndCheckProps({
+ button_label: "Button",
+ button_url: "https://mozilla.org",
+ });
+
+ const button = wrapper.find("button.ASRouterButton");
+ button.simulate("click");
+
+ assert.calledOnce(wrapper.props().onAction);
+ assert.calledWithExactly(wrapper.props().onAction, {
+ type: "OPEN_URL",
+ data: { args: "https://mozilla.org" },
+ });
+ });
+ it("should send an OPEN_ABOUT_PAGE action with entrypoint when the button is clicked", () => {
+ const wrapper = mountAndCheckProps({
+ button_label: "Button",
+ button_action: "OPEN_ABOUT_PAGE",
+ button_entrypoint_value: "snippet",
+ button_entrypoint_name: "entryPoint",
+ button_action_args: "logins",
+ });
+
+ const button = wrapper.find("button.ASRouterButton");
+ button.simulate("click");
+
+ assert.calledOnce(wrapper.props().onAction);
+ assert.calledWithExactly(wrapper.props().onAction, {
+ type: "OPEN_ABOUT_PAGE",
+ data: { args: "logins", entrypoint: "entryPoint=snippet" },
+ });
+ });
+ it("should send an OPEN_PREFERENCE_PAGE action with entrypoint when the button is clicked", () => {
+ const wrapper = mountAndCheckProps({
+ button_label: "Button",
+ button_action: "OPEN_PREFERENCE_PAGE",
+ button_entrypoint_value: "entry=snippet",
+ button_action_args: "home",
+ });
+
+ const button = wrapper.find("button.ASRouterButton");
+ button.simulate("click");
+
+ assert.calledOnce(wrapper.props().onAction);
+ assert.calledWithExactly(wrapper.props().onAction, {
+ type: "OPEN_PREFERENCE_PAGE",
+ data: { args: "home", entrypoint: "entry=snippet" },
+ });
+ });
+ it("should call props.onBlock and sendUserActionTelemetry when CTA button is clicked", () => {
+ const wrapper = mountAndCheckProps({ text: "bar" });
+
+ wrapper.instance().onButtonClick();
+
+ assert.calledOnce(onBlockStub);
+ assert.calledOnce(sendUserActionTelemetryStub);
+ });
+
+ it("should not call props.onBlock if do_not_autoblock is true", () => {
+ const wrapper = mountAndCheckProps({ text: "bar", do_not_autoblock: true });
+
+ wrapper.instance().onButtonClick();
+
+ assert.notCalled(onBlockStub);
+ });
+
+ it("should not call sendUserActionTelemetry for preview message when CTA button is clicked", () => {
+ const wrapper = mountAndCheckProps({ text: "bar" }, "preview");
+
+ wrapper.instance().onButtonClick();
+
+ assert.calledOnce(onBlockStub);
+ assert.notCalled(sendUserActionTelemetryStub);
+ });
+});
diff --git a/browser/components/newtab/test/unit/asrouter/templates/SubmitFormSnippet.test.jsx b/browser/components/newtab/test/unit/asrouter/templates/SubmitFormSnippet.test.jsx
new file mode 100644
index 0000000000..12e4f96863
--- /dev/null
+++ b/browser/components/newtab/test/unit/asrouter/templates/SubmitFormSnippet.test.jsx
@@ -0,0 +1,354 @@
+import { mount } from "enzyme";
+import React from "react";
+import { FluentBundle, FluentResource } from "@fluent/bundle";
+import { LocalizationProvider, ReactLocalization } from "@fluent/react";
+import { RichText } from "content-src/asrouter/components/RichText/RichText.jsx";
+import schema from "content-src/asrouter/templates/SubmitFormSnippet/SubmitFormSnippet.schema.json";
+import { SubmitFormSnippet } from "content-src/asrouter/templates/SubmitFormSnippet/SubmitFormSnippet.jsx";
+
+const DEFAULT_CONTENT = {
+ scene1_text: "foo",
+ scene2_text: "bar",
+ scene1_button_label: "Sign Up",
+ retry_button_label: "Try again",
+ form_action: "foo.com",
+ hidden_inputs: { foo: "foo" },
+ error_text: "error",
+ success_text: "success",
+};
+
+describe("SubmitFormSnippet", () => {
+ let sandbox;
+ let onBlockStub;
+
+ function mockL10nWrapper(content) {
+ const bundle = new FluentBundle("en-US");
+ for (const [id, value] of Object.entries(content)) {
+ if (typeof value === "string") {
+ bundle.addResource(new FluentResource(`${id} = ${value}`));
+ }
+ }
+ const l10n = new ReactLocalization([bundle]);
+ return {
+ wrappingComponent: LocalizationProvider,
+ wrappingComponentProps: { l10n },
+ };
+ }
+
+ /**
+ * mountAndCheckProps - Mounts a SubmitFormSnippet 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, title})
+ * @returns enzyme wrapper for SubmitFormSnippet
+ */
+ function mountAndCheckProps(content = {}) {
+ const props = {
+ content: Object.assign({}, DEFAULT_CONTENT, content),
+ onBlock: onBlockStub,
+ onDismiss: sandbox.stub(),
+ sendUserActionTelemetry: sandbox.stub(),
+ onAction: sandbox.stub(),
+ form_method: "POST",
+ };
+ assert.jsonSchema(props.content, schema);
+ return mount(
+ <SubmitFormSnippet {...props} />,
+ mockL10nWrapper(props.content)
+ );
+ }
+
+ beforeEach(() => {
+ sandbox = sinon.createSandbox();
+ onBlockStub = sandbox.stub();
+ });
+
+ afterEach(() => {
+ sandbox.restore();
+ });
+
+ it("should render .text", () => {
+ const wrapper = mountAndCheckProps({ scene1_text: "bar" });
+ assert.equal(wrapper.find(".body").text(), "bar");
+ });
+ it("should not render title element if no .title prop is supplied", () => {
+ const wrapper = mountAndCheckProps();
+ assert.lengthOf(wrapper.find(".title"), 0);
+ });
+ it("should render .title", () => {
+ const wrapper = mountAndCheckProps({ scene1_title: "Foo" });
+ assert.equal(wrapper.find(".title").text().trim(), "Foo");
+ });
+ it("should render light-theme .icon", () => {
+ const wrapper = mountAndCheckProps({
+ scene1_icon: "data:image/gif;base64,R0lGODl",
+ });
+ assert.equal(
+ wrapper.find(".icon-light-theme").prop("src"),
+ "data:image/gif;base64,R0lGODl"
+ );
+ });
+ it("should render dark-theme .icon", () => {
+ const wrapper = mountAndCheckProps({
+ scene1_icon_dark_theme: "data:image/gif;base64,R0lGODl",
+ });
+ assert.equal(
+ wrapper.find(".icon-dark-theme").prop("src"),
+ "data:image/gif;base64,R0lGODl"
+ );
+ });
+ it("should render .button_label and default className", () => {
+ const wrapper = mountAndCheckProps({ scene1_button_label: "Click here" });
+
+ const button = wrapper.find("button.ASRouterButton");
+ assert.equal(button.text(), "Click here");
+ assert.equal(button.prop("className"), "ASRouterButton secondary");
+ });
+
+ describe("#SignupView", () => {
+ let wrapper;
+ const fetchOk = { json: () => Promise.resolve({ status: "ok" }) };
+ const fetchFail = { json: () => Promise.resolve({ status: "fail" }) };
+
+ beforeEach(() => {
+ wrapper = mountAndCheckProps({
+ scene1_text: "bar",
+ scene2_email_placeholder_text: "Email",
+ scene2_text: "signup",
+ });
+ });
+
+ it("should set the input type if provided through props.inputType", () => {
+ wrapper.setProps({ inputType: "number" });
+ wrapper.setState({ expanded: true });
+ assert.equal(wrapper.find(".mainInput").instance().type, "number");
+ });
+
+ it("should validate via props.validateInput if provided", () => {
+ function validateInput(value, content) {
+ if (content.country === "CA" && value === "poutine") {
+ return "";
+ }
+ return "Must be poutine";
+ }
+ const setCustomValidity = sandbox.stub();
+ wrapper.setProps({
+ validateInput,
+ content: { ...DEFAULT_CONTENT, country: "CA" },
+ });
+ wrapper.setState({ expanded: true });
+ const input = wrapper.find(".mainInput");
+ input.instance().value = "poutine";
+ input.simulate("change", {
+ target: { value: "poutine", setCustomValidity },
+ });
+ assert.calledWith(setCustomValidity, "");
+
+ input.instance().value = "fried chicken";
+ input.simulate("change", {
+ target: { value: "fried chicken", setCustomValidity },
+ });
+ assert.calledWith(setCustomValidity, "Must be poutine");
+ });
+
+ it("should show the signup form if state.expanded is true", () => {
+ wrapper.setState({ expanded: true });
+
+ assert.isTrue(wrapper.find("form").exists());
+ });
+ it("should dismiss the snippet", () => {
+ wrapper.setState({ expanded: true });
+
+ wrapper.find(".ASRouterButton.secondary").simulate("click");
+
+ assert.calledOnce(wrapper.props().onDismiss);
+ });
+ it("should send a DISMISS event ping", () => {
+ wrapper.setState({ expanded: true });
+
+ wrapper.find(".ASRouterButton.secondary").simulate("click");
+
+ assert.equal(
+ wrapper.props().sendUserActionTelemetry.firstCall.args[0].event,
+ "DISMISS"
+ );
+ });
+ it("should render hidden inputs + email input", () => {
+ wrapper.setState({ expanded: true });
+
+ assert.lengthOf(wrapper.find("input[type='hidden']"), 1);
+ });
+ it("should open the SignupView when the action button is clicked", () => {
+ assert.isFalse(wrapper.find("form").exists());
+
+ wrapper.find(".ASRouterButton").simulate("click");
+
+ assert.isTrue(wrapper.state().expanded);
+ assert.isTrue(wrapper.find("form").exists());
+ });
+ it("should submit telemetry when the action button is clicked", () => {
+ assert.isFalse(wrapper.find("form").exists());
+
+ wrapper.find(".ASRouterButton").simulate("click");
+
+ assert.equal(
+ wrapper.props().sendUserActionTelemetry.firstCall.args[0].event_context,
+ "scene1-button-learn-more"
+ );
+ });
+ it("should submit form data when submitted", () => {
+ sandbox.stub(window, "fetch").resolves(fetchOk);
+ wrapper.setState({ expanded: true });
+
+ wrapper.find("form").simulate("submit");
+ assert.calledOnce(window.fetch);
+ });
+ it("should send user telemetry when submitted", () => {
+ wrapper.setState({ expanded: true });
+
+ wrapper.find("form").simulate("submit");
+
+ assert.equal(
+ wrapper.props().sendUserActionTelemetry.firstCall.args[0].event_context,
+ "conversion-subscribe-activation"
+ );
+ });
+ it("should set signupSuccess when submission status is ok", async () => {
+ sandbox.stub(window, "fetch").resolves(fetchOk);
+ wrapper.setState({ expanded: true });
+ await wrapper.instance().handleSubmit({ preventDefault: sandbox.stub() });
+
+ assert.equal(wrapper.state().signupSuccess, true);
+ assert.equal(wrapper.state().signupSubmitted, true);
+ assert.calledOnce(onBlockStub);
+ assert.calledWithExactly(onBlockStub, { preventDismiss: true });
+ });
+ it("should send user telemetry when submission status is ok", async () => {
+ sandbox.stub(window, "fetch").resolves(fetchOk);
+ wrapper.setState({ expanded: true });
+ await wrapper.instance().handleSubmit({ preventDefault: sandbox.stub() });
+
+ assert.equal(
+ wrapper.props().sendUserActionTelemetry.secondCall.args[0]
+ .event_context,
+ "subscribe-success"
+ );
+ });
+ it("should not block the snippet if submission failed", async () => {
+ sandbox.stub(window, "fetch").resolves(fetchFail);
+ wrapper.setState({ expanded: true });
+ await wrapper.instance().handleSubmit({ preventDefault: sandbox.stub() });
+
+ assert.equal(wrapper.state().signupSuccess, false);
+ assert.equal(wrapper.state().signupSubmitted, true);
+ assert.notCalled(onBlockStub);
+ });
+ it("should not block if do_not_autoblock is true", async () => {
+ sandbox.stub(window, "fetch").resolves(fetchOk);
+ wrapper = mountAndCheckProps({
+ scene1_text: "bar",
+ scene2_email_placeholder_text: "Email",
+ scene2_text: "signup",
+ do_not_autoblock: true,
+ });
+ wrapper.setState({ expanded: true });
+ await wrapper.instance().handleSubmit({ preventDefault: sandbox.stub() });
+
+ assert.equal(wrapper.state().signupSuccess, true);
+ assert.equal(wrapper.state().signupSubmitted, true);
+ assert.notCalled(onBlockStub);
+ });
+ it("should send user telemetry if submission failed", async () => {
+ sandbox.stub(window, "fetch").resolves(fetchFail);
+ wrapper.setState({ expanded: true });
+ await wrapper.instance().handleSubmit({ preventDefault: sandbox.stub() });
+
+ assert.equal(
+ wrapper.props().sendUserActionTelemetry.secondCall.args[0]
+ .event_context,
+ "subscribe-error"
+ );
+ });
+ it("should render the signup success message", () => {
+ wrapper.setProps({ content: { success_text: "success" } });
+ wrapper.setState({ signupSuccess: true, signupSubmitted: true });
+
+ assert.isTrue(wrapper.find(".submissionStatus").exists());
+ assert.propertyVal(
+ wrapper.find(RichText).props(),
+ "localization_id",
+ "success_text"
+ );
+ assert.propertyVal(
+ wrapper.find(RichText).props(),
+ "success_text",
+ "success"
+ );
+ assert.isFalse(wrapper.find(".ASRouterButton").exists());
+ });
+ it("should render the signup error message", () => {
+ wrapper.setProps({ content: { error_text: "trouble" } });
+ wrapper.setState({ signupSuccess: false, signupSubmitted: true });
+
+ assert.isTrue(wrapper.find(".submissionStatus").exists());
+ assert.propertyVal(
+ wrapper.find(RichText).props(),
+ "localization_id",
+ "error_text"
+ );
+ assert.propertyVal(
+ wrapper.find(RichText).props(),
+ "error_text",
+ "trouble"
+ );
+ assert.isTrue(wrapper.find(".ASRouterButton").exists());
+ });
+ it("should render the button to return to the signup form if there was an error", () => {
+ wrapper.setState({ signupSubmitted: true, signupSuccess: false });
+
+ const button = wrapper.find("button.ASRouterButton");
+ assert.equal(button.text(), "Try again");
+ wrapper.find(".ASRouterButton").simulate("click");
+
+ assert.equal(wrapper.state().signupSubmitted, false);
+ });
+ it("should not render the privacy notice checkbox if prop is missing", () => {
+ wrapper.setState({ expanded: true });
+
+ assert.isFalse(wrapper.find(".privacyNotice").exists());
+ });
+ it("should render the privacy notice checkbox if prop is provided", () => {
+ wrapper.setProps({
+ content: { ...DEFAULT_CONTENT, scene2_privacy_html: "privacy notice" },
+ });
+ wrapper.setState({ expanded: true });
+
+ assert.isTrue(wrapper.find(".privacyNotice").exists());
+ });
+ it("should not call fetch if form_method is GET", async () => {
+ sandbox.stub(window, "fetch").resolves(fetchOk);
+ wrapper.setProps({ form_method: "GET" });
+ wrapper.setState({ expanded: true });
+
+ await wrapper.instance().handleSubmit({ preventDefault: sandbox.stub() });
+
+ assert.notCalled(window.fetch);
+ });
+ it("should block the snippet when form_method is GET", () => {
+ wrapper.setProps({ form_method: "GET" });
+ wrapper.setState({ expanded: true });
+
+ wrapper.instance().handleSubmit({ preventDefault: sandbox.stub() });
+
+ assert.calledOnce(onBlockStub);
+ assert.calledWithExactly(onBlockStub, { preventDismiss: true });
+ });
+ it("should return to scene 2 alt when clicking the retry button", async () => {
+ wrapper.setState({ signupSubmitted: true });
+ wrapper.setProps({ expandedAlt: true });
+
+ wrapper.find(".ASRouterButton").simulate("click");
+
+ assert.isTrue(wrapper.find(".scene2Alt").exists());
+ });
+ });
+});
diff --git a/browser/components/newtab/test/unit/asrouter/templates/isEmailOrPhoneNumber.test.js b/browser/components/newtab/test/unit/asrouter/templates/isEmailOrPhoneNumber.test.js
new file mode 100644
index 0000000000..32eaf2160e
--- /dev/null
+++ b/browser/components/newtab/test/unit/asrouter/templates/isEmailOrPhoneNumber.test.js
@@ -0,0 +1,56 @@
+import { isEmailOrPhoneNumber } from "content-src/asrouter/templates/SendToDeviceSnippet/isEmailOrPhoneNumber";
+
+const CONTENT = {};
+
+describe("isEmailOrPhoneNumber", () => {
+ it("should return 'email' for emails", () => {
+ assert.equal(isEmailOrPhoneNumber("foobar@asd.com", CONTENT), "email");
+ assert.equal(isEmailOrPhoneNumber("foobar@asd.co.uk", CONTENT), "email");
+ });
+ it("should return 'phone' for valid en-US/en-CA phone numbers", () => {
+ assert.equal(
+ isEmailOrPhoneNumber("14582731273", { locale: "en-US" }),
+ "phone"
+ );
+ assert.equal(
+ isEmailOrPhoneNumber("4582731273", { locale: "en-CA" }),
+ "phone"
+ );
+ });
+ it("should return an empty string for invalid phone number lengths in en-US/en-CA", () => {
+ // Not enough digits
+ assert.equal(isEmailOrPhoneNumber("4522", { locale: "en-US" }), "");
+ assert.equal(isEmailOrPhoneNumber("4522", { locale: "en-CA" }), "");
+ });
+ it("should return 'phone' for valid German phone numbers", () => {
+ assert.equal(
+ isEmailOrPhoneNumber("145827312732", { locale: "de" }),
+ "phone"
+ );
+ });
+ it("should return 'phone' for any number of digits in other locales", () => {
+ assert.equal(isEmailOrPhoneNumber("4", CONTENT), "phone");
+ });
+ it("should return an empty string for other invalid inputs", () => {
+ assert.equal(
+ isEmailOrPhoneNumber("abc", CONTENT),
+ "",
+ "abc should be invalid"
+ );
+ assert.equal(
+ isEmailOrPhoneNumber("abc@", CONTENT),
+ "",
+ "abc@ should be invalid"
+ );
+ assert.equal(
+ isEmailOrPhoneNumber("abc@foo", CONTENT),
+ "",
+ "abc@foo should be invalid"
+ );
+ assert.equal(
+ isEmailOrPhoneNumber("123d1232", CONTENT),
+ "",
+ "123d1232 should be invalid"
+ );
+ });
+});