Source code

Revision control

Copy as Markdown

Other Tools

ChromeUtils.defineESModuleGetters(this, {
PlacesUtils: "resource://gre/modules/PlacesUtils.sys.mjs",
});
/**
* Called after opening a new window or switching windows, this will wait until
* we are sure that an attempt to display a notification will not fail.
*/
async function waitForWindowReadyForPopupNotifications(win) {
// These are the same checks that PopupNotifications.sys.mjs makes before it
// allows a notification to open.
await TestUtils.waitForCondition(
() => win.gBrowser.selectedBrowser.docShellIsActive,
"The browser should be active"
);
await TestUtils.waitForCondition(
() => Services.focus.activeWindow == win,
"The window should be active"
);
}
/**
* Waits for a load (or custom) event to finish in a given tab. If provided
* load an uri into the tab.
*
* @param tab
* The tab to load into.
* @param [optional] url
* The url to load, or the current url.
* @return {Promise} resolved when the event is handled.
* @resolves to the received event
* @rejects if a valid load event is not received within a meaningful interval
*/
function promiseTabLoadEvent(tab, url) {
let browser = tab.linkedBrowser;
if (url) {
BrowserTestUtils.startLoadingURIString(browser, url);
}
return BrowserTestUtils.browserLoaded(browser, false, url);
}
// Tests that call setup() should have a `tests` array defined for the actual
// tests to be run.
/* global tests */
function setup() {
// eslint-disable-next-line @microsoft/sdl/no-insecure-url
BrowserTestUtils.openNewForegroundTab(gBrowser, "http://example.com/").then(
goNext
);
registerCleanupFunction(() => {
gBrowser.removeTab(gBrowser.selectedTab);
});
}
function goNext() {
executeSoon(() => executeSoon(runNextTest));
}
async function runNextTest() {
if (!tests.length) {
executeSoon(finish);
return;
}
let nextTest = tests.shift();
if (nextTest.onShown) {
let shownState = false;
onPopupEvent("popupshowing", function () {
info("[" + nextTest.id + "] popup showing");
});
onPopupEvent("popupshown", function () {
shownState = true;
info("[" + nextTest.id + "] popup shown");
(nextTest.onShown(this) || Promise.resolve()).then(undefined, ex =>
Assert.ok(false, "onShown failed: " + ex)
);
});
onPopupEvent(
"popuphidden",
function () {
info("[" + nextTest.id + "] popup hidden");
(nextTest.onHidden(this) || Promise.resolve()).then(
() => goNext(),
ex => Assert.ok(false, "onHidden failed: " + ex)
);
},
() => shownState
);
info(
"[" +
nextTest.id +
"] added listeners; panel is open: " +
PopupNotifications.isPanelOpen
);
}
info("[" + nextTest.id + "] running test");
await nextTest.run();
}
function showNotification(notifyObj) {
info("Showing notification " + notifyObj.id);
return PopupNotifications.show(
notifyObj.browser,
notifyObj.id,
notifyObj.message,
notifyObj.anchorID,
notifyObj.mainAction,
notifyObj.secondaryActions,
notifyObj.options
);
}
function dismissNotification(popup) {
info("Dismissing notification " + popup.childNodes[0].id);
executeSoon(() => EventUtils.synthesizeKey("KEY_Escape"));
}
function BasicNotification(testId) {
this.browser = gBrowser.selectedBrowser;
this.id = "test-notification-" + testId;
this.message = testId + ": Will you allow <> to perform this action?";
this.anchorID = null;
this.mainAction = {
label: "Main Action",
accessKey: "M",
callback: ({ source }) => {
this.mainActionClicked = true;
this.mainActionSource = source;
},
};
this.secondaryActions = [
{
label: "Secondary Action",
accessKey: "S",
callback: ({ source }) => {
this.secondaryActionClicked = true;
this.secondaryActionSource = source;
},
},
];
this.options = {
// eslint-disable-next-line @microsoft/sdl/no-insecure-url
eventCallback: eventName => {
switch (eventName) {
case "dismissed":
this.dismissalCallbackTriggered = true;
break;
case "showing":
this.showingCallbackTriggered = true;
break;
case "shown":
this.shownCallbackTriggered = true;
break;
case "removed":
this.removedCallbackTriggered = true;
break;
case "swapping":
this.swappingCallbackTriggered = true;
break;
}
},
};
}
BasicNotification.prototype.addOptions = function (options) {
for (let [name, value] of Object.entries(options)) {
this.options[name] = value;
}
};
function ErrorNotification(testId) {
BasicNotification.call(this, testId);
this.mainAction.callback = () => {
this.mainActionClicked = true;
throw new Error("Oops!");
};
this.secondaryActions[0].callback = () => {
this.secondaryActionClicked = true;
throw new Error("Oops!");
};
}
ErrorNotification.prototype = BasicNotification.prototype;
function checkPopup(popup, notifyObj) {
info("Checking notification " + notifyObj.id);
ok(notifyObj.showingCallbackTriggered, "showing callback was triggered");
ok(notifyObj.shownCallbackTriggered, "shown callback was triggered");
let notifications = popup.childNodes;
is(notifications.length, 1, "one notification displayed");
let notification = notifications[0];
if (!notification) {
return;
}
// PopupNotifications are not expected to show icons
// unless popupIconURL or popupIconClass is passed in the options object.
if (notifyObj.options.popupIconURL || notifyObj.options.popupIconClass) {
let icon = notification.querySelector(".popup-notification-icon");
if (notifyObj.id == "geolocation") {
isnot(icon.getBoundingClientRect().width, 0, "icon for geo displayed");
ok(
popup.anchorNode.classList.contains("notification-anchor-icon"),
"notification anchored to icon"
);
}
}
let description = notifyObj.message.split("<>");
let text = {};
text.start = description[0];
text.end = description[1];
is(notification.getAttribute("label"), text.start, "message matches");
is(
notification.getAttribute("name"),
notifyObj.options.name,
"message matches"
);
is(notification.getAttribute("endlabel"), text.end, "message matches");
is(notification.id, notifyObj.id + "-notification", "id matches");
if (notifyObj.mainAction) {
is(
notification.getAttribute("buttonlabel"),
notifyObj.mainAction.label,
"main action label matches"
);
is(
notification.getAttribute("buttonaccesskey"),
notifyObj.mainAction.accessKey,
"main action accesskey matches"
);
}
if (notifyObj.secondaryActions && notifyObj.secondaryActions.length) {
let secondaryAction = notifyObj.secondaryActions[0];
is(
notification.getAttribute("secondarybuttonlabel"),
secondaryAction.label,
"secondary action label matches"
);
is(
notification.getAttribute("secondarybuttonaccesskey"),
secondaryAction.accessKey,
"secondary action accesskey matches"
);
}
// Additional secondary actions appear as menu items.
let actualExtraSecondaryActions = Array.prototype.filter.call(
notification.menupopup.childNodes,
child => child.nodeName == "menuitem"
);
let extraSecondaryActions = notifyObj.secondaryActions
? notifyObj.secondaryActions.slice(1)
: [];
is(
actualExtraSecondaryActions.length,
extraSecondaryActions.length,
"number of extra secondary actions matches"
);
extraSecondaryActions.forEach(function (a, i) {
is(
actualExtraSecondaryActions[i].getAttribute("label"),
a.label,
"label for extra secondary action " + i + " matches"
);
is(
actualExtraSecondaryActions[i].getAttribute("accesskey"),
a.accessKey,
"accessKey for extra secondary action " + i + " matches"
);
});
}
ChromeUtils.defineLazyGetter(this, "gActiveListeners", () => {
let listeners = new Map();
registerCleanupFunction(() => {
for (let [listener, eventName] of listeners) {
PopupNotifications.panel.removeEventListener(eventName, listener);
}
});
return listeners;
});
function onPopupEvent(eventName, callback, condition) {
let listener = event => {
if (
event.target != PopupNotifications.panel ||
(condition && !condition())
) {
return;
}
PopupNotifications.panel.removeEventListener(eventName, listener);
gActiveListeners.delete(listener);
executeSoon(() => callback.call(PopupNotifications.panel));
};
gActiveListeners.set(listener, eventName);
PopupNotifications.panel.addEventListener(eventName, listener);
}
function waitForNotificationPanel() {
return new Promise(resolve => {
onPopupEvent("popupshown", function () {
resolve(this);
});
});
}
function waitForNotificationPanelHidden() {
return new Promise(resolve => {
onPopupEvent("popuphidden", function () {
resolve(this);
});
});
}
function triggerMainCommand(popup) {
let notifications = popup.childNodes;
ok(!!notifications.length, "at least one notification displayed");
let notification = notifications[0];
info("Triggering main command for notification " + notification.id);
EventUtils.synthesizeMouseAtCenter(notification.button, {});
}
function triggerSecondaryCommand(popup, index) {
let notifications = popup.childNodes;
ok(!!notifications.length, "at least one notification displayed");
let notification = notifications[0];
info("Triggering secondary command for notification " + notification.id);
if (index == 0) {
EventUtils.synthesizeMouseAtCenter(notification.secondaryButton, {});
return;
}
// Extra secondary actions appear in a menu.
notification.secondaryButton.nextElementSibling.focus();
popup.addEventListener(
"popupshown",
function () {
info("Command popup open for notification " + notification.id);
// Press down until the desired command is selected. Decrease index by one
// since the secondary action was handled above.
for (let i = 0; i <= index - 1; i++) {
EventUtils.synthesizeKey("KEY_ArrowDown");
}
// Activate
EventUtils.synthesizeKey("KEY_Enter");
},
{ once: true }
);
// One down event to open the popup
info(
"Open the popup to trigger secondary command for notification " +
notification.id
);
EventUtils.synthesizeKey("KEY_ArrowDown", {
altKey: !navigator.platform.includes("Mac"),
});
}
/**
* The security delay calculation in PopupNotification.sys.mjs is dependent on
* the monotonically increasing value of Cu.now. This timestamp is
* not relative to a fixed date, but to runtime.
* We need to wait for the value Cu.now() to be larger than the
* security delay in order to observe the bug. Only then does the
* timeSinceShown check in PopupNotifications.sys.mjs lead to a timeSinceShown
* value that is unconditionally greater than lazy.buttonDelay for
* notification.timeShown = null = 0.
*
* When running in automation as part of a larger test suite Cu.now()
* should usually be already sufficiently high in which case this check should
* directly resolve.
*/
async function ensureSecurityDelayReady(timeNewWindowOpened = 0) {
let secDelay = Services.prefs.getIntPref(
"security.notification_enable_delay"
);
await TestUtils.waitForCondition(
() => Cu.now() - timeNewWindowOpened > secDelay,
"Wait for performance.now() > SECURITY_DELAY",
500,
50
);
}