"use strict"; const { ExperimentFakes } = ChromeUtils.importESModule( "resource://testing-common/NimbusTestUtils.sys.mjs" ); const { AboutWelcomeTelemetry } = ChromeUtils.import( "resource://activity-stream/aboutwelcome/lib/AboutWelcomeTelemetry.jsm" ); 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="${logo.alt ?? ""}"]${logo.height ? `[style*="height"]` : ""}${ logo.alt ? "" : `[role="presentation"]` }`, ]; let unexpected = []; if (!logo.height) { unexpected.push(`.brand-logo[style*="height"]`); } if (logo.alt) { unexpected.push(`.brand-logo[role="presentation"]`); } (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 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 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 } }, }) ) ) ); // 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": "rgba(21, 20, 26, 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"], "rgba(21, 20, 26, 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(--checkbox-checked-bgcolor);` "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; }); ok( 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", height: "16px", }); // 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(); });