summaryrefslogtreecommitdiffstats
path: root/comm/mail/base/test/webextensions
diff options
context:
space:
mode:
Diffstat (limited to 'comm/mail/base/test/webextensions')
-rw-r--r--comm/mail/base/test/webextensions/.eslintrc.js13
-rw-r--r--comm/mail/base/test/webextensions/browser.ini41
-rw-r--r--comm/mail/base/test/webextensions/browser_extension_install_experiment.js82
-rw-r--r--comm/mail/base/test/webextensions/browser_extension_sideloading.js352
-rw-r--r--comm/mail/base/test/webextensions/browser_extension_update_background.js263
-rw-r--r--comm/mail/base/test/webextensions/browser_extension_update_background_noprompt.js116
-rw-r--r--comm/mail/base/test/webextensions/browser_permissions_installTrigger.js27
-rw-r--r--comm/mail/base/test/webextensions/browser_permissions_local_file.js46
-rw-r--r--comm/mail/base/test/webextensions/browser_permissions_mozAddonManager.js19
-rw-r--r--comm/mail/base/test/webextensions/browser_permissions_optional.js53
-rw-r--r--comm/mail/base/test/webextensions/browser_permissions_pointerevent.js65
-rw-r--r--comm/mail/base/test/webextensions/browser_permissions_unsigned.js49
-rw-r--r--comm/mail/base/test/webextensions/browser_update_checkForUpdates.js17
-rw-r--r--comm/mail/base/test/webextensions/browser_update_interactive_noprompt.js82
-rw-r--r--comm/mail/base/test/webextensions/browser_webext_experiment.xpibin0 -> 2492 bytes
-rw-r--r--comm/mail/base/test/webextensions/browser_webext_experiment_permissions.xpibin0 -> 2510 bytes
-rw-r--r--comm/mail/base/test/webextensions/browser_webext_experiment_update1.xpibin0 -> 331 bytes
-rw-r--r--comm/mail/base/test/webextensions/browser_webext_experiment_update2.xpibin0 -> 2547 bytes
-rw-r--r--comm/mail/base/test/webextensions/browser_webext_nopermissions.xpibin0 -> 4273 bytes
-rw-r--r--comm/mail/base/test/webextensions/browser_webext_permissions.xpibin0 -> 16638 bytes
-rw-r--r--comm/mail/base/test/webextensions/browser_webext_unsigned.xpibin0 -> 12606 bytes
-rw-r--r--comm/mail/base/test/webextensions/browser_webext_update.json82
-rw-r--r--comm/mail/base/test/webextensions/browser_webext_update1.xpibin0 -> 4311 bytes
-rw-r--r--comm/mail/base/test/webextensions/browser_webext_update2.xpibin0 -> 4331 bytes
-rw-r--r--comm/mail/base/test/webextensions/browser_webext_update_icon1.xpibin0 -> 16585 bytes
-rw-r--r--comm/mail/base/test/webextensions/browser_webext_update_icon2.xpibin0 -> 16604 bytes
-rw-r--r--comm/mail/base/test/webextensions/browser_webext_update_origins1.xpibin0 -> 268 bytes
-rw-r--r--comm/mail/base/test/webextensions/browser_webext_update_origins2.xpibin0 -> 275 bytes
-rw-r--r--comm/mail/base/test/webextensions/browser_webext_update_perms1.xpibin0 -> 4273 bytes
-rw-r--r--comm/mail/base/test/webextensions/browser_webext_update_perms2.xpibin0 -> 4282 bytes
-rw-r--r--comm/mail/base/test/webextensions/file_install_extensions.html19
-rw-r--r--comm/mail/base/test/webextensions/head.js632
32 files changed, 1958 insertions, 0 deletions
diff --git a/comm/mail/base/test/webextensions/.eslintrc.js b/comm/mail/base/test/webextensions/.eslintrc.js
new file mode 100644
index 0000000000..12effd2e27
--- /dev/null
+++ b/comm/mail/base/test/webextensions/.eslintrc.js
@@ -0,0 +1,13 @@
+"use strict";
+
+module.exports = {
+ extends: ["plugin:mozilla/browser-test"],
+
+ env: {
+ webextensions: true,
+ },
+
+ rules: {
+ "func-names": "off",
+ },
+};
diff --git a/comm/mail/base/test/webextensions/browser.ini b/comm/mail/base/test/webextensions/browser.ini
new file mode 100644
index 0000000000..5d08a06f8f
--- /dev/null
+++ b/comm/mail/base/test/webextensions/browser.ini
@@ -0,0 +1,41 @@
+[DEFAULT]
+prefs =
+ mail.provider.suppress_dialog_on_startup=true
+ mail.spotlight.firstRunDone=true
+ mail.winsearch.firstRunDone=true
+ mailnews.start_page.override_url=about:blank
+ mailnews.start_page.url=about:blank
+ toolkit.telemetry.testing.overrideProductsCheck=true
+subsuite = thunderbird
+support-files =
+ head.js
+ file_install_extensions.html
+ browser_webext_experiment.xpi
+ browser_webext_experiment_permissions.xpi
+ browser_webext_experiment_update1.xpi
+ browser_webext_experiment_update2.xpi
+ browser_webext_permissions.xpi
+ browser_webext_nopermissions.xpi
+ browser_webext_unsigned.xpi
+ browser_webext_update1.xpi
+ browser_webext_update2.xpi
+ browser_webext_update_icon1.xpi
+ browser_webext_update_icon2.xpi
+ browser_webext_update_perms1.xpi
+ browser_webext_update_perms2.xpi
+ browser_webext_update_origins1.xpi
+ browser_webext_update_origins2.xpi
+ browser_webext_update.json
+
+[browser_extension_install_experiment.js]
+[browser_extension_sideloading.js]
+[browser_extension_update_background.js]
+[browser_extension_update_background_noprompt.js]
+[browser_permissions_installTrigger.js]
+[browser_permissions_local_file.js]
+[browser_permissions_mozAddonManager.js]
+[browser_permissions_optional.js]
+[browser_permissions_pointerevent.js]
+[browser_permissions_unsigned.js]
+[browser_update_checkForUpdates.js]
+[browser_update_interactive_noprompt.js]
diff --git a/comm/mail/base/test/webextensions/browser_extension_install_experiment.js b/comm/mail/base/test/webextensions/browser_extension_install_experiment.js
new file mode 100644
index 0000000000..d21d8bebce
--- /dev/null
+++ b/comm/mail/base/test/webextensions/browser_extension_install_experiment.js
@@ -0,0 +1,82 @@
+"use strict";
+
+async function installFile(filename) {
+ const ChromeRegistry = Cc["@mozilla.org/chrome/chrome-registry;1"].getService(
+ Ci.nsIChromeRegistry
+ );
+ let chromeUrl = Services.io.newURI(gTestPath);
+ let fileUrl = ChromeRegistry.convertChromeURL(chromeUrl);
+ let file = fileUrl.QueryInterface(Ci.nsIFileURL).file;
+ file.leafName = filename;
+
+ let MockFilePicker = SpecialPowers.MockFilePicker;
+ MockFilePicker.init(window);
+ MockFilePicker.setFiles([file]);
+ MockFilePicker.afterOpenCallback = MockFilePicker.cleanup;
+
+ let { document } = await openAddonsMgr("addons://list/extension");
+
+ // Do the install...
+ await waitAboutAddonsViewLoaded(document);
+ let installButton = document.querySelector('[action="install-from-file"]');
+ installButton.click();
+}
+
+async function testExperimentPrompt(filename) {
+ let installPromise = new Promise(resolve => {
+ let listener = {
+ onDownloadCancelled() {
+ AddonManager.removeInstallListener(listener);
+ resolve(false);
+ },
+
+ onDownloadFailed() {
+ AddonManager.removeInstallListener(listener);
+ resolve(false);
+ },
+
+ onInstallCancelled() {
+ AddonManager.removeInstallListener(listener);
+ resolve(false);
+ },
+
+ onInstallEnded() {
+ AddonManager.removeInstallListener(listener);
+ resolve(true);
+ },
+
+ onInstallFailed() {
+ AddonManager.removeInstallListener(listener);
+ resolve(false);
+ },
+ };
+ AddonManager.addInstallListener(listener);
+ });
+
+ await installFile(filename);
+
+ let panel = await promisePopupNotificationShown("addon-webext-permissions");
+ await checkNotification(
+ panel,
+ isDefaultIcon,
+ [["webext-perms-description-experiment"]],
+ false,
+ true
+ );
+ panel.secondaryButton.click();
+
+ let result = await installPromise;
+ ok(!result, "Installation was cancelled");
+ let addon = await AddonManager.getAddonByID(
+ "experiment_test@tests.mozilla.org"
+ );
+ is(addon, null, "Extension is not installed");
+
+ let tabmail = document.getElementById("tabmail");
+ tabmail.closeTab(tabmail.currentTabInfo);
+}
+
+add_task(async () => {
+ await testExperimentPrompt("browser_webext_experiment.xpi");
+ await testExperimentPrompt("browser_webext_experiment_permissions.xpi");
+});
diff --git a/comm/mail/base/test/webextensions/browser_extension_sideloading.js b/comm/mail/base/test/webextensions/browser_extension_sideloading.js
new file mode 100644
index 0000000000..eb0754a922
--- /dev/null
+++ b/comm/mail/base/test/webextensions/browser_extension_sideloading.js
@@ -0,0 +1,352 @@
+/* eslint-disable mozilla/no-arbitrary-setTimeout */
+const { AddonManagerPrivate } = ChromeUtils.importESModule(
+ "resource://gre/modules/AddonManager.sys.mjs"
+);
+
+var { AddonTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/AddonTestUtils.sys.mjs"
+);
+
+AddonTestUtils.initMochitest(this);
+
+AddonTestUtils.hookAMTelemetryEvents();
+
+const kSideloaded = true;
+
+async function createWebExtension(details) {
+ let options = {
+ manifest: {
+ applications: { gecko: { id: details.id } },
+
+ name: details.name,
+
+ permissions: details.permissions,
+ },
+ };
+
+ if (details.iconURL) {
+ options.manifest.icons = { 64: details.iconURL };
+ }
+
+ let xpi = AddonTestUtils.createTempWebExtensionFile(options);
+
+ await AddonTestUtils.manuallyInstall(xpi);
+}
+
+function promiseEvent(eventEmitter, event) {
+ return new Promise(resolve => {
+ eventEmitter.once(event, resolve);
+ });
+}
+
+function getAddonElement(managerWindow, addonId) {
+ return BrowserTestUtils.waitForCondition(
+ () =>
+ managerWindow.document.querySelector(`addon-card[addon-id="${addonId}"]`),
+ `Found entry for sideload extension addon "${addonId}" in HTML about:addons`
+ );
+}
+
+function assertSideloadedAddonElementState(addonElement, pressed) {
+ const enableBtn = addonElement.querySelector('[action="toggle-disabled"]');
+ is(
+ enableBtn.pressed,
+ pressed,
+ `The enable button is ${!pressed ? " not " : ""} pressed`
+ );
+ is(enableBtn.localName, "moz-toggle", "The enable button is a toggle");
+}
+
+function clickEnableExtension(addonElement) {
+ addonElement.querySelector('[action="toggle-disabled"]').click();
+}
+
+add_task(async function test_sideloading() {
+ const DEFAULT_ICON_URL =
+ "chrome://mozapps/skin/extensions/extensionGeneric.svg";
+
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["xpinstall.signatures.required", false],
+ ["extensions.autoDisableScopes", 15],
+ ["extensions.ui.ignoreUnsigned", true],
+ ["extensions.allowPrivateBrowsingByDefault", false],
+ ],
+ });
+
+ const ID1 = "addon1@tests.mozilla.org";
+ await createWebExtension({
+ id: ID1,
+ name: "Test 1",
+ userDisabled: true,
+ permissions: ["accountsRead", "https://*/*"],
+ iconURL: "foo-icon.png",
+ });
+
+ const ID2 = "addon2@tests.mozilla.org";
+ await createWebExtension({
+ id: ID2,
+ name: "Test 2",
+ permissions: ["<all_urls>"],
+ });
+
+ const ID3 = "addon3@tests.mozilla.org";
+ await createWebExtension({
+ id: ID3,
+ name: "Test 3",
+ permissions: ["<all_urls>"],
+ });
+
+ testCleanup = async function () {
+ // clear out ExtensionsUI state about sideloaded extensions so
+ // subsequent tests don't get confused.
+ ExtensionsUI.sideloaded.clear();
+ ExtensionsUI.emit("change");
+ };
+
+ let changePromise = new Promise(resolve => {
+ ExtensionsUI.on("change", function listener() {
+ ExtensionsUI.off("change", listener);
+ resolve();
+ });
+ });
+ ExtensionsUI._checkForSideloaded();
+ await changePromise;
+
+ // Check for the addons badge on the hamburger menu
+ let menuButton = document.getElementById("button-appmenu");
+ is(
+ menuButton.getAttribute("badge-status"),
+ "addon-alert",
+ "Should have addon alert badge"
+ );
+
+ // Find the menu entries for sideloaded extensions
+ await gCUITestUtils.openMainMenu();
+
+ let addons = PanelUI.addonNotificationContainer;
+ is(
+ addons.children.length,
+ 3,
+ "Have 3 menu entries for sideloaded extensions"
+ );
+
+ info(
+ "Test disabling sideloaded addon 1 using the permission prompt secondary button"
+ );
+
+ // Click the first sideloaded extension
+ let popupPromise = promisePopupNotificationShown("addon-webext-permissions");
+ addons.children[0].click();
+
+ // The click should hide the main menu. This is currently synchronous.
+ ok(PanelUI.panel.state != "open", "Main menu is closed or closing.");
+
+ let panel = await popupPromise;
+
+ // Check the contents of the notification, then choose "Cancel"
+ await checkNotification(
+ panel,
+ /\/foo-icon\.png$/,
+ [
+ ["webext-perms-host-description-all-urls"],
+ ["webext-perms-description-accountsRead"],
+ ],
+ kSideloaded
+ );
+
+ panel.secondaryButton.click();
+
+ let [addon1, addon2, addon3] = await AddonManager.getAddonsByIDs([
+ ID1,
+ ID2,
+ ID3,
+ ]);
+ ok(addon1.seen, "Addon should be marked as seen");
+ is(addon1.userDisabled, true, "Addon 1 should still be disabled");
+ is(addon2.userDisabled, true, "Addon 2 should still be disabled");
+ is(addon3.userDisabled, true, "Addon 3 should still be disabled");
+
+ // Should still have 2 entries in the hamburger menu
+ await gCUITestUtils.openMainMenu();
+
+ addons = PanelUI.addonNotificationContainer;
+ is(
+ addons.children.length,
+ 2,
+ "Have 2 menu entries for sideloaded extensions"
+ );
+
+ // Close the hamburger menu and go directly to the addons manager
+ await gCUITestUtils.hideMainMenu();
+
+ const VIEW = "addons://list/extension";
+ let win = await openAddonsMgr(VIEW);
+
+ await waitAboutAddonsViewLoaded(win.document);
+
+ // about:addons addon entry element.
+ const addonElement = await getAddonElement(win, ID2);
+
+ assertSideloadedAddonElementState(addonElement, false);
+
+ info("Test enabling sideloaded addon 2 from about:addons enable button");
+
+ // When clicking enable we should see the permissions notification
+ popupPromise = promisePopupNotificationShown("addon-webext-permissions");
+ clickEnableExtension(addonElement);
+ panel = await popupPromise;
+ await checkNotification(
+ panel,
+ DEFAULT_ICON_URL,
+ [["webext-perms-host-description-all-urls"]],
+ kSideloaded
+ );
+
+ // Setup async test for post install notification on addon 2
+ popupPromise = promisePopupNotificationShown("addon-installed");
+
+ // Accept the permissions
+ panel.button.click();
+ await promiseEvent(ExtensionsUI, "change");
+
+ addon2 = await AddonManager.getAddonByID(ID2);
+ is(addon2.userDisabled, false, "Addon 2 should be enabled");
+ assertSideloadedAddonElementState(addonElement, true);
+
+ // Test post install notification on addon 2.
+ panel = await popupPromise;
+ panel.button.click();
+
+ let tabmail = document.getElementById("tabmail");
+ tabmail.closeTab(tabmail.currentTabInfo);
+
+ // Should still have 1 entry in the hamburger menu
+ await gCUITestUtils.openMainMenu();
+
+ addons = PanelUI.addonNotificationContainer;
+ is(addons.children.length, 1, "Have 1 menu entry for sideloaded extensions");
+
+ PanelUI.hide();
+
+ // Open the Add-Ons Manager
+ win = await openAddonsMgr(`addons://detail/${encodeURIComponent(ID3)}`);
+
+ info("Test enabling sideloaded addon 3 from app menu");
+ // Trigger addon 3 install as triggered from the app menu, to be able to cover the
+ // post install notification that should be triggered when the permission
+ // dialog is accepted from that flow.
+ popupPromise = promisePopupNotificationShown("addon-webext-permissions");
+ ExtensionsUI.showSideloaded(tabmail, addon3);
+
+ panel = await popupPromise;
+ await checkNotification(
+ panel,
+ DEFAULT_ICON_URL,
+ [["webext-perms-host-description-all-urls"]],
+ kSideloaded
+ );
+
+ // Setup async test for post install notification on addon 3
+ popupPromise = promisePopupNotificationShown("addon-installed");
+
+ // Accept the permissions
+ panel.button.click();
+ await promiseEvent(ExtensionsUI, "change");
+
+ addon3 = await AddonManager.getAddonByID(ID3);
+ is(addon3.userDisabled, false, "Addon 3 should be enabled");
+
+ // Test post install notification on addon 3.
+ panel = await popupPromise;
+ panel.button.click();
+
+ isnot(
+ menuButton.getAttribute("badge-status"),
+ "addon-alert",
+ "Should no longer have addon alert badge"
+ );
+
+ await new Promise(resolve => setTimeout(resolve, 100));
+
+ for (let addon of [addon1, addon2, addon3]) {
+ await addon.uninstall();
+ }
+
+ tabmail.closeTab(tabmail.currentTabInfo);
+
+ // Assert that the expected AddonManager telemetry are being recorded.
+ const expectedExtra = { source: "app-profile", method: "sideload" };
+
+ const baseEvent = { object: "extension", extra: expectedExtra };
+ const createBaseEventAddon = n => ({
+ ...baseEvent,
+ value: `addon${n}@tests.mozilla.org`,
+ });
+ const getEventsForAddonId = (events, addonId) =>
+ events.filter(ev => ev.value === addonId);
+
+ const amEvents = AddonTestUtils.getAMTelemetryEvents();
+
+ // Test telemetry events for addon1 (1 permission and 1 origin).
+ info("Test telemetry events collected for addon1");
+
+ const baseEventAddon1 = createBaseEventAddon(1);
+ const collectedEventsAddon1 = getEventsForAddonId(
+ amEvents,
+ baseEventAddon1.value
+ );
+ const expectedEventsAddon1 = [
+ {
+ ...baseEventAddon1,
+ method: "sideload_prompt",
+ extra: { ...expectedExtra, num_strings: "2" },
+ },
+ { ...baseEventAddon1, method: "uninstall" },
+ ];
+
+ let i = 0;
+ for (let event of collectedEventsAddon1) {
+ Assert.deepEqual(
+ event,
+ expectedEventsAddon1[i++],
+ "Got the expected telemetry event"
+ );
+ }
+
+ is(
+ collectedEventsAddon1.length,
+ expectedEventsAddon1.length,
+ "Got the expected number of telemetry events for addon1"
+ );
+
+ const baseEventAddon2 = createBaseEventAddon(2);
+ const collectedEventsAddon2 = getEventsForAddonId(
+ amEvents,
+ baseEventAddon2.value
+ );
+ const expectedEventsAddon2 = [
+ {
+ ...baseEventAddon2,
+ method: "sideload_prompt",
+ extra: { ...expectedExtra, num_strings: "1" },
+ },
+ { ...baseEventAddon2, method: "enable" },
+ { ...baseEventAddon2, method: "uninstall" },
+ ];
+
+ i = 0;
+ for (let event of collectedEventsAddon2) {
+ Assert.deepEqual(
+ event,
+ expectedEventsAddon2[i++],
+ "Got the expected telemetry event"
+ );
+ }
+
+ is(
+ collectedEventsAddon2.length,
+ expectedEventsAddon2.length,
+ "Got the expected number of telemetry events for addon2"
+ );
+});
diff --git a/comm/mail/base/test/webextensions/browser_extension_update_background.js b/comm/mail/base/test/webextensions/browser_extension_update_background.js
new file mode 100644
index 0000000000..5b5909711a
--- /dev/null
+++ b/comm/mail/base/test/webextensions/browser_extension_update_background.js
@@ -0,0 +1,263 @@
+const { AddonManagerPrivate } = ChromeUtils.importESModule(
+ "resource://gre/modules/AddonManager.sys.mjs"
+);
+
+var { AddonTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/AddonTestUtils.sys.mjs"
+);
+
+AddonTestUtils.initMochitest(this);
+AddonTestUtils.hookAMTelemetryEvents();
+
+const ID = "update2@tests.mozilla.org";
+const ID_ICON = "update_icon2@tests.mozilla.org";
+const ID_PERMS = "update_perms@tests.mozilla.org";
+const ID_EXPERIMENT = "experiment_update@test.mozilla.org";
+const FAKE_INSTALL_TELEMETRY_SOURCE = "fake-install-source";
+
+requestLongerTimeout(2);
+
+function promiseViewLoaded(tab, viewid) {
+ let win = tab.linkedBrowser.contentWindow;
+ if (
+ win.gViewController &&
+ !win.gViewController.isLoading &&
+ win.gViewController.currentViewId == viewid
+ ) {
+ return Promise.resolve();
+ }
+
+ return waitAboutAddonsViewLoaded(win.document);
+}
+
+function getBadgeStatus() {
+ let menuButton = document.getElementById("button-appmenu");
+ return menuButton.getAttribute("badge-status");
+}
+
+function promiseBadgeChange() {
+ return new Promise(resolve => {
+ let menuButton = document.getElementById("button-appmenu");
+ new MutationObserver((mutationsList, observer) => {
+ for (let mutation of mutationsList) {
+ if (mutation.attributeName == "badge-status") {
+ observer.disconnect();
+ resolve();
+ return;
+ }
+ }
+ }).observe(menuButton, {
+ attributes: true,
+ });
+ });
+}
+
+// Set some prefs that apply to all the tests in this file
+add_setup(async function () {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ // We don't have pre-pinned certificates for the local mochitest server
+ ["extensions.install.requireBuiltInCerts", false],
+ ["extensions.update.requireBuiltInCerts", false],
+ ],
+ });
+});
+
+// Helper function to test background updates.
+async function backgroundUpdateTest(url, id, checkIconFn) {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ // Turn on background updates
+ ["extensions.update.enabled", true],
+
+ // Point updates to the local mochitest server
+ [
+ "extensions.update.background.url",
+ `${BASE}/browser_webext_update.json`,
+ ],
+ ],
+ });
+
+ // Install version 1.0 of the test extension
+ let addon = await promiseInstallAddon(url, {
+ source: FAKE_INSTALL_TELEMETRY_SOURCE,
+ });
+ let addonId = addon.id;
+
+ ok(addon, "Addon was installed");
+ is(getBadgeStatus(), "", "Should not start out with an addon alert badge");
+
+ // Trigger an update check and wait for the update for this addon
+ // to be downloaded.
+ let updatePromise = promiseInstallEvent(addon, "onDownloadEnded");
+ let badgePromise = promiseBadgeChange();
+
+ AddonManagerPrivate.backgroundUpdateCheck();
+ await Promise.all([updatePromise, badgePromise]);
+
+ is(getBadgeStatus(), "addon-alert", "Should have addon alert badge");
+
+ // Find the menu entry for the update
+ await gCUITestUtils.openMainMenu();
+
+ let addons = PanelUI.addonNotificationContainer;
+ is(addons.children.length, 1, "Have a menu entry for the update");
+
+ // Click the menu item
+ let popupPromise = promisePopupNotificationShown("addon-webext-permissions");
+ addons.children[0].click();
+
+ // The click should hide the main menu. This is currently synchronous.
+ ok(PanelUI.panel.state != "open", "Main menu is closed or closing.");
+
+ // Wait for the permission prompt, check the contents
+ let panel = await popupPromise;
+ checkIconFn(panel.getAttribute("icon"));
+
+ // The original extension has 1 promptable permission and the new one
+ // has 2 (history and <all_urls>) plus 1 non-promptable permission (cookies).
+ // So we should only see the 1 new promptable permission in the notification.
+ let singlePermissionEl = document.getElementById(
+ "addon-webext-perm-single-entry"
+ );
+ ok(!singlePermissionEl.hidden, "Single permission entry is not hidden");
+ ok(singlePermissionEl.textContent, "Single permission entry text is set");
+
+ // Cancel the update.
+ panel.secondaryButton.click();
+
+ addon = await AddonManager.getAddonByID(id);
+ is(addon.version, "1.0", "Should still be running the old version");
+
+ // Alert badge and hamburger menu items should be gone
+ is(getBadgeStatus(), "", "Addon alert badge should be gone");
+
+ await gCUITestUtils.openMainMenu();
+ addons = PanelUI.addonNotificationContainer;
+ is(addons.children.length, 0, "Update menu entries should be gone");
+ await gCUITestUtils.hideMainMenu();
+
+ // Re-check for an update
+ updatePromise = promiseInstallEvent(addon, "onDownloadEnded");
+ badgePromise = promiseBadgeChange();
+ await AddonManagerPrivate.backgroundUpdateCheck();
+ await Promise.all([updatePromise, badgePromise]);
+
+ is(getBadgeStatus(), "addon-alert", "Should have addon alert badge");
+
+ // Find the menu entry for the update
+ await gCUITestUtils.openMainMenu();
+
+ addons = PanelUI.addonNotificationContainer;
+ is(addons.children.length, 1, "Have a menu entry for the update");
+
+ // Click the menu item
+ popupPromise = promisePopupNotificationShown("addon-webext-permissions");
+ addons.children[0].click();
+
+ // Wait for the permission prompt and accept it this time
+ updatePromise = waitForUpdate(addon);
+ panel = await popupPromise;
+ panel.button.click();
+
+ addon = await updatePromise;
+ is(addon.version, "2.0", "Should have upgraded to the new version");
+
+ is(getBadgeStatus(), "", "Addon alert badge should be gone");
+
+ await addon.uninstall();
+ await SpecialPowers.popPrefEnv();
+
+ // Test that the expected telemetry events have been recorded (and that they include the
+ // permission_prompt event).
+ const amEvents = AddonTestUtils.getAMTelemetryEvents();
+ const updateEvents = amEvents
+ .filter(evt => evt.method === "update")
+ .map(evt => {
+ delete evt.value;
+ return evt;
+ });
+
+ Assert.deepEqual(
+ updateEvents.map(evt => evt.extra && evt.extra.step),
+ [
+ // First update (cancelled).
+ "started",
+ "download_started",
+ "download_completed",
+ "permissions_prompt",
+ "cancelled",
+ // Second update (completed).
+ "started",
+ "download_started",
+ "download_completed",
+ "permissions_prompt",
+ "completed",
+ ],
+ "Got the steps from the collected telemetry events"
+ );
+
+ const method = "update";
+ const object = "extension";
+ const baseExtra = {
+ addon_id: addonId,
+ source: FAKE_INSTALL_TELEMETRY_SOURCE,
+ step: "permissions_prompt",
+ updated_from: "app",
+ };
+
+ // Expect the telemetry events to have num_strings set to 1, as only the origin permissions is going
+ // to be listed in the permission prompt.
+ Assert.deepEqual(
+ updateEvents.filter(
+ evt => evt.extra && evt.extra.step === "permissions_prompt"
+ ),
+ [
+ { method, object, extra: { ...baseExtra, num_strings: "1" } },
+ { method, object, extra: { ...baseExtra, num_strings: "1" } },
+ ],
+ "Got the expected permission_prompts events"
+ );
+}
+
+function checkDefaultIcon(icon) {
+ is(
+ icon,
+ "chrome://mozapps/skin/extensions/extensionGeneric.svg",
+ "Popup has the default extension icon"
+ );
+}
+
+add_task(() =>
+ backgroundUpdateTest(
+ `${BASE}/browser_webext_update1.xpi`,
+ ID,
+ checkDefaultIcon
+ )
+);
+
+function checkNonDefaultIcon(icon) {
+ // The icon should come from the extension, don't bother with the precise
+ // path, just make sure we've got a jar url pointing to the right path
+ // inside the jar.
+ ok(icon.startsWith("jar:file://"), "Icon is a jar url");
+ ok(icon.endsWith("/icon.png"), "Icon is icon.png inside a jar");
+}
+
+add_task(() =>
+ backgroundUpdateTest(
+ `${BASE}/browser_webext_update_icon1.xpi`,
+ ID_ICON,
+ checkNonDefaultIcon
+ )
+);
+
+// Check bug 1710359 did not introduce a loophole and a simple WebExtension being
+// upgraded to an Experiment prompts for the permission update.
+add_task(() =>
+ backgroundUpdateTest(
+ `${BASE}/browser_webext_experiment_update1.xpi`,
+ ID_EXPERIMENT,
+ checkDefaultIcon
+ )
+);
diff --git a/comm/mail/base/test/webextensions/browser_extension_update_background_noprompt.js b/comm/mail/base/test/webextensions/browser_extension_update_background_noprompt.js
new file mode 100644
index 0000000000..d0cb135368
--- /dev/null
+++ b/comm/mail/base/test/webextensions/browser_extension_update_background_noprompt.js
@@ -0,0 +1,116 @@
+const { AddonManagerPrivate } = ChromeUtils.importESModule(
+ "resource://gre/modules/AddonManager.sys.mjs"
+);
+
+var { AddonTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/AddonTestUtils.sys.mjs"
+);
+
+AddonTestUtils.initMochitest(this);
+AddonTestUtils.hookAMTelemetryEvents();
+
+const ID_PERMS = "update_perms@tests.mozilla.org";
+const ID_ORIGINS = "update_origins@tests.mozilla.org";
+const ID_EXPERIMENT = "experiment_test@tests.mozilla.org";
+
+function getBadgeStatus() {
+ let menuButton = document.getElementById("button-appmenu");
+ return menuButton.getAttribute("badge-status");
+}
+
+// Set some prefs that apply to all the tests in this file
+add_setup(async function () {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ // We don't have pre-pinned certificates for the local mochitest server
+ ["extensions.install.requireBuiltInCerts", false],
+ ["extensions.update.requireBuiltInCerts", false],
+ // Don't require the extensions to be signed
+ ["xpinstall.signatures.required", false],
+ ],
+ });
+});
+
+// Helper function to test an upgrade that should not show a prompt
+async function testNoPrompt(origUrl, id) {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ // Turn on background updates
+ ["extensions.update.enabled", true],
+
+ // Point updates to the local mochitest server
+ [
+ "extensions.update.background.url",
+ `${BASE}/browser_webext_update.json`,
+ ],
+ ],
+ });
+
+ // Install version 1.0 of the test extension
+ let addon = await promiseInstallAddon(origUrl);
+
+ ok(addon, "Addon was installed");
+
+ let sawPopup = false;
+ PopupNotifications.panel.addEventListener(
+ "popupshown",
+ () => (sawPopup = true),
+ { once: true }
+ );
+
+ // Trigger an update check and wait for the update to be applied.
+ let updatePromise = waitForUpdate(addon);
+ AddonManagerPrivate.backgroundUpdateCheck();
+ await updatePromise;
+
+ // There should be no notifications about the update
+ is(getBadgeStatus(), "", "Should not have addon alert badge");
+
+ await gCUITestUtils.openMainMenu();
+ let addons = PanelUI.addonNotificationContainer;
+ is(addons.children.length, 0, "Have 0 updates in the PanelUI menu");
+ await gCUITestUtils.hideMainMenu();
+
+ ok(!sawPopup, "Should not have seen permissions notification");
+
+ addon = await AddonManager.getAddonByID(id);
+ is(addon.version, "2.0", "Update should have applied");
+
+ await addon.uninstall();
+ await SpecialPowers.popPrefEnv();
+
+ // Test that the expected telemetry events have been recorded (and that they do not
+ // include the permission_prompt event).
+ const amEvents = AddonTestUtils.getAMTelemetryEvents();
+ const updateEventsSteps = amEvents
+ .filter(evt => {
+ return evt.method === "update" && evt.extra && evt.extra.addon_id == id;
+ })
+ .map(evt => {
+ return evt.extra.step;
+ });
+
+ // Expect telemetry events related to a completed update with no permissions_prompt event.
+ Assert.deepEqual(
+ updateEventsSteps,
+ ["started", "download_started", "download_completed", "completed"],
+ "Got the steps from the collected telemetry events"
+ );
+}
+
+// Test that an update that adds new non-promptable permissions is just
+// applied without showing a notification dialog.
+add_task(() =>
+ testNoPrompt(`${BASE}/browser_webext_update_perms1.xpi`, ID_PERMS)
+);
+
+// Test that an update that narrows origin permissions is just applied without
+// showing a notification prompt
+add_task(() =>
+ testNoPrompt(`${BASE}/browser_webext_update_origins1.xpi`, ID_ORIGINS)
+);
+
+// Test that an Experiment is not prompting for additional permissions.
+add_task(() =>
+ testNoPrompt(`${BASE}/browser_webext_experiment.xpi`, ID_EXPERIMENT)
+);
diff --git a/comm/mail/base/test/webextensions/browser_permissions_installTrigger.js b/comm/mail/base/test/webextensions/browser_permissions_installTrigger.js
new file mode 100644
index 0000000000..37f8117ab3
--- /dev/null
+++ b/comm/mail/base/test/webextensions/browser_permissions_installTrigger.js
@@ -0,0 +1,27 @@
+"use strict";
+
+const INSTALL_PAGE = `${BASE}/file_install_extensions.html`;
+
+async function installTrigger(filename) {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["extensions.InstallTrigger.enabled", true],
+ ["extensions.InstallTriggerImpl.enabled", true],
+ // Relax the user input requirements while running this test.
+ ["xpinstall.userActivation.required", false],
+ ],
+ });
+ let gBrowser = document.getElementById("tabmail");
+ BrowserTestUtils.loadURIString(gBrowser.selectedBrowser, INSTALL_PAGE);
+ await BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
+
+ SpecialPowers.spawn(
+ gBrowser.selectedBrowser,
+ [`${BASE}/${filename}`],
+ async function (url) {
+ content.wrappedJSObject.installTrigger(url);
+ }
+ );
+}
+
+add_task(() => testInstallMethod(installTrigger));
diff --git a/comm/mail/base/test/webextensions/browser_permissions_local_file.js b/comm/mail/base/test/webextensions/browser_permissions_local_file.js
new file mode 100644
index 0000000000..03cf35226c
--- /dev/null
+++ b/comm/mail/base/test/webextensions/browser_permissions_local_file.js
@@ -0,0 +1,46 @@
+"use strict";
+
+async function installFile(filename) {
+ const ChromeRegistry = Cc["@mozilla.org/chrome/chrome-registry;1"].getService(
+ Ci.nsIChromeRegistry
+ );
+ let chromeUrl = Services.io.newURI(gTestPath);
+ let fileUrl = ChromeRegistry.convertChromeURL(chromeUrl);
+ let file = fileUrl.QueryInterface(Ci.nsIFileURL).file;
+ file.leafName = filename;
+
+ let MockFilePicker = SpecialPowers.MockFilePicker;
+ MockFilePicker.init(window);
+ MockFilePicker.setFiles([file]);
+ MockFilePicker.afterOpenCallback = MockFilePicker.cleanup;
+
+ let { document } = await openAddonsMgr("addons://list/extension");
+
+ // Do the install...
+ await waitAboutAddonsViewLoaded(document);
+ let installButton = document.querySelector('[action="install-from-file"]');
+ installButton.click();
+}
+
+add_task(async function test_install_extension_from_local_file() {
+ // Listen for the first installId so we can check it later.
+ let firstInstallId = null;
+ AddonManager.addInstallListener({
+ onNewInstall(install) {
+ firstInstallId = install.installId;
+ AddonManager.removeInstallListener(this);
+ },
+ });
+
+ // Install the add-ons.
+ await testInstallMethod(installFile);
+
+ // Check we got an installId.
+ ok(
+ firstInstallId != null && !isNaN(firstInstallId),
+ "There was an installId found"
+ );
+
+ let tabmail = document.getElementById("tabmail");
+ tabmail.closeTab(tabmail.currentTabInfo);
+});
diff --git a/comm/mail/base/test/webextensions/browser_permissions_mozAddonManager.js b/comm/mail/base/test/webextensions/browser_permissions_mozAddonManager.js
new file mode 100644
index 0000000000..4f1a064760
--- /dev/null
+++ b/comm/mail/base/test/webextensions/browser_permissions_mozAddonManager.js
@@ -0,0 +1,19 @@
+"use strict";
+
+const INSTALL_PAGE = `${BASE}/file_install_extensions.html`;
+
+async function installMozAM(filename) {
+ let browser = document.getElementById("tabmail").selectedBrowser;
+ BrowserTestUtils.loadURIString(browser, INSTALL_PAGE);
+ await BrowserTestUtils.browserLoaded(browser);
+
+ await SpecialPowers.spawn(
+ browser,
+ [`${BASE}/${filename}`],
+ async function (url) {
+ await content.wrappedJSObject.installMozAM(url);
+ }
+ );
+}
+
+add_task(() => testInstallMethod(installMozAM));
diff --git a/comm/mail/base/test/webextensions/browser_permissions_optional.js b/comm/mail/base/test/webextensions/browser_permissions_optional.js
new file mode 100644
index 0000000000..750658a8fd
--- /dev/null
+++ b/comm/mail/base/test/webextensions/browser_permissions_optional.js
@@ -0,0 +1,53 @@
+"use strict";
+add_task(async function test_request_permissions_without_prompt() {
+ async function pageScript() {
+ const NO_PROMPT_PERM = "activeTab";
+ window.addEventListener(
+ "keypress",
+ async () => {
+ let permGranted = await browser.permissions.request({
+ permissions: [NO_PROMPT_PERM],
+ });
+ browser.test.assertTrue(
+ permGranted,
+ `${NO_PROMPT_PERM} permission was granted.`
+ );
+ let perms = await browser.permissions.getAll();
+ browser.test.assertTrue(
+ perms.permissions.includes(NO_PROMPT_PERM),
+ `${NO_PROMPT_PERM} permission exists.`
+ );
+ browser.test.sendMessage("permsGranted");
+ },
+ { once: true }
+ );
+ browser.test.sendMessage("pageReady");
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ background() {
+ browser.test.sendMessage("ready", browser.runtime.getURL("page.html"));
+ },
+ files: {
+ "page.html": `<html><head><script src="page.js"></script></head></html>`,
+ "page.js": pageScript,
+ },
+ manifest: {
+ optional_permissions: ["activeTab"],
+ },
+ });
+ await extension.startup();
+
+ let url = await extension.awaitMessage("ready");
+
+ let tab = openContentTab(url, undefined, null);
+ await extension.awaitMessage("pageReady");
+ await new Promise(resolve => requestAnimationFrame(resolve));
+ await BrowserTestUtils.synthesizeMouseAtCenter(tab.browser, {}, tab.browser);
+ await BrowserTestUtils.synthesizeKey("a", {}, tab.browser);
+ await extension.awaitMessage("permsGranted");
+ await extension.unload();
+
+ let tabmail = document.getElementById("tabmail");
+ tabmail.closeTab(tab);
+});
diff --git a/comm/mail/base/test/webextensions/browser_permissions_pointerevent.js b/comm/mail/base/test/webextensions/browser_permissions_pointerevent.js
new file mode 100644
index 0000000000..7f273dc79c
--- /dev/null
+++ b/comm/mail/base/test/webextensions/browser_permissions_pointerevent.js
@@ -0,0 +1,65 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+add_task(async function test_pointerevent() {
+ async function contentScript() {
+ document.addEventListener("pointerdown", async e => {
+ browser.test.assertTrue(true, "Should receive pointerdown");
+ e.preventDefault();
+ });
+
+ document.addEventListener("mousedown", e => {
+ browser.test.assertTrue(true, "Should receive mousedown");
+ });
+
+ document.addEventListener("mouseup", e => {
+ browser.test.assertTrue(true, "Should receive mouseup");
+ });
+
+ document.addEventListener("pointerup", e => {
+ browser.test.assertTrue(true, "Should receive pointerup");
+ browser.test.sendMessage("done");
+ });
+ browser.test.sendMessage("pageReady");
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ background() {
+ browser.test.sendMessage("ready", browser.runtime.getURL("page.html"));
+ },
+ files: {
+ "page.html": `<html><head><script src="page.js"></script></head></html>`,
+ "page.js": contentScript,
+ },
+ });
+ await extension.startup();
+ await new Promise(resolve => {
+ SpecialPowers.pushPrefEnv(
+ { set: [["dom.w3c_pointer_events.enabled", true]] },
+ resolve
+ );
+ });
+ let url = await extension.awaitMessage("ready");
+ let tab = openContentTab(url, undefined, null);
+
+ await extension.awaitMessage("pageReady");
+ await new Promise(resolve => requestAnimationFrame(resolve));
+ tab.linkedBrowser.focus();
+ await BrowserTestUtils.synthesizeMouseAtCenter(
+ "html",
+ { type: "mousedown", button: 0 },
+ tab.linkedBrowser
+ );
+ await BrowserTestUtils.synthesizeMouseAtCenter(
+ "html",
+ { type: "mouseup", button: 0 },
+ tab.linkedBrowser
+ );
+ await extension.awaitMessage("done");
+
+ await extension.unload();
+
+ let tabmail = document.getElementById("tabmail");
+ tabmail.closeTab(tab);
+});
diff --git a/comm/mail/base/test/webextensions/browser_permissions_unsigned.js b/comm/mail/base/test/webextensions/browser_permissions_unsigned.js
new file mode 100644
index 0000000000..06f0b2aa14
--- /dev/null
+++ b/comm/mail/base/test/webextensions/browser_permissions_unsigned.js
@@ -0,0 +1,49 @@
+"use strict";
+
+const ID = "permissions@test.mozilla.org";
+const WARNING_ICON = "chrome://browser/skin/warning.svg";
+
+add_task(async function test_unsigned() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["extensions.InstallTrigger.enabled", true],
+ ["extensions.InstallTriggerImpl.enabled", true],
+ ["extensions.webapi.testing", true],
+ ["extensions.install.requireBuiltInCerts", false],
+ // Relax the user input requirements while running this test.
+ ["xpinstall.userActivation.required", false],
+ ],
+ });
+
+ let testURI = makeURI("https://example.com/");
+ PermissionTestUtils.add(testURI, "install", Services.perms.ALLOW_ACTION);
+ registerCleanupFunction(() => PermissionTestUtils.remove(testURI, "install"));
+
+ let tab = openContentTab("about:blank");
+ BrowserTestUtils.loadURIString(
+ tab.linkedBrowser,
+ `${BASE}/file_install_extensions.html`
+ );
+ await BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+
+ SpecialPowers.spawn(
+ tab.linkedBrowser,
+ [`${BASE}/browser_webext_unsigned.xpi`],
+ async function (url) {
+ content.wrappedJSObject.installTrigger(url);
+ }
+ );
+
+ let panel = await promisePopupNotificationShown("addon-webext-permissions");
+
+ // cancel the install
+ let promise = promiseInstallEvent({ id: ID }, "onInstallCancelled");
+ panel.secondaryButton.click();
+ await promise;
+
+ let addon = await AddonManager.getAddonByID(ID);
+ is(addon, null, "Extension is not installed");
+
+ let tabmail = document.getElementById("tabmail");
+ tabmail.closeTab(tab);
+});
diff --git a/comm/mail/base/test/webextensions/browser_update_checkForUpdates.js b/comm/mail/base/test/webextensions/browser_update_checkForUpdates.js
new file mode 100644
index 0000000000..b902527cae
--- /dev/null
+++ b/comm/mail/base/test/webextensions/browser_update_checkForUpdates.js
@@ -0,0 +1,17 @@
+// Invoke the "Check for Updates" menu item
+function checkAll(win) {
+ triggerPageOptionsAction(win, "check-for-updates");
+ return new Promise(resolve => {
+ let observer = {
+ observe(subject, topic, data) {
+ Services.obs.removeObserver(observer, "EM-update-check-finished");
+ resolve();
+ },
+ };
+ Services.obs.addObserver(observer, "EM-update-check-finished");
+ });
+}
+
+// Test "Check for Updates" with both auto-update settings
+add_task(() => interactiveUpdateTest(true, checkAll));
+add_task(() => interactiveUpdateTest(false, checkAll));
diff --git a/comm/mail/base/test/webextensions/browser_update_interactive_noprompt.js b/comm/mail/base/test/webextensions/browser_update_interactive_noprompt.js
new file mode 100644
index 0000000000..5d391de662
--- /dev/null
+++ b/comm/mail/base/test/webextensions/browser_update_interactive_noprompt.js
@@ -0,0 +1,82 @@
+// Set some prefs that apply to all the tests in this file.
+add_setup(async function () {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ // We don't have pre-pinned certificates for the local mochitest server.
+ ["extensions.install.requireBuiltInCerts", false],
+ ["extensions.update.requireBuiltInCerts", false],
+
+ // Don't require the extensions to be signed.
+ ["xpinstall.signatures.required", false],
+
+ // Point updates to the local mochitest server.
+ ["extensions.update.url", `${BASE}/browser_webext_update.json`],
+ ],
+ });
+});
+
+// Helper to test that an update of a given extension does not
+// generate any permission prompts.
+async function testUpdateNoPrompt(
+ filename,
+ id,
+ initialVersion = "1.0",
+ updateVersion = "2.0"
+) {
+ // Install initial version of the test extension
+ let addon = await promiseInstallAddon(`${BASE}/${filename}`);
+ ok(addon, "Addon was installed");
+ is(addon.version, initialVersion, "Version 1 of the addon is installed");
+
+ // Go to Extensions in about:addons
+ let win = await openAddonsMgr("addons://list/extension");
+
+ await waitAboutAddonsViewLoaded(win.document);
+
+ let sawPopup = false;
+ function popupListener() {
+ sawPopup = true;
+ }
+ PopupNotifications.panel.addEventListener("popupshown", popupListener);
+
+ // Trigger an update check, we should see the update get applied
+ let updatePromise = waitForUpdate(addon);
+ triggerPageOptionsAction(win, "check-for-updates");
+ await updatePromise;
+
+ addon = await AddonManager.getAddonByID(id);
+ is(addon.version, updateVersion, "Should have upgraded");
+
+ ok(!sawPopup, "Should not have seen a permission notification");
+ PopupNotifications.panel.removeEventListener("popupshown", popupListener);
+
+ let tabmail = document.getElementById("tabmail");
+ tabmail.closeTab(tabmail.currentTabInfo);
+ await addon.uninstall();
+}
+
+// Test that we don't see a prompt when no new promptable permissions
+// are added.
+add_task(() =>
+ testUpdateNoPrompt(
+ "browser_webext_update_perms1.xpi",
+ "update_perms@tests.mozilla.org"
+ )
+);
+
+// Test that an update that narrows origin permissions is just applied without
+// showing a notification prompt.
+add_task(() =>
+ testUpdateNoPrompt(
+ "browser_webext_update_origins1.xpi",
+ "update_origins@tests.mozilla.org"
+ )
+);
+
+// Test that an Experiment is not prompting for additional permissions.
+add_task(() =>
+ testUpdateNoPrompt(
+ "browser_webext_experiment.xpi",
+ "experiment_test@tests.mozilla.org"
+ )
+);
diff --git a/comm/mail/base/test/webextensions/browser_webext_experiment.xpi b/comm/mail/base/test/webextensions/browser_webext_experiment.xpi
new file mode 100644
index 0000000000..982d5d6b25
--- /dev/null
+++ b/comm/mail/base/test/webextensions/browser_webext_experiment.xpi
Binary files differ
diff --git a/comm/mail/base/test/webextensions/browser_webext_experiment_permissions.xpi b/comm/mail/base/test/webextensions/browser_webext_experiment_permissions.xpi
new file mode 100644
index 0000000000..d659b876fe
--- /dev/null
+++ b/comm/mail/base/test/webextensions/browser_webext_experiment_permissions.xpi
Binary files differ
diff --git a/comm/mail/base/test/webextensions/browser_webext_experiment_update1.xpi b/comm/mail/base/test/webextensions/browser_webext_experiment_update1.xpi
new file mode 100644
index 0000000000..bb4a5fb008
--- /dev/null
+++ b/comm/mail/base/test/webextensions/browser_webext_experiment_update1.xpi
Binary files differ
diff --git a/comm/mail/base/test/webextensions/browser_webext_experiment_update2.xpi b/comm/mail/base/test/webextensions/browser_webext_experiment_update2.xpi
new file mode 100644
index 0000000000..5f57efe3c2
--- /dev/null
+++ b/comm/mail/base/test/webextensions/browser_webext_experiment_update2.xpi
Binary files differ
diff --git a/comm/mail/base/test/webextensions/browser_webext_nopermissions.xpi b/comm/mail/base/test/webextensions/browser_webext_nopermissions.xpi
new file mode 100644
index 0000000000..ab97d96a11
--- /dev/null
+++ b/comm/mail/base/test/webextensions/browser_webext_nopermissions.xpi
Binary files differ
diff --git a/comm/mail/base/test/webextensions/browser_webext_permissions.xpi b/comm/mail/base/test/webextensions/browser_webext_permissions.xpi
new file mode 100644
index 0000000000..307c25a839
--- /dev/null
+++ b/comm/mail/base/test/webextensions/browser_webext_permissions.xpi
Binary files differ
diff --git a/comm/mail/base/test/webextensions/browser_webext_unsigned.xpi b/comm/mail/base/test/webextensions/browser_webext_unsigned.xpi
new file mode 100644
index 0000000000..2ebc23b4fe
--- /dev/null
+++ b/comm/mail/base/test/webextensions/browser_webext_unsigned.xpi
Binary files differ
diff --git a/comm/mail/base/test/webextensions/browser_webext_update.json b/comm/mail/base/test/webextensions/browser_webext_update.json
new file mode 100644
index 0000000000..e44372d50c
--- /dev/null
+++ b/comm/mail/base/test/webextensions/browser_webext_update.json
@@ -0,0 +1,82 @@
+{
+ "addons": {
+ "update2@tests.mozilla.org": {
+ "updates": [
+ {
+ "version": "2.0",
+ "update_link": "https://example.com/browser/comm/mail/base/test/webextensions/browser_webext_update2.xpi",
+ "applications": {
+ "gecko": {
+ "strict_min_version": "1"
+ }
+ }
+ }
+ ]
+ },
+ "update_icon2@tests.mozilla.org": {
+ "updates": [
+ {
+ "version": "2.0",
+ "update_link": "https://example.com/browser/comm/mail/base/test/webextensions/browser_webext_update_icon2.xpi",
+ "applications": {
+ "gecko": {
+ "strict_min_version": "1"
+ }
+ }
+ }
+ ]
+ },
+ "update_perms@tests.mozilla.org": {
+ "updates": [
+ {
+ "version": "2.0",
+ "update_link": "https://example.com/browser/comm/mail/base/test/webextensions/browser_webext_update_perms2.xpi",
+ "applications": {
+ "gecko": {
+ "strict_min_version": "1"
+ }
+ }
+ }
+ ]
+ },
+ "update_origins@tests.mozilla.org": {
+ "updates": [
+ {
+ "version": "2.0",
+ "update_link": "https://example.com/browser/comm/mail/base/test/webextensions/browser_webext_update_origins2.xpi",
+ "applications": {
+ "gecko": {
+ "strict_min_version": "1"
+ }
+ }
+ }
+ ]
+ },
+ "experiment_test@tests.mozilla.org": {
+ "updates": [
+ {
+ "version": "2.0",
+ "update_link": "https://example.com/browser/comm/mail/base/test/webextensions/browser_webext_experiment_permissions.xpi",
+ "applications": {
+ "gecko": {
+ "strict_min_version": "1"
+ }
+ }
+ }
+ ]
+ },
+ "experiment_update@test.mozilla.org": {
+ "updates": [
+ {
+ "version": "2.0",
+ "update_link": "https://example.com/browser/comm/mail/base/test/webextensions/browser_webext_experiment_update2.xpi",
+ "applications": {
+ "gecko": {
+ "strict_min_version": "1"
+ }
+ }
+ }
+ ]
+ }
+ }
+}
diff --git a/comm/mail/base/test/webextensions/browser_webext_update1.xpi b/comm/mail/base/test/webextensions/browser_webext_update1.xpi
new file mode 100644
index 0000000000..79be90636c
--- /dev/null
+++ b/comm/mail/base/test/webextensions/browser_webext_update1.xpi
Binary files differ
diff --git a/comm/mail/base/test/webextensions/browser_webext_update2.xpi b/comm/mail/base/test/webextensions/browser_webext_update2.xpi
new file mode 100644
index 0000000000..d1a12cadca
--- /dev/null
+++ b/comm/mail/base/test/webextensions/browser_webext_update2.xpi
Binary files differ
diff --git a/comm/mail/base/test/webextensions/browser_webext_update_icon1.xpi b/comm/mail/base/test/webextensions/browser_webext_update_icon1.xpi
new file mode 100644
index 0000000000..d3dcb3235d
--- /dev/null
+++ b/comm/mail/base/test/webextensions/browser_webext_update_icon1.xpi
Binary files differ
diff --git a/comm/mail/base/test/webextensions/browser_webext_update_icon2.xpi b/comm/mail/base/test/webextensions/browser_webext_update_icon2.xpi
new file mode 100644
index 0000000000..5cd7a8cec4
--- /dev/null
+++ b/comm/mail/base/test/webextensions/browser_webext_update_icon2.xpi
Binary files differ
diff --git a/comm/mail/base/test/webextensions/browser_webext_update_origins1.xpi b/comm/mail/base/test/webextensions/browser_webext_update_origins1.xpi
new file mode 100644
index 0000000000..2909f8e8fd
--- /dev/null
+++ b/comm/mail/base/test/webextensions/browser_webext_update_origins1.xpi
Binary files differ
diff --git a/comm/mail/base/test/webextensions/browser_webext_update_origins2.xpi b/comm/mail/base/test/webextensions/browser_webext_update_origins2.xpi
new file mode 100644
index 0000000000..b1051affb1
--- /dev/null
+++ b/comm/mail/base/test/webextensions/browser_webext_update_origins2.xpi
Binary files differ
diff --git a/comm/mail/base/test/webextensions/browser_webext_update_perms1.xpi b/comm/mail/base/test/webextensions/browser_webext_update_perms1.xpi
new file mode 100644
index 0000000000..f4942f9082
--- /dev/null
+++ b/comm/mail/base/test/webextensions/browser_webext_update_perms1.xpi
Binary files differ
diff --git a/comm/mail/base/test/webextensions/browser_webext_update_perms2.xpi b/comm/mail/base/test/webextensions/browser_webext_update_perms2.xpi
new file mode 100644
index 0000000000..2c023edc9d
--- /dev/null
+++ b/comm/mail/base/test/webextensions/browser_webext_update_perms2.xpi
Binary files differ
diff --git a/comm/mail/base/test/webextensions/file_install_extensions.html b/comm/mail/base/test/webextensions/file_install_extensions.html
new file mode 100644
index 0000000000..9dd8ae830d
--- /dev/null
+++ b/comm/mail/base/test/webextensions/file_install_extensions.html
@@ -0,0 +1,19 @@
+<!DOCTYPE html>
+
+<html>
+<head>
+ <meta charset="utf-8">
+</head>
+<body>
+<script type="text/javascript">
+function installMozAM(url) {
+ return navigator.mozAddonManager.createInstall({url})
+ .then(install => install.install());
+}
+
+function installTrigger(url) {
+ InstallTrigger.install({extension: url});
+}
+</script>
+</body>
+</html>
diff --git a/comm/mail/base/test/webextensions/head.js b/comm/mail/base/test/webextensions/head.js
new file mode 100644
index 0000000000..9fc4e05cbc
--- /dev/null
+++ b/comm/mail/base/test/webextensions/head.js
@@ -0,0 +1,632 @@
+/* globals openAddonsMgr, openContentTab */
+
+ChromeUtils.defineESModuleGetters(this, {
+ AddonTestUtils: "resource://testing-common/AddonTestUtils.sys.mjs",
+ Management: "resource://gre/modules/Extension.sys.mjs",
+});
+
+const BASE = getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content/",
+ "https://example.com/"
+);
+
+const l10n = new Localization([
+ "toolkit/global/extensions.ftl",
+ "toolkit/global/extensionPermissions.ftl",
+ "messenger/extensionsUI.ftl",
+ "messenger/extensionPermissions.ftl",
+ "messenger/addonNotifications.ftl",
+ "branding/brand.ftl",
+]);
+
+var { CustomizableUITestUtils } = ChromeUtils.import(
+ "resource://testing-common/CustomizableUITestUtils.jsm"
+);
+let gCUITestUtils = new CustomizableUITestUtils(window);
+
+const { PermissionTestUtils } = ChromeUtils.import(
+ "resource://testing-common/PermissionTestUtils.jsm"
+);
+
+/**
+ * Wait for the given PopupNotification to display
+ *
+ * @param {string} name
+ * The name of the notification to wait for.
+ *
+ * @returns {Promise}
+ * Resolves with the notification window.
+ */
+function promisePopupNotificationShown(name) {
+ return new Promise(resolve => {
+ function popupshown() {
+ let notification = PopupNotifications.getNotification(name);
+ if (!notification) {
+ return;
+ }
+
+ ok(notification, `${name} notification shown`);
+ ok(PopupNotifications.isPanelOpen, "notification panel open");
+
+ PopupNotifications.panel.removeEventListener("popupshown", popupshown);
+ resolve(PopupNotifications.panel.firstElementChild);
+ }
+
+ PopupNotifications.panel.addEventListener("popupshown", popupshown);
+ });
+}
+
+/**
+ * Wait for a specific install event to fire for a given addon
+ *
+ * @param {AddonWrapper} addon
+ * The addon to watch for an event on
+ * @param {string}
+ * The name of the event to watch for (e.g., onInstallEnded)
+ *
+ * @returns {Promise}
+ * Resolves when the event triggers with the first argument
+ * to the event handler as the resolution value.
+ */
+function promiseInstallEvent(addon, event) {
+ return new Promise(resolve => {
+ let listener = {};
+ listener[event] = (install, arg) => {
+ if (install.addon.id == addon.id) {
+ AddonManager.removeInstallListener(listener);
+ resolve(arg);
+ }
+ };
+ AddonManager.addInstallListener(listener);
+ });
+}
+
+/**
+ * Install an (xpi packaged) extension
+ *
+ * @param {string} url
+ * URL of the .xpi file to install
+ * @param {object?} installTelemetryInfo
+ * an optional object that contains additional details used by the telemetry events.
+ *
+ * @returns {Promise}
+ * Resolves when the extension has been installed with the Addon
+ * object as the resolution value.
+ */
+async function promiseInstallAddon(url, telemetryInfo) {
+ let install = await AddonManager.getInstallForURL(url, { telemetryInfo });
+ install.install();
+
+ let addon = await new Promise(resolve => {
+ install.addListener({
+ onInstallEnded(_install, _addon) {
+ resolve(_addon);
+ },
+ });
+ });
+
+ if (addon.isWebExtension) {
+ await new Promise(resolve => {
+ function listener(event, extension) {
+ if (extension.id == addon.id) {
+ Management.off("ready", listener);
+ resolve();
+ }
+ }
+ Management.on("ready", listener);
+ });
+ }
+
+ return addon;
+}
+
+/**
+ * Wait for an update to the given webextension to complete.
+ * (This does not actually perform an update, it just watches for
+ * the events that occur as a result of an update.)
+ *
+ * @param {AddonWrapper} addon
+ * The addon to be updated.
+ *
+ * @returns {Promise}
+ * Resolves when the extension has ben updated.
+ */
+async function waitForUpdate(addon) {
+ let installPromise = promiseInstallEvent(addon, "onInstallEnded");
+ let readyPromise = new Promise(resolve => {
+ function listener(event, extension) {
+ if (extension.id == addon.id) {
+ Management.off("ready", listener);
+ resolve();
+ }
+ }
+ Management.on("ready", listener);
+ });
+
+ let [newAddon] = await Promise.all([installPromise, readyPromise]);
+ return newAddon;
+}
+
+function waitAboutAddonsViewLoaded(doc) {
+ return BrowserTestUtils.waitForEvent(doc, "view-loaded");
+}
+
+/**
+ * Trigger an action from the page options menu.
+ */
+function triggerPageOptionsAction(win, action) {
+ win.document.querySelector(`#page-options [action="${action}"]`).click();
+}
+
+function isDefaultIcon(icon) {
+ // These are basically the same icon, but code within webextensions
+ // generates references to the former and generic add-ons manager code
+ // generates referces to the latter.
+ return (
+ icon == "chrome://browser/content/extension.svg" ||
+ icon == "chrome://mozapps/skin/extensions/extensionGeneric.svg"
+ );
+}
+
+/**
+ * Check the contents of a permission popup notification
+ *
+ * @param {Window} panel
+ * The popup window.
+ * @param {string | RegExp | Function} checkIcon
+ * The icon expected to appear in the notification. If this is a
+ * string, it must match the icon url exactly. If it is a
+ * regular expression it is tested against the icon url, and if
+ * it is a function, it is called with the icon url and returns
+ * true if the url is correct.
+ * @param {Object[]} permissions
+ * The expected entries in the permissions list. Each element
+ * in this array is itself a 2-element array with the string key
+ * for the item (e.g., "webext-perms-description-foo") for permission foo
+ * and an optional formatting parameter.
+ * @param {boolean} sideloaded
+ * Whether the notification is for a sideloaded extenion.
+ * @param {boolean} [warning]
+ * Whether the experiments warning should be visible.
+ */
+async function checkNotification(
+ panel,
+ checkIcon,
+ permissions,
+ sideloaded,
+ warning = false
+) {
+ let icon = panel.getAttribute("icon");
+ let ul = document.getElementById("addon-webext-perm-list");
+ let singleDataEl = document.getElementById("addon-webext-perm-single-entry");
+ let experimentWarning = document.getElementById(
+ "addon-webext-experiment-warning"
+ );
+ let learnMoreLink = document.getElementById("addon-webext-perm-info");
+
+ if (checkIcon instanceof RegExp) {
+ ok(
+ checkIcon.test(icon),
+ `Notification icon is correct ${JSON.stringify(icon)} ~= ${checkIcon}`
+ );
+ } else if (typeof checkIcon == "function") {
+ ok(checkIcon(icon), "Notification icon is correct");
+ } else {
+ is(icon, checkIcon, "Notification icon is correct");
+ }
+
+ is(
+ learnMoreLink.hidden,
+ !permissions.length,
+ "Permissions learn more is hidden if there are no permissions"
+ );
+
+ if (!permissions.length) {
+ ok(ul.hidden, "Permissions list is hidden");
+ ok(singleDataEl.hidden, "Single permission data entry is hidden");
+ ok(
+ !(ul.childElementCount || singleDataEl.textContent),
+ "Permission list and single permission element have no entries"
+ );
+ } else if (permissions.length === 1) {
+ ok(ul.hidden, "Permissions list is hidden");
+ ok(!ul.childElementCount, "Permission list has no entries");
+ ok(singleDataEl.textContent, "Single permission data label has been set");
+ } else {
+ ok(singleDataEl.hidden, "Single permission data entry is hidden");
+ ok(
+ !singleDataEl.textContent,
+ "Single permission data label has not been set"
+ );
+ }
+
+ if (warning) {
+ is(experimentWarning.hidden, false, "Experiments warning is visible");
+ } else {
+ is(experimentWarning.hidden, true, "Experiments warning is hidden");
+ }
+}
+
+/**
+ * Test that install-time permission prompts work for a given
+ * installation method.
+ *
+ * @param {Function} installFn
+ * Callable that takes the name of an xpi file to install and
+ * starts to install it. Should return a Promise that resolves
+ * when the install is finished or rejects if the install is canceled.
+ *
+ * @returns {Promise}
+ */
+async function testInstallMethod(installFn) {
+ const PERMS_XPI = "browser_webext_permissions.xpi";
+ const NO_PERMS_XPI = "browser_webext_nopermissions.xpi";
+ const ID = "permissions@test.mozilla.org";
+
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["extensions.webapi.testing", true],
+ ["extensions.install.requireBuiltInCerts", false],
+ ],
+ });
+
+ let testURI = makeURI("https://example.com/");
+ PermissionTestUtils.add(testURI, "install", Services.perms.ALLOW_ACTION);
+ registerCleanupFunction(() => PermissionTestUtils.remove(testURI, "install"));
+
+ async function runOnce(filename, cancel) {
+ let tab = openContentTab("about:blank");
+ if (tab.browser.webProgress.isLoadingDocument) {
+ await BrowserTestUtils.browserLoaded(tab.browser);
+ }
+
+ let installPromise = new Promise(resolve => {
+ let listener = {
+ onDownloadCancelled() {
+ AddonManager.removeInstallListener(listener);
+ resolve(false);
+ },
+
+ onDownloadFailed() {
+ AddonManager.removeInstallListener(listener);
+ resolve(false);
+ },
+
+ onInstallCancelled() {
+ AddonManager.removeInstallListener(listener);
+ resolve(false);
+ },
+
+ onInstallEnded() {
+ AddonManager.removeInstallListener(listener);
+ resolve(true);
+ },
+
+ onInstallFailed() {
+ AddonManager.removeInstallListener(listener);
+ resolve(false);
+ },
+ };
+ AddonManager.addInstallListener(listener);
+ });
+
+ let installMethodPromise = installFn(filename);
+
+ let panel = await promisePopupNotificationShown("addon-webext-permissions");
+ if (filename == PERMS_XPI) {
+ // The icon should come from the extension, don't bother with the precise
+ // path, just make sure we've got a jar url pointing to the right path
+ // inside the jar.
+ await checkNotification(panel, /^jar:file:\/\/.*\/icon\.png$/, [
+ ["webext-perms-host-description-wildcard", "domain"],
+ ["webext-perms-host-description-one-site", "domain"],
+ ["webext-perms-description-nativeMessaging"],
+ // The below permissions are deliberately in this order as permissions
+ // are sorted alphabetically by the permission string to match AMO.
+ ["webext-perms-description-accountsRead"],
+ ["webext-perms-description-tabs"],
+ ]);
+ } else if (filename == NO_PERMS_XPI) {
+ await checkNotification(panel, isDefaultIcon, []);
+ }
+
+ if (cancel) {
+ panel.secondaryButton.click();
+ try {
+ await installMethodPromise;
+ } catch (err) {}
+ } else {
+ // Look for post-install notification
+ let postInstallPromise = promisePopupNotificationShown("addon-installed");
+ panel.button.click();
+
+ // Press OK on the post-install notification
+ panel = await postInstallPromise;
+ panel.button.click();
+
+ await installMethodPromise;
+ }
+
+ let result = await installPromise;
+ let addon = await AddonManager.getAddonByID(ID);
+ if (cancel) {
+ ok(!result, "Installation was cancelled");
+ is(addon, null, "Extension is not installed");
+ } else {
+ ok(result, "Installation completed");
+ isnot(addon, null, "Extension is installed");
+ await addon.uninstall();
+ }
+
+ let tabmail = document.getElementById("tabmail");
+ tabmail.closeOtherTabs(tabmail.tabInfo[0]);
+ }
+
+ // A few different tests for each installation method:
+ // 1. Start installation of an extension that requests no permissions,
+ // verify the notification contents, then cancel the install
+ await runOnce(NO_PERMS_XPI, true);
+
+ // 2. Same as #1 but with an extension that requests some permissions.
+ await runOnce(PERMS_XPI, true);
+
+ // 3. Repeat with the same extension from step 2 but this time,
+ // accept the permissions to install the extension. (Then uninstall
+ // the extension to clean up.)
+ await runOnce(PERMS_XPI, false);
+
+ await SpecialPowers.popPrefEnv();
+}
+
+// Helper function to test a specific scenario for interactive updates.
+// `checkFn` is a callable that triggers a check for updates.
+// `autoUpdate` specifies whether the test should be run with
+// updates applied automatically or not.
+async function interactiveUpdateTest(autoUpdate, checkFn) {
+ AddonTestUtils.initMochitest(this);
+
+ const ID = "update2@tests.mozilla.org";
+ const FAKE_INSTALL_SOURCE = "fake-install-source";
+
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ // We don't have pre-pinned certificates for the local mochitest server
+ ["extensions.install.requireBuiltInCerts", false],
+ ["extensions.update.requireBuiltInCerts", false],
+
+ ["extensions.update.autoUpdateDefault", autoUpdate],
+
+ // Point updates to the local mochitest server
+ ["extensions.update.url", `${BASE}/browser_webext_update.json`],
+ ],
+ });
+
+ AddonTestUtils.hookAMTelemetryEvents();
+
+ // Trigger an update check, manually applying the update if we're testing
+ // without auto-update.
+ async function triggerUpdate(win, addon) {
+ let manualUpdatePromise;
+ if (!autoUpdate) {
+ manualUpdatePromise = new Promise(resolve => {
+ let listener = {
+ onNewInstall() {
+ AddonManager.removeInstallListener(listener);
+ resolve();
+ },
+ };
+ AddonManager.addInstallListener(listener);
+ });
+ }
+
+ let promise = checkFn(win, addon);
+
+ if (manualUpdatePromise) {
+ await manualUpdatePromise;
+
+ let doc = win.document;
+ if (win.gViewController.currentViewId !== "addons://updates/available") {
+ let showUpdatesBtn = doc.querySelector("addon-updates-message").button;
+ await TestUtils.waitForCondition(() => {
+ return !showUpdatesBtn.hidden;
+ }, "Wait for show updates button");
+ let viewChanged = waitAboutAddonsViewLoaded(doc);
+ showUpdatesBtn.click();
+ await viewChanged;
+ }
+ let card = await TestUtils.waitForCondition(() => {
+ return doc.querySelector(`addon-card[addon-id="${ID}"]`);
+ }, `Wait addon card for "${ID}"`);
+ let updateBtn = card.querySelector('panel-item[action="install-update"]');
+ ok(updateBtn, `Found update button for "${ID}"`);
+ updateBtn.click();
+ }
+
+ return { promise };
+ }
+
+ // Install version 1.0 of the test extension
+ let addon = await promiseInstallAddon(`${BASE}/browser_webext_update1.xpi`, {
+ source: FAKE_INSTALL_SOURCE,
+ });
+ ok(addon, "Addon was installed");
+ is(addon.version, "1.0", "Version 1 of the addon is installed");
+
+ let win = await openAddonsMgr("addons://list/extension");
+
+ await waitAboutAddonsViewLoaded(win.document);
+
+ // Trigger an update check
+ let popupPromise = promisePopupNotificationShown("addon-webext-permissions");
+ let { promise: checkPromise } = await triggerUpdate(win, addon);
+ let panel = await popupPromise;
+
+ // Click the cancel button, wait to see the cancel event
+ let cancelPromise = promiseInstallEvent(addon, "onInstallCancelled");
+ panel.secondaryButton.click();
+ await cancelPromise;
+
+ addon = await AddonManager.getAddonByID(ID);
+ is(addon.version, "1.0", "Should still be running the old version");
+
+ // Make sure the update check is completely finished.
+ await checkPromise;
+
+ // Trigger a new update check
+ popupPromise = promisePopupNotificationShown("addon-webext-permissions");
+ checkPromise = (await triggerUpdate(win, addon)).promise;
+
+ // This time, accept the upgrade
+ let updatePromise = waitForUpdate(addon);
+ panel = await popupPromise;
+ panel.button.click();
+
+ addon = await updatePromise;
+ is(addon.version, "2.0", "Should have upgraded");
+
+ await checkPromise;
+
+ let tabmail = document.getElementById("tabmail");
+ tabmail.closeTab(tabmail.currentTabInfo);
+ await addon.uninstall();
+ await SpecialPowers.popPrefEnv();
+
+ const collectedUpdateEvents = AddonTestUtils.getAMTelemetryEvents().filter(
+ evt => {
+ return evt.method === "update";
+ }
+ );
+
+ Assert.deepEqual(
+ collectedUpdateEvents.map(evt => evt.extra.step),
+ [
+ // First update is cancelled on the permission prompt.
+ "started",
+ "download_started",
+ "download_completed",
+ "permissions_prompt",
+ "cancelled",
+ // Second update is expected to be completed.
+ "started",
+ "download_started",
+ "download_completed",
+ "permissions_prompt",
+ "completed",
+ ],
+ "Got the expected sequence on update telemetry events"
+ );
+
+ ok(
+ collectedUpdateEvents.every(evt => evt.extra.addon_id === ID),
+ "Every update telemetry event should have the expected addon_id extra var"
+ );
+
+ ok(
+ collectedUpdateEvents.every(
+ evt => evt.extra.source === FAKE_INSTALL_SOURCE
+ ),
+ "Every update telemetry event should have the expected source extra var"
+ );
+
+ ok(
+ collectedUpdateEvents.every(evt => evt.extra.updated_from === "user"),
+ "Every update telemetry event should have the update_from extra var 'user'"
+ );
+
+ let hasPermissionsExtras = collectedUpdateEvents
+ .filter(evt => {
+ return evt.extra.step === "permissions_prompt";
+ })
+ .every(evt => {
+ return Number.isInteger(parseInt(evt.extra.num_strings, 10));
+ });
+
+ ok(
+ hasPermissionsExtras,
+ "Every 'permissions_prompt' update telemetry event should have the permissions extra vars"
+ );
+
+ let hasDownloadTimeExtras = collectedUpdateEvents
+ .filter(evt => {
+ return evt.extra.step === "download_completed";
+ })
+ .every(evt => {
+ const download_time = parseInt(evt.extra.download_time, 10);
+ return !isNaN(download_time) && download_time > 0;
+ });
+
+ ok(
+ hasDownloadTimeExtras,
+ "Every 'download_completed' update telemetry event should have a download_time extra vars"
+ );
+}
+
+// The tests in this directory install a bunch of extensions but they
+// need to uninstall them before exiting, as a stray leftover extension
+// after one test can foul up subsequent tests.
+// So, add a task to run before any tests that grabs a list of all the
+// add-ons that are pre-installed in the test environment and then checks
+// the list of installed add-ons at the end of the test to make sure no
+// new add-ons have been added.
+// Individual tests can store a cleanup function in the testCleanup global
+// to ensure it gets called before the final check is performed.
+let testCleanup;
+add_task(async function () {
+ let addons = await AddonManager.getAllAddons();
+ let existingAddons = new Set(addons.map(a => a.id));
+
+ registerCleanupFunction(async function () {
+ if (testCleanup) {
+ await testCleanup();
+ testCleanup = null;
+ }
+
+ for (let addon of await AddonManager.getAllAddons()) {
+ // Builtin search extensions may have been installed by SearchService
+ // during the test run, ignore those.
+ if (
+ !existingAddons.has(addon.id) &&
+ !(addon.isBuiltin && addon.id.endsWith("@search.mozilla.org"))
+ ) {
+ ok(
+ false,
+ `Addon ${addon.id} was left installed at the end of the test`
+ );
+ await addon.uninstall();
+ }
+ }
+ });
+});
+
+registerCleanupFunction(() => {
+ // The appmenu should be closed by the end of the test.
+ ok(PanelUI.panel.state == "closed", "Main menu is closed.");
+
+ // Any opened tabs should be closed by the end of the test.
+ let tabmail = document.getElementById("tabmail");
+ is(tabmail.tabInfo.length, 1, "All tabs are closed.");
+ tabmail.closeOtherTabs(0);
+});
+
+let collectedTelemetry = [];
+function hookExtensionsTelemetry() {
+ let originalHistogram = ExtensionsUI.histogram;
+ ExtensionsUI.histogram = {
+ add(value) {
+ collectedTelemetry.push(value);
+ },
+ };
+ registerCleanupFunction(() => {
+ is(
+ collectedTelemetry.length,
+ 0,
+ "No unexamined telemetry after test is finished"
+ );
+ ExtensionsUI.histogram = originalHistogram;
+ });
+}
+
+function expectTelemetry(values) {
+ Assert.deepEqual(values, collectedTelemetry);
+ collectedTelemetry = [];
+}