summaryrefslogtreecommitdiffstats
path: root/browser/components/aboutwelcome/tests
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 00:47:55 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 00:47:55 +0000
commit26a029d407be480d791972afb5975cf62c9360a6 (patch)
treef435a8308119effd964b339f76abb83a57c29483 /browser/components/aboutwelcome/tests
parentInitial commit. (diff)
downloadfirefox-26a029d407be480d791972afb5975cf62c9360a6.tar.xz
firefox-26a029d407be480d791972afb5975cf62c9360a6.zip
Adding upstream version 124.0.1.upstream/124.0.1
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'browser/components/aboutwelcome/tests')
-rw-r--r--browser/components/aboutwelcome/tests/browser/browser.toml57
-rw-r--r--browser/components/aboutwelcome/tests/browser/browser_aboutwelcome_attribution.js214
-rw-r--r--browser/components/aboutwelcome/tests/browser/browser_aboutwelcome_configurable_ui.js724
-rw-r--r--browser/components/aboutwelcome/tests/browser/browser_aboutwelcome_fxa_signin_flow.js303
-rw-r--r--browser/components/aboutwelcome/tests/browser/browser_aboutwelcome_glean.js162
-rw-r--r--browser/components/aboutwelcome/tests/browser/browser_aboutwelcome_import.js94
-rw-r--r--browser/components/aboutwelcome/tests/browser/browser_aboutwelcome_mobile_downloads.js112
-rw-r--r--browser/components/aboutwelcome/tests/browser/browser_aboutwelcome_multistage_addonspicker.js178
-rw-r--r--browser/components/aboutwelcome/tests/browser/browser_aboutwelcome_multistage_default.js794
-rw-r--r--browser/components/aboutwelcome/tests/browser/browser_aboutwelcome_multistage_experimentAPI.js641
-rw-r--r--browser/components/aboutwelcome/tests/browser/browser_aboutwelcome_multistage_languageSwitcher.js708
-rw-r--r--browser/components/aboutwelcome/tests/browser/browser_aboutwelcome_multistage_mr.js770
-rw-r--r--browser/components/aboutwelcome/tests/browser/browser_aboutwelcome_multistage_video.js97
-rw-r--r--browser/components/aboutwelcome/tests/browser/browser_aboutwelcome_observer.js73
-rw-r--r--browser/components/aboutwelcome/tests/browser/browser_aboutwelcome_rtamo.js299
-rw-r--r--browser/components/aboutwelcome/tests/browser/browser_aboutwelcome_screen_targeting.js274
-rw-r--r--browser/components/aboutwelcome/tests/browser/browser_aboutwelcome_upgrade_multistage_mr.js320
-rw-r--r--browser/components/aboutwelcome/tests/browser/head.js149
-rw-r--r--browser/components/aboutwelcome/tests/unit/AWScreenUtils.test.jsx140
-rw-r--r--browser/components/aboutwelcome/tests/unit/CTAParagraph.test.jsx49
-rw-r--r--browser/components/aboutwelcome/tests/unit/HelpText.test.jsx41
-rw-r--r--browser/components/aboutwelcome/tests/unit/HeroImage.test.jsx40
-rw-r--r--browser/components/aboutwelcome/tests/unit/LinkParagraph.test.jsx102
-rw-r--r--browser/components/aboutwelcome/tests/unit/MRColorways.test.jsx328
-rw-r--r--browser/components/aboutwelcome/tests/unit/MSLocalized.test.jsx48
-rw-r--r--browser/components/aboutwelcome/tests/unit/MobileDownloads.test.jsx69
-rw-r--r--browser/components/aboutwelcome/tests/unit/MultiSelect.test.jsx221
-rw-r--r--browser/components/aboutwelcome/tests/unit/MultiStageAWProton.test.jsx571
-rw-r--r--browser/components/aboutwelcome/tests/unit/MultiStageAboutWelcome.test.jsx859
-rw-r--r--browser/components/aboutwelcome/tests/unit/OnboardingVideoTest.test.jsx45
-rw-r--r--browser/components/aboutwelcome/tests/unit/addUtmParams.test.js34
-rw-r--r--browser/components/aboutwelcome/tests/unit/unit-entry.js716
32 files changed, 9232 insertions, 0 deletions
diff --git a/browser/components/aboutwelcome/tests/browser/browser.toml b/browser/components/aboutwelcome/tests/browser/browser.toml
new file mode 100644
index 0000000000..22d95272e8
--- /dev/null
+++ b/browser/components/aboutwelcome/tests/browser/browser.toml
@@ -0,0 +1,57 @@
+[DEFAULT]
+support-files = [
+ "head.js",
+]
+
+prefs = [
+ "intl.multilingual.aboutWelcome.languageMismatchEnabled=false",
+]
+
+["browser_aboutwelcome_attribution.js"]
+skip-if = [
+ "os == 'linux'", # Test setup only implemented for OSX and Windows
+ "os == 'win' && msix", # These tests rely on the ability to write postSigningData, which we can't do in MSIX builds. https://bugzilla.mozilla.org/show_bug.cgi?id=1805911
+]
+
+["browser_aboutwelcome_configurable_ui.js"]
+skip-if = ["os == 'linux' && bits == 64 && debug"] # Bug 1784548
+
+["browser_aboutwelcome_fxa_signin_flow.js"]
+
+["browser_aboutwelcome_glean.js"]
+
+["browser_aboutwelcome_import.js"]
+fail-if = ["a11y_checks"] # Bug 1854514 clicked primary button may not be focusable
+
+["browser_aboutwelcome_mobile_downloads.js"]
+
+["browser_aboutwelcome_multistage_addonspicker.js"]
+
+["browser_aboutwelcome_multistage_default.js"]
+
+["browser_aboutwelcome_multistage_experimentAPI.js"]
+
+["browser_aboutwelcome_multistage_languageSwitcher.js"]
+skip-if = ["os == 'linux' && bits == 64"] # Bug 1757875
+
+["browser_aboutwelcome_multistage_mr.js"]
+skip-if = ["os == 'linux' && bits == 64 && debug"] # Bug 1812050
+
+["browser_aboutwelcome_multistage_video.js"]
+
+["browser_aboutwelcome_observer.js"]
+https_first_disabled = true
+
+["browser_aboutwelcome_rtamo.js"]
+skip-if = [
+ "os == 'linux'", # Test setup only implemented for OSX and Windows
+ "os == 'win' && msix", # These tests rely on the ability to write postSigningData, which we can't do in MSIX builds. https://bugzilla.mozilla.org/show_bug.cgi?id=1805911
+]
+
+["browser_aboutwelcome_screen_targeting.js"]
+
+["browser_aboutwelcome_upgrade_multistage_mr.js"]
+skip-if = [
+ "win11_2009 && debug",
+ "os == 'linux' && debug", # Bug 1804804 - disabled on win && linux for extremely high failure rate
+]
diff --git a/browser/components/aboutwelcome/tests/browser/browser_aboutwelcome_attribution.js b/browser/components/aboutwelcome/tests/browser/browser_aboutwelcome_attribution.js
new file mode 100644
index 0000000000..f0727c9b6f
--- /dev/null
+++ b/browser/components/aboutwelcome/tests/browser/browser_aboutwelcome_attribution.js
@@ -0,0 +1,214 @@
+"use strict";
+
+const { ASRouter } = ChromeUtils.importESModule(
+ "resource:///modules/asrouter/ASRouter.sys.mjs"
+);
+const { AttributionCode } = ChromeUtils.importESModule(
+ "resource:///modules/AttributionCode.sys.mjs"
+);
+const { AddonRepository } = ChromeUtils.importESModule(
+ "resource://gre/modules/addons/AddonRepository.sys.mjs"
+);
+
+const TEST_ATTRIBUTION_DATA = {
+ source: "addons.mozilla.org",
+ medium: "referral",
+ campaign: "non-fx-button",
+ // with the sinon override, the id doesn't matter
+ content: "rta:whatever",
+};
+
+const TEST_ADDON_INFO = [
+ {
+ name: "Test Add-on",
+ sourceURI: { scheme: "https", spec: "https://test.xpi" },
+ icons: { 32: "test.png", 64: "test.png" },
+ type: "extension",
+ },
+];
+
+const TEST_UA_ATTRIBUTION_DATA = {
+ ua: "chrome",
+};
+
+const TEST_PROTON_CONTENT = [
+ {
+ id: "AW_STEP1",
+ content: {
+ title: "Step 1",
+ primary_button: {
+ label: "Next",
+ action: {
+ navigate: true,
+ },
+ },
+ has_noodles: true,
+ },
+ },
+ {
+ id: "AW_STEP2",
+ content: {
+ title: "Step 2",
+ primary_button: {
+ label: {
+ string_id: "onboarding-multistage-import-primary-button-label",
+ },
+ action: {
+ type: "SHOW_MIGRATION_WIZARD",
+ data: {},
+ },
+ },
+ has_noodles: true,
+ },
+ },
+];
+
+async function openRTAMOWithAttribution() {
+ const sandbox = sinon.createSandbox();
+ sandbox.stub(AddonRepository, "getAddonsByIDs").resolves(TEST_ADDON_INFO);
+
+ await AttributionCode.deleteFileAsync();
+ await ASRouter.forceAttribution(TEST_ATTRIBUTION_DATA);
+
+ AttributionCode._clearCache();
+ const data = await AttributionCode.getAttrDataAsync();
+
+ Assert.equal(
+ data.source,
+ "addons.mozilla.org",
+ "Attribution data should be set"
+ );
+
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "about:welcome",
+ true
+ );
+ registerCleanupFunction(async () => {
+ BrowserTestUtils.removeTab(tab);
+ await ASRouter.forceAttribution("");
+ sandbox.restore();
+ });
+ return tab.linkedBrowser;
+}
+
+/**
+ * Setup and test RTAMO welcome UI
+ */
+async function test_screen_content(
+ browser,
+ experiment,
+ expectedSelectors = [],
+ unexpectedSelectors = []
+) {
+ await ContentTask.spawn(
+ browser,
+ { expectedSelectors, experiment, unexpectedSelectors },
+ async ({
+ expectedSelectors: expected,
+ experiment: experimentName,
+ unexpectedSelectors: unexpected,
+ }) => {
+ for (let selector of expected) {
+ await ContentTaskUtils.waitForCondition(
+ () => content.document.querySelector(selector),
+ `Should render ${selector} in ${experimentName}`
+ );
+ }
+ for (let selector of unexpected) {
+ ok(
+ !content.document.querySelector(selector),
+ `Should not render ${selector} in ${experimentName}`
+ );
+ }
+ }
+ );
+}
+
+add_task(async function test_rtamo_attribution() {
+ let browser = await openRTAMOWithAttribution();
+
+ await test_screen_content(
+ browser,
+ "RTAMO UI",
+ // Expected selectors:
+ [
+ "div.onboardingContainer",
+ "h2[data-l10n-id='mr1-return-to-amo-addon-title']",
+ "div.rtamo-icon",
+ "button.primary",
+ "button.secondary",
+ ],
+ // Unexpected selectors:
+ [
+ "main.AW_STEP1",
+ "main.AW_STEP2",
+ "main.AW_STEP3",
+ "div.tiles-container.info",
+ ]
+ );
+});
+
+async function openMultiStageWithUserAgentAttribution() {
+ const sandbox = sinon.createSandbox();
+ await ASRouter.forceAttribution(TEST_UA_ATTRIBUTION_DATA);
+ const TEST_PROTON_JSON = JSON.stringify(TEST_PROTON_CONTENT);
+
+ await setAboutWelcomePref(true);
+ await pushPrefs(["browser.aboutwelcome.screens", TEST_PROTON_JSON]);
+
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "about:welcome",
+ true
+ );
+ registerCleanupFunction(async () => {
+ BrowserTestUtils.removeTab(tab);
+ await ASRouter.forceAttribution("");
+ sandbox.restore();
+ });
+ return tab.linkedBrowser;
+}
+
+async function onButtonClick(browser, elementId) {
+ await ContentTask.spawn(
+ browser,
+ { elementId },
+ async ({ elementId: buttonId }) => {
+ await ContentTaskUtils.waitForCondition(
+ () => content.document.querySelector(buttonId),
+ buttonId
+ );
+ let button = content.document.querySelector(buttonId);
+ button.click();
+ }
+ );
+}
+
+add_task(async function test_ua_attribution() {
+ let browser = await openMultiStageWithUserAgentAttribution();
+
+ await test_screen_content(
+ browser,
+ "multistage step 1 with ua attribution",
+ // Expected selectors:
+ ["div.onboardingContainer", "main.AW_STEP1", "button.primary"],
+ // Unexpected selectors:
+ ["main.AW_STEP2"]
+ );
+
+ await onButtonClick(browser, "button.primary");
+
+ await test_screen_content(
+ browser,
+ "multistage step 2 with ua attribution",
+ // Expected selectors:
+ [
+ "div.onboardingContainer",
+ "main.AW_STEP2",
+ "button.primary[data-l10n-args*='Google Chrome']",
+ ],
+ // Unexpected selectors:
+ ["main.AW_STEP1"]
+ );
+});
diff --git a/browser/components/aboutwelcome/tests/browser/browser_aboutwelcome_configurable_ui.js b/browser/components/aboutwelcome/tests/browser/browser_aboutwelcome_configurable_ui.js
new file mode 100644
index 0000000000..d53b5acc14
--- /dev/null
+++ b/browser/components/aboutwelcome/tests/browser/browser_aboutwelcome_configurable_ui.js
@@ -0,0 +1,724 @@
+"use strict";
+
+const { ExperimentFakes } = ChromeUtils.importESModule(
+ "resource://testing-common/NimbusTestUtils.sys.mjs"
+);
+
+const { AboutWelcomeTelemetry } = ChromeUtils.importESModule(
+ "resource:///modules/aboutwelcome/AboutWelcomeTelemetry.sys.mjs"
+);
+
+const BASE_SCREEN_CONTENT = {
+ title: "Step 1",
+ primary_button: {
+ label: "Next",
+ action: {
+ navigate: true,
+ },
+ },
+ secondary_button: {
+ label: "link",
+ },
+};
+
+const makeTestContent = (id, contentAdditions) => {
+ return {
+ id,
+ content: Object.assign({}, BASE_SCREEN_CONTENT, contentAdditions),
+ };
+};
+
+async function openAboutWelcome(json) {
+ if (json) {
+ await setAboutWelcomeMultiStage(json);
+ }
+
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "about:welcome",
+ true
+ );
+ registerCleanupFunction(() => {
+ BrowserTestUtils.removeTab(tab);
+ });
+ return tab.linkedBrowser;
+}
+
+async function testAboutWelcomeLogoFor(logo = {}) {
+ info(`Testing logo: ${JSON.stringify(logo)}`);
+
+ let screens = [makeTestContent("TEST_LOGO_SELECTION_STEP", { logo })];
+
+ let doExperimentCleanup = await ExperimentFakes.enrollWithFeatureConfig({
+ featureId: "aboutwelcome",
+ value: { enabled: true, screens },
+ });
+
+ let browser = await openAboutWelcome(JSON.stringify(screens));
+
+ let expected = [
+ `.brand-logo[src="${
+ logo.imageURL ?? "chrome://branding/content/about-logo.svg"
+ }"][alt=""]`,
+ ];
+ let unexpected = [];
+ (logo.alt ? unexpected : expected).push('.brand-logo[role="presentation"]');
+ (logo.width ? expected : unexpected).push(`.brand-logo[style*="width"]`);
+ (logo.height ? expected : unexpected).push(`.brand-logo[style*="height"]`);
+ (logo.marginBlock ? expected : unexpected).push(
+ `.logo-container[style*="margin-block"]`
+ );
+ (logo.marginInline ? expected : unexpected).push(
+ `.logo-container[style*="margin-inline"]`
+ );
+ (logo.darkModeImageURL ? expected : unexpected).push(
+ `.logo-container source[media="(prefers-color-scheme: dark)"]${
+ logo.darkModeImageURL ? `[srcset="${logo.darkModeImageURL}"]` : ""
+ }`
+ );
+ (logo.reducedMotionImageURL ? expected : unexpected).push(
+ `.logo-container source[media="(prefers-reduced-motion: reduce)"]${
+ logo.reducedMotionImageURL
+ ? `[srcset="${logo.reducedMotionImageURL}"]`
+ : ""
+ }`
+ );
+ (logo.darkModeReducedMotionImageURL ? expected : unexpected).push(
+ `.logo-container source[media="(prefers-color-scheme: dark) and (prefers-reduced-motion: reduce)"]${
+ logo.darkModeReducedMotionImageURL
+ ? `[srcset="${logo.darkModeReducedMotionImageURL}"]`
+ : ""
+ }`
+ );
+ await test_screen_content(
+ browser,
+ "renders screen with passed logo",
+ expected,
+ unexpected
+ );
+
+ await doExperimentCleanup();
+ browser.closeBrowser();
+}
+
+/**
+ * Test rendering a screen in about welcome with decorative noodles
+ */
+add_task(async function test_aboutwelcome_with_noodles() {
+ const TEST_NOODLE_CONTENT = makeTestContent("TEST_NOODLE_STEP", {
+ has_noodles: true,
+ });
+ const TEST_NOODLE_JSON = JSON.stringify([TEST_NOODLE_CONTENT]);
+ let browser = await openAboutWelcome(TEST_NOODLE_JSON);
+
+ await test_screen_content(
+ browser,
+ "renders screen with noodles",
+ // Expected selectors:
+ [
+ "main.TEST_NOODLE_STEP[pos='center']",
+ "div.noodle.purple-C",
+ "div.noodle.orange-L",
+ "div.noodle.outline-L",
+ "div.noodle.yellow-circle",
+ ]
+ );
+ browser.closeBrowser();
+});
+
+/**
+ * Test rendering a screen with a customized logo
+ */
+add_task(async function test_aboutwelcome_with_customized_logo() {
+ const TEST_LOGO_URL = "chrome://branding/content/icon64.png";
+ const TEST_LOGO_HEIGHT = "50px";
+ const TEST_LOGO_CONTENT = makeTestContent("TEST_LOGO_STEP", {
+ logo: {
+ height: TEST_LOGO_HEIGHT,
+ imageURL: TEST_LOGO_URL,
+ },
+ });
+ const TEST_LOGO_JSON = JSON.stringify([TEST_LOGO_CONTENT]);
+ let browser = await openAboutWelcome(TEST_LOGO_JSON);
+
+ await test_screen_content(
+ browser,
+ "renders screen with customized logo",
+ // Expected selectors:
+ ["main.TEST_LOGO_STEP[pos='center']", `.brand-logo[src="${TEST_LOGO_URL}"]`]
+ );
+
+ // Ensure logo has custom height
+ await test_element_styles(
+ browser,
+ ".brand-logo",
+ // Expected styles:
+ {
+ // Override default text-link styles
+ height: TEST_LOGO_HEIGHT,
+ }
+ );
+ browser.closeBrowser();
+});
+
+/**
+ * Test rendering a screen with empty logo used for padding
+ */
+add_task(async function test_aboutwelcome_with_empty_logo_spacing() {
+ const TEST_LOGO_HEIGHT = "50px";
+ const TEST_LOGO_CONTENT = makeTestContent("TEST_LOGO_STEP", {
+ logo: {
+ height: TEST_LOGO_HEIGHT,
+ imageURL: "none",
+ },
+ });
+ const TEST_LOGO_JSON = JSON.stringify([TEST_LOGO_CONTENT]);
+ let browser = await openAboutWelcome(TEST_LOGO_JSON);
+
+ await test_screen_content(
+ browser,
+ "renders screen with empty logo element",
+ // Expected selectors:
+ ["main.TEST_LOGO_STEP[pos='center']", ".brand-logo[src='none']"]
+ );
+
+ // Ensure logo has custom height
+ await test_element_styles(
+ browser,
+ ".brand-logo",
+ // Expected styles:
+ {
+ // Override default text-link styles
+ height: TEST_LOGO_HEIGHT,
+ }
+ );
+ browser.closeBrowser();
+});
+
+/**
+ * Test rendering a screen with a "Title Logo"
+ */
+add_task(async function test_title_logo() {
+ const TEST_TITLE_LOGO_URL = "chrome://branding/content/icon64.png";
+ const TEST_CONTENT = makeTestContent("TITLE_LOGO", {
+ title_logo: {
+ imageURL: TEST_TITLE_LOGO_URL,
+ },
+ });
+ const TEST_JSON = JSON.stringify([TEST_CONTENT]);
+ let browser = await openAboutWelcome(TEST_JSON);
+
+ await test_screen_content(
+ browser,
+ "renders screen with title_logo",
+ // Expected selectors:
+ [".inline-icon-container"]
+ );
+
+ browser.closeBrowser();
+});
+
+/**
+ * Test rendering a screen with a title with custom styles.
+ */
+add_task(async function test_aboutwelcome_with_title_styles() {
+ const TEST_TITLE_STYLE_CONTENT = makeTestContent("TEST_TITLE_STYLE_STEP", {
+ title: {
+ fontSize: "36px",
+ fontWeight: 276,
+ letterSpacing: 0,
+ raw: "test",
+ },
+ title_style: "fancy shine",
+ });
+
+ const TEST_TITLE_STYLE_JSON = JSON.stringify([TEST_TITLE_STYLE_CONTENT]);
+ let browser = await openAboutWelcome(TEST_TITLE_STYLE_JSON);
+
+ await test_screen_content(
+ browser,
+ "renders screen with customized title style",
+ // Expected selectors:
+ [`div.welcome-text.fancy.shine`]
+ );
+
+ await test_element_styles(
+ browser,
+ "#mainContentHeader",
+ // Expected styles:
+ {
+ "font-weight": "276",
+ "font-size": "36px",
+ animation: "50s linear 0s infinite normal none running shine",
+ "letter-spacing": "normal",
+ }
+ );
+ browser.closeBrowser();
+});
+
+/**
+ * Test rendering a screen with an image for the dialog window's background
+ */
+add_task(async function test_aboutwelcome_with_background() {
+ const BACKGROUND_URL =
+ "chrome://activity-stream/content/data/content/assets/confetti.svg";
+ const TEST_BACKGROUND_CONTENT = makeTestContent("TEST_BACKGROUND_STEP", {
+ background: `url(${BACKGROUND_URL}) no-repeat center/cover`,
+ });
+
+ const TEST_BACKGROUND_JSON = JSON.stringify([TEST_BACKGROUND_CONTENT]);
+ let browser = await openAboutWelcome(TEST_BACKGROUND_JSON);
+
+ await test_screen_content(
+ browser,
+ "renders screen with dialog background image",
+ // Expected selectors:
+ [`div.main-content[style*='${BACKGROUND_URL}'`]
+ );
+ browser.closeBrowser();
+});
+
+/**
+ * Test rendering a screen with defined dimensions
+ */
+add_task(async function test_aboutwelcome_with_dimensions() {
+ const TEST_DIMENSIONS_CONTENT = makeTestContent("TEST_DIMENSIONS_STEP", {
+ width: "100px",
+ position: "center",
+ });
+
+ const TEST_DIMENSIONS_JSON = JSON.stringify([TEST_DIMENSIONS_CONTENT]);
+ let browser = await openAboutWelcome(TEST_DIMENSIONS_JSON);
+
+ await test_screen_content(
+ browser,
+ "renders screen with defined dimensions",
+ // Expected selectors:
+ [`div.main-content[style*='width: 100px;']`]
+ );
+ browser.closeBrowser();
+});
+
+/**
+ * Test rendering a screen with a dismiss button
+ */
+add_task(async function test_aboutwelcome_dismiss_button() {
+ let browser = await openAboutWelcome(
+ JSON.stringify(
+ // Use 2 screens to test that the message is dismissed, not navigated
+ [1, 2].map(i =>
+ makeTestContent(`TEST_DISMISS_STEP_${i}`, {
+ dismiss_button: { action: { dismiss: true }, size: "small" },
+ })
+ )
+ )
+ );
+
+ await test_screen_content(
+ browser,
+ "renders screen with dismiss button",
+ // Expected selectors:
+ ['div.section-main button.dismiss-button[button-size="small"]']
+ );
+
+ // Click dismiss button
+ await onButtonClick(browser, "button.dismiss-button");
+
+ // Wait for about:home to load
+ await BrowserTestUtils.browserLoaded(browser, false, "about:home");
+ is(browser.currentURI.spec, "about:home", "about:home loaded");
+
+ browser.closeBrowser();
+});
+
+/**
+ * Test rendering a screen with the "split" position
+ */
+add_task(async function test_aboutwelcome_split_position() {
+ const TEST_SPLIT_STEP = makeTestContent("TEST_SPLIT_STEP", {
+ position: "split",
+ hero_text: "hero test",
+ });
+
+ const TEST_SPLIT_JSON = JSON.stringify([TEST_SPLIT_STEP]);
+ let browser = await openAboutWelcome(TEST_SPLIT_JSON);
+
+ await test_screen_content(
+ browser,
+ "renders screen secondary section containing hero text",
+ // Expected selectors:
+ [`main.screen[pos="split"]`, `.section-secondary`, `.message-text h1`]
+ );
+
+ // Ensure secondary section has split template styling
+ await test_element_styles(
+ browser,
+ "main.screen .section-secondary",
+ // Expected styles:
+ {
+ display: "flex",
+ margin: "auto 0px auto auto",
+ }
+ );
+
+ // Ensure secondary action has button styling
+ await test_element_styles(
+ browser,
+ ".action-buttons .secondary-cta .secondary",
+ // Expected styles:
+ {
+ // Override default text-link styles
+ "background-color": "color(srgb 0.0823529 0.0784314 0.101961 / 0.07)",
+ color: "rgb(21, 20, 26)",
+ }
+ );
+ browser.closeBrowser();
+});
+
+/**
+ * Test rendering a screen with a URL value and default color for backdrop
+ */
+add_task(async function test_aboutwelcome_with_url_backdrop() {
+ const TEST_BACKDROP_URL = `url("chrome://activity-stream/content/data/content/assets/confetti.svg")`;
+ const TEST_BACKDROP_VALUE = `#212121 ${TEST_BACKDROP_URL} center/cover no-repeat fixed`;
+ const TEST_URL_BACKDROP_CONTENT = makeTestContent("TEST_URL_BACKDROP_STEP");
+
+ let doExperimentCleanup = await ExperimentFakes.enrollWithFeatureConfig({
+ featureId: "aboutwelcome",
+ value: {
+ enabled: true,
+ backdrop: TEST_BACKDROP_VALUE,
+ screens: [TEST_URL_BACKDROP_CONTENT],
+ },
+ });
+ let browser = await openAboutWelcome();
+
+ await test_screen_content(
+ browser,
+ "renders screen with background image",
+ // Expected selectors:
+ [`div.outer-wrapper.onboardingContainer[style*='${TEST_BACKDROP_URL}']`]
+ );
+ await doExperimentCleanup();
+ browser.closeBrowser();
+});
+
+/**
+ * Test rendering a screen with a color name for backdrop
+ */
+add_task(async function test_aboutwelcome_with_color_backdrop() {
+ const TEST_BACKDROP_COLOR = "transparent";
+ const TEST_BACKDROP_COLOR_CONTENT = makeTestContent(
+ "TEST_COLOR_NAME_BACKDROP_STEP"
+ );
+
+ let doExperimentCleanup = await ExperimentFakes.enrollWithFeatureConfig({
+ featureId: "aboutwelcome",
+ value: {
+ enabled: true,
+ backdrop: TEST_BACKDROP_COLOR,
+ screens: [TEST_BACKDROP_COLOR_CONTENT],
+ },
+ });
+ let browser = await openAboutWelcome();
+
+ await test_screen_content(
+ browser,
+ "renders screen with background color",
+ // Expected selectors:
+ [`div.outer-wrapper.onboardingContainer[style*='${TEST_BACKDROP_COLOR}']`]
+ );
+ await doExperimentCleanup();
+ browser.closeBrowser();
+});
+
+/**
+ * Test rendering a screen with a text color override
+ */
+add_task(async function test_aboutwelcome_with_text_color_override() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ // Override the system color scheme to dark
+ ["ui.systemUsesDarkTheme", 1],
+ ],
+ });
+
+ let screens = [];
+ // we need at least two screens to test the step indicator
+ for (let i = 0; i < 2; i++) {
+ screens.push(
+ makeTestContent("TEST_TEXT_COLOR_OVERRIDE_STEP", {
+ text_color: "dark",
+ background: "white",
+ })
+ );
+ }
+
+ let doExperimentCleanup = await ExperimentFakes.enrollWithFeatureConfig({
+ featureId: "aboutwelcome",
+ value: {
+ enabled: true,
+ screens,
+ },
+ });
+ let browser = await openAboutWelcome(JSON.stringify(screens));
+
+ await test_screen_content(
+ browser,
+ "renders screen with dark text",
+ // Expected selectors:
+ [`main.screen.dark-text`, `.indicator.current`, `.indicator:not(.current)`],
+ // Unexpected selectors:
+ [`main.screen.light-text`]
+ );
+
+ // Ensure title inherits light text color
+ await test_element_styles(
+ browser,
+ "#mainContentHeader",
+ // Expected styles:
+ {
+ color: "rgb(21, 20, 26)",
+ }
+ );
+
+ // Ensure next step indicator inherits light color
+ await test_element_styles(
+ browser,
+ ".indicator:not(.current)",
+ // Expected styles:
+ {
+ color: "rgb(251, 251, 254)",
+ }
+ );
+
+ await doExperimentCleanup();
+ await SpecialPowers.popPrefEnv();
+ browser.closeBrowser();
+});
+
+/**
+ * Test rendering a screen with a "progress bar" style step indicator
+ */
+add_task(async function test_aboutwelcome_with_progress_bar() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["ui.systemUsesDarkTheme", 0],
+ ["ui.prefersReducedMotion", 0],
+ ],
+ });
+ let screens = [];
+ // we need at least three screens to test the progress bar styling
+ for (let i = 0; i < 3; i++) {
+ screens.push(
+ makeTestContent(`TEST_MR_PROGRESS_BAR_${i + 1}`, {
+ position: "split",
+ progress_bar: true,
+ primary_button: {
+ label: "next",
+ action: {
+ navigate: true,
+ },
+ },
+ })
+ );
+ }
+
+ let doExperimentCleanup = await ExperimentFakes.enrollWithFeatureConfig({
+ featureId: "aboutwelcome",
+ value: {
+ enabled: true,
+ screens,
+ },
+ });
+ let browser = await openAboutWelcome(JSON.stringify(screens));
+
+ await SpecialPowers.spawn(browser, [], async () => {
+ const progressBar = await ContentTaskUtils.waitForCondition(() =>
+ content.document.querySelector(".progress-bar")
+ );
+ const indicator = await ContentTaskUtils.waitForCondition(() =>
+ content.document.querySelector(".indicator")
+ );
+ // Progress bar should have a gray background.
+ is(
+ content.window.getComputedStyle(progressBar)["background-color"],
+ "color(srgb 0.0823529 0.0784314 0.101961 / 0.25)",
+ "Correct progress bar background"
+ );
+
+ const indicatorStyles = content.window.getComputedStyle(indicator);
+ for (let [key, val] of Object.entries({
+ // The filled "completed" element should have
+ // `background-color: var(--in-content-primary-button-background);`
+ "background-color": "rgb(0, 97, 224)",
+ // Base progress bar step styles.
+ height: "6px",
+ "margin-inline": "-1px",
+ "padding-block": "0px",
+ })) {
+ is(indicatorStyles[key], val, `Correct indicator ${key} style`);
+ }
+ const indicatorX = indicator.getBoundingClientRect().x;
+ content.document.querySelector("button.primary").click();
+ await ContentTaskUtils.waitForCondition(
+ () =>
+ content.document.querySelector(".indicator")?.getBoundingClientRect()
+ .x > indicatorX,
+ "Indicator should have grown"
+ );
+ });
+
+ await doExperimentCleanup();
+ browser.closeBrowser();
+});
+
+/**
+ * Test rendering a message with session history updates disabled
+ */
+add_task(async function test_aboutwelcome_history_updates_disabled() {
+ let screens = [];
+ // we need at least two screens to test the history state
+ for (let i = 1; i < 3; i++) {
+ screens.push(makeTestContent(`TEST_PUSH_STATE_STEP_${i}`));
+ }
+ let doExperimentCleanup = await ExperimentFakes.enrollWithFeatureConfig({
+ featureId: "aboutwelcome",
+ value: {
+ enabled: true,
+ disableHistoryUpdates: true,
+ screens,
+ },
+ });
+ let browser = await openAboutWelcome(JSON.stringify(screens));
+
+ let startHistoryLength = await SpecialPowers.spawn(browser, [], () => {
+ return content.window.history.length;
+ });
+ // Advance to second screen
+ await onButtonClick(browser, "button.primary");
+ let endHistoryLength = await SpecialPowers.spawn(browser, [], async () => {
+ // Ensure next screen has rendered
+ await ContentTaskUtils.waitForCondition(() =>
+ content.document.querySelector(".TEST_PUSH_STATE_STEP_2")
+ );
+ return content.window.history.length;
+ });
+
+ Assert.strictEqual(
+ startHistoryLength,
+ endHistoryLength,
+ "No entries added to the session's history stack with history updates disabled"
+ );
+
+ await doExperimentCleanup();
+ browser.closeBrowser();
+});
+
+/**
+ * Test rendering a screen with different logos depending on reduced motion and
+ * color scheme preferences
+ */
+add_task(async function test_aboutwelcome_logo_selection() {
+ // Test a screen config that includes every logo parameter
+ await testAboutWelcomeLogoFor({
+ imageURL: "chrome://branding/content/icon16.png",
+ darkModeImageURL: "chrome://branding/content/icon32.png",
+ reducedMotionImageURL: "chrome://branding/content/icon64.png",
+ darkModeReducedMotionImageURL: "chrome://branding/content/icon128.png",
+ alt: "TEST_LOGO_SELECTION_ALT",
+ width: "16px",
+ height: "16px",
+ marginBlock: "0px",
+ marginInline: "0px",
+ });
+ // Test a screen config with no animated/static logos
+ await testAboutWelcomeLogoFor({
+ imageURL: "chrome://branding/content/icon16.png",
+ darkModeImageURL: "chrome://branding/content/icon32.png",
+ });
+ // Test a screen config with no dark mode logos
+ await testAboutWelcomeLogoFor({
+ imageURL: "chrome://branding/content/icon16.png",
+ reducedMotionImageURL: "chrome://branding/content/icon64.png",
+ });
+ // Test a screen config that includes only the default logo
+ await testAboutWelcomeLogoFor({
+ imageURL: "chrome://branding/content/icon16.png",
+ });
+ // Test a screen config with no logos
+ await testAboutWelcomeLogoFor();
+});
+
+/**
+ * Test rendering a message that starts on a specific screen
+ */
+add_task(async function test_aboutwelcome_start_screen_configured() {
+ let startScreen = 1;
+ let screens = [];
+ // we need at least two screens to test
+ for (let i = 1; i < 3; i++) {
+ screens.push(makeTestContent(`TEST_START_STEP_${i}`));
+ }
+ let doExperimentCleanup = await ExperimentFakes.enrollWithFeatureConfig({
+ featureId: "aboutwelcome",
+ value: {
+ enabled: true,
+ startScreen,
+ screens,
+ },
+ });
+
+ let sandbox = sinon.createSandbox();
+ let spy = sandbox.spy(AboutWelcomeTelemetry.prototype, "sendTelemetry");
+
+ let browser = await openAboutWelcome(JSON.stringify(screens));
+
+ let secondScreenShown = await SpecialPowers.spawn(browser, [], async () => {
+ // Ensure screen has rendered
+ await ContentTaskUtils.waitForCondition(() =>
+ content.document.querySelector(".TEST_START_STEP_2")
+ );
+ return true;
+ });
+
+ ok(
+ secondScreenShown,
+ `Starts on second screen when configured with startScreen index equal to ${startScreen}`
+ );
+ // Wait for screen elements to render before checking impression pings
+ await test_screen_content(
+ browser,
+ "renders second screen elements",
+ // Expected selectors:
+ [`main.screen`, "div.secondary-cta"]
+ );
+
+ let expectedTelemetry = sinon.match({
+ event: "IMPRESSION",
+ message_id: `MR_WELCOME_DEFAULT_${startScreen}_TEST_START_STEP_${
+ startScreen + 1
+ }_${screens.map(({ id }) => id?.split("_")[1]?.[0]).join("")}`,
+ });
+ if (spy.calledWith(expectedTelemetry)) {
+ ok(
+ true,
+ "Impression events have the correct message id with start screen configured"
+ );
+ } else if (spy.called) {
+ ok(
+ false,
+ `Wrong telemetry sent: ${JSON.stringify(
+ spy.getCalls().map(c => c.args[0]),
+ null,
+ 2
+ )}`
+ );
+ } else {
+ ok(false, "No telemetry sent");
+ }
+
+ await doExperimentCleanup();
+ browser.closeBrowser();
+ sandbox.restore();
+});
diff --git a/browser/components/aboutwelcome/tests/browser/browser_aboutwelcome_fxa_signin_flow.js b/browser/components/aboutwelcome/tests/browser/browser_aboutwelcome_fxa_signin_flow.js
new file mode 100644
index 0000000000..9de9acb7b3
--- /dev/null
+++ b/browser/components/aboutwelcome/tests/browser/browser_aboutwelcome_fxa_signin_flow.js
@@ -0,0 +1,303 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const { UIState } = ChromeUtils.importESModule(
+ "resource://services-sync/UIState.sys.mjs"
+);
+
+const TEST_ROOT = "https://example.com/";
+
+add_setup(async () => {
+ await SpecialPowers.pushPrefEnv({
+ set: [["identity.fxaccounts.remote.root", TEST_ROOT]],
+ });
+});
+
+/**
+ * Tests that the FXA_SIGNIN_FLOW special action resolves to `true` and
+ * closes the FxA sign-in tab if sign-in is successful.
+ */
+add_task(async function test_fxa_sign_success() {
+ let sandbox = sinon.createSandbox();
+ registerCleanupFunction(() => {
+ sandbox.restore();
+ });
+
+ await BrowserTestUtils.withNewTab("about:welcome", async browser => {
+ let fxaTabPromise = BrowserTestUtils.waitForNewTab(gBrowser);
+ let resultPromise = SpecialPowers.spawn(browser, [], async () => {
+ return content.wrappedJSObject.AWSendToParent("SPECIAL_ACTION", {
+ type: "FXA_SIGNIN_FLOW",
+ });
+ });
+ let fxaTab = await fxaTabPromise;
+ let fxaTabClosing = BrowserTestUtils.waitForTabClosing(fxaTab);
+
+ // We'll fake-out the UIState being in the STATUS_SIGNED_IN status
+ // and not test the actual FxA sign-in mechanism.
+ sandbox.stub(UIState, "get").returns({
+ status: UIState.STATUS_SIGNED_IN,
+ syncEnabled: true,
+ email: "email@example.com",
+ });
+
+ Services.obs.notifyObservers(null, UIState.ON_UPDATE);
+
+ await fxaTabClosing;
+ Assert.ok(true, "FxA tab automatically closed.");
+ let result = await resultPromise;
+ Assert.ok(result, "FXA_SIGNIN_FLOW action's result should be true");
+ });
+
+ sandbox.restore();
+});
+
+/**
+ * Tests that the FXA_SIGNIN_FLOW action's data.autoClose parameter can
+ * disable the autoclose behavior.
+ */
+add_task(async function test_fxa_sign_success_no_autoclose() {
+ let sandbox = sinon.createSandbox();
+ registerCleanupFunction(() => {
+ sandbox.restore();
+ });
+
+ await BrowserTestUtils.withNewTab("about:welcome", async browser => {
+ let fxaTabPromise = BrowserTestUtils.waitForNewTab(gBrowser);
+ let resultPromise = SpecialPowers.spawn(browser, [], async () => {
+ return content.wrappedJSObject.AWSendToParent("SPECIAL_ACTION", {
+ type: "FXA_SIGNIN_FLOW",
+ data: { autoClose: false },
+ });
+ });
+ let fxaTab = await fxaTabPromise;
+
+ // We'll fake-out the UIState being in the STATUS_SIGNED_IN status
+ // and not test the actual FxA sign-in mechanism.
+ sandbox.stub(UIState, "get").returns({
+ status: UIState.STATUS_SIGNED_IN,
+ syncEnabled: true,
+ email: "email@example.com",
+ });
+
+ Services.obs.notifyObservers(null, UIState.ON_UPDATE);
+
+ let result = await resultPromise;
+ Assert.ok(result, "FXA_SIGNIN_FLOW should have resolved to true");
+ Assert.ok(!fxaTab.closing, "FxA tab was not asked to close.");
+ BrowserTestUtils.removeTab(fxaTab);
+ });
+
+ sandbox.restore();
+});
+
+/**
+ * Tests that the FXA_SIGNIN_FLOW action resolves to `false` if the tab
+ * closes before sign-in completes.
+ */
+add_task(async function test_fxa_signin_aborted() {
+ await BrowserTestUtils.withNewTab("about:welcome", async browser => {
+ let fxaTabPromise = BrowserTestUtils.waitForNewTab(gBrowser);
+ let resultPromise = SpecialPowers.spawn(browser, [], async () => {
+ return content.wrappedJSObject.AWSendToParent("SPECIAL_ACTION", {
+ type: "FXA_SIGNIN_FLOW",
+ });
+ });
+ let fxaTab = await fxaTabPromise;
+ Assert.ok(!fxaTab.closing, "FxA tab was not asked to close yet.");
+
+ BrowserTestUtils.removeTab(fxaTab);
+ let result = await resultPromise;
+ Assert.ok(!result, "FXA_SIGNIN_FLOW action's result should be false");
+ });
+});
+
+/**
+ * Tests that the FXA_SIGNIN_FLOW action can open a separate window, if need
+ * be, and that if that window closes, the flow is considered aborted.
+ */
+add_task(async function test_fxa_signin_window_aborted() {
+ await BrowserTestUtils.withNewTab("about:welcome", async browser => {
+ let fxaWindowPromise = BrowserTestUtils.waitForNewWindow();
+ let resultPromise = SpecialPowers.spawn(browser, [], async () => {
+ return content.wrappedJSObject.AWSendToParent("SPECIAL_ACTION", {
+ type: "FXA_SIGNIN_FLOW",
+ data: {
+ where: "window",
+ },
+ });
+ });
+ let fxaWindow = await fxaWindowPromise;
+ Assert.ok(!fxaWindow.closed, "FxA window was not asked to close yet.");
+
+ await BrowserTestUtils.closeWindow(fxaWindow);
+ let result = await resultPromise;
+ Assert.ok(!result, "FXA_SIGNIN_FLOW action's result should be false");
+ });
+});
+
+/**
+ * Tests that the FXA_SIGNIN_FLOW action can open a separate window, if need
+ * be, and that if sign-in completes, that new window will close automatically.
+ */
+add_task(async function test_fxa_signin_window_success() {
+ let sandbox = sinon.createSandbox();
+ registerCleanupFunction(() => {
+ sandbox.restore();
+ });
+
+ await BrowserTestUtils.withNewTab("about:welcome", async browser => {
+ let fxaWindowPromise = BrowserTestUtils.waitForNewWindow();
+ let resultPromise = SpecialPowers.spawn(browser, [], async () => {
+ return content.wrappedJSObject.AWSendToParent("SPECIAL_ACTION", {
+ type: "FXA_SIGNIN_FLOW",
+ data: {
+ where: "window",
+ },
+ });
+ });
+ let fxaWindow = await fxaWindowPromise;
+ Assert.ok(!fxaWindow.closed, "FxA window was not asked to close yet.");
+
+ let windowClosed = BrowserTestUtils.windowClosed(fxaWindow);
+
+ // We'll fake-out the UIState being in the STATUS_SIGNED_IN status
+ // and not test the actual FxA sign-in mechanism.
+ sandbox.stub(UIState, "get").returns({
+ status: UIState.STATUS_SIGNED_IN,
+ syncEnabled: true,
+ email: "email@example.com",
+ });
+
+ Services.obs.notifyObservers(null, UIState.ON_UPDATE);
+
+ let result = await resultPromise;
+ Assert.ok(result, "FXA_SIGNIN_FLOW action's result should be true");
+
+ await windowClosed;
+ Assert.ok(fxaWindow.closed, "Sign-in window was automatically closed.");
+ });
+
+ sandbox.restore();
+});
+
+/**
+ * Tests that the FXA_SIGNIN_FLOW action can open a separate window, if need
+ * be, and that if a new tab is opened in that window and the sign-in tab
+ * is closed:
+ *
+ * 1. The new window isn't closed
+ * 2. The sign-in is considered aborted.
+ */
+add_task(async function test_fxa_signin_window_multiple_tabs_aborted() {
+ await BrowserTestUtils.withNewTab("about:welcome", async browser => {
+ let fxaWindowPromise = BrowserTestUtils.waitForNewWindow();
+ let resultPromise = SpecialPowers.spawn(browser, [], async () => {
+ return content.wrappedJSObject.AWSendToParent("SPECIAL_ACTION", {
+ type: "FXA_SIGNIN_FLOW",
+ data: {
+ where: "window",
+ },
+ });
+ });
+ let fxaWindow = await fxaWindowPromise;
+ Assert.ok(!fxaWindow.closed, "FxA window was not asked to close yet.");
+ let fxaTab = fxaWindow.gBrowser.selectedTab;
+ await BrowserTestUtils.openNewForegroundTab(
+ fxaWindow.gBrowser,
+ "about:blank"
+ );
+ BrowserTestUtils.removeTab(fxaTab);
+
+ let result = await resultPromise;
+ Assert.ok(!result, "FXA_SIGNIN_FLOW action's result should be false");
+ Assert.ok(!fxaWindow.closed, "FxA window was not asked to close.");
+ await BrowserTestUtils.closeWindow(fxaWindow);
+ });
+});
+
+/**
+ * Tests that the FXA_SIGNIN_FLOW action can open a separate window, if need
+ * be, and that if a new tab is opened in that window but then sign-in
+ * completes
+ *
+ * 1. The new window isn't closed, but the sign-in tab is.
+ * 2. The sign-in is considered a success.
+ */
+add_task(async function test_fxa_signin_window_multiple_tabs_success() {
+ let sandbox = sinon.createSandbox();
+ registerCleanupFunction(() => {
+ sandbox.restore();
+ });
+
+ await BrowserTestUtils.withNewTab("about:welcome", async browser => {
+ let fxaWindowPromise = BrowserTestUtils.waitForNewWindow();
+ let resultPromise = SpecialPowers.spawn(browser, [], async () => {
+ return content.wrappedJSObject.AWSendToParent("SPECIAL_ACTION", {
+ type: "FXA_SIGNIN_FLOW",
+ data: {
+ where: "window",
+ },
+ });
+ });
+ let fxaWindow = await fxaWindowPromise;
+ Assert.ok(!fxaWindow.closed, "FxA window was not asked to close yet.");
+ let fxaTab = fxaWindow.gBrowser.selectedTab;
+
+ // This will open an about:blank tab in the background.
+ await BrowserTestUtils.addTab(fxaWindow.gBrowser);
+ let fxaTabClosed = BrowserTestUtils.waitForTabClosing(fxaTab);
+
+ // We'll fake-out the UIState being in the STATUS_SIGNED_IN status
+ // and not test the actual FxA sign-in mechanism.
+ sandbox.stub(UIState, "get").returns({
+ status: UIState.STATUS_SIGNED_IN,
+ syncEnabled: true,
+ email: "email@example.com",
+ });
+
+ Services.obs.notifyObservers(null, UIState.ON_UPDATE);
+
+ let result = await resultPromise;
+ Assert.ok(result, "FXA_SIGNIN_FLOW action's result should be true");
+ await fxaTabClosed;
+
+ Assert.ok(!fxaWindow.closed, "FxA window was not asked to close.");
+ await BrowserTestUtils.closeWindow(fxaWindow);
+ });
+
+ sandbox.restore();
+});
+
+/**
+ * Tests that we can pass an entrypoint and UTM parameters to the FxA sign-in
+ * page.
+ */
+add_task(async function test_fxa_signin_flow_entrypoint_utm_params() {
+ await BrowserTestUtils.withNewTab("about:welcome", async browser => {
+ let fxaTabPromise = BrowserTestUtils.waitForNewTab(gBrowser);
+ let resultPromise = SpecialPowers.spawn(browser, [], async () => {
+ return content.wrappedJSObject.AWSendToParent("SPECIAL_ACTION", {
+ type: "FXA_SIGNIN_FLOW",
+ data: {
+ entrypoint: "test-entrypoint",
+ extraParams: {
+ utm_test1: "utm_test1",
+ utm_test2: "utm_test2",
+ },
+ },
+ });
+ });
+ let fxaTab = await fxaTabPromise;
+
+ let uriParams = new URLSearchParams(fxaTab.linkedBrowser.currentURI.query);
+ Assert.equal(uriParams.get("entrypoint"), "test-entrypoint");
+ Assert.equal(uriParams.get("utm_test1"), "utm_test1");
+ Assert.equal(uriParams.get("utm_test2"), "utm_test2");
+
+ BrowserTestUtils.removeTab(fxaTab);
+ await resultPromise;
+ });
+});
diff --git a/browser/components/aboutwelcome/tests/browser/browser_aboutwelcome_glean.js b/browser/components/aboutwelcome/tests/browser/browser_aboutwelcome_glean.js
new file mode 100644
index 0000000000..d08a3c834c
--- /dev/null
+++ b/browser/components/aboutwelcome/tests/browser/browser_aboutwelcome_glean.js
@@ -0,0 +1,162 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Tests for the Glean version of onboarding telemetry.
+ */
+
+const { AboutWelcomeTelemetry } = ChromeUtils.importESModule(
+ "resource:///modules/aboutwelcome/AboutWelcomeTelemetry.sys.mjs"
+);
+
+const TEST_DEFAULT_CONTENT = [
+ {
+ id: "AW_STEP1",
+
+ content: {
+ position: "split",
+ title: "Step 1",
+ page: "page 1",
+ source: "test",
+ primary_button: {
+ label: "Next",
+ action: {
+ navigate: true,
+ },
+ },
+ secondary_button: {
+ label: "link",
+ },
+ secondary_button_top: {
+ label: "link top",
+ action: {
+ type: "SHOW_FIREFOX_ACCOUNTS",
+ data: { entrypoint: "test" },
+ },
+ },
+ },
+ },
+ {
+ id: "AW_STEP2",
+ content: {
+ position: "center",
+ title: "Step 2",
+ page: "page 1",
+ source: "test",
+ primary_button: {
+ label: "Next",
+ action: {
+ navigate: true,
+ },
+ },
+ secondary_button: {
+ label: "link",
+ },
+ has_noodles: true,
+ },
+ },
+];
+
+const TEST_DEFAULT_JSON = JSON.stringify(TEST_DEFAULT_CONTENT);
+
+async function openAboutWelcome() {
+ await setAboutWelcomePref(true);
+ await setAboutWelcomeMultiStage(TEST_DEFAULT_JSON);
+
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "about:welcome",
+ true
+ );
+ registerCleanupFunction(() => {
+ BrowserTestUtils.removeTab(tab);
+ });
+ return tab.linkedBrowser;
+}
+
+add_task(async function test_welcome_telemetry() {
+ // Have to turn on AS telemetry for anything to be recorded.
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.newtabpage.activity-stream.telemetry", true]],
+ });
+ registerCleanupFunction(async () => {
+ await SpecialPowers.popPrefEnv();
+ });
+
+ Services.fog.testResetFOG();
+ // Let's check that there is nothing in the impression event.
+ // This is useful in mochitests because glean inits fairly late in startup.
+ // We want to make sure we are fully initialized during testing so that
+ // when we call testGetValue() we get predictable behavior.
+ Assert.equal(undefined, Glean.messagingSystem.messageId.testGetValue());
+
+ // Setup testBeforeNextSubmit. We do this first, progress onboarding, submit
+ // and then check submission. We put the asserts inside testBeforeNextSubmit
+ // because metric lifetimes are 'ping' and are cleared after submission.
+ // See: https://firefox-source-docs.mozilla.org/toolkit/components/glean/user/instrumentation_tests.html#xpcshell-tests
+ let pingSubmitted = false;
+ GleanPings.messagingSystem.testBeforeNextSubmit(() => {
+ pingSubmitted = true;
+
+ const message = Glean.messagingSystem.messageId.testGetValue();
+ // Because of the asynchronous nature of receiving messages, we cannot
+ // guarantee that we will get the same message first. Instead we check
+ // that the one we get is a valid example of that type.
+ Assert.ok(
+ message.startsWith("MR_WELCOME_DEFAULT"),
+ "Ping is of an expected type"
+ );
+ Assert.equal(
+ Glean.messagingSystem.unknownKeyCount.testGetValue(),
+ undefined
+ );
+ });
+
+ let browser = await openAboutWelcome();
+ // `openAboutWelcome` isn't synchronous wrt the onboarding flow impressing.
+ await TestUtils.waitForCondition(
+ () => pingSubmitted,
+ "Ping was submitted, callback was called."
+ );
+
+ // Let's reset and assert some values in the next button click.
+ pingSubmitted = false;
+ GleanPings.messagingSystem.testBeforeNextSubmit(() => {
+ pingSubmitted = true;
+
+ // Sometimes the impression for MR_WELCOME_DEFAULT_0_AW_STEP1_SS reaches
+ // the parent process before the button click does.
+ // See https://bugzilla.mozilla.org/show_bug.cgi?id=1834620
+ if (Glean.messagingSystem.event.testGetValue() === "IMPRESSION") {
+ Assert.equal(
+ Glean.messagingSystem.eventPage.testGetValue(),
+ "about:welcome"
+ );
+ const message = Glean.messagingSystem.messageId.testGetValue();
+ Assert.ok(
+ message.startsWith("MR_WELCOME_DEFAULT"),
+ "Ping is of an expected type"
+ );
+ } else {
+ // This is the common and, to my mind, correct case:
+ // the click coming before the next steps' impression.
+ Assert.equal(Glean.messagingSystem.event.testGetValue(), "CLICK_BUTTON");
+ Assert.equal(
+ Glean.messagingSystem.eventSource.testGetValue(),
+ "primary_button"
+ );
+ Assert.equal(
+ Glean.messagingSystem.messageId.testGetValue(),
+ "MR_WELCOME_DEFAULT_0_AW_STEP1"
+ );
+ }
+ Assert.equal(
+ Glean.messagingSystem.unknownKeyCount.testGetValue(),
+ undefined
+ );
+ });
+ await onButtonClick(browser, "button.primary");
+ Assert.ok(pingSubmitted, "Ping was submitted, callback was called.");
+});
diff --git a/browser/components/aboutwelcome/tests/browser/browser_aboutwelcome_import.js b/browser/components/aboutwelcome/tests/browser/browser_aboutwelcome_import.js
new file mode 100644
index 0000000000..f2ec85001a
--- /dev/null
+++ b/browser/components/aboutwelcome/tests/browser/browser_aboutwelcome_import.js
@@ -0,0 +1,94 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+const IMPORT_SCREEN = {
+ id: "AW_IMPORT",
+ content: {
+ primary_button: {
+ label: "import",
+ action: {
+ navigate: true,
+ type: "SHOW_MIGRATION_WIZARD",
+ },
+ },
+ },
+};
+
+add_task(async function test_wait_import_modal() {
+ await setAboutWelcomeMultiStage(
+ JSON.stringify([IMPORT_SCREEN, { id: "AW_NEXT", content: {} }])
+ );
+ const { cleanup, browser } = await openMRAboutWelcome();
+
+ // execution
+ await test_screen_content(
+ browser,
+ "renders IMPORT screen",
+ //Expected selectors
+ ["main.AW_IMPORT", "button[value='primary_button']"],
+
+ //Unexpected selectors:
+ ["main.AW_NEXT"]
+ );
+
+ const wizardPromise = BrowserTestUtils.waitForMigrationWizard(window);
+ const prefsTab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "about:preferences"
+ );
+ await onButtonClick(browser, "button.primary");
+ const wizard = await wizardPromise;
+
+ await test_screen_content(
+ browser,
+ "still shows IMPORT screen",
+ //Expected selectors
+ ["main.AW_IMPORT", "button[value='primary_button']"],
+
+ //Unexpected selectors:
+ ["main.AW_NEXT"]
+ );
+
+ await BrowserTestUtils.removeTab(wizard);
+
+ await test_screen_content(
+ browser,
+ "moved to NEXT screen",
+ //Expected selectors
+ ["main.AW_NEXT"],
+
+ //Unexpected selectors:
+ []
+ );
+
+ // cleanup
+ await SpecialPowers.popPrefEnv(); // for setAboutWelcomeMultiStage
+ BrowserTestUtils.removeTab(prefsTab);
+ await cleanup();
+});
+
+add_task(async function test_wait_import_spotlight() {
+ const spotlightPromise = TestUtils.topicObserved("subdialog-loaded");
+ ChromeUtils.importESModule(
+ "resource:///modules/asrouter/Spotlight.sys.mjs"
+ ).Spotlight.showSpotlightDialog(gBrowser.selectedBrowser, {
+ content: { modal: "tab", screens: [IMPORT_SCREEN] },
+ });
+ const [win] = await spotlightPromise;
+
+ const wizardPromise = BrowserTestUtils.waitForMigrationWizard(window);
+ const prefsTab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "about:preferences"
+ );
+ win.document
+ .querySelector(".onboardingContainer button[value='primary_button']")
+ .click();
+ const wizard = await wizardPromise;
+
+ await BrowserTestUtils.removeTab(wizard);
+
+ // cleanup
+ BrowserTestUtils.removeTab(prefsTab);
+});
diff --git a/browser/components/aboutwelcome/tests/browser/browser_aboutwelcome_mobile_downloads.js b/browser/components/aboutwelcome/tests/browser/browser_aboutwelcome_mobile_downloads.js
new file mode 100644
index 0000000000..bb94d575fe
--- /dev/null
+++ b/browser/components/aboutwelcome/tests/browser/browser_aboutwelcome_mobile_downloads.js
@@ -0,0 +1,112 @@
+"use strict";
+
+const BASE_CONTENT = {
+ id: "MOBILE_DOWNLOADS",
+ content: {
+ tiles: {
+ type: "mobile_downloads",
+ data: {
+ QR_code: {
+ image_url: "chrome://browser/content/assets/focus-qr-code.svg",
+ alt_text: "Test alt",
+ },
+ email: {
+ link_text: {
+ string_id: "spotlight-focus-promo-email-link",
+ },
+ },
+ marketplace_buttons: ["ios", "android"],
+ },
+ },
+ },
+};
+
+async function openAboutWelcome(json) {
+ if (json) {
+ await setAboutWelcomeMultiStage(json);
+ }
+
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "about:welcome",
+ true
+ );
+ registerCleanupFunction(() => {
+ BrowserTestUtils.removeTab(tab);
+ });
+ return tab.linkedBrowser;
+}
+
+const ALT_TEXT = BASE_CONTENT.content.tiles.data.QR_code.alt_text;
+
+/**
+ * Test rendering a screen with a mobile downloads tile
+ * including QR code, email, and marketplace elements
+ */
+add_task(async function test_aboutwelcome_mobile_downloads_all() {
+ const TEST_JSON = JSON.stringify([BASE_CONTENT]);
+ let browser = await openAboutWelcome(TEST_JSON);
+
+ await test_screen_content(
+ browser,
+ "renders screen with all mobile download elements",
+ // Expected selectors:
+ [
+ `img.qr-code-image[alt="${ALT_TEXT}"]`,
+ "ul.mobile-download-buttons",
+ "li.android",
+ "li.ios",
+ "button.email-link",
+ ]
+ );
+});
+
+/**
+ * Test rendering a screen with a mobile downloads tile
+ * including only a QR code and marketplace elements
+ */
+add_task(
+ async function test_aboutwelcome_mobile_downloads_qr_and_marketplace() {
+ const SCREEN_CONTENT = structuredClone(BASE_CONTENT);
+ delete SCREEN_CONTENT.content.tiles.data.email;
+ const TEST_JSON = JSON.stringify([SCREEN_CONTENT]);
+ let browser = await openAboutWelcome(TEST_JSON);
+
+ await test_screen_content(
+ browser,
+ "renders screen with QR code and marketplace badges",
+ // Expected selectors:
+ [
+ `img.qr-code-image[alt="${ALT_TEXT}"]`,
+ "ul.mobile-download-buttons",
+ "li.android",
+ "li.ios",
+ ],
+ // Unexpected selectors:
+ [`button.email-link`]
+ );
+ }
+);
+
+/**
+ * Test rendering a screen with a mobile downloads tile
+ * including only a QR code
+ */
+add_task(async function test_aboutwelcome_mobile_downloads_qr() {
+ let SCREEN_CONTENT = structuredClone(BASE_CONTENT);
+ const QR_CODE_SRC = SCREEN_CONTENT.content.tiles.data.QR_code.image_url;
+
+ delete SCREEN_CONTENT.content.tiles.data.email;
+ delete SCREEN_CONTENT.content.tiles.data.marketplace_buttons;
+ const TEST_JSON = JSON.stringify([SCREEN_CONTENT]);
+ let browser = await openAboutWelcome(TEST_JSON);
+
+ await test_screen_content(
+ browser,
+ "renders screen with QR code",
+ // Expected selectors:
+ [`img.qr-code-image[alt="${ALT_TEXT}"][src="${QR_CODE_SRC}"]`],
+ // Unexpected selectors:
+ ["button.email-link", "li.android", "li.ios"]
+ );
+});
diff --git a/browser/components/aboutwelcome/tests/browser/browser_aboutwelcome_multistage_addonspicker.js b/browser/components/aboutwelcome/tests/browser/browser_aboutwelcome_multistage_addonspicker.js
new file mode 100644
index 0000000000..71c72f6d1d
--- /dev/null
+++ b/browser/components/aboutwelcome/tests/browser/browser_aboutwelcome_multistage_addonspicker.js
@@ -0,0 +1,178 @@
+"use strict";
+
+const { AboutWelcomeParent } = ChromeUtils.importESModule(
+ "resource:///actors/AboutWelcomeParent.sys.mjs"
+);
+
+const { AboutWelcomeTelemetry } = ChromeUtils.importESModule(
+ "resource:///modules/aboutwelcome/AboutWelcomeTelemetry.sys.mjs"
+);
+const { AWScreenUtils } = ChromeUtils.importESModule(
+ "resource:///modules/aboutwelcome/AWScreenUtils.sys.mjs"
+);
+const { InternalTestingProfileMigrator } = ChromeUtils.importESModule(
+ "resource:///modules/InternalTestingProfileMigrator.sys.mjs"
+);
+
+async function clickVisibleButton(browser, selector) {
+ // eslint-disable-next-line no-shadow
+ await ContentTask.spawn(browser, { selector }, async ({ selector }) => {
+ function getVisibleElement() {
+ for (const el of content.document.querySelectorAll(selector)) {
+ if (el.offsetParent !== null) {
+ return el;
+ }
+ }
+ return null;
+ }
+ await ContentTaskUtils.waitForCondition(
+ getVisibleElement,
+ selector,
+ 200, // interval
+ 100 // maxTries
+ );
+ getVisibleElement().click();
+ });
+}
+
+add_setup(async function () {
+ SpecialPowers.pushPrefEnv({
+ set: [
+ ["ui.prefersReducedMotion", 1],
+ ["browser.aboutwelcome.transitions", false],
+ ],
+ });
+});
+
+add_task(async function test_aboutwelcome_addonspicker() {
+ const TEST_ADDON_CONTENT = [
+ {
+ id: "AW_ADDONS_PICKER",
+ content: {
+ position: "center",
+ tiles: {
+ type: "addons-picker",
+ data: [
+ {
+ id: "addon-one-id",
+ name: "uBlock Origin",
+ install_label: "Add to Firefox",
+ icon: "",
+ type: "extension",
+ description: "An efficient wide-spectrum content blocker.",
+ source_id: "ADD_EXTENSION_BUTTON",
+ action: {
+ type: "INSTALL_ADDON_FROM_URL",
+ data: {
+ url: "https://test.xpi",
+ telemetrySource: "aboutwelcome-addon",
+ },
+ },
+ },
+ {
+ id: "addon-two-id",
+ name: "Tree-Style Tabs",
+ install_label: "Add to Firefox",
+ icon: "",
+ type: "extension",
+ description: "Show tabs like a tree.",
+ source_id: "ADD_EXTENSION_BUTTON",
+ action: {
+ type: "INSTALL_ADDON_FROM_URL",
+ data: {
+ url: "https://test.xpi",
+ telemetrySource: "aboutwelcome-addon",
+ },
+ },
+ },
+ ],
+ },
+ progress_bar: true,
+ logo: {},
+ title: {
+ raw: "Customize your Firefox",
+ },
+ subtitle: {
+ raw: "Extensions and themes are like apps for your browser, and they let you protect passwords, download videos, find deals, block annoying ads, change how your browser looks, and much more.",
+ },
+ additional_button: {
+ label: {
+ raw: "Explore more add-ons",
+ },
+ style: "link",
+ action: {
+ type: "OPEN_URL",
+ data: {
+ args: "https://test.xpi",
+ where: "tab",
+ },
+ },
+ },
+ secondary_button: {
+ label: {
+ string_id: "mr2-onboarding-start-browsing-button-label",
+ },
+ action: {
+ navigate: true,
+ },
+ },
+ },
+ },
+ ];
+
+ await setAboutWelcomeMultiStage(JSON.stringify(TEST_ADDON_CONTENT)); // NB: calls SpecialPowers.pushPrefEnv
+ let { cleanup, browser } = await openMRAboutWelcome();
+ let aboutWelcomeActor = await getAboutWelcomeParent(browser);
+ const messageSandbox = sinon.createSandbox();
+ registerCleanupFunction(() => {
+ messageSandbox.restore();
+ });
+ // Stub AboutWelcomeParent's Content Message Handler
+ const messageStub = messageSandbox
+ .stub(aboutWelcomeActor, "onContentMessage")
+ .withArgs("AWPage:SPECIAL_ACTION");
+
+ // execution
+ await test_screen_content(
+ browser,
+ "renders the addons-picker screen and tiles",
+ //Expected selectors
+ [
+ "main.AW_ADDONS_PICKER",
+ "div.addons-picker-container",
+ "button[value='secondary_button']",
+ "button[value='additional_button']",
+ ],
+
+ //Unexpected selectors:
+ [
+ `main.screen[pos="split"]`,
+ "main.AW_SET_DEFAULT",
+ "button[value='primary_button']",
+ ]
+ );
+
+ await clickVisibleButton(browser, ".addon-container button[value='0']"); //click the first install button
+
+ const installExtensionCall = messageStub.getCall(0);
+ info(
+ `Call #${installExtensionCall}: ${
+ installExtensionCall.args[0]
+ } ${JSON.stringify(installExtensionCall.args[1])}`
+ );
+ Assert.equal(
+ installExtensionCall.args[0],
+ "AWPage:SPECIAL_ACTION",
+ "send special action to install add on"
+ );
+ Assert.equal(
+ installExtensionCall.args[1].type,
+ "INSTALL_ADDON_FROM_URL",
+ "Special action type is INSTALL_ADDON_FROM_URL"
+ );
+
+ // cleanup
+ await SpecialPowers.popPrefEnv(); // for setAboutWelcomeMultiStage
+ await cleanup();
+ messageSandbox.restore();
+});
diff --git a/browser/components/aboutwelcome/tests/browser/browser_aboutwelcome_multistage_default.js b/browser/components/aboutwelcome/tests/browser/browser_aboutwelcome_multistage_default.js
new file mode 100644
index 0000000000..d234ff7d06
--- /dev/null
+++ b/browser/components/aboutwelcome/tests/browser/browser_aboutwelcome_multistage_default.js
@@ -0,0 +1,794 @@
+"use strict";
+const { SpecialMessageActions } = ChromeUtils.importESModule(
+ "resource://messaging-system/lib/SpecialMessageActions.sys.mjs"
+);
+
+const DID_SEE_ABOUT_WELCOME_PREF = "trailhead.firstrun.didSeeAboutWelcome";
+
+const TEST_DEFAULT_CONTENT = [
+ {
+ id: "AW_STEP1",
+ content: {
+ position: "split",
+ title: "Step 1",
+ primary_button: {
+ label: "Next",
+ action: {
+ navigate: true,
+ },
+ },
+ secondary_button: {
+ label: "link",
+ },
+ secondary_button_top: {
+ label: "link top",
+ action: {
+ type: "SHOW_FIREFOX_ACCOUNTS",
+ data: { entrypoint: "test" },
+ },
+ },
+ info_text: {
+ raw: "Here's some sample help text",
+ },
+ },
+ },
+ {
+ id: "AW_STEP2",
+ content: {
+ position: "center",
+ title: "Step 2",
+ primary_button: {
+ label: "Next",
+ action: {
+ navigate: true,
+ },
+ },
+ secondary_button: {
+ label: "link",
+ },
+ has_noodles: true,
+ },
+ },
+ {
+ id: "AW_STEP3",
+ content: {
+ title: "Step 3",
+ tiles: {
+ type: "theme",
+ action: {
+ theme: "<event>",
+ },
+ data: [
+ {
+ theme: "automatic",
+ label: "theme-1",
+ tooltip: "test-tooltip",
+ },
+ {
+ theme: "dark",
+ label: "theme-2",
+ },
+ ],
+ },
+ primary_button: {
+ label: "Next",
+ action: {
+ navigate: true,
+ },
+ },
+ secondary_button: {
+ label: "Import",
+ action: {
+ type: "SHOW_MIGRATION_WIZARD",
+ data: { source: "chrome" },
+ },
+ },
+ },
+ },
+ {
+ id: "AW_STEP4",
+ auto_advance: "primary_button",
+ content: {
+ title: "Step 4",
+ primary_button: {
+ label: "Next",
+ action: {
+ navigate: true,
+ },
+ },
+ secondary_button: {
+ label: "link",
+ },
+ },
+ },
+];
+
+const TEST_AMO_CONTENT = [
+ {
+ id: "AW_AMO_INTRODUCE",
+ content: {
+ position: "split",
+ split_narrow_bkg_position: "-58px",
+ progress_bar: true,
+ logo: {},
+ title: { string_id: "amo-screen-title" },
+ subtitle: { string_id: "amo-screen-subtitle" },
+ primary_button: {
+ label: { string_id: "amo-screen-primary-cta" },
+ },
+ secondary_button: {
+ label: {
+ string_id: "mr2022-onboarding-secondary-skip-button-label",
+ },
+ action: {
+ navigate: true,
+ },
+ },
+ },
+ },
+];
+
+const TEST_DEFAULT_JSON = JSON.stringify(TEST_DEFAULT_CONTENT);
+
+async function openAboutWelcome() {
+ await setAboutWelcomePref(true);
+ await setAboutWelcomeMultiStage(TEST_DEFAULT_JSON);
+
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "about:welcome",
+ true
+ );
+ registerCleanupFunction(() => {
+ BrowserTestUtils.removeTab(tab);
+ });
+ return tab.linkedBrowser;
+}
+
+/**
+ * Test the multistage welcome default UI
+ */
+add_task(async function test_multistage_aboutwelcome_default() {
+ const sandbox = sinon.createSandbox();
+ let browser = await openAboutWelcome();
+ let aboutWelcomeActor = await getAboutWelcomeParent(browser);
+ // Stub AboutWelcomeParent Content Message Handler
+ sandbox.spy(aboutWelcomeActor, "onContentMessage");
+ registerCleanupFunction(() => {
+ sandbox.restore();
+ });
+
+ await test_screen_content(
+ browser,
+ "multistage step 1",
+ // Expected selectors:
+ [
+ "main.AW_STEP1",
+ "div.onboardingContainer",
+ "div.section-secondary",
+ "div.secondary-cta.top",
+ "div.steps",
+ "div.indicator.current",
+ "span.info-text",
+ ],
+ // Unexpected selectors:
+ [
+ "main.AW_STEP2",
+ "main.AW_STEP3",
+ "main.dialog-initial",
+ "main.dialog-last",
+ ]
+ );
+
+ await onButtonClick(browser, "button.primary");
+
+ const { callCount } = aboutWelcomeActor.onContentMessage;
+ Assert.greaterOrEqual(callCount, 1, `${callCount} Stub was called`);
+ let clickCall;
+ for (let i = 0; i < callCount; i++) {
+ const call = aboutWelcomeActor.onContentMessage.getCall(i);
+ info(`Call #${i}: ${call.args[0]} ${JSON.stringify(call.args[1])}`);
+ if (call.calledWithMatch("", { event: "CLICK_BUTTON" })) {
+ clickCall = call;
+ }
+ }
+
+ Assert.ok(
+ clickCall.args[1].message_id === "MR_WELCOME_DEFAULT_0_AW_STEP1",
+ "AboutWelcome MR message id joined with screen id"
+ );
+
+ await test_screen_content(
+ browser,
+ "multistage step 2",
+ // Expected selectors:
+ [
+ "main.AW_STEP2",
+ "div.onboardingContainer",
+ "div.section-main",
+ "div.steps",
+ "div.indicator.current",
+ "main.with-noodles",
+ ],
+ // Unexpected selectors:
+ [
+ "main.AW_STEP1",
+ "main.AW_STEP3",
+ "div.section-secondary",
+ "main.dialog-last",
+ ]
+ );
+
+ await onButtonClick(browser, "button.primary");
+
+ await test_screen_content(
+ browser,
+ "multistage step 3",
+ // Expected selectors:
+ [
+ "main.AW_STEP3",
+ "div.onboardingContainer",
+ "div.section-main",
+ "div.tiles-theme-container",
+ "div.steps",
+ "div.indicator.current",
+ ],
+ // Unexpected selectors:
+ [
+ "main.AW_STEP2",
+ "main.AW_STEP1",
+ "div.section-secondary",
+ "main.dialog-initial",
+ "main.with-noodles",
+ "main.dialog-last",
+ ]
+ );
+
+ await onButtonClick(browser, "button.primary");
+
+ await test_screen_content(
+ browser,
+ "multistage step 4",
+ // Expected selectors:
+ [
+ "main.AW_STEP4.screen-1",
+ "main.AW_STEP4.dialog-last",
+ "div.onboardingContainer",
+ ],
+ // Unexpected selectors:
+ [
+ "main.AW_STEP2",
+ "main.AW_STEP1",
+ "main.AW_STEP3",
+ "div.steps",
+ "main.dialog-initial",
+ "main.AW_STEP4.screen-0",
+ "main.AW_STEP4.screen-2",
+ "main.AW_STEP4.screen-3",
+ ]
+ );
+});
+
+/**
+ * Test navigating back/forward between screens
+ */
+add_task(async function test_Multistage_About_Welcome_navigation() {
+ let browser = await openAboutWelcome();
+
+ await onButtonClick(browser, "button.primary");
+ await TestUtils.waitForCondition(() => browser.canGoBack);
+ browser.goBack();
+
+ await test_screen_content(
+ browser,
+ "multistage step 1",
+ // Expected selectors:
+ [
+ "div.onboardingContainer",
+ "main.AW_STEP1",
+ "div.secondary-cta",
+ "div.secondary-cta.top",
+ "button[value='secondary_button']",
+ "button[value='secondary_button_top']",
+ "span.info-text",
+ ],
+ // Unexpected selectors:
+ ["main.AW_STEP2", "main.AW_STEP3"]
+ );
+
+ await document.getElementById("forward-button").click();
+});
+
+/**
+ * Test the multistage welcome UI primary button action
+ */
+add_task(async function test_AWMultistage_Primary_Action() {
+ let browser = await openAboutWelcome();
+ let aboutWelcomeActor = await getAboutWelcomeParent(browser);
+ const sandbox = sinon.createSandbox();
+ sandbox.spy(aboutWelcomeActor, "onContentMessage");
+ registerCleanupFunction(() => {
+ sandbox.restore();
+ });
+
+ await onButtonClick(browser, "button.primary");
+ const { callCount } = aboutWelcomeActor.onContentMessage;
+ Assert.greaterOrEqual(callCount, 1, `${callCount} Stub was called`);
+
+ let clickCall;
+ let performanceCall;
+ for (let i = 0; i < callCount; i++) {
+ const call = aboutWelcomeActor.onContentMessage.getCall(i);
+ info(`Call #${i}: ${call.args[0]} ${JSON.stringify(call.args[1])}`);
+ if (call.calledWithMatch("", { event: "CLICK_BUTTON" })) {
+ clickCall = call;
+ } else if (
+ call.calledWithMatch("", {
+ event_context: { mountStart: sinon.match.number },
+ })
+ ) {
+ performanceCall = call;
+ }
+ }
+
+ // For some builds, we can stub fast enough to catch the performance
+ if (performanceCall) {
+ Assert.equal(
+ performanceCall.args[0],
+ "AWPage:TELEMETRY_EVENT",
+ "send telemetry event"
+ );
+ Assert.equal(
+ performanceCall.args[1].event,
+ "IMPRESSION",
+ "performance impression event recorded in telemetry"
+ );
+ Assert.equal(
+ typeof performanceCall.args[1].event_context.domComplete,
+ "number",
+ "numeric domComplete recorded in telemetry"
+ );
+ Assert.equal(
+ typeof performanceCall.args[1].event_context.domInteractive,
+ "number",
+ "numeric domInteractive recorded in telemetry"
+ );
+ Assert.equal(
+ typeof performanceCall.args[1].event_context.mountStart,
+ "number",
+ "numeric mountStart recorded in telemetry"
+ );
+ Assert.equal(
+ performanceCall.args[1].message_id,
+ "MR_WELCOME_DEFAULT",
+ "MessageId sent in performance event telemetry"
+ );
+ }
+
+ Assert.equal(
+ clickCall.args[0],
+ "AWPage:TELEMETRY_EVENT",
+ "send telemetry event"
+ );
+ Assert.equal(
+ clickCall.args[1].event,
+ "CLICK_BUTTON",
+ "click button event recorded in telemetry"
+ );
+ Assert.equal(
+ clickCall.args[1].event_context.source,
+ "primary_button",
+ "primary button click source recorded in telemetry"
+ );
+ Assert.equal(
+ clickCall.args[1].message_id,
+ "MR_WELCOME_DEFAULT_0_AW_STEP1",
+ "MessageId sent in click event telemetry"
+ );
+});
+
+add_task(async function test_AWMultistage_Secondary_Open_URL_Action() {
+ let browser = await openAboutWelcome();
+ let aboutWelcomeActor = await getAboutWelcomeParent(browser);
+ const sandbox = sinon.createSandbox();
+ // Stub AboutWelcomeParent Content Message Handler
+ sandbox.stub(aboutWelcomeActor, "onContentMessage").resolves(null);
+ registerCleanupFunction(() => {
+ sandbox.restore();
+ });
+
+ await onButtonClick(browser, "button[value='secondary_button_top']");
+ const { callCount } = aboutWelcomeActor.onContentMessage;
+ Assert.greaterOrEqual(
+ callCount,
+ 2,
+ `${callCount} Stub called twice to handle FxA open URL and Telemetry`
+ );
+
+ let actionCall;
+ let eventCall;
+ for (let i = 0; i < callCount; i++) {
+ const call = aboutWelcomeActor.onContentMessage.getCall(i);
+ info(`Call #${i}: ${call.args[0]} ${JSON.stringify(call.args[1])}`);
+ if (call.calledWithMatch("SPECIAL")) {
+ actionCall = call;
+ } else if (call.calledWithMatch("", { event: "CLICK_BUTTON" })) {
+ eventCall = call;
+ }
+ }
+
+ Assert.equal(
+ actionCall.args[0],
+ "AWPage:SPECIAL_ACTION",
+ "Got call to handle special action"
+ );
+ Assert.equal(
+ actionCall.args[1].type,
+ "SHOW_FIREFOX_ACCOUNTS",
+ "Special action SHOW_FIREFOX_ACCOUNTS event handled"
+ );
+ Assert.equal(
+ actionCall.args[1].data.extraParams.utm_term,
+ "aboutwelcome-default-screen",
+ "UTMTerm set in FxA URL"
+ );
+ Assert.equal(
+ actionCall.args[1].data.entrypoint,
+ "test",
+ "EntryPoint set in FxA URL"
+ );
+ Assert.equal(
+ eventCall.args[0],
+ "AWPage:TELEMETRY_EVENT",
+ "Got call to handle Telemetry event"
+ );
+ Assert.equal(
+ eventCall.args[1].event,
+ "CLICK_BUTTON",
+ "click button event recorded in Telemetry"
+ );
+ Assert.equal(
+ eventCall.args[1].event_context.source,
+ "secondary_button_top",
+ "secondary_top button click source recorded in Telemetry"
+ );
+});
+
+add_task(async function test_AWMultistage_Themes() {
+ let browser = await openAboutWelcome();
+ let aboutWelcomeActor = await getAboutWelcomeParent(browser);
+
+ const sandbox = sinon.createSandbox();
+ sandbox.spy(aboutWelcomeActor, "onContentMessage");
+
+ registerCleanupFunction(() => {
+ sandbox.restore();
+ });
+ await onButtonClick(browser, "button.primary");
+
+ await test_screen_content(
+ browser,
+ "multistage proton step 2",
+ // Expected selectors:
+ ["main.AW_STEP2"],
+ // Unexpected selectors:
+ ["main.AW_STEP1"]
+ );
+ await onButtonClick(browser, "button.primary");
+
+ await ContentTask.spawn(browser, "Themes", async () => {
+ await ContentTaskUtils.waitForCondition(
+ () => content.document.querySelector("label.theme"),
+ "Theme Icons"
+ );
+ let themes = content.document.querySelectorAll("label.theme");
+ Assert.equal(themes.length, 2, "Two themes displayed");
+ });
+
+ await onButtonClick(browser, "input[value=automatic]");
+
+ const { callCount } = aboutWelcomeActor.onContentMessage;
+ Assert.greaterOrEqual(callCount, 1, `${callCount} Stub was called`);
+
+ let actionCall;
+ let eventCall;
+ for (let i = 0; i < callCount; i++) {
+ const call = aboutWelcomeActor.onContentMessage.getCall(i);
+ info(`Call #${i}: ${call.args[0]} ${JSON.stringify(call.args[1])}`);
+ if (call.calledWithMatch("SELECT_THEME")) {
+ actionCall = call;
+ } else if (call.calledWithMatch("", { event: "CLICK_BUTTON" })) {
+ eventCall = call;
+ }
+ }
+
+ Assert.equal(
+ actionCall.args[0],
+ "AWPage:SELECT_THEME",
+ "Got call to handle select theme"
+ );
+ Assert.equal(
+ actionCall.args[1],
+ "AUTOMATIC",
+ "Theme value passed as AUTOMATIC"
+ );
+ Assert.equal(
+ eventCall.args[0],
+ "AWPage:TELEMETRY_EVENT",
+ "Got call to handle Telemetry event when theme tile clicked"
+ );
+ Assert.equal(
+ eventCall.args[1].event,
+ "CLICK_BUTTON",
+ "click button event recorded in Telemetry"
+ );
+ Assert.equal(
+ eventCall.args[1].event_context.source,
+ "automatic",
+ "automatic click source recorded in Telemetry"
+ );
+});
+
+add_task(async function test_AWMultistage_can_restore_theme() {
+ const { XPIExports } = ChromeUtils.importESModule(
+ "resource://gre/modules/addons/XPIExports.sys.mjs"
+ );
+ const sandbox = sinon.createSandbox();
+ registerCleanupFunction(() => sandbox.restore());
+
+ const fakeAddons = [];
+ class FakeAddon {
+ constructor({ id = "default-theme@mozilla.org", isActive = false } = {}) {
+ this.id = id;
+ this.isActive = isActive;
+ }
+
+ enable() {
+ for (let addon of fakeAddons) {
+ addon.isActive = false;
+ }
+ this.isActive = true;
+ }
+ }
+ fakeAddons.push(
+ new FakeAddon({ id: "fake-theme-1@mozilla.org", isActive: true }),
+ new FakeAddon({ id: "fake-theme-2@mozilla.org" })
+ );
+
+ let browser = await openAboutWelcome();
+ let aboutWelcomeActor = await getAboutWelcomeParent(browser);
+
+ sandbox.stub(XPIExports.XPIProvider, "getAddonsByTypes").resolves(fakeAddons);
+ sandbox
+ .stub(XPIExports.XPIProvider, "getAddonByID")
+ .callsFake(id => fakeAddons.find(addon => addon.id === id));
+ sandbox.spy(aboutWelcomeActor, "onContentMessage");
+
+ // Test that the active theme ID is stored in LIGHT_WEIGHT_THEMES
+ await aboutWelcomeActor.receiveMessage({
+ name: "AWPage:GET_SELECTED_THEME",
+ });
+ Assert.equal(
+ await aboutWelcomeActor.onContentMessage.lastCall.returnValue,
+ "automatic",
+ `Should return "automatic" for non-built-in theme`
+ );
+
+ await aboutWelcomeActor.receiveMessage({
+ name: "AWPage:SELECT_THEME",
+ data: "AUTOMATIC",
+ });
+ Assert.equal(
+ XPIExports.XPIProvider.getAddonByID.lastCall.args[0],
+ fakeAddons[0].id,
+ `LIGHT_WEIGHT_THEMES.AUTOMATIC should be ${fakeAddons[0].id}`
+ );
+
+ // Enable a different theme...
+ fakeAddons[1].enable();
+ // And test that AWGetSelectedTheme updates the active theme ID
+ await aboutWelcomeActor.receiveMessage({
+ name: "AWPage:GET_SELECTED_THEME",
+ });
+ await aboutWelcomeActor.receiveMessage({
+ name: "AWPage:SELECT_THEME",
+ data: "AUTOMATIC",
+ });
+ Assert.equal(
+ XPIExports.XPIProvider.getAddonByID.lastCall.args[0],
+ fakeAddons[1].id,
+ `LIGHT_WEIGHT_THEMES.AUTOMATIC should be ${fakeAddons[1].id}`
+ );
+});
+
+add_task(async function test_AWMultistage_Import() {
+ let browser = await openAboutWelcome();
+ let aboutWelcomeActor = await getAboutWelcomeParent(browser);
+
+ // Click twice to advance to screen 3
+ await onButtonClick(browser, "button.primary");
+ await test_screen_content(
+ browser,
+ "multistage proton step 2",
+ // Expected selectors:
+ ["main.AW_STEP2"],
+ // Unexpected selectors:
+ ["main.AW_STEP1"]
+ );
+ await onButtonClick(browser, "button.primary");
+
+ const sandbox = sinon.createSandbox();
+ sandbox.stub(SpecialMessageActions, "handleAction");
+ sandbox.spy(aboutWelcomeActor, "onContentMessage");
+
+ registerCleanupFunction(() => {
+ sandbox.restore();
+ });
+
+ await test_screen_content(
+ browser,
+ "multistage proton step 2",
+ // Expected selectors:
+ ["main.AW_STEP3"],
+ // Unexpected selectors:
+ ["main.AW_STEP2"]
+ );
+
+ await onButtonClick(browser, "button[value='secondary_button']");
+ const { callCount } = aboutWelcomeActor.onContentMessage;
+
+ let actionCall;
+ let eventCall;
+ for (let i = 0; i < callCount; i++) {
+ const call = aboutWelcomeActor.onContentMessage.getCall(i);
+ info(`Call #${i}: ${call.args[0]} ${JSON.stringify(call.args[1])}`);
+ if (call.calledWithMatch("SPECIAL")) {
+ actionCall = call;
+ } else if (call.calledWithMatch("", { event: "CLICK_BUTTON" })) {
+ eventCall = call;
+ }
+ }
+
+ Assert.equal(
+ actionCall.args[0],
+ "AWPage:SPECIAL_ACTION",
+ "Got call to handle special action"
+ );
+ Assert.equal(
+ actionCall.args[1].type,
+ "SHOW_MIGRATION_WIZARD",
+ "Special action SHOW_MIGRATION_WIZARD event handled"
+ );
+ Assert.equal(
+ actionCall.args[1].data.source,
+ "chrome",
+ "Source passed to event handler"
+ );
+ Assert.equal(
+ eventCall.args[0],
+ "AWPage:TELEMETRY_EVENT",
+ "Got call to handle Telemetry event"
+ );
+});
+
+add_task(async function test_updatesPrefOnAWOpen() {
+ Services.prefs.setBoolPref(DID_SEE_ABOUT_WELCOME_PREF, false);
+ await setAboutWelcomePref(true);
+
+ await openAboutWelcome();
+ await TestUtils.waitForCondition(
+ () =>
+ Services.prefs.getBoolPref(DID_SEE_ABOUT_WELCOME_PREF, false) === true,
+ "Updated pref to seen AW"
+ );
+ Services.prefs.clearUserPref(DID_SEE_ABOUT_WELCOME_PREF);
+});
+
+add_setup(async function () {
+ const sandbox = sinon.createSandbox();
+ // This needs to happen before any about:welcome page opens
+ sandbox.stub(FxAccounts.config, "promiseMetricsFlowURI").resolves("");
+ await setAboutWelcomeMultiStage("");
+
+ registerCleanupFunction(() => {
+ sandbox.restore();
+ });
+});
+
+add_task(async function test_FxA_metricsFlowURI() {
+ let browser = await openAboutWelcome();
+
+ await ContentTask.spawn(browser, {}, async () => {
+ Assert.ok(
+ await ContentTaskUtils.waitForCondition(
+ () => content.document.querySelector("div.onboardingContainer"),
+ "Wait for about:welcome to load"
+ ),
+ "about:welcome loaded"
+ );
+ });
+
+ Assert.ok(FxAccounts.config.promiseMetricsFlowURI.called, "Stub was called");
+ Assert.equal(
+ FxAccounts.config.promiseMetricsFlowURI.firstCall.args[0],
+ "aboutwelcome",
+ "Called by AboutWelcomeParent"
+ );
+
+ SpecialPowers.popPrefEnv();
+});
+
+add_task(async function test_send_aboutwelcome_as_page_in_event_telemetry() {
+ const sandbox = sinon.createSandbox();
+ let browser = await openAboutWelcome();
+ let aboutWelcomeActor = await getAboutWelcomeParent(browser);
+ sandbox.spy(aboutWelcomeActor, "onContentMessage");
+
+ await onButtonClick(browser, "button.primary");
+
+ const { callCount } = aboutWelcomeActor.onContentMessage;
+ Assert.greaterOrEqual(callCount, 1, `${callCount} Stub was called`);
+
+ let eventCall;
+ for (let i = 0; i < callCount; i++) {
+ const call = aboutWelcomeActor.onContentMessage.getCall(i);
+ info(`Call #${i}: ${call.args[0]} ${JSON.stringify(call.args[1])}`);
+ if (call.calledWithMatch("", { event: "CLICK_BUTTON" })) {
+ eventCall = call;
+ }
+ }
+
+ Assert.equal(
+ eventCall.args[1].event,
+ "CLICK_BUTTON",
+ "Event telemetry sent on primary button press"
+ );
+ Assert.equal(
+ eventCall.args[1].event_context.page,
+ "about:welcome",
+ "Event context page set to 'about:welcome' in event telemetry"
+ );
+
+ registerCleanupFunction(() => {
+ sandbox.restore();
+ });
+});
+
+add_task(async function test_AMO_untranslated_strings() {
+ const sandbox = sinon.createSandbox();
+
+ await setAboutWelcomePref(true);
+ await setAboutWelcomeMultiStage(JSON.stringify(TEST_AMO_CONTENT));
+
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "about:welcome",
+ true
+ );
+ registerCleanupFunction(() => {
+ BrowserTestUtils.removeTab(tab);
+ });
+
+ let browser = tab.linkedBrowser;
+
+ await test_screen_content(
+ browser,
+ "renders the AMO screen with preview strings",
+ //Expected selectors
+ [
+ "main.AW_AMO_INTRODUCE",
+ `main.screen[pos="split"]`,
+ "button.primary[data-l10n-id='amo-screen-primary-cta']",
+ "h1[data-l10n-id='amo-screen-title']",
+ "h2[data-l10n-id='amo-screen-subtitle']",
+ ],
+
+ //Unexpected selectors:
+ ["main.AW_EASY_SETUP_NEEDS_DEFAULT"]
+ );
+
+ registerCleanupFunction(async () => {
+ await popPrefs(); // for setAboutWelcomePref()
+ sandbox.restore();
+ });
+});
diff --git a/browser/components/aboutwelcome/tests/browser/browser_aboutwelcome_multistage_experimentAPI.js b/browser/components/aboutwelcome/tests/browser/browser_aboutwelcome_multistage_experimentAPI.js
new file mode 100644
index 0000000000..960d42a1f8
--- /dev/null
+++ b/browser/components/aboutwelcome/tests/browser/browser_aboutwelcome_multistage_experimentAPI.js
@@ -0,0 +1,641 @@
+"use strict";
+
+const { ExperimentAPI } = ChromeUtils.importESModule(
+ "resource://nimbus/ExperimentAPI.sys.mjs"
+);
+const { ExperimentFakes } = ChromeUtils.importESModule(
+ "resource://testing-common/NimbusTestUtils.sys.mjs"
+);
+const { TelemetryTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/TelemetryTestUtils.sys.mjs"
+);
+
+const TEST_PROTON_CONTENT = [
+ {
+ id: "AW_STEP1",
+ content: {
+ title: "Step 1",
+ primary_button: {
+ label: "Next",
+ action: {
+ navigate: true,
+ },
+ },
+ secondary_button: {
+ label: "link",
+ },
+ secondary_button_top: {
+ label: "link top",
+ action: {
+ type: "SHOW_FIREFOX_ACCOUNTS",
+ data: { entrypoint: "test" },
+ },
+ },
+ has_noodles: true,
+ },
+ },
+ {
+ id: "AW_STEP2",
+ content: {
+ title: "Step 2",
+ primary_button: {
+ label: "Next",
+ action: {
+ navigate: true,
+ },
+ },
+ secondary_button: {
+ label: "link",
+ },
+ has_noodles: true,
+ },
+ },
+ {
+ id: "AW_STEP3",
+ content: {
+ title: "Step 3",
+ tiles: {
+ type: "theme",
+ action: {
+ theme: "<event>",
+ },
+ data: [
+ {
+ theme: "automatic",
+ label: "theme-1",
+ tooltip: "test-tooltip",
+ },
+ {
+ theme: "dark",
+ label: "theme-2",
+ },
+ ],
+ },
+ primary_button: {
+ label: "Next",
+ action: {
+ navigate: true,
+ },
+ },
+ secondary_button: {
+ label: "Import",
+ action: {
+ type: "SHOW_MIGRATION_WIZARD",
+ data: { source: "chrome" },
+ },
+ },
+ has_noodles: true,
+ },
+ },
+ {
+ id: "AW_STEP4",
+ content: {
+ title: "Step 4",
+ primary_button: {
+ label: "Next",
+ action: {
+ navigate: true,
+ },
+ },
+ secondary_button: {
+ label: "link",
+ },
+ has_noodles: true,
+ },
+ },
+];
+
+/**
+ * Test the zero onboarding using ExperimentAPI
+ */
+add_task(async function test_multistage_zeroOnboarding_experimentAPI() {
+ await setAboutWelcomePref(true);
+ await ExperimentAPI.ready();
+ let doExperimentCleanup = await ExperimentFakes.enrollWithFeatureConfig({
+ featureId: "aboutwelcome",
+ value: { enabled: false },
+ });
+
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "about:welcome",
+ true
+ );
+
+ registerCleanupFunction(() => {
+ BrowserTestUtils.removeTab(tab);
+ });
+
+ const browser = tab.linkedBrowser;
+
+ await test_screen_content(
+ browser,
+ "Opens new tab",
+ // Expected selectors:
+ ["div.search-wrapper", "body.activity-stream"],
+ // Unexpected selectors:
+ ["div.onboardingContainer", "main.AW_STEP1"]
+ );
+
+ await doExperimentCleanup();
+});
+
+/**
+ * Test the multistage welcome UI with test content theme as first screen
+ */
+add_task(async function test_multistage_aboutwelcome_experimentAPI() {
+ const TEST_CONTENT = [
+ {
+ id: "AW_STEP1",
+ content: {
+ title: "Step 1",
+ tiles: {
+ type: "theme",
+ action: {
+ theme: "<event>",
+ },
+ data: [
+ {
+ theme: "automatic",
+ label: "theme-1",
+ tooltip: "test-tooltip",
+ },
+ {
+ theme: "dark",
+ label: "theme-2",
+ },
+ ],
+ },
+ primary_button: {
+ label: "Next",
+ action: {
+ navigate: true,
+ },
+ },
+ secondary_button: {
+ label: "link",
+ },
+ secondary_button_top: {
+ label: "link top",
+ action: {
+ type: "SHOW_FIREFOX_ACCOUNTS",
+ data: { entrypoint: "test" },
+ },
+ },
+ has_noodles: true,
+ },
+ },
+ {
+ id: "AW_STEP2",
+ content: {
+ zap: true,
+ title: "Step 2 test",
+ primary_button: {
+ label: "Next",
+ action: {
+ navigate: true,
+ },
+ },
+ secondary_button: {
+ label: "link",
+ },
+ has_noodles: true,
+ },
+ },
+ {
+ id: "AW_STEP3",
+ content: {
+ logo: {},
+ title: "Step 3",
+ primary_button: {
+ label: "Next",
+ action: {
+ navigate: true,
+ },
+ },
+ secondary_button: {
+ label: "Import",
+ action: {
+ type: "SHOW_MIGRATION_WIZARD",
+ data: { source: "chrome" },
+ },
+ },
+ has_noodles: true,
+ },
+ },
+ ];
+ const sandbox = sinon.createSandbox();
+ NimbusFeatures.aboutwelcome._didSendExposureEvent = false;
+ await setAboutWelcomePref(true);
+ await ExperimentAPI.ready();
+
+ let doExperimentCleanup = await ExperimentFakes.enrollWithFeatureConfig({
+ featureId: "aboutwelcome",
+ enabled: true,
+ value: {
+ id: "my-mochitest-experiment",
+ screens: TEST_CONTENT,
+ },
+ });
+
+ sandbox.spy(ExperimentAPI, "recordExposureEvent");
+
+ Services.telemetry.clearScalars();
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "about:welcome",
+ true
+ );
+
+ const browser = tab.linkedBrowser;
+
+ let aboutWelcomeActor = await getAboutWelcomeParent(browser);
+ // Stub AboutWelcomeParent Content Message Handler
+ sandbox.spy(aboutWelcomeActor, "onContentMessage");
+ registerCleanupFunction(() => {
+ BrowserTestUtils.removeTab(tab);
+ sandbox.restore();
+ });
+
+ // Test first (theme) screen.
+ await test_screen_content(
+ browser,
+ "multistage step 1",
+ // Expected selectors:
+ [
+ "div.onboardingContainer",
+ "main.AW_STEP1",
+ "div.secondary-cta",
+ "div.secondary-cta.top",
+ "button[value='secondary_button']",
+ "button[value='secondary_button_top']",
+ "label.theme",
+ "input[type='radio']",
+ ],
+ // Unexpected selectors:
+ ["main.AW_STEP2", "main.AW_STEP3", "div.tiles-container.info"]
+ );
+
+ await onButtonClick(browser, "button.primary");
+
+ const { callCount } = aboutWelcomeActor.onContentMessage;
+ Assert.greaterOrEqual(callCount, 1, `${callCount} Stub was called`);
+ let clickCall;
+ for (let i = 0; i < callCount; i++) {
+ const call = aboutWelcomeActor.onContentMessage.getCall(i);
+ info(`Call #${i}: ${call.args[0]} ${JSON.stringify(call.args[1])}`);
+ if (call.calledWithMatch("", { event: "CLICK_BUTTON" })) {
+ clickCall = call;
+ }
+ }
+
+ Assert.equal(
+ clickCall.args[0],
+ "AWPage:TELEMETRY_EVENT",
+ "send telemetry event"
+ );
+
+ Assert.equal(
+ clickCall.args[1].message_id,
+ "MY-MOCHITEST-EXPERIMENT_0_AW_STEP1",
+ "Telemetry should join id defined in feature value with screen"
+ );
+
+ await test_screen_content(
+ browser,
+ "multistage step 2",
+ // Expected selectors:
+ [
+ "div.onboardingContainer",
+ "main.AW_STEP2",
+ "button[value='secondary_button']",
+ ],
+ // Unexpected selectors:
+ ["main.AW_STEP1", "main.AW_STEP3", "div.secondary-cta.top"]
+ );
+ await onButtonClick(browser, "button.primary");
+ await test_screen_content(
+ browser,
+ "multistage step 3",
+ // Expected selectors:
+ [
+ "div.onboardingContainer",
+ "main.AW_STEP3",
+ "img.brand-logo",
+ "div.welcome-text",
+ ],
+ // Unexpected selectors:
+ ["main.AW_STEP1", "main.AW_STEP2"]
+ );
+ await onButtonClick(browser, "button.primary");
+ await test_screen_content(
+ browser,
+ "home",
+ // Expected selectors:
+ ["body.activity-stream"],
+ // Unexpected selectors:
+ ["div.onboardingContainer"]
+ );
+
+ Assert.equal(
+ ExperimentAPI.recordExposureEvent.callCount,
+ 1,
+ "Called only once for exposure event"
+ );
+
+ const scalars = TelemetryTestUtils.getProcessScalars("parent", true, true);
+ TelemetryTestUtils.assertKeyedScalar(
+ scalars,
+ "telemetry.event_counts",
+ "normandy#expose#nimbus_experiment",
+ 1
+ );
+
+ await doExperimentCleanup();
+});
+
+/**
+ * Test the multistage proton welcome UI using ExperimentAPI with transitions
+ */
+add_task(async function test_multistage_aboutwelcome_transitions() {
+ const sandbox = sinon.createSandbox();
+ await setAboutWelcomePref(true);
+ await ExperimentAPI.ready();
+
+ let doExperimentCleanup = await ExperimentFakes.enrollWithFeatureConfig({
+ featureId: "aboutwelcome",
+ value: {
+ id: "my-mochitest-experiment",
+ enabled: true,
+ screens: TEST_PROTON_CONTENT,
+ transitions: true,
+ },
+ });
+
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "about:welcome",
+ true
+ );
+
+ const browser = tab.linkedBrowser;
+
+ let aboutWelcomeActor = await getAboutWelcomeParent(browser);
+ // Stub AboutWelcomeParent Content Message Handler
+ sandbox.spy(aboutWelcomeActor, "onContentMessage");
+ registerCleanupFunction(() => {
+ BrowserTestUtils.removeTab(tab);
+ sandbox.restore();
+ });
+
+ await test_screen_content(
+ browser,
+ "multistage proton step 1",
+ // Expected selectors:
+ ["div.proton.transition- .screen"],
+ // Unexpected selectors:
+ ["div.proton.transition-out"]
+ );
+
+ // Double click should still only transition once.
+ await onButtonClick(browser, "button.primary");
+ await onButtonClick(browser, "button.primary");
+
+ await test_screen_content(
+ browser,
+ "multistage proton step 1 transition to 2",
+ // Expected selectors:
+ ["div.proton.transition-out .screen", "div.proton.transition- .screen-1"]
+ );
+
+ await doExperimentCleanup();
+});
+
+/**
+ * Test the multistage proton welcome UI using ExperimentAPI without transitions
+ */
+add_task(async function test_multistage_aboutwelcome_transitions_off() {
+ const sandbox = sinon.createSandbox();
+ await setAboutWelcomePref(true);
+ await ExperimentAPI.ready();
+
+ let doExperimentCleanup = await ExperimentFakes.enrollWithFeatureConfig({
+ featureId: "aboutwelcome",
+ value: {
+ id: "my-mochitest-experiment",
+ enabled: true,
+ screens: TEST_PROTON_CONTENT,
+ transitions: false,
+ },
+ });
+
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "about:welcome",
+ true
+ );
+
+ const browser = tab.linkedBrowser;
+
+ let aboutWelcomeActor = await getAboutWelcomeParent(browser);
+ // Stub AboutWelcomeParent Content Message Handler
+ sandbox.spy(aboutWelcomeActor, "onContentMessage");
+ registerCleanupFunction(() => {
+ BrowserTestUtils.removeTab(tab);
+ sandbox.restore();
+ });
+
+ await test_screen_content(
+ browser,
+ "multistage proton step 1",
+ // Expected selectors:
+ ["div.proton.transition- .screen"],
+ // Unexpected selectors:
+ ["div.proton.transition-out"]
+ );
+
+ await onButtonClick(browser, "button.primary");
+ await test_screen_content(
+ browser,
+ "multistage proton step 1 no transition to 2",
+ // Expected selectors:
+ [],
+ // Unexpected selectors:
+ ["div.proton.transition-out .screen-0"]
+ );
+
+ await doExperimentCleanup();
+});
+
+/* Test multistage custom backdrop
+ */
+add_task(async function test_multistage_aboutwelcome_backdrop() {
+ const sandbox = sinon.createSandbox();
+ const TEST_BACKDROP = "blue";
+
+ const TEST_CONTENT = [
+ {
+ id: "TEST_SCREEN",
+ content: {
+ position: "split",
+ logo: {},
+ title: "test",
+ },
+ },
+ ];
+ await setAboutWelcomePref(true);
+ await ExperimentAPI.ready();
+ await pushPrefs(["browser.aboutwelcome.backdrop", TEST_BACKDROP]);
+
+ const doExperimentCleanup = await ExperimentFakes.enrollWithFeatureConfig({
+ featureId: "aboutwelcome",
+ value: {
+ id: "my-mochitest-experiment",
+ screens: TEST_CONTENT,
+ },
+ });
+
+ const tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "about:welcome",
+ true
+ );
+
+ const browser = tab.linkedBrowser;
+
+ registerCleanupFunction(() => {
+ BrowserTestUtils.removeTab(tab);
+ sandbox.restore();
+ });
+
+ await test_screen_content(
+ browser,
+ "multistage step 1",
+ // Expected selectors:
+ [`div.outer-wrapper.onboardingContainer[style*='${TEST_BACKDROP}']`]
+ );
+
+ await doExperimentCleanup();
+});
+
+add_task(async function test_multistage_aboutwelcome_utm_term() {
+ const sandbox = sinon.createSandbox();
+
+ const TEST_CONTENT = [
+ {
+ id: "TEST_SCREEN",
+ content: {
+ position: "split",
+ logo: {},
+ title: "test",
+ secondary_button_top: {
+ label: "test",
+ style: "link",
+ action: {
+ type: "OPEN_URL",
+ data: {
+ args: "https://www.mozilla.org/",
+ },
+ },
+ },
+ },
+ },
+ ];
+ await setAboutWelcomePref(true);
+ await ExperimentAPI.ready();
+
+ const doExperimentCleanup = await ExperimentFakes.enrollWithFeatureConfig({
+ featureId: "aboutwelcome",
+ value: {
+ id: "my-mochitest-experiment",
+ screens: TEST_CONTENT,
+ UTMTerm: "test",
+ },
+ });
+
+ const tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "about:welcome",
+ true
+ );
+
+ const browser = tab.linkedBrowser;
+ const aboutWelcomeActor = await getAboutWelcomeParent(browser);
+
+ sandbox.stub(aboutWelcomeActor, "onContentMessage");
+
+ await onButtonClick(browser, "button[value='secondary_button_top']");
+
+ let actionCall;
+
+ const { callCount } = aboutWelcomeActor.onContentMessage;
+ for (let i = 0; i < callCount; i++) {
+ const call = aboutWelcomeActor.onContentMessage.getCall(i);
+ info(`Call #${i}: ${call.args[0]} ${JSON.stringify(call.args[1])}`);
+ if (call.calledWithMatch("SPECIAL")) {
+ actionCall = call;
+ }
+ }
+
+ Assert.equal(
+ actionCall.args[1].data.args,
+ "https://www.mozilla.org/?utm_source=activity-stream&utm_campaign=firstrun&utm_medium=referral&utm_term=test-screen",
+ "UTMTerm set in mobile"
+ );
+
+ registerCleanupFunction(() => {
+ sandbox.restore();
+ BrowserTestUtils.removeTab(tab);
+ });
+
+ await doExperimentCleanup();
+});
+
+add_task(async function test_multistage_aboutwelcome_newtab_urlbar_focus() {
+ const sandbox = sinon.createSandbox();
+
+ const TEST_CONTENT = [
+ {
+ id: "TEST_SCREEN",
+ content: {
+ position: "split",
+ logo: {},
+ title: "Test newtab url focus",
+ primary_button: {
+ label: "Next",
+ action: {
+ navigate: true,
+ },
+ },
+ },
+ },
+ ];
+ await setAboutWelcomePref(true);
+ await ExperimentAPI.ready();
+ await pushPrefs(["browser.aboutwelcome.newtabUrlBarFocus", true]);
+
+ const doExperimentCleanup = await ExperimentFakes.enrollWithFeatureConfig({
+ featureId: "aboutwelcome",
+ value: {
+ id: "my-mochitest-experiment",
+ screens: TEST_CONTENT,
+ },
+ });
+
+ const tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "about:welcome",
+ true
+ );
+ const browser = tab.linkedBrowser;
+ let focused = BrowserTestUtils.waitForEvent(gURLBar.inputField, "focus");
+ await onButtonClick(browser, "button.primary");
+ await focused;
+ Assert.ok(gURLBar.focused, "focus should be on url bar");
+
+ registerCleanupFunction(() => {
+ BrowserTestUtils.removeTab(tab);
+ sandbox.restore();
+ });
+ await doExperimentCleanup();
+});
diff --git a/browser/components/aboutwelcome/tests/browser/browser_aboutwelcome_multistage_languageSwitcher.js b/browser/components/aboutwelcome/tests/browser/browser_aboutwelcome_multistage_languageSwitcher.js
new file mode 100644
index 0000000000..5291a66f7e
--- /dev/null
+++ b/browser/components/aboutwelcome/tests/browser/browser_aboutwelcome_multistage_languageSwitcher.js
@@ -0,0 +1,708 @@
+"use strict";
+
+const { getAddonAndLocalAPIsMocker } = ChromeUtils.importESModule(
+ "resource://testing-common/LangPackMatcherTestUtils.sys.mjs"
+);
+
+const { AWScreenUtils } = ChromeUtils.importESModule(
+ "resource:///modules/aboutwelcome/AWScreenUtils.sys.mjs"
+);
+
+const sandbox = sinon.createSandbox();
+const mockAddonAndLocaleAPIs = getAddonAndLocalAPIsMocker(this, sandbox);
+add_task(function initSandbox() {
+ registerCleanupFunction(() => {
+ sandbox.restore();
+ });
+});
+
+/**
+ * Spy specifically on the button click telemetry.
+ *
+ * The returned function flushes the spy of all of the matching button click events, and
+ * returns the events.
+ *
+ * @returns {function(): TelemetryEvents[]}
+ */
+async function spyOnTelemetryButtonClicks(browser) {
+ let aboutWelcomeActor = await getAboutWelcomeParent(browser);
+ sandbox.spy(aboutWelcomeActor, "onContentMessage");
+ return () => {
+ const result = aboutWelcomeActor.onContentMessage
+ .getCalls()
+ .filter(
+ call =>
+ call.args[0] === "AWPage:TELEMETRY_EVENT" &&
+ call.args[1]?.event === "CLICK_BUTTON"
+ )
+ // The second argument is the telemetry event.
+ .map(call => call.args[1]);
+
+ aboutWelcomeActor.onContentMessage.resetHistory();
+ return result;
+ };
+}
+
+async function openAboutWelcome() {
+ await pushPrefs(
+ // Speed up the tests by disabling transitions.
+ ["browser.aboutwelcome.transitions", false],
+ ["intl.multilingual.aboutWelcome.languageMismatchEnabled", true]
+ );
+ await setAboutWelcomePref(true);
+
+ sandbox
+ .stub(AWScreenUtils, "evaluateScreenTargeting")
+ .resolves(true)
+ // Renders easy setup import screen as first screen to prevent pin/default dialog boxes breaking tests
+ .withArgs(
+ "doesAppNeedPin && 'browser.shell.checkDefaultBrowser'|preferenceValue && !isDefaultBrowser"
+ )
+ .resolves(false)
+ .withArgs(
+ "!doesAppNeedPin && 'browser.shell.checkDefaultBrowser'|preferenceValue && !isDefaultBrowser"
+ )
+ .resolves(false)
+ .withArgs(
+ "doesAppNeedPin && (!'browser.shell.checkDefaultBrowser'|preferenceValue || isDefaultBrowser)"
+ )
+ .resolves(false)
+ .withArgs("isDeviceMigration")
+ .resolves(false);
+
+ info("Opening about:welcome");
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "about:welcome",
+ true
+ );
+
+ registerCleanupFunction(async () => {
+ BrowserTestUtils.removeTab(tab);
+ });
+
+ return {
+ browser: tab.linkedBrowser,
+ flushClickTelemetry: await spyOnTelemetryButtonClicks(tab.linkedBrowser),
+ };
+}
+
+async function clickVisibleButton(browser, selector) {
+ // eslint-disable-next-line no-shadow
+ await ContentTask.spawn(browser, { selector }, async ({ selector }) => {
+ function getVisibleElement() {
+ for (const el of content.document.querySelectorAll(selector)) {
+ if (el.offsetParent !== null) {
+ return el;
+ }
+ }
+ return null;
+ }
+
+ await ContentTaskUtils.waitForCondition(
+ getVisibleElement,
+ selector,
+ 200, // interval
+ 100 // maxTries
+ );
+ getVisibleElement().click();
+ });
+}
+
+/**
+ * Test that selectors are present and visible.
+ */
+async function testScreenContent(
+ browser,
+ name,
+ expectedSelectors = [],
+ unexpectedSelectors = []
+) {
+ await ContentTask.spawn(
+ browser,
+ { expectedSelectors, name, unexpectedSelectors },
+ async ({
+ expectedSelectors: expected,
+ name: experimentName,
+ unexpectedSelectors: unexpected,
+ }) => {
+ function selectorIsVisible(selector) {
+ const els = content.document.querySelectorAll(selector);
+ // The offsetParent will be null if element is hidden through "display: none;"
+ return [...els].some(el => el.offsetParent !== null);
+ }
+
+ for (let selector of expected) {
+ await ContentTaskUtils.waitForCondition(
+ () => selectorIsVisible(selector),
+ `Should render ${selector} in ${experimentName}`
+ );
+ }
+ for (let selector of unexpected) {
+ ok(
+ !selectorIsVisible(selector),
+ `Should not render ${selector} in ${experimentName}`
+ );
+ }
+ }
+ );
+}
+
+/**
+ * Report telemetry mismatches nicely.
+ */
+function eventsMatch(
+ actualEvents,
+ expectedEvents,
+ message = "Telemetry events match"
+) {
+ if (actualEvents.length !== expectedEvents.length) {
+ console.error("Events do not match");
+ console.error("Actual: ", JSON.stringify(actualEvents, null, 2));
+ console.error("Expected: ", JSON.stringify(expectedEvents, null, 2));
+ }
+ for (let i = 0; i < actualEvents.length; i++) {
+ const actualEvent = JSON.stringify(actualEvents[i], null, 2);
+ const expectedEvent = JSON.stringify(expectedEvents[i], null, 2);
+ if (actualEvent !== expectedEvent) {
+ console.error("Events do not match");
+ dump(`Actual: ${actualEvent}`);
+ dump("\n");
+ dump(`Expected: ${expectedEvent}`);
+ dump("\n");
+ }
+ Assert.strictEqual(actualEvent, expectedEvent, message);
+ }
+}
+
+const liveLanguageSwitchSelectors = [
+ ".screen.AW_LANGUAGE_MISMATCH",
+ `[data-l10n-id*="onboarding-live-language"]`,
+ `[data-l10n-id="mr2022-onboarding-live-language-text"]`,
+];
+
+/**
+ * Accept the about:welcome offer to change the Firefox language when
+ * there is a mismatch between the operating system language and the Firefox
+ * language.
+ */
+add_task(async function test_aboutwelcome_languageSwitcher_accept() {
+ sandbox.restore();
+ const { resolveLangPacks, resolveInstaller, mockable } =
+ mockAddonAndLocaleAPIs({
+ systemLocale: "es-ES",
+ appLocale: "en-US",
+ });
+
+ const { browser, flushClickTelemetry } = await openAboutWelcome();
+ await testScreenContent(
+ browser,
+ "First Screen primary CTA loaded",
+ // Expected selectors:
+ [`button.primary[value="primary_button"]`],
+ // Unexpected selectors:
+ []
+ );
+
+ info("Clicking the primary button to start the onboarding process.");
+ await clickVisibleButton(browser, `button.primary[value="primary_button"]`);
+
+ await testScreenContent(
+ browser,
+ "Live language switching (waiting for languages)",
+ // Expected selectors:
+ [
+ ...liveLanguageSwitchSelectors,
+ `[data-l10n-id="mr2022-onboarding-live-language-text"]`,
+ `button[disabled] [data-l10n-id="onboarding-live-language-waiting-button"]`,
+ `[data-l10n-id="mr2022-onboarding-secondary-skip-button-label"]`,
+ ],
+ // Unexpected selectors:
+ []
+ );
+
+ // Ignore the telemetry of the initial welcome screen.
+ flushClickTelemetry();
+
+ resolveLangPacks(["es-MX", "es-ES", "fr-FR"]);
+
+ await testScreenContent(
+ browser,
+ "Live language switching, asking for a language",
+ // Expected selectors:
+ [
+ ...liveLanguageSwitchSelectors,
+ `button.primary[value="primary_button"]`,
+ `button.primary[value="decline"]`,
+ ],
+ // Unexpected selectors:
+ [
+ `button[disabled] [data-l10n-id="mr2022-onboarding-live-language-waiting-button"]`,
+ `[data-l10n-id="mr2022-onboarding-secondary-skip-button-label"]`,
+ ]
+ );
+
+ info("Clicking the primary button to view language switching page.");
+
+ await clickVisibleButton(browser, "button.primary");
+
+ await testScreenContent(
+ browser,
+ "Live language switching, waiting for langpack to download",
+ // Expected selectors:
+ [
+ ...liveLanguageSwitchSelectors,
+ `[data-l10n-id="onboarding-live-language-button-label-downloading"]`,
+ `[data-l10n-id="onboarding-live-language-secondary-cancel-download"]`,
+ ],
+ // Unexpected selectors:
+ [
+ `button[disabled] [data-l10n-id="onboarding-live-language-waiting-button"]`,
+ ]
+ );
+
+ eventsMatch(flushClickTelemetry(), [
+ {
+ event: "CLICK_BUTTON",
+ event_context: {
+ source: "download_langpack",
+ page: "about:welcome",
+ },
+ message_id: "MR_WELCOME_DEFAULT_1_AW_LANGUAGE_MISMATCH",
+ },
+ ]);
+
+ sinon.assert.notCalled(mockable.setRequestedAppLocales);
+
+ await resolveInstaller();
+
+ await testScreenContent(
+ browser,
+ "Language changed",
+ // Expected selectors:
+ [`.screen.AW_IMPORT_SETTINGS_EMBEDDED`],
+ // Unexpected selectors:
+ liveLanguageSwitchSelectors
+ );
+
+ info("The app locale was changed to the OS locale.");
+ sinon.assert.calledWith(mockable.setRequestedAppLocales, ["es-ES", "en-US"]);
+
+ eventsMatch(flushClickTelemetry(), [
+ {
+ event: "CLICK_BUTTON",
+ event_context: {
+ source: "download_complete",
+ page: "about:welcome",
+ },
+ message_id: "MR_WELCOME_DEFAULT_1_AW_LANGUAGE_MISMATCH",
+ },
+ ]);
+});
+
+/**
+ * Test declining the about:welcome offer to change the Firefox language when
+ * there is a mismatch between the operating system language and the Firefox
+ * language.
+ */
+add_task(async function test_aboutwelcome_languageSwitcher_decline() {
+ sandbox.restore();
+ const { resolveLangPacks, resolveInstaller, mockable } =
+ mockAddonAndLocaleAPIs({
+ systemLocale: "es-ES",
+ appLocale: "en-US",
+ });
+
+ const { browser, flushClickTelemetry } = await openAboutWelcome();
+ await testScreenContent(
+ browser,
+ "First Screen primary CTA loaded",
+ // Expected selectors:
+ [`button.primary[value="primary_button"]`],
+ // Unexpected selectors:
+ []
+ );
+
+ info("Clicking the primary button to view language switching page.");
+ await clickVisibleButton(browser, `button.primary[value="primary_button"]`);
+
+ await testScreenContent(
+ browser,
+ "Live language switching (waiting for languages)",
+ // Expected selectors:
+ [
+ ...liveLanguageSwitchSelectors,
+ `[data-l10n-id="mr2022-onboarding-live-language-text"]`,
+ `button[disabled] [data-l10n-id="onboarding-live-language-waiting-button"]`,
+ `[data-l10n-id="mr2022-onboarding-secondary-skip-button-label"]`,
+ ],
+ // Unexpected selectors:
+ []
+ );
+
+ // Ignore the telemetry of the initial welcome screen.
+ flushClickTelemetry();
+
+ resolveLangPacks(["es-MX", "es-ES", "fr-FR"]);
+ resolveInstaller();
+
+ await testScreenContent(
+ browser,
+ "Live language switching, asking for a language",
+ // Expected selectors:
+ [
+ ...liveLanguageSwitchSelectors,
+ `button.primary[value="primary_button"]`,
+ `button.primary[value="decline"]`,
+ ],
+ // Unexpected selectors:
+ [
+ `button[disabled] [data-l10n-id="onboarding-live-language-waiting-button"]`,
+ `[data-l10n-id="mr2022-onboarding-secondary-skip-button-label"]`,
+ ]
+ );
+
+ sinon.assert.notCalled(mockable.setRequestedAppLocales);
+
+ info("Clicking the secondary button to skip installing the langpack.");
+ await clickVisibleButton(browser, `button.primary[value="decline"]`);
+
+ await testScreenContent(
+ browser,
+ "Language selection declined",
+ // Expected selectors:
+ [`.screen.AW_IMPORT_SETTINGS_EMBEDDED`],
+ // Unexpected selectors:
+ liveLanguageSwitchSelectors
+ );
+
+ info("The requested locale should be set to the original en-US");
+ sinon.assert.calledWith(mockable.setRequestedAppLocales, ["en-US"]);
+
+ eventsMatch(flushClickTelemetry(), [
+ {
+ event: "CLICK_BUTTON",
+ event_context: {
+ source: "decline",
+ page: "about:welcome",
+ },
+ message_id: "MR_WELCOME_DEFAULT_1_AW_LANGUAGE_MISMATCH",
+ },
+ ]);
+});
+
+/**
+ * Ensure the langpack can be installed before the user gets to the language screen.
+ */
+add_task(async function test_aboutwelcome_languageSwitcher_asyncCalls() {
+ sandbox.restore();
+ const { resolveLangPacks, resolveInstaller, mockable } =
+ mockAddonAndLocaleAPIs({
+ systemLocale: "es-ES",
+ appLocale: "en-US",
+ });
+
+ await openAboutWelcome();
+
+ info("Waiting for getAvailableLangpacks to be called.");
+ await TestUtils.waitForCondition(
+ () => mockable.getAvailableLangpacks.called,
+ "getAvailableLangpacks called once"
+ );
+ ok(mockable.installLangPack.notCalled);
+
+ resolveLangPacks(["es-MX", "es-ES", "fr-FR"]);
+
+ await TestUtils.waitForCondition(
+ () => mockable.installLangPack.called,
+ "installLangPack was called once"
+ );
+ ok(mockable.getAvailableLangpacks.called);
+
+ resolveInstaller();
+});
+
+/**
+ * Test that the "en-US" langpack is installed, if it's already available as the last
+ * fallback locale.
+ */
+add_task(async function test_aboutwelcome_fallback_locale() {
+ sandbox.restore();
+ const { resolveLangPacks, resolveInstaller, mockable } =
+ mockAddonAndLocaleAPIs({
+ systemLocale: "en-US",
+ appLocale: "it",
+ });
+
+ await openAboutWelcome();
+
+ info("Waiting for getAvailableLangpacks to be called.");
+ await TestUtils.waitForCondition(
+ () => mockable.getAvailableLangpacks.called,
+ "getAvailableLangpacks called once"
+ );
+ ok(mockable.installLangPack.notCalled);
+
+ resolveLangPacks(["en-US"]);
+
+ await TestUtils.waitForCondition(
+ () => mockable.installLangPack.called,
+ "installLangPack was called once"
+ );
+ ok(mockable.getAvailableLangpacks.called);
+
+ resolveInstaller();
+});
+
+/**
+ * Test when AMO does not have a matching language.
+ */
+add_task(async function test_aboutwelcome_languageSwitcher_noMatch() {
+ sandbox.restore();
+ const { resolveLangPacks, mockable } = mockAddonAndLocaleAPIs({
+ systemLocale: "tlh", // Klingon
+ appLocale: "en-US",
+ });
+
+ const { browser } = await openAboutWelcome();
+
+ info("Clicking the primary button to start installing the langpack.");
+ await clickVisibleButton(browser, `button.primary[value="primary_button"]`);
+
+ // Klingon is not supported.
+ resolveLangPacks(["es-MX", "es-ES", "fr-FR"]);
+
+ await testScreenContent(
+ browser,
+ "Language selection skipped",
+ // Expected selectors:
+ [`.screen.AW_IMPORT_SETTINGS_EMBEDDED`],
+ // Unexpected selectors:
+ [
+ `[data-l10n-id*="onboarding-live-language"]`,
+ `[data-l10n-id="onboarding-live-language-header"]`,
+ ]
+ );
+ sinon.assert.notCalled(mockable.setRequestedAppLocales);
+});
+
+/**
+ * Test when bidi live reloading is not supported.
+ */
+add_task(async function test_aboutwelcome_languageSwitcher_bidiNotSupported() {
+ sandbox.restore();
+ await pushPrefs(["intl.multilingual.liveReloadBidirectional", false]);
+
+ const { mockable } = mockAddonAndLocaleAPIs({
+ systemLocale: "ar-EG", // Arabic (Egypt)
+ appLocale: "en-US",
+ });
+
+ const { browser } = await openAboutWelcome();
+
+ info("Clicking the primary button to start installing the langpack.");
+ await clickVisibleButton(browser, `button.primary[value="primary_button"]`);
+
+ await testScreenContent(
+ browser,
+ "Language selection skipped for bidi",
+ // Expected selectors:
+ [`.screen.AW_IMPORT_SETTINGS_EMBEDDED`],
+ // Unexpected selectors:
+ [
+ `[data-l10n-id*="onboarding-live-language"]`,
+ `[data-l10n-id="onboarding-live-language-header"]`,
+ ]
+ );
+
+ sinon.assert.notCalled(mockable.setRequestedAppLocales);
+});
+
+/**
+ * Test when bidi live reloading is not supported and no langpacks.
+ */
+add_task(
+ async function test_aboutwelcome_languageSwitcher_bidiNotSupported_noLangPacks() {
+ sandbox.restore();
+ await pushPrefs(["intl.multilingual.liveReloadBidirectional", false]);
+
+ const { resolveLangPacks, mockable } = mockAddonAndLocaleAPIs({
+ systemLocale: "ar-EG", // Arabic (Egypt)
+ appLocale: "en-US",
+ });
+ resolveLangPacks([]);
+
+ const { browser } = await openAboutWelcome();
+
+ info("Clicking the primary button to start installing the langpack.");
+ await clickVisibleButton(browser, `button.primary[value="primary_button"]`);
+
+ await testScreenContent(
+ browser,
+ "Language selection skipped for bidi",
+ // Expected selectors:
+ [`.screen.AW_IMPORT_SETTINGS_EMBEDDED`],
+ // Unexpected selectors:
+ [
+ `[data-l10n-id*="onboarding-live-language"]`,
+ `[data-l10n-id="onboarding-live-language-header"]`,
+ ]
+ );
+
+ sinon.assert.notCalled(mockable.setRequestedAppLocales);
+ }
+);
+
+/**
+ * Test when bidi live reloading is supported.
+ */
+add_task(async function test_aboutwelcome_languageSwitcher_bidiNotSupported() {
+ sandbox.restore();
+ await pushPrefs(["intl.multilingual.liveReloadBidirectional", true]);
+
+ const { resolveLangPacks, mockable } = mockAddonAndLocaleAPIs({
+ systemLocale: "ar-EG", // Arabic (Egypt)
+ appLocale: "en-US",
+ });
+
+ const { browser } = await openAboutWelcome();
+
+ info("Clicking the primary button to start installing the langpack.");
+ await clickVisibleButton(browser, `button.primary[value="primary_button"]`);
+
+ resolveLangPacks(["ar-EG", "es-ES", "fr-FR"]);
+
+ await testScreenContent(
+ browser,
+ "Live language switching with bidi supported",
+ // Expected selectors:
+ [...liveLanguageSwitchSelectors],
+ // Unexpected selectors:
+ []
+ );
+
+ sinon.assert.notCalled(mockable.setRequestedAppLocales);
+});
+
+/**
+ * Test hitting the cancel button when waiting on a langpack.
+ */
+add_task(async function test_aboutwelcome_languageSwitcher_cancelWaiting() {
+ sandbox.restore();
+ const { resolveLangPacks, resolveInstaller, mockable } =
+ mockAddonAndLocaleAPIs({
+ systemLocale: "es-ES",
+ appLocale: "en-US",
+ });
+
+ const { browser, flushClickTelemetry } = await openAboutWelcome();
+
+ info("Clicking the primary button to start the onboarding process.");
+ await clickVisibleButton(browser, `button.primary[value="primary_button"]`);
+ resolveLangPacks(["es-MX", "es-ES", "fr-FR"]);
+
+ await testScreenContent(
+ browser,
+ "Live language switching, asking for a language",
+ // Expected selectors:
+ liveLanguageSwitchSelectors,
+ // Unexpected selectors:
+ []
+ );
+
+ info("Clicking the primary button to view language switching page.");
+ await clickVisibleButton(browser, `button.primary[value="primary_button"]`);
+
+ await testScreenContent(
+ browser,
+ "Live language switching, waiting for langpack to download",
+ // Expected selectors:
+ [
+ ...liveLanguageSwitchSelectors,
+ `[data-l10n-id="onboarding-live-language-button-label-downloading"]`,
+ `[data-l10n-id="onboarding-live-language-secondary-cancel-download"]`,
+ ],
+ // Unexpected selectors:
+ [
+ `button[disabled] [data-l10n-id="onboarding-live-language-waiting-button"]`,
+ ]
+ );
+
+ // Ignore all the telemetry up to this point.
+ flushClickTelemetry();
+
+ info("Cancel the request for the language");
+ await clickVisibleButton(browser, "button.secondary");
+
+ await testScreenContent(
+ browser,
+ "Language selection declined waiting",
+ // Expected selectors:
+ [`.screen.AW_IMPORT_SETTINGS_EMBEDDED`],
+ // Unexpected selectors:
+ liveLanguageSwitchSelectors
+ );
+
+ eventsMatch(flushClickTelemetry(), [
+ {
+ event: "CLICK_BUTTON",
+ event_context: {
+ source: "cancel_waiting",
+ page: "about:welcome",
+ },
+ message_id: "MR_WELCOME_DEFAULT_1_AW_LANGUAGE_MISMATCH",
+ },
+ ]);
+
+ await resolveInstaller();
+
+ is(flushClickTelemetry().length, 0);
+ sinon.assert.notCalled(mockable.setRequestedAppLocales);
+});
+
+/**
+ * Test MR About Welcome language mismatch screen
+ */
+add_task(async function test_aboutwelcome_languageSwitcher_MR() {
+ sandbox.restore();
+
+ const { resolveLangPacks, resolveInstaller } = mockAddonAndLocaleAPIs({
+ systemLocale: "es-ES",
+ appLocale: "en-US",
+ });
+
+ const { browser } = await openAboutWelcome(true);
+
+ info("Clicking the primary button to view language switching screen.");
+ await clickVisibleButton(browser, `button.primary[value="primary_button"]`);
+
+ resolveLangPacks(["es-AR"]);
+ await testScreenContent(
+ browser,
+ "Live language switching, asking for a language",
+ // Expected selectors:
+ [
+ `#mainContentHeader[data-l10n-id="mr2022-onboarding-live-language-text"]`,
+ `[data-l10n-id="mr2022-language-mismatch-subtitle"]`,
+ `.section-secondary [data-l10n-id="mr2022-onboarding-live-language-text"]`,
+ `[data-l10n-id="mr2022-onboarding-live-language-switch-to"]`,
+ `button.primary[value="primary_button"]`,
+ `button.primary[value="decline"]`,
+ ],
+ // Unexpected selectors:
+ [`[data-l10n-id="onboarding-live-language-header"]`]
+ );
+
+ await resolveInstaller();
+ await testScreenContent(
+ browser,
+ "Switched some to langpack (raw) strings after install",
+ // Expected selectors:
+ [`#mainContentHeader[data-l10n-id="mr2022-onboarding-live-language-text"]`],
+ // Unexpected selectors:
+ [
+ `.section-secondary [data-l10n-id="mr2022-onboarding-live-language-text"]`,
+ `[data-l10n-id="mr2022-onboarding-live-language-switch-to"]`,
+ ]
+ );
+});
diff --git a/browser/components/aboutwelcome/tests/browser/browser_aboutwelcome_multistage_mr.js b/browser/components/aboutwelcome/tests/browser/browser_aboutwelcome_multistage_mr.js
new file mode 100644
index 0000000000..1832b75778
--- /dev/null
+++ b/browser/components/aboutwelcome/tests/browser/browser_aboutwelcome_multistage_mr.js
@@ -0,0 +1,770 @@
+"use strict";
+
+const { AboutWelcomeParent } = ChromeUtils.importESModule(
+ "resource:///actors/AboutWelcomeParent.sys.mjs"
+);
+
+const { AboutWelcomeTelemetry } = ChromeUtils.importESModule(
+ "resource:///modules/aboutwelcome/AboutWelcomeTelemetry.sys.mjs"
+);
+const { AWScreenUtils } = ChromeUtils.importESModule(
+ "resource:///modules/aboutwelcome/AWScreenUtils.sys.mjs"
+);
+const { InternalTestingProfileMigrator } = ChromeUtils.importESModule(
+ "resource:///modules/InternalTestingProfileMigrator.sys.mjs"
+);
+
+async function clickVisibleButton(browser, selector) {
+ // eslint-disable-next-line no-shadow
+ await ContentTask.spawn(browser, { selector }, async ({ selector }) => {
+ function getVisibleElement() {
+ for (const el of content.document.querySelectorAll(selector)) {
+ if (el.offsetParent !== null) {
+ return el;
+ }
+ }
+ return null;
+ }
+ await ContentTaskUtils.waitForCondition(
+ getVisibleElement,
+ selector,
+ 200, // interval
+ 100 // maxTries
+ );
+ getVisibleElement().click();
+ });
+}
+
+add_setup(async function () {
+ SpecialPowers.pushPrefEnv({
+ set: [
+ ["ui.prefersReducedMotion", 1],
+ ["browser.aboutwelcome.transitions", false],
+ ["browser.shell.checkDefaultBrowser", true],
+ ],
+ });
+});
+
+/**
+ * Test MR message telemetry
+ */
+add_task(async function test_aboutwelcome_mr_template_telemetry() {
+ const sandbox = sinon.createSandbox();
+
+ let { browser, cleanup } = await openMRAboutWelcome();
+ let aboutWelcomeActor = await getAboutWelcomeParent(browser);
+ // Stub AboutWelcomeParent's Content Message Handler
+ const messageStub = sandbox.spy(aboutWelcomeActor, "onContentMessage");
+ await clickVisibleButton(browser, ".action-buttons button.secondary");
+
+ const { callCount } = messageStub;
+ Assert.greaterOrEqual(callCount, 1, `${callCount} Stub was called`);
+ let clickCall;
+ for (let i = 0; i < callCount; i++) {
+ const call = messageStub.getCall(i);
+ info(`Call #${i}: ${call.args[0]} ${JSON.stringify(call.args[1])}`);
+ if (call.calledWithMatch("", { event: "CLICK_BUTTON" })) {
+ clickCall = call;
+ }
+ }
+
+ Assert.ok(
+ clickCall.args[1].message_id.startsWith("MR_WELCOME_DEFAULT"),
+ "Telemetry includes MR message id"
+ );
+
+ await cleanup();
+ sandbox.restore();
+});
+
+/**
+ * Telemetry Impression with Easy Setup Need Default and Pin as First Screen
+ */
+add_task(async function test_aboutwelcome_easy_setup_screen_impression() {
+ const sandbox = sinon.createSandbox();
+ sandbox
+ .stub(AWScreenUtils, "evaluateScreenTargeting")
+ .resolves(false)
+ .withArgs(
+ "doesAppNeedPin && 'browser.shell.checkDefaultBrowser'|preferenceValue && !isDefaultBrowser"
+ )
+ .resolves(true)
+ .withArgs("isDeviceMigration")
+ .resolves(false);
+
+ let impressionSpy = sandbox.spy(
+ AboutWelcomeTelemetry.prototype,
+ "sendTelemetry"
+ );
+
+ let { browser, cleanup } = await openMRAboutWelcome();
+ // Wait for screen elements to render before checking impression pings
+ await test_screen_content(
+ browser,
+ "Onboarding screen elements rendered",
+ // Expected selectors:
+ [
+ `main.screen[pos="split"]`,
+ "div.secondary-cta.top",
+ "button[value='secondary_button_top']",
+ ]
+ );
+
+ const { callCount } = impressionSpy;
+ Assert.greaterOrEqual(callCount, 1, `${callCount} impressionSpy was called`);
+ let impressionCall;
+ for (let i = 0; i < callCount; i++) {
+ const call = impressionSpy.getCall(i);
+ info(`Call #${i}: ${JSON.stringify(call.args[0])}`);
+ if (
+ call.calledWithMatch({ event: "IMPRESSION" }) &&
+ !call.calledWithMatch({ message_id: "MR_WELCOME_DEFAULT" })
+ ) {
+ info(`Screen Impression Call #${i}: ${JSON.stringify(call.args[0])}`);
+ impressionCall = call;
+ }
+ }
+
+ Assert.ok(
+ impressionCall.args[0].message_id.startsWith(
+ "MR_WELCOME_DEFAULT_0_AW_EASY_SETUP_NEEDS_DEFAULT_AND_PIN"
+ ),
+ "Impression telemetry includes correct message id"
+ );
+ await cleanup();
+ sandbox.restore();
+});
+
+add_task(async function test_aboutwelcome_gratitude() {
+ const TEST_CONTENT = [
+ {
+ id: "AW_GRATITUDE",
+ content: {
+ position: "split",
+ split_narrow_bkg_position: "-228px",
+ background:
+ "url('chrome://activity-stream/content/data/content/assets/mr-gratitude.svg') var(--mr-secondary-position) no-repeat, var(--mr-screen-background-color)",
+ progress_bar: true,
+ logo: {},
+ title: {
+ string_id: "mr2022-onboarding-gratitude-title",
+ },
+ subtitle: {
+ string_id: "mr2022-onboarding-gratitude-subtitle",
+ },
+ primary_button: {
+ label: {
+ string_id: "mr2022-onboarding-gratitude-primary-button-label",
+ },
+ action: {
+ navigate: true,
+ },
+ },
+ },
+ },
+ ];
+ await setAboutWelcomeMultiStage(JSON.stringify(TEST_CONTENT)); // NB: calls SpecialPowers.pushPrefEnv
+ let { cleanup, browser } = await openMRAboutWelcome();
+
+ // execution
+ await test_screen_content(
+ browser,
+ "doesn't render secondary button on gratitude screen",
+ //Expected selectors
+ ["main.AW_GRATITUDE", "button[value='primary_button']"],
+
+ //Unexpected selectors:
+ ["button[value='secondary_button']"]
+ );
+ await clickVisibleButton(browser, ".action-buttons button.primary");
+
+ // make sure the button navigates to newtab
+ await test_screen_content(
+ browser,
+ "home",
+ //Expected selectors
+ ["body.activity-stream"],
+
+ //Unexpected selectors:
+ ["main.AW_GRATITUDE"]
+ );
+
+ // cleanup
+ await SpecialPowers.popPrefEnv(); // for setAboutWelcomeMultiStage
+ await cleanup();
+});
+
+add_task(async function test_aboutwelcome_embedded_migration() {
+ // Let's make sure at least one migrator is available and enabled - the
+ // InternalTestingProfileMigrator.
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.migrate.internal-testing.enabled", true]],
+ });
+
+ const sandbox = sinon.createSandbox();
+ sandbox
+ .stub(InternalTestingProfileMigrator.prototype, "getResources")
+ .callsFake(() =>
+ Promise.resolve([
+ {
+ type: MigrationUtils.resourceTypes.BOOKMARKS,
+ migrate: () => {},
+ },
+ ])
+ );
+ sandbox.stub(MigrationUtils, "_importQuantities").value({
+ bookmarks: 123,
+ history: 123,
+ logins: 123,
+ });
+ const migrated = new Promise(resolve => {
+ sandbox
+ .stub(InternalTestingProfileMigrator.prototype, "migrate")
+ .callsFake((aResourceTypes, aStartup, aProfile, aProgressCallback) => {
+ aProgressCallback(MigrationUtils.resourceTypes.BOOKMARKS);
+ Services.obs.notifyObservers(null, "Migration:Ended");
+ resolve();
+ });
+ });
+
+ let telemetrySpy = sandbox.spy(
+ AboutWelcomeTelemetry.prototype,
+ "sendTelemetry"
+ );
+
+ const TEST_CONTENT = [
+ {
+ id: "AW_IMPORT_SETTINGS_EMBEDDED",
+ content: {
+ tiles: { type: "migration-wizard" },
+ position: "split",
+ split_narrow_bkg_position: "-42px",
+ image_alt_text: {
+ string_id: "mr2022-onboarding-import-image-alt",
+ },
+ background:
+ "url('chrome://activity-stream/content/data/content/assets/mr-import.svg') var(--mr-secondary-position) no-repeat var(--mr-screen-background-color)",
+ progress_bar: true,
+ migrate_start: {
+ action: {},
+ },
+ migrate_close: {
+ action: { navigate: true },
+ },
+ secondary_button: {
+ label: {
+ string_id: "mr2022-onboarding-secondary-skip-button-label",
+ },
+ action: {
+ navigate: true,
+ },
+ has_arrow_icon: true,
+ },
+ },
+ },
+ {
+ id: "AW_STEP2",
+ content: {
+ position: "split",
+ split_narrow_bkg_position: "-228px",
+ background:
+ "url('chrome://activity-stream/content/data/content/assets/mr-gratitude.svg') var(--mr-secondary-position) no-repeat, var(--mr-screen-background-color)",
+ progress_bar: true,
+ logo: {},
+ title: {
+ string_id: "mr2022-onboarding-gratitude-title",
+ },
+ subtitle: {
+ string_id: "mr2022-onboarding-gratitude-subtitle",
+ },
+ primary_button: {
+ label: {
+ string_id: "mr2022-onboarding-gratitude-primary-button-label",
+ },
+ action: {
+ navigate: true,
+ },
+ },
+ },
+ },
+ ];
+
+ await setAboutWelcomeMultiStage(JSON.stringify(TEST_CONTENT)); // NB: calls SpecialPowers.pushPrefEnv
+ let { cleanup, browser } = await openMRAboutWelcome();
+
+ // execution
+ await test_screen_content(
+ browser,
+ "Renders a <migration-wizard> custom element",
+ // We expect <migration-wizard> to automatically request the set of migrators
+ // upon binding to the DOM, and to not be in dialog mode.
+ [
+ "main.AW_IMPORT_SETTINGS_EMBEDDED",
+ "migration-wizard[auto-request-state]:not([dialog-mode])",
+ ]
+ );
+
+ // Do a basic test to make sure that the <migration-wizard> is on the right
+ // page and the <panel-list> can open.
+ await SpecialPowers.spawn(
+ browser,
+ [`panel-item[key="${InternalTestingProfileMigrator.key}"]`],
+ async menuitemSelector => {
+ const { MigrationWizardConstants } = ChromeUtils.importESModule(
+ "chrome://browser/content/migration/migration-wizard-constants.mjs"
+ );
+
+ let wizard = content.document.querySelector("migration-wizard");
+ await new Promise(resolve => content.requestAnimationFrame(resolve));
+ let shadow = wizard.openOrClosedShadowRoot;
+ let deck = shadow.querySelector("#wizard-deck");
+
+ // It's unlikely but possible that the deck might not yet be showing the
+ // selection page yet, in which case we wait for that page to appear.
+ if (deck.selectedViewName !== MigrationWizardConstants.PAGES.SELECTION) {
+ await ContentTaskUtils.waitForMutationCondition(
+ deck,
+ { attributeFilter: ["selected-view"] },
+ () => {
+ return (
+ deck.getAttribute("selected-view") ===
+ `page-${MigrationWizardConstants.PAGES.SELECTION}`
+ );
+ }
+ );
+ }
+
+ Assert.ok(true, "Selection page is being shown in the migration wizard.");
+
+ // Now let's make sure that the <panel-list> can appear.
+ let panelList = shadow.querySelector("panel-list");
+ Assert.ok(panelList, "Found the <panel-list>.");
+
+ // The "shown" event from the panel-list is coming from a lower level
+ // of privilege than where we're executing this SpecialPowers.spawn
+ // task. In order to properly listen for it, we have to ask
+ // ContentTaskUtils.waitForEvent to listen for untrusted events.
+ let shown = ContentTaskUtils.waitForEvent(
+ panelList,
+ "shown",
+ false /* capture */,
+ null /* checkFn */,
+ true /* wantsUntrusted */
+ );
+ let selector = shadow.querySelector("#browser-profile-selector");
+
+ // The migration wizard programmatically focuses the selector after
+ // the selection page is shown using an rAF. If we click the button
+ // before that occurs, then the focus can shift after the panel opens
+ // which will cause it to immediately close again. So we wait for the
+ // selection button to gain focus before continuing.
+ if (!selector.matches(":focus")) {
+ await ContentTaskUtils.waitForEvent(selector, "focus");
+ }
+
+ selector.click();
+ await shown;
+
+ let panelRect = panelList.getBoundingClientRect();
+ let selectorRect = selector.getBoundingClientRect();
+
+ // Recalculate the <panel-list> rect top value relative to the top-left
+ // of the selectorRect. We expect the <panel-list> to be tightly anchored
+ // to the bottom of the <button>, so we expect this new value to be close to 0,
+ // to account for subpixel rounding
+ let panelTopLeftRelativeToAnchorTopLeft =
+ panelRect.top - selectorRect.top - selectorRect.height;
+
+ function isfuzzy(actual, expected, epsilon, msg) {
+ if (actual >= expected - epsilon && actual <= expected + epsilon) {
+ ok(true, msg);
+ } else {
+ is(actual, expected, msg);
+ }
+ }
+
+ isfuzzy(
+ panelTopLeftRelativeToAnchorTopLeft,
+ 0,
+ 1,
+ "Panel should be tightly anchored to the bottom of the button shadow node."
+ );
+
+ let panelItem = shadow.querySelector(menuitemSelector);
+ panelItem.click();
+
+ let importButton = shadow.querySelector("#import");
+ importButton.click();
+ }
+ );
+
+ await migrated;
+ Assert.ok(
+ telemetrySpy.calledWithMatch({
+ event: "CLICK_BUTTON",
+ event_context: { source: "primary_button", page: "about:welcome" },
+ message_id: sinon.match.string,
+ }),
+ "Should have sent telemetry for clicking the 'Import' button."
+ );
+
+ await SpecialPowers.spawn(browser, [], async () => {
+ let wizard = content.document.querySelector("migration-wizard");
+ let shadow = wizard.openOrClosedShadowRoot;
+ let continueButton = shadow.querySelector(
+ "div[name='page-progress'] .continue-button"
+ );
+ continueButton.click();
+ await ContentTaskUtils.waitForCondition(
+ () => content.document.querySelector("main.AW_STEP2"),
+ "Waiting for step 2 to render"
+ );
+ });
+
+ Assert.ok(
+ telemetrySpy.calledWithMatch({
+ event: "CLICK_BUTTON",
+ event_context: { source: "migrate_close", page: "about:welcome" },
+ message_id: sinon.match.string,
+ }),
+ "Should have sent telemetry for clicking the 'Continue' button."
+ );
+
+ // Ensure that we can go back and get the migration wizard to appear
+ // again.
+ await SpecialPowers.spawn(browser, [], async () => {
+ const { MigrationWizardConstants } = ChromeUtils.importESModule(
+ "chrome://browser/content/migration/migration-wizard-constants.mjs"
+ );
+
+ let migrationWizardReady = ContentTaskUtils.waitForEvent(
+ content,
+ "MigrationWizard:Ready"
+ );
+
+ // Waiting for the history length to update seems to allow us to avoid
+ // an intermittent test failure.
+ await ContentTaskUtils.waitForCondition(() => {
+ return content.history.length === 2;
+ });
+
+ content.history.back();
+ await migrationWizardReady;
+
+ let wizard = content.document.querySelector("migration-wizard");
+ let shadow = wizard.openOrClosedShadowRoot;
+ let deck = shadow.querySelector("#wizard-deck");
+
+ Assert.equal(
+ deck.getAttribute("selected-view"),
+ `page-${MigrationWizardConstants.PAGES.SELECTION}`
+ );
+ });
+
+ // cleanup
+ await SpecialPowers.popPrefEnv(); // for the InternalTestingProfileMigrator.
+ await SpecialPowers.popPrefEnv(); // for setAboutWelcomeMultiStage
+ await cleanup();
+ sandbox.restore();
+ let migrator = await MigrationUtils.getMigrator(
+ InternalTestingProfileMigrator.key
+ );
+ migrator.flushResourceCache();
+});
+
+add_task(async function test_aboutwelcome_multiselect() {
+ const TEST_SCREENS = [
+ {
+ id: "AW_EASY_SETUP_X",
+ content: {
+ position: "split",
+ split_narrow_bkg_position: "-60px",
+ progress_bar: true,
+ logo: {},
+ title: { string_id: "mr2022-onboarding-set-default-title" },
+ tiles: {
+ type: "multiselect",
+ style: { flexDirection: "column", alignItems: "flex-start" },
+ data: [
+ {
+ id: "radio-1",
+ type: "radio",
+ group: "radios",
+ defaultValue: true,
+ label: {
+ raw: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.",
+ },
+ action: { type: "OPEN_PROTECTION_REPORT" },
+ },
+ {
+ id: "radio-2",
+ type: "radio",
+ group: "radios",
+ defaultValue: false,
+ label: {
+ raw: "Nulla facilisi nullam vehicula ipsum a arcu cursus vitae.",
+ },
+ action: { type: "OPEN_FIREFOX_VIEW" },
+ },
+ {
+ id: "radio-3",
+ type: "radio",
+ group: "radios",
+ defaultValue: false,
+ label: { raw: "Natoque penatibus et magnis dis." },
+ action: { type: "OPEN_PRIVATE_BROWSER_WINDOW" },
+ },
+ ],
+ },
+ secondary_button: {
+ label: {
+ string_id: "mr2022-onboarding-secondary-skip-button-label",
+ },
+ action: { navigate: true },
+ has_arrow_icon: true,
+ },
+ },
+ },
+ {
+ id: "AW_EASY_SETUP_Y",
+ content: {
+ position: "split",
+ split_narrow_bkg_position: "-60px",
+ progress_bar: true,
+ logo: {},
+ title: { string_id: "mr2022-onboarding-set-default-title" },
+ tiles: {
+ type: "multiselect",
+ style: { flexDirection: "row", gap: "24px" },
+ data: [
+ {
+ id: "checkbox-1",
+ defaultValue: true,
+ label: { raw: "Test1" },
+ action: { type: "OPEN_PROTECTION_REPORT" },
+ },
+ {
+ id: "checkbox-2",
+ defaultValue: true,
+ label: { raw: "Test2" },
+ action: { type: "OPEN_FIREFOX_VIEW" },
+ },
+ {
+ id: "radio-1",
+ type: "radio",
+ group: "radios",
+ defaultValue: true,
+ label: { raw: "Test3" },
+ action: { type: "OPEN_PROTECTION_REPORT" },
+ },
+ {
+ id: "radio-2",
+ type: "radio",
+ group: "radios",
+ defaultValue: false,
+ label: { raw: "Test4" },
+ action: { type: "OPEN_FIREFOX_VIEW" },
+ },
+ {
+ id: "radio-3",
+ type: "radio",
+ group: "radios",
+ defaultValue: false,
+ label: { raw: "Test5" },
+ action: { type: "OPEN_PRIVATE_BROWSER_WINDOW" },
+ },
+ ],
+ },
+ secondary_button: {
+ label: {
+ string_id: "mr2022-onboarding-secondary-skip-button-label",
+ },
+ action: { navigate: true },
+ has_arrow_icon: true,
+ },
+ },
+ },
+ {
+ id: "AW_EASY_SETUP_Z",
+ content: {
+ position: "split",
+ split_narrow_bkg_position: "-60px",
+ progress_bar: true,
+ logo: {},
+ title: { string_id: "mr2022-onboarding-set-default-title" },
+ tiles: {
+ type: "multiselect",
+ style: {
+ flexDirection: "column",
+ flexShrink: 1,
+ justifyContent: "flex-start",
+ },
+ data: [
+ {
+ id: "checkbox-1",
+ defaultValue: true,
+ label: { raw: "Test1" },
+ action: { type: "OPEN_PROTECTION_REPORT" },
+ },
+ {
+ id: "checkbox-2",
+ defaultValue: true,
+ label: { raw: "Test2" },
+ action: { type: "OPEN_FIREFOX_VIEW" },
+ },
+ {
+ id: "radio-1",
+ type: "radio",
+ group: "radios",
+ defaultValue: true,
+ label: { raw: "Test3" },
+ action: { type: "OPEN_PROTECTION_REPORT" },
+ },
+ {
+ id: "radio-2",
+ type: "radio",
+ group: "radios",
+ defaultValue: false,
+ label: { raw: "Test4" },
+ action: { type: "OPEN_FIREFOX_VIEW" },
+ },
+ {
+ id: "radio-3",
+ type: "radio",
+ group: "radios",
+ defaultValue: false,
+ label: { raw: "Test5" },
+ action: { type: "OPEN_PRIVATE_BROWSER_WINDOW" },
+ },
+ ],
+ },
+ secondary_button: {
+ label: {
+ string_id: "mr2022-onboarding-secondary-skip-button-label",
+ },
+ action: { navigate: true },
+ has_arrow_icon: true,
+ },
+ },
+ },
+ ];
+
+ const sandbox = sinon.createSandbox();
+ sandbox.stub(AWScreenUtils, "addScreenImpression").resolves();
+
+ await setAboutWelcomeMultiStage(JSON.stringify(TEST_SCREENS));
+ let { cleanup, browser } = await openMRAboutWelcome();
+
+ await test_screen_content(
+ browser,
+ "renders default screen",
+ ["main.AW_EASY_SETUP_X", "#radio-1:checked"],
+ ["#radio-2:checked", "#radio-3:checked"]
+ );
+
+ await clickVisibleButton(browser, "#radio-3");
+
+ await test_screen_content(
+ browser,
+ "renders radio button selection",
+ ["main.AW_EASY_SETUP_X", "#radio-3:checked"],
+ ["#radio-1:checked", "#radio-2:checked"]
+ );
+
+ await test_element_styles(
+ browser,
+ ".multi-select-container",
+ { flexDirection: "column", alignItems: "flex-start" },
+ {}
+ );
+
+ await clickVisibleButton(browser, ".action-buttons button.secondary");
+
+ await test_screen_content(
+ browser,
+ "renders screen 2",
+ [
+ "main.AW_EASY_SETUP_Y",
+ "#checkbox-1:checked",
+ "#checkbox-2:checked",
+ "#radio-1:checked",
+ ],
+ ["#radio-2:checked", "#radio-3:checked"]
+ );
+
+ await clickVisibleButton(browser, "#checkbox-1");
+ await clickVisibleButton(browser, "#checkbox-2");
+ await clickVisibleButton(browser, "#radio-2");
+
+ await test_screen_content(
+ browser,
+ "renders checkbox and radio button selection",
+ ["main.AW_EASY_SETUP_Y", "#radio-2:checked"],
+ ["#checkbox-1:checked", "#checkbox-2:checked", "#radio-1:checked"]
+ );
+
+ await test_element_styles(
+ browser,
+ ".multi-select-container",
+ { flexDirection: "row", gap: "24px" },
+ {}
+ );
+
+ browser.goBack();
+
+ await test_screen_content(
+ browser,
+ "renders screen 1 and remembers selection",
+ ["main.AW_EASY_SETUP_X", "#radio-3:checked"],
+ ["#radio-1:checked", "#radio-2:checked"]
+ );
+
+ browser.goForward();
+
+ await test_screen_content(
+ browser,
+ "renders screen 2 and remembers selection",
+ ["main.AW_EASY_SETUP_Y", "#radio-2:checked"],
+ ["#checkbox-1:checked", "#checkbox-2:checked", "#radio-1:checked"]
+ );
+
+ await clickVisibleButton(browser, ".action-buttons button.secondary");
+
+ await test_screen_content(
+ browser,
+ "renders screen 3",
+ [
+ "main.AW_EASY_SETUP_Z",
+ "#checkbox-1:checked",
+ "#checkbox-2:checked",
+ "#radio-1:checked",
+ ],
+ ["#radio-2:checked", "#radio-3:checked"]
+ );
+
+ await clickVisibleButton(browser, "#radio-3");
+
+ await test_screen_content(
+ browser,
+ "renders radio button selection without removing checkbox selection",
+ [
+ "main.AW_EASY_SETUP_Z",
+ "#checkbox-1:checked",
+ "#checkbox-2:checked",
+ "#radio-3:checked",
+ ],
+ ["#radio-1:checked", "#radio-2:checked"]
+ );
+
+ await test_element_styles(
+ browser,
+ ".multi-select-container",
+ { flexDirection: "column", flexShrink: 1, justifyContent: "flex-start" },
+ {}
+ );
+
+ await SpecialPowers.popPrefEnv();
+ await cleanup();
+
+ sandbox.restore();
+});
diff --git a/browser/components/aboutwelcome/tests/browser/browser_aboutwelcome_multistage_video.js b/browser/components/aboutwelcome/tests/browser/browser_aboutwelcome_multistage_video.js
new file mode 100644
index 0000000000..ed331e6752
--- /dev/null
+++ b/browser/components/aboutwelcome/tests/browser/browser_aboutwelcome_multistage_video.js
@@ -0,0 +1,97 @@
+"use strict";
+
+const { PermissionTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/PermissionTestUtils.sys.mjs"
+);
+
+const videoUrl =
+ "https://www.mozilla.org/tests/dom/media/webaudio/test/noaudio.webm";
+
+function testAutoplayPermission(browser) {
+ let principal = browser.contentPrincipal;
+ is(
+ PermissionTestUtils.testPermission(principal, "autoplay-media"),
+ Services.perms.ALLOW_ACTION,
+ `Autoplay is allowed on ${principal.origin}`
+ );
+}
+
+async function openAWWithVideo({
+ autoPlay = false,
+ video_url = videoUrl,
+ ...rest
+} = {}) {
+ const content = [
+ {
+ id: "VIDEO_ONBOARDING",
+ content: {
+ position: "center",
+ logo: {},
+ title: "Video onboarding",
+ secondary_button: { label: "Skip video", action: { navigate: true } },
+ video_container: {
+ video_url,
+ action: { navigate: true },
+ autoPlay,
+ ...rest,
+ },
+ },
+ },
+ ];
+ await setAboutWelcomeMultiStage(JSON.stringify(content));
+ let { cleanup, browser } = await openMRAboutWelcome();
+ return {
+ browser,
+ content,
+ async cleanup() {
+ await SpecialPowers.popPrefEnv();
+ await cleanup();
+ },
+ };
+}
+
+add_task(async function test_aboutwelcome_video_autoplay() {
+ let { cleanup, browser } = await openAWWithVideo({ autoPlay: true });
+
+ testAutoplayPermission(browser);
+
+ await SpecialPowers.spawn(browser, [videoUrl], async url => {
+ await ContentTaskUtils.waitForCondition(
+ () => content.document.querySelector("main.with-video"),
+ "Waiting for video onboarding screen"
+ );
+ let video = content.document.querySelector(`video[src='${url}'][autoplay]`);
+ await ContentTaskUtils.waitForCondition(
+ () =>
+ video.currentTime > 0 &&
+ !video.paused &&
+ !video.ended &&
+ video.readyState > 2,
+ "Waiting for video to play"
+ );
+ ok(!video.error, "Video should not have an error");
+ });
+
+ await cleanup();
+});
+
+add_task(async function test_aboutwelcome_video_no_autoplay() {
+ let { cleanup, browser } = await openAWWithVideo();
+
+ testAutoplayPermission(browser);
+
+ await SpecialPowers.spawn(browser, [videoUrl], async url => {
+ let video = await ContentTaskUtils.waitForCondition(
+ () =>
+ content.document.querySelector(`video[src='${url}']:not([autoplay])`),
+ "Waiting for video element to render"
+ );
+ await ContentTaskUtils.waitForCondition(
+ () => video.paused && !video.ended && video.readyState > 2,
+ "Waiting for video to be playable but not playing"
+ );
+ ok(!video.error, "Video should not have an error");
+ });
+
+ await cleanup();
+});
diff --git a/browser/components/aboutwelcome/tests/browser/browser_aboutwelcome_observer.js b/browser/components/aboutwelcome/tests/browser/browser_aboutwelcome_observer.js
new file mode 100644
index 0000000000..58f9059532
--- /dev/null
+++ b/browser/components/aboutwelcome/tests/browser/browser_aboutwelcome_observer.js
@@ -0,0 +1,73 @@
+"use strict";
+
+const { AboutWelcomeParent } = ChromeUtils.importESModule(
+ "resource:///actors/AboutWelcomeParent.sys.mjs"
+);
+
+async function openAboutWelcomeTab() {
+ await setAboutWelcomePref(true);
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "about:welcome"
+ );
+ registerCleanupFunction(() => {
+ BrowserTestUtils.removeTab(tab);
+ });
+ return tab;
+}
+
+/**
+ * Test simplified welcome UI tab closed terminate reason
+ */
+add_task(async function test_About_Welcome_Tab_Close() {
+ await setAboutWelcomePref(true);
+
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "about:welcome",
+ false
+ );
+
+ Assert.ok(Services.focus.activeWindow, "Active window is not null");
+ let AWP = new AboutWelcomeParent();
+ Assert.ok(AWP.AboutWelcomeObserver, "AboutWelcomeObserver is not null");
+
+ BrowserTestUtils.removeTab(tab);
+ Assert.equal(
+ AWP.AboutWelcomeObserver.terminateReason,
+ AWP.AboutWelcomeObserver.AWTerminate.TAB_CLOSED,
+ "Terminated due to tab closed"
+ );
+});
+
+/**
+ * Test simplified welcome UI closed due to change in location uri
+ */
+add_task(async function test_About_Welcome_Location_Change() {
+ await openAboutWelcomeTab();
+ let windowGlobalParent =
+ gBrowser.selectedBrowser.browsingContext.currentWindowGlobal;
+
+ let aboutWelcomeActor = await windowGlobalParent.getActor("AboutWelcome");
+
+ Assert.ok(
+ aboutWelcomeActor.AboutWelcomeObserver,
+ "AboutWelcomeObserver is not null"
+ );
+ BrowserTestUtils.startLoadingURIString(
+ gBrowser.selectedBrowser,
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ "http://example.com/#foo"
+ );
+ await BrowserTestUtils.waitForLocationChange(
+ gBrowser,
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ "http://example.com/#foo"
+ );
+
+ Assert.equal(
+ aboutWelcomeActor.AboutWelcomeObserver.terminateReason,
+ aboutWelcomeActor.AboutWelcomeObserver.AWTerminate.ADDRESS_BAR_NAVIGATED,
+ "Terminated due to location uri changed"
+ );
+});
diff --git a/browser/components/aboutwelcome/tests/browser/browser_aboutwelcome_rtamo.js b/browser/components/aboutwelcome/tests/browser/browser_aboutwelcome_rtamo.js
new file mode 100644
index 0000000000..4db4569eb6
--- /dev/null
+++ b/browser/components/aboutwelcome/tests/browser/browser_aboutwelcome_rtamo.js
@@ -0,0 +1,299 @@
+"use strict";
+
+const { ASRouter } = ChromeUtils.importESModule(
+ "resource:///modules/asrouter/ASRouter.sys.mjs"
+);
+const { AddonRepository } = ChromeUtils.importESModule(
+ "resource://gre/modules/addons/AddonRepository.sys.mjs"
+);
+const { ExperimentFakes } = ChromeUtils.importESModule(
+ "resource://testing-common/NimbusTestUtils.sys.mjs"
+);
+
+const TEST_ADDON_INFO = [
+ {
+ name: "Test Add-on",
+ sourceURI: { scheme: "https", spec: "https://test.xpi" },
+ icons: { 32: "test.png", 64: "test.png" },
+ type: "extension",
+ },
+];
+
+const TEST_ADDON_INFO_THEME = [
+ {
+ name: "Test Add-on",
+ sourceURI: { scheme: "https", spec: "https://test.xpi" },
+ icons: { 32: "test.png", 64: "test.png" },
+ screenshots: [{ url: "test.png" }],
+ type: "theme",
+ },
+];
+
+async function openRTAMOWelcomePage() {
+ // Can't properly stub the child/parent actors so instead
+ // we stub the modules they depend on for the RTAMO flow
+ // to ensure the right thing is rendered.
+ await ASRouter.forceAttribution({
+ source: "addons.mozilla.org",
+ medium: "referral",
+ campaign: "non-fx-button",
+ // with the sinon override, the id doesn't matter
+ content: "rta:whatever",
+ experiment: "ua-onboarding",
+ variation: "chrome",
+ ua: "Google Chrome 123",
+ dltoken: "00000000-0000-0000-0000-000000000000",
+ });
+
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "about:welcome",
+ true
+ );
+
+ registerCleanupFunction(async () => {
+ BrowserTestUtils.removeTab(tab);
+ // Clear cache call is only possible in a testing environment
+ Services.env.set("XPCSHELL_TEST_PROFILE_DIR", "testing");
+ await ASRouter.forceAttribution({
+ source: "",
+ medium: "",
+ campaign: "",
+ content: "",
+ experiment: "",
+ variation: "",
+ ua: "",
+ dltoken: "",
+ });
+ });
+
+ return tab.linkedBrowser;
+}
+
+/**
+ * Setup and test RTAMO welcome UI
+ */
+async function test_screen_content(
+ browser,
+ experiment,
+ expectedSelectors = [],
+ unexpectedSelectors = []
+) {
+ await ContentTask.spawn(
+ browser,
+ { expectedSelectors, experiment, unexpectedSelectors },
+ async ({
+ expectedSelectors: expected,
+ experiment: experimentName,
+ unexpectedSelectors: unexpected,
+ }) => {
+ for (let selector of expected) {
+ await ContentTaskUtils.waitForCondition(
+ () => content.document.querySelector(selector),
+ `Should render ${selector} in ${experimentName}`
+ );
+ }
+ for (let selector of unexpected) {
+ ok(
+ !content.document.querySelector(selector),
+ `Should not render ${selector} in ${experimentName}`
+ );
+ }
+ }
+ );
+}
+
+async function onButtonClick(browser, elementId) {
+ await ContentTask.spawn(
+ browser,
+ { elementId },
+ async ({ elementId: buttonId }) => {
+ await ContentTaskUtils.waitForCondition(
+ () => content.document.querySelector(buttonId),
+ buttonId
+ );
+ let button = content.document.querySelector(buttonId);
+ button.click();
+ }
+ );
+}
+
+/**
+ * Test the RTAMO welcome UI
+ */
+add_task(async function test_rtamo_aboutwelcome() {
+ let sandbox = sinon.createSandbox();
+ sandbox.stub(AddonRepository, "getAddonsByIDs").resolves(TEST_ADDON_INFO);
+
+ let browser = await openRTAMOWelcomePage();
+
+ await test_screen_content(
+ browser,
+ "RTAMO UI",
+ // Expected selectors:
+ [
+ `div.onboardingContainer[style*='background: var(--mr-welcome-background-color) var(--mr-welcome-background-gradient)']`,
+ "h2[data-l10n-id='mr1-return-to-amo-addon-title']",
+ `h2[data-l10n-args='{"addon-name":"${TEST_ADDON_INFO[0].name}"}'`,
+ "div.rtamo-icon",
+ "button.primary[data-l10n-id='mr1-return-to-amo-add-extension-label']",
+ "button[data-l10n-id='onboarding-not-now-button-label']",
+ ],
+ // Unexpected selectors:
+ [
+ "main.AW_STEP1",
+ "main.AW_STEP2",
+ "main.AW_STEP3",
+ "div.tiles-container.info",
+ ]
+ );
+
+ await onButtonClick(
+ browser,
+ "button[data-l10n-id='onboarding-not-now-button-label']"
+ );
+ Assert.ok(gURLBar.focused, "Focus should be on awesome bar");
+
+ let windowGlobalParent = browser.browsingContext.currentWindowGlobal;
+ let aboutWelcomeActor = windowGlobalParent.getActor("AboutWelcome");
+ const messageSandbox = sinon.createSandbox();
+ // Stub AboutWelcomeParent Content Message Handler
+ messageSandbox.stub(aboutWelcomeActor, "onContentMessage");
+ registerCleanupFunction(() => {
+ messageSandbox.restore();
+ });
+
+ await onButtonClick(browser, "button.primary");
+ const { callCount } = aboutWelcomeActor.onContentMessage;
+ Assert.strictEqual(
+ callCount,
+ 2,
+ `${callCount} Stub called twice to install extension and send telemetry`
+ );
+
+ const installExtensionCall = aboutWelcomeActor.onContentMessage.getCall(0);
+ Assert.equal(
+ installExtensionCall.args[0],
+ "AWPage:SPECIAL_ACTION",
+ "send special action to install add on"
+ );
+ Assert.equal(
+ installExtensionCall.args[1].type,
+ "INSTALL_ADDON_FROM_URL",
+ "Special action type is INSTALL_ADDON_FROM_URL"
+ );
+ Assert.equal(
+ installExtensionCall.args[1].data.url,
+ "https://test.xpi",
+ "Install add on url"
+ );
+ Assert.equal(
+ installExtensionCall.args[1].data.telemetrySource,
+ "rtamo",
+ "Install add on telemetry source"
+ );
+ const telemetryCall = aboutWelcomeActor.onContentMessage.getCall(1);
+ Assert.equal(
+ telemetryCall.args[0],
+ "AWPage:TELEMETRY_EVENT",
+ "send add extension telemetry"
+ );
+ Assert.equal(
+ telemetryCall.args[1].event,
+ "CLICK_BUTTON",
+ "Telemetry event sent as INSTALL"
+ );
+ Assert.equal(
+ telemetryCall.args[1].event_context.source,
+ "ADD_EXTENSION_BUTTON",
+ "Source of the event is Add Extension Button"
+ );
+ Assert.equal(
+ telemetryCall.args[1].message_id,
+ "RTAMO_DEFAULT_WELCOME_EXTENSION",
+ "Message Id sent in telemetry for default RTAMO"
+ );
+
+ sandbox.restore();
+});
+
+add_task(async function test_rtamo_over_experiments() {
+ let sandbox = sinon.createSandbox();
+ sandbox.stub(AddonRepository, "getAddonsByIDs").resolves(TEST_ADDON_INFO);
+
+ let doExperimentCleanup = await ExperimentFakes.enrollWithFeatureConfig({
+ featureId: "aboutwelcome",
+ value: { screens: [], enabled: true },
+ });
+
+ let browser = await openRTAMOWelcomePage();
+
+ // If addon attribution exist, we should see RTAMO even if enrolled
+ // in about:welcome experiment
+ await test_screen_content(
+ browser,
+ "Experiment RTAMO UI",
+ // Expected selectors:
+ ["h2[data-l10n-id='mr1-return-to-amo-addon-title']"],
+ // Unexpected selectors:
+ []
+ );
+
+ await doExperimentCleanup();
+
+ browser = await openRTAMOWelcomePage();
+
+ await test_screen_content(
+ browser,
+ "No Experiment RTAMO UI",
+ // Expected selectors:
+ [
+ "div.onboardingContainer",
+ "h2[data-l10n-id='mr1-return-to-amo-addon-title']",
+ "div.rtamo-icon",
+ "button.primary",
+ "button.secondary",
+ ],
+ // Unexpected selectors:
+ [
+ "main.AW_STEP1",
+ "main.AW_STEP2",
+ "main.AW_STEP3",
+ "div.tiles-container.info",
+ ]
+ );
+
+ sandbox.restore();
+});
+
+add_task(async function test_rtamo_primary_button_theme() {
+ let themeSandbox = sinon.createSandbox();
+ themeSandbox
+ .stub(AddonRepository, "getAddonsByIDs")
+ .resolves(TEST_ADDON_INFO_THEME);
+
+ let browser = await openRTAMOWelcomePage();
+
+ await test_screen_content(
+ browser,
+ "RTAMO UI",
+ // Expected selectors:
+ [
+ "div.onboardingContainer",
+ "h2[data-l10n-id='mr1-return-to-amo-addon-title']",
+ "div.rtamo-icon",
+ "button.primary[data-l10n-id='return-to-amo-add-theme-label']",
+ "button[data-l10n-id='onboarding-not-now-button-label']",
+ "img.rtamo-theme-icon",
+ ],
+ // Unexpected selectors:
+ [
+ "main.AW_STEP1",
+ "main.AW_STEP2",
+ "main.AW_STEP3",
+ "div.tiles-container.info",
+ ]
+ );
+
+ themeSandbox.restore();
+});
diff --git a/browser/components/aboutwelcome/tests/browser/browser_aboutwelcome_screen_targeting.js b/browser/components/aboutwelcome/tests/browser/browser_aboutwelcome_screen_targeting.js
new file mode 100644
index 0000000000..1d91d4ca05
--- /dev/null
+++ b/browser/components/aboutwelcome/tests/browser/browser_aboutwelcome_screen_targeting.js
@@ -0,0 +1,274 @@
+"use strict";
+
+const TEST_DEFAULT_CONTENT = [
+ {
+ id: "AW_STEP1",
+ content: {
+ title: "Step 1",
+ primary_button: {
+ label: "Next",
+ action: {
+ navigate: true,
+ },
+ },
+ secondary_button: {
+ label: "Secondary",
+ },
+ },
+ },
+ {
+ id: "AW_STEP2",
+ targeting: "false",
+ content: {
+ title: "Step 2",
+ primary_button: {
+ label: "Next",
+ action: {
+ navigate: true,
+ },
+ },
+ secondary_button: {
+ label: "Secondary",
+ },
+ },
+ },
+ {
+ id: "AW_STEP3",
+ content: {
+ title: "Step 3",
+ primary_button: {
+ label: "Next",
+ action: {
+ navigate: true,
+ },
+ },
+ secondary_button: {
+ label: "Secondary",
+ },
+ },
+ },
+];
+
+const TEST_DEFAULT_JSON = JSON.stringify(TEST_DEFAULT_CONTENT);
+async function openAboutWelcome() {
+ await setAboutWelcomePref(true);
+ await setAboutWelcomeMultiStage(TEST_DEFAULT_JSON);
+
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "about:welcome",
+ true
+ );
+ registerCleanupFunction(() => {
+ BrowserTestUtils.removeTab(tab);
+ });
+ return tab.linkedBrowser;
+}
+
+add_task(async function second_screen_filtered_by_targeting() {
+ const sandbox = sinon.createSandbox();
+ let browser = await openAboutWelcome();
+ let aboutWelcomeActor = await getAboutWelcomeParent(browser);
+ // Stub AboutWelcomeParent Content Message Handler
+ sandbox.spy(aboutWelcomeActor, "onContentMessage");
+
+ await test_screen_content(
+ browser,
+ "multistage step 1",
+ // Expected selectors:
+ ["main.AW_STEP1"],
+ // Unexpected selectors:
+ ["main.AW_STEP2", "main.AW_STEP3"]
+ );
+
+ await onButtonClick(browser, "button.primary");
+
+ await test_screen_content(
+ browser,
+ "multistage step 3",
+ // Expected selectors:
+ ["main.AW_STEP3"],
+ // Unexpected selectors:
+ ["main.AW_STEP2", "main.AW_STEP1"]
+ );
+
+ sandbox.restore();
+ await popPrefs();
+});
+
+/**
+ * Test MR template easy setup default content - Browser is not pinned
+ * and not set as default
+ */
+add_task(async function test_aboutwelcome_mr_template_easy_setup_default() {
+ const sandbox = sinon.createSandbox();
+ await pushPrefs(
+ ["browser.shell.checkDefaultBrowser", true],
+ ["messaging-system-action.showEmbeddedImport", false]
+ );
+ sandbox.stub(ShellService, "doesAppNeedPin").returns(true);
+ sandbox.stub(ShellService, "isDefaultBrowser").returns(false);
+
+ await clearHistoryAndBookmarks();
+
+ const { browser, cleanup } = await openMRAboutWelcome();
+
+ //should render easy setup with all checkboxes (default, pin, import)
+ await test_screen_content(
+ browser,
+ "doesn't render only pin, default, or import easy setup",
+ //Expected selectors:
+ ["main.AW_EASY_SETUP_NEEDS_DEFAULT_AND_PIN"],
+ //Unexpected selectors:
+ [
+ "main.AW_EASY_SETUP_NEEDS_DEFAULT",
+ "main.AW_EASY_SETUP_NEEDS_PIN",
+ "main.AW_ONLY_IMPORT",
+ ]
+ );
+
+ await onButtonClick(browser, ".action-buttons button.secondary");
+
+ await test_screen_content(
+ browser,
+ "renders mobile download screen",
+ //Expected selectors:
+ ["main.AW_MOBILE_DOWNLOAD"],
+ //Unexpected selectors:
+ ["main.AW_IMPORT_SETTINGS_EMBEDDED"]
+ );
+
+ await cleanup();
+ await popPrefs();
+ sandbox.restore();
+});
+
+/**
+ * Test MR template easy setup content - Browser is not pinned
+ * and set as default
+ */
+add_task(async function test_aboutwelcome_mr_template_easy_setup_needs_pin() {
+ const sandbox = sinon.createSandbox();
+ await pushPrefs(
+ ["browser.shell.checkDefaultBrowser", true],
+ ["messaging-system-action.showEmbeddedImport", false]
+ );
+ sandbox.stub(ShellService, "doesAppNeedPin").returns(true);
+ sandbox.stub(ShellService, "isDefaultBrowser").returns(true);
+
+ await clearHistoryAndBookmarks();
+
+ const { browser, cleanup } = await openMRAboutWelcome();
+
+ //should render easy setup needs pin
+ await test_screen_content(
+ browser,
+ "doesn't render default and pin, only default or import easy setup",
+ //Expected selectors:
+ ["main.AW_EASY_SETUP_NEEDS_PIN"],
+ //Unexpected selectors:
+ [
+ "main.AW_EASY_SETUP_NEEDS_DEFAULT",
+ "main.AW_EASY_SETUP_NEEDS_DEFAULT_AND_PIN",
+ "main.AW_ONLY_IMPORT",
+ ]
+ );
+
+ await cleanup();
+ await popPrefs();
+ sandbox.restore();
+});
+
+/**
+ * Test MR template easy setup content - Browser is pinned and
+ * not set as default
+ */
+add_task(
+ async function test_aboutwelcome_mr_template_easy_setup_needs_default() {
+ const sandbox = sinon.createSandbox();
+ await pushPrefs(
+ ["browser.shell.checkDefaultBrowser", true],
+ ["messaging-system-action.showEmbeddedImport", false]
+ );
+ sandbox.stub(ShellService, "doesAppNeedPin").returns(false);
+ sandbox.stub(ShellService, "isDefaultBrowser").returns(false);
+
+ await clearHistoryAndBookmarks();
+
+ const { browser, cleanup } = await openMRAboutWelcome();
+
+ //should render easy setup needs default
+ await test_screen_content(
+ browser,
+ "doesn't render pin, import and set to default",
+ //Expected selectors:
+ ["main.AW_EASY_SETUP_NEEDS_DEFAULT"],
+ //Unexpected selectors:
+ [
+ "main.AW_EASY_SETUP_NEEDS_PIN",
+ "main.AW_EASY_SETUP_NEEDS_DEFAULT_AND_PIN",
+ "main.AW_ONLY_IMPORT",
+ ]
+ );
+
+ await onButtonClick(browser, ".action-buttons button.secondary");
+ await test_screen_content(
+ browser,
+ "renders mobile download screen",
+ //Expected selectors:
+ ["main.AW_MOBILE_DOWNLOAD"],
+ //Unexpected selectors:
+ ["main.AW_IMPORT_SETTINGS_EMBEDDED"]
+ );
+
+ await cleanup();
+ await popPrefs();
+ sandbox.restore();
+ }
+);
+
+/**
+ * Test MR template easy setup content - Browser is pinned and
+ * set as default
+ */
+add_task(async function test_aboutwelcome_mr_template_easy_setup_only_import() {
+ const sandbox = sinon.createSandbox();
+ await pushPrefs(
+ ["browser.shell.checkDefaultBrowser", true],
+ ["messaging-system-action.showEmbeddedImport", false]
+ );
+ sandbox.stub(ShellService, "doesAppNeedPin").returns(false);
+ sandbox.stub(ShellService, "isDefaultBrowser").returns(true);
+
+ await clearHistoryAndBookmarks();
+
+ const { browser, cleanup } = await openMRAboutWelcome();
+
+ //should render easy setup - only import
+ await test_screen_content(
+ browser,
+ "doesn't render any combination of pin and default",
+ //Expected selectors:
+ ["main.AW_EASY_SETUP_ONLY_IMPORT"],
+ //Unexpected selectors:
+ [
+ "main.AW_EASY_SETUP_NEEDS_PIN",
+ "main.AW_EASY_SETUP_NEEDS_DEFAULT_AND_PIN",
+ "main.AW_EASY_SETUP_NEEDS_DEFAULT",
+ ]
+ );
+
+ await onButtonClick(browser, ".action-buttons button.secondary");
+ await test_screen_content(
+ browser,
+ "renders mobile download screen",
+ //Expected selectors:
+ ["main.AW_MOBILE_DOWNLOAD"],
+ //Unexpected selectors:
+ ["main.AW_IMPORT_SETTINGS_EMBEDDED"]
+ );
+
+ await cleanup();
+ await popPrefs();
+ sandbox.restore();
+});
diff --git a/browser/components/aboutwelcome/tests/browser/browser_aboutwelcome_upgrade_multistage_mr.js b/browser/components/aboutwelcome/tests/browser/browser_aboutwelcome_upgrade_multistage_mr.js
new file mode 100644
index 0000000000..d6bc53d8ce
--- /dev/null
+++ b/browser/components/aboutwelcome/tests/browser/browser_aboutwelcome_upgrade_multistage_mr.js
@@ -0,0 +1,320 @@
+"use strict";
+
+const { OnboardingMessageProvider } = ChromeUtils.importESModule(
+ "resource:///modules/asrouter/OnboardingMessageProvider.sys.mjs"
+);
+const { SpecialMessageActions } = ChromeUtils.importESModule(
+ "resource://messaging-system/lib/SpecialMessageActions.sys.mjs"
+);
+const {
+ assertFirefoxViewTabSelected,
+ closeFirefoxViewTab,
+ init: FirefoxViewTestUtilsInit,
+} = ChromeUtils.importESModule(
+ "resource://testing-common/FirefoxViewTestUtils.sys.mjs"
+);
+FirefoxViewTestUtilsInit(this);
+
+const HOMEPAGE_PREF = "browser.startup.homepage";
+const NEWTAB_PREF = "browser.newtabpage.enabled";
+const PINPBM_DISABLED_PREF = "browser.startup.upgradeDialog.pinPBM.disabled";
+
+// A bunch of the helper functions here are variants of the helper functions in
+// browser_aboutwelcome_multistage_mr.js, because the onboarding
+// experience runs in the parent process rather than elsewhere.
+// If these start to get used in more than just the two files, it may become
+// worth refactoring them to avoid duplicated code, and hoisting them
+// into head.js.
+
+let sandbox;
+
+add_setup(async () => {
+ requestLongerTimeout(2);
+
+ await setAboutWelcomePref(true);
+
+ sandbox = sinon.createSandbox();
+ sandbox.stub(OnboardingMessageProvider, "_doesAppNeedPin").resolves(false);
+ sandbox
+ .stub(OnboardingMessageProvider, "_doesAppNeedDefault")
+ .resolves(false);
+
+ sandbox.stub(SpecialMessageActions, "pinFirefoxToTaskbar").resolves();
+
+ registerCleanupFunction(async () => {
+ await popPrefs();
+ sandbox.restore();
+ });
+});
+
+/**
+ * Get the content by OnboardingMessageProvider.getUpgradeMessage(),
+ * discard any screens whose ids are not in the "screensToTest" array,
+ * and then open an upgrade dialog with just those screens.
+ *
+ * @param {Array} screensToTest
+ * A list of which screen ids to be displayed
+ *
+ * @returns {Promise<Window>}
+ * Resolves to the window global object for the dialog once it has been
+ * opened
+ */
+async function openMRUpgradeWelcome(screensToTest) {
+ const data = await OnboardingMessageProvider.getUpgradeMessage();
+
+ if (screensToTest) {
+ data.content.screens = data.content.screens.filter(screen =>
+ screensToTest.includes(screen.id)
+ );
+ }
+
+ sandbox.stub(OnboardingMessageProvider, "getUpgradeMessage").resolves(data);
+
+ let dialogOpenPromise = BrowserTestUtils.promiseAlertDialogOpen(
+ null,
+ "chrome://browser/content/spotlight.html",
+ { isSubDialog: true }
+ );
+
+ Cc["@mozilla.org/browser/browserglue;1"]
+ .getService()
+ .wrappedJSObject._showUpgradeDialog();
+
+ let browser = await dialogOpenPromise;
+
+ OnboardingMessageProvider.getUpgradeMessage.restore();
+ return Promise.resolve(browser);
+}
+
+async function clickVisibleButton(browser, selector) {
+ await BrowserTestUtils.waitForCondition(
+ () => browser.document.querySelector(selector),
+ `waiting for selector ${selector}`,
+ 200, // interval
+ 100 // maxTries
+ );
+ browser.document.querySelector(selector).click();
+}
+
+async function test_upgrade_screen_content(
+ browser,
+ expected = [],
+ unexpected = []
+) {
+ for (let selector of expected) {
+ await TestUtils.waitForCondition(
+ () => browser.document.querySelector(selector),
+ `Should render ${selector}`
+ );
+ }
+ for (let selector of unexpected) {
+ Assert.ok(
+ !browser.document.querySelector(selector),
+ `Should not render ${selector}`
+ );
+ }
+}
+
+async function waitForDialogClose(browser) {
+ await BrowserTestUtils.waitForCondition(
+ () => !browser.top?.document.querySelector(".dialogFrame"),
+ "waiting for dialog to close"
+ );
+}
+
+/**
+ * Test homepage/newtab prefs start off as defaults and do not change
+ */
+add_task(async function test_aboutwelcome_upgrade_mr_prefs_off() {
+ let browser = await openMRUpgradeWelcome(["UPGRADE_GET_STARTED"]);
+
+ await test_upgrade_screen_content(
+ browser,
+ //Expected selectors:
+ ["main.UPGRADE_GET_STARTED"],
+ //Unexpected selectors:
+ ["main.PIN_FIREFOX"]
+ );
+
+ await clickVisibleButton(browser, ".action-buttons button.secondary");
+ await clickVisibleButton(browser, ".action-buttons button.primary");
+ await waitForDialogClose(browser);
+
+ Assert.ok(
+ !Services.prefs.prefHasUserValue(HOMEPAGE_PREF),
+ "homepage pref should be default"
+ );
+ Assert.ok(
+ !Services.prefs.prefHasUserValue(NEWTAB_PREF),
+ "newtab pref should be default"
+ );
+
+ await BrowserTestUtils.removeTab(gBrowser.selectedTab);
+});
+
+/*
+ *Test checkbox if needPrivatePin is true
+ */
+add_task(async function test_aboutwelcome_upgrade_mr_private_pin() {
+ OnboardingMessageProvider._doesAppNeedPin.resolves(true);
+ let browser = await openMRUpgradeWelcome(["UPGRADE_PIN_FIREFOX"]);
+
+ await test_upgrade_screen_content(
+ browser,
+ //Expected selectors:
+ ["main.UPGRADE_PIN_FIREFOX", "input#action-checkbox"],
+ //Unexpected selectors:
+ ["main.UPGRADE_COLORWAY"]
+ );
+ await clickVisibleButton(browser, ".action-buttons button.primary");
+ await waitForDialogClose(browser);
+
+ const pinStub = SpecialMessageActions.pinFirefoxToTaskbar;
+ Assert.equal(
+ pinStub.callCount,
+ 2,
+ "pinFirefoxToTaskbar should have been called twice"
+ );
+ Assert.ok(
+ // eslint-disable-next-line eqeqeq
+ pinStub.firstCall.lastArg != pinStub.secondCall.lastArg,
+ "pinFirefoxToTaskbar should have been called once for private, once not"
+ );
+
+ await BrowserTestUtils.removeTab(gBrowser.selectedTab);
+});
+
+/*
+ *Test checkbox shouldn't be shown in get started screen
+ */
+
+add_task(async function test_aboutwelcome_upgrade_mr_private_pin_get_started() {
+ OnboardingMessageProvider._doesAppNeedPin.resolves(false);
+
+ let browser = await openMRUpgradeWelcome(["UPGRADE_GET_STARTED"]);
+
+ await test_upgrade_screen_content(
+ browser,
+ //Expected selectors
+ ["main.UPGRADE_GET_STARTED"],
+ //Unexpected selectors:
+ ["input#action-checkbox"]
+ );
+
+ await clickVisibleButton(browser, ".action-buttons button.secondary");
+
+ await waitForDialogClose(browser);
+ await BrowserTestUtils.removeTab(gBrowser.selectedTab);
+});
+
+/*
+ *Test checkbox shouldn't be shown if needPrivatePin is false
+ */
+add_task(async function test_aboutwelcome_upgrade_mr_private_pin_not_needed() {
+ OnboardingMessageProvider._doesAppNeedPin
+ .resolves(true)
+ .withArgs(true)
+ .resolves(false);
+
+ let browser = await openMRUpgradeWelcome(["UPGRADE_PIN_FIREFOX"]);
+
+ await test_upgrade_screen_content(
+ browser,
+ //Expected selectors
+ ["main.UPGRADE_PIN_FIREFOX"],
+ //Unexpected selectors:
+ ["input#action-checkbox"]
+ );
+
+ await clickVisibleButton(browser, ".action-buttons button.secondary");
+ await waitForDialogClose(browser);
+ await BrowserTestUtils.removeTab(gBrowser.selectedTab);
+});
+
+/*
+ * Make sure we don't get an extraneous checkbox here.
+ */
+add_task(
+ async function test_aboutwelcome_upgrade_mr_pin_not_needed_default_needed() {
+ OnboardingMessageProvider._doesAppNeedPin.resolves(false);
+ OnboardingMessageProvider._doesAppNeedDefault.resolves(false);
+
+ let browser = await openMRUpgradeWelcome(["UPGRADE_GET_STARTED"]);
+
+ await test_upgrade_screen_content(
+ browser,
+ //Expected selectors
+ ["main.UPGRADE_GET_STARTED"],
+ //Unexpected selectors:
+ ["input#action-checkbox"]
+ );
+
+ await clickVisibleButton(browser, ".action-buttons button.secondary");
+ await waitForDialogClose(browser);
+ await BrowserTestUtils.removeTab(gBrowser.selectedTab);
+ }
+);
+
+add_task(async function test_aboutwelcome_privacy_segmentation_pref() {
+ async function testPrivacySegmentation(enabled = false) {
+ await pushPrefs(["browser.privacySegmentation.preferences.show", enabled]);
+ let screenIds = ["UPGRADE_DATA_RECOMMENDATION", "UPGRADE_GRATITUDE"];
+ let browser = await openMRUpgradeWelcome(screenIds);
+ await test_upgrade_screen_content(
+ browser,
+ //Expected selectors
+ [`main.${screenIds[enabled ? 0 : 1]}`],
+ //Unexpected selectors:
+ [`main.${screenIds[enabled ? 1 : 0]}`]
+ );
+ await BrowserTestUtils.removeTab(gBrowser.selectedTab);
+ await popPrefs();
+ }
+
+ for (let enabled of [true, false]) {
+ await testPrivacySegmentation(enabled);
+ }
+});
+
+add_task(async function test_aboutwelcome_upgrade_show_firefox_view() {
+ let browser = await openMRUpgradeWelcome(["UPGRADE_GRATITUDE"]);
+
+ // execution
+ await test_upgrade_screen_content(
+ browser,
+ //Expected selectors
+ ["main.UPGRADE_GRATITUDE"],
+ //Unexpected selectors:
+ []
+ );
+ await clickVisibleButton(browser, ".action-buttons button.primary");
+
+ // verification
+ await BrowserTestUtils.waitForEvent(gBrowser, "TabSwitchDone");
+ assertFirefoxViewTabSelected(gBrowser.ownerGlobal);
+
+ closeFirefoxViewTab(gBrowser.ownerGlobal);
+ await BrowserTestUtils.removeTab(gBrowser.selectedTab);
+});
+
+/*
+ *Checkbox shouldn't be shown if pinPBMDisabled pref is true
+ */
+add_task(async function test_aboutwelcome_upgrade_mr_private_pin_not_needed() {
+ OnboardingMessageProvider._doesAppNeedPin.resolves(true);
+ await pushPrefs([PINPBM_DISABLED_PREF, true]);
+
+ const browser = await openMRUpgradeWelcome(["UPGRADE_PIN_FIREFOX"]);
+
+ await test_upgrade_screen_content(
+ browser,
+ //Expected selectors
+ ["main.UPGRADE_PIN_FIREFOX"],
+ //Unexpected selectors:
+ ["input#action-checkbox"]
+ );
+
+ await clickVisibleButton(browser, ".action-buttons button.secondary");
+ await waitForDialogClose(browser);
+ await BrowserTestUtils.removeTab(gBrowser.selectedTab);
+});
diff --git a/browser/components/aboutwelcome/tests/browser/head.js b/browser/components/aboutwelcome/tests/browser/head.js
new file mode 100644
index 0000000000..0854a3efb0
--- /dev/null
+++ b/browser/components/aboutwelcome/tests/browser/head.js
@@ -0,0 +1,149 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+ChromeUtils.defineESModuleGetters(this, {
+ FxAccounts: "resource://gre/modules/FxAccounts.sys.mjs",
+ QueryCache: "resource:///modules/asrouter/ASRouterTargeting.sys.mjs",
+ sinon: "resource://testing-common/Sinon.sys.mjs",
+});
+
+// Set the content pref to make it available across tests
+const ABOUT_WELCOME_OVERRIDE_CONTENT_PREF = "browser.aboutwelcome.screens";
+
+function popPrefs() {
+ return SpecialPowers.popPrefEnv();
+}
+function pushPrefs(...prefs) {
+ return SpecialPowers.pushPrefEnv({ set: prefs });
+}
+
+async function getAboutWelcomeParent(browser) {
+ let windowGlobalParent = browser.browsingContext.currentWindowGlobal;
+ return windowGlobalParent.getActor("AboutWelcome");
+}
+
+async function setAboutWelcomeMultiStage(value = "") {
+ return pushPrefs([ABOUT_WELCOME_OVERRIDE_CONTENT_PREF, value]);
+}
+
+async function setAboutWelcomePref(value) {
+ return pushPrefs(["browser.aboutwelcome.enabled", value]);
+}
+
+async function openMRAboutWelcome() {
+ await setAboutWelcomePref(true); // NB: Calls pushPrefs
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "about:welcome",
+ true
+ );
+
+ return {
+ browser: tab.linkedBrowser,
+ cleanup: async () => {
+ BrowserTestUtils.removeTab(tab);
+ await popPrefs(); // for setAboutWelcomePref()
+ },
+ };
+}
+
+async function onButtonClick(browser, elementId) {
+ await ContentTask.spawn(
+ browser,
+ { elementId },
+ async ({ elementId: buttonId }) => {
+ let button = await ContentTaskUtils.waitForCondition(
+ () => content.document.querySelector(buttonId),
+ buttonId
+ );
+ button.click();
+ }
+ );
+}
+
+async function clearHistoryAndBookmarks() {
+ await PlacesUtils.bookmarks.eraseEverything();
+ await PlacesUtils.history.clear();
+ QueryCache.expireAll();
+}
+
+/**
+ * Setup functions to test welcome UI
+ */
+async function test_screen_content(
+ browser,
+ experiment,
+ expectedSelectors = [],
+ unexpectedSelectors = []
+) {
+ await ContentTask.spawn(
+ browser,
+ { expectedSelectors, experiment, unexpectedSelectors },
+ async ({
+ expectedSelectors: expected,
+ experiment: experimentName,
+ unexpectedSelectors: unexpected,
+ }) => {
+ for (let selector of expected) {
+ await ContentTaskUtils.waitForCondition(
+ () => content.document.querySelector(selector),
+ `Should render ${selector} in ${experimentName}`
+ );
+ }
+ for (let selector of unexpected) {
+ ok(
+ !content.document.querySelector(selector),
+ `Should not render ${selector} in ${experimentName}`
+ );
+ }
+
+ if (experimentName === "home") {
+ Assert.equal(
+ content.document.location.href,
+ "about:home",
+ "Navigated to about:home"
+ );
+ } else {
+ Assert.equal(
+ content.document.location.href,
+ "about:welcome",
+ "Navigated to a welcome screen"
+ );
+ }
+ }
+ );
+}
+
+async function test_element_styles(
+ browser,
+ elementSelector,
+ expectedStyles = {},
+ unexpectedStyles = {}
+) {
+ await ContentTask.spawn(
+ browser,
+ [elementSelector, expectedStyles, unexpectedStyles],
+ async ([selector, expected, unexpected]) => {
+ const element = await ContentTaskUtils.waitForCondition(() =>
+ content.document.querySelector(selector)
+ );
+ const computedStyles = content.window.getComputedStyle(element);
+ Object.entries(expected).forEach(([attr, val]) =>
+ is(
+ computedStyles[attr],
+ val,
+ `${selector} should have computed ${attr} of ${val}`
+ )
+ );
+ Object.entries(unexpected).forEach(([attr, val]) =>
+ isnot(
+ computedStyles[attr],
+ val,
+ `${selector} should not have computed ${attr} of ${val}`
+ )
+ );
+ }
+ );
+}
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));
+});