Bug 1359733 - Move menu notification state to jsm r=Gijs

Right now, app menu doorhangers/badges have their state managed
directly inside panelUI.js. This is problematic because these
doorhangers and badges usually have to do with Firefox itself,
and not the specific window that's showing them. Accordingly, the
simplest solution was to move panelUI.js's notification state out
into a jsm file, which will fire notifications that all panelUI
instances can listen to.

MozReview-Commit-ID: 7b8w1WsQ29p

--HG--
extra : rebase_source : 23575df8176b862ec0e6a039173b105c45c76de9
This commit is contained in:
Doug Thayer 2017-05-18 13:22:27 -07:00
parent 4202c4feab
commit 646edb5fab
7 changed files with 408 additions and 321 deletions

View File

@ -10,6 +10,8 @@ XPCOMUtils.defineLazyModuleGetter(this, "ShortcutUtils",
"resource://gre/modules/ShortcutUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "AppConstants",
"resource://gre/modules/AppConstants.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "AppMenuNotifications",
"resource://gre/modules/AppMenuNotifications.jsm");
XPCOMUtils.defineLazyPreferenceGetter(this, "gPhotonStructure",
"browser.photon.structure.enabled", false);
@ -46,17 +48,17 @@ const PanelUI = {
},
_initialized: false,
_notifications: null,
init() {
this._initElements();
this.notifications = [];
this.menuButton.addEventListener("mousedown", this);
this.menuButton.addEventListener("keypress", this);
this._overlayScrollListenerBoundFn = this._overlayScrollListener.bind(this);
Services.obs.addObserver(this, "fullscreen-nav-toolbox");
Services.obs.addObserver(this, "panelUI-notification-main-action");
Services.obs.addObserver(this, "panelUI-notification-dismissed");
Services.obs.addObserver(this, "appMenu-notifications");
window.addEventListener("fullscreen", this);
window.addEventListener("activate", this);
@ -68,6 +70,7 @@ const PanelUI = {
}
this._initPhotonPanel();
Services.obs.notifyObservers(null, "appMenu-notifications-request", "refresh");
this._initialized = true;
},
@ -142,8 +145,7 @@ const PanelUI = {
}
Services.obs.removeObserver(this, "fullscreen-nav-toolbox");
Services.obs.removeObserver(this, "panelUI-notification-main-action");
Services.obs.removeObserver(this, "panelUI-notification-dismissed");
Services.obs.removeObserver(this, "appMenu-notifications");
window.removeEventListener("fullscreen", this);
window.removeEventListener("activate", this);
@ -232,69 +234,6 @@ const PanelUI = {
});
},
showNotification(id, mainAction, secondaryActions = [], options = {}) {
let notification = new PanelUINotification(id, mainAction, secondaryActions, options);
let existingIndex = this.notifications.findIndex(n => n.id == id);
if (existingIndex != -1) {
this.notifications.splice(existingIndex, 1);
}
// We don't want to clobber doorhanger notifications just to show a badge,
// so don't dismiss any of them and the badge will show once the doorhanger
// gets resolved.
if (!options.badgeOnly && !options.dismissed) {
this.notifications.forEach(n => { n.dismissed = true; });
}
// Since notifications are generally somewhat pressing, the ideal case is that
// we never have two notifications at once. However, in the event that we do,
// it's more likely that the older notification has been sitting around for a
// bit, and so we don't want to hide the new notification behind it. Thus,
// we want our notifications to behave like a stack instead of a queue.
this.notifications.unshift(notification);
this._updateNotifications();
return notification;
},
showBadgeOnlyNotification(id) {
return this.showNotification(id, null, null, { badgeOnly: true });
},
removeNotification(id) {
let notifications;
if (typeof id == "string") {
notifications = this.notifications.filter(n => n.id == id);
} else {
// If it's not a string, assume RegExp
notifications = this.notifications.filter(n => id.test(n.id));
}
// _updateNotifications can be expensive if it forces attachment of XBL
// bindings that haven't been used yet, so return early if we haven't found
// any notification to remove, as callers may expect this removeNotification
// method to be a no-op for non-existent notifications.
if (!notifications.length) {
return;
}
notifications.forEach(n => {
this._removeNotification(n);
});
this._updateNotifications();
},
dismissNotification(id) {
let notifications;
if (typeof id == "string") {
notifications = this.notifications.filter(n => n.id == id);
} else {
// If it's not a string, assume RegExp
notifications = this.notifications.filter(n => id.test(n.id));
}
notifications.forEach(n => n.dismissed = true);
this._updateNotifications();
},
/**
* If the menu panel is being shown, hide it.
*/
@ -309,17 +248,17 @@ const PanelUI = {
observe(subject, topic, status) {
switch (topic) {
case "fullscreen-nav-toolbox":
this._updateNotifications();
break;
case "panelUI-notification-main-action":
if (subject != window) {
this.removeNotification(status);
if (this._notifications) {
this._updateNotifications(false);
}
break;
case "panelUI-notification-dismissed":
if (subject != window) {
this.dismissNotification(status);
case "appMenu-notifications":
// Don't initialize twice.
if (status == "init" && this._notifications) {
break;
}
this._notifications = AppMenuNotifications.notifications;
this._updateNotifications(true);
break;
}
},
@ -376,16 +315,6 @@ const PanelUI = {
return panelState == "showing" || panelState == "open";
},
get activeNotification() {
if (this.notifications.length > 0) {
const doorhanger =
this.notifications.find(n => !n.dismissed && !n.options.badgeOnly);
return doorhanger || this.notifications[0];
}
return null;
},
/**
* Registering the menu panel is done lazily for performance reasons. This
* method is exposed so that CustomizationMode can force panel-readyness in the
@ -749,10 +678,13 @@ const PanelUI = {
}
},
_updateNotifications() {
if (!this.notifications.length) {
this._clearAllNotifications();
this._hidePopup();
_updateNotifications(notificationsChanged) {
let notifications = this._notifications;
if (!notifications || !notifications.length) {
if (notificationsChanged) {
this._clearAllNotifications();
this._hidePopup();
}
return;
}
@ -762,7 +694,7 @@ const PanelUI = {
}
let doorhangers =
this.notifications.filter(n => !n.dismissed && !n.options.badgeOnly);
notifications.filter(n => !n.dismissed && !n.options.badgeOnly);
if (this.panel.state == "showing" || this.panel.state == "open") {
// If the menu is already showing, then we need to dismiss all notifications
@ -770,8 +702,8 @@ const PanelUI = {
doorhangers.forEach(n => { n.dismissed = true; })
this._hidePopup();
this._clearBadge();
if (!this.notifications[0].options.badgeOnly) {
this._showBannerItem(this.notifications[0]);
if (!notifications[0].options.badgeOnly) {
this._showBannerItem(notifications[0]);
}
} else if (doorhangers.length > 0) {
// Only show the doorhanger if the window is focused and not fullscreen
@ -785,8 +717,8 @@ const PanelUI = {
}
} else {
this._hidePopup();
this._showBadge(this.notifications[0]);
this._showBannerItem(this.notifications[0]);
this._showBadge(notifications[0]);
this._showBannerItem(notifications[0]);
}
},
@ -864,21 +796,6 @@ const PanelUI = {
}
},
_removeNotification(notification) {
// This notification may already be removed, in which case let's just fail
// silently.
let notifications = this.notifications;
if (!notifications)
return;
var index = notifications.indexOf(notification);
if (index == -1)
return;
// Remove the notification
notifications.splice(index, 1);
},
_onNotificationButtonEvent(event, type) {
let notificationEl = getNotificationFromElement(event.originalTarget);
@ -890,35 +807,11 @@ const PanelUI = {
let notification = notificationEl.notification;
let action = notification.mainAction;
if (type == "secondarybuttoncommand") {
action = notification.secondaryActions[0];
}
let dismiss = true;
if (action) {
try {
if (action === notification.mainAction) {
action.callback(true);
this._notify(notification.id, "main-action");
} else {
action.callback();
}
} catch (error) {
Cu.reportError(error);
}
dismiss = action.dismiss;
}
if (dismiss) {
notification.dismissed = true;
this._notify(notification.id, "dismissed");
AppMenuNotifications.callSecondaryAction(window, notification);
} else {
this._removeNotification(notification);
AppMenuNotifications.callMainAction(window, notification, true);
}
this._updateNotifications();
},
_onBannerItemSelected(event) {
@ -927,16 +820,7 @@ const PanelUI = {
throw "menucommand target has no associated action/notification";
event.stopPropagation();
try {
target.notification.mainAction.callback(false);
this._notify(target.notification.id, "main-action");
} catch (error) {
Cu.reportError(error);
}
this._removeNotification(target.notification);
this._updateNotifications();
AppMenuNotifications.callMainAction(window, target.notification, false);
},
_getPopupId(notification) { return "appMenu-" + notification.id + "-notification"; },
@ -965,10 +849,6 @@ const PanelUI = {
button.setAttribute("shortcut", ShortcutUtils.prettifyShortcut(key));
}
},
_notify(status, topic) {
Services.obs.notifyObservers(window, "panelUI-notification-" + topic, status);
}
};
XPCOMUtils.defineConstant(this, "PanelUI", PanelUI);
@ -981,14 +861,6 @@ function getLocale() {
return Services.locale.getAppLocaleAsLangTag();
}
function PanelUINotification(id, mainAction, secondaryActions = [], options = {}) {
this.id = id;
this.mainAction = mainAction;
this.secondaryActions = secondaryActions;
this.options = options;
this.dismissed = this.options.dismissed || false;
}
function getNotificationFromElement(aElement) {
// Need to find the associated notification object, which is a bit tricky
// since it isn't associated with the element directly - this is kind of

View File

@ -151,6 +151,8 @@ skip-if = os == "mac"
[browser_overflow_use_subviews.js]
[browser_panel_toggle.js]
[browser_panelUINotifications.js]
[browser_panelUINotifications_fullscreen.js]
[browser_panelUINotifications_multiWindow.js]
[browser_switch_to_customize_mode.js]
[browser_synced_tabs_menu.js]
[browser_check_tooltips_in_navbar.js]

View File

@ -1,5 +1,7 @@
"use strict";
Cu.import("resource://gre/modules/AppMenuNotifications.jsm");
/**
* Tests that when we click on the main call-to-action of the doorhanger, the provided
* action is called, and the doorhanger removed.
@ -18,7 +20,7 @@ add_task(async function testMainActionCalled() {
let mainAction = {
callback: () => { mainActionCalled = true; }
};
PanelUI.showNotification("update-manual", mainAction);
AppMenuNotifications.showNotification("update-manual", mainAction);
isnot(PanelUI.notificationPanel.state, "closed", "update-manual doorhanger is showing.");
let notifications = [...PanelUI.notificationPanel.children].filter(n => !n.hidden);
@ -35,108 +37,6 @@ add_task(async function testMainActionCalled() {
});
});
/**
* Tests that when we try to show a notification in a background window, it
* does not display until the window comes back into the foreground. However,
* it should display a badge.
*/
add_task(async function testDoesNotShowDoorhangerForBackgroundWindow() {
let options = {
gBrowser: window.gBrowser,
url: "about:blank"
};
await BrowserTestUtils.withNewTab(options, async function(browser) {
let doc = browser.ownerDocument;
let win = await BrowserTestUtils.openNewBrowserWindow();
let mainActionCalled = false;
let mainAction = {
callback: () => { mainActionCalled = true; }
};
PanelUI.showNotification("update-manual", mainAction);
is(PanelUI.notificationPanel.state, "closed", "The background window's doorhanger is closed.");
is(PanelUI.menuButton.hasAttribute("badge-status"), true, "The background window has a badge.");
await BrowserTestUtils.closeWindow(win);
await SimpleTest.promiseFocus(window);
isnot(PanelUI.notificationPanel.state, "closed", "update-manual doorhanger is showing.");
let notifications = [...PanelUI.notificationPanel.children].filter(n => !n.hidden);
is(notifications.length, 1, "PanelUI doorhanger is only displaying one notification.");
let doorhanger = notifications[0];
is(doorhanger.id, "appMenu-update-manual-notification", "PanelUI is displaying the update-manual notification.");
let button = doc.getAnonymousElementByAttribute(doorhanger, "anonid", "button");
button.click();
ok(mainActionCalled, "Main action callback was called");
is(PanelUI.notificationPanel.state, "closed", "update-manual doorhanger is closed.");
is(PanelUI.menuButton.hasAttribute("badge-status"), false, "Should not have a badge status");
});
});
/**
* Tests that when we try to show a notification in a background window and in
* a foreground window, if the foreground window's main action is called, the
* background window's doorhanger will be removed.
*/
add_task(async function testBackgroundWindowNotificationsAreRemovedByForeground() {
let options = {
gBrowser: window.gBrowser,
url: "about:blank"
};
await BrowserTestUtils.withNewTab(options, async function(browser) {
let win = await BrowserTestUtils.openNewBrowserWindow();
PanelUI.showNotification("update-manual", {callback() {}});
win.PanelUI.showNotification("update-manual", {callback() {}});
let doc = win.gBrowser.ownerDocument;
let notifications = [...win.PanelUI.notificationPanel.children].filter(n => !n.hidden);
let doorhanger = notifications[0];
let button = doc.getAnonymousElementByAttribute(doorhanger, "anonid", "button");
button.click();
await BrowserTestUtils.closeWindow(win);
await SimpleTest.promiseFocus(window);
is(PanelUI.notificationPanel.state, "closed", "update-manual doorhanger is closed.");
is(PanelUI.menuButton.hasAttribute("badge-status"), false, "Should not have a badge status");
});
});
/**
* Tests that when we try to show a notification in a background window and in
* a foreground window, if the foreground window's doorhanger is dismissed,
* the background window's doorhanger will also be dismissed once the window
* regains focus.
*/
add_task(async function testBackgroundWindowNotificationsAreDismissedByForeground() {
let options = {
gBrowser: window.gBrowser,
url: "about:blank"
};
await BrowserTestUtils.withNewTab(options, async function(browser) {
let win = await BrowserTestUtils.openNewBrowserWindow();
PanelUI.showNotification("update-manual", {callback() {}});
win.PanelUI.showNotification("update-manual", {callback() {}});
let doc = win.gBrowser.ownerDocument;
let notifications = [...win.PanelUI.notificationPanel.children].filter(n => !n.hidden);
let doorhanger = notifications[0];
let button = doc.getAnonymousElementByAttribute(doorhanger, "anonid", "secondarybutton");
button.click();
await BrowserTestUtils.closeWindow(win);
await SimpleTest.promiseFocus(window);
is(PanelUI.notificationPanel.state, "closed", "The background window's doorhanger is closed.");
is(PanelUI.menuButton.hasAttribute("badge-status"), true,
"The dismissed notification should still have a badge status");
PanelUI.removeNotification(/.*/);
});
});
/**
* This tests that when we click the secondary action for a notification,
* it will display the badge for that notification on the PanelUI menu button.
@ -158,7 +58,7 @@ add_task(async function testSecondaryActionWorkflow() {
let mainAction = {
callback: () => { mainActionCalled = true; },
};
PanelUI.showNotification("update-manual", mainAction);
AppMenuNotifications.showNotification("update-manual", mainAction);
isnot(PanelUI.notificationPanel.state, "closed", "update-manual doorhanger is showing.");
let notifications = [...PanelUI.notificationPanel.children].filter(n => !n.hidden);
@ -186,7 +86,7 @@ add_task(async function testSecondaryActionWorkflow() {
menuItem.click();
ok(mainActionCalled, "Main action callback was called");
PanelUI.removeNotification(/.*/);
AppMenuNotifications.removeNotification(/.*/);
});
});
@ -200,7 +100,7 @@ add_task(async function testInteractionWithBadges() {
await BrowserTestUtils.withNewTab("about:blank", async function(browser) {
let doc = browser.ownerDocument;
PanelUI.showBadgeOnlyNotification("fxa-needs-authentication");
AppMenuNotifications.showBadgeOnlyNotification("fxa-needs-authentication");
is(PanelUI.menuButton.getAttribute("badge-status"), "fxa-needs-authentication", "Fxa badge is shown on PanelUI button.");
is(PanelUI.notificationPanel.state, "closed", "update-manual doorhanger is closed.");
@ -208,7 +108,7 @@ add_task(async function testInteractionWithBadges() {
let mainAction = {
callback: () => { mainActionCalled = true; },
};
PanelUI.showNotification("update-manual", mainAction);
AppMenuNotifications.showNotification("update-manual", mainAction);
isnot(PanelUI.menuButton.getAttribute("badge-status"), "fxa-needs-authentication", "Fxa badge is hidden on PanelUI button.");
isnot(PanelUI.notificationPanel.state, "closed", "update-manual doorhanger is showing.");
@ -234,7 +134,7 @@ add_task(async function testInteractionWithBadges() {
ok(mainActionCalled, "Main action callback was called");
is(PanelUI.menuButton.getAttribute("badge-status"), "fxa-needs-authentication", "Fxa badge is shown on PanelUI button.");
PanelUI.removeNotification(/.*/);
AppMenuNotifications.removeNotification(/.*/);
is(PanelUI.menuButton.hasAttribute("badge-status"), false, "Should not have a badge status");
});
});
@ -251,8 +151,8 @@ add_task(async function testAddingBadgeWhileDoorhangerIsShowing() {
let mainAction = {
callback: () => { mainActionCalled = true; }
};
PanelUI.showNotification("update-manual", mainAction);
PanelUI.showBadgeOnlyNotification("fxa-needs-authentication");
AppMenuNotifications.showNotification("update-manual", mainAction);
AppMenuNotifications.showBadgeOnlyNotification("fxa-needs-authentication");
isnot(PanelUI.menuButton.getAttribute("badge-status"), "fxa-needs-authentication", "Fxa badge is hidden on PanelUI button.");
isnot(PanelUI.notificationPanel.state, "closed", "update-manual doorhanger is showing.");
@ -267,7 +167,7 @@ add_task(async function testAddingBadgeWhileDoorhangerIsShowing() {
ok(mainActionCalled, "Main action callback was called");
is(PanelUI.notificationPanel.state, "closed", "update-manual doorhanger is closed.");
is(PanelUI.menuButton.getAttribute("badge-status"), "fxa-needs-authentication", "Fxa badge is shown on PanelUI button.");
PanelUI.removeNotification(/.*/);
AppMenuNotifications.removeNotification(/.*/);
is(PanelUI.menuButton.hasAttribute("badge-status"), false, "Should not have a badge status");
});
});
@ -283,37 +183,37 @@ add_task(async function testMultipleBadges() {
is(menuButton.hasAttribute("badge-status"), false, "Should not have a badge status");
is(menuButton.hasAttribute("badge"), false, "Should not have the badge attribute set");
PanelUI.showBadgeOnlyNotification("fxa-needs-authentication");
AppMenuNotifications.showBadgeOnlyNotification("fxa-needs-authentication");
is(menuButton.getAttribute("badge-status"), "fxa-needs-authentication", "Should have fxa-needs-authentication badge status");
PanelUI.showBadgeOnlyNotification("update-succeeded");
AppMenuNotifications.showBadgeOnlyNotification("update-succeeded");
is(menuButton.getAttribute("badge-status"), "update-succeeded", "Should have update-succeeded badge status (update > fxa)");
PanelUI.showBadgeOnlyNotification("update-failed");
AppMenuNotifications.showBadgeOnlyNotification("update-failed");
is(menuButton.getAttribute("badge-status"), "update-failed", "Should have update-failed badge status");
PanelUI.showBadgeOnlyNotification("download-severe");
AppMenuNotifications.showBadgeOnlyNotification("download-severe");
is(menuButton.getAttribute("badge-status"), "download-severe", "Should have download-severe badge status");
PanelUI.showBadgeOnlyNotification("download-warning");
AppMenuNotifications.showBadgeOnlyNotification("download-warning");
is(menuButton.getAttribute("badge-status"), "download-warning", "Should have download-warning badge status");
PanelUI.removeNotification(/^download-/);
AppMenuNotifications.removeNotification(/^download-/);
is(menuButton.getAttribute("badge-status"), "update-failed", "Should have update-failed badge status");
PanelUI.removeNotification(/^update-/);
AppMenuNotifications.removeNotification(/^update-/);
is(menuButton.getAttribute("badge-status"), "fxa-needs-authentication", "Should have fxa-needs-authentication badge status");
PanelUI.removeNotification(/^fxa-/);
AppMenuNotifications.removeNotification(/^fxa-/);
is(menuButton.hasAttribute("badge-status"), false, "Should not have a badge status");
await PanelUI.show();
is(menuButton.hasAttribute("badge-status"), false, "Should not have a badge status (Hamburger menu opened)");
PanelUI.hide();
PanelUI.showBadgeOnlyNotification("fxa-needs-authentication");
PanelUI.showBadgeOnlyNotification("update-succeeded");
PanelUI.removeNotification(/.*/);
AppMenuNotifications.showBadgeOnlyNotification("fxa-needs-authentication");
AppMenuNotifications.showBadgeOnlyNotification("update-succeeded");
AppMenuNotifications.removeNotification(/.*/);
is(menuButton.hasAttribute("badge-status"), false, "Should not have a badge status");
});
});
@ -336,7 +236,7 @@ add_task(async function testMultipleNonBadges() {
callback: () => { updateRestartAction.called = true; },
};
PanelUI.showNotification("update-manual", updateManualAction);
AppMenuNotifications.showNotification("update-manual", updateManualAction);
let notifications;
let doorhanger;
@ -347,7 +247,7 @@ add_task(async function testMultipleNonBadges() {
doorhanger = notifications[0];
is(doorhanger.id, "appMenu-update-manual-notification", "PanelUI is displaying the update-manual notification.");
PanelUI.showNotification("update-restart", updateRestartAction);
AppMenuNotifications.showNotification("update-restart", updateRestartAction);
isnot(PanelUI.notificationPanel.state, "closed", "Doorhanger is showing.");
notifications = [...PanelUI.notificationPanel.children].filter(n => !n.hidden);
@ -382,42 +282,3 @@ add_task(async function testMultipleNonBadges() {
ok(updateManualAction.called, "update-manual main action callback was called");
});
});
add_task(async function testFullscreen() {
let doc = document;
is(PanelUI.notificationPanel.state, "closed", "update-manual doorhanger is closed.");
let mainActionCalled = false;
let mainAction = {
callback: () => { mainActionCalled = true; }
};
PanelUI.showNotification("update-manual", mainAction);
isnot(PanelUI.notificationPanel.state, "closed", "update-manual doorhanger is showing.");
let notifications = [...PanelUI.notificationPanel.children].filter(n => !n.hidden);
is(notifications.length, 1, "PanelUI doorhanger is only displaying one notification.");
let doorhanger = notifications[0];
is(doorhanger.id, "appMenu-update-manual-notification", "PanelUI is displaying the update-manual notification.");
let popuphiddenPromise = BrowserTestUtils.waitForEvent(PanelUI.notificationPanel, "popuphidden");
EventUtils.synthesizeKey("VK_F11", {});
await popuphiddenPromise;
await new Promise(executeSoon);
is(PanelUI.notificationPanel.state, "closed", "update-manual doorhanger is closed.");
window.FullScreen.showNavToolbox();
is(PanelUI.menuButton.getAttribute("badge-status"), "update-manual", "Badge is displaying on PanelUI button.");
let popupshownPromise = BrowserTestUtils.waitForEvent(PanelUI.notificationPanel, "popupshown");
EventUtils.synthesizeKey("VK_F11", {});
await popupshownPromise;
await new Promise(executeSoon);
isnot(PanelUI.notificationPanel.state, "closed", "update-manual doorhanger is showing.");
isnot(PanelUI.menuButton.getAttribute("badge-status"), "update-manual", "Badge is not displaying on PanelUI button.");
let mainActionButton = doc.getAnonymousElementByAttribute(doorhanger, "anonid", "button");
mainActionButton.click();
ok(mainActionCalled, "Main action callback was called");
is(PanelUI.notificationPanel.state, "closed", "update-manual doorhanger is closed.");
is(PanelUI.menuButton.hasAttribute("badge-status"), false, "Should not have a badge status");
});

View File

@ -0,0 +1,42 @@
"use strict";
Cu.import("resource://gre/modules/AppMenuNotifications.jsm");
add_task(async function testFullscreen() {
let doc = document;
is(PanelUI.notificationPanel.state, "closed", "update-manual doorhanger is closed.");
let mainActionCalled = false;
let mainAction = {
callback: () => { mainActionCalled = true; }
};
AppMenuNotifications.showNotification("update-manual", mainAction);
isnot(PanelUI.notificationPanel.state, "closed", "update-manual doorhanger is showing.");
let notifications = [...PanelUI.notificationPanel.children].filter(n => !n.hidden);
is(notifications.length, 1, "PanelUI doorhanger is only displaying one notification.");
let doorhanger = notifications[0];
is(doorhanger.id, "appMenu-update-manual-notification", "PanelUI is displaying the update-manual notification.");
let popuphiddenPromise = BrowserTestUtils.waitForEvent(PanelUI.notificationPanel, "popuphidden");
EventUtils.synthesizeKey("VK_F11", {});
await popuphiddenPromise;
await new Promise(executeSoon);
is(PanelUI.notificationPanel.state, "closed", "update-manual doorhanger is closed.");
FullScreen.showNavToolbox();
is(PanelUI.menuButton.getAttribute("badge-status"), "update-manual", "Badge is displaying on PanelUI button.");
let popupshownPromise = BrowserTestUtils.waitForEvent(PanelUI.notificationPanel, "popupshown");
EventUtils.synthesizeKey("VK_F11", {});
await popupshownPromise;
await new Promise(executeSoon);
isnot(PanelUI.notificationPanel.state, "closed", "update-manual doorhanger is showing.");
isnot(PanelUI.menuButton.getAttribute("badge-status"), "update-manual", "Badge is not displaying on PanelUI button.");
let mainActionButton = doc.getAnonymousElementByAttribute(doorhanger, "anonid", "button");
mainActionButton.click();
ok(mainActionCalled, "Main action callback was called");
is(PanelUI.notificationPanel.state, "closed", "update-manual doorhanger is closed.");
is(PanelUI.menuButton.hasAttribute("badge-status"), false, "Should not have a badge status");
});

View File

@ -0,0 +1,130 @@
"use strict";
Cu.import("resource://gre/modules/AppMenuNotifications.jsm");
/**
* Tests that when we try to show a notification in a background window, it
* does not display until the window comes back into the foreground. However,
* it should display a badge.
*/
add_task(async function testDoesNotShowDoorhangerForBackgroundWindow() {
let options = {
gBrowser: window.gBrowser,
url: "about:blank"
};
await BrowserTestUtils.withNewTab(options, async function(browser) {
let doc = browser.ownerDocument;
let win = await BrowserTestUtils.openNewBrowserWindow();
let mainActionCalled = false;
let mainAction = {
callback: () => { mainActionCalled = true; }
};
AppMenuNotifications.showNotification("update-manual", mainAction);
is(PanelUI.notificationPanel.state, "closed", "The background window's doorhanger is closed.");
is(PanelUI.menuButton.hasAttribute("badge-status"), true, "The background window has a badge.");
await BrowserTestUtils.closeWindow(win);
await SimpleTest.promiseFocus(window);
isnot(PanelUI.notificationPanel.state, "closed", "update-manual doorhanger is showing.");
let notifications = [...PanelUI.notificationPanel.children].filter(n => !n.hidden);
is(notifications.length, 1, "PanelUI doorhanger is only displaying one notification.");
let doorhanger = notifications[0];
is(doorhanger.id, "appMenu-update-manual-notification", "PanelUI is displaying the update-manual notification.");
let button = doc.getAnonymousElementByAttribute(doorhanger, "anonid", "button");
button.click();
ok(mainActionCalled, "Main action callback was called");
is(PanelUI.notificationPanel.state, "closed", "update-manual doorhanger is closed.");
is(PanelUI.menuButton.hasAttribute("badge-status"), false, "Should not have a badge status");
});
});
/**
* Tests that when we try to show a notification in a background window and in
* a foreground window, if the foreground window's main action is called, the
* background window's doorhanger will be removed.
*/
add_task(async function testBackgroundWindowNotificationsAreRemovedByForeground() {
let options = {
gBrowser: window.gBrowser,
url: "about:blank"
};
await BrowserTestUtils.withNewTab(options, async function(browser) {
let win = await BrowserTestUtils.openNewBrowserWindow();
AppMenuNotifications.showNotification("update-manual", {callback() {}});
let doc = win.gBrowser.ownerDocument;
let notifications = [...win.PanelUI.notificationPanel.children].filter(n => !n.hidden);
is(notifications.length, 1, "PanelUI doorhanger is only displaying one notification.");
let doorhanger = notifications[0];
let button = doc.getAnonymousElementByAttribute(doorhanger, "anonid", "button");
button.click();
await BrowserTestUtils.closeWindow(win);
await SimpleTest.promiseFocus(window);
is(PanelUI.notificationPanel.state, "closed", "update-manual doorhanger is closed.");
is(PanelUI.menuButton.hasAttribute("badge-status"), false, "Should not have a badge status");
});
});
/**
* Tests that when we try to show a notification in a background window and in
* a foreground window, if the foreground window's doorhanger is dismissed,
* the background window's doorhanger will also be dismissed once the window
* regains focus.
*/
add_task(async function testBackgroundWindowNotificationsAreDismissedByForeground() {
let options = {
gBrowser: window.gBrowser,
url: "about:blank"
};
await BrowserTestUtils.withNewTab(options, async function(browser) {
let win = await BrowserTestUtils.openNewBrowserWindow();
AppMenuNotifications.showNotification("update-manual", {callback() {}});
let doc = win.gBrowser.ownerDocument;
let notifications = [...win.PanelUI.notificationPanel.children].filter(n => !n.hidden);
is(notifications.length, 1, "PanelUI doorhanger is only displaying one notification.");
let doorhanger = notifications[0];
let button = doc.getAnonymousElementByAttribute(doorhanger, "anonid", "secondarybutton");
button.click();
await BrowserTestUtils.closeWindow(win);
await SimpleTest.promiseFocus(window);
is(PanelUI.notificationPanel.state, "closed", "The background window's doorhanger is closed.");
is(PanelUI.menuButton.hasAttribute("badge-status"), true,
"The dismissed notification should still have a badge status");
AppMenuNotifications.removeNotification(/.*/);
});
});
/**
* Tests that when we open a new window while a notification is showing, the
* notification also shows on the new window.
*/
add_task(async function testOpenWindowAfterShowingNotification() {
AppMenuNotifications.showNotification("update-manual", {callback() {}});
let win = await BrowserTestUtils.openNewBrowserWindow();
let doc = win.gBrowser.ownerDocument;
let notifications = [...win.PanelUI.notificationPanel.children].filter(n => !n.hidden);
is(notifications.length, 1, "PanelUI doorhanger is only displaying one notification.");
let doorhanger = notifications[0];
let button = doc.getAnonymousElementByAttribute(doorhanger, "anonid", "secondarybutton");
button.click();
await BrowserTestUtils.closeWindow(win);
await SimpleTest.promiseFocus(window);
is(PanelUI.notificationPanel.state, "closed", "The background window's doorhanger is closed.");
is(PanelUI.menuButton.hasAttribute("badge-status"), true,
"The dismissed notification should still have a badge status");
AppMenuNotifications.removeNotification(/.*/);
});

View File

@ -0,0 +1,179 @@
/* 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/. */
"use strict";
this.EXPORTED_SYMBOLS = ["AppMenuNotifications"];
const {utils: Cu, classes: Cc, interfaces: Ci} = Components;
Cu.import("resource://gre/modules/Services.jsm");
function AppMenuNotification(id, mainAction, secondaryAction, options = {}) {
this.id = id;
this.mainAction = mainAction;
this.secondaryAction = secondaryAction;
this.options = options;
this.dismissed = this.options.dismissed || false;
}
var AppMenuNotifications = {
_notifications: [],
_hasInitialized: false,
get notifications() {
return Array.from(this._notifications);
},
_lazyInit() {
if (!this._hasInitialized) {
Services.obs.addObserver(this, "xpcom-shutdown");
Services.obs.addObserver(this, "appMenu-notifications-request");
}
},
uninit() {
Services.obs.removeObserver(this, "xpcom-shutdown");
Services.obs.removeObserver(this, "appMenu-notifications-request");
},
observe(subject, topic, status) {
switch (topic) {
case "xpcom-shutdown":
this.uninit();
break;
case "appMenu-notifications-request":
if (this._notifications.length != 0) {
Services.obs.notifyObservers(null, "appMenu-notifications", "init");
}
break;
}
},
get activeNotification() {
if (this._notifications.length > 0) {
const doorhanger =
this._notifications.find(n => !n.dismissed && !n.options.badgeOnly);
return doorhanger || this._notifications[0];
}
return null;
},
showNotification(id, mainAction, secondaryAction, options = {}) {
let notification = new AppMenuNotification(id, mainAction, secondaryAction, options);
let existingIndex = this._notifications.findIndex(n => n.id == id);
if (existingIndex != -1) {
this._notifications.splice(existingIndex, 1);
}
// We don't want to clobber doorhanger notifications just to show a badge,
// so don't dismiss any of them and the badge will show once the doorhanger
// gets resolved.
if (!options.badgeOnly && !options.dismissed) {
this._notifications.forEach(n => { n.dismissed = true; });
}
// Since notifications are generally somewhat pressing, the ideal case is that
// we never have two notifications at once. However, in the event that we do,
// it's more likely that the older notification has been sitting around for a
// bit, and so we don't want to hide the new notification behind it. Thus,
// we want our notifications to behave like a stack instead of a queue.
this._notifications.unshift(notification);
this._lazyInit();
this._updateNotifications();
return notification;
},
showBadgeOnlyNotification(id) {
return this.showNotification(id, null, null, { badgeOnly: true });
},
removeNotification(id) {
let notifications;
if (typeof id == "string") {
notifications = this._notifications.filter(n => n.id == id);
} else {
// If it's not a string, assume RegExp
notifications = this._notifications.filter(n => id.test(n.id));
}
// _updateNotifications can be expensive if it forces attachment of XBL
// bindings that haven't been used yet, so return early if we haven't found
// any notification to remove, as callers may expect this removeNotification
// method to be a no-op for non-existent notifications.
if (!notifications.length) {
return;
}
notifications.forEach(n => {
this._removeNotification(n);
});
this._updateNotifications();
},
dismissNotification(id) {
let notifications;
if (typeof id == "string") {
notifications = this._notifications.filter(n => n.id == id);
} else {
// If it's not a string, assume RegExp
notifications = this._notifications.filter(n => id.test(n.id));
}
notifications.forEach(n => {
n.dismissed = true;
});
this._updateNotifications();
},
callMainAction(win, notification, fromDoorhanger) {
let action = notification.mainAction;
this._callAction(win, notification, action, fromDoorhanger);
},
callSecondaryAction(win, notification) {
let action = notification.secondaryAction;
this._callAction(win, notification, action, true);
},
_callAction(win, notification, action, fromDoorhanger) {
let dismiss = true;
if (action) {
try {
action.callback(win, fromDoorhanger);
} catch (error) {
Cu.reportError(error);
}
dismiss = action.dismiss;
}
if (dismiss) {
notification.dismissed = true;
} else {
this._removeNotification(notification);
}
this._updateNotifications();
},
_removeNotification(notification) {
// This notification may already be removed, in which case let's just ignore.
let notifications = this._notifications;
if (!notifications)
return;
var index = notifications.indexOf(notification);
if (index == -1)
return;
// Remove the notification
notifications.splice(index, 1);
},
_updateNotifications() {
Services.obs.notifyObservers(null, "appMenu-notifications", "update");
},
};

View File

@ -173,6 +173,7 @@ EXTRA_JS_MODULES += [
'addons/WebRequestCommon.jsm',
'addons/WebRequestContent.js',
'addons/WebRequestUpload.jsm',
'AppMenuNotifications.jsm',
'AsyncPrefs.jsm',
'Battery.jsm',
'BinarySearch.jsm',