1
0
Fork 0
firefox/toolkit/components/normandy/test/browser/browser_Heartbeat.js
Daniel Baumann 5e9a113729
Adding upstream version 140.0.
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
2025-06-25 09:37:52 +02:00

293 lines
8.3 KiB
JavaScript

"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 */
async function assertTelemetrySent(hb, eventNames) {
let submitted = false;
await GleanPings.heartbeat.testSubmission(
() => {
let events = [0];
for (const name of eventNames) {
const metricName = name.slice(0, -2); // strip the "TS".
const ts = Glean.heartbeat[metricName].testGetValue().getTime();
Assert.equal(typeof ts, "number");
events.push(ts);
}
events.push(Date.now());
assertOrdered(events);
submitted = true;
},
async () => {
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);
Assert.ok(submitted, "'heartbeat' ping was submitted.");
resolve();
});
});
}
);
}
async function getUpdatedNotice(heartbeat) {
let notice = await heartbeat.noticePromise;
// ensure notice is updated and the DOM is ready to be queried
await notice.updateComplete;
return notice;
}
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",
});
let notice = await getUpdatedNotice(hb);
// Check UI
const learnMoreEl = notice.supportLinkEls[0];
Assert.equal(
notificationBox.allNotifications.length,
preCount + 1,
"Correct number of notifications open"
);
const fiveStarComponent =
notice.buttonContainer.querySelector("moz-five-star");
Assert.ok(fiveStarComponent, "moz-five-star component exists");
Assert.equal(
fiveStarComponent.selectable,
true,
"moz-five-star component is set to selectable"
);
Assert.equal(
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(
notice.messageText.textContent.trim(),
"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",
});
let notice = await getUpdatedNotice(hb);
const engagementButton = notice.buttonContainer.querySelector(
".notification-button"
);
Assert.ok(
!notice.buttonContainer.querySelector("moz-five-star"),
"moz-five-star does not exist"
);
Assert.ok(engagementButton, "Engagement button added");
Assert.equal(
engagementButton.label,
"Click me!",
"Engagement button has correct label"
);
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 }
);
});
engagementButton.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);
});