diff options
Diffstat (limited to '')
30 files changed, 2173 insertions, 0 deletions
diff --git a/browser/base/content/test/webextensions/.eslintrc.js b/browser/base/content/test/webextensions/.eslintrc.js new file mode 100644 index 0000000000..e57058ecb1 --- /dev/null +++ b/browser/base/content/test/webextensions/.eslintrc.js @@ -0,0 +1,7 @@ +"use strict"; + +module.exports = { + env: { + webextensions: true, + }, +}; diff --git a/browser/base/content/test/webextensions/browser.ini b/browser/base/content/test/webextensions/browser.ini new file mode 100644 index 0000000000..d90da6c891 --- /dev/null +++ b/browser/base/content/test/webextensions/browser.ini @@ -0,0 +1,33 @@ +[DEFAULT] +support-files = + head.js + file_install_extensions.html + browser_legacy_webext.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_aboutaddons_blanktab.js] +[browser_extension_sideloading.js] +[browser_extension_update_background.js] +[browser_extension_update_background_noprompt.js] +[browser_permissions_dismiss.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] +skip-if = require_signing +[browser_update_checkForUpdates.js] +skip-if = os == "win" && os_version == "6.1" # Skip on Azure - frequent failure +[browser_update_interactive_noprompt.js] diff --git a/browser/base/content/test/webextensions/browser_aboutaddons_blanktab.js b/browser/base/content/test/webextensions/browser_aboutaddons_blanktab.js new file mode 100644 index 0000000000..228fe71815 --- /dev/null +++ b/browser/base/content/test/webextensions/browser_aboutaddons_blanktab.js @@ -0,0 +1,26 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +add_task(async function testBlankTabReusedAboutAddons() { + await BrowserTestUtils.withNewTab({ gBrowser }, async browser => { + let tabCount = gBrowser.tabs.length; + is(browser, gBrowser.selectedBrowser, "New tab is selected"); + + // Opening about:addons shouldn't change the selected tab. + BrowserOpenAddonsMgr(); + + is(browser, gBrowser.selectedBrowser, "No new tab was opened"); + + // Wait for about:addons to load. + await BrowserTestUtils.browserLoaded(browser); + + is( + browser.currentURI.spec, + "about:addons", + "about:addons should load into blank tab." + ); + + is(gBrowser.tabs.length, tabCount, "Still the same number of tabs"); + }); +}); diff --git a/browser/base/content/test/webextensions/browser_extension_sideloading.js b/browser/base/content/test/webextensions/browser_extension_sideloading.js new file mode 100644 index 0000000000..958393336f --- /dev/null +++ b/browser/base/content/test/webextensions/browser_extension_sideloading.js @@ -0,0 +1,410 @@ +/* eslint-disable mozilla/no-arbitrary-setTimeout */ +const { AddonManagerPrivate } = ChromeUtils.import( + "resource://gre/modules/AddonManager.jsm" +); + +const { AddonTestUtils } = ChromeUtils.import( + "resource://testing-common/AddonTestUtils.jsm" +); + +AddonTestUtils.initMochitest(this); + +hookExtensionsTelemetry(); +AddonTestUtils.hookAMTelemetryEvents(); + +const kSideloaded = true; + +async function createWebExtension(details) { + let options = { + manifest: { + browser_specific_settings: { 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 TestUtils.waitForCondition( + () => + managerWindow.document.querySelector(`addon-card[addon-id="${addonId}"]`), + `Found entry for sideload extension addon "${addonId}" in HTML about:addons` + ); +} + +function assertSideloadedAddonElementState(addonElement, checked) { + const enableBtn = addonElement.querySelector('[action="toggle-disabled"]'); + is( + enableBtn.checked, + checked, + `The enable button is ${!checked ? " not " : ""} checked` + ); + is(enableBtn.localName, "input", "The enable button is an input"); + is(enableBtn.type, "checkbox", "It's a checkbox"); +} + +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], + ], + }); + + const ID1 = "addon1@tests.mozilla.org"; + await createWebExtension({ + id: ID1, + name: "Test 1", + userDisabled: true, + permissions: ["history", "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"); + }; + + // Navigate away from the starting page to force about:addons to load + // in a new tab during the tests below. + BrowserTestUtils.loadURI(gBrowser.selectedBrowser, "about:robots"); + await BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser); + + registerCleanupFunction(async function() { + // Return to about:blank when we're done + BrowserTestUtils.loadURI(gBrowser.selectedBrowser, "about:blank"); + await BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser); + }); + + 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("PanelUI-menu-button"); + 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."); + + // When we get the permissions prompt, we should be at the extensions + // list in about:addons + let panel = await popupPromise; + is( + gBrowser.currentURI.spec, + "about:addons", + "Foreground tab is at about:addons" + ); + + const VIEW = "addons://list/extension"; + let win = gBrowser.selectedBrowser.contentWindow; + + await TestUtils.waitForCondition( + () => !win.gViewController.isLoading, + "about:addons view is fully loaded" + ); + is( + win.gViewController.currentViewId, + VIEW, + "about:addons is at extensions list" + ); + + // Check the contents of the notification, then choose "Cancel" + checkNotification( + panel, + /\/foo-icon\.png$/, + [ + ["webextPerms.hostDescription.allUrls"], + ["webextPerms.description.history"], + ], + 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"); + + BrowserTestUtils.removeTab(gBrowser.selectedTab); + + // 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(); + + win = await BrowserOpenAddonsMgr(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; + checkNotification( + panel, + DEFAULT_ICON_URL, + [["webextPerms.hostDescription.allUrls"]], + kSideloaded + ); + + // Test incognito checkbox in post install notification + function setupPostInstallNotificationTest() { + let promiseNotificationShown = promiseAppMenuNotificationShown( + "addon-installed" + ); + return async function(addon) { + info(`Expect post install notification for "${addon.name}"`); + let postInstallPanel = await promiseNotificationShown; + let incognitoCheckbox = postInstallPanel.querySelector( + "#addon-incognito-checkbox" + ); + is( + window.AppMenuNotifications.activeNotification.options.name, + addon.name, + "Got the expected addon name in the active notification" + ); + ok( + incognitoCheckbox, + "Got an incognito checkbox in the post install notification panel" + ); + ok(!incognitoCheckbox.hidden, "Incognito checkbox should not be hidden"); + // Dismiss post install notification. + postInstallPanel.button.click(); + }; + } + + // Setup async test for post install notification on addon 2 + let testPostInstallIncognitoCheckbox = setupPostInstallNotificationTest(); + + // 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. + await testPostInstallIncognitoCheckbox(addon2); + + // 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"); + + // Close the hamburger menu and go to the detail page for this addon + await gCUITestUtils.hideMainMenu(); + + win = await BrowserOpenAddonsMgr( + `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(gBrowser, addon3); + + panel = await popupPromise; + checkNotification( + panel, + DEFAULT_ICON_URL, + [["webextPerms.hostDescription.allUrls"]], + kSideloaded + ); + + // Setup async test for post install notification on addon 3 + testPostInstallIncognitoCheckbox = setupPostInstallNotificationTest(); + + // 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. + await testPostInstallIncognitoCheckbox(addon3); + + // We should have recorded 1 cancelled followed by 2 accepted sideloads. + expectTelemetry(["sideloadRejected", "sideloadAccepted", "sideloadAccepted"]); + + 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(); + } + + BrowserTestUtils.removeTab(gBrowser.selectedTab); + + // 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/browser/base/content/test/webextensions/browser_extension_update_background.js b/browser/base/content/test/webextensions/browser_extension_update_background.js new file mode 100644 index 0000000000..3647c07901 --- /dev/null +++ b/browser/base/content/test/webextensions/browser_extension_update_background.js @@ -0,0 +1,287 @@ +const { AddonManagerPrivate } = ChromeUtils.import( + "resource://gre/modules/AddonManager.jsm" +); + +const { AddonTestUtils } = ChromeUtils.import( + "resource://testing-common/AddonTestUtils.jsm" +); + +AddonTestUtils.initMochitest(this); + +const ID = "update2@tests.mozilla.org"; +const ID_ICON = "update_icon2@tests.mozilla.org"; +const ID_PERMS = "update_perms@tests.mozilla.org"; +const ID_LEGACY = "legacy_update@tests.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("PanelUI-menu-button"); + 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], + ], + }); + + // Navigate away from the initial page so that about:addons always + // opens in a new tab during tests + BrowserTestUtils.loadURI(gBrowser.selectedBrowser, "about:robots"); + await BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser); + + registerCleanupFunction(async function() { + // Return to about:blank when we're done + BrowserTestUtils.loadURI(gBrowser.selectedBrowser, "about:blank"); + await BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser); + }); +}); + +hookExtensionsTelemetry(); +AddonTestUtils.hookAMTelemetryEvents(); + +// 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"); + + AddonManagerPrivate.backgroundUpdateCheck(); + await updatePromise; + + 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 tabPromise = BrowserTestUtils.waitForNewTab(gBrowser, "about:addons"); + 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."); + + // about:addons should load and go to the list of extensions + let tab = await tabPromise; + is( + tab.linkedBrowser.currentURI.spec, + "about:addons", + "Browser is at about:addons" + ); + + const VIEW = "addons://list/extension"; + await promiseViewLoaded(tab, VIEW); + let win = tab.linkedBrowser.contentWindow; + ok(!win.gViewController.isLoading, "about:addons view is fully loaded"); + is( + win.gViewController.currentViewId, + VIEW, + "about:addons is at extensions list" + ); + + // 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"); + + BrowserTestUtils.removeTab(tab); + + // 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"); + await AddonManagerPrivate.backgroundUpdateCheck(); + await updatePromise; + + 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 + tabPromise = BrowserTestUtils.waitForNewTab(gBrowser, "about:addons", true); + popupPromise = promisePopupNotificationShown("addon-webext-permissions"); + + addons.children[0].click(); + + // Wait for about:addons to load + tab = await tabPromise; + is(tab.linkedBrowser.currentURI.spec, "about:addons"); + + await promiseViewLoaded(tab, VIEW); + win = tab.linkedBrowser.contentWindow; + ok(!win.gViewController.isLoading, "about:addons view is fully loaded"); + is( + win.gViewController.currentViewId, + VIEW, + "about:addons is at extensions list" + ); + + // 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"); + + BrowserTestUtils.removeTab(tab); + + is(getBadgeStatus(), "", "Addon alert badge should be gone"); + + // Should have recorded 1 canceled followed by 1 accepted update. + expectTelemetry(["updateRejected", "updateAccepted"]); + + 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 + ) +); diff --git a/browser/base/content/test/webextensions/browser_extension_update_background_noprompt.js b/browser/base/content/test/webextensions/browser_extension_update_background_noprompt.js new file mode 100644 index 0000000000..bdc1f09d51 --- /dev/null +++ b/browser/base/content/test/webextensions/browser_extension_update_background_noprompt.js @@ -0,0 +1,123 @@ +const { AddonManagerPrivate } = ChromeUtils.import( + "resource://gre/modules/AddonManager.jsm" +); + +const { AddonTestUtils } = ChromeUtils.import( + "resource://testing-common/AddonTestUtils.jsm" +); + +AddonTestUtils.initMochitest(this); + +hookExtensionsTelemetry(); +AddonTestUtils.hookAMTelemetryEvents(); + +const ID_PERMS = "update_perms@tests.mozilla.org"; +const ID_ORIGINS = "update_origins@tests.mozilla.org"; + +function getBadgeStatus() { + let menuButton = document.getElementById("PanelUI-menu-button"); + 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], + ], + }); + + // Navigate away from the initial page so that about:addons always + // opens in a new tab during tests + BrowserTestUtils.loadURI(gBrowser.selectedBrowser, "about:robots"); + await BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser); + + registerCleanupFunction(async function() { + // Return to about:blank when we're done + BrowserTestUtils.loadURI(gBrowser.selectedBrowser, "about:blank"); + await BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser); + }); +}); + +// 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 promt +add_task(() => + testNoPrompt(`${BASE}/browser_webext_update_origins1.xpi`, ID_ORIGINS) +); diff --git a/browser/base/content/test/webextensions/browser_legacy_webext.xpi b/browser/base/content/test/webextensions/browser_legacy_webext.xpi Binary files differnew file mode 100644 index 0000000000..a3bdf6f832 --- /dev/null +++ b/browser/base/content/test/webextensions/browser_legacy_webext.xpi diff --git a/browser/base/content/test/webextensions/browser_permissions_dismiss.js b/browser/base/content/test/webextensions/browser_permissions_dismiss.js new file mode 100644 index 0000000000..ac1e9c4bd4 --- /dev/null +++ b/browser/base/content/test/webextensions/browser_permissions_dismiss.js @@ -0,0 +1,112 @@ +"use strict"; + +const INSTALL_PAGE = `${BASE}/file_install_extensions.html`; +const INSTALL_XPI = `${BASE}/browser_webext_permissions.xpi`; + +add_setup(async function() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["extensions.webapi.testing", true], + ["extensions.install.requireBuiltInCerts", false], + ], + }); + + registerCleanupFunction(async () => { + await SpecialPowers.popPrefEnv(); + }); +}); + +add_task(async function test_tab_switch_dismiss() { + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, INSTALL_PAGE); + + let installCanceled = new Promise(resolve => { + let listener = { + onInstallCancelled() { + AddonManager.removeInstallListener(listener); + resolve(); + }, + }; + AddonManager.addInstallListener(listener); + }); + + SpecialPowers.spawn(gBrowser.selectedBrowser, [INSTALL_XPI], function(url) { + content.wrappedJSObject.installMozAM(url); + }); + + await promisePopupNotificationShown("addon-webext-permissions"); + let permsUL = document.getElementById("addon-webext-perm-list"); + is(permsUL.childElementCount, 5, `Permissions list has 5 entries`); + + let permsLearnMore = document.getElementById("addon-webext-perm-info"); + ok( + BrowserTestUtils.is_visible(permsLearnMore), + "Learn more link is shown on Permission popup" + ); + is( + permsLearnMore.href, + Services.urlFormatter.formatURLPref("app.support.baseURL") + + "extension-permissions", + "Learn more link has desired URL" + ); + + // Switching tabs dismisses the notification and cancels the install. + let switchTo = await BrowserTestUtils.openNewForegroundTab(gBrowser); + BrowserTestUtils.removeTab(switchTo); + await installCanceled; + + let addon = await AddonManager.getAddonByID("permissions@test.mozilla.org"); + is(addon, null, "Extension is not installed"); + + BrowserTestUtils.removeTab(tab); +}); + +add_task(async function test_add_tab_by_user_and_switch() { + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, INSTALL_PAGE); + + let listener = { + onInstallCancelled() { + this.canceledPromise = Promise.resolve(); + }, + }; + AddonManager.addInstallListener(listener); + + SpecialPowers.spawn(gBrowser.selectedBrowser, [INSTALL_XPI], function(url) { + content.wrappedJSObject.installMozAM(url); + }); + + // Show addon permission notification. + await promisePopupNotificationShown("addon-webext-permissions"); + is( + document.getElementById("addon-webext-perm-list").childElementCount, + 5, + "Permissions list has 5 entries" + ); + + // Open about:newtab page in a new tab. + let newTab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + "about:newtab", + false + ); + + // Switch to tab that is opening addon permission notification. + gBrowser.selectedTab = tab; + is( + document.getElementById("addon-webext-perm-list").childElementCount, + 5, + "Permission notification is shown again" + ); + ok(!listener.canceledPromise, "Extension installation is not canceled"); + + // Cancel installation. + document.querySelector(".popup-notification-secondary-button").click(); + await listener.canceledPromise; + info("Extension installation is canceled"); + + let addon = await AddonManager.getAddonByID("permissions@test.mozilla.org"); + is(addon, null, "Extension is not installed"); + + AddonManager.removeInstallListener(listener); + BrowserTestUtils.removeTab(tab); + BrowserTestUtils.removeTab(newTab); +}); diff --git a/browser/base/content/test/webextensions/browser_permissions_installTrigger.js b/browser/base/content/test/webextensions/browser_permissions_installTrigger.js new file mode 100644 index 0000000000..774cf9bb10 --- /dev/null +++ b/browser/base/content/test/webextensions/browser_permissions_installTrigger.js @@ -0,0 +1,26 @@ +"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], + ], + }); + BrowserTestUtils.loadURI(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, "installAmo")); diff --git a/browser/base/content/test/webextensions/browser_permissions_local_file.js b/browser/base/content/test/webextensions/browser_permissions_local_file.js new file mode 100644 index 0000000000..0d06ee9730 --- /dev/null +++ b/browser/base/content/test/webextensions/browser_permissions_local_file.js @@ -0,0 +1,85 @@ +"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 BrowserOpenAddonsMgr("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() { + // Clear any telemetry data that might be from a separate test. + Services.telemetry.clearEvents(); + + // 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, "installLocal"); + + // Check we got an installId. + ok( + firstInstallId != null && !isNaN(firstInstallId), + "There was an installId found" + ); + + // Check the telemetry. + let snapshot = Services.telemetry.snapshotEvents( + Ci.nsITelemetry.DATASET_PRERELEASE_CHANNELS, + true + ); + + // Make sure we got some data. + ok( + snapshot.parent && !!snapshot.parent.length, + "Got parent telemetry events in the snapshot" + ); + + // Only look at the related events after stripping the timestamp and category. + let relatedEvents = snapshot.parent + .filter( + ([timestamp, category, method, object]) => + category == "addonsManager" && + method == "action" && + object == "aboutAddons" + ) + .map(relatedEvent => relatedEvent.slice(4, 6)); + + // testInstallMethod installs the extension three times. + Assert.deepEqual( + relatedEvents, + [ + [firstInstallId.toString(), { action: "installFromFile", view: "list" }], + [ + (++firstInstallId).toString(), + { action: "installFromFile", view: "list" }, + ], + [ + (++firstInstallId).toString(), + { action: "installFromFile", view: "list" }, + ], + ], + "The telemetry is recorded correctly" + ); +}); diff --git a/browser/base/content/test/webextensions/browser_permissions_mozAddonManager.js b/browser/base/content/test/webextensions/browser_permissions_mozAddonManager.js new file mode 100644 index 0000000000..d66996c5b8 --- /dev/null +++ b/browser/base/content/test/webextensions/browser_permissions_mozAddonManager.js @@ -0,0 +1,18 @@ +"use strict"; + +const INSTALL_PAGE = `${BASE}/file_install_extensions.html`; + +async function installMozAM(filename) { + BrowserTestUtils.loadURI(gBrowser.selectedBrowser, INSTALL_PAGE); + await BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser); + + await SpecialPowers.spawn( + gBrowser.selectedBrowser, + [`${BASE}/${filename}`], + async function(url) { + await content.wrappedJSObject.installMozAM(url); + } + ); +} + +add_task(() => testInstallMethod(installMozAM, "installAmo")); diff --git a/browser/base/content/test/webextensions/browser_permissions_optional.js b/browser/base/content/test/webextensions/browser_permissions_optional.js new file mode 100644 index 0000000000..7c8a654cbc --- /dev/null +++ b/browser/base/content/test/webextensions/browser_permissions_optional.js @@ -0,0 +1,52 @@ +"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"); + + await BrowserTestUtils.withNewTab({ gBrowser, url }, async browser => { + await extension.awaitMessage("pageReady"); + + await BrowserTestUtils.synthesizeKey("a", {}, browser); + + await extension.awaitMessage("permsGranted"); + }); + + await extension.unload(); +}); diff --git a/browser/base/content/test/webextensions/browser_permissions_pointerevent.js b/browser/base/content/test/webextensions/browser_permissions_pointerevent.js new file mode 100644 index 0000000000..188aa8e3bf --- /dev/null +++ b/browser/base/content/test/webextensions/browser_permissions_pointerevent.js @@ -0,0 +1,53 @@ +/* -*- 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(); + let url = await extension.awaitMessage("ready"); + await BrowserTestUtils.withNewTab({ gBrowser, url }, async browser => { + await extension.awaitMessage("pageReady"); + await BrowserTestUtils.synthesizeMouseAtCenter( + "html", + { type: "mousedown", button: 0 }, + browser + ); + await BrowserTestUtils.synthesizeMouseAtCenter( + "html", + { type: "mouseup", button: 0 }, + browser + ); + await extension.awaitMessage("done"); + }); + await extension.unload(); +}); diff --git a/browser/base/content/test/webextensions/browser_permissions_unsigned.js b/browser/base/content/test/webextensions/browser_permissions_unsigned.js new file mode 100644 index 0000000000..68f884ebab --- /dev/null +++ b/browser/base/content/test/webextensions/browser_permissions_unsigned.js @@ -0,0 +1,59 @@ +"use strict"; + +const ID = "permissions@test.mozilla.org"; +const WARNING_ICON = "chrome://global/skin/icons/warning.svg"; + +add_task(async function test_unsigned() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["extensions.webapi.testing", true], + ["extensions.install.requireBuiltInCerts", false], + ["extensions.InstallTrigger.enabled", true], + ["extensions.InstallTriggerImpl.enabled", true], + // 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 = await BrowserTestUtils.openNewForegroundTab(gBrowser); + + BrowserTestUtils.loadURI( + gBrowser.selectedBrowser, + `${BASE}/file_install_extensions.html` + ); + await BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser); + + SpecialPowers.spawn( + gBrowser.selectedBrowser, + [`${BASE}/browser_webext_unsigned.xpi`], + async function(url) { + content.wrappedJSObject.installTrigger(url); + } + ); + + let panel = await promisePopupNotificationShown("addon-webext-permissions"); + + is(panel.getAttribute("icon"), WARNING_ICON); + let description = panel.querySelector(".popup-notification-description") + .textContent; + checkPermissionString( + description, + "webextPerms.headerUnsignedWithPerms", + undefined, + `Install notification includes unsigned warning` + ); + + // 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"); + + BrowserTestUtils.removeTab(tab); +}); diff --git a/browser/base/content/test/webextensions/browser_update_checkForUpdates.js b/browser/base/content/test/webextensions/browser_update_checkForUpdates.js new file mode 100644 index 0000000000..b902527cae --- /dev/null +++ b/browser/base/content/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/browser/base/content/test/webextensions/browser_update_interactive_noprompt.js b/browser/base/content/test/webextensions/browser_update_interactive_noprompt.js new file mode 100644 index 0000000000..7de49d3566 --- /dev/null +++ b/browser/base/content/test/webextensions/browser_update_interactive_noprompt.js @@ -0,0 +1,77 @@ +// 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" +) { + // Navigate away to ensure that BrowserOpenAddonMgr() opens a new tab + BrowserTestUtils.loadURI(gBrowser.selectedBrowser, "about:mozilla"); + await BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser); + + // 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 BrowserOpenAddonsMgr("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); + + BrowserTestUtils.removeTab(gBrowser.selectedTab); + 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 promt +add_task(() => + testUpdateNoPrompt( + "browser_webext_update_origins1.xpi", + "update_origins@tests.mozilla.org" + ) +); diff --git a/browser/base/content/test/webextensions/browser_webext_nopermissions.xpi b/browser/base/content/test/webextensions/browser_webext_nopermissions.xpi Binary files differnew file mode 100644 index 0000000000..ab97d96a11 --- /dev/null +++ b/browser/base/content/test/webextensions/browser_webext_nopermissions.xpi diff --git a/browser/base/content/test/webextensions/browser_webext_permissions.xpi b/browser/base/content/test/webextensions/browser_webext_permissions.xpi Binary files differnew file mode 100644 index 0000000000..a8c8c38ef8 --- /dev/null +++ b/browser/base/content/test/webextensions/browser_webext_permissions.xpi diff --git a/browser/base/content/test/webextensions/browser_webext_unsigned.xpi b/browser/base/content/test/webextensions/browser_webext_unsigned.xpi Binary files differnew file mode 100644 index 0000000000..55779530ce --- /dev/null +++ b/browser/base/content/test/webextensions/browser_webext_unsigned.xpi diff --git a/browser/base/content/test/webextensions/browser_webext_update.json b/browser/base/content/test/webextensions/browser_webext_update.json new file mode 100644 index 0000000000..ae18044e9c --- /dev/null +++ b/browser/base/content/test/webextensions/browser_webext_update.json @@ -0,0 +1,70 @@ +{ + "addons": { + "update2@tests.mozilla.org": { + "updates": [ + { + "version": "2.0", + "update_link": "https://example.com/browser/browser/base/content/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/browser/base/content/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/browser/base/content/test/webextensions/browser_webext_update_perms2.xpi", + "applications": { + "gecko": { + "strict_min_version": "1" + } + } + } + ] + }, + "legacy_update@tests.mozilla.org": { + "updates": [ + { + "version": "2.0", + "update_link": "https://example.com/browser/browser/base/content/test/webextensions/browser_legacy_webext.xpi", + "applications": { + "gecko": { + "strict_min_version": "1", + "advisory_max_version": "*" + } + } + } + ] + }, + "update_origins@tests.mozilla.org": { + "updates": [ + { + "version": "2.0", + "update_link": "https://example.com/browser/browser/base/content/test/webextensions/browser_webext_update_origins2.xpi", + "applications": { + "gecko": { + "strict_min_version": "1" + } + } + } + ] + } + } +} diff --git a/browser/base/content/test/webextensions/browser_webext_update1.xpi b/browser/base/content/test/webextensions/browser_webext_update1.xpi Binary files differnew file mode 100644 index 0000000000..086b3839b9 --- /dev/null +++ b/browser/base/content/test/webextensions/browser_webext_update1.xpi diff --git a/browser/base/content/test/webextensions/browser_webext_update2.xpi b/browser/base/content/test/webextensions/browser_webext_update2.xpi Binary files differnew file mode 100644 index 0000000000..19967c39c0 --- /dev/null +++ b/browser/base/content/test/webextensions/browser_webext_update2.xpi diff --git a/browser/base/content/test/webextensions/browser_webext_update_icon1.xpi b/browser/base/content/test/webextensions/browser_webext_update_icon1.xpi Binary files differnew file mode 100644 index 0000000000..24cb7616d2 --- /dev/null +++ b/browser/base/content/test/webextensions/browser_webext_update_icon1.xpi diff --git a/browser/base/content/test/webextensions/browser_webext_update_icon2.xpi b/browser/base/content/test/webextensions/browser_webext_update_icon2.xpi Binary files differnew file mode 100644 index 0000000000..fd9cf7eb0e --- /dev/null +++ b/browser/base/content/test/webextensions/browser_webext_update_icon2.xpi diff --git a/browser/base/content/test/webextensions/browser_webext_update_origins1.xpi b/browser/base/content/test/webextensions/browser_webext_update_origins1.xpi Binary files differnew file mode 100644 index 0000000000..2909f8e8fd --- /dev/null +++ b/browser/base/content/test/webextensions/browser_webext_update_origins1.xpi diff --git a/browser/base/content/test/webextensions/browser_webext_update_origins2.xpi b/browser/base/content/test/webextensions/browser_webext_update_origins2.xpi Binary files differnew file mode 100644 index 0000000000..b1051affb1 --- /dev/null +++ b/browser/base/content/test/webextensions/browser_webext_update_origins2.xpi diff --git a/browser/base/content/test/webextensions/browser_webext_update_perms1.xpi b/browser/base/content/test/webextensions/browser_webext_update_perms1.xpi Binary files differnew file mode 100644 index 0000000000..f4942f9082 --- /dev/null +++ b/browser/base/content/test/webextensions/browser_webext_update_perms1.xpi diff --git a/browser/base/content/test/webextensions/browser_webext_update_perms2.xpi b/browser/base/content/test/webextensions/browser_webext_update_perms2.xpi Binary files differnew file mode 100644 index 0000000000..2c023edc9d --- /dev/null +++ b/browser/base/content/test/webextensions/browser_webext_update_perms2.xpi diff --git a/browser/base/content/test/webextensions/file_install_extensions.html b/browser/base/content/test/webextensions/file_install_extensions.html new file mode 100644 index 0000000000..9dd8ae830d --- /dev/null +++ b/browser/base/content/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/browser/base/content/test/webextensions/head.js b/browser/base/content/test/webextensions/head.js new file mode 100644 index 0000000000..11dc18acdd --- /dev/null +++ b/browser/base/content/test/webextensions/head.js @@ -0,0 +1,699 @@ +ChromeUtils.defineModuleGetter( + this, + "AddonTestUtils", + "resource://testing-common/AddonTestUtils.jsm" +); + +const BASE = getRootDirectory(gTestPath).replace( + "chrome://mochitests/content/", + "https://example.com/" +); + +var { ExtensionsUI } = ChromeUtils.import( + "resource:///modules/ExtensionsUI.jsm" +); +XPCOMUtils.defineLazyGetter(this, "Management", () => { + // eslint-disable-next-line no-shadow + const { Management } = ChromeUtils.import( + "resource://gre/modules/Extension.jsm" + ); + return Management; +}); + +let { 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); + }); +} + +function promiseAppMenuNotificationShown(id) { + const { AppMenuNotifications } = ChromeUtils.importESModule( + "resource://gre/modules/AppMenuNotifications.sys.mjs" + ); + return new Promise(resolve => { + function popupshown() { + let notification = AppMenuNotifications.activeNotification; + if (!notification) { + return; + } + + is(notification.id, id, `${id} notification shown`); + ok(PanelUI.isNotificationPanelOpen, "notification panel open"); + + PanelUI.notificationPanel.removeEventListener("popupshown", popupshown); + + let popupnotificationID = PanelUI._getPopupId(notification); + let popupnotification = document.getElementById(popupnotificationID); + + resolve(popupnotification); + } + PanelUI.notificationPanel.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) { + return icon == "chrome://mozapps/skin/extensions/extensionGeneric.svg"; +} + +/** + * Check the contents of an individual permission string. + * This function is fairly specific to the use here and probably not + * suitable for re-use elsewhere... + * + * @param {string} string + * The string value to check (i.e., pulled from the DOM) + * @param {string} key + * The key in browser.properties for the localized string to + * compare with. + * @param {string|null} param + * Optional string to substitute for %S in the localized string. + * @param {string} msg + * The message to be emitted as part of the actual test. + */ +function checkPermissionString(string, key, param, msg) { + let localizedString = param + ? gBrowserBundle.formatStringFromName(key, [param]) + : gBrowserBundle.GetStringFromName(key); + + // If this is a parameterized string and the parameter isn't given, + // just do a simple comparison of the text before and after the %S + if (localizedString.includes("%S")) { + let i = localizedString.indexOf("%S"); + ok(string.startsWith(localizedString.slice(0, i)), msg); + ok(string.endsWith(localizedString.slice(i + 2)), msg); + } else { + is(string, localizedString, msg); + } +} + +/** + * 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 {array} 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., "webextPerms.description.foo") and an + * optional formatting parameter. + * @param {boolean} sideloaded + * Whether the notification is for a sideloaded extenion. + */ +function checkNotification(panel, checkIcon, permissions, sideloaded) { + let icon = panel.getAttribute("icon"); + let ul = document.getElementById("addon-webext-perm-list"); + let singleDataEl = document.getElementById("addon-webext-perm-single-entry"); + 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"); + } + + let description = panel.querySelector(".popup-notification-description") + .textContent; + let expectedDescription = "webextPerms.header"; + if (permissions.length) { + expectedDescription += "WithPerms"; + } + if (sideloaded) { + expectedDescription = "webextPerms.sideloadHeader"; + } + checkPermissionString( + description, + expectedDescription, + undefined, + `Description is the expected one` + ); + 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" + ); + for (let i in permissions) { + let [key, param] = permissions[i]; + checkPermissionString( + ul.children[i].textContent, + key, + param, + `Permission number ${i + 1} is correct` + ); + } + } +} + +/** + * 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. + * @param {string} telemetryBase + * If supplied, the base type for telemetry events that should be + * recorded for this install method. + * + * @returns {Promise} + */ +async function testInstallMethod(installFn, telemetryBase) { + 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], + ], + }); + + if (telemetryBase !== undefined) { + hookExtensionsTelemetry(); + } + + 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 = await BrowserTestUtils.openNewForegroundTab(gBrowser); + + 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. + checkNotification(panel, /^jar:file:\/\/.*\/icon\.png$/, [ + ["webextPerms.hostDescription.wildcard", "wildcard.domain"], + ["webextPerms.hostDescription.oneSite", "singlehost.domain"], + ["webextPerms.description.nativeMessaging"], + // The below permissions are deliberately in this order as permissions + // are sorted alphabetically by the permission string to match AMO. + ["webextPerms.description.history"], + ["webextPerms.description.tabs"], + ]); + } else if (filename == NO_PERMS_XPI) { + checkNotification(panel, isDefaultIcon, []); + } + + if (cancel) { + panel.secondaryButton.click(); + try { + await installMethodPromise; + } catch (err) {} + } else { + // Look for post-install notification + let postInstallPromise = promiseAppMenuNotificationShown( + "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(); + } + + BrowserTestUtils.removeTab(tab); + } + + // 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); + + if (telemetryBase !== undefined) { + // Should see 2 canceled installs followed by 1 successful install + // for this method. + expectTelemetry([ + `${telemetryBase}Rejected`, + `${telemetryBase}Rejected`, + `${telemetryBase}Accepted`, + ]); + } + + 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 }; + } + + // Navigate away from the starting page to force about:addons to load + // in a new tab during the tests below. + BrowserTestUtils.loadURI(gBrowser.selectedBrowser, "about:mozilla"); + await BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser); + + // 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 BrowserOpenAddonsMgr("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; + + BrowserTestUtils.removeTab(gBrowser.selectedTab); + 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_setup(async function head_setup() { + 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(); + } + } + }); +}); + +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 = []; +} |