diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 17:32:43 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 17:32:43 +0000 |
commit | 6bf0a5cb5034a7e684dcc3500e841785237ce2dd (patch) | |
tree | a68f146d7fa01f0134297619fbe7e33db084e0aa /comm/mail/base/test/webextensions | |
parent | Initial commit. (diff) | |
download | thunderbird-6bf0a5cb5034a7e684dcc3500e841785237ce2dd.tar.xz thunderbird-6bf0a5cb5034a7e684dcc3500e841785237ce2dd.zip |
Adding upstream version 1:115.7.0.upstream/1%115.7.0upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'comm/mail/base/test/webextensions')
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 Binary files differnew file mode 100644 index 0000000000..982d5d6b25 --- /dev/null +++ b/comm/mail/base/test/webextensions/browser_webext_experiment.xpi diff --git a/comm/mail/base/test/webextensions/browser_webext_experiment_permissions.xpi b/comm/mail/base/test/webextensions/browser_webext_experiment_permissions.xpi Binary files differnew file mode 100644 index 0000000000..d659b876fe --- /dev/null +++ b/comm/mail/base/test/webextensions/browser_webext_experiment_permissions.xpi diff --git a/comm/mail/base/test/webextensions/browser_webext_experiment_update1.xpi b/comm/mail/base/test/webextensions/browser_webext_experiment_update1.xpi Binary files differnew file mode 100644 index 0000000000..bb4a5fb008 --- /dev/null +++ b/comm/mail/base/test/webextensions/browser_webext_experiment_update1.xpi diff --git a/comm/mail/base/test/webextensions/browser_webext_experiment_update2.xpi b/comm/mail/base/test/webextensions/browser_webext_experiment_update2.xpi Binary files differnew file mode 100644 index 0000000000..5f57efe3c2 --- /dev/null +++ b/comm/mail/base/test/webextensions/browser_webext_experiment_update2.xpi diff --git a/comm/mail/base/test/webextensions/browser_webext_nopermissions.xpi b/comm/mail/base/test/webextensions/browser_webext_nopermissions.xpi Binary files differnew file mode 100644 index 0000000000..ab97d96a11 --- /dev/null +++ b/comm/mail/base/test/webextensions/browser_webext_nopermissions.xpi diff --git a/comm/mail/base/test/webextensions/browser_webext_permissions.xpi b/comm/mail/base/test/webextensions/browser_webext_permissions.xpi Binary files differnew file mode 100644 index 0000000000..307c25a839 --- /dev/null +++ b/comm/mail/base/test/webextensions/browser_webext_permissions.xpi diff --git a/comm/mail/base/test/webextensions/browser_webext_unsigned.xpi b/comm/mail/base/test/webextensions/browser_webext_unsigned.xpi Binary files differnew file mode 100644 index 0000000000..2ebc23b4fe --- /dev/null +++ b/comm/mail/base/test/webextensions/browser_webext_unsigned.xpi 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 Binary files differnew file mode 100644 index 0000000000..79be90636c --- /dev/null +++ b/comm/mail/base/test/webextensions/browser_webext_update1.xpi diff --git a/comm/mail/base/test/webextensions/browser_webext_update2.xpi b/comm/mail/base/test/webextensions/browser_webext_update2.xpi Binary files differnew file mode 100644 index 0000000000..d1a12cadca --- /dev/null +++ b/comm/mail/base/test/webextensions/browser_webext_update2.xpi diff --git a/comm/mail/base/test/webextensions/browser_webext_update_icon1.xpi b/comm/mail/base/test/webextensions/browser_webext_update_icon1.xpi Binary files differnew file mode 100644 index 0000000000..d3dcb3235d --- /dev/null +++ b/comm/mail/base/test/webextensions/browser_webext_update_icon1.xpi diff --git a/comm/mail/base/test/webextensions/browser_webext_update_icon2.xpi b/comm/mail/base/test/webextensions/browser_webext_update_icon2.xpi Binary files differnew file mode 100644 index 0000000000..5cd7a8cec4 --- /dev/null +++ b/comm/mail/base/test/webextensions/browser_webext_update_icon2.xpi diff --git a/comm/mail/base/test/webextensions/browser_webext_update_origins1.xpi b/comm/mail/base/test/webextensions/browser_webext_update_origins1.xpi Binary files differnew file mode 100644 index 0000000000..2909f8e8fd --- /dev/null +++ b/comm/mail/base/test/webextensions/browser_webext_update_origins1.xpi diff --git a/comm/mail/base/test/webextensions/browser_webext_update_origins2.xpi b/comm/mail/base/test/webextensions/browser_webext_update_origins2.xpi Binary files differnew file mode 100644 index 0000000000..b1051affb1 --- /dev/null +++ b/comm/mail/base/test/webextensions/browser_webext_update_origins2.xpi diff --git a/comm/mail/base/test/webextensions/browser_webext_update_perms1.xpi b/comm/mail/base/test/webextensions/browser_webext_update_perms1.xpi Binary files differnew file mode 100644 index 0000000000..f4942f9082 --- /dev/null +++ b/comm/mail/base/test/webextensions/browser_webext_update_perms1.xpi diff --git a/comm/mail/base/test/webextensions/browser_webext_update_perms2.xpi b/comm/mail/base/test/webextensions/browser_webext_update_perms2.xpi Binary files differnew file mode 100644 index 0000000000..2c023edc9d --- /dev/null +++ b/comm/mail/base/test/webextensions/browser_webext_update_perms2.xpi 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 = []; +} |