--- a/browser/app/profile/firefox.js
+++ b/browser/app/profile/firefox.js
@@ -110,30 +110,35 @@ pref("app.update.log", false);
// The number of general background check failures to allow before notifying the
// user of the failure. User initiated update checks always notify the user of
// the failure.
pref("app.update.backgroundMaxErrors", 10);
// Whether or not app updates are enabled
pref("app.update.enabled", true);
+// Whether or not to use the doorhanger application update UI.
+pref("app.update.doorhanger", true);
+
+// How many times we should let downloads fail before prompting the user to
+// download a fresh installer.
+pref("app.update.download.promptMaxAttempts", 2);
+
+// How many times we should let an elevation prompt fail before prompting the user to
+// download a fresh installer.
+pref("app.update.elevation.promptMaxAttempts", 2);
+
// If set to true, the Update Service will automatically download updates when
// app updates are enabled per the app.update.enabled preference and if the user
// can apply updates.
pref("app.update.auto", true);
// If set to true, the Update Service will present no UI for any event.
pref("app.update.silent", false);
-// If set to true, the hamburger button will show badges for update events.
-#ifndef RELEASE_OR_BETA
-pref("app.update.badge", true);
-#else
-pref("app.update.badge", false);
-#endif
// app.update.badgeWaitTime is in branding section
// If set to true, the Update Service will apply updates in the background
// when it finishes downloading them.
pref("app.update.staging.enabled", true);
// Update service URL:
pref("app.update.url", "https://aus5.mozilla.org/update/6/%PRODUCT%/%VERSION%/%BUILD_ID%/%BUILD_TARGET%/%LOCALE%/%CHANNEL%/%OS_VERSION%/%SYSTEM_CAPABILITIES%/%DISTRIBUTION%/%DISTRIBUTION_VERSION%/update.xml");
--- a/browser/base/content/browser-addons.js
+++ b/browser/base/content/browser-addons.js
@@ -499,20 +499,19 @@ const gExtensionsNotifications = {
}
ExtensionsUI.off("change", this.boundUpdate);
},
updateAlerts() {
let sideloaded = ExtensionsUI.sideloaded;
let updates = ExtensionsUI.updates;
if (sideloaded.size + updates.size == 0) {
- gMenuButtonBadgeManager.removeBadge(gMenuButtonBadgeManager.BADGEID_ADDONS);
+ PanelUI.removeNotification("addon-alert");
} else {
- gMenuButtonBadgeManager.addBadge(gMenuButtonBadgeManager.BADGEID_ADDONS,
- "addon-alert");
+ PanelUI.showBadgeOnlyNotification("addon-alert");
}
let container = document.getElementById("PanelUI-footer-addons");
while (container.firstChild) {
container.firstChild.remove();
}
--- a/browser/base/content/browser-fullScreenAndPointerLock.js
+++ b/browser/base/content/browser-fullScreenAndPointerLock.js
@@ -532,26 +532,31 @@ var FullScreen = {
},
_setPopupOpen(aEvent) {
// Popups should only veto chrome collapsing if they were opened when the chrome was not collapsed.
// Otherwise, they would not affect chrome and the user would expect the chrome to go away.
// e.g. we wouldn't want the autoscroll icon firing this event, so when the user
// toggles chrome when moving mouse to the top, it doesn't go away again.
if (aEvent.type == "popupshown" && !FullScreen._isChromeCollapsed &&
- aEvent.target.localName != "tooltip" && aEvent.target.localName != "window")
+ aEvent.target.localName != "tooltip" && aEvent.target.localName != "window" &&
+ aEvent.target.getAttribute("nopreventnavboxhide") != "true")
FullScreen._isPopupOpen = true;
else if (aEvent.type == "popuphidden" && aEvent.target.localName != "tooltip" &&
aEvent.target.localName != "window") {
FullScreen._isPopupOpen = false;
// Try again to hide toolbar when we close the popup.
FullScreen.hideNavToolbox(true);
}
},
+ get navToolboxHidden() {
+ return this._isChromeCollapsed;
+ },
+
// Autohide helpers for the context menu item
getAutohide(aItem) {
aItem.setAttribute("checked", gPrefService.getBoolPref("browser.fullscreen.autohide"));
},
setAutohide() {
gPrefService.setBoolPref("browser.fullscreen.autohide", !gPrefService.getBoolPref("browser.fullscreen.autohide"));
// Try again to hide toolbar when we change the pref.
FullScreen.hideNavToolbox(true);
@@ -574,16 +579,17 @@ var FullScreen = {
bottom: rect.bottom,
left: rect.left,
right: rect.right
};
MousePosTracker.addListener(this);
}
this._isChromeCollapsed = false;
+ Services.obs.notifyObservers(null, "fullscreen-nav-toolbox", "shown");
},
hideNavToolbox(aAnimate = false) {
if (this._isChromeCollapsed || !this._safeToCollapse())
return;
this._fullScrToggler.hidden = false;
@@ -597,16 +603,18 @@ var FullScreen = {
};
gNavToolbox.addEventListener("transitionend", listener, true);
this._fullScrToggler.hidden = true;
}
gNavToolbox.style.marginTop =
-gNavToolbox.getBoundingClientRect().height + "px";
this._isChromeCollapsed = true;
+ Services.obs.notifyObservers(null, "fullscreen-nav-toolbox", "hidden");
+
MousePosTracker.removeListener(this);
},
_updateToolbars(aEnterFS) {
for (let el of document.querySelectorAll("toolbar[fullscreentoolbar=true]")) {
if (aEnterFS) {
// Give the main nav bar and the tab bar the fullscreen context menu,
// otherwise remove context menu to prevent breakage
--- a/browser/base/content/browser-fxaccounts.js
+++ b/browser/base/content/browser-fxaccounts.js
@@ -212,19 +212,19 @@ var gFxAccounts = {
this.panelUIFooter.setAttribute("fxastatus", "signedin");
this.panelUILabel.setAttribute("label", userData.email);
}
if (profileInfoEnabled) {
this.panelUIFooter.setAttribute("fxaprofileimage", "enabled");
}
}
if (showErrorBadge) {
- gMenuButtonBadgeManager.addBadge(gMenuButtonBadgeManager.BADGEID_FXA, "fxa-needs-authentication");
+ PanelUI.showBadgeOnlyNotification("fxa-needs-authentication");
} else {
- gMenuButtonBadgeManager.removeBadge(gMenuButtonBadgeManager.BADGEID_FXA);
+ PanelUI.removeNotification("fxa-needs-authentication");
}
}
let updateWithProfile = (profile) => {
if (profileInfoEnabled) {
if (profile.displayName) {
this.panelUILabel.setAttribute("label", profile.displayName);
}
--- a/browser/base/content/browser.css
+++ b/browser/base/content/browser.css
@@ -1178,17 +1178,17 @@ toolbarpaletteitem[place="palette"][hidd
#customization-palette .toolbarpaletteitem-box {
-moz-box-pack: center;
-moz-box-flex: 1;
width: 10em;
max-width: 10em;
}
-#main-window[customizing=true] #PanelUI-update-status {
+#main-window[customizing=true] .PanelUI-notification-menu-item {
display: none;
}
/* UI Tour */
@keyframes uitour-wobble {
from {
transform: rotate(0deg) translateX(3px) rotate(0deg);
--- a/browser/base/content/browser.js
+++ b/browser/base/content/browser.js
@@ -1510,18 +1510,16 @@ var gBrowserInit = {
gSyncUI.init();
gFxAccounts.init();
if (AppConstants.MOZ_DATA_REPORTING)
gDataNotificationInfoBar.init();
gBrowserThumbnails.init();
- gMenuButtonBadgeManager.init();
-
gMenuButtonUpdateBadge.init();
gExtensionsNotifications.init();
let wasMinimized = window.windowState == window.STATE_MINIMIZED;
window.addEventListener("sizemodechange", () => {
let isMinimized = window.windowState == window.STATE_MINIMIZED;
if (wasMinimized != isMinimized) {
@@ -1688,18 +1686,16 @@ var gBrowserInit = {
TrackingProtection.uninit();
RefreshBlocker.uninit();
CaptivePortalWatcher.uninit();
gMenuButtonUpdateBadge.uninit();
- gMenuButtonBadgeManager.uninit();
-
SidebarUI.uninit();
// Now either cancel delayedStartup, or clean up the services initialized from
// it.
if (this._boundDelayedStartup) {
this._cancelDelayedStartup();
} else {
if (Win7Features)
@@ -2744,195 +2740,227 @@ function UpdatePopupNotificationsVisibil
PopupNotifications.anchorVisibilityChange();
}
function PageProxyClickHandler(aEvent) {
if (aEvent.button == 1 && gPrefService.getBoolPref("middlemouse.paste"))
middleMousePaste(aEvent);
}
-var gMenuButtonBadgeManager = {
- BADGEID_APPUPDATE: "update",
- BADGEID_DOWNLOAD: "download",
- BADGEID_FXA: "fxa",
- BADGEID_ADDONS: "addons",
-
- fxaBadge: null,
- downloadBadge: null,
- appUpdateBadge: null,
- addonsBadge: null,
+// Setup the hamburger button badges for updates, if enabled.
+var gMenuButtonUpdateBadge = {
+ kTopics: [
+ "update-staged",
+ "update-downloaded",
+ "update-available",
+ "update-error",
+ ],
+
+ timeouts: [],
+
+ get enabled() {
+ return Services.prefs.getBoolPref("app.update.doorhanger", false);
+ },
+
+ get badgeWaitTime() {
+ return Services.prefs.getIntPref("app.update.badgeWaitTime", 4 * 24 * 3600); // 4 days
+ },
init() {
- PanelUI.panel.addEventListener("popupshowing", this, true);
- },
-
- uninit() {
- PanelUI.panel.removeEventListener("popupshowing", this, true);
- },
-
- handleEvent(e) {
- if (e.type === "popupshowing") {
- this.clearBadges();
- }
- },
-
- _showBadge() {
- let badgeToShow = this.downloadBadge || this.appUpdateBadge || this.fxaBadge || this.addonsBadge;
-
- if (badgeToShow) {
- PanelUI.menuButton.setAttribute("badge-status", badgeToShow);
- } else {
- PanelUI.menuButton.removeAttribute("badge-status");
- }
- },
-
- _changeBadge(badgeId, badgeStatus = null) {
- if (badgeId == this.BADGEID_APPUPDATE) {
- this.appUpdateBadge = badgeStatus;
- } else if (badgeId == this.BADGEID_DOWNLOAD) {
- this.downloadBadge = badgeStatus;
- } else if (badgeId == this.BADGEID_FXA) {
- this.fxaBadge = badgeStatus;
- } else if (badgeId == this.BADGEID_ADDONS) {
- this.addonsBadge = badgeStatus;
- } else {
- Cu.reportError("The badge ID '" + badgeId + "' is unknown!");
- }
- this._showBadge();
- },
-
- addBadge(badgeId, badgeStatus) {
- if (!badgeStatus) {
- Cu.reportError("badgeStatus must be defined");
- return;
- }
- this._changeBadge(badgeId, badgeStatus);
- },
-
- removeBadge(badgeId) {
- this._changeBadge(badgeId);
- },
-
- clearBadges() {
- this.appUpdateBadge = null;
- this.downloadBadge = null;
- this.fxaBadge = null;
- this._showBadge();
- }
-};
-
-// Setup the hamburger button badges for updates, if enabled.
-var gMenuButtonUpdateBadge = {
- enabled: false,
- badgeWaitTime: 0,
- timer: null,
- cancelObserverRegistered: false,
-
- init() {
- this.enabled = Services.prefs.getBoolPref("app.update.badge", false);
if (this.enabled) {
- this.badgeWaitTime = Services.prefs.getIntPref("app.update.badgeWaitTime",
- 345600); // 4 days
- Services.obs.addObserver(this, "update-staged", false);
- Services.obs.addObserver(this, "update-downloaded", false);
+ this.kTopics.forEach(t => {
+ Services.obs.addObserver(this, t, false);
+ });
}
},
uninit() {
- if (this.timer)
- this.timer.cancel();
if (this.enabled) {
- Services.obs.removeObserver(this, "update-staged");
- Services.obs.removeObserver(this, "update-downloaded");
- this.enabled = false;
- }
- if (this.cancelObserverRegistered) {
- Services.obs.removeObserver(this, "update-canceled");
- this.cancelObserverRegistered = false;
- }
- },
-
- onMenuPanelCommand(event) {
- if (event.originalTarget.getAttribute("update-status") === "succeeded") {
- // restart the app
- let cancelQuit = Cc["@mozilla.org/supports-PRBool;1"]
- .createInstance(Ci.nsISupportsPRBool);
- Services.obs.notifyObservers(cancelQuit, "quit-application-requested", "restart");
-
- if (!cancelQuit.data) {
- Services.startup.quit(Services.startup.eAttemptQuit | Services.startup.eRestart);
+ this.kTopics.forEach(t => {
+ Services.obs.removeObserver(this, t);
+ });
+ }
+
+ this.reset();
+ },
+
+ reset() {
+ PanelUI.removeNotification(/^update-/);
+ this.clearCallbacks();
+ },
+
+ clearCallbacks() {
+ this.timeouts.forEach(t => clearTimeout(t));
+ this.timeouts = [];
+ },
+
+ addTimeout(time, callback) {
+ this.timeouts.push(setTimeout(() => {
+ this.clearCallbacks();
+ callback();
+ }, time));
+ },
+
+ replaceReleaseNotes(update, whatsNewId) {
+ let whatsNewLink = document.getElementById(whatsNewId);
+ if (update && update.detailsURL) {
+ whatsNewLink.href = update.detailsURL;
+ } else {
+ whatsNewLink.href = Services.urlFormatter.formatURLPref("app.update.url.details");
+ }
+ },
+
+ requestRestart() {
+ let cancelQuit = Cc["@mozilla.org/supports-PRBool;1"]
+ .createInstance(Ci.nsISupportsPRBool);
+ Services.obs.notifyObservers(cancelQuit, "quit-application-requested", "restart");
+
+ if (!cancelQuit.data) {
+ Services.startup.quit(Services.startup.eAttemptQuit | Services.startup.eRestart);
+ }
+ },
+
+ openManualUpdateUrl() {
+ let manualUpdateUrl = Services.urlFormatter.formatURLPref("app.update.url.manual");
+ openUILinkIn(manualUpdateUrl, "tab");
+ },
+
+ showUpdateNotification(type, dismissed, mainAction) {
+ let action = {
+ callback(fromDoorhanger) {
+ if (fromDoorhanger) {
+ Services.telemetry.getHistogramById("UPDATE_NOTIFICATION_MAIN_ACTION_DOORHANGER").add(type);
+ } else {
+ Services.telemetry.getHistogramById("UPDATE_NOTIFICATION_MAIN_ACTION_MENU").add(type);
+ }
+ mainAction();
}
- } else {
- // open the page for manual update
- let url = Services.urlFormatter.formatURLPref("app.update.url.manual");
- openUILinkIn(url, "tab");
+ };
+
+ let secondaryAction = {
+ callback() {
+ Services.telemetry.getHistogramById("UPDATE_NOTIFICATION_DISMISSED").add(type);
+ },
+ dismiss: true
+ };
+
+ PanelUI.showNotification("update-" + type, action, [secondaryAction], { dismissed });
+ Services.telemetry.getHistogramById("UPDATE_NOTIFICATION_SHOWN").add(type);
+ },
+
+ showRestartNotification(dismissed) {
+ this.showUpdateNotification("restart", dismissed, () => gMenuButtonUpdateBadge.requestRestart());
+ },
+
+ showUpdateAvailableNotification(update, dismissed) {
+ this.replaceReleaseNotes(update, "update-available-whats-new");
+ this.showUpdateNotification("available", dismissed, () => {
+ let updateService = Cc["@mozilla.org/updates/update-service;1"]
+ .getService(Ci.nsIApplicationUpdateService);
+ updateService.downloadUpdate(update, true);
+ });
+ },
+
+ showManualUpdateNotification(update, dismissed) {
+ this.replaceReleaseNotes(update, "update-manual-whats-new");
+
+ this.showUpdateNotification("manual", dismissed, () => gMenuButtonUpdateBadge.openManualUpdateUrl());
+ },
+
+ handleUpdateError(update, status) {
+ switch (status) {
+ case "download-attempt-failed":
+ this.clearCallbacks();
+ this.showUpdateAvailableNotification(update, false);
+ break;
+ case "download-attempts-exceeded":
+ this.clearCallbacks();
+ this.showManualUpdateNotification(update, false);
+ break;
+ case "elevation-attempt-failed":
+ this.clearCallbacks();
+ this.showRestartNotification(update, false);
+ break;
+ case "elevation-attempts-exceeded":
+ this.clearCallbacks();
+ this.showManualUpdateNotification(update, false);
+ break;
+ case "check-attempts-exceeded":
+ case "unknown":
+ // Background update has failed, let's show the UI responsible for
+ // prompting the user to update manually.
+ this.clearCallbacks();
+ this.showManualUpdateNotification(update, false);
+ break;
+ }
+ },
+
+ handleUpdateStagedOrDownloaded(update, status) {
+ switch (status) {
+ case "applied":
+ case "pending":
+ case "applied-service":
+ case "pending-service":
+ case "success":
+ this.clearCallbacks();
+
+ let badgeWaitTimeMs = this.badgeWaitTime * 1000;
+ let doorhangerWaitTimeMs = update.promptWaitTime * 1000;
+
+ if (badgeWaitTimeMs < doorhangerWaitTimeMs) {
+ this.addTimeout(badgeWaitTimeMs, () => {
+ this.showRestartNotification(true);
+
+ // doorhangerWaitTimeMs is relative to when we initially received
+ // the event. Since we've already waited badgeWaitTimeMs, subtract
+ // that from doorhangerWaitTimeMs.
+ let remainingTime = doorhangerWaitTimeMs - badgeWaitTimeMs;
+ this.addTimeout(remainingTime, () => {
+ this.showRestartNotification(false);
+ });
+ });
+ } else {
+ this.addTimeout(doorhangerWaitTimeMs, () => {
+ this.showRestartNotification(false);
+ });
+ }
+ break;
+ }
+ },
+
+ handleUpdateAvailable(update, status) {
+ switch (status) {
+ case "show-prompt":
+ // If an update is available and had the showPrompt flag set, then
+ // show an update available doorhanger.
+ this.clearCallbacks();
+ this.showUpdateAvailableNotification(update, false);
+ break;
}
},
observe(subject, topic, status) {
- if (topic == "update-canceled") {
- this.reset();
- return;
- }
- if (status == "failed") {
- // Background update has failed, let's show the UI responsible for
- // prompting the user to update manually.
- this.uninit();
- this.displayBadge(false);
+ if (!this.enabled) {
return;
}
- // Give the user badgeWaitTime seconds to react before prompting.
- this.timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
- this.timer.initWithCallback(this, this.badgeWaitTime * 1000,
- this.timer.TYPE_ONE_SHOT);
- // The timer callback will call uninit() when it completes.
- },
-
- notify() {
- // If the update is successfully applied, or if the updater has fallen back
- // to non-staged updates, add a badge to the hamburger menu to indicate an
- // update will be applied once the browser restarts.
- this.uninit();
- this.displayBadge(true);
- },
-
- displayBadge(succeeded) {
- let status = succeeded ? "succeeded" : "failed";
- let badgeStatus = "update-" + status;
- gMenuButtonBadgeManager.addBadge(gMenuButtonBadgeManager.BADGEID_APPUPDATE, badgeStatus);
-
- let stringId;
- let updateButtonText;
- if (succeeded) {
- let brandBundle = document.getElementById("bundle_brand");
- let brandShortName = brandBundle.getString("brandShortName");
- stringId = "appmenu.restartNeeded.description";
- updateButtonText = gNavigatorBundle.getFormattedString(stringId,
- [brandShortName]);
- Services.obs.addObserver(this, "update-canceled", false);
- this.cancelObserverRegistered = true;
- } else {
- stringId = "appmenu.updateFailed.description";
- updateButtonText = gNavigatorBundle.getString(stringId);
- }
-
- let updateButton = document.getElementById("PanelUI-update-status");
- updateButton.setAttribute("label", updateButtonText);
- updateButton.setAttribute("update-status", status);
- updateButton.hidden = false;
- },
-
- reset() {
- gMenuButtonBadgeManager.removeBadge(
- gMenuButtonBadgeManager.BADGEID_APPUPDATE);
- let updateButton = document.getElementById("PanelUI-update-status");
- updateButton.hidden = true;
- this.uninit();
- this.init();
+ let update = subject && subject.QueryInterface(Ci.nsIUpdate);
+
+ switch (topic) {
+ case "update-available":
+ this.handleUpdateAvailable(update, status);
+ break;
+ case "update-staged":
+ case "update-downloaded":
+ this.handleUpdateStagedOrDownloaded(update, status);
+ break;
+ case "update-error":
+ this.handleUpdateError(update, status);
+ break;
+ }
}
};
// Values for telemtery bins: see TLS_ERROR_REPORT_UI in Histograms.json
const TLS_ERROR_REPORT_TELEMETRY_AUTO_CHECKED = 2;
const TLS_ERROR_REPORT_TELEMETRY_AUTO_UNCHECKED = 3;
const TLS_ERROR_REPORT_TELEMETRY_MANUAL_SEND = 4;
const TLS_ERROR_REPORT_TELEMETRY_AUTO_SEND = 5;
new file mode 100644
--- /dev/null
+++ b/browser/base/content/test/appUpdate/.eslintrc.js
@@ -0,0 +1,7 @@
+"use strict";
+
+module.exports = {
+ "extends": [
+ "plugin:mozilla/browser-test"
+ ]
+};
new file mode 100644
--- /dev/null
+++ b/browser/base/content/test/appUpdate/browser.ini
@@ -0,0 +1,22 @@
+[DEFAULT]
+tags = appupdate
+support-files =
+ head.js
+ downloadPage.html
+ testConstants.js
+
+[browser_updatesBasicPrompt.js]
+skip-if = asan
+reason = Bug 1168003
+[browser_updatesBasicPromptNoStaging.js]
+[browser_updatesCompleteAndPartialPatchesWithBadCompleteSize.js]
+[browser_updatesCompleteAndPartialPatchesWithBadPartialSize.js]
+[browser_updatesCompleteAndPartialPatchesWithBadSizes.js]
+[browser_updatesCompletePatchApplyFailure.js]
+[browser_updatesCompletePatchWithBadCompleteSize.js]
+[browser_updatesDownloadFailures.js]
+[browser_updatesMalformedXml.js]
+[browser_updatesPartialPatchApplyFailure.js]
+[browser_updatesPartialPatchApplyFailureWithCompleteAvailable.js]
+[browser_updatesPartialPatchApplyFailureWithCompleteValidationFailure.js]
+[browser_updatesPartialPatchWithBadPartialSize.js]
new file mode 100644
--- /dev/null
+++ b/browser/base/content/test/appUpdate/browser_updatesBasicPrompt.js
@@ -0,0 +1,28 @@
+add_task(function* testBasicPrompt() {
+ SpecialPowers.pushPrefEnv({set: [[PREF_APP_UPDATE_STAGING_ENABLED, true]]});
+ let updateParams = "showPrompt=1&promptWaitTime=0";
+ gUseTestUpdater = true;
+
+ // Open a new window to make sure that it doesn't get in the way
+ // of the notification management.
+ let extraWindow = yield BrowserTestUtils.openNewBrowserWindow();
+
+ yield runUpdateTest(updateParams, 1, [
+ {
+ notificationId: "update-available",
+ button: "button",
+ beforeClick() {
+ checkWhatsNewLink("update-available-whats-new");
+ }
+ },
+ {
+ notificationId: "update-restart",
+ button: "secondarybutton",
+ *cleanup() {
+ PanelUI.removeNotification(/.*/);
+ }
+ },
+ ]);
+
+ yield BrowserTestUtils.closeWindow(extraWindow);
+});
new file mode 100644
--- /dev/null
+++ b/browser/base/content/test/appUpdate/browser_updatesBasicPromptNoStaging.js
@@ -0,0 +1,22 @@
+add_task(function* testBasicPromptNoStaging() {
+ SpecialPowers.pushPrefEnv({set: [[PREF_APP_UPDATE_STAGING_ENABLED, false]]});
+
+ let updateParams = "showPrompt=1&promptWaitTime=0";
+
+ yield runUpdateTest(updateParams, 1, [
+ {
+ notificationId: "update-available",
+ button: "button",
+ beforeClick() {
+ checkWhatsNewLink("update-available-whats-new");
+ }
+ },
+ {
+ notificationId: "update-restart",
+ button: "secondarybutton",
+ cleanup() {
+ PanelUI.removeNotification(/.*/);
+ }
+ },
+ ]);
+});
new file mode 100644
--- /dev/null
+++ b/browser/base/content/test/appUpdate/browser_updatesCompleteAndPartialPatchesWithBadCompleteSize.js
@@ -0,0 +1,13 @@
+add_task(function* testCompleteAndPartialPatchesWithBadCompleteSize() {
+ let updateParams = "invalidCompleteSize=1&promptWaitTime=0";
+
+ yield runUpdateTest(updateParams, 1, [
+ {
+ notificationId: "update-restart",
+ button: "secondarybutton",
+ cleanup() {
+ PanelUI.removeNotification(/.*/);
+ }
+ },
+ ]);
+});
new file mode 100644
--- /dev/null
+++ b/browser/base/content/test/appUpdate/browser_updatesCompleteAndPartialPatchesWithBadPartialSize.js
@@ -0,0 +1,13 @@
+add_task(function* testCompleteAndPartialPatchesWithBadPartialSize() {
+ let updateParams = "invalidPartialSize=1&promptWaitTime=0";
+
+ yield runUpdateTest(updateParams, 1, [
+ {
+ notificationId: "update-restart",
+ button: "secondarybutton",
+ cleanup() {
+ PanelUI.removeNotification(/.*/);
+ }
+ },
+ ]);
+});
new file mode 100644
--- /dev/null
+++ b/browser/base/content/test/appUpdate/browser_updatesCompleteAndPartialPatchesWithBadSizes.js
@@ -0,0 +1,33 @@
+add_task(function* testCompleteAndPartialPatchesWithBadSizes() {
+ SpecialPowers.pushPrefEnv({set: [[PREF_APP_UPDATE_DOWNLOADPROMPTMAXATTEMPTS, 2]]});
+ let updateParams = "invalidPartialSize=1&invalidCompleteSize=1";
+
+ yield runUpdateTest(updateParams, 1, [
+ {
+ // if we fail maxBackgroundErrors download attempts, then we want to
+ // first show the user an update available prompt.
+ notificationId: "update-available",
+ button: "button"
+ },
+ {
+ notificationId: "update-available",
+ button: "button"
+ },
+ {
+ // if we have only an invalid patch, then something's wrong and we don't
+ // have an automatic way to fix it, so show the manual update
+ // doorhanger.
+ notificationId: "update-manual",
+ button: "button",
+ beforeClick() {
+ checkWhatsNewLink("update-manual-whats-new");
+ },
+ *cleanup() {
+ yield BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
+ is(gBrowser.selectedBrowser.currentURI.spec,
+ URL_MANUAL_UPDATE, "Landed on manual update page.")
+ gBrowser.removeTab(gBrowser.selectedTab);
+ }
+ },
+ ]);
+});
new file mode 100644
--- /dev/null
+++ b/browser/base/content/test/appUpdate/browser_updatesCompletePatchApplyFailure.js
@@ -0,0 +1,25 @@
+add_task(function* testCompletePatchApplyFailure() {
+ let patches = getLocalPatchString("complete", null, null, null, null, null,
+ STATE_PENDING);
+ let updates = getLocalUpdateString(patches, null, null, null,
+ Services.appinfo.version, null);
+
+ yield runUpdateProcessingTest(updates, [
+ {
+ // if we have only an invalid patch, then something's wrong and we don't
+ // have an automatic way to fix it, so show the manual update
+ // doorhanger.
+ notificationId: "update-manual",
+ button: "button",
+ beforeClick() {
+ checkWhatsNewLink("update-manual-whats-new");
+ },
+ *cleanup() {
+ yield BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
+ is(gBrowser.selectedBrowser.currentURI.spec,
+ URL_MANUAL_UPDATE, "Landed on manual update page.")
+ gBrowser.removeTab(gBrowser.selectedTab);
+ }
+ },
+ ]);
+});
new file mode 100644
--- /dev/null
+++ b/browser/base/content/test/appUpdate/browser_updatesCompletePatchWithBadCompleteSize.js
@@ -0,0 +1,34 @@
+add_task(function* testCompletePatchWithBadCompleteSize() {
+ SpecialPowers.pushPrefEnv({set: [[PREF_APP_UPDATE_DOWNLOADPROMPTMAXATTEMPTS, 2]]});
+
+ let updateParams = "completePatchOnly=1&invalidCompleteSize=1";
+
+ yield runUpdateTest(updateParams, 1, [
+ {
+ // if we fail maxBackgroundErrors download attempts, then we want to
+ // first show the user an update available prompt.
+ notificationId: "update-available",
+ button: "button"
+ },
+ {
+ notificationId: "update-available",
+ button: "button"
+ },
+ {
+ // if we have only an invalid patch, then something's wrong and we don't
+ // have an automatic way to fix it, so show the manual update
+ // doorhanger.
+ notificationId: "update-manual",
+ button: "button",
+ beforeClick() {
+ checkWhatsNewLink("update-manual-whats-new");
+ },
+ *cleanup() {
+ yield BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
+ is(gBrowser.selectedBrowser.currentURI.spec,
+ URL_MANUAL_UPDATE, "Landed on manual update page.")
+ gBrowser.removeTab(gBrowser.selectedTab);
+ }
+ },
+ ]);
+});
new file mode 100644
--- /dev/null
+++ b/browser/base/content/test/appUpdate/browser_updatesDownloadFailures.js
@@ -0,0 +1,37 @@
+add_task(function* testDownloadFailures() {
+ const maxBackgroundErrors = 5;
+ SpecialPowers.pushPrefEnv({set: [
+ [PREF_APP_UPDATE_BACKGROUNDMAXERRORS, maxBackgroundErrors],
+ [PREF_APP_UPDATE_DOWNLOADPROMPTMAXATTEMPTS, 2]
+ ]});
+ let updateParams = "badURL=1";
+
+ // Open a new window to make sure that our pref management isn't duplicated.
+ let extraWindow = yield BrowserTestUtils.openNewBrowserWindow();
+
+ yield runUpdateTest(updateParams, 1, [
+ {
+ // if we fail maxBackgroundErrors download attempts, then we want to
+ // first show the user an update available prompt.
+ notificationId: "update-available",
+ button: "button"
+ },
+ {
+ notificationId: "update-available",
+ button: "button"
+ },
+ {
+ notificationId: "update-manual",
+ button: "button",
+ *cleanup() {
+ yield BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
+ is(gBrowser.selectedBrowser.currentURI.spec,
+ URL_MANUAL_UPDATE, "Landed on manual update page.");
+ gBrowser.removeTab(gBrowser.selectedTab);
+ gMenuButtonUpdateBadge.reset();
+ }
+ },
+ ]);
+
+ yield BrowserTestUtils.closeWindow(extraWindow);
+});
new file mode 100644
--- /dev/null
+++ b/browser/base/content/test/appUpdate/browser_updatesMalformedXml.js
@@ -0,0 +1,29 @@
+add_task(function* testMalformedXml() {
+ const updateDetailsUrl = "http://example.com/details";
+ const maxBackgroundErrors = 10;
+ SpecialPowers.pushPrefEnv({set: [
+ [PREF_APP_UPDATE_BACKGROUNDMAXERRORS, maxBackgroundErrors],
+ [PREF_APP_UPDATE_URL_DETAILS, updateDetailsUrl]
+ ]});
+
+ let updateParams = "xmlMalformed=1";
+
+ yield runUpdateTest(updateParams, maxBackgroundErrors, [
+ {
+ // if we fail 10 check attempts, then we want to just show the user a manual update
+ // workflow.
+ notificationId: "update-manual",
+ button: "button",
+ beforeClick() {
+ checkWhatsNewLink("update-manual-whats-new", updateDetailsUrl);
+ },
+ *cleanup() {
+ yield BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
+ is(gBrowser.selectedBrowser.currentURI.spec,
+ URL_MANUAL_UPDATE, "Landed on manual update page.")
+ gBrowser.removeTab(gBrowser.selectedTab);
+ gMenuButtonUpdateBadge.reset();
+ }
+ },
+ ]);
+});
new file mode 100644
--- /dev/null
+++ b/browser/base/content/test/appUpdate/browser_updatesPartialPatchApplyFailure.js
@@ -0,0 +1,26 @@
+add_task(function* testPartialPatchApplyFailure() {
+ let patches = getLocalPatchString("partial", null, null, null, null, null,
+ STATE_PENDING);
+ let updates = getLocalUpdateString(patches, null, null, null,
+ Services.appinfo.version, null,
+ null, null, null, null, "false");
+
+ yield runUpdateProcessingTest(updates, [
+ {
+ // if we have only an invalid patch, then something's wrong and we don't
+ // have an automatic way to fix it, so show the manual update
+ // doorhanger.
+ notificationId: "update-manual",
+ button: "button",
+ beforeClick() {
+ checkWhatsNewLink("update-manual-whats-new");
+ },
+ *cleanup() {
+ yield BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
+ is(gBrowser.selectedBrowser.currentURI.spec,
+ URL_MANUAL_UPDATE, "Landed on manual update page.")
+ gBrowser.removeTab(gBrowser.selectedTab);
+ }
+ },
+ ]);
+});
new file mode 100644
--- /dev/null
+++ b/browser/base/content/test/appUpdate/browser_updatesPartialPatchApplyFailureWithCompleteAvailable.js
@@ -0,0 +1,22 @@
+add_task(function* testPartialPatchApplyFailureWithCompleteAvailable() {
+ let patches = getLocalPatchString("partial", null, null, null, null, null,
+ STATE_PENDING) +
+ getLocalPatchString("complete", null, null, null,
+ null, "false");
+
+ let promptWaitTime = "0";
+ let updates = getLocalUpdateString(patches, null, null, null,
+ Services.appinfo.version, null,
+ null, null, null, null, "false",
+ null, null, null, null, promptWaitTime);
+
+ yield runUpdateProcessingTest(updates, [
+ {
+ notificationId: "update-restart",
+ button: "secondarybutton",
+ cleanup() {
+ PanelUI.removeNotification(/.*/);
+ }
+ },
+ ]);
+});
new file mode 100644
--- /dev/null
+++ b/browser/base/content/test/appUpdate/browser_updatesPartialPatchApplyFailureWithCompleteValidationFailure.js
@@ -0,0 +1,33 @@
+add_task(function* testPartialPatchApplyFailureWithCompleteValidationFailure() {
+ // because of the way we're simulating failure, we have to just pretend we've already
+ // retried.
+ SpecialPowers.pushPrefEnv({set: [[PREF_APP_UPDATE_DOWNLOADPROMPTMAXATTEMPTS, 0]]});
+ let patches = getLocalPatchString("partial", null, null, null, null, null,
+ STATE_PENDING) +
+ getLocalPatchString("complete", null, "MD5",
+ null, "1234",
+ "false");
+
+ let updates = getLocalUpdateString(patches, null, null, null,
+ Services.appinfo.version, null,
+ null, null, null, null, "false");
+
+ yield runUpdateProcessingTest(updates, [
+ {
+ // if we have only an invalid patch, then something's wrong and we don't
+ // have an automatic way to fix it, so show the manual update
+ // doorhanger.
+ notificationId: "update-manual",
+ button: "button",
+ beforeClick() {
+ checkWhatsNewLink("update-manual-whats-new");
+ },
+ *cleanup() {
+ yield BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
+ is(gBrowser.selectedBrowser.currentURI.spec,
+ URL_MANUAL_UPDATE, "Landed on manual update page.")
+ gBrowser.removeTab(gBrowser.selectedTab);
+ }
+ },
+ ]);
+});
new file mode 100644
--- /dev/null
+++ b/browser/base/content/test/appUpdate/browser_updatesPartialPatchWithBadPartialSize.js
@@ -0,0 +1,33 @@
+add_task(function* testPartialPatchWithBadPartialSize() {
+ SpecialPowers.pushPrefEnv({set: [[PREF_APP_UPDATE_DOWNLOADPROMPTMAXATTEMPTS, 2]]});
+ let updateParams = "partialPatchOnly=1&invalidPartialSize=1";
+
+ yield runUpdateTest(updateParams, 1, [
+ {
+ // if we fail maxBackgroundErrors download attempts, then we want to
+ // first show the user an update available prompt.
+ notificationId: "update-available",
+ button: "button"
+ },
+ {
+ notificationId: "update-available",
+ button: "button"
+ },
+ {
+ // if we have only an invalid patch, then something's wrong and we don't
+ // have an automatic way to fix it, so show the manual update
+ // doorhanger.
+ notificationId: "update-manual",
+ button: "button",
+ beforeClick() {
+ checkWhatsNewLink("update-manual-whats-new");
+ },
+ *cleanup() {
+ yield BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
+ is(gBrowser.selectedBrowser.currentURI.spec,
+ URL_MANUAL_UPDATE, "Landed on manual update page.")
+ gBrowser.removeTab(gBrowser.selectedTab);
+ }
+ },
+ ]);
+});
new file mode 100644
--- /dev/null
+++ b/browser/base/content/test/appUpdate/downloadPage.html
@@ -0,0 +1,13 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title>Download page</title>
+ <meta charset="utf-8">
+</head>
+<body>
+<!-- just use simple.mar since we have it available and it will result in a download dialog -->
+<a id="download-link" href="http://example.com/browser/browser/base/content/test/appUpdate/simple.mar" data-link-type="download">
+ Download
+</a>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/browser/base/content/test/appUpdate/head.js
@@ -0,0 +1,360 @@
+Cu.import("resource://gre/modules/FileUtils.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+const IS_MACOSX = ("nsILocalFileMac" in Ci);
+const IS_WIN = ("@mozilla.org/windows-registry-key;1" in Cc);
+
+const BIN_SUFFIX = (IS_WIN ? ".exe" : "");
+const FILE_UPDATER_BIN = "updater" + (IS_MACOSX ? ".app" : BIN_SUFFIX);
+const FILE_UPDATER_BIN_BAK = FILE_UPDATER_BIN + ".bak";
+
+let gRembemberedPrefs = [];
+
+const DATA_URI_SPEC = "chrome://mochitests/content/browser/browser/base/content/test/appUpdate/";
+
+var DEBUG_AUS_TEST = true;
+var gUseTestUpdater = false;
+
+const LOG_FUNCTION = info;
+
+/* import-globals-from testConstants.js */
+Services.scriptloader.loadSubScript(DATA_URI_SPEC + "testConstants.js", this);
+/* import-globals-from ../../../../../toolkit/mozapps/update/tests/data/shared.js */
+Services.scriptloader.loadSubScript(DATA_URI_SPEC + "shared.js", this);
+
+var gURLData = URL_HOST + "/" + REL_PATH_DATA;
+const URL_MANUAL_UPDATE = gURLData + "downloadPage.html";
+
+const NOTIFICATIONS = [
+ "update-available",
+ "update-manual",
+ "update-restart"
+];
+
+/**
+ * Delay for a very short period. Useful for moving the code after this
+ * to the back of the event loop.
+ *
+ * @return A promise which will resolve after a very short period.
+ */
+function delay() {
+ return new Promise(resolve => executeSoon(resolve));
+}
+
+/**
+ * Gets the update version info for the update url parameters to send to
+ * update.sjs.
+ *
+ * @param aAppVersion (optional)
+ * The application version for the update snippet. If not specified the
+ * current application version will be used.
+ * @return The url parameters for the application and platform version to send
+ * to update.sjs.
+ */
+function getVersionParams(aAppVersion) {
+ let appInfo = Services.appinfo;
+ return "&appVersion=" + (aAppVersion ? aAppVersion : appInfo.version);
+}
+
+/**
+ * Clean up updates list and the updates directory.
+ */
+function cleanUpUpdates() {
+ gUpdateManager.activeUpdate = null;
+ gUpdateManager.saveUpdates();
+
+ removeUpdateDirsAndFiles();
+}
+
+/**
+ * Runs a typical update test. Will set various common prefs for using the
+ * updater doorhanger, runs the provided list of steps, and makes sure
+ * everything is cleaned up afterwards.
+ *
+ * @param updateParams
+ * URL-encoded params which will be sent to update.sjs.
+ * @param checkAttempts
+ * How many times to check for updates. Useful for testing the UI
+ * for check failures.
+ * @param steps
+ * A list of test steps to perform, specifying expected doorhangers
+ * and additional validation/cleanup callbacks.
+ * @return A promise which will resolve once all of the steps have been run
+ * and cleanup has been performed.
+ */
+function runUpdateTest(updateParams, checkAttempts, steps) {
+ return Task.spawn(function*() {
+ registerCleanupFunction(() => {
+ gMenuButtonUpdateBadge.uninit();
+ gMenuButtonUpdateBadge.init();
+ cleanUpUpdates();
+ });
+ yield SpecialPowers.pushPrefEnv({
+ set: [
+ [PREF_APP_UPDATE_DOWNLOADPROMPTATTEMPTS, 0],
+ [PREF_APP_UPDATE_ENABLED, true],
+ [PREF_APP_UPDATE_IDLETIME, 0],
+ [PREF_APP_UPDATE_URL_MANUAL, URL_MANUAL_UPDATE],
+ [PREF_APP_UPDATE_LOG, DEBUG_AUS_TEST],
+ ]});
+
+ yield setupTestUpdater();
+
+ let url = URL_HTTP_UPDATE_SJS +
+ "?" + updateParams +
+ getVersionParams();
+
+ setUpdateURL(url);
+
+ executeSoon(() => {
+ Task.spawn(function*() {
+ gAUS.checkForBackgroundUpdates();
+ for (var i = 0; i < checkAttempts - 1; i++) {
+ yield waitForEvent("update-error", "check-attempt-failed");
+ gAUS.checkForBackgroundUpdates();
+ }
+ });
+ });
+
+ for (let step of steps) {
+ yield processStep(step);
+ }
+
+ yield finishTestRestoreUpdaterBackup();
+ });
+}
+
+/**
+ * Runs a test which processes an update. Similar to runUpdateTest.
+ *
+ * @param updates
+ * A list of updates to process.
+ * @param steps
+ * A list of test steps to perform, specifying expected doorhangers
+ * and additional validation/cleanup callbacks.
+ * @return A promise which will resolve once all of the steps have been run
+ * and cleanup has been performed.
+ */
+function runUpdateProcessingTest(updates, steps) {
+ return Task.spawn(function*() {
+ registerCleanupFunction(() => {
+ gMenuButtonUpdateBadge.reset();
+ cleanUpUpdates();
+ });
+
+ SpecialPowers.pushPrefEnv({
+ set: [
+ [PREF_APP_UPDATE_DOWNLOADPROMPTATTEMPTS, 0],
+ [PREF_APP_UPDATE_ENABLED, true],
+ [PREF_APP_UPDATE_IDLETIME, 0],
+ [PREF_APP_UPDATE_URL_MANUAL, URL_MANUAL_UPDATE],
+ [PREF_APP_UPDATE_LOG, DEBUG_AUS_TEST],
+ ]});
+
+ yield setupTestUpdater();
+
+ writeUpdatesToXMLFile(getLocalUpdatesXMLString(updates), true);
+
+ writeUpdatesToXMLFile(getLocalUpdatesXMLString(""), false);
+ writeStatusFile(STATE_FAILED_CRC_ERROR);
+ reloadUpdateManagerData();
+
+ testPostUpdateProcessing();
+
+ for (let step of steps) {
+ yield processStep(step);
+ }
+
+ yield finishTestRestoreUpdaterBackup();
+ });
+}
+
+function processStep({notificationId, button, beforeClick, cleanup}) {
+ return Task.spawn(function*() {
+
+ yield BrowserTestUtils.waitForEvent(PanelUI.notificationPanel, "popupshown");
+ const shownNotification = PanelUI.activeNotification.id;
+
+ is(shownNotification, notificationId, "The right notification showed up.");
+ if (shownNotification != notificationId) {
+ if (cleanup) {
+ yield cleanup();
+ }
+ return;
+ }
+
+ let notification = document.getElementById(`PanelUI-${notificationId}-notification`);
+ is(notification.hidden, false, `${notificationId} notification is showing`);
+ if (beforeClick) {
+ yield Task.spawn(beforeClick);
+ }
+
+ let buttonEl = document.getAnonymousElementByAttribute(notification, "anonid", button);
+
+ buttonEl.click();
+
+ if (cleanup) {
+ yield cleanup();
+ }
+ });
+}
+
+/**
+ * Waits for the specified topic and (optionally) status.
+ * @param topic
+ * String representing the topic to wait for.
+ * @param status
+ * Optional String representing the status on said topic to wait for.
+ * @return A promise which will resolve the first time an event occurs on the
+ * specified topic, and (optionally) with the specified status.
+ */
+function waitForEvent(topic, status = null) {
+ return new Promise(resolve => Services.obs.addObserver({
+ observe(subject, innerTopic, innerStatus) {
+ if (!status || status == innerStatus) {
+ Services.obs.removeObserver(this, topic);
+ resolve(innerStatus);
+ }
+ }
+ }, topic, false))
+}
+
+/**
+ * Ensures that the "What's new" link with the provided ID is displayed and
+ * matches the url parameter provided. If no URL is provided, it will instead
+ * ensure that the link matches the default link URL.
+ *
+ * @param id
+ * The ID of the "What's new" link element.
+ * @param url (optional)
+ * The URL to check against. If none is provided, a default will be used.
+ */
+function checkWhatsNewLink(id, url) {
+ let whatsNewLink = document.getElementById(id);
+ is(whatsNewLink.href,
+ url || URL_HTTP_UPDATE_SJS + "?uiURL=DETAILS",
+ "What's new link points to the test_details URL");
+ is(whatsNewLink.hidden, false, "What's new link is not hidden.");
+}
+
+/**
+ * For tests that use the test updater restores the backed up real updater if
+ * it exists and tries again on failure since Windows debug builds at times
+ * leave the file in use. After success moveRealUpdater is called to continue
+ * the setup of the test updater. For tests that don't use the test updater
+ * runTest will be called.
+ */
+function setupTestUpdater() {
+ return Task.spawn(function*() {
+ if (gUseTestUpdater) {
+ try {
+ restoreUpdaterBackup();
+ } catch (e) {
+ logTestInfo("Attempt to restore the backed up updater failed... " +
+ "will try again, Exception: " + e);
+ yield delay();
+ yield setupTestUpdater();
+ return;
+ }
+ yield moveRealUpdater();
+ }
+ });
+}
+
+/**
+ * Backs up the real updater and tries again on failure since Windows debug
+ * builds at times leave the file in use. After success it will call
+ * copyTestUpdater to continue the setup of the test updater.
+ */
+function moveRealUpdater() {
+ return Task.spawn(function*() {
+ try {
+ // Move away the real updater
+ let baseAppDir = getAppBaseDir();
+ let updater = baseAppDir.clone();
+ updater.append(FILE_UPDATER_BIN);
+ updater.moveTo(baseAppDir, FILE_UPDATER_BIN_BAK);
+ } catch (e) {
+ logTestInfo("Attempt to move the real updater out of the way failed... " +
+ "will try again, Exception: " + e);
+ yield delay();
+ yield moveRealUpdater();
+ return;
+ }
+
+ yield copyTestUpdater();
+ });
+}
+
+/**
+ * Copies the test updater so it can be used by tests and tries again on failure
+ * since Windows debug builds at times leave the file in use. After success it
+ * will call runTest to continue the test.
+ */
+function copyTestUpdater() {
+ return Task.spawn(function*() {
+ try {
+ // Copy the test updater
+ let baseAppDir = getAppBaseDir();
+ let testUpdaterDir = Services.dirsvc.get("CurWorkD", Ci.nsILocalFile);
+ let relPath = REL_PATH_DATA;
+ let pathParts = relPath.split("/");
+ for (let i = 0; i < pathParts.length; ++i) {
+ testUpdaterDir.append(pathParts[i]);
+ }
+
+ let testUpdater = testUpdaterDir.clone();
+ testUpdater.append(FILE_UPDATER_BIN);
+ testUpdater.copyToFollowingLinks(baseAppDir, FILE_UPDATER_BIN);
+ } catch (e) {
+ logTestInfo("Attempt to copy the test updater failed... " +
+ "will try again, Exception: " + e);
+ yield delay();
+ yield copyTestUpdater();
+ }
+ });
+}
+
+/**
+ * Restores the updater that was backed up. This is called in setupTestUpdater
+ * before the backup of the real updater is done in case the previous test
+ * failed to restore the updater, in finishTestDefaultWaitForWindowClosed when
+ * the test has finished, and in test_9999_cleanup.xul after all tests have
+ * finished.
+ */
+function restoreUpdaterBackup() {
+ let baseAppDir = getAppBaseDir();
+ let updater = baseAppDir.clone();
+ let updaterBackup = baseAppDir.clone();
+ updater.append(FILE_UPDATER_BIN);
+ updaterBackup.append(FILE_UPDATER_BIN_BAK);
+ if (updaterBackup.exists()) {
+ if (updater.exists()) {
+ updater.remove(true);
+ }
+ updaterBackup.moveTo(baseAppDir, FILE_UPDATER_BIN);
+ }
+}
+
+/**
+ * When a test finishes this will repeatedly attempt to restore the real updater
+ * for tests that use the test updater and then call
+ * finishTestDefaultWaitForWindowClosed after the restore is successful.
+ */
+function finishTestRestoreUpdaterBackup() {
+ return Task.spawn(function*() {
+ if (gUseTestUpdater) {
+ try {
+ // Windows debug builds keep the updater file in use for a short period of
+ // time after the updater process exits.
+ restoreUpdaterBackup();
+ } catch (e) {
+ logTestInfo("Attempt to restore the backed up updater failed... " +
+ "will try again, Exception: " + e);
+
+ yield delay();
+ yield finishTestRestoreUpdaterBackup();
+ }
+ }
+ });
+}
new file mode 100644
--- /dev/null
+++ b/browser/base/content/test/appUpdate/testConstants.js
@@ -0,0 +1,4 @@
+const REL_PATH_DATA = "browser/browser/base/content/test/appUpdate/";
+const URL_HOST = "http://example.com";
+const URL_PATH_UPDATE_XML = "/" + REL_PATH_DATA + "update.sjs";
+const URL_HTTP_UPDATE_SJS = URL_HOST + URL_PATH_UPDATE_XML;
--- a/browser/base/content/test/general/browser.ini
+++ b/browser/base/content/test/general/browser.ini
@@ -633,17 +633,16 @@ tags = psm
# DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
[browser_contextmenu_childprocess.js]
# DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
[browser_bug963945.js]
# DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
[browser_domFullscreen_fullscreenMode.js]
tags = fullscreen
# DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
-[browser_menuButtonBadgeManager.js]
# DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
[browser_newTabDrop.js]
# DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
[browser_newWindowDrop.js]
# DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
[browser_newwindow_focus.js]
skip-if = (os == "linux" && !e10s) # Bug 1263254 - Perma fails on Linux without e10s for some reason.
# DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
deleted file mode 100644
--- a/browser/base/content/test/general/browser_menuButtonBadgeManager.js
+++ /dev/null
@@ -1,46 +0,0 @@
-/* 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/. */
-
-var menuButton = document.getElementById("PanelUI-menu-button");
-
-add_task(function* testButtonActivities() {
- is(menuButton.hasAttribute("badge-status"), false, "Should not have a badge status");
- is(menuButton.hasAttribute("badge"), false, "Should not have the badge attribute set");
-
- gMenuButtonBadgeManager.addBadge(gMenuButtonBadgeManager.BADGEID_FXA, "fxa-needs-authentication");
- is(menuButton.getAttribute("badge-status"), "fxa-needs-authentication", "Should have fxa-needs-authentication badge status");
-
- gMenuButtonBadgeManager.addBadge(gMenuButtonBadgeManager.BADGEID_APPUPDATE, "update-succeeded");
- is(menuButton.getAttribute("badge-status"), "update-succeeded", "Should have update-succeeded badge status (update > fxa)");
-
- gMenuButtonBadgeManager.addBadge(gMenuButtonBadgeManager.BADGEID_APPUPDATE, "update-failed");
- is(menuButton.getAttribute("badge-status"), "update-failed", "Should have update-failed badge status");
-
- gMenuButtonBadgeManager.addBadge(gMenuButtonBadgeManager.BADGEID_DOWNLOAD, "download-severe");
- is(menuButton.getAttribute("badge-status"), "download-severe", "Should have download-severe badge status");
-
- gMenuButtonBadgeManager.addBadge(gMenuButtonBadgeManager.BADGEID_DOWNLOAD, "download-warning");
- is(menuButton.getAttribute("badge-status"), "download-warning", "Should have download-warning badge status");
-
- gMenuButtonBadgeManager.addBadge("unknownbadge", "attr");
- is(menuButton.getAttribute("badge-status"), "download-warning", "Should not have changed badge status");
-
- gMenuButtonBadgeManager.removeBadge(gMenuButtonBadgeManager.BADGEID_DOWNLOAD);
- is(menuButton.getAttribute("badge-status"), "update-failed", "Should have update-failed badge status");
-
- gMenuButtonBadgeManager.removeBadge(gMenuButtonBadgeManager.BADGEID_APPUPDATE);
- is(menuButton.getAttribute("badge-status"), "fxa-needs-authentication", "Should have fxa-needs-authentication badge status");
-
- gMenuButtonBadgeManager.removeBadge(gMenuButtonBadgeManager.BADGEID_FXA);
- is(menuButton.hasAttribute("badge-status"), false, "Should not have a badge status");
-
- yield PanelUI.show();
- is(menuButton.hasAttribute("badge-status"), false, "Should not have a badge status (Hamburger menu opened)");
- PanelUI.hide();
-
- gMenuButtonBadgeManager.addBadge(gMenuButtonBadgeManager.BADGEID_FXA, "fxa-needs-authentication");
- gMenuButtonBadgeManager.addBadge(gMenuButtonBadgeManager.BADGEID_APPUPDATE, "update-succeeded");
- gMenuButtonBadgeManager.clearBadges();
- is(menuButton.hasAttribute("badge-status"), false, "Should not have a badge status (clearBadges called)");
-});
--- a/browser/base/moz.build
+++ b/browser/base/moz.build
@@ -32,23 +32,33 @@ BROWSER_CHROME_MANIFESTS += [
'content/test/tabcrashed/browser.ini',
'content/test/tabPrompts/browser.ini',
'content/test/tabs/browser.ini',
'content/test/urlbar/browser.ini',
'content/test/webextensions/browser.ini',
'content/test/webrtc/browser.ini',
]
+if CONFIG['MOZ_UPDATER']:
+ BROWSER_CHROME_MANIFESTS += ['content/test/appUpdate/browser.ini']
+
DEFINES['MOZ_APP_VERSION'] = CONFIG['MOZ_APP_VERSION']
DEFINES['MOZ_APP_VERSION_DISPLAY'] = CONFIG['MOZ_APP_VERSION_DISPLAY']
DEFINES['APP_LICENSE_BLOCK'] = '%s/content/overrides/app-license.html' % SRCDIR
if CONFIG['MOZ_WIDGET_TOOLKIT'] in ('windows', 'gtk2', 'gtk3', 'cocoa'):
DEFINES['CONTEXT_COPY_IMAGE_CONTENTS'] = 1
if CONFIG['MOZ_WIDGET_TOOLKIT'] in ('windows', 'cocoa'):
DEFINES['CAN_DRAW_IN_TITLEBAR'] = 1
if CONFIG['MOZ_WIDGET_TOOLKIT'] in ('windows', 'gtk2', 'gtk3'):
DEFINES['MENUBAR_CAN_AUTOHIDE'] = 1
+TEST_HARNESS_FILES.testing.mochitest.browser.browser.base.content.test.appUpdate += [
+ '/toolkit/mozapps/update/tests/chrome/update.sjs',
+ '/toolkit/mozapps/update/tests/data/shared.js',
+ '/toolkit/mozapps/update/tests/data/sharedUpdateXML.js',
+ '/toolkit/mozapps/update/tests/data/simple.mar',
+]
+
JAR_MANIFESTS += ['jar.mn']
--- a/browser/branding/official/pref/firefox-branding.js
+++ b/browser/branding/official/pref/firefox-branding.js
@@ -21,14 +21,14 @@ pref("app.update.url.manual", "https://w
pref("app.update.url.details", "https://www.mozilla.org/%LOCALE%/firefox/notes");
// The number of days a binary is permitted to be old
// without checking for an update. This assumes that
// app.update.checkInstallTime is true.
pref("app.update.checkInstallTime.days", 63);
// Give the user x seconds to reboot before showing a badge on the hamburger
-// button. default=immediately
-pref("app.update.badgeWaitTime", 0);
+// button. default=4 days
+pref("app.update.badgeWaitTime", 345600);
// Number of usages of the web console or scratchpad.
// If this is less than 5, then pasting code into the web console or scratchpad is disabled
pref("devtools.selfxss.count", 0);
--- a/browser/components/customizableui/content/panelUI.inc.xul
+++ b/browser/components/customizableui/content/panelUI.inc.xul
@@ -12,19 +12,27 @@
<panelmultiview id="PanelUI-multiView" mainViewId="PanelUI-mainView">
<panelview id="PanelUI-mainView" context="customizationPanelContextMenu">
<vbox id="PanelUI-contents-scroller">
<vbox id="PanelUI-contents" class="panelUI-grid"/>
</vbox>
<footer id="PanelUI-footer">
<vbox id="PanelUI-footer-addons"></vbox>
- <toolbarbutton id="PanelUI-update-status"
- oncommand="gMenuButtonUpdateBadge.onMenuPanelCommand(event);"
+ <toolbarbutton id="PanelUI-update-available-menu-item"
+ wrap="true"
+ label="&updateAvailable.panelUI.label;"
+ hidden="true"/>
+ <toolbarbutton id="PanelUI-update-manual-menu-item"
wrap="true"
+ label="&updateManual.panelUI.label;"
+ hidden="true"/>
+ <toolbarbutton id="PanelUI-update-restart-menu-item"
+ wrap="true"
+ label="&updateRestart.panelUI.label;"
hidden="true"/>
<hbox id="PanelUI-footer-fxa">
<hbox id="PanelUI-fxa-status"
defaultlabel="&fxaSignIn.label;"
signedinTooltiptext="&fxaSignedIn.tooltip;"
tooltiptext="&fxaSignedIn.tooltip;"
errorlabel="&fxaSignInError.label;"
unverifiedlabel="&fxaUnverified.label;"
@@ -409,8 +417,71 @@
<description>&panicButton.thankyou.msg1;</description>
<description>&panicButton.thankyou.msg2;</description>
</vbox>
</hbox>
<button label="&panicButton.thankyou.buttonlabel;"
id="panic-button-success-closebutton"
oncommand="PanicButtonNotifier.close()"/>
</panel>
+
+<panel id="PanelUI-notification-popup"
+ class="popup-notification-panel"
+ type="arrow"
+ position="after_start"
+ hidden="true"
+ orient="vertical"
+ noautofocus="true"
+ noautohide="true"
+ nopreventnavboxhide="true"
+ role="alert">
+ <popupnotification id="PanelUI-update-available-notification"
+ popupid="update-available"
+ label="&updateAvailable.header.message;"
+ buttonlabel="&updateAvailable.acceptButton.label;"
+ buttonaccesskey="&updateAvailable.acceptButton.accesskey;"
+ closebuttonhidden="true"
+ secondarybuttonlabel="&updateAvailable.cancelButton.label;"
+ secondarybuttonaccesskey="&updateAvailable.cancelButton.accesskey;"
+ dropmarkerhidden="true"
+ checkboxhidden="true"
+ hidden="true">
+ <popupnotificationcontent id="update-available-notification-content" orient="vertical">
+ <description id="update-available-description">&updateAvailable.message;
+ <label id="update-available-whats-new" class="text-link" value="&updateAvailable.whatsnew.label;" />
+ </description>
+ </popupnotificationcontent>
+ </popupnotification>
+
+ <popupnotification id="PanelUI-update-manual-notification"
+ popupid="update-manual"
+ label="&updateManual.header.message;"
+ buttonlabel="&updateManual.acceptButton.label;"
+ buttonaccesskey="&updateManual.acceptButton.accesskey;"
+ closebuttonhidden="true"
+ secondarybuttonlabel="&updateManual.cancelButton.label;"
+ secondarybuttonaccesskey="&updateManual.cancelButton.accesskey;"
+ dropmarkerhidden="true"
+ checkboxhidden="true"
+ hidden="true">
+ <popupnotificationcontent id="update-manual-notification-content" orient="vertical">
+ <description id="update-manual-description">&updateManual.message;
+ <label id="update-manual-whats-new" class="text-link" value="&updateManual.whatsnew.label;" />
+ </description>
+ </popupnotificationcontent>
+ </popupnotification>
+
+ <popupnotification id="PanelUI-update-restart-notification"
+ popupid="update-restart"
+ label="&updateRestart.header.message;"
+ buttonlabel="&updateRestart.acceptButton.label;"
+ buttonaccesskey="&updateRestart.acceptButton.accesskey;"
+ closebuttonhidden="true"
+ secondarybuttonlabel="&updateRestart.cancelButton.label;"
+ secondarybuttonaccesskey="&updateRestart.cancelButton.accesskey;"
+ dropmarkerhidden="true"
+ checkboxhidden="true"
+ hidden="true">
+ <popupnotificationcontent id="update-restart-notification-content" orient="vertical">
+ <description id="update-restart-description">&updateRestart.message;</description>
+ </popupnotificationcontent>
+ </popupnotification>
+</panel>
--- a/browser/components/customizableui/content/panelUI.js
+++ b/browser/components/customizableui/content/panelUI.js
@@ -27,37 +27,51 @@ const PanelUI = {
get kElements() {
return {
contents: "PanelUI-contents",
mainView: "PanelUI-mainView",
multiView: "PanelUI-multiView",
helpView: "PanelUI-helpView",
menuButton: "PanelUI-menu-button",
panel: "PanelUI-popup",
- scroller: "PanelUI-contents-scroller"
+ notificationPanel: "PanelUI-notification-popup",
+ scroller: "PanelUI-contents-scroller",
+ footer: "PanelUI-footer"
};
},
_initialized: false,
init() {
for (let [k, v] of Object.entries(this.kElements)) {
// Need to do fresh let-bindings per iteration
let getKey = k;
let id = v;
this.__defineGetter__(getKey, function() {
delete this[getKey];
return this[getKey] = document.getElementById(id);
});
}
+ 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", false);
+ Services.obs.addObserver(this, "panelUI-notification-main-action", false);
+ Services.obs.addObserver(this, "panelUI-notification-dismissed", false);
+
+ window.addEventListener("fullscreen", this);
window.matchMedia("(-moz-overlay-scrollbars)").addListener(this._overlayScrollListenerBoundFn);
CustomizableUI.addListener(this);
+
+ for (let event of this.kEvents) {
+ this.notificationPanel.addEventListener(event, this);
+ }
+
this._initialized = true;
},
_eventListenersAdded: false,
_ensureEventListenersAdded() {
if (this._eventListenersAdded)
return;
this._addEventListeners();
@@ -70,17 +84,24 @@ const PanelUI = {
this.helpView.addEventListener("ViewShowing", this._onHelpViewShow);
this._eventListenersAdded = true;
},
uninit() {
for (let event of this.kEvents) {
this.panel.removeEventListener(event, this);
+ this.notificationPanel.removeEventListener(event, this);
}
+
+ Services.obs.removeObserver(this, "fullscreen-nav-toolbox");
+ Services.obs.removeObserver(this, "panelUI-notification-main-action");
+ Services.obs.removeObserver(this, "panelUI-notification-dismissed");
+
+ window.removeEventListener("fullscreen", this);
this.helpView.removeEventListener("ViewShowing", this._onHelpViewShow);
this.menuButton.removeEventListener("mousedown", this);
this.menuButton.removeEventListener("keypress", this);
window.matchMedia("(-moz-overlay-scrollbars)").removeListener(this._overlayScrollListenerBoundFn);
CustomizableUI.removeListener(this);
this._overlayScrollListenerBoundFn = null;
},
@@ -151,68 +172,160 @@ const PanelUI = {
} else {
anchor = aEvent.target;
}
this.panel.addEventListener("popupshown", function() {
resolve();
}, {once: true});
- let iconAnchor =
- document.getAnonymousElementByAttribute(anchor, "class",
- "toolbarbutton-icon");
- this.panel.openPopup(iconAnchor || anchor);
+ anchor = this._getPanelAnchor(anchor);
+ this.panel.openPopup(anchor);
}, (reason) => {
console.error("Error showing the PanelUI menu", reason);
});
});
},
+ 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));
+ }
+
+ 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.
*/
hide() {
if (document.documentElement.hasAttribute("customizing")) {
return;
}
this.panel.hidePopup();
},
+ observe(subject, topic, status) {
+ switch (topic) {
+ case "fullscreen-nav-toolbox":
+ this._updateNotifications();
+ break;
+ case "panelUI-notification-main-action":
+ if (subject != window) {
+ this.removeNotification(status);
+ }
+ break;
+ case "panelUI-notification-dismissed":
+ if (subject != window) {
+ this.dismissNotification(status);
+ }
+ break;
+ }
+ },
+
handleEvent(aEvent) {
// Ignore context menus and menu button menus showing and hiding:
if (aEvent.type.startsWith("popup") &&
aEvent.target != this.panel) {
return;
}
switch (aEvent.type) {
case "popupshowing":
this._adjustLabelsForAutoHyphens();
// Fall through
case "popupshown":
// Fall through
case "popuphiding":
// Fall through
case "popuphidden":
+ this._updateNotifications();
this._updatePanelButton(aEvent.target);
break;
case "mousedown":
if (aEvent.button == 0)
this.toggle(aEvent);
break;
case "keypress":
this.toggle(aEvent);
break;
+ case "fullscreen":
+ this._updateNotifications();
+ break;
}
},
get isReady() {
return !!this._isReady;
},
+ get isNotificationPanelOpen() {
+ let panelState = this.notificationPanel.state;
+
+ 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
* event that customization mode is started before the panel has been opened
* by the user.
*
* @param aCustomizing (optional) set to true if this was called while entering
* customization mode. If that's the case, we trust that customization
@@ -388,24 +501,23 @@ const PanelUI = {
panelRemover();
return;
}
viewShown = true;
CustomizableUI.addPanelCloseListeners(tempPanel);
tempPanel.addEventListener("popuphidden", panelRemover);
- let iconAnchor =
- document.getAnonymousElementByAttribute(aAnchor, "class",
- "toolbarbutton-icon");
+ let anchor = this._getPanelAnchor(aAnchor);
- if (iconAnchor && aAnchor.id) {
- iconAnchor.setAttribute("consumeanchor", aAnchor.id);
+ if (aAnchor != anchor && aAnchor.id) {
+ anchor.setAttribute("consumeanchor", aAnchor.id);
}
- tempPanel.openPopup(iconAnchor || aAnchor, "bottomcenter topright");
+
+ tempPanel.openPopup(anchor, "bottomcenter topright");
}
}),
/**
* NB: The enable- and disableSingleSubviewPanelAnimations methods only
* affect the hiding/showing animations of single-subview panels (tempPanel
* in the showSubView method).
*/
@@ -533,19 +645,246 @@ const PanelUI = {
quitButton.setAttribute("tooltiptext", tooltipString);
},
_overlayScrollListenerBoundFn: null,
_overlayScrollListener(aMQL) {
ScrollbarSampler.resetSystemScrollbarWidth();
this._scrollWidth = null;
},
+
+ _hidePopup() {
+ if (this.isNotificationPanelOpen) {
+ this.notificationPanel.hidePopup();
+ }
+ },
+
+ _updateNotifications() {
+ if (!this.notifications.length) {
+ this._clearAllNotifications();
+ this._hidePopup();
+ return;
+ }
+
+ if (window.fullScreen && FullScreen.navToolboxHidden) {
+ this._hidePopup();
+ return;
+ }
+
+ let doorhangers =
+ this.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
+ // since we don't want their doorhangers competing for attention
+ doorhangers.forEach(n => { n.dismissed = true; })
+ this._hidePopup();
+ this._clearBadge();
+ if (!this.notifications[0].options.badgeOnly) {
+ this._showMenuItem(this.notifications[0]);
+ }
+ } else if (doorhangers.length > 0) {
+ this._clearBadge();
+ this._showNotificationPanel(doorhangers[0]);
+ } else {
+ this._hidePopup();
+ this._showBadge(this.notifications[0]);
+ this._showMenuItem(this.notifications[0]);
+ }
+ },
+
+ _showNotificationPanel(notification) {
+ this._refreshNotificationPanel(notification);
+
+ if (this.isNotificationPanelOpen) {
+ return;
+ }
+
+ let anchor = this._getPanelAnchor(this.menuButton);
+
+ this.notificationPanel.hidden = false;
+ this.notificationPanel.openPopup(anchor, "bottomcenter topright");
+ },
+
+ _clearNotificationPanel() {
+ for (let popupnotification of this.notificationPanel.children) {
+ popupnotification.hidden = true;
+ popupnotification.notification = null;
+ }
+ },
+
+ _clearAllNotifications() {
+ this._clearNotificationPanel();
+ this._clearBadge();
+ this._clearMenuItems();
+ },
+
+ _refreshNotificationPanel(notification) {
+ this._clearNotificationPanel();
+
+ let popupnotificationID = this._getPopupId(notification);
+ let popupnotification = document.getElementById(popupnotificationID);
+
+ popupnotification.setAttribute("id", popupnotificationID);
+ popupnotification.setAttribute("buttoncommand", "PanelUI._onNotificationButtonEvent(event, 'buttoncommand');");
+ popupnotification.setAttribute("secondarybuttoncommand", "PanelUI._onNotificationButtonEvent(event, 'secondarybuttoncommand');");
+
+ popupnotification.notification = notification;
+ popupnotification.hidden = false;
+ },
+
+ _showBadge(notification) {
+ let badgeStatus = this._getBadgeStatus(notification);
+ this.menuButton.setAttribute("badge-status", badgeStatus);
+ },
+
+ // "Menu item" here refers to an item in the hamburger panel menu. They will
+ // typically show up as a colored row near the bottom of the panel.
+ _showMenuItem(notification) {
+ this._clearMenuItems();
+
+ let menuItemId = this._getMenuItemId(notification);
+ let menuItem = document.getElementById(menuItemId);
+ if (menuItem) {
+ menuItem.notification = notification;
+ menuItem.setAttribute("oncommand", "PanelUI._onNotificationMenuItemSelected(event)");
+ menuItem.classList.add("PanelUI-notification-menu-item");
+ menuItem.hidden = false;
+ menuItem.fromPanelUINotifications = true;
+ }
+ },
+
+ _clearBadge() {
+ this.menuButton.removeAttribute("badge-status");
+ },
+
+ _clearMenuItems() {
+ for (let child of this.footer.children) {
+ if (child.fromPanelUINotifications) {
+ child.notification = null;
+ child.hidden = true;
+ }
+ }
+ },
+
+ _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);
+
+ if (!notificationEl)
+ throw "PanelUI._onNotificationButtonEvent: couldn't find notification element";
+
+ if (!notificationEl.notification)
+ throw "PanelUI._onNotificationButtonEvent: couldn't find notification";
+
+ 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");
+ } else {
+ this._removeNotification(notification);
+ }
+ this._updateNotifications();
+ },
+
+ _onNotificationMenuItemSelected(event) {
+ let target = event.originalTarget;
+ if (!target.notification)
+ 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();
+ },
+
+ _getPopupId(notification) { return "PanelUI-" + notification.id + "-notification"; },
+
+ _getBadgeStatus(notification) { return notification.id; },
+
+ _getMenuItemId(notification) { return "PanelUI-" + notification.id + "-menu-item"; },
+
+ _getPanelAnchor(candidate) {
+ let iconAnchor =
+ document.getAnonymousElementByAttribute(candidate, "class",
+ "toolbarbutton-icon");
+ return iconAnchor || candidate;
+ },
+
+ _notify(status, topic) {
+ Services.obs.notifyObservers(window, "panelUI-notification-" + topic, status);
+ }
};
XPCOMUtils.defineConstant(this, "PanelUI", PanelUI);
/**
* Gets the currently selected locale for display.
* @return the selected locale
*/
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
+ // gross and very dependent on the structure of the popupnotification
+ // binding's content.
+ let notificationEl;
+ let parent = aElement;
+ while (parent && (parent = aElement.ownerDocument.getBindingParent(parent))) {
+ notificationEl = parent;
+ }
+ return notificationEl;
+}
--- a/browser/components/customizableui/test/browser.ini
+++ b/browser/components/customizableui/test/browser.ini
@@ -145,11 +145,12 @@ skip-if = os == "mac"
[browser_1087303_button_preferences.js]
[browser_1089591_still_customizable_after_reset.js]
[browser_1096763_seen_widgets_post_reset.js]
[browser_1161838_inserted_new_default_buttons.js]
[browser_bootstrapped_custom_toolbar.js]
[browser_customizemode_contextmenu_menubuttonstate.js]
[browser_exit_background_customize_mode.js]
[browser_panel_toggle.js]
+[browser_panelUINotifications.js]
[browser_switch_to_customize_mode.js]
[browser_synced_tabs_menu.js]
[browser_check_tooltips_in_navbar.js]
new file mode 100644
--- /dev/null
+++ b/browser/components/customizableui/test/browser_panelUINotifications.js
@@ -0,0 +1,305 @@
+"use strict";
+
+/**
+ * Tests that when we click on the main call-to-action of the doorhanger, the provided
+ * action is called, and the doorhanger removed.
+ */
+add_task(function* testMainActionCalled() {
+ let options = {
+ gBrowser: window.gBrowser,
+ url: "about:blank"
+ };
+
+ let extraWindow = yield BrowserTestUtils.openNewBrowserWindow();
+
+ yield BrowserTestUtils.withNewTab(options, function*(browser) {
+ let doc = browser.ownerDocument;
+
+ is(PanelUI.notificationPanel.state, "closed", "update-manual doorhanger is closed.");
+ let mainActionCalled = false;
+ let mainAction = {
+ callback: () => { mainActionCalled = true; }
+ };
+ PanelUI.showNotification("update-manual", mainAction);
+
+ let extraMainActionCalled = false;
+ let extraMainAction = {
+ callback: () => { extraMainActionCalled = true; }
+ };
+ extraWindow.PanelUI.showNotification("update-manual", extraMainAction)
+
+ 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, "PanelUI-update-manual-notification", "PanelUI is displaying the update-manual notification.");
+
+ let mainActionButton = doc.getAnonymousElementByAttribute(doorhanger, "anonid", "button");
+ mainActionButton.click();
+
+ ok(mainActionCalled, "Main action callback was called");
+ isnot(extraMainActionCalled, true, "Extra window's main action callback was not called");
+ is(PanelUI.notificationPanel.state, "closed", "update-manual doorhanger is closed.");
+ is(extraWindow.PanelUI.notificationPanel.state, "closed", "Extra window's update-manual doorhanger is closed.");
+ is(PanelUI.menuButton.hasAttribute("badge-status"), false, "Should not have a badge status");
+ });
+
+ yield BrowserTestUtils.closeWindow(extraWindow);
+});
+
+/**
+ * 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.
+ * Once we click on this button, we should see an item in the menu which will
+ * call our main action.
+ */
+add_task(function* testSecondaryActionWorkflow() {
+ let options = {
+ gBrowser: window.gBrowser,
+ url: "about:blank"
+ };
+
+ let extraWindow = yield BrowserTestUtils.openNewBrowserWindow();
+
+ yield BrowserTestUtils.withNewTab(options, function*(browser) {
+ let doc = browser.ownerDocument;
+
+ is(PanelUI.notificationPanel.state, "closed", "update-manual doorhanger is closed.");
+
+ let mainActionCalled = false;
+ let mainAction = {
+ callback: () => { mainActionCalled = true; },
+ };
+ PanelUI.showNotification("update-manual", mainAction);
+
+ let extraMainActionCalled = false;
+ let extraMainAction = {
+ callback: () => { extraMainActionCalled = true; }
+ };
+ extraWindow.PanelUI.showNotification("update-manual", extraMainAction)
+
+ 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, "PanelUI-update-manual-notification", "PanelUI is displaying the update-manual notification.");
+
+ let secondaryActionButton = doc.getAnonymousElementByAttribute(doorhanger, "anonid", "secondarybutton");
+ secondaryActionButton.click();
+
+ is(PanelUI.notificationPanel.state, "closed", "update-manual doorhanger is closed.");
+ is(extraWindow.PanelUI.notificationPanel.state, "closed", "Extra window's update-manual doorhanger is closed.");
+
+ is(PanelUI.menuButton.getAttribute("badge-status"), "update-manual", "Badge is displaying on PanelUI button.");
+
+ yield PanelUI.show();
+ isnot(PanelUI.menuButton.getAttribute("badge-status"), "update-manual", "Badge is hidden on PanelUI button.");
+ let menuItem = doc.getElementById("PanelUI-update-manual-menu-item");
+ is(menuItem.hidden, false, "update-manual menu item is showing.");
+
+ yield PanelUI.hide();
+ is(PanelUI.menuButton.getAttribute("badge-status"), "update-manual", "Badge is shown on PanelUI button.");
+
+ yield PanelUI.show();
+ menuItem.click();
+ ok(mainActionCalled, "Main action callback was called");
+ isnot(extraMainActionCalled, true, "Extra window's main action callback was not called");
+
+ PanelUI.removeNotification(/.*/);
+ });
+
+ yield BrowserTestUtils.closeWindow(extraWindow);
+});
+
+/**
+ * We want to ensure a few things with this:
+ * - Adding a doorhanger will make a badge disappear
+ * - once the notification for the doorhanger is resolved (removed, not just dismissed),
+ * then we display any other badges that are remaining.
+ */
+add_task(function* testInteractionWithBadges() {
+ yield BrowserTestUtils.withNewTab("about:blank", function*(browser) {
+ let doc = browser.ownerDocument;
+
+ PanelUI.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.");
+
+ let mainActionCalled = false;
+ let mainAction = {
+ callback: () => { mainActionCalled = true; },
+ };
+ PanelUI.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.");
+ 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, "PanelUI-update-manual-notification", "PanelUI is displaying the update-manual notification.");
+
+ let secondaryActionButton = doc.getAnonymousElementByAttribute(doorhanger, "anonid", "secondarybutton");
+ secondaryActionButton.click();
+
+ is(PanelUI.notificationPanel.state, "closed", "update-manual doorhanger is closed.");
+
+ is(PanelUI.menuButton.getAttribute("badge-status"), "update-manual", "Badge is displaying on PanelUI button.");
+
+ yield PanelUI.show();
+ isnot(PanelUI.menuButton.getAttribute("badge-status"), "update-manual", "Badge is hidden on PanelUI button.");
+ let menuItem = doc.getElementById("PanelUI-update-manual-menu-item");
+ is(menuItem.hidden, false, "update-manual menu item is showing.");
+
+ menuItem.click();
+ 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(/.*/);
+ is(PanelUI.menuButton.hasAttribute("badge-status"), false, "Should not have a badge status");
+ });
+});
+
+/**
+ * This tests that adding a badge will not dismiss any existing doorhangers.
+ */
+add_task(function* testAddingBadgeWhileDoorhangerIsShowing() {
+ yield BrowserTestUtils.withNewTab("about:blank", function*(browser) {
+ let doc = browser.ownerDocument;
+
+ is(PanelUI.notificationPanel.state, "closed", "update-manual doorhanger is closed.");
+ let mainActionCalled = false;
+ let mainAction = {
+ callback: () => { mainActionCalled = true; }
+ };
+ PanelUI.showNotification("update-manual", mainAction);
+ PanelUI.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.");
+ 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, "PanelUI-update-manual-notification", "PanelUI is displaying the update-manual notification.");
+
+ 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.getAttribute("badge-status"), "fxa-needs-authentication", "Fxa badge is shown on PanelUI button.");
+ PanelUI.removeNotification(/.*/);
+ is(PanelUI.menuButton.hasAttribute("badge-status"), false, "Should not have a badge status");
+ });
+});
+
+/**
+ * Tests that badges operate like a stack.
+ */
+add_task(function* testMultipleBadges() {
+ yield BrowserTestUtils.withNewTab("about:blank", function*(browser) {
+ let doc = browser.ownerDocument;
+ let menuButton = doc.getElementById("PanelUI-menu-button");
+
+ 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");
+ is(menuButton.getAttribute("badge-status"), "fxa-needs-authentication", "Should have fxa-needs-authentication badge status");
+
+ PanelUI.showBadgeOnlyNotification("update-succeeded");
+ is(menuButton.getAttribute("badge-status"), "update-succeeded", "Should have update-succeeded badge status (update > fxa)");
+
+ PanelUI.showBadgeOnlyNotification("update-failed");
+ is(menuButton.getAttribute("badge-status"), "update-failed", "Should have update-failed badge status");
+
+ PanelUI.showBadgeOnlyNotification("download-severe");
+ is(menuButton.getAttribute("badge-status"), "download-severe", "Should have download-severe badge status");
+
+ PanelUI.showBadgeOnlyNotification("download-warning");
+ is(menuButton.getAttribute("badge-status"), "download-warning", "Should have download-warning badge status");
+
+ PanelUI.removeNotification(/^download-/);
+ is(menuButton.getAttribute("badge-status"), "update-failed", "Should have update-failed badge status");
+
+ PanelUI.removeNotification(/^update-/);
+ is(menuButton.getAttribute("badge-status"), "fxa-needs-authentication", "Should have fxa-needs-authentication badge status");
+
+ PanelUI.removeNotification(/^fxa-/);
+ is(menuButton.hasAttribute("badge-status"), false, "Should not have a badge status");
+
+ yield 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(/.*/);
+ is(menuButton.hasAttribute("badge-status"), false, "Should not have a badge status");
+ });
+});
+
+/**
+ * Tests that non-badges also operate like a stack.
+ */
+add_task(function* testMultipleNonBadges() {
+ yield BrowserTestUtils.withNewTab("about:blank", function*(browser) {
+ let doc = browser.ownerDocument;
+
+ is(PanelUI.notificationPanel.state, "closed", "update-manual doorhanger is closed.");
+
+ let updateManualAction = {
+ called: false,
+ callback: () => { updateManualAction.called = true; },
+ };
+ let updateRestartAction = {
+ called: false,
+ callback: () => { updateRestartAction.called = true; },
+ };
+
+ PanelUI.showNotification("update-manual", updateManualAction);
+
+ let notifications;
+ let doorhanger;
+
+ isnot(PanelUI.notificationPanel.state, "closed", "Doorhanger is showing.");
+ notifications = [...PanelUI.notificationPanel.children].filter(n => !n.hidden);
+ is(notifications.length, 1, "PanelUI doorhanger is only displaying one notification.");
+ doorhanger = notifications[0];
+ is(doorhanger.id, "PanelUI-update-manual-notification", "PanelUI is displaying the update-manual notification.");
+
+ PanelUI.showNotification("update-restart", updateRestartAction);
+
+ isnot(PanelUI.notificationPanel.state, "closed", "Doorhanger is showing.");
+ notifications = [...PanelUI.notificationPanel.children].filter(n => !n.hidden);
+ is(notifications.length, 1, "PanelUI doorhanger is only displaying one notification.");
+ doorhanger = notifications[0];
+ is(doorhanger.id, "PanelUI-update-restart-notification", "PanelUI is displaying the update-restart notification.");
+
+ let secondaryActionButton = doc.getAnonymousElementByAttribute(doorhanger, "anonid", "secondarybutton");
+ secondaryActionButton.click();
+
+ is(PanelUI.notificationPanel.state, "closed", "update-manual doorhanger is closed.");
+ is(PanelUI.menuButton.getAttribute("badge-status"), "update-restart", "update-restart badge is displaying on PanelUI button.");
+
+ let menuItem;
+
+ yield PanelUI.show();
+ isnot(PanelUI.menuButton.getAttribute("badge-status"), "update-restart", "update-restart badge is hidden on PanelUI button.");
+ menuItem = doc.getElementById("PanelUI-update-restart-menu-item");
+ is(menuItem.hidden, false, "update-restart menu item is showing.");
+
+ menuItem.click();
+ ok(updateRestartAction.called, "update-restart main action callback was called");
+
+ is(PanelUI.notificationPanel.state, "closed", "update-manual doorhanger is closed.");
+ is(PanelUI.menuButton.getAttribute("badge-status"), "update-manual", "update-manual badge is displaying on PanelUI button.");
+
+ yield PanelUI.show();
+ isnot(PanelUI.menuButton.getAttribute("badge-status"), "update-manual", "update-manual badge is hidden on PanelUI button.");
+ menuItem = doc.getElementById("PanelUI-update-manual-menu-item");
+ is(menuItem.hidden, false, "update-manual menu item is showing.");
+
+ menuItem.click();
+ ok(updateManualAction.called, "update-manual main action callback was called");
+ });
+});
--- a/browser/components/downloads/content/indicator.js
+++ b/browser/components/downloads/content/indicator.js
@@ -502,25 +502,23 @@ const DownloadsIndicatorView = {
// progress in toolbar
let suppressAttention = DownloadsCommon.arrowStyledIndicator && !inMenu &&
this._attention == DownloadsCommon.ATTENTION_SUCCESS &&
this._percentComplete >= 0;
if (suppressAttention || this._attention == DownloadsCommon.ATTENTION_NONE) {
this.indicator.removeAttribute("attention");
if (inMenu) {
- gMenuButtonBadgeManager.removeBadge(
- gMenuButtonBadgeManager.BADGEID_DOWNLOAD);
+ PanelUI.removeNotification(/^download-/);
}
} else {
this.indicator.setAttribute("attention", this._attention);
if (inMenu) {
let badgeClass = "download-" + this._attention;
- gMenuButtonBadgeManager.addBadge(
- gMenuButtonBadgeManager.BADGEID_DOWNLOAD, badgeClass);
+ PanelUI.showBadgeOnlyNotification(badgeClass);
}
}
},
_attention: DownloadsCommon.ATTENTION_NONE,
// User interface event functions
onWindowUnload() {
--- a/browser/locales/en-US/chrome/browser/browser.dtd
+++ b/browser/locales/en-US/chrome/browser/browser.dtd
@@ -893,8 +893,36 @@ you can use these alternative items. Oth
<!ENTITY panicButton.view.forgetButton "Forget!">
<!ENTITY panicButton.thankyou.msg1 "Your recent history is cleared.">
<!ENTITY panicButton.thankyou.msg2 "Safe browsing!">
<!ENTITY panicButton.thankyou.buttonlabel "Thanks!">
<!ENTITY emeLearnMoreContextMenu.label "Learn more about DRM…">
<!ENTITY emeLearnMoreContextMenu.accesskey "D">
+
+<!ENTITY updateAvailable.message "Update your &brandShorterName; for the latest in speed and privacy.">
+<!ENTITY updateAvailable.whatsnew.label "See what’s new.">
+<!ENTITY updateAvailable.whatsnew.href "http://www.mozilla.org/">
+<!ENTITY updateAvailable.header.message "A new &brandShorterName; update is available.">
+<!ENTITY updateAvailable.acceptButton.label "Download Update">
+<!ENTITY updateAvailable.acceptButton.accesskey "D">
+<!ENTITY updateAvailable.cancelButton.label "Not Now">
+<!ENTITY updateAvailable.cancelButton.accesskey "N">
+<!ENTITY updateAvailable.panelUI.label "Download &brandShorterName; update">
+
+<!ENTITY updateManual.message "Download a fresh copy of &brandShorterName; and we’ll help you to install it.">
+<!ENTITY updateManual.whatsnew.label "See what’s new.">
+<!ENTITY updateManual.whatsnew.href "http://www.mozilla.org/">
+<!ENTITY updateManual.header.message "&brandShorterName; can’t update to the latest version.">
+<!ENTITY updateManual.acceptButton.label "Download &brandShorterName;">
+<!ENTITY updateManual.acceptButton.accesskey "D">
+<!ENTITY updateManual.cancelButton.label "Not Now">
+<!ENTITY updateManual.cancelButton.accesskey "N">
+<!ENTITY updateManual.panelUI.label "Download a fresh copy of &brandShorterName;">
+
+<!ENTITY updateRestart.message "After a quick restart, &brandShorterName; will restore all your open tabs and windows.">
+<!ENTITY updateRestart.header.message "Restart &brandShorterName; to apply update.">
+<!ENTITY updateRestart.acceptButton.label "Restart and Restore">
+<!ENTITY updateRestart.acceptButton.accesskey "R">
+<!ENTITY updateRestart.cancelButton.label "Not Now">
+<!ENTITY updateRestart.cancelButton.accesskey "N">
+<!ENTITY updateRestart.panelUI.label "Restart &brandShorterName; to apply update">
--- a/browser/locales/en-US/chrome/browser/browser.properties
+++ b/browser/locales/en-US/chrome/browser/browser.properties
@@ -756,25 +756,16 @@ flashHang.helpButton.accesskey = L
# be replaced with a hyperlink containing the text defined in customizeTips.tip0.learnMore.
customizeTips.tip0 = %1$S: You can customize %2$S to work the way you do. Simply drag any of the above to the menu or toolbar. %3$S about customizing %2$S.
customizeTips.tip0.hint = Hint
customizeTips.tip0.learnMore = Learn more
# LOCALIZATION NOTE (customizeMode.tabTitle): %S is brandShortName
customizeMode.tabTitle = Customize %S
-# LOCALIZATION NOTE(appmenu.*.description, appmenu.*.label): these are used for
-# the appmenu labels and buttons that appear when an update is staged for
-# installation or a background update has failed and a manual download is required.
-# %S is brandShortName
-appmenu.restartNeeded.description = Restart %S to apply updates
-appmenu.updateFailed.description = Background update failed, please download update
-appmenu.restartBrowserButton.label = Restart %S
-appmenu.downloadUpdateButton.label = Download Update
-
# LOCALIZATION NOTE : FILE Reader View is a feature name and therefore typically used as a proper noun.
readingList.promo.firstUse.readerView.title = Reader View
readingList.promo.firstUse.readerView.body = Remove clutter so you can focus exactly on what you want to read.
# LOCALIZATION NOTE (appMenuRemoteTabs.mobilePromo.text2):
# %1$S will be replaced with a link, the text of which is
# appMenuRemoteTabs.mobilePromo.android and the link will be to
--- a/browser/themes/shared/customizableui/panelUI.inc.css
+++ b/browser/themes/shared/customizableui/panelUI.inc.css
@@ -105,24 +105,41 @@
background-size: contain;
border: none;
}
#PanelUI-menu-button[badge-status="download-success"] > .toolbarbutton-badge-stack > .toolbarbutton-badge {
display: none;
}
-#PanelUI-menu-button[badge-status="update-succeeded"] > .toolbarbutton-badge-stack > .toolbarbutton-badge {
+#PanelUI-menu-button[badge-status="update-available"] > .toolbarbutton-badge-stack > .toolbarbutton-badge,
+#PanelUI-menu-button[badge-status="update-manual"] > .toolbarbutton-badge-stack > .toolbarbutton-badge,
+#PanelUI-menu-button[badge-status="update-restart"] > .toolbarbutton-badge-stack > .toolbarbutton-badge {
background: #74BF43 url(chrome://browser/skin/update-badge.svg) no-repeat center;
- height: 13px;
+ border-radius: 50%;
+ box-shadow: none;
+ border: 1px solid -moz-dialog;
+ /* "!important" is necessary to override the rule in toolbarbutton.css */
+ margin: -9px 0 0 !important;
+ margin-inline-end: -6px !important;
+ min-width: 16px;
+ min-height: 16px;
}
-#PanelUI-menu-button[badge-status="update-failed"] > .toolbarbutton-badge-stack > .toolbarbutton-badge {
- background: #D90000 url(chrome://browser/skin/update-badge-failed.svg) no-repeat center;
- height: 13px;
+#PanelUI-update-restart-menu-item::after,
+#PanelUI-update-available-menu-item::after,
+#PanelUI-update-manual-menu-item::after {
+ background: #74BF43 url(chrome://browser/skin/update-badge.svg) no-repeat center;
+ border-radius: 50%;
+}
+
+#PanelUI-update-restart-menu-item,
+#PanelUI-update-available-menu-item,
+#PanelUI-update-manual-menu-item {
+ list-style-image: url(chrome://branding/content/icon16.png);
}
#PanelUI-menu-button[badge-status="download-warning"] > .toolbarbutton-badge-stack > .toolbarbutton-badge,
#PanelUI-menu-button[badge-status="fxa-needs-authentication"] > .toolbarbutton-badge-stack > .toolbarbutton-badge {
box-shadow: none;
filter: drop-shadow(0 1px 0 hsla(206, 50%, 10%, .15));
}
@@ -439,21 +456,21 @@ toolbaritem[cui-areatype="menu-panel"][s
}
#PanelUI-multiView[viewtype="subview"] > .panel-viewcontainer > .panel-viewstack > .panel-mainview > #PanelUI-mainView {
background-color: var(--arrowpanel-dimmed);
}
#PanelUI-multiView[viewtype="subview"] #PanelUI-mainView > #PanelUI-contents-scroller > #PanelUI-contents > .panel-wide-item,
#PanelUI-multiView[viewtype="subview"] #PanelUI-mainView > #PanelUI-contents-scroller > #PanelUI-contents > .toolbarbutton-1:not([panel-multiview-anchor="true"]),
-#PanelUI-multiView[viewtype="subview"] #PanelUI-mainView > #PanelUI-footer > #PanelUI-update-status,
+#PanelUI-multiView[viewtype="subview"] #PanelUI-mainView > #PanelUI-footer > .PanelUI-notification-menu-item,
#PanelUI-multiView[viewtype="subview"] #PanelUI-mainView > #PanelUI-footer > #PanelUI-footer-fxa > #PanelUI-fxa-status > #PanelUI-fxa-avatar,
#PanelUI-multiView[viewtype="subview"] #PanelUI-mainView > #PanelUI-footer > #PanelUI-footer-fxa > #PanelUI-fxa-status > #PanelUI-fxa-label,
#PanelUI-multiView[viewtype="subview"] #PanelUI-mainView > #PanelUI-footer > #PanelUI-footer-fxa > #PanelUI-fxa-icon,
-#PanelUI-multiView[viewtype="subview"] #PanelUI-mainView > #PanelUI-footer > #PanelUI-footer-inner > toolbarseparator,
+#PanelUI-multiView[viewtype="subview"] #PanelUI-mainView > #PanelUI-footer > #PanelUI-footer-inner > #PanelUI-footer-inner > toolbarseparator,
#PanelUI-multiView[viewtype="subview"] #PanelUI-mainView > #PanelUI-footer > #PanelUI-footer-inner > #PanelUI-customize,
#PanelUI-multiView[viewtype="subview"] #PanelUI-mainView > #PanelUI-footer > #PanelUI-footer-inner > #PanelUI-help:not([panel-multiview-anchor="true"]) {
opacity: .5;
}
/*
* XXXgijs: this is a workaround for a layout issue that was caused by these iframes,
* which was affecting subview display. Because of this, we're hiding the iframe *only*
@@ -561,38 +578,25 @@ toolbarpaletteitem[place="palette"] > to
width: 47px;
padding-top: 1px;
display: block;
text-align: center;
position: relative;
top: 25%;
}
-#PanelUI-update-status[update-status]::after,
-#PanelUI-footer-addons > toolbarbutton::after {
+#PanelUI-footer-addons > toolbarbutton::after,
+.PanelUI-notification-menu-item::after {
content: "";
- width: 14px;
- height: 14px;
+ width: 16px;
+ height: 16px;
margin-inline-end: 16.5px;
- box-shadow: 0px 1px 0px rgba(255,255,255,.2) inset, 0px -1px 0px rgba(0,0,0,.1) inset, 0px 1px 0px rgba(12,27,38,.2);
- border-radius: 2px;
- background-size: contain;
display: -moz-box;
}
-#PanelUI-update-status[update-status="succeeded"]::after {
- background-image: url(chrome://browser/skin/update-badge.svg);
- background-color: #74BF43;
-}
-
-#PanelUI-update-status[update-status="failed"]::after {
- background-image: url(chrome://browser/skin/update-badge-failed.svg);
- background-color: #D90000;
-}
-
#PanelUI-footer-addons > toolbarbutton {
background-color: #FFEFBF;
/* Force border to override `#PanelUI-footer-addons > toolbarbutton` selector below */
border-top: 1px solid hsl(45, 100%, 77%) !important;
display: flex;
flex: 1 1 0%;
width: calc(@menuPanelWidth@ + 30px);
padding-inline-start: 15px;
@@ -641,17 +645,17 @@ toolbarpaletteitem[place="palette"] > to
-moz-appearance: none;
}
#PanelUI-footer-inner:hover > toolbarseparator,
#PanelUI-footer-fxa:hover > toolbarseparator {
margin: 0;
}
-#PanelUI-update-status,
+.PanelUI-notification-menu-item,
#PanelUI-help,
#PanelUI-fxa-label,
#PanelUI-fxa-icon,
#PanelUI-footer-addons > toolbarbutton,
#PanelUI-customize,
#PanelUI-quit {
margin: 0;
padding: 11px 0;
@@ -660,50 +664,47 @@ toolbarpaletteitem[place="palette"] > to
-moz-appearance: none;
box-shadow: none;
border: none;
border-radius: 0;
transition: background-color;
-moz-box-orient: horizontal;
}
-#PanelUI-update-status {
+.PanelUI-notification-menu-item {
border-top: 1px solid var(--panel-separator-color);
-}
-
-#PanelUI-update-status {
border-bottom: 1px solid transparent;
margin-bottom: -1px;
}
-#PanelUI-update-status > .toolbarbutton-text {
+.PanelUI-notification-menu-item > .toolbarbutton-text {
width: 0; /* Fancy cropping solution for flexbox. */
}
#PanelUI-help,
#PanelUI-quit {
min-width: 46px;
}
-#PanelUI-update-status > .toolbarbutton-text,
+.PanelUI-notification-menu-item > .toolbarbutton-text,
#PanelUI-fxa-label > .toolbarbutton-text,
#PanelUI-footer-addons > toolbarbutton > .toolbarbutton-text,
#PanelUI-customize > .toolbarbutton-text {
margin: 0;
padding: 0 6px;
text-align: start;
}
#PanelUI-help > .toolbarbutton-text,
#PanelUI-quit > .toolbarbutton-text,
#PanelUI-fxa-avatar > .toolbarbutton-text {
display: none;
}
-#PanelUI-update-status > .toolbarbutton-icon,
+.PanelUI-notification-menu-item > .toolbarbutton-icon,
#PanelUI-fxa-label > .toolbarbutton-icon,
#PanelUI-fxa-icon > .toolbarbutton-icon,
#PanelUI-customize > .toolbarbutton-icon,
#PanelUI-help > .toolbarbutton-icon,
#PanelUI-quit > .toolbarbutton-icon {
margin-inline-end: 0;
}
@@ -720,26 +721,24 @@ toolbarpaletteitem[place="palette"] > to
border-inline-start-style: none;
}
#PanelUI-footer-fxa[fxaprofileimage="set"] > #PanelUI-fxa-status > #PanelUI-fxa-label,
#PanelUI-footer-fxa[fxaprofileimage="enabled"]:not([fxastatus="error"]) > #PanelUI-fxa-status > #PanelUI-fxa-label {
padding-inline-start: 0px;
}
-#PanelUI-update-status {
+/* descend from #PanelUI-footer to add specificity, or else the
+ padding-inline-start will be overridden */
+#PanelUI-footer > .PanelUI-notification-menu-item {
width: calc(@menuPanelWidth@ + 30px);
padding-inline-start: 15px;
border-inline-start-style: none;
}
-#PanelUI-update-status {
- list-style-image: url(chrome://branding/content/icon16.png);
-}
-
#PanelUI-fxa-label,
#PanelUI-fxa-icon {
list-style-image: url(chrome://browser/skin/sync-horizontalbar.png);
}
#PanelUI-remotetabs {
--panel-ui-sync-illustration-height: 157.5px;
}
@@ -975,44 +974,29 @@ toolbarpaletteitem[place="palette"] > to
background-color: hsl(42,94%,85%);
}
#PanelUI-footer-fxa[fxastatus="error"] > #PanelUI-fxa-status:hover:active {
background-color: hsl(42,94%,82%);
box-shadow: 0 1px 0 hsla(210,4%,10%,.05) inset;
}
-#PanelUI-update-status {
+.PanelUI-notification-menu-item {
color: black;
-}
-
-#PanelUI-update-status[update-status="succeeded"] {
background-color: hsla(96,65%,75%,.5);
}
-#PanelUI-update-status[update-status="succeeded"]:not([disabled]):hover {
+.PanelUI-notification-menu-item:not([disabled]):hover {
background-color: hsla(96,65%,75%,.8);
}
-#PanelUI-update-status[update-status="succeeded"]:not([disabled]):hover:active {
+.PanelUI-notification-menu-item:not([disabled]):hover:active {
background-color: hsl(96,65%,75%);
}
-#PanelUI-update-status[update-status="failed"] {
- background-color: hsla(359,69%,84%,.5);
-}
-
-#PanelUI-update-status[update-status="failed"]:not([disabled]):hover {
- background-color: hsla(359,69%,84%,.8);
-}
-
-#PanelUI-update-status[update-status="failed"]:not([disabled]):hover:active {
- background-color: hsl(359,69%,84%);
-}
-
#PanelUI-quit:not([disabled]):hover {
background-color: #d94141;
outline-color: #c23a3a;
}
#PanelUI-quit:not([disabled]):hover:active {
background-color: #ad3434;
outline-color: #992e2e;
@@ -1707,17 +1691,19 @@ menuitem[checked="true"].subviewbutton >
}
#PanelUI-help[panel-multiview-anchor="true"]:-moz-locale-dir(rtl)::after,
toolbarbutton[panel-multiview-anchor="true"]:-moz-locale-dir(rtl) {
background-image: url(chrome://browser/skin/customizableui/subView-arrow-back-inverted-rtl@2x.png),
linear-gradient(rgba(255,255,255,0.3), transparent);
}
- #PanelUI-update-status {
+ #PanelUI-update-restart-menu-item,
+ #PanelUI-update-available-menu-item,
+ #PanelUI-update-manual-menu-item {
list-style-image: url(chrome://branding/content/icon32.png);
}
#PanelUI-fxa-label,
#PanelUI-fxa-icon {
list-style-image: url(chrome://browser/skin/sync-horizontalbar@2x.png);
}
@@ -1744,17 +1730,17 @@ menuitem[checked="true"].subviewbutton >
#PanelUI-fxa-label,
#PanelUI-fxa-icon,
#PanelUI-customize,
#PanelUI-help,
#PanelUI-quit {
-moz-image-region: rect(0, 32px, 32px, 0);
}
- #PanelUI-update-status > .toolbarbutton-icon,
+ .PanelUI-notification-menu-item > .toolbarbutton-icon,
#PanelUI-fxa-label > .toolbarbutton-icon,
#PanelUI-fxa-icon > .toolbarbutton-icon,
#PanelUI-customize > .toolbarbutton-icon,
#PanelUI-help > .toolbarbutton-icon,
#PanelUI-quit > .toolbarbutton-icon {
width: 16px;
}
--- a/browser/themes/shared/notification-icons.inc.css
+++ b/browser/themes/shared/notification-icons.inc.css
@@ -301,8 +301,16 @@ html|*#webRTC-previewVideo {
-moz-image-region: rect(0px, 32px, 32px, 0px);
}
.translation-icon.in-use {
-moz-image-region: rect(0px, 64px, 32px, 32px);
}
}
%endif
+
+/* UPDATE */
+.popup-notification-icon[popupid="update-available"],
+.popup-notification-icon[popupid="update-manual"],
+.popup-notification-icon[popupid="update-restart"] {
+ background: #74BF43 url(chrome://browser/skin/notification-icons.svg#update) no-repeat center;
+ border-radius: 50%;
+}
--- a/browser/themes/shared/notification-icons.svg
+++ b/browser/themes/shared/notification-icons.svg
@@ -39,16 +39,22 @@
}
#camera-indicator,
#microphone-indicator,
#screen-indicator {
fill: white;
fill-opacity: 1;
}
+
+ #update-icon {
+ stroke: #fff;
+ stroke-width: 3px;
+ stroke-linecap: round;
+ }
</style>
<defs>
<path id="camera-icon" d="m 2,23 a 3,3 0 0 0 3,3 l 14,0 a 3,3 0 0 0 3,-3 l 0,-4 6,5.5 c 0.5,0.5 1,0.7 2,0.5 l 0,-18 c -1,-0.2 -1.5,0 -2,0.5 l -6,5.5 0,-4 a 3,3 0 0 0 -3,-3 l -14,0 a 3,3 0 0 0 -3,3 z" />
<path id="desktop-notification-icon" d="m 2,20 a 4,4 0 0 0 4,4 l 13,0 7,7 0,-7 a 4,4 0 0 0 4,-4 l 0,-12 a 4,4 0 0 0 -4,-4 l -20,0 a 4,4 0 0 0 -4,4 z m 5,-2 a 1,1 0 1 1 0,-2 l 10,0 a 1,1 0 1 1 0,2 z m 0,-4 a 1,1 0 1 1 0,-2 l 14,0 a 1,1 0 1 1 0,2 z m 0,-4 a 1,1 0 1 1 0,-2 l 18,0 a 1,1 0 1 1 0,2 z" />
<path id="geo-linux-icon" d="m 2,15.9 a 14,14 0 1 1 0,0.2 z m 4,2.1 a 10,10 0 0 0 8,8 l 0,-4 4,0 0,4 a 10,10 0 0 0 8,-8 l -4,0 0,-4 4,0 a 10,10 0 0 0 -8,-8 l 0,4 -4,0 0,-4 a 10,10 0 0 0 -8,8 l 4,0 0,4 z" />
<path id="geo-linux-detailed-icon" d="m 2,15.9 a 14,14 0 1 1 0,0.2 z m 3,2.1 a 11,11 0 0 0 9,9 l 1,-5 2,0 1,5 a 11,11 0 0 0 9,-9 l -5,-1 0,-2 5,-1 a 11,11 0 0 0 -9,-9 l -1,5 -2,0 -1,-5 a 11,11 0 0 0 -9,9 l 5,1 0,2 z" />
<path id="geo-osx-icon" d="m 0,16 16,0 0,16 12,-28 z" />
@@ -57,16 +63,17 @@
<path id="indexedDB-icon" d="m 2,24 a 4,4 0 0 0 4,4 l 2,0 0,-4 -2,0 0,-16 20,0 0,16 -2,0 0,4 2,0 a 4,4 0 0 0 4,-4 l 0,-16 a 4,4 0 0 0 -4,-4 l -20,0 a 4,4 0 0 0 -4,4 z m 8,-2 6,7 6,-7 -4,0 0,-8 -4,0 0,8 z" />
<path id="login-icon" d="m 2,26 0,4 6,0 0,-2 2,0 0,-2 1,0 0,-1 2,0 0,-3 2,0 2.5,-2.5 1.5,1.5 3,-3 a 8,8 0 1 0 -8,-8 l -3,3 2,2 z m 20,-18.1 a 2,2 0 1 1 0,0.2 z" />
<path id="login-detailed-icon" d="m 1,27 0,3.5 a 0.5,0.5 0 0 0 0.5,0.5 l 5,0 a 0.5,0.5 0 0 0 0.5,-0.5 l 0,-1.5 1.5,0 a 0.5,0.5 0 0 0 0.5,-0.5 l 0,-1.5 1,0 a 0.5,0.5 0 0 0 0.5,-0.5 l 0,-1 1,0 a 0.5,0.5 0 0 0 0.5,-0.5 l 0,-2 2,0 2.5,-2.5 q 0.5,-0.5 1,0 l 1,1 c 0.5,0.5 1,0.5 1.5,-0.5 l 1,-2 a 9,9 0 1 0 -8,-8 l -2,1 c -1,0.5 -1,1 -0.5,1.5 l 1.5,1.5 q 0.5,0.5 0,1 z m 21,-19.1 a 2,2 0 1 1 0,0.2 z" />
<path id="microphone-icon" d="m 8,14 0,4 a 8,8 0 0 0 6,7.7 l 0,2.3 -2,0 a 2,2 0 0 0 -2,2 l 12,0 a 2,2 0 0 0 -2,-2 l -2,0 0,-2.3 a 8,8 0 0 0 6,-7.7 l 0,-4 -2,0 0,4 a 6,6 0 0 1 -12,0 l 0,-4 z m 4,4 a 4,4 0 0 0 8,0 l 0,-12 a 4,4 0 0 0 -8,0 z" />
<path id="microphone-detailed-icon" d="m 8,18 a 8,8 0 0 0 6,7.7 l 0,2.3 -1,0 a 3,2 0 0 0 -3,2 l 12,0 a 3,2 0 0 0 -3,-2 l -1,0 0,-2.3 a 8,8 0 0 0 6,-7.7 l 0,-4 a 1,1 0 0 0 -2,0 l 0,4 a 6,6 0 0 1 -12,0 l 0,-4 a 1,1 0 0 0 -2,0 z m 4,0 a 4,4 0 0 0 8,0 l 0,-12 a 4,4 0 0 0 -8,0 z" />
<path id="plugin-icon" d="m 2,26 a 2,2 0 0 0 2,2 l 24,0 a 2,2 0 0 0 2,-2 l 0,-16 a 2,2 0 0 0 -2,-2 l -24,0 a 2,2 0 0 0 -2,2 z m 2,-20 10,0 0,-2 a 2,2 0 0 0 -2,-2 l -6,0 a 2,2 0 0 0 -2,2 z m 14,0 10,0 0,-2 a 2,2 0 0 0 -2,-2 l -6,0 a 2,2 0 0 0 -2,2 z" />
<path id="popup-icon" d="m 2,24 a 4,4 0 0 0 4,4 l 8,0 a 10,10 0 0 1 -2,-4 l -4,0 a 2,2 0 0 1 -2,-2 l 0,-12 18,0 0,2 a 10,10 0 0 1 4,2 l 0,-8 a 4,4 0 0 0 -4,-4 l -18,0 a 4,4 0 0 0 -4,4 z m 12,-2.1 a 8,8 0 1 1 0,0.2 m 10.7,-4.3 a 5,5 0 0 0 -6.9,6.9 z m -5.4,8.4 a 5,5 0 0 0 6.9,-6.9 z" />
<path id="screen-icon" d="m 2,18 a 2,2 0 0 0 2,2 l 2,0 0,-6 a 4,4 0 0 1 4,-4 l 14,0 0,-6 a 2,2 0 0 0 -2,-2 l -18,0 a 2,2 0 0 0 -2,2 z m 6,10 a 2,2 0 0 0 2,2 l 18,0 a 2,2 0 0 0 2,-2 l 0,-14 a 2,2 0 0 0 -2,-2 l -18,0 a 2,2 0 0 0 -2,2 z" />
+ <path id="update-icon" d="M 16,9 L 16,24 M 16,9 L 11,14 M 16,9 L 21,14" />
<clipPath id="blocked-clipPath">
<path d="m 0,0 0,31 31,-31 z m 6,32 26,0 0,-26 z"/>
</clipPath>
<mask id="i-mask" style="fill-opacity: 1;">
<rect fill="white" width="32" height="32"/>
<circle fill="black" cx="16" cy="9" r="2.5"/>
@@ -104,11 +111,12 @@
<use id="microphone-detailed" xlink:href="#microphone-detailed-icon" />
<use id="plugin" xlink:href="#plugin-icon" />
<use id="plugin-blocked" class="blocked" xlink:href="#plugin-icon" />
<use id="popup" xlink:href="#popup-icon" />
<use id="screen" xlink:href="#screen-icon" />
<use id="screen-sharing" xlink:href="#screen-icon"/>
<use id="screen-indicator" xlink:href="#screen-icon"/>
<use id="screen-blocked" class="blocked" xlink:href="#screen-icon" />
+ <use id="update" xlink:href="#update-icon" />
<path id="strikeout" d="m 2,28 2,2 26,-26 -2,-2 z"/>
</svg>
--- a/browser/themes/shared/update-badge.svg
+++ b/browser/themes/shared/update-badge.svg
@@ -1,6 +1,8 @@
<!-- 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/. -->
<svg xmlns="http://www.w3.org/2000/svg" width="10px" height="10px">
- <polygon points="4,9 4,5 2,5 5,1 8,5 6,5 6,9" fill="#fff"/>
+ <line x1="5" x2="5" y1="9" y2="2" stroke="#fff" stroke-width="1.5" stroke-linecap="round"/>
+ <line x1="5" x2="2" y1="2" y2="5" stroke="#fff" stroke-width="1.5" stroke-linecap="round"/>
+ <line x1="5" x2="8" y1="2" y2="5" stroke="#fff" stroke-width="1.5" stroke-linecap="round"/>
</svg>
--- a/testing/talos/talos/config.py
+++ b/testing/talos/talos/config.py
@@ -169,17 +169,16 @@ DEFAULTS = dict(
'browser.webapps.checkForUpdates': 0,
'browser.search.geoSpecificDefaults': False,
'browser.snippets.enabled': False,
'browser.snippets.syncPromo.enabled': False,
'toolkit.telemetry.server': 'https://127.0.0.1/telemetry-dummy/',
'experiments.manifest.uri':
'https://127.0.0.1/experiments-dummy/manifest',
'network.http.speculative-parallel-limit': 0,
- 'app.update.badge': False,
'lightweightThemes.selectedThemeID': "",
'devtools.webide.widget.enabled': False,
'devtools.webide.widget.inNavbarByDefault': False,
'devtools.chrome.enabled': False,
'devtools.debugger.remote-enabled': False,
'devtools.theme': "light",
'devtools.timeline.enabled': False,
'identity.fxaccounts.migrateToDevEdition': False,
--- a/toolkit/components/telemetry/Histograms.json
+++ b/toolkit/components/telemetry/Histograms.json
@@ -5147,16 +5147,52 @@
"UPDATE_WIZ_LAST_PAGE_CODE": {
"alert_emails": ["application-update-telemetry-alerts@mozilla.com"],
"expires_in_version": "never",
"kind": "enumerated",
"n_values": 30,
"releaseChannelCollection": "opt-out",
"description": "Update: the update wizard page displayed when the UI was closed (mapped in toolkit/mozapps/update/UpdateTelemetry.jsm)"
},
+ "UPDATE_NOTIFICATION_SHOWN": {
+ "alert_emails": ["application-update-telemetry-alerts@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "categorical",
+ "bug_numbers": [893505],
+ "releaseChannelCollection": "opt-out",
+ "description": "Update: the application update doorhanger type that was displayed.",
+ "labels": ["restart", "available", "manual"]
+ },
+ "UPDATE_NOTIFICATION_DISMISSED": {
+ "alert_emails": ["application-update-telemetry-alerts@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "categorical",
+ "bug_numbers": [893505],
+ "releaseChannelCollection": "opt-out",
+ "description": "Update: the dismiss action was executed for this application update doorhanger type.",
+ "labels": ["restart", "available", "manual"]
+ },
+ "UPDATE_NOTIFICATION_MAIN_ACTION_DOORHANGER": {
+ "alert_emails": ["application-update-telemetry-alerts@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "categorical",
+ "bug_numbers": [893505],
+ "releaseChannelCollection": "opt-out",
+ "description": "Update: the main update action was initiated for this application update doorhanger type.",
+ "labels": ["restart", "available", "manual"]
+ },
+ "UPDATE_NOTIFICATION_MAIN_ACTION_MENU": {
+ "alert_emails": ["application-update-telemetry-alerts@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "categorical",
+ "bug_numbers": [893505],
+ "releaseChannelCollection": "opt-out",
+ "description": "Update: the update action was initiated from the PanelUI application update menu item.",
+ "labels": ["restart", "available", "manual"]
+ },
"THUNDERBIRD_GLODA_SIZE_MB": {
"expires_in_version": "never",
"kind": "linear",
"high": 1000,
"n_buckets": 40,
"description": "Gloda: size of global-messages-db.sqlite (MB)"
},
"THUNDERBIRD_CONVERSATIONS_TIME_TO_2ND_GLODA_QUERY_MS": {
--- a/toolkit/mozapps/update/nsUpdateService.js
+++ b/toolkit/mozapps/update/nsUpdateService.js
@@ -22,18 +22,23 @@ const UPDATESERVICE_CONTRACTID = "@mozil
const PREF_APP_UPDATE_ALTWINDOWTYPE = "app.update.altwindowtype";
const PREF_APP_UPDATE_AUTO = "app.update.auto";
const PREF_APP_UPDATE_BACKGROUNDINTERVAL = "app.update.download.backgroundInterval";
const PREF_APP_UPDATE_BACKGROUNDERRORS = "app.update.backgroundErrors";
const PREF_APP_UPDATE_BACKGROUNDMAXERRORS = "app.update.backgroundMaxErrors";
const PREF_APP_UPDATE_CANCELATIONS = "app.update.cancelations";
const PREF_APP_UPDATE_CANCELATIONS_OSX = "app.update.cancelations.osx";
const PREF_APP_UPDATE_CANCELATIONS_OSX_MAX = "app.update.cancelations.osx.max";
+const PREF_APP_UPDATE_DOORHANGER = "app.update.doorhanger";
+const PREF_APP_UPDATE_DOWNLOAD_ATTEMPTS = "app.update.download.attempts";
+const PREF_APP_UPDATE_DOWNLOAD_MAXATTEMPTS = "app.update.download.maxAttempts";
const PREF_APP_UPDATE_ELEVATE_NEVER = "app.update.elevate.never";
const PREF_APP_UPDATE_ELEVATE_VERSION = "app.update.elevate.version";
+const PREF_APP_UPDATE_ELEVATE_ATTEMPTS = "app.update.elevate.attempts";
+const PREF_APP_UPDATE_ELEVATE_MAXATTEMPTS = "app.update.elevate.maxAttempts";
const PREF_APP_UPDATE_ENABLED = "app.update.enabled";
const PREF_APP_UPDATE_IDLETIME = "app.update.idletime";
const PREF_APP_UPDATE_LOG = "app.update.log";
const PREF_APP_UPDATE_NOTIFIEDUNSUPPORTED = "app.update.notifiedUnsupported";
const PREF_APP_UPDATE_POSTUPDATE = "app.update.postupdate";
const PREF_APP_UPDATE_PROMPTWAITTIME = "app.update.promptWaitTime";
const PREF_APP_UPDATE_SERVICE_ENABLED = "app.update.service.enabled";
const PREF_APP_UPDATE_SERVICE_ERRORS = "app.update.service.errors";
@@ -1185,16 +1190,33 @@ function handleUpdateFailure(update, err
Cc["@mozilla.org/updates/update-prompt;1"].
createInstance(Ci.nsIUpdatePrompt).
showUpdateError(update);
writeStatusFile(getUpdatesDir(), update.state = STATE_PENDING);
return true;
}
if (update.errorCode == ELEVATION_CANCELED) {
+ if (getPref("getBoolPref", PREF_APP_UPDATE_DOORHANGER, false)) {
+ let elevationAttempts = getPref("getIntPref", PREF_APP_UPDATE_ELEVATE_ATTEMPTS, 0);
+ elevationAttempts++;
+ Services.prefs.setIntPref(PREF_APP_UPDATE_ELEVATE_ATTEMPTS, elevationAttempts);
+ let maxAttempts = Math.min(getPref("getIntPref", PREF_APP_UPDATE_ELEVATE_MAXATTEMPTS, 2), 10);
+
+ if (elevationAttempts > maxAttempts) {
+ LOG("handleUpdateFailure - notifying observers of error. " +
+ "topic: update-error, status: elevation-attempts-exceeded");
+ Services.obs.notifyObservers(update, "update-error", "elevation-attempts-exceeded");
+ } else {
+ LOG("handleUpdateFailure - notifying observers of error. " +
+ "topic: update-error, status: elevation-attempt-failed");
+ Services.obs.notifyObservers(update, "update-error", "elevation-attempt-failed");
+ }
+ }
+
let cancelations = getPref("getIntPref", PREF_APP_UPDATE_CANCELATIONS, 0);
cancelations++;
Services.prefs.setIntPref(PREF_APP_UPDATE_CANCELATIONS, cancelations);
if (AppConstants.platform == "macosx") {
let osxCancelations = getPref("getIntPref",
PREF_APP_UPDATE_CANCELATIONS_OSX, 0);
osxCancelations++;
Services.prefs.setIntPref(PREF_APP_UPDATE_CANCELATIONS_OSX,
@@ -1280,17 +1302,21 @@ function handleFallbackToCompleteUpdate(
"failed, downloading complete patch");
var status = Cc["@mozilla.org/updates/update-service;1"].
getService(Ci.nsIApplicationUpdateService).
downloadUpdate(update, !postStaging);
if (status == STATE_NONE)
cleanupActiveUpdate();
} else {
LOG("handleFallbackToCompleteUpdate - install of complete or " +
- "only one patch offered failed.");
+ "only one patch offered failed. Notifying observers. topic: " +
+ "update-error, status: unknown, " +
+ "update.patchCount: " + update.patchCount +
+ "oldType: " + oldType);
+ Services.obs.notifyObservers(update, "update-error", "unknown");
}
update.QueryInterface(Ci.nsIWritablePropertyBag);
update.setProperty("patchingFailed", oldType);
}
function pingStateAndStatusCodes(aUpdate, aStartup, aStatus) {
let patchType = AUSTLMY.PATCH_UNKNOWN;
if (aUpdate && aUpdate.selectedPatch && aUpdate.selectedPatch.type) {
@@ -2073,16 +2099,18 @@ UpdateService.prototype = {
if (status == STATE_SUCCEEDED) {
update.statusText = gUpdateBundle.GetStringFromName("installSuccess");
// Update the patch's metadata.
um.activeUpdate = update;
// Done with this update. Clean it up.
cleanupActiveUpdate();
+
+ Services.prefs.setIntPref(PREF_APP_UPDATE_ELEVATE_ATTEMPTS, 0);
} else if (status == STATE_PENDING_ELEVATE) {
let prompter = Cc["@mozilla.org/updates/update-prompt;1"].
createInstance(Ci.nsIUpdatePrompt);
prompter.showUpdateElevationRequired();
} else {
// If there was an I/O error it is assumed that the patch is not invalid
// and it is set to pending so an attempt to apply it again will happen
// when the application is restarted.
@@ -2159,19 +2187,25 @@ UpdateService.prototype = {
errCount++;
Services.prefs.setIntPref(PREF_APP_UPDATE_BACKGROUNDERRORS, errCount);
// Don't allow the preference to set a value greater than 20 for max errors.
let maxErrors = Math.min(getPref("getIntPref", PREF_APP_UPDATE_BACKGROUNDMAXERRORS, 10), 20);
if (errCount >= maxErrors) {
let prompter = Cc["@mozilla.org/updates/update-prompt;1"].
createInstance(Ci.nsIUpdatePrompt);
+ LOG("UpdateService:onError - notifying observers of error. " +
+ "topic: update-error, status: check-attempts-exceeded");
+ Services.obs.notifyObservers(update, "update-error", "check-attempts-exceeded");
prompter.showUpdateError(update);
AUSTLMY.pingCheckCode(this._pingSuffix, AUSTLMY.CHK_GENERAL_ERROR_PROMPT);
} else {
+ LOG("UpdateService:onError - notifying observers of error. " +
+ "topic: update-error, status: check-attempt-failed");
+ Services.obs.notifyObservers(update, "update-error", "check-attempt-failed");
AUSTLMY.pingCheckCode(this._pingSuffix, AUSTLMY.CHK_GENERAL_ERROR_SILENT);
}
},
/**
* Called when a connection should be resumed
*/
_attemptResume: function AUS_attemptResume() {
@@ -2513,29 +2547,35 @@ UpdateService.prototype = {
var update = this.selectUpdate(updates, updates.length);
if (!update || update.elevationFailure) {
return;
}
if (update.unsupported) {
LOG("UpdateService:_selectAndInstallUpdate - update not supported for " +
- "this system");
+ "this system. Notifying observers. topic: update-available, " +
+ "status: unsupported");
if (!getPref("getBoolPref", PREF_APP_UPDATE_NOTIFIEDUNSUPPORTED, false)) {
LOG("UpdateService:_selectAndInstallUpdate - notifying that the " +
"update is not supported for this system");
this._showPrompt(update);
}
+
+ Services.obs.notifyObservers(null, "update-available", "unsupported");
AUSTLMY.pingCheckCode(this._pingSuffix, AUSTLMY.CHK_UNSUPPORTED);
return;
}
if (!getCanApplyUpdates()) {
LOG("UpdateService:_selectAndInstallUpdate - the user is unable to " +
- "apply updates... prompting");
+ "apply updates... prompting. Notifying observers. " +
+ "topic: update-available, status: cant-apply");
+
+ Services.obs.notifyObservers(null, "update-available", "cant-apply");
this._showPrompt(update);
AUSTLMY.pingCheckCode(this._pingSuffix, AUSTLMY.CHK_UNABLE_TO_APPLY);
return;
}
/**
* From this point on there are two possible outcomes:
* 1. download and install the update automatically
@@ -2549,26 +2589,32 @@ UpdateService.prototype = {
* If the update when it is first read does not have an appVersion attribute
* the following deprecated behavior will occur:
* Update Type Outcome
* Major Notify
* Minor Auto Install
*/
if (update.showPrompt) {
LOG("UpdateService:_selectAndInstallUpdate - prompting because the " +
- "update snippet specified showPrompt");
+ "update snippet specified showPrompt. Notifying observers. " +
+ "topic: update-available, status: showPrompt");
AUSTLMY.pingCheckCode(this._pingSuffix, AUSTLMY.CHK_SHOWPROMPT_SNIPPET);
+
+ Services.obs.notifyObservers(update, "update-available", "show-prompt");
this._showPrompt(update);
return;
}
if (!getPref("getBoolPref", PREF_APP_UPDATE_AUTO, true)) {
LOG("UpdateService:_selectAndInstallUpdate - prompting because silent " +
- "install is disabled");
+ "install is disabled. Notifying observers. topic: update-available, " +
+ "status: show-prompt");
AUSTLMY.pingCheckCode(this._pingSuffix, AUSTLMY.CHK_SHOWPROMPT_PREF);
+
+ Services.obs.notifyObservers(update, "update-available", "show-prompt");
this._showPrompt(update);
return;
}
LOG("UpdateService:_selectAndInstallUpdate - download the update");
let status = this.downloadUpdate(update, true);
if (status == STATE_NONE) {
cleanupActiveUpdate();
@@ -3100,21 +3146,21 @@ UpdateManager.prototype = {
if (!handleUpdateFailure(update, parts[1])) {
handleFallbackToCompleteUpdate(update, true);
}
}
if (update.state == STATE_APPLIED && shouldUseService()) {
writeStatusFile(getUpdatesDir(), update.state = STATE_APPLIED_SERVICE);
}
- // Send an observer notification which the update wizard uses in
- // order to update its UI.
+ // Send an observer notification which the app update doorhanger uses to
+ // display a restart notification
LOG("UpdateManager:refreshUpdateStatus - Notifying observers that " +
- "the update was staged. state: " + update.state + ", status: " + status);
- Services.obs.notifyObservers(null, "update-staged", update.state);
+ "the update was staged. topic: update-staged, status: " + update.state);
+ Services.obs.notifyObservers(update, "update-staged", update.state);
if (AppConstants.platform == "gonk") {
// Do this after everything else, since it will likely cause the app to
// shut down.
if (update.state == STATE_APPLIED) {
// Notify the user that an update has been staged and is ready for
// installation (i.e. that they should restart the application). We do
// not notify on failed update attempts.
@@ -3229,18 +3275,16 @@ Checker.prototype = {
/**
* See nsIUpdateService.idl
*/
checkForUpdates: function UC_checkForUpdates(listener, force) {
LOG("Checker: checkForUpdates, force: " + force);
if (!listener)
throw Cr.NS_ERROR_NULL_POINTER;
- Services.obs.notifyObservers(null, "update-check-start", null);
-
var url = this.getUpdateURL(force);
if (!url || (!this.enabled && !force))
return;
this._request = new XMLHttpRequest();
this._request.open("GET", url, true);
this._request.channel.notificationCallbacks = new gCertUtils.BadCertHandler(false);
// Prevent the request from reading from the cache.
@@ -3812,19 +3856,19 @@ Downloader.prototype = {
}
}
}
update.QueryInterface(Ci.nsIPropertyBag);
let interval = this.background ? update.getProperty("backgroundInterval")
: DOWNLOAD_FOREGROUND_INTERVAL;
+ LOG("Downloader:downloadUpdate - url: " + this._patch.URL + ", path: " +
+ patchFile.path + ", interval: " + interval);
var uri = Services.io.newURI(this._patch.URL);
- LOG("Downloader:downloadUpdate - url: " + uri.spec + ", path: " +
- patchFile.path + ", interval: " + interval);
this._request = Cc["@mozilla.org/network/incremental-download;1"].
createInstance(Ci.nsIIncrementalDownload);
this._request.init(uri, patchFile, DOWNLOAD_CHUNK_SIZE, interval);
this._request.start(this, null);
writeStatusFile(updateDir, STATE_DOWNLOADING);
this._patch.QueryInterface(Ci.nsIWritablePropertyBag);
@@ -4012,16 +4056,17 @@ Downloader.prototype = {
}
AUSTLMY.pingDownloadCode(this.isCompleteUpdate, AUSTLMY.DWNLD_SUCCESS);
// Tell the updater.exe we're ready to apply.
writeStatusFile(getUpdatesDir(), state);
writeVersionFile(getUpdatesDir(), this._update.appVersion);
this._update.installDate = (new Date()).getTime();
this._update.statusText = gUpdateBundle.GetStringFromName("installPending");
+ Services.prefs.setIntPref(PREF_APP_UPDATE_DOWNLOAD_ATTEMPTS, 0);
} else {
LOG("Downloader:onStopRequest - download verification failed");
state = STATE_DOWNLOAD_FAILED;
status = Cr.NS_ERROR_CORRUPTED_CONTENT;
// Yes, this code is a string.
const vfCode = "verification_failed";
var message = getStatusTextFromCode(vfCode, vfCode);
@@ -4131,17 +4176,36 @@ Downloader.prototype = {
if (updateStatus == STATE_NONE) {
cleanupActiveUpdate();
} else {
allFailed = false;
}
}
if (allFailed) {
- LOG("Downloader:onStopRequest - all update patch downloads failed");
+ if (getPref("getBoolPref", PREF_APP_UPDATE_DOORHANGER, false)) {
+ let downloadAttempts = getPref("getIntPref", PREF_APP_UPDATE_DOWNLOAD_ATTEMPTS, 0);
+ downloadAttempts++;
+ Services.prefs.setIntPref(PREF_APP_UPDATE_DOWNLOAD_ATTEMPTS, downloadAttempts);
+ let maxAttempts = Math.min(getPref("getIntPref", PREF_APP_UPDATE_DOWNLOAD_MAXATTEMPTS, 2), 10);
+
+ if (downloadAttempts > maxAttempts) {
+ LOG("Downloader:onStopRequest - notifying observers of error. " +
+ "topic: update-error, status: download-attempts-exceeded, " +
+ "downloadAttempts: " + downloadAttempts + " " +
+ "maxAttempts: " + maxAttempts);
+ Services.obs.notifyObservers(this._update, "update-error", "download-attempts-exceeded");
+ } else {
+ this._update.selectedPatch.selected = false;
+ LOG("Downloader:onStopRequest - notifying observers of error. " +
+ "topic: update-error, status: download-attempt-failed");
+ Services.obs.notifyObservers(this._update, "update-error", "download-attempt-failed");
+ }
+ }
+
// If the update UI is not open (e.g. the user closed the window while
// downloading) and if at any point this was a foreground download
// notify the user about the error. If the update was a background
// update there is no notification since the user won't be expecting it.
if (!Services.wm.getMostRecentWindow(UPDATE_WINDOW_NAME)) {
this._update.QueryInterface(Ci.nsIWritablePropertyBag);
if (this._update.getProperty("foregroundDownload") == "true") {
let prompter = Cc["@mozilla.org/updates/update-prompt;1"].
@@ -4257,33 +4321,41 @@ UpdatePrompt.prototype = {
null, null);
},
/**
* See nsIUpdateService.idl
*/
showUpdateAvailable: function UP_showUpdateAvailable(update) {
if (getPref("getBoolPref", PREF_APP_UPDATE_SILENT, false) ||
+ getPref("getBoolPref", PREF_APP_UPDATE_DOORHANGER, false) ||
this._getUpdateWindow() || this._getAltUpdateWindow()) {
return;
}
this._showUnobtrusiveUI(null, URI_UPDATE_PROMPT_DIALOG, null,
UPDATE_WINDOW_NAME, "updatesavailable", update);
},
/**
* See nsIUpdateService.idl
*/
showUpdateDownloaded: function UP_showUpdateDownloaded(update, background) {
if (background && getPref("getBoolPref", PREF_APP_UPDATE_SILENT, false)) {
return;
}
- // Trigger the display of the hamburger menu badge.
- Services.obs.notifyObservers(null, "update-downloaded", update.state);
+
+ // Trigger the display of the hamburger doorhanger.
+ LOG("showUpdateDownloaded - Notifying observers that " +
+ "an update was downloaded. topic: update-downloaded, status: " + update.state);
+ Services.obs.notifyObservers(update, "update-downloaded", update.state);
+
+ if (getPref("getBoolPref", PREF_APP_UPDATE_DOORHANGER, false)) {
+ return;
+ }
if (this._getAltUpdateWindow())
return;
if (background) {
this._showUnobtrusiveUI(null, URI_UPDATE_PROMPT_DIALOG, null,
UPDATE_WINDOW_NAME, "finishedBackground", update);
} else {
@@ -4292,16 +4364,17 @@ UpdatePrompt.prototype = {
}
},
/**
* See nsIUpdateService.idl
*/
showUpdateError: function UP_showUpdateError(update) {
if (getPref("getBoolPref", PREF_APP_UPDATE_SILENT, false) ||
+ getPref("getBoolPref", PREF_APP_UPDATE_DOORHANGER, false) ||
this._getAltUpdateWindow())
return;
// In some cases, we want to just show a simple alert dialog.
// Replace with Array.prototype.includes when it has stabilized.
if (update.state == STATE_FAILED &&
(WRITE_ERRORS.indexOf(update.errorCode) != -1 ||
update.errorCode == FILESYSTEM_MOUNT_READWRITE_ERROR ||
--- a/toolkit/mozapps/update/tests/chrome/chrome.ini
+++ b/toolkit/mozapps/update/tests/chrome/chrome.ini
@@ -1,15 +1,16 @@
# 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/.
[DEFAULT]
tags = appupdate
support-files =
+ testConstants.js
utils.js
update.sjs
# mochitest-chrome tests must start with "test_" and are executed in sorted
# order and not in the order specified in the manifest.
[test_0010_background_basic.xul]
[test_0011_check_basic.xul]
[test_0012_check_basic_staging.xul]
new file mode 100644
--- /dev/null
+++ b/toolkit/mozapps/update/tests/chrome/testConstants.js
@@ -0,0 +1,4 @@
+const REL_PATH_DATA = "chrome/toolkit/mozapps/update/tests/data/";
+const URL_HOST = "http://example.com";
+const URL_PATH_UPDATE_XML = "/chrome/toolkit/mozapps/update/tests/chrome/update.sjs";
+const URL_HTTP_UPDATE_SJS = URL_HOST + URL_PATH_UPDATE_XML;
--- a/toolkit/mozapps/update/tests/chrome/update.sjs
+++ b/toolkit/mozapps/update/tests/chrome/update.sjs
@@ -3,45 +3,48 @@
*/
/**
* Server side http server script for application update tests.
*/
const { classes: Cc, interfaces: Ci } = Components;
-const REL_PATH_DATA = "chrome/toolkit/mozapps/update/tests/data/";
-
function getTestDataFile(aFilename) {
let file = Cc["@mozilla.org/file/directory_service;1"].
getService(Ci.nsIProperties).get("CurWorkD", Ci.nsILocalFile);
let pathParts = REL_PATH_DATA.split("/");
for (let i = 0; i < pathParts.length; ++i) {
file.append(pathParts[i]);
}
if (aFilename) {
file.append(aFilename);
}
return file;
}
-function loadHelperScript() {
- let scriptFile = getTestDataFile("sharedUpdateXML.js");
+function loadHelperScript(aScriptFile) {
let io = Cc["@mozilla.org/network/io-service;1"].getService(Ci.nsIIOService2);
- let scriptSpec = io.newFileURI(scriptFile).spec;
+ let scriptSpec = io.newFileURI(aScriptFile).spec;
let scriptloader = Cc["@mozilla.org/moz/jssubscript-loader;1"].
getService(Ci.mozIJSSubScriptLoader);
scriptloader.loadSubScript(scriptSpec, this);
}
-loadHelperScript();
-const URL_HOST = "http://example.com";
-const URL_PATH_UPDATE_XML = "/chrome/toolkit/mozapps/update/tests/chrome/update.sjs";
-const URL_HTTP_UPDATE_SJS = URL_HOST + URL_PATH_UPDATE_XML;
+var scriptFile = Cc['@mozilla.org/file/local;1'].createInstance(Ci.nsILocalFile);
+scriptFile.initWithPath(getState("__LOCATION__"));
+scriptFile = scriptFile.parent;
+scriptFile.append("testConstants.js");
+loadHelperScript(scriptFile);
+
+scriptFile = getTestDataFile("sharedUpdateXML.js");
+loadHelperScript(scriptFile);
+
const SERVICE_URL = URL_HOST + "/" + REL_PATH_DATA + FILE_SIMPLE_MAR;
+const BAD_SERVICE_URL = URL_HOST + "/" + REL_PATH_DATA + "not_here.mar";
const SLOW_MAR_DOWNLOAD_INTERVAL = 100;
var gTimer;
function handleRequest(aRequest, aResponse) {
let params = { };
if (aRequest.queryString) {
params = parseQueryString(aRequest.queryString);
@@ -103,25 +106,27 @@ function handleRequest(aRequest, aRespon
"unsupported=\"true\" " +
"detailsURL=\"" + URL_HOST +
"\"></update>\n"));
return;
}
let size;
let patches = "";
+ let url = params.badURL ? BAD_SERVICE_URL : SERVICE_URL;
+
if (!params.partialPatchOnly) {
size = SIZE_SIMPLE_MAR + (params.invalidCompleteSize ? "1" : "");
- patches += getRemotePatchString("complete", SERVICE_URL, "SHA512",
+ patches += getRemotePatchString("complete", url, "SHA512",
SHA512_HASH_SIMPLE_MAR, size);
}
if (!params.completePatchOnly) {
size = SIZE_SIMPLE_MAR + (params.invalidPartialSize ? "1" : "");
- patches += getRemotePatchString("partial", SERVICE_URL, "SHA512",
+ patches += getRemotePatchString("partial", url, "SHA512",
SHA512_HASH_SIMPLE_MAR, size);
}
let type = params.type ? params.type : "major";
let name = params.name ? params.name : "App Update Test";
let appVersion = params.appVersion ? params.appVersion : "999999.9";
let displayVersion = params.displayVersion ? params.displayVersion
: "version " + appVersion;
--- a/toolkit/mozapps/update/tests/chrome/utils.js
+++ b/toolkit/mozapps/update/tests/chrome/utils.js
@@ -71,16 +71,19 @@
/* globals TESTS, runTest, finishTest */
const { classes: Cc, interfaces: Ci, manager: Cm, results: Cr,
utils: Cu } = Components;
Cu.import("resource://gre/modules/Services.jsm", this);
+/* import-globals-from testConstants.js */
+Services.scriptloader.loadSubScript("chrome://mochitests/content/chrome/toolkit/mozapps/update/tests/chrome/testConstants.js", this);
+
const IS_MACOSX = ("nsILocalFileMac" in Ci);
const IS_WIN = ("@mozilla.org/windows-registry-key;1" in Cc);
// The tests have to use the pageid instead of the pageIndex due to the
// app update wizard's access method being random.
const PAGEID_DUMMY = "dummy"; // Done
const PAGEID_CHECKING = "checking"; // Done
const PAGEID_NO_UPDATES_FOUND = "noupdatesfound"; // Done
@@ -91,23 +94,19 @@ const PAGEID_DOWNLOADING = "downloa
const PAGEID_ERRORS = "errors"; // Done
const PAGEID_ERROR_EXTRA = "errorextra"; // Done
const PAGEID_ERROR_PATCHING = "errorpatching"; // Done
const PAGEID_FINISHED = "finished"; // Done
const PAGEID_FINISHED_BKGRD = "finishedBackground"; // Done
const UPDATE_WINDOW_NAME = "Update:Wizard";
-const URL_HOST = "http://example.com";
-const URL_PATH_UPDATE_XML = "/chrome/toolkit/mozapps/update/tests/chrome/update.sjs";
-const REL_PATH_DATA = "chrome/toolkit/mozapps/update/tests/data";
-
// These two URLs must not contain parameters since tests add their own
// test specific parameters.
-const URL_HTTP_UPDATE_XML = URL_HOST + URL_PATH_UPDATE_XML;
+const URL_HTTP_UPDATE_XML = URL_HTTP_UPDATE_SJS;
const URL_HTTPS_UPDATE_XML = "https://example.com" + URL_PATH_UPDATE_XML;
const URI_UPDATE_PROMPT_DIALOG = "chrome://mozapps/content/update/updates.xul";
const PREF_APP_UPDATE_INTERVAL = "app.update.interval";
const PREF_APP_UPDATE_LASTUPDATETIME = "app.update.lastUpdateTime.background-update-timer";
const LOG_FUNCTION = info;
@@ -484,17 +483,17 @@ function delayedDefaultCallback() {
* downloading for slow download mar file tests without creating it.
*
* @return nsILocalFile for the continue file.
*/
function getContinueFile() {
let continueFile = Cc["@mozilla.org/file/directory_service;1"].
getService(Ci.nsIProperties).
get("CurWorkD", Ci.nsILocalFile);
- let continuePath = REL_PATH_DATA + "/continue";
+ let continuePath = REL_PATH_DATA + "continue";
let continuePathParts = continuePath.split("/");
for (let i = 0; i < continuePathParts.length; ++i) {
continueFile.append(continuePathParts[i]);
}
return continueFile;
}
/**
@@ -808,16 +807,17 @@ function setupPrefs() {
if (Services.prefs.prefHasUserValue(PREF_APP_UPDATE_STAGING_ENABLED)) {
gAppUpdateStagingEnabled = Services.prefs.getBoolPref(PREF_APP_UPDATE_STAGING_ENABLED);
}
Services.prefs.setBoolPref(PREF_APP_UPDATE_STAGING_ENABLED, false);
Services.prefs.setIntPref(PREF_APP_UPDATE_IDLETIME, 0);
Services.prefs.setIntPref(PREF_APP_UPDATE_PROMPTWAITTIME, 0);
Services.prefs.setBoolPref(PREF_APP_UPDATE_SILENT, false);
+ Services.prefs.setBoolPref(PREF_APP_UPDATE_DOORHANGER, false);
}
/**
* Restores files that were backed up for the tests and general file cleanup.
*/
function resetFiles() {
// Restore the backed up updater-settings.ini if it exists.
let baseAppDir = getGREDir();
@@ -901,16 +901,20 @@ function resetPrefs() {
if (Services.prefs.prefHasUserValue(PREF_APP_UPDATE_BACKGROUNDERRORS)) {
Services.prefs.clearUserPref(PREF_APP_UPDATE_BACKGROUNDERRORS);
}
if (Services.prefs.prefHasUserValue(PREF_APP_UPDATE_BACKGROUNDMAXERRORS)) {
Services.prefs.clearUserPref(PREF_APP_UPDATE_BACKGROUNDMAXERRORS);
}
+ if (Services.prefs.prefHasUserValue(PREF_APP_UPDATE_DOORHANGER)) {
+ Services.prefs.clearUserPref(PREF_APP_UPDATE_DOORHANGER);
+ }
+
try {
Services.prefs.deleteBranch(PREFBRANCH_APP_UPDATE_NEVER);
} catch (e) {
}
}
function setupTimer(aTestTimeout) {
gTestTimeout = aTestTimeout;
--- a/toolkit/mozapps/update/tests/data/shared.js
+++ b/toolkit/mozapps/update/tests/data/shared.js
@@ -3,32 +3,37 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
/* Shared code for xpcshell and mochitests-chrome */
/* eslint-disable no-undef */
Cu.import("resource://gre/modules/FileUtils.jsm");
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
-const PREF_APP_UPDATE_AUTO = "app.update.auto";
-const PREF_APP_UPDATE_BACKGROUNDERRORS = "app.update.backgroundErrors";
-const PREF_APP_UPDATE_BACKGROUNDMAXERRORS = "app.update.backgroundMaxErrors";
-const PREF_APP_UPDATE_CHANNEL = "app.update.channel";
-const PREF_APP_UPDATE_ENABLED = "app.update.enabled";
-const PREF_APP_UPDATE_IDLETIME = "app.update.idletime";
-const PREF_APP_UPDATE_LOG = "app.update.log";
-const PREF_APP_UPDATE_NOTIFIEDUNSUPPORTED = "app.update.notifiedUnsupported";
-const PREF_APP_UPDATE_PROMPTWAITTIME = "app.update.promptWaitTime";
-const PREF_APP_UPDATE_RETRYTIMEOUT = "app.update.socket.retryTimeout";
-const PREF_APP_UPDATE_SERVICE_ENABLED = "app.update.service.enabled";
-const PREF_APP_UPDATE_SILENT = "app.update.silent";
-const PREF_APP_UPDATE_SOCKET_MAXERRORS = "app.update.socket.maxErrors";
-const PREF_APP_UPDATE_STAGING_ENABLED = "app.update.staging.enabled";
-const PREF_APP_UPDATE_URL = "app.update.url";
-const PREF_APP_UPDATE_URL_DETAILS = "app.update.url.details";
+const PREF_APP_UPDATE_AUTO = "app.update.auto";
+const PREF_APP_UPDATE_BACKGROUNDERRORS = "app.update.backgroundErrors";
+const PREF_APP_UPDATE_BACKGROUNDMAXERRORS = "app.update.backgroundMaxErrors";
+const PREF_APP_UPDATE_CHANNEL = "app.update.channel";
+const PREF_APP_UPDATE_DOORHANGER = "app.update.doorhanger";
+const PREF_APP_UPDATE_DOWNLOADPROMPTATTEMPTS = "app.update.download.attempts";
+const PREF_APP_UPDATE_DOWNLOADPROMPTMAXATTEMPTS = "app.update.download.maxAttempts";
+const PREF_APP_UPDATE_ENABLED = "app.update.enabled";
+const PREF_APP_UPDATE_IDLETIME = "app.update.idletime";
+const PREF_APP_UPDATE_LOG = "app.update.log";
+const PREF_APP_UPDATE_NOTIFIEDUNSUPPORTED = "app.update.notifiedUnsupported";
+const PREF_APP_UPDATE_PROMPTWAITTIME = "app.update.promptWaitTime";
+const PREF_APP_UPDATE_RETRYTIMEOUT = "app.update.socket.retryTimeout";
+const PREF_APP_UPDATE_SERVICE_ENABLED = "app.update.service.enabled";
+const PREF_APP_UPDATE_SILENT = "app.update.silent";
+const PREF_APP_UPDATE_SOCKET_MAXERRORS = "app.update.socket.maxErrors";
+const PREF_APP_UPDATE_STAGING_ENABLED = "app.update.staging.enabled";
+const PREF_APP_UPDATE_URL = "app.update.url";
+const PREF_APP_UPDATE_URL_DETAILS = "app.update.url.details";
+const PREF_APP_UPDATE_URL_MANUAL = "app.update.url.manual";
+
const PREFBRANCH_APP_UPDATE_NEVER = "app.update.never.";
const PREFBRANCH_APP_PARTNER = "app.partner.";
const PREF_DISTRIBUTION_ID = "distribution.id";
const PREF_DISTRIBUTION_VERSION = "distribution.version";
const PREF_TOOLKIT_TELEMETRY_ENABLED = "toolkit.telemetry.enabled";
--- a/toolkit/mozapps/update/tests/data/sharedUpdateXML.js
+++ b/toolkit/mozapps/update/tests/data/sharedUpdateXML.js
@@ -226,18 +226,18 @@ function getLocalPatchString(aType, aURL
* The update's application version.
* If not specified it will default to the value of
* DEFAULT_UPDATE_VERSION.
* @param aBuildID (optional)
* The update's build id.
* If not specified it will default to '20080811053724'.
* @param aDetailsURL (optional)
* The update's details url.
- * If not specified it will default to 'http://test_details/' due to due
- * to bug 470244.
+ * If not specified it will default to
+ * URL_HTTP_UPDATE_SJS + "?uiURL=DETAILS" due to bug 470244.
* @param aShowPrompt (optional)
* Whether to show the prompt for the update when auto update is
* enabled.
* If not specified it will not be present and the update service will
* default to false.
* @param aShowNeverForVersion (optional)
* Whether to show the 'No Thanks' button in the update prompt.
* If not specified it will not be present and the update service will
@@ -268,17 +268,17 @@ function getUpdateString(aType, aName, a
let appVersion = "appVersion=\"" +
(aAppVersion ? aAppVersion : DEFAULT_UPDATE_VERSION) +
"\" ";
let buildID = aBuildID ? aBuildID : "20080811053724";
// XXXrstrong - not specifying a detailsURL will cause a leak due to bug 470244
// let detailsURL = aDetailsURL ? "detailsURL=\"" + aDetailsURL + "\" " : "";
let detailsURL = "detailsURL=\"" +
(aDetailsURL ? aDetailsURL
- : "http://test_details/") + "\" ";
+ : URL_HTTP_UPDATE_SJS + "?uiURL=DETAILS") + "\" ";
let showPrompt = aShowPrompt ? "showPrompt=\"" + aShowPrompt + "\" " : "";
let showNeverForVersion = aShowNeverForVersion ? "showNeverForVersion=\"" +
aShowNeverForVersion + "\" "
: "";
let promptWaitTime = aPromptWaitTime ? "promptWaitTime=\"" + aPromptWaitTime +
"\" "
: "";
let backgroundInterval = aBackgroundInterval ? "backgroundInterval=\"" +
--- a/toolkit/mozapps/update/tests/data/xpcshellUtilsAUS.js
+++ b/toolkit/mozapps/update/tests/data/xpcshellUtilsAUS.js
@@ -29,16 +29,18 @@
*/
"use strict";
/* eslint-disable no-undef */
const { classes: Cc, interfaces: Ci, manager: Cm, results: Cr,
utils: Cu } = Components;
+const URL_HTTP_UPDATE_SJS = "http://test_details/";
+
/* global INSTALL_LOCALE, MOZ_APP_NAME, BIN_SUFFIX, MOZ_APP_VENDOR */
/* global MOZ_APP_BASENAME, APP_BIN_SUFFIX, APP_INFO_NAME, APP_INFO_VENDOR */
/* global IS_WIN, IS_MACOSX, IS_UNIX, IS_ANDROID, IS_TOOLKIT_GONK */
/* global MOZ_VERIFY_MAR_SIGNATURE, MOZ_VERIFY_MAR_SIGNATURE, IS_AUTHENTICODE_CHECK_ENABLED */
load("../data/xpcshellConstantsPP.js");
function getLogSuffix() {
if (IS_WIN) {
--- a/toolkit/mozapps/update/updater/updater-xpcshell/Makefile.in
+++ b/toolkit/mozapps/update/updater/updater-xpcshell/Makefile.in
@@ -1,39 +1,42 @@
# vim:set ts=8 sw=8 sts=8 noet:
# 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/.
# For changes here, also consider ../Makefile.in
-XPCSHELLTESTROOT = $(topobjdir)/_tests/xpcshell/toolkit/mozapps/update/tests
-MOCHITESTROOT = $(topobjdir)/_tests/testing/mochitest/chrome/toolkit/mozapps/update/tests
+XPCSHELLTESTDIR = $(topobjdir)/_tests/xpcshell/toolkit/mozapps/update/tests
+MOCHITESTCHROMEDIR = $(topobjdir)/_tests/testing/mochitest/chrome/toolkit/mozapps/update/tests
+MOCHITESTBROWSERDIR = $(topobjdir)/_tests/testing/mochitest/browser/browser/base/content/test/appUpdate
include $(topsrcdir)/config/rules.mk
ifndef MOZ_WINCONSOLE
ifdef MOZ_DEBUG
MOZ_WINCONSOLE = 1
else
MOZ_WINCONSOLE = 0
endif
endif
tools::
ifeq (cocoa,$(MOZ_WIDGET_TOOLKIT))
# Copy for xpcshell tests
- $(NSINSTALL) -D $(XPCSHELLTESTROOT)/data/updater-xpcshell.app
- rsync -a -C --exclude '*.in' $(srcdir)/../macbuild/Contents $(XPCSHELLTESTROOT)/data/updater-xpcshell.app
+ $(NSINSTALL) -D $(XPCSHELLTESTDIR)/data/updater-xpcshell.app
+ rsync -a -C --exclude '*.in' $(srcdir)/../macbuild/Contents $(XPCSHELLTESTDIR)/data/updater-xpcshell.app
sed -e 's/%APP_NAME%/$(MOZ_APP_DISPLAYNAME)/' $(srcdir)/../macbuild/Contents/Resources/English.lproj/InfoPlist.strings.in | \
- iconv -f UTF-8 -t UTF-16 > $(XPCSHELLTESTROOT)/data/updater-xpcshell.app/Contents/Resources/English.lproj/InfoPlist.strings
- $(NSINSTALL) -D $(XPCSHELLTESTROOT)/data/updater-xpcshell.app/Contents/MacOS
- $(NSINSTALL) $(FINAL_TARGET)/updater-xpcshell $(XPCSHELLTESTROOT)/data/updater-xpcshell.app/Contents/MacOS
- rm -Rf $(XPCSHELLTESTROOT)/data/updater.app
- mv $(XPCSHELLTESTROOT)/data/updater-xpcshell.app $(XPCSHELLTESTROOT)/data/updater.app
- mv $(XPCSHELLTESTROOT)/data/updater.app/Contents/MacOS/updater-xpcshell $(XPCSHELLTESTROOT)/data/updater.app/Contents/MacOS/org.mozilla.updater
+ iconv -f UTF-8 -t UTF-16 > $(XPCSHELLTESTDIR)/data/updater-xpcshell.app/Contents/Resources/English.lproj/InfoPlist.strings
+ $(NSINSTALL) -D $(XPCSHELLTESTDIR)/data/updater-xpcshell.app/Contents/MacOS
+ $(NSINSTALL) $(FINAL_TARGET)/updater-xpcshell $(XPCSHELLTESTDIR)/data/updater-xpcshell.app/Contents/MacOS
+ rm -Rf $(XPCSHELLTESTDIR)/data/updater.app
+ mv $(XPCSHELLTESTDIR)/data/updater-xpcshell.app $(XPCSHELLTESTDIR)/data/updater.app
+ mv $(XPCSHELLTESTDIR)/data/updater.app/Contents/MacOS/updater-xpcshell $(XPCSHELLTESTDIR)/data/updater.app/Contents/MacOS/org.mozilla.updater
# Copy for mochitest chrome tests
- rsync -a -C $(XPCSHELLTESTROOT)/data/updater.app $(MOCHITESTROOT)/data/
+ rsync -a -C $(XPCSHELLTESTDIR)/data/updater.app $(MOCHITESTCHROMEDIR)/data/
+ rsync -a -C $(XPCSHELLTESTDIR)/data/updater.app $(MOCHITESTBROWSERDIR)/
else
- cp $(FINAL_TARGET)/updater-xpcshell$(BIN_SUFFIX) $(XPCSHELLTESTROOT)/data/updater$(BIN_SUFFIX)
- cp $(FINAL_TARGET)/updater-xpcshell$(BIN_SUFFIX) $(MOCHITESTROOT)/data/updater$(BIN_SUFFIX)
+ cp $(FINAL_TARGET)/updater-xpcshell$(BIN_SUFFIX) $(XPCSHELLTESTDIR)/data/updater$(BIN_SUFFIX)
+ cp $(FINAL_TARGET)/updater-xpcshell$(BIN_SUFFIX) $(MOCHITESTCHROMEDIR)/data/updater$(BIN_SUFFIX)
+ cp $(FINAL_TARGET)/updater-xpcshell$(BIN_SUFFIX) $(MOCHITESTBROWSERDIR)/updater$(BIN_SUFFIX)
endif