From 6bf0a5cb5034a7e684dcc3500e841785237ce2dd Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Sun, 7 Apr 2024 19:32:43 +0200 Subject: Adding upstream version 1:115.7.0. Signed-off-by: Daniel Baumann --- .../test/browser/browser_html_detail_view.js | 1198 ++++++++++++++++++++ 1 file changed, 1198 insertions(+) create mode 100644 toolkit/mozapps/extensions/test/browser/browser_html_detail_view.js (limited to 'toolkit/mozapps/extensions/test/browser/browser_html_detail_view.js') diff --git a/toolkit/mozapps/extensions/test/browser/browser_html_detail_view.js b/toolkit/mozapps/extensions/test/browser/browser_html_detail_view.js new file mode 100644 index 0000000000..a466b5c1a3 --- /dev/null +++ b/toolkit/mozapps/extensions/test/browser/browser_html_detail_view.js @@ -0,0 +1,1198 @@ +/* eslint max-len: ["error", 80] */ + +"use strict"; + +const { AddonTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/AddonTestUtils.sys.mjs" +); + +const { ExtensionPermissions } = ChromeUtils.importESModule( + "resource://gre/modules/ExtensionPermissions.sys.mjs" +); + +const SUPPORT_URL = Services.urlFormatter.formatURL( + Services.prefs.getStringPref("app.support.baseURL") +); +const PB_SUMO_URL = SUPPORT_URL + "extensions-pb"; +const DEFAULT_THEME_ID = "default-theme@mozilla.org"; +const DARK_THEME_ID = "firefox-compact-dark@mozilla.org"; + +let gProvider; +let promptService; + +AddonTestUtils.initMochitest(this); + +function getDetailRows(card) { + return Array.from( + card.querySelectorAll('[name="details"] .addon-detail-row:not([hidden])') + ); +} + +function checkLabel(row, name) { + let id; + if (name == "private-browsing") { + // This id is carried over from the old about:addons. + id = "detail-private-browsing-label"; + } else { + id = `addon-detail-${name}-label`; + } + is( + row.ownerDocument.l10n.getAttributes(row.querySelector("label")).id, + id, + `The ${name} label is set` + ); +} + +function formatUrl(contentAttribute, url) { + let parsedUrl = new URL(url); + parsedUrl.searchParams.set("utm_source", "firefox-browser"); + parsedUrl.searchParams.set("utm_medium", "firefox-browser"); + parsedUrl.searchParams.set("utm_content", contentAttribute); + return parsedUrl.href; +} + +function checkLink(link, url, text = url) { + ok(link, "There is a link"); + is(link.href, url, "The link goes to the URL"); + if (text instanceof Object) { + // Check the fluent data. + Assert.deepEqual( + link.ownerDocument.l10n.getAttributes(link), + text, + "The fluent data is set correctly" + ); + } else { + // Just check text. + is(link.textContent, text, "The text is set"); + } + is(link.getAttribute("target"), "_blank", "The link opens in a new tab"); +} + +function checkOptions(doc, options, expectedOptions) { + let numOptions = expectedOptions.length; + is(options.length, numOptions, `There are ${numOptions} options`); + for (let i = 0; i < numOptions; i++) { + let option = options[i]; + is(option.children.length, 2, "There are 2 children for the option"); + let input = option.firstElementChild; + is(input.tagName, "INPUT", "The input is first"); + let text = option.lastElementChild; + is(text.tagName, "SPAN", "The label text is second"); + let expected = expectedOptions[i]; + is(input.value, expected.value, "The value is right"); + is(input.checked, expected.checked, "The checked property is correct"); + Assert.deepEqual( + doc.l10n.getAttributes(text), + { id: expected.label, args: null }, + "The label has the right text" + ); + } +} + +function assertDeckHeadingHidden(group) { + ok(group.hidden, "The tab group is hidden"); + let buttons = group.querySelectorAll(".tab-button"); + for (let button of buttons) { + ok(button.offsetHeight == 0, `The ${button.name} is hidden`); + } +} + +function assertDeckHeadingButtons(group, visibleButtons) { + ok(!group.hidden, "The tab group is shown"); + let buttons = group.querySelectorAll(".tab-button"); + ok( + buttons.length >= visibleButtons.length, + `There should be at least ${visibleButtons.length} buttons` + ); + for (let button of buttons) { + if (visibleButtons.includes(button.name)) { + ok(!button.hidden, `The ${button.name} is shown`); + } else { + ok(button.hidden, `The ${button.name} is hidden`); + } + } +} + +async function hasPrivateAllowed(id) { + let perms = await ExtensionPermissions.get(id); + return perms.permissions.includes("internal:privateBrowsingAllowed"); +} + +async function assertBackButtonIsDisabled(win) { + let backButton = await BrowserTestUtils.waitForCondition(async () => { + let backButton = win.document.querySelector(".back-button"); + + // Wait until the button is visible in the page. + return backButton && !backButton.hidden ? backButton : false; + }); + + ok(backButton, "back button is rendered"); + ok(backButton.disabled, "back button is disabled"); +} + +add_setup(async function enableHtmlViews() { + gProvider = new MockProvider(["extension", "sitepermission"]); + gProvider.createAddons([ + { + id: "addon1@mochi.test", + name: "Test add-on 1", + creator: { name: "The creator", url: "http://addons.mozilla.org/me" }, + version: "3.1", + description: "Short description", + fullDescription: "Longer description\nWith brs!", + type: "extension", + contributionURL: "http://example.com/contribute", + averageRating: 4.279, + userPermissions: { + origins: ["", "file://*/*"], + permissions: ["alarms", "contextMenus", "tabs", "webNavigation"], + }, + reviewCount: 5, + reviewURL: "http://addons.mozilla.org/reviews", + homepageURL: "http://example.com/addon1", + updateDate: new Date("2019-03-07T01:00:00"), + applyBackgroundUpdates: AddonManager.AUTOUPDATE_ENABLE, + }, + { + id: "addon2@mochi.test", + name: "Test add-on 2", + creator: { name: "I made it" }, + description: "Short description", + userPermissions: { + origins: [], + permissions: ["alarms", "contextMenus"], + }, + type: "extension", + }, + { + id: "addon3@mochi.test", + name: "Test add-on 3", + creator: { name: "Look a super long description" }, + description: "Short description", + fullDescription: "Mozilla\n".repeat(100), + userPermissions: { + origins: [], + permissions: ["alarms", "contextMenus"], + }, + type: "extension", + contributionURL: "http://example.com/contribute", + updateDate: new Date("2022-03-07T01:00:00"), + }, + { + // NOTE: Keep the mock properties in sync with the one that + // SitePermsAddonWrapper would be providing in real synthetic + // addon entries managed by the SitePermsAddonProvider. + id: "sitepermission@mochi.test", + version: "2.0", + name: "Test site permission add-on", + description: "permission description", + fullDescription: "detailed description", + siteOrigin: "http://mochi.test", + sitePermissions: ["midi"], + type: "sitepermission", + permissions: AddonManager.PERM_CAN_UNINSTALL, + }, + { + id: "theme1@mochi.test", + name: "Test theme", + creator: { name: "Artist", url: "http://example.com/artist" }, + description: "A nice tree", + type: "theme", + screenshots: [ + { + url: "http://example.com/preview-wide.png", + width: 760, + height: 92, + }, + { + url: "http://example.com/preview.png", + width: 680, + height: 92, + }, + ], + }, + ]); + + promptService = mockPromptService(); +}); + +add_task(async function testOpenDetailView() { + let id = "test@mochi.test"; + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + name: "Test", + browser_specific_settings: { gecko: { id } }, + }, + useAddonManager: "temporary", + }); + let id2 = "test2@mochi.test"; + let extension2 = ExtensionTestUtils.loadExtension({ + manifest: { + name: "Test", + browser_specific_settings: { gecko: { id: id2 } }, + }, + useAddonManager: "temporary", + }); + + await extension.startup(); + await extension2.startup(); + + const goBack = async win => { + let loaded = waitForViewLoad(win); + let backButton = win.document.querySelector(".back-button"); + ok(!backButton.disabled, "back button is enabled"); + backButton.click(); + await loaded; + }; + + let win = await loadInitialView("extension"); + + // Test click on card to open details. + let card = getAddonCard(win, id); + ok(!card.querySelector("addon-details"), "The card doesn't have details"); + let loaded = waitForViewLoad(win); + EventUtils.synthesizeMouseAtCenter(card, { clickCount: 1 }, win); + await loaded; + + card = getAddonCard(win, id); + ok(card.querySelector("addon-details"), "The card now has details"); + + await goBack(win); + + // Test using more options menu. + card = getAddonCard(win, id); + loaded = waitForViewLoad(win); + card.querySelector('[action="expand"]').click(); + await loaded; + + card = getAddonCard(win, id); + ok(card.querySelector("addon-details"), "The card now has details"); + + await goBack(win); + + card = getAddonCard(win, id2); + loaded = waitForViewLoad(win); + card.querySelector('[action="expand"]').click(); + await loaded; + + await goBack(win); + + // Test click on add-on name. + card = getAddonCard(win, id2); + ok(!card.querySelector("addon-details"), "The card isn't expanded"); + let addonName = card.querySelector(".addon-name"); + loaded = waitForViewLoad(win); + EventUtils.synthesizeMouseAtCenter(addonName, {}, win); + await loaded; + card = getAddonCard(win, id2); + ok(card.querySelector("addon-details"), "The card is expanded"); + + await closeView(win); + await extension.unload(); + await extension2.unload(); +}); + +add_task(async function testDetailOperations() { + let id = "test@mochi.test"; + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + name: "Test", + browser_specific_settings: { gecko: { id } }, + }, + useAddonManager: "temporary", + }); + + await extension.startup(); + + let win = await loadInitialView("extension"); + let doc = win.document; + + let card = getAddonCard(win, id); + ok(!card.querySelector("addon-details"), "The card doesn't have details"); + let loaded = waitForViewLoad(win); + EventUtils.synthesizeMouseAtCenter(card, { clickCount: 1 }, win); + await loaded; + + card = getAddonCard(win, id); + let panel = card.querySelector("panel-list"); + + // Check button visibility. + let disableButton = card.querySelector('[action="toggle-disabled"]'); + ok(!disableButton.hidden, "The disable button is visible"); + + let removeButton = panel.querySelector('[action="remove"]'); + ok(!removeButton.hidden, "The remove button is visible"); + + let separator = panel.querySelector("hr:last-of-type"); + ok(separator.hidden, "The separator is hidden"); + + let expandButton = panel.querySelector('[action="expand"]'); + ok(expandButton.hidden, "The expand button is hidden"); + + // Check toggling disabled. + let name = card.addonNameEl; + is(name.textContent, "Test", "The name is set when enabled"); + is(doc.l10n.getAttributes(name).id, null, "There is no l10n name"); + + // Disable the extension. + let disableToggled = BrowserTestUtils.waitForEvent(card, "update"); + disableButton.click(); + await disableToggled; + + // The (disabled) text should be shown now. + Assert.deepEqual( + doc.l10n.getAttributes(name), + { id: "addon-name-disabled", args: { name: "Test" } }, + "The name is updated to the disabled text" + ); + + // Enable the add-on. + let extensionStarted = AddonTestUtils.promiseWebExtensionStartup(id); + disableToggled = BrowserTestUtils.waitForEvent(card, "update"); + disableButton.click(); + await Promise.all([disableToggled, extensionStarted]); + + // Name is just the add-on name again. + is(name.textContent, "Test", "The name is reset when enabled"); + is(doc.l10n.getAttributes(name).id, null, "There is no l10n name"); + + // Remove but cancel. + let cancelled = BrowserTestUtils.waitForEvent(card, "remove-cancelled"); + removeButton.click(); + await cancelled; + + // Remove the extension. + let viewChanged = waitForViewLoad(win); + // Tell the mock prompt service that the prompt was accepted. + promptService._response = 0; + removeButton.click(); + await viewChanged; + + // We're on the list view now and there's no card for this extension. + const addonList = doc.querySelector("addon-list"); + ok(addonList, "There's an addon-list now"); + ok(!getAddonCard(win, id), "The extension no longer has a card"); + let 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(addonList, 1); + assertHasPendingUninstallAddon(addonList, addon); + + extensionStarted = AddonTestUtils.promiseWebExtensionStartup(addon.id); + await testUndoPendingUninstall(addonList, addon); + info("Wait for the pending uninstall addon complete restart"); + await extensionStarted; + + card = getAddonCard(win, addon.id); + ok(card, "Addon card rendered after clicking pending uninstall undo button"); + + await closeView(win); + await extension.unload(); +}); + +add_task(async function testFullDetails() { + let id = "addon1@mochi.test"; + let headingId = "addon1_mochi_test-heading"; + let win = await loadInitialView("extension"); + let doc = win.document; + + // The list card. + let card = getAddonCard(win, id); + ok(!card.hasAttribute("expanded"), "The list card is not expanded"); + + // Make sure the preview is hidden. + let preview = card.querySelector(".card-heading-image"); + ok(preview, "There is a preview"); + is(preview.hidden, true, "The preview is hidden"); + + let loaded = waitForViewLoad(win); + card.querySelector('[action="expand"]').click(); + await loaded; + + // This is now the detail card. + card = getAddonCard(win, id); + ok(card.hasAttribute("expanded"), "The detail card is expanded"); + + let cardHeading = card.querySelector("h1"); + is(cardHeading.textContent, "Test add-on 1", "Card heading is set"); + is(cardHeading.id, headingId, "Heading has correct id"); + is( + card.querySelector(".card").getAttribute("aria-labelledby"), + headingId, + "Card is labelled by the heading" + ); + + // Make sure the preview is hidden. + preview = card.querySelector(".card-heading-image"); + ok(preview, "There is a preview"); + is(preview.hidden, true, "The preview is hidden"); + + let details = card.querySelector("addon-details"); + + // Check all the deck buttons are hidden. + assertDeckHeadingButtons(details.tabGroup, ["details", "permissions"]); + + let desc = details.querySelector(".addon-detail-description"); + is( + desc.innerHTML, + "Longer description
With brs!", + "The full description replaces newlines with
" + ); + + let sitepermissionsRow = details.querySelector( + ".addon-detail-sitepermissions" + ); + is( + sitepermissionsRow.hidden, + true, + "AddonSitePermissionsList should be hidden for this addon type" + ); + + // Check the show more button is not there + const showMoreBtn = card.querySelector(".addon-detail-description-toggle"); + ok(showMoreBtn.hidden, "The show more button is not visible"); + + let contrib = details.querySelector(".addon-detail-contribute"); + ok(contrib, "The contribution section is visible"); + + let waitForTab = BrowserTestUtils.waitForNewTab( + gBrowser, + "http://example.com/contribute" + ); + contrib.querySelector("button").click(); + BrowserTestUtils.removeTab(await waitForTab); + + let rows = getDetailRows(card); + + // Auto updates. + let row = rows.shift(); + checkLabel(row, "updates"); + let expectedOptions = [ + { value: "1", label: "addon-detail-updates-radio-default", checked: false }, + { value: "2", label: "addon-detail-updates-radio-on", checked: true }, + { value: "0", label: "addon-detail-updates-radio-off", checked: false }, + ]; + let options = row.lastElementChild.querySelectorAll("label"); + checkOptions(doc, options, expectedOptions); + + // Private browsing, functionality checked in another test. + row = rows.shift(); + checkLabel(row, "private-browsing"); + + // Private browsing help text. + row = rows.shift(); + ok(row.classList.contains("addon-detail-help-row"), "There's a help row"); + ok(!row.hidden, "The help row is shown"); + is( + doc.l10n.getAttributes(row).id, + "addon-detail-private-browsing-help", + "The help row is for private browsing" + ); + + // Author. + row = rows.shift(); + checkLabel(row, "author"); + let link = row.querySelector("a"); + let authorLink = formatUrl( + "addons-manager-user-profile-link", + "http://addons.mozilla.org/me" + ); + checkLink(link, authorLink, "The creator"); + + // Version. + row = rows.shift(); + checkLabel(row, "version"); + let text = row.lastChild; + is(text.textContent, "3.1", "The version is set"); + + // Last updated. + row = rows.shift(); + checkLabel(row, "last-updated"); + text = row.lastChild; + is(text.textContent, "March 7, 2019", "The last updated date is set"); + + // Homepage. + row = rows.shift(); + checkLabel(row, "homepage"); + link = row.querySelector("a"); + checkLink(link, "http://example.com/addon1"); + + // Reviews. + row = rows.shift(); + checkLabel(row, "rating"); + let rating = row.lastElementChild; + ok(rating.classList.contains("addon-detail-rating"), "Found the rating el"); + let starsElem = rating.querySelector("five-star-rating"); + is(starsElem.rating, 4.279, "Exact rating used for calculations"); + let stars = Array.from(starsElem.shadowRoot.querySelectorAll(".rating-star")); + let fullAttrs = stars.map(star => star.getAttribute("fill")).join(","); + is(fullAttrs, "full,full,full,full,half", "Four and a half stars are full"); + link = rating.querySelector("a"); + let reviewsLink = formatUrl( + "addons-manager-reviews-link", + "http://addons.mozilla.org/reviews" + ); + checkLink(link, reviewsLink, { + id: "addon-detail-reviews-link", + args: { numberOfReviews: 5 }, + }); + + // While we are here, let's test edge cases of star ratings. + async function testRating(rating, ratingRounded, expectation) { + starsElem.rating = rating; + await starsElem.ownerDocument.l10n.translateElements([starsElem]); + is( + starsElem.ratingBuckets.join(","), + expectation, + `Rendering of rating ${rating}` + ); + + is( + starsElem.title, + `Rated ${ratingRounded} out of 5`, + "Rendered title must contain at most one fractional digit" + ); + } + await testRating(0.0, "0", "empty,empty,empty,empty,empty"); + await testRating(0.123, "0.1", "empty,empty,empty,empty,empty"); + await testRating(0.249, "0.2", "empty,empty,empty,empty,empty"); + await testRating(0.25, "0.3", "half,empty,empty,empty,empty"); + await testRating(0.749, "0.7", "half,empty,empty,empty,empty"); + await testRating(0.75, "0.8", "full,empty,empty,empty,empty"); + await testRating(1.0, "1", "full,empty,empty,empty,empty"); + await testRating(4.249, "4.2", "full,full,full,full,empty"); + await testRating(4.25, "4.3", "full,full,full,full,half"); + await testRating(4.749, "4.7", "full,full,full,full,half"); + await testRating(5.0, "5", "full,full,full,full,full"); + + // That should've been all the rows. + is(rows.length, 0, "There are no more rows left"); + + await closeView(win); +}); + +add_task(async function testFullDetailsShowMoreButton() { + const id = "addon3@mochi.test"; + const win = await loadInitialView("extension"); + + // The list card. + let card = getAddonCard(win, id); + const loaded = waitForViewLoad(win); + card.querySelector('[action="expand"]').click(); + await loaded; + + // This is now the detail card. + card = getAddonCard(win, id); + + // Check the show more button is there + const showMoreBtn = card.querySelector(".addon-detail-description-toggle"); + ok(!showMoreBtn.hidden, "The show more button is visible"); + + const descriptionWrapper = card.querySelector( + ".addon-detail-description-wrapper" + ); + ok( + descriptionWrapper.classList.contains("addon-detail-description-collapse"), + "The long description is collapsed" + ); + + // After click the description should be expanded + showMoreBtn.click(); + ok( + !descriptionWrapper.classList.contains("addon-detail-description-collapse"), + "The long description is expanded" + ); + + await closeView(win); +}); + +add_task(async function testMinimalExtension() { + let win = await loadInitialView("extension"); + let doc = win.document; + + let card = getAddonCard(win, "addon2@mochi.test"); + ok(!card.hasAttribute("expanded"), "The list card is not expanded"); + let loaded = waitForViewLoad(win); + card.querySelector('[action="expand"]').click(); + await loaded; + + card = getAddonCard(win, "addon2@mochi.test"); + let details = card.querySelector("addon-details"); + + // Check all the deck buttons are hidden. + assertDeckHeadingButtons(details.tabGroup, ["details", "permissions"]); + + let desc = details.querySelector(".addon-detail-description"); + is(desc.textContent, "", "There is no full description"); + + let contrib = details.querySelector(".addon-detail-contribute"); + ok(contrib.hidden, "The contribution element is hidden"); + + let rows = getDetailRows(card); + + // Automatic updates. + let row = rows.shift(); + checkLabel(row, "updates"); + + // Private browsing settings. + row = rows.shift(); + checkLabel(row, "private-browsing"); + + // Private browsing help text. + row = rows.shift(); + ok(row.classList.contains("addon-detail-help-row"), "There's a help row"); + ok(!row.hidden, "The help row is shown"); + is( + doc.l10n.getAttributes(row).id, + "addon-detail-private-browsing-help", + "The help row is for private browsing" + ); + + // Author. + row = rows.shift(); + checkLabel(row, "author"); + let text = row.lastChild; + is(text.textContent, "I made it", "The author is set"); + ok(Text.isInstance(text), "The author is a text node"); + + is(rows.length, 0, "There are no more rows"); + + await closeView(win); +}); + +add_task(async function testDefaultTheme() { + let win = await loadInitialView("theme"); + + // The list card. + let card = getAddonCard(win, DEFAULT_THEME_ID); + ok(!card.hasAttribute("expanded"), "The list card is not expanded"); + + let preview = card.querySelector(".card-heading-image"); + ok(preview, "There is a preview"); + ok(!preview.hidden, "The preview is visible"); + + let loaded = waitForViewLoad(win); + card.querySelector('[action="expand"]').click(); + await loaded; + + card = getAddonCard(win, DEFAULT_THEME_ID); + + preview = card.querySelector(".card-heading-image"); + ok(preview, "There is a preview"); + ok(!preview.hidden, "The preview is visible"); + + // Check all the deck buttons are hidden. + assertDeckHeadingHidden(card.details.tabGroup); + + let rows = getDetailRows(card); + + // Author. + let author = rows.shift(); + checkLabel(author, "author"); + let text = author.lastChild; + is(text.textContent, "Mozilla", "The author is set"); + + // Version. + let version = rows.shift(); + checkLabel(version, "version"); + is(version.lastChild.textContent, "1.3", "It's always version 1.3"); + + // Last updated. + let lastUpdated = rows.shift(); + checkLabel(lastUpdated, "last-updated"); + let dateText = lastUpdated.lastChild.textContent; + ok(dateText, "There is a date set"); + ok(!dateText.includes("Invalid Date"), `"${dateText}" should be a date`); + + is(rows.length, 0, "There are no more rows"); + + await closeView(win); +}); + +add_task(async function testStaticTheme() { + let win = await loadInitialView("theme"); + + // The list card. + let card = getAddonCard(win, "theme1@mochi.test"); + ok(!card.hasAttribute("expanded"), "The list card is not expanded"); + + // Make sure the preview is set. + let preview = card.querySelector(".card-heading-image"); + ok(preview, "There is a preview"); + is(preview.src, "http://example.com/preview.png", "The preview URL is set"); + is(preview.width, 664, "The width is set"); + is(preview.height, 90, "The height is set"); + is(preview.hidden, false, "The preview is visible"); + + // Load the detail view. + let loaded = waitForViewLoad(win); + card.querySelector('[action="expand"]').click(); + await loaded; + + card = getAddonCard(win, "theme1@mochi.test"); + + // Make sure the preview is still set. + preview = card.querySelector(".card-heading-image"); + ok(preview, "There is a preview"); + is(preview.src, "http://example.com/preview.png", "The preview URL is set"); + is(preview.width, 664, "The width is set"); + is(preview.height, 90, "The height is set"); + is(preview.hidden, false, "The preview is visible"); + + // Check all the deck buttons are hidden. + assertDeckHeadingHidden(card.details.tabGroup); + + let rows = getDetailRows(card); + + // Automatic updates. + let row = rows.shift(); + checkLabel(row, "updates"); + + // Author. + let author = rows.shift(); + checkLabel(author, "author"); + let text = author.lastElementChild; + is(text.textContent, "Artist", "The author is set"); + + is(rows.length, 0, "There was only 1 row"); + + await closeView(win); +}); + +add_task(async function testSitePermission() { + let win = await loadInitialView("sitepermission"); + + // The list card. + let card = getAddonCard(win, "sitepermission@mochi.test"); + ok(!card.hasAttribute("expanded"), "The list card is not expanded"); + + // Load the detail view. + let loaded = waitForViewLoad(win); + card.querySelector('[action="expand"]').click(); + await loaded; + + card = getAddonCard(win, "sitepermission@mochi.test"); + + // Check all the deck buttons are hidden. + assertDeckHeadingHidden(card.details.tabGroup); + + let sitepermissionsRow = card.querySelector(".addon-detail-sitepermissions"); + is( + BrowserTestUtils.is_visible(sitepermissionsRow), + true, + "AddonSitePermissionsList should be visible for this addon type" + ); + + let [versionRow, ...restRows] = getDetailRows(card); + checkLabel(versionRow, "version"); + + Assert.deepEqual( + restRows.map(row => row.getAttribute("class")), + [], + "All other details row are hidden as expected" + ); + + let permissions = Array.from( + card.querySelectorAll(".addon-permissions-list .permission-info") + ); + is(permissions.length, 1, "a permission is listed"); + is(permissions[0].textContent, "Access MIDI devices", "got midi permission"); + + await closeView(win); +}); + +add_task(async function testPrivateBrowsingExtension() { + let id = "pb@mochi.test"; + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + name: "My PB extension", + browser_specific_settings: { gecko: { id } }, + }, + useAddonManager: "permanent", + }); + + await extension.startup(); + + let win = await loadInitialView("extension"); + let doc = win.document; + + // The add-on shouldn't show that it's allowed yet. + let card = getAddonCard(win, id); + let badge = card.querySelector(".addon-badge-private-browsing-allowed"); + ok(badge.hidden, "The PB badge is hidden initially"); + ok(!(await hasPrivateAllowed(id)), "PB is not allowed"); + + // Load the detail view. + let loaded = waitForViewLoad(win); + card.querySelector('[action="expand"]').click(); + await loaded; + + // The badge is still hidden on the detail view. + card = getAddonCard(win, id); + badge = card.querySelector(".addon-badge-private-browsing-allowed"); + ok(badge.hidden, "The PB badge is hidden on the detail view"); + ok(!(await hasPrivateAllowed(id)), "PB is not allowed"); + + let pbRow = card.querySelector(".addon-detail-row-private-browsing"); + let name = card.querySelector(".addon-name"); + + // Allow private browsing. + let [allow, disallow] = pbRow.querySelectorAll("input"); + let updated = BrowserTestUtils.waitForEvent(card, "update"); + + // Check that the disabled state isn't shown while reloading the add-on. + let addonDisabled = AddonTestUtils.promiseAddonEvent("onDisabled"); + allow.click(); + await addonDisabled; + is( + doc.l10n.getAttributes(name).id, + null, + "The disabled message is not shown for the add-on" + ); + + // Check the PB stuff. + await updated; + + // Not sure what better to await here. + await TestUtils.waitForCondition(() => !badge.hidden); + + ok(!badge.hidden, "The PB badge is now shown"); + ok(await hasPrivateAllowed(id), "PB is allowed"); + is( + doc.l10n.getAttributes(name).id, + null, + "The disabled message is not shown for the add-on" + ); + + info("Verify the badge links to the support page"); + let tabOpened = BrowserTestUtils.waitForNewTab(gBrowser, PB_SUMO_URL); + EventUtils.synthesizeMouseAtCenter(badge, {}, win); + let tab = await tabOpened; + BrowserTestUtils.removeTab(tab); + + // Disable the add-on and change the value. + updated = BrowserTestUtils.waitForEvent(card, "update"); + card.querySelector('[action="toggle-disabled"]').click(); + await updated; + + // It's still allowed in PB. + ok(await hasPrivateAllowed(id), "PB is allowed"); + ok(!badge.hidden, "The PB badge is shown"); + + // Disallow PB. + updated = BrowserTestUtils.waitForEvent(card, "update"); + disallow.click(); + await updated; + + ok(badge.hidden, "The PB badge is hidden"); + ok(!(await hasPrivateAllowed(id)), "PB is disallowed"); + + // Allow PB. + updated = BrowserTestUtils.waitForEvent(card, "update"); + allow.click(); + await updated; + + ok(!badge.hidden, "The PB badge is hidden"); + ok(await hasPrivateAllowed(id), "PB is disallowed"); + + await closeView(win); + await extension.unload(); +}); + +add_task(async function testInvalidExtension() { + let win = await open_manager("addons://detail/foo"); + let categoryUtils = new CategoryUtilities(win); + is( + categoryUtils.selectedCategory, + "discover", + "Should fall back to the discovery pane" + ); + + ok(!gBrowser.canGoBack, "The view has been replaced"); + + await close_manager(win); +}); + +add_task(async function testInvalidExtensionNoDiscover() { + await SpecialPowers.pushPrefEnv({ + set: [["extensions.getAddons.showPane", false]], + }); + + let win = await open_manager("addons://detail/foo"); + let categoryUtils = new CategoryUtilities(win); + is( + categoryUtils.selectedCategory, + "extension", + "Should fall back to the extension list if discover is disabled" + ); + + ok(!gBrowser.canGoBack, "The view has been replaced"); + + await close_manager(win); + await SpecialPowers.popPrefEnv(); +}); + +add_task(async function testExternalUninstall() { + let id = "remove@mochi.test"; + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + name: "Remove 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; + + // Load the detail view. + let card = doc.querySelector(`addon-card[addon-id="${id}"]`); + let detailsLoaded = waitForViewLoad(win); + card.querySelector('[action="expand"]').click(); + await detailsLoaded; + + // Uninstall the add-on with undo. Should go to extension list. + let listLoaded = waitForViewLoad(win); + await addon.uninstall(true); + await listLoaded; + + // Verify the list view was loaded and the card is gone. + let list = doc.querySelector("addon-list"); + ok(list, "Moved to a list page"); + is(list.type, "extension", "We're on the extension list page"); + card = list.querySelector(`addon-card[addon-id="${id}"]`); + ok(!card, "The card has been removed"); + + await extension.unload(); + closeView(win); +}); + +add_task(async function testExternalThemeUninstall() { + let id = "remove-theme@mochi.test"; + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + browser_specific_settings: { gecko: { id } }, + name: "Remove theme", + theme: {}, + }, + useAddonManager: "temporary", + }); + await extension.startup(); + let addon = await AddonManager.getAddonByID(id); + + let win = await loadInitialView("theme"); + let doc = win.document; + + // Load the detail view. + let card = doc.querySelector(`addon-card[addon-id="${id}"]`); + let detailsLoaded = waitForViewLoad(win); + card.querySelector('[action="expand"]').click(); + await detailsLoaded; + + // Uninstall the add-on without undo. Should go to theme list. + let listLoaded = waitForViewLoad(win); + await addon.uninstall(); + await listLoaded; + + // Verify the list view was loaded and the card is gone. + let list = doc.querySelector("addon-list"); + ok(list, "Moved to a list page"); + is(list.type, "theme", "We're on the theme list page"); + card = list.querySelector(`addon-card[addon-id="${id}"]`); + ok(!card, "The card has been removed"); + + await extension.unload(); + closeView(win); +}); + +add_task(async function testPrivateBrowsingAllowedListView() { + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + name: "Allowed PB extension", + browser_specific_settings: { gecko: { id: "allowed@mochi.test" } }, + }, + useAddonManager: "permanent", + }); + + await extension.startup(); + let perms = { permissions: ["internal:privateBrowsingAllowed"], origins: [] }; + await ExtensionPermissions.add("allowed@mochi.test", perms); + let addon = await AddonManager.getAddonByID("allowed@mochi.test"); + await addon.reload(); + + let win = await loadInitialView("extension"); + + // The allowed extension should have a badge on load. + let card = getAddonCard(win, "allowed@mochi.test"); + let badge = card.querySelector(".addon-badge-private-browsing-allowed"); + ok(!badge.hidden, "The PB badge is shown for the allowed add-on"); + + await extension.unload(); + await closeView(win); +}); + +// When the back button is used, its disabled state will be updated. If it +// isn't updated when showing a view, then it will be disabled on the next +// use (bug 1551213) if the last use caused it to become disabled. +add_task(async function testGoBackButton() { + // Make sure the list view is the first loaded view so you cannot go back. + Services.prefs.setCharPref(PREF_UI_LASTCATEGORY, "addons://list/extension"); + + let id = "addon1@mochi.test"; + let win = await loadInitialView("extension"); + let doc = win.document; + let backButton = doc.querySelector(".back-button"); + + let loadDetailView = () => { + let loaded = waitForViewLoad(win); + getAddonCard(win, id).querySelector("[action=expand]").click(); + return loaded; + }; + + let checkBackButtonState = () => { + is_element_visible(backButton, "Back button is visible on the detail page"); + ok(!backButton.disabled, "Back button is enabled"); + }; + + // Load the detail view, first time should be fine. + await loadDetailView(); + checkBackButtonState(); + + // Use the back button directly to pop from history and trigger its disabled + // state to be updated. + let loaded = waitForViewLoad(win); + backButton.click(); + await loaded; + + await loadDetailView(); + checkBackButtonState(); + + await closeView(win); +}); + +add_task(async function testEmptyMoreOptionsMenu() { + let theme = await AddonManager.getAddonByID(DEFAULT_THEME_ID); + ok(theme.isActive, "The default theme is enabled"); + + let win = await loadInitialView("theme"); + + let card = getAddonCard(win, DEFAULT_THEME_ID); + let enabledItems = card.options.visibleItems; + is(enabledItems.length, 1, "There is one enabled item"); + is(enabledItems[0].getAttribute("action"), "expand", "Expand is enabled"); + let moreOptionsButton = card.querySelector(".more-options-button"); + ok(!moreOptionsButton.hidden, "The more options button is visible"); + + let loaded = waitForViewLoad(win); + enabledItems[0].click(); + await loaded; + + card = getAddonCard(win, DEFAULT_THEME_ID); + let toggleDisabledButton = card.querySelector('[action="toggle-disabled"]'); + enabledItems = card.options.visibleItems; + is(enabledItems.length, 0, "There are no enabled items"); + moreOptionsButton = card.querySelector(".more-options-button"); + ok(moreOptionsButton.hidden, "The more options button is now hidden"); + ok(toggleDisabledButton.hidden, "The disable button is hidden"); + + // Switch themes, the menu should be hidden, but enable button should appear. + let darkTheme = await AddonManager.getAddonByID(DARK_THEME_ID); + let updated = BrowserTestUtils.waitForEvent(card, "update"); + await darkTheme.enable(); + await updated; + + ok(moreOptionsButton.hidden, "The more options button is still hidden"); + ok(!toggleDisabledButton.hidden, "The enable button is visible"); + + updated = BrowserTestUtils.waitForEvent(card, "update"); + await toggleDisabledButton.click(); + await updated; + + ok(moreOptionsButton.hidden, "The more options button is hidden"); + ok(toggleDisabledButton.hidden, "The disable button is hidden"); + + await closeView(win); +}); + +add_task(async function testGoBackButtonIsDisabledWhenHistoryIsEmpty() { + let extension = ExtensionTestUtils.loadExtension({ + manifest: { name: "Test Go Back Button" }, + useAddonManager: "temporary", + }); + await extension.startup(); + + let viewID = `addons://detail/${encodeURIComponent(extension.id)}`; + + // When we have a fresh new tab, `about:addons` is opened in it. + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, null); + // Simulate a click on "Manage extension" from a context menu. + let win = await BrowserOpenAddonsMgr(viewID); + await assertBackButtonIsDisabled(win); + + BrowserTestUtils.removeTab(tab); + await extension.unload(); +}); + +add_task(async function testGoBackButtonIsDisabledWhenHistoryIsEmptyInNewTab() { + let extension = ExtensionTestUtils.loadExtension({ + manifest: { name: "Test Go Back Button" }, + useAddonManager: "temporary", + }); + await extension.startup(); + + let viewID = `addons://detail/${encodeURIComponent(extension.id)}`; + + // When we have a tab with a page loaded, `about:addons` will be opened in a + // new tab. + let tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + "https://example.org" + ); + let addonsTabLoaded = BrowserTestUtils.waitForNewTab( + gBrowser, + "about:addons", + true + ); + // Simulate a click on "Manage extension" from a context menu. + let win = await BrowserOpenAddonsMgr(viewID); + let addonsTab = await addonsTabLoaded; + await assertBackButtonIsDisabled(win); + + BrowserTestUtils.removeTab(addonsTab); + BrowserTestUtils.removeTab(tab); + await extension.unload(); +}); + +add_task(async function testGoBackButtonIsDisabledAfterBrowserBackButton() { + let extension = ExtensionTestUtils.loadExtension({ + manifest: { name: "Test Go Back Button" }, + useAddonManager: "temporary", + }); + await extension.startup(); + + let viewID = `addons://detail/${encodeURIComponent(extension.id)}`; + + // When we have a fresh new tab, `about:addons` is opened in it. + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, null); + // Simulate a click on "Manage extension" from a context menu. + let win = await BrowserOpenAddonsMgr(viewID); + await assertBackButtonIsDisabled(win); + + // Navigate to the extensions list. + await new CategoryUtilities(win).openType("extension"); + + // Click on the browser back button. + gBrowser.goBack(); + await assertBackButtonIsDisabled(win); + + BrowserTestUtils.removeTab(tab); + await extension.unload(); +}); -- cgit v1.2.3