diff options
Diffstat (limited to 'browser/components/aboutwelcome/tests/unit')
14 files changed, 3263 insertions, 0 deletions
diff --git a/browser/components/aboutwelcome/tests/unit/AWScreenUtils.test.jsx b/browser/components/aboutwelcome/tests/unit/AWScreenUtils.test.jsx new file mode 100644 index 0000000000..b6e9489ef9 --- /dev/null +++ b/browser/components/aboutwelcome/tests/unit/AWScreenUtils.test.jsx @@ -0,0 +1,140 @@ +import { AWScreenUtils } from "modules/AWScreenUtils.sys.mjs"; +import { GlobalOverrider } from "newtab/test/unit/utils"; +import { ASRouter } from "asrouter/modules/ASRouter.sys.mjs"; + +describe("AWScreenUtils", () => { + let sandbox; + let globals; + + beforeEach(() => { + globals = new GlobalOverrider(); + globals.set({ + ASRouter, + ASRouterTargeting: { + Environment: {}, + }, + }); + + sandbox = sinon.createSandbox(); + }); + afterEach(() => { + sandbox.restore(); + globals.restore(); + }); + describe("removeScreens", () => { + it("should run callback function once for each array element", async () => { + const callback = sandbox.stub().resolves(false); + const arr = ["foo", "bar"]; + await AWScreenUtils.removeScreens(arr, callback); + assert.calledTwice(callback); + }); + it("should remove screen when passed function evaluates true", async () => { + const callback = sandbox.stub().resolves(true); + const arr = ["foo", "bar"]; + await AWScreenUtils.removeScreens(arr, callback); + assert.deepEqual(arr, []); + }); + }); + describe("evaluateScreenTargeting", () => { + it("should return the eval result if the eval succeeds", async () => { + const evalStub = sandbox.stub(ASRouter, "evaluateExpression").resolves({ + evaluationStatus: { + success: true, + result: false, + }, + }); + const result = await AWScreenUtils.evaluateScreenTargeting( + "test expression" + ); + assert.calledOnce(evalStub); + assert.equal(result, false); + }); + it("should return true if the targeting eval fails", async () => { + const evalStub = sandbox.stub(ASRouter, "evaluateExpression").resolves({ + evaluationStatus: { + success: false, + result: false, + }, + }); + const result = await AWScreenUtils.evaluateScreenTargeting( + "test expression" + ); + assert.calledOnce(evalStub); + assert.equal(result, true); + }); + }); + describe("evaluateTargetingAndRemoveScreens", () => { + it("should manipulate an array of screens", async () => { + const screens = [ + { + id: "first", + targeting: true, + }, + { + id: "second", + targeting: false, + }, + ]; + + const expectedScreens = [ + { + id: "first", + targeting: true, + }, + ]; + sandbox.stub(ASRouter, "evaluateExpression").callsFake(targeting => { + return { + evaluationStatus: { + success: true, + result: targeting.expression, + }, + }; + }); + const evaluatedStrings = + await AWScreenUtils.evaluateTargetingAndRemoveScreens(screens); + assert.deepEqual(evaluatedStrings, expectedScreens); + }); + it("should not remove screens with no targeting", async () => { + const screens = [ + { + id: "first", + }, + { + id: "second", + targeting: false, + }, + ]; + + const expectedScreens = [ + { + id: "first", + }, + ]; + sandbox + .stub(AWScreenUtils, "evaluateScreenTargeting") + .callsFake(targeting => { + if (targeting === undefined) { + return true; + } + return targeting; + }); + const evaluatedStrings = + await AWScreenUtils.evaluateTargetingAndRemoveScreens(screens); + assert.deepEqual(evaluatedStrings, expectedScreens); + }); + }); + + describe("addScreenImpression", () => { + it("Should call addScreenImpression with provided screen ID", () => { + const addScreenImpressionStub = sandbox.stub( + ASRouter, + "addScreenImpression" + ); + const testScreen = { id: "test" }; + AWScreenUtils.addScreenImpression(testScreen); + + assert.calledOnce(addScreenImpressionStub); + assert.equal(addScreenImpressionStub.firstCall.args[0].id, testScreen.id); + }); + }); +}); diff --git a/browser/components/aboutwelcome/tests/unit/CTAParagraph.test.jsx b/browser/components/aboutwelcome/tests/unit/CTAParagraph.test.jsx new file mode 100644 index 0000000000..c60e8e2666 --- /dev/null +++ b/browser/components/aboutwelcome/tests/unit/CTAParagraph.test.jsx @@ -0,0 +1,49 @@ +import React from "react"; +import { shallow } from "enzyme"; +import { CTAParagraph } from "content-src/components/CTAParagraph"; + +describe("CTAParagraph component", () => { + let sandbox; + let wrapper; + let handleAction; + + beforeEach(() => { + sandbox = sinon.createSandbox(); + handleAction = sandbox.stub(); + wrapper = shallow( + <CTAParagraph + content={{ + text: { + raw: "Link Text", + string_name: "Test Name", + }, + }} + handleAction={handleAction} + /> + ); + }); + + afterEach(() => { + sandbox.restore(); + }); + + it("should render CTAParagraph component", () => { + assert.ok(wrapper.exists()); + }); + + it("should render CTAParagraph component if only CTA text is passed", () => { + wrapper.setProps({ content: { text: "CTA Text" } }); + assert.ok(wrapper.exists()); + }); + + it("should call handleAction method when button is link is clicked", () => { + const btnLink = wrapper.find(".cta-paragraph span"); + btnLink.simulate("click"); + assert.calledOnce(handleAction); + }); + + it("should not render CTAParagraph component if CTA text is not passed", () => { + wrapper.setProps({ content: { text: null } }); + assert.ok(wrapper.isEmptyRender()); + }); +}); diff --git a/browser/components/aboutwelcome/tests/unit/HelpText.test.jsx b/browser/components/aboutwelcome/tests/unit/HelpText.test.jsx new file mode 100644 index 0000000000..e9b722b9d8 --- /dev/null +++ b/browser/components/aboutwelcome/tests/unit/HelpText.test.jsx @@ -0,0 +1,41 @@ +import { HelpText } from "content-src/components/HelpText"; +import { Localized } from "content-src/components/MSLocalized"; +import React from "react"; +import { shallow } from "enzyme"; + +describe("<HelpText>", () => { + it("should render text inside Localized", () => { + const shallowWrapper = shallow(<HelpText text="test" />); + + assert.equal(shallowWrapper.find(Localized).props().text, "test"); + }); + it("should render the img if there is an img and a string_id", () => { + const shallowWrapper = shallow( + <HelpText + text={{ string_id: "test_id" }} + hasImg={{ + src: "chrome://activity-stream/content/data/content/assets/cfr_fb_container.png", + }} + /> + ); + assert.ok( + shallowWrapper + .find(Localized) + .findWhere(n => n.text.string_id === "test_id") + ); + assert.lengthOf(shallowWrapper.find("p.helptext"), 1); + assert.lengthOf(shallowWrapper.find("img[data-l10n-name='help-img']"), 1); + }); + it("should render the img if there is an img and plain text", () => { + const shallowWrapper = shallow( + <HelpText + text={"Sample help text"} + hasImg={{ + src: "chrome://activity-stream/content/data/content/assets/cfr_fb_container.png", + }} + /> + ); + assert.equal(shallowWrapper.find("p.helptext").text(), "Sample help text"); + assert.lengthOf(shallowWrapper.find("img.helptext-img"), 1); + }); +}); diff --git a/browser/components/aboutwelcome/tests/unit/HeroImage.test.jsx b/browser/components/aboutwelcome/tests/unit/HeroImage.test.jsx new file mode 100644 index 0000000000..244e64f906 --- /dev/null +++ b/browser/components/aboutwelcome/tests/unit/HeroImage.test.jsx @@ -0,0 +1,40 @@ +import React from "react"; +import { shallow } from "enzyme"; +import { HeroImage } from "content-src/components/HeroImage"; + +describe("HeroImage component", () => { + const imageUrl = "https://example.com"; + const imageHeight = "100px"; + const imageAlt = "Alt text"; + + let wrapper; + beforeEach(() => { + wrapper = shallow( + <HeroImage url={imageUrl} alt={imageAlt} height={imageHeight} /> + ); + }); + + it("should render HeroImage component", () => { + assert.ok(wrapper.exists()); + }); + + it("should render an image element with src prop", () => { + let imgEl = wrapper.find("img"); + assert.strictEqual(imgEl.prop("src"), imageUrl); + }); + + it("should render image element with alt text prop", () => { + let imgEl = wrapper.find("img"); + assert.equal(imgEl.prop("alt"), imageAlt); + }); + + it("should render an image with a set height prop", () => { + let imgEl = wrapper.find("img"); + assert.propertyVal(imgEl.prop("style"), "height", imageHeight); + }); + + it("should not render HeroImage component", () => { + wrapper.setProps({ url: null }); + assert.ok(wrapper.isEmptyRender()); + }); +}); diff --git a/browser/components/aboutwelcome/tests/unit/LinkParagraph.test.jsx b/browser/components/aboutwelcome/tests/unit/LinkParagraph.test.jsx new file mode 100644 index 0000000000..240342b5e2 --- /dev/null +++ b/browser/components/aboutwelcome/tests/unit/LinkParagraph.test.jsx @@ -0,0 +1,102 @@ +import React from "react"; +import { mount } from "enzyme"; +import { LinkParagraph } from "content-src/components/LinkParagraph"; + +describe("LinkParagraph component", () => { + let sandbox; + let wrapper; + let handleAction; + + beforeEach(() => { + sandbox = sinon.createSandbox(); + handleAction = sandbox.stub(); + + wrapper = mount( + <LinkParagraph + text_content={{ + text: { + string_id: + "shopping-onboarding-opt-in-privacy-policy-and-terms-of-use3", + }, + link_keys: ["privacy_policy"], + font_styles: "legal", + }} + handleAction={handleAction} + /> + ); + }); + + afterEach(() => { + sandbox.restore(); + }); + + it("should render LinkParagraph component", () => { + assert.ok(wrapper.exists()); + }); + + it("should render copy with legal style if legal is passed to font_styles", () => { + assert.strictEqual(wrapper.find(".legal-paragraph").length, 1); + }); + + it("should render one link when only one link id is passed", () => { + assert.strictEqual(wrapper.find(".legal-paragraph a").length, 1); + }); + + it("should call handleAction method when link is clicked", () => { + const linkEl = wrapper.find(".legal-paragraph a"); + linkEl.simulate("click"); + assert.calledOnce(handleAction); + }); + + it("should render two links if an additional link id is passed", () => { + wrapper.setProps({ + text_content: { + text: { + string_id: + "shopping-onboarding-opt-in-privacy-policy-and-terms-of-use3", + }, + link_keys: ["privacy_policy", "terms_of_use"], + font_styles: "legal", + }, + }); + assert.strictEqual(wrapper.find(".legal-paragraph a").length, 2); + }); + + it("should render no links when no link id is passed", () => { + wrapper.setProps({ + text_content: { links: null }, + }); + assert.strictEqual(wrapper.find(".legal-paragraph a").length, 0); + }); + + it("should render copy even when no link id is passed", () => { + wrapper.setProps({ + text_content: { links: null }, + }); + assert.ok(wrapper.find(".legal-paragraph")); + }); + + it("should not render LinkParagraph component if text is not passed", () => { + wrapper.setProps({ text_content: { text: null } }); + assert.ok(wrapper.isEmptyRender()); + }); + + it("should render copy in link style if no font style is passed", () => { + wrapper.setProps({ + text_content: { + text: { + string_id: "shopping-onboarding-body", + }, + link_keys: ["learn_more"], + }, + }); + assert.strictEqual(wrapper.find(".link-paragraph").length, 1); + }); + + it("should not render links if string_id is not provided", () => { + wrapper.setProps({ + text_content: { text: { string_id: null } }, + }); + assert.strictEqual(wrapper.find(".link-paragraph a").length, 0); + }); +}); diff --git a/browser/components/aboutwelcome/tests/unit/MRColorways.test.jsx b/browser/components/aboutwelcome/tests/unit/MRColorways.test.jsx new file mode 100644 index 0000000000..2d9ebf7ec9 --- /dev/null +++ b/browser/components/aboutwelcome/tests/unit/MRColorways.test.jsx @@ -0,0 +1,328 @@ +import React from "react"; +import { shallow } from "enzyme"; +import { + Colorways, + computeColorWay, + ColorwayDescription, + computeVariationIndex, +} from "content-src/components/MRColorways"; +import { WelcomeScreen } from "content-src/components/MultiStageAboutWelcome"; + +describe("Multistage AboutWelcome module", () => { + let sandbox; + let COLORWAY_SCREEN_PROPS; + beforeEach(() => { + sandbox = sinon.createSandbox(); + COLORWAY_SCREEN_PROPS = { + id: "test-colorway-screen", + totalNumberofScreens: 1, + content: { + subtitle: "test subtitle", + tiles: { + type: "colorway", + action: { + theme: "<event>", + }, + defaultVariationIndex: 0, + systemVariations: ["automatic", "light"], + variations: ["soft", "bold"], + colorways: [ + { + id: "default", + label: "Default", + }, + { + id: "abstract", + label: "Abstract", + }, + ], + }, + primary_button: { + action: {}, + label: "test button", + }, + }, + messageId: "test-mr-colorway-screen", + activeTheme: "automatic", + }; + }); + afterEach(() => { + sandbox.restore(); + }); + + describe("MRColorway component", () => { + it("should render WelcomeScreen", () => { + const wrapper = shallow(<WelcomeScreen {...COLORWAY_SCREEN_PROPS} />); + + assert.ok(wrapper.exists()); + }); + + it("should use default when activeTheme is not set", () => { + const wrapper = shallow(<Colorways {...COLORWAY_SCREEN_PROPS} />); + wrapper.setProps({ activeTheme: null }); + + const colorwaysOptionIcons = wrapper.find( + ".tiles-theme-section .theme .icon" + ); + assert.strictEqual(colorwaysOptionIcons.length, 2); + + // Default automatic theme is selected by default + assert.strictEqual( + colorwaysOptionIcons.first().prop("className").includes("selected"), + true + ); + + assert.strictEqual( + colorwaysOptionIcons.first().prop("className").includes("default"), + true + ); + }); + + it("should use default when activeTheme is alpenglow", () => { + const wrapper = shallow(<Colorways {...COLORWAY_SCREEN_PROPS} />); + wrapper.setProps({ activeTheme: "alpenglow" }); + + const colorwaysOptionIcons = wrapper.find( + ".tiles-theme-section .theme .icon" + ); + assert.strictEqual(colorwaysOptionIcons.length, 2); + + // Default automatic theme is selected when unsupported in colorway alpenglow theme is active + assert.strictEqual( + colorwaysOptionIcons.first().prop("className").includes("selected"), + true + ); + + assert.strictEqual( + colorwaysOptionIcons.first().prop("className").includes("default"), + true + ); + }); + + it("should render colorways options", () => { + const wrapper = shallow(<Colorways {...COLORWAY_SCREEN_PROPS} />); + + const colorwaysOptions = wrapper.find( + ".tiles-theme-section .theme input[name='theme']" + ); + + const colorwaysOptionIcons = wrapper.find( + ".tiles-theme-section .theme .icon" + ); + + const colorwaysLabels = wrapper.find( + ".tiles-theme-section .theme span.sr-only" + ); + + assert.strictEqual(colorwaysOptions.length, 2); + assert.strictEqual(colorwaysOptionIcons.length, 2); + assert.strictEqual(colorwaysLabels.length, 2); + + // First colorway option + // Default theme radio option is selected by default + assert.strictEqual( + colorwaysOptionIcons.first().prop("className").includes("selected"), + true + ); + + //Colorway should be using id property + assert.strictEqual( + colorwaysOptions.first().prop("data-colorway"), + "default" + ); + + // Second colorway option + assert.strictEqual( + colorwaysOptionIcons.last().prop("className").includes("selected"), + false + ); + + //Colorway should be using id property + assert.strictEqual( + colorwaysOptions.last().prop("data-colorway"), + "abstract" + ); + + //Colorway should be labelled for screen readers (parent label is for tooltip only, and does not describe the Colorway) + assert.strictEqual( + colorwaysOptions.last().prop("aria-labelledby"), + "abstract-label" + ); + }); + + it("should handle colorway clicks", () => { + sandbox.stub(React, "useEffect").callsFake((fn, vals) => { + if (vals === undefined) { + fn(); + } else if (vals[0] === "in") { + fn(); + } + }); + + const handleAction = sandbox.stub(); + const wrapper = shallow( + <Colorways handleAction={handleAction} {...COLORWAY_SCREEN_PROPS} /> + ); + const colorwaysOptions = wrapper.find( + ".tiles-theme-section .theme input[name='theme']" + ); + + let props = wrapper.find(ColorwayDescription).props(); + assert.propertyVal(props.colorway, "label", "Default"); + + const option = colorwaysOptions.last(); + assert.propertyVal(option.props(), "value", "abstract-soft"); + colorwaysOptions.last().simulate("click"); + assert.calledOnce(handleAction); + }); + + it("should render colorway description", () => { + const wrapper = shallow(<Colorways {...COLORWAY_SCREEN_PROPS} />); + + let descriptionsWrapper = wrapper.find(ColorwayDescription); + assert.ok(descriptionsWrapper.exists()); + + let props = descriptionsWrapper.props(); + + // Colorway description should display Default theme desc by default + assert.strictEqual(props.colorway.label, "Default"); + }); + + it("ColorwayDescription should display active colorway desc", () => { + let TEST_COLORWAY_PROPS = { + colorway: { + label: "Activist", + description: "Test Activist", + }, + }; + const descWrapper = shallow( + <ColorwayDescription {...TEST_COLORWAY_PROPS} /> + ); + assert.ok(descWrapper.exists()); + const descText = descWrapper.find(".colorway-text"); + assert.equal( + descText.props()["data-l10n-args"].includes("Activist"), + true + ); + }); + + it("should computeColorWayId for default active theme", () => { + let TEST_COLORWAY_PROPS = { + ...COLORWAY_SCREEN_PROPS, + }; + + const colorwayId = computeColorWay( + TEST_COLORWAY_PROPS.activeTheme, + TEST_COLORWAY_PROPS.content.tiles.systemVariations + ); + assert.strictEqual(colorwayId, "default"); + }); + + it("should computeColorWayId for non-default active theme", () => { + let TEST_COLORWAY_PROPS = { + ...COLORWAY_SCREEN_PROPS, + activeTheme: "abstract-soft", + }; + + const colorwayId = computeColorWay( + TEST_COLORWAY_PROPS.activeTheme, + TEST_COLORWAY_PROPS.content.tiles.systemVariations + ); + assert.strictEqual(colorwayId, "abstract"); + }); + + it("should computeVariationIndex for default active theme", () => { + let TEST_COLORWAY_PROPS = { + ...COLORWAY_SCREEN_PROPS, + }; + + const variationIndex = computeVariationIndex( + TEST_COLORWAY_PROPS.activeTheme, + TEST_COLORWAY_PROPS.content.tiles.systemVariations, + TEST_COLORWAY_PROPS.content.tiles.variations, + TEST_COLORWAY_PROPS.content.tiles.defaultVariationIndex + ); + assert.strictEqual( + variationIndex, + TEST_COLORWAY_PROPS.content.tiles.defaultVariationIndex + ); + }); + + it("should computeVariationIndex for active theme", () => { + let TEST_COLORWAY_PROPS = { + ...COLORWAY_SCREEN_PROPS, + }; + + const variationIndex = computeVariationIndex( + "light", + TEST_COLORWAY_PROPS.content.tiles.systemVariations, + TEST_COLORWAY_PROPS.content.tiles.variations, + TEST_COLORWAY_PROPS.content.tiles.defaultVariationIndex + ); + assert.strictEqual(variationIndex, 1); + }); + + it("should computeVariationIndex for colorway theme", () => { + let TEST_COLORWAY_PROPS = { + ...COLORWAY_SCREEN_PROPS, + }; + + const variationIndex = computeVariationIndex( + "abstract-bold", + TEST_COLORWAY_PROPS.content.tiles.systemVariations, + TEST_COLORWAY_PROPS.content.tiles.variations, + TEST_COLORWAY_PROPS.content.tiles.defaultVariationIndex + ); + assert.strictEqual(variationIndex, 1); + }); + + describe("random colorways", () => { + let test; + beforeEach(() => { + COLORWAY_SCREEN_PROPS.handleAction = sandbox.stub(); + sandbox.stub(window, "matchMedia"); + // eslint-disable-next-line max-nested-callbacks + sandbox.stub(React, "useEffect").callsFake((fn, vals) => { + if (vals?.length === 0) { + fn(); + } + }); + test = () => { + shallow(<Colorways {...COLORWAY_SCREEN_PROPS} />); + return COLORWAY_SCREEN_PROPS.handleAction.firstCall.firstArg + .currentTarget; + }; + }); + + it("should select a random colorway", () => { + const { value } = test(); + + assert.strictEqual(value, "abstract-soft"); + assert.calledThrice(React.useEffect); + assert.notCalled(window.matchMedia); + }); + + it("should select a random soft colorway when not dark", () => { + window.matchMedia.returns({ matches: false }); + COLORWAY_SCREEN_PROPS.content.tiles.darkVariation = 1; + + const { value } = test(); + + assert.strictEqual(value, "abstract-soft"); + assert.calledThrice(React.useEffect); + assert.calledOnce(window.matchMedia); + }); + + it("should select a random bold colorway when dark", () => { + window.matchMedia.returns({ matches: true }); + COLORWAY_SCREEN_PROPS.content.tiles.darkVariation = 1; + + const { value } = test(); + + assert.strictEqual(value, "abstract-bold"); + assert.calledThrice(React.useEffect); + assert.calledOnce(window.matchMedia); + }); + }); + }); +}); diff --git a/browser/components/aboutwelcome/tests/unit/MSLocalized.test.jsx b/browser/components/aboutwelcome/tests/unit/MSLocalized.test.jsx new file mode 100644 index 0000000000..57f7e5526c --- /dev/null +++ b/browser/components/aboutwelcome/tests/unit/MSLocalized.test.jsx @@ -0,0 +1,48 @@ +import { Localized } from "content-src/components/MSLocalized"; +import React from "react"; +import { shallow } from "enzyme"; + +describe("<MSLocalized>", () => { + it("should render span with no children", () => { + const shallowWrapper = shallow(<Localized text="test" />); + + assert.ok(shallowWrapper.find("span").exists()); + assert.equal(shallowWrapper.text(), "test"); + }); + it("should render span when using string_id with no children", () => { + const shallowWrapper = shallow( + <Localized text={{ string_id: "test_id" }} /> + ); + + assert.ok(shallowWrapper.find("span[data-l10n-id='test_id']").exists()); + }); + it("should render text inside child", () => { + const shallowWrapper = shallow( + <Localized text="test"> + <div /> + </Localized> + ); + + assert.ok(shallowWrapper.find("div").text(), "test"); + }); + it("should use l10n id on child", () => { + const shallowWrapper = shallow( + <Localized text={{ string_id: "test_id" }}> + <div /> + </Localized> + ); + + assert.ok(shallowWrapper.find("div[data-l10n-id='test_id']").exists()); + }); + it("should keep original children", () => { + const shallowWrapper = shallow( + <Localized text={{ string_id: "test_id" }}> + <h1> + <span data-l10n-name="test" /> + </h1> + </Localized> + ); + + assert.ok(shallowWrapper.find("span[data-l10n-name='test']").exists()); + }); +}); diff --git a/browser/components/aboutwelcome/tests/unit/MobileDownloads.test.jsx b/browser/components/aboutwelcome/tests/unit/MobileDownloads.test.jsx new file mode 100644 index 0000000000..143c7d2f8d --- /dev/null +++ b/browser/components/aboutwelcome/tests/unit/MobileDownloads.test.jsx @@ -0,0 +1,69 @@ +import React from "react"; +import { shallow, mount } from "enzyme"; +import { GlobalOverrider } from "newtab/test/unit/utils"; +import { MobileDownloads } from "content-src/components/MobileDownloads"; + +describe("Multistage AboutWelcome MobileDownloads module", () => { + let globals; + let sandbox; + + beforeEach(async () => { + globals = new GlobalOverrider(); + globals.set({ + AWFinish: () => Promise.resolve(), + AWSendToDeviceEmailsSupported: () => Promise.resolve(), + }); + sandbox = sinon.createSandbox(); + }); + + afterEach(() => { + sandbox.restore(); + globals.restore(); + }); + + describe("Mobile Downloads component", () => { + const MOBILE_DOWNLOADS_PROPS = { + data: { + QR_code: { + image_url: + "chrome://browser/components/privatebrowsing/content/assets/focus-qr-code.svg", + alt_text: { + string_id: "spotlight-focus-promo-qr-code", + }, + }, + email: { + link_text: "Email yourself a link", + }, + marketplace_buttons: ["ios", "android"], + }, + handleAction: () => { + window.AWFinish(); + }, + }; + + it("should render MobileDownloads", () => { + const wrapper = shallow(<MobileDownloads {...MOBILE_DOWNLOADS_PROPS} />); + + assert.ok(wrapper.exists()); + }); + + it("should handle action on markeplace badge click", () => { + const wrapper = mount(<MobileDownloads {...MOBILE_DOWNLOADS_PROPS} />); + + const stub = sandbox.stub(global, "AWFinish"); + wrapper.find(".ios button").simulate("click"); + wrapper.find(".android button").simulate("click"); + + assert.calledTwice(stub); + }); + + it("should handle action on email button click", () => { + const wrapper = shallow(<MobileDownloads {...MOBILE_DOWNLOADS_PROPS} />); + + const stub = sandbox.stub(global, "AWFinish"); + wrapper.find("button.email-link").simulate("click"); + + assert.calledOnce(stub); + }); + }); +}); diff --git a/browser/components/aboutwelcome/tests/unit/MultiSelect.test.jsx b/browser/components/aboutwelcome/tests/unit/MultiSelect.test.jsx new file mode 100644 index 0000000000..b42964f906 --- /dev/null +++ b/browser/components/aboutwelcome/tests/unit/MultiSelect.test.jsx @@ -0,0 +1,221 @@ +import React from "react"; +import { mount } from "enzyme"; +import { MultiSelect } from "content-src/components/MultiSelect"; + +describe("MultiSelect component", () => { + let sandbox; + let MULTISELECT_SCREEN_PROPS; + let setScreenMultiSelects; + let setActiveMultiSelect; + beforeEach(() => { + sandbox = sinon.createSandbox(); + setScreenMultiSelects = sandbox.stub(); + setActiveMultiSelect = sandbox.stub(); + MULTISELECT_SCREEN_PROPS = { + id: "multiselect-screen", + content: { + position: "split", + split_narrow_bkg_position: "-60px", + image_alt_text: { + string_id: "mr2022-onboarding-default-image-alt", + }, + background: + "url('chrome://activity-stream/content/data/content/assets/mr-settodefault.svg') var(--mr-secondary-position) no-repeat var(--mr-screen-background-color)", + progress_bar: true, + logo: {}, + title: "Test Title", + tiles: { + type: "multiselect", + label: "Test Subtitle", + data: [ + { + id: "checkbox-1", + defaultValue: true, + label: { + string_id: "mr2022-onboarding-set-default-primary-button-label", + }, + action: { + type: "SET_DEFAULT_BROWSER", + }, + }, + { + id: "checkbox-2", + defaultValue: true, + label: "Test Checkbox 2", + action: { + type: "SHOW_MIGRATION_WIZARD", + data: {}, + }, + }, + { + id: "checkbox-3", + defaultValue: false, + label: "Test Checkbox 3", + action: { + type: "SHOW_MIGRATION_WIZARD", + data: {}, + }, + }, + ], + }, + primary_button: { + label: "Save and Continue", + action: { + type: "MULTI_ACTION", + collectSelect: true, + navigate: true, + data: { actions: [] }, + }, + }, + secondary_button: { + label: "Skip", + action: { + navigate: true, + }, + has_arrow_icon: true, + }, + }, + setScreenMultiSelects, + setActiveMultiSelect, + }; + }); + afterEach(() => { + sandbox.restore(); + }); + + it("should call setScreenMultiSelects with all ids of checkboxes", () => { + mount(<MultiSelect {...MULTISELECT_SCREEN_PROPS} />); + + assert.calledOnce(setScreenMultiSelects); + assert.calledWith(setScreenMultiSelects, [ + "checkbox-1", + "checkbox-2", + "checkbox-3", + ]); + }); + + it("should not call setScreenMultiSelects if it's already set", () => { + let map = sandbox + .stub() + .returns(MULTISELECT_SCREEN_PROPS.content.tiles.data); + + mount( + <MultiSelect screenMultiSelects={{ map }} {...MULTISELECT_SCREEN_PROPS} /> + ); + + assert.notCalled(setScreenMultiSelects); + assert.calledOnce(map); + assert.calledWith(map, sinon.match.func); + }); + + it("should call setActiveMultiSelect with ids of checkboxes with defaultValue true", () => { + const wrapper = mount(<MultiSelect {...MULTISELECT_SCREEN_PROPS} />); + + wrapper.setProps({ activeMultiSelect: null }); + assert.calledOnce(setActiveMultiSelect); + assert.calledWith(setActiveMultiSelect, ["checkbox-1", "checkbox-2"]); + }); + + it("should use activeMultiSelect ids to set checked state for respective checkbox", () => { + const wrapper = mount(<MultiSelect {...MULTISELECT_SCREEN_PROPS} />); + + wrapper.setProps({ activeMultiSelect: ["checkbox-1", "checkbox-2"] }); + const checkBoxes = wrapper.find(".checkbox-container input"); + assert.strictEqual(checkBoxes.length, 3); + + assert.strictEqual(checkBoxes.first().props().checked, true); + assert.strictEqual(checkBoxes.at(1).props().checked, true); + assert.strictEqual(checkBoxes.last().props().checked, false); + }); + + it("cover the randomize property", async () => { + MULTISELECT_SCREEN_PROPS.content.tiles.data.forEach( + item => (item.randomize = true) + ); + + const wrapper = mount(<MultiSelect {...MULTISELECT_SCREEN_PROPS} />); + + const checkBoxes = wrapper.find(".checkbox-container input"); + assert.strictEqual(checkBoxes.length, 3); + + // We don't want to actually test the randomization, just that it doesn't + // throw. We _could_ render the component until we get a different order, + // and that should work the vast majority of the time, but it's + // theoretically possible that we get the same order over and over again + // until we hit the 2 second timeout. That would be an extremely low failure + // rate, but we already know Math.random() works, so we don't really need to + // test it anyway. It's not worth the added risk of false failures. + }); + + it("should filter out id when checkbox is unchecked", () => { + const wrapper = mount(<MultiSelect {...MULTISELECT_SCREEN_PROPS} />); + wrapper.setProps({ activeMultiSelect: ["checkbox-1", "checkbox-2"] }); + + const ckbx1 = wrapper.find(".checkbox-container input").at(0); + assert.strictEqual(ckbx1.prop("value"), "checkbox-1"); + ckbx1.getDOMNode().checked = false; + ckbx1.simulate("change"); + assert.calledWith(setActiveMultiSelect, ["checkbox-2"]); + }); + + it("should add id when checkbox is checked", () => { + const wrapper = mount(<MultiSelect {...MULTISELECT_SCREEN_PROPS} />); + wrapper.setProps({ activeMultiSelect: ["checkbox-1", "checkbox-2"] }); + + const ckbx3 = wrapper.find(".checkbox-container input").at(2); + assert.strictEqual(ckbx3.prop("value"), "checkbox-3"); + ckbx3.getDOMNode().checked = true; + ckbx3.simulate("change"); + assert.calledWith(setActiveMultiSelect, [ + "checkbox-1", + "checkbox-2", + "checkbox-3", + ]); + }); + + it("should render radios and checkboxes with correct styles", async () => { + const SCREEN_PROPS = { ...MULTISELECT_SCREEN_PROPS }; + SCREEN_PROPS.content.tiles.style = { flexDirection: "row", gap: "24px" }; + SCREEN_PROPS.content.tiles.data = [ + { + id: "checkbox-1", + defaultValue: true, + label: { raw: "Test1" }, + action: { type: "OPEN_PROTECTION_REPORT" }, + style: { color: "red" }, + icon: { style: { color: "blue" } }, + }, + { + id: "radio-1", + type: "radio", + group: "radios", + defaultValue: true, + label: { raw: "Test3" }, + action: { type: "OPEN_PROTECTION_REPORT" }, + style: { color: "purple" }, + icon: { style: { color: "yellow" } }, + }, + ]; + const wrapper = mount(<MultiSelect {...SCREEN_PROPS} />); + + // wait for effect hook + await new Promise(resolve => queueMicrotask(resolve)); + // activeMultiSelect was called on effect hook with default values + assert.calledWith(setActiveMultiSelect, ["checkbox-1", "radio-1"]); + + const container = wrapper.find(".multi-select-container"); + assert.strictEqual(container.prop("style").flexDirection, "row"); + assert.strictEqual(container.prop("style").gap, "24px"); + + // checkboxes/radios are rendered with correct styles + const checkBoxes = wrapper.find(".checkbox-container"); + assert.strictEqual(checkBoxes.length, 2); + assert.strictEqual(checkBoxes.first().prop("style").color, "red"); + assert.strictEqual(checkBoxes.at(1).prop("style").color, "purple"); + + const checks = wrapper.find(".checkbox-container input"); + assert.strictEqual(checks.length, 2); + assert.strictEqual(checks.first().prop("style").color, "blue"); + assert.strictEqual(checks.at(1).prop("style").color, "yellow"); + }); +}); diff --git a/browser/components/aboutwelcome/tests/unit/MultiStageAWProton.test.jsx b/browser/components/aboutwelcome/tests/unit/MultiStageAWProton.test.jsx new file mode 100644 index 0000000000..a40af1c4a1 --- /dev/null +++ b/browser/components/aboutwelcome/tests/unit/MultiStageAWProton.test.jsx @@ -0,0 +1,571 @@ +import { AboutWelcomeDefaults } from "modules/AboutWelcomeDefaults.sys.mjs"; +import { MultiStageProtonScreen } from "content-src/components/MultiStageProtonScreen"; +import { AWScreenUtils } from "modules/AWScreenUtils.sys.mjs"; +import React from "react"; +import { mount } from "enzyme"; + +describe("MultiStageAboutWelcomeProton module", () => { + let sandbox; + let clock; + beforeEach(() => { + clock = sinon.useFakeTimers(); + sandbox = sinon.createSandbox(); + }); + afterEach(() => { + clock.restore(); + sandbox.restore(); + }); + + describe("MultiStageAWProton component", () => { + it("should render MultiStageProton Screen", () => { + const SCREEN_PROPS = { + content: { + title: "test title", + subtitle: "test subtitle", + }, + }; + const wrapper = mount(<MultiStageProtonScreen {...SCREEN_PROPS} />); + assert.ok(wrapper.exists()); + }); + + it("should render secondary section for split positioned screens", () => { + const SCREEN_PROPS = { + content: { + position: "split", + title: "test title", + hero_text: "test subtitle", + }, + }; + const wrapper = mount(<MultiStageProtonScreen {...SCREEN_PROPS} />); + assert.ok(wrapper.exists()); + assert.equal(wrapper.find(".welcome-text h1").text(), "test title"); + assert.equal( + wrapper.find(".section-secondary h1").text(), + "test subtitle" + ); + assert.equal(wrapper.find("main").prop("pos"), "split"); + }); + + it("should render secondary section with content background for split positioned screens", () => { + const BACKGROUND_URL = + "chrome://activity-stream/content/data/content/assets/confetti.svg"; + const SCREEN_PROPS = { + content: { + position: "split", + background: `url(${BACKGROUND_URL}) var(--mr-secondary-position) no-repeat`, + split_narrow_bkg_position: "10px", + title: "test title", + }, + }; + const wrapper = mount(<MultiStageProtonScreen {...SCREEN_PROPS} />); + assert.ok(wrapper.exists()); + assert.ok( + wrapper + .find("div.section-secondary") + .prop("style") + .background.includes("--mr-secondary-position") + ); + assert.ok( + wrapper.find("div.section-secondary").prop("style")[ + "--mr-secondary-background-position-y" + ], + "10px" + ); + }); + + it("should render with secondary section for split positioned screens", () => { + const SCREEN_PROPS = { + content: { + position: "split", + title: "test title", + hero_text: "test subtitle", + }, + }; + const wrapper = mount(<MultiStageProtonScreen {...SCREEN_PROPS} />); + assert.ok(wrapper.exists()); + assert.equal(wrapper.find(".welcome-text h1").text(), "test title"); + assert.equal( + wrapper.find(".section-secondary h1").text(), + "test subtitle" + ); + assert.equal(wrapper.find("main").prop("pos"), "split"); + }); + + it("should render with no secondary section for center positioned screens", () => { + const SCREEN_PROPS = { + content: { + position: "center", + title: "test title", + }, + }; + const wrapper = mount(<MultiStageProtonScreen {...SCREEN_PROPS} />); + assert.ok(wrapper.exists()); + assert.equal(wrapper.find(".section-secondary").exists(), false); + assert.equal(wrapper.find(".welcome-text h1").text(), "test title"); + assert.equal(wrapper.find("main").prop("pos"), "center"); + }); + + it("should not render multiple action buttons if an additional button does not exist", () => { + const SCREEN_PROPS = { + content: { + title: "test title", + primary_button: { + label: "test primary button", + }, + }, + }; + const wrapper = mount(<MultiStageProtonScreen {...SCREEN_PROPS} />); + assert.ok(wrapper.exists()); + assert.isFalse(wrapper.find(".additional-cta").exists()); + }); + + it("should render an additional action button with primary styling if no style has been specified", () => { + const SCREEN_PROPS = { + content: { + title: "test title", + primary_button: { + label: "test primary button", + }, + additional_button: { + label: "test additional button", + }, + }, + }; + const wrapper = mount(<MultiStageProtonScreen {...SCREEN_PROPS} />); + assert.ok(wrapper.exists()); + assert.isTrue(wrapper.find(".additional-cta.primary").exists()); + }); + + it("should render an additional action button with secondary styling", () => { + const SCREEN_PROPS = { + content: { + title: "test title", + primary_button: { + label: "test primary button", + }, + additional_button: { + label: "test additional button", + style: "secondary", + }, + }, + }; + const wrapper = mount(<MultiStageProtonScreen {...SCREEN_PROPS} />); + assert.ok(wrapper.exists()); + assert.equal(wrapper.find(".additional-cta.secondary").exists(), true); + }); + + it("should render an additional action button with primary styling", () => { + const SCREEN_PROPS = { + content: { + title: "test title", + primary_button: { + label: "test primary button", + }, + additional_button: { + label: "test additional button", + style: "primary", + }, + }, + }; + const wrapper = mount(<MultiStageProtonScreen {...SCREEN_PROPS} />); + assert.ok(wrapper.exists()); + assert.equal(wrapper.find(".additional-cta.primary").exists(), true); + }); + + it("should render an additional action with link styling", () => { + const SCREEN_PROPS = { + content: { + position: "split", + title: "test title", + primary_button: { + label: "test primary button", + }, + additional_button: { + label: "test additional button", + style: "link", + }, + }, + }; + const wrapper = mount(<MultiStageProtonScreen {...SCREEN_PROPS} />); + assert.ok(wrapper.exists()); + assert.equal(wrapper.find(".additional-cta.cta-link").exists(), true); + }); + + it("should render an additional button with vertical orientation", () => { + const SCREEN_PROPS = { + content: { + position: "center", + title: "test title", + primary_button: { + label: "test primary button", + }, + additional_button: { + label: "test additional button", + style: "secondary", + flow: "column", + }, + }, + }; + const wrapper = mount(<MultiStageProtonScreen {...SCREEN_PROPS} />); + assert.ok(wrapper.exists()); + assert.equal( + wrapper.find(".additional-cta-container[flow='column']").exists(), + true + ); + }); + + it("should render disabled primary button if activeMultiSelect is in disabled property", () => { + const SCREEN_PROPS = { + content: { + title: "test title", + primary_button: { + label: "test primary button", + disabled: "activeMultiSelect", + }, + }, + }; + const wrapper = mount(<MultiStageProtonScreen {...SCREEN_PROPS} />); + assert.ok(wrapper.exists()); + assert.isTrue(wrapper.find("button.primary[disabled]").exists()); + }); + + it("should render disabled secondary button if activeMultiSelect is in disabled property", () => { + const SCREEN_PROPS = { + content: { + title: "test title", + secondary_button: { + label: "test secondary button", + disabled: "activeMultiSelect", + }, + }, + }; + const wrapper = mount(<MultiStageProtonScreen {...SCREEN_PROPS} />); + assert.ok(wrapper.exists()); + assert.isTrue(wrapper.find("button.secondary[disabled]").exists()); + }); + + it("should not render a progress bar if there is 1 step", () => { + const SCREEN_PROPS = { + content: { + title: "test title", + progress_bar: true, + }, + isSingleScreen: true, + }; + const wrapper = mount(<MultiStageProtonScreen {...SCREEN_PROPS} />); + assert.ok(wrapper.exists()); + assert.equal(wrapper.find(".steps.progress-bar").exists(), false); + }); + + it("should not render a steps indicator if steps indicator is force hidden", () => { + const SCREEN_PROPS = { + content: { + title: "test title", + }, + forceHideStepsIndicator: true, + }; + const wrapper = mount(<MultiStageProtonScreen {...SCREEN_PROPS} />); + assert.ok(wrapper.exists()); + assert.equal(wrapper.find(".steps").exists(), false); + }); + + it("should render a steps indicator above action buttons", () => { + const SCREEN_PROPS = { + content: { + title: "test title", + progress_bar: true, + primary_button: {}, + }, + aboveButtonStepsIndicator: true, + totalNumberOfScreens: 2, + }; + const wrapper = mount(<MultiStageProtonScreen {...SCREEN_PROPS} />); + assert.ok(wrapper.exists()); + + const stepsIndicator = wrapper.find(".steps"); + assert.ok(stepsIndicator, true); + + const stepsDOMNode = stepsIndicator.getDOMNode(); + const siblingElement = stepsDOMNode.nextElementSibling; + assert.equal(siblingElement.classList.contains("action-buttons"), true); + }); + + it("should render a progress bar if there are 2 steps", () => { + const SCREEN_PROPS = { + content: { + title: "test title", + progress_bar: true, + }, + totalNumberOfScreens: 2, + }; + const wrapper = mount(<MultiStageProtonScreen {...SCREEN_PROPS} />); + assert.ok(wrapper.exists()); + assert.equal(wrapper.find(".steps.progress-bar").exists(), true); + }); + + it("should render confirmation-screen if layout property is set to inline", () => { + const SCREEN_PROPS = { + content: { + title: "test title", + layout: "inline", + }, + }; + const wrapper = mount(<MultiStageProtonScreen {...SCREEN_PROPS} />); + assert.ok(wrapper.exists()); + assert.equal(wrapper.find("[layout='inline']").exists(), true); + }); + + it("should render an inline image with alt text and height property", async () => { + const SCREEN_PROPS = { + content: { + above_button_content: [ + { + type: "image", + url: "https://example.com/test.svg", + height: "auto", + alt_text: "test alt text", + }, + ], + }, + }; + const wrapper = mount(<MultiStageProtonScreen {...SCREEN_PROPS} />); + assert.ok(wrapper.exists()); + const imageEl = wrapper.find(".inline-image img"); + assert.equal(imageEl.exists(), true); + assert.propertyVal(imageEl.prop("style"), "height", "auto"); + const altTextCointainer = wrapper.find(".sr-only"); + assert.equal(altTextCointainer.contains("test alt text"), true); + }); + + it("should render multiple inline elements in correct order", async () => { + const SCREEN_PROPS = { + content: { + above_button_content: [ + { + type: "image", + url: "https://example.com/test.svg", + height: "auto", + alt_text: "test alt text", + }, + { + type: "text", + text: { + string_id: "test-string-id", + }, + link_keys: ["privacy_policy", "terms_of_use"], + }, + { + type: "image", + url: "https://example.com/test_2.svg", + height: "auto", + alt_text: "test alt text 2", + }, + { + type: "text", + text: { + string_id: "test-string-id-2", + }, + link_keys: ["privacy_policy", "terms_of_use"], + }, + ], + }, + }; + + const wrapper = mount(<MultiStageProtonScreen {...SCREEN_PROPS} />); + assert.ok(wrapper.exists()); + const imageEl = wrapper.find(".inline-image img"); + const textEl = wrapper.find(".link-paragraph"); + + assert.equal(imageEl.length, 2); + assert.equal(textEl.length, 2); + + assert.equal(imageEl.at(0).prop("src"), "https://example.com/test.svg"); + assert.equal(imageEl.at(1).prop("src"), "https://example.com/test_2.svg"); + + assert.equal(textEl.at(0).prop("data-l10n-id"), "test-string-id"); + assert.equal(textEl.at(1).prop("data-l10n-id"), "test-string-id-2"); + }); + }); + + describe("AboutWelcomeDefaults for proton", () => { + const getData = () => AboutWelcomeDefaults.getDefaults(); + + async function prepConfig(config, evalFalseScreenIds) { + let data = await getData(); + + if (evalFalseScreenIds?.length) { + data.screens.forEach(async screen => { + if (evalFalseScreenIds.includes(screen.id)) { + screen.targeting = false; + } + }); + data.screens = await AWScreenUtils.evaluateTargetingAndRemoveScreens( + data.screens + ); + } + + return AboutWelcomeDefaults.prepareContentForReact({ + ...data, + ...config, + }); + } + beforeEach(() => { + sandbox.stub(global.Services.prefs, "getBoolPref").returns(true); + sandbox.stub(AWScreenUtils, "evaluateScreenTargeting").returnsArg(0); + // This is necessary because there are still screens being removed with + // `removeScreens` in `prepareContentForReact()`. Once we've migrated + // to using screen targeting instead of manually removing screens, + // we can remove this stub. + sandbox + .stub(global.AWScreenUtils, "removeScreens") + .callsFake((screens, callback) => + AWScreenUtils.removeScreens(screens, callback) + ); + }); + it("should have a multi action primary button by default", async () => { + const data = await prepConfig({}, ["AW_WELCOME_BACK"]); + assert.propertyVal( + data.screens[0].content.primary_button.action, + "type", + "MULTI_ACTION" + ); + }); + it("should have a FxA button", async () => { + const data = await prepConfig({}, ["AW_WELCOME_BACK"]); + + assert.notProperty(data, "skipFxA"); + assert.property(data.screens[0].content, "secondary_button_top"); + }); + it("should remove the FxA button if pref disabled", async () => { + global.Services.prefs.getBoolPref.returns(false); + + const data = await prepConfig(); + + assert.property(data, "skipFxA", true); + assert.notProperty(data.screens[0].content, "secondary_button_top"); + }); + }); + + describe("AboutWelcomeDefaults for MR split template proton", () => { + const getData = () => AboutWelcomeDefaults.getDefaults(true); + beforeEach(() => { + sandbox.stub(global.Services.prefs, "getBoolPref").returns(true); + }); + + it("should use 'split' position template by default", async () => { + const data = await getData(); + assert.propertyVal(data.screens[0].content, "position", "split"); + }); + + it("should not include noodles by default", async () => { + const data = await getData(); + assert.notProperty(data.screens[0].content, "has_noodles"); + }); + }); + + describe("AboutWelcomeDefaults prepareMobileDownload", () => { + const TEST_CONTENT = { + screens: [ + { + id: "AW_MOBILE_DOWNLOAD", + content: { + title: "test", + hero_image: { + url: "https://example.com/test.svg", + }, + cta_paragraph: { + text: {}, + action: {}, + }, + }, + }, + ], + }; + it("should not set url for default qrcode svg", async () => { + sandbox.stub(global.AppConstants, "isChinaRepack").returns(false); + const data = await AboutWelcomeDefaults.prepareContentForReact( + TEST_CONTENT + ); + assert.propertyVal( + data.screens[0].content.hero_image, + "url", + "https://example.com/test.svg" + ); + }); + it("should set url for cn qrcode svg", async () => { + sandbox.stub(global.AppConstants, "isChinaRepack").returns(true); + const data = await AboutWelcomeDefaults.prepareContentForReact( + TEST_CONTENT + ); + assert.propertyVal( + data.screens[0].content.hero_image, + "url", + "https://example.com/test-cn.svg" + ); + }); + }); + + describe("AboutWelcomeDefaults prepareContentForReact", () => { + it("should not set action without screens", async () => { + const data = await AboutWelcomeDefaults.prepareContentForReact({ + ua: "test", + }); + + assert.propertyVal(data, "ua", "test"); + assert.notProperty(data, "screens"); + }); + it("should set action for import action", async () => { + const TEST_CONTENT = { + ua: "test", + screens: [ + { + id: "AW_IMPORT_SETTINGS", + content: { + primary_button: { + action: { + type: "SHOW_MIGRATION_WIZARD", + }, + }, + }, + }, + ], + }; + const data = await AboutWelcomeDefaults.prepareContentForReact( + TEST_CONTENT + ); + assert.propertyVal(data, "ua", "test"); + assert.propertyVal( + data.screens[0].content.primary_button.action.data, + "source", + "test" + ); + }); + it("should not set action if the action type != SHOW_MIGRATION_WIZARD", async () => { + const TEST_CONTENT = { + ua: "test", + screens: [ + { + id: "AW_IMPORT_SETTINGS", + content: { + primary_button: { + action: { + type: "SHOW_FIREFOX_ACCOUNTS", + data: {}, + }, + }, + }, + }, + ], + }; + const data = await AboutWelcomeDefaults.prepareContentForReact( + TEST_CONTENT + ); + assert.propertyVal(data, "ua", "test"); + assert.notPropertyVal( + data.screens[0].content.primary_button.action.data, + "source", + "test" + ); + }); + }); +}); diff --git a/browser/components/aboutwelcome/tests/unit/MultiStageAboutWelcome.test.jsx b/browser/components/aboutwelcome/tests/unit/MultiStageAboutWelcome.test.jsx new file mode 100644 index 0000000000..b4593a45f3 --- /dev/null +++ b/browser/components/aboutwelcome/tests/unit/MultiStageAboutWelcome.test.jsx @@ -0,0 +1,859 @@ +import { GlobalOverrider } from "newtab/test/unit/utils"; +import { + MultiStageAboutWelcome, + SecondaryCTA, + StepsIndicator, + ProgressBar, + WelcomeScreen, +} from "content-src/components/MultiStageAboutWelcome"; +import { Themes } from "content-src/components/Themes"; +import React from "react"; +import { shallow, mount } from "enzyme"; +import { AboutWelcomeDefaults } from "modules/AboutWelcomeDefaults.sys.mjs"; +import { AboutWelcomeUtils } from "content-src/lib/aboutwelcome-utils.mjs"; + +describe("MultiStageAboutWelcome module", () => { + let globals; + let sandbox; + + const DEFAULT_PROPS = { + defaultScreens: AboutWelcomeDefaults.getDefaults().screens, + metricsFlowUri: "http://localhost/", + message_id: "DEFAULT_ABOUTWELCOME", + utm_term: "default", + startScreen: 0, + }; + + beforeEach(async () => { + globals = new GlobalOverrider(); + globals.set({ + AWGetSelectedTheme: () => Promise.resolve("automatic"), + AWSendEventTelemetry: () => {}, + AWWaitForMigrationClose: () => Promise.resolve(), + AWSelectTheme: () => Promise.resolve(), + AWFinish: () => Promise.resolve(), + }); + sandbox = sinon.createSandbox(); + }); + + afterEach(() => { + sandbox.restore(); + globals.restore(); + }); + + describe("MultiStageAboutWelcome functional component", () => { + it("should render MultiStageAboutWelcome", () => { + const wrapper = shallow(<MultiStageAboutWelcome {...DEFAULT_PROPS} />); + + assert.ok(wrapper.exists()); + }); + + it("should pass activeTheme and initialTheme props to WelcomeScreen", async () => { + let wrapper = mount(<MultiStageAboutWelcome {...DEFAULT_PROPS} />); + // Spin the event loop to allow the useEffect hooks to execute, + // any promises to resolve, and re-rendering to happen after the + // promises have updated the state/props + await new Promise(resolve => setTimeout(resolve, 0)); + // sync up enzyme's representation with the real DOM + wrapper.update(); + + let welcomeScreenWrapper = wrapper.find(WelcomeScreen); + assert.strictEqual(welcomeScreenWrapper.prop("activeTheme"), "automatic"); + assert.strictEqual( + welcomeScreenWrapper.prop("initialTheme"), + "automatic" + ); + }); + + it("should handle primary Action", () => { + const screens = [ + { + content: { + title: "test title", + subtitle: "test subtitle", + primary_button: { + label: "Test button", + action: { + navigate: true, + }, + }, + }, + }, + ]; + + const PRIMARY_ACTION_PROPS = { + defaultScreens: screens, + metricsFlowUri: "http://localhost/", + message_id: "DEFAULT_ABOUTWELCOME", + utm_term: "default", + startScreen: 0, + }; + + const stub = sinon.stub(AboutWelcomeUtils, "sendActionTelemetry"); + let wrapper = mount(<MultiStageAboutWelcome {...PRIMARY_ACTION_PROPS} />); + wrapper.update(); + + let welcomeScreenWrapper = wrapper.find(WelcomeScreen); + const btnPrimary = welcomeScreenWrapper.find(".primary"); + btnPrimary.simulate("click"); + assert.calledOnce(stub); + assert.equal( + stub.firstCall.args[0], + welcomeScreenWrapper.props().messageId + ); + assert.equal(stub.firstCall.args[1], "primary_button"); + stub.restore(); + }); + + it("should autoAdvance on last screen and send appropriate telemetry", () => { + let clock = sinon.useFakeTimers(); + const screens = [ + { + auto_advance: "primary_button", + content: { + title: "test title", + subtitle: "test subtitle", + primary_button: { + label: "Test Button", + action: { + navigate: true, + }, + }, + }, + }, + ]; + const AUTO_ADVANCE_PROPS = { + defaultScreens: screens, + metricsFlowUri: "http://localhost/", + message_id: "DEFAULT_ABOUTWELCOME", + utm_term: "default", + startScreen: 0, + }; + const wrapper = mount(<MultiStageAboutWelcome {...AUTO_ADVANCE_PROPS} />); + wrapper.update(); + const finishStub = sandbox.stub(global, "AWFinish"); + const telemetryStub = sinon.stub( + AboutWelcomeUtils, + "sendActionTelemetry" + ); + + assert.notCalled(finishStub); + clock.tick(20001); + assert.calledOnce(finishStub); + assert.calledOnce(telemetryStub); + assert.equal(telemetryStub.lastCall.args[2], "AUTO_ADVANCE"); + clock.restore(); + finishStub.restore(); + telemetryStub.restore(); + }); + + it("should send telemetry ping on collectSelect", () => { + const screens = [ + { + id: "EASY_SETUP_TEST", + content: { + tiles: { + type: "multiselect", + data: [ + { + id: "checkbox-1", + defaultValue: true, + }, + ], + }, + primary_button: { + label: "Test Button", + action: { + collectSelect: true, + }, + }, + }, + }, + ]; + const EASY_SETUP_PROPS = { + defaultScreens: screens, + message_id: "DEFAULT_ABOUTWELCOME", + startScreen: 0, + }; + const stub = sinon.stub(AboutWelcomeUtils, "sendActionTelemetry"); + let wrapper = mount(<MultiStageAboutWelcome {...EASY_SETUP_PROPS} />); + wrapper.update(); + + let welcomeScreenWrapper = wrapper.find(WelcomeScreen); + const btnPrimary = welcomeScreenWrapper.find(".primary"); + btnPrimary.simulate("click"); + assert.calledTwice(stub); + assert.equal( + stub.firstCall.args[0], + welcomeScreenWrapper.props().messageId + ); + assert.equal(stub.firstCall.args[1], "primary_button"); + assert.equal( + stub.lastCall.args[0], + welcomeScreenWrapper.props().messageId + ); + assert.ok(stub.lastCall.args[1].includes("checkbox-1")); + assert.equal(stub.lastCall.args[2], "SELECT_CHECKBOX"); + stub.restore(); + }); + }); + + describe("WelcomeScreen component", () => { + describe("easy setup screen", () => { + const screen = AboutWelcomeDefaults.getDefaults().screens.find( + s => s.id === "AW_EASY_SETUP_NEEDS_DEFAULT_AND_PIN" + ); + let EASY_SETUP_SCREEN_PROPS; + + beforeEach(() => { + EASY_SETUP_SCREEN_PROPS = { + id: screen.id, + content: screen.content, + messageId: `${DEFAULT_PROPS.message_id}_${screen.id}`, + UTMTerm: DEFAULT_PROPS.utm_term, + flowParams: null, + totalNumberOfScreens: 1, + setScreenMultiSelects: sandbox.stub(), + setActiveMultiSelect: sandbox.stub(), + }; + }); + + it("should render Easy Setup screen", () => { + const wrapper = shallow(<WelcomeScreen {...EASY_SETUP_SCREEN_PROPS} />); + assert.ok(wrapper.exists()); + }); + + it("should render secondary.top button", () => { + let SCREEN_PROPS = { + content: { + title: "Step", + secondary_button_top: { + text: "test", + label: "test label", + }, + }, + position: "top", + }; + const wrapper = mount(<SecondaryCTA {...SCREEN_PROPS} />); + assert.ok(wrapper.find("div.secondary-cta.top").exists()); + }); + + it("should render the arrow icon in the secondary button", () => { + let SCREEN_PROPS = { + content: { + title: "Step", + secondary_button: { + has_arrow_icon: true, + label: "test label", + }, + }, + }; + const wrapper = mount(<SecondaryCTA {...SCREEN_PROPS} />); + assert.ok(wrapper.find("button.arrow-icon").exists()); + }); + + it("should render steps indicator", () => { + let PROPS = { totalNumberOfScreens: 1 }; + const wrapper = mount(<StepsIndicator {...PROPS} />); + assert.ok(wrapper.find("div.indicator").exists()); + }); + + it("should assign the total number of screens and current screen to the aria-valuemax and aria-valuenow labels", () => { + const EXTRA_PROPS = { totalNumberOfScreens: 3, order: 1 }; + const wrapper = mount( + <WelcomeScreen {...EASY_SETUP_SCREEN_PROPS} {...EXTRA_PROPS} /> + ); + + const steps = wrapper.find(`div.steps`); + assert.ok(steps.exists()); + const { attributes } = steps.getDOMNode(); + assert.equal( + parseInt(attributes.getNamedItem("aria-valuemax").value, 10), + EXTRA_PROPS.totalNumberOfScreens + ); + assert.equal( + parseInt(attributes.getNamedItem("aria-valuenow").value, 10), + EXTRA_PROPS.order + 1 + ); + }); + + it("should render progress bar", () => { + let SCREEN_PROPS = { + step: 1, + previousStep: 0, + totalNumberOfScreens: 2, + }; + const wrapper = mount(<ProgressBar {...SCREEN_PROPS} />); + assert.ok(wrapper.find("div.indicator").exists()); + assert.propertyVal( + wrapper.find("div.indicator").prop("style"), + "--progress-bar-progress", + "50%" + ); + }); + + it("should have a primary, secondary and secondary.top button in the rendered input", () => { + const wrapper = mount(<WelcomeScreen {...EASY_SETUP_SCREEN_PROPS} />); + assert.ok(wrapper.find(".primary").exists()); + assert.ok( + wrapper + .find(".secondary-cta button.secondary[value='secondary_button']") + .exists() + ); + assert.ok( + wrapper + .find( + ".secondary-cta.top button.secondary[value='secondary_button_top']" + ) + .exists() + ); + }); + }); + + describe("theme screen", () => { + const THEME_SCREEN_PROPS = { + id: "test-theme-screen", + totalNumberOfScreens: 1, + content: { + title: "test title", + subtitle: "test subtitle", + tiles: { + type: "theme", + action: { + theme: "<event>", + }, + data: [ + { + theme: "automatic", + label: "test-label", + tooltip: "test-tooltip", + description: "test-description", + }, + ], + }, + primary_button: { + action: {}, + label: "test button", + }, + }, + navigate: null, + messageId: `${DEFAULT_PROPS.message_id}_"test-theme-screen"`, + UTMTerm: DEFAULT_PROPS.utm_term, + flowParams: null, + activeTheme: "automatic", + }; + + it("should render WelcomeScreen", () => { + const wrapper = shallow(<WelcomeScreen {...THEME_SCREEN_PROPS} />); + + assert.ok(wrapper.exists()); + }); + + it("should check this.props.activeTheme in the rendered input", () => { + const wrapper = shallow(<Themes {...THEME_SCREEN_PROPS} />); + + const selectedThemeInput = wrapper.find(".theme input[checked=true]"); + assert.strictEqual( + selectedThemeInput.prop("value"), + THEME_SCREEN_PROPS.activeTheme + ); + }); + }); + describe("import screen", () => { + const IMPORT_SCREEN_PROPS = { + content: { + title: "test title", + subtitle: "test subtitle", + }, + }; + it("should render ImportScreen", () => { + const wrapper = mount(<WelcomeScreen {...IMPORT_SCREEN_PROPS} />); + assert.ok(wrapper.exists()); + }); + it("should not have a primary or secondary button", () => { + const wrapper = mount(<WelcomeScreen {...IMPORT_SCREEN_PROPS} />); + assert.isFalse(wrapper.find(".primary").exists()); + assert.isFalse( + wrapper.find(".secondary button[value='secondary_button']").exists() + ); + assert.isFalse( + wrapper + .find(".secondary button[value='secondary_button_top']") + .exists() + ); + }); + }); + describe("#handleAction", () => { + let SCREEN_PROPS; + let TEST_ACTION; + beforeEach(() => { + SCREEN_PROPS = { + content: { + title: "test title", + subtitle: "test subtitle", + primary_button: { + action: {}, + label: "test button", + }, + }, + navigate: sandbox.stub(), + setActiveTheme: sandbox.stub(), + UTMTerm: "you_tee_emm", + }; + TEST_ACTION = SCREEN_PROPS.content.primary_button.action; + sandbox.stub(AboutWelcomeUtils, "handleUserAction").resolves(); + }); + it("should handle navigate", () => { + TEST_ACTION.navigate = true; + const wrapper = mount(<WelcomeScreen {...SCREEN_PROPS} />); + + wrapper.find(".primary").simulate("click"); + + assert.calledOnce(SCREEN_PROPS.navigate); + }); + it("should handle theme", () => { + TEST_ACTION.theme = "test"; + const wrapper = mount(<WelcomeScreen {...SCREEN_PROPS} />); + + wrapper.find(".primary").simulate("click"); + + assert.calledWith(SCREEN_PROPS.setActiveTheme, "test"); + }); + it("should handle dismiss", () => { + SCREEN_PROPS.content.dismiss_button = { + action: { dismiss: true }, + }; + const finishStub = sandbox.stub(global, "AWFinish"); + const wrapper = mount(<WelcomeScreen {...SCREEN_PROPS} />); + + wrapper.find(".dismiss-button").simulate("click"); + + assert.calledOnce(finishStub); + }); + it("should handle SHOW_FIREFOX_ACCOUNTS", () => { + TEST_ACTION.type = "SHOW_FIREFOX_ACCOUNTS"; + const wrapper = mount(<WelcomeScreen {...SCREEN_PROPS} />); + + wrapper.find(".primary").simulate("click"); + + assert.calledWith(AboutWelcomeUtils.handleUserAction, { + data: { + extraParams: { + utm_campaign: "firstrun", + utm_medium: "referral", + utm_source: "activity-stream", + utm_term: "you_tee_emm-screen", + }, + }, + type: "SHOW_FIREFOX_ACCOUNTS", + }); + }); + it("should handle OPEN_URL", () => { + TEST_ACTION.type = "OPEN_URL"; + TEST_ACTION.data = { + args: "https://example.com?utm_campaign=test-campaign", + }; + TEST_ACTION.addFlowParams = true; + let flowBeginTime = Date.now(); + const wrapper = mount( + <WelcomeScreen + {...SCREEN_PROPS} + flowParams={{ + deviceId: "test-device-id", + flowId: "test-flow-id", + flowBeginTime, + }} + /> + ); + + wrapper.find(".primary").simulate("click"); + + let [handledAction] = AboutWelcomeUtils.handleUserAction.firstCall.args; + assert.equal(handledAction.type, "OPEN_URL"); + let { searchParams } = new URL(handledAction.data.args); + assert.equal(searchParams.get("utm_campaign"), "test-campaign"); + assert.equal(searchParams.get("utm_medium"), "referral"); + assert.equal(searchParams.get("utm_source"), "activity-stream"); + assert.equal(searchParams.get("utm_term"), "you_tee_emm-screen"); + assert.equal(searchParams.get("device_id"), "test-device-id"); + assert.equal(searchParams.get("flow_id"), "test-flow-id"); + assert.equal( + searchParams.get("flow_begin_time"), + flowBeginTime.toString() + ); + }); + it("should handle SHOW_MIGRATION_WIZARD", () => { + TEST_ACTION.type = "SHOW_MIGRATION_WIZARD"; + const wrapper = mount(<WelcomeScreen {...SCREEN_PROPS} />); + + wrapper.find(".primary").simulate("click"); + + assert.calledWith(AboutWelcomeUtils.handleUserAction, { + type: "SHOW_MIGRATION_WIZARD", + }); + }); + it("should handle SHOW_MIGRATION_WIZARD INSIDE MULTI_ACTION", async () => { + const migrationCloseStub = sandbox.stub( + global, + "AWWaitForMigrationClose" + ); + const MULTI_ACTION_SCREEN_PROPS = { + content: { + title: "test title", + subtitle: "test subtitle", + primary_button: { + action: { + type: "MULTI_ACTION", + navigate: true, + data: { + actions: [ + { + type: "PIN_FIREFOX_TO_TASKBAR", + }, + { + type: "SET_DEFAULT_BROWSER", + }, + { + type: "SHOW_MIGRATION_WIZARD", + data: {}, + }, + ], + }, + }, + label: "test button", + }, + }, + navigate: sandbox.stub(), + }; + const wrapper = mount(<WelcomeScreen {...MULTI_ACTION_SCREEN_PROPS} />); + + wrapper.find(".primary").simulate("click"); + assert.calledWith(AboutWelcomeUtils.handleUserAction, { + type: "MULTI_ACTION", + navigate: true, + data: { + actions: [ + { + type: "PIN_FIREFOX_TO_TASKBAR", + }, + { + type: "SET_DEFAULT_BROWSER", + }, + { + type: "SHOW_MIGRATION_WIZARD", + data: {}, + }, + ], + }, + }); + // handleUserAction returns a Promise, so let's let the microtask queue + // flush so that anything waiting for the handleUserAction Promise to + // resolve can run. + await new Promise(resolve => queueMicrotask(resolve)); + assert.calledOnce(migrationCloseStub); + }); + + it("should handle SHOW_MIGRATION_WIZARD INSIDE NESTED MULTI_ACTION", async () => { + const migrationCloseStub = sandbox.stub( + global, + "AWWaitForMigrationClose" + ); + const MULTI_ACTION_SCREEN_PROPS = { + content: { + title: "test title", + subtitle: "test subtitle", + primary_button: { + action: { + type: "MULTI_ACTION", + navigate: true, + data: { + actions: [ + { + type: "PIN_FIREFOX_TO_TASKBAR", + }, + { + type: "SET_DEFAULT_BROWSER", + }, + { + type: "MULTI_ACTION", + data: { + actions: [ + { + type: "SET_PREF", + }, + { + type: "SHOW_MIGRATION_WIZARD", + data: {}, + }, + ], + }, + }, + ], + }, + }, + label: "test button", + }, + }, + navigate: sandbox.stub(), + }; + const wrapper = mount(<WelcomeScreen {...MULTI_ACTION_SCREEN_PROPS} />); + + wrapper.find(".primary").simulate("click"); + assert.calledWith(AboutWelcomeUtils.handleUserAction, { + type: "MULTI_ACTION", + navigate: true, + data: { + actions: [ + { + type: "PIN_FIREFOX_TO_TASKBAR", + }, + { + type: "SET_DEFAULT_BROWSER", + }, + { + type: "MULTI_ACTION", + data: { + actions: [ + { + type: "SET_PREF", + }, + { + type: "SHOW_MIGRATION_WIZARD", + data: {}, + }, + ], + }, + }, + ], + }, + }); + // handleUserAction returns a Promise, so let's let the microtask queue + // flush so that anything waiting for the handleUserAction Promise to + // resolve can run. + await new Promise(resolve => queueMicrotask(resolve)); + assert.calledOnce(migrationCloseStub); + }); + it("should unset prefs from unchecked checkboxes", () => { + const PREF_SCREEN_PROPS = { + content: { + title: "Checkboxes", + tiles: { + type: "multiselect", + data: [ + { + id: "checkbox-1", + label: "checkbox 1", + checkedAction: { + type: "SET_PREF", + data: { + pref: { + name: "pref-a", + value: true, + }, + }, + }, + uncheckedAction: { + type: "SET_PREF", + data: { + pref: { + name: "pref-a", + }, + }, + }, + }, + { + id: "checkbox-2", + label: "checkbox 2", + checkedAction: { + type: "MULTI_ACTION", + data: { + actions: [ + { + type: "SET_PREF", + data: { + pref: { + name: "pref-b", + value: "pref-b", + }, + }, + }, + { + type: "SET_PREF", + data: { + pref: { + name: "pref-c", + value: 3, + }, + }, + }, + ], + }, + }, + uncheckedAction: { + type: "SET_PREF", + data: { + pref: { name: "pref-b" }, + }, + }, + }, + ], + }, + primary_button: { + label: "Set Prefs", + action: { + type: "MULTI_ACTION", + collectSelect: true, + isDynamic: true, + navigate: true, + data: { + actions: [], + }, + }, + }, + }, + navigate: sandbox.stub(), + setScreenMultiSelects: sandbox.stub(), + setActiveMultiSelect: sandbox.stub(), + }; + + // No checkboxes checked. All prefs will be unset and pref-c will not be + // reset. + { + const wrapper = mount( + <WelcomeScreen {...PREF_SCREEN_PROPS} activeMultiSelect={[]} /> + ); + wrapper.find(".primary").simulate("click"); + assert.calledWith(AboutWelcomeUtils.handleUserAction, { + type: "MULTI_ACTION", + collectSelect: true, + isDynamic: true, + navigate: true, + data: { + actions: [ + { type: "SET_PREF", data: { pref: { name: "pref-a" } } }, + { type: "SET_PREF", data: { pref: { name: "pref-b" } } }, + ], + }, + }); + + AboutWelcomeUtils.handleUserAction.resetHistory(); + } + + // The first checkbox is checked. Only pref-a will be set and pref-c + // will not be reset. + { + const wrapper = mount( + <WelcomeScreen + {...PREF_SCREEN_PROPS} + activeMultiSelect={["checkbox-1"]} + /> + ); + wrapper.find(".primary").simulate("click"); + assert.calledWith(AboutWelcomeUtils.handleUserAction, { + type: "MULTI_ACTION", + collectSelect: true, + isDynamic: true, + navigate: true, + data: { + actions: [ + { + type: "SET_PREF", + data: { + pref: { + name: "pref-a", + value: true, + }, + }, + }, + { type: "SET_PREF", data: { pref: { name: "pref-b" } } }, + ], + }, + }); + + AboutWelcomeUtils.handleUserAction.resetHistory(); + } + + // The second checkbox is checked. Prefs pref-b and pref-c will be set. + { + const wrapper = mount( + <WelcomeScreen + {...PREF_SCREEN_PROPS} + activeMultiSelect={["checkbox-2"]} + /> + ); + wrapper.find(".primary").simulate("click"); + assert.calledWith(AboutWelcomeUtils.handleUserAction, { + type: "MULTI_ACTION", + collectSelect: true, + isDynamic: true, + navigate: true, + data: { + actions: [ + { type: "SET_PREF", data: { pref: { name: "pref-a" } } }, + { + type: "MULTI_ACTION", + data: { + actions: [ + { + type: "SET_PREF", + data: { pref: { name: "pref-b", value: "pref-b" } }, + }, + { + type: "SET_PREF", + data: { pref: { name: "pref-c", value: 3 } }, + }, + ], + }, + }, + ], + }, + }); + + AboutWelcomeUtils.handleUserAction.resetHistory(); + } + + // // Both checkboxes are checked. All prefs will be set. + { + const wrapper = mount( + <WelcomeScreen + {...PREF_SCREEN_PROPS} + activeMultiSelect={["checkbox-1", "checkbox-2"]} + /> + ); + wrapper.find(".primary").simulate("click"); + assert.calledWith(AboutWelcomeUtils.handleUserAction, { + type: "MULTI_ACTION", + collectSelect: true, + isDynamic: true, + navigate: true, + data: { + actions: [ + { + type: "SET_PREF", + data: { pref: { name: "pref-a", value: true } }, + }, + { + type: "MULTI_ACTION", + data: { + actions: [ + { + type: "SET_PREF", + data: { pref: { name: "pref-b", value: "pref-b" } }, + }, + { + type: "SET_PREF", + data: { pref: { name: "pref-c", value: 3 } }, + }, + ], + }, + }, + ], + }, + }); + + AboutWelcomeUtils.handleUserAction.resetHistory(); + } + }); + }); + }); +}); diff --git a/browser/components/aboutwelcome/tests/unit/OnboardingVideoTest.test.jsx b/browser/components/aboutwelcome/tests/unit/OnboardingVideoTest.test.jsx new file mode 100644 index 0000000000..078c8e17c4 --- /dev/null +++ b/browser/components/aboutwelcome/tests/unit/OnboardingVideoTest.test.jsx @@ -0,0 +1,45 @@ +import React from "react"; +import { mount } from "enzyme"; +import { OnboardingVideo } from "content-src/components/OnboardingVideo"; + +describe("OnboardingVideo component", () => { + let sandbox; + + beforeEach(async () => { + sandbox = sinon.createSandbox(); + }); + + afterEach(() => { + sandbox.restore(); + }); + + const SCREEN_PROPS = { + content: { + title: "Test title", + video_container: { + video_url: "test url", + }, + }, + }; + + it("should handle video_start action when video is played", () => { + const handleAction = sandbox.stub(); + const wrapper = mount( + <OnboardingVideo handleAction={handleAction} {...SCREEN_PROPS} /> + ); + wrapper.find("video").simulate("play"); + assert.calledWith(handleAction, { + currentTarget: { value: "video_start" }, + }); + }); + it("should handle video_end action when video has completed playing", () => { + const handleAction = sandbox.stub(); + const wrapper = mount( + <OnboardingVideo handleAction={handleAction} {...SCREEN_PROPS} /> + ); + wrapper.find("video").simulate("ended"); + assert.calledWith(handleAction, { + currentTarget: { value: "video_end" }, + }); + }); +}); diff --git a/browser/components/aboutwelcome/tests/unit/addUtmParams.test.js b/browser/components/aboutwelcome/tests/unit/addUtmParams.test.js new file mode 100644 index 0000000000..2c078b4f49 --- /dev/null +++ b/browser/components/aboutwelcome/tests/unit/addUtmParams.test.js @@ -0,0 +1,34 @@ +import { addUtmParams, BASE_PARAMS } from "content-src/lib/addUtmParams.mjs"; + +describe("addUtmParams", () => { + const originalBaseParams = JSON.parse(JSON.stringify(BASE_PARAMS)); + afterEach(() => Object.assign(BASE_PARAMS, originalBaseParams)); + it("should convert a string URL", () => { + const result = addUtmParams("https://foo.com", "foo"); + assert.equal(result.hostname, "foo.com"); + }); + it("should add all base params", () => { + assert.match( + addUtmParams(new URL("https://foo.com"), "foo").toString(), + /utm_source=activity-stream&utm_campaign=firstrun&utm_medium=referral/ + ); + }); + it("should allow updating base params utm values", () => { + BASE_PARAMS.utm_campaign = "firstrun-default"; + assert.match( + addUtmParams(new URL("https://foo.com"), "foo", "default").toString(), + /utm_source=activity-stream&utm_campaign=firstrun-default&utm_medium=referral/ + ); + }); + it("should add utm_term", () => { + const params = addUtmParams(new URL("https://foo.com"), "foo").searchParams; + assert.equal(params.get("utm_term"), "foo", "utm_term"); + }); + it("should not override the URL's existing utm param values", () => { + const url = new URL("https://foo.com/?utm_source=foo&utm_campaign=bar"); + const params = addUtmParams(url, "foo").searchParams; + assert.equal(params.get("utm_source"), "foo", "utm_source"); + assert.equal(params.get("utm_campaign"), "bar", "utm_campaign"); + assert.equal(params.get("utm_medium"), "referral", "utm_medium"); + }); +}); diff --git a/browser/components/aboutwelcome/tests/unit/unit-entry.js b/browser/components/aboutwelcome/tests/unit/unit-entry.js new file mode 100644 index 0000000000..fb70eeb843 --- /dev/null +++ b/browser/components/aboutwelcome/tests/unit/unit-entry.js @@ -0,0 +1,716 @@ +import { + EventEmitter, + FakePrefs, + FakensIPrefService, + GlobalOverrider, + FakeConsoleAPI, + FakeLogger, +} from "newtab/test/unit/utils"; +import Adapter from "enzyme-adapter-react-16"; +import { chaiAssertions } from "newtab/test/schemas/pings"; +import enzyme from "enzyme"; + +enzyme.configure({ adapter: new Adapter() }); + +// Cause React warnings to make tests that trigger them fail +const origConsoleError = console.error; +console.error = function (msg, ...args) { + origConsoleError.apply(console, [msg, ...args]); + + if ( + /(Invalid prop|Failed prop type|Check the render method|React Intl)/.test( + msg + ) + ) { + throw new Error(msg); + } +}; + +const req = require.context(".", true, /\.test\.jsx?$/); +const files = req.keys(); + +// This exposes sinon assertions to chai.assert +sinon.assert.expose(assert, { prefix: "" }); + +chai.use(chaiAssertions); + +const overrider = new GlobalOverrider(); + +const RemoteSettings = name => ({ + get: () => { + if (name === "attachment") { + return Promise.resolve([{ attachment: {} }]); + } + return Promise.resolve([]); + }, + on: () => {}, + off: () => {}, +}); +RemoteSettings.pollChanges = () => {}; + +class JSWindowActorParent { + sendAsyncMessage(name, data) { + return { name, data }; + } +} + +class JSWindowActorChild { + sendAsyncMessage(name, data) { + return { name, data }; + } + + sendQuery(name, data) { + return Promise.resolve({ name, data }); + } + + get contentWindow() { + return { + Promise, + }; + } +} + +// Detect plain object passed to lazy getter APIs, and set its prototype to +// global object, and return the global object for further modification. +// Returns the object if it's not plain object. +// +// This is a workaround to make the existing testharness and testcase keep +// working even after lazy getters are moved to plain `lazy` object. +const cachedPlainObject = new Set(); +function updateGlobalOrObject(object) { + // Given this function modifies the prototype, and the following + // condition doesn't meet on the second call, cache the result. + if (cachedPlainObject.has(object)) { + return global; + } + + if (Object.getPrototypeOf(object).constructor.name !== "Object") { + return object; + } + + cachedPlainObject.add(object); + Object.setPrototypeOf(object, global); + return global; +} + +const TEST_GLOBAL = { + JSWindowActorParent, + JSWindowActorChild, + AboutReaderParent: { + addMessageListener: (messageName, listener) => {}, + removeMessageListener: (messageName, listener) => {}, + }, + AboutWelcomeTelemetry: class { + submitGleanPingForPing() {} + }, + AddonManager: { + getActiveAddons() { + return Promise.resolve({ addons: [], fullData: false }); + }, + }, + AppConstants: { + MOZILLA_OFFICIAL: true, + MOZ_APP_VERSION: "69.0a1", + isChinaRepack() { + return false; + }, + isPlatformAndVersionAtMost() { + return false; + }, + platform: "win", + }, + ASRouterPreferences: { + console: new FakeConsoleAPI({ + maxLogLevel: "off", // set this to "debug" or "all" to get more ASRouter logging in tests + prefix: "ASRouter", + }), + }, + AWScreenUtils: { + evaluateTargetingAndRemoveScreens() { + return true; + }, + async removeScreens() { + return true; + }, + evaluateScreenTargeting() { + return true; + }, + }, + BrowserUtils: { + sendToDeviceEmailsSupported() { + return true; + }, + }, + UpdateUtils: { getUpdateChannel() {} }, + BasePromiseWorker: class { + constructor() { + this.ExceptionHandlers = []; + } + post() {} + }, + browserSearchRegion: "US", + BrowserWindowTracker: { getTopWindow() {} }, + ChromeUtils: { + defineLazyGetter(object, name, f) { + updateGlobalOrObject(object)[name] = f(); + }, + defineModuleGetter: updateGlobalOrObject, + defineESModuleGetters: updateGlobalOrObject, + generateQI() { + return {}; + }, + import() { + return global; + }, + importESModule() { + return global; + }, + }, + ClientEnvironment: { + get userId() { + return "foo123"; + }, + }, + Components: { + Constructor(classId) { + switch (classId) { + case "@mozilla.org/referrer-info;1": + return function (referrerPolicy, sendReferrer, originalReferrer) { + this.referrerPolicy = referrerPolicy; + this.sendReferrer = sendReferrer; + this.originalReferrer = originalReferrer; + }; + } + return function () {}; + }, + isSuccessCode: () => true, + }, + ConsoleAPI: FakeConsoleAPI, + // NB: These are functions/constructors + // eslint-disable-next-line object-shorthand + ContentSearchUIController: function () {}, + // eslint-disable-next-line object-shorthand + ContentSearchHandoffUIController: function () {}, + Cc: { + "@mozilla.org/browser/nav-bookmarks-service;1": { + addObserver() {}, + getService() { + return this; + }, + removeObserver() {}, + SOURCES: {}, + TYPE_BOOKMARK: {}, + }, + "@mozilla.org/browser/nav-history-service;1": { + addObserver() {}, + executeQuery() {}, + getNewQuery() {}, + getNewQueryOptions() {}, + getService() { + return this; + }, + insert() {}, + markPageAsTyped() {}, + removeObserver() {}, + }, + "@mozilla.org/io/string-input-stream;1": { + createInstance() { + return {}; + }, + }, + "@mozilla.org/security/hash;1": { + createInstance() { + return { + init() {}, + updateFromStream() {}, + finish() { + return "0"; + }, + }; + }, + }, + "@mozilla.org/updates/update-checker;1": { createInstance() {} }, + "@mozilla.org/widget/useridleservice;1": { + getService() { + return { + idleTime: 0, + addIdleObserver() {}, + removeIdleObserver() {}, + }; + }, + }, + "@mozilla.org/streamConverters;1": { + getService() { + return this; + }, + }, + "@mozilla.org/network/stream-loader;1": { + createInstance() { + return {}; + }, + }, + }, + Ci: { + nsICryptoHash: {}, + nsIReferrerInfo: { UNSAFE_URL: 5 }, + nsITimer: { TYPE_ONE_SHOT: 1 }, + nsIWebProgressListener: { LOCATION_CHANGE_SAME_DOCUMENT: 1 }, + nsIDOMWindow: Object, + nsITrackingDBService: { + TRACKERS_ID: 1, + TRACKING_COOKIES_ID: 2, + CRYPTOMINERS_ID: 3, + FINGERPRINTERS_ID: 4, + SOCIAL_ID: 5, + }, + nsICookieBannerService: { + MODE_DISABLED: 0, + MODE_REJECT: 1, + MODE_REJECT_OR_ACCEPT: 2, + MODE_UNSET: 3, + }, + }, + Cu: { + importGlobalProperties() {}, + now: () => window.performance.now(), + cloneInto: o => JSON.parse(JSON.stringify(o)), + }, + console: { + ...console, + error() {}, + }, + dump() {}, + EveryWindow: { + registerCallback: (id, init, uninit) => {}, + unregisterCallback: id => {}, + }, + setTimeout: window.setTimeout.bind(window), + clearTimeout: window.clearTimeout.bind(window), + fetch() {}, + // eslint-disable-next-line object-shorthand + Image: function () {}, // NB: This is a function/constructor + IOUtils: { + writeJSON() { + return Promise.resolve(0); + }, + readJSON() { + return Promise.resolve({}); + }, + read() { + return Promise.resolve(new Uint8Array()); + }, + makeDirectory() { + return Promise.resolve(0); + }, + write() { + return Promise.resolve(0); + }, + exists() { + return Promise.resolve(0); + }, + remove() { + return Promise.resolve(0); + }, + stat() { + return Promise.resolve(0); + }, + }, + NewTabUtils: { + activityStreamProvider: { + getTopFrecentSites: () => [], + executePlacesQuery: async (sql, options) => ({ sql, options }), + }, + }, + OS: { + File: { + writeAtomic() {}, + makeDir() {}, + stat() {}, + Error: {}, + read() {}, + exists() {}, + remove() {}, + removeEmptyDir() {}, + }, + Path: { + join() { + return "/"; + }, + }, + Constants: { + Path: { + localProfileDir: "/", + }, + }, + }, + PathUtils: { + join(...parts) { + return parts[parts.length - 1]; + }, + joinRelative(...parts) { + return parts[parts.length - 1]; + }, + getProfileDir() { + return Promise.resolve("/"); + }, + getLocalProfileDir() { + return Promise.resolve("/"); + }, + }, + PlacesUtils: { + get bookmarks() { + return TEST_GLOBAL.Cc["@mozilla.org/browser/nav-bookmarks-service;1"]; + }, + get history() { + return TEST_GLOBAL.Cc["@mozilla.org/browser/nav-history-service;1"]; + }, + observers: { + addListener() {}, + removeListener() {}, + }, + }, + Preferences: FakePrefs, + PrivateBrowsingUtils: { + isBrowserPrivate: () => false, + isWindowPrivate: () => false, + permanentPrivateBrowsing: false, + }, + DownloadsViewUI: { + getDisplayName: () => "filename.ext", + getSizeWithUnits: () => "1.5 MB", + }, + FileUtils: { + // eslint-disable-next-line object-shorthand + File: function () {}, // NB: This is a function/constructor + }, + Region: { + home: "US", + REGION_TOPIC: "browser-region-updated", + }, + Services: { + dirsvc: { + get: () => ({ parent: { parent: { path: "appPath" } } }), + }, + env: { + set: () => undefined, + }, + locale: { + get appLocaleAsBCP47() { + return "en-US"; + }, + negotiateLanguages() {}, + }, + urlFormatter: { formatURL: str => str, formatURLPref: str => str }, + mm: { + addMessageListener: (msg, cb) => this.receiveMessage(), + removeMessageListener() {}, + }, + obs: { + addObserver() {}, + removeObserver() {}, + notifyObservers() {}, + }, + telemetry: { + setEventRecordingEnabled: () => {}, + recordEvent: eventDetails => {}, + scalarSet: () => {}, + keyedScalarAdd: () => {}, + }, + uuid: { + generateUUID() { + return "{foo-123-foo}"; + }, + }, + console: { logStringMessage: () => {} }, + prefs: new FakensIPrefService(), + tm: { + dispatchToMainThread: cb => cb(), + idleDispatchToMainThread: cb => cb(), + }, + eTLD: { + getBaseDomain({ spec }) { + return spec.match(/\/([^/]+)/)[1]; + }, + getBaseDomainFromHost(host) { + return host.match(/.*?(\w+\.\w+)$/)[1]; + }, + getPublicSuffix() {}, + }, + io: { + newURI: spec => ({ + mutate: () => ({ + setRef: ref => ({ + finalize: () => ({ + ref, + spec, + }), + }), + }), + spec, + }), + }, + search: { + init() { + return Promise.resolve(); + }, + getVisibleEngines: () => + Promise.resolve([{ identifier: "google" }, { identifier: "bing" }]), + defaultEngine: { + identifier: "google", + searchForm: + "https://www.google.com/search?q=&ie=utf-8&oe=utf-8&client=firefox-b", + aliases: ["@google"], + }, + defaultPrivateEngine: { + identifier: "bing", + searchForm: "https://www.bing.com", + aliases: ["@bing"], + }, + getEngineByAlias: async () => null, + }, + scriptSecurityManager: { + createNullPrincipal() {}, + getSystemPrincipal() {}, + }, + wm: { + getMostRecentWindow: () => window, + getMostRecentBrowserWindow: () => window, + getEnumerator: () => [], + }, + ww: { registerNotification() {}, unregisterNotification() {} }, + appinfo: { appBuildID: "20180710100040", version: "69.0a1" }, + scriptloader: { loadSubScript: () => {} }, + startup: { + getStartupInfo() { + return { + process: { + getTime() { + return 1588010448000; + }, + }, + }; + }, + }, + }, + XPCOMUtils: { + defineLazyGlobalGetters: updateGlobalOrObject, + defineLazyModuleGetters: updateGlobalOrObject, + defineLazyServiceGetter: updateGlobalOrObject, + defineLazyServiceGetters: updateGlobalOrObject, + defineLazyPreferenceGetter(object, name) { + updateGlobalOrObject(object)[name] = ""; + }, + generateQI() { + return {}; + }, + }, + EventEmitter, + ShellService: { + doesAppNeedPin: () => false, + isDefaultBrowser: () => true, + }, + FilterExpressions: { + eval() { + return Promise.resolve(false); + }, + }, + RemoteSettings, + Localization: class { + async formatMessages(stringsIds) { + return Promise.resolve( + stringsIds.map(({ id, args }) => ({ value: { string_id: id, args } })) + ); + } + async formatValue(stringId) { + return Promise.resolve(stringId); + } + }, + FxAccountsConfig: { + promiseConnectAccountURI(id) { + return Promise.resolve(id); + }, + }, + FX_MONITOR_OAUTH_CLIENT_ID: "fake_client_id", + ExperimentAPI: { + getExperiment() {}, + getExperimentMetaData() {}, + getRolloutMetaData() {}, + }, + NimbusFeatures: { + glean: { + getVariable() {}, + }, + newtab: { + getVariable() {}, + getAllVariables() {}, + onUpdate() {}, + offUpdate() {}, + }, + pocketNewtab: { + getVariable() {}, + getAllVariables() {}, + onUpdate() {}, + offUpdate() {}, + }, + cookieBannerHandling: { + getVariable() {}, + }, + }, + TelemetryEnvironment: { + setExperimentActive() {}, + currentEnvironment: { + profile: { + creationDate: 16587, + }, + settings: {}, + }, + }, + TelemetryStopwatch: { + start: () => {}, + finish: () => {}, + }, + Sampling: { + ratioSample(seed, ratios) { + return Promise.resolve(0); + }, + }, + BrowserHandler: { + get kiosk() { + return false; + }, + }, + TelemetrySession: { + getMetadata(reason) { + return { + reason, + sessionId: "fake_session_id", + }; + }, + }, + PageThumbs: { + addExpirationFilter() {}, + removeExpirationFilter() {}, + }, + Logger: FakeLogger, + getFxAccountsSingleton() {}, + AboutNewTab: {}, + Glean: { + newtab: { + opened: { + record() {}, + }, + closed: { + record() {}, + }, + locale: { + set() {}, + }, + newtabCategory: { + set() {}, + }, + homepageCategory: { + set() {}, + }, + blockedSponsors: { + set() {}, + }, + sovAllocation: { + set() {}, + }, + }, + newtabSearch: { + enabled: { + set() {}, + }, + }, + pocket: { + enabled: { + set() {}, + }, + impression: { + record() {}, + }, + isSignedIn: { + set() {}, + }, + sponsoredStoriesEnabled: { + set() {}, + }, + click: { + record() {}, + }, + save: { + record() {}, + }, + topicClick: { + record() {}, + }, + }, + topsites: { + enabled: { + set() {}, + }, + sponsoredEnabled: { + set() {}, + }, + impression: { + record() {}, + }, + click: { + record() {}, + }, + rows: { + set() {}, + }, + showPrivacyClick: { + record() {}, + }, + dismiss: { + record() {}, + }, + prefChanged: { + record() {}, + }, + }, + topSites: { + pingType: { + set() {}, + }, + position: { + set() {}, + }, + source: { + set() {}, + }, + tileId: { + set() {}, + }, + reportingUrl: { + set() {}, + }, + advertiser: { + set() {}, + }, + contextId: { + set() {}, + }, + }, + }, + GleanPings: { + newtab: { + submit() {}, + }, + topSites: { + submit() {}, + }, + }, + Utils: { + SERVER_URL: "bogus://foo", + }, +}; +overrider.set(TEST_GLOBAL); + +describe("activity-stream", () => { + after(() => overrider.restore()); + files.forEach(file => req(file)); +}); |