summaryrefslogtreecommitdiffstats
path: root/browser/components/aboutwelcome/tests/browser
diff options
context:
space:
mode:
Diffstat (limited to 'browser/components/aboutwelcome/tests/browser')
-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
18 files changed, 5969 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}`
+ )
+ );
+ }
+ );
+}