diff options
Diffstat (limited to 'toolkit/components/normandy/test/browser/browser_Heartbeat.js')
-rw-r--r-- | toolkit/components/normandy/test/browser/browser_Heartbeat.js | 262 |
1 files changed, 262 insertions, 0 deletions
diff --git a/toolkit/components/normandy/test/browser/browser_Heartbeat.js b/toolkit/components/normandy/test/browser/browser_Heartbeat.js new file mode 100644 index 0000000000..0166c4d7b0 --- /dev/null +++ b/toolkit/components/normandy/test/browser/browser_Heartbeat.js @@ -0,0 +1,262 @@ +"use strict"; + +const { Heartbeat } = ChromeUtils.importESModule( + "resource://normandy/lib/Heartbeat.sys.mjs" +); + +/** + * Assert an array is in non-descending order, and that every element is a number + */ +function assertOrdered(arr) { + for (let i = 0; i < arr.length; i++) { + Assert.equal(typeof arr[i], "number", `element ${i} is type "number"`); + } + for (let i = 0; i < arr.length - 1; i++) { + Assert.lessOrEqual( + arr[i], + arr[i + 1], + `element ${i} is less than or equal to element ${i + 1}` + ); + } +} + +/* Close every notification in a target window and notification box */ +function closeAllNotifications(targetWindow, notificationBox) { + if (notificationBox.allNotifications.length === 0) { + return Promise.resolve(); + } + + return new Promise(resolve => { + const notificationSet = new Set(notificationBox.allNotifications); + + const observer = new targetWindow.MutationObserver(mutations => { + for (const mutation of mutations) { + for (let i = 0; i < mutation.removedNodes.length; i++) { + const node = mutation.removedNodes.item(i); + if (notificationSet.has(node)) { + notificationSet.delete(node); + } + } + } + if (notificationSet.size === 0) { + Assert.equal( + notificationBox.allNotifications.length, + 0, + "No notifications left" + ); + observer.disconnect(); + resolve(); + } + }); + + observer.observe(notificationBox.stack, { childList: true }); + + for (const notification of notificationBox.allNotifications) { + notification.close(); + } + }); +} + +/* Check that the correct telemetry was sent */ +function assertTelemetrySent(hb, eventNames) { + return new Promise(resolve => { + hb.eventEmitter.once("TelemetrySent", payload => { + const events = [0]; + for (const name of eventNames) { + Assert.equal( + typeof payload[name], + "number", + `payload field ${name} is a number` + ); + events.push(payload[name]); + } + events.push(Date.now()); + + assertOrdered(events); + resolve(); + }); + }); +} + +function getStars(notice) { + return notice.buttonContainer.querySelectorAll(".star-x"); +} + +add_setup(async function () { + let win = await BrowserTestUtils.openNewBrowserWindow(); + // Open a new tab to keep the window open. + await BrowserTestUtils.openNewForegroundTab( + win.gBrowser, + "https://example.com" + ); +}); + +// Several of the behaviors of heartbeat prompt are mutually exclusive, so checks are broken up +// into three batches. + +/* Batch #1 - General UI, Stars, and telemetry data */ +add_task(async function () { + const targetWindow = Services.wm.getMostRecentWindow("navigator:browser"); + const notificationBox = targetWindow.gNotificationBox; + + const preCount = notificationBox.allNotifications.length; + const hb = new Heartbeat(targetWindow, { + testing: true, + flowId: "test", + message: "test", + engagementButtonLabel: undefined, + learnMoreMessage: "Learn More", + learnMoreUrl: "https://example.org/learnmore", + }); + + // Check UI + const learnMoreEl = hb.notice.messageText.querySelector(".text-link"); + Assert.equal( + notificationBox.allNotifications.length, + preCount + 1, + "Correct number of notifications open" + ); + Assert.equal(getStars(hb.notice).length, 5, "Correct number of stars"); + Assert.equal( + hb.notice.buttonContainer.querySelectorAll(".notification-button").length, + 0, + "Engagement button not shown" + ); + Assert.equal( + learnMoreEl.href, + "https://example.org/learnmore", + "Learn more url correct" + ); + Assert.equal(learnMoreEl.value, "Learn More", "Learn more label correct"); + // There's a space included before the learn more link in proton. + Assert.equal( + hb.notice.messageText.textContent, + "test ", + "Message is correct" + ); + + // Check that when clicking the learn more link, a tab opens with the right URL + let loadedPromise; + const tabOpenPromise = new Promise(resolve => { + targetWindow.gBrowser.tabContainer.addEventListener( + "TabOpen", + event => { + let tab = event.target; + loadedPromise = BrowserTestUtils.browserLoaded( + tab.linkedBrowser, + true, + url => url && url !== "about:blank" + ); + resolve(tab); + }, + { once: true } + ); + }); + learnMoreEl.click(); + const tab = await tabOpenPromise; + const tabUrl = await loadedPromise; + + Assert.equal( + tabUrl, + "https://example.org/learnmore", + "Learn more link opened the right url" + ); + + const telemetrySentPromise = assertTelemetrySent(hb, [ + "offeredTS", + "learnMoreTS", + "closedTS", + ]); + // Close notification to trigger telemetry to be sent + await closeAllNotifications(targetWindow, notificationBox); + await telemetrySentPromise; + BrowserTestUtils.removeTab(tab); +}); + +// Batch #2 - Engagement buttons +add_task(async function () { + const targetWindow = Services.wm.getMostRecentWindow("navigator:browser"); + const notificationBox = targetWindow.gNotificationBox; + const hb = new Heartbeat(targetWindow, { + testing: true, + flowId: "test", + message: "test", + engagementButtonLabel: "Click me!", + postAnswerUrl: "https://example.org/postAnswer", + learnMoreMessage: "Learn More", + learnMoreUrl: "https://example.org/learnMore", + }); + const engagementButton = hb.notice.buttonContainer.querySelector( + ".notification-button" + ); + + Assert.equal(getStars(hb.notice).length, 0, "Stars not shown"); + Assert.ok(engagementButton, "Engagement button added"); + Assert.equal( + engagementButton.label, + "Click me!", + "Engagement button has correct label" + ); + + const engagementEl = hb.notice.buttonContainer.querySelector( + ".notification-button" + ); + let loadedPromise; + const tabOpenPromise = new Promise(resolve => { + targetWindow.gBrowser.tabContainer.addEventListener( + "TabOpen", + event => { + let tab = event.target; + loadedPromise = BrowserTestUtils.browserLoaded( + tab.linkedBrowser, + true, + url => url && url !== "about:blank" + ); + resolve(tab); + }, + { once: true } + ); + }); + engagementEl.click(); + const tab = await tabOpenPromise; + const tabUrl = await loadedPromise; + // the postAnswer url gets query parameters appended onto the end, so use Assert.startsWith instead of Assert.equal + Assert.ok( + tabUrl.startsWith("https://example.org/postAnswer"), + "Engagement button opened the right url" + ); + + const telemetrySentPromise = assertTelemetrySent(hb, [ + "offeredTS", + "engagedTS", + "closedTS", + ]); + // Close notification to trigger telemetry to be sent + await closeAllNotifications(targetWindow, notificationBox); + await telemetrySentPromise; + BrowserTestUtils.removeTab(tab); +}); + +// Batch 3 - Closing the window while heartbeat is open +add_task(async function () { + const targetWindow = await BrowserTestUtils.openNewBrowserWindow(); + + const hb = new Heartbeat(targetWindow, { + testing: true, + flowId: "test", + message: "test", + }); + + const telemetrySentPromise = assertTelemetrySent(hb, [ + "offeredTS", + "windowClosedTS", + ]); + // triggers sending ping to normandy + await BrowserTestUtils.closeWindow(targetWindow); + await telemetrySentPromise; +}); + +add_task(async function cleanup() { + const win = Services.wm.getMostRecentWindow("navigator:browser"); + await BrowserTestUtils.closeWindow(win); +}); |