/* Any copyright is dedicated to the Public Domain. * http://creativecommons.org/publicdomain/zero/1.0/ */ const { CustomizableUITestUtils } = ChromeUtils.importESModule( "resource://testing-common/CustomizableUITestUtils.sys.mjs" ); const { DefaultBrowserCheck } = ChromeUtils.importESModule( "resource:///modules/BrowserGlue.sys.mjs" ); const PDF_TEST_URL = "https://example.com/browser/browser/components/newtab/test/browser/file_pdf.PDF"; async function openURLInWindow(window, url) { const { selectedBrowser } = window.gBrowser; BrowserTestUtils.startLoadingURIString(selectedBrowser, url); await BrowserTestUtils.browserLoaded(selectedBrowser, false, url); return selectedBrowser; } async function openURLInNewTab(window, ...args) { return BrowserTestUtils.openNewForegroundTab(window.gBrowser, ...args); } const pdfMatch = sinon.match(val => { return ( val?.id === "pdfJsFeatureCalloutCheck" && val?.context?.source === "open" ); }); const validateCalloutCustomPosition = (element, absolutePosition, doc) => { const browserBox = doc.querySelector("hbox#browser"); for (let position in absolutePosition) { if (Object.prototype.hasOwnProperty.call(absolutePosition, position)) { // remove the `px` at the end of our absolute position strings const relativePos = parseFloat(absolutePosition[position]); const elPos = element.getBoundingClientRect()[position]; const browserPos = browserBox.getBoundingClientRect()[position]; if (position in ["top", "left"]) { if (elPos !== browserPos + relativePos) { return false; } } else if (position in ["right", "bottom"]) { if (elPos !== browserPos - relativePos) { return false; } } } } return true; }; const validateCalloutRTLPosition = (element, absolutePosition) => { for (let position in absolutePosition) { if (Object.prototype.hasOwnProperty.call(absolutePosition, position)) { const pixels = parseFloat(absolutePosition[position]); if (position === "left") { if (element.getBoundingClientRect().right !== pixels) { return false; } } else if (position === "right") { if (element.getBoundingClientRect().left !== pixels) { return false; } } } } return true; }; const testMessage = { message: { id: "TEST_MESSAGE", template: "feature_callout", content: { id: "TEST_MESSAGE", template: "multistage", backdrop: "transparent", transitions: false, screens: [ { id: "TEST_MESSAGE_1", anchors: [ { selector: "#PanelUI-menu-button", arrow_position: "top-end" }, ], content: { position: "callout", title: { raw: "Test title", }, subtitle: { raw: "Test subtitle", }, primary_button: { label: { raw: "Done", }, action: { navigate: true, }, }, }, }, ], }, priority: 1, targeting: "true", trigger: { id: "pdfJsFeatureCalloutCheck" }, }, }; const newtabTestMessage = { id: "TEST_MESSAGE", template: "feature_callout", content: { id: "TEST_MESSAGE", template: "multistage", backdrop: "transparent", transitions: false, disableHistoryUpdates: true, tour_pref_name: "browser.newtab.feature-tour", tour_pref_default_value: JSON.stringify({ screen: "TEST_MESSAGE_1", complete: false, }), screens: [ { id: "TEST_MESSAGE_1", anchors: [ { selector: "hbox#browser", arrow_position: "top-end", absolute_position: { top: "45px", right: "55px" }, }, ], content: { position: "callout", title: "Test callout title", subtitle: "Test callout subtitle", primary_button: { label: "Test callout button", }, }, }, ], }, priority: 1, targeting: "true", trigger: { id: "newtabFeatureCalloutCheck" }, }; const testMessageScreenId = testMessage.message.content.screens[0].id; const newtabTestMessageScreenId = newtabTestMessage.content.screens[0].id; const inChaosMode = !!parseInt(Services.env.get("MOZ_CHAOSMODE"), 16); add_setup(async function () { let timeoutFactor = 3; // Runtime increases in chaos mode on Mac. if (inChaosMode && AppConstants.platform === "macosx") { timeoutFactor = 5; } requestLongerTimeout(timeoutFactor); }); // Test that a feature callout message can be loaded into ASRouter and displayed // via a standard trigger. Also test that the callout can be a feature tour, // even if its tour pref doesn't exist in Firefox. The tour pref will be created // and cleaned up automatically. This allows a feature callout to be implemented // entirely off-train in an experiment, without landing anything in tree. add_task(async function triggered_feature_tour_with_custom_pref() { let sandbox = sinon.createSandbox(); const TEST_MESSAGES = [ { id: "TEST_FEATURE_TOUR", template: "feature_callout", content: { id: "TEST_FEATURE_TOUR", template: "multistage", backdrop: "transparent", transitions: false, disableHistoryUpdates: true, tour_pref_name: "messaging-system-action.browser.test.feature-tour", tour_pref_default_value: JSON.stringify({ screen: "FEATURE_CALLOUT_1", complete: false, }), screens: [ { id: "FEATURE_CALLOUT_1", anchors: [ { selector: "#PanelUI-menu-button", arrow_position: "top-center-arrow-end", }, ], content: { position: "callout", title: { string_id: "callout-pdfjs-edit-title" }, subtitle: { string_id: "callout-pdfjs-edit-body-b" }, primary_button: { label: { string_id: "callout-pdfjs-edit-button" }, action: { type: "SET_PREF", data: { pref: { name: "messaging-system-action.browser.test.feature-tour", value: JSON.stringify({ screen: "FEATURE_CALLOUT_2", complete: false, }), }, }, }, }, dismiss_button: { action: { type: "MULTI_ACTION", data: { actions: [ { type: "BLOCK_MESSAGE", data: { id: "TEST_FEATURE_TOUR" }, }, { type: "SET_PREF", data: { pref: { name: "messaging-system-action.browser.test.feature-tour", }, }, }, ], }, }, }, }, }, { id: "FEATURE_CALLOUT_2", anchors: [ { selector: "#back-button", arrow_position: "top-center-arrow-start", }, ], content: { position: "callout", title: { string_id: "callout-pdfjs-draw-title" }, subtitle: { string_id: "callout-pdfjs-draw-body-b" }, primary_button: { label: { string_id: "callout-pdfjs-draw-button" }, action: { type: "MULTI_ACTION", data: { actions: [ { type: "BLOCK_MESSAGE", data: { id: "TEST_FEATURE_TOUR" }, }, { type: "SET_PREF", data: { pref: { name: "messaging-system-action.browser.test.feature-tour", }, }, }, ], }, }, }, dismiss_button: { action: { type: "MULTI_ACTION", data: { actions: [ { type: "BLOCK_MESSAGE", data: { id: "TEST_FEATURE_TOUR" }, }, { type: "SET_PREF", data: { pref: { name: "messaging-system-action.browser.test.feature-tour", }, }, }, ], }, }, }, }, }, ], }, priority: 2, targeting: `(('messaging-system-action.browser.test.feature-tour' | preferenceValue) ? (('messaging-system-action.browser.test.feature-tour' | preferenceValue | regExpMatch('(?<=complete":)(.*)(?=})')) ? ('messaging-system-action.browser.test.feature-tour' | preferenceValue | regExpMatch('(?<=complete":)(.*)(?=})')[1] != "true") : true) : true)`, trigger: { id: "nthTabClosed" }, }, { id: "TEST_FEATURE_TOUR_2", template: "feature_callout", content: { id: "TEST_FEATURE_TOUR_2", template: "multistage", backdrop: "transparent", transitions: false, disableHistoryUpdates: true, screens: [ { id: "FEATURE_CALLOUT_TEST", anchors: [ { selector: "#PanelUI-menu-button", arrow_position: "top-center-arrow-end", }, ], content: { position: "callout", title: { string_id: "callout-pdfjs-edit-title" }, subtitle: { string_id: "callout-pdfjs-edit-body-b" }, primary_button: { label: { string_id: "callout-pdfjs-edit-button" }, action: { dismiss: true }, }, }, }, ], }, priority: 1, targeting: "true", trigger: { id: "nthTabClosed" }, }, ]; const getMessagesStub = sandbox.stub(FeatureCalloutMessages, "getMessages"); getMessagesStub.returns(TEST_MESSAGES); await ASRouter._updateMessageProviders(); await ASRouter.loadMessagesFromAllProviders( ASRouter.state.providers.filter(p => p.id === "onboarding") ); // Test that callout is triggered and shown in browser chrome const win1 = await BrowserTestUtils.openNewBrowserWindow(); win1.focus(); const tab1 = await BrowserTestUtils.openNewForegroundTab(win1.gBrowser); await TestUtils.waitForTick(); win1.gBrowser.removeTab(tab1); await waitForCalloutScreen( win1.document, TEST_MESSAGES[0].content.screens[0].id ); ok( win1.document.querySelector(calloutSelector), "Feature Callout is rendered in the browser chrome when a message is available" ); // Test that a callout does NOT appear if another is already shown in any window. const showFeatureCalloutSpy = sandbox.spy( FeatureCalloutBroker, "showFeatureCallout" ); const win2 = await BrowserTestUtils.openNewBrowserWindow(); win2.focus(); const tab2 = await BrowserTestUtils.openNewForegroundTab(win2.gBrowser); await TestUtils.waitForTick(); win2.gBrowser.removeTab(tab2); await BrowserTestUtils.waitForCondition(async () => { const rvs = await Promise.all(showFeatureCalloutSpy.returnValues); return ( showFeatureCalloutSpy.calledWith( win2.gBrowser.selectedBrowser, sinon.match(TEST_MESSAGES[0]) ) && rvs.every(rv => !rv) ); }, "Waiting for showFeatureCallout to be called"); ok( !win2.document.querySelector(calloutSelector), "Feature Callout is not rendered when a callout is already shown in any window" ); await BrowserTestUtils.closeWindow(win2); win1.focus(); await BrowserTestUtils.waitForCondition( async () => Services.focus.activeWindow === win1, "Waiting for window 1 to be active" ); // Test that the tour pref doesn't exist yet ok( !Services.prefs.prefHasUserValue(TEST_MESSAGES[0].content.tour_pref_name), "Tour pref does not exist yet" ); // Test that the callout advances screen and sets the tour pref win1.document.querySelector(calloutCTASelector).click(); await BrowserTestUtils.waitForCondition( () => Services.prefs.prefHasUserValue(TEST_MESSAGES[0].content.tour_pref_name), "Waiting for tour pref to be set" ); SimpleTest.isDeeply( JSON.parse( Services.prefs.getStringPref( TEST_MESSAGES[0].content.tour_pref_name, "{}" ) ), { screen: "FEATURE_CALLOUT_2", complete: false }, "Tour pref is set correctly" ); await waitForCalloutScreen( win1.document, TEST_MESSAGES[0].content.screens[1].id ); ok( win1.document.querySelector(calloutSelector), "Feature Callout screen 2 is rendered" ); // Test that the callout is dismissed and cleans up the tour pref win1.document.querySelector(calloutCTASelector).click(); await waitForCalloutRemoved(win1.document); ok( !win1.document.querySelector(calloutSelector), "Feature Callout is not rendered after being dismissed" ); ok( !Services.prefs.prefHasUserValue(TEST_MESSAGES[0].content.tour_pref_name), "Tour pref is cleaned up correctly" ); await BrowserTestUtils.waitForCondition( () => !FeatureCalloutBroker.isCalloutShowing, "Waiting for all callouts to empty from the callout broker" ); // Test that the message was blocked so a different callout is shown const tab3 = await BrowserTestUtils.openNewForegroundTab(win1.gBrowser); await TestUtils.waitForTick(); win1.gBrowser.removeTab(tab3); await waitForCalloutScreen( win1.document, TEST_MESSAGES[1].content.screens[0].id ); ok( win1.document.querySelector(calloutSelector), "A different Feature Callout is rendered" ); win1.document.querySelector(calloutCTASelector).click(); await waitForCalloutRemoved(win1.document); ok(!FeatureCalloutBroker.isCalloutShowing, "No Feature Callout is shown"); BrowserTestUtils.closeWindow(win1); sandbox.restore(); await ASRouter.unblockMessageById(TEST_MESSAGES[0].id); await ASRouter.resetMessageState(); await ASRouter._updateMessageProviders(); await ASRouter.loadMessagesFromAllProviders( ASRouter.state.providers.filter(p => p.id === "onboarding") ); }); add_task(async function callout_not_shown_if_dialog_open() { const win = await BrowserTestUtils.openNewBrowserWindow(); let dialogPromise = BrowserTestUtils.promiseAlertDialog(null, undefined, { callback: async dialogWin => { let rv = await FeatureCalloutBroker.showFeatureCallout( win.gBrowser.selectedBrowser, testMessage.message ); ok( !rv, "Feature callout not shown when a dialog is open in the same window" ); dialogWin.document.querySelector("dialog").getButton("cancel").click(); }, isSubDialog: true, }); DefaultBrowserCheck.prompt(win); await dialogPromise; await BrowserTestUtils.closeWindow(win); await SpecialPowers.popPrefEnv(); }); add_task(async function callout_not_shown_if_panel_open() { const win = await BrowserTestUtils.openNewBrowserWindow(); const gCUITestUtils = new CustomizableUITestUtils(win); await gCUITestUtils.openMainMenu(); let rv = await FeatureCalloutBroker.showFeatureCallout( win.gBrowser.selectedBrowser, testMessage.message ); ok(!rv, "Feature callout not shown when a panel is open in the same window"); await gCUITestUtils.hideMainMenu(); await BrowserTestUtils.closeWindow(win); }); add_task(async function feature_callout_renders_in_browser_chrome_for_pdf() { const sandbox = sinon.createSandbox(); const sendTriggerStub = sandbox.stub(ASRouter, "sendTriggerMessage"); sendTriggerStub.withArgs(pdfMatch).resolves(testMessage); sendTriggerStub.callThrough(); const win = await BrowserTestUtils.openNewBrowserWindow(); await openURLInWindow(win, PDF_TEST_URL); const doc = win.document; await waitForCalloutScreen(doc, testMessageScreenId); const container = doc.querySelector(calloutSelector); ok( container, "Feature Callout is rendered in the browser chrome with a new window when a message is available" ); // click CTA to close doc.querySelector(calloutCTASelector).click(); await waitForCalloutRemoved(doc); ok( true, "Feature callout removed from browser chrome after clicking button configured to navigate" ); await BrowserTestUtils.closeWindow(win); sandbox.restore(); }); add_task( async function feature_callout_renders_and_hides_in_chrome_when_switching_tabs() { const sandbox = sinon.createSandbox(); const sendTriggerStub = sandbox.stub(ASRouter, "sendTriggerMessage"); sendTriggerStub.withArgs(pdfMatch).resolves(testMessage); sendTriggerStub.callThrough(); const win = await BrowserTestUtils.openNewBrowserWindow(); const doc = win.document; const tab1 = await BrowserTestUtils.openNewForegroundTab( win.gBrowser, PDF_TEST_URL ); tab1.focus(); await waitForCalloutScreen(doc, testMessageScreenId); ok( doc.querySelector(`.${testMessageScreenId}`), "Feature callout rendered when opening a new tab with PDF url" ); const tab2 = await openURLInNewTab(win, "about:preferences"); tab2.focus(); await BrowserTestUtils.waitForCondition(() => { return !doc.body.querySelector( "#multi-stage-message-root.featureCallout" ); }); ok( !doc.querySelector(`.${testMessageScreenId}`), "Feature callout removed when tab without PDF URL is navigated to" ); const tab3 = await openURLInNewTab(win, PDF_TEST_URL); tab3.focus(); await waitForCalloutScreen(doc, testMessageScreenId); ok( doc.querySelector(`.${testMessageScreenId}`), "Feature callout still renders when opening a new tab with PDF url after being initially rendered on another tab" ); tab1.focus(); await waitForCalloutScreen(doc, testMessageScreenId); ok( doc.querySelector(`.${testMessageScreenId}`), "Feature callout rendered on original tab after switching tabs multiple times" ); await BrowserTestUtils.closeWindow(win); sandbox.restore(); } ); add_task( async function feature_callout_disappears_when_navigating_to_non_pdf_url_in_same_tab() { const sandbox = sinon.createSandbox(); const sendTriggerStub = sandbox.stub(ASRouter, "sendTriggerMessage"); sendTriggerStub.withArgs(pdfMatch).resolves(testMessage); sendTriggerStub.callThrough(); const win = await BrowserTestUtils.openNewBrowserWindow(); const doc = win.document; const tab1 = await BrowserTestUtils.openNewForegroundTab( win.gBrowser, PDF_TEST_URL ); tab1.focus(); await waitForCalloutScreen(doc, testMessageScreenId); ok( doc.querySelector(`.${testMessageScreenId}`), "Feature callout rendered when opening a new tab with PDF url" ); BrowserTestUtils.startLoadingURIString(win.gBrowser, "about:preferences"); await BrowserTestUtils.waitForLocationChange( win.gBrowser, "about:preferences" ); await waitForCalloutRemoved(doc); ok( !doc.querySelector(`.${testMessageScreenId}`), "Feature callout not rendered on original tab after navigating to non pdf URL" ); await BrowserTestUtils.closeWindow(win); sandbox.restore(); } ); add_task( async function feature_callout_disappears_when_closing_foreground_pdf_tab() { const sandbox = sinon.createSandbox(); const sendTriggerStub = sandbox.stub(ASRouter, "sendTriggerMessage"); sendTriggerStub.withArgs(pdfMatch).resolves(testMessage); sendTriggerStub.callThrough(); const win = await BrowserTestUtils.openNewBrowserWindow(); const doc = win.document; const tab1 = await BrowserTestUtils.openNewForegroundTab( win.gBrowser, PDF_TEST_URL ); tab1.focus(); await waitForCalloutScreen(doc, testMessageScreenId); ok( doc.querySelector(`.${testMessageScreenId}`), "Feature callout rendered when opening a new tab with PDF url" ); BrowserTestUtils.removeTab(tab1); await waitForCalloutRemoved(doc); ok( !doc.querySelector(`.${testMessageScreenId}`), "Feature callout disappears after closing foreground tab" ); await BrowserTestUtils.closeWindow(win); sandbox.restore(); } ); add_task( async function feature_callout_does_not_appear_when_opening_background_pdf_tab() { const sandbox = sinon.createSandbox(); const getMessagesStub = sandbox.stub(FeatureCalloutMessages, "getMessages"); const TEST_MESSAGES = [newtabTestMessage]; getMessagesStub.returns(TEST_MESSAGES); await ASRouter._updateMessageProviders(); await ASRouter.loadMessagesFromAllProviders( ASRouter.state.providers.filter(p => p.id === "onboarding") ); const win = await BrowserTestUtils.openNewBrowserWindow(); const doc = win.document; const tab1 = await BrowserTestUtils.addTab(win.gBrowser, PDF_TEST_URL); ok( !doc.querySelector(`.${testMessageScreenId}`), "Feature callout not rendered when opening a background tab with PDF url" ); BrowserTestUtils.removeTab(tab1); ok( !doc.querySelector(`.${testMessageScreenId}`), "Feature callout still not rendered after closing background tab with PDF url" ); await BrowserTestUtils.closeWindow(win); sandbox.restore(); await ASRouter._updateMessageProviders(); await ASRouter.loadMessagesFromAllProviders(); } ); add_task( async function newtab_feature_callout_appears_in_browser_chrome_on_newtab() { const sandbox = sinon.createSandbox(); const getMessagesStub = sandbox.stub(FeatureCalloutMessages, "getMessages"); const TEST_MESSAGES = [newtabTestMessage]; getMessagesStub.returns(TEST_MESSAGES); await ASRouter._updateMessageProviders(); await ASRouter.loadMessagesFromAllProviders(); const win = await BrowserTestUtils.openNewBrowserWindow(); const doc = win.document; const tab1 = await BrowserTestUtils.openNewForegroundTab( win.gBrowser, "about:newtab" ); tab1.focus(); await waitForCalloutScreen(doc, newtabTestMessageScreenId); ok( doc.querySelector(`.${newtabTestMessageScreenId}`), "Newtab feature callout rendered when opening a focused newtab" ); BrowserTestUtils.removeTab(tab1); await waitForCalloutRemoved(doc); ok( !doc.querySelector(`.${newtabTestMessageScreenId}`), "Feature callout disappears after closing new tab" ); await BrowserTestUtils.closeWindow(win); sandbox.restore(); await ASRouter._updateMessageProviders(); await ASRouter.loadMessagesFromAllProviders(); } ); add_task( async function newtab_feature_callout_does_not_appear_when_opening_background_newtab_tab() { const sandbox = sinon.createSandbox(); const getMessagesStub = sandbox.stub(FeatureCalloutMessages, "getMessages"); const TEST_MESSAGES = [newtabTestMessage]; getMessagesStub.returns(TEST_MESSAGES); await ASRouter._updateMessageProviders(); await ASRouter.loadMessagesFromAllProviders(); const win = await BrowserTestUtils.openNewBrowserWindow(); const doc = win.document; await BrowserTestUtils.openNewForegroundTab( win.gBrowser, "about:preferences" ); const tab2 = await BrowserTestUtils.addTab(win.gBrowser, "about:newtab"); ok( !doc.querySelector(`.${newtabTestMessageScreenId}`), "Newtab feature callout not rendered when opening a background newtab" ); BrowserTestUtils.removeTab(tab2); await waitForCalloutRemoved(doc); ok( !doc.querySelector(`.${newtabTestMessageScreenId}`), "Feature callout still not rendered after closing background tab" ); await BrowserTestUtils.closeWindow(win); sandbox.restore(); await ASRouter._updateMessageProviders(); await ASRouter.loadMessagesFromAllProviders(); } ); add_task( async function newtab_feature_callout_does_not_appear_in_browser_chrome_on_new_window() { const sandbox = sinon.createSandbox(); const getMessagesStub = sandbox.stub(FeatureCalloutMessages, "getMessages"); const TEST_MESSAGES = [newtabTestMessage]; getMessagesStub.returns(TEST_MESSAGES); await ASRouter._updateMessageProviders(); await ASRouter.loadMessagesFromAllProviders(); const win = await BrowserTestUtils.openNewBrowserWindow(); await openURLInWindow(win, "about:newtab"); const doc = win.document; await waitForCalloutScreen(doc, newtabTestMessageScreenId); ok( doc.querySelector(`.${newtabTestMessageScreenId}`), "Newtab Feature Callout is in the browser chrome of first window when a message is available" ); const win2 = await BrowserTestUtils.openNewBrowserWindow(); await openURLInWindow(win2, "about:newtab"); const doc2 = win2.document; ok( !doc2.querySelector(`.${newtabTestMessageScreenId}`), "Newtab Feature Callout is not in the browser chrome new window when a message is available" ); await BrowserTestUtils.closeWindow(win); await BrowserTestUtils.closeWindow(win2); sandbox.restore(); await ASRouter._updateMessageProviders(); await ASRouter.loadMessagesFromAllProviders(); } ); add_task( async function feature_callout_disappears_when_navigating_from_newtab_to_pdf_url_in_same_tab() { const sandbox = sinon.createSandbox(); const getMessagesStub = sandbox.stub(FeatureCalloutMessages, "getMessages"); const TEST_MESSAGES = [newtabTestMessage]; getMessagesStub.returns(TEST_MESSAGES); await ASRouter._updateMessageProviders(); await ASRouter.loadMessagesFromAllProviders(); const win = await BrowserTestUtils.openNewBrowserWindow(); const doc = win.document; const tab1 = await BrowserTestUtils.openNewForegroundTab( win.gBrowser, "about:newtab" ); tab1.focus(); await waitForCalloutScreen(doc, newtabTestMessageScreenId); ok( doc.querySelector(`.${newtabTestMessageScreenId}`), "Feature callout rendered when opening a newtab" ); BrowserTestUtils.startLoadingURIString(win.gBrowser, PDF_TEST_URL); await BrowserTestUtils.waitForLocationChange(win.gBrowser, PDF_TEST_URL); await waitForCalloutRemoved(doc); ok( !doc.querySelector(`.${testMessageScreenId}`), "Feature callout not rendered on original tab after navigating to PDF" ); await BrowserTestUtils.closeWindow(win); sandbox.restore(); await ASRouter._updateMessageProviders(); await ASRouter.loadMessagesFromAllProviders(); } ); add_task( async function feature_callout_disappears_when_navigating_from_newtab_to_pdf_url_in_different_tab() { const sandbox = sinon.createSandbox(); const getMessagesStub = sandbox.stub(FeatureCalloutMessages, "getMessages"); const TEST_MESSAGES = [newtabTestMessage]; getMessagesStub.returns(TEST_MESSAGES); await ASRouter._updateMessageProviders(); await ASRouter.loadMessagesFromAllProviders(); const win = await BrowserTestUtils.openNewBrowserWindow(); const doc = win.document; const tab1 = await BrowserTestUtils.openNewForegroundTab( win.gBrowser, "about:newtab" ); tab1.focus(); await waitForCalloutScreen(doc, newtabTestMessageScreenId); ok( doc.querySelector(`.${newtabTestMessageScreenId}`), "Feature callout rendered when opening a newtab" ); const tab2 = await BrowserTestUtils.openNewForegroundTab( win.gBrowser, PDF_TEST_URL ); tab2.focus(); await waitForCalloutRemoved(doc); ok( !doc.querySelector(`.${testMessageScreenId}`), "Newtab feature callout not rendered after navigating to PDF" ); await BrowserTestUtils.closeWindow(win); sandbox.restore(); await ASRouter._updateMessageProviders(); await ASRouter.loadMessagesFromAllProviders(); } ); add_task( async function feature_callout_is_positioned_relative_to_browser_window() { // Deep copying our test message so we can alter it without disrupting future tests const pdfTestMessage = JSON.parse(JSON.stringify(testMessage)); const pdfTestMessageCalloutSelector = pdfTestMessage.message.content.screens[0].id; pdfTestMessage.message.content.screens[0].anchors[0] = { selector: "hbox#browser", absolute_position: { top: "45px", right: "25px" }, }; const sandbox = sinon.createSandbox(); const getMessagesStub = sandbox.stub(FeatureCalloutMessages, "getMessages"); const TEST_MESSAGES = [pdfTestMessage.message]; getMessagesStub.returns(TEST_MESSAGES); await ASRouter._updateMessageProviders(); await ASRouter.loadMessagesFromAllProviders(); const win = await BrowserTestUtils.openNewBrowserWindow(); await openURLInWindow(win, PDF_TEST_URL); const doc = win.document; await waitForCalloutScreen(doc, pdfTestMessageCalloutSelector); // Verify that callout renders in appropriate position (without infobar element) const callout = doc.querySelector(`.${pdfTestMessageCalloutSelector}`); ok(callout, "Callout is rendered when navigating to PDF file"); ok( validateCalloutCustomPosition( callout, pdfTestMessage.message.content.screens[0].anchors[0].absolute_position, doc ), "Callout custom position is as expected" ); // Add height to the top of the browser to simulate an infobar or other element const navigatorToolBox = doc.querySelector("#navigator-toolbox"); navigatorToolBox.style.height = "150px"; // We test in a new tab because the callout does not adjust itself // when size of the navigator-toolbox-background box changes. const tab = await openURLInNewTab(win, "https://example.com/some2.pdf"); // Verify that callout renders in appropriate position (with infobar element displayed) ok( validateCalloutCustomPosition( callout, pdfTestMessage.message.content.screens[0].anchors[0].absolute_position, doc ), "Callout custom position is as expected while navigator toolbox height is extended" ); BrowserTestUtils.removeTab(tab); await BrowserTestUtils.closeWindow(win); sandbox.restore(); await ASRouter._updateMessageProviders(); await ASRouter.loadMessagesFromAllProviders(); } ); add_task( async function custom_position_callout_is_horizontally_reversed_in_rtl_layouts() { // Deep copying our test message so we can alter it without disrupting future tests const pdfTestMessage = JSON.parse(JSON.stringify(testMessage)); const pdfTestMessageCalloutSelector = pdfTestMessage.message.content.screens[0].id; pdfTestMessage.message.content.screens[0].anchors[0] = { selector: "hbox#browser", absolute_position: { top: "45px", right: "25px" }, }; const sandbox = sinon.createSandbox(); const sendTriggerStub = sandbox.stub(ASRouter, "sendTriggerMessage"); sendTriggerStub.withArgs(pdfMatch).resolves(pdfTestMessage); sendTriggerStub.callThrough(); const win = await BrowserTestUtils.openNewBrowserWindow(); win.document.dir = "rtl"; Assert.strictEqual( win.document.documentElement.getAttribute("dir"), "rtl", "browser window is in RTL" ); await openURLInWindow(win, PDF_TEST_URL); const doc = win.document; await waitForCalloutScreen(doc, pdfTestMessageCalloutSelector); const callout = doc.querySelector(`.${pdfTestMessageCalloutSelector}`); ok(callout, "Callout is rendered when navigating to PDF file"); ok( validateCalloutRTLPosition( callout, pdfTestMessage.message.content.screens[0].anchors[0].absolute_position ), "Callout custom position is rendered appropriately in RTL mode" ); await BrowserTestUtils.closeWindow(win); sandbox.restore(); } ); add_task(async function feature_callout_dismissed_on_escape() { const sandbox = sinon.createSandbox(); const sendTriggerStub = sandbox.stub(ASRouter, "sendTriggerMessage"); sendTriggerStub.withArgs(pdfMatch).resolves(testMessage); sendTriggerStub.callThrough(); const win = await BrowserTestUtils.openNewBrowserWindow(); await openURLInWindow(win, PDF_TEST_URL); const doc = win.document; await waitForCalloutScreen(doc, testMessageScreenId); const container = doc.querySelector(calloutSelector); ok( container, "Feature Callout is rendered in the browser chrome with a new window when a message is available" ); // Ensure the browser is focused win.gBrowser.selectedBrowser.focus(); // Press Escape to close EventUtils.synthesizeKey("KEY_Escape", {}, win); await waitForCalloutRemoved(doc); ok(true, "Feature callout dismissed after pressing Escape"); await BrowserTestUtils.closeWindow(win); sandbox.restore(); }); add_task( async function feature_callout_not_dismissed_on_escape_with_interactive_elm_focused() { const sandbox = sinon.createSandbox(); const sendTriggerStub = sandbox.stub(ASRouter, "sendTriggerMessage"); sendTriggerStub.withArgs(pdfMatch).resolves(testMessage); sendTriggerStub.callThrough(); const win = await BrowserTestUtils.openNewBrowserWindow(); await openURLInWindow(win, PDF_TEST_URL); const doc = win.document; await waitForCalloutScreen(doc, testMessageScreenId); const container = doc.querySelector(calloutSelector); ok( container, "Feature Callout is rendered in the browser chrome with a new window when a message is available" ); // Ensure an interactive element is focused win.gURLBar.focus(); // Press Escape to close EventUtils.synthesizeKey("KEY_Escape", {}, win); await TestUtils.waitForTick(); // Wait 500ms for transition to complete // eslint-disable-next-line mozilla/no-arbitrary-setTimeout await new Promise(resolve => setTimeout(resolve, 500)); ok( doc.querySelector(calloutSelector), "Feature callout is not dismissed after pressing Escape because an interactive element is focused" ); await BrowserTestUtils.closeWindow(win); sandbox.restore(); } ); add_task(async function first_anchor_selected_is_valid() { const win = await BrowserTestUtils.openNewBrowserWindow(); const config = { win, location: "chrome", context: "chrome", browser: win.gBrowser.selectedBrowser, theme: { preset: "chrome" }, }; const message = JSON.parse(JSON.stringify(testMessage.message)); const sandbox = sinon.createSandbox(); const doc = win.document; const featureCallout = new FeatureCallout(config); const getAnchorSpy = sandbox.spy(featureCallout, "_getAnchor"); featureCallout.showFeatureCallout(message); await waitForCalloutScreen(doc, message.content.screens[0].id); ok( getAnchorSpy.alwaysReturned( sandbox.match(message.content.screens[0].anchors[0]) ), "The first anchor is selected" ); win.document.querySelector(calloutCTASelector).click(); await waitForCalloutRemoved(win.document); await BrowserTestUtils.closeWindow(win); sandbox.restore(); }); add_task(async function first_anchor_selected_is_invalid() { const win = await BrowserTestUtils.openNewBrowserWindow(); const doc = win.document; const config = { win, location: "chrome", context: "chrome", browser: win.gBrowser.selectedBrowser, theme: { preset: "chrome" }, }; let stopReloadButton = doc.getElementById("stop-reload-button"); await gCustomizeMode.addToPanel(stopReloadButton); const message = JSON.parse(JSON.stringify(testMessage.message)); message.content.screens[0].anchors = [ // element that does not exist { selector: "#some-fake-id.some-fake-class", arrow_position: "top" }, // element that exists but has no height/width { selector: "#a11y-announcement", arrow_position: "top" }, // element that exists but is hidden by CSS { selector: "#window-modal-dialog", arrow_position: "top" }, // customizable widget that's in the overflow panel { selector: "#stop-reload-button", arrow_position: "top" }, // element that is fully visible { selector: "#PanelUI-menu-button", arrow_position: "top" }, ]; const sandbox = sinon.createSandbox(); const featureCallout = new FeatureCallout(config); const getAnchorSpy = sandbox.spy(featureCallout, "_getAnchor"); featureCallout.showFeatureCallout(message); await waitForCalloutScreen(doc, message.content.screens[0].id); is( getAnchorSpy.lastCall.returnValue.selector, message.content.screens[0].anchors[4].selector, "The first valid anchor (anchor 5) is selected" ); win.document.querySelector(calloutCTASelector).click(); await waitForCalloutRemoved(win.document); CustomizableUI.reset(); await BrowserTestUtils.closeWindow(win); sandbox.restore(); });