Bug 1525173 - HTML about:addons detail view r=jaws,flod

Differential Revision: https://phabricator.services.mozilla.com/D22766

--HG--
extra : moz-landing-system : lando
This commit is contained in:
Mark Striemer 2019-04-05 07:54:35 +00:00
parent d7be8a0205
commit 0978afd594
9 changed files with 808 additions and 60 deletions

View File

@ -111,6 +111,10 @@ detail-last-updated =
detail-contributions-description = The developer of this add-on asks that you help support its continued development by making a small contribution.
detail-contributions-button = Contribute
.title = Contribute to the development of this add-on
.accesskey = C
detail-update-type =
.value = Automatic Updates
@ -347,3 +351,23 @@ expand-addon-button = More Options
addons-enabled-heading = Enabled
addons-disabled-heading = Disabled
addon-detail-author-label = Author
addon-detail-version-label = Version
addon-detail-last-updated-label = Last Updated
addon-detail-homepage-label = Homepage
addon-detail-rating-label = Rating
# This string is used to show that an add-on is disabled.
# Variables:
# $name (string) - The name of the add-on
addon-name-disabled = { $name } (disabled)
# The number of reviews that an add-on has received on AMO.
# Variables:
# $numberOfReviews (number) - The number of reviews received
addon-detail-reviews-link =
{ $numberOfReviews ->
[one] { $numberOfReviews } review
*[other] { $numberOfReviews } reviews
}

View File

@ -1,5 +1,6 @@
:root {
--section-width: 664px;
--addon-icon-size: 32px;
}
#main {
@ -25,17 +26,24 @@
.addon.card {
margin-bottom: 16px;
display: flex;
}
.addon.card:hover {
box-shadow: var(--card-shadow);
}
.addon-card-collapsed {
display: flex;
}
addon-list .addon.card {
-moz-user-select: none;
}
.card-heading-icon {
flex-shrink: 0;
width: 32px;
height: 32px;
width: var(--addon-icon-size);
height: var(--addon-icon-size);
margin-inline-end: 16px;
}
@ -53,7 +61,9 @@
font-size: 16px;
font-weight: 600;
color: var(--grey-90);
line-height: 1;
line-height: 22px;
/* Subtract the top line-height so the text and icon align at the top. */
margin-top: -3px;
}
.addon-description {
@ -63,6 +73,21 @@
font-weight: 400;
}
/* Prevent the content from wrapping unless expanded. */
addon-card:not([expanded]) .card-contents {
/* Card padding times 4: 2 times for card, 1 between icon and content,
* 1 for the ... button. */
width: calc(var(--section-width) - var(--card-padding) * 4 - var(--addon-icon-size));
white-space: nowrap;
}
/* Ellipsize if the content is too long. */
addon-card:not([expanded]) .addon-name,
addon-card:not([expanded]) .addon-description {
text-overflow: ellipsis;
overflow-x: hidden;
}
.more-options-menu {
position: relative;
/* Add some negative margin to account for the button's padding */
@ -70,6 +95,91 @@
margin-inline-end: -8px;
}
addon-details {
color: var(--grey-60);
}
.addon-detail-description {
margin: 16px 0;
}
.addon-detail-contribute {
padding: var(--card-padding);
border: 1px solid var(--grey-90-a20);
border-radius: var(--panel-border-radius);
margin-bottom: var(--card-padding);
display: flex;
flex-direction: column;
}
.addon-detail-contribute > label {
font-style: italic;
}
.addon-detail-contribute-button {
-moz-context-properties: fill;
fill: currentColor;
background-image: url("chrome://global/skin/icons/heart.svg");
background-repeat: no-repeat;
background-position: 8px;
padding-inline-start: 28px;
margin-top: var(--card-padding);
margin-bottom: 0;
align-self: flex-end;
}
.addon-detail-row {
display: flex;
justify-content: space-between;
border-top: 1px solid var(--grey-90-a20);
margin: 0 calc(var(--card-padding) * -1);
padding: var(--card-padding);
}
.addon-detail-row:last-of-type {
padding-bottom: 0;
}
.addon-detail-row {
-moz-context-properties: fill;
fill: currentColor;
}
.addon-detail-rating {
display: flex;
}
.addon-detail-rating-star {
display: inline-block;
width: 16px;
height: 16px;
background: url("chrome://browser/skin/bookmark-hollow.svg");
}
.addon-detail-rating-star[fill="full"] {
background: url("chrome://browser/skin/bookmark.svg");
}
.addon-detail-rating-star[fill="half"] {
background: url("chrome://browser/skin/bookmark.svg");
width: 8px;
margin-inline-end: 8px;
}
.addon-detail-rating-star[fill="half"]::after {
content: "";
display: inline-block;
background: url("chrome://browser/skin/bookmark-hollow.svg");
background-position: 8px;
width: 8px;
height: 16px;
margin-left: 8px;
}
.addon-detail-rating > a {
margin-inline-start: 8px;
}
button[action="more-options"] {
min-width: auto;
min-height: auto;

View File

@ -7,6 +7,7 @@
<link rel="localization" href="branding/brand.ftl"/>
<link rel="localization" href="toolkit/about/aboutAddons.ftl"/>
<script type="application/javascript" src="chrome://global/content/contentAreaUtils.js"></script>
<script type="application/javascript" src="chrome://mozapps/content/extensions/aboutaddons.js"></script>
</head>
<body>
@ -15,19 +16,58 @@
<template name="card">
<div class="card addon">
<img class="card-heading-icon addon-icon"/>
<div class="card-contents">
<span class="addon-name"></span>
<span class="addon-description"></span>
<div class="addon-card-collapsed">
<img class="card-heading-icon addon-icon"/>
<div class="card-contents">
<span class="addon-name"></span>
<span class="addon-description"></span>
</div>
<div class="more-options-menu">
<button class="ghost-button" action="more-options"></button>
<panel-list>
<panel-item action="toggle-disabled"></panel-item>
<panel-item data-l10n-id="remove-addon-button" action="remove"></panel-item>
<panel-item-separator></panel-item-separator>
<panel-item data-l10n-id="expand-addon-button" action="expand"></panel-item>
</panel-list>
</div>
</div>
<div class="more-options-menu">
<button class="ghost-button" action="more-options"></button>
<panel-list>
<panel-item action="toggle-disabled"></panel-item>
<panel-item data-l10n-id="remove-addon-button" action="remove"></panel-item>
<panel-item-separator></panel-item-separator>
<panel-item data-l10n-id="expand-addon-button" action="expand"></panel-item>
</panel-list>
</div>
</template>
<template name="addon-details">
<div class="addon-detail-description"></div>
<div class="addon-detail-contribute">
<label data-l10n-id="detail-contributions-description"></label>
<button
class="addon-detail-contribute-button"
action="contribute"
data-l10n-id="detail-contributions-button"
data-l10n-attrs="accesskey">
</button>
</div>
<div class="addon-detail-row addon-detail-row-author">
<label data-l10n-id="addon-detail-author-label"></label>
</div>
<div class="addon-detail-row addon-detail-row-version">
<label data-l10n-id="addon-detail-version-label"></label>
</div>
<div class="addon-detail-row addon-detail-row-lastUpdated">
<label data-l10n-id="addon-detail-last-updated-label"></label>
</div>
<div class="addon-detail-row addon-detail-row-homepage">
<label data-l10n-id="addon-detail-homepage-label"></label>
<a target="_blank"></a>
</div>
<div class="addon-detail-row addon-detail-row-rating">
<label data-l10n-id="addon-detail-rating-label"></label>
<div class="addon-detail-rating">
<span class="addon-detail-rating-star"></span>
<span class="addon-detail-rating-star"></span>
<span class="addon-detail-rating-star"></span>
<span class="addon-detail-rating-star"></span>
<span class="addon-detail-rating-star"></span>
<a target="_blank"></a>
</div>
</div>
</template>

View File

@ -3,14 +3,11 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
/* eslint max-len: ["error", 80] */
/* exported initialize, hide, show */
/* import-globals-from ../../../content/contentAreaUtils.js */
/* global windowRoot */
"use strict";
const {XPCOMUtils} = ChromeUtils.import(
"resource://gre/modules/XPCOMUtils.jsm");
const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm");
XPCOMUtils.defineLazyModuleGetters(this, {
AddonManager: "resource://gre/modules/AddonManager.jsm",
});
@ -26,6 +23,15 @@ function hasPermission(addon, permission) {
return !!(addon.permissions & PERMISSION_MASKS[permission]);
}
/**
* This function is set in initialize() by the parent about:addons window. It
* is a helper for gViewController.loadView().
*
* @param {string} type The view type to load.
* @param {string} param The (optional) param for the view.
*/
let loadViewFn;
let _templates = {};
/**
@ -42,6 +48,19 @@ function importTemplate(name) {
throw new Error(`Unknown template: ${name}`);
}
function nl2br(text) {
let frag = document.createDocumentFragment();
let hasAppended = false;
for (let part of text.split("\n")) {
if (hasAppended) {
frag.appendChild(document.createElement("br"));
}
frag.appendChild(new Text(part));
hasAppended = true;
}
return frag;
}
class PanelList extends HTMLElement {
static get observedAttributes() {
return ["open"];
@ -213,6 +232,122 @@ class PanelItem extends HTMLElement {
}
customElements.define("panel-item", PanelItem);
class AddonDetails extends HTMLElement {
constructor() {
super();
this.hasConnected = false;
}
connectedCallback() {
if (!this.hasConnected) {
this.hasConnected = true;
this.render();
this.addEventListener("click", this);
}
}
setAddon(addon) {
this.addon = addon;
}
handleEvent(e) {
if (e.type == "click" && e.target.getAttribute("action") == "contribute") {
openURL(this.addon.contributionURL);
}
}
render() {
let {addon} = this;
if (!addon) {
throw new Error("addon-details must be initialized by setAddon");
}
this.appendChild(importTemplate("addon-details"));
// Full description.
let description = this.querySelector(".addon-detail-description");
if (addon.getFullDescription) {
description.appendChild(addon.getFullDescription(document));
} else if (addon.fullDescription) {
description.appendChild(nl2br(addon.fullDescription));
}
// Contribute.
if (!addon.contributionURL) {
this.querySelector(".addon-detail-contribute").remove();
}
// Author.
let creatorRow = this.querySelector(".addon-detail-row-author");
if (addon.creator) {
let creator;
if (addon.creator.url) {
creator = document.createElement("a");
creator.href = addon.creator.url;
creator.target = "_blank";
creator.textContent = addon.creator.name;
} else {
creator = new Text(addon.creator.name);
}
creatorRow.appendChild(creator);
} else {
creatorRow.remove();
}
// Version. Don't show a version for LWTs.
let version = this.querySelector(".addon-detail-row-version");
if (addon.version && !/@personas\.mozilla\.org/.test(addon.id)) {
version.appendChild(new Text(addon.version));
} else {
version.remove();
}
// Last updated.
let updateDate = this.querySelector(".addon-detail-row-lastUpdated");
if (addon.updateDate) {
let lastUpdated = addon.updateDate.toLocaleDateString(undefined, {
year: "numeric",
month: "long",
day: "numeric",
});
updateDate.appendChild(new Text(lastUpdated));
} else {
updateDate.remove();
}
// Homepage.
let homepageRow = this.querySelector(".addon-detail-row-homepage");
if (addon.homepageURL) {
let homepageURL = homepageRow.querySelector("a");
homepageURL.href = addon.homepageURL;
homepageURL.textContent = addon.homepageURL;
} else {
homepageRow.remove();
}
// Rating.
let ratingRow = this.querySelector(".addon-detail-row-rating");
if (addon.averageRating) {
let stars = ratingRow.querySelectorAll(".addon-detail-rating-star");
for (let i = 0; i < stars.length; i++) {
let fill = "";
if (addon.averageRating > i) {
fill = addon.averageRating > i + 0.5 ? "full" : "half";
}
stars[i].setAttribute("fill", fill);
}
let reviews = ratingRow.querySelector("a");
reviews.href = addon.reviewURL;
document.l10n.setAttributes(reviews, "addon-detail-reviews-link", {
numberOfReviews: addon.reviewCount,
});
} else {
ratingRow.remove();
}
}
}
customElements.define("addon-details", AddonDetails);
/**
* A card component for managing an add-on. It should be initialized by setting
* the add-on with `setAddon()` before being connected to the document.
@ -224,15 +359,33 @@ customElements.define("panel-item", PanelItem);
class AddonCard extends HTMLElement {
constructor() {
super();
this.connected = false;
this.hasRendered = false;
}
connectedCallback() {
if (this.connected) {
return;
// If we've already rendered we can just update, otherwise render.
if (this.hasRendered) {
this.update();
} else {
this.render();
}
this.registerListener();
}
disconnectedCallback() {
this.removeListener();
}
get expanded() {
return this.hasAttribute("expanded");
}
set expanded(val) {
if (val) {
this.setAttribute("expanded", "true");
} else {
this.removeAttribute("expanded");
}
this.connected = true;
this.render();
}
/**
@ -245,12 +398,34 @@ class AddonCard extends HTMLElement {
this.addon = addon;
}
registerListener() {
AddonManager.addAddonListener(this);
}
removeListener() {
AddonManager.removeAddonListener(this);
}
onDisabled(addon) {
if (addon.id == this.addon.id) {
this.update();
}
}
onEnabled(addon) {
if (addon.id == this.addon.id) {
this.update();
}
}
/**
* Update the card's contents based on the previously set add-on. This should
* be called if there has been a change to the add-on.
*/
update() {
let {addon, card} = this;
// Update the icon.
let icon;
if (addon.type == "plugin") {
icon = PLUGIN_ICON_URL;
@ -258,20 +433,50 @@ class AddonCard extends HTMLElement {
icon = AddonManager.getPreferredIconURL(addon, 32, window);
}
card.querySelector(".addon-icon").src = icon;
card.querySelector(".addon-name").textContent = addon.name;
// Update the name.
let name = card.querySelector(".addon-name");
if (addon.isActive) {
name.textContent = addon.name;
name.removeAttribute("data-l10n-id");
} else {
document.l10n.setAttributes(name, "addon-name-disabled", {
name: addon.name,
});
}
// Update description.
card.querySelector(".addon-description").textContent = addon.description;
// Hide remove button if not allowed.
let removeButton = card.querySelector('[action="remove"]');
removeButton.hidden = !hasPermission(addon, "uninstall");
// Set disable label and hide if not allowed.
let disableButton = card.querySelector('[action="toggle-disabled"]');
let disableAction = addon.userDisabled ? "enable" : "disable";
document.l10n.setAttributes(
disableButton, `${disableAction}-addon-button`);
disableButton.hidden = !hasPermission(addon, disableAction);
// If disable and remove are hidden, we don't need the separator.
// The separator isn't needed when expanded (nothing under it) or when the
// remove and disable buttons are hidden (nothing above it).
let separator = card.querySelector("panel-item-separator");
separator.hidden = removeButton.hidden && disableButton.hidden;
separator.hidden = this.expanded ||
removeButton.hidden && disableButton.hidden;
// Hide the expand button if we're expanded.
card.querySelector('[action="expand"]').hidden = this.expanded;
this.sendEvent("update");
}
expand() {
if (!this.hasRendered) {
this.expanded = true;
} else {
throw new Error("expand() is only supported before render()");
}
}
render() {
@ -289,13 +494,14 @@ class AddonCard extends HTMLElement {
this.update();
let panel = this.card.querySelector("panel-list");
let moreOptionsButton = this.card.querySelector('[action="more-options"]');
// Open on mousedown when the mouse is used.
// Open panel on mousedown when the mouse is used.
moreOptionsButton.addEventListener("mousedown", (e) => {
panel.toggle(e);
});
// Open when there's a click from the keyboard.
// Open panel on click from the keyboard.
moreOptionsButton.addEventListener("click", (e) => {
if (e.mozInputSource == MouseEvent.MOZ_SOURCE_KEYBOARD) {
panel.toggle(e);
@ -328,10 +534,28 @@ class AddonCard extends HTMLElement {
}
}
break;
case "expand":
loadViewFn("detail", this.addon.id);
break;
}
});
if (this.expanded) {
let details = document.createElement("addon-details");
details.setAddon(this.addon);
this.card.appendChild(details);
} else {
// Expand on double click.
this.addEventListener("dblclick", (e) => {
// Don't expand if a button is double clicked.
if (e.target.tagName != "BUTTON") {
loadViewFn("detail", this.addon.id);
}
});
}
this.appendChild(this.card);
this.hasRendered = true;
}
sendEvent(name, detail) {
@ -356,15 +580,10 @@ customElements.define("addon-card", AddonCard);
class AddonList extends HTMLElement {
constructor() {
super();
this.connected = false;
this.sections = [];
}
async connectedCallback() {
if (this.connected) {
return;
}
this.connected = true;
// Register the listener and get the add-ons, these operations should
// happpen as close to each other as possible.
this.registerListener();
@ -374,7 +593,6 @@ class AddonList extends HTMLElement {
disconnectedCallback() {
// Remove content and stop listening until this is connected again.
this.connected = false;
this.textContent = "";
this.removeListener();
}
@ -519,7 +737,6 @@ class AddonList extends HTMLElement {
if (card) {
let sectionIndex = this.sections.findIndex(s => s.filterFn(addon));
if (sectionIndex != -1) {
card.update();
// Move the card, if needed. This will allow an animation between
// page sections and provides clearer events for testing.
if (card.parentNode.getAttribute("section") != sectionIndex) {
@ -527,8 +744,6 @@ class AddonList extends HTMLElement {
this.insertCardInto(card, sectionIndex);
this.updateSectionIfEmpty(oldSection);
this.sendEvent("move", {id: addon.id});
} else {
this.sendEvent("update", {id: addon.id});
}
} else {
this.removeAddon(addon);
@ -624,14 +839,34 @@ class ListView {
}
}
class DetailView {
constructor({param, root}) {
this.id = param;
this.root = root;
}
async render() {
let addon = await AddonManager.getAddonByID(this.id);
let card = document.createElement("addon-card");
card.setAddon(addon);
card.expand();
// Go back to the list view when the add-on is removed.
card.addEventListener("remove", () => loadViewFn("list", addon.type));
this.root.appendChild(card);
}
}
// Generic view management.
let root = null;
/**
* Called from extensions.js once, when about:addons is loading.
*/
function initialize() {
function initialize(opts) {
root = document.getElementById("main");
loadViewFn = opts.loadViewFn;
window.addEventListener("unload", () => {
// Clear out the root node so the disconnectedCallback will trigger
// properly and all of the custom elements can cleanup.
@ -647,6 +882,8 @@ function initialize() {
async function show(type, param) {
if (type == "list") {
await new ListView({param, root}).render();
} else if (type == "detail") {
await new DetailView({param, root}).render();
}
}

View File

@ -783,14 +783,15 @@ var gViewController = {
this.viewObjects.discover = gDiscoverView;
this.viewObjects.legacy = gLegacyView;
this.viewObjects.detail = gDetailView;
this.viewObjects.updates = gUpdatesView;
this.viewObjects.shortcuts = gShortcutsView;
if (useHtmlViews) {
this.viewObjects.list = htmlView("list");
this.viewObjects.detail = htmlView("detail");
} else {
this.viewObjects.list = gListView;
this.viewObjects.detail = gDetailView;
}
for (let type in this.viewObjects) {
@ -3884,6 +3885,16 @@ var gBrowser = {
}, true);
}
const htmlViewOpts = {
loadViewFn(type, param) {
let viewId = `addons://${type}`;
if (param) {
viewId += "/" + encodeURIComponent(param);
}
gViewController.loadView(viewId);
},
};
// View wrappers for the HTML version of about:addons. These delegate to an
// HTML browser that renders the actual views.
let htmlBrowser;
@ -3896,7 +3907,7 @@ function getHtmlBrowser() {
});
htmlBrowserLoaded = new Promise(
resolve => htmlBrowser.addEventListener("load", resolve, {once: true})
).then(() => htmlBrowser.contentWindow.initialize());
).then(() => htmlBrowser.contentWindow.initialize(htmlViewOpts));
}
return htmlBrowser;
}
@ -3904,7 +3915,7 @@ function getHtmlBrowser() {
function htmlView(type) {
return {
node: null,
isRoot: true,
isRoot: type != "detail",
initialize() {
this.node = getHtmlBrowser();

View File

@ -117,3 +117,4 @@ skip-if = os == 'linux' || (os == 'mac' && debug) # bug 1483347
[browser_webext_options_addon_reload.js]
tags = webextensions
[browser_html_list_view.js]
[browser_html_detail_view.js]

View File

@ -0,0 +1,322 @@
/* eslint max-len: ["error", 80] */
let gProvider;
let promptService;
function getAddonCard(doc, addonId) {
return doc.querySelector(`addon-card[addon-id="${addonId}"]`);
}
function checkLabel(row, name) {
is(row.ownerDocument.l10n.getAttributes(row.querySelector("label")).id,
`addon-detail-${name}-label`, `The ${name} label is set`);
}
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");
}
add_task(async function enableHtmlViews() {
await SpecialPowers.pushPrefEnv({
set: [["extensions.htmlaboutaddons.enabled", true]],
});
gProvider = new MockProvider();
gProvider.createAddons([{
id: "addon1@mochi.test",
name: "Test add-on 1",
creator: {name: "The creator", url: "http://example.com/me"},
version: "3.1",
description: "Short description",
fullDescription: "Longer description\nWith brs!",
type: "extension",
contributionURL: "http://foo.com",
averageRating: 4.3,
reviewCount: 5,
reviewURL: "http://example.com/reviews",
homepageURL: "http://example.com/addon1",
updateDate: new Date("2019-03-07T01:00:00"),
}, {
id: "addon2@mochi.test",
name: "Test add-on 2",
creator: {name: "I made it"},
description: "Short description",
type: "extension",
}]);
promptService = mockPromptService();
});
add_task(async function testOpenDetailView() {
let extension = ExtensionTestUtils.loadExtension({
manifest: {
name: "Test",
applications: {gecko: {id: "test@mochi.test"}},
},
useAddonManager: "temporary",
});
await extension.startup();
let win = await loadInitialView("extension");
let doc = win.document;
// Test double click on card to open details.
let card = getAddonCard(doc, "test@mochi.test");
ok(!card.querySelector("addon-details"), "The card doesn't have details");
let loaded = waitForViewLoad(win);
EventUtils.synthesizeMouseAtCenter(card, {clickCount: 1}, win);
EventUtils.synthesizeMouseAtCenter(card, {clickCount: 2}, win);
await loaded;
card = getAddonCard(doc, "test@mochi.test");
ok(card.querySelector("addon-details"), "The card now has details");
loaded = waitForViewLoad(win);
win.managerWindow.document.getElementById("go-back").click();
await loaded;
// Test using more options menu.
card = getAddonCard(doc, "test@mochi.test");
loaded = waitForViewLoad(win);
card.querySelector('[action="expand"]').click();
await loaded;
card = getAddonCard(doc, "test@mochi.test");
ok(card.querySelector("addon-details"), "The card now has details");
await closeView(win);
await extension.unload();
});
add_task(async function testDetailOperations() {
let extension = ExtensionTestUtils.loadExtension({
manifest: {
name: "Test",
applications: {gecko: {id: "test@mochi.test"}},
},
useAddonManager: "temporary",
});
await extension.startup();
let win = await loadInitialView("extension");
let doc = win.document;
let card = getAddonCard(doc, "test@mochi.test");
ok(!card.querySelector("addon-details"), "The card doesn't have details");
let loaded = waitForViewLoad(win);
EventUtils.synthesizeMouseAtCenter(card, {clickCount: 1}, win);
EventUtils.synthesizeMouseAtCenter(card, {clickCount: 2}, win);
await loaded;
card = getAddonCard(doc, "test@mochi.test");
let panel = card.querySelector("panel-list");
// Check button visibility.
let disableButton = panel.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("panel-item-separator");
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.querySelector(".addon-name");
is(name.textContent, "Test", "The name is set when enabled");
is(doc.l10n.getAttributes(name).id, "", "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.
disableToggled = BrowserTestUtils.waitForEvent(card, "update");
disableButton.click();
await disableToggled;
// Name is just the add-on name again.
is(name.textContent, "Test", "The name is reset when enabled");
is(doc.l10n.getAttributes(name).id, "", "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.
ok(doc.querySelector("addon-list"), "There's an addon-list now");
ok(!getAddonCard(doc, "test@mochi.test"),
"The extension no longer has a card");
let addon = await AddonManager.getAddonByID("test@mochi.test");
ok(!addon, "The extension can't be found now");
await closeView(win);
await extension.unload();
});
add_task(async function testFullDetails() {
let win = await loadInitialView("extension");
let doc = win.document;
// The list card.
let card = getAddonCard(doc, "addon1@mochi.test");
ok(!card.hasAttribute("expanded"), "The list card is not expanded");
let loaded = waitForViewLoad(win);
card.querySelector('[action="expand"]').click();
await loaded;
// This is now the detail card.
card = getAddonCard(doc, "addon1@mochi.test");
ok(card.hasAttribute("expanded"), "The detail card is expanded");
let details = card.querySelector("addon-details");
let desc = details.querySelector(".addon-detail-description");
is(desc.innerHTML, "Longer description<br>With brs!",
"The full description replaces newlines with <br>");
let contrib = details.querySelector(".addon-detail-contribute");
ok(contrib, "The contribution section is visible");
let rows = Array.from(details.querySelectorAll(".addon-detail-row"));
// The first row is the author.
let row = rows.shift();
checkLabel(row, "author");
let link = row.querySelector("a");
checkLink(link, "http://example.com/me", "The creator");
// The version is next.
row = rows.shift();
checkLabel(row, "version");
let text = row.lastChild;
is(text.textContent, "3.1", "The version is set");
// Last updated is next.
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 stars = Array.from(rating.querySelectorAll(".addon-detail-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");
checkLink(link, "http://example.com/reviews", {
id: "addon-detail-reviews-link",
args: {numberOfReviews: 5},
});
// That should've been all the rows.
is(rows.length, 0, "There are no more rows left");
await closeView(win);
});
add_task(async function testMinimalExtension() {
let win = await loadInitialView("extension");
let doc = win.document;
let card = getAddonCard(doc, "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(doc, "addon2@mochi.test");
let details = card.querySelector("addon-details");
let desc = details.querySelector(".addon-detail-description");
is(desc.textContent, "", "There is no full description");
let contrib = details.querySelector(".addon-detail-contribute");
ok(!contrib, "There is no contribution element");
let rows = Array.from(details.querySelectorAll(".addon-detail-row"));
let row = rows.shift();
checkLabel(row, "author");
let text = row.lastChild;
is(text.textContent, "I made it", "The author is set");
ok(text instanceof Text, "The author is a text node");
is(rows.length, 0, "There was only 1 row");
await closeView(win);
});
add_task(async function testDefaultTheme() {
let win = await loadInitialView("theme");
let doc = win.document;
// The list card.
let card = getAddonCard(doc, "default-theme@mozilla.org");
ok(!card.hasAttribute("expanded"), "The list card is not expanded");
let loaded = waitForViewLoad(win);
card.querySelector('[action="expand"]').click();
await loaded;
card = getAddonCard(doc, "default-theme@mozilla.org");
let rows = Array.from(card.querySelectorAll(".addon-detail-row"));
// 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.0", "It's always version 1.0");
// Last updated.
let lastUpdated = rows.shift();
checkLabel(lastUpdated, "last-updated");
ok(lastUpdated.lastChild.textContent, "There is a date set");
is(rows.length, 0, "There are no more rows");
await closeView(win);
});

View File

@ -2,22 +2,6 @@
let promptService;
let gManagerWindow;
let gCategoryUtilities;
async function loadInitialView(type) {
gManagerWindow = await open_manager(null);
gCategoryUtilities = new CategoryUtilities(gManagerWindow);
await gCategoryUtilities.openType(type);
let browser = gManagerWindow.document.getElementById("html-view-browser");
return browser.contentWindow;
}
function closeView() {
return close_manager(gManagerWindow);
}
const SECTION_INDEXES = {
enabled: 0,
disabled: 1,

View File

@ -1468,6 +1468,25 @@ function assertTelemetryMatches(events, {filterMethods} = {}) {
}
/* HTML view helpers */
async function loadInitialView(type) {
let managerWindow = await open_manager(null);
let categoryUtilities = new CategoryUtilities(managerWindow);
await categoryUtilities.openType(type);
let browser = managerWindow.document.getElementById("html-view-browser");
let win = browser.contentWindow;
win.managerWindow = managerWindow;
return win;
}
function waitForViewLoad(win) {
return wait_for_view_load(win.managerWindow, undefined, true);
}
function closeView(win) {
return close_manager(win.managerWindow);
}
function mockPromptService() {
let {prompt} = Services;
let promptService = {