diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 00:47:55 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 00:47:55 +0000 |
commit | 26a029d407be480d791972afb5975cf62c9360a6 (patch) | |
tree | f435a8308119effd964b339f76abb83a57c29483 /browser/components/asrouter/tests/browser/browser_feature_callout_in_chrome.js | |
parent | Initial commit. (diff) | |
download | firefox-26a029d407be480d791972afb5975cf62c9360a6.tar.xz firefox-26a029d407be480d791972afb5975cf62c9360a6.zip |
Adding upstream version 124.0.1.upstream/124.0.1
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'browser/components/asrouter/tests/browser/browser_feature_callout_in_chrome.js')
-rw-r--r-- | browser/components/asrouter/tests/browser/browser_feature_callout_in_chrome.js | 1122 |
1 files changed, 1122 insertions, 0 deletions
diff --git a/browser/components/asrouter/tests/browser/browser_feature_callout_in_chrome.js b/browser/components/asrouter/tests/browser/browser_feature_callout_in_chrome.js new file mode 100644 index 0000000000..b7169d6be7 --- /dev/null +++ b/browser/components/asrouter/tests/browser/browser_feature_callout_in_chrome.js @@ -0,0 +1,1122 @@ +/* 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(); +}); |