From 26a029d407be480d791972afb5975cf62c9360a6 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Fri, 19 Apr 2024 02:47:55 +0200 Subject: Adding upstream version 124.0.1. Signed-off-by: Daniel Baumann --- .../aboutwelcome/tests/browser/browser.toml | 57 ++ .../browser/browser_aboutwelcome_attribution.js | 214 +++++ .../browser_aboutwelcome_configurable_ui.js | 724 +++++++++++++++++ .../browser_aboutwelcome_fxa_signin_flow.js | 303 ++++++++ .../tests/browser/browser_aboutwelcome_glean.js | 162 ++++ .../tests/browser/browser_aboutwelcome_import.js | 94 +++ .../browser_aboutwelcome_mobile_downloads.js | 112 +++ ...browser_aboutwelcome_multistage_addonspicker.js | 178 +++++ .../browser_aboutwelcome_multistage_default.js | 794 +++++++++++++++++++ ...rowser_aboutwelcome_multistage_experimentAPI.js | 641 +++++++++++++++ ...ser_aboutwelcome_multistage_languageSwitcher.js | 708 +++++++++++++++++ .../browser/browser_aboutwelcome_multistage_mr.js | 770 ++++++++++++++++++ .../browser_aboutwelcome_multistage_video.js | 97 +++ .../tests/browser/browser_aboutwelcome_observer.js | 73 ++ .../tests/browser/browser_aboutwelcome_rtamo.js | 299 +++++++ .../browser_aboutwelcome_screen_targeting.js | 274 +++++++ .../browser_aboutwelcome_upgrade_multistage_mr.js | 320 ++++++++ .../components/aboutwelcome/tests/browser/head.js | 149 ++++ .../aboutwelcome/tests/unit/AWScreenUtils.test.jsx | 140 ++++ .../aboutwelcome/tests/unit/CTAParagraph.test.jsx | 49 ++ .../aboutwelcome/tests/unit/HelpText.test.jsx | 41 + .../aboutwelcome/tests/unit/HeroImage.test.jsx | 40 + .../aboutwelcome/tests/unit/LinkParagraph.test.jsx | 102 +++ .../aboutwelcome/tests/unit/MRColorways.test.jsx | 328 ++++++++ .../aboutwelcome/tests/unit/MSLocalized.test.jsx | 48 ++ .../tests/unit/MobileDownloads.test.jsx | 69 ++ .../aboutwelcome/tests/unit/MultiSelect.test.jsx | 221 ++++++ .../tests/unit/MultiStageAWProton.test.jsx | 571 ++++++++++++++ .../tests/unit/MultiStageAboutWelcome.test.jsx | 859 +++++++++++++++++++++ .../tests/unit/OnboardingVideoTest.test.jsx | 45 ++ .../aboutwelcome/tests/unit/addUtmParams.test.js | 34 + .../aboutwelcome/tests/unit/unit-entry.js | 716 +++++++++++++++++ 32 files changed, 9232 insertions(+) create mode 100644 browser/components/aboutwelcome/tests/browser/browser.toml create mode 100644 browser/components/aboutwelcome/tests/browser/browser_aboutwelcome_attribution.js create mode 100644 browser/components/aboutwelcome/tests/browser/browser_aboutwelcome_configurable_ui.js create mode 100644 browser/components/aboutwelcome/tests/browser/browser_aboutwelcome_fxa_signin_flow.js create mode 100644 browser/components/aboutwelcome/tests/browser/browser_aboutwelcome_glean.js create mode 100644 browser/components/aboutwelcome/tests/browser/browser_aboutwelcome_import.js create mode 100644 browser/components/aboutwelcome/tests/browser/browser_aboutwelcome_mobile_downloads.js create mode 100644 browser/components/aboutwelcome/tests/browser/browser_aboutwelcome_multistage_addonspicker.js create mode 100644 browser/components/aboutwelcome/tests/browser/browser_aboutwelcome_multistage_default.js create mode 100644 browser/components/aboutwelcome/tests/browser/browser_aboutwelcome_multistage_experimentAPI.js create mode 100644 browser/components/aboutwelcome/tests/browser/browser_aboutwelcome_multistage_languageSwitcher.js create mode 100644 browser/components/aboutwelcome/tests/browser/browser_aboutwelcome_multistage_mr.js create mode 100644 browser/components/aboutwelcome/tests/browser/browser_aboutwelcome_multistage_video.js create mode 100644 browser/components/aboutwelcome/tests/browser/browser_aboutwelcome_observer.js create mode 100644 browser/components/aboutwelcome/tests/browser/browser_aboutwelcome_rtamo.js create mode 100644 browser/components/aboutwelcome/tests/browser/browser_aboutwelcome_screen_targeting.js create mode 100644 browser/components/aboutwelcome/tests/browser/browser_aboutwelcome_upgrade_multistage_mr.js create mode 100644 browser/components/aboutwelcome/tests/browser/head.js create mode 100644 browser/components/aboutwelcome/tests/unit/AWScreenUtils.test.jsx create mode 100644 browser/components/aboutwelcome/tests/unit/CTAParagraph.test.jsx create mode 100644 browser/components/aboutwelcome/tests/unit/HelpText.test.jsx create mode 100644 browser/components/aboutwelcome/tests/unit/HeroImage.test.jsx create mode 100644 browser/components/aboutwelcome/tests/unit/LinkParagraph.test.jsx create mode 100644 browser/components/aboutwelcome/tests/unit/MRColorways.test.jsx create mode 100644 browser/components/aboutwelcome/tests/unit/MSLocalized.test.jsx create mode 100644 browser/components/aboutwelcome/tests/unit/MobileDownloads.test.jsx create mode 100644 browser/components/aboutwelcome/tests/unit/MultiSelect.test.jsx create mode 100644 browser/components/aboutwelcome/tests/unit/MultiStageAWProton.test.jsx create mode 100644 browser/components/aboutwelcome/tests/unit/MultiStageAboutWelcome.test.jsx create mode 100644 browser/components/aboutwelcome/tests/unit/OnboardingVideoTest.test.jsx create mode 100644 browser/components/aboutwelcome/tests/unit/addUtmParams.test.js create mode 100644 browser/components/aboutwelcome/tests/unit/unit-entry.js (limited to 'browser/components/aboutwelcome/tests') 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: "", + }, + 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: "", + }, + 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: "", + }, + 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 custom element", + // We expect 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 is on the right + // page and the 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 can appear. + let panelList = shadow.querySelector("panel-list"); + Assert.ok(panelList, "Found the ."); + + // 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 rect top value relative to the top-left + // of the selectorRect. We expect the to be tightly anchored + // to the bottom of the