summaryrefslogtreecommitdiffstats
path: root/toolkit/mozapps/extensions/test/browser/browser_html_detail_view.js
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/mozapps/extensions/test/browser/browser_html_detail_view.js')
-rw-r--r--toolkit/mozapps/extensions/test/browser/browser_html_detail_view.js1198
1 files changed, 1198 insertions, 0 deletions
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: ["<all_urls>", "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<br>With brs!",
+ "The full description replaces newlines with <br>"
+ );
+
+ 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();
+});