/* Any copyright is dedicated to the Public Domain. * http://creativecommons.org/publicdomain/zero/1.0/ */ /* * Test Permission Popup for Sideloaded Extensions. */ const { AddonTestUtils } = ChromeUtils.importESModule( "resource://testing-common/AddonTestUtils.sys.mjs" ); const ADDON_ID = "addon1@test.mozilla.org"; const CUSTOM_THEME_ID = "theme1@test.mozilla.org"; const DEFAULT_THEME_ID = "default-theme@mozilla.org"; ChromeUtils.defineESModuleGetters(this, { PERMISSION_L10N: "resource://gre/modules/ExtensionPermissionMessages.sys.mjs", ExtensionPermissions: "resource://gre/modules/ExtensionPermissions.sys.mjs", }); AddonTestUtils.initMochitest(this); const l10n = new Localization(["browser/browser.ftl"], true); const LABEL_FOR_TECHNICAL_AND_INTERACTION_DATA_CHECKBOX = l10n .formatMessagesSync([ "popup-notification-addon-technical-and-interaction-checkbox", ])[0] .attributes.find(attr => attr.name === "label").value; function assertDisabledSideloadedExtensionElement(managerWindow, addonElement) { const doc = addonElement.ownerDocument; const toggleDisabled = addonElement.querySelector( '[action="toggle-disabled"]' ); is( doc.l10n.getAttributes(toggleDisabled).id, "extension-enable-addon-button-label", "Addon toggle-disabled action has the enable label" ); ok(!toggleDisabled.checked, "toggle-disable isn't checked"); } function assertEnabledSideloadedExtensionElement(managerWindow, addonElement) { const doc = addonElement.ownerDocument; const toggleDisabled = addonElement.querySelector( '[action="toggle-disabled"]' ); is( doc.l10n.getAttributes(toggleDisabled).id, "extension-enable-addon-button-label", "Addon toggle-disabled action has the enable label" ); ok(!toggleDisabled.checked, "toggle-disable isn't checked"); } function clickEnableExtension(addonElement) { addonElement.querySelector('[action="toggle-disabled"]').click(); } function assertSectionHeaders(popupContentEl, expectedHeaders = []) { for (const { id, isVisible, fluentId } of expectedHeaders) { const titleEl = popupContentEl.querySelector(`#${id}`); ok(titleEl, `Expected element for ${id}`); Assert.equal( BrowserTestUtils.isVisible(titleEl), isVisible, `Expected ${id} to${isVisible ? "" : " not"} be visible` ); if (isVisible) { Assert.equal( titleEl.textContent, PERMISSION_L10N.formatValueSync(fluentId), `Expected formatted string for ${id}` ); } } } // Test for bug 1647931 // Install a theme, enable it and then enable the default theme again add_task(async function test_theme_enable() { await SpecialPowers.pushPrefEnv({ set: [ ["xpinstall.signatures.required", false], ["extensions.autoDisableScopes", 15], ], }); let theme = { manifest: { browser_specific_settings: { gecko: { id: CUSTOM_THEME_ID } }, name: "Theme 1", theme: { colors: { frame: "#000000", tab_background_text: "#ffffff", }, }, }, }; let xpi = AddonTestUtils.createTempWebExtensionFile(theme); await AddonTestUtils.manuallyInstall(xpi); let changePromise = new Promise(resolve => ExtensionsUI.once("change", resolve) ); ExtensionsUI._checkForSideloaded(); await changePromise; // enable fresh installed theme let manager = await open_manager("addons://list/theme"); let customTheme = getAddonCard(manager, CUSTOM_THEME_ID); clickEnableExtension(customTheme); // enable default theme again let defaultTheme = getAddonCard(manager, DEFAULT_THEME_ID); clickEnableExtension(defaultTheme); let addon = await AddonManager.getAddonByID(CUSTOM_THEME_ID); await close_manager(manager); await addon.uninstall(); }); // Loading extension by sideloading method add_task(async function test_sideloaded_extension_permissions_prompt() { await SpecialPowers.pushPrefEnv({ set: [ ["xpinstall.signatures.required", false], ["extensions.autoDisableScopes", 15], ], }); let options = { manifest: { browser_specific_settings: { gecko: { id: ADDON_ID } }, name: "Test 1", permissions: ["history", "https://*/*"], icons: { 64: "foo-icon.png" }, }, }; let xpi = AddonTestUtils.createTempWebExtensionFile(options); await AddonTestUtils.manuallyInstall(xpi); let changePromise = new Promise(resolve => ExtensionsUI.once("change", resolve) ); ExtensionsUI._checkForSideloaded(); await changePromise; // Test click event on permission cancel option. let manager = await open_manager("addons://list/extension"); let addon = getAddonCard(manager, ADDON_ID); Assert.notEqual(addon, null, "Found sideloaded addon in about:addons"); assertDisabledSideloadedExtensionElement(manager, addon); let popupPromise = promisePopupNotificationShown("addon-webext-permissions"); clickEnableExtension(addon); let panel = await popupPromise; ok(PopupNotifications.isPanelOpen, "Permission popup should be visible"); panel.secondaryButton.click(); ok( !PopupNotifications.isPanelOpen, "Permission popup should be closed / closing" ); addon = await AddonManager.getAddonByID(ADDON_ID); ok( !addon.seen, "Seen flag should remain false after permissions are refused" ); // Test click event on permission accept option. addon = getAddonCard(manager, ADDON_ID); Assert.notEqual(addon, null, "Found sideloaded addon in about:addons"); assertEnabledSideloadedExtensionElement(manager, addon); popupPromise = promisePopupNotificationShown("addon-webext-permissions"); clickEnableExtension(addon); panel = await popupPromise; ok(PopupNotifications.isPanelOpen, "Permission popup should be visible"); let notificationPromise = acceptAppMenuNotificationWhenShown( "addon-installed", ADDON_ID ); panel.button.click(); ok( !PopupNotifications.isPanelOpen, "Permission popup should be closed / closing" ); await notificationPromise; addon = await AddonManager.getAddonByID(ADDON_ID); ok(addon.seen, "Seen flag should be true after permissions are accepted"); ok(!PopupNotifications.isPanelOpen, "Permission popup should not be visible"); await close_manager(manager); await addon.uninstall(); }); add_task(async function testInstallDialogShowsFullDomainsList() { const createTestExtensionXPI = ({ id, domainsListLength = 0, permissions = [], incognito = "spanning", }) => AddonTestUtils.createTempWebExtensionFile({ manifest: { // Set the generated id as a name to make it easier to recognize the test case // from dialog screenshots (e.g. in the screenshot captured when the test hits // a failure). name: id, version: "1.0", browser_specific_settings: { gecko: { id }, }, incognito, permissions: permissions.concat( new Array(domainsListLength).fill("examplehost").map((v, i) => { return `*://${v}${i}.com/*`; }) ), }, }); const LONG_DOMAIN_NAME = `averylongdomainname.${new Array(40) .fill("x") .join("")}.com`; const assertPermsElVisibility = popupContentEl => { Assert.equal( BrowserTestUtils.isVisible(popupContentEl.permsListEl), true, "Expect the permissions list element to be visible" ); }; const assertNoDomainsList = popupContentEl => { const domainsListEl = popupContentEl.querySelector( ".webext-perm-domains-list" ); Assert.ok(!domainsListEl, "Expect no domain list element to be found"); }; const TEST_CASES = [ { msg: "Test install extension with no host permissions", id: "no-domains", domainsListLength: 0, verifyDialog(popupContentEl, noIncognitoCheckbox) { assertSectionHeaders(popupContentEl, [ { id: "addon-webext-perm-title-required", isVisible: false }, { id: "addon-webext-perm-title-data-collection", isVisible: false }, { id: "addon-webext-perm-title-optional", isVisible: !noIncognitoCheckbox, fluentId: "webext-perms-header-optional-settings", }, ]); assertNoDomainsList(popupContentEl); Assert.equal( BrowserTestUtils.isHidden(popupContentEl.permsListOptionalEl), noIncognitoCheckbox, `Expect the permissions list element to be ${ noIncognitoCheckbox ? "hidden" : "visible" }` ); }, }, { msg: "Test install extension with one domain listed in host permissions", id: "one-domain", domainsListLength: 1, verifyDialog(popupContentEl, noIncognitoCheckbox) { assertSectionHeaders(popupContentEl, [ { id: "addon-webext-perm-title-required", isVisible: true, fluentId: "webext-perms-header-required-perms", }, { id: "addon-webext-perm-title-data-collection", isVisible: false, }, { id: "addon-webext-perm-title-optional", isVisible: !noIncognitoCheckbox, fluentId: "webext-perms-header-optional-settings", }, ]); assertPermsElVisibility(popupContentEl); assertNoDomainsList(popupContentEl); const hostPermStringEl = popupContentEl.permsListEl.querySelector( "li.webext-perm-granted" ); Assert.ok( hostPermStringEl, "Expect one granted permission string element" ); Assert.equal( hostPermStringEl.textContent, PERMISSION_L10N.formatValueSync( "webext-perms-host-description-one-domain", { domain: "examplehost0.com", } ), "Got the expected host permission string on extension with only one granted domain" ); Assert.ok( BrowserTestUtils.isVisible(hostPermStringEl), "Expect the host permission string to be visible" ); }, }, { msg: "Test install extension with less than 6 domains listed in host permissions", id: "few-domains", domainsListLength: 5, verifyDialog(popupContentEl) { assertPermsElVisibility(popupContentEl); const domainsListEl = popupContentEl.permsListEl.querySelector( ".webext-perm-domains-list" ); Assert.ok( domainsListEl, "Expect domains list element to be found inside the permission list element" ); Assert.ok( BrowserTestUtils.isVisible(domainsListEl), "Expect the domains list element to be visible" ); Assert.equal( domainsListEl.scrollTopMax, 0, "Expect domains list to not be scrollable (chromeOnly scrollTopMax set to 0)" ); // The permission string associated to XUL label element can be reached as labelEl.value. Assert.equal( domainsListEl.previousElementSibling.value, PERMISSION_L10N.formatValueSync( "webext-perms-host-description-multiple-domains", { domainCount: this.domainsListLength, } ), `Got the expected host permission string on extension with ${this.domainsListLength} granted domain` ); Assert.deepEqual( Array.from(domainsListEl.querySelectorAll("li")).map( el => el.textContent ), new Array(this.domainsListLength) .fill("examplehost") .map((v, i) => `${v}${i}.com`), "Got the expected domains listed in the domains list element" ); }, }, { msg: "Test install extension with many domains listed in host permissions", id: "many-domains", domainsListLength: 20, verifyDialog(popupContentEl) { assertPermsElVisibility(popupContentEl); const domainsListEl = popupContentEl.permsListEl.querySelector( ".webext-perm-domains-list" ); Assert.ok( domainsListEl, "Expect domains list element to be found inside the permission list element" ); Assert.ok( BrowserTestUtils.isVisible(domainsListEl), "Expect the domains list element to be visible" ); Assert.greater( domainsListEl.scrollTopMax, domainsListEl.clientHeight, "Expect domains list to be scrollable (chromeOnly scrollTopMax greater than clientHeight)" ); // The permission string associated to XUL label element can be reached as labelEl.value. Assert.equal( domainsListEl.previousElementSibling.value, PERMISSION_L10N.formatValueSync( "webext-perms-host-description-multiple-domains", { domainCount: this.domainsListLength, } ), `Got the expected host permission string on extension with ${this.domainsListLength} granted domain` ); Assert.deepEqual( Array.from(domainsListEl.querySelectorAll("li")).map( el => el.textContent ), new Array(this.domainsListLength) .fill("examplehost") .map((v, i) => `${v}${i}.com`), "Got the expected domains listed in the domains list element" ); }, }, { msg: "Test text wrapping on a single long domain name", id: "one-long-domain", domainsListLength: 0, permissions: [`*://${LONG_DOMAIN_NAME}/*`], verifyDialog(popupContentEl) { assertPermsElVisibility(popupContentEl); const hostPermStringEl = popupContentEl.permsListEl.querySelector( "li.webext-perm-granted" ); Assert.equal( hostPermStringEl.childNodes[0].nodeType, hostPermStringEl.TEXT_NODE, "Expect to have host permission element child to be a text node" ); Assert.equal( hostPermStringEl.childNodes[0].nodeValue, PERMISSION_L10N.formatValueSync( "webext-perms-host-description-one-domain", { domain: LONG_DOMAIN_NAME, } ), "Got the expected host permission string set as the nextNode value" ); Assert.deepEqual( // Calling getBoxQuads on the text node is expected to be returning // one DOMQuad instance for each line the text node has been broken // into (e.g. 3 DOMQuad instances when the long domain name forces // the text node to be broken over 3 lines). // // This check is asserting that none of the lines the text node has // been broken into has a width larger than the width of the parent // element. // // NOTE: this assertion is expected to hit a failure if // .webext-perm-granted is missing the overflow-wrap CSS rule. hostPermStringEl.childNodes[0] .getBoxQuads() .map(quad => quad.getBounds().width) .filter(width => { return width > hostPermStringEl.getBoundingClientRect().width; }), [], "The host permission text node should NOT overflow the parent element" ); }, }, { msg: "Test text wrapping on long domain name in domains list", id: "one-long-domain-in-domains-list", domainsListLength: 10, permissions: [`*://${LONG_DOMAIN_NAME}/*`], verifyDialog(popupContentEl) { assertPermsElVisibility(popupContentEl); const domainsListEl = popupContentEl.permsListEl.querySelector( ".webext-perm-domains-list" ); Assert.ok( domainsListEl, "Expect domains list element to be found inside the permission list element" ); Assert.ok( BrowserTestUtils.isVisible(domainsListEl), "Expect the domains list element to be visible" ); Assert.equal( domainsListEl.firstElementChild.childNodes[0].nodeType, domainsListEl.TEXT_NODE, "Found text node for the long domain name item part of the domain list item" ); Assert.equal( domainsListEl.firstElementChild.childNodes[0].nodeValue, LONG_DOMAIN_NAME, "Got the expected domain name set on the first domain list item" ); Assert.deepEqual( // This check is asserting that none of the lines the text node has // been broken into has a width larger than the width of the parent // element. // // NOTE: this assertion is expected to hit a failure if .webext-perm-domains-list // list items elements are overflowing (e.g. if it is not inheriting the // overflow-wrap CSS rule from its ascending notes and doesn't have one set on // its own). domainsListEl.firstElementChild.childNodes[0] .getBoxQuads() .map(quad => quad.getBounds().width) .filter(width => { return ( width > domainsListEl.firstElementChild.getBoundingClientRect().width ); }), [], "The domain name text node should NOT overflow the parent element" ); }, }, { msg: "Test wildcard subdomains shown as single host permission", id: "with-wildcard-subdomains", domainsListLength: 0, permissions: ["*://*.example.com/*", "*://example.com/*"], verifyDialog(popupContentEl) { assertPermsElVisibility(popupContentEl); const hostPermStringEl = popupContentEl.permsListEl.querySelector( "li.webext-perm-granted" ); Assert.equal( hostPermStringEl.textContent, PERMISSION_L10N.formatValueSync( "webext-perms-host-description-one-domain", { domain: "example.com", } ), "Expected *.example.com and example.com host permissions to be reported as a single domain permission string" ); }, }, { msg: "Test wildcard subdomains in domains list", id: "with-wildcard-subdomains", domainsListLength: 0, permissions: [ "*://*.example.com/*", "*://example.com/*`", "*://*.example.org/*", "*://example.org/*", ], verifyDialog(popupContentEl) { assertPermsElVisibility(popupContentEl); const domainsListEl = popupContentEl.permsListEl.querySelector( ".webext-perm-domains-list" ); Assert.ok( domainsListEl, "Expect domains list element to be found inside the permission list element" ); Assert.ok( BrowserTestUtils.isVisible(domainsListEl), "Expect the domains list element to be visible" ); // Expect the domains list to only include 2 domains and the host permissions string // to reflect that as well. Assert.deepEqual( Array.from(domainsListEl.querySelectorAll("li")).map( el => el.textContent ), ["example.com", "example.org"], "Got the expected domains listed in the domains list element" ); Assert.equal( domainsListEl.previousElementSibling.value, PERMISSION_L10N.formatValueSync( "webext-perms-host-description-multiple-domains", { domainCount: 2, } ), "Got the expected host permission string on extension with 2 granted domain" ); }, }, ]; for (const testCase of TEST_CASES) { // Repeat each test without and without the private browsing checkbox // (to test the dialog when the host permissions is expected to be part of a list // or to be the only permissions listed in the dialog). for (const incognito of ["spanning", "not_allowed"]) { const noIncognitoCheckbox = incognito === "not_allowed"; info( `${testCase.msg} ${ noIncognitoCheckbox ? "and no other permissions" : "" }` ); const xpi = createTestExtensionXPI({ ...testCase, id: `${testCase.id}-with-incognito-${incognito}@test-ext`, incognito, }); await BrowserTestUtils.withNewTab("about:blank", async () => { const dialogPromise = promisePopupNotificationShown( "addon-webext-permissions" ); gURLBar.value = xpi.path; gURLBar.focus(); EventUtils.synthesizeKey("KEY_Enter"); const popupContentEl = await dialogPromise; testCase.verifyDialog(popupContentEl, noIncognitoCheckbox); let popupHiddenPromise = BrowserTestUtils.waitForEvent( window.PopupNotifications.panel, "popuphidden" ); // hide the panel (this simulates the user dismissing it) popupContentEl.closest("panel").hidePopup(); await popupHiddenPromise; }); } } }); add_task(async function testInstallDialogShowsDataCollectionPermissions() { await SpecialPowers.pushPrefEnv({ set: [["extensions.dataCollectionPermissions.enabled", true]], }); const createTestExtensionXPI = ({ id, manifest_version, incognito = undefined, permissions = undefined, data_collection_permissions = undefined, }) => { return AddonTestUtils.createTempWebExtensionFile({ manifest: { manifest_version, // Set the generated id as a name to make it easier to recognize the // test case from dialog screenshots (e.g. in the screenshot captured // when the test hits a failure). name: id, version: "1.0", incognito, permissions, browser_specific_settings: { gecko: { id, data_collection_permissions, }, }, }, }); }; const assertHeader = (popupContentEl, extensionId) => { Assert.equal( popupContentEl.querySelector(".popup-notification-description") .textContent, PERMISSION_L10N.formatValueSync( "webext-perms-header2", // We set the extension name to the extension id in `createTestExtensionXPI`. { extension: extensionId } ), "Expected header string" ); }; const TEST_CASES = [ { title: "With no data collection and incognito not allowed", incognito: "not_allowed", verifyDialog(popupContentEl, { extensionId }) { assertHeader(popupContentEl, extensionId); Assert.equal( popupContentEl.introEl.textContent, PERMISSION_L10N.formatValueSync("webext-perms-list-intro-unsigned"), "Expected list intro string" ); assertSectionHeaders(popupContentEl, [ { id: "addon-webext-perm-title-required", isVisible: false }, { id: "addon-webext-perm-title-data-collection", isVisible: false }, { id: "addon-webext-perm-title-optional", isVisible: false }, ]); Assert.equal( popupContentEl.permsListEl.childElementCount, 0, "Expected no required permissions" ); Assert.equal( popupContentEl.permsListDataCollectionEl.childElementCount, 0, "Expected no data collection permissions" ); Assert.equal( popupContentEl.permsListOptionalEl.childElementCount, 0, "Expected no optional settings" ); Assert.ok( !popupContentEl.hasAttribute("learnmoreurl"), "Expected no learn more link" ); }, }, // Make sure we have the incognito checkbox when no other permissions are // specified. { title: "With no data collection", verifyDialog(popupContentEl, { extensionId }) { assertHeader(popupContentEl, extensionId); assertSectionHeaders(popupContentEl, [ { id: "addon-webext-perm-title-required", isVisible: false }, { id: "addon-webext-perm-title-data-collection", isVisible: false }, { id: "addon-webext-perm-title-optional", isVisible: true, fluentId: "webext-perms-header-optional-settings", }, ]); Assert.equal( popupContentEl.permsListEl.childElementCount, 0, "Expected no required permissions" ); Assert.equal( popupContentEl.permsListDataCollectionEl.childElementCount, 0, "Expected no data collection permissions" ); Assert.equal( popupContentEl.permsListOptionalEl.childElementCount, 1, "Expected optional settings" ); Assert.ok( popupContentEl.permsListOptionalEl.querySelector( "li.webext-perm-privatebrowsing > moz-checkbox" ), "Expected private browsing checkbox" ); Assert.ok( popupContentEl.hasAttribute("learnmoreurl"), "Expected a learn more link" ); }, }, { title: "With required data collection", data_collection_permissions: { required: ["locationInfo"], }, verifyDialog(popupContentEl, { extensionId }) { assertHeader(popupContentEl, extensionId); assertSectionHeaders(popupContentEl, [ { id: "addon-webext-perm-title-required", isVisible: false }, { id: "addon-webext-perm-title-data-collection", isVisible: true, fluentId: "webext-perms-header-data-collection-perms", }, { id: "addon-webext-perm-title-optional", isVisible: true, fluentId: "webext-perms-header-optional-settings", }, ]); Assert.equal( popupContentEl.permsListEl.childElementCount, 0, "Expected no required permissions" ); Assert.equal( popupContentEl.permsListDataCollectionEl.childElementCount, 1, "Expected a data collection permission" ); Assert.ok( popupContentEl.permsListDataCollectionEl.querySelector( "li.webext-data-collection-perm-granted" ), "Expected data collection item" ); Assert.equal( popupContentEl.permsListDataCollectionEl.firstChild.textContent, PERMISSION_L10N.formatValueSync( "webext-perms-description-data-some", { permissions: "location", } ), "Expected formatted data collection permission string" ); Assert.equal( popupContentEl.permsListOptionalEl.childElementCount, 1, "Expected optional settings" ); Assert.ok( popupContentEl.permsListOptionalEl.querySelector( "li.webext-perm-privatebrowsing > moz-checkbox" ), "Expected private browsing checkbox" ); Assert.ok( popupContentEl.hasAttribute("learnmoreurl"), "Expected a learn more link" ); }, }, { title: "With multiple required data collection", data_collection_permissions: { required: ["locationInfo", "financialAndPaymentInfo", "websiteContent"], }, verifyDialog(popupContentEl, { extensionId }) { assertHeader(popupContentEl, extensionId); assertSectionHeaders(popupContentEl, [ { id: "addon-webext-perm-title-required", isVisible: false }, { id: "addon-webext-perm-title-data-collection", isVisible: true, fluentId: "webext-perms-header-data-collection-perms", }, { id: "addon-webext-perm-title-optional", isVisible: true, fluentId: "webext-perms-header-optional-settings", }, ]); Assert.equal( popupContentEl.permsListEl.childElementCount, 0, "Expected no required permissions" ); Assert.equal( popupContentEl.permsListDataCollectionEl.childElementCount, 1, "Expected a data collection permission" ); Assert.equal( popupContentEl.permsListDataCollectionEl.firstChild.textContent, PERMISSION_L10N.formatValueSync( "webext-perms-description-data-some", { // We pass the result of the `Intl.ListFormat` here to verify its // output. permissions: "location, financial and payment information, website content", } ), "Expected formatted data collection permission string" ); Assert.equal( popupContentEl.permsListOptionalEl.childElementCount, 1, "Expected optional settings" ); Assert.ok( popupContentEl.permsListOptionalEl.querySelector( "li.webext-perm-privatebrowsing > moz-checkbox" ), "Expected private browsing checkbox" ); Assert.ok( popupContentEl.hasAttribute("learnmoreurl"), "Expected a learn more link" ); }, }, { title: "With explicit no data collection", data_collection_permissions: { required: ["none"], }, verifyDialog(popupContentEl, { extensionId }) { assertHeader(popupContentEl, extensionId); assertSectionHeaders(popupContentEl, [ { id: "addon-webext-perm-title-required", isVisible: false }, { id: "addon-webext-perm-title-data-collection", isVisible: true, fluentId: "webext-perms-header-data-collection-is-none", }, { id: "addon-webext-perm-title-optional", isVisible: true, fluentId: "webext-perms-header-optional-settings", }, ]); Assert.equal( popupContentEl.permsListEl.childElementCount, 0, "Expected no required permissions" ); Assert.equal( popupContentEl.permsListDataCollectionEl.childElementCount, 1, "Expected a data collection permission" ); Assert.equal( popupContentEl.permsListDataCollectionEl.textContent, PERMISSION_L10N.formatValueSync("webext-perms-description-data-none"), "Expected formatted data collection permission string" ); Assert.equal( popupContentEl.permsListOptionalEl.childElementCount, 1, "Expected optional settings" ); Assert.ok( popupContentEl.permsListOptionalEl.querySelector( "li.webext-perm-privatebrowsing > moz-checkbox" ), "Expected private browsing checkbox" ); Assert.ok( popupContentEl.hasAttribute("learnmoreurl"), "Expected a learn more link" ); }, }, // This test verifies that we omit "none" when other required data // collection permissions are also defined. { title: "With required data collection and 'none'", // This test extension requires "none" with another data collection permission, // and so it is expected to log a manifest warning. expectManifestWarnings: true, data_collection_permissions: { required: ["none", "locationInfo"], }, verifyDialog(popupContentEl, { extensionId }) { assertHeader(popupContentEl, extensionId); assertSectionHeaders(popupContentEl, [ { id: "addon-webext-perm-title-required", isVisible: false }, { id: "addon-webext-perm-title-data-collection", isVisible: true, fluentId: "webext-perms-header-data-collection-perms", }, { id: "addon-webext-perm-title-optional", isVisible: true, fluentId: "webext-perms-header-optional-settings", }, ]); Assert.equal( popupContentEl.permsListEl.childElementCount, 0, "Expected no required permissions" ); Assert.equal( popupContentEl.permsListDataCollectionEl.childElementCount, 1, "Expected data collection permission" ); Assert.equal( popupContentEl.permsListDataCollectionEl.textContent, PERMISSION_L10N.formatValueSync( "webext-perms-description-data-some", { permissions: "location", } ), "Expected formatted data collection permission string" ); Assert.equal( popupContentEl.permsListOptionalEl.childElementCount, 1, "Expected optional settings" ); Assert.ok( popupContentEl.permsListOptionalEl.querySelector( "li.webext-perm-privatebrowsing > moz-checkbox" ), "Expected private browsing checkbox" ); Assert.ok( popupContentEl.hasAttribute("learnmoreurl"), "Expected a learn more link" ); }, }, { title: "With optional data collection", data_collection_permissions: { optional: ["locationInfo"], }, verifyDialog(popupContentEl, { extensionId }) { assertHeader(popupContentEl, extensionId); assertSectionHeaders(popupContentEl, [ { id: "addon-webext-perm-title-required", isVisible: false }, { id: "addon-webext-perm-title-data-collection", isVisible: false, }, { id: "addon-webext-perm-title-optional", isVisible: true, fluentId: "webext-perms-header-optional-settings", }, ]); Assert.equal( popupContentEl.permsListEl.childElementCount, 0, "Expected no required permissions" ); Assert.equal( popupContentEl.permsListDataCollectionEl.childElementCount, 0, "Expected no data collection permissions" ); Assert.equal( popupContentEl.permsListOptionalEl.childElementCount, 1, "Expected optional settings" ); Assert.ok( popupContentEl.permsListOptionalEl.querySelector( "li.webext-perm-privatebrowsing > moz-checkbox" ), "Expected private browsing checkbox" ); Assert.ok( popupContentEl.hasAttribute("learnmoreurl"), "Expected a learn more link" ); }, }, // This test case verifies that we show a checkbox for the // `technicalAndInteraction` optional data collection permission. { title: "With technical and interaction data", data_collection_permissions: { optional: ["technicalAndInteraction"], }, verifyDialog(popupContentEl, { extensionId }) { assertHeader(popupContentEl, extensionId); assertSectionHeaders(popupContentEl, [ { id: "addon-webext-perm-title-required", isVisible: false }, { id: "addon-webext-perm-title-data-collection", isVisible: false, }, { id: "addon-webext-perm-title-optional", isVisible: true, fluentId: "webext-perms-header-optional-settings", }, ]); Assert.equal( popupContentEl.permsListEl.childElementCount, 0, "Expected no required permissions" ); Assert.equal( popupContentEl.permsListDataCollectionEl.childElementCount, 0, "Expected no data collection permissions" ); Assert.equal( popupContentEl.permsListOptionalEl.childElementCount, 2, "Expected optional settings" ); let checkboxEl = popupContentEl.permsListOptionalEl.querySelector( "li.webext-data-collection-perm-optional > moz-checkbox" ); Assert.ok(checkboxEl, "Expected technical and interaction checkbox"); Assert.ok(checkboxEl.checked, "Expected checkbox to be checked"); Assert.equal( checkboxEl.label, LABEL_FOR_TECHNICAL_AND_INTERACTION_DATA_CHECKBOX, "Expected formatted data collection permission string" ); Assert.ok( popupContentEl.permsListOptionalEl.querySelector( "li.webext-perm-privatebrowsing > moz-checkbox" ), "Expected private browsing checkbox" ); Assert.ok( popupContentEl.hasAttribute("learnmoreurl"), "Expected a learn more link" ); }, }, { title: "With technical and interaction data and no private browsing", incognito: "not_allowed", data_collection_permissions: { optional: ["technicalAndInteraction"], }, verifyDialog(popupContentEl, { extensionId }) { assertHeader(popupContentEl, extensionId); assertSectionHeaders(popupContentEl, [ { id: "addon-webext-perm-title-required", isVisible: false }, { id: "addon-webext-perm-title-data-collection", isVisible: false, }, { id: "addon-webext-perm-title-optional", isVisible: true, fluentId: "webext-perms-header-optional-settings", }, ]); Assert.equal( popupContentEl.permsListEl.childElementCount, 0, "Expected no required permissions" ); Assert.equal( popupContentEl.permsListDataCollectionEl.childElementCount, 0, "Expected no data collection permissions" ); Assert.equal( popupContentEl.permsListOptionalEl.childElementCount, 1, "Expected optional settings" ); let checkboxEl = popupContentEl.permsListOptionalEl.querySelector( "li.webext-data-collection-perm-optional > moz-checkbox" ); Assert.ok(checkboxEl, "Expected technical and interaction checkbox"); Assert.equal( checkboxEl.label, LABEL_FOR_TECHNICAL_AND_INTERACTION_DATA_CHECKBOX, "Expected formatted data collection permission string" ); Assert.ok( popupContentEl.hasAttribute("learnmoreurl"), "Expected a learn more link" ); }, }, { title: "No required data collection and technical and interaction data", data_collection_permissions: { required: ["none"], optional: ["technicalAndInteraction"], }, verifyDialog(popupContentEl, { extensionId }) { assertHeader(popupContentEl, extensionId); assertSectionHeaders(popupContentEl, [ { id: "addon-webext-perm-title-required", isVisible: false }, { id: "addon-webext-perm-title-data-collection", isVisible: true, fluentId: "webext-perms-header-data-collection-is-none", }, { id: "addon-webext-perm-title-optional", isVisible: true, fluentId: "webext-perms-header-optional-settings", }, ]); Assert.equal( popupContentEl.permsListEl.childElementCount, 0, "Expected no required permissions" ); Assert.equal( popupContentEl.permsListDataCollectionEl.childElementCount, 1, "Expected data collection permission" ); Assert.equal( popupContentEl.permsListOptionalEl.childElementCount, 2, "Expected optional settings" ); Assert.equal( popupContentEl.permsListDataCollectionEl.firstChild.textContent, PERMISSION_L10N.formatValueSync("webext-perms-description-data-none"), "Expected formatted data collection permission string" ); const checkboxEl = popupContentEl.permsListOptionalEl.querySelector( "li.webext-data-collection-perm-optional > moz-checkbox" ); Assert.ok(checkboxEl, "Expected technical and interaction checkbox"); Assert.equal( checkboxEl.label, LABEL_FOR_TECHNICAL_AND_INTERACTION_DATA_CHECKBOX, "Expected formatted data collection permission string" ); Assert.ok( popupContentEl.permsListOptionalEl.querySelector( "li.webext-perm-privatebrowsing > moz-checkbox" ), "Expected private browsing checkbox" ); Assert.ok( popupContentEl.hasAttribute("learnmoreurl"), "Expected a learn more link" ); }, }, { title: "With required permission and data collection", permissions: ["bookmarks"], data_collection_permissions: { required: ["bookmarksInfo"], }, verifyDialog(popupContentEl, { extensionId }) { assertHeader(popupContentEl, extensionId); assertSectionHeaders(popupContentEl, [ { id: "addon-webext-perm-title-required", isVisible: true, fluentId: "webext-perms-header-required-perms", }, { id: "addon-webext-perm-title-data-collection", isVisible: true, fluentId: "webext-perms-header-data-collection-perms", }, { id: "addon-webext-perm-title-optional", isVisible: true, fluentId: "webext-perms-header-optional-settings", }, ]); Assert.equal( popupContentEl.permsListEl.childElementCount, 1, "Expected required permission" ); Assert.equal( popupContentEl.permsListDataCollectionEl.childElementCount, 1, "Expected data collection permission" ); Assert.equal( popupContentEl.permsListOptionalEl.childElementCount, 1, "Expected optional settings" ); Assert.ok( popupContentEl.permsListEl.firstChild.classList.contains( "webext-perm-granted" ), "Expected first entry to be the required API permission" ); Assert.ok( popupContentEl.permsListDataCollectionEl.querySelector( "li.webext-data-collection-perm-granted" ), "Expected data collection item" ); Assert.equal( popupContentEl.permsListDataCollectionEl.textContent, PERMISSION_L10N.formatValueSync( "webext-perms-description-data-some", { permissions: "bookmarks", } ), "Expected formatted data collection permission string" ); Assert.ok( popupContentEl.permsListOptionalEl.querySelector( "li.webext-perm-privatebrowsing > moz-checkbox" ), "Expected private browsing checkbox" ); Assert.ok( popupContentEl.hasAttribute("learnmoreurl"), "Expected a learn more link" ); }, }, { title: "With required permission and data collection, and optional collection", permissions: ["bookmarks"], data_collection_permissions: { required: ["bookmarksInfo"], optional: ["technicalAndInteraction"], }, verifyDialog(popupContentEl, { extensionId }) { assertHeader(popupContentEl, extensionId); assertSectionHeaders(popupContentEl, [ { id: "addon-webext-perm-title-required", isVisible: true, fluentId: "webext-perms-header-required-perms", }, { id: "addon-webext-perm-title-data-collection", isVisible: true, fluentId: "webext-perms-header-data-collection-perms", }, { id: "addon-webext-perm-title-optional", isVisible: true, fluentId: "webext-perms-header-optional-settings", }, ]); Assert.equal( popupContentEl.permsListEl.childElementCount, 1, "Expected required permission" ); Assert.equal( popupContentEl.permsListDataCollectionEl.childElementCount, 1, "Expected data collection permission" ); Assert.equal( popupContentEl.permsListOptionalEl.childElementCount, 2, "Expected optional settings" ); Assert.ok( popupContentEl.permsListEl.firstChild.classList.contains( "webext-perm-granted" ), "Expected first entry to be the required API permission" ); Assert.ok( popupContentEl.permsListDataCollectionEl.querySelector( "li.webext-data-collection-perm-granted" ), "Expected data collection item" ); Assert.equal( popupContentEl.permsListDataCollectionEl.textContent, PERMISSION_L10N.formatValueSync( "webext-perms-description-data-some", { permissions: "bookmarks", } ), "Expected formatted data collection permission string" ); const checkboxEl = popupContentEl.permsListOptionalEl.childNodes[0].querySelector( "moz-checkbox" ); Assert.ok(checkboxEl, "Expected technical and interaction checkbox"); Assert.equal( checkboxEl.label, LABEL_FOR_TECHNICAL_AND_INTERACTION_DATA_CHECKBOX, "Expected formatted data collection permission string" ); // Make sure the incognito checkbox is the last item. Assert.ok( popupContentEl.permsListOptionalEl.childNodes[1].querySelector( "li.webext-perm-privatebrowsing > moz-checkbox" ), "Expected private browsing checkbox" ); Assert.ok( popupContentEl.hasAttribute("learnmoreurl"), "Expected a learn more link" ); }, }, ]; for (const manifest_version of [2, 3]) { for (const { title, verifyDialog, ...testCase } of TEST_CASES) { info(`MV${manifest_version} - ${title}`); const extensionId = `@${title.toLowerCase().replaceAll(/[^\w]+/g, "-")}`; const xpi = createTestExtensionXPI({ id: extensionId, manifest_version, ...testCase, }); await BrowserTestUtils.withNewTab("about:blank", async () => { const dialogPromise = promisePopupNotificationShown( "addon-webext-permissions" ); if (testCase.expectManifestWarnings) { await ExtensionTestUtils.failOnSchemaWarnings(false); } gURLBar.value = xpi.path; gURLBar.focus(); EventUtils.synthesizeKey("KEY_Enter"); const popupContentEl = await dialogPromise; verifyDialog(popupContentEl, { extensionId }); let popupHiddenPromise = BrowserTestUtils.waitForEvent( window.PopupNotifications.panel, "popuphidden" ); // Hide the panel (this simulates the user dismissing it). popupContentEl.closest("panel").hidePopup(); await popupHiddenPromise; if (testCase.expectManifestWarnings) { ExtensionTestUtils.failOnSchemaWarnings(true); } }); } } await SpecialPowers.popPrefEnv(); }); add_task(async function testTechnicalAndInteractionData() { await SpecialPowers.pushPrefEnv({ set: [["extensions.dataCollectionPermissions.enabled", true]], }); const extensionId = "@test-id"; const extension = AddonTestUtils.createTempWebExtensionFile({ manifest: { version: "1.0", browser_specific_settings: { gecko: { id: extensionId, data_collection_permissions: { optional: ["technicalAndInteraction"], }, }, }, }, }); let perms = await ExtensionPermissions.get(extensionId); Assert.deepEqual( perms, { permissions: [], origins: [], data_collection: [] }, "Expected no permissions" ); await BrowserTestUtils.withNewTab("about:blank", async () => { const dialogPromise = promisePopupNotificationShown( "addon-webext-permissions" ); gURLBar.value = extension.path; gURLBar.focus(); EventUtils.synthesizeKey("KEY_Enter"); const popupContentEl = await dialogPromise; // Install the add-on. let notificationPromise = acceptAppMenuNotificationWhenShown( "addon-installed", extensionId ); popupContentEl.button.click(); await notificationPromise; perms = await ExtensionPermissions.get(extensionId); Assert.deepEqual( perms, { permissions: [], origins: [], data_collection: ["technicalAndInteraction"], }, "Expected data collection permission" ); const addon = await AddonManager.getAddonByID(extensionId); Assert.ok(addon, "Expected add-on"); await addon.uninstall(); }); // Repeat but uncheck the checkbox this time. await BrowserTestUtils.withNewTab("about:blank", async () => { const dialogPromise = promisePopupNotificationShown( "addon-webext-permissions" ); gURLBar.value = extension.path; gURLBar.focus(); EventUtils.synthesizeKey("KEY_Enter"); const popupContentEl = await dialogPromise; const checkboxEl = popupContentEl.permsListOptionalEl.querySelector( "li.webext-data-collection-perm-optional > moz-checkbox" ); checkboxEl.click(); // Install the add-on. let notificationPromise = acceptAppMenuNotificationWhenShown( "addon-installed", extensionId ); popupContentEl.button.click(); await notificationPromise; perms = await ExtensionPermissions.get(extensionId); Assert.deepEqual( perms, { permissions: [], origins: [], data_collection: [], }, "Expected no data collection permission" ); const addon = await AddonManager.getAddonByID(extensionId); Assert.ok(addon, "Expected add-on"); await addon.uninstall(); }); await SpecialPowers.popPrefEnv(); }); add_task(async function testVerifyPostInstallPopupWithDataCollection() { await SpecialPowers.pushPrefEnv({ set: [["extensions.dataCollectionPermissions.enabled", true]], }); const extensionId = "@test-id"; const extension = AddonTestUtils.createTempWebExtensionFile({ manifest: { browser_specific_settings: { gecko: { id: extensionId }, }, }, }); await BrowserTestUtils.withNewTab( { gBrowser, url: "about:robots" }, async () => { const dialogPromise = promisePopupNotificationShown( "addon-webext-permissions" ); gURLBar.value = extension.path; gURLBar.focus(); EventUtils.synthesizeKey("KEY_Enter"); const popupContentEl = await dialogPromise; // Install the add-on. let notificationPromise = waitAppMenuNotificationShown( "addon-installed", extensionId ); popupContentEl.button.click(); let notification = await notificationPromise; // Verify the post-install popup. Assert.ok( notification .querySelector("#addon-install-description") .textContent.startsWith( "Update permissions and data preferences any time" ), "Expected notification content with data collection" ); let settingsLink = notification.querySelector( "#addon-install-description > a" ); Assert.ok(settingsLink, "Expected a link in the post-install popup"); const tabPromise = BrowserTestUtils.waitForNewTab( gBrowser, "about:addons", true ); settingsLink.click(); const tab = await tabPromise; Assert.ok(tab, "Expected tab"); is( gBrowser.selectedBrowser.contentWindow.gViewController.currentViewId, `addons://detail/${encodeURIComponent(extensionId)}`, "Expected about:addons to show the detail view of the extension" ); BrowserTestUtils.removeTab(tab); // Dismiss the popup by clicking "OK". notification.button.click(); const addon = await AddonManager.getAddonByID(extensionId); Assert.ok(addon, "Expected add-on"); await addon.uninstall(); } ); // Same as above with keyboard navigation. await BrowserTestUtils.withNewTab( { gBrowser, url: "about:robots" }, async () => { const dialogPromise = promisePopupNotificationShown( "addon-webext-permissions" ); gURLBar.value = extension.path; gURLBar.focus(); EventUtils.synthesizeKey("KEY_Enter"); const popupContentEl = await dialogPromise; // Install the add-on. let notificationPromise = waitAppMenuNotificationShown( "addon-installed", extensionId ); popupContentEl.button.click(); let notification = await notificationPromise; // Verify the post-install popup. let settingsLink = notification.querySelector( "#addon-install-description > a" ); Assert.ok(settingsLink, "Expected a link in the post-install popup"); const tabPromise = BrowserTestUtils.waitForNewTab( gBrowser, "about:addons", true ); const focused = BrowserTestUtils.waitForEvent(settingsLink, "focus"); settingsLink.focus(); await focused; EventUtils.synthesizeKey("KEY_Enter"); const tab = await tabPromise; Assert.ok(tab, "Expected tab"); is( gBrowser.selectedBrowser.contentWindow.gViewController.currentViewId, `addons://detail/${encodeURIComponent(extensionId)}`, "Expected about:addons to show the detail view of the extension" ); BrowserTestUtils.removeTab(tab); // Dismiss the popup by clicking "OK". notification.button.click(); const addon = await AddonManager.getAddonByID(extensionId); Assert.ok(addon, "Expected add-on"); await addon.uninstall(); } ); await SpecialPowers.popPrefEnv(); });