summaryrefslogtreecommitdiffstats
path: root/browser/base/content/test/popupNotifications
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 19:33:14 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 19:33:14 +0000
commit36d22d82aa202bb199967e9512281e9a53db42c9 (patch)
tree105e8c98ddea1c1e4784a60a5a6410fa416be2de /browser/base/content/test/popupNotifications
parentInitial commit. (diff)
downloadfirefox-esr-36d22d82aa202bb199967e9512281e9a53db42c9.tar.xz
firefox-esr-36d22d82aa202bb199967e9512281e9a53db42c9.zip
Adding upstream version 115.7.0esr.upstream/115.7.0esr
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'browser/base/content/test/popupNotifications')
-rw-r--r--browser/base/content/test/popupNotifications/browser.ini38
-rw-r--r--browser/base/content/test/popupNotifications/browser_displayURI.js159
-rw-r--r--browser/base/content/test/popupNotifications/browser_popupNotification.js394
-rw-r--r--browser/base/content/test/popupNotifications/browser_popupNotification_2.js315
-rw-r--r--browser/base/content/test/popupNotifications/browser_popupNotification_3.js377
-rw-r--r--browser/base/content/test/popupNotifications/browser_popupNotification_4.js290
-rw-r--r--browser/base/content/test/popupNotifications/browser_popupNotification_5.js501
-rw-r--r--browser/base/content/test/popupNotifications/browser_popupNotification_accesskey.js44
-rw-r--r--browser/base/content/test/popupNotifications/browser_popupNotification_checkbox.js248
-rw-r--r--browser/base/content/test/popupNotifications/browser_popupNotification_hide_after_identity_panel.js36
-rw-r--r--browser/base/content/test/popupNotifications/browser_popupNotification_hide_after_protections_panel.js44
-rw-r--r--browser/base/content/test/popupNotifications/browser_popupNotification_keyboard.js273
-rw-r--r--browser/base/content/test/popupNotifications/browser_popupNotification_learnmore.js64
-rw-r--r--browser/base/content/test/popupNotifications/browser_popupNotification_no_anchors.js288
-rw-r--r--browser/base/content/test/popupNotifications/browser_popupNotification_security_delay.js296
-rw-r--r--browser/base/content/test/popupNotifications/browser_popupNotification_selection_required.js57
-rw-r--r--browser/base/content/test/popupNotifications/browser_reshow_in_background.js72
-rw-r--r--browser/base/content/test/popupNotifications/head.js367
18 files changed, 3863 insertions, 0 deletions
diff --git a/browser/base/content/test/popupNotifications/browser.ini b/browser/base/content/test/popupNotifications/browser.ini
new file mode 100644
index 0000000000..a5a8ab4eb9
--- /dev/null
+++ b/browser/base/content/test/popupNotifications/browser.ini
@@ -0,0 +1,38 @@
+[DEFAULT]
+support-files =
+ head.js
+
+[browser_displayURI.js]
+skip-if = (os == "linux" && (debug || asan))
+[browser_popupNotification.js]
+skip-if = (os == "linux" && (debug || asan))
+[browser_popupNotification_2.js]
+https_first_disabled = true
+skip-if = (os == "linux" && (debug || asan)) || (os == "linux" && bits == 64 && os_version == "18.04") # bug 1251135
+[browser_popupNotification_3.js]
+https_first_disabled = true
+skip-if = (os == "linux" && (debug || asan)) || verify
+[browser_popupNotification_4.js]
+skip-if = (os == "linux" && (debug || asan))
+[browser_popupNotification_5.js]
+skip-if = true # bug 1332646
+[browser_popupNotification_accesskey.js]
+skip-if = (os == "linux" && (debug || asan)) || os == "mac"
+[browser_popupNotification_checkbox.js]
+skip-if = (os == "linux" && (debug || asan))
+[browser_popupNotification_hide_after_identity_panel.js]
+skip-if = (os == "linux" && (debug || asan))
+[browser_popupNotification_hide_after_protections_panel.js]
+skip-if = (os == "linux" && (debug || asan))
+[browser_popupNotification_keyboard.js]
+skip-if = (os == "linux" && (debug || asan))
+[browser_popupNotification_learnmore.js]
+skip-if = (os == "linux" && (debug || asan))
+[browser_popupNotification_no_anchors.js]
+https_first_disabled = true
+skip-if = (os == "linux" && (debug || asan))
+[browser_popupNotification_security_delay.js]
+[browser_popupNotification_selection_required.js]
+skip-if = (os == "linux" && (debug || asan))
+[browser_reshow_in_background.js]
+skip-if = (os == "linux" && (debug || asan))
diff --git a/browser/base/content/test/popupNotifications/browser_displayURI.js b/browser/base/content/test/popupNotifications/browser_displayURI.js
new file mode 100644
index 0000000000..c9e677cd45
--- /dev/null
+++ b/browser/base/content/test/popupNotifications/browser_displayURI.js
@@ -0,0 +1,159 @@
+/*
+ * Make sure that the correct origin is shown for permission prompts.
+ */
+
+async function check(contentTask, options = {}) {
+ await BrowserTestUtils.withNewTab(
+ "https://test1.example.com/",
+ async function (browser) {
+ let popupShownPromise = waitForNotificationPanel();
+ await SpecialPowers.spawn(browser, [], contentTask);
+ let panel = await popupShownPromise;
+ let notification = panel.children[0];
+ let body = notification.querySelector(".popup-notification-body");
+ ok(
+ body.innerHTML.includes("example.com"),
+ "Check that at least the eTLD+1 is present in the markup"
+ );
+ }
+ );
+
+ let channel = NetUtil.newChannel({
+ uri: getRootDirectory(gTestPath),
+ loadUsingSystemPrincipal: true,
+ });
+ channel = channel.QueryInterface(Ci.nsIFileChannel);
+
+ await BrowserTestUtils.withNewTab(
+ channel.file.path,
+ async function (browser) {
+ let popupShownPromise = waitForNotificationPanel();
+ await SpecialPowers.spawn(browser, [], contentTask);
+ let panel = await popupShownPromise;
+ let notification = panel.children[0];
+ let body = notification.querySelector(".popup-notification-body");
+ if (
+ notification.id == "geolocation-notification" ||
+ notification.id == "xr-notification"
+ ) {
+ ok(
+ body.innerHTML.includes("local file"),
+ `file:// URIs should be displayed as local file.`
+ );
+ } else {
+ ok(
+ body.innerHTML.includes("Unknown origin"),
+ "file:// URIs should be displayed as unknown origin."
+ );
+ }
+ }
+ );
+
+ if (!options.skipOnExtension) {
+ // Test the scenario also on the extension page if not explicitly unsupported
+ // (e.g. an extension page can't be navigated on a blob URL).
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ name: "Test Extension Name",
+ },
+ background() {
+ let { browser } = this;
+ browser.test.sendMessage(
+ "extension-tab-url",
+ browser.runtime.getURL("extension-tab-page.html")
+ );
+ },
+ files: {
+ "extension-tab-page.html": `<!DOCTYPE html><html><body></body></html>`,
+ },
+ });
+
+ await extension.startup();
+ let extensionURI = await extension.awaitMessage("extension-tab-url");
+
+ await BrowserTestUtils.withNewTab(extensionURI, async function (browser) {
+ let popupShownPromise = waitForNotificationPanel();
+ await SpecialPowers.spawn(browser, [], contentTask);
+ let panel = await popupShownPromise;
+ let notification = panel.children[0];
+ let body = notification.querySelector(".popup-notification-body");
+ ok(
+ body.innerHTML.includes("Test Extension Name"),
+ "Check the the extension name is present in the markup"
+ );
+ });
+
+ await extension.unload();
+ }
+}
+
+add_setup(async function () {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["media.navigator.permission.fake", true],
+ ["media.navigator.permission.force", true],
+ ["dom.vr.always_support_vr", true],
+ ],
+ });
+});
+
+add_task(async function test_displayURI_geo() {
+ await check(async function () {
+ content.navigator.geolocation.getCurrentPosition(() => {});
+ });
+});
+
+const kVREnabled = SpecialPowers.getBoolPref("dom.vr.enabled");
+if (kVREnabled) {
+ add_task(async function test_displayURI_xr() {
+ await check(async function () {
+ content.navigator.getVRDisplays();
+ });
+ });
+}
+
+add_task(async function test_displayURI_camera() {
+ await check(async function () {
+ content.navigator.mediaDevices.getUserMedia({ video: true, fake: true });
+ });
+});
+
+add_task(async function test_displayURI_geo_blob() {
+ await check(
+ async function () {
+ let text =
+ "<script>navigator.geolocation.getCurrentPosition(() => {})</script>";
+ let blob = new Blob([text], { type: "text/html" });
+ let url = content.URL.createObjectURL(blob);
+ content.location.href = url;
+ },
+ { skipOnExtension: true }
+ );
+});
+
+if (kVREnabled) {
+ add_task(async function test_displayURI_xr_blob() {
+ await check(
+ async function () {
+ let text = "<script>navigator.getVRDisplays()</script>";
+ let blob = new Blob([text], { type: "text/html" });
+ let url = content.URL.createObjectURL(blob);
+ content.location.href = url;
+ },
+ { skipOnExtension: true }
+ );
+ });
+}
+
+add_task(async function test_displayURI_camera_blob() {
+ await check(
+ async function () {
+ let text =
+ "<script>navigator.mediaDevices.getUserMedia({video: true, fake: true})</script>";
+ let blob = new Blob([text], { type: "text/html" });
+ let url = content.URL.createObjectURL(blob);
+ content.location.href = url;
+ },
+ { skipOnExtension: true }
+ );
+});
diff --git a/browser/base/content/test/popupNotifications/browser_popupNotification.js b/browser/base/content/test/popupNotifications/browser_popupNotification.js
new file mode 100644
index 0000000000..235aa90b5f
--- /dev/null
+++ b/browser/base/content/test/popupNotifications/browser_popupNotification.js
@@ -0,0 +1,394 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// These are shared between test #4 to #5
+var wrongBrowserNotificationObject = new BasicNotification("wrongBrowser");
+var wrongBrowserNotification;
+
+function test() {
+ waitForExplicitFinish();
+
+ ok(PopupNotifications, "PopupNotifications object exists");
+ ok(PopupNotifications.panel, "PopupNotifications panel exists");
+
+ setup();
+}
+
+var tests = [
+ {
+ id: "Test#1",
+ run() {
+ this.notifyObj = new BasicNotification(this.id);
+ showNotification(this.notifyObj);
+ },
+ onShown(popup) {
+ checkPopup(popup, this.notifyObj);
+ triggerMainCommand(popup);
+ },
+ onHidden(popup) {
+ ok(this.notifyObj.mainActionClicked, "mainAction was clicked");
+ ok(
+ !this.notifyObj.dismissalCallbackTriggered,
+ "dismissal callback wasn't triggered"
+ );
+ ok(this.notifyObj.removedCallbackTriggered, "removed callback triggered");
+ is(
+ this.notifyObj.mainActionSource,
+ "button",
+ "main action should have been triggered by button."
+ );
+ is(
+ this.notifyObj.secondaryActionSource,
+ undefined,
+ "shouldn't have a secondary action source."
+ );
+ },
+ },
+ {
+ id: "Test#2",
+ run() {
+ this.notifyObj = new BasicNotification(this.id);
+ showNotification(this.notifyObj);
+ },
+ onShown(popup) {
+ checkPopup(popup, this.notifyObj);
+ triggerSecondaryCommand(popup, 0);
+ },
+ onHidden(popup) {
+ ok(this.notifyObj.secondaryActionClicked, "secondaryAction was clicked");
+ ok(
+ !this.notifyObj.dismissalCallbackTriggered,
+ "dismissal callback wasn't triggered"
+ );
+ ok(this.notifyObj.removedCallbackTriggered, "removed callback triggered");
+ is(
+ this.notifyObj.mainActionSource,
+ undefined,
+ "shouldn't have a main action source."
+ );
+ is(
+ this.notifyObj.secondaryActionSource,
+ "button",
+ "secondary action should have been triggered by button."
+ );
+ },
+ },
+ {
+ id: "Test#2b",
+ run() {
+ this.notifyObj = new BasicNotification(this.id);
+ this.notifyObj.secondaryActions.push({
+ label: "Extra Secondary Action",
+ accessKey: "E",
+ callback: () => (this.extraSecondaryActionClicked = true),
+ });
+ showNotification(this.notifyObj);
+ },
+ onShown(popup) {
+ checkPopup(popup, this.notifyObj);
+ triggerSecondaryCommand(popup, 1);
+ },
+ onHidden(popup) {
+ ok(
+ this.extraSecondaryActionClicked,
+ "extra secondary action was clicked"
+ );
+ ok(
+ !this.notifyObj.dismissalCallbackTriggered,
+ "dismissal callback wasn't triggered"
+ );
+ ok(this.notifyObj.removedCallbackTriggered, "removed callback triggered");
+ },
+ },
+ {
+ id: "Test#2c",
+ run() {
+ this.notifyObj = new BasicNotification(this.id);
+ this.notifyObj.secondaryActions.push(
+ {
+ label: "Extra Secondary Action",
+ accessKey: "E",
+ callback: () => ok(false, "unexpected callback invocation"),
+ },
+ {
+ label: "Other Extra Secondary Action",
+ accessKey: "O",
+ callback: () => (this.extraSecondaryActionClicked = true),
+ }
+ );
+ showNotification(this.notifyObj);
+ },
+ onShown(popup) {
+ checkPopup(popup, this.notifyObj);
+ triggerSecondaryCommand(popup, 2);
+ },
+ onHidden(popup) {
+ ok(
+ this.extraSecondaryActionClicked,
+ "extra secondary action was clicked"
+ );
+ ok(
+ !this.notifyObj.dismissalCallbackTriggered,
+ "dismissal callback wasn't triggered"
+ );
+ ok(this.notifyObj.removedCallbackTriggered, "removed callback triggered");
+ },
+ },
+ {
+ id: "Test#3",
+ run() {
+ this.notifyObj = new BasicNotification(this.id);
+ this.notification = showNotification(this.notifyObj);
+ },
+ onShown(popup) {
+ checkPopup(popup, this.notifyObj);
+ dismissNotification(popup);
+ },
+ onHidden(popup) {
+ ok(
+ this.notifyObj.dismissalCallbackTriggered,
+ "dismissal callback triggered"
+ );
+ this.notification.remove();
+ ok(this.notifyObj.removedCallbackTriggered, "removed callback triggered");
+ },
+ },
+ // test opening a notification for a background browser
+ // Note: test 4 to 6 share a tab.
+ {
+ id: "Test#4",
+ async run() {
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ let tab = BrowserTestUtils.addTab(gBrowser, "http://example.com/");
+ await BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+ isnot(gBrowser.selectedTab, tab, "new tab isn't selected");
+ wrongBrowserNotificationObject.browser = gBrowser.getBrowserForTab(tab);
+ let promiseTopic = TestUtils.topicObserved(
+ "PopupNotifications-backgroundShow"
+ );
+ wrongBrowserNotification = showNotification(
+ wrongBrowserNotificationObject
+ );
+ await promiseTopic;
+ is(PopupNotifications.isPanelOpen, false, "panel isn't open");
+ ok(
+ !wrongBrowserNotificationObject.mainActionClicked,
+ "main action wasn't clicked"
+ );
+ ok(
+ !wrongBrowserNotificationObject.secondaryActionClicked,
+ "secondary action wasn't clicked"
+ );
+ ok(
+ !wrongBrowserNotificationObject.dismissalCallbackTriggered,
+ "dismissal callback wasn't called"
+ );
+ goNext();
+ },
+ },
+ // now select that browser and test to see that the notification appeared
+ {
+ id: "Test#5",
+ run() {
+ this.oldSelectedTab = gBrowser.selectedTab;
+ gBrowser.selectedTab = gBrowser.tabs[gBrowser.tabs.length - 1];
+ },
+ onShown(popup) {
+ checkPopup(popup, wrongBrowserNotificationObject);
+ is(
+ PopupNotifications.isPanelOpen,
+ true,
+ "isPanelOpen getter doesn't lie"
+ );
+
+ // switch back to the old browser
+ gBrowser.selectedTab = this.oldSelectedTab;
+ },
+ onHidden(popup) {
+ // actually remove the notification to prevent it from reappearing
+ ok(
+ wrongBrowserNotificationObject.dismissalCallbackTriggered,
+ "dismissal callback triggered due to tab switch"
+ );
+ wrongBrowserNotification.remove();
+ ok(
+ wrongBrowserNotificationObject.removedCallbackTriggered,
+ "removed callback triggered"
+ );
+ wrongBrowserNotification = null;
+ },
+ },
+ // test that the removed notification isn't shown on browser re-select
+ {
+ id: "Test#6",
+ async run() {
+ let promiseTopic = TestUtils.topicObserved(
+ "PopupNotifications-updateNotShowing"
+ );
+ gBrowser.selectedTab = gBrowser.tabs[gBrowser.tabs.length - 1];
+ await promiseTopic;
+ is(PopupNotifications.isPanelOpen, false, "panel isn't open");
+ gBrowser.removeTab(gBrowser.selectedTab);
+ goNext();
+ },
+ },
+ // Test that two notifications with the same ID result in a single displayed
+ // notification.
+ {
+ id: "Test#7",
+ run() {
+ this.notifyObj = new BasicNotification(this.id);
+ // Show the same notification twice
+ this.notification1 = showNotification(this.notifyObj);
+ this.notification2 = showNotification(this.notifyObj);
+ },
+ onShown(popup) {
+ checkPopup(popup, this.notifyObj);
+ this.notification2.remove();
+ },
+ onHidden(popup) {
+ ok(
+ !this.notifyObj.dismissalCallbackTriggered,
+ "dismissal callback wasn't triggered"
+ );
+ ok(this.notifyObj.removedCallbackTriggered, "removed callback triggered");
+ },
+ },
+ // Test that two notifications with different IDs are displayed
+ {
+ id: "Test#8",
+ run() {
+ this.testNotif1 = new BasicNotification(this.id);
+ this.testNotif1.message += " 1";
+ showNotification(this.testNotif1);
+ this.testNotif2 = new BasicNotification(this.id);
+ this.testNotif2.message += " 2";
+ this.testNotif2.id += "-2";
+ showNotification(this.testNotif2);
+ },
+ onShown(popup) {
+ is(popup.children.length, 2, "two notifications are shown");
+ // Trigger the main command for the first notification, and the secondary
+ // for the second. Need to do mainCommand first since the secondaryCommand
+ // triggering is async.
+ triggerMainCommand(popup);
+ is(popup.children.length, 1, "only one notification left");
+ triggerSecondaryCommand(popup, 0);
+ },
+ onHidden(popup) {
+ ok(this.testNotif1.mainActionClicked, "main action #1 was clicked");
+ ok(
+ !this.testNotif1.secondaryActionClicked,
+ "secondary action #1 wasn't clicked"
+ );
+ ok(
+ !this.testNotif1.dismissalCallbackTriggered,
+ "dismissal callback #1 wasn't called"
+ );
+
+ ok(!this.testNotif2.mainActionClicked, "main action #2 wasn't clicked");
+ ok(
+ this.testNotif2.secondaryActionClicked,
+ "secondary action #2 was clicked"
+ );
+ ok(
+ !this.testNotif2.dismissalCallbackTriggered,
+ "dismissal callback #2 wasn't called"
+ );
+ },
+ },
+ // Test notification without mainAction or secondaryActions, it should fall back
+ // to a default button that dismisses the notification in place of the main action.
+ {
+ id: "Test#9",
+ run() {
+ this.notifyObj = new BasicNotification(this.id);
+ this.notifyObj.mainAction = null;
+ this.notifyObj.secondaryActions = null;
+ this.notification = showNotification(this.notifyObj);
+ },
+ onShown(popup) {
+ let notification = popup.children[0];
+ ok(
+ notification.hasAttribute("buttonhighlight"),
+ "default action is highlighted"
+ );
+ triggerMainCommand(popup);
+ },
+ onHidden(popup) {
+ ok(!this.notifyObj.mainActionClicked, "mainAction was not clicked");
+ ok(
+ !this.notifyObj.dismissalCallbackTriggered,
+ "dismissal callback wasn't triggered"
+ );
+ ok(this.notifyObj.removedCallbackTriggered, "removed callback triggered");
+ },
+ },
+ // Test notification without mainAction but with secondaryActions, it should fall back
+ // to a default button that dismisses the notification in place of the main action
+ // and ignore the passed secondaryActions.
+ {
+ id: "Test#10",
+ run() {
+ this.notifyObj = new BasicNotification(this.id);
+ this.notifyObj.mainAction = null;
+ this.notification = showNotification(this.notifyObj);
+ },
+ onShown(popup) {
+ let notification = popup.children[0];
+ is(
+ notification.getAttribute("secondarybuttonhidden"),
+ "true",
+ "secondary button is hidden"
+ );
+ ok(
+ notification.hasAttribute("buttonhighlight"),
+ "default action is highlighted"
+ );
+ triggerMainCommand(popup);
+ },
+ onHidden(popup) {
+ ok(!this.notifyObj.mainActionClicked, "mainAction was not clicked");
+ ok(
+ !this.notifyObj.dismissalCallbackTriggered,
+ "dismissal callback wasn't triggered"
+ );
+ ok(this.notifyObj.removedCallbackTriggered, "removed callback triggered");
+ },
+ },
+ // Test two notifications with different anchors
+ {
+ id: "Test#11",
+ run() {
+ this.notifyObj = new BasicNotification(this.id);
+ this.firstNotification = showNotification(this.notifyObj);
+ this.notifyObj2 = new BasicNotification(this.id);
+ this.notifyObj2.id += "-2";
+ this.notifyObj2.anchorID = "addons-notification-icon";
+ // Second showNotification() overrides the first
+ this.secondNotification = showNotification(this.notifyObj2);
+ },
+ onShown(popup) {
+ // This also checks that only one element is shown.
+ checkPopup(popup, this.notifyObj2);
+ is(
+ document.getElementById("geo-notification-icon").getBoundingClientRect()
+ .width,
+ 0,
+ "geo anchor shouldn't be visible"
+ );
+ dismissNotification(popup);
+ },
+ onHidden(popup) {
+ // Remove the notifications
+ this.firstNotification.remove();
+ this.secondNotification.remove();
+ ok(this.notifyObj.removedCallbackTriggered, "removed callback triggered");
+ ok(
+ this.notifyObj2.removedCallbackTriggered,
+ "removed callback triggered"
+ );
+ },
+ },
+];
diff --git a/browser/base/content/test/popupNotifications/browser_popupNotification_2.js b/browser/base/content/test/popupNotifications/browser_popupNotification_2.js
new file mode 100644
index 0000000000..8738a3b605
--- /dev/null
+++ b/browser/base/content/test/popupNotifications/browser_popupNotification_2.js
@@ -0,0 +1,315 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+function test() {
+ waitForExplicitFinish();
+
+ ok(PopupNotifications, "PopupNotifications object exists");
+ ok(PopupNotifications.panel, "PopupNotifications panel exists");
+
+ setup();
+}
+
+var tests = [
+ // Test optional params
+ {
+ id: "Test#1",
+ run() {
+ this.notifyObj = new BasicNotification(this.id);
+ this.notifyObj.secondaryActions = undefined;
+ this.notification = showNotification(this.notifyObj);
+ },
+ onShown(popup) {
+ checkPopup(popup, this.notifyObj);
+ dismissNotification(popup);
+ },
+ onHidden(popup) {
+ ok(
+ this.notifyObj.dismissalCallbackTriggered,
+ "dismissal callback triggered"
+ );
+ this.notification.remove();
+ ok(this.notifyObj.removedCallbackTriggered, "removed callback triggered");
+ },
+ },
+ // Test that icons appear
+ {
+ id: "Test#2",
+ run() {
+ this.notifyObj = new BasicNotification(this.id);
+ this.notifyObj.id = "geolocation";
+ this.notifyObj.anchorID = "geo-notification-icon";
+ this.notification = showNotification(this.notifyObj);
+ },
+ onShown(popup) {
+ checkPopup(popup, this.notifyObj);
+ isnot(
+ document.getElementById("geo-notification-icon").getBoundingClientRect()
+ .width,
+ 0,
+ "geo anchor should be visible"
+ );
+ dismissNotification(popup);
+ },
+ onHidden(popup) {
+ let icon = document.getElementById("geo-notification-icon");
+ isnot(
+ icon.getBoundingClientRect().width,
+ 0,
+ "geo anchor should be visible after dismissal"
+ );
+ this.notification.remove();
+ is(
+ icon.getBoundingClientRect().width,
+ 0,
+ "geo anchor should not be visible after removal"
+ );
+ },
+ },
+
+ // Test that persistence allows the notification to persist across reloads
+ {
+ id: "Test#3",
+ async run() {
+ this.oldSelectedTab = gBrowser.selectedTab;
+ await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ "http://example.com/"
+ );
+ this.notifyObj = new BasicNotification(this.id);
+ this.notifyObj.addOptions({
+ persistence: 2,
+ });
+ this.notification = showNotification(this.notifyObj);
+ },
+ async onShown(popup) {
+ this.complete = false;
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ await promiseTabLoadEvent(gBrowser.selectedTab, "http://example.org/");
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ await promiseTabLoadEvent(gBrowser.selectedTab, "http://example.com/");
+ // Next load will remove the notification
+ this.complete = true;
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ await promiseTabLoadEvent(gBrowser.selectedTab, "http://example.org/");
+ },
+ onHidden(popup) {
+ ok(
+ this.complete,
+ "Should only have hidden the notification after 3 page loads"
+ );
+ ok(this.notifyObj.removedCallbackTriggered, "removal callback triggered");
+ gBrowser.removeTab(gBrowser.selectedTab);
+ gBrowser.selectedTab = this.oldSelectedTab;
+ },
+ },
+ // Test that a timeout allows the notification to persist across reloads
+ {
+ id: "Test#4",
+ async run() {
+ this.oldSelectedTab = gBrowser.selectedTab;
+ await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ "http://example.com/"
+ );
+ this.notifyObj = new BasicNotification(this.id);
+ // Set a timeout of 10 minutes that should never be hit
+ this.notifyObj.addOptions({
+ timeout: Date.now() + 600000,
+ });
+ this.notification = showNotification(this.notifyObj);
+ },
+ async onShown(popup) {
+ this.complete = false;
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ await promiseTabLoadEvent(gBrowser.selectedTab, "http://example.org/");
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ await promiseTabLoadEvent(gBrowser.selectedTab, "http://example.com/");
+ // Next load will hide the notification
+ this.notification.options.timeout = Date.now() - 1;
+ this.complete = true;
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ await promiseTabLoadEvent(gBrowser.selectedTab, "http://example.org/");
+ },
+ onHidden(popup) {
+ ok(
+ this.complete,
+ "Should only have hidden the notification after the timeout was passed"
+ );
+ this.notification.remove();
+ gBrowser.removeTab(gBrowser.selectedTab);
+ gBrowser.selectedTab = this.oldSelectedTab;
+ },
+ },
+ // Test that setting persistWhileVisible allows a visible notification to
+ // persist across location changes
+ {
+ id: "Test#5",
+ async run() {
+ this.oldSelectedTab = gBrowser.selectedTab;
+ await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ "http://example.com/"
+ );
+ this.notifyObj = new BasicNotification(this.id);
+ this.notifyObj.addOptions({
+ persistWhileVisible: true,
+ });
+ this.notification = showNotification(this.notifyObj);
+ },
+ async onShown(popup) {
+ this.complete = false;
+
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ await promiseTabLoadEvent(gBrowser.selectedTab, "http://example.org/");
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ await promiseTabLoadEvent(gBrowser.selectedTab, "http://example.com/");
+ // Notification should persist across location changes
+ this.complete = true;
+ dismissNotification(popup);
+ },
+ onHidden(popup) {
+ ok(
+ this.complete,
+ "Should only have hidden the notification after it was dismissed"
+ );
+ this.notification.remove();
+ gBrowser.removeTab(gBrowser.selectedTab);
+ gBrowser.selectedTab = this.oldSelectedTab;
+ },
+ },
+
+ // Test that nested icon nodes correctly activate popups
+ {
+ id: "Test#6",
+ run() {
+ // Add a temporary box as the anchor with a button
+ this.box = document.createXULElement("box");
+ PopupNotifications.iconBox.appendChild(this.box);
+
+ let button = document.createXULElement("button");
+ button.setAttribute("label", "Please click me!");
+ this.box.appendChild(button);
+
+ // The notification should open up on the box
+ this.notifyObj = new BasicNotification(this.id);
+ this.notifyObj.anchorID = this.box.id = "nested-box";
+ this.notifyObj.addOptions({ dismissed: true });
+ this.notification = showNotification(this.notifyObj);
+
+ // This test places a normal button in the notification area, which has
+ // standard GTK styling and dimensions. Due to the clip-path, this button
+ // gets clipped off, which makes it necessary to synthesize the mouse click
+ // a little bit downward. To be safe, I adjusted the x-offset with the same
+ // amount.
+ EventUtils.synthesizeMouse(button, 4, 4, {});
+ },
+ onShown(popup) {
+ checkPopup(popup, this.notifyObj);
+ dismissNotification(popup);
+ },
+ onHidden(popup) {
+ this.notification.remove();
+ this.box.remove();
+ },
+ },
+ // Test that popupnotifications without popups have anchor icons shown
+ {
+ id: "Test#7",
+ async run() {
+ let notifyObj = new BasicNotification(this.id);
+ notifyObj.anchorID = "geo-notification-icon";
+ notifyObj.addOptions({ neverShow: true });
+ let promiseTopic = TestUtils.topicObserved(
+ "PopupNotifications-updateNotShowing"
+ );
+ showNotification(notifyObj);
+ await promiseTopic;
+ isnot(
+ document.getElementById("geo-notification-icon").getBoundingClientRect()
+ .width,
+ 0,
+ "geo anchor should be visible"
+ );
+ goNext();
+ },
+ },
+ // Test that autoplay media icon is shown
+ {
+ id: "Test#8",
+ async run() {
+ let notifyObj = new BasicNotification(this.id);
+ notifyObj.anchorID = "autoplay-media-notification-icon";
+ notifyObj.addOptions({ neverShow: true });
+ let promiseTopic = TestUtils.topicObserved(
+ "PopupNotifications-updateNotShowing"
+ );
+ showNotification(notifyObj);
+ await promiseTopic;
+ isnot(
+ document
+ .getElementById("autoplay-media-notification-icon")
+ .getBoundingClientRect().width,
+ 0,
+ "autoplay media icon should be visible"
+ );
+ goNext();
+ },
+ },
+ // Test notification close button
+ {
+ id: "Test#9",
+ run() {
+ this.notifyObj = new BasicNotification(this.id);
+ this.notification = showNotification(this.notifyObj);
+ },
+ onShown(popup) {
+ checkPopup(popup, this.notifyObj);
+ let notification = popup.children[0];
+ EventUtils.synthesizeMouseAtCenter(notification.closebutton, {});
+ },
+ onHidden(popup) {
+ ok(
+ this.notifyObj.dismissalCallbackTriggered,
+ "dismissal callback triggered"
+ );
+ this.notification.remove();
+ ok(this.notifyObj.removedCallbackTriggered, "removed callback triggered");
+ ok(
+ !this.notifyObj.secondaryActionClicked,
+ "secondary action not clicked"
+ );
+ },
+ },
+ // Test notification when chrome is hidden
+ {
+ id: "Test#11",
+ run() {
+ window.locationbar.visible = false;
+ this.notifyObj = new BasicNotification(this.id);
+ this.notification = showNotification(this.notifyObj);
+ },
+ onShown(popup) {
+ checkPopup(popup, this.notifyObj);
+ is(
+ popup.anchorNode.className,
+ "tabbrowser-tab",
+ "notification anchored to tab"
+ );
+ dismissNotification(popup);
+ },
+ onHidden(popup) {
+ ok(
+ this.notifyObj.dismissalCallbackTriggered,
+ "dismissal callback triggered"
+ );
+ this.notification.remove();
+ ok(this.notifyObj.removedCallbackTriggered, "removed callback triggered");
+ window.locationbar.visible = true;
+ },
+ },
+];
diff --git a/browser/base/content/test/popupNotifications/browser_popupNotification_3.js b/browser/base/content/test/popupNotifications/browser_popupNotification_3.js
new file mode 100644
index 0000000000..e0954e39ca
--- /dev/null
+++ b/browser/base/content/test/popupNotifications/browser_popupNotification_3.js
@@ -0,0 +1,377 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+/* eslint-disable mozilla/no-arbitrary-setTimeout */
+
+function test() {
+ waitForExplicitFinish();
+
+ ok(PopupNotifications, "PopupNotifications object exists");
+ ok(PopupNotifications.panel, "PopupNotifications panel exists");
+
+ setup();
+}
+
+var tests = [
+ // Test notification is removed when dismissed if removeOnDismissal is true
+ {
+ id: "Test#1",
+ run() {
+ this.notifyObj = new BasicNotification(this.id);
+ this.notifyObj.addOptions({
+ removeOnDismissal: true,
+ });
+ this.notification = showNotification(this.notifyObj);
+ },
+ onShown(popup) {
+ checkPopup(popup, this.notifyObj);
+ dismissNotification(popup);
+ },
+ onHidden(popup) {
+ ok(
+ !this.notifyObj.dismissalCallbackTriggered,
+ "dismissal callback wasn't triggered"
+ );
+ ok(this.notifyObj.removedCallbackTriggered, "removed callback triggered");
+ },
+ },
+ // Test multiple notification icons are shown
+ {
+ id: "Test#2",
+ run() {
+ this.notifyObj1 = new BasicNotification(this.id);
+ this.notifyObj1.id += "_1";
+ this.notifyObj1.anchorID = "default-notification-icon";
+ this.notification1 = showNotification(this.notifyObj1);
+
+ this.notifyObj2 = new BasicNotification(this.id);
+ this.notifyObj2.id += "_2";
+ this.notifyObj2.anchorID = "geo-notification-icon";
+ this.notification2 = showNotification(this.notifyObj2);
+ },
+ onShown(popup) {
+ checkPopup(popup, this.notifyObj2);
+
+ // check notifyObj1 anchor icon is showing
+ isnot(
+ document
+ .getElementById("default-notification-icon")
+ .getBoundingClientRect().width,
+ 0,
+ "default anchor should be visible"
+ );
+ // check notifyObj2 anchor icon is showing
+ isnot(
+ document.getElementById("geo-notification-icon").getBoundingClientRect()
+ .width,
+ 0,
+ "geo anchor should be visible"
+ );
+
+ dismissNotification(popup);
+ },
+ onHidden(popup) {
+ this.notification1.remove();
+ ok(
+ this.notifyObj1.removedCallbackTriggered,
+ "removed callback triggered"
+ );
+
+ this.notification2.remove();
+ ok(
+ this.notifyObj2.removedCallbackTriggered,
+ "removed callback triggered"
+ );
+ },
+ },
+ // Test that multiple notification icons are removed when switching tabs
+ {
+ id: "Test#3",
+ async run() {
+ // show the notification on old tab.
+ this.notifyObjOld = new BasicNotification(this.id);
+ this.notifyObjOld.anchorID = "default-notification-icon";
+ this.notificationOld = showNotification(this.notifyObjOld);
+
+ // switch tab
+ this.oldSelectedTab = gBrowser.selectedTab;
+ await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ "http://example.com/"
+ );
+
+ // show the notification on new tab.
+ this.notifyObjNew = new BasicNotification(this.id);
+ this.notifyObjNew.anchorID = "geo-notification-icon";
+ this.notificationNew = showNotification(this.notifyObjNew);
+ },
+ onShown(popup) {
+ checkPopup(popup, this.notifyObjNew);
+
+ // check notifyObjOld anchor icon is removed
+ is(
+ document
+ .getElementById("default-notification-icon")
+ .getBoundingClientRect().width,
+ 0,
+ "default anchor shouldn't be visible"
+ );
+ // check notifyObjNew anchor icon is showing
+ isnot(
+ document.getElementById("geo-notification-icon").getBoundingClientRect()
+ .width,
+ 0,
+ "geo anchor should be visible"
+ );
+
+ dismissNotification(popup);
+ },
+ onHidden(popup) {
+ this.notificationNew.remove();
+ gBrowser.removeTab(gBrowser.selectedTab);
+
+ gBrowser.selectedTab = this.oldSelectedTab;
+ this.notificationOld.remove();
+ },
+ },
+ // test security delay - too early
+ {
+ id: "Test#4",
+ async run() {
+ // Set the security delay to 100s
+ await SpecialPowers.pushPrefEnv({
+ set: [["security.notification_enable_delay", 100000]],
+ });
+
+ this.notifyObj = new BasicNotification(this.id);
+ showNotification(this.notifyObj);
+ },
+ onShown(popup) {
+ checkPopup(popup, this.notifyObj);
+ triggerMainCommand(popup);
+
+ // Wait to see if the main command worked
+ executeSoon(function delayedDismissal() {
+ dismissNotification(popup);
+ });
+ },
+ onHidden(popup) {
+ ok(
+ !this.notifyObj.mainActionClicked,
+ "mainAction was not clicked because it was too soon"
+ );
+ ok(
+ this.notifyObj.dismissalCallbackTriggered,
+ "dismissal callback was triggered"
+ );
+ },
+ },
+ // test security delay - after delay
+ {
+ id: "Test#5",
+ async run() {
+ // Set the security delay to 10ms
+
+ await SpecialPowers.pushPrefEnv({
+ set: [["security.notification_enable_delay", 10]],
+ });
+
+ this.notifyObj = new BasicNotification(this.id);
+ showNotification(this.notifyObj);
+ },
+ onShown(popup) {
+ checkPopup(popup, this.notifyObj);
+
+ // Wait until after the delay to trigger the main action
+ setTimeout(function delayedDismissal() {
+ triggerMainCommand(popup);
+ }, 500);
+ },
+ onHidden(popup) {
+ ok(
+ this.notifyObj.mainActionClicked,
+ "mainAction was clicked after the delay"
+ );
+ ok(
+ !this.notifyObj.dismissalCallbackTriggered,
+ "dismissal callback was not triggered"
+ );
+ },
+ },
+ // reload removes notification
+ {
+ id: "Test#6",
+ async run() {
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ await promiseTabLoadEvent(gBrowser.selectedTab, "http://example.com/");
+ let notifyObj = new BasicNotification(this.id);
+ notifyObj.options.eventCallback = function (eventName) {
+ if (eventName == "removed") {
+ ok(true, "Notification removed in background tab after reloading");
+ goNext();
+ }
+ };
+ showNotification(notifyObj);
+ executeSoon(function () {
+ gBrowser.selectedBrowser.reload();
+ });
+ },
+ },
+ // location change in background tab removes notification
+ {
+ id: "Test#7",
+ async run() {
+ let oldSelectedTab = gBrowser.selectedTab;
+ let newTab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ "http://example.com/"
+ );
+ gBrowser.selectedTab = oldSelectedTab;
+ let browser = gBrowser.getBrowserForTab(newTab);
+
+ let notifyObj = new BasicNotification(this.id);
+ notifyObj.browser = browser;
+ notifyObj.options.eventCallback = function (eventName) {
+ if (eventName == "removed") {
+ ok(true, "Notification removed in background tab after reloading");
+ executeSoon(function () {
+ gBrowser.removeTab(newTab);
+ goNext();
+ });
+ }
+ };
+ showNotification(notifyObj);
+ executeSoon(function () {
+ browser.reload();
+ });
+ },
+ },
+ // Popup notification anchor shouldn't disappear when a notification with the same ID is re-added in a background tab
+ {
+ id: "Test#8",
+ async run() {
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ await promiseTabLoadEvent(gBrowser.selectedTab, "http://example.com/");
+ let originalTab = gBrowser.selectedTab;
+ let bgTab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ "http://example.com/"
+ );
+ let anchor = document.createXULElement("box");
+ anchor.id = "test26-anchor";
+ anchor.className = "notification-anchor-icon";
+ PopupNotifications.iconBox.appendChild(anchor);
+
+ gBrowser.selectedTab = originalTab;
+
+ let fgNotifyObj = new BasicNotification(this.id);
+ fgNotifyObj.anchorID = anchor.id;
+ fgNotifyObj.options.dismissed = true;
+ let fgNotification = showNotification(fgNotifyObj);
+
+ let bgNotifyObj = new BasicNotification(this.id);
+ bgNotifyObj.anchorID = anchor.id;
+ bgNotifyObj.browser = gBrowser.getBrowserForTab(bgTab);
+ // show the notification in the background tab ...
+ let bgNotification = showNotification(bgNotifyObj);
+ // ... and re-show it
+ bgNotification = showNotification(bgNotifyObj);
+
+ ok(fgNotification.id, "notification has id");
+ is(fgNotification.id, bgNotification.id, "notification ids are the same");
+ is(anchor.getAttribute("showing"), "true", "anchor still showing");
+
+ fgNotification.remove();
+ gBrowser.removeTab(bgTab);
+ goNext();
+ },
+ },
+ // location change in an embedded frame should not remove a notification
+ {
+ id: "Test#9",
+ async run() {
+ await promiseTabLoadEvent(
+ gBrowser.selectedTab,
+ "data:text/html;charset=utf8,<iframe%20id='iframe'%20src='http://example.com/'>"
+ );
+ this.notifyObj = new BasicNotification(this.id);
+ this.notifyObj.options.eventCallback = function (eventName) {
+ if (eventName == "removed") {
+ ok(
+ false,
+ "Notification removed from browser when subframe navigated"
+ );
+ }
+ };
+ showNotification(this.notifyObj);
+ },
+ async onShown(popup) {
+ info("Adding observer and performing navigation");
+
+ await Promise.all([
+ BrowserUtils.promiseObserved("window-global-created", wgp =>
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ wgp.documentURI.spec.startsWith("http://example.org/")
+ ),
+ SpecialPowers.spawn(gBrowser.selectedBrowser, [], function () {
+ content.document
+ .getElementById("iframe")
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ .setAttribute("src", "http://example.org/");
+ }),
+ ]);
+
+ executeSoon(() => {
+ let notification = PopupNotifications.getNotification(
+ this.notifyObj.id,
+ this.notifyObj.browser
+ );
+ ok(
+ notification != null,
+ "Notification remained when subframe navigated"
+ );
+ this.notifyObj.options.eventCallback = undefined;
+
+ notification.remove();
+ });
+ },
+ onHidden() {},
+ },
+ // Popup Notifications should catch exceptions from callbacks
+ {
+ id: "Test#10",
+ run() {
+ this.testNotif1 = new BasicNotification(this.id);
+ this.testNotif1.message += " 1";
+ this.notification1 = showNotification(this.testNotif1);
+ this.testNotif1.options.eventCallback = function (eventName) {
+ info("notifyObj1.options.eventCallback: " + eventName);
+ if (eventName == "dismissed") {
+ throw new Error("Oops 1!");
+ }
+ };
+
+ this.testNotif2 = new BasicNotification(this.id);
+ this.testNotif2.message += " 2";
+ this.testNotif2.id += "-2";
+ this.testNotif2.options.eventCallback = function (eventName) {
+ info("notifyObj2.options.eventCallback: " + eventName);
+ if (eventName == "dismissed") {
+ throw new Error("Oops 2!");
+ }
+ };
+ this.notification2 = showNotification(this.testNotif2);
+ },
+ onShown(popup) {
+ is(popup.children.length, 2, "two notifications are shown");
+ dismissNotification(popup);
+ },
+ onHidden() {
+ this.notification1.remove();
+ this.notification2.remove();
+ },
+ },
+];
diff --git a/browser/base/content/test/popupNotifications/browser_popupNotification_4.js b/browser/base/content/test/popupNotifications/browser_popupNotification_4.js
new file mode 100644
index 0000000000..b0e8f016ef
--- /dev/null
+++ b/browser/base/content/test/popupNotifications/browser_popupNotification_4.js
@@ -0,0 +1,290 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+function test() {
+ waitForExplicitFinish();
+
+ ok(PopupNotifications, "PopupNotifications object exists");
+ ok(PopupNotifications.panel, "PopupNotifications panel exists");
+
+ setup();
+}
+
+var tests = [
+ // Popup Notifications main actions should catch exceptions from callbacks
+ {
+ id: "Test#1",
+ run() {
+ this.testNotif = new ErrorNotification(this.id);
+ showNotification(this.testNotif);
+ },
+ onShown(popup) {
+ checkPopup(popup, this.testNotif);
+ triggerMainCommand(popup);
+ },
+ onHidden(popup) {
+ ok(this.testNotif.mainActionClicked, "main action has been triggered");
+ },
+ },
+ // Popup Notifications secondary actions should catch exceptions from callbacks
+ {
+ id: "Test#2",
+ run() {
+ this.testNotif = new ErrorNotification(this.id);
+ showNotification(this.testNotif);
+ },
+ onShown(popup) {
+ checkPopup(popup, this.testNotif);
+ triggerSecondaryCommand(popup, 0);
+ },
+ onHidden(popup) {
+ ok(
+ this.testNotif.secondaryActionClicked,
+ "secondary action has been triggered"
+ );
+ },
+ },
+ // Existing popup notification shouldn't disappear when adding a dismissed notification
+ {
+ id: "Test#3",
+ run() {
+ this.notifyObj1 = new BasicNotification(this.id);
+ this.notifyObj1.id += "_1";
+ this.notifyObj1.anchorID = "default-notification-icon";
+ this.notification1 = showNotification(this.notifyObj1);
+ },
+ onShown(popup) {
+ // Now show a dismissed notification, and check that it doesn't clobber
+ // the showing one.
+ this.notifyObj2 = new BasicNotification(this.id);
+ this.notifyObj2.id += "_2";
+ this.notifyObj2.anchorID = "geo-notification-icon";
+ this.notifyObj2.options.dismissed = true;
+ this.notification2 = showNotification(this.notifyObj2);
+
+ checkPopup(popup, this.notifyObj1);
+
+ // check that both anchor icons are showing
+ is(
+ document
+ .getElementById("default-notification-icon")
+ .getAttribute("showing"),
+ "true",
+ "notification1 anchor should be visible"
+ );
+ is(
+ document
+ .getElementById("geo-notification-icon")
+ .getAttribute("showing"),
+ "true",
+ "notification2 anchor should be visible"
+ );
+
+ dismissNotification(popup);
+ },
+ onHidden(popup) {
+ this.notification1.remove();
+ this.notification2.remove();
+ },
+ },
+ // Showing should be able to modify the popup data
+ {
+ id: "Test#4",
+ run() {
+ this.notifyObj = new BasicNotification(this.id);
+ let normalCallback = this.notifyObj.options.eventCallback;
+ this.notifyObj.options.eventCallback = function (eventName) {
+ if (eventName == "showing") {
+ this.mainAction.label = "Alternate Label";
+ }
+ normalCallback.call(this, eventName);
+ };
+ showNotification(this.notifyObj);
+ },
+ onShown(popup) {
+ // checkPopup checks for the matching label. Note that this assumes that
+ // this.notifyObj.mainAction is the same as notification.mainAction,
+ // which could be a problem if we ever decided to deep-copy.
+ checkPopup(popup, this.notifyObj);
+ triggerMainCommand(popup);
+ },
+ onHidden() {},
+ },
+ // Moving a tab to a new window should remove non-swappable notifications.
+ {
+ id: "Test#5",
+ async run() {
+ await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ "http://example.com/"
+ );
+
+ let notifyObj = new BasicNotification(this.id);
+
+ let shown = waitForNotificationPanel();
+ showNotification(notifyObj);
+ await shown;
+
+ let promiseWin = BrowserTestUtils.waitForNewWindow();
+ gBrowser.replaceTabWithWindow(gBrowser.selectedTab);
+ let win = await promiseWin;
+
+ let anchor = win.document.getElementById("default-notification-icon");
+ win.PopupNotifications._reshowNotifications(anchor);
+ ok(
+ !win.PopupNotifications.panel.children.length,
+ "no notification displayed in new window"
+ );
+ ok(
+ notifyObj.swappingCallbackTriggered,
+ "the swapping callback was triggered"
+ );
+ ok(
+ notifyObj.removedCallbackTriggered,
+ "the removed callback was triggered"
+ );
+
+ await BrowserTestUtils.closeWindow(win);
+ await waitForWindowReadyForPopupNotifications(window);
+
+ goNext();
+ },
+ },
+ // Moving a tab to a new window should preserve swappable notifications.
+ {
+ id: "Test#6",
+ async run() {
+ await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ "http://example.com/"
+ );
+ let notifyObj = new BasicNotification(this.id);
+ let originalCallback = notifyObj.options.eventCallback;
+ notifyObj.options.eventCallback = function (eventName) {
+ originalCallback(eventName);
+ return eventName == "swapping";
+ };
+
+ let shown = waitForNotificationPanel();
+ let notification = showNotification(notifyObj);
+ await shown;
+
+ let promiseWin = BrowserTestUtils.waitForNewWindow();
+ gBrowser.replaceTabWithWindow(gBrowser.selectedTab);
+ let win = await promiseWin;
+ await waitForWindowReadyForPopupNotifications(win);
+
+ await new Promise(resolve => {
+ let callback = notification.options.eventCallback;
+ notification.options.eventCallback = function (eventName) {
+ callback(eventName);
+ if (eventName == "shown") {
+ resolve();
+ }
+ };
+ info("Showing the notification again");
+ notification.reshow();
+ });
+
+ checkPopup(win.PopupNotifications.panel, notifyObj);
+ ok(
+ notifyObj.swappingCallbackTriggered,
+ "the swapping callback was triggered"
+ );
+
+ await BrowserTestUtils.closeWindow(win);
+ await waitForWindowReadyForPopupNotifications(window);
+
+ goNext();
+ },
+ },
+ // the main action callback can keep the notification.
+ {
+ id: "Test#8",
+ run() {
+ this.notifyObj = new BasicNotification(this.id);
+ this.notifyObj.mainAction.dismiss = true;
+ this.notification = showNotification(this.notifyObj);
+ },
+ onShown(popup) {
+ checkPopup(popup, this.notifyObj);
+ triggerMainCommand(popup);
+ },
+ onHidden(popup) {
+ ok(
+ this.notifyObj.dismissalCallbackTriggered,
+ "dismissal callback was triggered"
+ );
+ ok(
+ !this.notifyObj.removedCallbackTriggered,
+ "removed callback wasn't triggered"
+ );
+ this.notification.remove();
+ },
+ },
+ // a secondary action callback can keep the notification.
+ {
+ id: "Test#9",
+ run() {
+ this.notifyObj = new BasicNotification(this.id);
+ this.notifyObj.secondaryActions[0].dismiss = true;
+ this.notification = showNotification(this.notifyObj);
+ },
+ onShown(popup) {
+ checkPopup(popup, this.notifyObj);
+ triggerSecondaryCommand(popup, 0);
+ },
+ onHidden(popup) {
+ ok(
+ this.notifyObj.dismissalCallbackTriggered,
+ "dismissal callback was triggered"
+ );
+ ok(
+ !this.notifyObj.removedCallbackTriggered,
+ "removed callback wasn't triggered"
+ );
+ this.notification.remove();
+ },
+ },
+ // returning true in the showing callback should dismiss the notification.
+ {
+ id: "Test#10",
+ run() {
+ let notifyObj = new BasicNotification(this.id);
+ let originalCallback = notifyObj.options.eventCallback;
+ notifyObj.options.eventCallback = function (eventName) {
+ originalCallback(eventName);
+ return eventName == "showing";
+ };
+
+ let notification = showNotification(notifyObj);
+ ok(
+ notifyObj.showingCallbackTriggered,
+ "the showing callback was triggered"
+ );
+ ok(
+ !notifyObj.shownCallbackTriggered,
+ "the shown callback wasn't triggered"
+ );
+ notification.remove();
+ goNext();
+ },
+ },
+ // the main action button should apply non-default(no highlight) style.
+ {
+ id: "Test#11",
+ run() {
+ this.notifyObj = new BasicNotification(this.id);
+ this.notifyObj.secondaryActions = undefined;
+ this.notification = showNotification(this.notifyObj);
+ },
+ onShown(popup) {
+ checkPopup(popup, this.notifyObj);
+ dismissNotification(popup);
+ },
+ onHidden() {},
+ },
+];
diff --git a/browser/base/content/test/popupNotifications/browser_popupNotification_5.js b/browser/base/content/test/popupNotifications/browser_popupNotification_5.js
new file mode 100644
index 0000000000..839262caa0
--- /dev/null
+++ b/browser/base/content/test/popupNotifications/browser_popupNotification_5.js
@@ -0,0 +1,501 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+function test() {
+ waitForExplicitFinish();
+
+ ok(PopupNotifications, "PopupNotifications object exists");
+ ok(PopupNotifications.panel, "PopupNotifications panel exists");
+
+ setup();
+}
+
+var gNotification;
+
+var tests = [
+ // panel updates should fire the showing and shown callbacks again.
+ {
+ id: "Test#1",
+ run() {
+ this.notifyObj = new BasicNotification(this.id);
+ this.notification = showNotification(this.notifyObj);
+ },
+ onShown(popup) {
+ checkPopup(popup, this.notifyObj);
+
+ this.notifyObj.showingCallbackTriggered = false;
+ this.notifyObj.shownCallbackTriggered = false;
+
+ // Force an update of the panel. This is typically called
+ // automatically when receiving 'activate' or 'TabSelect' events,
+ // but from a setTimeout, which is inconvenient for the test.
+ PopupNotifications._update();
+
+ checkPopup(popup, this.notifyObj);
+
+ this.notification.remove();
+ },
+ onHidden() {},
+ },
+ // A first dismissed notification shouldn't stop _update from showing a second notification
+ {
+ id: "Test#2",
+ run() {
+ this.notifyObj1 = new BasicNotification(this.id);
+ this.notifyObj1.id += "_1";
+ this.notifyObj1.anchorID = "default-notification-icon";
+ this.notifyObj1.options.dismissed = true;
+ this.notification1 = showNotification(this.notifyObj1);
+
+ this.notifyObj2 = new BasicNotification(this.id);
+ this.notifyObj2.id += "_2";
+ this.notifyObj2.anchorID = "geo-notification-icon";
+ this.notifyObj2.options.dismissed = true;
+ this.notification2 = showNotification(this.notifyObj2);
+
+ this.notification2.dismissed = false;
+ PopupNotifications._update();
+ },
+ onShown(popup) {
+ checkPopup(popup, this.notifyObj2);
+ this.notification1.remove();
+ this.notification2.remove();
+ },
+ onHidden(popup) {},
+ },
+ // The anchor icon should be shown for notifications in background windows.
+ {
+ id: "Test#3",
+ async run() {
+ let notifyObj = new BasicNotification(this.id);
+ notifyObj.options.dismissed = true;
+
+ let win = await BrowserTestUtils.openNewBrowserWindow();
+
+ // Open the notification in the original window, now in the background.
+ let notification = showNotification(notifyObj);
+ let anchor = document.getElementById("default-notification-icon");
+ is(anchor.getAttribute("showing"), "true", "the anchor is shown");
+ notification.remove();
+
+ await BrowserTestUtils.closeWindow(win);
+ await waitForWindowReadyForPopupNotifications(window);
+
+ goNext();
+ },
+ },
+ // Test that persistent doesn't allow the notification to persist after
+ // navigation.
+ {
+ id: "Test#4",
+ async run() {
+ this.oldSelectedTab = gBrowser.selectedTab;
+ await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ "http://example.com/"
+ );
+ this.notifyObj = new BasicNotification(this.id);
+ this.notifyObj.addOptions({
+ persistent: true,
+ });
+ this.notification = showNotification(this.notifyObj);
+ },
+ async onShown(popup) {
+ this.complete = false;
+
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ await promiseTabLoadEvent(gBrowser.selectedTab, "http://example.org/");
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ await promiseTabLoadEvent(gBrowser.selectedTab, "http://example.com/");
+
+ // This code should not be executed.
+ ok(false, "Should have removed the notification after navigation");
+ // Properly dismiss and cleanup in case the unthinkable happens.
+ this.complete = true;
+ triggerSecondaryCommand(popup, 0);
+ },
+ onHidden(popup) {
+ ok(
+ !this.complete,
+ "Should have hidden the notification after navigation"
+ );
+ this.notification.remove();
+ gBrowser.removeTab(gBrowser.selectedTab);
+ gBrowser.selectedTab = this.oldSelectedTab;
+ },
+ },
+ // Test that persistent allows the notification to persist until explicitly
+ // dismissed.
+ {
+ id: "Test#5",
+ async run() {
+ this.oldSelectedTab = gBrowser.selectedTab;
+ await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ "http://example.com/"
+ );
+ this.notifyObj = new BasicNotification(this.id);
+ this.notifyObj.addOptions({
+ persistent: true,
+ });
+ this.notification = showNotification(this.notifyObj);
+ },
+ async onShown(popup) {
+ this.complete = false;
+
+ // Notification should persist after attempt to dismiss by clicking on the
+ // content area.
+ let browser = gBrowser.selectedBrowser;
+ await BrowserTestUtils.synthesizeMouseAtCenter("body", {}, browser);
+
+ // Notification should be hidden after dismissal via Don't Allow.
+ this.complete = true;
+ triggerSecondaryCommand(popup, 0);
+ },
+ onHidden(popup) {
+ ok(
+ this.complete,
+ "Should have hidden the notification after clicking Not Now"
+ );
+ this.notification.remove();
+ gBrowser.removeTab(gBrowser.selectedTab);
+ gBrowser.selectedTab = this.oldSelectedTab;
+ },
+ },
+ // Test that persistent panels are still open after switching to another tab
+ // and back.
+ {
+ id: "Test#6a",
+ run() {
+ this.notifyObj = new BasicNotification(this.id);
+ this.notifyObj.options.persistent = true;
+ gNotification = showNotification(this.notifyObj);
+ },
+ async onShown(popup) {
+ this.oldSelectedTab = gBrowser.selectedTab;
+ await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ "http://example.com/"
+ );
+ },
+ onHidden(popup) {
+ ok(true, "Should have hidden the notification after tab switch");
+ gBrowser.removeTab(gBrowser.selectedTab);
+ gBrowser.selectedTab = this.oldSelectedTab;
+ },
+ },
+ // Second part of the previous test that compensates for the limitation in
+ // runNextTest that expects a single onShown/onHidden invocation per test.
+ {
+ id: "Test#6b",
+ run() {
+ let id =
+ PopupNotifications.panel.firstElementChild.getAttribute("popupid");
+ ok(
+ id.endsWith("Test#6a"),
+ "Should have found the notification from Test6a"
+ );
+ ok(
+ PopupNotifications.isPanelOpen,
+ "Should have shown the popup again after getting back to the tab"
+ );
+ gNotification.remove();
+ gNotification = null;
+ goNext();
+ },
+ },
+ // Test that persistent panels are still open after switching to another
+ // window and back.
+ {
+ id: "Test#7",
+ async run() {
+ this.oldSelectedTab = gBrowser.selectedTab;
+ await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ "http://example.com/"
+ );
+ let firstTab = gBrowser.selectedTab;
+
+ await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ "http://example.com/"
+ );
+
+ let shown = waitForNotificationPanel();
+ let notifyObj = new BasicNotification(this.id);
+ notifyObj.options.persistent = true;
+ this.notification = showNotification(notifyObj);
+ await shown;
+
+ ok(
+ notifyObj.shownCallbackTriggered,
+ "Should have triggered the shown event"
+ );
+ ok(
+ notifyObj.showingCallbackTriggered,
+ "Should have triggered the showing event"
+ );
+ // Reset to false so that we can ensure these are not fired a second time.
+ notifyObj.shownCallbackTriggered = false;
+ notifyObj.showingCallbackTriggered = false;
+ let timeShown = this.notification.timeShown;
+
+ let promiseWin = BrowserTestUtils.waitForNewWindow();
+ gBrowser.replaceTabWithWindow(firstTab);
+ let win = await promiseWin;
+
+ let anchor = win.document.getElementById("default-notification-icon");
+ win.PopupNotifications._reshowNotifications(anchor);
+ ok(
+ !win.PopupNotifications.panel.children.length,
+ "no notification displayed in new window"
+ );
+
+ await BrowserTestUtils.closeWindow(win);
+ await waitForWindowReadyForPopupNotifications(window);
+
+ let id =
+ PopupNotifications.panel.firstElementChild.getAttribute("popupid");
+ ok(
+ id.endsWith("Test#7"),
+ "Should have found the notification from Test7"
+ );
+ ok(
+ PopupNotifications.isPanelOpen,
+ "Should have kept the popup on the first window"
+ );
+ ok(
+ !notifyObj.dismissalCallbackTriggered,
+ "Should not have triggered a dismissed event"
+ );
+ ok(
+ !notifyObj.shownCallbackTriggered,
+ "Should not have triggered a second shown event"
+ );
+ ok(
+ !notifyObj.showingCallbackTriggered,
+ "Should not have triggered a second showing event"
+ );
+ ok(
+ this.notification.timeShown > timeShown,
+ "should have updated timeShown to restart the security delay"
+ );
+
+ this.notification.remove();
+ gBrowser.removeTab(gBrowser.selectedTab);
+ gBrowser.selectedTab = this.oldSelectedTab;
+
+ goNext();
+ },
+ },
+ // Test that only the first persistent notification is shown on update
+ {
+ id: "Test#8",
+ run() {
+ this.notifyObj1 = new BasicNotification(this.id);
+ this.notifyObj1.id += "_1";
+ this.notifyObj1.anchorID = "default-notification-icon";
+ this.notifyObj1.options.persistent = true;
+ this.notification1 = showNotification(this.notifyObj1);
+
+ this.notifyObj2 = new BasicNotification(this.id);
+ this.notifyObj2.id += "_2";
+ this.notifyObj2.anchorID = "geo-notification-icon";
+ this.notifyObj2.options.persistent = true;
+ this.notification2 = showNotification(this.notifyObj2);
+
+ PopupNotifications._update();
+ },
+ onShown(popup) {
+ checkPopup(popup, this.notifyObj1);
+ this.notification1.remove();
+ this.notification2.remove();
+ },
+ onHidden(popup) {},
+ },
+ // Test that persistent notifications are shown stacked by anchor on update
+ {
+ id: "Test#9",
+ run() {
+ this.notifyObj1 = new BasicNotification(this.id);
+ this.notifyObj1.id += "_1";
+ this.notifyObj1.anchorID = "default-notification-icon";
+ this.notifyObj1.options.persistent = true;
+ this.notification1 = showNotification(this.notifyObj1);
+
+ this.notifyObj2 = new BasicNotification(this.id);
+ this.notifyObj2.id += "_2";
+ this.notifyObj2.anchorID = "geo-notification-icon";
+ this.notifyObj2.options.persistent = true;
+ this.notification2 = showNotification(this.notifyObj2);
+
+ this.notifyObj3 = new BasicNotification(this.id);
+ this.notifyObj3.id += "_3";
+ this.notifyObj3.anchorID = "default-notification-icon";
+ this.notifyObj3.options.persistent = true;
+ this.notification3 = showNotification(this.notifyObj3);
+
+ PopupNotifications._update();
+ },
+ onShown(popup) {
+ let notifications = popup.children;
+ is(notifications.length, 2, "two notifications displayed");
+ let [notification1, notification2] = notifications;
+ is(
+ notification1.id,
+ this.notifyObj1.id + "-notification",
+ "id 1 matches"
+ );
+ is(
+ notification2.id,
+ this.notifyObj3.id + "-notification",
+ "id 2 matches"
+ );
+
+ this.notification1.remove();
+ this.notification2.remove();
+ this.notification3.remove();
+ },
+ onHidden(popup) {},
+ },
+ // Test that on closebutton click, only the persistent notification
+ // that contained the closebutton loses its persistent status.
+ {
+ id: "Test#10",
+ run() {
+ this.notifyObj1 = new BasicNotification(this.id);
+ this.notifyObj1.id += "_1";
+ this.notifyObj1.anchorID = "geo-notification-icon";
+ this.notifyObj1.options.persistent = true;
+ this.notifyObj1.options.hideClose = false;
+ this.notification1 = showNotification(this.notifyObj1);
+
+ this.notifyObj2 = new BasicNotification(this.id);
+ this.notifyObj2.id += "_2";
+ this.notifyObj2.anchorID = "geo-notification-icon";
+ this.notifyObj2.options.persistent = true;
+ this.notifyObj2.options.hideClose = false;
+ this.notification2 = showNotification(this.notifyObj2);
+
+ this.notifyObj3 = new BasicNotification(this.id);
+ this.notifyObj3.id += "_3";
+ this.notifyObj3.anchorID = "geo-notification-icon";
+ this.notifyObj3.options.persistent = true;
+ this.notifyObj3.options.hideClose = false;
+ this.notification3 = showNotification(this.notifyObj3);
+
+ PopupNotifications._update();
+ },
+ onShown(popup) {
+ let notifications = popup.children;
+ is(notifications.length, 3, "three notifications displayed");
+ EventUtils.synthesizeMouseAtCenter(notifications[1].closebutton, {});
+ },
+ onHidden(popup) {
+ let notifications = popup.children;
+ is(notifications.length, 2, "two notifications displayed");
+
+ ok(this.notification1.options.persistent, "notification 1 is persistent");
+ ok(
+ !this.notification2.options.persistent,
+ "notification 2 is not persistent"
+ );
+ ok(this.notification3.options.persistent, "notification 3 is persistent");
+
+ this.notification1.remove();
+ this.notification2.remove();
+ this.notification3.remove();
+ },
+ },
+ // Test clicking the anchor icon.
+ // Clicking the anchor of an already visible persistent notification should
+ // focus the main action button, but not cause additional showing/shown event
+ // callback calls.
+ // Clicking the anchor of a dismissed notification should show it, even when
+ // the currently displayed notification is a persistent one.
+ {
+ id: "Test#11",
+ async run() {
+ await SpecialPowers.pushPrefEnv({ set: [["accessibility.tabfocus", 7]] });
+
+ function clickAnchor(notifyObj) {
+ let anchor = document.getElementById(notifyObj.anchorID);
+ EventUtils.synthesizeMouseAtCenter(anchor, {});
+ }
+
+ let popup = PopupNotifications.panel;
+
+ let notifyObj1 = new BasicNotification(this.id);
+ notifyObj1.id += "_1";
+ notifyObj1.anchorID = "default-notification-icon";
+ notifyObj1.options.persistent = true;
+ let shown = waitForNotificationPanel();
+ let notification1 = showNotification(notifyObj1);
+ await shown;
+ checkPopup(popup, notifyObj1);
+ ok(
+ !notifyObj1.dismissalCallbackTriggered,
+ "Should not have dismissed the notification"
+ );
+ notifyObj1.shownCallbackTriggered = false;
+ notifyObj1.showingCallbackTriggered = false;
+
+ // Click the anchor. This should focus the closebutton
+ // (because it's the first focusable element), but not
+ // call event callbacks on the notification object.
+ clickAnchor(notifyObj1);
+ is(document.activeElement, popup.children[0].closebutton);
+ ok(
+ !notifyObj1.dismissalCallbackTriggered,
+ "Should not have dismissed the notification"
+ );
+ ok(
+ !notifyObj1.shownCallbackTriggered,
+ "Should have triggered the shown event again"
+ );
+ ok(
+ !notifyObj1.showingCallbackTriggered,
+ "Should have triggered the showing event again"
+ );
+
+ // Add another notification.
+ let notifyObj2 = new BasicNotification(this.id);
+ notifyObj2.id += "_2";
+ notifyObj2.anchorID = "geo-notification-icon";
+ notifyObj2.options.dismissed = true;
+ let notification2 = showNotification(notifyObj2);
+
+ // Click the anchor of the second notification, this should dismiss the
+ // first notification.
+ shown = waitForNotificationPanel();
+ clickAnchor(notifyObj2);
+ await shown;
+ checkPopup(popup, notifyObj2);
+ ok(
+ notifyObj1.dismissalCallbackTriggered,
+ "Should have dismissed the first notification"
+ );
+
+ // Click the anchor of the first notification, it should be shown again.
+ shown = waitForNotificationPanel();
+ clickAnchor(notifyObj1);
+ await shown;
+ checkPopup(popup, notifyObj1);
+ ok(
+ notifyObj2.dismissalCallbackTriggered,
+ "Should have dismissed the second notification"
+ );
+
+ // Cleanup.
+ notification1.remove();
+ notification2.remove();
+ goNext();
+ },
+ },
+];
diff --git a/browser/base/content/test/popupNotifications/browser_popupNotification_accesskey.js b/browser/base/content/test/popupNotifications/browser_popupNotification_accesskey.js
new file mode 100644
index 0000000000..4a68105e27
--- /dev/null
+++ b/browser/base/content/test/popupNotifications/browser_popupNotification_accesskey.js
@@ -0,0 +1,44 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+function test() {
+ waitForExplicitFinish();
+
+ ok(PopupNotifications, "PopupNotifications object exists");
+ ok(PopupNotifications.panel, "PopupNotifications panel exists");
+
+ setup();
+}
+
+let buttonPressed = false;
+
+function commandTriggered() {
+ buttonPressed = true;
+}
+
+var tests = [
+ // This test ensures that the accesskey closes the popup.
+ {
+ id: "Test#1",
+ run() {
+ this.notifyObj = new BasicNotification(this.id);
+ showNotification(this.notifyObj);
+ },
+ onShown(popup) {
+ window.addEventListener("command", commandTriggered, true);
+ checkPopup(popup, this.notifyObj);
+ EventUtils.synthesizeKey("VK_ALT", { type: "keydown" });
+ EventUtils.synthesizeKey("M", { altKey: true });
+ EventUtils.synthesizeKey("VK_ALT", { type: "keyup" });
+
+ // If bug xxx was present, then the popup would be in the
+ // process of being hidden right now.
+ isnot(popup.state, "hiding", "popup is not hiding");
+ },
+ onHidden(popup) {
+ window.removeEventListener("command", commandTriggered, true);
+ ok(buttonPressed, "button pressed");
+ },
+ },
+];
diff --git a/browser/base/content/test/popupNotifications/browser_popupNotification_checkbox.js b/browser/base/content/test/popupNotifications/browser_popupNotification_checkbox.js
new file mode 100644
index 0000000000..c1d82042c8
--- /dev/null
+++ b/browser/base/content/test/popupNotifications/browser_popupNotification_checkbox.js
@@ -0,0 +1,248 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+function test() {
+ waitForExplicitFinish();
+
+ ok(PopupNotifications, "PopupNotifications object exists");
+ ok(PopupNotifications.panel, "PopupNotifications panel exists");
+
+ setup();
+}
+
+function checkCheckbox(checkbox, label, checked = false, hidden = false) {
+ is(checkbox.label, label, "Checkbox should have the correct label");
+ is(checkbox.hidden, hidden, "Checkbox should be shown");
+ is(checkbox.checked, checked, "Checkbox should be checked by default");
+}
+
+function checkMainAction(notification, disabled = false) {
+ let mainAction = notification.button;
+ let warningLabel = notification.querySelector(".popup-notification-warning");
+ is(warningLabel.hidden, !disabled, "Warning label should be shown");
+ is(mainAction.disabled, disabled, "MainAction should be disabled");
+}
+
+function promiseElementVisible(element) {
+ // HTMLElement.offsetParent is null when the element is not visisble
+ // (or if the element has |position: fixed|). See:
+ // https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/offsetParent
+ return TestUtils.waitForCondition(
+ () => element.offsetParent !== null,
+ "Waiting for element to be visible"
+ );
+}
+
+var gNotification;
+
+var tests = [
+ // Test that passing the checkbox field shows the checkbox.
+ {
+ id: "show_checkbox",
+ run() {
+ this.notifyObj = new BasicNotification(this.id);
+ this.notifyObj.options.checkbox = {
+ label: "This is a checkbox",
+ };
+ gNotification = showNotification(this.notifyObj);
+ },
+ onShown(popup) {
+ checkPopup(popup, this.notifyObj);
+ let notification = popup.children[0];
+ checkCheckbox(notification.checkbox, "This is a checkbox");
+ triggerMainCommand(popup);
+ },
+ onHidden() {},
+ },
+
+ // Test checkbox being checked by default
+ {
+ id: "checkbox_checked",
+ run() {
+ this.notifyObj = new BasicNotification(this.id);
+ this.notifyObj.options.checkbox = {
+ label: "Check this",
+ checked: true,
+ };
+ gNotification = showNotification(this.notifyObj);
+ },
+ onShown(popup) {
+ checkPopup(popup, this.notifyObj);
+ let notification = popup.children[0];
+ checkCheckbox(notification.checkbox, "Check this", true);
+ triggerMainCommand(popup);
+ },
+ onHidden() {},
+ },
+
+ // Test checkbox passing the checkbox state on mainAction
+ {
+ id: "checkbox_passCheckboxChecked_mainAction",
+ run() {
+ this.notifyObj = new BasicNotification(this.id);
+ this.notifyObj.mainAction.callback = ({ checkboxChecked }) =>
+ (this.mainActionChecked = checkboxChecked);
+ this.notifyObj.options.checkbox = {
+ label: "This is a checkbox",
+ };
+ gNotification = showNotification(this.notifyObj);
+ },
+ async onShown(popup) {
+ checkPopup(popup, this.notifyObj);
+ let notification = popup.children[0];
+ let checkbox = notification.checkbox;
+ checkCheckbox(checkbox, "This is a checkbox");
+ await promiseElementVisible(checkbox);
+ EventUtils.synthesizeMouseAtCenter(checkbox, {});
+ checkCheckbox(checkbox, "This is a checkbox", true);
+ triggerMainCommand(popup);
+ },
+ onHidden() {
+ is(
+ this.mainActionChecked,
+ true,
+ "mainAction callback is passed the correct checkbox value"
+ );
+ },
+ },
+
+ // Test checkbox passing the checkbox state on secondaryAction
+ {
+ id: "checkbox_passCheckboxChecked_secondaryAction",
+ run() {
+ this.notifyObj = new BasicNotification(this.id);
+ this.notifyObj.secondaryActions = [
+ {
+ label: "Test Secondary",
+ accessKey: "T",
+ callback: ({ checkboxChecked }) =>
+ (this.secondaryActionChecked = checkboxChecked),
+ },
+ ];
+ this.notifyObj.options.checkbox = {
+ label: "This is a checkbox",
+ };
+ gNotification = showNotification(this.notifyObj);
+ },
+ async onShown(popup) {
+ checkPopup(popup, this.notifyObj);
+ let notification = popup.children[0];
+ let checkbox = notification.checkbox;
+ checkCheckbox(checkbox, "This is a checkbox");
+ await promiseElementVisible(checkbox);
+ EventUtils.synthesizeMouseAtCenter(checkbox, {});
+ checkCheckbox(checkbox, "This is a checkbox", true);
+ triggerSecondaryCommand(popup, 0);
+ },
+ onHidden() {
+ is(
+ this.secondaryActionChecked,
+ true,
+ "secondaryAction callback is passed the correct checkbox value"
+ );
+ },
+ },
+
+ // Test checkbox preserving its state through re-opening the doorhanger
+ {
+ id: "checkbox_reopen",
+ run() {
+ this.notifyObj = new BasicNotification(this.id);
+ this.notifyObj.options.checkbox = {
+ label: "This is a checkbox",
+ checkedState: {
+ disableMainAction: true,
+ warningLabel: "Testing disable",
+ },
+ };
+ gNotification = showNotification(this.notifyObj);
+ },
+ async onShown(popup) {
+ checkPopup(popup, this.notifyObj);
+ let notification = popup.children[0];
+ let checkbox = notification.checkbox;
+ checkCheckbox(checkbox, "This is a checkbox");
+ await promiseElementVisible(checkbox);
+ EventUtils.synthesizeMouseAtCenter(checkbox, {});
+ dismissNotification(popup);
+ },
+ async onHidden(popup) {
+ let icon = document.getElementById("default-notification-icon");
+ let shown = waitForNotificationPanel();
+ EventUtils.synthesizeMouseAtCenter(icon, {});
+ await shown;
+ let notification = popup.children[0];
+ let checkbox = notification.checkbox;
+ checkCheckbox(checkbox, "This is a checkbox", true);
+ checkMainAction(notification, true);
+ gNotification.remove();
+ },
+ },
+
+ // Test no checkbox hides warning label
+ {
+ id: "no_checkbox",
+ run() {
+ this.notifyObj = new BasicNotification(this.id);
+ this.notifyObj.options.checkbox = null;
+ gNotification = showNotification(this.notifyObj);
+ },
+ onShown(popup) {
+ checkPopup(popup, this.notifyObj);
+ let notification = popup.children[0];
+ checkCheckbox(notification.checkbox, "", false, true);
+ checkMainAction(notification);
+ triggerMainCommand(popup);
+ },
+ onHidden() {},
+ },
+];
+
+// Test checkbox disabling the main action in different combinations
+["checkedState", "uncheckedState"].forEach(function (state) {
+ [true, false].forEach(function (checked) {
+ tests.push({
+ id: `checkbox_disableMainAction_${state}_${
+ checked ? "checked" : "unchecked"
+ }`,
+ run() {
+ this.notifyObj = new BasicNotification(this.id);
+ this.notifyObj.options.checkbox = {
+ label: "This is a checkbox",
+ checked,
+ [state]: {
+ disableMainAction: true,
+ warningLabel: "Testing disable",
+ },
+ };
+ gNotification = showNotification(this.notifyObj);
+ },
+ async onShown(popup) {
+ checkPopup(popup, this.notifyObj);
+ let notification = popup.children[0];
+ let checkbox = notification.checkbox;
+ let disabled =
+ (state === "checkedState" && checked) ||
+ (state === "uncheckedState" && !checked);
+
+ checkCheckbox(checkbox, "This is a checkbox", checked);
+ checkMainAction(notification, disabled);
+ await promiseElementVisible(checkbox);
+ EventUtils.synthesizeMouseAtCenter(checkbox, {});
+ checkCheckbox(checkbox, "This is a checkbox", !checked);
+ checkMainAction(notification, !disabled);
+ EventUtils.synthesizeMouseAtCenter(checkbox, {});
+ checkCheckbox(checkbox, "This is a checkbox", checked);
+ checkMainAction(notification, disabled);
+
+ // Unblock the main command if it's currently disabled.
+ if (disabled) {
+ EventUtils.synthesizeMouseAtCenter(checkbox, {});
+ }
+ triggerMainCommand(popup);
+ },
+ onHidden() {},
+ });
+ });
+});
diff --git a/browser/base/content/test/popupNotifications/browser_popupNotification_hide_after_identity_panel.js b/browser/base/content/test/popupNotifications/browser_popupNotification_hide_after_identity_panel.js
new file mode 100644
index 0000000000..de930375f6
--- /dev/null
+++ b/browser/base/content/test/popupNotifications/browser_popupNotification_hide_after_identity_panel.js
@@ -0,0 +1,36 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+add_task(async function test_displayURI_geo() {
+ await BrowserTestUtils.withNewTab(
+ "https://test1.example.com/",
+ async function (browser) {
+ let popupShownPromise = waitForNotificationPanel();
+ await SpecialPowers.spawn(browser, [], async function () {
+ content.navigator.geolocation.getCurrentPosition(() => {});
+ });
+ await popupShownPromise;
+
+ popupShownPromise = BrowserTestUtils.waitForEvent(
+ window,
+ "popupshown",
+ true,
+ event => event.target == gIdentityHandler._identityPopup
+ );
+ EventUtils.synthesizeMouseAtCenter(gIdentityHandler._identityIconBox, {});
+ await popupShownPromise;
+
+ Assert.ok(!PopupNotifications.isPanelOpen, "Geolocation popup is hidden");
+
+ let popupHidden = BrowserTestUtils.waitForEvent(
+ gIdentityHandler._identityPopup,
+ "popuphidden"
+ );
+ gIdentityHandler._identityPopup.hidePopup();
+ await popupHidden;
+
+ Assert.ok(PopupNotifications.isPanelOpen, "Geolocation popup is showing");
+ }
+ );
+});
diff --git a/browser/base/content/test/popupNotifications/browser_popupNotification_hide_after_protections_panel.js b/browser/base/content/test/popupNotifications/browser_popupNotification_hide_after_protections_panel.js
new file mode 100644
index 0000000000..f47f20a2d7
--- /dev/null
+++ b/browser/base/content/test/popupNotifications/browser_popupNotification_hide_after_protections_panel.js
@@ -0,0 +1,44 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+add_task(async function test_hide_popup_with_protections_panel_showing() {
+ await BrowserTestUtils.withNewTab(
+ "https://test1.example.com/",
+ async function (browser) {
+ // Request location permissions and wait for that prompt to appear.
+ let popupShownPromise = waitForNotificationPanel();
+ await SpecialPowers.spawn(browser, [], async function () {
+ content.navigator.geolocation.getCurrentPosition(() => {});
+ });
+ await popupShownPromise;
+
+ // Click on the icon for the protections panel, to show the panel.
+ popupShownPromise = BrowserTestUtils.waitForEvent(
+ window,
+ "popupshown",
+ true,
+ event => event.target == gProtectionsHandler._protectionsPopup
+ );
+ EventUtils.synthesizeMouseAtCenter(
+ document.getElementById("tracking-protection-icon-container"),
+ {}
+ );
+ await popupShownPromise;
+
+ // Make sure the location permission prompt closed.
+ Assert.ok(!PopupNotifications.isPanelOpen, "Geolocation popup is hidden");
+
+ // Close the protections panel.
+ let popupHidden = BrowserTestUtils.waitForEvent(
+ gProtectionsHandler._protectionsPopup,
+ "popuphidden"
+ );
+ gProtectionsHandler._protectionsPopup.hidePopup();
+ await popupHidden;
+
+ // Make sure the location permission prompt came back.
+ Assert.ok(PopupNotifications.isPanelOpen, "Geolocation popup is showing");
+ }
+ );
+});
diff --git a/browser/base/content/test/popupNotifications/browser_popupNotification_keyboard.js b/browser/base/content/test/popupNotifications/browser_popupNotification_keyboard.js
new file mode 100644
index 0000000000..5c20751c3f
--- /dev/null
+++ b/browser/base/content/test/popupNotifications/browser_popupNotification_keyboard.js
@@ -0,0 +1,273 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+function test() {
+ waitForExplicitFinish();
+
+ ok(PopupNotifications, "PopupNotifications object exists");
+ ok(PopupNotifications.panel, "PopupNotifications panel exists");
+
+ // Force tabfocus for all elements on OSX.
+ SpecialPowers.pushPrefEnv({ set: [["accessibility.tabfocus", 7]] }).then(
+ setup
+ );
+}
+
+// Focusing on notification icon buttons is handled by the ToolbarKeyboardNavigator
+// component and arrow keys (see browser/base/content/browser-toolbarKeyNav.js).
+function focusNotificationAnchor(anchor) {
+ let urlbarContainer = anchor.closest("#urlbar-container");
+ urlbarContainer.querySelector("toolbartabstop").focus();
+ const trackingProtectionIconContainer = urlbarContainer.querySelector(
+ "#tracking-protection-icon-container"
+ );
+ is(
+ document.activeElement,
+ trackingProtectionIconContainer,
+ "tracking protection icon container is focused."
+ );
+ while (document.activeElement !== anchor) {
+ EventUtils.synthesizeKey("ArrowRight");
+ }
+}
+
+var tests = [
+ // Test that for persistent notifications,
+ // the secondary action is triggered by pressing the escape key.
+ {
+ id: "Test#1",
+ run() {
+ this.notifyObj = new BasicNotification(this.id);
+ this.notifyObj.options.persistent = true;
+ showNotification(this.notifyObj);
+ },
+ onShown(popup) {
+ checkPopup(popup, this.notifyObj);
+ EventUtils.synthesizeKey("KEY_Escape");
+ },
+ onHidden(popup) {
+ ok(!this.notifyObj.mainActionClicked, "mainAction was not clicked");
+ ok(this.notifyObj.secondaryActionClicked, "secondaryAction was clicked");
+ ok(
+ !this.notifyObj.dismissalCallbackTriggered,
+ "dismissal callback wasn't triggered"
+ );
+ ok(this.notifyObj.removedCallbackTriggered, "removed callback triggered");
+ is(
+ this.notifyObj.mainActionSource,
+ undefined,
+ "shouldn't have a main action source."
+ );
+ is(
+ this.notifyObj.secondaryActionSource,
+ "esc-press",
+ "secondary action should be from ESC key press"
+ );
+ },
+ },
+ // Test that for non-persistent notifications, the escape key dismisses the notification.
+ {
+ id: "Test#2",
+ run() {
+ this.notifyObj = new BasicNotification(this.id);
+ this.notification = showNotification(this.notifyObj);
+ },
+ onShown(popup) {
+ checkPopup(popup, this.notifyObj);
+ EventUtils.synthesizeKey("KEY_Escape");
+ },
+ onHidden(popup) {
+ ok(!this.notifyObj.mainActionClicked, "mainAction was not clicked");
+ ok(
+ !this.notifyObj.secondaryActionClicked,
+ "secondaryAction was not clicked"
+ );
+ ok(
+ this.notifyObj.dismissalCallbackTriggered,
+ "dismissal callback triggered"
+ );
+ ok(
+ !this.notifyObj.removedCallbackTriggered,
+ "removed callback was not triggered"
+ );
+ is(
+ this.notifyObj.mainActionSource,
+ undefined,
+ "shouldn't have a main action source."
+ );
+ is(
+ this.notifyObj.secondaryActionSource,
+ undefined,
+ "shouldn't have a secondary action source."
+ );
+ this.notification.remove();
+ },
+ },
+ // Test that the space key on an anchor element focuses an active notification
+ {
+ id: "Test#3",
+ run() {
+ this.notifyObj = new BasicNotification(this.id);
+ this.notifyObj.anchorID = "geo-notification-icon";
+ this.notifyObj.addOptions({
+ persistent: true,
+ });
+ this.notification = showNotification(this.notifyObj);
+ },
+ onShown(popup) {
+ checkPopup(popup, this.notifyObj);
+ let anchor = document.getElementById(this.notifyObj.anchorID);
+ focusNotificationAnchor(anchor);
+ EventUtils.sendString(" ");
+ is(document.activeElement, popup.children[0].closebutton);
+ this.notification.remove();
+ },
+ onHidden(popup) {},
+ },
+ // Test that you can switch between active notifications with the space key
+ // and that the notification is focused on selection.
+ {
+ id: "Test#4",
+ async run() {
+ let notifyObj1 = new BasicNotification(this.id);
+ notifyObj1.id += "_1";
+ notifyObj1.anchorID = "default-notification-icon";
+ notifyObj1.addOptions({
+ hideClose: true,
+ checkbox: {
+ label: "Test that elements inside the panel can be focused",
+ },
+ persistent: true,
+ });
+ let opened = waitForNotificationPanel();
+ let notification1 = showNotification(notifyObj1);
+ await opened;
+
+ let notifyObj2 = new BasicNotification(this.id);
+ notifyObj2.id += "_2";
+ notifyObj2.anchorID = "geo-notification-icon";
+ notifyObj2.addOptions({
+ persistent: true,
+ });
+ opened = waitForNotificationPanel();
+ let notification2 = showNotification(notifyObj2);
+ let popup = await opened;
+
+ // Make sure notification 2 is visible
+ checkPopup(popup, notifyObj2);
+
+ // Activate the anchor for notification 1 and wait until it's shown.
+ let anchor = document.getElementById(notifyObj1.anchorID);
+ focusNotificationAnchor(anchor);
+ is(document.activeElement, anchor);
+ opened = waitForNotificationPanel();
+ EventUtils.sendString(" ");
+ popup = await opened;
+ checkPopup(popup, notifyObj1);
+
+ is(document.activeElement, popup.children[0].checkbox);
+
+ // Activate the anchor for notification 2 and wait until it's shown.
+ anchor = document.getElementById(notifyObj2.anchorID);
+ focusNotificationAnchor(anchor);
+ is(document.activeElement, anchor);
+ opened = waitForNotificationPanel();
+ EventUtils.sendString(" ");
+ popup = await opened;
+ checkPopup(popup, notifyObj2);
+
+ is(document.activeElement, popup.children[0].closebutton);
+
+ notification1.remove();
+ notification2.remove();
+ goNext();
+ },
+ },
+ // Test that passing the autofocus option will focus an opened notification.
+ {
+ id: "Test#5",
+ run() {
+ this.notifyObj = new BasicNotification(this.id);
+ this.notifyObj.anchorID = "geo-notification-icon";
+ this.notifyObj.addOptions({
+ autofocus: true,
+ });
+ this.notification = showNotification(this.notifyObj);
+ },
+ onShown(popup) {
+ checkPopup(popup, this.notifyObj);
+
+ // Initial focus on open is null because a panel itself
+ // can not be focused, next tab focus will be inside the panel.
+ is(Services.focus.focusedElement, null);
+
+ EventUtils.synthesizeKey("KEY_Tab");
+ is(Services.focus.focusedElement, popup.children[0].closebutton);
+ dismissNotification(popup);
+ },
+ async onHidden() {
+ // Focus the urlbar to check that it stays focused.
+ gURLBar.focus();
+
+ // Show another notification and make sure it's not autofocused.
+ let notifyObj = new BasicNotification(this.id);
+ notifyObj.id += "_2";
+ notifyObj.anchorID = "default-notification-icon";
+
+ let opened = waitForNotificationPanel();
+ let notification = showNotification(notifyObj);
+ let popup = await opened;
+ checkPopup(popup, notifyObj);
+
+ // Check that the urlbar is still focused.
+ is(Services.focus.focusedElement, gURLBar.inputField);
+
+ this.notification.remove();
+ notification.remove();
+ },
+ },
+ // Test that focus is not moved out of a content element if autofocus is not set.
+ {
+ id: "Test#6",
+ async run() {
+ let id = this.id;
+ await BrowserTestUtils.withNewTab(
+ "data:text/html,<input id='test-input'/>",
+ async function (browser) {
+ let notifyObj = new BasicNotification(id);
+ await SpecialPowers.spawn(browser, [], function () {
+ content.document.getElementById("test-input").focus();
+ });
+
+ let opened = waitForNotificationPanel();
+ let notification = showNotification(notifyObj);
+ await opened;
+
+ // Check that the focused element in the chrome window
+ // is either the browser in case we're running on e10s
+ // or the input field in case of non-e10s.
+ if (gMultiProcessBrowser) {
+ is(Services.focus.focusedElement, browser);
+ } else {
+ is(
+ Services.focus.focusedElement,
+ browser.contentDocument.getElementById("test-input")
+ );
+ }
+
+ // Check that the input field is still focused inside the browser.
+ await SpecialPowers.spawn(browser, [], function () {
+ is(
+ content.document.activeElement,
+ content.document.getElementById("test-input")
+ );
+ });
+
+ notification.remove();
+ }
+ );
+ goNext();
+ },
+ },
+];
diff --git a/browser/base/content/test/popupNotifications/browser_popupNotification_learnmore.js b/browser/base/content/test/popupNotifications/browser_popupNotification_learnmore.js
new file mode 100644
index 0000000000..fc3946598c
--- /dev/null
+++ b/browser/base/content/test/popupNotifications/browser_popupNotification_learnmore.js
@@ -0,0 +1,64 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+function test() {
+ waitForExplicitFinish();
+
+ ok(PopupNotifications, "PopupNotifications object exists");
+ ok(PopupNotifications.panel, "PopupNotifications panel exists");
+
+ setup();
+}
+
+var tests = [
+ // Test checkbox being checked by default
+ {
+ id: "without_learn_more",
+ run() {
+ this.notifyObj = new BasicNotification(this.id);
+ showNotification(this.notifyObj);
+ },
+ onShown(popup) {
+ checkPopup(popup, this.notifyObj);
+ let notification = popup.children[0];
+ let link = notification.querySelector(
+ ".popup-notification-learnmore-link"
+ );
+ ok(!link.href, "no href");
+ is(
+ window.getComputedStyle(link).getPropertyValue("display"),
+ "none",
+ "link hidden"
+ );
+ dismissNotification(popup);
+ },
+ onHidden() {},
+ },
+
+ // Test that passing the learnMoreURL field sets up the link.
+ {
+ id: "with_learn_more",
+ run() {
+ this.notifyObj = new BasicNotification(this.id);
+ this.notifyObj.options.learnMoreURL = "https://mozilla.org";
+ showNotification(this.notifyObj);
+ },
+ onShown(popup) {
+ checkPopup(popup, this.notifyObj);
+ let notification = popup.children[0];
+ let link = notification.querySelector(
+ ".popup-notification-learnmore-link"
+ );
+ is(link.textContent, "Learn more", "correct label");
+ is(link.href, "https://mozilla.org", "correct href");
+ isnot(
+ window.getComputedStyle(link).getPropertyValue("display"),
+ "none",
+ "link not hidden"
+ );
+ dismissNotification(popup);
+ },
+ onHidden() {},
+ },
+];
diff --git a/browser/base/content/test/popupNotifications/browser_popupNotification_no_anchors.js b/browser/base/content/test/popupNotifications/browser_popupNotification_no_anchors.js
new file mode 100644
index 0000000000..a73e1f5948
--- /dev/null
+++ b/browser/base/content/test/popupNotifications/browser_popupNotification_no_anchors.js
@@ -0,0 +1,288 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+function test() {
+ waitForExplicitFinish();
+
+ ok(PopupNotifications, "PopupNotifications object exists");
+ ok(PopupNotifications.panel, "PopupNotifications panel exists");
+
+ setup();
+}
+
+const FALLBACK_ANCHOR = gURLBar.searchButton
+ ? "urlbar-search-button"
+ : "identity-icon";
+
+var tests = [
+ // Test that popupnotifications are anchored to the fallback anchor on
+ // about:blank, where anchor icons are hidden.
+ {
+ id: "Test#1",
+ async run() {
+ this.oldSelectedTab = gBrowser.selectedTab;
+ await BrowserTestUtils.openNewForegroundTab(gBrowser, "about:blank");
+
+ this.notifyObj = new BasicNotification(this.id);
+ this.notifyObj.anchorID = "geo-notification-icon";
+ this.notification = showNotification(this.notifyObj);
+ },
+ onShown(popup) {
+ checkPopup(popup, this.notifyObj);
+ is(
+ document.getElementById("geo-notification-icon").getBoundingClientRect()
+ .width,
+ 0,
+ "geo anchor shouldn't be visible"
+ );
+ is(
+ popup.anchorNode.id,
+ FALLBACK_ANCHOR,
+ "notification anchored to fallback anchor"
+ );
+ dismissNotification(popup);
+ },
+ onHidden(popup) {
+ this.notification.remove();
+ gBrowser.removeTab(gBrowser.selectedTab);
+ gBrowser.selectedTab = this.oldSelectedTab;
+ },
+ },
+ // Test that popupnotifications are anchored to the fallback anchor after
+ // navigation to about:blank.
+ {
+ id: "Test#2",
+ async run() {
+ this.oldSelectedTab = gBrowser.selectedTab;
+ await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ "http://example.com/"
+ );
+
+ this.notifyObj = new BasicNotification(this.id);
+ this.notifyObj.anchorID = "geo-notification-icon";
+ this.notifyObj.addOptions({
+ persistence: 1,
+ });
+ this.notification = showNotification(this.notifyObj);
+ },
+ async onShown(popup) {
+ await promiseTabLoadEvent(gBrowser.selectedTab, "about:blank");
+
+ checkPopup(popup, this.notifyObj);
+ is(
+ document.getElementById("geo-notification-icon").getBoundingClientRect()
+ .width,
+ 0,
+ "geo anchor shouldn't be visible"
+ );
+ is(
+ popup.anchorNode.id,
+ FALLBACK_ANCHOR,
+ "notification anchored to fallback anchor"
+ );
+ dismissNotification(popup);
+ },
+ onHidden(popup) {
+ this.notification.remove();
+ gBrowser.removeTab(gBrowser.selectedTab);
+ gBrowser.selectedTab = this.oldSelectedTab;
+ },
+ },
+ // Test that dismissed popupnotifications cannot be opened on about:blank, but
+ // can be opened after navigation.
+ {
+ id: "Test#3",
+ async run() {
+ this.oldSelectedTab = gBrowser.selectedTab;
+ await BrowserTestUtils.openNewForegroundTab(gBrowser, "about:blank");
+
+ this.notifyObj = new BasicNotification(this.id);
+ this.notifyObj.anchorID = "geo-notification-icon";
+ this.notifyObj.addOptions({
+ dismissed: true,
+ persistence: 1,
+ });
+ this.notification = showNotification(this.notifyObj);
+
+ is(
+ document.getElementById("geo-notification-icon").getBoundingClientRect()
+ .width,
+ 0,
+ "geo anchor shouldn't be visible"
+ );
+
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ await promiseTabLoadEvent(gBrowser.selectedTab, "http://example.com/");
+
+ isnot(
+ document.getElementById("geo-notification-icon").getBoundingClientRect()
+ .width,
+ 0,
+ "geo anchor should be visible"
+ );
+
+ EventUtils.synthesizeMouse(
+ document.getElementById("geo-notification-icon"),
+ 2,
+ 2,
+ {}
+ );
+ },
+ onShown(popup) {
+ checkPopup(popup, this.notifyObj);
+ dismissNotification(popup);
+ },
+ onHidden(popup) {
+ this.notification.remove();
+ gBrowser.removeTab(gBrowser.selectedTab);
+ gBrowser.selectedTab = this.oldSelectedTab;
+ },
+ },
+ // Test that popupnotifications are hidden while editing the URL in the
+ // location bar, anchored to the fallback anchor when the focus is moved away
+ // from the location bar, and restored when the URL is reverted.
+ {
+ id: "Test#4",
+ async run() {
+ for (let persistent of [false, true]) {
+ let shown = waitForNotificationPanel();
+ this.notifyObj = new BasicNotification(this.id);
+ this.notifyObj.anchorID = "geo-notification-icon";
+ this.notifyObj.addOptions({ persistent });
+ this.notification = showNotification(this.notifyObj);
+ await shown;
+
+ checkPopup(PopupNotifications.panel, this.notifyObj);
+
+ // Typing in the location bar should hide the notification.
+ let hidden = waitForNotificationPanelHidden();
+ gURLBar.select();
+ EventUtils.sendString("*");
+ await hidden;
+
+ is(
+ document
+ .getElementById("geo-notification-icon")
+ .getBoundingClientRect().width,
+ 0,
+ "geo anchor shouldn't be visible"
+ );
+
+ // Moving focus to the next control should show the notifications again,
+ // anchored to the fallback anchor. We clear the URL bar before moving the
+ // focus so that the awesomebar popup doesn't get in the way.
+ shown = waitForNotificationPanel();
+ EventUtils.synthesizeKey("KEY_Backspace");
+ EventUtils.synthesizeKey("KEY_Tab");
+ await shown;
+
+ is(
+ PopupNotifications.panel.anchorNode.id,
+ FALLBACK_ANCHOR,
+ "notification anchored to fallback anchor"
+ );
+
+ // Moving focus to the location bar should hide the notification again.
+ hidden = waitForNotificationPanelHidden();
+ EventUtils.synthesizeKey("KEY_Tab", { shiftKey: true });
+ await hidden;
+
+ // Reverting the URL should show the notification again.
+ shown = waitForNotificationPanel();
+ EventUtils.synthesizeKey("KEY_Escape");
+ await shown;
+
+ checkPopup(PopupNotifications.panel, this.notifyObj);
+
+ hidden = waitForNotificationPanelHidden();
+ this.notification.remove();
+ await hidden;
+ }
+ goNext();
+ },
+ },
+ // Test that popupnotifications triggered while editing the URL in the
+ // location bar are only shown later when the URL is reverted.
+ {
+ id: "Test#5",
+ async run() {
+ for (let persistent of [false, true]) {
+ // Start editing the URL, ensuring that the awesomebar popup is hidden.
+ gURLBar.select();
+ EventUtils.sendString("*");
+ EventUtils.synthesizeKey("KEY_Backspace");
+ // autoOpen behavior will show the panel, so it must be closed.
+ gURLBar.view.close();
+
+ // Trying to show a notification should display nothing.
+ let notShowing = TestUtils.topicObserved(
+ "PopupNotifications-updateNotShowing"
+ );
+ this.notifyObj = new BasicNotification(this.id);
+ this.notifyObj.anchorID = "geo-notification-icon";
+ this.notifyObj.addOptions({ persistent });
+ this.notification = showNotification(this.notifyObj);
+ await notShowing;
+
+ // Reverting the URL should show the notification.
+ let shown = waitForNotificationPanel();
+ EventUtils.synthesizeKey("KEY_Escape");
+ await shown;
+
+ checkPopup(PopupNotifications.panel, this.notifyObj);
+
+ let hidden = waitForNotificationPanelHidden();
+ this.notification.remove();
+ await hidden;
+ }
+
+ goNext();
+ },
+ },
+ // Test that persistent panels are still open after switching to another tab
+ // and back, even while editing the URL in the new tab.
+ {
+ id: "Test#6",
+ async run() {
+ let shown = waitForNotificationPanel();
+ this.notifyObj = new BasicNotification(this.id);
+ this.notifyObj.anchorID = "geo-notification-icon";
+ this.notifyObj.addOptions({
+ persistent: true,
+ });
+ this.notification = showNotification(this.notifyObj);
+ await shown;
+
+ // Switching to a new tab should hide the notification.
+ let hidden = waitForNotificationPanelHidden();
+ this.oldSelectedTab = gBrowser.selectedTab;
+ await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ "http://example.com/"
+ );
+ await hidden;
+
+ // Start editing the URL.
+ gURLBar.select();
+ EventUtils.sendString("*");
+
+ // Switching to the old tab should show the notification again.
+ shown = waitForNotificationPanel();
+ gBrowser.removeTab(gBrowser.selectedTab);
+ gBrowser.selectedTab = this.oldSelectedTab;
+ await shown;
+
+ checkPopup(PopupNotifications.panel, this.notifyObj);
+
+ hidden = waitForNotificationPanelHidden();
+ this.notification.remove();
+ await hidden;
+
+ goNext();
+ },
+ },
+];
diff --git a/browser/base/content/test/popupNotifications/browser_popupNotification_security_delay.js b/browser/base/content/test/popupNotifications/browser_popupNotification_security_delay.js
new file mode 100644
index 0000000000..515895f35a
--- /dev/null
+++ b/browser/base/content/test/popupNotifications/browser_popupNotification_security_delay.js
@@ -0,0 +1,296 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const TEST_SECURITY_DELAY = 5000;
+
+/**
+ * Shows a test PopupNotification.
+ */
+function showNotification() {
+ PopupNotifications.show(
+ gBrowser.selectedBrowser,
+ "foo",
+ "Hello, World!",
+ "default-notification-icon",
+ {
+ label: "ok",
+ accessKey: "o",
+ callback: () => {},
+ },
+ [
+ {
+ label: "cancel",
+ accessKey: "c",
+ callback: () => {},
+ },
+ ],
+ {
+ // Make test notifications persistent to ensure they are only closed
+ // explicitly by test actions and survive tab switches.
+ persistent: true,
+ }
+ );
+}
+
+add_setup(async function () {
+ // Set a longer security delay for PopupNotification actions so we can test
+ // the delay even if the test runs slowly.
+ await SpecialPowers.pushPrefEnv({
+ set: [["security.notification_enable_delay", TEST_SECURITY_DELAY]],
+ });
+});
+
+async function ensureSecurityDelayReady() {
+ /**
+ * The security delay calculation in PopupNotification.sys.mjs is dependent on
+ * the monotonically increasing value of performance.now. This timestamp is
+ * not relative to a fixed date, but to runtime.
+ * We need to wait for the value performance.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.
+ * See: https://searchfox.org/mozilla-central/rev/f32d5f3949a3f4f185122142b29f2e3ab776836e/toolkit/modules/PopupNotifications.sys.mjs#1870-1872
+ *
+ * When running in automation as part of a larger test suite performance.now()
+ * should usually be already sufficiently high in which case this check should
+ * directly resolve.
+ */
+ await TestUtils.waitForCondition(
+ () => performance.now() > TEST_SECURITY_DELAY,
+ "Wait for performance.now() > SECURITY_DELAY",
+ 500,
+ 50
+ );
+}
+
+/**
+ * Tests that when we show a second notification while the panel is open the
+ * timeShown attribute is correctly set and the security delay is enforced
+ * properly.
+ */
+add_task(async function test_timeShownMultipleNotifications() {
+ await ensureSecurityDelayReady();
+
+ ok(
+ !PopupNotifications.isPanelOpen,
+ "PopupNotification panel should not be open initially."
+ );
+
+ info("Open the first notification.");
+ let popupShownPromise = waitForNotificationPanel();
+ showNotification();
+ await popupShownPromise;
+ ok(
+ PopupNotifications.isPanelOpen,
+ "PopupNotification should be open after first show call."
+ );
+
+ is(
+ PopupNotifications._currentNotifications.length,
+ 1,
+ "There should only be one notification"
+ );
+
+ let notification = PopupNotifications.getNotification(
+ "foo",
+ gBrowser.selectedBrowser
+ );
+ is(notification?.id, "foo", "There should be a notification with id foo");
+ ok(notification.timeShown, "The notification should have timeShown set");
+
+ info(
+ "Call show again with the same notification id while the PopupNotification panel is still open."
+ );
+ showNotification();
+ ok(
+ PopupNotifications.isPanelOpen,
+ "PopupNotification should still open after second show call."
+ );
+ notification = PopupNotifications.getNotification(
+ "foo",
+ gBrowser.selectedBrowser
+ );
+ is(
+ PopupNotifications._currentNotifications.length,
+ 1,
+ "There should still only be one notification"
+ );
+
+ is(
+ notification?.id,
+ "foo",
+ "There should still be a notification with id foo"
+ );
+ ok(notification.timeShown, "The notification should have timeShown set");
+
+ let notificationHiddenPromise = waitForNotificationPanelHidden();
+
+ info("Trigger main action via button click during security delay");
+ triggerMainCommand(PopupNotifications.panel);
+
+ await new Promise(resolve => setTimeout(resolve, 0));
+
+ ok(PopupNotifications.isPanelOpen, "PopupNotification should still be open.");
+ notification = PopupNotifications.getNotification(
+ "foo",
+ gBrowser.selectedBrowser
+ );
+ ok(
+ notification,
+ "Notification should still be open because we clicked during the security delay."
+ );
+
+ // If the notification is no longer shown (test failure) skip the remaining
+ // checks.
+ if (!notification) {
+ return;
+ }
+
+ // Ensure that once the security delay has passed the notification can be
+ // closed again.
+ let fakeTimeShown = TEST_SECURITY_DELAY + 500;
+ info(`Manually set timeShown to ${fakeTimeShown}ms in the past.`);
+ notification.timeShown = performance.now() - fakeTimeShown;
+
+ info("Trigger main action via button click outside security delay");
+ triggerMainCommand(PopupNotifications.panel);
+
+ info("Wait for panel to be hidden.");
+ await notificationHiddenPromise;
+
+ ok(
+ !PopupNotifications.getNotification("foo", gBrowser.selectedBrowser),
+ "Should not longer see the notification."
+ );
+});
+
+/**
+ * Tests that when we reshow a notification after a tab switch the timeShown
+ * attribute is correctly reset and the security delay is enforced.
+ */
+add_task(async function test_notificationReshowTabSwitch() {
+ await ensureSecurityDelayReady();
+
+ ok(
+ !PopupNotifications.isPanelOpen,
+ "PopupNotification panel should not be open initially."
+ );
+
+ info("Open the first notification.");
+ let popupShownPromise = waitForNotificationPanel();
+ showNotification();
+ await popupShownPromise;
+ ok(
+ PopupNotifications.isPanelOpen,
+ "PopupNotification should be open after first show call."
+ );
+
+ let notification = PopupNotifications.getNotification(
+ "foo",
+ gBrowser.selectedBrowser
+ );
+ is(notification?.id, "foo", "There should be a notification with id foo");
+ ok(notification.timeShown, "The notification should have timeShown set");
+
+ info("Trigger main action via button click during security delay");
+ triggerMainCommand(PopupNotifications.panel);
+
+ await new Promise(resolve => setTimeout(resolve, 0));
+
+ ok(PopupNotifications.isPanelOpen, "PopupNotification should still be open.");
+ notification = PopupNotifications.getNotification(
+ "foo",
+ gBrowser.selectedBrowser
+ );
+ ok(
+ notification,
+ "Notification should still be open because we clicked during the security delay."
+ );
+
+ // If the notification is no longer shown (test failure) skip the remaining
+ // checks.
+ if (!notification) {
+ return;
+ }
+
+ let panelHiddenPromise = waitForNotificationPanelHidden();
+ let panelShownPromise;
+
+ info("Open a new tab which hides the notification panel.");
+ await BrowserTestUtils.withNewTab("https://example.com", async browser => {
+ info("Wait for panel to be hidden by tab switch.");
+ await panelHiddenPromise;
+ info(
+ "Keep the tab open until the security delay for the original notification show has expired."
+ );
+ await new Promise(resolve =>
+ // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
+ setTimeout(resolve, TEST_SECURITY_DELAY + 500)
+ );
+
+ panelShownPromise = waitForNotificationPanel();
+ });
+ info(
+ "Wait for the panel to show again after the tab close. We're showing the original tab again."
+ );
+ await panelShownPromise;
+
+ ok(
+ PopupNotifications.isPanelOpen,
+ "PopupNotification should be shown after tab close."
+ );
+ notification = PopupNotifications.getNotification(
+ "foo",
+ gBrowser.selectedBrowser
+ );
+ is(
+ notification?.id,
+ "foo",
+ "There should still be a notification with id foo"
+ );
+
+ let notificationHiddenPromise = waitForNotificationPanelHidden();
+
+ info(
+ "Because we re-show the panel after tab close / switch the security delay should have reset."
+ );
+ info("Trigger main action via button click during the new security delay.");
+ triggerMainCommand(PopupNotifications.panel);
+
+ await new Promise(resolve => setTimeout(resolve, 0));
+
+ ok(PopupNotifications.isPanelOpen, "PopupNotification should still be open.");
+ notification = PopupNotifications.getNotification(
+ "foo",
+ gBrowser.selectedBrowser
+ );
+ ok(
+ notification,
+ "Notification should still be open because we clicked during the security delay."
+ );
+ // If the notification is no longer shown (test failure) skip the remaining
+ // checks.
+ if (!notification) {
+ return;
+ }
+
+ // Ensure that once the security delay has passed the notification can be
+ // closed again.
+ let fakeTimeShown = TEST_SECURITY_DELAY + 500;
+ info(`Manually set timeShown to ${fakeTimeShown}ms in the past.`);
+ notification.timeShown = performance.now() - fakeTimeShown;
+
+ info("Trigger main action via button click outside security delay");
+ triggerMainCommand(PopupNotifications.panel);
+
+ info("Wait for panel to be hidden.");
+ await notificationHiddenPromise;
+
+ ok(
+ !PopupNotifications.getNotification("foo", gBrowser.selectedBrowser),
+ "Should not longer see the notification."
+ );
+});
diff --git a/browser/base/content/test/popupNotifications/browser_popupNotification_selection_required.js b/browser/base/content/test/popupNotifications/browser_popupNotification_selection_required.js
new file mode 100644
index 0000000000..31463f5345
--- /dev/null
+++ b/browser/base/content/test/popupNotifications/browser_popupNotification_selection_required.js
@@ -0,0 +1,57 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+function test() {
+ waitForExplicitFinish();
+
+ ok(PopupNotifications, "PopupNotifications object exists");
+ ok(PopupNotifications.panel, "PopupNotifications panel exists");
+
+ setup();
+}
+
+function promiseElementVisible(element) {
+ // HTMLElement.offsetParent is null when the element is not visisble
+ // (or if the element has |position: fixed|). See:
+ // https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/offsetParent
+ return TestUtils.waitForCondition(
+ () => element.offsetParent !== null,
+ "Waiting for element to be visible"
+ );
+}
+
+var gNotification;
+
+var tests = [
+ // Test that passing selection required prevents the button from clicking
+ {
+ id: "require_selection_check",
+ run() {
+ this.notifyObj = new BasicNotification(this.id);
+ this.notifyObj.options.checkbox = {
+ label: "This is a checkbox",
+ };
+ gNotification = showNotification(this.notifyObj);
+ },
+ async onShown(popup) {
+ checkPopup(popup, this.notifyObj);
+ let notification = popup.children[0];
+ notification.setAttribute("invalidselection", true);
+ await promiseElementVisible(notification.checkbox);
+ EventUtils.synthesizeMouseAtCenter(notification.checkbox, {});
+ ok(
+ notification.button.disabled,
+ "should be disabled when invalidselection"
+ );
+ notification.removeAttribute("invalidselection");
+ EventUtils.synthesizeMouseAtCenter(notification.checkbox, {});
+ ok(
+ !notification.button.disabled,
+ "should not be disabled when invalidselection is not present"
+ );
+ triggerMainCommand(popup);
+ },
+ onHidden() {},
+ },
+];
diff --git a/browser/base/content/test/popupNotifications/browser_reshow_in_background.js b/browser/base/content/test/popupNotifications/browser_reshow_in_background.js
new file mode 100644
index 0000000000..bb2494a5b5
--- /dev/null
+++ b/browser/base/content/test/popupNotifications/browser_reshow_in_background.js
@@ -0,0 +1,72 @@
+"use strict";
+
+/**
+ * Tests that when PopupNotifications for background tabs are reshown, they
+ * don't show up in the foreground tab, but only in the background tab that
+ * they belong to.
+ */
+add_task(
+ async function test_background_notifications_dont_reshow_in_foreground() {
+ // Our initial tab will be A. Let's open two more tabs B and C, but keep
+ // A selected. Then, we'll trigger a PopupNotification in C, and then make
+ // it reshow.
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ let tabB = BrowserTestUtils.addTab(gBrowser, "http://example.com/");
+ await BrowserTestUtils.browserLoaded(tabB.linkedBrowser);
+
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ let tabC = BrowserTestUtils.addTab(gBrowser, "http://example.com/");
+ await BrowserTestUtils.browserLoaded(tabC.linkedBrowser);
+
+ let seenEvents = [];
+
+ let options = {
+ dismissed: false,
+ eventCallback(popupEvent) {
+ seenEvents.push(popupEvent);
+ },
+ };
+
+ let notification = PopupNotifications.show(
+ tabC.linkedBrowser,
+ "test-notification",
+ "",
+ "plugins-notification-icon",
+ null,
+ null,
+ options
+ );
+ Assert.deepEqual(seenEvents, [], "Should have seen no events yet.");
+
+ await BrowserTestUtils.switchTab(gBrowser, tabB);
+ Assert.deepEqual(seenEvents, [], "Should have seen no events yet.");
+
+ notification.reshow();
+ Assert.deepEqual(seenEvents, [], "Should have seen no events yet.");
+
+ let panelShown = BrowserTestUtils.waitForEvent(
+ PopupNotifications.panel,
+ "popupshown"
+ );
+ await BrowserTestUtils.switchTab(gBrowser, tabC);
+ await panelShown;
+
+ Assert.equal(seenEvents.length, 2, "Should have seen two events.");
+ Assert.equal(
+ seenEvents[0],
+ "showing",
+ "Should have said popup was showing."
+ );
+ Assert.equal(seenEvents[1], "shown", "Should have said popup was shown.");
+
+ let panelHidden = BrowserTestUtils.waitForEvent(
+ PopupNotifications.panel,
+ "popuphidden"
+ );
+ PopupNotifications.remove(notification);
+ await panelHidden;
+
+ BrowserTestUtils.removeTab(tabB);
+ BrowserTestUtils.removeTab(tabC);
+ }
+);
diff --git a/browser/base/content/test/popupNotifications/head.js b/browser/base/content/test/popupNotifications/head.js
new file mode 100644
index 0000000000..f347f8dbf2
--- /dev/null
+++ b/browser/base/content/test/popupNotifications/head.js
@@ -0,0 +1,367 @@
+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.loadURIString(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
+ name: "http://example.com",
+ 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"
+ );
+ });
+}
+
+XPCOMUtils.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"),
+ });
+}