diff options
Diffstat (limited to '')
18 files changed, 4019 insertions, 0 deletions
diff --git a/browser/base/content/test/popupNotifications/browser.toml b/browser/base/content/test/popupNotifications/browser.toml new file mode 100644 index 0000000000..6a7ff4e14e --- /dev/null +++ b/browser/base/content/test/popupNotifications/browser.toml @@ -0,0 +1,98 @@ +[DEFAULT] +support-files = ["head.js"] + +["browser_displayURI.js"] +skip-if = [ + "os == 'linux' && debug", + "os == 'linux' && asan", +] + +["browser_popupNotification.js"] +skip-if = [ + "os == 'linux' && debug", + "os == 'linux' && asan", +] + +["browser_popupNotification_2.js"] +https_first_disabled = true +skip-if = [ + "os == 'linux' && debug", # bug 1251135 + "os == 'linux' && asan", # bug 1251135 + "os == 'linux' && bits == 64 && os_version == '18.04'", # bug 1251135 +] + +["browser_popupNotification_3.js"] +https_first_disabled = true +skip-if = [ + "os == 'linux' && debug", + "os == 'linux' && asan", + "verify", +] + +["browser_popupNotification_4.js"] +skip-if = [ + "os == 'linux' && debug", + "os == 'linux' && asan", +] + +["browser_popupNotification_5.js"] +skip-if = ["true"] # bug 1332646 + +["browser_popupNotification_accesskey.js"] +skip-if = [ + "os == 'linux' && debug", + "os == 'linux' && asan", + "os == 'mac'", +] + +["browser_popupNotification_checkbox.js"] +skip-if = [ + "os == 'linux' && debug", + "os == 'linux' && asan", + "a11y_checks", # Bug 1858037 to investigate intermittent a11y_checks results (fails on Try, passes on Autoland) +] + +["browser_popupNotification_hide_after_identity_panel.js"] +skip-if = [ + "os == 'linux' && debug", + "os == 'linux' && asan", +] + +["browser_popupNotification_hide_after_protections_panel.js"] +skip-if = [ + "os == 'linux' && debug", + "os == 'linux' && asan", +] + +["browser_popupNotification_keyboard.js"] +skip-if = [ + "os == 'linux' && debug", + "os == 'linux' && asan", +] + +["browser_popupNotification_learnmore.js"] +skip-if = [ + "os == 'linux' && debug", + "os == 'linux' && asan", +] + +["browser_popupNotification_no_anchors.js"] +https_first_disabled = true +skip-if = [ + "os == 'linux' && debug", + "os == 'linux' && asan", +] + +["browser_popupNotification_security_delay.js"] + +["browser_popupNotification_selection_required.js"] +skip-if = [ + "os == 'linux' && debug", + "os == 'linux' && asan", +] + +["browser_reshow_in_background.js"] +skip-if = [ + "os == 'linux' && debug", + "os == 'linux' && 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..d25e343452 --- /dev/null +++ b/browser/base/content/test/popupNotifications/browser_displayURI.js @@ -0,0 +1,149 @@ +/* + * 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"); + ok( + body.innerHTML.includes("local file"), + `file:// URIs should be displayed as local file.` + ); + } + ); + + 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..1b7626c660 --- /dev/null +++ b/browser/base/content/test/popupNotifications/browser_popupNotification_3.js @@ -0,0 +1,378 @@ +/* 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 + ); + Assert.notEqual( + 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..0ec5de0c3a --- /dev/null +++ b/browser/base/content/test/popupNotifications/browser_popupNotification_5.js @@ -0,0 +1,502 @@ +/* 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" + ); + Assert.greater( + 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..4a9276512c --- /dev/null +++ b/browser/base/content/test/popupNotifications/browser_popupNotification_security_delay.js @@ -0,0 +1,402 @@ +/* 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"); + + // Wait for a tick of the event loop to ensure the button we're clicking + // has been slotted into moz-button-group + await new Promise(resolve => setTimeout(resolve, 0)); + + 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." + ); +}); + +/** + * Tests that the security delay gets reset when a window is repositioned and + * the PopupNotifications panel position is updated. + */ +add_task(async function test_notificationWindowMove() { + await ensureSecurityDelayReady(); + + info("Open a notification."); + let popupShownPromise = waitForNotificationPanel(); + showNotification(); + await popupShownPromise; + ok( + PopupNotifications.isPanelOpen, + "PopupNotification should be open after show call." + ); + + // Test that the initial security delay works. + 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."); + let 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; + } + + info("Wait for security delay to expire."); + await new Promise(resolve => + // eslint-disable-next-line mozilla/no-arbitrary-setTimeout + setTimeout(resolve, TEST_SECURITY_DELAY + 500) + ); + + info("Reposition the window"); + // Remember original window position. + let { screenX, screenY } = window; + + let promisePopupPositioned = BrowserTestUtils.waitForEvent( + PopupNotifications.panel, + "popuppositioned" + ); + + // Move the window. + window.moveTo(200, 200); + + // Wait for the panel to reposition and the PopupNotifications listener to run. + await promisePopupPositioned; + await new Promise(resolve => setTimeout(resolve, 0)); + + 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"); + let notificationHiddenPromise = waitForNotificationPanelHidden(); + triggerMainCommand(PopupNotifications.panel); + + info("Wait for panel to be hidden."); + await notificationHiddenPromise; + + ok( + !PopupNotifications.getNotification("foo", gBrowser.selectedBrowser), + "Should not longer see the notification." + ); + + // Reset window position + window.moveTo(screenX, screenY); +}); 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..8dd7e65de0 --- /dev/null +++ b/browser/base/content/test/popupNotifications/browser_reshow_in_background.js @@ -0,0 +1,70 @@ +"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. + let tabB = BrowserTestUtils.addTab(gBrowser, "https://example.com/"); + await BrowserTestUtils.browserLoaded(tabB.linkedBrowser); + + let tabC = BrowserTestUtils.addTab(gBrowser, "https://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", + "", + "geo-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..57c393e2a2 --- /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.startLoadingURIString(browser, url); + } + + return BrowserTestUtils.browserLoaded(browser, false, url); +} + +// Tests that call setup() should have a `tests` array defined for the actual +// tests to be run. +/* global tests */ +function setup() { + // eslint-disable-next-line @microsoft/sdl/no-insecure-url + BrowserTestUtils.openNewForegroundTab(gBrowser, "http://example.com/").then( + goNext + ); + registerCleanupFunction(() => { + gBrowser.removeTab(gBrowser.selectedTab); + }); +} + +function goNext() { + executeSoon(() => executeSoon(runNextTest)); +} + +async function runNextTest() { + if (!tests.length) { + executeSoon(finish); + return; + } + + let nextTest = tests.shift(); + if (nextTest.onShown) { + let shownState = false; + onPopupEvent("popupshowing", function () { + info("[" + nextTest.id + "] popup showing"); + }); + onPopupEvent("popupshown", function () { + shownState = true; + info("[" + nextTest.id + "] popup shown"); + (nextTest.onShown(this) || Promise.resolve()).then(undefined, ex => + Assert.ok(false, "onShown failed: " + ex) + ); + }); + onPopupEvent( + "popuphidden", + function () { + info("[" + nextTest.id + "] popup hidden"); + (nextTest.onHidden(this) || Promise.resolve()).then( + () => goNext(), + ex => Assert.ok(false, "onHidden failed: " + ex) + ); + }, + () => shownState + ); + info( + "[" + + nextTest.id + + "] added listeners; panel is open: " + + PopupNotifications.isPanelOpen + ); + } + + info("[" + nextTest.id + "] running test"); + await nextTest.run(); +} + +function showNotification(notifyObj) { + info("Showing notification " + notifyObj.id); + return PopupNotifications.show( + notifyObj.browser, + notifyObj.id, + notifyObj.message, + notifyObj.anchorID, + notifyObj.mainAction, + notifyObj.secondaryActions, + notifyObj.options + ); +} + +function dismissNotification(popup) { + info("Dismissing notification " + popup.childNodes[0].id); + executeSoon(() => EventUtils.synthesizeKey("KEY_Escape")); +} + +function BasicNotification(testId) { + this.browser = gBrowser.selectedBrowser; + this.id = "test-notification-" + testId; + this.message = testId + ": Will you allow <> to perform this action?"; + this.anchorID = null; + this.mainAction = { + label: "Main Action", + accessKey: "M", + callback: ({ source }) => { + this.mainActionClicked = true; + this.mainActionSource = source; + }, + }; + this.secondaryActions = [ + { + label: "Secondary Action", + accessKey: "S", + callback: ({ source }) => { + this.secondaryActionClicked = true; + this.secondaryActionSource = source; + }, + }, + ]; + this.options = { + // eslint-disable-next-line @microsoft/sdl/no-insecure-url + 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" + ); + }); +} + +ChromeUtils.defineLazyGetter(this, "gActiveListeners", () => { + let listeners = new Map(); + registerCleanupFunction(() => { + for (let [listener, eventName] of listeners) { + PopupNotifications.panel.removeEventListener(eventName, listener); + } + }); + return listeners; +}); + +function onPopupEvent(eventName, callback, condition) { + let listener = event => { + if ( + event.target != PopupNotifications.panel || + (condition && !condition()) + ) { + return; + } + PopupNotifications.panel.removeEventListener(eventName, listener); + gActiveListeners.delete(listener); + executeSoon(() => callback.call(PopupNotifications.panel)); + }; + gActiveListeners.set(listener, eventName); + PopupNotifications.panel.addEventListener(eventName, listener); +} + +function waitForNotificationPanel() { + return new Promise(resolve => { + onPopupEvent("popupshown", function () { + resolve(this); + }); + }); +} + +function waitForNotificationPanelHidden() { + return new Promise(resolve => { + onPopupEvent("popuphidden", function () { + resolve(this); + }); + }); +} + +function triggerMainCommand(popup) { + let notifications = popup.childNodes; + ok(!!notifications.length, "at least one notification displayed"); + let notification = notifications[0]; + info("Triggering main command for notification " + notification.id); + EventUtils.synthesizeMouseAtCenter(notification.button, {}); +} + +function triggerSecondaryCommand(popup, index) { + let notifications = popup.childNodes; + ok(!!notifications.length, "at least one notification displayed"); + let notification = notifications[0]; + info("Triggering secondary command for notification " + notification.id); + + if (index == 0) { + EventUtils.synthesizeMouseAtCenter(notification.secondaryButton, {}); + return; + } + + // Extra secondary actions appear in a menu. + notification.secondaryButton.nextElementSibling.focus(); + + popup.addEventListener( + "popupshown", + function () { + info("Command popup open for notification " + notification.id); + // Press down until the desired command is selected. Decrease index by one + // since the secondary action was handled above. + for (let i = 0; i <= index - 1; i++) { + EventUtils.synthesizeKey("KEY_ArrowDown"); + } + // Activate + EventUtils.synthesizeKey("KEY_Enter"); + }, + { once: true } + ); + + // One down event to open the popup + info( + "Open the popup to trigger secondary command for notification " + + notification.id + ); + EventUtils.synthesizeKey("KEY_ArrowDown", { + altKey: !navigator.platform.includes("Mac"), + }); +} |