/* Any copyright is dedicated to the Public Domain. * http://creativecommons.org/publicdomain/zero/1.0/ */ "use strict"; const { ASRouter } = ChromeUtils.import( "resource://activity-stream/lib/ASRouter.jsm" ); const calloutId = "multi-stage-message-root"; const calloutSelector = `#${calloutId}.featureCallout`; const primaryButtonSelector = `#${calloutId} .primary`; const PDF_TEST_URL = "https://example.com/browser/browser/components/newtab/test/browser/file_pdf.PDF"; const waitForCalloutScreen = async (doc, screenId) => { await BrowserTestUtils.waitForCondition(() => { return doc.querySelector(`${calloutSelector}:not(.hidden) .${screenId}`); }); }; const waitForRemoved = async doc => { await BrowserTestUtils.waitForCondition(() => { return !doc.querySelector(calloutSelector); }); }; async function openURLInWindow(window, url) { const { selectedBrowser } = window.gBrowser; BrowserTestUtils.loadURIString(selectedBrowser, url); await BrowserTestUtils.browserLoaded(selectedBrowser, false, url); } async function openURLInNewTab(window, url) { return BrowserTestUtils.openNewForegroundTab(window.gBrowser, url); } const pdfMatch = sinon.match(val => { return val?.id === "featureCalloutCheck" && val?.context?.source === "chrome"; }); const validateCalloutCustomPosition = (element, positionOverride, doc) => { const browserBox = doc.querySelector("hbox#browser"); for (let position in positionOverride) { if (Object.prototype.hasOwnProperty.call(positionOverride, position)) { // The substring here is to remove the `px` at the end of our position override strings const relativePos = positionOverride[position].substring( 0, positionOverride[position].length - 2 ); 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, positionOverride) => { for (let position in positionOverride) { if (Object.prototype.hasOwnProperty.call(positionOverride, position)) { const pixelPosition = positionOverride[position]; if (position === "left") { const actualLeft = Number( pixelPosition.substring(0, pixelPosition.length - 2) ); if (element.getBoundingClientRect().right !== actualLeft) { return false; } } else if (position === "right") { const expectedLeft = Number( pixelPosition.substring(0, pixelPosition.length - 2) ); if (element.getBoundingClientRect().left !== expectedLeft) { 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", parent_selector: "#urlbar-container", content: { position: "callout", arrow_position: "top-end", title: { raw: "Test title", }, subtitle: { raw: "Test subtitle", }, primary_button: { label: { raw: "Done", }, action: { navigate: true, }, }, }, }, ], }, priority: 1, targeting: "true", trigger: { id: "featureCalloutCheck" }, }, }; const testMessageCalloutSelector = testMessage.message.content.screens[0].id; add_setup(async function () { requestLongerTimeout(2); }); 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, testMessageCalloutSelector); 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 primary button to close doc.querySelector(primaryButtonSelector).click(); await waitForRemoved(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, testMessageCalloutSelector); ok( doc.querySelector(`.${testMessageCalloutSelector}`), "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(`.${testMessageCalloutSelector}`), "Feature callout removed when tab without PDF URL is navigated to" ); const tab3 = await openURLInNewTab(win, PDF_TEST_URL); tab3.focus(); await waitForCalloutScreen(doc, testMessageCalloutSelector); ok( doc.querySelector(`.${testMessageCalloutSelector}`), "Feature callout still renders when opening a new tab with PDF url after being initially rendered on another tab" ); tab1.focus(); await waitForCalloutScreen(doc, testMessageCalloutSelector); ok( doc.querySelector(`.${testMessageCalloutSelector}`), "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, testMessageCalloutSelector); ok( doc.querySelector(`.${testMessageCalloutSelector}`), "Feature callout rendered when opening a new tab with PDF url" ); BrowserTestUtils.loadURIString(win.gBrowser, "about:preferences"); await BrowserTestUtils.waitForLocationChange( win.gBrowser, "about:preferences" ); await waitForRemoved(doc); ok( !doc.querySelector(`.${testMessageCalloutSelector}`), "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, testMessageCalloutSelector); ok( doc.querySelector(`.${testMessageCalloutSelector}`), "Feature callout rendered when opening a new tab with PDF url" ); BrowserTestUtils.removeTab(tab1); await waitForRemoved(doc); ok( !doc.querySelector(`.${testMessageCalloutSelector}`), "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 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.addTab(win.gBrowser, PDF_TEST_URL); ok( !doc.querySelector(`.${testMessageCalloutSelector}`), "Feature callout not rendered when opening a background tab with PDF url" ); BrowserTestUtils.removeTab(tab1); ok( !doc.querySelector(`.${testMessageCalloutSelector}`), "Feature callout still not rendered after closing background tab with PDF url" ); await BrowserTestUtils.closeWindow(win); sandbox.restore(); } ); 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].parent_selector = "hbox#browser"; pdfTestMessage.message.content.screens[0].content.callout_position_override = { 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(); 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].content .callout_position_override, 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-background"); 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].content .callout_position_override, doc ), "Callout custom position is as expected while navigator toolbox height is extended" ); BrowserTestUtils.removeTab(tab); await BrowserTestUtils.closeWindow(win); sandbox.restore(); } ); 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].parent_selector = "hbox#browser"; pdfTestMessage.message.content.screens[0].content.callout_position_override = { 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"; ok( 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].content .callout_position_override ), "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, testMessageCalloutSelector); 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 waitForRemoved(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, testMessageCalloutSelector); 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(); } );