diff options
Diffstat (limited to 'toolkit/mozapps/extensions/test/browser/browser_html_list_view.js')
-rw-r--r-- | toolkit/mozapps/extensions/test/browser/browser_html_list_view.js | 1063 |
1 files changed, 1063 insertions, 0 deletions
diff --git a/toolkit/mozapps/extensions/test/browser/browser_html_list_view.js b/toolkit/mozapps/extensions/test/browser/browser_html_list_view.js new file mode 100644 index 0000000000..2631a164df --- /dev/null +++ b/toolkit/mozapps/extensions/test/browser/browser_html_list_view.js @@ -0,0 +1,1063 @@ +/* eslint max-len: ["error", 80] */ + +const { AddonTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/AddonTestUtils.sys.mjs" +); + +AddonTestUtils.initMochitest(this); + +let promptService; + +const SUPPORT_URL = Services.urlFormatter.formatURL( + Services.prefs.getStringPref("app.support.baseURL") +); +const REMOVE_SUMO_URL = SUPPORT_URL + "cant-remove-addon"; + +function getTestCards(root) { + return root.querySelectorAll('addon-card[addon-id$="@mochi.test"]'); +} + +function getCardByAddonId(root, id) { + return root.querySelector(`addon-card[addon-id="${id}"]`); +} + +function isEmpty(el) { + return !el.children.length; +} + +function waitForThemeChange(list) { + // Wait for two move events. One theme will be enabled and another disabled. + let moveCount = 0; + return BrowserTestUtils.waitForEvent(list, "move", () => ++moveCount == 2); +} + +let mockProvider; + +add_setup(async function () { + mockProvider = new MockProvider(["extension", "sitepermission"]); + promptService = mockPromptService(); +}); + +let extensionsCreated = 0; + +function createExtensions(manifestExtras) { + return manifestExtras.map(extra => + ExtensionTestUtils.loadExtension({ + manifest: { + name: "Test extension", + browser_specific_settings: { + gecko: { id: `test-${extensionsCreated++}@mochi.test` }, + }, + icons: { + 32: "test-icon.png", + }, + ...extra, + }, + useAddonManager: "temporary", + }) + ); +} + +add_task(async function testExtensionList() { + let id = "test@mochi.test"; + let headingId = "test_mochi_test-heading"; + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + name: "Test extension", + browser_specific_settings: { gecko: { id } }, + icons: { + 32: "test-icon.png", + }, + }, + useAddonManager: "temporary", + }); + await extension.startup(); + + let addon = await AddonManager.getAddonByID(id); + ok(addon, "The add-on can be found"); + + let win = await loadInitialView("extension"); + let doc = win.document; + + // Find the addon-list to listen for events. + let list = doc.querySelector("addon-list"); + + // There shouldn't be any disabled extensions. + let disabledSection = getSection(doc, "extension-disabled-section"); + ok(isEmpty(disabledSection), "The disabled section is empty"); + + // The loaded extension should be in the enabled list. + let enabledSection = getSection(doc, "extension-enabled-section"); + ok( + enabledSection && !isEmpty(enabledSection), + "The enabled section isn't empty" + ); + let card = getCardByAddonId(enabledSection, id); + ok(card, "The card is in the enabled section"); + + // Check the properties of the card. + is(card.addonNameEl.textContent, "Test extension", "The name is set"); + is( + card.querySelector("h3").id, + headingId, + "The add-on name has the correct id" + ); + is( + card.querySelector(".card").getAttribute("aria-labelledby"), + headingId, + "The card is labelled by the heading" + ); + let icon = card.querySelector(".addon-icon"); + ok(icon.src.endsWith("/test-icon.png"), "The icon is set"); + + // Disable the extension. + let disableToggle = card.querySelector('[action="toggle-disabled"]'); + ok(disableToggle.pressed, "The disable toggle is pressed"); + is( + doc.l10n.getAttributes(disableToggle).id, + "extension-enable-addon-button-label", + "The toggle has the enable label" + ); + ok(disableToggle.getAttribute("aria-label"), "There's an aria-label"); + ok(!disableToggle.hidden, "The toggle is visible"); + + let disabled = BrowserTestUtils.waitForEvent(list, "move"); + disableToggle.click(); + await disabled; + is( + card.parentNode, + disabledSection, + "The card is now in the disabled section" + ); + + // The disable button is now enabled. + ok(!disableToggle.pressed, "The disable toggle is not pressed"); + is( + doc.l10n.getAttributes(disableToggle).id, + "extension-enable-addon-button-label", + "The button has the same enable label" + ); + ok(disableToggle.getAttribute("aria-label"), "There's an aria-label"); + + // Remove the add-on. + let removeButton = card.querySelector('[action="remove"]'); + is( + doc.l10n.getAttributes(removeButton).id, + "remove-addon-button", + "The button has the remove label" + ); + // There is a support link when the add-on isn't removeable, verify we don't + // always include one. + ok(!removeButton.querySelector("a"), "There isn't a link in the item"); + + // Remove but cancel. + let cancelled = BrowserTestUtils.waitForEvent(card, "remove-cancelled"); + removeButton.click(); + await cancelled; + + let removed = BrowserTestUtils.waitForEvent(list, "remove"); + // Tell the mock prompt service that the prompt was accepted. + promptService._response = 0; + removeButton.click(); + await removed; + + addon = await AddonManager.getAddonByID(id); + ok( + addon && !!(addon.pendingOperations & AddonManager.PENDING_UNINSTALL), + "The addon is pending uninstall" + ); + + // Ensure that a pending uninstall bar has been created for the + // pending uninstall extension, and pressing the undo button will + // refresh the list and render a card to the re-enabled extension. + assertHasPendingUninstalls(list, 1); + assertHasPendingUninstallAddon(list, addon); + + // Add a second pending uninstall extension. + info("Install a second test extension and wait for addon card rendered"); + let added = BrowserTestUtils.waitForEvent(list, "add"); + const extension2 = ExtensionTestUtils.loadExtension({ + manifest: { + name: "Test extension 2", + browser_specific_settings: { gecko: { id: "test-2@mochi.test" } }, + icons: { + 32: "test-icon.png", + }, + }, + useAddonManager: "temporary", + }); + await extension2.startup(); + + await added; + ok( + getCardByAddonId(list, extension2.id), + "Got a card added for the second extension" + ); + + info("Uninstall the second test extension and wait for addon card removed"); + removed = BrowserTestUtils.waitForEvent(list, "remove"); + const addon2 = await AddonManager.getAddonByID(extension2.id); + addon2.uninstall(true); + await removed; + + ok( + !getCardByAddonId(list, extension2.id), + "Addon card for the second extension removed" + ); + + assertHasPendingUninstalls(list, 2); + assertHasPendingUninstallAddon(list, addon2); + + // Addon2 was enabled before entering the pending uninstall state, + // wait for its startup after pressing undo. + let addon2Started = AddonTestUtils.promiseWebExtensionStartup(addon2.id); + await testUndoPendingUninstall(list, addon); + await testUndoPendingUninstall(list, addon2); + info("Wait for the second pending uninstal add-ons startup"); + await addon2Started; + + ok( + getCardByAddonId(disabledSection, addon.id), + "The card for the first extension is in the disabled section" + ); + ok( + getCardByAddonId(enabledSection, addon2.id), + "The card for the second extension is in the enabled section" + ); + + await extension2.unload(); + await extension.unload(); + + // Install a theme and verify that it is not listed in the pending + // uninstall message bars while the list extensions view is loaded. + const themeXpi = AddonTestUtils.createTempWebExtensionFile({ + manifest: { + name: "My theme", + browser_specific_settings: { gecko: { id: "theme@mochi.test" } }, + theme: {}, + }, + }); + const themeAddon = await AddonManager.installTemporaryAddon(themeXpi); + // Leave it pending uninstall, the following assertions related to + // the pending uninstall message bars will fail if the theme is listed. + await themeAddon.uninstall(true); + + // Install a third addon to verify that is being fully removed once the + // about:addons page is closed. + const xpi = AddonTestUtils.createTempWebExtensionFile({ + manifest: { + name: "Test extension 3", + browser_specific_settings: { gecko: { id: "test-3@mochi.test" } }, + icons: { + 32: "test-icon.png", + }, + }, + }); + + added = BrowserTestUtils.waitForEvent(list, "add"); + const addon3 = await AddonManager.installTemporaryAddon(xpi); + await added; + ok( + getCardByAddonId(list, addon3.id), + "Addon card for the third extension added" + ); + + removed = BrowserTestUtils.waitForEvent(list, "remove"); + addon3.uninstall(true); + await removed; + ok( + !getCardByAddonId(list, addon3.id), + "Addon card for the third extension removed" + ); + + assertHasPendingUninstalls(list, 1); + ok( + addon3 && !!(addon3.pendingOperations & AddonManager.PENDING_UNINSTALL), + "The third addon is pending uninstall" + ); + + await closeView(win); + + ok( + !(await AddonManager.getAddonByID(addon3.id)), + "The third addon has been fully uninstalled" + ); + + ok( + themeAddon.pendingOperations & AddonManager.PENDING_UNINSTALL, + "The theme addon is pending after the list extension view is closed" + ); + + await themeAddon.uninstall(); + + ok( + !(await AddonManager.getAddonByID(themeAddon.id)), + "The theme addon is fully uninstalled" + ); +}); + +add_task(async function testMouseSupport() { + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + name: "Test extension", + browser_specific_settings: { gecko: { id: "test@mochi.test" } }, + }, + useAddonManager: "temporary", + }); + await extension.startup(); + + let win = await loadInitialView("extension"); + let doc = win.document; + + let [card] = getTestCards(doc); + is(card.addon.id, "test@mochi.test", "The right card is found"); + + let panel = card.querySelector("panel-list"); + + ok(!panel.open, "The panel is initially closed"); + await BrowserTestUtils.synthesizeMouseAtCenter( + "addon-card[addon-id$='@mochi.test'] button[action='more-options']", + { type: "mousedown" }, + win.docShell.browsingContext + ); + ok(panel.open, "The panel is now open"); + + await closeView(win); + await extension.unload(); +}); + +add_task(async function testKeyboardSupport() { + let id = "test@mochi.test"; + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + name: "Test extension", + browser_specific_settings: { gecko: { id } }, + }, + useAddonManager: "temporary", + }); + await extension.startup(); + + let win = await loadInitialView("extension"); + let doc = win.document; + + // Some helpers. + let tab = event => EventUtils.synthesizeKey("VK_TAB", event); + let space = () => EventUtils.synthesizeKey(" ", {}); + let isFocused = (el, msg) => is(doc.activeElement, el, msg); + + // Find the addon-list to listen for events. + let list = doc.querySelector("addon-list"); + let enabledSection = getSection(doc, "extension-enabled-section"); + let disabledSection = getSection(doc, "extension-disabled-section"); + + // Find the card. + let [card] = getTestCards(list); + is(card.addon.id, "test@mochi.test", "The right card is found"); + + // Focus the more options menu button. + let moreOptionsButton = card.querySelector('[action="more-options"]'); + moreOptionsButton.focus(); + isFocused(moreOptionsButton, "The more options button is focused"); + + // Test opening and closing the menu. + let moreOptionsMenu = card.querySelector("panel-list"); + let expandButton = moreOptionsMenu.querySelector('[action="expand"]'); + let removeButton = card.querySelector('[action="remove"]'); + is(moreOptionsMenu.open, false, "The menu is closed"); + let shown = BrowserTestUtils.waitForEvent(moreOptionsMenu, "shown"); + space(); + await shown; + is(moreOptionsMenu.open, true, "The menu is open"); + isFocused(removeButton, "The remove button is now focused"); + tab({ shiftKey: true }); + is(moreOptionsMenu.open, true, "The menu stays open"); + isFocused(expandButton, "The focus has looped to the bottom"); + tab(); + is(moreOptionsMenu.open, true, "The menu stays open"); + isFocused(removeButton, "The focus has looped to the top"); + + let hidden = BrowserTestUtils.waitForEvent(moreOptionsMenu, "hidden"); + EventUtils.synthesizeKey("Escape", {}); + await hidden; + isFocused(moreOptionsButton, "Escape closed the menu"); + + // Disable the add-on. + let disableButton = card.querySelector('[action="toggle-disabled"]'); + tab({ shiftKey: true }); + isFocused(disableButton, "The disable toggle is focused"); + is(card.parentNode, enabledSection, "The card is in the enabled section"); + space(); + // Wait for the add-on state to change. + let [disabledAddon] = await AddonTestUtils.promiseAddonEvent("onDisabled"); + is(disabledAddon.id, id, "The right add-on was disabled"); + is( + card.parentNode, + enabledSection, + "The card is still in the enabled section" + ); + isFocused(disableButton, "The disable button is still focused"); + let moved = BrowserTestUtils.waitForEvent(list, "move"); + // We intentionally turn off this a11y check, because the following click + // is purposefully targeting a non-interactive element to clear the focused + // state with a mouse which can be done by assistive technology and keyboard + // by pressing `Esc`, this rule check shall be ignored by a11y_checks suite. + AccessibilityUtils.setEnv({ mustHaveAccessibleRule: false }); + // Click outside the list to clear any focus. + EventUtils.synthesizeMouseAtCenter( + doc.querySelector(".header-name"), + {}, + win + ); + AccessibilityUtils.resetEnv(); + await moved; + is( + card.parentNode, + disabledSection, + "The card moved when keyboard focus left the list" + ); + + // Remove the add-on. + moreOptionsButton.focus(); + shown = BrowserTestUtils.waitForEvent(moreOptionsMenu, "shown"); + space(); + is(moreOptionsMenu.open, true, "The menu is open"); + await shown; + isFocused(removeButton, "The remove button is focused"); + let removed = BrowserTestUtils.waitForEvent(list, "remove"); + space(); + await removed; + is(card.parentNode, null, "The card is no longer on the page"); + + await extension.unload(); + await closeView(win); +}); + +add_task(async function testOpenDetailFromNameKeyboard() { + let id = "details@mochi.test"; + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + name: "Detail extension", + browser_specific_settings: { gecko: { id } }, + }, + useAddonManager: "temporary", + }); + await extension.startup(); + + let win = await loadInitialView("extension"); + + let card = getCardByAddonId(win.document, id); + + info("focus the add-on's name, which should be an <a>"); + card.addonNameEl.focus(); + + let detailsLoaded = waitForViewLoad(win); + EventUtils.synthesizeKey("KEY_Enter", {}, win); + await detailsLoaded; + + card = getCardByAddonId(win.document, id); + is( + card.addonNameEl.textContent, + "Detail extension", + "The right detail view is laoded" + ); + + await extension.unload(); + await closeView(win); +}); + +add_task(async function testExtensionReordering() { + let extensions = createExtensions([ + { name: "Extension One" }, + { name: "This is last" }, + { name: "An extension, is first" }, + ]); + + await Promise.all(extensions.map(extension => extension.startup())); + + let win = await loadInitialView("extension"); + let doc = win.document; + + // Get a reference to the addon-list for events. + let list = doc.querySelector("addon-list"); + + // Find the related cards, they should all have @mochi.test ids. + let enabledSection = getSection(doc, "extension-enabled-section"); + let cards = getTestCards(enabledSection); + + is(cards.length, 3, "Each extension has an addon-card"); + + let order = Array.from(cards).map(card => card.addon.name); + Assert.deepEqual( + order, + ["An extension, is first", "Extension One", "This is last"], + "The add-ons are sorted by name" + ); + + // Disable the second extension. + let disabledSection = getSection(doc, "extension-disabled-section"); + ok(isEmpty(disabledSection), "The disabled section is initially empty"); + + // Disable the add-ons in a different order. + let reorderedCards = [cards[1], cards[0], cards[2]]; + for (let { addon } of reorderedCards) { + let moved = BrowserTestUtils.waitForEvent(list, "move"); + await addon.disable(); + await moved; + } + + order = Array.from(getTestCards(disabledSection)).map( + card => card.addon.name + ); + Assert.deepEqual( + order, + ["An extension, is first", "Extension One", "This is last"], + "The add-ons are sorted by name" + ); + + // All of our installed add-ons are disabled, install a new one. + let [newExtension] = createExtensions([{ name: "Extension New" }]); + let added = BrowserTestUtils.waitForEvent(list, "add"); + await newExtension.startup(); + await added; + + let [newCard] = getTestCards(enabledSection); + is( + newCard.addon.name, + "Extension New", + "The new add-on is in the enabled list" + ); + + // Enable everything again. + for (let { addon } of cards) { + let moved = BrowserTestUtils.waitForEvent(list, "move"); + await addon.enable(); + await moved; + } + + order = Array.from(getTestCards(enabledSection)).map(card => card.addon.name); + Assert.deepEqual( + order, + [ + "An extension, is first", + "Extension New", + "Extension One", + "This is last", + ], + "The add-ons are sorted by name" + ); + + // Remove the new extension. + let removed = BrowserTestUtils.waitForEvent(list, "remove"); + await newExtension.unload(); + await removed; + is(newCard.parentNode, null, "The new card has been removed"); + + await Promise.all(extensions.map(extension => extension.unload())); + await closeView(win); +}); + +add_task(async function testThemeList() { + let theme = ExtensionTestUtils.loadExtension({ + manifest: { + browser_specific_settings: { gecko: { id: "theme@mochi.test" } }, + name: "My theme", + theme: {}, + }, + useAddonManager: "temporary", + }); + + let win = await loadInitialView("theme"); + let doc = win.document; + + let list = doc.querySelector("addon-list"); + + let cards = getTestCards(list); + is(cards.length, 0, "There are no test themes to start"); + + let added = BrowserTestUtils.waitForEvent(list, "add"); + await theme.startup(); + await added; + + cards = getTestCards(list); + is(cards.length, 1, "There is now one custom theme"); + + let [card] = cards; + is(card.addon.name, "My theme", "The card is for the test theme"); + + let enabledSection = getSection(doc, "theme-enabled-section"); + let disabledSection = getSection(doc, "theme-disabled-section"); + + await TestUtils.waitForCondition( + () => enabledSection.querySelectorAll("addon-card").length == 1 + ); + + is( + card.parentNode, + enabledSection, + "The new theme card is in the enabled section" + ); + is( + enabledSection.querySelectorAll("addon-card").length, + 1, + "There is one enabled theme" + ); + + let toggleThemeEnabled = async () => { + let themesChanged = waitForThemeChange(list); + card.querySelector('[action="toggle-disabled"]').click(); + await themesChanged; + + await TestUtils.waitForCondition( + () => enabledSection.querySelectorAll("addon-card").length == 1 + ); + }; + + await toggleThemeEnabled(); + + is( + card.parentNode, + disabledSection, + "The card is now in the disabled section" + ); + is( + enabledSection.querySelectorAll("addon-card").length, + 1, + "There is one enabled theme" + ); + + // Re-enable the theme. + await toggleThemeEnabled(); + is(card.parentNode, enabledSection, "Card is back in the Enabled section"); + + // Remove theme and verify that the default theme is re-enabled. + let removed = BrowserTestUtils.waitForEvent(list, "remove"); + // Confirm removal. + promptService._response = 0; + card.querySelector('[action="remove"]').click(); + await removed; + is(card.parentNode, null, "Card has been removed from the view"); + await TestUtils.waitForCondition( + () => enabledSection.querySelectorAll("addon-card").length == 1 + ); + + let defaultTheme = getCardByAddonId(doc, "default-theme@mozilla.org"); + is(defaultTheme.parentNode, enabledSection, "The default theme is reenabled"); + + await testUndoPendingUninstall(list, card.addon); + await TestUtils.waitForCondition( + () => enabledSection.querySelectorAll("addon-card").length == 1 + ); + is(defaultTheme.parentNode, disabledSection, "The default theme is disabled"); + ok(getCardByAddonId(enabledSection, theme.id), "Theme should be reenabled"); + + await theme.unload(); + await closeView(win); +}); + +add_task(async function testBuiltInThemeButtons() { + let win = await loadInitialView("theme"); + let doc = win.document; + + // Find the addon-list to listen for events. + let list = doc.querySelector("addon-list"); + let enabledSection = getSection(doc, "theme-enabled-section"); + let disabledSection = getSection(doc, "theme-disabled-section"); + + let defaultTheme = getCardByAddonId(doc, "default-theme@mozilla.org"); + let darkTheme = getCardByAddonId(doc, "firefox-compact-dark@mozilla.org"); + + // Check that themes are in the expected spots. + is(defaultTheme.parentNode, enabledSection, "The default theme is enabled"); + is(darkTheme.parentNode, disabledSection, "The dark theme is disabled"); + + // The default theme shouldn't have remove or disable options. + let defaultButtons = { + toggleDisabled: defaultTheme.querySelector('[action="toggle-disabled"]'), + remove: defaultTheme.querySelector('[action="remove"]'), + }; + is(defaultButtons.toggleDisabled.hidden, true, "Disable is hidden"); + is(defaultButtons.remove.hidden, true, "Remove is hidden"); + + // The dark theme should have an enable button, but not remove. + let darkButtons = { + toggleDisabled: darkTheme.querySelector('[action="toggle-disabled"]'), + remove: darkTheme.querySelector('[action="remove"]'), + }; + is(darkButtons.toggleDisabled.hidden, false, "Enable is visible"); + is(darkButtons.remove.hidden, true, "Remove is hidden"); + + // Enable the dark theme and check the buttons again. + let themesChanged = waitForThemeChange(list); + darkButtons.toggleDisabled.click(); + await themesChanged; + + await TestUtils.waitForCondition( + () => enabledSection.querySelectorAll("addon-card").length == 1 + ); + + // Check the buttons. + is(defaultButtons.toggleDisabled.hidden, false, "Enable is visible"); + is(defaultButtons.remove.hidden, true, "Remove is hidden"); + is(darkButtons.toggleDisabled.hidden, false, "Disable is visible"); + is(darkButtons.remove.hidden, true, "Remove is hidden"); + + // Disable the dark theme. + themesChanged = waitForThemeChange(list); + darkButtons.toggleDisabled.click(); + await themesChanged; + + await TestUtils.waitForCondition( + () => enabledSection.querySelectorAll("addon-card").length == 1 + ); + + // The themes are back to their starting posititons. + is(defaultTheme.parentNode, enabledSection, "Default is enabled"); + is(darkTheme.parentNode, disabledSection, "Dark is disabled"); + + await closeView(win); +}); + +add_task(async function testSideloadRemoveButton() { + const id = "sideload@mochi.test"; + mockProvider.createAddons([ + { + id, + name: "Sideloaded", + permissions: 0, + }, + ]); + + let win = await loadInitialView("extension"); + let doc = win.document; + + let card = getCardByAddonId(doc, id); + + let moreOptionsPanel = card.querySelector("panel-list"); + let moreOptionsButton = card.querySelector('[action="more-options"]'); + let panelOpened = BrowserTestUtils.waitForEvent(moreOptionsPanel, "shown"); + EventUtils.synthesizeMouseAtCenter(moreOptionsButton, {}, win); + await panelOpened; + + // Verify the remove button is visible with a SUMO link. + let removeButton = card.querySelector('[action="remove"]'); + ok(removeButton.disabled, "Remove is disabled"); + ok(!removeButton.hidden, "Remove is visible"); + + // Remove but cancel. + let prevented = BrowserTestUtils.waitForEvent(card, "remove-disabled"); + // We intentionally turn off this a11y check, because the following click + // is purposefully targeting a disabled control to confirm the click event + // won't come through. It is not meant to be interactive and is not expected + // to be accessible, therefore the rule check shall be ignored by a11y_checks. + AccessibilityUtils.setEnv({ mustHaveAccessibleRule: false }); + removeButton.click(); + AccessibilityUtils.resetEnv(); + await prevented; + + // reopen the panel + panelOpened = BrowserTestUtils.waitForEvent(moreOptionsPanel, "shown"); + EventUtils.synthesizeMouseAtCenter(moreOptionsButton, {}, win); + await panelOpened; + + let sumoLink = removeButton.querySelector("a"); + ok(sumoLink, "There's a link"); + is( + doc.l10n.getAttributes(removeButton).id, + "remove-addon-disabled-button", + "The can't remove text is shown" + ); + sumoLink.focus(); + is(doc.activeElement, sumoLink, "The link can be focused"); + + let newTabOpened = BrowserTestUtils.waitForNewTab(gBrowser, REMOVE_SUMO_URL); + sumoLink.click(); + BrowserTestUtils.removeTab(await newTabOpened); + + await closeView(win); +}); + +add_task(async function testOnlyTypeIsShown() { + let win = await loadInitialView("theme"); + let doc = win.document; + + // Find the addon-list to listen for events. + let list = doc.querySelector("addon-list"); + + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + name: "Test extension", + browser_specific_settings: { gecko: { id: "test@mochi.test" } }, + }, + useAddonManager: "temporary", + }); + + let skipped = BrowserTestUtils.waitForEvent( + list, + "skip-add", + e => e.detail == "type-mismatch" + ); + await extension.startup(); + await skipped; + + let cards = getTestCards(list); + is(cards.length, 0, "There are no test extension cards"); + + await extension.unload(); + await closeView(win); +}); + +add_task(async function testPluginIcons() { + const pluginIconUrl = "chrome://global/skin/icons/plugin.svg"; + + let win = await loadInitialView("plugin"); + let doc = win.document; + + // Check that the icons are set to the plugin icon. + let icons = doc.querySelectorAll(".card-heading-icon"); + ok(!!icons.length, "There are some plugins listed"); + + for (let icon of icons) { + is(icon.src, pluginIconUrl, "Plugins use the plugin icon"); + } + + await closeView(win); +}); + +add_task(async function testExtensionGenericIcon() { + const extensionIconUrl = + "chrome://mozapps/skin/extensions/extensionGeneric.svg"; + + let id = "test@mochi.test"; + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + name: "Test extension", + browser_specific_settings: { gecko: { id } }, + }, + useAddonManager: "temporary", + }); + await extension.startup(); + + let win = await loadInitialView("extension"); + let doc = win.document; + + let card = getCardByAddonId(doc, id); + let icon = card.querySelector(".addon-icon"); + is(icon.src, extensionIconUrl, "Extensions without icon use the generic one"); + + await extension.unload(); + await closeView(win); +}); + +add_task(async function testSectionHeadingKeys() { + mockProvider.createAddons([ + { + id: "test-theme", + name: "Test Theme", + type: "theme", + }, + { + id: "test-extension-disabled", + name: "Test Disabled Extension", + type: "extension", + userDisabled: true, + }, + { + id: "test-plugin-disabled", + name: "Test Disabled Plugin", + type: "plugin", + userDisabled: true, + }, + { + id: "test-locale", + name: "Test Enabled Locale", + type: "locale", + }, + { + id: "test-locale-disabled", + name: "Test Disabled Locale", + type: "locale", + userDisabled: true, + }, + { + id: "test-dictionary", + name: "Test Enabled Dictionary", + type: "dictionary", + }, + { + id: "test-dictionary-disabled", + name: "Test Disabled Dictionary", + type: "dictionary", + userDisabled: true, + }, + { + id: "test-sitepermission", + name: "Test Enabled Site Permission", + type: "sitepermission", + }, + { + id: "test-sitepermission-disabled", + name: "Test Disabled Site Permission", + type: "sitepermission", + userDisabled: true, + }, + ]); + + for (let type of [ + "extension", + "theme", + "plugin", + "locale", + "dictionary", + "sitepermission", + ]) { + info(`loading view for addon type ${type}`); + let win = await loadInitialView(type); + let doc = win.document; + + for (let status of ["enabled", "disabled"]) { + let section = getSection(doc, `${type}-${status}-section`); + let el = section?.querySelector(".list-section-heading"); + isnot(el, null, `Should have ${status} heading for ${type} section`); + is( + el && doc.l10n.getAttributes(el).id, + win.getL10nIdMapping(`${type}-${status}-heading`), + `Should have correct ${status} heading for ${type} section` + ); + } + + await closeView(win); + } +}); + +add_task(async function testDisabledDimming() { + const id = "disabled@mochi.test"; + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + name: "Disable me", + browser_specific_settings: { gecko: { id } }, + }, + useAddonManager: "temporary", + }); + await extension.startup(); + + let addon = await AddonManager.getAddonByID(id); + + let win = await loadInitialView("extension"); + let doc = win.document; + let pageHeader = doc.querySelector("addon-page-header"); + + // We intentionally turn off this a11y check, because the following click + // is purposefully targeting a non-interactive element to clear the focused + // state with a mouse which can be done by assistive technology and keyboard + // by pressing `Esc`, this rule check shall be ignored by a11y_checks suite. + AccessibilityUtils.setEnv({ mustHaveAccessibleRule: false }); + // Ensure there's no focus on the list. + EventUtils.synthesizeMouseAtCenter(pageHeader, {}, win); + AccessibilityUtils.resetEnv(); + + const checkOpacity = (card, expected, msg) => { + let { opacity } = card.ownerGlobal.getComputedStyle(card.firstElementChild); + let normalize = val => Math.floor(val * 10); + is(normalize(opacity), normalize(expected), msg); + }; + const waitForTransition = card => + BrowserTestUtils.waitForEvent( + card.firstElementChild, + "transitionend", + /* capture = */ false, + e => e.propertyName === "opacity" && e.target.classList.contains("card") + ); + + let card = getCardByAddonId(doc, id); + checkOpacity(card, "1", "The opacity is 1 when enabled"); + + // Disable the add-on, check again. + let list = doc.querySelector("addon-list"); + let moved = BrowserTestUtils.waitForEvent(list, "move"); + await addon.disable(); + await moved; + + let disabledSection = getSection(doc, "extension-disabled-section"); + is(card.parentNode, disabledSection, "The card is in the disabled section"); + checkOpacity(card, "0.6", "The opacity is dimmed when disabled"); + + // Click on the menu button, this should un-dim the card. + let transitionEnded = waitForTransition(card); + let moreOptionsButton = card.querySelector(".more-options-button"); + EventUtils.synthesizeMouseAtCenter(moreOptionsButton, {}, win); + await transitionEnded; + checkOpacity(card, "1", "The opacity is 1 when the menu is open"); + + // Close the menu, opacity should return. + transitionEnded = waitForTransition(card); + // We intentionally turn off this a11y check, because the following click + // is purposefully targeting a non-interactive element to dismiss the opened + // menu with a mouse which can be done by assistive technology and keyboard + // by pressing `Esc`, this rule check shall be ignored by a11y_checks suite. + AccessibilityUtils.setEnv({ mustHaveAccessibleRule: false }); + EventUtils.synthesizeMouseAtCenter(pageHeader, {}, win); + AccessibilityUtils.resetEnv(); + await transitionEnded; + checkOpacity(card, "0.6", "The card is dimmed again"); + + await closeView(win); + await extension.unload(); +}); + +add_task(async function testEmptyMessage() { + let tests = [ + { + type: "extension", + message: "Get extensions and themes on ", + }, + { + type: "theme", + message: "Get extensions and themes on ", + }, + { + type: "plugin", + message: "Get extensions and themes on ", + }, + { + type: "locale", + message: "Get language packs on ", + }, + { + type: "dictionary", + message: "Get dictionaries on ", + }, + ]; + + for (let test of tests) { + let win = await loadInitialView(test.type); + let doc = win.document; + let enabledSection = getSection(doc, `${test.type}-enabled-section`); + let disabledSection = getSection(doc, `${test.type}-disabled-section`); + const message = doc.querySelector("#empty-addons-message"); + + // Test if the correct locale has been applied. + ok( + message.textContent.startsWith(test.message), + `View ${test.type} has correct empty list message` + ); + + // With at least one enabled/disabled add-on (see testSectionHeadingKeys), + // the message is hidden. + is_element_hidden(message, "Empty addons message hidden"); + + // The test runner (Mochitest) relies on add-ons that should not be removed. + // Simulate the scenario of zero add-ons by clearing all rendered sections. + while (enabledSection.firstChild) { + enabledSection.firstChild.remove(); + } + + while (disabledSection.firstChild) { + disabledSection.firstChild.remove(); + } + + // Message should now be displayed + is_element_visible(message, "Empty addons message visible"); + + await closeView(win); + } +}); |